포스트

PyTorch 기초 (2) - autograd: 자동 미분과 계산 그래프

딥러닝 기초에서 손으로 짰던 역전파를 PyTorch가 대신 계산해주는 원리 — requires_grad와 계산 그래프, backward()와 grad, 기울기 누적과 zero_grad, no_grad와 detach — 를 정리하고 autograd만으로 경사하강을 돌려본 PyTorch 기초 (2)편입니다.

PyTorch 기초 (2) - autograd: 자동 미분과 계산 그래프

PyTorch 기초 시리즈(6편)의 2편입니다. 전체 목차는 0편에 있습니다. 이 편이 PyTorch를 NumPy와 결정적으로 다르게 만드는 지점, autograd입니다.

손으로 짜던 역전파를 대신해준다

딥러닝 기초 (4)에서 XOR 신경망을 학습시킬 때, 우리는 역전파를 한 줄 한 줄 손으로 구현했습니다. 출력층 오차를 구하고, 그것을 가중치를 타고 은닉층으로 거슬러 전파하며 각 층의 기울기를 계산했습니다. 층이 조금만 복잡해져도 이 작업은 금세 감당하기 어려워집니다.

PyTorch의 autograd는 이걸 자동으로 합니다. 순전파를 계산하는 동안 어떤 연산이 있었는지를 기록해두었다가, backward() 한 번으로 모든 기울기를 연쇄법칙에 따라 되짚어 계산합니다. 우리가 손으로 짠 그 과정을, 프레임워크가 대신 돌리는 것입니다.

requires_grad와 계산 그래프

시작은 텐서에 “이 값에 대한 기울기를 추적하라”고 표시하는 것입니다. requires_grad=True를 준 텐서가 연산에 참여하면, PyTorch는 그 연산들을 계산 그래프로 이어 붙입니다.

1
2
3
4
5
6
7
import torch

x = torch.tensor(2.0, requires_grad=True)
y = x ** 2                # y가 x로부터 어떻게 나왔는지 기록됨

y.backward()              # dy/dx를 계산해 x.grad에 채운다
x.grad                    # tensor(4.)  —  dy/dx = 2x = 2·2 = 4

y.backward()를 부르면 y를 각 입력으로 미분한 값이 그 입력의 .grad에 저장됩니다. y = x²의 미분은 2x이고, x=2에서 값은 정확히 4입니다. 손으로 미분한 결과와 같습니다.

연결이 여러 단계여도 똑같습니다. 연쇄법칙이 자동으로 적용됩니다.

1
2
3
4
5
6
w = torch.tensor(3.0, requires_grad=True)
x, target = torch.tensor(2.0), torch.tensor(10.0)

loss = (w * x - target) ** 2   # 예측 w·x 와 정답의 제곱오차
loss.backward()
w.grad                         # tensor(-16.)  —  dL/dw = 2(w·x − t)·x = 2(6−10)·2

기울기는 누적된다: zero_grad

여기서 초보자가 가장 자주 걸리는 함정이 있습니다. .grad는 덮어쓰이지 않고 누적(accumulate)됩니다. backward()를 두 번 부르면 기울기가 더해집니다.

1
2
3
4
5
6
w = torch.tensor(3.0, requires_grad=True)

loss = (w * 2 - 10) ** 2
loss.backward();  print(w.grad)   # tensor(-16.)
loss = (w * 2 - 10) ** 2
loss.backward();  print(w.grad)   # tensor(-32.)  —  지워지지 않고 쌓였다

그래서 학습 루프에서는 매 스텝 기울기를 계산하기 전에 이전 기울기를 0으로 비워야 합니다. 이걸 빼먹으면 기울기가 계속 쌓여 학습이 이상해집니다. (누적이 기본값인 것은 RNN처럼 여러 번에 나눠 기울기를 더해야 하는 경우를 위한 설계입니다.)

no_grad와 detach: 그래프에서 빼기

기울기를 추적하지 말아야 할 때도 있습니다. 추론(예측)만 할 때나, 가중치를 직접 갱신할 때입니다. 이때 torch.no_grad() 블록 안에서 계산하면 그래프를 만들지 않아 메모리와 시간을 아낍니다.

1
2
with torch.no_grad():
    prediction = w * x        # 그래프에 기록하지 않음 (추론용)

특정 텐서 하나를 그래프에서 떼어낼 때는 .detach()를 씁니다.

autograd만으로 경사하강 한 바퀴

이제 조각을 모아, nn이나 옵티마이저 없이 autograd만으로 선형 함수 Y = 3·X를 학습시켜 봅니다. 목표는 w가 3으로 수렴하는 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
X = torch.arange(1, 11, dtype=torch.float32)   # 1..10
Y = 3 * X                                       # 정답

w = torch.tensor(0.0, requires_grad=True)
lr = 0.01

for step in range(30):
    loss = ((w * X - Y) ** 2).mean()   # 순전파: 예측과 손실
    loss.backward()                    # 역전파: dL/dw 자동 계산
    with torch.no_grad():
        w -= lr * w.grad               # 갱신: 기울기 반대로
        w.grad.zero_()                 # 다음 스텝을 위해 기울기 비우기

print(round(w.item(), 4))              # 3.0 근처로 수렴

딥러닝 기초 (4)의 경사하강 루프와 구조가 똑같습니다. 다른 점은 단 하나, 손으로 미분해 grad를 구하던 자리를 loss.backward()가 대신한다는 것입니다. 이것이 autograd가 하는 일의 전부입니다.

정리

개념한 줄 요약
requires_grad=True이 텐서에 대한 기울기를 추적하라는 표시
계산 그래프순전파 연산을 기록해 역전파 경로를 만듦
backward()연쇄법칙으로 모든 기울기를 계산해 .grad에 채움
기울기 누적.grad는 덮이지 않고 쌓임 → 매 스텝 zero_() 필요
no_grad() / detach()추론·직접 갱신 때 그래프 추적을 끔

autograd 덕분에 우리는 더 이상 역전파를 손으로 짜지 않아도 됩니다. 그런데 위 예제처럼 파라미터가 w 하나면 직접 관리해도 되지만, 층이 많아지면 수많은 가중치를 일일이 챙기기가 번거롭습니다. 다음 편은 이 파라미터들을 묶어 관리하고 학습 루프를 정형화하는 nn.Module과 옵티마이저입니다.

다음 글: PyTorch 기초 (3) - 모델 조립: nn.Module, 손실, 옵티마이저, 학습 루프

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.