밑바닥부터 시작하는 딥러닝 1 - 계층 구현과 기울기 확인
계산 그래프의 덧셈과 곱셈 규칙을 이해했다면, 이제 신경망의 실제 계층을 같은 관점으로 볼 수 있다.
각 계층은 순전파 때 필요한 값을 저장하고, 역전파 때 상류에서 온 미분값을 자기 규칙에 맞게 변환해 하류로 보낸다.
이번 글에서 잡을 것
- ReLU는 입력이 0 이하였던 위치를 mask로 기억한다.
- Sigmoid는 순전파 출력값을 기억한다.
- Affine은 행렬 곱과 편향 더하기를 담당한다.
- Softmax-with-Loss는 출력층과 손실 계산을 함께 묶는다.
- 기울기 확인은 수치 미분과 역전파 결과를 비교해 구현 오류를 찾는 방법이다.
계층을 보는 기준
계층 구현을 읽을 때는 코드 줄을 모두 외우기보다, 이 계층이 순전파 때 무엇을 저장하고 역전파 때 어떤 규칙으로 미분값을 보내는지 보면 된다.
ReLU와 Sigmoid
| 계층 | 순전파 | 저장하는 값 | 역전파 |
|---|---|---|---|
| ReLU | 0 이하는 0, 양수는 그대로 | 0 이하였던 위치(mask) | 막혔던 위치의 미분을 0으로 차단 |
| Sigmoid | S자 함수로 변환 | 출력값 out | out을 이용해 미분값 계산 |
ReLU의 역전파 기준
순전파 때 0 이하라서 신호가 막힌 위치는 역전파 때도 기울기를 흘리지 않는다.
Affine 계층
Affine 계층은 신경망에서 자주 보던 `XW+B` 계산을 담당한다. 입력 X와 가중치 W의 행렬 곱을 만들고 편향 B를 더한다. 역전파에서는 X, W, B 각각에 대한 기울기를 구해야 한다.
# 개념적으로 보면 Affine 계층의 순전파는 이 한 줄이다.
out = np.dot(x, W) + b
x의 기울기
상류 미분이 입력 쪽으로 얼마나 돌아가는지
W의 기울기
가중치를 어떻게 바꿔야 손실이 줄어드는지
b의 기울기
편향을 어떻게 바꿔야 하는지
기울기 확인은 왜 필요한가
수치 미분은 느리지만 구현이 단순해서 버그가 적다. 오차역전파법은 빠르지만 구현이 복잡하다. 그래서 두 방식으로 구한 기울기가 거의 같은지 비교해 역전파 구현이 맞는지 확인한다.
| 방법 | 장점 | 단점 | 쓰임 |
|---|---|---|---|
| 수치 미분 | 단순하고 검증하기 쉬움 | 느림 | 정답에 가까운 비교 기준 |
| 오차역전파 | 빠름 | 구현 실수 가능 | 실제 학습에 사용 |
ReLU mask를 배열로 보면
ReLU는 순전파 때 0 이하였던 위치를 기억한다. 역전파 때 그 위치로 들어오는 미분은 0으로 막는다.
x = np.array([[1.0, -0.5],
[-2.0, 3.0]])
mask = (x <= 0)
# [[False, True],
# [ True, False]]
dout = np.ones_like(x)
dout[mask] = 0
print(dout)
예상 출력
[[1. 0.]
[0. 1.]]
| 입력 x | mask | 역전파 결과 |
|---|---|---|
| 1.0 | False | 통과 |
| -0.5 | True | 0으로 차단 |
| -2.0 | True | 0으로 차단 |
| 3.0 | False | 통과 |
Affine 계층의 shape 추적
Affine 계층은 행렬 곱이므로 역전파에서도 shape가 맞아야 한다. 입력이 `X(N,D)`, 가중치가 `W(D,H)`, 출력 쪽 미분이 `dout(N,H)`라면 각 기울기의 shape는 다음처럼 정리된다.
| 값 | shape | 의미 |
|---|---|---|
| X | (N, D) | 입력 배치 |
| W | (D, H) | 가중치 |
| dout | (N, H) | 상류에서 온 미분 |
| dW | (D, H) | 가중치 기울기 |
| db | (H,) | 편향 기울기 |
| dx | (N, D) | 입력 쪽으로 되돌릴 미분 |
Affine backward를 코드로 풀어보기
Affine 계층의 역전파는 세 가지를 구한다. 입력 쪽으로 돌려보낼 `dx`, 가중치를 고칠 때 쓸 `dW`, 편향을 고칠 때 쓸 `db`다. 모두 행렬 곱의 shape 규칙에서 나온다.
# 순전파: out = X @ W + b
# X: (N, D)
# W: (D, H)
# dout: (N, H)
dx = np.dot(dout, W.T) # (N,H) @ (H,D) -> (N,D)
dW = np.dot(X.T, dout) # (D,N) @ (N,H) -> (D,H)
db = np.sum(dout, axis=0) # (N,H) -> (H,)
| 구하는 값 | 계산 | 왜 이 shape인가 |
|---|---|---|
| dx | dout · W.T | 다시 입력 X의 shape인 (N,D)로 돌아가야 함 |
| dW | X.T · dout | 가중치 W의 shape인 (D,H)와 같아야 함 |
| db | batch 방향 합 | 편향은 출력 뉴런마다 하나씩 있으므로 (H,) |
읽는 기준
Affine backward는 새로운 공식 암기가 아니라, 순전파의 `XW+B`에서 X, W, b 각각이 손실에 미친 영향을 shape가 맞게 되돌리는 과정이다.
작은 숫자로 한 번 확인하기
숫자를 작게 놓고 보면 `db`가 왜 batch 방향 합인지 바로 보인다. 편향 b는 모든 데이터에 더해지므로, 역전파 때도 각 데이터에서 온 미분을 출력 뉴런별로 더한다.
| dout | 출력 뉴런 0 | 출력 뉴런 1 |
|---|---|---|
| 데이터 1 | 1 | 2 |
| 데이터 2 | 3 | 4 |
| db | 1+3=4 | 2+4=6 |
스스로 점검
- ReLU가 mask를 저장하는 이유를 설명할 수 있는가?
- Affine 계층의 순전파 식을 말할 수 있는가?
- 수치 미분이 느린데도 기울기 확인에 쓰이는 이유를 설명할 수 있는가?
이번 글에서 기억할 것
- ReLU는 입력이 0 이하였던 위치를 mask로 기억한다.
- Sigmoid는 순전파 출력값을 기억한다.
- Affine은 행렬 곱과 편향 더하기를 담당한다.
- Softmax-with-Loss는 출력층과 손실 계산을 함께 묶는다.
- 기울기 확인은 수치 미분과 역전파 결과를 비교해 구현 오류를 찾는 방법이다.
다음 글로 이어지는 질문
다음 글에서는 매개변수를 갱신하는 여러 방법, 특히 SGD와 Momentum, AdaGrad, Adam의 차이를 본다.
한 줄 정리: 계층 구현은 forward에서 값을 계산하고 필요한 것을 저장한 뒤, backward에서 그 저장값으로 미분을 되돌려주는 구조다.