Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

4 P1 프로젝트: 의류 이미지 분류

Updated: 08 sep 2026

MNIST 실습에서는 기본 신경망을 구성하고 훈련하는 전체 과정을 Keras와 PyTorch로 확인했다.

이번 프로젝트에서는 같은 원리를 Fashion-MNIST 의류 이미지 분류 문제에 적용한다.

프로젝트의 목표는 실습 코드를 그대로 반복하는 것이 아니라,

문제를 이해하고 → 모델을 구성하고 → 결과를 비교하고 → 오류를 분석하여 → 최종 판단을 내리는 것

이다.

Keras와 PyTorch 중 하나의 프레임워크를 선택하여 프로젝트를 진행한다.

4.1프로젝트에서 해야 할 일

  1. Fashion-MNIST 데이터와 타깃의 의미를 확인한다.

  2. 이미지를 완전연결 신경망에 맞게 변환하고 정규화한다.

  3. MNIST 실습의 기본 신경망을 출발점으로 기준 모델을 만든다.

  4. 기준 모델에서 구조를 하나 이상 변경한 비교 모델을 만든다.

  5. 두 모델의 훈련 결과와 검증 결과를 비교한다.

  6. 선택한 모델을 테스트 데이터에서 최종 평가한다.

  7. 오분류 사례를 살펴보고 모델이 어려워하는 경우를 분석한다.

  8. 모델 선택의 근거와 한계를 정리한다.

성능 수치 하나만 높이는 것이 목표가 아니다. 왜 그 모델을 선택했는지 설명할 수 있어야 한다.

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 크기의 흑백 이미지다.

labelclass
0T-shirt/top
1Trouser
2Pullover
3Dress
4Coat
5Sandal
6Shirt
7Sneaker
8Bag
9Ankle 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데이터 구조 설명

다음 질문에 답하라.

  1. train_images.shape의 각 숫자는 무엇을 의미하는가?

  2. train_labels.shape은 무엇을 의미하는가?

  3. MNIST와 Fashion-MNIST의 입력 데이터 구조에서 공통점은 무엇인가?

  4. 두 데이터셋의 예측 대상은 어떻게 다른가?

답:

여기에 설명을 작성한다.

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관찰

  • 사람이 보기에 쉽게 구분되는 클래스는 무엇인가?

  • 서로 비슷해 보여 혼동될 가능성이 있는 클래스는 무엇인가?

  • 그 이유는 무엇이라고 생각하는가?

답:

여기에 관찰 결과를 작성한다.

4.6입력 데이터 준비

MNIST 실습과 같은 방식으로 이미지를 완전연결층에 맞게 펼치고 픽셀값을 0~1 범위로 정규화한다.

(28, 28) → (784,)

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전처리 확인

  1. reshape 전후에 이미지 한 장의 픽셀 수는 달라졌는가?

  2. 왜 완전연결 신경망에서는 이미지를 (784,)로 펼쳐 사용하는가?

  3. 픽셀값을 0~1 범위로 바꾼 이유는 무엇인가?

답:

여기에 설명을 작성한다.


4.7모델링

Keras 또는 PyTorch 중 하나를 선택한다.

MNIST 실습에서 사용한

784 → 512 → 10

구조를 기준 모델의 출발점으로 사용하고, 비교 모델에서는 다음 중 하나 이상을 바꿀 수 있다.

  • 은닉 유닛 수

  • 은닉층 수

  • 배치 크기

  • 에포크 수

여러 항목을 동시에 크게 바꾸기보다 무엇을 바꾸었는지 설명할 수 있도록 비교 조건을 단순하게 유지한다.

Dropout, Early Stopping 등의 규제 기법은 P2에서 다룬다. P1에서는 사용하지 않는다.

4.7.1선택한 프레임워크

  • 선택: Keras / PyTorch

  • 선택 이유:

여기에 작성한다.


4.8Keras를 선택한 경우

PyTorch를 선택했다면 이 절은 실행하지 않아도 된다.

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,
)

4.8.2Keras 비교 모델

기준 모델에서 무엇을 바꾸었는지와 그 이유를 먼저 작성한다.

변경 내용과 이유:

여기에 작성한다.

# 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.9PyTorch를 선택한 경우

Keras를 선택했다면 이 절은 실행하지 않아도 된다.

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_base

4.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, history
torch_base, torch_history_base = train_torch_model(
    torch_base,
    train_loader,
    val_loader,
    epochs=5,
)

4.9.4PyTorch 비교 모델

변경 내용과 이유:

여기에 작성한다.

# TODO: 비교 모델을 구성한다.
torch_compare = nn.Sequential(
    # nn.Linear(...),
    # ...
)

# TODO: 비교 모델을 훈련한다.
# torch_compare, torch_history_compare = train_torch_model(
#     torch_compare,
#     train_loader,
#     val_loader,
#     epochs=...
# )

4.10모델 비교

선택한 프레임워크에서 기준 모델과 비교 모델의 훈련·검증 결과를 비교한다.

# TODO: 선택한 프레임워크에 맞게 history를 지정한다.
#
# Keras:
# base_history = history_base.history
# compare_history = history_compare.history
#
# PyTorch:
# base_history = torch_history_base
# compare_history = torch_history_compare
Source
# 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결과 비교와 모델 선택

  1. 기준 모델과 비교 모델에서 무엇을 다르게 했는가?

  2. 훈련 정확도는 어떻게 달라졌는가?

  3. 검증 정확도와 검증 loss는 어떻게 달라졌는가?

  4. 단순히 마지막 에포크의 정확도만 보고 모델을 선택해도 되는가?

  5. 두 모델 중 하나를 최종 모델로 선택한다면 무엇을 선택하겠는가?

  6. 그 이유는 무엇인가?

답:

여기에 비교와 판단을 작성한다.


4.11최종 테스트 평가

모델 비교가 끝난 뒤 선택한 최종 모델 하나만 테스트 데이터에서 평가한다.

테스트 데이터는 모델 선택 과정에 사용하지 않는다.

# 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}")

4.11.1테스트 결과 해석

  • 테스트 정확도:

  • 검증 정확도와 테스트 정확도의 차이:

  • 이 결과만으로 모델이 충분히 좋다고 말할 수 있는가?

답:

여기에 작성한다.


4.12오류 분석

정확도만으로는 모델이 어떤 이미지를 잘못 분류하는지 알 수 없다. 최종 모델이 틀린 이미지를 찾아 직접 살펴본다.

# 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오분류 사례 분석

  1. 어떤 의류 클래스들이 서로 혼동되는가?

  2. 사람이 보기에도 구분하기 어려운 사례가 있는가?

  3. 정확도 하나만 보았을 때 알 수 없었던 모델의 특성은 무엇인가?

  4. 현재의 완전연결 신경망이 이미지 분류에서 가질 수 있는 한계는 무엇이라고 생각하는가?

답:

여기에 분석을 작성한다.


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 활용 내용과 직접 검증한 내용을 기록했다.