직접 해 보기: 넘파이로 신경망을 처음부터 만들기
NumPy만으로 2층 신경망을 짜 손글씨 숫자를 분류하는 실습(실행 확인, 테스트 정확도 0.9556)
먼저 알아야 할 것
다루는 내용
프레임워크 없이 NumPy로 64-64-10 신경망을 만들어 scikit-learn 내장 손글씨 숫자 데이터를 분류합니다. 순전파 · 소프트맥스 교차 엔트로피 · 역전파 · 수치 기울기 점검 · 미니배치 SGD · 검증 기반 모델 선택을 단계별 코드로 구현했고, 직접 실행한 출력(테스트 정확도 0.9556)과 해석, 바꿔 보기 과제, 흔한 오류를 함께 실었습니다.
해설
직접 해 보기: 넘파이로 신경망을 처음부터 만들기
프레임워크 없이 NumPy만으로 2층 신경망을 짜서 손글씨 숫자 10종을 분류합니다. 순전파 · 소프트맥스 교차 엔트로피 · 역전파 · 수치 기울기 점검 · 미니배치 SGD · 조기 종료식 모델 선택까지, 앞 수업들에서 손으로 계산한 것을 코드 100여 줄로 옮깁니다. 아래 코드는 실제로 실행했고, 출력도 그대로 붙였습니다.
| 항목 | 내용 |
|---|---|
| 참고 자료 | CS231n '신경망 사례 연구' 노트 · cs231n.github.io/neural-networks-case-study, scikit-learn load_digits |
| 트랙 · 수준 | 딥러닝 기초 · 기초 |
| 만드는 것 | 64 → 64(ReLU) → 10 완전연결 신경망, 테스트 정확도 약 95% |
| 실행 환경 | Python 3.14, NumPy 2.5.1, scikit-learn 1.9.0, CPU (실행 시간 수 초) |
| 먼저 알면 좋은 것 | 다층 퍼셉트론, 역전파 손계산, SGD, 디버깅 레시피 |
들어가기 전에
- 역전파를 계산 그래프로 손계산하기 — 4절의 2층 신경망 역전파를 그대로 행렬 코드로 옮깁니다.
- 신경망 학습 디버깅과 실험 관리 레시피 — 초기 손실 확인 · 기울기 점검을 이 프로젝트에서 직접 해 봅니다.
한눈에 보기
- 데이터를 학습 · 검증 · 테스트로 나누고 0~1로 맞춥니다.
- He 초기화한 2층 신경망의 순전파와 소프트맥스 교차 엔트로피를 구현합니다.
- 역전파를 직접 짜고, 수치 기울기로 맞는지 확인합니다(최대 상대 오차 약 ).
- 미니배치 SGD로 30에포크 학습하고, 검증 정확도가 가장 좋았던 가중치로 테스트를 한 번만 잽니다.
- 어떤 숫자끼리 헷갈리는지 혼동 행렬로 봅니다.
준비물
pip install numpy scikit-learn데이터는 scikit-learn에 들어 있는 손글씨 숫자 데이터(load_digits)를 씁니다. 8×8 흑백 이미지 1,797장이고, 픽셀 값은 0~16 정수, 클래스는 0~9 열 개입니다. 내려받기가 필요 없습니다.
flowchart LR
A[load_digits 1,797장] --> B[0~1 정규화 · 학습/검증/테스트 나누기]
B --> C[He 초기화]
C --> D[순전파: XW1+b1 → ReLU → hW2+b2]
D --> E[소프트맥스 교차 엔트로피]
E --> F[역전파: p-y → W2 · ReLU 문 → W1]
F --> G[수치 기울기 점검]
G --> H[미니배치 SGD 30에포크]
H --> I[검증 최고 시점 가중치 선택]
I --> J[테스트 한 번 · 혼동 행렬]아래 코드 조각 1~8을 순서대로 한 파일(numpy_mlp.py)에 붙이면 실제로 실행한 스크립트와 같습니다.
1단계: 데이터 준비
import numpy as np
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
# 1. 데이터 준비
X, y = load_digits(return_X_y=True) # (1797, 64), 값 0~16
X = X / 16.0 # 0~1로 맞추기
X_tr, X_te, y_tr, y_te = train_test_split(
X, y, test_size=0.2, random_state=0, stratify=y)
X_tr, X_va, y_tr, y_va = train_test_split(
X_tr, y_tr, test_size=0.2, random_state=0, stratify=y_tr)
print("학습", X_tr.shape, "검증", X_va.shape, "테스트", X_te.shape)stratify는 나눈 묶음마다 숫자 0~9의 비율을 같게 유지합니다. 테스트 20%를 먼저 떼고, 남은 것에서 다시 20%를 검증으로 뗍니다(1,149 · 288 · 360장).
2단계: 파라미터 초기화
# 2. 파라미터 초기화 (He 초기화)
rng = np.random.default_rng(0)
D, H, C = 64, 64, 10
params = {
"W1": rng.standard_normal((D, H)) * np.sqrt(2.0 / D),
"b1": np.zeros(H),
"W2": rng.standard_normal((H, C)) * np.sqrt(2.0 / H),
"b2": np.zeros(C),
}ReLU를 쓰므로 표준편차 의 He 초기화를 씁니다(초기화 수업 2절). 파라미터 수는 개입니다. 여기서는 행이 예제인 에 오른쪽으로 곱하도록 의 모양을 (입력, 출력)으로 잡았습니다.
3단계: 순전파와 손실
# 3. 순전파와 손실
def forward(p, X):
z1 = X @ p["W1"] + p["b1"]
h = np.maximum(0, z1) # ReLU
o = h @ p["W2"] + p["b2"] # 로짓
return z1, h, o
def softmax(o):
o = o - o.max(axis=1, keepdims=True) # 넘침 방지
e = np.exp(o)
return e / e.sum(axis=1, keepdims=True)
def loss_fn(p, X, y, lam=0.0):
_, _, o = forward(p, X)
P = softmax(o)
n = len(y)
ce = -np.log(P[np.arange(n), y] + 1e-12).mean()
return ce + 0.5 * lam * ((p["W1"] ** 2).sum() + (p["W2"] ** 2).sum())왜 최댓값을 빼나: 소프트맥스는 모든 로짓에 같은 수를 빼도 결과가 같습니다. 가장 큰 로짓을 빼 두면 exp가 넘치지 않습니다. 손실에는 L2 항 을 더했습니다(편향은 제외).
4단계: 역전파
# 4. 역전파
def backward(p, X, y, lam=0.0):
z1, h, o = forward(p, X)
n = len(y)
dO = softmax(o)
dO[np.arange(n), y] -= 1 # p - y(원-핫)
dO /= n
g = {}
g["W2"] = h.T @ dO + lam * p["W2"]
g["b2"] = dO.sum(axis=0)
dH = dO @ p["W2"].T
dZ1 = dH * (z1 > 0) # ReLU 문
g["W1"] = X.T @ dZ1 + lam * p["W1"]
g["b1"] = dZ1.sum(axis=0)
return g역전파 수업의 세 줄 규칙($\partial W = $ 입력ᵀ × , , 입력 )이 층마다 반복됩니다. 소프트맥스 + 교차 엔트로피의 로짓 기울기가 라는 것(다층 퍼셉트론 5절)을 그대로 씁니다. 배치 평균 손실이므로 으로 나눕니다.
5단계: 수치 기울기 점검
# 5. 수치 기울기 점검 (작은 배치, 몇 칸만)
def grad_check(p, X, y, lam=1e-3, eps=1e-5):
g = backward(p, X, y, lam)
worst = 0.0
for name in ["W1", "b1", "W2", "b2"]:
for _ in range(5):
idx = tuple(rng.integers(0, s) for s in p[name].shape)
old = p[name][idx]
p[name][idx] = old + eps; lp = loss_fn(p, X, y, lam)
p[name][idx] = old - eps; lm = loss_fn(p, X, y, lam)
p[name][idx] = old
num = (lp - lm) / (2 * eps)
rel = abs(num - g[name][idx]) / max(1e-12, abs(num) + abs(g[name][idx]))
worst = max(worst, rel)
return worst
print("초기 손실 %.4f (ln 10 = %.4f)" % (loss_fn(params, X_tr, y_tr), np.log(10)))
print("기울기 점검 최대 상대 오차 %.2e" % grad_check(params, X_tr[:20], y_tr[:20]))
def accuracy(p, X, y):
return (forward(p, X)[2].argmax(axis=1) == y).mean()파라미터마다 무작위 칸 5개씩, 모두 20칸을 양쪽 차분으로 재서 역전파 값과 비교합니다. 학습을 시작하기 전에 반드시 통과시켜야 하는 관문입니다.
6단계: 미니배치 SGD 학습
# 6. 미니배치 SGD 학습
lr, lam, batch, epochs = 0.1, 1e-4, 32, 30
best = (0, None, -1)
for ep in range(1, epochs + 1):
perm = rng.permutation(len(y_tr))
for i in range(0, len(perm), batch):
idx = perm[i:i + batch]
g = backward(params, X_tr[idx], y_tr[idx], lam)
for k in params:
params[k] -= lr * g[k]
va = accuracy(params, X_va, y_va)
if va > best[0]:
best = (va, {k: v.copy() for k, v in params.items()}, ep)
if ep in (1, 5, 10, 20, 30):
print("에포크 %2d 학습 손실 %.4f 학습 정확도 %.4f 검증 정확도 %.4f"
% (ep, loss_fn(params, X_tr, y_tr), accuracy(params, X_tr, y_tr), va))에포크마다 순서를 섞고, 32장씩 끊어 기울기를 구해 한 걸음씩 갑니다. 검증 정확도가 가장 좋았던 시점의 가중치를 복사해 둡니다(조기 종료와 같은 효과).
7~8단계: 테스트와 혼동 행렬
# 7. 가장 좋았던 검증 시점의 가중치로 테스트 한 번
va, p_best, ep_best = best
print("검증 최고 %.4f (에포크 %d) → 테스트 정확도 %.4f" % (va, ep_best, accuracy(p_best, X_te, y_te)))
# 8. 혼동이 많은 숫자 쌍
pred = forward(p_best, X_te)[2].argmax(axis=1)
cm = np.zeros((10, 10), dtype=int)
for t, q in zip(y_te, pred):
cm[t, q] += 1
off = [(int(cm[i, j]), i, j) for i in range(10) for j in range(10) if i != j and cm[i, j] > 0]
print("틀린 예 (개수, 정답, 예측):", sorted(off, reverse=True)[:5])실제 실행 출력
python -X utf8 numpy_mlp.py로 실행한 결과입니다(Python 3.14, NumPy 2.5.1, scikit-learn 1.9.0, CPU, 시드 0).
학습 (1149, 64) 검증 (288, 64) 테스트 (360, 64)
초기 손실 2.4723 (ln 10 = 2.3026)
기울기 점검 최대 상대 오차 8.33e-07
에포크 1 학습 손실 1.2939 학습 정확도 0.8242 검증 정확도 0.8194
에포크 5 학습 손실 0.3080 학습 정확도 0.9391 검증 정확도 0.9271
에포크 10 학습 손실 0.1772 학습 정확도 0.9634 검증 정확도 0.9444
에포크 20 학습 손실 0.1018 학습 정확도 0.9826 검증 정확도 0.9653
에포크 30 학습 손실 0.0680 학습 정확도 0.9913 검증 정확도 0.9653
검증 최고 0.9653 (에포크 20) → 테스트 정확도 0.9556
틀린 예 (개수, 정답, 예측): [(2, 8, 1), (2, 4, 8), (1, 9, 5), (1, 9, 3), (1, 8, 7)]결과 읽기
- 초기 손실 2.47 ≈ ln 10 = 2.30: 출발점이 "거의 찍기"라는 뜻이라 정상입니다. He 초기화된 로짓이 조금 퍼져 있어 보다 약간 큽니다.
- 기울기 점검 : 역전파 구현이 맞습니다. 만약 이상이 나오면 역전파에 버그가 있습니다.
- 학습 곡선: 학습 정확도는 0.99까지 오르는데 검증은 0.965에서 멈춥니다. 20에포크 이후로는 학습 데이터만 더 외우는 중입니다(약한 과적합).
- 테스트 0.9556: 검증으로 고른 모델을 테스트에 한 번만 적용한 정직한 점수입니다. 검증(0.965)보다 조금 낮은 것은 흔한 일입니다.
- 혼동: 8을 1로, 4를 8로 본 경우가 각각 2번입니다. 8×8 해상도에서 획이 뭉개지는 숫자들입니다.
이 숫자들은 시드 · 라이브러리 버전에 따라 조금씩 달라질 수 있습니다.
바꿔 보기 과제
- 은닉 크기:
H를 16, 128로 바꿔 검증 정확도와 학습 정확도의 차이(과적합 정도)를 비교해 보세요. - 학습률:
lr을 0.01, 0.5, 2.0으로 바꿔 보세요. 어느 값에서 손실이 줄지 않거나 튀나요? - L2 세기:
lam을 0, 1e-3, 1e-2로 바꿔 학습 · 검증 정확도 차이가 어떻게 변하는지 보세요. - 초기화: He 대신
* 0.01로 바꾸면 초반 학습 속도가 어떻게 달라지나요? - 모멘텀 추가: 파라미터마다 속도
v[k] = 0.9 * v[k] + g[k]를 두고params[k] -= lr * v[k]로 바꿔 보세요(옵티마이저 수업).
과제 결과는 직접 실행해 확인하세요(이 글에서는 실행 결과를 싣지 않았습니다).
흔한 오류
| 증상 | 원인 · 처방 |
|---|---|
RuntimeWarning: overflow in exp | 소프트맥스 전에 최댓값을 빼지 않음 |
손실이 nan | log(0) — 확률에 작은 수(1e-12)를 더하거나 학습률을 낮춤 |
| 기울기 점검 오차가 큼 | dO /= n 누락, ReLU 마스크를 h > 0 대신 엉뚱한 값으로 씀, 전치 방향 실수 |
| 정확도가 0.1에서 안 오름 | 입력 정규화 누락(0~16 그대로), 학습률이 너무 작거나 큼, 라벨과 입력 순서 섞임 |
| 콘솔 한글이 깨짐(Windows) | python -X utf8로 실행 |
스스로 점검
- 왜 테스트 정확도는 검증 최고 시점의 가중치로 한 번만 재나요?
- 답: 테스트로 모델을 고르면 테스트 데이터에 맞춘 셈이 되어 점수가 부풀려집니다. 고르는 일은 검증으로, 보고는 테스트로 합니다.
dO[np.arange(n), y] -= 1은 무엇을 계산하나요?- 답: 소프트맥스 확률에서 정답 칸만 1을 빼서 , 즉 로짓에 대한 교차 엔트로피 기울기를 만듭니다.
dZ1 = dH * (z1 > 0)은 역전파의 어떤 규칙인가요?- 답: ReLU 마디의 "문 여닫기" — 순전파 때 양수였던 칸으로만 기울기를 통과시킵니다.
용어 정리
| 용어 | 쉬운 뜻 |
|---|---|
| 에포크(epoch) | 학습 데이터 전체를 한 번 다 도는 것 |
| 미니배치(minibatch) | 한 걸음에 쓰는 예제 묶음(여기서는 32장) |
| 층화 분할(stratified split) | 나눈 묶음마다 클래스 비율을 같게 유지하는 분할 |
| 로짓(logit) | 소프트맥스 전 점수 |
| 수치 기울기 점검(gradient check) | 양쪽 차분과 역전파 값을 비교하는 검사 |
| 혼동 행렬(confusion matrix) | 정답 × 예측 개수를 센 표 |
| 모델 선택(model selection) | 검증 점수로 가중치 · 설정을 고르는 일 |
다음에 볼 것
- 합성곱 신경망 기초 — 이미지의 공간 구조를 쓰는 층으로 같은 문제를 더 잘 풉니다.
- 직접 해 보기: PyTorch로 합성곱 신경망 학습하기 — 같은 데이터를 PyTorch와 합성곱 신경망으로 다시 풉니다.
라이선스
이 글과 코드는 KAITABASE가 직접 작성했습니다(원문 번역 아님). 구성은 CS231n '신경망 사례 연구' 노트(MIT 라이선스)의 "넘파이로 작은 신경망 만들기" 흐름을 참고했습니다. 데이터는 scikit-learn에 포함된 UCI 손글씨 숫자 데이터의 사본입니다. 출력은 위 환경에서 직접 실행한 결과입니다.
출처
- 참고: cs231n.github.io/neural-networks-case-stu… (CS231n 신경망 사례 연구)
- scikit-learn load_digits: scikit-learn.org/stable/modules/generated…
- scikit-learn train_test_split: scikit-learn.org/stable/modules/generated…
AI 원리부터 이해하기 · 딥러닝 기초 모듈
딥러닝 기초 모듈 학습
이 글은 딥러닝 기초 모듈의 학습 자료입니다. 전체 29개 자료 중 13번째입니다.
출처·원문
원문 보기요약만