← Day 3 Phase 0 · Week 1 · Day 4 Day 5 →

Day 4 — micrograd ① define-by-run 그래프

Day 3의 절차형 tape를 객체지향 Value로 캡슐화한다. 연산자 오버로딩으로 forward 실행 중 그래프가 자동 구축되게 한다 — 이게 PyTorch가 쓰는 define-by-run 패러다임이다. 각 노드는 자기 VJP를 클로저로 들고, 그래프의 위상 구조를 부모 집합으로 기억한다. 오늘은 forward + 그래프 구축 + 풍부한 연산집합까지. backward 오케스트레이션은 내일. 코드는 네가 쓴다 — 설계 결정과 힌트만 준다.
약속: 완성본을 베끼지 마라. 막히면 정답이 아니라 "이 부분 힌트"를 요청해라. 너는 최고수준이니, 이 과제의 진짜 도전은 "동작"이 아니라 설계의 정당성을 스스로 논증하는 것이다.

🎯 오늘의 목표

📖 개념 (수업)

1. define-by-run — 그래프가 실행 중 자라난다

두 가지 AD 설계 철학이 있다:

define-and-run (정적)define-by-run (동적)
그래프 구축실행 전 한 번(컴파일)forward 실행하며 매번
대표TF1, Theano, ONNXPyTorch, autograd, micrograd
장점전역 최적화·배포 용이제어흐름(if/loop) 자유, 디버깅 쉬움
단점동적 구조 표현 어려움매 실행 재구축 오버헤드

우리(그리고 PyTorch eager)는 define-by-run. c = a * b를 실행하는 순간, 새 노드 c가 만들어지며 자기 부모 {a, b}와 자기 VJP를 기록한다. 그래프는 forward의 부산물로 자라난다. 파이썬 if·for로 매번 다른 그래프를 만들 수 있는 유연함이 여기서 나온다(가변 길이 시퀀스 등).

맥락 torch.compile(2.x)와 JAX는 define-by-run의 유연함과 define-and-run의 최적화를 tracing/JIT로 절충한다 — 동적으로 추적해 정적 그래프를 뽑아 융합·최적화한다. Phase 3에서 torch.compile 속도 이득을 측정할 때 이 긴장을 다시 만난다.

2. Value의 책임 분리

Value 노드가 들고 다니는 것 — Day 3의 tape 항목을 객체로 흡수한 것이다:

필드역할Day 3 대응
dataforward 값v.value
gradadjoint $\bar v$, 초기 0v.grad
_prev부모 노드 집합(그래프 엣지)tape의 inputs
_backward이 노드의 VJP를 부모 grad에 누적하는 클로저op.vjp
_op연산 라벨(디버그/시각화)tape의 op

3. 클로저로 VJP 캡슐화 — 왜 이 설계가 우아한가

핵심 설계 결정: 각 연산의 VJP를 그 연산이 노드를 만드는 시점에 클로저로 박아넣는다. 클로저는 그 시점의 self, other, out을 자유변수로 포획하므로, 나중에 out._backward()만 호출하면 올바른 VJP가 올바른 부모로 흘러간다. 곱셈을 예로:

def __mul__(self, other):
    other = other if isinstance(other, Value) else Value(other)
    out = Value(self.data * other.data, (self, other), '*')

    def _backward():
        # VJP of multiply:  ū_self += out.grad * other.data
        #                   ū_other += out.grad * self.data
        # (Day 2 표의 'x⊙z' VJP, fan-out 대비 += 누적)
        ...                       # ← 네가 채운다
    out._backward = _backward
    return out

이 구조의 우아함: backward()(내일)는 그래프 구조를 전혀 몰라도 된다. 그냥 위상 역순으로 node._backward()를 호출하면, 각 노드가 자기 VJP를 알아서 적용한다. 관심사 분리: 노드는 "어떻게 미분하나"(local VJP), 오케스트레이터는 "어떤 순서로"(위상).

4. 연산집합 — 처음부터 넉넉히

최고수준 답게 장난감 수준(+, *)을 넘어, 신경망에 필요한 연산을 처음부터 갖춘다. 각 연산의 forward와 VJP를 Day 2 표에서 가져와 구현하라:

연산forwardVJP (out.grad = ḡ)
__add__$a+b$$\bar a{+}{=}\bar g,\ \bar b{+}{=}\bar g$
__mul__$ab$$\bar a{+}{=}\bar g\,b,\ \bar b{+}{=}\bar g\,a$
__pow__ (상수 n)$a^n$$\bar a{+}{=}\bar g\,n a^{n-1}$
relu$\max(0,a)$$\bar a{+}{=}\bar g\,[a>0]$
tanh$\tanh a$$\bar a{+}{=}\bar g\,(1-\tanh^2 a)$
exp$e^a$$\bar a{+}{=}\bar g\,e^a$
log$\ln a$$\bar a{+}{=}\bar g\,/a$

__neg__, __sub__, __truediv__는 위들의 조합으로 정의하면 된다 ($a-b = a+(-b)$, $a/b = a\cdot b^{-1}$). 이렇게 하면 VJP를 새로 짤 필요 없이 그래프가 자동으로 합성된다 — 원시연산을 최소화하고 나머지는 합성하는 것이 라이브러리 설계의 정석이다.

5. 스칼라 혼합과 reflected 연산자

a * 2a.__mul__(2)로 잡히지만, 2 * aint.__mul__(a)가 먼저 시도돼 실패한다. 파이썬은 이때 reflected a.__rmul__(2)를 부른다. __radd__ = __add__, __rmul__ = __mul__로 위임하면 좌우 어느 쪽이든 동작한다. 이런 디테일이 "라이브러리스럽게" 만든다.

6. 수치 안정성 — 처음부터 의식

중요 exp는 큰 입력에서 overflow하고, log는 0 근처에서 $-\infty$. 지금은 스칼라라 큰 문제 없지만, Phase 1의 softmax/cross-entropy에서 이게 학습을 터뜨린다. 미리 습관을 들여라: softmax는 max를 빼서 계산($\mathrm{softmax}(x)=\mathrm{softmax}(x-\max x)$), cross-entropy는 log-sum-exp trick으로 $\log\sum e^{x_i} = m + \log\sum e^{x_i-m}$. Day 5 도전 과제에서 다룬다.

✍️ 직접 해보기 (강도 ↑)

과제 1 — Value 핵심 구현 (forward + 그래프). data, grad, _prev, _op, _backward 필드와 위 표의 7개 원시연산 + 4개 합성연산 (neg, sub, truediv, rsub 등)의 forward·그래프 연결을 구현하라. __radd__/__rmul__로 스칼라 혼합 지원. _backward 본문은 클로저 틀만 두고 내일 채워도 되지만, VJP 식을 주석으로 미리 적어둬라.
과제 2 — 그래프 자동구축 검증. a=Value(2); b=Value(-3); c=Value(10)에서 L = (a*b + c.exp()).relu() / a 같은 식을 만들고, _prev를 재귀 순회해 노드 수·엣지 수·fan-out 노드를 출력하는 함수를 짜라. a가 여러 곳에 나타나는지(fan-out) 확인.
과제 3 — graphviz 시각화. _prev를 따라 DAG를 graphviz(또는 networkx)로 그려라. 각 노드에 data, _op를 라벨로. Day 3에서 손으로 그린 그래프와 같은 위상인지 대조. (Karpathy 영상의 그 그림을 네 손으로 재현)
과제 4 — 설계 비평(글). 우리 설계는 tape-free, 노드가 클로저를 들고 있는 방식이다. PyTorch는 별도 grad_fn 객체 그래프를, JAX는 함수 변환(grad)을 쓴다. 세 접근의 메모리·유연성·성능 트레이드오프를 한 단락으로 비교하라.
도전 현재 Value는 스칼라다. 왜 스칼라 AD가 실전에서 못 쓰이나를 연산 그래프 크기·파이썬 오버헤드·벡터화 부재 관점에서 정량적으로 논증하고, "텐서로 올려야 하는 이유"를 적어라(Week 2 동기).

✅ 스스로 확인

📝 오늘의 기록

docs/00_day04.md:
다음 (Day 5): _backward 클로저들을 채우고, 명시적 위상정렬 + adjoint 전파로 backward()를 완성한다. torch.autograd.gradcheck 스타일의 검증기를 만들어 전 연산의 VJP를 복소스텝 미분과 대조한다. 그리고 softmax+cross-entropy를 수치 안정적으로 추가해 Phase 1의 손실함수를 미리 갖춘다.

← Day 3 Day 4 끝 Day 5 — backward() & gradcheck →