Python/ML & DL 공부

[DL] 08-2 합성곱 신경망을 사용한 이미지 분류 (+ 08-1)

dori_0 2022. 4. 6. 14:46

혼자 공부하는 머신러닝+딥러닝 책을 바탕으로 공부한 내용입니다.

 

CH8 이미지를 위한 인공 신경망 ①②

케라스 API로 합성곱 신경망 구현하기

 


 

케라스 API를 사용해 합성곱 신경망 모델을 만들어 패션 MNIST 이미지를 분류해보자

 

▶ 08-1장

합성곱 신경망의 개념을 설명한 단원이라 키워드만 정리하고 넘어가겠습니다

  • 합성곱
    1. 밀집층과 비슷하게 입력과 가중치를 곱하고 절편을 더하는 선형 계산
    2. 밀집층과 달리 각 합성곱은 입력 전체가 아닌 일부만 사용하여 선형 계산을 수행함
    3. 합성곱 층의 필터는 밀집층의 뉴런에 해당 (필터의 가중치와 절편을 종종 커널이라고 부름)
    4. 자주 사용되는 커널의 크기는 (3, 3) 또는 (5, 5), 커널의 깊이는 입력의 깊이와 같음
  • 특성 맵
    1. 합성곱 층이나 풀링 층의 출력 배열을 의미
    2. 필터 하나가 하나의 특성 맵을 만든다 (합성곱 층에서 5개의 필터를 적용하면 5개의 특성 맵이 만들어짐)
  • 패딩
    1. 합성곱 층의 입력 주위에 추가한 0으로 채워진 픽셀
    2. 밸리드 패딩 - 패딩을 사용하지 않는 것
    3. 세임 패딩 - 합성곱 층의 출력 크기를 입력과 동일하게 만들기 위해 입력에 패딩을 추가하는 것
  • 스트라이드 
    1. 합성곱 층에서 필터가 입력 위를 이동하는 크기
    2. 일반적으로 스트라이드는 1픽셀을 사용합
  • 풀링
    1. 가중치가 없고 특성 맵의 가로세로 크기를 줄이는 역할을 수행
    2. 대표적으로 최대풀링과 평균풀링이 있으며 (2, 2) 풀링으로 입력을 절반으로 줄임

 

 

▶ 패션 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
train_scaled, val_scaled, train_target, val_target = train_test_split(
    train_scaled, train_target, test_size=0.2, random_state=42)

완전 연결 신경망에서는 입력 이미지를 밀집층에 연결하기 위해 일렬로 펼쳐야 했지만, 합성곱 신경망은 2차원 이미지를 그대로 사용하기 때문에 일렬로 펼치지 않아도 된다.

  • 입력 이미지는 항상 깊이(채널) 차원이 있어야 하기 때문에 reshape() 메서드를 사용하여 마지막에 차원을 추가했다.

 

 

▶ 합성곱 신경망 만들기

먼저 Sequential 클래스의 객체를 만들고 첫 번째 합성곱 층인 Conv2D를 추가해주자

# 첫번째 합성곱
model = keras.Sequential()
model.add(keras.layers.Conv2D(32, kernel_size=3, activation='relu',
                              padding='same', input_shape=(28, 28, 1)))
  • 32개의 필터 사용, 커널의 크기는 (3, 3), 렐루 활성화 함수와 세임 패딩을 사용
  • 완전 연결 신경망에서처럼 케라스 신경망 모델의 첫번째 층에서 입력의 차원을 지정해주어야 함

 

(2, 2) 풀링 층을 추가해주면

# 풀링 층 추가
model.add(keras.layers.MaxPooling2D(2))  #(14, 14, 32)
  • 32개의 필터를 사용했으므로 이 특성 맵의 깊이는 32가 된다.
  • 최대 풀링을 통과한 특성 맵의 크기는 (14, 14, 32)가 될 것이다.

 

 

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

# 두번째 합성곱-풀링 층 추가
model.add(keras.layers.Conv2D(64, kernel_size=3, activation='relu',
                              padding='same'))
model.add(keras.layers.MaxPooling2D(2))  #(7, 7, 64)
  • 64개의 필터를 사용했으므로 최종적으로 만들어지는 특성 맵의 크기는 (7, 7, 64)이다.

 

이제 이 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개의 뉴런을 사용하고 활성화 함수는 렐루 함수이다.
  • 출력층의 활성화 함수는 소프트맥스이다. (다중 분류 문제이므로)

 

 

케라스 모델의 구성을 마쳤으니 모델 구조를 출력해보자

# 모델 구조 출력
model.summary()

 

 

plot_model() 함수를 사용하면 층의 구성을 그림으로 표현해준다.

keras.utils.plot_model(model)

  • 왼쪽에는 층의 이름, 오른쪽에는 클래스가 쓰여있다.
  • 맨 처음의 InputLayer 클래스는 케라스가 자동으로 추가해주는 것으로 입력층의 역할을 해준다. 

 

 

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

keras.utils.plot_model(model, show_shapes=True)

  • to_file 매개변수에 파일 이름을 지정하면 출력한 이미지를 파일로 저장한다.
  • dpi 매개변수로 해상도를 지정할 수도 있다.

 

 

패션 MNIST 데이터에 적용할 합성곱 신경망 모델의 구성을 마쳤다.

이제 모델을 컴파일하고 훈련해보자

 

 

▶ 모델 컴파일과 훈련

케라스 API의 장점은 딥러닝 모델의 종류나 구성 방식에 상관없이 컴파일과 훈련 과정이 같다는 점이다.

 

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

  • 검증 세트에 대한 손실이 감소하다가 정체되기 시작하고 훈련 세트에 대한 손실은 점점 낮아지고 있다.
  • 9번째 에포크가 최적의 에포크라고 생각 된다.

 

 

EarlyStopping 클래스에서 restore_best_weights 매개변수를 True로 지정했기 때문에

현재 model 객체가 최적의 모델 파라미터로 복원되어 있다. (best-cnn-model.h5 파일 다시 읽을 필요 X)

 

이번에는 세트에 대한 성능을 평가해보자

# 성능 평가
model.evaluate(val_scaled, val_target)

  • fit() 메서드의 출력 중 9번째 에포크의 출력과 동일하다.
  • EarlyStopping 콜백이 model 객체를 최상의 모델 파라미터로 잘 복원한 것으로 보인다.

 

 

이번에는 predict() 메서드를 사용해 훈련된 모델을 사용하여 새로운 데이터에 대해 예측을 만들어 볼 것이다.

( 첫 번째 샘플을 처음 본 이미지라고 가정 후 해보자 )

# 첫 번째 이미지 확인
plt.imshow(val_scaled[0].reshape(28, 28), cmap='gray_r')
plt.show()

  • 흑백 이미지는 깊이 차원이 없으므로 (28, 28, 1) 크기를 (28, 28)로 바꿔주었다.

 

 

이 핸드백 이미지에 대해 모델은 어떤 예측을 만드는지 확인해보자

# 10개 클래스에 대한 예측 확률 출력
preds = model.predict(val_scaled[0:1])
print(preds)

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

  • 9번째 값일 확률이 1이고 다른 값들은 거의 0에 가까운 것을 확인할 수 있다.

 

실제로 9번째 값이 무엇인지 MNIST 데이터셋의 레이블을 통해 확인해보면

import numpy as np

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

이 샘플을 '가방'으로 잘 예측한 것을 확인할 수 있다.

 

 

마지막으로 이 모델을 실전에 투입했을 때 어느 정도의 성능을 낼 수 있는지 확인해보자

훈련 세트와 검증 세트에서 했던 것처럼 픽셀값의 범위를 0~1 사이로 바꾼 뒤 reshape 해주었다.

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

  • 이 모델을 실전에 투입하여 패션 아이템을 분류한다면 약 92%의 성능을 기대할 수 있을 것으로 보인다

 

 

 

▶ 진행한 순서

  1. 케라스의 Conv2D 클래스를 사용해 32개, 64개의 필터를 둔 2개의 합성곱 층을 추가
  2. 두 합성곱 층 다음에는 모두 (2, 2) 크기의 최대 풀링 층을 배치
  3. 두 번째 풀링 층을 통과한 특성 맵을 펼친 다음 밀집 은닉층에 연결
  4. 최종적으로 10개의 뉴런을 가진 출력층에서 샘플에 대한 확률을 출력
  5. 7장에서 사용했던 조기 종료 기법을 사용해 모델 훈련 후 검증 세트로 최적의 에포크에서 성능을 평가
  6. 샘플 데이터 하나를 선택해 예측 클래스를 출력하는 방법을 살펴봄
  7. 테스트 세트를 사용해 최종 모델의 일반화 성능을 평가