MNIST Lab 기본 구현 2편
Softmax는 모델이 만든 10개 점수를 확률처럼 바꾸는 출력층 함수다. 핵심은 각 행의 합이 1이 되게 만들고, 큰 값에서 exp가 터지지 않도록 안정화하는 것이다.
1. Softmax가 필요한 이유
모델의 마지막 Affine 출력은 아직 점수(logit)일 뿐이다. 숫자 0~9 중 무엇일 가능성이 높은지 비교하려면 이 점수를 확률처럼 바꿔야 한다.
1
logit
예: [2.0, 1.0, 0.1]처럼 정규화되지 않은 점수다.
2
exp
점수를 양수로 바꾸고 큰 점수의 영향이 더 커지게 한다.
3
normalize
각 행을 자기 행의 exp 합으로 나눠 합이 1이 되게 한다.
2. 최종 구현 코드
class Softmax:
def forward(self, x):
shifted = x - np.max(x, axis=1, keepdims=True)
exp_x = np.exp(shifted)
out = exp_x / np.sum(exp_x, axis=1, keepdims=True)
return out
def backward(self, dout):
return dout
왜 최댓값을 빼는가
np.exp()는 입력이 크면 매우 쉽게 overflow가 난다. 행별 최댓값을 빼도 softmax 결과의 비율은 변하지 않으므로, 계산만 안정적으로 만든다.
3. shape 기준으로 읽기
| 코드 | shape | 의미 |
|---|---|---|
x |
(batch, classes) | 각 데이터의 클래스별 점수 |
np.max(x, axis=1, keepdims=True) |
(batch, 1) | 각 행의 최댓값 |
exp_x |
(batch, classes) | 안정화된 exp 값 |
out |
(batch, classes) | 각 행 합이 1인 확률 분포 |
4. 테스트가 묻는 것
| 테스트 | 확인 조건 |
|---|---|
| test_softmax_forward_sum_one | 각 행의 출력 합이 1인가 |
| test_softmax_forward_non_negative | 출력이 0 이상 1 이하인가 |
| test_softmax_backward_shape | backward가 gradient shape를 유지하는가 |
왜 backward가 그대로 반환되는가
이 과제에서는 Softmax와 Cross Entropy를 합친 미분 시작값을 train()에서 직접 만든다. 그래서 Softmax.backward()는 받은 gradient를 그대로 넘기는 통로 역할만 한다.
이번 글에서 기억할 것
Softmax는 출력 점수를 행별 확률 분포로 바꾸며, 구현에서는 overflow를 막기 위한 최댓값 보정이 중요하다.
스스로 점검
- Softmax 출력의 각 행 합은 왜 1이어야 하는가?
- 행별 최댓값을 빼도 결과가 변하지 않는 이유는 무엇인가?
- 이 과제에서 Softmax.backward가 짧은 이유는 무엇인가?
다음 글 예고
다음 글에서는 Affine 계층을 구현한다. Affine은 xW+b로 층의 차원을 바꾸는 핵심 계층이다.