합성곱 신경망(CNN)에 대한 이론이 필요하신 분들은 아래 링크를 참조해주시기 바랍니다.
[인공지능][개념] 합성곱 신경망(CNN) - 패딩(Padding)과 스트라이드(Strides), 풀링(Pooling) 완전정복하기
: https://itstory1592.tistory.com/23
지난 포스팅에서 합성곱 신경망(CNN)에 대한 구조와 개념에 대해 알아보았다.
패딩(Padding)을 통해 입력 크기와 출력 크기를 동일하게 만들어주고, 풀링(Pooling)층을 사용하여 특성맵의 크기를 줄이는 방법을 알 수 있었다.
합성곱 신경망은 이미지를 분류할 때 효과적인데,
예를 들어 숫자 이미지를 학습시켜 무슨 숫자인지를 맞춘다거나, 여러 옷 이미지 중에서 어떤 카테고리에 속하는 의류인지 맞추는 모델을 만들 수 있다.
이번 포스팅에서는 합성곱 신경망을 통해 의류 이미지 데이터를 학습시키고 옷 카테고리를 맞추는 모델을 구현해보자.
패션 아이템 분류 모델 구현하기
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)
모델을 학습시키기 위해서는 데이터셋이 필요한데, 우리는 옷 카테고리를 분류하는 모델을 구현할 것이기 때문에
케라스(Keras)에서 제공해주는 패션 MNIST 데이터셋을 사용하도록 하자.
데이터셋을 불러왔다면 데이터의 형태를 살짝 변형(reshape)시켜줄 필요가 있다.
reshape() 함수의 첫 번째 매개변수는 몇 개의 데이터를 변형할지 선택할 수 있는데, 우리는 -1을 입력하여 모든 데이터를 변형시킬 것이다.
그리고 '패션MNIST 데이터셋'은 28 x 28 픽셀 크기이며, Conv2D(합성곱)층은 3차원 배열을 입력받기 때문에
2차원 흑백 이미지여도 데이터를 3차원으로 만들어주어야 한다.
따라서 (28, 28, 1)을 입력해주어야 하며,
만약 컬러 이미지라면 RGB 채널에 맞게 마지막 매개변수를 '3'으로 설정해주면 된다.
마지막으로 픽셀의 색상은 0 ~ 255 사이의 16진수로 이루어져 있는데, 각 픽셀의 값이 너무 많은 차이가 나면 올바른 손실 함수를 구할 수 없어 모델의 성능이 좋지 않을 수 있다.
때문에 255.0으로 나누어 전처리(Scaling)를 해주고, 훈련 데이터와 검증 데이터로 분리시켜준다.
model = keras.Sequential()
model.add(keras.layers.Conv2D(32, kernel_size=3, activation='relu', padding='same', input_shape=(28,28,1)))
model.add(keras.layers.MaxPooling2D(2))
model.add(keras.layers.Conv2D(64, kernel_size=(3,3), activation='relu', padding='same'))
model.add(keras.layers.MaxPooling2D(2))
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()
합성곱 신경망의 구조는 합성곱 층으로 이미지에서 특징을 감지한 후 밀집층으로 클래스에 따른 분류 확률을 계산한다.
먼저 Sequential 클래스의 객체를 만들고 첫 번째 합성곱 층인 Conv2D를 추가한다.
이 합성곱 층은 32개의 필터를 사용하고, 커널의 크기는 (3, 3)이며, 렐루 함수와 세임 패딩을 사용하여 입출력 크기를 동일하게 만들어 준다.
그다음 풀링층(Pooling Layer)을 추가한다.
케라스는 최대 풀링(Max Pooling)과 평균 풀링(Average Pooling) 모두 keras.layers 패키지 아래 MaxPooling2D와 AveragePooling2D 클래스로 지원한다.
해당 예제에서는 전형적인 풀링 크기인 (2, 2) 풀링을 사용하여 특성 맵의 크기를 절반으로 줄일 것이다.
그럼 32개의 특성 맵의 크기가 기존 (28, 28)에서 (14, 14)으로 줄어들게 된다.
첫 번째 합성곱ㅡ풀링 층 다음에 두 번째 합성곱ㅡ풀링층을 추가해보자.
두번째 합성곱ㅡ풀링층은 필터의 개수를 64개로 늘린 것 외에는 첫 번째와 동일하다.
그다음 Flatten 클래스로 특성맵을 펼쳐주어야 하는데, 그 이유는 마지막에 10개의 뉴런을 가진 출력층에서 확률을 계산하기 때문이다.
중간에 렐루 함수를 사용하는 은닉층을 추가하고 드롭아웃(Dropout)을 넣은 후,
패션 MNIST 데이터셋은 10개의 클래스를 분류하는 다중 분류 문제이기 때문에 활성화 함수로 소프트맥스를 사용하는 층으로 마무리한다.
이렇게 합성곱 신경망의 구성을 마쳤다.
다소 복잡해 보일 수 있지만, 이전 포스팅에서 배운 커널, 패딩, 풀링 등에 대한 개념을 확실하게 잡았다면 해당 코드가 어느 정도 이해가 될 것이다.
이제 모델의 구성을 마쳤으니 summary() 메소드를 통해 모델의 구조를 출력해보자.
summary() 메소드로 출력 결과를 확인해보면 합성곱 층과 풀링 층의 효과가 잘 나타나 있다.
데이터가 첫 번째 합성곱 층을 통과하면서 특성 맵의 개수는 32개가 되어 (28, 28, 32) 형태를 띠고 있고,
두 번째 합성곱에서는 특성 맵의 크기가 64로 늘어난다.
중간중간에 풀링층이 포함되어 있어 특성맵의 크기가 줄어드는 모습도 확인할 수 있다.
keras.utils.plot_model(model)
케라스는 summary() 메소드 외에도 층의 구성을 그림으로 표현해주는 plot_model() 메소드를 제공해준다.
메소드의 매개변수로 모델 객체를 넣어 호출하면 아래와 같은 이미지가 출력된다.
네모 상자 안의 내용 중 콜론 왼쪽에는 층의 이름이 쓰여 있고 오른쪽에는 클래스가 나타난다.
맨 처음 나오는 InputLayer 클래스는 케라스가 자동으로 추가해 주는 것으로 입력층 역할을 한다.
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 상자에 층으로 입력되는 크기와 출력되는 크기가 모두 나타나기 때문에 이해가 훨씬 쉽다.
한번 그림과 설명을 비교해보면서 이해하면 합성곱 층의 전반적인 구조를 파악하는 데에 도움이 될 것이다.
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])
이제 모델을 컴파일하고 훈련해보자.
옵티마이저(Optimizer)는 'adam'을 사용하고, 콜백(Callback)으로 ModelCheckpoint와 EalryStopping을 사용하여
최상의 매개변수를 저장하고, 조기종료(patience = 2)하는 합성곱을 구현하였다.
실제로 코드를 훈련시켜보면 지금까지 했던 예제와는 달리 수많은 이미지 데이터로 모델을 학습시키는 것이기 때문에 훈련 시간이 꽤 오래 소모될 것이다.
원래 같았으면 에포크 횟수 20번을 수행해야 하지만, 조기종료(EalryStopping)에 의해 9번 만에 훈련을 끝마쳤다.
훈련 과정에서 검증 손실(val_loss)을 확인해보면 7 epoch 이후로 손실 값이 다시 높아지기 때문에 patience가 2인 조기종료 객체가 모델의 훈련을 종료시킨 듯하다.
그래프를 통해 훈련 손실과 검증 손실 곡선을 파악해보자.
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()
파이썬 배열은 0부터 시작하기 때문에 X축이 0 ~ 8까지 표시되어 있지만, 에포크는 총 9회 실시되었다.
그래프를 확인해보아도 7번째 에포크에 가장 낮은 검증 손실을 기록하고 그 이후로 손실 값이 다시 증가하기 시작한다.
모델이 적절한 시기에 조기 종료되었다.
이제 모델을 평가하여 손실 값과 정확도가 몇인지 확인해보겠다.
model.evaluate(val_scaled, val_target)
ModelCheckpoint 콜백을 추가하였기 때문에 모델이 자체적으로 최상의 모델 파라미터를 저장하였다.
훈련 과정 결과에서 7번째 에포크와 동일한 점수를 나타내고 있다.
이처럼 합성곱과 콜백을 적절히 사용하면 모델을 끝까지 학습시킬 필요가 없기 때문에,
훈련 시간을 절약할 수 있고 최상의 가중치와 절편을 얻을 수 있다.
모델이 최상의 모델 파라미터로 학습을 마쳤으므로 특정 데이터를 주고 예측을 하는 실습을 해보자.
plt.imshow(val_scaled[0].reshape(28, 28), cmap='gray_r')
plt.show()
케라스와 다르게 맷플롭립에서는 흑백 이미지에 깊이 차원이 없다.
따라서 (28, 28, 1) 크기를 (28, 28)으로 다시 바꾸어 출력해주어야 한다.
검증 데이터 중에서 첫 번째 데이터를 선택하여 (28, 28) 크기로 reshape한 후 어떤 아이템인지 출력해보자.
X Y축 선상에 핸드백으로 보이는 이미지가 출력되었다.
모델은 이 이미지에 대해 어떤 예측을 하는지 확인해보겠다.
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()
막대 그래프로 결과를 출력해보아도 아홉 번째 클래스에 대한 확률을 제외한 다른 클래스의 확률은 눈에 보이지 않을 정도로 0에 가까움을 알 수 있다.
아홉 번째 클래스가 실제로 무엇인지 패션 MNIST 데이터셋의 정의를 참고하여 확인해보자.
import numpy as np
classes = ['티셔츠', '바지', '스웨터', '드레스', '코트',
'샌달', '셔츠', '스니커즈', '가방', '앵클 부츠']
print(classes[np.argmax(preds)])
클래스 리스트가 있으면 레이블을 출력하기 쉽다.
넘파이의 argmax() 메소드를 사용하여 preds 배열에서 가장 큰 인덱스를 찾아 classes 리스트의 인덱스로 전달해주면,
아래와 같은 결과가 출력된다.
모델이 샘플을 정확히 예측한듯하다.
패션 MNIST 레이블에서는 우리가 핸드백처럼 보인다는 이미지 데이터를 '가방'으로 분류하고 있다.
이 외에도 다른 샘플을 맞춰보도록 하고 싶다면 글 맨 아래에 위치한 전체 소스코드 링크로 들어가서,
검증 세트(val_sacled)의 인덱스를 0이 아닌 다른 값으로 변경해보며 테스트할 수 있다.
이제 맨 처음에 떼어 놓았던 테스트 세트로 합성곱 신경망의 일반화 성능을 가늠해보며 마무리하겠다.
즉, 이 모델이 실전에 투입됐을 때 얻을 수 있는 예상 성능을 측정하는 것이다.
test_scaled = test_input.reshape(-1, 28, 28, 1) / 255.0
model.evaluate(test_scaled, test_target)
evaluate() 메소드로 테스트 세트에 대한 성능을 측정해본 결과,
이 모델을 실전에 투입하여 패션 아이템을 분류한다면 대략 91% 정도의 성능을 얻을 수 있다고 말해주고 있다.
이처럼 테스트 세트는 검증 세트와 달리 맨 마지막에 딱 한 번만 사용해야 모델을 실전에 투입했을 때의 성능을 올바르게 예측할 수 있다.
이번 포스팅에서는 케라스 API를 사용하여 합성곱 신경망(CNN)의 주요 개념을 직접 모델로 구현해보았다.
합성곱 신경망은 이미지를 주로 다루기 때문에 각 층의 출력을 시각화하기 좋다.
맨 아래에 이번 포스팅에 대한 코드 링크를 올려놓았으니, 패딩, 커널 사이즈, 필터 개수, 풀링의 종류를 변경해가며 모델을 훈련해보아도 좋다.
전체 소스 코드 :
https://colab.research.google.com/drive/1Wh7mJ03iPFR-rY9UKwEfgQB5NSes1pNk
👍클릭으로 구독하기👍
(이해가 다소 힘들거나, 틀린 부분이 있다면 댓글 부탁드리겠습니다! 😊)
💖도움이 되셨다면 '구독'과 '공감' 부탁드립니다!💖