11-3. Fashion MNIST를 Keras를 이용한 ConvNet으로 풀어보자

2020. 7. 13. 15:35AI/모두를 위한 딥러닝

이 포스트는 모두를 위한 딥러닝 - Tensor Flow를 정리한 내용을 담았습니다.
누구나 이해할 수 있는 수준으로 설명하고자 하였으며 LAB의 코드는 TF2.0에 맞추어 재작성되었습니다.

이번 포스트는 모두를 위한 딥러닝 LAB 11-2의 "MNIST 99% with CNN"를 응용해 "Fashion MNIST with CNN, TF2.X"로 재해석하여 작성했습니다. 포스트의 코드는 책 "시작하세요! 텐서플로 2.0 프로그래밍"을 참고하여 작성했습니다.

 

  • Fashion MNIST

MNIST는 손으로 쓴 숫자 글씨를 모아놓은 데이터셋으로, 프로그래밍 언어를 처음 배울 때 "Hello World"를 출력하는 것과 같이 기계학습 분야를 대표하는 고전적인 문제입니다. Fashion MNIST는 MNIST에 영향을 받아 옷과 신발, 가방의 이미지들을 모아놓은 데이터셋입니다. Fashion MNIST의 이미지 크기는 28x28픽셀이며 60000개의 학습셋과 10000개의 테스트셋을 가집니다.

Fashion MNIST

Label Category
0 티셔츠/상의
1 바지
2 스웨터
3 드레스
4 코트
5 샌들
6 셔츠
7 운동화
8 가방
9 부츠

이제 Fasion MNIST를 TF2버전으로 Keras와 CNN 구조를 이용해 풀어보겠습니다. 먼저 아래는 코드 전문입니다.

"""
@IDE: Pycharm
@Environment: TF 2.1.0, python 3.7.7

@citation: 시작하세요! 텐서플로 2.0 프로그래밍
@author: Hwanhee kim

@Rewrite: childult-programmer
@Github: https://github.com/childult-programmer
"""

import tensorflow as tf
import matplotlib.pyplot as plt
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Conv2D, MaxPool2D
from tensorflow.keras.layers import Flatten, Dense, Dropout

# Fashion MNIST 데이터세트
# Label     Category
# 0         티셔츠/상의
# 1         바지
# 2         스웨터
# 3         드레스
# 4         코트
# 5         샌들
# 6         셔츠
# 7         운동화
# 8         가방
# 9         부츠

# Fashion MNIST 데이터셋 로딩
fashion_mnist = tf.keras.datasets.fashion_mnist
(train_X, train_Y), (test_X, test_Y) = fashion_mnist.load_data()

# # 첫번째 이미지 출력
# plt.imshow(train_X[0], cmap='gray')
# plt.colorbar()
# plt.show()
#
# print(train_Y[0])

# 정규화
train_X = train_X / 255.0
test_X = test_X / 255.0

# reshape 이전
# print(train_X.shape, test_X.shape)          # (60000, 28, 28), (10000, 28, 28)

# reshape 이후
# 채널(컬러 이미지는 RGB의 3채널, 흑백 이미지는 1채널)을 갖도록 reshape
train_X = train_X.reshape(-1, 28, 28, 1)
test_X = test_X.reshape(-1, 28, 28, 1)
# print(train_X.shape, test_X.shape)          # (60000, 28, 28, 1) (10000, 28, 28, 1)

# # 그래프를 그려 Fashion MNIST 데이터 확인
# # 전체 그래프의 크기를 width=10, height=10으로 지정
# plt.figure(figsize=(10, 10))
# for c in range(16):
#     # 4X4 그리드에서 c+1번째 칸에 그래프를 그립니다. 1~16
#     plt.subplot(4, 4, c+1)
#     plt.imshow(train_X[c].reshape(28, 28), cmap='gray')
#
# plt.show()

# Fashion MNIST 분류를 위한 CNN 모델 정의
model = Sequential([
    # 특징 추출기 (Feature Extractor)
    Conv2D(input_shape=(28, 28, 1), kernel_size=(3, 3), filters=32),
    MaxPool2D(strides=(2, 2)),
    Conv2D(kernel_size=(3, 3), filters=64),
    MaxPool2D(strides=(2, 2)),
    Conv2D(kernel_size=(3, 3), filters=128),
    # 다차원 이미지 => 1차원
    Flatten(),
    # 분류기 (Classifier)
    Dense(units=128, activation='relu'),
    Dropout(rate=0.3),
    Dense(units=10, activation='softmax')
])

# 모델 학습 과정 설정
model.compile(optimizer=tf.keras.optimizers.Adam(),
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

# 모델 학습
history = model.fit(train_X, train_Y, epochs=15, validation_split=0.25)

plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(history.history['loss'], 'b-', label='loss')
plt.plot(history.history['val_loss'], 'r--', label='val_loss')
plt.xlabel('Epoch')
plt.legend()

plt.subplot(1, 2, 2)
plt.plot(history.history['acc'], 'g-', label='acc')
plt.plot(history.history['val_acc'], 'k--', label='val_acc')
plt.xlabel('Epoch')
plt.ylim(0.7, 1)
plt.legend()

plt.show()

# 모델 평가
print(model.evaluate(test_X, test_Y, verbose=0))

Keras에는이미 Fashion MNIST 데이터셋이 탑재되어 있습니다. tf.keras.datasets.fashion_mnist로 Fashion MNIST 데이터셋을 training set과 test set 튜플로 나누었습니다. load_data()는 train_X와 test_X에는 이미지 데이터를, train_Y와 test_Y에는 범주 라벨의 형태를 취한 2개의 튜플을 반환합니다. line 36 - 40의 주석을 해제한 후 실행하면 데이터셋의 첫 번째 이미지를 확인하면 데이터의 이미지가 0에서 255까지의 값을 가지는 28x28 픽셀 크기의 2차원 이미지임을 확인할 수 있습니다.

Fashion MNIST 첫 번째 이미지

이후 우리는 최댓값과 최솟값을 알고 있기 때문에 이미지 픽셀값을 255로 나눠 0.0과 1.0 사이의 값으로 정규화합니다. 또한 Conv2D 레이어로 연산할 것이기 때문에, 이미지가 채널을 가진 형태로 만들기 위해 reshape해줍니다. 참고로 코드에 주석이 많이 씌어져 있는데, 이미지를 출력해 확인하거나 데이터의 형태를 확인하기 위해 작성한 코드이기 때문에 실제로 작성하실 때는 주석 부분을 지우고 작성하셔도 됩니다. 이제 CNN 모델을 정의하고 학습하는 과정을 살펴보겠습니다.

line 66 - 79에서는 Fashion MNIST 분류를 위해 CNN 모델을 정의하고 있습니다. 모델은 Convolution 레이어와 Pooling 레이어를 사용하여 이미지의 특징을 추출하고 Dropout 레이어와 Dense(Fully Connected) 레이어를 사용해 과적합을 방지하고 이미지를 분류하도록 구성했습니다. 아래는 CNN 모델의 구조를 그림으로 나타냈습니다.

CNN 구조

Conv2D 레이어를 보겠습니다. 맨 처음의 Conv2D 레이어에는 이미지의 input_shape를 지정해주어야합니다. 입력이미지의 높이, 너비, 채널 수에 따라 (28, 28, 1)로 정의한 뒤 kernel_sizefilter의 개수를 정해주었습니다. 필터의 수는 32부터 시작해 64, 128로 3개의 Conv2D 레이어에 뒤로 갈수록 2배씩 늘렸습니다. Conv2D 레이어 위에 MaxPool2D 레이어를 쌓은 모습을 볼 수 있습니다. max pooling 레이어는 이미지의 특정 데이터를 강조하고 싶을 때나 특히 과적합을 방지하고자 할때 많이 사용되는데, 과적합을 방지하는 것에 대해 잠시 살펴보고 넘어가겠습니다. 처음 Conv2D레이어에서 우리는 28x28크기의 이미지를 3x3크기의 필터 32개로 Feature Map을 만들었습니다. stride는 따로 정하지 않았기 때문에 디폴트 값인 1로 놓고 각 Conv2D 레이어에 있는 feature의 수를 계산하면 ((28 - 3)/1 + 1) * ((28 - 3)/1 + 1) = 26 * 26 = 676개 입니다. 이런 layer를 32층 쌓았으니 총 feature의 개수는 676 * 32 = 21632개가 됩니다. 이렇게 feature가 많아지면 과적합(overfitting)의 우려가 있으므로, 우리는 이를 max pooling을 사용해 이미지의 크기를 줄임으로써 feature의 개수를 줄여 방지할 수 있습니다. 코드를 보면 MaxPool2D 레이어의 pool_sizestrides를 (2, 2)로 정했습니다. 이후 Conv2D와 MaxPool 레이어를 반복해서 쌓으며 이미지의 특징을 추출한 뒤, 이미지를 1차원으로 바꿔(Flatten()) Label로 분류하는 모델을 쌓고 있습니다. 이 때 Dropout 레이어를 넣어 과적합을 방지하고자 했으며 마지막 Dense 레이어를 보면 Label 개수에 맞춰 unit을 10으로 정하고 활성화함수를 softmax로 설정한 것을 알 수 있습니다. line 82 - 87에서는 CNN 모델의 학습 과정을 설정하고 실제로 모델을 학습하는 모습을 볼 수 있는데, 여기서 sparse_categorical_crossentropy라는 다소 생소한 손실 함수를 사용했습니다. sparse_categorical_crossentropy는 categorical_crossentropy와 같은 다중 분류 손실 함수지만, 샘플 값을 입력하는 부분에서 One-hot encoding을 하지 않고 정수 값을 넘겨줄 수 있다는 점에서 차이가 있습니다. 또한 모델 학습에서 validation_split을 0.25로 지정했는데, validation_split은 train_X와 train_Y에서 일정 비율을 분리해 검증 데이터로 사용하며, 검증 데이터는 각 Epoch마다 검증 데이터의 정확도를 출력해 훈련이 잘 이루어지고 있는지를 알고싶을때 사용됩니다. 이 검증 데이터는 훈련에는 반영되지 않고 단지 훈련 과정을 지켜보기 위한 용도로만 사용됩니다.

마지막으로 loss와 accuracy 그래프를 출력하고 모델을 평가하는 코드입니다. matplotlib 라이브러리를 이용해 그래프를 그리고 model.evaluate()로 모델을 평가하고 있습니다. 그래프를 그리는 코드에서 loss매 에포크 마다의 훈련 손실값, val_loss매 에포크 마다의 검증 손실값, acc매 에포크 마다의 훈련 정확도, val_acc매 에포크 마다의 검증 정확도를 나타냅니다. 위에서 validation_split으로 분류한 검증 데이터가 val_loss와 val_acc의 데이터가 됩니다. 그래프를 그렸을 때 만약 loss는 감소하는데 val_loss는 증가한다는 것은 과적합이 일어났다는 것을 뜻합니다. verbose는 학습 중 출력되는 문구를 설정하는데, verbose=0은 아무것도 출력하지 않으며 verbose=1은 훈련의 진행도를 보여주는 진행 막대를, verbose=2는 미니 배치마다 손실 정보를 출력합니다.

출처: 딥러닝을 이용한 자연어 처리 입문

이제 코드를 실행하여 출력화면을 보겠습니다. 먼저 아래의 그래프를 보면 loss는 Epoch에 따라 급격히 줄어드는 반면 val_loss는 줄어들다 늘어났다를 반복하며 0.35와 0.4 사이에 머물고 있습니다. 언뜻보면 과적합이 일어난 상태라고 해석할 수도 있지만, MaxPooling 레이어와 Dropout 레이어를 사용하지 않았을 때에 비하면 훨씬 개선된 수준이라는 것을 알 수 있습니다. loss, accuracy의 최종 결과를 보면 [0.3989..., 0.8932]로, Conv2D와 Dense 레이어만 사용해 해결했을 때에 비해 좋아졌다는 것을 알 수 있습니다. Conv2D와 Dense 레이어만 사용해 문제를 해결한 코드는 따로 작성하진 않지만, 실제로 두 코드를 실행해 결과를 확인해보면 정확도가 0.8577에서 0.8932로 4%가량 높아졌음을 알 수 있습니다. 손실에서는 큰 차이를 보입니다. 

case 1. 코드 실행 그래프
case 2. Conv2D, Dense 레이어만 사용해 이미지를 분류했을 때
case 1. 코드 실행 출력 화면
case 2. Conv2D, Dense 레이어만 사용해 이미지를 분류했을 때

  • CNN 퍼포먼스 향상

CNN 모델을 사용해 Fashion MNIST를 분류하는 문제에서 손실을 줄이고 정확도를 높이는 등 더 좋은 퍼포먼스를 내는데에는 여러 방법들이 있지만, 그 중 대표적인 두 방법을 알아보겠습니다. 첫 번째는 더 많은 레이어 쌓기 입니다. 당연하게끔 더 많은 레이어를 쌓으면 이미지의 특징을 더 많이 추출할 수 있을 것으로 기대할 수 있습니다. Fashion MNIST를 CNN을 사용해 분류하는 유명한 모델중 하나인 VGG-19는 Convolution과 Pooling 레이어를 겹쳐 총 19개의 레이어를 겹쳐 만들었습니다. 이렇게 되면 이미지 분류에 대한 정확도는 높아지는데, 다만 굉장한 양의 연산을 필요로 하기 때문에 실행에 많은 메모리와 시간을 필요로합니다. 따라서 네트워크를 축소화하여 Convolution 레이어를 2개 겹치고 Pooling 레이어를 하나 겹치는 패턴을 두번 반복하는 VGG-7 Net으로 실행해 정확도를 측정해보면 92%의 매우 높은 정확도를 보임을 알 수 있습니다. 두 번째 방법은 이미지 보강입니다. 이미지 보강은 훈련 데이터에 없는 이미지를 새롭게 만들어 내어 훈련 데이터를 보강하는 작업인데, 예를 들어 위의 Fashion MNIST 첫 번째 이미지를 보면 운동화의 코가 왼쪽을 향하고 있습니다. 이 때 만약 Fashion MNIST에 운동화의 코가 오른쪽을 향하는 이미지가 들어왔다면 신경망은 새로운 이미지에 대해 좋은 결과를 내지 못할 것 입니다. 이를 위해 이미지를 가로로 뒤집거나(horizonal flip) 운동화의 코가 오른쪽으로 향하도록 회전시키거나(rotate), 기울이거나(shear) 확대하는(zoom) 등으로 다양한 이미지를 만들어 내어 데이터의 표현력을 조금 더 좋게 만들 수 있습니다. keras에는 이미지 보강을 위해 ImageDataGenerator가 내장되어 있습니다. Fashion MNIST의 첫 번째 이미지에 대해서만 코드로 확인해보겠습니다.

# Image Augmentation 데이터 표시
image_generator = ImageDataGenerator(
    rotation_range=10,
    zoom_range=0.10,
    shear_range=0.5,
    width_shift_range=0.10,
    height_shift_range=0.10,
    horizontal_flip=True,
    vertical_flip=False)

augment_size = 100

x_augmented = image_generator.flow(np.tile(train_X[0].reshape(28 * 28), 100).reshape(-1, 28, 28, 1),
                                   np.zeros(augment_size), batch_size=augment_size, shuffle=False).next()[0]

# 새롭게 생성된 이미지 표시
plt.figure(figsize=(10, 10))
for c in range(100):
    plt.subplot(10, 10, c + 1)
    plt.axis('off')
    plt.imshow(x_augmented[c].reshape(28, 28), cmap='gray')

plt.show()

운동화 이미지가 여러 가지 형태로 조금씩 바뀐 것을 확인할 수 있습니다.각 이미지는 비슷하면서도 자세히 보면 조금씩 다릅니다. 이미지 보강 코드에 대해서는 다음번에 자세히 알아보도록 하겠습니다.