본문 바로가기
HRDI_AI/[AI특화] AI 기반 실시간 객체탐지 모델 구현

4. 합성곱 신경망 - (2) 손글씨 숫자 인식을 위한 CNN 구현

by Toddler_AD 2026. 1. 20.

2.1 손글씨 숫자 인식하기

  • 우편번호 자동 분류기처럼 손으로 쓴 숫자를 분류할 수 있도록 만드는 데 필요한 것들을 들면 아래와 같습니다.
    • 딥러닝 구현 알고리즘
    • 손으로 쓴 숫자 데이터
  • 이 문제를 해결하기 위해 앞에서 배운 인공신경망 딥러닝 모델을 사용할 수 있습니다. 그런데 손으로 쓴 숫자 데이터는 어디서 구할 수 있을까요?
  • 우리가 손으로 일일이 숫자를 쓰는 것은 너무 불편한 일입니다. 데이터를 준비하려면 0~9까지 숫자를 숫자마다 1,000개 이상 써야 하고 그 숫자들을 읽어 파일로 저장하여 전처리하는 이러한 일들은 너무 불편한 일입니다. 다행히도 준비된 손글씨 숫자 데이터셋이 있습니다.

2.2 MNIST 손글씨 숫자 데이터

  • 우편번호 자동 분류를 위한 손글씨 숫자 데이터를 직접 만들지 않고 이미 만들어진 데이터셋을 사용할 수 있습니다. 그것이 바로 MNIST 데이터베이스입니다. MNIST 데이터베이스는 손으로 쓴 숫자들의 이미지 데이터베이스입니다.
  • MNIST 데이터는 딥러닝을 배우는 사람이 가장 먼저 만나는 데이터 중의 하나입니다. 이 데이터는 60,000개 예가 들어있는 학습용 데이터셋(Training set)과 10,000개 예가 들어있는 테스트 데이터셋(Test set)이 있습니다. 이것은 MNIST에서 제공하는 더 큰 세트의 하위 집합입니다. 각 이미지는 28x28 크기이며, 이미 전처리 되어 있습니다. 숫자는 크기가 표준화되어 있고 고정된 크기의 이미지 가운데에 위치합니다.
  • MNIST 데이터는 http://yann.lecun.com/exdb/mnist/에서 내려받을 수 있습니다. 그러나 텐서플로우에는 학습자를 위한 데이터셋을 제공하므로 이 책의 예제를 위해서 데이터를 내려받지 않아도 됩니다.
  • 이 데이터는 "훈련용" 데이터셋과 "검증용" 데이터셋으로 구분되어 있습니다. 그리고 이것들은 각각 "이미지"와 "레이블(또는 라벨)"로 구분됩니다. 이미지는 28x28 크기 화소의 숫자 데이터를 가지고 있으며, 레이블은 그 이미지가 0~9중 어떤 숫자인지를 알려줍니다.


2.3 손글씨 숫자 인식을 위한 케라스 CNN 모델

  • 케라스를 이용해서 CNN 모델을 구현해 보겠습니다. 케라스로 CNN 모델을 만들기 위한 구조를 다음과 같이 정의했습니다.

  • 위의 모델을  코드로 옮기면 다음과 같습니다.
from tensorflow.keras import Sequential, layers
model = Sequential([
    layers.Input(shape=(28,28,1)),
    layers.Conv2D(32, 3, padding='same', activation='relu'),
    layers.MaxPooling2D(),
    layers.Dropout(0.1),
    layers.Conv2D(64, 3, padding='same', activation='relu'),
    layers.MaxPooling2D(),
    layers.Dropout(0.25),
    layers.Flatten(),
    layers.Dense(128, activation='relu'),
    layers.Dropout(0.5),
    layers.Dense(10, activation='softmax')
])
model.summary()
Model: "sequential"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃ Layer (type)                         ┃ Output Shape                ┃         Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ conv2d (Conv2D)                      │ (None, 28, 28, 32)          │             320 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ max_pooling2d (MaxPooling2D)         │ (None, 14, 14, 32)          │               0 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dropout (Dropout)                    │ (None, 14, 14, 32)          │               0 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ conv2d_1 (Conv2D)                    │ (None, 14, 14, 64)          │          18,496 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ max_pooling2d_1 (MaxPooling2D)       │ (None, 7, 7, 64)            │               0 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dropout_1 (Dropout)                  │ (None, 7, 7, 64)            │               0 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ flatten (Flatten)                    │ (None, 3136)                │               0 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dense (Dense)                        │ (None, 128)                 │         401,536 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dropout_2 (Dropout)                  │ (None, 128)                 │               0 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ dense_1 (Dense)                      │ (None, 10)                  │           1,290 │
└──────────────────────────────────────┴─────────────────────────────┴─────────────────┘
 Total params: 421,642 (1.61 MB)
 Trainable params: 421,642 (1.61 MB)
 Non-trainable params: 0 (0.00 B)
  • 이 모델의 가중치를 저장하는 파라미터수는 다음과 같습니다.
    • 합성곱 1 : 필터크기(3x3) * 뉴런(32) + 바이어스(32) = 320개
    • 합성곱 2 : 이전층뉴런(32개)*3*3*64 + 64 = 18,496개
    • 은닉층 : 3,136*128 + 128 = 401,536개
    • 출력층 : 128*10 + 10 = 1,290개
  • Trainable params는 가중치(Weights)와 편향(Biases)들의 개수를 의미하며, Non-trainable params(학습되지 않은 파라미터)는 합성곱필터의 크기나, 풀링 크기 등 모델의 구조적인 요소로 모델이 훈련 과정 중에 업데이트하지 않는 파라미터입니다. 그런데 케라스에서는 학습되지 않는 파라미터는 경사하강법을 사용하여 학습되지 않는 파라미터를 의미하고, trainable=False를 추가한 레이어의 가중치 및 편향과 배치정규화(BatchNormalization)을 사용했을 경우 저장되는 4개의 변수 [gamma weights, beta weights, moving_mean, moving_variance] 중 마지막 두 개가 학습되지 않는 변수에 해당합니다.
  • 다음 코드는 데이터를 불러오고 훈련을 정의해서 학습시킵니다.
from tensorflow.keras.datasets import mnist
(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train = X_train.reshape(-1, 28, 28, 1) / 255.0
X_test = X_test.reshape(-1, 28, 28, 1) / 255.0

model.compile(loss='sparse_categorical_crossentropy',
              optimizer='adam', metrics=['accuracy'])

hist = model.fit(X_train, y_train,
                 validation_data=(X_test, y_test),
                 batch_size=128, epochs=10)
                 
loss, accuracy = model.evaluate(X_test, y_test, verbose=0)
print(f'Loss: {loss:.6f}, Accuracy: {accuracy:.6f}')
Epoch 1/10
469/469 ━━━━━━━━━━━━━━━━━━━━ 17s 32ms/step - accuracy: 0.7914 - loss: 0.6373 - val_accuracy: 0.9798 - val_loss: 0.0595
Epoch 2/10
469/469 ━━━━━━━━━━━━━━━━━━━━ 17s 35ms/step - accuracy: 0.9643 - loss: 0.1153 - val_accuracy: 0.9861 - val_loss: 0.0410
Epoch 3/10
469/469 ━━━━━━━━━━━━━━━━━━━━ 15s 33ms/step - accuracy: 0.9770 - loss: 0.0761 - val_accuracy: 0.9885 - val_loss: 0.0368
Epoch 4/10
469/469 ━━━━━━━━━━━━━━━━━━━━ 15s 32ms/step - accuracy: 0.9796 - loss: 0.0659 - val_accuracy: 0.9894 - val_loss: 0.0333
Epoch 5/10
469/469 ━━━━━━━━━━━━━━━━━━━━ 15s 31ms/step - accuracy: 0.9829 - loss: 0.0575 - val_accuracy: 0.9904 - val_loss: 0.0280
Epoch 6/10
469/469 ━━━━━━━━━━━━━━━━━━━━ 15s 32ms/step - accuracy: 0.9847 - loss: 0.0515 - val_accuracy: 0.9916 - val_loss: 0.0269
Epoch 7/10
469/469 ━━━━━━━━━━━━━━━━━━━━ 15s 32ms/step - accuracy: 0.9861 - loss: 0.0453 - val_accuracy: 0.9928 - val_loss: 0.0224
Epoch 8/10
469/469 ━━━━━━━━━━━━━━━━━━━━ 16s 34ms/step - accuracy: 0.9869 - loss: 0.0429 - val_accuracy: 0.9919 - val_loss: 0.0249
Epoch 9/10
469/469 ━━━━━━━━━━━━━━━━━━━━ 15s 32ms/step - accuracy: 0.9879 - loss: 0.0372 - val_accuracy: 0.9937 - val_loss: 0.0197
Epoch 10/10
469/469 ━━━━━━━━━━━━━━━━━━━━ 15s 31ms/step - accuracy: 0.9886 - loss: 0.0364 - val_accuracy: 0.9931 - val_loss: 0.0225
Loss: 0.022469, Accuracy: 0.993100
  • 손실함수로 sparse_categorical_crossentropy를 사용하면 종속변수 y를 원-핫 인코딩하지 않아도 됩니다.

2.4 학습 과정 시각화

  • fit() 함수가 반환하는 값은 각 epoch에서의 loss, val_loss, accuracy, val_accuracy입니다. fit() 함수의 결과를 저장한 변수를 이용하면 모델의 학습 과정을 그래프로 시각화할 수 있습니다.
import matplotlib.pyplot as plt

fig, loss_ax = plt.subplots()
loss_ax.plot(hist.history['loss'], 'y', label='train loss')
loss_ax.plot(hist.history['val_loss'], 'r', label='val loss')
loss_ax.set_xlabel('epoch')
loss_ax.set_ylabel('loss')
loss_ax.legend(loc='upper left')

acc_ax = loss_ax.twinx()
acc_ax.plot(hist.history['accuracy'], 'b', label='train acc')
acc_ax.plot(hist.history['val_accuracy'],'g',label='val acc')
acc_ax.set_ylabel('accuracy')
acc_ax.legend(loc='lower left')

plt.show()

  • 텐서보드 콜백을 이용하면 더 많은 정보를 볼 수 있습니다.

2.5 텐서플로우에서 GPU 사용하기

  • 여러분의 컴퓨터에 고성능 그래픽카드가 장착되어 있다면 GPU를 이용해서 학습을 더 빠르게 시킬 수 있습니다.
conada install -c conda-forge cudatoolkit=11.2 cudnn=8.1.0
pip install "tensorflow<2.11"
pip install "tensorflow-gpu<2.11"
  • 다음 코드는 GPU를 사용할 수 있는 상태인지 알아봅니다.
from tensorflow.python.client import device_lib
print(device_lib.list_local_devices())
  • 다음 코드는 사용 가능한 GPU 목록을 출력합니다. 디바이스가 보이면 GPU를 사용할 수 있는 상태입니다.
import tensorflow as tf
tf.config.list_physical_devices("GPU")
  • CUDA에는 환경 변수 CUDA_VISIBLE_DEVICES를 이용해서 GPU를 제한하는 기능이 존재합니다. 아래 코드를 실행하면 CPU만으로 동작합니다.
import os
os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
  • 텐서플로우 2.10은 기본 윈도우 운영체제에서 GPU를 지원하는 마지막 버전입니다. 여러분의 텐서플로우 버전이 2.11 이상이라면 WSL2에 텐서플로우를 설치하는 것입니다.
  • 윈도우 운영체제에 WSL2를 설치하려면 다음 주소를 참고하세요.
  • WSL2에서 NVIDIA GPU 지원 설정은 다음 주소를 참고하세요
  • WSL2에 GPU를 사용하기 위한 텐서플로우는 아래 명령으로 설치할 수 있습니다.
pip install tensorflow[and-cuda]