Sigmoid 함수의 Vanishing Gradient 문제를 ReLU로 해결하고,
실제 MNIST 손글씨 데이터셋으로 숫자를 분류하는 모델을 구현해보겠습니다.
전체 코드
import tensorflow as tf
import numpy as np
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.datasets import mnist
def load_mnist():
(train_data, train_labels), (test_data, test_labels) = mnist.load_data()
train_data = np.reshape(train_data, [-1, 28, 28, 1])
test_data = np.expand_dims(test_data, axis=-1)
train_data, test_data = normalize(train_data, test_data)
train_labels = to_categorical(train_labels, 10)
test_labels = to_categorical(test_labels, 10)
return train_data, train_labels, test_data, test_labels
def normalize(train_data, test_data):
train_data = train_data.astype(np.float32) / 255.0
test_data = test_data.astype(np.float32) / 255.0
return train_data, test_data
def flatten():
return tf.keras.layers.Flatten()
def dense(channel, weight_init):
return tf.keras.layers.Dense(units=channel, use_bias=True, kernel_initializer=weight_init)
def relu():
return tf.keras.layers.Activation(tf.keras.activations.relu)
class create_model(tf.keras.Model):
def __init__(self, label_dim):
super(create_model, self).__init__()
weight_init = tf.keras.initializers.RandomNormal()
self.model = tf.keras.Sequential()
self.model.add(flatten())
for i in range(2):
self.model.add(dense(256, weight_init))
self.model.add(relu())
self.model.add(dense(label_dim, weight_init))
def call(self, x, training=None, mask=None):
x = self.model(x)
return x
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
def accuracy_fn(model, images, labels):
logits = model(images, training=True)
predicted = tf.equal(tf.argmax(logits, -1), tf.argmax(labels, -1))
accuracy = tf.reduce_mean(tf.cast(predicted, dtype=tf.float32))
return accuracy
def grad(model, images, labels):
with tf.GradientTape() as tape:
loss = loss_fn(model, images, labels)
return tape.gradient(loss, model.trainable_variables)
train_x, train_y, test_x, test_y = load_mnist()
learning_rate = 0.001
batch_size = 128
training_epochs = 1
training_iterations = len(train_x) // batch_size
label_dim = 10
train_dataset = tf.data.Dataset.from_tensor_slices((train_x, train_y)) \
.shuffle(buffer_size=100000).batch(batch_size).prefetch(buffer_size=batch_size)
test_dataset = tf.data.Dataset.from_tensor_slices((test_x, test_y)).batch(batch_size)
network = create_model(label_dim)
optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)
checkpoint = tf.train.Checkpoint(dnn=network)
for epoch in range(training_epochs):
for step, (train_input, train_label) in enumerate(train_dataset):
grads = grad(network, train_input, train_label)
optimizer.apply_gradients(zip(grads, network.trainable_variables))
train_loss = loss_fn(network, train_input, train_label)
train_accuracy = accuracy_fn(network, train_input, train_label)
for test_input, test_label in test_dataset.take(1):
test_accuracy = accuracy_fn(network, test_input, test_label)
print(f"Epoch: {epoch}, Step: {step}, Loss: {train_loss:.4f}, Train Acc: {train_accuracy:.4f}, Test Acc: {test_accuracy:.4f}")
1. MNIST 데이터셋
MNIST는 0~9까지의 손글씨 숫자 이미지 데이터셋입니다.
60,000개의 학습 데이터와 10,000개의 테스트 데이터로 구성되어 있습니다.
(train_data, train_labels), (test_data, test_labels) = mnist.load_data()
각 이미지는 28×28 픽셀 크기입니다.
2. 데이터 전처리
Shape 변환
train_data = np.reshape(train_data, [-1, 28, 28, 1])
test_data = np.expand_dims(test_data, axis=-1)
TensorFlow가 이미지를 받는 shape는 [batch_size, height, width, channel] 입니다.
MNIST는 흑백(그레이스케일) 이미지이기 때문에 채널이 1입니다. (컬러는 RGB로 3)
원래 shape가 [N, 28, 28] 이기 때문에 채널 차원을 추가해 [N, 28, 28, 1] 로 만들어줍니다.
두 가지 방법이 있습니다:
- np.reshape : 전체 구조를 강제로 재배열 (데이터 순서가 꼬일 수 있음)
- np.expand_dims : 차원 하나만 추가 (더 안전, 일반적으로 더 많이 사용)
정규화 (Normalization)
train_data = train_data.astype(np.float32) / 255.0
test_data = test_data.astype(np.float32) / 255.0
이미지 픽셀값은 0~255 사이의 값인데, 이것을 0~1 사이로 정규화해야 합니다.
데이터의 범위가 너무 크면 학습이 잘 되지 않기 때문에 255로 나눠서 스케일을 맞춰줍니다.
One-Hot Encoding
train_labels = to_categorical(train_labels, 10) # [N,] → [N, 10]
test_labels = to_categorical(test_labels, 10)
정답 레이블(0~9)을 One-Hot 형태로 변환합니다.
Loss 함수를 계산할 때 정수값보다 One-Hot 형태가 훨씬 편리하기 때문입니다.
7 → [0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
3 → [0, 0, 0, 1, 0, 0, 0, 0, 0, 0]
3. 네트워크 구성 함수
def flatten():
return tf.keras.layers.Flatten()
def dense(channel, weight_init):
return tf.keras.layers.Dense(units=channel, use_bias=True, kernel_initializer=weight_init)
def relu():
return tf.keras.layers.Activation(tf.keras.activations.relu)
네트워크를 짤 때 어떤 함수를 사용할지 먼저 정의합니다.
- Flatten : shape를 펼쳐주는 역할 [N, 28, 28, 1] → [N, 784]
- Dense : Fully Connected Layer (모든 뉴런이 연결된 레이어)
- ReLU : Activation Function f(x) = max(0, x)
Sigmoid 대신 ReLU를 사용하는 이유는 Sigmoid의 Gradient가 항상 0~0.25로 작아서 네트워크가 깊어질수록 Vanishing Gradient가 발생하기 때문입니다.
ReLU는 양수 구간에서 Gradient = 1 이기 때문에 깊은 네트워크에서도 Gradient가 잘 전달됩니다.
4. 모델 클래스
class create_model(tf.keras.Model):
def __init__(self, label_dim):
super(create_model, self).__init__()
weight_init = tf.keras.initializers.RandomNormal()
self.model = tf.keras.Sequential()
self.model.add(flatten())
for i in range(2):
self.model.add(dense(256, weight_init))
self.model.add(relu())
self.model.add(dense(label_dim, weight_init))
def call(self, x, training=None, mask=None):
x = self.model(x)
return x
tf.keras.Model 을 상속받아 모델 클래스를 만듭니다.
- weight_init = tf.keras.initializers.RandomNormal() : 평균 0, 분산 1인 가우시안 분포로 W를 랜덤하게 초기화합니다
- tf.keras.Sequential() : 레이어를 순서대로 쌓아나가는 리스트 자료구조 같은 것입니다
모델의 레이어 구조:
[N, 28, 28, 1]
↓ Flatten
[N, 784]
↓ Dense(256) + ReLU
[N, 256]
↓ Dense(256) + ReLU
[N, 256]
↓ Dense(10)
[N, 10] ← 숫자 0~9 각각의 점수(logit)
call 함수는 모델을 호출했을 때 실행되는 함수입니다.
5. Gradient와 Backpropagation
def grad(model, images, labels):
with tf.GradientTape() as tape:
loss = loss_fn(model, images, labels)
return tape.gradient(loss, model.trainable_variables)
여기서 자주 헷갈리는 두 개념을 정리하면:
Backpropagation 은 Gradient를 계산하는 방법입니다.
Gradient Descent 는 계산된 Gradient를 사용해서 W를 업데이트하는 방법입니다.
1. Forward
Input → Network → Output → Loss 계산
2. Backpropagation ← tape.gradient()
Loss를 미분 → 각 W의 Gradient 계산 (Chain Rule)
3. Gradient Descent ← optimizer.apply_gradients()
W := W - α * Gradient
tape.gradient 가 Backpropagation을 수행하고,
optimizer.apply_gradients 가 Gradient Descent로 W를 업데이트합니다.
이전 코드에서는 W.assign_sub(learning_rate * W_grad) 로 직접 업데이트했는데,
optimizer.apply_gradients 가 이 과정을 자동으로 처리해주는 것입니다.
6. 데이터셋 파이프라인
train_dataset = tf.data.Dataset.from_tensor_slices((train_x, train_y)) \
.shuffle(buffer_size=100000) \
.batch(batch_size) \
.prefetch(buffer_size=batch_size)
60,000개의 데이터를 한 번에 메모리에 올리면 부담이 크기 때문에 batch_size(128개)씩 쪼개서 네트워크에 넣습니다.
- shuffle(buffer_size) : 데이터를 섞습니다. buffer_size는 데이터셋 크기보다 크면 됩니다
- batch(128) : 128개씩 묶어서 네트워크에 넣습니다
- prefetch(buffer_size) : 현재 배치를 학습하는 동안 다음 배치를 미리 메모리에 올려둬서 속도를 높입니다
training_iterations = len(train_x) // batch_size
# 60,000 // 128 = 468
# 전체 데이터를 128개씩 나누면 468번의 스텝이 필요
7. Checkpoint
checkpoint = tf.train.Checkpoint(dnn=network)
Checkpoint는 두 가지 역할을 합니다:
- 학습 중간에 끊겼을 때 저장된 Weight를 불러와 이어서 학습 가능
- 학습이 끝난 후 테스트 이미지에 대한 정확도 확인 시 저장된 모델 불러오기 가능
8. Adam Optimizer
optimizer = tf.keras.optimizers.Adam(learning_rate=0.001)
이전 코드에서는 SGD(확률적 경사 하강법)를 사용했는데, 이번엔 Adam 을 사용합니다.
Adam은 SGD보다 대부분의 경우 더 빠르고 안정적으로 수렴합니다.
학습률을 자동으로 조절하는 기능이 있어 실무에서 가장 많이 사용되는 옵티마이저입니다.
일반적으로 learning_rate = 0.001 또는 0.0003 을 사용합니다.
학습 결과
Epoch: 0, Step: 0, Loss: 2.1713, Train Acc: 0.4453, Test Acc: 0.2891
Epoch: 0, Step: 10, Loss: 1.2406, Train Acc: 0.7969, Test Acc: 0.7500
Epoch: 0, Step: 50, Loss: 0.3108, Train Acc: 0.9297, Test Acc: 0.8672
Epoch: 0, Step: 100, Loss: 0.3457, Train Acc: 0.8906, Test Acc: 0.9375
Epoch: 0, Step: 200, Loss: 0.1797, Train Acc: 0.9531, Test Acc: 0.9766
Epoch: 0, Step: 300, Loss: 0.1506, Train Acc: 0.9375, Test Acc: 0.9844
Epoch: 0, Step: 400, Loss: 0.1634, Train Acc: 0.9609, Test Acc: 0.9844
Epoch: 0, Step: 468, Loss: 0.0909, Train Acc: 0.9688, Test Acc: 0.9844
단 1 Epoch(전체 데이터를 한 번 학습) 만에 약 98%의 정확도를 달성했습니다.
전체 흐름 정리
MNIST 이미지 로드 (28×28, 흑백)
↓
Shape 변환 : [N, 28, 28] → [N, 28, 28, 1]
↓
정규화 : 0~255 → 0~1
↓
One-Hot Encoding : 7 → [0,0,0,0,0,0,0,1,0,0]
↓
Flatten : [N, 28, 28, 1] → [N, 784]
↓
Dense(256) + ReLU → Dense(256) + ReLU → Dense(10)
↓
Softmax Cross-Entropy Loss 계산
↓
GradientTape → Backpropagation → Gradient 계산
↓
Adam optimizer → Gradient Descent → W 업데이트
↓
1 Epoch 학습 → Test Accuracy ~98% ✅
정리
개념 설명 코드
| Flatten | 이미지를 1차원으로 펼침 | tf.keras.layers.Flatten() |
| Dense | Fully Connected Layer | tf.keras.layers.Dense(256) |
| ReLU | Vanishing Gradient 해결 | tf.keras.activations.relu |
| normalize | 0~255 → 0~1 정규화 | data / 255.0 |
| prefetch | 다음 배치 미리 메모리에 올림 | .prefetch(buffer_size) |
| Checkpoint | 학습 중단 시 모델 저장/복원 | tf.train.Checkpoint(dnn=network) |
| Adam | 학습률 자동 조절 옵티마이저 | tf.keras.optimizers.Adam(0.001) |
| Backpropagation | Gradient를 계산하는 방법 | tape.gradient(loss, variables) |
| Gradient Descent | Gradient로 W를 업데이트하는 방법 | optimizer.apply_gradients(...) |
ReLU와 Adam 옵티마이저를 사용해 단 1 Epoch만에 98%의 정확도를 달성했습니다. 🚀
'AI > ML' 카테고리의 다른 글
| Dropout : 과적합을 막는 정규화 기법 (0) | 2026.03.25 |
|---|---|
| Weight Initialization : 웨이트 초기화의 중요성 (0) | 2026.03.25 |
| ReLU : Vanishing Gradient 문제와 해결책 (0) | 2026.03.22 |
| XOR 문제 Neural Network로 구현하기 (TensorFlow) (0) | 2026.03.21 |
| Backpropagation : 역전파 알고리즘 (0) | 2026.03.21 |