8.4 자동미분과 역전파

실습 · 연습문제 2개

상대오차로 gradient 검증하기 gradient-check

해석적으로 구한(또는 직접 만든 자동미분이 계산한) gradient가 맞는지 중앙차분으로 확인하는 함수 grad_check(f, grad, x, eps=1e-6)를 완성한다.

인자 설명
f 1차원 배열 (n,)을 받아 스칼라를 돌려주는 함수
grad 검사할 gradient, shape (n,)
x 검사할 점, shape (n,)
eps 중앙차분 간격

반환값은 성분별 상대오차의 최댓값(스칼라)이다. 성분 ii의 상대오차는

ri=∣gi−g^i∣max⁡(∣gi∣+∣g^i∣, 10−12)r_i = \frac{\lvert g_i - \hat{g}_i \rvert}{\max\big(\lvert g_i \rvert + \lvert \hat{g}_i \rvert,\ 10^{-12}\big)}

이고, g^i=(f(x+εei)−f(x−εei))/(2ε)\hat{g}_i = \big(f(\mathbf{x} + \varepsilon\mathbf{e}_i) - f(\mathbf{x} - \varepsilon\mathbf{e}_i)\big)/(2\varepsilon)는 중앙차분이다. 분모의 10−1210^{-12}는 두 값이 모두 0인 성분에서 0으로 나누지 않기 위한 것이다.

요구 사항:

  • x가 float32 배열이나 정수 배열로 주어져도 float64로 바꿔 계산한다. float32로 차분하면 반올림 오차 때문에 올바른 gradient도 큰 상대오차를 보인다(본문 참고).
  • 입력 x를 제자리에서 바꾸지 않는다.

테스트는 올바른 gradient에서 상대오차가 10−710^{-7} 미만인지, 상수배가 틀린 gradient와 한 성분의 부호가 틀린 gradient에서 10−210^{-2}보다 큰지, gradient가 0인 성분에서 nan이 나오지 않는지, float32 입력을 float64로 처리하는지 확인한다.

힌트: x = np.array(x, dtype=np.float64)는 복사본을 만든다.

스칼라 자동미분 엔진 만들기 value-engine

본문의 엔진을 직접 다시 만든다. Value는 스칼라 값 data, 그 값에 대한 출력의 gradient grad, 이 노드를 만든 입력 노드 parents, 각 입력에 대한 국소 미분 local_grads를 가진다. 생성자는 주어져 있다.

다음 네 메서드를 완성한다.

메서드 순전파 값 기록할 국소 미분
a + b (__add__) a+ba + b (1,1)(1, 1)
a * b (__mul__) abab (b,a)(b, a)
a.tanh() t=tanh⁡at = \tanh a (1−t2,)(1 - t^2,)
out.backward() — 아래 설명

backward()는 out.grad = 1로 두고, 그래프의 노드를 위상 순서의 역순(출력에서 입력 쪽)으로 방문하며 각 노드 v의 입력 p에 p.grad += local * v.grad를 더한다.

b가 Value가 아닌 숫자(float, int)여도 a + 2.0, a * 3이 동작해야 한다. 숫자는 Value로 감싼다.

테스트가 확인하는 것:

  • 뉴런 하나 tanh⁡(w1x1+w2x2+b)\tanh(w_1x_1 + w_2x_2 + b)의 gradient가 손 유도와 같은가.
  • 같은 노드를 여러 번 쓰는 식 z=u⋅u+uz = u \cdot u + u에서 dz/du=2u+1dz/du = 2u + 1인가. gradient를 덮어쓰지 말고 더해야 한다.
  • 마름모 모양 그래프 a=xy, c=a⋅(a+x)a = xy,\ c = a \cdot (a + x)에서 위상 순서가 맞는가. 어떤 노드의 gradient를 그 노드를 쓰는 모든 노드가 처리되기 전에 전달하면 틀린다.

힌트: 위상 정렬은 깊이 우선 탐색으로 만든다. 노드의 모든 입력을 먼저 방문한 뒤 자신을 리스트에 넣으면, 리스트의 역순이 출력에서 입력으로 가는 순서다.