이번엔 Convolutional Neural Network(CNN)를 사용해 MNIST 손글씨 숫자를 분류하는 모델을 구현해보겠습니다.
CNN 구현의 전체 흐름은 아래 9단계로 정리할 수 있습니다.
1. 하이퍼파라미터 설정
2. 데이터 파이프라인 구성
3. 모델 구성
4. Loss 함수 정의
5. Gradient 계산
6. Optimizer 선택
7. 성능 지표(정확도) 정의
8. Checkpoint 저장 (선택)
9. 학습 및 검증
전체 코드
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras.utils import to_categorical
import numpy as np
import os
# 1. 하이퍼파라미터 설정
learning_rate = 0.001
training_epochs = 15
batch_size = 100
# 2. 데이터 파이프라인
mnist = keras.datasets.mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images = train_images.astype(np.float32) / 255.
test_images = test_images.astype(np.float32) / 255.
train_images = np.expand_dims(train_images, axis=-1)
test_images = np.expand_dims(test_images, axis=-1)
train_labels = to_categorical(train_labels, 10)
test_labels = to_categorical(test_labels, 10)
train_dataset = tf.data.Dataset.from_tensor_slices((train_images, train_labels)) \
.shuffle(buffer_size=100000).batch(batch_size).prefetch(buffer_size=batch_size)
test_dataset = tf.data.Dataset.from_tensor_slices((test_images, test_labels)).batch(batch_size)
# 3. 모델 구성
def create_model():
model = keras.Sequential()
model.add(keras.layers.Conv2D(filters=32, kernel_size=3, activation=tf.nn.relu, padding='SAME', input_shape=(28, 28, 1)))
model.add(keras.layers.MaxPool2D(padding='same'))
model.add(keras.layers.Conv2D(filters=64, kernel_size=3, activation=tf.nn.relu, padding='SAME'))
model.add(keras.layers.MaxPool2D(padding='same'))
model.add(keras.layers.Conv2D(filters=128, kernel_size=3, activation=tf.nn.relu, padding='SAME'))
model.add(keras.layers.MaxPool2D(padding='same'))
model.add(keras.layers.Flatten())
model.add(keras.layers.Dense(256, activation=tf.nn.relu))
model.add(keras.layers.Dropout(0.4))
model.add(keras.layers.Dense(10))
return model
model = create_model()
model.summary()
# 4. Loss 함수
def loss_fn(model, images, labels):
logits = model(images, training=True)
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=labels))
return loss
# 5. Gradient 계산
def grad(model, images, labels):
with tf.GradientTape() as tape:
loss = loss_fn(model, images, labels)
return tape.gradient(loss, model.trainable_variables)
# 6. Optimizer / 7. 정확도 정의
optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)
def evaluate(model, images, labels):
logits = model(images, training=False)
correct_prediction = tf.equal(tf.argmax(logits, 1), tf.argmax(labels, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, dtype=tf.float32))
return accuracy
# 9. 학습
for epoch in range(training_epochs):
avg_loss = 0.
avg_train_acc = 0.
avg_test_acc = 0.
train_step = 0
test_step = 0
for images, labels in train_dataset:
grads = grad(model, images, labels)
optimizer.apply_gradients(zip(grads, model.trainable_variables))
loss = loss_fn(model, images, labels)
acc = evaluate(model, images, labels)
avg_loss += loss.numpy()
avg_train_acc += acc.numpy()
train_step += 1
avg_loss /= train_step
avg_train_acc /= train_step
for images, labels in test_dataset:
acc = evaluate(model, images, labels)
avg_test_acc += acc
test_step += 1
avg_test_acc /= test_step
print(f"Epoch: {epoch+1}, Loss: {avg_loss:.4f}, Train Acc: {avg_train_acc:.4f}, Test Acc: {avg_test_acc:.4f}")
1. 하이퍼파라미터 설정
learning_rate = 0.001
training_epochs = 15
batch_size = 100
모델 학습을 위한 설정값들입니다.
- learning_rate : Gradient를 W에 얼마나 반영할지 결정하는 값
- training_epochs : 전체 데이터를 몇 번 반복 학습할지
- batch_size : 한 번에 네트워크에 넣는 데이터 수
2. 데이터 파이프라인
train_images = train_images.astype(np.float32) / 255.
train_images = np.expand_dims(train_images, axis=-1) # [N,28,28] → [N,28,28,1]
train_labels = to_categorical(train_labels, 10) # 7 → [0,0,0,0,0,0,0,1,0,0]
- / 255. : 픽셀값 0~255를 0~1 사이로 정규화
- expand_dims : CNN이 받는 shape [N, H, W, C] 에 맞게 채널 차원 추가 (흑백이라 채널=1)
- to_categorical : 정답 레이블을 One-Hot Encoding으로 변환
3. 모델 구성 : CNN 레이어 쌓기
model.add(keras.layers.Conv2D(filters=32, kernel_size=3, activation=tf.nn.relu, padding='SAME', input_shape=(28,28,1)))
model.add(keras.layers.MaxPool2D(padding='same'))
각 레이어를 통과하면서 shape가 어떻게 변하는지 확인해보면:
입력 [N, 28, 28, 1]
Conv2D(32) → [N, 28, 28, 32] 파라미터: (3×3×1 + 1) × 32 = 320
MaxPool2D → [N, 14, 14, 32] 크기 절반으로 감소
Conv2D(64) → [N, 14, 14, 64] 파라미터: (3×3×32 + 1) × 64 = 18,496
MaxPool2D → [N, 7, 7, 64] 크기 절반으로 감소
Conv2D(128) → [N, 7, 7, 128]
MaxPool2D → [N, 4, 4, 128] (SAME 패딩으로 올림)
Flatten → [N, 2048]
Dense(256) → [N, 256]
Dropout(0.4)→ [N, 256] 40% 뉴런 OFF
Dense(10) → [N, 10] 최종 출력 (숫자 0~9)
Conv2D 파라미터 계산:
(kernel_size × kernel_size × 입력채널 + 1(bias)) × filters
(3 × 3 × 1 + 1) × 32 = 320
필터 개수가 늘어날수록(32→64→128) 더 복잡한 특징을 추출합니다.
Max Pooling을 거칠 때마다 크기가 절반으로 줄어들며 중요한 특징만 남습니다.
4 & 5. Loss 함수와 Gradient
def loss_fn(model, images, labels):
logits = model(images, training=True) # training=True → Dropout 적용
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=labels))
return loss
def grad(model, images, labels):
with tf.GradientTape() as tape:
loss = loss_fn(model, images, labels)
return tape.gradient(loss, model.trainable_variables)
- training=True : 학습 중이므로 Dropout 적용
- softmax_cross_entropy_with_logits : Softmax + Cross-Entropy를 한 번에 계산
- tape.gradient : Backpropagation으로 모든 W의 Gradient 계산
6 & 7. Optimizer와 정확도
optimizer = tf.keras.optimizers.Adam(learning_rate=0.001)
def evaluate(model, images, labels):
logits = model(images, training=False) # training=False → Dropout 미적용
correct_prediction = tf.equal(tf.argmax(logits, 1), tf.argmax(labels, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, dtype=tf.float32))
return accuracy
- Adam : 학습률을 자동으로 조절하는 옵티마이저로 실무에서 가장 많이 사용
- training=False : 테스트 시에는 Dropout을 끄고 모든 뉴런 사용
- tf.argmax(logits, 1) : 가장 높은 확률의 클래스 인덱스 반환
9. 학습 루프
for epoch in range(training_epochs):
avg_loss = 0.
avg_train_acc = 0.
train_step = 0
for images, labels in train_dataset:
grads = grad(model, images, labels) # Gradient 계산 (Backpropagation)
optimizer.apply_gradients(zip(grads, model.trainable_variables)) # W 업데이트 (Gradient Descent)
avg_loss += loss_fn(model, images, labels).numpy()
avg_train_acc += evaluate(model, images, labels).numpy()
train_step += 1
avg_loss /= train_step # 전체 스텝의 평균 Loss
avg_train_acc /= train_step # 전체 스텝의 평균 정확도
매 Epoch마다 Loss와 정확도를 누적해서 평균을 냅니다.
train_dataset을 한 번 다 돌면 1 Epoch가 완료됩니다.
전체 흐름 정리
MNIST 이미지 로드 (28×28, 흑백)
↓
정규화 (0~255 → 0~1) + One-Hot Encoding
↓
Conv2D(32) → ReLU → MaxPool [28×28×32 → 14×14×32]
↓
Conv2D(64) → ReLU → MaxPool [14×14×64 → 7×7×64]
↓
Conv2D(128) → ReLU → MaxPool [7×7×128 → 4×4×128]
↓
Flatten → Dense(256) → Dropout(0.4) → Dense(10)
↓
Softmax Cross-Entropy Loss
↓
GradientTape → Backpropagation
↓
Adam → Gradient Descent → W 업데이트
↓
15 Epoch 반복 → 높은 정확도 달성 ✅
정리
단계 개념 코드
| 데이터 | 정규화 + One-Hot + 채널 추가 | / 255., expand_dims, to_categorical |
| Conv2D | 필터로 특징 추출 | keras.layers.Conv2D(filters, kernel_size) |
| MaxPool2D | 크기 절반 축소 | keras.layers.MaxPool2D(padding='same') |
| Dropout | 40% 뉴런 OFF → 과적합 방지 | keras.layers.Dropout(0.4) |
| training=True/False | 학습/테스트 시 Dropout 제어 | model(images, training=True/False) |
| Backpropagation | Gradient 계산 | tape.gradient(loss, model.trainable_variables) |
| Gradient Descent | W 업데이트 | optimizer.apply_gradients(...) |
CNN은 Fully Connected Network보다 이미지 분류에서 훨씬 높은 성능을 보여줍니다. 🚀
'AI > ML' 카테고리의 다른 글
| RNN : 순서가 있는 데이터를 처리하는 신경망 (0) | 2026.04.02 |
|---|---|
| CNN : Convolutional Neural Network (0) | 2026.03.29 |
| Dropout : 과적합을 막는 정규화 기법 (0) | 2026.03.25 |
| Weight Initialization : 웨이트 초기화의 중요성 (0) | 2026.03.25 |
| ReLU로 MNIST 손글씨 분류하기 (TensorFlow) (0) | 2026.03.23 |