이번 프로젝트의 질문은 단순하다.
손글씨 숫자를 신경망으로 얼마나 잘 구분할 수 있을까?
하지만 목표는 정확도 경쟁이 아니다.
세 번의 실습을 통해
데이터 이해 → 첫 모델 → 훈련과 비교 → 오류 분석 → 최종 판단
의 전체 흐름을 경험한다.
Keras와 PyTorch로 같은 문제를 풀고, 두 프레임워크의 차이보다 공통된 딥러닝 원리를 확인한다.
3.1프로젝트 목표¶
프로젝트가 끝나면 다음을 할 수 있어야 한다.
MNIST 데이터의 입력과 타깃을 설명한다.
이미지와 배치의 shape을 읽는다.
간단한 MLP를 Keras와 PyTorch로 구성한다.
훈련 손실과 정확도의 변화를 해석한다.
테스트셋에서 최종 성능을 확인한다.
잘못 분류된 이미지를 찾아 오류를 분석한다.
두 구현의 공통점과 차이점을 설명한다.
코드 작성 자체보다 코드가 무엇을 하는지 설명하고 결과를 판단하는 것이 중요하다.
3.2준비¶
아래 셀을 실행해 필요한 라이브러리와 난수 시드를 준비한다.
import random
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import torch
from torch import nn
from torch.utils.data import TensorDataset, DataLoader
SEED = 42
random.seed(SEED)
np.random.seed(SEED)
tf.random.set_seed(SEED)
torch.manual_seed(SEED)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("PyTorch device:", device)from tensorflow.keras.datasets import mnist
(train_images_raw, train_labels), (test_images_raw, test_labels) = mnist.load_data()print("train_images:", train_images_raw.shape, train_images_raw.dtype)
print("train_labels:", train_labels.shape, train_labels.dtype)
print("test_images :", test_images_raw.shape, test_images_raw.dtype)
print("pixel range:", train_images_raw.min(), "~", train_images_raw.max())
print("labels:", np.unique(train_labels))판단 1
이 문제의 입력은 무엇인가?
타깃은 수치형 값인가, 범주형 값인가?
회귀와 분류 중 어떤 문제인가?
마지막 출력층에는 몇 개의 출력이 필요할까?
아래에 자신의 답을 작성한다.
답:
3.3.33. 이미지 직접 보기¶
무작위로 몇 장을 확인한다.
fig, axes = plt.subplots(2, 5, figsize=(10, 4))
indices = np.random.choice(len(train_images_raw), size=10, replace=False)
for ax, idx in zip(axes.ravel(), indices):
ax.imshow(train_images_raw[idx], cmap="gray")
ax.set_title(f"label={train_labels[idx]}")
ax.axis("off")
plt.tight_layout()
plt.show()관찰 1
숫자마다 글씨체가 얼마나 다른가? 사람이 보아도 애매한 이미지가 있는가?
관찰:
3.3.44. 전처리¶
완전연결 신경망에 넣기 위해 각 이미지를 784개의 값으로 펼치고 픽셀값을 0~1 범위로 바꾼다.
train_images = train_images_raw.reshape((60000, 28 * 28)).astype("float32") / 255
test_images = test_images_raw.reshape((10000, 28 * 28)).astype("float32") / 255
print(train_images.shape)
print(test_images.shape)
print(train_images.min(), train_images.max())판단 2
(60000, 784)에서 두 숫자는 각각 무엇을 의미하는가?
답:
3.3.55. Keras 기본 모델¶
기존 dlp2의 기본 구조를 사용한다.
keras_model = keras.Sequential([
layers.Dense(512, activation="relu"),
layers.Dense(10, activation="softmax")
])
keras_model.compile(
optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"]
)
keras_model.summary()history = keras_model.fit(
train_images,
train_labels,
epochs=5,
batch_size=128,
validation_split=0.1,
verbose=1
)3.3.77. 학습 곡선 확인¶
history_dict = history.history
epochs = range(1, len(history_dict["loss"]) + 1)
plt.figure(figsize=(7, 4))
plt.plot(epochs, history_dict["loss"], marker="o", label="train loss")
plt.plot(epochs, history_dict["val_loss"], marker="o", label="validation loss")
plt.xlabel("epoch")
plt.ylabel("loss")
plt.legend()
plt.show()
plt.figure(figsize=(7, 4))
plt.plot(epochs, history_dict["accuracy"], marker="o", label="train accuracy")
plt.plot(epochs, history_dict["val_accuracy"], marker="o", label="validation accuracy")
plt.xlabel("epoch")
plt.ylabel("accuracy")
plt.legend()
plt.show()판단 3
에포크가 증가하면서 훈련 손실은 어떻게 변하는가?
검증 손실도 같은 방식으로 변하는가?
현재 결과만으로 과대적합이 나타난다고 볼 수 있는가?
답:
3.4실습 2 — PyTorch로 같은 훈련 과정 확인¶
Keras에서는 fit()이 훈련 루프를 처리했다.
이번에는 같은 데이터를 PyTorch 텐서와 DataLoader로 만들고, 훈련 루프를 직접 확인한다.
x_train_t = torch.tensor(train_images, dtype=torch.float32)
y_train_t = torch.tensor(train_labels, dtype=torch.long)
x_test_t = torch.tensor(test_images, dtype=torch.float32)
y_test_t = torch.tensor(test_labels, dtype=torch.long)
# Keras의 validation_split=0.1과 비슷하게 뒤 6,000개를 검증셋으로 사용
x_val_t = x_train_t[-6000:]
y_val_t = y_train_t[-6000:]
x_train_sub_t = x_train_t[:-6000]
y_train_sub_t = y_train_t[:-6000]
train_ds = TensorDataset(x_train_sub_t, y_train_sub_t)
val_ds = TensorDataset(x_val_t, y_val_t)
test_ds = TensorDataset(x_test_t, y_test_t)
train_loader = DataLoader(train_ds, batch_size=128, shuffle=True)
val_loader = DataLoader(val_ds, batch_size=256, shuffle=False)
test_loader = DataLoader(test_ds, batch_size=256, shuffle=False)
xb, yb = next(iter(train_loader))
print("input batch :", xb.shape)
print("target batch:", yb.shape)판단 4
배치 하나의 입력 shape이 (128, 784)라면 128과 784는 각각 무엇을 의미하는가?
답:
3.4.1PyTorch 모델 구성¶
Keras와 동일하게 784 → 512 → 10 구조를 사용한다.
마지막에 Softmax를 넣지 않는다. CrossEntropyLoss가 모델의 원시 출력인 logits를 직접 사용하기 때문이다.
torch_model = nn.Sequential(
nn.Linear(28 * 28, 512),
nn.ReLU(),
nn.Linear(512, 10)
).to(device)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.RMSprop(torch_model.parameters())
print(torch_model)def evaluate_torch(model, loader, loss_fn, device):
model.eval()
total_loss = 0.0
total_correct = 0
total_count = 0
with torch.no_grad():
for x_batch, y_batch in loader:
x_batch = x_batch.to(device)
y_batch = y_batch.to(device)
logits = model(x_batch)
loss = loss_fn(logits, y_batch)
total_loss += loss.item() * len(x_batch)
total_correct += (logits.argmax(dim=1) == y_batch).sum().item()
total_count += len(x_batch)
return total_loss / total_count, total_correct / total_count
torch_history = {
"loss": [],
"accuracy": [],
"val_loss": [],
"val_accuracy": []
}
for epoch in range(5):
torch_model.train()
running_loss = 0.0
running_correct = 0
running_count = 0
for x_batch, y_batch in train_loader:
x_batch = x_batch.to(device)
y_batch = y_batch.to(device)
# 1. 이전 그레이디언트 초기화
optimizer.zero_grad()
# 2. 순전파
logits = torch_model(x_batch)
# 3. 손실 계산
loss = loss_fn(logits, y_batch)
# 4. 역전파: 그레이디언트 계산
loss.backward()
# 5. 파라미터 갱신
optimizer.step()
running_loss += loss.item() * len(x_batch)
running_correct += (logits.argmax(dim=1) == y_batch).sum().item()
running_count += len(x_batch)
train_loss = running_loss / running_count
train_acc = running_correct / running_count
val_loss, val_acc = evaluate_torch(torch_model, val_loader, loss_fn, device)
torch_history["loss"].append(train_loss)
torch_history["accuracy"].append(train_acc)
torch_history["val_loss"].append(val_loss)
torch_history["val_accuracy"].append(val_acc)
print(
f"epoch {epoch+1}: "
f"loss={train_loss:.4f}, acc={train_acc:.4f}, "
f"val_loss={val_loss:.4f}, val_acc={val_acc:.4f}"
)3.4.3PyTorch 학습 곡선¶
epochs = range(1, len(torch_history["loss"]) + 1)
plt.figure(figsize=(7, 4))
plt.plot(epochs, torch_history["loss"], marker="o", label="train loss")
plt.plot(epochs, torch_history["val_loss"], marker="o", label="validation loss")
plt.xlabel("epoch")
plt.ylabel("loss")
plt.legend()
plt.show()
plt.figure(figsize=(7, 4))
plt.plot(epochs, torch_history["accuracy"], marker="o", label="train accuracy")
plt.plot(epochs, torch_history["val_accuracy"], marker="o", label="validation accuracy")
plt.xlabel("epoch")
plt.ylabel("accuracy")
plt.legend()
plt.show()판단 5
PyTorch 훈련 루프에서 다음 코드의 역할을 한 문장씩 설명하라.
optimizer.zero_grad()logits = torch_model(x_batch)loss = loss_fn(logits, y_batch)loss.backward()optimizer.step()
답:
3.4.4Keras와 PyTorch 결과 비교¶
두 프레임워크의 결과가 완전히 같을 필요는 없다.
초기 파라미터, 데이터가 섞이는 순서, 구현 세부사항 등이 다르기 때문이다.
비교할 것은 다음이다.
훈련이 진행되면서 손실이 감소하는가?
정확도가 증가하는가?
훈련과 검증 성능의 관계는 비슷한가?
두 코드에서 순전파·손실·파라미터 갱신이라는 공통 구조를 찾을 수 있는가?
비교 결과:
3.5실습 3 — 최종 평가와 오류 분석¶
이제 마지막으로 테스트셋을 사용한다.
테스트셋은 모델을 선택하거나 반복적으로 수정하기 위한 데이터가 아니라 최종 모델의 일반화 성능을 확인하기 위한 데이터로 사용한다.
3.5.1Keras 테스트 성능¶
keras_test_loss, keras_test_acc = keras_model.evaluate(
test_images, test_labels, verbose=0
)
print("Keras test loss :", keras_test_loss)
print("Keras test accuracy:", keras_test_acc)3.5.2PyTorch 테스트 성능¶
torch_test_loss, torch_test_acc = evaluate_torch(
torch_model, test_loader, loss_fn, device
)
print("PyTorch test loss :", torch_test_loss)
print("PyTorch test accuracy:", torch_test_acc)3.5.3잘못 분류된 이미지 찾기¶
먼저 Keras 모델의 예측을 이용해 오류를 확인한다.
keras_probs = keras_model.predict(test_images, verbose=0)
keras_pred = keras_probs.argmax(axis=1)
wrong_idx = np.where(keras_pred != test_labels)[0]
print("오분류 수:", len(wrong_idx))
print("전체 테스트 이미지 수:", len(test_labels))n_show = min(12, len(wrong_idx))
chosen = wrong_idx[:n_show]
fig, axes = plt.subplots(3, 4, figsize=(9, 7))
for ax, idx in zip(axes.ravel(), chosen):
ax.imshow(test_images_raw[idx], cmap="gray")
ax.set_title(
f"true={test_labels[idx]}, pred={keras_pred[idx]}"
)
ax.axis("off")
plt.tight_layout()
plt.show()오류 분석
오분류 이미지들을 보고 다음을 작성하라.
사람이 보아도 애매한 이미지가 있는가?
특정 숫자 쌍에서 혼동이 자주 보이는가?
글씨가 기울었거나 일부 획이 약한 경우가 있는가?
단순히 테스트 정확도만 볼 때는 알 수 없었던 사실은 무엇인가?
분석:
3.5.4선택 과제 — 어떤 숫자를 어떤 숫자로 혼동했는가?¶
아래 코드를 완성하거나 생성형 AI의 도움을 받아 오분류 빈도표를 만들어보자.
예를 들어 실제 숫자 4를 9로 잘못 예측한 횟수를 확인할 수 있어야 한다.
AI의 도움을 받았다면 생성된 코드가 무엇을 세는지 직접 설명할 수 있어야 한다.
# TODO:
# 1. 실제 라벨과 예측 라벨을 이용한다.
# 2. 같은 조합이 몇 번 나타나는지 센다.
# 3. 가장 자주 발생한 오분류 조합을 찾아본다.
# 여기에 코드를 작성하세요.3.6작은 실험 — 은닉층 크기를 바꾸면?¶
기본 모델은 은닉층에 512개의 유닛을 사용했다.
다음 중 하나를 선택하여 다시 훈련해보자.
32
128
256
1024
비교할 것은 단순히 테스트 정확도 하나가 아니다.
파라미터 수
훈련 시간
훈련 정확도
검증 정확도
테스트 정확도
를 함께 본다.
# TODO: units 값을 바꾸어 실험한다.
units = 128
experiment_model = keras.Sequential([
layers.Dense(units, activation="relu"),
layers.Dense(10, activation="softmax")
])
experiment_model.compile(
optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"]
)
experiment_model.summary()
experiment_history = experiment_model.fit(
train_images,
train_labels,
epochs=5,
batch_size=128,
validation_split=0.1,
verbose=1
)판단 6
은닉층을 크게 만들면 항상 더 좋은 모델이 되는가?
실험 결과를 근거로 답한다.
답:
3.7생성형 AI 활용 점검¶
이번 프로젝트에서 생성형 AI를 사용했다면 아래를 기록한다.
3.7.1AI를 사용한 작업¶
코드 설명
오류 수정
PyTorch/Keras 코드 변환
그래프 작성
오류 분석 아이디어
기타
3.7.2AI 결과를 어떻게 검증했는가?¶
다음 중 최소 두 가지를 구체적으로 작성한다.
데이터 shape을 확인했다.
입력과 타깃이 맞게 연결되었는지 확인했다.
손실함수가 문제 유형에 적절한지 확인했다.
테스트셋을 모델 선택에 반복 사용하지 않았는지 확인했다.
생성된 코드의 각 단계가 무엇을 하는지 설명해 보았다.
결과가 비정상적으로 높거나 낮지 않은지 확인했다.
검증 기록:
3.8최종 제출 내용¶
길게 작성할 필요는 없다. 다음 내용을 중심으로 정리한다.
3.8.11. 문제¶
MNIST 프로젝트에서 무엇을 예측했는가?
3.8.22. 데이터¶
입력과 타깃, 한 샘플의 shape을 설명한다.
3.8.33. 모델¶
사용한 MLP 구조를 설명한다.
3.8.44. 훈련¶
손실함수와 옵티마이저의 역할을 설명한다.
3.8.55. 결과¶
Keras 또는 PyTorch 모델의 최종 테스트 성능을 제시한다.
3.8.66. 오류 분석¶
대표적인 오분류 사례 2~3개를 설명한다.
3.8.77. 최종 판단¶
모델이 잘 작동한다고 판단하는가? 어떤 한계가 있는가?
3.8.88. Keras와 PyTorch¶
두 구현에서 가장 중요한 공통점 하나와 차이점 하나를 설명한다.
3.8.99. AI 활용¶
AI를 어디에 사용했고 무엇을 검증했는지 기록한다.
3.9P1에서 남겨야 할 한 문장¶
프로젝트를 마친 뒤 다음 문장을 자신의 말로 설명할 수 있어야 한다.
모델 훈련은 훈련 데이터를 이용하여 손실이 작아지도록 모델의 파라미터를 반복적으로 결정하는 과정이다.
그리고 Keras의 fit()이나 PyTorch의 training loop를 보았을 때,
순전파 → 손실 계산 → 역전파 → 파라미터 갱신
의 흐름을 찾을 수 있어야 한다.