# Part 5.5: NumPy로 배우는 선형대수학 - 직관적 이해 중심

**⬅️ 이전 시간: [Part 5: AI 핵심 라이브러리](../05_ai_core_libraries/part_5_ai_core_libraries.md)**
**➡️ 다음 시간: [Part 5.6: 머신러닝/딥러닝을 위한 미적분](../05.6_calculus_for_ml/part_5.6_calculus_for_ml.md)**

---

## 🗺️ 학습 로드맵: 왜 여기서 선형대수를 배우나요?

### 🎯 이번 파트의 목적
이제 AI 핵심 라이브러리(NumPy, Pandas, Matplotlib)를 마스터했으니, **머신러닝의 수학적 기초**를 다져보겠습니다. 선형대수는 AI의 "언어"라고 할 수 있습니다.

### 🔗 이전 파트와의 연결점
- **Part 5 (AI Core Libraries)**: NumPy 배열을 다루는 방법을 배웠습니다
- **이번 파트 (Linear Algebra)**: 그 NumPy 배열이 실제로는 **벡터와 행렬**이라는 것을 이해합니다
- **다음 파트 (Calculus)**: 선형대수 + 미적분 = 머신러닝의 완성

### 🚀 머신러닝에서의 활용 예시
- **이미지 처리**: 픽셀 데이터를 행렬로 표현하고 변환
- **추천 시스템**: 사용자-아이템 행렬을 분해하여 패턴 발견
- **신경망**: 가중치를 행렬로 표현하고 최적화
- **차원 축소**: PCA로 고차원 데이터를 저차원으로 압축

### 📈 학습 진행도
```
Part 5: AI Core Libraries ✅
Part 5.5: Linear Algebra 🔄 (현재)
Part 5.6: Calculus ⏳
Part 6: Machine Learning ⏳
```

---

> ## 🎯 학습 목표 (Learning Objectives)
>
> 이번 파트가 끝나면, 여러분은 다음을 할 수 있게 됩니다.
> 
> - **직관적으로 이해하기**: 복잡한 수식보다는 시각화와 비유를 통해 선형대수의 핵심 개념을 직관적으로 이해할 수 있습니다.
> - **실습 중심 학습**: Python과 NumPy를 사용하여 개념을 직접 구현하고 시각화할 수 있습니다.
> - **실제 활용**: 머신러닝에서 선형대수가 어떻게 활용되는지 구체적인 예제를 통해 이해할 수 있습니다.


> ## 🔑 핵심 접근법 (Key Approach)
> 
> **"왜 이걸 배워야 하는가?"** 에 집중합니다.
> - 벡터 = "방향과 크기를 가진 화살표"
> - 행렬 = "데이터를 변형하는 마법 상자"
> - 고유벡터 = "변하지 않는 특별한 방향"
> - PCA = "데이터의 핵심 방향 찾기"


> ## 🛠️ 사용 도구 (Tools)
> - **Python + NumPy**: 실제 계산과 구현
> - **Matplotlib**: 시각화로 직관적 이해
> - **Jupyter Notebook**: 대화형 학습
> - **실제 예제**: 이미지 처리, 데이터 분석 등

---


## 1. 벡터: 방향과 크기를 가진 화살표 🏹

### 1-1. 벡터란 무엇인가? (직관적 이해)

> **💡 비유**: 벡터는 "화살표"입니다!
> - **크기**: 화살표의 길이
> - **방향**: 화살표가 가리키는 방향
> - **위치**: 화살표가 시작하는 점

```python
import numpy as np
import matplotlib.pyplot as plt

# 벡터를 화살표로 시각화
def plot_vector(ax, start, vector, color='blue', label=''):
    """벡터를 화살표로 그리는 함수"""
    ax.quiver(start[0], start[1], vector[0], vector[1], 
              angles='xy', scale_units='xy', scale=1, color=color, label=label)

# 그래프 설정
fig, ax = plt.subplots(figsize=(10, 8))
ax.set_xlim(-5, 5)
ax.set_ylim(-5, 5)
ax.grid(True, alpha=0.3)
ax.set_aspect('equal')

# 원점
origin = np.array([0, 0])

# 다양한 벡터들
vectors = {
    'v1': np.array([3, 2]),    # 오른쪽 위로
    'v2': np.array([-2, 1]),   # 왼쪽 위로  
    'v3': np.array([0, -3]),   # 아래로
    'v4': np.array([4, 0])     # 오른쪽으로
}

colors = ['red', 'blue', 'green', 'purple']

# 벡터들을 화살표로 그리기
for i, (name, vector) in enumerate(vectors.items()):
    plot_vector(ax, origin, vector, colors[i], name)
    
    # 벡터의 크기 계산
    magnitude = np.linalg.norm(vector)
    print(f"{name}: 크기 = {magnitude:.2f}, 방향 = {vector}")

ax.set_xlabel('X축')
ax.set_ylabel('Y축')
ax.set_title('벡터 = 방향과 크기를 가진 화살표')
ax.legend()
plt.show()
```

### 1-2. 벡터 연산의 직관적 의미

#### 벡터 덧셈: "이동 경로 합치기"
```python
# 벡터 덧셈 시각화
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))

# 첫 번째 그래프: 개별 벡터들
ax1.set_xlim(-5, 5)
ax1.set_ylim(-5, 5)
ax1.grid(True, alpha=0.3)
ax1.set_aspect('equal')

v1 = np.array([2, 1])
v2 = np.array([1, 3])

plot_vector(ax1, origin, v1, 'red', 'v1')
plot_vector(ax1, origin, v2, 'blue', 'v2')
ax1.set_title('개별 벡터들')
ax1.legend()

# 두 번째 그래프: 벡터 덧셈
ax2.set_xlim(-5, 5)
ax2.set_ylim(-5, 5)
ax2.grid(True, alpha=0.3)
ax2.set_aspect('equal')

# v1을 먼저 그리고, 그 끝점에서 v2를 그리기
plot_vector(ax2, origin, v1, 'red', 'v1')
plot_vector(ax2, v1, v2, 'blue', 'v2')  # v1의 끝점에서 v2 시작

# 합 벡터 (원점에서 최종 위치까지)
v_sum = v1 + v2
plot_vector(ax2, origin, v_sum, 'green', 'v1 + v2', linewidth=3)

ax2.set_title('벡터 덧셈: 이동 경로 합치기')
ax2.legend()

plt.tight_layout()
plt.show()

print(f"v1 = {v1}")
print(f"v2 = {v2}")
print(f"v1 + v2 = {v_sum}")
```

#### 벡터 내적: "서로 얼마나 같은 방향인지"
```python
# 벡터 내적의 직관적 이해
def cosine_similarity(v1, v2):
    """두 벡터의 코사인 유사도 계산"""
    dot_product = np.dot(v1, v2)
    norm_v1 = np.linalg.norm(v1)
    norm_v2 = np.linalg.norm(v2)
    return dot_product / (norm_v1 * norm_v2)

# 다양한 각도의 벡터들
angles = [0, 45, 90, 135, 180]  # 도
vectors = []

for angle in angles:
    # 각도를 라디안으로 변환
    rad = np.radians(angle)
    # 단위 벡터 생성
    v = np.array([np.cos(rad), np.sin(rad)])
    vectors.append(v)

# 기준 벡터 (오른쪽 방향)
base_vector = np.array([1, 0])

print("벡터 간 유사도 (내적의 직관적 의미):")
print("-" * 50)
for i, angle in enumerate(angles):
    similarity = cosine_similarity(base_vector, vectors[i])
    print(f"각도 {angle}°: 유사도 = {similarity:.3f} ({'같은 방향' if similarity > 0.9 else '수직' if abs(similarity) < 0.1 else '반대 방향' if similarity < -0.9 else '대각선'})")
```

---




## 2. 행렬: 데이터를 변형하는 마법 상자 🎁

### 2-1. 행렬이란 무엇인가? (직관적 이해)

> **💡 비유**: 행렬은 "데이터 변형기"입니다!
> - **입력**: 원본 데이터 (벡터)
> - **변형**: 행렬이 데이터를 어떻게 바꿀지 정의
> - **출력**: 변형된 데이터 (새로운 벡터)

```python
# 행렬 변환의 시각화
def plot_transformation(original_points, transformed_points, title):
    """점들의 변환을 시각화"""
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
    
    # 원본 점들
    ax1.scatter(original_points[:, 0], original_points[:, 1], c='blue', alpha=0.6)
    ax1.set_xlim(-3, 3)
    ax1.set_ylim(-3, 3)
    ax1.grid(True, alpha=0.3)
    ax1.set_aspect('equal')
    ax1.set_title('변환 전')
    
    # 변환된 점들
    ax2.scatter(transformed_points[:, 0], transformed_points[:, 1], c='red', alpha=0.6)
    ax2.set_xlim(-3, 3)
    ax2.set_ylim(-3, 3)
    ax2.grid(True, alpha=0.3)
    ax2.set_aspect('equal')
    ax2.set_title('변환 후')
    
    plt.suptitle(title)
    plt.tight_layout()
    plt.show()

# 원형 점들 생성
theta = np.linspace(0, 2*np.pi, 50)
circle_points = np.column_stack([np.cos(theta), np.sin(theta)])

# 1. 회전 변환 (90도 회전)
rotation_matrix = np.array([[0, -1], [1, 0]])  # 90도 회전
rotated_points = circle_points @ rotation_matrix.T
plot_transformation(circle_points, rotated_points, "회전 변환: 원형 → 회전된 원형")

# 2. 크기 조절 변환 (X축으로 2배 늘리기)
scaling_matrix = np.array([[2, 0], [0, 1]])  # X축 2배
scaled_points = circle_points @ scaling_matrix.T
plot_transformation(circle_points, scaled_points, "크기 조절: 원형 → 타원형")

# 3. 전단 변환 (X축 방향으로 기울이기)
shear_matrix = np.array([[1, 0.5], [0, 1]])  # X축 방향 전단
sheared_points = circle_points @ shear_matrix.T
plot_transformation(circle_points, sheared_points, "전단 변환: 원형 → 기울어진 원형")
```

### 2-2. 행렬곱의 직관적 의미

```python
# 행렬곱의 시각화: 복합 변환
fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# 원본 점들
original = circle_points

# 변환 행렬들
A = np.array([[0.8, -0.6], [0.6, 0.8]])  # 회전 + 축소
B = np.array([[1.5, 0], [0, 0.8]])       # X축 확대, Y축 축소

# 단계별 변환
step1 = original @ A.T
step2 = step1 @ B.T

# 복합 변환 (A × B)
combined = original @ (A @ B).T

# 시각화
titles = ['원본', 'A 변환 후', 'B 변환 후', 'A×B 복합 변환']
points_list = [original, step1, step2, combined]

for i, (ax, title, points) in enumerate(zip(axes.flat, titles, points_list)):
    ax.scatter(points[:, 0], points[:, 1], c='blue', alpha=0.6)
    ax.set_xlim(-2, 2)
    ax.set_ylim(-2, 2)
    ax.grid(True, alpha=0.3)
    ax.set_aspect('equal')
    ax.set_title(title)

plt.tight_layout()
plt.show()

print("행렬곱의 의미: 변환을 순서대로 적용")
print(f"A = {A}")
print(f"B = {B}")
print(f"A × B = {A @ B}")
```

---

<br>

## 3. 고유값과 고유벡터: 변하지 않는 특별한 방향 🧭

### 3-1. 고유벡터의 직관적 이해

> **💡 비유**: 고유벡터는 "변하지 않는 특별한 방향"입니다!
> - 행렬이 데이터를 변형할 때, 특정 방향의 벡터는 방향이 바뀌지 않습니다
> - 단지 크기만 변할 뿐입니다 (고유값만큼)

```python
# 고유벡터 시각화
def visualize_eigenvectors(matrix, title):
    """고유벡터를 시각화하는 함수"""
    # 고유값과 고유벡터 계산
    eigenvalues, eigenvectors = np.linalg.eig(matrix)
    
    # 원형 점들
    theta = np.linspace(0, 2*np.pi, 100)
    circle = np.column_stack([np.cos(theta), np.sin(theta)])
    
    # 변환 전후
    transformed = circle @ matrix.T
    
    # 고유벡터들
    eigenvector1 = eigenvectors[:, 0]
    eigenvector2 = eigenvectors[:, 1]
    
    # 시각화
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
    
    # 변환 전
    ax1.scatter(circle[:, 0], circle[:, 1], c='lightblue', alpha=0.6, s=20)
    ax1.quiver(0, 0, eigenvector1[0], eigenvector1[1], color='red', scale=3, label=f'고유벡터1 (λ={eigenvalues[0]:.2f})')
    ax1.quiver(0, 0, eigenvector2[0], eigenvector2[1], color='green', scale=3, label=f'고유벡터2 (λ={eigenvalues[1]:.2f})')
    ax1.set_xlim(-2, 2)
    ax1.set_ylim(-2, 2)
    ax1.grid(True, alpha=0.3)
    ax1.set_aspect('equal')
    ax1.set_title('변환 전: 고유벡터 방향')
    ax1.legend()
    
    # 변환 후
    ax2.scatter(transformed[:, 0], transformed[:, 1], c='lightcoral', alpha=0.6, s=20)
    # 변환된 고유벡터들
    transformed_eigenvector1 = eigenvector1 @ matrix.T
    transformed_eigenvector2 = eigenvector2 @ matrix.T
    ax2.quiver(0, 0, transformed_eigenvector1[0], transformed_eigenvector1[1], color='red', scale=3, label=f'변환된 고유벡터1')
    ax2.quiver(0, 0, transformed_eigenvector2[0], transformed_eigenvector2[1], color='green', scale=3, label=f'변환된 고유벡터2')
    ax2.set_xlim(-2, 2)
    ax2.set_ylim(-2, 2)
    ax2.grid(True, alpha=0.3)
    ax2.set_aspect('equal')
    ax2.set_title('변환 후: 고유벡터는 방향이 유지됨')
    ax2.legend()
    
    plt.suptitle(title)
    plt.tight_layout()
    plt.show()
    
    print(f"고유값: {eigenvalues}")
    print(f"고유벡터:\n{eigenvectors}")

# 예제 행렬들
matrix1 = np.array([[2, 0], [0, 1]])  # 대각 행렬
matrix2 = np.array([[1, 0.5], [0.5, 1]])  # 대칭 행렬

visualize_eigenvectors(matrix1, "대각 행렬의 고유벡터")
visualize_eigenvectors(matrix2, "대칭 행렬의 고유벡터")
```

### 3-2. PCA: 데이터의 핵심 방향 찾기

```python
# PCA의 직관적 이해
np.random.seed(42)

# 2D 데이터 생성 (상관관계가 있는 데이터)
mean = [0, 0]
cov = [[3, 2], [2, 3]]  # 상관관계가 있는 공분산 행렬
data = np.random.multivariate_normal(mean, cov, 200)

# 데이터 중앙 정렬
X = data - data.mean(axis=0)

# 공분산 행렬 계산
cov_matrix = np.cov(X.T)

# 고유값과 고유벡터 계산 (PCA)
eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)

# 고유값 순으로 정렬
sorted_indices = np.argsort(eigenvalues)[::-1]
sorted_eigenvalues = eigenvalues[sorted_indices]
sorted_eigenvectors = eigenvectors[:, sorted_indices]

# 시각화
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))

# 원본 데이터와 주성분
ax1.scatter(X[:, 0], X[:, 1], alpha=0.6, c='lightblue')
# 주성분 벡터 그리기
pc1 = sorted_eigenvectors[:, 0]
pc2 = sorted_eigenvectors[:, 1]
ax1.quiver(0, 0, pc1[0]*np.sqrt(sorted_eigenvalues[0]), pc1[1]*np.sqrt(sorted_eigenvalues[0]), 
           color='red', scale=10, label=f'PC1 (분산: {sorted_eigenvalues[0]:.2f})')
ax1.quiver(0, 0, pc2[0]*np.sqrt(sorted_eigenvalues[1]), pc2[1]*np.sqrt(sorted_eigenvalues[1]), 
           color='green', scale=10, label=f'PC2 (분산: {sorted_eigenvalues[1]:.2f})')
ax1.set_xlim(-4, 4)
ax1.set_ylim(-4, 4)
ax1.grid(True, alpha=0.3)
ax1.set_aspect('equal')
ax1.set_title('원본 데이터와 주성분 (PC1, PC2)')
ax1.legend()

# PCA 변환된 데이터
transformed_data = X @ sorted_eigenvectors
ax2.scatter(transformed_data[:, 0], transformed_data[:, 1], alpha=0.6, c='lightcoral')
ax2.set_xlim(-4, 4)
ax2.set_ylim(-4, 4)
ax2.grid(True, alpha=0.3)
ax2.set_aspect('equal')
ax2.set_title('PCA 변환 후: PC1이 가장 큰 분산을 가짐')
ax2.set_xlabel('PC1')
ax2.set_ylabel('PC2')

plt.tight_layout()
plt.show()

print("PCA의 직관적 의미:")
print(f"PC1 (첫 번째 주성분): {pc1}")
print(f"PC2 (두 번째 주성분): {pc2}")
print(f"PC1 분산: {sorted_eigenvalues[0]:.2f} ({sorted_eigenvalues[0]/sum(sorted_eigenvalues)*100:.1f}%)")
print(f"PC2 분산: {sorted_eigenvalues[1]:.2f} ({sorted_eigenvalues[1]/sum(sorted_eigenvalues)*100:.1f}%)")
```

---

<br>

## 4. 실제 활용: 이미지 처리와 데이터 분석 🖼️

### 4-1. 이미지 필터링으로 이해하는 행렬

```python
# 간단한 이미지 필터링 예제
from PIL import Image
import matplotlib.pyplot as plt

# 간단한 이미지 생성 (체크무늬 패턴)
def create_checkerboard(size=50):
    """체크무늬 이미지 생성"""
    img = np.zeros((size, size))
    for i in range(size):
        for j in range(size):
            if (i // 10 + j // 10) % 2 == 0:
                img[i, j] = 255
    return img

# 필터 행렬들 (커널)
filters = {
    '블러': np.array([[1/9, 1/9, 1/9], [1/9, 1/9, 1/9], [1/9, 1/9, 1/9]]),
    '엣지 검출': np.array([[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]]),
    '수평 엣지': np.array([[-1, -1, -1], [0, 0, 0], [1, 1, 1]]),
    '수직 엣지': np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]])
}

# 이미지 생성
original = create_checkerboard()

# 필터 적용 함수
def apply_filter(image, filter_kernel):
    """이미지에 필터 적용"""
    from scipy import ndimage
    return ndimage.convolve(image, filter_kernel, mode='constant', cval=0)

# 시각화
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
axes = axes.flatten()

# 원본 이미지
axes[0].imshow(original, cmap='gray')
axes[0].set_title('원본 이미지')
axes[0].axis('off')

# 필터 적용 결과
for i, (filter_name, filter_kernel) in enumerate(filters.items()):
    filtered = apply_filter(original, filter_kernel)
    axes[i+1].imshow(filtered, cmap='gray')
    axes[i+1].set_title(f'{filter_name} 필터')
    axes[i+1].axis('off')

plt.tight_layout()
plt.show()

print("이미지 필터링 = 행렬 곱셈의 실제 활용")
print("각 픽셀 주변의 값들을 행렬(커널)로 가중 평균하여 새로운 픽셀 값 계산")

#### 🏢 Google - 이미지 검색 사례
Google의 이미지 검색 시스템은 선형대수를 어떻게 활용할까요?

```python
# Google 이미지 검색의 핵심: 특성 벡터와 유사도 계산
def google_image_search_simulation():
    """Google 이미지 검색 시뮬레이션"""
    
    # 이미지 특성 벡터 (색상, 질감, 모양 등의 수치화된 특성)
    image_features = {
        'cat1.jpg': [0.8, 0.6, 0.9, 0.3, 0.7],  # 고양이 이미지
        'cat2.jpg': [0.7, 0.5, 0.8, 0.4, 0.6],  # 다른 고양이
        'dog1.jpg': [0.6, 0.8, 0.7, 0.5, 0.8],  # 강아지 이미지
        'car1.jpg': [0.2, 0.1, 0.3, 0.9, 0.4],  # 자동차 이미지
        'tree1.jpg': [0.4, 0.7, 0.5, 0.2, 0.9]  # 나무 이미지
    }
    
    # 검색 쿼리: "고양이" 이미지 찾기
    query_vector = np.array([0.8, 0.6, 0.9, 0.3, 0.7])  # 고양이 특성
    
    print("Google 이미지 검색 시뮬레이션")
    print("=" * 50)
    
    # 각 이미지와의 유사도 계산 (코사인 유사도)
    similarities = {}
    for image_name, features in image_features.items():
        similarity = np.dot(query_vector, features) / (np.linalg.norm(query_vector) * np.linalg.norm(features))
        similarities[image_name] = similarity
    
    # 유사도 순으로 정렬
    sorted_results = sorted(similarities.items(), key=lambda x: x[1], reverse=True)
    
    print("검색 결과 (유사도 순):")
    for i, (image, sim) in enumerate(sorted_results, 1):
        print(f"{i}. {image}: 유사도 {sim:.3f}")
    
    print("\n선형대수 활용:")
    print("- 이미지 특성 → 벡터로 표현")
    print("- 유사도 계산 → 벡터 내적 활용")
    print("- 검색 결과 정렬 → 수치 기반 순위 결정")

google_image_search_simulation()
```

### 4-2. 추천 시스템으로 이해하는 행렬 분해

```python
# 간단한 추천 시스템 시뮬레이션
np.random.seed(42)

# 사용자-아이템 평점 행렬 (5명 사용자, 4개 아이템)
# 0은 평점이 없는 경우
ratings = np.array([
    [5, 3, 0, 1],  # 사용자 1
    [4, 0, 0, 1],  # 사용자 2
    [1, 1, 0, 5],  # 사용자 3
    [1, 0, 0, 4],  # 사용자 4
    [0, 1, 5, 4]   # 사용자 5
])

print("사용자-아이템 평점 행렬:")
print(ratings)
print("\n행렬 분해의 직관적 의미:")
print("- 사용자 행렬: 각 사용자의 취향 (예: 액션 선호도, 로맨스 선호도)")
print("- 아이템 행렬: 각 아이템의 특성 (예: 액션 요소, 로맨스 요소)")
print("- 두 행렬의 곱으로 누락된 평점을 예측")

# 간단한 행렬 분해 (SVD의 직관적 이해)
# 실제로는 더 복잡한 알고리즘 사용
U = np.random.rand(5, 2)  # 사용자 특성 행렬
V = np.random.rand(2, 4)  # 아이템 특성 행렬

# 예측 평점
predicted = U @ V
print(f"\n예측된 평점 행렬:\n{predicted.round(2)}")

# 원본과 예측 비교
print(f"\n원본 평점 (0은 누락):\n{ratings}")
print(f"예측 평점:\n{predicted.round(2)}")

#### 🏢 Netflix - 추천 시스템 사례
Netflix의 추천 시스템은 행렬 분해를 어떻게 활용할까요?

```python
# Netflix 추천 시스템 시뮬레이션
def netflix_recommendation_simulation():
    """Netflix 추천 시스템 시뮬레이션"""
    
    # 사용자-영화 평점 행렬 (실제로는 수백만 명의 사용자와 수만 개의 영화)
    ratings_matrix = np.array([
        # 액션  로맨스  코미디  스릴러  SF
        [5,    1,     2,     4,     5],  # 사용자1 (액션/SF 선호)
        [4,    2,     3,     5,     4],  # 사용자2 (스릴러 선호)
        [1,    5,     4,     2,     1],  # 사용자3 (로맨스/코미디 선호)
        [2,    4,     5,     1,     2],  # 사용자4 (코미디 선호)
        [3,    3,     3,     3,     3]   # 사용자5 (중립적)
    ])
    
    print("Netflix 추천 시스템 시뮬레이션")
    print("=" * 50)
    print("사용자-영화 평점 행렬:")
    print(ratings_matrix)
    
    # SVD를 통한 행렬 분해 (실제로는 더 복잡한 알고리즘 사용)
    U, S, Vt = np.linalg.svd(ratings_matrix, full_matrices=False)
    
    # 잠재 요인 수 (실제로는 하이퍼파라미터 튜닝으로 결정)
    k = 2
    
    # 차원 축소
    U_k = U[:, :k]
    S_k = np.diag(S[:k])
    Vt_k = Vt[:k, :]
    
    # 재구성된 평점 행렬
    reconstructed = U_k @ S_k @ Vt_k
    
    print(f"\n행렬 분해 결과 (k={k}):")
    print("사용자 특성 행렬 (U):")
    print(U_k.round(3))
    print("\n특이값 행렬 (S):")
    print(S_k.round(3))
    print("\n영화 특성 행렬 (V^T):")
    print(Vt_k.round(3))
    
    print(f"\n재구성된 평점 행렬:")
    print(reconstructed.round(2))
    
    # 새로운 사용자에 대한 추천
    new_user = np.array([4, 1, 2, 5, 4])  # 액션/스릴러 선호
    print(f"\n새로운 사용자 평점: {new_user}")
    
    # 사용자 특성 추출
    user_features = new_user @ Vt_k.T @ np.linalg.inv(S_k)
    print(f"사용자 특성: {user_features.round(3)}")
    
    # 추천 점수 계산
    recommendations = user_features @ S_k @ Vt_k
    print(f"추천 점수: {recommendations.round(2)}")
    
    print("\n선형대수 활용:")
    print("- 사용자 취향 → 벡터로 표현")
    print("- 영화 특성 → 벡터로 표현")
    print("- 행렬 분해 → 잠재 요인 발견")
    print("- 추천 점수 → 벡터 연산으로 계산")

netflix_recommendation_simulation()
```
```

---

<br>

## 5. 실습: 직접 만들어보는 선형대수 🛠️

### 5-1. 나만의 벡터 클래스 만들기

```python
class MyVector:
    """직관적인 벡터 클래스"""
    
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        """벡터 덧셈: 이동 경로 합치기"""
        return MyVector(self.x + other.x, self.y + other.y)
    
    def __mul__(self, scalar):
        """스칼라 곱: 크기 조절"""
        return MyVector(self.x * scalar, self.y * scalar)
    
    def magnitude(self):
        """벡터의 크기"""
        return np.sqrt(self.x**2 + self.y**2)
    
    def dot(self, other):
        """내적: 서로 얼마나 같은 방향인지"""
        return self.x * other.x + self.y * other.y
    
    def plot(self, ax, color='blue', label=''):
        """벡터를 화살표로 그리기"""
        ax.quiver(0, 0, self.x, self.y, color=color, scale=10, label=label)
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"

# 사용 예제
v1 = MyVector(3, 2)
v2 = MyVector(1, 4)

print(f"v1 = {v1}")
print(f"v2 = {v2}")
print(f"v1 + v2 = {v1 + v2}")
print(f"v1의 크기 = {v1.magnitude():.2f}")
print(f"v1 · v2 = {v1.dot(v2)}")

# 시각화
fig, ax = plt.subplots(figsize=(8, 8))
ax.set_xlim(-5, 5)
ax.set_ylim(-5, 5)
ax.grid(True, alpha=0.3)
ax.set_aspect('equal')

v1.plot(ax, 'red', 'v1')
v2.plot(ax, 'blue', 'v2')
(v1 + v2).plot(ax, 'green', 'v1 + v2')

ax.legend()
ax.set_title('나만의 벡터 클래스')
plt.show()
```

#### 🎯 과제 1: 벡터 클래스 확장하기
기존 MyVector 클래스에 다음 메서드들을 추가해보세요:

```python
# 추가할 메서드들
def angle(self, other):
    """두 벡터 사이의 각도 계산 (라디안)"""
    # TODO: 구현해보세요!
    pass

def normalize(self):
    """단위 벡터로 정규화"""
    # TODO: 구현해보세요!
    pass

def cross(self, other):
    """외적 계산 (3D 벡터용)"""
    # TODO: 구현해보세요!
    pass
```

### 5-2. 행렬 변환 시각화 과제

```python
# 2D 변환 행렬 구현
class TransformationMatrix:
    """2D 변환 행렬 클래스"""
    
    @staticmethod
    def rotation(angle_degrees):
        """회전 변환 행렬"""
        angle_rad = np.radians(angle_degrees)
        return np.array([
            [np.cos(angle_rad), -np.sin(angle_rad)],
            [np.sin(angle_rad), np.cos(angle_rad)]
        ])
    
    @staticmethod
    def scaling(sx, sy):
        """크기 조절 변환 행렬"""
        return np.array([[sx, 0], [0, sy]])
    
    @staticmethod
    def shear(sx, sy):
        """전단 변환 행렬"""
        return np.array([[1, sx], [sy, 1]])

def visualize_transformations():
    """다양한 변환을 시각화"""
    # 원본 점들 (사각형)
    points = np.array([
        [-1, -1], [1, -1], [1, 1], [-1, 1], [-1, -1]
    ])
    
    # 변환들
    transformations = {
        '회전 45도': TransformationMatrix.rotation(45),
        'X축 2배 확대': TransformationMatrix.scaling(2, 1),
        '전단 변환': TransformationMatrix.shear(0.5, 0)
    }
    
    # 시각화
    fig, axes = plt.subplots(2, 2, figsize=(12, 10))
    axes = axes.flatten()
    
    # 원본
    axes[0].plot(points[:, 0], points[:, 1], 'b-', linewidth=2)
    axes[0].set_title('원본')
    axes[0].set_xlim(-3, 3)
    axes[0].set_ylim(-3, 3)
    axes[0].grid(True, alpha=0.3)
    axes[0].set_aspect('equal')
    
    # 변환들
    for i, (name, matrix) in enumerate(transformations.items()):
        transformed_points = points @ matrix.T
        axes[i+1].plot(transformed_points[:, 0], transformed_points[:, 1], 'r-', linewidth=2)
        axes[i+1].set_title(name)
        axes[i+1].set_xlim(-3, 3)
        axes[i+1].set_ylim(-3, 3)
        axes[i+1].grid(True, alpha=0.3)
        axes[i+1].set_aspect('equal')
    
    plt.tight_layout()
    plt.show()

visualize_transformations()
```

#### 🎯 과제 2: 복합 변환 구현하기
여러 변환을 순서대로 적용하는 복합 변환을 구현해보세요:

```python
def composite_transformation():
    """복합 변환 구현"""
    # TODO: 회전 + 확대 + 전단을 순서대로 적용하는 함수를 만들어보세요!
    # 힌트: 행렬곱을 사용하여 변환들을 결합하세요
    pass
```

### 5-3. 간단한 신경망 가중치 시각화

```python
# 간단한 신경망 가중치를 행렬로 이해하기
def create_simple_network():
    """간단한 신경망 가중치 행렬 생성"""
    # 입력층(2) -> 은닉층(3) -> 출력층(1)
    W1 = np.random.randn(2, 3) * 0.1  # 입력층 -> 은닉층 가중치
    W2 = np.random.randn(3, 1) * 0.1  # 은닉층 -> 출력층 가중치
    
    return W1, W2

def visualize_weights(W1, W2):
    """신경망 가중치 시각화"""
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
    
    # W1 시각화 (2x3 행렬)
    im1 = ax1.imshow(W1, cmap='RdBu', aspect='auto')
    ax1.set_title('입력층 → 은닉층 가중치 (W1)')
    ax1.set_xlabel('은닉층 뉴런')
    ax1.set_ylabel('입력층 뉴런')
    plt.colorbar(im1, ax=ax1)
    
    # W2 시각화 (3x1 행렬)
    im2 = ax2.imshow(W2, cmap='RdBu', aspect='auto')
    ax2.set_title('은닉층 → 출력층 가중치 (W2)')
    ax2.set_xlabel('출력층 뉴런')
    ax2.set_ylabel('은닉층 뉴런')
    plt.colorbar(im2, ax=ax2)
    
    plt.tight_layout()
    plt.show()
    
    print("신경망 가중치 = 행렬의 실제 활용")
    print(f"W1 모양: {W1.shape} (입력 2개 → 은닉층 3개)")
    print(f"W2 모양: {W2.shape} (은닉층 3개 → 출력 1개)")

# 신경망 생성 및 시각화
W1, W2 = create_simple_network()
visualize_weights(W1, W2)
```

#### 🎯 과제 3: 신경망 순전파 구현하기
가중치 행렬을 사용하여 신경망의 순전파를 구현해보세요:

```python
def forward_propagation(input_data, W1, W2, b1, b2):
    """신경망 순전파"""
    # TODO: 입력 데이터를 신경망에 통과시켜 출력을 계산하세요!
    # 힌트: z1 = input @ W1 + b1, a1 = sigmoid(z1), z2 = a1 @ W2 + b2, output = sigmoid(z2)
    pass

def sigmoid(x):
    """시그모이드 활성화 함수"""
    return 1 / (1 + np.exp(-x))
```

### 5-4. 난이도별 실습 과제

#### 🟢 초급 과제: 기본 개념 이해 및 구현
1. **벡터 연산 계산기**: 두 벡터의 덧셈, 뺄셈, 내적, 외적을 계산하는 함수 구현
2. **행렬 기본 연산**: 행렬 덧셈, 곱셈, 전치를 구현하는 함수 작성
3. **단위 벡터 생성**: 주어진 방향의 단위 벡터를 생성하는 함수 구현

#### 🟡 중급 과제: 실제 데이터 적용
1. **이미지 회전**: NumPy 배열로 표현된 이미지를 행렬 변환으로 회전시키기
2. **데이터 정규화**: 데이터셋을 평균 0, 표준편차 1로 정규화하는 함수 구현
3. **거리 계산**: 여러 점들 간의 유클리드 거리를 행렬 연산으로 계산

#### 🔴 고급 과제: 창의적 활용 및 확장
1. **PCA 구현**: 고유값 분해를 사용하여 주성분 분석 직접 구현
2. **행렬 분해**: SVD를 사용하여 이미지 압축 알고리즘 구현
3. **신경망 시각화**: 가중치 행렬의 변화를 애니메이션으로 표현

---

<br>

## 6. 마무리: 선형대수의 핵심 메시지 🎯

### 6-1. 우리가 배운 것들

1. **벡터**: 방향과 크기를 가진 화살표 → 데이터의 기본 단위
2. **행렬**: 데이터 변형기 → 모델의 핵심 구성 요소
3. **고유벡터**: 변하지 않는 특별한 방향 → 데이터의 핵심 특성
4. **행렬 분해**: 복잡한 데이터를 단순한 요소로 분해 → 차원 축소, 추천 시스템

### 6-2. 머신러닝에서의 활용

- **데이터 표현**: 벡터, 행렬, 텐서로 데이터 표현
- **모델 학습**: 가중치 행렬의 최적화
- **특성 추출**: PCA, SVD로 중요한 특성 찾기
- **차원 축소**: 고차원 데이터를 저차원으로 압축

### 6-3. 다음 단계

이제 미적분을 배워서 **"어떻게 최적화할 것인가?"**를 알아보겠습니다!

> **💡 핵심 메시지**: 
> 선형대수는 머신러닝의 "언어"입니다. 
> 복잡한 수식보다는 **직관적 이해**와 **실제 활용**에 집중하세요!

---

## ✅ Part 5.5 완료 체크리스트

### 🎯 달성 목표 확인
다음 항목들을 모두 완료했다면 선형대수의 핵심을 이해한 것입니다!

#### 📐 벡터 이해도
- [ ] 벡터를 화살표로 직관적 이해
- [ ] 벡터 덧셈의 기하학적 의미 설명 가능
- [ ] 벡터 내적이 유사도를 측정한다는 것 이해
- [ ] NumPy로 벡터 연산 구현 가능

#### 🎁 행렬 이해도
- [ ] 행렬을 데이터 변형기로 이해
- [ ] 행렬곱을 복합 변환으로 설명 가능
- [ ] 이미지 필터링에서 행렬이 어떻게 사용되는지 앎
- [ ] NumPy로 행렬 연산 구현 가능

#### 🧭 고유값과 고유벡터 이해도
- [ ] 고유벡터가 변하지 않는 특별한 방향이라는 것 이해
- [ ] PCA의 직관적 의미 이해
- [ ] 차원 축소의 필요성과 방법 이해
- [ ] 실제 데이터에 PCA 적용 가능

#### 🛠️ 실습 완료도
- [ ] 나만의 벡터 클래스 구현
- [ ] 행렬 변환 시각화 코드 작성
- [ ] 이미지 필터링 예제 실행
- [ ] 신경망 가중치 시각화 이해

### 🚀 다음 단계 준비 완료
- [ ] 미적분 학습을 위한 기초 수학 준비
- [ ] 기울기 하강법의 필요성 이해
- [ ] 최적화 문제에 대한 관심과 동기 부여

## 🎯 이제 할 수 있는 것들

### 🚀 즉시 활용 가능한 기술
- **이미지 필터링 알고리즘 구현**: 엣지 검출, 블러, 샤프닝 필터
- **간단한 추천 시스템 만들기**: 사용자-아이템 행렬 분해
- **데이터 차원 축소 (PCA) 적용**: 고차원 데이터를 저차원으로 압축
- **신경망 가중치 시각화**: 행렬로 표현된 가중치 분석

### 💼 포트폴리오 프로젝트 아이디어
- **얼굴 인식 시스템**: 선형대수 기반 특성 추출 및 유사도 계산
- **음악 추천 앱**: 행렬 분해를 활용한 사용자 취향 분석
- **이미지 스타일 변환**: 행렬 변환을 통한 이미지 처리
- **데이터 시각화 도구**: PCA를 활용한 다차원 데이터 시각화

### 🔬 실무 적용 예시
- **금융**: 포트폴리오 최적화, 위험 분석
- **의료**: 의료 이미지 처리, 환자 데이터 분석
- **게임**: 3D 그래픽 변환, 물리 시뮬레이션
- **마케팅**: 고객 세분화, 추천 시스템

> **💡 체크리스트 완료 후**: 
> 80% 이상 완료했다면 다음 파트(Part 5.6: 미적분)로 진행하세요!
> 미완료 항목이 있다면 해당 섹션을 다시 학습하고 실습해보세요.

---

**⬅️ 이전 시간: [Part 5: AI 핵심 라이브러리](../05_ai_core_libraries/part_5_ai_core_libraries.md)**
**➡️ 다음 시간: [Part 5.6: 머신러닝/딥러닝을 위한 미적분](../05.6_calculus_for_ml/part_5.6_calculus_for_ml.md)** 