# Autograd를 활용한 간단한 예제

### Autograd 개요

PyTorch의 `autograd`는 텐서 연산에 대해 자동으로 미분을 계산할 수 있도록 해주는 기능입니다. 이 기능은 텐서의 연산 기록을 추적하여, 역전파(Backpropagation)를 통해 기울기(Gradient)를 계산할 수 있습니다. 모든 텐서는 기본적으로 연산 그래프의 노드로 작동하며, `requires_grad` 속성을 통해 기울기 계산이 필요한 텐서인지 여부를 설정할 수 있습니다.

### 간단한 선형 회귀 모델 예제

선형 회귀 모델은 가장 기본적인 기계 학습 모델 중 하나로, 독립 변수 $\mathbf{x}$와 종속 변수 $y$ 사이의 관계를 직선 형태로 모델링합니다. 여기서는 $y = wx + b$ 형태의 간단한 선형 모델을 설정하고, PyTorch의 `autograd`를 활용해 기울기를 계산하는 과정을 살펴보겠습니다.

#### 수학적 배경

먼저 선형 회귀 모델에서 예측 값 $\hat{y}$는 다음과 같이 정의됩니다.

\[ \hat{y} = w x + b ]

여기서:

* $w$: 가중치 (Weight)
* $b$: 편향 (Bias)
* $x$: 입력 값 (Input)

손실 함수로는 평균 제곱 오차(MSE)를 사용할 것입니다. 손실 함수 $L$은 다음과 같이 정의됩니다.

\[ L = \frac{1}{N} \sum\_{i=1}^{N} (y\_i - \hat{y}*i)^2 = \frac{1}{N} \sum*{i=1}^{N} (y\_i - (w x\_i + b))^2 ]

여기서:

* $N$: 데이터의 총 개수
* $y\_i$: 실제 출력 값 (Actual Output)
* $\hat{y}\_i$: 예측된 출력 값 (Predicted Output)

### PyTorch 구현

다음으로, 이를 코드로 구현해 보겠습니다. PyTorch에서는 손실 함수를 직접 계산하고 `autograd`를 통해 기울기를 자동으로 계산할 수 있습니다.

```python
import torch

# 데이터 초기화
x = torch.tensor([1.0, 2.0, 3.0, 4.0])
y = torch.tensor([2.0, 4.0, 6.0, 8.0])

# 가중치와 편향 초기화
w = torch.tensor(0.0, requires_grad=True)
b = torch.tensor(0.0, requires_grad=True)

# 학습률 설정
learning_rate = 0.01

# 예측 함수 정의
def forward(x):
    return w * x + b

# 손실 함수 정의
def loss(y_pred, y):
    return ((y_pred - y) ** 2).mean()

# 10번 반복 학습
for epoch in range(10):
    # 예측값 계산
    y_pred = forward(x)
    
    # 손실 계산
    l = loss(y_pred, y)
    
    # 역전파를 통해 기울기 계산
    l.backward()
    
    # 경사 하강법으로 파라미터 업데이트
    with torch.no_grad():
        w -= learning_rate * w.grad
        b -= learning_rate * b.grad
    
    # 기울기 초기화
    w.grad.zero_()
    b.grad.zero_()
    
    # 손실 출력
    print(f'Epoch {epoch+1}: w = {w.item():.3f}, b = {b.item():.3f}, loss = {l.item():.3f}')
```

### 그래프의 역할과 계산 그래프

위 코드에서 `autograd`는 텐서들의 연산을 기록하여, 이들을 통해 생성된 그래프를 구성합니다. 이를 통해 역전파 시, 그래프를 탐색하며 각 파라미터의 기울기를 계산할 수 있습니다. `l.backward()` 명령어를 실행하면, `w`와 `b`에 대한 기울기가 자동으로 계산됩니다.

아래는 `l.backward()` 실행 시, `autograd`가 구축한 계산 그래프의 개념을 보여줍니다.

{% @mermaid/diagram content="graph LR
A\[x] --> B\[wx + b]
B --> C\["(y - (wx + b))^2"]
C --> D\[L]" %}

### 기울기 계산

역전파를 통해 손실 $L$의 각 파라미터에 대한 기울기를 구하는 과정은 다음과 같습니다.

먼저 $L$을 $w$와 $b$에 대해 편미분합니다:

\[ \frac{\partial L}{\partial w} = \frac{2}{N} \sum\_{i=1}^{N} (y\_i - (w x\_i + b)) (-x\_i) ]

\[ \frac{\partial L}{\partial b} = \frac{2}{N} \sum\_{i=1}^{N} (y\_i - (w x\_i + b)) (-1) ]

계산 그래프에서 각 연산의 노드는 역전파 시, 상류에서 내려온 기울기를 곱하여 전달하게 됩니다. 이 기울기 전달 과정이 PyTorch의 `autograd` 기능을 통해 자동으로 수행되므로, 사용자는 수식의 복잡성을 신경 쓰지 않아도 됩니다.

### 기울기 추적과 텐서의 `requires_grad` 속성

위 예제에서 `w`와 `b`는 `requires_grad=True`로 설정되었습니다. 이는 PyTorch에게 이 변수들이 학습 중에 기울기를 추적해야 한다고 알리는 역할을 합니다. 만약 `requires_grad=False`로 설정하면, 역전파 시 해당 변수의 기울기가 계산되지 않습니다.

다음은 예제의 텐서 초기화 부분에서 `requires_grad`의 사용 예입니다:

```python
w = torch.tensor(0.0, requires_grad=True)
b = torch.tensor(0.0, requires_grad=True)
```

이제, 기울기 추적을 위해 어떤 방식으로 작동하는지 알아보겠습니다. 예를 들어, 다음과 같이 $w$에 대한 함수 $y = w^2 + 3w + 2$가 있다고 가정합시다:

\[ y = w^2 + 3w + 2 ]

여기서 $w=1.0$일 때 $y$의 기울기를 계산해보겠습니다.

```python
w = torch.tensor(1.0, requires_grad=True)
y = w**2 + 3*w + 2
y.backward()
print(w.grad)  # 출력: 5.0
```

#### 수학적 설명

위의 예에서 기울기를 수학적으로 계산해 보면:

\[ \frac{\partial y}{\partial w} = 2w + 3 ]

따라서, $w=1.0$일 때:

\[ \frac{\partial y}{\partial w} = 2(1.0) + 3 = 5.0 ]

위의 코드와 일치하는 결과를 얻을 수 있습니다. PyTorch의 `autograd`는 연산을 추적하여 그래프를 구성하고, 이 그래프를 역전파 시 탐색하여 자동으로 기울기를 계산합니다.

### 기울기 추적 멈추기: `torch.no_grad()`

때로는 기울기 추적이 필요하지 않을 때가 있습니다. 예를 들어, 학습이 완료된 후에 모델을 평가할 때, 파라미터를 변경하지 않기 때문에 기울기를 계산할 필요가 없습니다. 이때는 `torch.no_grad()` 블록을 사용하여 불필요한 메모리 사용과 계산을 방지할 수 있습니다:

```python
with torch.no_grad():
    y_pred = forward(x)
    print(y_pred)
```

이 블록 안에서는 `w`와 `b`의 기울기가 추적되지 않으며, 이를 통해 모델 평가 시 성능을 최적화할 수 있습니다.

### `retain_graph` 옵션

`autograd`를 통해 역전파를 수행하면 기본적으로 그래프는 삭제됩니다. 동일한 그래프를 여러 번 사용하여 역전파를 실행하고 싶다면, `retain_graph=True` 옵션을 설정해야 합니다. 다음은 그 사용 예입니다:

```python
y = forward(x)
l = loss(y, y_true)
l.backward(retain_graph=True)  # 그래프를 유지하여 이후의 backward()에서도 사용 가능
```

이 옵션을 사용하는 상황은 드물지만, 복잡한 그래프에서 여러 번의 역전파를 해야 하는 경우에 유용합니다.

### 다중 입력과 다중 출력에서의 기울기 계산

`autograd`는 다중 입력과 다중 출력에 대해서도 유연하게 동작합니다. 예를 들어, 입력 벡터 $\mathbf{x} \in \mathbb{R}^n$와 출력 스칼라 $y \in \mathbb{R}$의 경우, $y$를 각 입력 요소 $x\_i$에 대해 미분하면 다음과 같은 벡터 기울기를 얻게 됩니다:

\[ \mathbf{g} = \nabla\_{\mathbf{x}} y = \left\[ \frac{\partial y}{\partial x\_1}, \frac{\partial y}{\partial x\_2}, \ldots, \frac{\partial y}{\partial x\_n} \right] ]

이는 벡터 $\mathbf{x}$가 주어졌을 때 `autograd`가 자동으로 $\mathbf{g}$를 계산할 수 있음을 의미합니다.

또한, 다중 출력 $\mathbf{y} \in \mathbb{R}^m$일 경우, $\mathbf{y}$와 $\mathbf{x}$ 사이의 야코비안(Jacobian) 행렬 $\mathbf{J}$를 생성할 수 있으며, 이는 다음과 같이 정의됩니다:

\[ \mathbf{J} = \frac{\partial \mathbf{y}}{\partial \mathbf{x}} = \begin{bmatrix} \frac{\partial y\_1}{\partial x\_1} & \frac{\partial y\_1}{\partial x\_2} & \cdots & \frac{\partial y\_1}{\partial x\_n} \ \frac{\partial y\_2}{\partial x\_1} & \frac{\partial y\_2}{\partial x\_2} & \cdots & \frac{\partial y\_2}{\partial x\_n} \ \vdots & \vdots & \ddots & \vdots \ \frac{\partial y\_m}{\partial x\_1} & \frac{\partial y\_m}{\partial x\_2} & \cdots & \frac{\partial y\_m}{\partial x\_n} \end{bmatrix} ]

PyTorch의 `autograd`는 이와 같은 복잡한 구조도 자동으로 처리할 수 있습니다.
