CNN을 활용한 Fashion MNIST 예측


이전에 다층 퍼셉트론을 이용하여 Fashion MNIST를 예측해 보았는데요.

이번엔 이미지 데이터에 효과적인 CNN(컨볼루션 층 + 풀링 층)을 공부해 보았으니

CNN을 활용하여 Fashion MNIST를 예측하는 모델을 구성해보겠습니다.

이전과 동일하게 케라스에서 제공하는 Fashion MNIST 데이터를 불러오고,

X데이터를 픽셀값 255로 나눠줌으로써 각 값들이 0 ~ 1사이의 값을 가질 수 있도록 만듭니다.

또한, CNN을 공부할때, 입력되는 이미지 데이터는 (높이, 너비, 깊이) 형태인 것을 알 수 있었습니다.

깊이는 이미지 데이터의 채널을 뜻하며, 흑백은 1, 컬러는 3이었죠?

Fashion MNIST 데이터는 흑백 이미지이기 때문에 numpy의 reshape함수를 사용하여 깊이 차원을 추가해줍니다.

그리고 모델 학습 시 과적합 여부를 확인하기 위해 훈련 데이터셋과 검증 데이터셋으로 나눕니다.

from tensorflow.keras.datasets import fashion_mnist
from sklearn.model_selection import train_test_split

# 데이터 불러오기
(X_train, y_train), (X_test, y_test) = fashion_mnist.load_data()

# X데이터를 0 ~ 1 값을 가질 수 있도록 맞춰주기
X_train = X_train / 255.0
X_test = X_test / 255.0

# 이미지 데이터에 깊이 차원을 만들어주기
X_train = X_train.reshape(-1, 28, 28, 1)
X_test = X_test.reshape(-1, 28, 28, 1)

# 검증 데이터셋 분리
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train,
                                                 test_size=0.2, random_state=42)

# 입력 데이터의 크기 확인
X_train.shape, X_val.shape, X_test.shape
((48000, 28, 28, 1), (12000, 28, 28, 1), (10000, 28, 28, 1))

모델 구성

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Dense, Flatten, Dropout

model = Sequential()

# 첫번째 합성곱 층
model.add(Conv2D(32, kernel_size=3, activation='relu', padding='same', input_shape=(28,28,1)))
model.add(MaxPooling2D(2))

# 두번째 합성곱 층
model.add(Conv2D(64, kernel_size=3, activation='relu', padding='same'))
model.add(MaxPooling2D(2))

# 완전 연결층
model.add(Flatten())
model.add(Dense(100, activation='relu'))
model.add(Dropout(0.4))
model.add(Dense(10, activation='softmax'))   # 10개의 클래스 중 택1, 10개의 확률을 얻기위해 softmax 사용

첫번째 컨볼루션 층에서는 32개의 커널을 사용, 커널의 사이즈는 (3,3,1)이며, 활성화 함수는 relu를 사용하겠습니다.

(입력 데이터의 채널이 1이므로 커널의 채널도 1이 됩니다. 만약 입력 데이터가 컬러이면 채널은 3이 되겠습니다.).

same 패딩을 사용할 것이고, 입력 사이즈는 배치 차원없이 (28, 28, 1)이 됩니다.

그럼 출력되는 특성 맵은 (28, 28, 32) 크기가 됩니다.


특성 맵을 맥스 풀링을 해줍니다. 이때 풀링에 사용되는 커널은 2x2사이즈로 해서 절반으로 줄여줍니다.

최종적으로 첫번째 (컨볼루션 + 풀링)층에서 나오는 특성 맵의 크기는 (14, 14, 32)가 됩니다.


두 번째 합성곱 층에선 64개의 필터를 사용했습니다.

동일한 커널 사이즈와 활성화 함수 패딩을 사용했으므로 나오는 특성 맵의 크기는 (14, 14, 64)가 됩니다.

동일하게 맥스 풀링을 통과하면 특성 맵의 크기는 최종적으로 (7, 7, 64)가 됩니다.


그 다음엔 Dense층을 놓고 클래스 개수에 맞는 10개의 확률값을 얻어야하기 때문에 Flatten()을 사용하여

(7, 7, 64) 특성 맵을 1차원 배열로 만들어줍니다.


첫 번째 Dense층에서 활성화 함수 relu를 사용하는 뉴런 100개를 은닉층으로 두었습니다.

3136개의 입력이 100개의 뉴런에 완전 연결이 되었으므로 굉장히 많은 가중치가 생성될 거 같네요.

그래서 과적합을 피해보기 위해 은닉층 뒤에 Dropout을 사용해서 훈련 시에 40%의 뉴런을 끄도록 지정해 보았습니다.

최종 출력층에는 10개의 출력을 가지고 softmax를 사용해야 10개의 확률을 얻을 수 있습니다.

모델 summary

각 층마다 출력되는 특성 맵과 출력 데이터의 크기와 파라미터 개수는 summary()함수를 통해 다시 한 번 확인해봅니다.

model.summary()
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 conv2d (Conv2D)             (None, 28, 28, 32)        320       
                                                                 
 max_pooling2d (MaxPooling2D  (None, 14, 14, 32)       0         
 )                                                               
                                                                 
 conv2d_1 (Conv2D)           (None, 14, 14, 64)        18496     
                                                                 
 max_pooling2d_1 (MaxPooling  (None, 7, 7, 64)         0         
 2D)                                                             
                                                                 
 flatten (Flatten)           (None, 3136)              0         
                                                                 
 dense (Dense)               (None, 100)               313700    
                                                                 
 dropout (Dropout)           (None, 100)               0         
                                                                 
 dense_1 (Dense)             (None, 10)                1010      
                                                                 
=================================================================
Total params: 333,526
Trainable params: 333,526
Non-trainable params: 0
_________________________________________________________________

summary 내용을 확인해 보았을 때, 첫번째 Dense층의 100개의 뉴런이 Flatten 층에서 나온 3136개의 입력 배열과

완전 연결이 되었으므로 굉장히 많은 가중치가 생긴 걸 확인할 수 있네요.

그 위에 있는 합성곱층 2개의 가중치를 모두 더한것보다 훨씬 많은 것을 확인할 수 있는데요.

이 점을 보았을 때 완전 연결층은 과적합이 쉽게 나타날 수 있고

반대로, 합성곱 층은 적은 개수의 파라미터로 효과적으로 이미지의 특징을 잘 잡아낼 수 있는 거 같습니다.

plot_model

keras에는 plot_model()이라는 라이브러리가 있습니다.

모델 구성 정보에 대한, summary()함수로 확인할 수 있는 정보들을 시각화해주는 라이브러리입니다.

show_shapes=True로 주면 각 층의 input shape과 output shape을 나타내줍니다.

from tensorflow.keras.utils import plot_model

plot_model(model, show_shapes=True)

모델 컴파일 및 훈련

옵티마이저는 ‘adam’을 사용했고, y데이터를 원 핫 인코딩 하지않고 그대로 사용하기 위해

손실 함수를 ‘sparse_categorical_crossentropy’로 지정했습니다.

# 모델 컴파일
model.compile(optimizer='adam',
             loss='sparse_categorical_crossentropy',
             metrics=['acc'])

체크 포인트를 models 폴더 안에 ‘fashion_mnist_cnn_model.h5’로 지정하고

검증 셋의 점수가 2회 이상 증가했을 때 조기종료를 하고, 가장 손실이 낮았던 곳으로 되돌리기 위해 콜백을 지정했습니다.

# 모델 훈련
from tensorflow.keras import callbacks
import os

# models라는 폴더에 모델(h5파일)을 저장하려는데 해당 폴더가 없으면 만들어주기
if not os.path.exists('./models/'):
    os.mkdir('./models/')

checkpoint_ch = callbacks.ModelCheckpoint('./models/fasion_mnist_cnn_model.h5')

early_stopping_cb = callbacks.EarlyStopping(patience=2, restore_best_weights=True)

history = model.fit(X_train, y_train, verbose=0,
                   epochs=20, validation_data=(X_val, y_val),
                   callbacks=[checkpoint_ch, early_stopping_cb])

훈련 시각화

훈련 셋과 검증 셋의 정확도와 손실을 시각화 해보겠습니다.

아래와 같이 8번째 epoch(0부터)에서 훈련이 멈췄고 patience를 2로 주었기 때문에

6번째 에포크에서 가장 낮은 검증 손실을 얻을 수 있다고 확인할 수 있습니다.

이 이후에는 검증 셋의 손실이 올라가기 때문에 과적합이 존재한다고 볼 수 있습니다.

import matplotlib.pyplot as plt

loss = history.history['loss']
val_loss = history.history['val_loss']
acc = history.history['acc']
val_acc = history.history['val_acc']

epochs = range(1, len(loss) + 1)
fig = plt.figure(figsize=(10, 5))

ax1 = fig.add_subplot(1, 2, 1)
ax1.plot(epochs, loss, color='blue', label='train_loss')
ax1.plot(epochs, val_loss, color='orange', label='val_loss')
ax1.set_title('train and val loss')
ax1.set_xlabel('epochs')
ax1.set_ylabel('loss')
ax1.legend()

ax2 = fig.add_subplot(1, 2, 2)
ax2.plot(epochs, acc, color='green', label='train_acc')
ax2.plot(epochs, val_acc, color='red', label='val_acc')
ax2.set_title('train and val acc')
ax2.set_xlabel('epochs')
ax2.set_ylabel('acc')
ax2.legend()
<matplotlib.legend.Legend at 0x7fa6ebe92820>

모델 평가와 예측

model.evaluate(X_val, y_val)
375/375 [==============================] - 1s 3ms/step - loss: 0.2228 - acc: 0.9231
[0.22275471687316895, 0.9230833053588867]

evaluate 메소드를 사용해서 이 모델을 검증 데이터로 평가해보면

손실값은 약 0.22 정도 나오고, 정확도는 0.92정도가 나옵니다.


이미지 하나를 28x28로 reshape 해서 이미지를 출력해보면 아래와 같은 가방 이미지가 출력됩니다.

그리고 predict 메소드를 사용해서 각 10개의 카테고리에 포함될 10개의 확률을 확인 할 수 있습니다.

9번째 확률값이 제일 높은 1이므로, 이 이미지는 카테고리 9에 해당하는 데이터라고 예측 됨을 확인 할 수 있습니다.

plt.imshow(X_val[0].reshape(28, 28), cmap='gray')
plt.show()

preds = model.predict(X_val[0:1])
print(preds)

[[2.5790641e-21 1.0722967e-27 1.9750217e-24 1.0985332e-21 5.8698841e-18
  2.0373612e-21 2.8928411e-18 3.8926373e-19 1.0000000e+00 3.8500301e-20]]

이번에는 테스트 데이터를 evaluate 메소드를 통해 예측을 해보면 결과는 검증 셋보다

약간 더 높은 0.25 정도의 손실값과 약 0.91의 정확도가 나오는 것을 확인 할 수 있습니다.

model.evaluate(X_test, y_test)
313/313 [==============================] - 1s 4ms/step - loss: 0.2519 - acc: 0.9155
[0.2519080638885498, 0.9154999852180481]

댓글남기기