5.1 합성곱 신경망 소개

In [1]:
import keras
keras.__version__
Using TensorFlow backend.
'2.3.1'

컨브넷의 정의와 컨브넷이 컴퓨터 비전 관련 작업에 잘 맞는 이유에 대해 이론적 배경을 알아보겠습니다. 하지만 먼저 간단한 컨브넷 예제를 둘러 보죠. 2장에서 완전 연결 네트워크(densely connected network)1로 풀었던(이 방식의 테스트 정확도는 97.8%였습니다) MNIST 숫자 이미지 분류에 컨브넷을 사용해 보겠습니다. 기본적인 컨브넷이더라도 2장에서 다룬 완전 연결된 모델의 성능을 훨씬 앞지를 것입니다.

다음 코드는 기본적인 컨브넷의 모습입니다. Conv2DMaxPooling2D 층을 쌓아 올렸습니다. 컨브넷은 (image_height, image_width, image_channels) 크기의 입력 텐서를 사용합니다(배치 차원은 포함하지 않습니다).

In [2]:
# 코드 5-1. 간단한 컨브넷 만들기
from keras import layers
from keras import models

model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))

이 예제에서는 MNIST 이미지 포맷인 (28, 28, 1) 크기의 입력을 처리하도록 첫 번째 층의 매개변수로 input_shape=(28, 28, 1)을 전달합니다.

지금까지의 컨브넷 구조를 출력해 보죠.2

In [3]:
model.summary()
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 26, 26, 32)        320       
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 13, 13, 32)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 11, 11, 64)        18496     
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 5, 5, 64)          0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 3, 3, 64)          36928     
=================================================================
Total params: 55,744
Trainable params: 55,744
Non-trainable params: 0
_________________________________________________________________

Conv2DMaxPooling2D 층의 출력은 (height, width, channels) 크기의 3D 텐서입니다. 높이와 넓이 차원은 네트워크가 깊어질수록 작아지는 경향이 있습니다. 채널의 수는 Conv2D 층에 전달된 첫 번째 매개변수에 의해 조절됩니다(코드 5-1에서는 32개 또는 64개).

다음 단계에서 마지막 층의 ((3, 3, 64) 크기인) 출력 텐서를 완전 연결 네트워크(Dense 층을 쌓은 분류기)에 주입합니다. 이전 층의 출력이 3D 텐서이고 이 분류기는 1D 벡터를 처리하므로 먼저 텐서로 펼쳐야 합니다.

In [4]:
# 코드 5-2. 컨브넷 위에 분류기 추가하기
model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))

10개의 클래스를 분류하기 위해 마지막 층의 출력 크기를 10으로 하고 소프트맥스 활성화 함수를 사용합니다. 지금까지 구성한 전체 네트워크는 다음과 같습니다.

In [5]:
model.summary()
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 26, 26, 32)        320       
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 13, 13, 32)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 11, 11, 64)        18496     
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 5, 5, 64)          0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 3, 3, 64)          36928     
_________________________________________________________________
flatten_1 (Flatten)          (None, 576)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 64)                36928     
_________________________________________________________________
dense_2 (Dense)              (None, 10)                650       
=================================================================
Total params: 93,322
Trainable params: 93,322
Non-trainable params: 0
_________________________________________________________________

여기에서 볼 수 있듯이 (3, 3, 64) 출력이 (576,) 크기의 벡터로 펼쳐진 후 Dense 층으로 주입되었습니다.

이제 MNIST 숫자 이미지에 이 컨브넷을 훈련합니다. 2장의 MNIST 예제 코드를 많이 재사용합니다.

In [6]:
# 코드 5-3. MNIST 이미지에 컨브넷 훈련하기
from keras.datasets import mnist
from keras.utils import to_categorical

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

train_images = train_images.reshape((60000, 28, 28, 1))
train_images = train_images.astype('float32') / 255

test_images = test_images.reshape((10000, 28, 28, 1))
test_images = test_images.astype('float32') / 255

train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)

model.compile(optimizer='rmsprop',
              loss='categorical_crossentropy',
              metrics=['accuracy'])
model.fit(train_images, train_labels, epochs=5, batch_size=64)
Epoch 1/5
60000/60000 [==============================] - 8s 130us/step - loss: 0.1745 - accuracy: 0.9459
Epoch 2/5
60000/60000 [==============================] - 6s 101us/step - loss: 0.0476 - accuracy: 0.9854
Epoch 3/5
60000/60000 [==============================] - 6s 100us/step - loss: 0.0333 - accuracy: 0.9897
Epoch 4/5
60000/60000 [==============================] - 6s 98us/step - loss: 0.0255 - accuracy: 0.9920
Epoch 5/5
60000/60000 [==============================] - 6s 99us/step - loss: 0.0199 - accuracy: 0.9939
<keras.callbacks.callbacks.History at 0x1b2659ba448>

테스트 데이터에서 모델을 평가해 보죠.3

In [7]:
test_loss, test_acc = model.evaluate(test_images, test_labels)
10000/10000 [==============================] - 1s 93us/step
In [8]:
test_acc
0.9909999966621399

2장에서 완전 연결 네트워크는 97.8%의 테스트 정확도를 얻은 반면, 기본적인 컨브넷은 99%의 테스트 정확도를 얻었습니다. 에러율이 (상대적으로) 60% 이상 줄었습니다.

완전 연결된 모델보다 왜 간단한 컨브넷이 더 잘 작동할까요? 이에 대해 알아보기 위해 Conv2D와 MaxPooling2D 층이 어떤 일을 하는지 살펴보겠습니다.

5.1.1 합성곱 연산

완전 연결 층과 합성곱 층 사이의 근본적인 차이는 학습의 내용입니다.

Dense 층은 입력 특성 공간에 있는 전역 패턴(예로 MNIST 숫자 이미지에서는 모든 픽셀에 걸친 패턴)을 학습하지만 합성곱 층은 지역 패턴을 학습합니다(그림 5-1 참고). 앞의 예에서는 모두 3×3 크기의 2D 윈도우(window)로 패턴을 찾습니다.

나타낼 수 없음
그림 5-1. 이미지는 에지(edge), 질감(texture) 등 지역 패턴으로 분해될 수 있다

이 핵심 특징은 컨브넷에 두 가지 흥미로운 성질을 제공합니다.

  • 학습된 패턴은 평행 이동 불변성(translation invariant)을 가집니다. 컨브넷이 이미지의 오른쪽 아래 모서리에서 어떤 패턴을 학습했다면 다른 곳(예를 들어 왼쪽 위 모서리)에서도 이 패턴을 인식할 수 있습니다. 완전 연결 네트워크는 새로운 위치에 나타난 것은 새로운 패턴으로 학습해야 합니다. 이런 성질은 컨브넷이 이미지를 효율적으로 처리하게 만들어 줍니다(근본적으로 우리가 보는 세상은 평행 이동으로 인해 다르게 인식되지 않습니다). 적은 수의 훈련 샘플을 사용해서 일반화 능력을 가진 표현을 학습할 수 있습니다.
  • 컨브넷은 패턴의 공간적 계층 구조를 학습할 수 있습니다(그림 5-2 참고). 첫 번째 합성곱 층이 에지 같은 작은 지역 패턴을 학습합니다. 두 번째 합성곱 층은 첫 번째 층의 특성으로 구성된 더 큰 패턴을 학습하는 식입니다. 이런 방식을 사용하여 컨브넷은 매우 복잡하고 추상적인 시각적 개념을 효과적으로 학습할 수 있습니다.

합성곱 연산은 특성 맵(feature map)이라고 부르는 3D 텐서에 적용됩니다. 이 텐서는 2개의 공간 축(높이와 너비)과 깊이 축(채널 축)으로 구성됩니다. RGB 이미지는 3개의 컬러 채널(빨간색, 녹색, 파란색)을 가지므로 깊이 축의 차원이 3이 됩니다. 합성곱 연산은 입력 특성 맵에서 작은 패치(patch)들을 추출하고 이런 모든 패치에 같은 변환을 적용하여 출력 특성 맵(output feature map)을 만듭니다.

나타낼 수 없음
그림 5-2 우리가 보는 세상은 시각적 구성 요소들의 공간적인 계층 구조로 구성되어 있으며, 아주 좁은 지역의 에지들이 연결되어 눈이나 귀 같은 국부적인 구성 요소를 만들고 이들이 모여서 “고양이”처럼 고수준의 개념을 만든다

출력 특성 맵도 높이와 너비를 가진 3D 텐서입니다. 출력 텐서의 깊이는 층의 매개변수로 결정되기 때문에 상황에 따라 다릅니다. 따라서 깊이 축의 채널은 더 이상 특정 컬러가 아닌 일종의 필터(filter)를 의미하게 됩니다.4 필터는 입력 데이터의 어떤 특성을 인코딩합니다.

MNIST 예제에서는 첫 번째 합성곱 층이 (28, 28, 1) 크기의 특성 맵을 입력으로 받아 (26, 26, 32) 크기의 특성 맵을 출력합니다. 즉 입력에 대해 32개의 필터를 적용5하여 각 채널 당 26x26 크기의 배열 값을 가집니다. 이 값은 입력에 대한 필터의 응답 맵(response map)입니다. 입력의 각 위치에서 필터 패턴에 대한 응답을 나타냅니다(그림 5-3 참고).

나타낼 수 없음
그림 5-3. 응답 맵의 개념: 입력의 각 위치에서 한 패턴의 존재에 대한 2D 맵

합성곱은 핵심적인 2개의 파라미터로 정의됩니다.

  • 입력으로부터 뽑아낼 패치의 크기: 전형적으로 3×3 또는 5×5 크기를 사용합니다. (이 예에서는 3×3 크기)
  • 특성 맵의 출력 깊이: 합성곱으로 계산할 필터의 수입니다. (이 예에서는 깊이 32, 64)

케라스의 Conv2D 층에서 이 파라미터는 Conv2D(output_depth, (window_height, window_width))로 2개의 매개변수로 전달됩니다.

3D 입력 특성 맵 위를 3×3 또는 5×5 크기의 윈도우가 슬라이딩(sliding)하면서 모든 위치에서 3D 특성 패치((window_height, window_width, input_depth) 크기)를 추출하는 방식으로 합성곱이 작동합니다.

3D 입력 패치는 합성곱 커널(convolution kernel)6과의 점곱을 통해 (output_depth,) 크기의 1D 벡터로 변환됩니다. 변환된 모든 벡터는 (height, width, output_depth) 크기의 3D 특성 맵으로 재구성됩니다. 출력 특성 맵의 공간상 위치는 입력 특성 맵의 같은 위치에 대응됩니다. 3×3 윈도우의 경우 3D 패치 input[i-1:i+2, j-1:j+2, :]로부터 벡터 output[i, j, :]가 만들어집니다. 그림 5-4에 전체 과정이 자세히 나타나 있습니다.

나타낼 수 없음
그림 5-4. 합성곱 작동 방식7

출력 높이와 너비는 입력의 높이, 너비와 다를 수 있습니다. 여기에는 두 가지 이유가 있습니다.

  • 경계 문제. 입력 특성 맵에 패딩을 추가하여 대응할 수 있습니다.
  • 잠시 후에 설명할 스트라이드(stride)의 사용 여부에 따라 다릅니다.

이 개념을 좀 더 자세히 알아봅시다.

경계 문제와 패딩 이해하기

5×5 크기의 특성 맵을 생각해 보겠습니다(총 25개의 타일이 있다고 생각합니다). 3×3 크기인 윈도우의 중앙을 맞출 수 있는 타일은 3×3 격자를 형성하는 9개뿐입니다(그림 5-5 참고). 따라서 출력 특성 맵은 3×3 크기가 됩니다. 크기가 조금 줄어들었습니다. 여기에서는 높이와 너비 차원을 따라 정확히 2개의 타일이 줄어들었습니다. 앞선 예에서도 이런 경계 문제를 볼 수 있습니다. 첫 번째 합성곱 층에서 28×28 크기의 입력이 26×26 크기가 되었습니다.

나타낼 수 없음
그림 5-5. 5×5 입력 특성 맵에서 가능한 3×3 패치 위치

입력과 동일한 높이와 너비를 가진 출력 특성 맵을 얻고 싶다면 패딩(padding)을 사용할 수 있습니다. 패딩은 입력 특성 맵의 가장자리에 적절한 개수의 행과 열을 추가합니다8. 그래서 모든 입력 타일에 합성곱 윈도우의 중앙을 위치시킬 수 있습니다. 3×3 윈도우라면 위아래에 하나의 행을 추가하고 오른쪽, 왼쪽에 하나의 열을 추가합니다(그림 5-6 참고). 5×5 윈도우라면 2개의 행과 열을 추가합니다.

나타낼 수 없음
그림 5-6. 25개의 3×3 패치를 뽑기 위해 5×5 입력에 패딩 추가하기

Conv2D 층에서 패딩은 padding 매개변수로 설정할 수 있습니다. 2개의 값이 가능합니다. "valid"는 패딩을 사용하지 않는다는 뜻입니다(윈도우를 놓을 수 있는 위치만 사용합니다). "same"은 “입력과 동일한 높이와 너비를 가진 출력을 만들기 위해 패딩한다.”라는 뜻입니다. padding 매개변수의 기본값은 "valid"입니다.

합성곱 스트라이드 이해하기

출력 크기에 영향을 미치는 다른 요소는 스트라이드입니다. 지금까지 합성곱에 대한 설명은 합성곱 윈도우의 중앙 타일이 연속적으로 지나간다고 가정한 것입니다. 두 번의 연속적인 윈도우 사이의 거리가 스트라이드라고 불리는 합성곱의 파라미터입니다. 스트라이드의 기본값은 1입니다. 스트라이드가 1보다 큰 스트라이드 합성곱도 가능합니다. 그림 5-7에서 5×5 크기의 입력(패딩 없음)에 스트라이드 2를 사용한 3×3 크기의 윈도우로 합성곱하여 추출한 패치를 볼 수 있습니다.

나타낼 수 없음
그림 5-7. 2×2 스트라이드를 사용한 3×3 합성곱의 패치

스트라이드 2를 사용했다는 것은 특성 맵의 너비와 높이가 2의 배수로 다운샘플링되었다는 뜻입니다(경계 문제가 있다면 더 줄어듭니다). 스트라이드 합성곱은 실전에서 드물게 사용됩니다. 하지만 어떤 모델에서는 유용하게 사용될 수 있으므로 잘 알아 둘 필요가 있습니다.9

특성 맵을 다운샘플링하기 위해서 스트라이드 대신에 첫 번째 컨브넷 예제에 사용된 최대 풀링(max pooling) 연산을 사용하는 경우가 많습니다. 최대 풀링에 대해 좀 더 자세히 알아보겠습니다.

5.1.2 최대 풀링 연산

앞선 컨브넷 예제에서 특성 맵의 크기가 MaxPooling2D 층마다 절반으로 줄어들었습니다. 스트라이드 합성곱과 매우 비슷하게 강제적으로 특성 맵을 다운샘플링하는 것이 최대 풀링의 역할입니다.

최대 풀링은 입력 특성 맵에서 윈도우에 맞는 패치를 추출하고 각 채널별로 최댓값을 출력합니다. 합성곱과 개념적으로 비슷하지만 추출한 패치에 학습된 선형 변환(합성곱 커널)을 적용하는 대신 하드코딩된 최댓값 추출 연산을 사용합니다. 또한, 전형적으로 최대 풀링은 2×2 윈도우와 스트라이드 2를 사용하여 특성 맵을 절반 크기로 다운샘플링하고, 합성곱은 3×3 윈도우와 스트라이드 1을 사용합니다.

나타낼 수 없음
그림 5-A1. 2x2 최대 풀링 예

왜 이런 식으로 특성 맵을 다운샘플링할까요? 왜 최대 풀링 층을 빼고 큰 특성 맵을 계속 유지하지 않을까요? 이런 방식을 한번 테스트해 보죠. 합성곱으로만 이루어진 모델은 다음과 같습니다.

In [9]:
model_no_max_pool = models.Sequential()
model_no_max_pool.add(layers.Conv2D(32, (3, 3), activation='relu',
                      input_shape=(28, 28, 1)))
model_no_max_pool.add(layers.Conv2D(64, (3, 3), activation='relu'))
model_no_max_pool.add(layers.Conv2D(64, (3, 3), activation='relu'))
In [10]:
model_no_max_pool.summary()
Model: "sequential_2"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_4 (Conv2D)            (None, 26, 26, 32)        320       
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 24, 24, 64)        18496     
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 22, 22, 64)        36928     
=================================================================
Total params: 55,744
Trainable params: 55,744
Non-trainable params: 0
_________________________________________________________________

이 설정에서 무엇이 문제일까요? 두 가지가 있습니다.

  • 합성곱 층으로만 이루어진 네트워크는 특성의 공간적 계층 구조를 학습하는데 도움이 되지 않습니다. 합성곱을 하며 줄어든 윈도우는 특성에 대한 정보가 부족합니다.10
  • 최종 특성 맵은 22 × 22 × 64 = 30,976개의 원소로 아주 많습니다. 이 컨브넷을 펼친 후 512 크기의 Dense 층과 연결한다면 약 15.8백만 개의 가중치 파라미터가 생깁니다. 작은 모델치고는 최종 가중치가 너무 많아 심각한 과대적합이 발생할 것입니다.

간단히 말해서 다운샘플링을 사용하는 이유는 처리할 특성 맵의 가중치 개수를 줄이기 위해서입니다. 또 연속적인 합성곱 층이 (원본 입력에서 커버되는 영역 측면에서) 점점 커진 윈도우를 통해 바라보도록 만들어 필터의 공간적인 계층 구조를 구성합니다.

최대 풀링이 다운샘플링을 할 수 있는 유일한 방법은 아닙니다. 이미 알고 있듯이 앞선 합성곱 층에서 스트라이드나 채널별 평균값을 계산하여 변환하는 평균 풀링(average pooling)을 사용할 수도 있습니다. 하지만 최대 풀링이 다른 방법들보다 더 잘 작동하는 편입니다. 그 이유는 특성이 특성 맵의 각 타일에서 어떤 패턴이나 개념의 존재 여부를 인코딩하는 경향이 있기 때문입니다(그래서 특성의 지도(맵)입니다). 따라서 가장 납득할 만한 서브샘플링(subsampling) 전략은 먼저 (스트라이드가 없는 합성곱으로) 조밀한 특성 맵을 만들고 그다음 작은 패치에 대해서 최대로 활성화된 특성을 고르는 것입니다.


Note

5장부터 등장하는 예제는 CPU만 사용할 경우 컴퓨터 사양에 따라 실행 시간이 다소 오래 걸릴 수 있습니다. 부록 C를 참고하여 아마존 GPU 인스턴스를 사용하거나 구글의 코랩(Colab, https://colab.research.google.com/)을 사용할 수 있습니다. 코랩은 구글이 만든 교육과 연구를 위한 주피터 노트북 환경으로 구글 클라우드의 컴퓨팅 자원을 무료로 사용할 수 있습니다. 코랩에 대한 자세한 내용은 부록 C.8을 참고하세요.

local로 gpu를 설정하기 위해선 아래의 링크에서 호환 버전을 확인 할 수 있습니다.
https://www.tensorflow.org/install/gpu


  1. 이 책에서는 가능하면 ‘densely connected’를 ‘밀집 연결’로 번역하지 않고 동일한 뜻으로 더 널리 사용되는 ‘완전 연결(fully connected)’로 번역합니다. (완전 연결 층(fully connected layer), 밀집 층(dense layer), 밀집 연결 층(densely connected layer)(케라스는 Dense 클래스)) 

  2. 모델의 summary() 메서드는 신경망 구조를 일목요연하게 출력해 줍니다. 출력의 시작 부분이 신경망 입력에 가까운 하위 층이고 끝부분이 신경망 출력에 가까운 상위 층입니다. 이 메서드는 keras.utils.print_summary() 함수를 사용합니다. keras.utils.print_summary(model)처럼 쓰면 동일한 출력을 얻을 수 있습니다. 

  3. 4장에서 배웠듯이 모델을 비교하려면 검증 세트를 사용해야 합니다. 책에서는 간단한 예제를 만들기 위해서 테스트 세트를 검증 세트처럼 사용합니다. 

  4. 1장과 2장에서는 신경망의 층을 데이터를 처리하는 필터로 비유했습니다. 여기서 필터는 합성곱 층에서 사용하는 모델 파라미터를 의미합니다. Conv2D의 첫 번째 매개변수(필터 또는 채널 수)가 출력 특성 맵의 깊이 차원을 결정합니다. 

  5. 필터 하나의 크기는 (patch_height, patch_width, input_depth)입니다. 첫 번째 합성곱은 (3, 3, 1) 크기의 필터를 32개 적용하고, 두 번째 합성곱은 (3, 3, 32) 크기의 필터를 64개 적용합니다. 

  6. 여기서 합성곱 커널은 합성곱 층의 필터를 하나의 행렬로 합친 것을 말합니다. 첫 번째 합성곱 층의 커널 크기는 (3, 3, 1, 32)이고, 두 번째 합성곱 층의 커널 크기는 (3, 3, 32, 64)입니다. 

  7. 이 그림에서 출력의 깊이가 3이므로 패치마다 (3, 3, 2) 크기의 필터가 3개 적용된 것입니다. 다르게 말하면 (3, 3, 2, 3) 크기의 커널과 점곱한 것입니다. 

  8. 추가되는 행과 열은 0으로 채워지기 때문에 제로 패딩(zero padding)이라고도 부릅니다. 

  9. 스트라이드와 패딩에 대한 좀 더 자세한 설명과 시뮬레이션은 제 블로그의 ‘딥러닝을 위한 콘볼루션 계산 가이드(https://goo.gl/qvNTyu)’를 참고하세요. 

  10. 7×7 크기의 입력을 3×3 윈도우로 합성곱하면 5×5로 줄어들고, 다시 한 번 합성곱하면 3×3으로 줄어듭니다. 바꾸어 말하면 두 번째 합성곱을 통과한 특성 맵의 3×3 크기에는 입력에 있는 7×7 크기의 정보만 담겨 있습니다. 

댓글남기기