Step 5. MLP로 이미지 분류하기: MNIST 데이터셋 이해¶

딥러닝의 세계에 발을 들일 때 가장 먼저 만나게 되는 'Hello World', 바로 MNIST(Modified National Institute of Standards and Technology) 데이터셋입니다.


1. MNIST란 무엇인가?¶

MNIST는 0부터 9까지의 숫자들로 이루어진 손글씨 이미지 데이터셋입니다. 1990년대에 수집된 이 데이터는 오늘날까지도 새로운 알고리즘의 성능을 검증하는 표준 벤치마크로 널리 사용되고 있습니다.

  • 데이터 구성:
    • Training Set: 60,000개의 학습 데이터
    • Test Set: 10,000개의 테스트 데이터
  • 이미지 규격: 28 x 28 픽셀 (흑백 이미지)
  • 레이블(Label): 0부터 9까지의 정수 (총 10개 클래스)

2. MLP(다층 퍼셉트론)의 이미지 처리 방식¶

우리는 지금까지 배운 MLP(Multi-Layer Perceptron)를 사용하여 이 숫자를 맞춰볼 것입니다. 하지만 여기서 아주 중요한 과정 하나가 필요합니다.

데이터의 평탄화 (Flattening)¶

MLP는 1차원 배열 형태의 입력만 받을 수 있습니다. 하지만 MNIST 이미지는 $28 \times 28$ 크기의 2차원 행렬입니다. 따라서 이 행렬을 한 줄로 길게 이어 붙이는 작업이 필요합니다.

  • 입력 데이터 변환: $28 \times 28 = 784$
  • 결과적으로 각 이미지는 784개의 특징(Feature)을 가진 하나의 벡터가 되어 신경망에 입력됩니다.

3. Step 5의 핵심 질문: "공간 정보의 손실"¶

이 방식은 단순하고 강력하지만 한 가지 치명적인 약점이 있습니다. 이미지를 1차원으로 펼치는 순간, 픽셀 간의 상하좌우 관계(공간 정보)가 완전히 파괴된다는 점입니다.

Note: "숫자 '8'의 위아래 동그라미가 붙어 있다"라는 공간적 특징을 MLP는 이해하지 못하고, 단지 784개의 독립된 픽셀 값이 얼마나 밝은지만을 보고 판단하게 됩니다. 이 한계를 극복하는 방법은 이후 Part 2 - Step 6(CNN)에서 다루게 됩니다.

In [ ]:
import setup_env
✅ GPU 활성화: NVIDIA GeForce RTX 5070 Ti
✅ 전체 VRAM 용량: 15.92 GB
✅ CUDA 버전: 13.0
✅ PyTorch 버전: 2.9.0a0+145a3a7bda.nv25.10
--------------------------------------------------

2. 데이터 로드 및 전처리 (Data Loading & Preprocessing)¶

모델이 학습을 시작하기 전, '문제지'인 데이터를 준비하는 과정입니다. 파이토치(PyTorch)의 torchvision 라이브러리를 사용하면 복잡한 데이터 로딩 과정을 아주 간단하게 처리할 수 있습니다.


🌐 1) 데이터는 어디서 오나요? (datasets.MNIST)¶

torchvision.datasets.MNIST 함수는 얀 르쿤(Yann LeCun) 교수가 제공하는 MNIST 원본 서버에 접속하여 데이터를 내려받습니다.

  • root='./data': 데이터를 저장할 폴더 위치입니다. 현재 디렉토리에 data 폴더가 생성됩니다.
  • download=True: 지정된 경로에 데이터가 없으면 인터넷에서 자동으로 다운로드합니다.
  • 학습용 vs 테스트용: train=True는 모델 학습을 위한 6만 장의 데이터를, False는 학습 결과 검증을 위한 1만 장의 데이터를 가져옵니다.

🛠️ 2) 모델이 먹기 좋게 가공하기 (Transforms)¶

컴퓨터는 날것의 이미지(0~255 픽셀 값)보다 정제된 수치를 더 잘 학습합니다.

  • ToTensor(): 0~255 사이의 정수 값을 가진 이미지를 0.0~1.0 사이의 실수 값을 가진 '파이토치 텐서'로 변환합니다.
  • Normalize((0.1307,), (0.3081,)): MNIST 데이터 전체의 평균(0.1307)과 표준편차(0.3081)를 사용하여 데이터를 표준화합니다. 이는 모델이 특정 픽셀에 치우치지 않고 가중치를 고르게 학습하도록 돕습니다.

📦 3) 효율적인 배달 시스템 (DataLoader)¶

6만 장의 데이터를 한꺼번에 GPU에 넣으면 메모리가 버티지 못합니다. 그래서 DataLoader라는 배달 트럭이 필요합니다.

  • batch_size=64: 데이터를 64개씩 묶어서 전달합니다. RTX와 같은 GPU는 여러 작업을 동시에 처리(병렬 연산)하므로 이렇게 묶어서 처리하는 것이 훨씬 빠릅니다.
  • shuffle=True: 학습할 때마다 데이터의 순서를 섞어줍니다. 모델이 문제의 순서를 외우는 '편향'을 방지하기 위해 필수적입니다.
In [2]:
# ==========================================
# 2. 데이터 로드 및 전처리 (Normalization)
# ==========================================
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))
])

# MNIST 데이터셋 다운로드
train_dataset = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = torchvision.datasets.MNIST(root='./data', train=False, download=True, transform=transform)

# DataLoader 설정
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=64, shuffle=False)

print(f"✅ 데이터 로드 완료 (학습: {len(train_dataset)}개, 테스트: {len(test_dataset)}개)")
✅ 데이터 로드 완료 (학습: 60000개, 테스트: 10000개)
In [3]:
import os

def list_files(startpath):
    print(f"📁 대상 경로: {os.path.abspath(startpath)}")
    for root, dirs, files in os.walk(startpath):
        level = root.replace(startpath, '').count(os.sep)
        indent = ' ' * 4 * (level)
        print(f"{indent}📂 {os.path.basename(root)}/")
        subindent = ' ' * 4 * (level + 1)
        for f in files:
            print(f"{subindent}📜 {f}")

# MNIST 데이터가 저장된 경로 확인
list_files('./data')
📁 대상 경로: /workspace/ai-deeplearning/tutorial/data
📂 data/
    📂 MNIST/
        📂 raw/
            📜 t10k-images-idx3-ubyte
            📜 t10k-images-idx3-ubyte.gz
            📜 t10k-labels-idx1-ubyte
            📜 t10k-labels-idx1-ubyte.gz
            📜 train-images-idx3-ubyte
            📜 train-images-idx3-ubyte.gz
            📜 train-labels-idx1-ubyte
            📜 train-labels-idx1-ubyte.gz
In [4]:
# 1번째(인덱스 0) 데이터 세트 가져오기
# train_dataset[0]은 (image_tensor, label_integer)를 반환합니다.
image, label = train_dataset[0]

print(f"✅ 데이터 타입: {type(image)}")       # <class 'torch.Tensor'>
print(f"✅ 이미지 모양: {image.shape}")       # torch.Size([1, 28, 28])
print(f"✅ 이 이미지의 정답(Label): {label}")  # 5 (MNIST의 첫 번째 데이터는 5입니다)

# 시각화 (Matplotlib)
import matplotlib.pyplot as plt

plt.imshow(image.squeeze(), cmap='gray')      # squeeze()로 [1, 28, 28] -> [28, 28] 변환
plt.title(f"Label (Ground Truth): {label}")
plt.show()
✅ 데이터 타입: <class 'torch.Tensor'>
✅ 이미지 모양: torch.Size([1, 28, 28])
✅ 이 이미지의 정답(Label): 5
No description has been provided for this image

🔧 데이터 전처리: Normalization (정규화)¶

📊 왜 정규화가 필요한가?¶

원본 MNIST 이미지는 픽셀값이 0~255 범위를 가집니다. 이를 그대로 사용하면:

  1. 학습 불안정: 큰 값으로 인해 gradient가 폭발하거나 소실될 수 있음
  2. 느린 수렴: 최적화 알고리즘이 최적점을 찾는데 오래 걸림
  3. 활성화 함수 포화: ReLU, Sigmoid 등이 제대로 작동하지 않을 수 있음

🧮 정규화 수식¶

MNIST의 경우 다음과 같이 정규화합니다:

$$ X_{\text{normalized}} = \frac{X_{\text{raw}} - \mu}{\sigma} $$

여기서:

  • $X_{\text{raw}}$: 원본 픽셀값 (0~255를 ToTensor()로 0~1로 변환한 값)
  • $\mu = 0.1307$: MNIST 전체 데이터셋의 평균
  • $\sigma = 0.3081$: MNIST 전체 데이터셋의 표준편차

📈 변환 과정¶

원본 픽셀 (0~255) 
    ↓ ToTensor()
범위 (0~1)
    ↓ Normalize(mean=0.1307, std=0.3081)
정규화된 값 (평균≈0, 표준편차≈1)

🎯 정규화 효과¶

  • ✅ 평균 0, 표준편차 1에 가까운 분포로 변환
  • ✅ 학습 속도 향상 및 안정성 증가
  • ✅ 더 나은 일반화 성능
  • ✅ 활성화 함수의 효율적인 작동

💡 예시¶

픽셀값 128인 경우:

  1. ToTensor(): 128/255 ≈ 0.502
  2. Normalize(): (0.502 - 0.1307) / 0.3081 ≈ 1.205
In [5]:
import torch
from torchvision import datasets, transforms
import numpy as np
import sys

# 터미널/노트북에서 배열 생략 없이 전체를 보여주도록 설정
np.set_printoptions(threshold=sys.maxsize, linewidth=150)

# 1. 원본 데이터 로드 (전처리 없음)
dataset_raw = datasets.MNIST(root='./data', train=True, download=True)
img_raw, label = dataset_raw[0]
pixels_raw = np.array(img_raw)

# 2. 정규화 데이터 로드 (ToTensor + Normalize 적용)
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))
])
dataset_norm = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
tensor_norm, _ = dataset_norm[0]
pixels_norm = tensor_norm.squeeze().numpy()

print(f"--- [첫 번째 이미지 (Label: {label}) - Raw 0~255] ---")
print(pixels_raw)

print("\n" + "="*50 + "\n")

print(f"--- [첫 번째 이미지 (Label: {label}) - Normalized Float] ---")
# 가독성을 위해 소수점 둘째자리까지 반올림
print(np.round(pixels_norm, 2))
--- [첫 번째 이미지 (Label: 5) - Raw 0~255] ---
[[  0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0   0   0   3  18  18  18 126 136 175  26 166 255 247 127   0   0   0   0]
 [  0   0   0   0   0   0   0   0  30  36  94 154 170 253 253 253 253 253 225 172 253 242 195  64   0   0   0   0]
 [  0   0   0   0   0   0   0  49 238 253 253 253 253 253 253 253 253 251  93  82  82  56  39   0   0   0   0   0]
 [  0   0   0   0   0   0   0  18 219 253 253 253 253 253 198 182 247 241   0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0  80 156 107 253 253 205  11   0  43 154   0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0  14   1 154 253  90   0   0   0   0   0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0   0 139 253 190   2   0   0   0   0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0   0  11 190 253  70   0   0   0   0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0   0   0  35 241 225 160 108   1   0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0   0   0   0  81 240 253 253 119  25   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0   0   0   0   0  45 186 253 253 150  27   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0   0   0   0   0   0  16  93 252 253 187   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0 249 253 249  64   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0   0   0   0   0  46 130 183 253 253 207   2   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0   0   0  39 148 229 253 253 253 250 182   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0  24 114 221 253 253 253 253 201  78   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0  23  66 213 253 253 253 253 198  81   2   0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0  18 171 219 253 253 253 253 195  80   9   0   0   0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0  55 172 226 253 253 253 253 244 133  11   0   0   0   0   0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0 136 253 253 253 212 135 132  16   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0   0]]

==================================================

--- [첫 번째 이미지 (Label: 5) - Normalized Float] ---
[[-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.39 -0.2  -0.2  -0.2   1.18  1.31  1.8  -0.09  1.69  2.82  2.72  1.19
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.04  0.03  0.77  1.54  1.74  2.8   2.8   2.8   2.8   2.8   2.44  1.77  2.8   2.66  2.06  0.39
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42  0.2   2.61  2.8   2.8   2.8   2.8   2.8   2.8   2.8   2.8   2.77  0.76  0.62  0.62  0.29  0.07 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.2   2.36  2.8   2.8   2.8   2.8   2.8   2.1   1.89  2.72  2.64 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42  0.59  1.56  0.94  2.8   2.8   2.19 -0.28 -0.42  0.12  1.54 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.25 -0.41  1.54  2.8   0.72 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42  1.35  2.8   1.99 -0.4  -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.28  1.99  2.8   0.47 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42  0.02  2.64  2.44  1.61  0.95 -0.41 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42  0.61  2.63  2.8   2.8   1.09 -0.11 -0.42 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42  0.15  1.94  2.8   2.8   1.49 -0.08 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.22  0.76  2.78  2.8   1.96 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42  2.75  2.8   2.75  0.39 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42  0.16  1.23  1.91  2.8   2.8   2.21 -0.4  -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42  0.07  1.46  2.49  2.8   2.8   2.8   2.76  1.89 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.12  1.03  2.39  2.8   2.8   2.8   2.8   2.13  0.57 -0.42 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.13  0.42  2.29  2.8   2.8   2.8   2.8   2.1   0.61 -0.4  -0.42 -0.42 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.2   1.75  2.36  2.8   2.8   2.8   2.8   2.06  0.59 -0.31 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42  0.28  1.77  2.45  2.8   2.8   2.8   2.8   2.68  1.27 -0.28 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42  1.31  2.8   2.8   2.8   2.27  1.29  1.26 -0.22 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]
 [-0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42 -0.42
  -0.42 -0.42 -0.42 -0.42]]

🏗️ MLP 모델 정의¶

📐 MLP (Multi-Layer Perceptron)란?¶

다층 퍼셉트론은 가장 기본적인 신경망 구조로, 여러 층의 뉴런들이 완전히 연결된 구조입니다.

🧱 모델 구조¶

입력층 (784) → 은닉층1 (128) → 은닉층2 (64) → 출력층 (10)
    ↓              ↓                ↓              ↓
  28×28 평탄화    ReLU 활성화      ReLU 활성화    10개 클래스

🔢 각 층의 역할¶

1️⃣ 입력층 (Input Layer)¶

  • 크기: 784 (= 28 × 28 픽셀)
  • 역할: 2D 이미지(28×28)를 1D 벡터(784)로 평탄화
  • 수식: x.view(batch_size, -1) → (batch, 1, 28, 28) → (batch, 784)

2️⃣ 은닉층 1 (Hidden Layer 1)¶

  • 크기: 784 → 128
  • 활성화 함수: ReLU (Rectified Linear Unit)
  • 수식: $$h_1 = \text{ReLU}(W_1 \cdot x + b_1)$$ $$\text{ReLU}(z) = \max(0, z)$$
  • 역할: 입력의 차원을 축소하면서 중요한 특징 추출

3️⃣ 은닉층 2 (Hidden Layer 2)¶

  • 크기: 128 → 64
  • 활성화 함수: ReLU
  • 수식: $$h_2 = \text{ReLU}(W_2 \cdot h_1 + b_2)$$
  • 역할: 더 고차원적인 특징 학습 및 추상화

4️⃣ 출력층 (Output Layer)¶

  • 크기: 64 → 10
  • 활성화 함수: 없음 (CrossEntropyLoss가 내부적으로 Softmax 적용)
  • 수식: $$\text{output} = W_3 \cdot h_2 + b_3$$
  • 역할: 10개 클래스(0~9 숫자)에 대한 점수(logit) 출력

🎯 ReLU 활성화 함수¶

ReLU (Rectified Linear Unit)는 가장 널리 사용되는 활성화 함수입니다.

$$ \text{ReLU}(x) = \max(0, x) = \begin{cases} x & \text{if } x > 0 \\ 0 & \text{if } x \leq 0 \end{cases} $$

장점:¶

  • ✅ 계산 효율성: 단순한 max 연산
  • ✅ Gradient 소실 방지: 양수 영역에서 gradient가 1
  • ✅ 희소 활성화: 일부 뉴런만 활성화되어 효율적

단점:¶

  • ❌ Dying ReLU: 음수 입력에 대해 gradient가 0 (학습 중단 가능)

📊 파라미터 수 계산¶

  • Layer 1: $(784 \times 128) + 128 = 100,480$ 개
  • Layer 2: $(128 \times 64) + 64 = 8,256$ 개
  • Layer 3: $(64 \times 10) + 10 = 650$ 개
  • 총합: $109,386$ 개의 학습 가능한 파라미터
In [6]:
import torch.nn as nn
import torch.nn.functional as F

class MLP(nn.Module):
    def __init__(self, input_size=784, hidden_size=128, num_classes=10):
        super(MLP, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)   # 784 -> 128
        self.fc2 = nn.Linear(hidden_size, 64)            # 128 -> 64
        self.fc3 = nn.Linear(64, num_classes)            # 64 -> 10
        
    def forward(self, x):
        # 입력 이미지를 1차원 벡터로 변환 (batch_size, 28*28)
        x = x.view(x.size(0), -1)
        
        # Hidden Layer 1 + ReLU
        x = F.relu(self.fc1(x))
        
        # Hidden Layer 2 + ReLU
        x = F.relu(self.fc2(x))
        
        # Output Layer (활성화 함수 없음)
        x = self.fc3(x)
        return x

model = MLP()
print("✅ MLP 모델 생성 완료")
print(model)
✅ MLP 모델 생성 완료
MLP(
  (fc1): Linear(in_features=784, out_features=128, bias=True)
  (fc2): Linear(in_features=128, out_features=64, bias=True)
  (fc3): Linear(in_features=64, out_features=10, bias=True)
)

🎯 손실 함수 및 옵티마이저¶

📉 손실 함수: CrossEntropyLoss¶

교차 엔트로피 손실은 다중 클래스 분류에서 가장 많이 사용되는 손실 함수입니다.

🧮 수식¶

$$ \text{CrossEntropyLoss} = -\sum_{i=1}^{C} y_i \log(\hat{y}_i) $$

여기서:

  • $C$: 클래스 개수 (MNIST는 10)
  • $y_i$: 실제 레이블 (one-hot encoding)
  • $\hat{y}_i$: 예측 확률 (softmax 적용 후)

📊 내부 동작¶

CrossEntropyLoss는 내부적으로 다음 두 단계를 수행합니다:

  1. Softmax: 로짓(logit)을 확률로 변환 $$\text{Softmax}(z_i) = \frac{e^{z_i}}{\sum_{j=1}^{C} e^{z_j}}$$

  2. Negative Log-Likelihood: 음의 로그 가능도 계산 $$\text{NLL}(y, \hat{y}) = -\log(\hat{y}_{\text{true class}})$$

💡 예시¶

정답이 "5"이고, 모델의 출력이 다음과 같다면:

Logits:    [0.1, 0.3, 0.2, 0.1, 0.5, 2.1, 0.4, 0.2, 0.3, 0.1]
           [  0,   1,   2,   3,   4,   5,   6,   7,   8,   9 ]
  1. Softmax 적용 → 확률값으로 변환
  2. 정답 클래스(5)의 확률에 -log 적용
  3. Loss = -log(0.65) ≈ 0.43

⚡ 옵티마이저: Adam¶

Adam (Adaptive Moment Estimation)은 가장 인기있는 최적화 알고리즘입니다.

🧮 핵심 아이디어¶

Adam은 다음 두 가지를 결합합니다:

  1. Momentum: 과거 gradient의 이동 평균 사용 $$m_t = \beta_1 m_{t-1} + (1-\beta_1) g_t$$

  2. RMSprop: Gradient의 제곱에 대한 이동 평균 사용 $$v_t = \beta_2 v_{t-1} + (1-\beta_2) g_t^2$$

  3. 파라미터 업데이트: $$\theta_{t+1} = \theta_t - \frac{\alpha}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t$$

여기서:

  • $\alpha$: 학습률 (learning rate) = 0.001
  • $\beta_1$: momentum 계수 = 0.9 (기본값)
  • $\beta_2$: RMSprop 계수 = 0.999 (기본값)
  • $\epsilon$: 0으로 나누기 방지 = 1e-8

✅ Adam의 장점¶

  • 적응형 학습률: 각 파라미터마다 다른 학습률 적용
  • 빠른 수렴: Momentum과 RMSprop의 장점 결합
  • 하이퍼파라미터 튜닝 불필요: 기본값이 대부분의 경우 잘 작동

5.4 손실 함수, 최적화 및 학습 루프¶

모델이 설계되었으니, 이제 '어떻게 공부할지'를 정해야 합니다.

1. 손실 함수 (Loss Function)¶

  • CrossEntropyLoss: 다중 클래스 분류 문제(0~9 숫자 맞추기)에서 가장 표준적으로 사용되는 함수입니다. 모델의 예측값과 실제 정답 사이의 오차를 계산합니다.

2. 최적화 도구 (Optimizer)¶

  • Adam: 학습률을 자동으로 조절하며 빠르게 수렴하는 효율적인 최적화 알고리즘입니다.

3. 학습 루프 (Training Loop)¶

  • model.train(): 모델을 학습 모드로 설정합니다.
  • zero_grad(): 이전 단계의 기울기를 초기화합니다.
  • backward(): 오차 역전파를 통해 기울기를 계산합니다.
  • step(): 계산된 기울기를 바탕으로 가중치($W$)를 업데이트합니다.

🏋️ 학습 (Training)¶

🔄 학습 과정 (Epoch 개념)¶

1 Epoch = 전체 학습 데이터를 한 번 모두 학습하는 것

1 Epoch = 60,000개 샘플 / 64개(배치 크기) = 937.5 ≈ 938 배치
10 Epochs = 총 9,380 배치 반복

📊 학습 루프 (Training Loop)¶

학습은 다음 4단계를 반복합니다:

1️⃣ Forward Pass (순전파)¶

In [7]:
import torch.optim as optim

# 1. 히스토리를 저장할 리스트 초기화
train_loss_history = []
train_acc_history = []

# 2. 손실 함수 및 최적화 설정
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

epochs = 30
model.to(device)

print(f"🚀 학습 시작 (기록 기능 활성화)")

for epoch in range(epochs):
    model.train()
    running_loss = 0.0
    correct = 0
    total = 0
    
    for images, labels in train_loader:
        images, labels = images.to(device), labels.to(device)
        
        # 순전파 및 가중치 업데이트
        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        
        # 통계 기록
        running_loss += loss.item()
        
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
    
    # 에포크 종료 후 평균값 저장
    epoch_loss = running_loss / len(train_loader)
    epoch_acc = 100 * correct / total
    
    train_loss_history.append(epoch_loss)
    train_acc_history.append(epoch_acc)
    
    print(f"Epoch [{epoch+1}/{epochs}] - Loss: {epoch_loss:.4f}, Acc: {epoch_acc:.2f}%")

print("✨ 모든 학습 및 기록 완료!")
🚀 학습 시작 (기록 기능 활성화)
Epoch [1/30] - Loss: 0.2709, Acc: 92.03%
Epoch [2/30] - Loss: 0.1137, Acc: 96.52%
Epoch [3/30] - Loss: 0.0792, Acc: 97.54%
Epoch [4/30] - Loss: 0.0609, Acc: 98.08%
Epoch [5/30] - Loss: 0.0492, Acc: 98.43%
Epoch [6/30] - Loss: 0.0413, Acc: 98.67%
Epoch [7/30] - Loss: 0.0374, Acc: 98.80%
Epoch [8/30] - Loss: 0.0291, Acc: 99.02%
Epoch [9/30] - Loss: 0.0239, Acc: 99.22%
Epoch [10/30] - Loss: 0.0226, Acc: 99.24%
Epoch [11/30] - Loss: 0.0205, Acc: 99.34%
Epoch [12/30] - Loss: 0.0188, Acc: 99.35%
Epoch [13/30] - Loss: 0.0210, Acc: 99.33%
Epoch [14/30] - Loss: 0.0162, Acc: 99.44%
Epoch [15/30] - Loss: 0.0128, Acc: 99.57%
Epoch [16/30] - Loss: 0.0157, Acc: 99.48%
Epoch [17/30] - Loss: 0.0154, Acc: 99.47%
Epoch [18/30] - Loss: 0.0148, Acc: 99.47%
Epoch [19/30] - Loss: 0.0120, Acc: 99.59%
Epoch [20/30] - Loss: 0.0103, Acc: 99.67%
Epoch [21/30] - Loss: 0.0128, Acc: 99.57%
Epoch [22/30] - Loss: 0.0123, Acc: 99.61%
Epoch [23/30] - Loss: 0.0112, Acc: 99.63%
Epoch [24/30] - Loss: 0.0108, Acc: 99.68%
Epoch [25/30] - Loss: 0.0127, Acc: 99.60%
Epoch [26/30] - Loss: 0.0101, Acc: 99.64%
Epoch [27/30] - Loss: 0.0065, Acc: 99.78%
Epoch [28/30] - Loss: 0.0143, Acc: 99.59%
Epoch [29/30] - Loss: 0.0103, Acc: 99.69%
Epoch [30/30] - Loss: 0.0074, Acc: 99.77%
✨ 모든 학습 및 기록 완료!

5.5 모델 평가 (Model Evaluation)¶

학습에 사용하지 않은 테스트 데이터(10,000개)를 사용하여 모델의 실제 성능을 측정합니다.

1. 평가 모드 (model.eval())¶

  • 모델을 평가 모드로 전환합니다. 드롭아웃(Dropout)이나 배치 정규화(Batch Normalization) 같은 기능이 학습 때와 다르게 동작하도록 설정합니다.

2. 기울기 계산 비활성화 (torch.no_grad())¶

  • 평가 시에는 가중치를 업데이트할 필요가 없으므로 기울기를 계산하지 않습니다. 이는 메모리를 절약하고 연산 속도를 높여줍니다.

3. 정확도(Accuracy) 계산¶

  • 모델이 출력한 10개의 숫자 점수 중 가장 높은 값의 인덱스를 예측값으로 선택하고, 실제 정답과 비교합니다.

$$\text{Accuracy} = \frac{\text{Correct Predictions}}{\text{Total Predictions}} \times 100$$

In [8]:
# 1. 모델을 평가 모드로 전환
model.eval()

correct = 0
total = 0

# 2. 기울기 계산 비활성화 (메모리 절약 및 속도 향상)
with torch.no_grad():
    for images, labels in test_loader:
        # 데이터를 GPU로 전송
        images, labels = images.to(device), labels.to(device)
        
        # 순전파 연산
        outputs = model(images)
        
        # 가장 높은 점수를 받은 인덱스 선택 (예측값)
        _, predicted = torch.max(outputs.data, 1)
        
        # 전체 개수 및 맞은 개수 카운트
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

# 3. 최종 정확도 출력
accuracy = 100 * correct / total
print(f"🎯 테스트 데이터 최종 정확도: {accuracy:.2f}%")
🎯 테스트 데이터 최종 정확도: 98.00%
In [9]:
import matplotlib.pyplot as plt

# 1. 테스트 데이터에서 샘플 추출 및 예측
model.eval()
images, labels = next(iter(test_loader))
images, labels = images.to(device), labels.to(device)

outputs = model(images)
_, preds = torch.max(outputs, 1)

# CPU로 다시 옮겨서 시각화 준비
images = images.cpu().numpy()
labels = labels.cpu().numpy()
preds = preds.cpu().numpy()

# 2. 결과 시각화 (너비 100% 스타일)
plt.figure(figsize=(15, 4)) # 가로로 길게 설정

for idx in range(10): # 앞의 10개만 출력
    plt.subplot(1, 10, idx + 1)
    plt.imshow(images[idx].squeeze(), cmap='gray')
    
    # 정답 여부에 따라 타이틀 색상 변경 (맞으면 파랑, 틀리면 빨강)
    color = 'blue' if preds[idx] == labels[idx] else 'red'
    plt.title(f"P: {preds[idx]}\n(A: {labels[idx]})", color=color)
    plt.axis('off')

plt.tight_layout()
plt.show()
No description has been provided for this image
In [10]:
import matplotlib.pyplot as plt

# 1. 가로로 넓은 캔버스 설정 (18x5 인치)
plt.figure(figsize=(18, 5))

# --- [왼쪽: Loss 그래프] ---
plt.subplot(1, 2, 1)
plt.plot(range(1, epochs + 1), train_loss_history, marker='o', color='royalblue', linewidth=2, label='Train Loss')
plt.title('Training Loss Trend', fontsize=15, pad=15)
plt.xlabel('Epoch', fontsize=12)
plt.ylabel('Loss Value', fontsize=12)
plt.xticks(range(1, epochs + 1))
plt.grid(True, linestyle='--', alpha=0.7)
plt.legend(fontsize=12)

# --- [오른쪽: Accuracy 그래프] ---
plt.subplot(1, 2, 2)
plt.plot(range(1, epochs + 1), train_acc_history, marker='s', color='darkorange', linewidth=2, label='Train Accuracy')
plt.title('Training Accuracy Trend', fontsize=15, pad=15)
plt.xlabel('Epoch', fontsize=12)
plt.ylabel('Accuracy (%)', fontsize=12)
plt.xticks(range(1, epochs + 1))
plt.grid(True, linestyle='--', alpha=0.7)
plt.legend(fontsize=12)

# 레이아웃 정렬 및 출력
plt.tight_layout()
plt.show()
No description has been provided for this image