[혼자공부하는 머신러닝 + 딥러닝] 20_합성곱 신경망을 사용한 이미지 분류
19. 합성곱 신경망을 사용한 이미지 분류
- 이전 시간에 합성곱 신경망에 등장하는 여러가지 새로운 개념을 살펴보았다. 여기에는 합성곱, 필터, 패딩, 스트라이드, 풀링 등이 포함된다.
- 이번 시간에는 텐서플로 케라스 API를 사용해 이전에 만든 패션 MNIST데이터를 합성곱 신경망으로 분류해 보도록 하겠다.
1. 패선 MNIST 데이터 불러오기
- 먼저 케라스 API를 사용해 패션 MNIST 데이터를 불러오고 적절히 전처리 한다. 하지만 합성곱 신경망은 일렬로 펼쳐 사용하지 않고 2차원 그대로 사용한다. 다만 이미지는 항상 깊이 차원이 있어야 한다. 흑백 이미지 파일은 채널 차원이 없는 2차원 배열이지만 Conv2D층을 사용하기 위해서는 마지막에 이 채널을 추가해야한다. 넘파이 reshape()을 이용해 전체 배열 차원을 그대로 유지하면서 마지막에 차원추가가 가능하다.
from tensorflow import keras
from sklearn.model_selection import train_test_split
from keras.utils.vis_utils import plot_model
import matplotlib.pyplot as plt
import numpy as np
(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)
- 전형적인 합성곱 신경망의 구조는 합성곱 층으로 이미지에서 특징을 감지한 후 밀집층으로 클래스에 따른 분류 확률을 계산한다.
- 케라스의 Sequential 클래스를 사용해 순서대로 이 구조를 정의해보겠다. 먼저 Sequential 클래스의 객체를 만들고 첫 번쨰 합성곱 층인 Conv2D를 추가한다. 이 클래스는 다른 층 클래스와 마찬가지로 keras.layers 패키지 아래 있다. 이전장에서 보았던 모델의 add()메서드를 사용해 층을 하나씩 차례대로 추가한다.
model = keras.Sequential()
model.add(keras.layers.Conv2D(32, kernel_size=3, activation='relu',
padding='same', input_shape=(28, 28, 1)))
2021-10-11 22:55:38.206988: I tensorflow/core/platform/cpu_feature_guard.cc:142] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations: AVX2 FMA
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
- 해당 코드를 살펴보면 합성곱 층은 32개의 필터를 사용한다. 커널의 크기는 (3, 3)이며 렐루 활성화 함수와 세임 패딩을 사용한다.
- 완전 연결 신경망에서처럼 케라스 신경망 모델의 첫 번째 층에서 입력의 차원을 지정해 주어야 한다. input_shape을 (28, 28, 1)로 지정한다. 차원을 지정해주기 위해서이다.
- 그다음 출력층을 추가해야 한다. 케라스는 최대 풀링과 평균 풀링을 keras.layers 패키지 아래 MaxPooling2D과 AveragePooling2D 클래스를 제공한다. 전형적인 풀링 크기인 (2,2)풀링을 사용해보자. Conv2D클래스의 kernel_size처럼 가로세로 크기가 같으면 정수 하나로 지정할 수 있다.
model.add(keras.layers.MaxPooling2D(2))
- 패션 MNIST 이미지가 (28, 28)크기에 세임 패딩을 적용했기 때문에 합성곱 층에서 출력된 특성 맵의 가로세로 크기는 입력과 동일하다. 그 다음 (2, 2)풀링을 적용했기 때문에 특성 맵의 크기는 절반으로 줄어든다. 합성곱 층에서 32개의 필터를 사용했기 떄문에 이 특성 맵의 깊이는 32가 된다. 따라서 최대 풀링을 통과한 특성 맵의 크기는 (14, 14, 32)가 될 것이다.
- 첫 번째 합성곱-풀링 층 다음에 두 번째 합성곱-풀링 층을 추가해보자. 두 번째 합성곱-풀링 층은 첫 번째와 거의 동일하다.
model.add(keras.layers.Conv2D(64., kernel_size=3, activation='relu', padding='same'))
model.add(keras.layers.MaxPooling2D(2))
- 이제 3차원 특성 맵을 일렬로 펼칠 차례이다. 마지막 10개의 뉴런을 가진 출력층에서 확률을 계산하기 때문이다. 여기서는 특성 맵을 일렬로 펼쳐서 바로 출력층에 전달하지 않고 중간에 하나의 밀집 은닉층을 하나 더 두도록 한다.
model.add(keras.layers.Flatten())
model.add(keras.layers.Dense(100, activation='relu'))
model.add(keras.layers.Dropout(0.4))
model.add(keras.layers.Dense(10, activation='softmax'))
- 은닉층과 출력층 사이에 드롭아웃을 넣었다. 드롭아웃이 은닉층의 과대적합을 막아 성능을 조금 더 개선해 줄 것이다. 은닉층은 100개의 뉴런을 사용하고 활성화 함수는 렐루함수를 사용한다. 패션 MNIST 데이터셋은 클래스 10개를 분류하는 다중 분류 문제이므로 마지막 층의 활성화 함수는 소프트 맥스를 사용한다.
- 케라스 모델의 구성을 마쳤으니 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 (MaxPooling2 (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가 되고 두 번째 합성곱에서 특성 맵의 크기가 64로 늘어난다. 반면 특성 맵의 가로세로 크기는 첫 번째 풀링 층에서 절반으로 줄어들고, 두 번쨰 풀링층에서 다시 절반으로 줄어든다. 따라서 최종 특성 맵 크기는 (7, 7, 64)입니다.
- 완전 연결 신경망에서 했던 것처럼 모델 파라미터의 개수를 계산해 보자. 첫 번째 합성곱 층은 32개의 필터를 가지고 있고 크기가 (3,3)깊이가 1이다. 또 필터마다 하나의 절편이 있다. 따라서 총 3x3x1x32+32=320개의 파라미터가 존재한다.
- 두 번째 합성곱 층은 64개의 필터를 사용하고 크기가 (3,3) 깊이가 32이다. 필터마다 하나의 절편이 존재한다 따라서 3x3x32x64+64=18496개의 파라미터가 있다.
- Flatten 클래스에서 (7, 7, 64) 크기의 특성 맵을 1차원 배열로 펼치면 (3164,)크기의 배열이 된다. 이를 100개의 뉴런과 완전히 연결해야 하므로 은닉층의 모델 파라미터 개수는 3164x100+100=313700개 이다.
- 마지막 출력층 모델 파라미터 개수는 1010개 이다.
- 케라스는 summary() 메서드 외에 층의 구성을 그림으로 표현해주는 plot_model()함수를 keras.utils패키지에서 제공한다.
keras.utils.plot_model(model)
- 네모 상자 안의 내용 중 콜론 왼쪽에는 층의 이름이 쓰여있고 오른쪽에는 클래스가 나타난다. 맨 처음에 나오는 InputLayer 클래스는 케라스가 자동으로 추가해주는 것으로 입력층의 역할을 한다. 이 입력층은 첫 번째 Conv2D 클래스에 추가한 input_shape 매개변수를 사용한다.
- plot_model()함수의 show_shapes 매개변수를 True로 설정하면 이 그림에 입려고가 출력의 크기를 표시해준다. 또 to_file 매개변수에 파일 이름을 지정하면 출력한 이미지를 파일로 저장한다. dpi 매개변수로 해상도를 지정할 수도 있다.
keras.utils.plot_model(model, show_shapes=True, to_file='cnn-architecture.png', dpi=300)
- 오른쪽 input, output 상자에 층으로 입력되는 크기와 출력되는 크기가 나타나기 때문에 이해가 훨씬 쉽다.
- 이제 적용할 합성곱 신경망 모델의 구성을 마쳤다. 이제 모델을 컴파일 하고 훈련해 보자.
2. 모델 컴파일과 훈련
- 케라스 API의 장점은 딥러닝 모델의 종류나 구성 방식에 상관없이 컴파일 훈련 과정이 같다는 것이다.
- 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])
2021-10-11 23:05:02.888958: I tensorflow/compiler/mlir/mlir_graph_optimization_pass.cc:185] None of the MLIR Optimization Passes are enabled (registered 2)
Epoch 1/20
1500/1500 [==============================] - 30s 20ms/step - loss: 0.5278 - accuracy: 0.8110 - val_loss: 0.3236 - val_accuracy: 0.8806
Epoch 2/20
1500/1500 [==============================] - 28s 19ms/step - loss: 0.3481 - accuracy: 0.8753 - val_loss: 0.2761 - val_accuracy: 0.8995
Epoch 3/20
1500/1500 [==============================] - 23s 15ms/step - loss: 0.2964 - accuracy: 0.8927 - val_loss: 0.2529 - val_accuracy: 0.9086
Epoch 4/20
1500/1500 [==============================] - 23s 15ms/step - loss: 0.2636 - accuracy: 0.9050 - val_loss: 0.2350 - val_accuracy: 0.9137
Epoch 5/20
1500/1500 [==============================] - 23s 15ms/step - loss: 0.2422 - accuracy: 0.9119 - val_loss: 0.2506 - val_accuracy: 0.9094
Epoch 6/20
1500/1500 [==============================] - 24s 16ms/step - loss: 0.2242 - accuracy: 0.9186 - val_loss: 0.2409 - val_accuracy: 0.9148
- 훈련 세트의 정확도가 이전보다 훨씬 좋아졌다. 손실 그래프를 그려서 조기종료가 잘 이루어진 것인지 확인해 보자.
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train', 'val'])
plt.show
<function matplotlib.pyplot.show(close=None, block=None)>
- 제대로 조기종료가 작동한 것을 확인 할 수 있다. EarlyStopping 클래스에서 restore_best_weight 매개변수를 True로 지정했으므로 현재 model 객체가 최적의 모델 파라미터로 복원되었다.
- 세트에 대한 성능을 평가해보자.
model.evaluate(val_scaled, val_target)
375/375 [==============================] - 1s 3ms/step - loss: 0.2350 - accuracy: 0.9137
[0.2350253164768219, 0.9137499928474426]
- pedict() 메서드를 사용해 훈련된 모델을 사용하여 새로운 데이터에 대해 예측을 만들어보자. 맷플롯립에서 흑백이미지에 깊이 차원은 존재하지 않는다. 따라서 2차원으로 바꾸어 줘야 한다. 먼저 샘플 이미지를 확인해보자.
plt.imshow(val_scaled[0].reshape(28, 28), cmap='gray_r')
plt.show()
- 핸드백 이미지로 보인다. 모델은 이 이미지에 대해 어떤 예측을 만드는지 확인해보자. predict()메서드는 10개의 클래스에 대한 예측 확률을 출력한다.
preds = model.predict(val_scaled[0:1])
print(preds)
[[2.15914977e-11 4.73785724e-13 1.77096524e-13 6.48665895e-14
2.48525983e-12 1.14795656e-11 2.97451733e-12 2.44211688e-13
1.00000000e+00 1.23677395e-13]]
- 결과를 보면 9번째 값이 1이고 다른 값은 거의 0에 가깝다. 이를 막대그래프로 그리면 더 명확하게 볼 수 있다.
plt.bar(range(1, 11), preds[0])
plt.xlabel('class')
plt.ylabel('prob.')
plt.show()
- 실제로 다른 값들은 사실상 0이라 볼 수 있다. 아홉번째 클래스가 실제로 무엇인지 원본 데이터셋의 레이블을 확인해야 한다.
classes = ['티셔츠', '바지', '스웨터', '드레스', '코트', '샌달,', '셔츠', '스니커즈', '가방', '앵클 부츠']
- 클래스 리스트가 있으면 레이블을 출력하기 쉽다. preds배열에서 가장 큰 인덱스를 찾아 classes 리스트의 인덱스로 사용하면 된다.
print(classes[np.argmax(preds)])
가방
- 해당 샘플을 가방으로 정확히 예측했다. 합성곱 신경망을 만들고 훈련하여 새로운 샘플에 대해 예측을 수행하는 방법도 알아보았다. 마지막으로 맨 처음에 떼어 높은 테스트 세트로 합성곱 신경망의 일반화 성능을 가늠해보자.
- 훈련세트와 동일하게 픽셀값의 범위를 0에서 1사이로 바꾸고 이미지 크기를 (28,28,1)로 바꾼다.
test_scaled = test_input.reshape(-1, 28, 28, 1) / 255.0
model.evaluate(test_scaled, test_target)
313/313 [==============================] - 1s 4ms/step - loss: 0.2591 - accuracy: 0.9073
[0.259100079536438, 0.9072999954223633]
- 해당 모델을 실전에 투입해 패션아이템을 분류한다면 약 90%의 정확도를 기대할 수 있다는 결과가 나왔다.
Leave a comment