# 기울기 추적 중지하기: torch.no\_grad()

딥러닝 모델을 학습할 때, 파이토치의 Autograd 기능은 자동으로 모든 텐서 연산에 대한 기울기(gradient)를 추적합니다. 이를 통해 역전파(backpropagation)를 통해 기울기를 계산하고, 가중치를 업데이트할 수 있습니다. 그러나 학습 단계가 아닌 추론(inference) 단계에서는 기울기 추적이 필요하지 않기 때문에, 불필요한 연산을 줄여 계산 효율성을 높일 수 있는 방법이 필요합니다. 이때 사용할 수 있는 것이 `torch.no_grad()`입니다.

### Autograd와 기울기 추적

Autograd는 텐서의 연산 그래프를 생성하고, 이 그래프를 통해 역전파 시 기울기를 자동으로 계산합니다. 예를 들어, 텐서 (\mathbf{x})와 파라미터 (\mathbf{W})가 주어진다면, 연산 결과 (\mathbf{y})는 다음과 같이 표현될 수 있습니다.

\[ \mathbf{y} = \mathbf{W} \mathbf{x} + \mathbf{b} ]

이때, 손실 함수 (\mathcal{L})이 주어지면, Autograd는 (\frac{\partial \mathcal{L}}{\partial \mathbf{W}})와 같은 기울기를 자동으로 계산해 줍니다. 이는 학습 과정에서 매우 유용하지만, 모델의 추론 단계에서는 기울기를 계산할 필요가 없습니다. 따라서 이 경우에도 기울기 추적이 이루어진다면, 메모리와 계산 자원이 불필요하게 낭비됩니다.

### torch.no\_grad()의 역할

`torch.no_grad()`는 블록 내에서 기울기 추적을 비활성화합니다. 이 문맥 관리자(context manager)를 사용하면, 블록 안에서 이루어지는 모든 텐서 연산이 그래디언트 계산을 위해 기록되지 않습니다. 이를 통해 메모리 사용량을 줄이고, 추론 속도를 높일 수 있습니다.

예를 들어, 다음과 같은 코드를 생각해 봅시다:

```python
import torch

# 학습된 모델을 불러온다고 가정
model = MyModel()
input_tensor = torch.randn(1, 3, 224, 224)

# 기울기 추적 활성화 (학습 시 사용)
output_train = model(input_tensor)

# 기울기 추적 비활성화 (추론 시 사용)
with torch.no_grad():
    output_inference = model(input_tensor)
```

여기서 `torch.no_grad()`를 사용함으로써, 추론 단계에서 Autograd가 메모리와 연산을 차지하지 않도록 합니다. 이는 메모리 사용량을 줄이는 데 매우 유용하며, 특히 대규모 모델에서 추론을 여러 번 실행해야 하는 경우 큰 이점을 제공합니다.

### torch.no\_grad()의 효과

`torch.no_grad()`를 사용하면, 여러 가지 이점을 얻을 수 있습니다. 이를 좀 더 구체적으로 알아보겠습니다.

1. **메모리 사용량 감소**\
   일반적으로 Autograd는 텐서 연산을 추적하면서 중간 결과(Intermediate results)를 저장합니다. 이는 역전파 단계에서 기울기를 계산하기 위해 필요한 정보입니다. 하지만 `torch.no_grad()`를 사용하면 이러한 중간 결과를 저장하지 않기 때문에 메모리 사용량이 크게 줄어듭니다. 이는 특히 큰 모델을 다룰 때 유용하며, GPU 메모리 사용량을 관리하는 데 도움을 줍니다.
2. **추론(inference) 속도 향상**\
   기울기 추적을 비활성화하면, 연산 그래프를 구성하지 않기 때문에 연산 속도가 빨라집니다. 이는 추론 속도를 크게 향상시킬 수 있으며, 실시간 응용 프로그램에서는 매우 중요한 요소가 됩니다.
3. **모델 평가 단계에서의 사용**\
   모델 평가(evaluation) 단계에서는 파라미터를 업데이트하지 않습니다. 따라서 손실 함수의 기울기를 계산할 필요가 없으며, `torch.no_grad()`를 사용하여 연산을 더욱 효율적으로 처리할 수 있습니다. 이는 보통 `model.eval()`과 함께 사용됩니다.

### torch.no\_grad()와 requires\_grad\_

파이토치에서 텐서의 속성 중 하나인 `requires_grad_`는 기울기 추적을 설정할 수 있는 또 다른 방법입니다. 특정 텐서의 `requires_grad`를 `False`로 설정하면, 해당 텐서와 관련된 모든 연산에서 기울기 추적이 비활성화됩니다. 예를 들어 다음과 같습니다.

```python
x = torch.randn(3, 3, requires_grad=True)
print(x.requires_grad)  # 출력: True

# x 텐서의 기울기 추적을 비활성화
x.requires_grad_(False)
print(x.requires_grad)  # 출력: False
```

하지만 `requires_grad_`는 특정 텐서의 속성만을 변경할 수 있으며, 모든 텐서 연산을 포함한 더 넓은 범위를 비활성화할 때는 `torch.no_grad()`를 사용하는 것이 효율적입니다. `torch.no_grad()`는 블록 내에서 선언된 텐서뿐 아니라, 해당 블록 내에서 수행되는 모든 연산의 기울기 추적을 비활성화합니다.

### torch.no\_grad()와 torch.enable\_grad()

기본적으로 파이토치는 기울기 추적이 활성화되어 있습니다. 그러나 `torch.no_grad()`를 사용하면, 기울기 추적이 블록 내에서만 비활성화되고, 블록이 끝나면 다시 활성화됩니다. 이와 반대로 `torch.enable_grad()`를 사용하면, `torch.no_grad()` 블록 내부에서도 기울기 추적을 강제로 활성화할 수 있습니다. 이 두 기능을 적절히 활용하면 기울기 추적 설정을 세밀하게 제어할 수 있습니다.

```python
# 예시 코드
with torch.no_grad():
    # 여기서는 기울기 추적이 비활성화됨
    output = model(input_tensor)
    with torch.enable_grad():
        # 강제로 기울기 추적을 다시 활성화
        output_grad_enabled = model(input_tensor)
```

위의 코드에서, `torch.no_grad()` 블록은 일반적으로 기울기 추적을 비활성화하지만, 내부의 `torch.enable_grad()` 블록을 통해 특정 부분에서는 다시 기울기 추적을 활성화할 수 있습니다. 이를 통해 유연한 연산 제어가 가능합니다.

### 기울기 추적의 수식적 이해

텐서 (\mathbf{x})와 파라미터 (\mathbf{W}), (\mathbf{b})가 주어졌을 때, 출력 (\mathbf{y})는 다음과 같이 정의할 수 있습니다:

\[ \mathbf{y} = \mathbf{W} \mathbf{x} + \mathbf{b} ]

손실 함수 (\mathcal{L}(\mathbf{y}, \mathbf{t}))에서 기울기를 계산할 때, Autograd는 다음과 같은 연산을 수행합니다:

\[ \frac{\partial \mathcal{L}}{\partial \mathbf{W}} = \frac{\partial \mathcal{L}}{\partial \mathbf{y}} \frac{\partial \mathbf{y}}{\partial \mathbf{W}} ]

그러나 `torch.no_grad()`가 사용되면, 이와 같은 연산 그래프가 생성되지 않기 때문에 (\frac{\partial \mathcal{L}}{\partial \mathbf{W}})와 같은 기울기를 계산할 수 없게 됩니다. 따라서 학습과 추론 단계에서의 자원 낭비를 막을 수 있는 것입니다.

### torch.no\_grad()의 사용 예시

`torch.no_grad()`는 모델의 추론(inference)뿐만 아니라, 여러 다른 상황에서도 유용하게 사용될 수 있습니다. 아래에서는 몇 가지 예시를 통해 그 활용 방안을 설명하겠습니다.

#### 1. 추론 단계에서의 활용

가장 일반적인 사용 사례는 학습이 완료된 모델을 사용해 예측을 수행하는 추론 단계입니다. 추론 시에는 역전파(backpropagation)나 기울기 계산이 필요 없기 때문에, `torch.no_grad()`를 사용하여 효율적인 추론을 할 수 있습니다. 예를 들면 다음과 같습니다:

```python
import torch

# 사전 학습된 모델 불러오기
model = MyModel()
model.eval()  # 모델을 평가 모드로 설정
input_tensor = torch.randn(1, 3, 224, 224)

# 추론 단계에서는 기울기 추적이 필요 없음
with torch.no_grad():
    output = model(input_tensor)
print(output)
```

위 코드에서 `model.eval()`을 통해 모델을 평가 모드로 전환하였으며, 이는 배치 정규화(batch normalization) 및 드롭아웃(dropout)과 같은 계층(layer)을 학습 모드에서 평가 모드로 변경합니다. 이후 `torch.no_grad()`를 사용하여 기울기 추적을 비활성화함으로써, 추론에 필요한 계산 자원과 메모리를 절약합니다.

#### 2. 특정 계산 단계에서의 기울기 추적 비활성화

때로는 모델 학습 과정에서도 특정 연산에 대해 기울기 추적을 원하지 않을 수 있습니다. 예를 들어, 어떤 정규화 기술을 사용하거나, 보조적인 예측을 수행할 때 기울기 계산을 피하고 싶을 수 있습니다. 이때 `torch.no_grad()`를 사용하여 특정 계산에 대해서만 기울기 추적을 일시적으로 비활성화할 수 있습니다.

```python
# 예시: 보조적인 정규화 값을 계산할 때
loss = primary_loss
with torch.no_grad():
    regularization_term = compute_regularization(model)
loss += regularization_term
loss.backward()
```

위 코드에서는 `compute_regularization` 함수가 모델 파라미터를 기반으로 계산을 수행하지만, 이 계산은 역전파 과정에 포함되지 않아야 합니다. 따라서 `torch.no_grad()`를 사용하여 해당 연산에서 기울기 추적을 비활성화합니다.

#### 3. 메모리 절약을 위한 활용

`torch.no_grad()`는 추론을 위한 메모리 사용량을 줄이는 데 매우 유용합니다. 예를 들어, 대규모 배치(batch)를 처리해야 하는 상황에서 `torch.no_grad()`를 사용하면 메모리 사용량이 감소하여 더 큰 배치를 처리하거나, 추론 시간을 단축할 수 있습니다.

다음은 대규모 배치에 대해 추론을 수행할 때 메모리 효율성을 높이는 예시입니다:

```python
# 대규모 데이터셋에 대해 배치 단위로 추론을 수행
for batch in data_loader:
    with torch.no_grad():
        predictions = model(batch)
    process_predictions(predictions)
```

이 경우, 각 배치에 대해 기울기 추적이 비활성화되어, 필요 없는 연산을 수행하지 않고 메모리와 계산 자원을 절약할 수 있습니다.

### torch.no\_grad()와 Model Freezing

모델의 일부 파라미터를 고정(freezing)하고 다른 파라미터만 학습하는 경우에도 `torch.no_grad()`를 활용할 수 있습니다. 예를 들어, 전이 학습(transfer learning)을 할 때, 사전 학습된 모델의 파라미터를 고정하고 마지막 층만 학습하는 경우가 있습니다.

```python
# 사전 학습된 모델의 파라미터를 고정
for param in model.features.parameters():
    param.requires_grad = False

# 추론 단계에서 추가적으로 torch.no_grad() 사용
with torch.no_grad():
    output = model(input_tensor)
```

이 코드에서는 `param.requires_grad = False`를 통해 특정 파라미터에 대해 기울기 추적을 비활성화했으며, 모델의 추론 단계에서도 `torch.no_grad()`를 사용하여 메모리를 더욱 절약할 수 있습니다.

### torch.no\_grad()와 torch.inference\_mode()

PyTorch 1.9 버전 이후, `torch.no_grad()`와 유사한 새로운 기능으로 `torch.inference_mode()`가 도입되었습니다. `torch.inference_mode()`는 `torch.no_grad()`와 마찬가지로 기울기 추적을 비활성화하지만, 추가적으로 내부적으로 더 많은 최적화가 이루어져 추론(inference) 성능이 더 개선될 수 있습니다.

### torch.inference\_mode()와의 차이점

`torch.inference_mode()`는 `torch.no_grad()`와 비슷한 역할을 하지만, 몇 가지 중요한 차이점이 있습니다. 이는 특히 추론을 위한 최적화에 초점을 맞추고 설계된 기능으로, 더 빠르고 메모리 효율적인 성능을 제공합니다.

#### 1. 내부 동작 최적화

`torch.inference_mode()`는 기울기 추적을 비활성화할 뿐만 아니라, 내부적으로 PyTorch의 텐서 메모리 관리 방식도 최적화합니다. 이로 인해 일반적으로 `torch.no_grad()`보다 더 나은 성능을 발휘할 수 있습니다. 이 모드는 반복적으로 추론을 수행해야 하는 상황에서 유용합니다.

#### 2. 변경 불가능한(immutable) 버퍼

추론 시 모델의 상태가 불필요하게 수정되는 것을 방지하기 위해, `torch.inference_mode()`는 모든 버퍼를 읽기 전용(immutable)으로 처리합니다. 이는 메모리 무결성을 유지하면서도, 더 빠르게 연산할 수 있도록 도와줍니다. `torch.no_grad()`는 단순히 기울기 추적을 비활성화할 뿐, 이러한 변경을 제공하지 않으므로 추가적인 성능 향상이 필요한 경우 `torch.inference_mode()`가 더 적합할 수 있습니다.

```python
# torch.no_grad()와 torch.inference_mode() 비교 예시
import torch

model = MyModel()
input_tensor = torch.randn(1, 3, 224, 224)

# 기존 방식: torch.no_grad()
with torch.no_grad():
    output_no_grad = model(input_tensor)

# 새로운 방식: torch.inference_mode()
with torch.inference_mode():
    output_inference_mode = model(input_tensor)
```

위의 코드에서는 `torch.inference_mode()`를 사용해 기존 방식보다 더 최적화된 추론을 수행할 수 있습니다. 특히 대규모 배치 처리나 실시간 응용 프로그램에서 성능 향상 효과가 두드러질 수 있습니다.

### 기울기 추적 비활성화의 주의 사항

`torch.no_grad()`를 사용할 때 몇 가지 주의해야 할 점이 있습니다. 기울기 추적이 비활성화되면 텐서 연산의 결과가 기울기 계산에 포함되지 않기 때문에, 이를 의도하지 않게 사용하면 학습에 부정적인 영향을 미칠 수 있습니다.

#### 1. 기울기 누락 문제

`torch.no_grad()` 블록 안에서 연산된 텐서가 예기치 않게 기울기를 갖지 않게 될 경우, 모델 학습에 필요한 파라미터 업데이트가 제대로 이루어지지 않을 수 있습니다. 이는 학습 코드를 작성할 때 의도치 않은 버그를 유발할 수 있으므로, 반드시 학습 단계와 추론 단계를 명확히 구분해야 합니다.

#### 2. requires\_grad와의 혼동

`torch.no_grad()`는 블록 내 모든 연산에 대해 기울기 추적을 비활성화하지만, 이는 텐서의 `requires_grad` 속성을 변경하지 않습니다. 따라서 블록이 끝난 후에도 텐서의 `requires_grad` 속성은 여전히 그대로 유지됩니다. 반면 `requires_grad = False`는 해당 텐서에 대해서만 기울기 추적을 비활성화하는 속성 설정입니다.

```python
# 예시 코드: requires_grad와 torch.no_grad() 비교
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)

# torch.no_grad() 블록 내에서 연산 수행
with torch.no_grad():
    y = x * 2

print(y.requires_grad)  # 출력: False

# 텐서의 requires_grad 속성 직접 변경
x.requires_grad_(False)
print(x.requires_grad)  # 출력: False
```

위 코드에서, `torch.no_grad()` 블록 내에서 연산된 `y` 텐서는 기울기를 추적하지 않지만, 블록이 종료된 후에는 여전히 `x`의 `requires_grad` 속성은 유지됩니다. 따라서 모델의 학습 파이프라인을 설계할 때, 이러한 동작 방식을 이해하고 사용하는 것이 중요합니다.

### 기울기 추적 중지의 성능 비교

기울기 추적을 비활성화함으로써 얼마나 성능 향상이 가능한지 구체적인 실험을 통해 확인할 수 있습니다. 다음은 예시로, 동일한 모델을 추론할 때 `torch.no_grad()`와 그렇지 않은 경우의 성능 차이를 측정하는 방법을 보여줍니다.

```python
import torch
import time

model = MyModel()
input_tensor = torch.randn(1, 3, 224, 224)

# 기울기 추적 활성화 상태에서의 추론 시간 측정
start = time.time()
for _ in range(1000):
    output = model(input_tensor)
print(f"기울기 추적 활성화 추론 시간: {time.time() - start:.4f}초")

# torch.no_grad() 사용 시 추론 시간 측정
start = time.time()
with torch.no_grad():
    for _ in range(1000):
        output = model(input_tensor)
print(f"torch.no_grad() 사용 추론 시간: {time.time() - start:.4f}초")
```

위 실험은 동일한 모델을 1000번 반복하여 추론하는 데 소요된 시간을 비교합니다. 일반적으로 `torch.no_grad()`를 사용하면 추론 속도가 더 빠르고, 메모리 사용량도 줄어듭니다. 이는 기울기 추적이 필요하지 않은 상황에서 자원을 효율적으로 사용할 수 있게 해주는 좋은 예시입니다.
