본문 바로가기
ML, DL, RL

[DL] Classifying Handwriting with Keras

by llHoYall 2020. 11. 10.

딥러닝을 배우면 처음으로 항상 하는 MNIST 손글씨 데이터세트를 분류하는 것을 해보겠습니다. ㅎㅎ

Keras를 사용해서 DNN 모델로 구현을 해보겠습니다.

Input Dataset

Getting Dataset

Keras를 사용하면 유명한 데이터세트는 이미 다 포함이 되어 있어 쉽게 가져다 쓸 수 있습니다.

from keras.datasets import mnist

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

Analyzing Dataset

이제 이 데이터세트가 어떻게 생겼는지 한 번 살펴볼까요?

print(train_images.shape, train_labels.shape)
# (60000, 28, 28) (60000,)

print(test_images.shape, test_labels.shape)
# (10000, 28, 28) (10000,)

훈련 이미지는 6만개의 28 x 28 크기의 이미지이고, 테스트 이미지는 1만개가 들어있네요.

또, 훈련 레이블은 6만개의 값이고, 테스트 레이블은 1만개의 값입니다.

 

이번엔 안에 들어있는 데이터를 살펴보죠.

import matplotlib.pyplot as plt

%matplotlib inline

for i in range(10):
    plt.subplot(1, 10, i + 1)
    plt.imshow(train_images[i], 'gray')
    print(train_labels[i], end=", ")
plt.show()

저는 주로 주피터 노트북을 사용하기 때문에 바로 출력을 해보려고 %matplotlib inline을 넣어주었습니다.

요렇게 손글씨와 해당하는 정답 값이 적혀 있었군요.

Preprocessing Dataset

이제 이 데이터세트를 DNN의 입력으로 사용하기 위해 전처리를 해줍시다.

먼저, 2차원의 28 x 28크기의 이미지를 1차원으로 바꿔 784로 변경합니다.

train_images = train_images.reshape((train_images.shape[0], 784))
test_images = test_images.reshape((test_images.shape[0], 784))

 

다음으로, 레이블을 one-hot encoding으로 변환해줍니다.

원-핫 인코딩은 특정 값만 1이고, 나머지는 0으로 만듭니다.

즉, 손글씨의 경우 0~9까지의 숫자 중 해당하는 숫자만 1인 값을 갖도록 만드는 것이죠.

from keras.utils import to_categorical

train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)

 

이제 바뀐 데이터를 확인해 봅시다.

print(train_images.shape, train_labels.shape)
# (60000, 784) (60000, 10)

print(test_images.shape, test_labels.shape)
# (10000, 784) (10000, 10)

print(train_labels[0])
# [0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]

이미지들은 각각 6만개와 1만개의 784 크기의 배열로 바뀌었고, 레이블도 적절히 원-핫 인코딩이 되었네요.

위로 올라가서 기존에 5였던 것을 확인해 보시고, 요 결과를 보시면 해당하는 값만 1로 설정된 것을 확인하실 수 있죠?

Model

Generating Model

케라스 덕분에 아주 쉽게 데이터를 가져와서 손질까지 할 수 있었죠? ㅎㅎㅎ

이제 모델을 만들어 봅시다.

from keras.models import Sequential
from keras.layers import Dense, Dropout

model = Sequential()
model.add(Dense(256, activation='sigmoid', input_shape=(784,)))
model.add(Dense(128, activation='sigmoid'))
model.add(Dropout(rate=0.5))
model.add(Dense(10, activation='softmax'))

먼저 Sequential로 모델을 만들어 줍니다. 쉽게 말해 각 레이어들이 선형으로 이어진 모델이에요.

 

다음은 입력 레이어로 Dense를 사용하여 256개의 유닛이 있는 fully-connected layer로 만듭니다.

활성화 함수는 초기에 널리 쓰였던 sigmoid 함수를 사용하고, 입력 형태는 784의 1D 배열로 받습니다.

바로 이전에 저희가 변환해준 크기이죠.

 

이어서, 입력 레이어에 이어지는 히든 레이어를 만들어 줍니다.

입력 유닛 여러개를 모아 모델에 좀 더 통찰력을 주기 위해 유닛 수를 반으로 줄였습니다.

입력 형태는 따로 입력하지 않아도, 이전 레이어의 출력에 맞춰 케라스가 알아서 해줍니다.

 

이어서, 과적합을 방지하기 위해 Dropout 레이어를 두었고, drop율은 0.5로 주었습니다.

즉, 학습 시마다 임의로 절반의 유닛은 학습을 하지 않습니다.

 

마지막으로 출력 레이어를 만들어 줍니다.

출력은 0~9까지의 결과를 내기 위해 10개의 유닛만을 두었고, softmax 함수를 활성화 함수로 선택해습니다.

소프트맥스 함수는 총 합계가 1이 되는 함수로, 약간의 발상의 전환을 하면 확률로 사용할 수 있죠. ^^

다시 말하면, 해당 예측이 0이 될 확률, 1이 될 확률, ..., 9가 될 확률이 되는 것이죠.

Compiling Model

이제 만든 모델을 컴파일을 합니다.

from keras.optimizers import SGD

model.compile(loss='categorical_crossentropy', optimizer=SGD(lr=0.1), metrics=['acc'])

컴파일은 compile 메소드를 사용합니다.

손실 함수로는 categorical_crossentropy를 사용했습니다. 이것은 다중 분류 평가에 좋은 특성을 갖고 있다고 알려져 있죠.

최적화 함수로는 역시 초기에 널리 사용했던 SGD (Stochastic Gradient Descent)를 사용했습니다.

lrlearning rate으로 얼마나 많은 학습을 할지를 나타냅니다.

평가지표로는 acc를 사용했는데, 정확도를 이용하겠다는 뜻이고 주로 분류에서 이용하는 방법 입니다.

Training

학습 데이터를 넣어서 모델을 학습시킵니다.

history = model.fit(train_images, train_labels, batch_size=500, epochs=5, validation_split=0.2)

# Epoch 1/5
# 96/96 [==============================] - 2s 20ms/step - loss: 1.6940 - acc: 0.4509 - val_loss: 0.9789 - val_acc: 0.8224
# Epoch 2/5
# 96/96 [==============================] - 2s 18ms/step - loss: 0.9257 - acc: 0.7407 - val_loss: 0.5771 - val_acc: 0.8816
# Epoch 3/5
# 96/96 [==============================] - 2s 18ms/step - loss: 0.6659 - acc: 0.8153 - val_loss: 0.4404 - val_acc: 0.8958
# Epoch 4/5
# 96/96 [==============================] - 2s 17ms/step - loss: 0.5487 - acc: 0.8489 - val_loss: 0.3679 - val_acc: 0.9064
# Epoch 5/5
# 96/96 [==============================] - 2s 17ms/step - loss: 0.4778 - acc: 0.8682 - val_loss: 0.3270 - val_acc: 0.9138

학습은 fit 메소드를 사용합니다.

batch_size는 한 번에 학습할 데이터의 크기를 나타냅니다. 클수록 학습 속도가 빠르지만, 메모리를 많이 차지합니다.

따라서, 하드웨어 제약에 따라 적절하게 나눠줍니다.

epochs는 전체 데이터를 사용하여 학습을 몇 번을 반복할지를 지정합니다.

학습 데이터의 일부를 검증으로 사용할 수 있는데, validation_split에 이 비율을 지정합니다.

과적합 되지 않고 학습이 잘 되고 있는지 파악하기 위해 사용하는 방법인데, 주로 20%를 지정합니다.

 

※ 예제와 같은 간단한 경우는 크게 상관이 없겠지만, 학습에 엄청난 시간이 소요될 경우 학습 상황을 지켜보다 무의미한 결과를 보일 경우 중간에 멈추고 하이퍼파라미터나 모델을 조정하여 다시 돌리게 됩니다.

 

결과를 그래프로 나타내어 좀 더 보기 편하도록 해보겠습니다.

plt.plot(history.history['acc'], label='acc')
plt.plot(history.history['val_acc'], label='val_acc')
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.legend(loc='best')
plt.show()

결과가 그래프로 나오는 것을 보실 수 있습니다.

Evaluation

이제 테스트 데이터를 사용하여 평가를 해보겠습니다.

test_loss, test_acc = model.evaluate(test_images, test_labels)
# 313/313 [==============================] - 1s 2ms/step - loss: 0.2018 - acc: 0.9391

간단한 모델로도 꽤 높은 정확도가 나왔습니다.

Prediction

한 번, 직접 모델을 사용해 보겠습니다.

for i in range(10):
    plt.subplot(1, 10, i + 1)
    plt.imshow(test_images[i].reshape((28, 28)), 'gray')
plt.show()

test_predictions = model.predict(test_images[:10])
print(np.argmax(test_predictions, axis=1))
print(np.argmax(test_labels[:10], axis=1))

테스트 이미지 10개를 가져와서 출력을 해주었습니다.

그리고, predict 메소드를 사용해서 해당 이미지를 모델에 넣어 예측을 시켰습니다.

출력의 결과는 0~9까지 해당하는 숫자를 예측하는 값이 나오기 때문에, np.argmax 메소드를 사용해 최대값만을 뽑았습니다.

첫번째 이미지에 대한 출력 예시를 가져와 이해를 하실 수 있게 편집했습니다.

0: 2.38857217e-04
1: 1.30205135e-05
2: 4.06736071e-04
3: 8.33703147e-04
4: 2.65469657e-06
5: 2.58843829e-05
6: 2.70700298e-07
7: 9.97685075e-01
8: 1.17583540e-05
9: 7.82060495e-04

7에 해당하는 예측값만 유달리 높게 나온 것이 보이죠? 이 결과를 np.argmax에 넣으면 바로 7이 나오는 겁니다.

이제 모든 결과를 보겠습니다.

높은 정확도를 자랑했던 것처럼 사람도 헷갈릴만한 5만 6으로 잘못 예측하고, 나머지는 모두 맞췄습니다.

 

한 번 직접 여러가지 파라미터들을 수정해보면서 갖고 놀면서 감을 익혀보시길 바래요.

적은 수정만으로도 98% 까지는 손쉽게 올리실 수 있을거에요.

댓글