Blog

Python + NumPy로 신경망 만들기 (0) — 시작하며: 왜, 무엇을, 어떤 순서로

3 minute read

이 시리즈는 PyTorch나 TensorFlow 없이, numpy만으로 MNIST 손글씨 숫자를 인식하는 신경망을 밑바닥부터 만든 기록입니다. 수학이나 인공지능 배경이 없어도 따라올 수 있게, 중학생에게 설명한다는 마음으로 썼습니다.

왜 이걸 했나

딥러닝 라이브러리를 쓰면 신경망은 열 줄이면 만들어집니다. nn.Linear, nn.ReLU, loss.backward(), optimizer.step(). 편리하지만, 그 네 줄 안에서 무슨 일이 일어나는지는 보이지 않습니다.

  • backward()는 정확히 무엇을 계산하나?
  • optimizer.step()은 가중치를 어떻게 고치나?
  • 손실이 2.3026에서 시작하는 이유는?
  • 왜 ReLU 같은 걸 중간에 끼워야 하나?

이 질문들에 “라이브러리가 해 주니까”가 아니라 내 손으로 짠 코드로 답하고 싶었습니다. 그래서 규칙을 하나 정했습니다.

numpy 외의 라이브러리는 쓰지 않는다. 데이터 읽기, 레이어, 손실 함수, 역전파, 학습 루프 전부 직접 만든다.

무엇을 만들었나

최종 결과는 이것입니다.

$ python train.py
epoch 0: loss = 0.5473
epoch 1: loss = 0.2454
epoch 2: loss = 0.1816
test accuracy: 0.9543

학습 사진 6만 장으로 3바퀴 학습한 뒤, 한 번도 본 적 없는 테스트 사진 1만 장 중 95.43%를 맞춥니다. 코드는 전부 합쳐 150줄 정도이고, 실행에 1초가 안 걸립니다.

구조는 이렇습니다.

mnist/
├── data.py      # MNIST 파일 파싱, 전처리, 미니배치
├── layers.py    # Linear, ReLU, Sequential (forward + backward)
├── losses.py    # softmax, cross-entropy
└── optim.py     # SGD
train.py         # 조립해서 학습 + 평가
tests/           # 각 부품의 테스트, 수치 기울기 검사
notebooks/       # 만들면서 실험한 기록
docs/            # 이 시리즈

시리즈 순서

노트북 하나에 글 하나입니다. 만든 순서 그대로이고, 뒤로 갈수록 앞 글을 전제로 합니다.

글내용한 줄로
01데이터 탐색압축된 바이너리 파일을 열어 숫자 그림을 꺼내 보기
02DataLoader사진을 신경망이 먹을 수 있게 손질하고 64장씩 나눠 주기
03모델숫자 784개를 받아 “0~9 중 뭘까” 점수 10개로 바꾸기
04손실 함수점수를 확률로 바꾸고 “얼마나 틀렸나”를 숫자 하나로
05역전파손실에서 거꾸로 올라가며 “가중치를 어떻게 고칠지” 계산하기
06학습 루프지시서대로 가중치를 고쳐서 95% 만들기

글의 형식

글마다 같은 구조입니다.

  1. 한 줄 요약 — 이 단계에서 한 일
  2. 본문 — 실제로 실행한 코드와 실제로 찍힌 출력. 추상적인 설명 대신 “내가 직접 본 숫자”로
  3. 만들면서 겪은 실수 — 틀린 코드, 왜 틀렸는지, 어떻게 알아챘는지
  4. 용어 표 — 그 글에서 처음 나온 말
  5. 스스로 확인해 보기 — 답을 접어 둔 문제 몇 개

“실수” 절을 일부러 넣었습니다. 처음부터 맞는 코드만 보면 “왜 이렇게 짜야 하는지”가 안 보이는데, 틀린 코드와 그걸 잡아낸 과정을 보면 보입니다. 이 시리즈에서 가장 많이 배운 건 거의 전부 그 지점이었습니다.

이 시리즈를 관통하는 습관 세 가지

6편을 쓰고 나서 돌아보니, 매번 같은 세 가지가 반복됐습니다.

1. 정답을 아는 입력으로 확인한다. “행 합이 1”처럼 항상 참인 성질로 검사하면 테스트는 통과하지만 아무것도 확인하지 못합니다. [1000, 0, 0]을 넣으면 [1, 0, 0]이 나와야 한다처럼, 답을 미리 아는 입력을 넣어야 버그가 드러납니다. (04)

2. 공식은 수치로 검증한다. 역전파 공식은 맞는지 눈으로 알 수 없습니다. “입력을 아주 조금 밀어 보고 출력이 얼마나 변하나”를 직접 재서 공식과 비교했습니다. 이 검사가 버그 세 개를 잡았습니다. (05)

3. shape을 따라간다. numpy 코드의 버그 대부분은 shape 불일치입니다. 줄마다 # (64, 784)를 적고, 에러 메시지의 숫자를 읽고, 작은 숫자(3, 2, 4)로 먼저 테스트하는 습관이 가장 많이 시간을 아꼈습니다. (05)

따라 하려면

  • Python 3과 numpy만 있으면 됩니다. Jupyter 노트북(VS Code 확장이면 충분)이 있으면 셀 단위로 실행하기 편합니다.
  • MNIST 데이터는 01의 다운로드 명령으로 받습니다.
  • 각 글에 그 단계의 코드가 전부 들어 있습니다. 따로 내려받을 저장소 없이, 글의 코드를 노트북에 붙여 넣고 한 셀씩 실행하면서 읽는 걸 추천합니다. 특히 “스스로 확인해 보기”는 답을 보기 전에 직접 돌려 보세요.
  • 완성된 모듈(data.py, layers.py, losses.py, optim.py, train.py)은 각 글의 마지막 절에 최종 형태로 정리되어 있습니다.

아직 안 한 것

95%는 “기본 모델이 동작한다”는 증명이지 최선이 아닙니다. 다음 글들에서 다룰 예정입니다.

  • 하이퍼파라미터 실험: 중간 뉴런 수, 학습률, epoch 수를 바꿔 가며 정확도 표 만들기
  • 옵티마이저: SGD → Adam
  • 가중치 초기화: * 0.01 → He 초기화
  • 활성화 함수 비교: ReLU vs Sigmoid
  • 손실 함수 비교: cross-entropy vs MSE

그럼 01. 데이터 탐색부터 시작합니다.