IT/[혼공머신] 혼자 공부하는 머신러닝 + 딥러닝

[혼공머신] Ch.8 이미지를 위한 인공 신경망_2. 합성곱 신경망을 사용한 이미지 분류

vulter3653 2023. 10. 11. 21:47

<혼자 공부하는 머신러닝 + 딥러닝>의 'Ch.8 트리 알고리즘_2. 합성곱 신경망을 사용한 이미지 분류'의 내용을 요약 및 정리한 내용입니다.

 

https://product.kyobobook.co.kr/detail/S000001810330

 

혼자 공부하는 머신러닝+딥러닝 | 박해선 - 교보문고

혼자 공부하는 머신러닝+딥러닝 | 혼자 해도 충분하다! 1:1 과외하듯 배우는 인공지능 자습서이 책은 수식과 이론으로 중무장한 머신러닝, 딥러닝 책에 지친 ‘독학하는 입문자’가 ‘꼭 필요한

product.kyobobook.co.kr


케라스 API를 사용해 합성곱 신경망 모델을 만들어 패션 MNIST 이미지를 분류하는 방법을 배웁니다.

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

먼저 주피터 노트북에서 케라스 API를 사용해 패션 MNIST 데이터를 불러오고 적절히 전처리해줍니다.

 

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

 

완전 연결 신경망이라면 입력 이미지를 밀집층에 연결하기 위해 일렬로 펼쳐야 하기에, 넘파이 reshape() 메서드를 사용하거나 Flatten 클래스를 사용해야 합니다. 하지만 합성곱 신경망은 2차원 이미지를 그대로 사용하기 때문에 이렇게 일렬로 펼치는 과정이 필요 없습니다.

 

다만 입력 이미지는 항상 깊이 차원이 있어야 합니다. 흑백 이미지의 경우 채널 차원이 없는 2차원 배열이지만 Conv2D 층을 사용하기 위해 마지막에 이 채널 차원을 추가해야 합니다. 그렇기에 넘파이 reshape() 메서드를 사용해 전체 배열 차원을 그대로 유지하면서 마지막에 차원을 간단히 추가해줍니다.

 

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)

 

(48000, 28, 28) 크기인 train_input이 (48000, 28, 28, 1) 크기인 train_scaled 가 되었습니다.

 

데이터를 준비가 끝났으니 이제 본격적으로 합성곱 신경망을 만들어 보겠습니다.

 

2. 합성곱 신경망 만들기

전형적인 합성곱 신경망의 구조는 합성곱 층으로 이미지에서 특징을 감지한 후 밀집층으로 클래스에 따른 분류 확률을 계산합니다. 케라스의 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)))

 

 

이 코드의 매개변수를 자세히 살펴보겠습니다. 이 합성곱 층은 32개의 필터를 사용합니다. 커널의 크기는 (3,3)이고 렐루 활성화 함수와 세임 패딩을 사용합니다. 그리고 케라스 모델의 첫 번째 층에서 입력의 차원을 나타내는 input_shape 매개변수에 앞서 변경한 패션 MNIST 이미지 차원과 동일하게 (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)가 될 것입니다. 나중에 각 층의 출력 크기를 summary() 메서드로 확인해 보겠습니다.

 

첫 번째 합성곱-풀링 층 다음에 두 번째 합성곱-풀링 층을 추가해 주겠습니다. 두 번째 합성곱-풀링 층은 첫 번째와 거의 동일하나, 필터의 개수를 64개로 늘린 점만 다릅니다.

 

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

 

첫 번째 합성곱-풀링 층과 마찬가지로 이 합성곱 층은 세임 패딩을 사용합니다. 따라서 입력의 가로세로 크기를 줄이지 않습니다. 이어지는 풀링 층에서 이 크기를 절반으로 줄입니다. 64개의 필터를 사용했으므로 최종적으로 만들어지는 특성 맵의 크기는 (7, 7, 64)가 될 것입니다.

 

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

 

은닉층과 출력층 사이에 드롭아웃을 넣었습니다. 드롭아웃 층이 은닉층의 과대적합을 막아 성능을 조금 더 개선해 줄 것입니다. 은닉층은 100개의 뉴런을 사용하고 활성화 함수는 합성곱 층과 마찬가지로 렐루 함수를 사용합니다. 패션 MNIST 데이터셋을 클래스 10개를 분류하는 다중 분류 문제이므로 마지막 층의 활성화 함수는 소프트맥스를 사용합니다.

 

이렇게 합성곱 신경망의 구성을 마쳤습니다. 케라스 모델의 구성을 마쳤으니 summary()메서드로 모델 구조를 출력해 보겠습니다.

 

 

summary() 메서드의 출력 결과를 보면 합성곱 층과 풀링 층의 효과가 잘 나타나 있습니다. 첫 번째 합성곱 층을 통과하면서 특성 맵의 깊이는 32가 되고 두 번째 합성곱에서 특성 맵의 크기가 64로 늘어납니다. 반면 특성 맵의 가로세로 크기는 첫 번째 풀링 층에서 절반으로 줄어들고 두 번째 풀링 층에서 다시 절반으로 더 줄어듭니다. 따라서 최종 특성 맵의 크기는 (7, 7, 64)입니다.

 

모델 파라미터의 개수를 계산해보면, 첫 번째 합성곱 층은 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 = 18,496 개의 파라미터가 있습니다. 

 

Flatten 클래스에서 (7, 7, 64) 크기의 특성 맵을 1차원 배열로 펼치면 (3136, ) 크기의 배열이 됩니다. 이를 100개의 뉴런과 완전히 연결해야 하므로 은닉층과 모델 파라미터 개수는 3,136 x 100 + 100 = 313,700개입니다. 마찬가지 방식으로 계산하면 마지막 출력층의 모델 파라미터 개수는 1,010개가 됩니다.

 

합성곱 신경망 모델을 잘 구성했고 각 층의 파라미터 개수도 검증해 보았습니다. 케라스는 summary() 메서드 외에 층의 구성을 그림으로 표현해 주는 plot_model() 함수를 keras.untils 패키지에서 제공합니다. 이 함수에 만든 model 객체를 넣어 호출해보겠습니다.

 

네모 상자 안의 내용 중 콜론 왼쪽에는 층의 이름이 쓰여 있고 오른쪽에는 클래스가 나타납니다. 맨 처음에 나오는 InputLayer 클래스는 케라스가 자동으로 추가해 주는 것으로 입력층의 역할을 합니다. 이 입력층은 첫 번째 Conv2D 클래스에 추가한 input_shape 매개변수를 사용합니다.

 

plot_model () 함수의 show_shapes 매개변수를 True로 설정하면 이 그림에서 입력과 출력의 크기를 표시해 줍니다. 또 to_file 매개변수에 파일 이름을 지정하면 출력한 이미지를 파일로 저장합니다. dpi 매개변수로 해상도를 지정할 수도 있습니다. 이 옵션들을 사용해 다시 그래프를 그려 보겠습니다.

 

 

오른쪽의 input, output 상자에서 층으로 입력되는 크기와 출력되는 크기가 나타나기 때문에 이해하기 훨씬 쉽습니다.

 

패션 MNIST 데이터에 적용할 합성곱 신경망 모델의 구성을 마쳤습니다. 이제 모델을 컴파일하고 훈련해 보겠습니다.

 

3. 모델 컴파일과 훈련

케라스 API의 장점은 딥러닝 모델의 종류나 구성 방식에 상관없이 컴파일과 훈련 과정이 같다는 점입니다. 이러한 코드는 완전 연결 신경망 모델을 컴파일하고 훈련하는 코드와도 거의 같습니다.

 

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

 

 

얼핏 보아도 훈련 세트의 정확도가 이전보다 훨씬 좋아진 것을 알 수 있습니다. 손실 그래프를 그려서 조기 종료가 잘 이루어졌는지 확인해보겠습니다.

 

 

검증 세트에 대한 손실이 점차 감소하다가 정체되기 시작하고 훈련 세트에 대한 손실을 점점 더 낮아지고 있습니다. 이 그래프를 기반으로 일곱 번째 에포크를 최적으로 생각할 수 있습니다.

 

EarlyStopping 클래스에서 restore_best_weights 매개변수를 True로 지정했으므로 현재 model 객체가 최적의 모델 파라미터로 복원되어 있습니다. 즉 ModelCheckpoint 콜백이 저장한 best-cnn-model.h5 파일을 다시 읽을 필요가 없습니다. 이번에는 세트에 대한 성능을 평가해 보겠습니다.

 

 

이 결과는 fit() 메서드의 출력 중 일곱 번째 에포크의 출력과 동일합니다. EarlyStopping 콜백이 model 객체를 최상의 모델 파라미터로 잘 복원한 것까지 확인했습니다.

 

이제 predict() 메서드를 사용해 훈련된 모델을 사용하여 새로운 데이터에 대해 예측을 만들어 보겠습니다. 여기에서는 편의상 검증 세트의 첫 번째 샘플을 처음 본 이미지라고 가정합니다. 맷플롯립에서는 흑백 이미지에 깊이 차원은 없습니다. 따라서 (28, 28, 1) 크기를 (28, 28)로 바꾸어 출력해야 합니다. 첫 번째 샘플 이미지를 먼저 확인해 보겠습니다.

 

 

핸드백 이미지 같습니다. 모델은 이 이미지에 대해 어떤 예측을 만드는지 확인해보겠습니다. predict() 메서드는 10개의 클래스에 대한 예측 확률을 출력합니다.

 

 

출력 결과를 보면 아홉 번째 값이 1이고 다른 값은 거의 0에 가깝습니다. 다시 말해 아홉 번째 클래스라고 말하고 있는 것입니다. 이를 막대그래프로 그리면 더욱 확실하게 알 수 있습니다.

 

 

정말 그렇습니다. 다른 클래스의 값을 사실상 모두 0입니다. 아홉 번째 클래스가 실제로 무엇인지는 패션 MNIST 데이터셋의 레이블을 통해 알 수 있으며, 이번에는 파이썬에서 레이블을 다루기 위해 리스트로 저장하겠습니다.

 

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

 

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

 

 

해당 샘플을 '가방'으로 잘 예측한 것 같습니다. 이렇게 합성곱 신경망을 만들고 훈련하여 새로운 샘플에 대해 예측을 수행하는 방법도 알아보았습니다. 마지막으로 맨 처음에 떼어 놓았던 테스트 세트로 합성곱 신경망의 일반화 성능을 가늠해 보겠습니다. 즉 이 모델을 실전에 투입했을 때 얻을 수 있는 예상 성능을 측정해 보겠습니다.

 

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

 

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

 

그다음 evalute() 메서드로 테스트 세트에 대한 성능을 측정합니다.

 

 

역시 예상대로 테스트 세트에서의 점수는 검증 세트보다 조금 더 작습니다. 이 모델을 실전에 투입하여 패션 아이템을 분류한다면 약 91.5%의 성능을 기대할 수 있을 것입니다!

 

4. 케라스 API로 합성곱 신경망 구현

이번에는 케라스 API를 사용해 합성곱 신경망을 만들어 보았습니다. 케라스 Conv2D 클래스를 사용해 32개의 필터와 64개의 필터를 둔 2개의 합성곱 층을 추가했습니다. 두 합성곱 층 다음에는 모두 (2,2) 크기의 최대 풀링 층을 배치했습니다. 두 번째 풀링 층을 통과한 특성 맵을 펼친 다음 밀집 은닉층에 연결하고 최종적으로 10개의 뉴런을 가진 출력층에서 샘플에 대한 확률을 출력했습니다.

 

이후 조기 종료 기법을 모델을 훈련한 다음 검증 세트로 최적의 에포크에서 성능을 평가했습니다. 또 샘플 데이터 하나를 선택해 예측 클래스를 출력하는 방법을 살펴보았습니다.

 

마지막으로 이제까지 사용하지 않았던 테스트 세트를 사용해 최종 모델의 일반화 성능을 평가하였습니다. 항상 테스트 세트는 모델을 출시하기 직전 딱 한 번만 사용해야 합니다. 그렇지 않다면 모델을 실전에 투입했을 때 성능을 올바르게 예측하지 못합니다.

 

이렇게 합성곱 신경망은 이미지를 주로 다루기 때문에 각 층의 출력을 시각화하기가 좋습니다.