4장에서 우리는 신경망이 스스로 배우는 것을 처음으로 목격했다. 손실 함수를 정하고, 그 손실을 파라미터로 미분해 기울기를 구하고, 기울기의 반대 방향으로 조금씩 내려가자 손실이 정말로 줄었다. 그런데 그 기울기를 수치 미분으로 구했다. 를 파라미터 하나하나마다 계산하는 방식이다. 옳다. 구현도 단순하다. 그러나 파라미터가 수만 개인 신경망에서, 기울기 한 번을 얻으려고 순전파를 수만 번 다시 돌려야 한다. 느려도 너무 느리다.
이 장은 그 병목을 정면으로 부순다. **오차역전파법(backpropagation)**은 순전파를 딱 한 번, 역전파를 딱 한 번 흘려보내는 것만으로 모든 파라미터의 기울기를 동시에 얻는다. 수치 미분이 파라미터 수에 비례해 순전파를 반복하는 것과 달리, 역전파는 그 반복을 “미분의 연쇄”라는 하나의 역방향 흐름으로 접어 넣는다. 이 장이 이 책의 심장인 이유가 여기 있다 — 여기서 만드는 각 계층의 backward가, 앞으로 나올 모든 신경망이 학습하는 실제 엔진이 된다. 그러니 이 장은 결과 공식을 외우는 장이 아니라, 각 계층의 미분이 왜 그 모양인지를 손으로 유도해 두는 장이다. 미분·연쇄 법칙이 낯설면 먼저 수학부록2를, 행렬 곱·전치가 낯설면 수학부록3을 한 번 보고 오면 이 장이 훨씬 순해진다.
계산 그래프 — 국소적 계산의 릴레이
역전파를 이해하는 가장 쉬운 길은 계산을 그래프로 그리는 것이다. 계산 그래프는 계산 과정을 노드(연산)와 엣지(값의 흐름)로 나타낸다. 책의 예제를 빌리면 이렇다. 슈퍼에서 사과를 개당 100원에 2개 사고, 소비세 10%가 붙는다. 최종 지불 금액은?
그래프로 그리면 노드가 둘이다. 첫 번째 곱셈 노드에 사과 값 100과 개수 2가 들어가 200이 나오고, 두 번째 곱셈 노드에 200과 세율 1.1이 들어가 220이 나온다. 왼쪽에서 오른쪽으로 값을 흘려보내는 이 방향이 **순전파(forward)**다.
계산 그래프의 힘은 국소성에 있다. 각 노드는 자기에게 들어온 입력만으로 자기 출력을 계산한다. 두 번째 곱셈 노드는 “200이 어디서 왔는지”를 전혀 몰라도 된다 — 200과 1.1을 곱해 220을 내면 그만이다. 전체 계산이 아무리 복잡해도, 각 노드는 자기 앞의 국소적 계산에만 집중한다. 복잡한 문제를 단순한 국소 계산의 릴레이로 분해하는 것 — 이것이 계산 그래프가 주는 첫 번째 선물이다.
두 번째 선물이 진짜다. 각 노드가 자기 입력에 대한 국소적 미분을 안다면, 그 미분들을 곱해 전달하는 것만으로 전체 미분을 얻을 수 있다. “사과 값이 1원 오르면 최종 지불액은 얼마나 오르는가”라는 질문()의 답을, 각 노드의 국소 미분을 오른쪽에서 왼쪽으로 곱해 오며 구할 수 있다. 이 역방향 흐름이 **역전파(backward)**다. 그리고 이것을 수학적으로 떠받치는 것이 연쇄 법칙이다.
판단 기준: 계산이 여러 단계로 쪼개지면, 전체를 한 번에 미분하려 들지 말고 각 단계의 국소 미분을 따로 구한 뒤 곱하라. 함정: 계산 그래프를 “그림 그리기 놀이”로 여기면 요점을 놓친다 — 그래프의 목적은 그림이 아니라, 미분을 국소 미분들의 곱으로 분해할 수 있음을 보이는 것이다.
연쇄 법칙 — 국소 미분을 곱해 전달한다
합성 함수의 미분 규칙이 연쇄 법칙이다. 이 규칙 자체가 낯설면 수학부록2의 “겉미분 × 속미분”을 먼저 보고 오면 좋다 — 여기서는 그것을 계산 그래프의 역전파로 다시 읽는다. 가 의 함수이고 가 의 함수라면,
가 조금 변하면 가 그만큼() 변하고, 가 변하면 가 또 그만큼() 변한다. 두 변화율을 곱하면 에서 까지의 변화율이 된다. 구체적으로 , 라 하자.
이것을 계산 그래프의 역전파로 읽으면 그림이 딱 맞아떨어진다. 그래프의 오른쪽 끝(출력 )에서 미분값 1이 출발한다(). 제곱 노드를 거꾸로 지날 때 국소 미분 를 곱해 가 되고, 덧셈 노드를 거꾸로 지날 때 국소 미분 을 곱해 가 그대로 쪽으로 흘러간다. 역전파란 결국, 오른쪽에서 흘러온 미분값에 그 노드의 국소 미분을 곱해 왼쪽으로 넘기는 일이다. 이 한 문장이 이 장 전체를 관통한다.
여기서 신경망 학습의 전체 구도가 드러난다. 신경망의 손실 은 층을 겹겹이 쌓은 거대한 합성 함수다. 우리가 원하는 것은 각 파라미터에 대한 의 미분()이다. 연쇄 법칙은 그 거대한 미분을, 각 계층의 국소 미분을 뒤에서부터 곱해 오는 일로 분해한다. 그러니 우리가 할 일은 명확하다 — 각 계층마다, 위에서 흘러온 미분값 를 받아 자기 국소 미분을 곱해 아래로 내려보내는 backward를 구현하는 것. 이제 노드별로 그 국소 미분을 하나씩 유도한다.
판단 기준: 어떤 계층의 역전파를 구현할 때는 늘 “위에서 내려온 미분(보통 dout)에 무엇을 곱해 아래로 넘길 것인가”만 물어라. 그 곱할 것이 바로 그 노드의 국소 미분이다. 함정: 역전파를 전체 손실에서 파라미터까지 한 번에 미분하려는 시도로 오해하는 것. 실제로는 각 노드가 자기 국소 미분만 알면 되고, 전체 연결은 연쇄 법칙이 자동으로 처리한다.
곱셈 노드와 덧셈 노드 — 가장 작은 두 부품
모든 계층은 결국 곱셈과 덧셈으로 환원된다. 이 두 노드의 역전파를 먼저 손에 넣자.
덧셈 노드 . 국소 미분은 , 이다. 그러니 위에서 내려온 미분 에 1을 곱해 두 입력 쪽으로 그대로 흘려보낸다. 덧셈 노드는 미분을 바꾸지 않고 나눠 준다.
곱셈 노드 . 국소 미분은 , 이다. 그러니 쪽으로는 가, 쪽으로는 가 흘러간다. 재미있는 것은 서로의 입력값이 뒤바뀌어 곱해진다는 점이다 — 의 미분에는 가, 의 미분에는 가 곱해진다. 그래서 곱셈 노드는 순전파 때 들어온 입력값을 기억해 두었다가 역전파에서 꺼내 쓴다. 각 계층이 forward에서 입력을 인스턴스 변수에 저장하는 이유가 여기서 처음 나온다.
이제 이 둘을 코드로 만든다. 계층은 항상 forward와 backward 두 메서드를 갖는 객체다 — 이 인터페이스가 앞으로 모든 계층에서 반복된다.
class MulLayer: def __init__(self): self.x = None # 순전파 입력을 저장해 둔다(역전파에서 뒤바꿔 쓰려고) self.y = None def forward(self, x, y): self.x = x self.y = y return x * y # z = x * y def backward(self, dout): # dout = 위에서 내려온 dL/dz dx = dout * self.y # dL/dx = dout * y (상대 입력이 곱해진다) dy = dout * self.x # dL/dy = dout * x return dx, dy
마지막 스텝이 역전파의 정신을 한 화면에 담는다. 입력이 5개(사과값·사과수·귤값·귤수·세율)인데, 각각에 대한 기울기를 다섯 번 따로 계산하지 않았다. 순전파 한 번, 역전파 한 번으로 다섯 기울기가 한꺼번에 손에 들어왔다. 수치 미분이었다면 입력마다 순전파를 다시 돌려 다섯 번을 반복했을 것이다. 이 “한 번의 역방향 흐름으로 전부”가, 파라미터 수만 개짜리 신경망에서 수치 미분과 역전파의 속도를 가른다.
판단 기준: 계층을 만들 때 순전파에서 쓴 입력값이 역전파에서 필요한지 먼저 판단하라. 곱셈처럼 상대 입력을 곱해야 하면 저장하고, 덧셈처럼 국소 미분이 상수면 저장하지 않는다. 함정: 모든 계층이 입력을 저장해야 한다고 오해하는 것 — 저장은 국소 미분이 입력에 의존할 때만 필요하다. 불필요한 저장은 메모리 낭비다.
ReLU 계층 — 스위치처럼 열리거나 닫힌다
이제 활성화 함수 계층으로 간다. ReLU는 이다. 미분은 간단하다.
가 양수였으면 국소 미분이 1이니 위에서 내려온 미분을 그대로 통과시키고, 가 0 이하였으면 국소 미분이 0이니 미분을 막아 버린다(0을 흘려보낸다). ReLU는 전기 스위치와 같다 — 순전파 때 켜져 있던(입력이 양수였던) 경로는 역전파에서 미분을 그대로 통과시키고, 꺼져 있던 경로는 미분을 차단한다. 그래서 순전파 때 “어디가 0 이하였는지”를 마스크로 기억해 두었다가 역전파에서 그 자리를 0으로 막는다.
Sigmoid 계층 — 가 나오는 이유를 유도한다
시그모이드 의 역전파는 결과가 놀랍도록 깔끔하다 — . 이 가 어디서 오는지 계산 그래프의 노드를 하나씩 거꾸로 밟아 유도한다. 시그모이드는 네 개의 국소 노드로 쪼개진다: .
역방향으로 각 노드의 국소 미분을 곱해 온다.
1) 나눗셈 노드 (여기서 ). 미분은 . 위에서 온 에 곱하면 .
2) 덧셈 노드 . 국소 미분 1이라 그대로 통과. 여전히 .
3) exp 노드 . 미분은 . 곱하면 .
4) 곱셈() 노드. 미분 을 곱하면 .
여기까지가 그래프를 그대로 따라온 결과다. 이제 를 로 정리한다. 에서 양변을 뒤집으면 이고, 여기서 이므로,
의 하나와 분모 가 약분되어 가 떨어졌다. 이 유도가 주는 실전 이득은 단순하다 — 역전파에 입력 가 필요 없다. 순전파의 출력 만 저장해 두면 로 국소 미분을 다 만든다. 네 개 노드를 일일이 거치지 않고 출력값 하나로 끝난다.
import numpy as npclass Relu: def __init__(self): self.mask = None # x <= 0 인 위치를 True로 기억(역전파에서 0으로 막으려고) def forward(self, x): self.mask = (x <= 0) # mask: x와 같은 shape의 불리언 배열 out = x.copy() out[self.mask] = 0 # 0 이하인 자리를 0으로 → max(0, x) return out # out shape == x shape def backward(self, dout): dout[self.mask] = 0 # 순전파 때 꺼졌던 자리는 미분을 차단(0) dx = dout # 켜졌던 자리는 그대로 통과(국소 미분 1) return dx # dx shape == dout shape
두 계층 모두 shape를 바꾸지 않는다. 활성화 함수는 원소마다 독립적으로 적용되므로, 들어온 배열과 나가는 배열의 shape가 같다. 이것이 곧 볼 Affine 계층과의 결정적 차이다 — Affine은 행렬 곱이 끼어들어 shape가 바뀌고, 그래서 역전파에 전치가 등장한다.
판단 기준: 활성화 계층의 역전파는 입력 배열과 같은 shape를 유지해야 한다. dx.shape == dout.shape가 아니면 어딘가 원소별 연산이 아니라 축을 건드린 것이다. 함정: ReLU 역전파에서 순전파의 마스크가 아니라 dout > 0 같은 엉뚱한 조건으로 막는 실수 — 차단 여부는 역전파 값이 아니라 순전파 때 입력의 부호로 결정된다.
Affine 계층 — 전치가 나오는 이유를 shape로 유도한다
신경망의 한 층은 다. 이 행렬 곱과 편향 덧셈을 기하학에서 어파인 변환이라 부르므로 Affine 계층이라 한다. 행렬 곱과 전치가 낯설면 수학부록3에서 와 을 손계산으로 익히고 오면, 아래 유도가 규칙 하나의 반복으로 읽힌다. 역전파 결과는 이 장에서 가장 중요한 두 식이다.
왜 하필 전치 , 가 나오는가. 성분별로 미분해 유도할 수도 있지만, 더 빠르고 절대 틀리지 않는 길은 shape로 강제하는 것이다. 미분 는 와 shape가 같아야 하고, 는 와 shape가 같아야 한다 — 어떤 변수에 대한 손실의 미분은 그 변수와 같은 모양이다(각 원소마다 미분값이 하나씩). 이 제약만으로 전치가 강제된다.
shape를 깔아 보자. 배치 크기 , 입력 차원 , 출력 차원 이라 하면:
- : , : , : → :
- 위에서 내려온 (코드에서
dout): 와 같은
이제 를 만든다. 이것은 와 같은 여야 한다. 손에 있는 재료는 dout 과 이다. 에서 를 만들려면 을 없애고 를 붙여야 한다. 를 전치한 는 이니,
가운데 이 맞물려 사라지고 가 남는다 — 정확히 의 shape다. 그래서 일 수밖에 없다. 같은 논리로 는 와 같은 이어야 하고, 재료 와 dout 으로 이걸 만들려면 를 전치해 로 두면,
가운데 (배치)이 맞물려 사라지고 이 남는다 — 의 shape다. 전치는 미학이 아니라 shape를 맞추기 위한 필연이다. 어느 쪽을 전치할지, 어느 순서로 곱할지 헷갈릴 때마다 “결과가 원래 변수와 같은 shape가 되게” 맞추면 답이 하나로 정해진다. 수학부록3에서 예고했던 “역전파에서 가 로 뒤집혀 나온다”는 예고편이, 여기서 shape 제약이라는 이유와 함께 본편이 됐다.
편향의 역전파는 조금 다르다. 에서 는 브로드캐스트로 개 샘플 모두에 더해졌다. 순전파에서 하나가 여러 곳으로 복사(브로드캐스트)됐다는 것은, 역전파에서는 그 여러 갈래의 미분을 합쳐야 한다는 뜻이다(덧셈 노드가 여러 개인 셈이다). 그래서 는 dout 을 배치 축(0번 축)으로 합산한 이 된다 — np.sum(dout, axis=0).
import numpy as npclass AffineSingle: def __init__(self, W, b): self.W = W # W: (D, M) self.b = b # b: (M,) self.x = None def forward(self, x): self.x = x # x: (D,) 샘플 하나 return np.dot(x, self.W) + self.b # (D,)·(D,M) → (M,) def backward(self, dout): # dout: (M,) dx = np.dot(dout, self.W.T) # (M,)·(M,D) → (D,) = dout · W^T self.dW = np.outer(self.x, dout) # (D,1)·(1,M) → (D,M) = x^T · dout self.db = dout # (M,) return dx # 단일 샘플이라 편향 합산이 안 보인다 — 배치로 가면 축 합산이 필요해진다
두 스텝을 나란히 두면 배치가 무엇을 바꾸는지 또렷하다. 단일 샘플에서는 편향 미분이 그냥 dout이었는데, 배치에서는 np.sum(dout, axis=0)으로 바뀐다. 순전파에서 편향 하나가 개 샘플에 브로드캐스트됐으니, 역전파에서는 갈래로 흩어진 미분을 배치 축으로 도로 모아야 하기 때문이다. 순전파의 “복사”와 역전파의 “합산”은 언제나 짝을 이룬다 — 이 대응을 기억하면 편향뿐 아니라 앞으로 마주칠 모든 브로드캐스트의 역전파를 스스로 유도할 수 있다.
판단 기준: Affine 역전파에서 전치와 곱셈 순서가 헷갈리면, dW는 W와, dx는 x와 같은 shape가 되도록 맞춰라 — 그 제약이 전치 위치와 곱셈 순서를 하나로 결정한다. 함정: 편향 역전파에서 axis=0 합산을 빠뜨리는 것. db가 (N, M)인 채로 남으면 b의 (M,)와 shape가 안 맞아 이후 갱신에서 브로드캐스트로 조용히 틀린 값이 퍼진다.
Softmax-with-Loss 계층 — 역전파가 로 떨어지는 이유
출력층은 소프트맥스로 확률을 내고, 손실은 교차 엔트로피로 잰다(소프트맥스·교차 엔트로피의 정의가 낯설면 수학부록4를 먼저 보고 오면 좋다). 이 둘을 따로 두지 않고 하나의 Softmax-with-Loss 계층으로 묶는 데는 이유가 있다. 묶으면 역전파가 거짓말처럼 단순해진다 — 소프트맥스 출력 와 정답 레이블 에 대해,
소프트맥스의 입력 (출력 직전의 점수)에 대한 손실의 미분이 그냥 “예측 확률 빼기 정답”이다. 왜 이렇게 깨끗하게 떨어지는지 보자. 소프트맥스와 교차 엔트로피는 각각 이렇다.
를 구하려면 연쇄 법칙으로 를 모든 에 대해 합해야 한다( 하나가 모든 에 영향을 주기 때문이다 — 분모의 에 가 들어 있다). 두 조각을 각각 구한다. 먼저 손실 쪽은 의 미분이라 간단하다:
다음이 까다로운 소프트맥스의 미분이다. (단, )를 로 미분하는데, 일 때와 일 때가 다르다. 분수의 미분 을 쓴다. 이때 임에 주의한다( 안에서 가 든 항은 하나뿐).
일 때 — 분자 도 에 의존한다:
일 때 — 분자 는 와 무관해 미분하면 0, 분모만 미분된다:
정리하면,
이제 합친다.
항을 풀면 가 약분되어 . 항들은 가 약분되어 . 두 덩어리를 더하면,
정답 레이블 는 원-핫이라 이다(정답 한 칸만 1, 나머지 0). 따라서
이 결과의 의미가 아름답다. 역전파로 흘러 나가는 오차가 곧 “예측이 정답에서 얼마나 벗어났는가”의 크기 그 자체다. 예측이 정답과 같으면() 오차가 0이라 그 방향으로는 파라미터를 안 밀고, 크게 틀렸으면 그만큼 큰 오차가 흘러 파라미터를 크게 민다. 학습 신호가 오차에 정확히 비례하는 것 — 이것은 우연이 아니다. 교차 엔트로피는 소프트맥스와 짝을 이루라고 설계된 손실이고(회귀에서 항등 함수와 평균제곱오차가 짝을 이뤄 역전파가 똑같이 로 떨어지듯), 그래서 둘을 하나의 계층으로 묶었을 때 이렇게 깔끔한 형태가 나온다.
import numpy as npdef softmax(a): a = a - np.max(a, axis=-1, keepdims=True) # 최댓값 빼기 → exp 오버플로 방지 exp_a = np.exp(a) return exp_a / np.sum(exp_a, axis=-1, keepdims=True)def cross_entropy_error(y, t): if y.ndim == 1: # 단일 샘플이면 배치 축을 세운다 t = t.reshape(1, t.size) y = y.reshape(1, y.size) batch_size = y.shape[0] # 1e-7을 더해 log(0) = -inf 를 막는다 return -np.sum(t * np.log(y + 1e-7)) / batch_size
backward가 dout=1을 기본값으로 받는 것에 주목하자. 이 계층은 손실 을 출력하는 맨 끝 계층이므로, 역전파의 출발점이다. 연쇄 법칙에서 이니 여기서 1이 흘러 나가기 시작해, 앞서 만든 Affine·ReLU 계층들을 거꾸로 거치며 각 파라미터의 기울기를 만든다. 배치 손실을 으로 평균 냈으므로 미분도 으로 나눈다 — 순전파에서 나눈 상수는 역전파에서도 그대로 나눈다.
판단 기준: 출력층과 손실 함수는 문제 유형에 맞춰 짝지어라 — 분류는 소프트맥스+교차 엔트로피, 회귀는 항등+평균제곱오차. 그래야 역전파가 로 떨어져 학습 신호가 오차에 비례한다. 함정: 소프트맥스와 교차 엔트로피를 따로 구현해 각자 역전파시키면 중간에 같은 항이 남아 수치적으로 불안정하고 코드도 길어진다 — 묶어야 의 안정성과 간결함을 얻는다.
계층을 딕셔너리로 조립한 신경망
이제 부품이 다 모였다. 신경망을 만드는 일은 계층들을 순서대로 쌓고, 순전파는 앞에서부터, 역전파는 뒤에서부터 흘리는 것으로 끝난다. 계층을 순서 있는 딕셔너리(OrderedDict)에 담아 두면, 순전파는 딕셔너리를 정방향으로, 역전파는 그 리스트를 뒤집어 순회하는 것만으로 자동으로 연결된다. 계층 하나를 끼워 넣거나 빼는 일이 딕셔너리에 한 줄 추가/삭제로 끝난다 — 계층이라는 인터페이스(forward/backward)로 통일해 둔 덕이다.
import numpy as npfrom collections import OrderedDictclass TwoLayerNet: def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01): # 파라미터: W1 (input, hidden), W2 (hidden, output) self.params = {} self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size) self.params['b1'] = np.zeros(hidden_size) self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size) self.params['b2'] = np.zeros(output_size) # 계층을 순서대로 — 이 순서가 곧 순전파 흐름이다 self.layers = OrderedDict() self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1']) self.layers['Relu1'] = Relu() self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2']) self.lastLayer = SoftmaxWithLoss() # 손실까지 포함한 맨 끝 계층
gradient 메서드가 이 장의 결실이다. 4장에서 파라미터 하나하나마다 순전파를 다시 돌리던 수치 미분과 비교해 보라. 여기서는 순전파 한 번, 역전파 한 번이면 W1·b1·W2·b2 네 배열의 기울기가 전부 채워진다. 그리고 각 Affine 계층이 backward에서 자기 dW·db를 조용히 저장해 두었기 때문에, 우리는 마지막에 그것을 딕셔너리로 수확하기만 하면 된다. 계층이 100개로 늘어도 이 구조는 그대로다 — OrderedDict에 쌓고, 정방향으로 순전파, 역방향으로 역전파.
판단 기준: 계층을 추가·삭제할 때 순전파 순서와 역전파 순서(뒤집힌 순서)가 자동으로 맞물리는지 확인하라. OrderedDict + reverse() 구조를 쓰면 이 정합성이 공짜로 따라온다. 함정: gradient를 호출하기 전에 loss(순전파)를 부르지 않는 것 — 각 계층의 backward는 순전파에서 저장한 입력값에 의존하므로, 순전파 없이 역전파하면 None을 참조하거나 이전 배치의 낡은 값으로 계산한다.
숫자로 따라가기 — 2→2→2 신경망을 손으로 역전파한다
지금까지 유도한 계층들이 정말로 맞물려 도는지, 추상 수식을 걷어내고 구체적인 숫자 하나로 끝까지 굴려 보자. 아주 작은 신경망을 정하고, 순전파의 모든 중간값 → 손실 → 역전파의 계층별 미분을 손으로 계산해, 각 파라미터의 기울기를 직접 뽑아낸다. 종이와 계산기만 있으면 그대로 따라 적을 수 있다. 이 절이 이 장의 하이라이트다 — 여기까지 손으로 한 번 굴려 보면, 앞의 모든 유도가 “실제로 이렇게 흐르는구나”로 몸에 붙는다.
신경망 구성. 입력 2 → 은닉 2 → 출력 2. 은닉층 활성화는 Sigmoid, 출력은 Softmax-with-Loss. 배치는 샘플 하나()로 둔다(shape는 지만 계산은 벡터로 따라간다). 파라미터와 입력·정답을 다음 숫자로 못 박는다.
1) 순전파 — 모든 중간값을 숫자로
Affine1 . 행렬 곱은 “왼쪽 행 × 오른쪽 열”이다(수학부록3). 의 열을 세로로 읽어(, ) 각각 와 짝지어 곱해 더하고 편향을 더한다.
Sigmoid . 원소마다 독립으로 적용한다.
Affine2 . 의 열은 , 다.
Softmax . , , 합은 .
정답이 0번인데 예측은 0번에 0.40밖에 안 줬다 — 아직 덜 배운 신경망이다.
손실 . 이라 정답 클래스 항만 남는다.
순전파를 표로 모으면 이렇다. 이 값들은 전부 역전파에서 다시 꺼내 쓰이므로 손에 쥐고 있어야 한다.
| 단계 | 값 | shape |
|---|---|---|
| 스칼라 |
2) 역전파 — 맨 끝에서 미분을 거꾸로 흘린다
Softmax-with-Loss backward. 앞에서 유도한 대로 이고 이다.
이 값을 dout으로 삼아 Affine2로 내려보낸다. 오차가 음수()인 0번은 “확률을 더 키워라”, 양수()인 1번은 “확률을 줄여라”는 신호다.
Affine2 backward. 계층에 저장된 것은 (입력), 다. 세 미분을 순서대로 만든다.
먼저 . 는 , dout은 이니 결과는 — 와 같은 shape다. 각 칸은 다.
편향은 배치 축 합산인데 이라 그대로다.
아래로 내려보낼 . 이다.
Sigmoid backward. 유도한 대로 국소 미분은 이다. 순전파 출력 만 있으면 된다.
(은 원소별 곱이다.)
Affine1 backward. 저장된 입력은 다. 이고 여기서 이다.
여기까지가 이 신경망의 모든 기울기다. 순전파 한 번, 역전파 한 번으로 , , , — 파라미터 12개의 기울기가 전부 손에 들어왔다. 표로 모으면:
| 파라미터 | 기울기 |
|---|---|
한 가지 눈여겨볼 점 — 출력에 가까운 의 기울기(약 )가 깊은 의 기울기(약 )보다 10배 넘게 크다. 시그모이드의 국소 미분 가 최대 0.25라, 층을 하나 거슬러 올라갈 때마다 미분이 그만큼 작아지기 때문이다. 이 “깊은 층일수록 기울기가 옅어지는” 현상이 6장에서 다룰 기울기 소실의 씨앗이다 — 여기 숫자에서 이미 그 조짐이 보인다.
3) gradient check — 수치 미분과 숫자로 대조
역전파가 낸 이 값들이 정말 맞는지, 4장의 수치 미분으로 직접 검산하자. 중앙차분 를 로 잡아, 파라미터 하나를 살짝 흔들고 손실이 얼마나 변하는지 본다.
먼저 출력 근처 (현재 )를 검산한다. 역전파 값은 이었다. 이 파라미터는 에만 영향을 주고 은 안 건드린다.
- : → 다시 softmax·손실 →
- : →
역전파의 과 소수 넷째 자리까지 맞는다.
이번엔 가장 깊은 (현재 )을 검산한다. 역전파 값은 . 이건 을 바꿔 을 바꾸고, 그게 를 지나 손실까지 파급되므로 순전파 전체를 다시 굴려야 한다.
- :
- :
역전파의 과 다시 맞는다. 완전히 다른 두 경로 — 계층별 국소 미분을 곱해 온 역전파와, 손실을 직접 흔들어 잰 수치 미분 — 이 같은 숫자에 도달했다. 역전파 구현이 옳다는 강한 증거다. (손으로 하는 중앙차분이라 에서 넷째 자리까지 맞췄다. 코드에서 로 돌리면 다음 절처럼 수준까지 붙는다.)
판단 기준: 새 계층이나 역전파를 손봤으면, 이렇게 파라미터 하나를 골라 수치 미분으로 대조해 보라 — 소수 넷째 자리까지 맞으면 그 계층은 신뢰할 수 있다. 함정: 순전파 전체가 파급되는 깊은 파라미터()를 검산할 때, 흔든 파라미터 아래로 이어지는 값들()을 다시 계산하지 않고 얕은 층 값을 재활용하는 것 — 그러면 엉뚱한 수치 미분이 나와 멀쩡한 역전파를 틀렸다고 오판한다.
기울기 확인 — 역전파가 옳음을 수치 미분으로 증명한다
앞 절에서 손으로 두 파라미터를 검산했다면, 실전에서는 이 대조를 코드로 모든 파라미터에 한꺼번에 돌린다. 역전파는 빠르지만, 유도와 구현 어디선가 전치를 뒤집거나 축을 잘못 합산하기 쉽다. 그리고 그런 실수는 대개 에러 없이 그럴싸한 shape로 굴러가 조용히 학습을 망친다. 그래서 반드시 해야 할 마지막 절차가 **기울기 확인(gradient check)**이다. 느리지만 단순해서 틀릴 여지가 적은 수치 미분을 정답지로 삼아, 역전파가 낸 기울기와 대조한다.
# 역전파(gradient)와 수치 미분(numerical_gradient)의 기울기를 대조한다
x_batch, t_batch = x_train[:3], t_train[:3] # 작은 배치면 충분하다
grad_numerical = network.numerical_gradient(x_batch, t_batch) # 느리지만 믿을 만함
grad_backprop = network.gradient(x_batch, t_batch) # 빠른 역전파
for key in grad_numerical.keys():
# 두 기울기 차이의 절댓값 평균 — 각 파라미터 배열별로
diff = np.average(np.abs(grad_backprop[key] - grad_numerical[key]))
print(key, diff)
# W1 1.8e-10, b1 8.9e-13, W2 6.7e-09, b2 1.4e-07 처럼
# 1e-7 이하 극소값이면 역전파가 옳다는 강한 증거다두 방식의 차이가 수준으로 극히 작으면, 완전히 다른 두 경로(느린 수치 미분과 빠른 역전파)가 같은 답에 도달했다는 뜻이므로 역전파 구현을 신뢰할 수 있다. 방금 손으로 한 검산을 규모만 키워 자동화한 것이다. 이것은 4장의 수치 미분을 버리는 게 아니라 검증 도구로 재활용하는 것이다 — 실전 학습은 빠른 역전파로 하되, 구현이 의심스러울 때 느린 수치 미분을 정답지로 꺼내 쓴다. 4장에서 옳지만 느리다던 것이 여기서 “옳음의 기준”이라는 제 역할을 찾는다.
판단 기준: 새 계층을 구현했거나 역전파를 손봤으면, 학습을 돌리기 전에 반드시 gradient check로 수치 미분과 대조하라. 차이가 근처면 통과, 이상이면 어딘가 틀린 것이다. 함정: 차이가 크게 나오는데 “부동소수점 오차겠지” 하고 넘기는 것 — 이상의 불일치는 오차가 아니라 버그(대개 전치 방향이나 축 합산 실수)다. 반대로 를 너무 작게(예: ) 잡으면 수치 미분 쪽이 반올림 오차로 망가져 멀쩡한 역전파를 틀렸다고 오판하니, 정도가 안전하다.
정리 — 이 장이 세운 학습 엔진
- 수치 미분은 옳지만 파라미터마다 순전파를 반복해 느리다. 역전파는 순전파 한 번·역전파 한 번으로 모든 파라미터의 기울기를 동시에 얻는다 — 이 속도 차가 실전 신경망을 가능하게 한다.
- 계산 그래프는 계산을 국소 계산의 릴레이로 분해하고, 역전파는 연쇄 법칙에 따라 “위에서 온 미분 × 국소 미분”을 뒤에서 앞으로 곱해 오는 하나의 흐름이다. 각 계층은
forward/backward인터페이스로 이 흐름의 한 마디가 된다. - 노드별 국소 미분을 유도해 두면 계층이 손에 잡힌다 — 덧셈은 그대로 전달, 곱셈은 상대 입력 교환, ReLU는 순전파 마스크로 차단, Sigmoid는 출력만으로 .
- Affine의 전치(, )는 미학이 아니라 “미분은 원래 변수와 같은 shape”라는 제약이 강제한 필연이다. 편향은 순전파의 브로드캐스트에 대응해 역전파에서 배치 축으로 합산한다.
- Softmax-with-Loss의 역전파가 로 떨어지는 것은 우연이 아니라, 교차 엔트로피가 소프트맥스와 짝을 이루도록 설계됐기 때문이다 — 학습 신호가 예측 오차에 정확히 비례한다.
- 2→2→2 신경망을 숫자로 끝까지 굴려 보니, 순전파의 중간값이 역전파에서 그대로 국소 미분의 재료가 되고, 두 갈래(역전파·수치 미분)가 같은 기울기에 도달했다. 계층을
OrderedDict에 쌓으면 순전파는 정방향, 역전파는 역방향 순회로 자동 연결되고, gradient check로 그 정당성을 반드시 증명한 뒤 학습에 들어간다.
다음 장은 이 엔진 위에 학습을 더 잘 굴리는 기술들을 얹는다 — 파라미터를 어떻게 갱신하고(옵티마이저), 가중치를 어떻게 초기화하고, 과적합을 어떻게 막을 것인가. 이 절 끝에서 본 “깊은 층의 옅은 기울기”가, 다음 장 가중치 초깃값 이야기의 출발점이 된다.
다음장으로 6장