zero_grad 가 필요한 이유
학습 루프에서 자주 빠뜨리는 게 optimizer.zero_grad()다. 그런데 이게 없으면 학습이 망가진다. 왜 매 스텝마다 기울기를 초기화해야 하는지 짧게 정리해봤다.
loss.backward()는 기울기를 누적한다#
핵심은 loss.backward()의 동작이다. loss.backward()는 새로운 기울기를 재할당하는 게 아니라, 계속 += 방식으로 기존 기울기에 더해서 누적한다.
어디에 누적되나#
기울기가 쌓이는 곳은 파라미터 텐서 자신이 들고 있는 .grad 속성이다. 별도의 전역 저장소가 아니라 w.grad, b.grad 처럼 각 텐서에 붙어 있는 grad 버퍼다. loss.backward()는 연산 그래프를 거꾸로 타면서 requires_grad=True 인 leaf 텐서(보통 모델 파라미터)의 .grad에 계산한 기울기를 더해 넣는다. 중간 계산 결과 텐서는 미분이 계산되긴 해도 .grad에는 남지 않는다.
그래서 optimizer.step()은 이 .grad를 읽어서 파라미터를 갱신하고, optimizer.zero_grad()는 이 .grad를 0으로 비운다. 셋 다 같은 .grad를 공유한다.
같은 파라미터에 backward를 두 번 하면 .grad가 쌓이는 걸 그대로 볼 수 있다.
w = torch.tensor([2.0], requires_grad=True)
loss1 = (w ** 2).sum() # dloss/dw = 2w = 4
loss1.backward()
print(w.grad) # tensor([4.])
loss2 = (w ** 2).sum() # 또 4
loss2.backward()
print(w.grad) # tensor([8.]) ← 덮어쓰는 게 아니라 4+4 누적
왜 덮어쓰지 않고 누적하나#
이건 버그가 아니라 PyTorch의 의도된 설계다. 여러 경로에서 흘러온 기울기를 자연스럽게 합치기 위해서다.
- 한 파라미터가 여러 번 쓰이는 경우(가중치 공유 등)에는 각 경로의 기울기를 더해야 올바른 전체 기울기가 나온다.
+=누적이라 별도 처리 없이 자동으로 합쳐진다. - gradient accumulation 기법처럼 작은 batch로 backward만 여러 번 하고 step은 가끔 한 번씩 하는 방식도 누적이 기본 동작이라 그냥 된다.
즉 여러 기울기를 합치는 게 정상 시나리오라서 누적이 기본값이고, 매 스텝 독립적으로 학습할 때는 우리가 zero_grad()로 직접 끊어줘야 한다. 안 그러면 이전 스텝의 기울기가 .grad에 그대로 남은 채 이번 기울기가 더해져서 엉뚱한 방향으로 갱신된다.
학습 루프에서의 순서#
for epoch in range(epochs):
optimizer.zero_grad() # 누적된 기울기 초기화
pred = model(x) # 순전파
loss = criterion(pred, t)
loss.backward() # 기울기 계산 (grad에 누적됨)
optimizer.step() # w, b 갱신
순서가 중요하다. backward로 기울기를 구하기 전에 zero_grad로 이전 기울기를 비워주고, 마지막에 step으로 갱신한다. zero_grad를 빼먹으면 backward의 += 누적 때문에 기울기가 스텝마다 부풀어 학습이 깨진다.
댓글