MNIST 실습에서는 기본 신경망을 구성하고 훈련하는 전체 과정을 Keras와 PyTorch로 확인했다.
이번 프로젝트에서는 같은 원리를 Fashion-MNIST 의류 이미지 분류 문제에 적용한다.
프로젝트의 목표는 실습 코드를 그대로 반복하는 것이 아니라,
문제를 이해하고 → 모델을 구성하고 → 결과를 비교하고 → 오류를 분석하여 → 최종 판단을 내리는 것
이다.
Keras와 PyTorch 중 하나의 프레임워크를 선택하여 프로젝트를 진행한다.
4.1프로젝트에서 해야 할 일¶
Fashion-MNIST 데이터와 타깃의 의미를 확인한다.
이미지를 완전연결 신경망에 맞게 변환하고 정규화한다.
MNIST 실습의 기본 신경망을 출발점으로 기준 모델을 만든다.
기준 모델에서 구조를 하나 이상 변경한 비교 모델을 만든다.
두 모델의 훈련 결과와 검증 결과를 비교한다.
선택한 모델을 테스트 데이터에서 최종 평가한다.
오분류 사례를 살펴보고 모델이 어려워하는 경우를 분석한다.
모델 선택의 근거와 한계를 정리한다.
성능 수치 하나만 높이는 것이 목표가 아니다. 왜 그 모델을 선택했는지 설명할 수 있어야 한다.
4.2생성형 AI 활용¶
생성형 AI는 코드 작성·수정, 오류 메시지 해석, Keras와 PyTorch 코드 변환, 그래프 작성, 분석 아이디어 제안 등에 활용할 수 있다.
다만 입력과 타깃의 의미, 모델 구조, 비교 조건, 결과 해석, 오분류 분석, 최종 모델 선택은 직접 확인하고 판단해야 한다.
프로젝트 마지막에 AI를 어디에 활용했고 무엇을 직접 검증했는지 기록한다.
4.3실행 환경 확인¶
import os
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
import tensorflow as tf
import keras
import torch
tf_gpus = tf.config.list_physical_devices("GPU")
print("=== TensorFlow ===")
print("Version:", tf.__version__)
print("GPU 사용 가능:", len(tf_gpus) > 0)
if tf_gpus:
print("GPU:", tf_gpus[0].name)
print()
print("=== PyTorch ===")
print("Version:", torch.__version__)
print("GPU 사용 가능:", torch.cuda.is_available())
if torch.cuda.is_available():
print("GPU:", torch.cuda.get_device_name(0))
print("CUDA version:", torch.version.cuda)4.4Fashion-MNIST 데이터¶
Fashion-MNIST는 10종류의 의류 이미지로 구성된 데이터셋이다. 각 이미지는 MNIST와 마찬가지로 28 × 28 크기의 흑백 이미지다.
| label | class |
|---|---|
| 0 | T-shirt/top |
| 1 | Trouser |
| 2 | Pullover |
| 3 | Dress |
| 4 | Coat |
| 5 | Sandal |
| 6 | Shirt |
| 7 | Sneaker |
| 8 | Bag |
| 9 | Ankle boot |
from keras.datasets import fashion_mnist
(train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data()
class_names = [
"T-shirt/top", "Trouser", "Pullover", "Dress", "Coat",
"Sandal", "Shirt", "Sneaker", "Bag", "Ankle boot",
]
print("train_images:", train_images.shape)
print("train_labels:", train_labels.shape)
print("test_images :", test_images.shape)
print("test_labels :", test_labels.shape)4.4.1데이터 구조 설명¶
다음 질문에 답하라.
train_images.shape의 각 숫자는 무엇을 의미하는가?train_labels.shape은 무엇을 의미하는가?MNIST와 Fashion-MNIST의 입력 데이터 구조에서 공통점은 무엇인가?
두 데이터셋의 예측 대상은 어떻게 다른가?
답:
여기에 설명을 작성한다.
4.5실제 이미지 확인¶
Source
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(10, 5))
for i in range(12):
ax = fig.add_subplot(3, 4, i + 1)
ax.imshow(train_images[i], cmap="gray")
ax.set_title(class_names[train_labels[i]])
ax.axis("off")
plt.tight_layout()
plt.show()4.5.1관찰¶
사람이 보기에 쉽게 구분되는 클래스는 무엇인가?
서로 비슷해 보여 혼동될 가능성이 있는 클래스는 무엇인가?
그 이유는 무엇이라고 생각하는가?
답:
여기에 관찰 결과를 작성한다.
train_images = train_images.reshape((60000, 28 * 28)).astype("float32") / 255
test_images = test_images.reshape((10000, 28 * 28)).astype("float32") / 255
print("train_images:", train_images.shape)
print("test_images :", test_images.shape)
print("픽셀값 범위:", train_images.min(), "~", train_images.max())4.6.1전처리 확인¶
reshape 전후에 이미지 한 장의 픽셀 수는 달라졌는가?
왜 완전연결 신경망에서는 이미지를
(784,)로 펼쳐 사용하는가?픽셀값을
0~1범위로 바꾼 이유는 무엇인가?
답:
여기에 설명을 작성한다.
4.7모델링¶
Keras 또는 PyTorch 중 하나를 선택한다.
MNIST 실습에서 사용한
784 → 512 → 10
구조를 기준 모델의 출발점으로 사용하고, 비교 모델에서는 다음 중 하나 이상을 바꿀 수 있다.
은닉 유닛 수
은닉층 수
배치 크기
에포크 수
여러 항목을 동시에 크게 바꾸기보다 무엇을 바꾸었는지 설명할 수 있도록 비교 조건을 단순하게 유지한다.
Dropout, Early Stopping 등의 규제 기법은 P2에서 다룬다. P1에서는 사용하지 않는다.
4.8.1Keras 기준 모델¶
from keras import layers
keras_base = keras.Sequential([
layers.Dense(512, activation="relu"),
layers.Dense(10, activation="softmax"),
])
keras_base.build(input_shape=(None, 28 * 28))
keras_base.summary()keras_base.compile(
optimizer="adam",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)
history_base = keras_base.fit(
train_images,
train_labels,
epochs=5,
batch_size=128,
validation_split=0.2,
)# TODO: 비교 모델을 구성한다.
keras_compare = keras.Sequential([
# layers.Dense(...),
# ...
])
# TODO: 입력 shape에 맞게 build하고 구조를 확인한다.
# keras_compare.build(input_shape=(None, 28 * 28))
# keras_compare.summary()# TODO: 비교 모델을 compile하고 훈련한다.
# keras_compare.compile(...)
#
# history_compare = keras_compare.fit(
# train_images,
# train_labels,
# ...
# )4.9.1PyTorch 데이터 준비¶
from torch import nn
from torch.utils.data import TensorDataset, DataLoader, random_split
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)
full_train_dataset = TensorDataset(x_train_t, y_train_t)
test_dataset = TensorDataset(x_test_t, y_test_t)
train_size = int(0.8 * len(full_train_dataset))
val_size = len(full_train_dataset) - train_size
generator = torch.Generator().manual_seed(42)
train_dataset, val_dataset = random_split(
full_train_dataset,
[train_size, val_size],
generator=generator,
)
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=256, shuffle=False)
test_loader = DataLoader(test_dataset, batch_size=256, shuffle=False)
print("train:", len(train_dataset))
print("validation:", len(val_dataset))
print("test:", len(test_dataset))4.9.2PyTorch 기준 모델¶
torch_base = nn.Sequential(
nn.Linear(28 * 28, 512),
nn.ReLU(),
nn.Linear(512, 10),
)
torch_base4.9.3PyTorch 훈련 함수¶
프로젝트의 초점은 반복문의 문법이 아니라 모델을 비교하고 결과를 판단하는 것이므로 훈련 함수의 기본 틀을 제공한다.
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
def train_torch_model(model, train_loader, val_loader, epochs=5):
model = model.to(device)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters())
history = {"loss": [], "accuracy": [], "val_loss": [], "val_accuracy": []}
for epoch in range(epochs):
model.train()
running_loss, correct, total = 0.0, 0, 0
for x_batch, y_batch in train_loader:
x_batch = x_batch.to(device)
y_batch = y_batch.to(device)
optimizer.zero_grad()
outputs = model(x_batch)
loss = loss_fn(outputs, y_batch)
loss.backward()
optimizer.step()
running_loss += loss.item() * x_batch.size(0)
correct += (outputs.argmax(dim=1) == y_batch).sum().item()
total += y_batch.size(0)
train_loss = running_loss / total
train_acc = correct / total
model.eval()
val_running_loss, val_correct, val_total = 0.0, 0, 0
with torch.no_grad():
for x_batch, y_batch in val_loader:
x_batch = x_batch.to(device)
y_batch = y_batch.to(device)
outputs = model(x_batch)
loss = loss_fn(outputs, y_batch)
val_running_loss += loss.item() * x_batch.size(0)
val_correct += (outputs.argmax(dim=1) == y_batch).sum().item()
val_total += y_batch.size(0)
val_loss = val_running_loss / val_total
val_acc = val_correct / val_total
history["loss"].append(train_loss)
history["accuracy"].append(train_acc)
history["val_loss"].append(val_loss)
history["val_accuracy"].append(val_acc)
print(
f"epoch {epoch + 1}: "
f"loss={train_loss:.4f}, accuracy={train_acc:.4f}, "
f"val_loss={val_loss:.4f}, val_accuracy={val_acc:.4f}"
)
return model, historytorch_base, torch_history_base = train_torch_model(
torch_base,
train_loader,
val_loader,
epochs=5,
)# TODO: 비교 모델을 구성한다.
torch_compare = nn.Sequential(
# nn.Linear(...),
# ...
)
# TODO: 비교 모델을 훈련한다.
# torch_compare, torch_history_compare = train_torch_model(
# torch_compare,
# train_loader,
# val_loader,
# epochs=...
# )# TODO: 선택한 프레임워크에 맞게 history를 지정한다.
#
# Keras:
# base_history = history_base.history
# compare_history = history_compare.history
#
# PyTorch:
# base_history = torch_history_base
# compare_history = torch_history_compareSource
# TODO: 두 모델의 검증 정확도를 비교한다.
#
# plt.figure(figsize=(7, 4))
# plt.plot(base_history["val_accuracy"], marker="o", label="base")
# plt.plot(compare_history["val_accuracy"], marker="o", label="compare")
# plt.xlabel("epoch")
# plt.ylabel("validation accuracy")
# plt.legend()
# plt.show()4.10.1결과 비교와 모델 선택¶
기준 모델과 비교 모델에서 무엇을 다르게 했는가?
훈련 정확도는 어떻게 달라졌는가?
검증 정확도와 검증 loss는 어떻게 달라졌는가?
단순히 마지막 에포크의 정확도만 보고 모델을 선택해도 되는가?
두 모델 중 하나를 최종 모델로 선택한다면 무엇을 선택하겠는가?
그 이유는 무엇인가?
답:
여기에 비교와 판단을 작성한다.
# TODO: Keras를 선택한 경우
#
# final_model = ...
# test_loss, test_acc = final_model.evaluate(test_images, test_labels, verbose=0)
# print(f"test loss : {test_loss:.4f}")
# print(f"test accuracy : {test_acc:.4f}")# TODO: PyTorch를 선택한 경우
#
# final_model = ...
# final_model.eval()
# correct = 0
# total = 0
#
# with torch.no_grad():
# for x_batch, y_batch in test_loader:
# x_batch = x_batch.to(device)
# y_batch = y_batch.to(device)
# outputs = final_model(x_batch)
# predictions_t = outputs.argmax(dim=1)
# correct += (predictions_t == y_batch).sum().item()
# total += y_batch.size(0)
#
# test_acc = correct / total
# print(f"test accuracy: {test_acc:.4f}")# TODO: 선택한 프레임워크에서 테스트 데이터 전체의 예측 클래스 번호를 만든다.
#
# Keras 예:
# final_predictions = final_model.predict(test_images, verbose=0).argmax(axis=1)
#
# PyTorch에서는 test_loader를 사용해 예측 결과를 하나의 배열로 모은다.# TODO: final_predictions가 준비되면 실행한다.
#
# import numpy as np
# wrong_indices = np.where(final_predictions != test_labels)[0]
# print("오분류 개수:", len(wrong_indices))
# print("전체 테스트 이미지:", len(test_labels))Source
# TODO: 오분류 이미지 일부를 시각화한다.
#
# fig = plt.figure(figsize=(10, 6))
# for j, idx in enumerate(wrong_indices[:12]):
# ax = fig.add_subplot(3, 4, j + 1)
# ax.imshow(test_images[idx].reshape(28, 28), cmap="gray")
# ax.set_title(
# f"pred={class_names[final_predictions[idx]]}\n"
# f"label={class_names[test_labels[idx]]}"
# )
# ax.axis("off")
# plt.tight_layout()
# plt.show()4.12.1오분류 사례 분석¶
어떤 의류 클래스들이 서로 혼동되는가?
사람이 보기에도 구분하기 어려운 사례가 있는가?
정확도 하나만 보았을 때 알 수 없었던 모델의 특성은 무엇인가?
현재의 완전연결 신경망이 이미지 분류에서 가질 수 있는 한계는 무엇이라고 생각하는가?
답:
여기에 분석을 작성한다.
4.13프로젝트 최종 정리¶
4.13.1문제와 데이터¶
무엇을 예측했고 입력과 타깃은 무엇이었는가?
4.13.2모델 비교¶
기준 모델과 비교 모델의 차이는 무엇이었는가?
4.13.3최종 모델 선택¶
어떤 모델을 선택했고 그 근거는 무엇인가?
4.13.4오류와 한계¶
어떤 오류가 눈에 띄었고 현재 모델의 한계는 무엇이라고 판단하는가?
4.13.5개선 아이디어¶
다음에 모델을 개선한다면 무엇을 시도해보고 싶은가?
4.13.6생성형 AI 활용과 검증¶
AI를 어디에 사용했으며, AI의 제안이나 코드를 무엇을 기준으로 직접 확인했는가?
4.14제출 전 확인¶
Fashion-MNIST 데이터와 타깃의 의미를 설명했다.
기준 모델을 훈련했다.
기준 모델과 다른 비교 모델을 하나 이상 만들었다.
변경한 내용과 이유를 설명했다.
훈련 결과와 검증 결과를 비교했다.
최종 모델을 선택하고 선택 근거를 설명했다.
테스트 데이터에서 최종 성능을 확인했다.
오분류 이미지를 분석했다.
모델의 한계와 개선 아이디어를 제시했다.
생성형 AI 활용 내용과 직접 검증한 내용을 기록했다.