MNIST Lab 기본 구현 시리즈 0편
이 시리즈는 결과 요약글이 아니라, MNIST Lab의 투두를 하나씩 직접 구현하기 위한 글이다. 먼저 전체 구현 지도를 잡고, 각 테스트가 무엇을 묻는지 확인한 뒤, 다음 글부터 ReLU, Softmax, Affine 순서로 실제 코드를 채운다.
핵심부터 말하면
이 과제는 "MNIST 정확도 몇 퍼센트"만 찍는 과제가 아니다. 딥러닝 프레임워크 없이 NumPy만으로 계층, 손실 함수, 옵티마이저, 네트워크 조립, 학습 루프를 직접 만드는 과제다. 그래서 앞으로의 글은 테스트가 요구하는 작은 동작 조건을 코드로 채우는 방식으로 간다.
1. 전체 구조는 부품 조립이다
완성된 모델을 한 번에 이해하려고 하면 복잡하다. 하지만 투두 단위로 쪼개면 구조가 단순해진다. 각 계층은 forward로 값을 보내고, backward로 gradient를 되돌려 보낸다. 마지막 train 루프는 이 부품들을 한 줄로 연결한다.
MNIST 이미지는 28x28 픽셀을 펼친 784차원 벡터로 들어온다.
xW+b로 입력을 다음 층 크기에 맞게 바꾼다.
값의 분포를 안정화하고, 비선형성을 넣고, 일부 뉴런 의존을 줄인다.
10개 숫자 점수를 확률처럼 바꾸고, 정답 칸 기준으로 벌점을 계산한다.
gradient를 보고 W, b, gamma, beta를 갱신한다.
2. 투두별 구현 글과 대응 테스트
앞으로의 기본 구현 시리즈는 아래 순서로 읽으면 된다. 각 글은 개념 설명, 실제 코드, 테스트가 확인하는 조건, 헷갈리기 쉬운 지점을 함께 다룬다.
| 글 | 구현 대상 | 대응 테스트 | 핵심 질문 |
|---|---|---|---|
| 1편 | ReLU | test_relu.py | 왜 forward에서 만든 mask가 backward에도 필요한가? |
| 2편 | Softmax | test_softmax.py | 점수를 어떻게 행별 확률 분포로 바꾸는가? |
| 3편 | Affine | test_affine.py | xW+b와 backward shape가 어떻게 맞는가? |
| 4편 | Cross Entropy | test_cross_entropy_loss.py | 왜 정답 칸의 확률만 뽑아 벌점을 계산하는가? |
| 5편 | SGD | test_sgd.py | gradient 반대 방향으로 움직인다는 말은 코드에서 무엇인가? |
| 6편 | Adam | test_adam.py | 방향 기록과 크기 기록을 왜 따로 두는가? |
| 7편 | NeuralNetwork | test_neural_network.py | 계층을 어떤 순서로 조립해야 하는가? |
| 8편 | BatchNorm | test_batchnorm.py | 학습 모드와 평가 모드가 왜 다른가? |
| 9편 | Dropout | test_dropout.py | 학습 때 꺼진 뉴런의 gradient는 왜 막아야 하는가? |
| 10편 | train/evaluate | test_training.py, test_evaluate.py | 모든 부품을 학습 루프로 어떻게 연결하는가? |
| 11편 | 결과 해석 | 전체 21 passed + 실제 학습 | 테스트 통과와 98.41% 정확도는 각각 무엇을 뜻하는가? |
3. 21개 테스트는 각각 무엇을 묻나
"21 passed"라고만 쓰면 알맹이가 없다. 아래 표는 각 테스트가 실제로 어떤 구현 조건을 확인하는지 정리한 것이다.
| 번호 | 테스트 | 확인하는 조건 |
|---|---|---|
| 1 | test_relu_forward_positive | 양수 입력은 그대로 통과한다. |
| 2 | test_relu_forward_negative_zero | 음수와 0은 0으로 막힌다. |
| 3 | test_relu_backward | 막힌 위치는 backward에서도 gradient가 0이다. |
| 4 | test_softmax_forward_sum_one | 각 행의 softmax 합은 1이다. |
| 5 | test_softmax_forward_non_negative | 출력은 0 이상 1 이하 범위에 있다. |
| 6 | test_softmax_backward_shape | backward 출력 shape가 입력 gradient와 같다. |
| 7 | test_affine_forward_shape | x @ W + b 값과 출력 shape가 맞다. |
| 8 | test_affine_backward_grad_shape | dx, dW, db shape가 각각 x, W, b와 맞다. |
| 9 | test_cross_entropy_loss_scalar | loss는 스칼라이고 0보다 크다. |
| 10 | test_cross_entropy_loss_perfect | 정답 확률이 거의 1이면 loss가 거의 0이다. |
| 11 | test_sgd_update_changes_params | SGD가 파라미터를 gradient 반대 방향으로 바꾼다. |
| 12 | test_adam_update_changes_params | Adam update 후 파라미터가 실제로 바뀐다. |
| 13 | test_neural_network_forward_shape | 모델 출력은 batch x 10이다. |
| 14 | test_neural_network_params_exist | optimizer가 수정할 params가 존재한다. |
| 15 | test_neural_network_backward_produces_grads | params와 같은 이름의 grads가 만들어진다. |
| 16 | test_batchnorm_forward_shape | BatchNorm forward는 입력 shape를 유지한다. |
| 17 | test_batchnorm_backward_shape | BatchNorm backward도 입력 gradient shape를 유지한다. |
| 18 | test_dropout_forward_train_shape | 학습 모드 Dropout 출력 shape가 입력과 같다. |
| 19 | test_dropout_forward_inference_scale | 평가 모드에서는 살아남는 평균 비율만큼 scale한다. |
| 20 | test_train_returns_loss_history | train()이 epoch별 loss_history를 반환한다. |
| 21 | test_evaluate_returns_acc_and_params | evaluate()가 정확도와 총 파라미터 수를 반환한다. |
4. 검증 상태
현재 구현은 테스트 전체를 통과했다. 이 말은 단순히 숫자가 예쁘다는 뜻이 아니라, 각 부품의 최소 동작 조건이 모두 충족되었다는 뜻이다.
python -m pytest tests -q
21 passed
테스트 21개는 구현 단위 검증이다. 실제 MNIST 정확도는 따로 학습을 돌려 확인해야 한다. 그래서 이 시리즈 마지막에서는 15 epoch 학습 결과인 98.41%를 별도로 해석한다.
이번 글에서 기억할 것
- 이 시리즈는 결과 요약이 아니라 투두별 직접 구현 시리즈다.
- 21개 테스트는 각 부품의 동작 조건을 나눠 확인한다.
- 테스트 통과와 실제 정확도는 서로 다른 검증이다.
스스로 점검
- ReLU 테스트 3개는 각각 무엇을 확인하는가?
- NeuralNetwork 테스트는 왜 params와 grads의 이름 대응을 보는가?
- 21 passed만으로 실제 정확도를 보장할 수 없는 이유는 무엇인가?
다음 글 예고
다음 글에서는 첫 번째 구현 대상인 ReLU를 본다. ReLU는 단순히 음수를 0으로 만드는 함수처럼 보이지만, backward에서 gradient를 어디까지 흘릴지 결정하는 mask를 함께 다뤄야 한다.