MNIST Lab 기본 구현 1편
첫 번째 구현 대상은 ReLU다. ReLU는 음수를 0으로 만드는 함수처럼 보이지만, 실제 구현에서는 forward에서 막힌 위치를 기억해 backward에서도 gradient를 막아야 한다.
1. ReLU가 맡은 역할
Affine 계층은 xW+b 계산으로 음수와 양수가 섞인 값을 만든다. ReLU는 그중 0 이하 값을 막고, 양수만 다음 계층으로 보낸다.
1
입력
[-1, 2, 0, 3]처럼 음수, 0, 양수가 섞여 들어온다.
2
forward
0 이하 값은 0으로 바꾸고, 양수는 그대로 둔다.
3
backward
forward에서 막힌 위치는 gradient도 흐르지 않게 한다.
2. 최종 구현 코드
class ReLU:
def __init__(self):
self.mask = None
def forward(self, x):
self.mask = x <= 0
out = x.copy()
out[self.mask] = 0
return out
def backward(self, dout):
dout[self.mask] = 0
dx = dout
return dx
코드에서 가장 중요한 줄
self.mask = x <= 0이다. 이 mask는 forward에서 꺼진 위치를 기록하고, backward에서 같은 위치의 gradient를 0으로 만드는 데 다시 쓰인다.
3. 작은 값으로 추적하기
| 구분 | 값 | 의미 |
|---|---|---|
| 입력 x | [[-1, 2], [0, -3]] |
0 이하와 양수가 섞여 있다. |
| mask | [[True, False], [True, True]] |
True인 위치는 꺼야 할 위치다. |
| forward 출력 | [[0, 2], [0, 0]] |
양수 2만 살아남는다. |
| backward 출력 | [[0, 1], [0, 0]] |
살아남았던 위치로만 gradient가 흐른다. |
4. 테스트가 묻는 것
| 테스트 | 확인 조건 |
|---|---|
| test_relu_forward_positive | 양수 입력은 값이 그대로 유지되는가 |
| test_relu_forward_negative_zero | 음수와 0은 0으로 바뀌는가 |
| test_relu_backward | forward에서 막힌 위치의 gradient도 0이 되는가 |
비판적 코드 리뷰
현재 backward는 받은 dout을 제자리에서 수정한다. 과제 테스트에서는 문제 없지만, 같은 gradient 배열을 다른 곳에서도 재사용하는 구조라면 dout.copy()로 방어하는 편이 더 안전하다.
이번 글에서 기억할 것
ReLU는 forward에서 0 이하 값을 막고, backward에서 같은 위치의 gradient도 막는 계층이다.
스스로 점검
- 왜 mask를 forward에서 저장해야 하는가?
- x가 0일 때 이 구현은 gradient를 흘리는가, 막는가?
- dout을 제자리 수정하는 방식의 장단점은 무엇인가?
다음 글 예고
다음 글에서는 Softmax를 구현한다. ReLU가 은닉층의 문이라면, Softmax는 출력층 점수를 확률처럼 바꾸는 마지막 문이다.