이미지를 위한 인공 신경망

신민제·2023년 8월 5일
0

머신러닝, 딥러닝

목록 보기
5/6

이번 시간은 이미지를 위한 인공 신경망에 대해서 알아보겠다.

먼저 배워볼 것은 합성곱 신경망을 구성하는 기본 개념동작 원리이고, 간단한 합성곱, 폴링 계산 방법을 익혀볼 것이다.

합성곱 신경망의 구성 요소

합성곱 신경망(Convolutional Neural Network, ConvNet)이란?

합성곱 신경망은, 시각적 이미지 분석에 주로 사용되는 신경망으로, 입력 이미지로부터 유용한 특성만 추출하여 입력 이미지가 어떤 이미지인지 클래스로 분류하는 신경망이다. 인공 신경망의 경우 입력 데이터 전체에 가중치를 적용하는 반면, 합성곱 신경망의 경우 적은 수의 가중치를 곱하게 된다. 합성곱에서는 뉴런이라는 말 대신 필터라고 부르고, 필터에서 말하는 가중치커널이라고 한다. 필터를 커널이라고 혼용하는 경우도 많다. 그리고, 출력으로 얻게 된 결과를 특성 맵이라고 한다.

중요한 점 하나는 합성곱 층에서도 여러 개의 필터를 사용할 수 있다. 위 그림의 경우 필터(커널)이 한 개여서 출력이 2x2인 2차원인 특성 맵이 결과이다. 만약 서로 다른 필터 3개를 사용한다면 특성맵이 3개가 출력되므로 2x2x3인 3차원의 결과가 나타나게 된다. 결국 앞의 두 차원은 필터를 의미하고, 마지막 차원은 필터의 개수를 의미한다.


(출처: https://velog.velcdn.com/images%2Fjaehyeong%2Fpost%2F7da83172-a056-48b8-914a-67a37a472917%2Fconvnet.jpg)

위는 합성곱 신경망의 구조이다. 합성곱 신경망은 크게 3덩어리로 이루어져있다.

1. Input layer
2. Convolutional layer ~ Max pooling layer
3. Fully-connected layer ~ Output layers로

위 구조를 간단히 설명하면 위에서도 말했듯이 ConvNet은 단순 FC Layer로만 구성되어 있지 않다. Convolutional Layer와 Pooling Layer라는 과정을 거치게 되는데 이는 Input Image의 주요 특징 벡터를 추출하는 과정이라고 할 수 있다. 그 후 이렇게 추출된 주요 특징 벡터들은 그제야 FC Layer를 거치며 1차원 벡터로 변환되고 마지막 Output layer에서 활성화 함수인 Softmax함수를 통해 각 해당 클래스의 확률로 출력되게 된다. 아래에서 각 과정을 좀 더 상세히 살펴보겠다.

1. Input Layer

  • Input Layer는 입력된 이미지 데이터가 최초로 거치게 되는 Layer이다. 모두가 알고 있듯이 이미지는 단순 1차원의 데이터가 아니다. 이미지는 기본적으로 (높이, 넓이, 채널)의 크기를 갖는 3차원의 크기를 가지며, 여기서 채널(channels)의 경우 Gray Scale(1)이냐 RGB(3)이냐 에 따라 크기가 달라지게 된다. (채널의 컬러 공간은 Gray, RGB, HSV, CMYK 등 다양하다)

위와 같은 형태는 높이 4, 넓이 4, 채널 RGB를 갖고 있으므로 위 이미지의 shape은 (4, 4, 3)으로 표현할 수 있으며, 이미지 인식의 교과서라 할 수 있는 위 MNIST 손글씨 데이터 셋의 경우 높이 28, 넓이 28, 채널 Gray를 가지고 있으므로 (28, 28, 1)의 shape을 가졌다고 말할 수 있다. 또한 다른 말로 특성 맵(Feature Map)이라고도 한다.

2. Convolutional Layer

  • Convolutional Layer와 FC Layer의 경우 근본적은 차이가 존재하는데, Dense 층의 경우 특성 공간에 있는 전역 패턴(입력된 이미지의 모든 픽셀에 걸친 패턴)을 학습하는 반면 합성곱 층의 경우 지역 패턴을 학습하게 된다.
  • kernel
    : Convolutional Layer에서는 Input Image의 크기인 특성 맵(Feature Map)을 입력으로 받게 되는데 지역 패턴 학습을 위하여 이러한 특성 맵에 커널(kernel) 혹은 필터(Filter)라 불리는 정사각 행렬을 적용하며 합성곱 연산을 수행하게 된다.
    커널의 경우 3 x 3, 5 x 5크기로 적용되는 것이 일반적이며 스트라이드(Stride)라고 불리는 지정된 간격에 따라 순차적으로 이동하게 된다.


위 그림의 경우 Image의 크기는 (5, 5, 1)의 크기를 가지고 있으며, 현재 3 x 3크기의 kernel이 1 Stride의 간격으로 이동하며 합성곱 연산을 수행하는 것을 보여준다. 만약 커널이 2개의 크기만큼 이동하고 있다면 2 Stride 간격으로 이동한다고 할 수 있다.

이렇게 커널은 스트라이드 간격만큼 순회하며 모든 채널의 합성곱의 합을 새로운 특성 맵으로 만들게 되며, 결국 위 그림의 경우 커널과 스트라이드의 상호작용으로 인해 원본 (5, 5, 1) 크기의 Feature Map아 (3, 3, 1)크기의 Feature Map의 크기로 줄어들게 되었다.

커널과 스트라이드의 경우 크기가 클 수 록 좀더 빨리 이미지를 처리할 수 있지만, 넓은 특성을 큰 보폭으로 이동하는 만큼 주요 특성을 놓칠 수 있다는 단점이 존재한다.

3. Padding

합성곱 연산을 수행할 경우 단점이 존재하는데, 바로 위에서 살펴보았듯이 kernel과 stride의 작용으로 인해 원본 크기가 줄어든다는 것이다. 따라서 이렇게 Feature Map의 크기가 작아지는 것을 방지하기 위해서 Padding이란 기법을 이용하게 되는데, 쉽게 말해 단순히 원본 이미지에 0이라는 padding값을 채워 넣어 이미지를 확장한 후 합성곱 연산을 적용하는 것을 말한다.

위 그림을 보면 위에서 살펴본 바와 같이 똑같은 (5, 5, 1)크기의 이미지 데이터가 놓여있다. 다른 점은 사방으로 빈 공간(0)이 1칸씩 더 채워져 있다는 것인데 이것이 바로 padding이다. 이후 위와 똑같은 3 x 3 크기의 커널을 적용하게 되는데 출력되는 feature map의 크기는 (3, 3, 1)이 아닌 원본 이미지와 똑같은 (5, 5, 1)크기의 feature map이다.

이렇듯, 원본 이미지의 크기를 줄이지 않으면서 합성곱 연산을 수행가능하게 해주는 것이 바로 padding의 역할이라고 할 수 있다.

4. Pooling Layer

Convolutional Layer와 유사하게 feature map의 차원을 다운 샘플링하여 연산량을 감소시키고 주요한 특징 벡터를 추출하여 학습을 효과적으로 하는 것이 pooling layer의 역할이라고 할 수 있다.

풀링 연산에는 대표적으로 두 가지가 사용된다.

  • Max Pooling : 각 커널에서 다루는 이미지 패치에서 최대값을 추출
  • Average Pooling: 각 커널에서 다루는 이미지 패치에서 모든 값의 평균을 반환

하지만 대부분의 ConvNet에서는 Avg Pooling이 아닌 Max Pooling이 사용된다. Avg Pooling의 경우 각 커널의 값을 평균화시키기 때문에 주요한 가중치를 갖는 value의 특성이 희미해질 수 있는 문제가 있기 때문이다.

또한 Pooling 사이즈의 경우 Stride와 같은 크기로 설정하여 모든 원소가 한번씩 처리되도록 하는것이 일반적이며, 보통 Max Pooling의 경우 2 x 2커널과 2 stride를 사용하여 feature map을 절반 크기로 다운샘플링하게 된다.

5. Fully Connected Layer

위에서 설명한 Convolutional Layer - ReLU Activation Function - Pooling Layer의 과정을 거치며 차원이 축소 된 feature map은 최종적으로 Fully Connected Layer라는 완전 연결 층으로 전달되게 된다.

이 부분에서는 이미지의 3차원 벡터는 1차원으로 Flatten되게 되고 신경망에서 흔히 사용되는 활성화 함수(relu)와 함께 Output Layer로 학습이 진행된다.
Output Layer는 Softmax 활성화 함수가 사용되는데, Softmax 함수는 입력받은 값을 모두 0 ~ 1사이의 값으로 정규화하하고 이렇게 정규화된 값들의 총합은 항상 1이되는 특성을 가지는 함수이다.

따라서 마지막 Output layer의 softmax함수를 통해 이미지가 각 레이블에 속할 확률값이 레이블마다 각각 출력되게 되고 이중 가장 높은 확률값을 가지는 레이블이 최종 예측치로 선정되게 된다.

합성곱 신경망을 사용한 이미지 분류

다음은 텐서플로와 케라스 API로 실제 합성곱 신경망을 만들어 보겠다. 먼저 데이터를 불러와보자.

주의할 점은 입력 이미지는 항상 깊이(채널) 차원이 있어야 한다. 흑백 이미지의 경우 채널 차원이 없는 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()
   
#reshape() 메서드를 사용해 전체 배열 차원을 그대로 유지하며 마지막에 차원을 간단히 추가하기   
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)

합성곱 신경망 만들기

앞서 설명했듯이, 합성곱 신경망의 구조는 합성곱 층으로 이미지에서 특징을 감지한 후 밀집층으로 클래스에 따른 분류 확률을 계산한다. 먼저 케라스의 Sequential 클래스를 사용해 순서대로 이 구조를 정의해 보자.

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

코드를 분석해보면, 이 합성곱 층은 32개의 필터를 사용하고 커널의 크기는 (3,3) 이다.

다음은 폴링 층을 추가해보자.

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

패션 MNIST 이미지가 (28,28) 크기에 세임 패딩을 적용했기에 합성곱 층에서 출력된 특성 맵의 가로세로 크기는 입력과 동일하다. 다음 (2,2) 풀링을 적용했기에 특성 맵의 크기는 절반으로 줄어들게 된다. 또한 합성곱 층에서 32개의 필터를 사용했기 때문에 특성 맵의 깊이는 32가 된다.

#첫 번째 합성곱-풀링 층 다음 두 번째 층 추가해보기
model.add(keras.layers.Conv2D(64, kernel_size=3, activation='relu', padding='same'))
model.add(keras.layers.MaxPooling2D(2))

첫 번째 합성곱-풀링 층과 마찬가지로 이 합성곱 층은 세임 패딩을 사용한다. 따라서 입력의 가로 세로 크기를 줄이지 않았다. 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))

#다중 분류 문제이기 Softmax 함수 사용하기
model.add(keras.layers.Dense(10, activation='softmax'))

#모델 구조 출력
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
_________________________________________________________________

모델의 구조를 살펴보면 합성곱 층과 풀링 층의 효과가 잘 나타나 있다. 첫 번째 합성곱 층을 통과하면서 특성 맵의 깊이는 32가 되고, 두 번째 합성곱에서 특성 맵의 크기가 64로 늘어난다.
합성곱 신경망이 잘 구성된 것을 볼 수 있다.

모델 컴파일과 훈련

#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번째 에포크가 최적인 것으로 생각할 수 있다. 이번에는 세트에 대한 성능을 평가해보자.

model.evaluate(val_scaled, val_target)
375/375 [==============================] - 5s 15ms/step - loss: 0.2213 - accuracy: 0.9204
[0.22125719487667084, 0.9204166531562805]

EarlyStopping 콜백이 model 객체를 최상의 모델 파라미터로 잘 복원한 것 같다.

그럼 샘플 이미지도 한번 확인해 보자.

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/1 [==============================] - 0s 26ms/step
[[1.9030054e-19 7.5744140e-31 2.1325303e-22 2.2798049e-21 1.5084044e-23
  5.9712011e-17 6.8054902e-20 2.5462222e-18 1.0000000e+00 1.3888053e-18]]

출력 결과를 보면 9번째 값이 1이고 다른 값은 거의 0에 가까운 것을 볼 수 있다. 즉 9번째 클래스라고 예측한다는 것이다. 이를 막대그래프로 그려보면 확실히 이해할 수 있을 것이다.

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

다른 클래스들의 값은 0이고, 9번째 클래스의 값만 1인 것을 볼 수 있다. 9번째 클래스가 실제로 무엇인지는 패션 MNIST 데이터셋의 정의를 참고해야 한다. 파이썬에서 레이블을 다루기 위해 리스트로 저장해보자.

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

import numpy as np

print(classes[np.argmax(preds)])

가방

이 샘플을 가방으로 잘 예측한 것 같다.

합성곱 신경망의 시각화

가중치 시각화

합성곱 층은 여러 개의 필터를 사용해 이미지에서 특징을 학습한다. 그럼 앞서 만든 모델이 어떤 가중치를 학습했는지를 확인하기 위해 체크포인트 파일을 읽어보자.

from tensorflow import keras
model = keras.models.load_model('best-cnn-model.h5')
#model_layers 출력해보기
model.layers
[<keras.layers.convolutional.conv2d.Conv2D at 0x7979cc4b92d0>,
 <keras.layers.pooling.max_pooling2d.MaxPooling2D at 0x7979cc4b8d30>,
 <keras.layers.convolutional.conv2d.Conv2D at 0x7979cd0f8b50>,
 <keras.layers.pooling.max_pooling2d.MaxPooling2D at 0x7979cd0fa4d0>,
 <keras.layers.reshaping.flatten.Flatten at 0x7979cd0fbdc0>,
 <keras.layers.core.dense.Dense at 0x7979cd0fb7f0>,
 <keras.layers.regularization.dropout.Dropout at 0x7979cd0f9e10>,
 <keras.layers.core.dense.Dense at 0x7979cc44d7b0>]

다음은 첫 번째 합성곱 층의 가중치를 조사해보겠다. Layers 속성의 첫 번째 원소를 선택해 weights 의 첫 번째 원소와 두 번째 원소의 크기를 출력해보자.

conv = model.layers[0]
print(conv.weights[0].shape, conv.weights[1].shape)

(3, 3, 1, 32) (32,)

이전에 커널 크기를 (3,3)으로 지정했었다. 이 합성곱 층에 전달되는 입력의 깊이가 1이므로 실제 커널의 크기는 (3,3,1)이다. 또, 필터의 개수가 32개 이므로 Weights의 첫 번째 원소인 가중치의 크기는 (3,3,1,32) 가 된다. Weights의 두 번째 원소는 절편의 개수를 나타낸다. 필터마다 1개의 절편이 있으므로 (32,) 가 되는 것이다.

다음은 numpy 메서드를 이용해 넘파이 배열로 바꾸고 가중치 배열의 평균과 표준편차를 계산해보자.

conv_weights = conv.weights[0].numpy()
print(conv_weights.mean(), conv_weights.std())

-0.020620763 0.22533041

가중치의 평균값은 0에 가깝고, 표준편차는 0.27정도 되는 결과를 얻었다. 이 가중치가 어떤 분포를 가졌는지를 이해하기 쉽게 그래프를 그려서 보자.

import matplotlib.pyplot as plt
plt.hist(conv_weights.reshape(-1,1))
plt.xlabel('weights')
plt.ylabel('count')
plt.show()

그래프를 보면 0을 중심으로 종 모양 분포를 띠고 있는 것을 알 수 있다. 그럼 이번에는 32개의 커널을 16개씩 2줄로 출력해보자. 전에 사용했던 맷플롯립의 subplot() 함수를 사용할 것이다.

fig, axs = plt.subplots(2, 16, figsize=(15,2))
for i in range(2):
  for j in range(16):
    axs[i,j].imshow(conv_weights[:,:,0,i*16 + j], vmin=-0.5, vmax=0.5)
    axs[i,j].axis('off')
plt.show()

코드에 대한 설명을 하자면, conv_weights에 32개의 가중치를 먼저 저장했다. 이 배열의 마지막 차원을 순회하면, 0부터 i*16 + j 까지의 가중치를 차례대로 출력한다. 여기서 i는 행 인덱스이고, j는 열 인덱스로 각각 0~1, 0~15까지의 범위를 가진다. 따라서 conv_weights[:,:,0,0] 에서 conv_weights[:,:,0,31] 까지 출력한다.

imshow() 함수는 배열에 있는 최댓값과 최솟값을 활용해 픽셀의 강도를 표현한다. 위의 코드에서 vmin과 vmax로 맷플롯립의 컬러맵으로 표현할 범위를 지정했다.

이번에는 훈련하지 않은 빈 신경망을 만들어보자. Sequential 클래스로 모델을 만들고, Conv2D 층을 하나 추가해준다.

no_training_model = keras.Sequential()
no_training_model.add(keras.layers.Conv2D(32, kernel_size=3, activation = \
                      'relu', padding='same', input_shape=(28,28,1)))
                      
#모델의 첫 번째 층의 가중치를 no_training_conv 변수에 저장하기
no_training_conv = no_training_model.layers[0]
print(no_training_conv.weights[0].shape)

함수형 API

지금까지는 신경망 모델을 만들 때 케라스 Sequential 클래스를 사용했다. 이 클래스는 층을 차례대로 쌓은 모델을 만든다. 딥러닝에는 조금 더 복잡한 모델들이 있는데, 그 중 하나가 함수형 API이다.

함수형 API는 케라스의 Model 클래스를 사용해 모델을 만든다. 간단히 말하면 model.input과 model.layers[0].output 을 연결하는 새로운 conv_acti 모델을 만드는 것이다.

conv_acti = keras.Model(model.input, model.layers[0].output)

model 객체의 predict() 메서드를 호출하면 최종 출력층의 확률을 반환한다. 하지만, conv_acti의 predict() 메서드를 호출하면 Conv2D 의 출력을 반환할 것이다. 그럼 이제 특성 맵을 시각화 해보자.

특성 맵 시각화

먼저 케라스로 패션 MNIST 데이터셋을 읽은 후 훈련 세트에 있는 첫 번째 샘플을 그려보자.

(train_input, train_target), (test_input, test_target) = \
  keras.datasets.fashion_mnist.load_data()
plt.imshow(train_input[0], cmap='gray_r')
plt.show()

부츠인것 같다. 이 샘플을 conv_acti 모델에 넣어 Conv2D 층이 만드는 특성 맵을 출력해보자.

inputs = train_input[0:1].reshape(-1, 28, 28, 1) / 255.0
feature_maps = conv_acti.predict(inputs)

#conv_acti.predict() 메서드가 출력한 feature_maps의 크기 확인
print(feature_maps.shape)

(1, 28, 28, 32)

세임 패딩과 32개의 필터를 사용한 합성곱 층의 출력이므로, (28, 28, 32) 이다.

특성 맵을 그려보자.

fig, axs = plt.subplots(4, 8, figsize=(15,8))
for i in range(4):
  for j in range(8):
    axs[i,j].imshow(feature_maps[0,:,:,i*8 + j])
    axs[i,j].axis('off')
plt.show()

위 그림에서 첫 번째 필터는 오른쪽에 있는 수직선을 감지한다. 첫 번째 특성 맵은 이 필터가 감지한 수직선이 강하게 활성화 되었다. 세 번째 필터는 전체적으로 밝은 색이다. 전면이 모두 칠해진 영역을 감지한 것이다. 검은 영역이 활성화되어 있는 것을 보면 알 수 있다. 이와 반대로 마지막 필터는 전체적으로 낮은 음수값이다. 이 필터와 큰 양수가 곱해지면 더 큰 음수가 되고 배경처럼 0에 가까운 값과 곱해지면 작은 음수가 될 것이다. 즉 부츠의 배경이 상대적으로 크게 활성화 될 수 있다는 것이다.

이번시간에는 합성곱 신경망에 대해서 배워보았다. 오늘은 여기서 마무리 하겠다.

0개의 댓글