← Day 4 Phase 0 · Week 1 · Day 5 Day 6 →
_backward 클로저(VJP)를 채우고, 명시적 위상정렬 + adjoint 전파로
backward()를 구현한다. 그 다음 복소스텝 gradcheck로 전 연산을 머신정밀도로 검증하고,
수치 안정적 softmax + cross-entropy를 추가해 Phase 1의 손실을 미리 갖춘다.
완성되면 L.backward() 한 줄이 PyTorch의 그것과 의미상 동일해진다.
🎯 오늘의 목표
📖 개념 (수업)
Day 3 알고리즘을 객체 그래프에 옮긴다. 세 단계뿐이다:
def backward(self):
# (1) 위상정렬: 자식을 먼저, 자기를 나중에 → topo는 leaf→root 순
topo, visited = [], set()
def build(v):
if v not in visited:
visited.add(v)
for child in v._prev: build(child)
topo.append(v)
build(self)
# (2) 루트 adjoint = 1
self.grad = 1.0
# (3) 위상 역순(root→leaf)으로 각 노드의 VJP 적용
for v in reversed(topo):
v._backward()
reversed(topo)가 root→leaf 순서를 보장하므로, 각 v._backward() 호출 시점에
v.grad(=adjoint)는 이미 완성돼 있다. 이게 Day 3에서 증명한 "위상 역순 정확성"의 구현이다.
파이썬 재귀깊이 한계가 걸리면 명시적 스택으로 바꾸면 된다(깊은 그래프 대비).
+=(fan-out 합, Day 3 증명). 따라서 backward()는
grad가 0에서 시작한다고 가정한다. 재호출 전 반드시 grad를 0으로 리셋해야 하며, 안 그러면 누적이 오염된다.
PyTorch의 zero_grad()가 이 불변식의 강제다.
우리 micrograd는 backward 후에도 그래프(_prev, _backward 클로저)가 살아있어 재호출 가능하다.
PyTorch는 기본적으로 backward 후 그래프를 해제한다(메모리 절약). 같은 그래프로 두 번 미분하려면
retain_graph=True가 필요한 이유다. 이 차이를 이해하면 PyTorch의 그 에러 메시지가 더는 미스터리가 아니다.
파라미터 벡터 $\theta\in\mathbb{R}^n$에 대해, 해석적 gradient $g$와 수치 gradient $\tilde g$의 상대오차
$$\text{rel} = \frac{\|g - \tilde g\|_2}{\|g\|_2 + \|\tilde g\|_2 + \varepsilon}$$
를 본다. 중심차분이면 $\sim10^{-7}$, 복소스텝(해석함수만)이면 $\sim10^{-12}$ 이하가 정상.
ReLU 같은 비매끈 지점은 복소스텝이 부적합하니 중심차분으로 검사하되, kink 근처는 피한다.
이게 PyTorch torch.autograd.gradcheck가 내부에서 하는 일이고, 새 연산을 추가할 때마다 돌려야 하는 습관이다.
분류/언어모델 손실. 로짓 $z\in\mathbb{R}^K$, 정답 클래스 $t$에 대해
$$\mathrm{CE}(z,t) = -\log \mathrm{softmax}(z)_t = -z_t + \log\sum_{k} e^{z_k}$$순진하게 $e^{z_k}$를 계산하면 큰 로짓에서 overflow. log-sum-exp trick: $m=\max_k z_k$로 두면
$$\log\sum_k e^{z_k} = m + \log\sum_k e^{z_k - m}$$지수의 인자가 $\le 0$이라 overflow가 없고, 적어도 한 항이 $e^0=1$이라 underflow로 인한 $\log 0$도 없다. CE의 gradient는 놀랄 만큼 깔끔하다: $\partial\mathrm{CE}/\partial z = \mathrm{softmax}(z) - e_t$ (one-hot 차이). 이 우아함이 softmax+CE를 함께 묶는 이유이고, 직접 유도해보면 Day 2의 softmax VJP와 맞물린다.
exp/log/div 합성으로 짜면 동작은 하지만,
중간 노드가 많아 비효율적이고 수치 안정 트릭을 그래프에 녹이기 까다롭다. 그래서 실전 프레임워크는
cross-entropy를 하나의 fused 원시연산으로 제공한다(안정적 forward + 닫힌형 VJP $\mathrm{softmax}(z)-e_t$).
과제에서 둘 다 구현해 정확도/안정성을 비교하라.
✍️ 직접 해보기 (강도 ↑)
_backward를 += 누적으로 구현하고, 위 의사코드로 backward() 완성.
파이썬 재귀깊이를 우회하는 명시적 스택 버전도 만들어 깊이 5000 체인에서 동작 확인.
float라 미분 그래프가 다시 추적되지 않는다.
grad도 Value가 되게 확장하면(또는 backward를 두 번) Hessian-vector product를 얻을 수 있다.
작은 2차 함수에서 $Hv$를 forward-over-reverse로 구해 해석값과 대조하라(Day 2 Pearlmutter 참조).
✅ 스스로 확인
📝 오늘의 기록
docs/00_day05.md:
← Day 4 Day 5 끝 Day 6 — 최적화 & 학습 →