본문 바로가기

데이터/머신러닝

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

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

 

- 케라스 API를 사용해 패션 MNIST 데이터를 불러오고 적절히 전처리 

- 데이터 스케일을 0~255 사이에서 0~1 사이로 바꾸고 훈련 세트와 검증 세트로 나눈다. 

- 합성곱 신경망은 2차원 이미지를 그대로 사용하기 때문에 일렬로 펼칠 필요는 없다. 

- 다만, 입력 이미지는 항상 깊이(채널) 차원이 있어야 한다. 

  -> 흑백 이미지의 경우 채널 차원이 없는 2차원 배열이지만 Conv2D층을 사용하기 위해 마지막에 이 채널 차원을 추가해야 한다. 

 

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. 합성곱 신경망 만들기 

 

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

 

- Sequential 클래스의 객체를 만들고 첫 번째 합성곱 층인 Conv2D를 추가 

- 이 합성곱층은 32개의 필터를 사용

- 커널의 크기는 (3,3)이고 렐루 활성화 함수와 세임 패딩을 사용 

- 완전 연결 신경망에서처럼 케라스 신경망 모델의 첫 번째 층에서 입력의 차원을 지정해 주어야 한다.  > input_shape 매개변수를 이 값으로 사용 

model = keras.Sequential()
model.add(keras.layers.Conv2D(32, kernel_size=3, activation='relu',
                              padding='same', input_shape=(28,28,1)))

 

- 그 다음 폴링 층을 추가. 

- 케라스는 최대 풀링과 평균 풀링을 keras.layres 패키지 아래 MaxPooling2D와 AveragePooling2D 클래스로 제공한다. 

- 전형적인 풀링 크기인 (2,2) 풀링을 사용

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

 

- 패션 MNIST 이미지가 (28,28) 크기에 세임 패딩을 적용했기 때문에 합성층 곱에서도 출력된 특성 맵의 가로세로 크기는 동일하다. 

- 그 다음 (2,2) 풀링을 적용했으므로 특성 맵의 크기는 절반으로 줄어든다. 

- 합성곱 층에서 32개의 필터를 사용했기 때문에 이 특성 맵의 깊이는 32가 된다. 따라서 최대 풀링을 통과한 특성 맵의 크기는 (14,14,32)가 된다. 

 

- 첫 번째 합성곱-풀링 층 다음에 두 번째 합성곱-풀링층을 추가 

- 필터의 개수를 64개로 늘림

- 최종적으로 만들어지는 특성 맵의 크기는 (7,7,64)

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

 

- 이제 이 3차원 특성 맵을 일렬로 펼칠 차례이다. 이렇게 하는 이유는 마지막에 10개의 뉴런을 가진 (밀집) 출력층에서 확률을 계산하기 때문이다. 

- 여기서는 특성 맵을 일렬로 펼쳐서 바로 출력층에 전달하지 않고 중간에 하나의 밀집 은닉층을 더 두도록한다. 

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


model.summary()

 

 

- 은닉층과 출력층 사이에 드롭아웃을 넣었다. 드롭아웃 층은닉층의 과대적합을 막아 성능을 조금 더 개선해 줄 것이다. 

- 은닉층은 100개의 뉴런을 사용하고 활성화 함수는 합성곱 층과 마찬가지로 렐루 함수를 사용한다. 

- 패션 MNIST 데이터셋은 클래스 10개를 분류하는 다중 분류 문제이므로 마지막 층의 활성화 함수는 소프트맥스를 사용한다. 

 

- summary() 메서드의 출력 결과를 보면 합성곱 층과 풀링 층의 효과가 잘 나타나 있다. 

- 첫 번째 합성곱 층을 통과하면서 특성 맵의 깊이는 32가 되고 두 번째 합성곱에서 특성 맵의 크기가 64로 늘어난다. 

- 반면 특성 맵의 가로세로 크기는 첫 번째 풀링 층에서는 절반으로 줄어들고 두 번째 풀링층에서 다시 절반으로 줄어든다. 

- 따라서 최종 특성 맵의 크기는 (7,7,64)이다. 

 

모델 파라미터 개수 계산 

 

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

  -> 3*3*1*32+32 = 320개의 파라미터 

 

- 두 번쩨 합성곱 층은 64개의 필터를 사용하고 크기가 (3,3), 깊이가 32이다. 역시 필터마다 하나의 절편이 있다. 

 -> 3*3*32*64+64 = 18,496개의 파라미터 

 

- Flatten 클래스에서 (7,7,64) 크기의 특성 맵을 1차원 배열로 펼치면 (3136, ) 크기의 배열이 된다. 

- 이를 100개의 뉴런과 완전히 연결해야 하므로 은닉층의 모델 파라미터 개수는 3136*100 +100 = 313,700개 이다. 

 

- 마지막 출력층의 모델 파라미터 개수는 1,010개이다. 

 

- 케라스는 층의 구성을 그림으로 표현해 주는 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)

 

 

 

3. 모델 컴파일과 훈련 

 

- Adam 옵티마이저를 사용하고 ModelCheckpoint 콜백과 EarlyStopping 콜백을 함께 사용해 조기 종료 기법을 구현 

 

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

checkpoint_cb = keras.callbacks.ModelCheckpoint('best-cnn-model.h5',
                                                save_best_only=True)
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])

 

 

- 손실 그래프를 그려서 조기 종료가 잘 이루어졌는지 확인 

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

 

- EarlyStopping 클래스에서 restore_best_weights 매개변수를 True로 지정했으므로 현재 model 객체가 최적의 모델 파라미터로 복원되어 있다. 즉 ModelCheckpoint 콜백이 저장한 best_cnn_model.h5 파일을 다시 읽을 필요가 없다. 

 

- 세트에 대한 성능 평가 

model.evaluate(val_scaled, val_target)

 

 

- predict() 메서드를 사용해 훈련된 모델을 사용하여 새로운 데이터에 대해 예측 만들기 

- 맷플롯립에서는 흑백 이미지에 깊이 차원은 없다. 따라서 (28,28,1) 크기를 (28,28)로 바꾸어 출력 

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

 

 

- 이 이미지에 대해 어떤 예측을 만드는지 확인 

- predict() 메서드는 10개의 클래스에 대한 예측 확률을 출력한다. 

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

 

- 출력 결과를 보면 아홉번째 값이 1이고 다른 값은 거의 0에 가깝다. 

 

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

 

 

- 아홉 번째 클래스가 실제로 무엇인지는 패션 MNIST 데이터셋의 정의를 참고해야 한다. 

- 클래스 리스트가 있으면 레이블을 출력하기 쉽다. preds 배열에서 가장 큰 인덱스를 찾아 classes 리스트의 인덱스로 사용하면된다. 

import numpy as np

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

 

 

- 이 샘플을 '가방'으로 잘 예측하였다. 

 

- 맨 처음에 떼어 놓았던 테스트 세트로 합성곱 신경망의 일반화 성능을 가늠 

- 훈련 세트와 검증 세트에서의 했던 것처럼 픽셀값의 범위를 0~1 사이로 바꾸고 이미지 크기를 (28,28)에서 (28,28,1)로 바꾼다. 

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

 

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

model.evaluate(test_scaled, test_target)

 

 

- 예상대로 테스트 세트에서의 점수는 검증 세트보다 조금 더 작다.