카테고리 없음

MNIST Lab 0편 - TODO와 21개 테스트로 보는 NumPy 신경망 구현 지도

cedis 2026. 5. 28. 02:05

 

 

MNIST Lab 기본 구현 시리즈 0편

이 시리즈는 결과 요약글이 아니라, MNIST Lab의 투두를 하나씩 직접 구현하기 위한 글이다. 먼저 전체 구현 지도를 잡고, 각 테스트가 무엇을 묻는지 확인한 뒤, 다음 글부터 ReLU, Softmax, Affine 순서로 실제 코드를 채운다.

핵심부터 말하면

이 과제는 "MNIST 정확도 몇 퍼센트"만 찍는 과제가 아니다. 딥러닝 프레임워크 없이 NumPy만으로 계층, 손실 함수, 옵티마이저, 네트워크 조립, 학습 루프를 직접 만드는 과제다. 그래서 앞으로의 글은 테스트가 요구하는 작은 동작 조건을 코드로 채우는 방식으로 간다.

1. 전체 구조는 부품 조립이다

완성된 모델을 한 번에 이해하려고 하면 복잡하다. 하지만 투두 단위로 쪼개면 구조가 단순해진다. 각 계층은 forward로 값을 보내고, backward로 gradient를 되돌려 보낸다. 마지막 train 루프는 이 부품들을 한 줄로 연결한다.

입력 데이터

MNIST 이미지는 28x28 픽셀을 펼친 784차원 벡터로 들어온다.

Affine

xW+b로 입력을 다음 층 크기에 맞게 바꾼다.

BatchNorm, ReLU, Dropout

값의 분포를 안정화하고, 비선형성을 넣고, 일부 뉴런 의존을 줄인다.

Softmax + Loss

10개 숫자 점수를 확률처럼 바꾸고, 정답 칸 기준으로 벌점을 계산한다.

Optimizer

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개 테스트는 각 부품의 동작 조건을 나눠 확인한다.
  • 테스트 통과와 실제 정확도는 서로 다른 검증이다.

스스로 점검

  1. ReLU 테스트 3개는 각각 무엇을 확인하는가?
  2. NeuralNetwork 테스트는 왜 params와 grads의 이름 대응을 보는가?
  3. 21 passed만으로 실제 정확도를 보장할 수 없는 이유는 무엇인가?

다음 글 예고

다음 글에서는 첫 번째 구현 대상인 ReLU를 본다. ReLU는 단순히 음수를 0으로 만드는 함수처럼 보이지만, backward에서 gradient를 어디까지 흘릴지 결정하는 mask를 함께 다뤄야 한다.

한 줄 정리

MNIST Lab 기본 구현은 하나의 긴 코드가 아니라, 21개 테스트가 요구하는 작은 동작 조건을 하나씩 채워 학습 루프로 연결하는 작업이다.