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]