본문 바로가기

머신러닝 + 딥러닝

[혼공딥] CHAPTER 08-2 합성곱 신경망을 사용한 이미지 분류

728x90

1. 패션 MNIST 데이터 불러오기

 

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 #48000개, 너비28, 높이28, 채널차원 1
	#train_scaled = train_input.reshape(-1, 28, 28, 1)
    	#train_scaled = train_scaled / 255.0
train_scaled, val_scaled, train_target, val_target = train_test_split(train_scaled, train_target, test_size=0.2, random_state=42)

28x28 크기의 패션 MNIST 흑백 이미지를 그대로 사용하되 Keras API 합성곱 클래스는 3차원 클래스로 depth가 있을 것으로 생각한다.

28x28의 2차원 배열이지만 끝에 한 차원을 두어서 28x28x1의 3차원 배열로 변경한다.

끝에 크기가 1인 차원을 하나 더 추가해도 배열의 원소 개수는 변함이 없다.

 

데이터 스케일을 255로 나누어서 0~255사이의 값을 0~1사이의 값으로 바꾸고 훈련세트와 검증세트로 나눈다

 

2. 합성곱 신경망 만들기

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

 

① 첫 번째 합성곱 층 만들기

model = keras.Sequential() #케라스에서 신경망 모델을 만드는 클래스
model.add(keras.layers.Conv2D(32, kernel_size=3, activation='relu', padding='same', input_shape=(28,28,1)))

- add() 메서드로 객체를 따로 변수에 담지 않고 모델에 추가한다.

- Conv2D로 첫 번째 합성곱 층을 추가한다

 

- 이 합성곱 층은 32개의 필터를 사용하고 커널 크기가 (3, 3)이다 -> 채널 차원을 따로 지정하지 않는다, 입력 데이터와 동일한 크기의 커널 채널 차원이 구성된다

- 활성화 함수로 렐루 함수를 사용하고, 패딩은 세임패딩을 사용한다.

* 렐루 함수 : 입력이 양수일 경우 입력을 그대로 통과시키고 음수일 경우 0으로 만든다.

* 세임 패딩 : 입력과 특성 맵의 크기를 동일하게 만들기 위해 입력 주위에 0으로 패딩하는 것

 

완전 신경망에서처럼 케라스 신경망 모델의 첫 번째 층에서 입력의 차원을 지정해주어야 한다

이렇게 안하면 나중에 따로 build method라는 것을 호출해야 하는데 번거로워서 입력 크기를 미리 지정해준다

(28, 28, 1)로 파이썬 튜플로 만들어서 지정해준다

 

필터 하나의 결과가 차곡차곡 쌓여서 32개가 쌓인다. same padding을 사용했기 때문에 너비와 높이가 28*28로 유지되고, depth 혹은 채널 차원은 필터의 개수와 동일하게 32개가 채워진다

 

② 첫 번째 풀링 층 추가

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

최대 풀링을 사용

 

너비와 높이가 2인 풀링을 사용해 28*28 크기가 14*14로 줄어들었음을 알 수 있다

풀링은 각 채널별로 따로따로 적용이 되어 depth 차원은 동일하게 유지된다

 

 

③ 두 번째 합성곱 층 추가

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

합성곱 층에서 필터의 개수를 64로 늘린 것만 다르고 나머지는 모두 같다

 

풀링 층에서 입력의 가로, 세로 크기를 절반으로 줄인다. 64개의 필터를 사용했으므로 최종적으로 만들어지는 특성 맵의 크기는 (7, 7, 64)가 될 것이다.

 

④ flatten층, dense 은닉층, dense 출력층

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'))

3차원 특성 맵을 일렬로 펼친다 -> 10개의 뉴런을 가진 (밀집) 출력층에서 확률을 계산하기 때문이다

3136개의 입력 뉴런이 된다

* flatten()층은 곱하고 더하는 연산이 없고, 가중치도 없고, 입력 배열을 1차원으로 펼치는 역할

 

특성 맵을 일렬로 펼쳐서 바로 출력층에 전달하지 않고 중간에 밀집 은닉층을 둔다

첫 번째 dense층에서 활성화 함수 relu를 가지는 뉴런 100개를 두었다.

3136개의 입력이 100개의 뉴런에 완전 연결이 된다

이 100개의 뉴런을 10개의 뉴런을 가지는 최종 출력층에 두었고, softmax 함수를 사용해 10개의 확률을 얻는다

 

두 dense층 사이에 과대적합을 막기 위해 dropout을 사용하였다. 40% 정도의 뉴런을 훈련시에 끄도록 지정했다.

검증 세트의 점수가 훈련 세트에 비해 좋지 않은 과대적합을 막기 위해 사용

 

⑤ 케라스 모델 구조 출력

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
_________________________________________________________________

첫번째 합성곱 층(conv2d)를 통과하면 특성 맵의 깊이는 32가 된다. 두번째 합성곱 층(conv2d_1)에서는 특성 맵의 크기가 64로 늘어난다

 

특성 맵의 가로세로 크기가 첫 번째 풀링 층(max_pooling2d)에서 절반인 14로 줄어들고, 두번째 풀링층(max_pooling2d_1)에서 절반인 7로 줄어든다

최종 특성 맵의 크기는 (7, 7, 64)이다

 

모델 파라미터의 개수는, 첫번째 합성곱 층(conv2d)은 32개의 필터를 가지고 있고 크기가 (3,3)이며 깊이는 1이다.

3 x 3 x 1 x 32 + 32 = 320개의 파라미터

 

두번째 합성곱 층은 64개의 필터를 가지고 있고, 크기 (3, 3), 깊이 32이다.

3 x 3 x 32 x 64 + 64 = 18496개의 파라미터

합성곱 층은 적은 개수의 파라미터로도 효과적으로 이미지의 특징을 잘 잡아낼 수 있다.

Flatten 클래스에서 (7, 7, 64) 크기의 특성 맵을 1차원 배열로 펼치면 (3136, ) 크기의 배열이 된다. 이를 100개의 뉴런과 연결해야해서 은닉층의 모델 파라미터 개수는 3136 * 100 + 100 = 313700개이다. 

완전연결층은 과대적합 되기가 쉽다

 

출력층의 모델 파라미터 개수는 100 * 10 + 10 = 1010개

 

keras.utils.plot_model(model)

케라스는 plot_model()함수를 keras.utils 패키지에서 제공한다

 

처음에 보이는 InputLayer 클래스는 케라스가 자동으로 추가해 주는 것으로 입력층의 역할을 한다. 

첫번째 Conv2D 클래스에 추가한 input_shape 매개변수를 사용한다.

 

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

plot_model() 함수의 show_shapes 매개변수를 True로 설정하면 입력과 출력의 크기를 표시해준다.

to_file 매개변수에 파일 이름을 지정하면 출력한 이미지를 파일로 저장한다.

dpi 매개변수로 해상도를 지정할 수 있다.

 


모델 컴파일, 훈련

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])

Adam 옵티마이저를 사용하고

ModelCheckpoint 콜백과 EarlyStopping 콜백을 함께 사용해 조기종료기법을 구현한다.

Epoch 1/20
1500/1500 [==============================] - 81s 53ms/step - loss: 0.5205 - accuracy: 0.8137 - val_loss: 0.3309 - val_accuracy: 0.8759
Epoch 2/20
1500/1500 [==============================] - 72s 48ms/step - loss: 0.3429 - accuracy: 0.8775 - val_loss: 0.2825 - val_accuracy: 0.8950
Epoch 3/20
1500/1500 [==============================] - 72s 48ms/step - loss: 0.2914 - accuracy: 0.8950 - val_loss: 0.2576 - val_accuracy: 0.9039
Epoch 4/20
1500/1500 [==============================] - 72s 48ms/step - loss: 0.2608 - accuracy: 0.9046 - val_loss: 0.2407 - val_accuracy: 0.9088
Epoch 5/20
1500/1500 [==============================] - 71s 48ms/step - loss: 0.2389 - accuracy: 0.9135 - val_loss: 0.2416 - val_accuracy: 0.9120
Epoch 6/20
1500/1500 [==============================] - 71s 47ms/step - loss: 0.2180 - accuracy: 0.9208 - val_loss: 0.2366 - val_accuracy: 0.9152
Epoch 7/20
1500/1500 [==============================] - 71s 47ms/step - loss: 0.2007 - accuracy: 0.9249 - val_loss: 0.2286 - val_accuracy: 0.9142
Epoch 8/20
1500/1500 [==============================] - 71s 47ms/step - loss: 0.1865 - accuracy: 0.9315 - val_loss: 0.2287 - val_accuracy: 0.9170
Epoch 9/20
1500/1500 [==============================] - 71s 47ms/step - loss: 0.1725 - accuracy: 0.9346 - val_loss: 0.2177 - val_accuracy: 0.9236
Epoch 10/20
1500/1500 [==============================] - 72s 48ms/step - loss: 0.1613 - accuracy: 0.9400 - val_loss: 0.2283 - val_accuracy: 0.9210
Epoch 11/20
1500/1500 [==============================] - 72s 48ms/step - loss: 0.1527 - accuracy: 0.9429 - val_loss: 0.2261 - val_accuracy: 0.9217

훈련 세트의 정확도가 좋아진 것을 확인할 수 있다.

 

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()

손실 그래프를 그려본다

대략 9번째 에포크가 최적이라고 생각할 수 있다

 

model.evaluate(val_scaled, val_target)

모델의 성능을 평가해보면, fit() 메서드의 출력 중 9번째 에포크의 출력과 동일하다

375/375 [==============================] - 5s 14ms/step - loss: 0.2177 - accuracy: 0.9236
[0.21768520772457123, 0.9235833287239075]

 

plt.imshow(val_scaled[10].reshape(28, 28), cmap='gray_r')
plt.show()

흑백 이미지에 깊이 차원은 없어서 (28, 28, 1) 크기를 (28, 28)로 바꾸어 출력해야한다.

 

preds = model.predict(val_scaled[10:11])
print(preds)

출력을 확인해보면 4번째 값이 거의 1에 가깝고 나머지는 예측값이 보다 낮음을 볼 수 있다.

[[1.4831944e-03 1.5091890e-08 2.0763587e-06 9.9661571e-01 3.6763682e-05
  1.5661797e-09 1.8609024e-03 5.0563796e-11 1.3901989e-06 2.8029434e-10]]

슬라이싱을 사용한 이유 : 케라스의 fit(), predict(), evaluate() 메서드는 모두 입력의 첫 번째 차원이 배치 차원일 것으로 기대한다. 따라서 샘플 하나를 전달할 때 (28, 28, 1)이 아니라 (1, 28, 28, 1) 크기로 전달해야한다. 배열 슬라이싱은 인덱싱과 다르게 선택된 원소가 하나이더라도 전체 차원이 유지되어 (1, 28, 28, 1) 크기를 만든다.

 

plt.bar(range(1, 11), preds[0])
plt.xlabel('class')
plt.ylabel('prob.')
plt.show()

막대그래프를 그려본다

 

classes = ['티셔츠', '바지', '스웨터', '드레스', '코트', '샌달', '셔츠', '스니커즈', '가방', '앵클부츠']

import numpy as np
print(classes[np.argmax(preds)])

4번째 클래스가 실제로 무엇인지 알기위해 MNIST 데이터셋의 레이블을 리스트로 저장해서 출력해본다.

preds 배열에서 가장 큰 인덱스를 찾아 classes 리스트의 인덱스로 사용한다.

드레스

실제로 잘 예측했음을 확인할 수 있다

 

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

test_scaled = test_input.reshape(-1, 28, 28, 1) / 255.0

픽셀값의 범위를 0~1 사이로 바꾸고 이미지 크기를 (28, 28)에서 (28, 28, 1)로 바꾼다

 

model.evaluate(test_scaled, test_target)

evaluate() 메서드로 테스트 세트에 대한 성능을 측정하면

313/313 [==============================] - 5s 14ms/step - loss: 0.2465 - accuracy: 0.9152
[0.24654965102672577, 0.9151999950408936]

테스트세트의 점수는 검증세트 점수보다 조금 더 작음을 볼 수 있다.

실전에 투입하면 약 91%의 성능을 기대할 수 있다.

 

728x90