카테고리 없음

MNIST Lab 1편 - ReLU forward/backward 직접 구현하기

cedis 2026. 5. 28. 02:06

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도 막는 계층이다.

스스로 점검

  1. 왜 mask를 forward에서 저장해야 하는가?
  2. x가 0일 때 이 구현은 gradient를 흘리는가, 막는가?
  3. dout을 제자리 수정하는 방식의 장단점은 무엇인가?

다음 글 예고

다음 글에서는 Softmax를 구현한다. ReLU가 은닉층의 문이라면, Softmax는 출력층 점수를 확률처럼 바꾸는 마지막 문이다.

한 줄 정리

ReLU는 forward에서 0 이하 값을 막고, backward에서 같은 위치의 gradient도 막는 계층이다.