[혼공머신] 합성곱 신경망을 이용한 이미지 분류 모델

강민우·2022년 2월 14일
0
post-thumbnail

[혼자 공부하는 머신러닝+딥러닝] 책에 기반한 정리글입니다.
전체 소스코드는 아래 Github 링크에서 확인할 수 있습니다.

Github 링크

0. 개요

이번 편에서는 텐서플로 케라스 API를 이용해 패션 MNIST 데이터를 합성곱 신경망(Convolutional Neural Network, CNN)으로 분류한다. 모델을 만들며 합성곱, 패딩, 스트라이드, 풀링의 개념도 같이 알아본다.

1. 데이터 준비하기

패션 MNIST 데이터를 불러오고 표준화 전처리 후 훈련세트와 검증세트로 나눈다.
이때, 합성곱 신경망은 2차원 이미지를 그대로 사용하기 때문에 일렬로 펼치지 않는다.
흑백 이미지이기 때문에 1차원 채널이 추가되며, 컬러 이미지는 3차원이 추가된다.

from tensorflow import keras
from sklearn.model_selection import train_test_split

(train_input, train_target), (test_input, test_target) = keras.datasets.fashion_mnist.load_data()

# 흑백 이미지에 채널 추가
train_scaled = train_input.reshape(-1, 28, 28, 1) / 255.0
train_scaled, val_scaled, train_target, val_target = train_test_split(train_scaled, train_target, test_size=0.2, random_state=42)

2. 합성곱 신경망 만들기

2-1. 합성곱 층 추가하기

합성곱 신경망의 구조는 합성곱 층에서 이미지의 특징을 감지한 후 밀집층에서 클래스에 따른 분류 확률을 계산한다.

먼저 Sequential 클래스 객체를 만들고, 첫 번째 합성곱 층인 Conv2D를 추가한다. Conv2D() 매개변수로 커널의 개수, 커널 사이즈, 활성화 함수, 패딩, 입력 데이터 크기가 필요하다.

# 32개 필터, 커널 크기 3x3, 렐루 함수, 세임패딩
model = keras.Sequential()
model.add(keras.layers.Conv2D(32, kernel_size=3, activation='relu', padding='same', input_shape=(28,28,1)))

커널의 개수를 32개로 지정하고, 커널 사이즈를 3으로 놓으면 (3, 3) 크기가 된다.
렐루 함수를 활성화 함수로 지정하고, 세임패딩 적용, 인풋 데이터 크기를 지정한다.


세임 패딩과 밸리드 패딩

패딩이란 입력 배열 주위를 가상의 원소로 채우는 것을 의미한다. 예로, (4, 4) 크기의 입력에 0을 1개 패딩하면 (6, 6)크기의 입력이 횐다.

  • 세임 패딩 : 합성곱 층의 출력 크기를 입력과 동일하게 만들기 위해 입력에 패딩을 추가하는 것이다.
  • 밸리드 패딩 : 패딩 없이 순수한 입력 배열에서만 합성곱을 하여 특성 맵을 만드는 것, 특성 맵의 크기가 줄어든다.

만약 패딩이 없다면 원소들이 2번 이상 커널과 계산되는 것과 달리, 네 모서리에 있는 4개의 값은 커널에 한번만 계산되게 된다. 만약 이 입력이 이미지라면 모서리에 있는 중요한 정보가 특성 맵에 잘 전달되지 않을 가능성이 높다. 반대로 가운데 있는 정보는 잘 표현된다.


2-2. 풀링 층 추가하기

풀링과 스트라이드

  • 풀링 : 합성곱 층에서 만든 특성 맵의 가로세로 크기를 줄이는 역할을 수행한다. 특성 맵의 개수는 줄이지 않는다.
  • 최대 풀링 : 커널 영역에서 가장 큰 값을 고른다.
  • 평균 풀링 : 커널 영역의 값을 평균화한다.
  • 스트라이드 : 합성곱 층에서 필터가 입력 위를 이동하는 크기

예를 들어 (2,2,3) 크기의 특성 맵에 스트라이드가 1인 풀링을 적용하면 (1,1,3) 크기의 특성 맵이 된다.

많은 경우 평균 풀링보다 최대 풀링을 사용하는데, 평균 풀링은 특성 맵의 중요한 정보를 평균화하여 희석시킬 수 있기 때문이다.


케라스는 최대 풀링과 평균 풀링을 MaxPooling2D, AveragePooling2D 로 제공한다. 그 중에서 최대풀링을 사용하며, 풀링 크기를 (2,2)로 지정한다.

model.add(keras.layers.MaxPooling2D(2))

패선 MNIST 이미지가 (28,28) 크기에 세임 패딩을 적용하여 합성곱 층에서 출력된 특성 맵의 가로세로 크기는 입력과 동일하다. 이후 (2,2) 풀링을 적용하여 특성 맵의 크기는 절반으로 줄어들고, 합성곱 층에서 32개의 필터를 사용하여 최대 풀링을 통과한 특성 맵의 크기는 (14,14,32) 이다.

이제 두 번째 합성곱-풀링 층을 추가한다. 첫번째와 동일하지만, 필터 개수를 64개로 늘렸다.

model.add(keras.layers.Conv2D(64, kernel_size=3, activation='relu', padding='same'))
model.add(keras.layers.MaxPooling2D(2))

이 층을 통과하면 특성 맵의 크기는 (7,7,64)가 된다.

2-3. Flatten, 은닉층, Drop, 출력층 구성하기

이제, 마지막에 10개의 뉴런을 가진 출력층에서 확률을 계산하기 위해 3차원 특성 맵을 펼쳐야 한다. Flatten 층을 만들고, Dense은닉층 , Dropout , Dense출력층 순서대로 층을 구성한다.

model.add(keras.layers.Flatten())
model.add(keras.layers.Dense(100, activation='relu')) #은닉층
model.add(keras.layers.Dropout(0.4)) # 40% 드롭아웃
model.add(keras.layers.Dense(10, activation='softmax')) #출력층

은닉층과 출력층 사이에 드롭아웃을 넣어 은닉층의 과대적합을 막아 성능을 개선할 수 있다.
은닉층에 100개의 뉴런을 사용하고 렐루 활성화 함수를 사용한다.
클래스 10개를 분류할 다중 분류 문제이기 때문에 출력층의 활성화 함수는 소프트맥스 함수를 사용한다.

3. 모델 구조 확인하기

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 (MaxPooling2D) (None, 7, 7, 64)         0         
 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
_________________________________________________________________

각 층의 파라미터의 개수를 계산할 수 있다.

첫 번째 합성곱 층은 32개의 필터를 가지고 있고 크기가 (3,3), 깊이가 1이다. 또 필터마다 하나의 절편이 있다. 3x3x1x32+32 = 320개의 파라미터가 있다.

두 번째 합성곱 층은 64개의 필터, 크기 (3,3), 깊이 32이다. 필터마다 하나의 절편이 존지하므로 3x3x32x64+64 = 18496개의 파라미터가 있다.

Flatten 층에서 (7,7,64) 크기의 특성 맵을 1차원으로 펼치면 (3136,)이며, 은닉층에서는 3136개가 100개의 뉴런과 연결되어야 하고, 100개의 절편이 있으므로 3136x100+100 = 313700개의 파라미터가 있다.

마지막 출력층은 100개의 특성이 10개의 뉴런과 연결되고, 10개의 절편이 있으므로 100x10+10 = 1010개의 파라미터가 있다.


keras.utils 패키지의 plot_model() 으로 층의 구성을 그림으로 볼 수 있다.

keras.utils.plot_model(model, show_shapes=True, to_file='cnn-architecture.png', dpi=300)


show_shapes=True 로, 입력과 출력의 크기가 표시되며, to_file 매개변수는 출력한 이미지를 파일로 저장한다. dpi 매개변수는 해상도를 지정한다.

4. 모델 컴파일과 훈련

Adam 옵티마이저를 사용하고, ModelCheckpoint , EarlyStopping 콜백을 사용하여 조기 종료 법을 구현한다.

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

checkpoint_cb = keras.callbacks.ModelCheckpoint('best-cnn-model.h5')
early_stopping_cb = keras.callbacks.EarlyStopping(patience=2, restore_best_weights=True)

history = model.fit(train_scaled, train_target, epochs=20, 
                    validation_data=(val_scaled, val_target), callbacks=[checkpoint_cb, early_stopping_cb])
출력
Epoch 1/20
1500/1500 [==============================] - 19s 7ms/step - loss: 0.5307 - accuracy: 0.8096 - val_loss: 0.3467 - val_accuracy: 0.8756
Epoch 2/20
1500/1500 [==============================] - 10s 7ms/step - loss: 0.3584 - accuracy: 0.8720 - val_loss: 0.3089 - val_accuracy: 0.8859
Epoch 3/20
1500/1500 [==============================] - 11s 7ms/step - loss: 0.3119 - accuracy: 0.8876 - val_loss: 0.2708 - val_accuracy: 0.8968
Epoch 4/20
1500/1500 [==============================] - 10s 7ms/step - loss: 0.2770 - accuracy: 0.8992 - val_loss: 0.2580 - val_accuracy: 0.9047
Epoch 5/20
1500/1500 [==============================] - 10s 7ms/step - loss: 0.2544 - accuracy: 0.9065 - val_loss: 0.2518 - val_accuracy: 0.9101
Epoch 6/20
1500/1500 [==============================] - 11s 7ms/step - loss: 0.2322 - accuracy: 0.9149 - val_loss: 0.2452 - val_accuracy: 0.9122
Epoch 7/20
1500/1500 [==============================] - 10s 7ms/step - loss: 0.2171 - accuracy: 0.9195 - val_loss: 0.2294 - val_accuracy: 0.9176
Epoch 8/20
1500/1500 [==============================] - 11s 7ms/step - loss: 0.2013 - accuracy: 0.9257 - val_loss: 0.2560 - val_accuracy: 0.9134
Epoch 9/20
1500/1500 [==============================] - 10s 7ms/step - loss: 0.1881 - accuracy: 0.9299 - val_loss: 0.2308 - val_accuracy: 0.9186

이전보다 정확도가 훨씬 좋아진 것을 확인할 수 있다.


손실 그래프를 그려, 조기 종료가 잘 이루어졌는지 확인한다.

import matplotlib.pyplot as plt
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train', 'val'])
plt.show()

일곱 번째 에포크가 최적임을 알 수 있다.


predict() 메서드를 사용하여 데이터에 대한 예측을 만들어 본다.

import numpy as np

classes = ['티셔츠','바지','스웨터','드레스','코트','샌달','셔츠','스니커즈','가방','앵클 부츠']
preds = model.predict(val_scaled[0:1])

print(classes[np.argmax(preds)])
출력 가방

테스트 세트로 합성곱 신경망의 일반화 성능을 가늠해본다.

test_scaled = test_input.reshape(-1, 28, 28, 1) / 255.0
model.evaluate(test_scaled, test_target)
출력
313/313 [==============================] - 2s 7ms/step - loss: 0.2460 - accuracy: 0.9108
[0.24599412083625793, 0.9107999801635742]

약 91% 정도의 성능을 기대할 수 있다.

profile
어제보다 성장한 오늘

0개의 댓글