역전파는 장부다
앞 편에서는 기울기를 손으로 적었다. 파라미터가 둘이고 식이 한 줄이라 가능했다. 층이 쌓이면 그럴 수 없고, 그래서 역전파가 필요하다.
역전파를 “어렵다”고 느끼는 대부분의 이유는 새로운 미분법처럼 소개되기 때문이다. 아니다. 미분법은 고등학교에서 배운 연쇄법칙 하나뿐이고, 역전파는 중간에 계산한 값을 버리지 않고 장부에 적어두는 요령이다.
뉴런 하나
가장 작은 실물로 시작한다. 입력 하나, 가중치 하나, 편향 하나, 시그모이드, 그리고 제곱오차.
import numpy as np
w, b, x, t = 0.5, -0.2, 2.0, 1.0
z = w * x + b # 0.8
a = 1 / (1 + np.exp(-z)) # 0.689974
L = (a - t) ** 2 # 0.096116
z=0.800000 a=0.689974 L=0.096116
이제 dL/dw 를 알고 싶다. 연쇄법칙은 이렇게 읽는다. w 가 흔들리면 z 가
흔들리고, z 가 흔들리면 a 가 흔들리고, a 가 흔들리면 L 이 흔들린다.
그러니 흔들림의 비율을 곱해 나가면 된다.
세 조각을 따로 구한다.
dL_da = 2 * (a - t) # -0.620051
da_dz = a * (1 - a) # 0.213910
dz_dw = x # 2.0
a * (1 - a) 는 시그모이드 미분이다. 여기서 첫 번째 요령이 나온다. a 를
이미 순전파에서 계산해 두었으므로 시그모이드를 다시 계산할 필요가 없다.
장부에 적어둔 값을 그대로 쓴다.
곱하면 끝이다.
dL_dz = dL_da * da_dz # -0.132635
print(dL_dz * dz_dw) # dL/dw
print(dL_dz * 1.0) # dL/db
dL/dw=-0.265270 dL/db=-0.132635
믿지 말고 대조한다
손으로 유도한 식은 틀리기 쉽다. 그래서 수치미분과 맞춰 본다. 정의 그대로, 아주 조금 흔들어 보고 손실이 얼마나 변하는지 잰다.
def L_of(w_, b_):
return (1 / (1 + np.exp(-(w_ * x + b_))) - t) ** 2
h = 1e-6
num_w = (L_of(w + h, b) - L_of(w - h, b)) / (2 * h)
num_b = (L_of(w, b + h) - L_of(w, b - h)) / (2 * h)
해석적 수치적 일치
dL/dw -0.26526985862215685 -0.2652698586486091 소수점 10자리
dL/db -0.13263492931107843 -0.1326349292687934 소수점 9자리
dL/dw 는 열 자리, dL/db 는 아홉 자리까지 맞는다. 같은 방법으로 잰 두 값이
자릿수가 다른 게 이미 힌트다 - 수치미분의 정확도는 고정된 값이 아니라 재는
대상과 h 에 달려 있다.
이 대조를 그래디언트 체크 라고 부르고, 직접 층을 구현할 때 가장 먼저
붙여야 하는 안전장치다. 수치미분은 파라미터 하나당 순전파를 두 번 해야 해서
학습에는 못 쓴다. 검산용으로도 아래에서 볼 h 문제를 안고 있으니, “상대오차
1e-8 이하면 통과” 처럼 기준을 정해두고 쓰는 게 낫다.
한쪽만 흔드는 (L(w+h) - L(w)) / h 대신 양쪽을 흔든 이유가 있다. 중앙차분의
절단오차는 h² 에 비례해서, h 를 10배 줄이면 오차가 100배 준다. 아래 왼쪽
열이 그것이다.
h=1e-2 오차 8.9e-06 h=1e-6 오차 2.6e-11
h=1e-3 오차 8.9e-08 h=1e-8 오차 2.3e-09
h=1e-4 오차 8.9e-10 h=1e-10 오차 3.1e-08
h=1e-5 오차 6.3e-12 h=1e-12 오차 2.9e-06
그런데 1e-5 를 지나면 방향이 뒤집힌다. L(w+h) 와 L(w-h) 가 거의 같은
값이라 빼면 유효숫자가 날아가고, 그걸 아주 작은 2h 로 나누니 오차가 증폭된다.
1e-12 에서는 1e-2 만도 못하다. 잘 드는 h 는 더 작은 쪽이 아니라 가운데
어딘가이고, 배정밀도에서는 대개 1e-5 부근이다.
갈라지는 길에서 미분은 더해진다
노드가 두 갈래로 쓰이면 어떻게 되나. 역전파에서 헷갈리는 지점은 사실상 여기 하나다.
a, b = 3.0, 4.0
c = a * b # 12
d = a + c # 15 -> a 가 두 번 쓰였다
a 는 d 로 직접 가는 길과 c 를 거쳐 가는 길, 두 개를 갖는다. 규칙은
간단하다. 모든 경로의 기여를 더한다.
dd/da = 1 (직접) + 1 x b (c 를 거쳐) = 1 + 4 = 5
수치미분: 5.000000
역전파 한 번 안에서 갈라진 노드의 기여를 += 로 모으는 이유가 이것이다.
zero_grad() 는 여기서 한 겹 더 떨어진 이야기다. 그건 역전파 한 번 안이
아니라 여러 번 사이의 누적이고, 프레임워크가 그렇게 만든 건 규칙 때문이
아니라 그게 쓸모 있어서다. 배치를 쪼개 여러 번 역전파한 뒤 한 번에 갱신하는
방식이 그것으로 굴러간다. 대신 매 스텝 비워 주지 않으면 지난 스텝의 기울기가
남는다.
왜 거꾸로 가는가
미분을 앞에서부터 곱해 나가도 답은 같다. 실제로 그렇게 하는 방식도 있고 전방 모드라고 부른다. 그런데 딥러닝에서는 아무도 안 쓴다. 모양 때문이다.
파라미터가 N 개, 손실이 1 개다. 앞에서부터 가면 파라미터마다 한 번씩, N 번을 훑어야 한다. 뒤에서부터 가면 손실 하나에서 출발해 한 번에 모든 파라미터의 기울기를 얻는다. 파라미터가 1억 개면 1억 배 차이다.
역전파가 특별한 게 아니라, 입구가 넓고 출구가 하나인 구조에서 뒤에서부터 세는 쪽이 압도적으로 싸다는 사실이 특별하다.
그래서
- 역전파의 수학은 연쇄법칙 하나다. 나머지는 순전파에서 나온 값을 버리지 않는 것
- 갈라진 노드에서는 경로별 기여를 더한다.
zero_grad()는 그와 별개로, 역전파 사이의 누적을 지우는 일 - 손으로 유도했으면 수치미분과 대조한다.
h=1e-5부근에서 열 자리쯤 맞으면 믿어도 된다 - 뒤에서부터 계산하는 이유는 미분이 아니라 개수 때문이다
여기까지가 텐서, 걸음, 기울기다. 세 편 모두 눈에 안 보이던 것을 한 번씩 그려서 확인했고, 다음에 무엇을 그릴지는 아직 정하지 않았다.
Comments