합성곱 신경망(CNN; Convolutional Neural Network)

합성곱 신경망이란?

합성곱 신경망(Convolutional neural network, CNN)은 시각적 영상을 분석하는 데 사용되는 다층의 피드-포워드적인 인공신경망의 한 종류이다. 딥 러닝에서 심층 신경망으로 분류되며, 시각적 영상 분석에 주로 적용된다. 또한 공유 가중치 구조변환 불변성 특성에 기초하여 변이 불변 또는 공간 불변 인공 신경망 (SIANN)으로도 알려져 있다. 영상 및 동영상 인식, 추천 시스템, 영상 분류, 의료 영상 분석 및 자연어 처리 등에 응용된다.

- 위키백과 (합성곱 신경망)

합성곱 신경망은 주로 영상 분석에서 많이 사용되는 신경망 구조이다.
(여기서 영상은 picture, image만을 의미한다. video는 동영상이라고 해야 한다.)

합성곱 신경망이니 합성곱이라는 것의 원리를 사용할텐데, 이게 뭘까?
합성곱을 검색해보면 두 함수가 있고, 둘 중 하나를 뒤집은 후에 곱한 결과를 적분한다고는 하는데...

$$(f*g)(t) = \int_{-\infty}^{\infty} {f(\tau)g(t-\tau)d\tau} \\무슨\ 말인고?$$

그러나 이는 함수에 들어가는 값들이 연속적인 경우이고, 합성곱 신경망에서의 합성곱은 각각의 값이 따로 따로, 즉 이산적이다.
따라서 위 식은 아래와 같이 표현할 수 있을 것이다.

$$(f*g)(t) = \sum_{\tau} {f(\tau)g(t-\tau)}$$

결론적으로 간단히 말하면 짝이 맞는 숫자 둘($f(\tau)$와 $g(t-\tau)$)이 만나 곱해진 값들을 한 곳에 모아 합을 구해준다고 생각하면 된다.

아래 합성곱 신경망의 구조 및 작동 방식을 통해서 이게 구체적으로 어떻게 돌아가는 시츄에이션인지 알아보자.

합성곱 신경망의 구조 및 작동 방식

합성곱 신경망 구조

전체적인 합성곱 신경망의 구조는 위와 같다.

입력 데이터(이미지)가 들어오면 특징 추출 부분에서 합성곱 연산 및 풀링 등의 과정을 거쳐 이미지의 특징을 잡아낸다.
신경망의 초기에는 저수준, 즉 이미지의 아주 작고 세부적인 특징부터 찾는다. 그리고 층이 깊어질수록 고수준, 즉 이미지의 전체적인 특징을 찾아낸다.

특징 추출 과정

처음에는 가로, 세로, 대각선 정도의 특징부터 잡기 시작해서 점차 (사람 얼굴인 경우) 눈, 코, 입 등을 구분하고, 마지막에는 전체적인 얼굴 윤곽을 추출하는 것을 볼 수 있다.

합성곱(Convolution)

위에서 합성곱 신경망에서의 합성곱은 결국 눈이 맞아 서로 곱해진 두 숫자의 커플들을 한데 모아 합한 것이라고 하였다. 이 발칙한 상황(?)이 어떤 것인지 눈으로 확인(?)해보자.

인풋&필터

우선 5X5 사이즈의 그레이 스케일(흑백) 이미지가 있다고 해보자. 이 이미지 위를 2X2 사이즈의 필터(Convolution Filter)$^*$$^1$가 왼쪽 위부터 시작해서 몇 칸 단위$^*$$^2$로 차례차례 돌아다닌다. 한 칸씩 움직인다고 하면 가로로 네 번, 세로로 네 번 해서 총 16번 훑을 것이다.

이때 매번 움직일 때마다 필터 위의 숫자와 이들 각각의 위치에 해당하는 이미지 위의 숫자를 곱한 후, 결과를 전부 합한다.
이렇게 해서 각 결과를 계산된 자리대로 배치해서 내놓은 것을 Feature Map이라고 한다.
(이 과정에서 Filter의 가중치가 학습이 된다.)

Feature Map계산 과정

Feature Map에 활성화 함수를 적용한 결과를 Activation Map이라 하고, 여기에 편향(bias)까지 합해준 것이 합성곱 층의 최종 결과물(Output)이다.

*1 필터(Filter)와 커널(Kernel)의 차이

이미지가 그레이 스케일(채널(Channel)이 1개)인 경우 필터(Filter)는 커널(Kernel)과 같다.

1 channel input

이미지가 컬러(RGB -> 채널이 3개)인 경우 하나의 필터 안에 3개의 커널이 있다.
즉, 각 채널마다 1개의 커널이 있고, 필터는 커널의 집합이다.

3 channels input

※ 위 이미지를 보면 들어올 때(input)는 RGB의 3개 채널이지만, 나갈 때(output)는 1개의 채널로 통합된다.
여기서 주의할 것은 각 필터별로 채널이 1개씩 산출된다는 점이다.
즉, 합성곱 층에 지정된 필터가 32개라면 Feature Map의 채널은 32개가 되는 것이다.
이는 그레이 스케일도 동일하다.

*2 스트라이드(Stride)

이 '몇 칸 단위'를 의미하는 말이 스트라이드(Stride) 이다.
이는 영어로 걸음, 보폭이라는 의미를 갖고 있다.
실제 코드 작성 시 이 값을 지정하여 몇 칸 단위로 필터가 돌아다니게 할지 정한다.

패딩(Padding)

패딩

패딩은 이미지의 외곽을 어떤 값으로 둘러싸는 것을 말한다.
사람이 겨울에 패딩 점퍼를 입듯, 이미지도 패딩을 입었다고 생각하면 된다.
이미지를 우리의 몸으로, 패딩은 패딩 점퍼로 비유해보겠다.

패딩은 왜 해줄까?
만약 패딩을 해주지 않고 그대로 합성곱을 진행하면 이미지의 테두리 부분은 안쪽에 비해 연산 과정에 덜 포함된다.
이는 달리 말하면 해당 부분의 정보가 제대로 반영되지 못한다는 의미와도 같다.
겨울에 패딩을 입지 않아 차가워진 우리의 피부를 상상해보자.

여기서 패딩을 통해 바깥을 감싸면 기존에 연산에 덜 포함된 부분이 보다 많이 반영된다. 한 번 되던 것이 두 번 이상 들어오니 이전보다 훨씬 낫다고 볼 수 있다.
패딩 점퍼를 입어서 피부가 따뜻해진 듯하다.

결국 패딩은 전체 이미지의 값을 충분히 활용하기 위해서 사용한다.
이와 더불어 입력 데이터의 크기가 패딩에 의해 변하게 되면서 Feature Map의 크기도 조절 가능하다.

위 움짤을 보자. 만약 5X5 사이즈 이미지에 3X3 필터를 적용하면 3X3 Feature Map이 나올 것이다.
여기서 이미지에 패딩을 적용하여 7X7 사이즈가 되게끔 하였고, 여기에 3X3 필터를 적용하니 5X5 사이즈의 Feature Map이 나왔다.
즉, 원본 이미지의 사이즈 5X5와 동일한 크기의 Feature Map을 출력으로 얻을 수 있다.

패딩의 값으로는 보통 0을 사용하는데, 이를 제로 패딩(Zero-Padding)이라고 한다.
이렇게 이미지를 패딩으로 씌움으로써 인공 신경망이 이미지의 외곽을 인식하게끔 만드는 효과도 있다.

※ 필터 크기(Filter Size), 패딩(Padding), 스트라이드(Stride)와 Feature Map 크기의 관계

$$
N_{\text{out}} = \bigg[\frac{N_{\text{in}} + 2p - k}{s}\bigg] + 1
$$

$N_{\text{in}}$ : 입력되는 이미지의 크기(=특성 수)
$N_{\text{out}}$ : 출력되는 이미지의 크기(=특성 수)
$k$ : 합성곱에 사용되는 필터(=커널)의 크기
$p$ : 합성곱에 적용한 패딩 값
$s$ : 합성곱에 적용한 스트라이드 값

풀링(Pooling)

Pooling

풀링(pooling)을 찾아보면 일단 swimming pool의 pool이 제일 먼저 나온다.
여기서 좀 더 들어가면 웅덩이, 그리고 공동으로 이용하기 위한 자금을 모은다는 의미도 나온다.

그럼 결국 CNN에서의 풀링은 무엇일까?
CNN에서 풀링을 거치면 이미지의 주요한 특징이 갈무리된 결과를 얻을 수 있다.
위 그림에서 보여지듯 일정한 간격으로 영역을 나눈 후, 그 영역의 특징을 가장 잘 설명하는 값을 가져온다.
여기서 이 값으로 영역 내 최댓값을 갖고 올지 평균값을 갖고 올지에 따라 Max Pooling과 Average Pooling으로 구분한다.
(주로 Max Pooling이 사용된다.)

또한 풀링을 통해 결과물, 즉 다음 층으로 향하는 데이터의 입력 크기(input size)가 작아진다. 크기가 작아진 만큼 데이터가 차지하는 메모리 크기도 작아지는 효과가 있다.
동시에 파라미터 수를 줄여주는 효과도 있어 과적합을 방지할 수도 있다(Dropout 같은 느낌).

풀링 층은 따로 학습되는 가중치가 없고, 채널 수도 변하지 않는다.

완전 연결 신경망(Fully Connected Layer)

특징 추출 부분에서 충분히 특징을 추출했다면 이를 토대로 분류를 해야 한다.

분류 문제 해결을 위해서 다층 퍼셉트론 신경망으로 구성된 신경망을 구축하고, 이진 분류인지 다중 분류인지에 따라 마지막 출력층까지 만들어주면 된다.

코드 예시(Tensorflow & Keras)

(Github 통해서 확인하시려면 클릭)

10개의 클래스로 분류되는 이미지 데이터셋인 CIFAR-10을 사용하여 CNN을 어떻게 구현하는지 알아보자.

  1. 패키지 및 라이브러리 불러오기
from tensorflow.keras.datasets import cifar10
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense, Conv2D, MaxPooling2D, Flatten
from tensorflow.keras.layers import Dense, Flatten
from tensorflow.keras.models import Sequential
from tensorflow.keras.datasets import cifar10

from sklearn.model_selection import train_test_split

import numpy as np
import tensorflow as tf
  1. 시드(seed) 고정하기
np.random.seed(42)
tf.random.set_seed(42)
  1. 데이터셋 불러오기 & 훈련 / 검증 / 테스트셋으로 나누기 & 이미지 픽셀값 정규화
(X_train, y_train), (X_test, y_test) = cifar10.load_data()
X_train = X_train.astype('float32') / 255.
X_test = X_test.astype('float32') / 255.
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=.2)
X_train.shape

(40000, 32, 32, 3)

훈련 데이터셋으로 가로 32, 세로 32 픽셀의 컬러(RGB, 3개 채널) 이미지 40000개가 있다.

  1. CNN 모델 구축
model = Sequential()

# 특징 추출 부분
# 합성곱 층(Conv2D)와 풀링 층(MaxPooling2D)를 번갈아가며 사용
model.add(Conv2D(32, (3,3), padding='same', activation='relu'))
model.add(MaxPooling2D(2,2))
model.add(Conv2D(32, (3,3), padding='same', activation='relu'))
model.add(MaxPooling2D(2,2))
model.add(Conv2D(32, (3,3), padding='same', activation='relu'))
model.add(MaxPooling2D(2,2))

# 분류기 역할의 완전 연결 신경망
# 특징 추출 부분을 거쳐온 데이터를 신경망에 입력할 수 있도록 1차원으로 변환
model.add(Flatten())
model.add(Dense(128, activation='relu'))
# 10개의 클래스 분류이므로 출력층에 10개의 노드를, 활성화 함수로 softmax를 지정
model.add(Dense(10, activation='softmax'))

Conv2D에서 반드시 지정해야 하는 첫 번째 파라미터는 필터의 수(filters), 두 번째는 필터(커널)의 크기(kernel_size)이다.
padding은 'valid' 또는 'same'로 지정 가능하다.
'valid'는 패딩을 적용하지 않아 Conv2D를 지나면 입력된 이미지의 shape이 작아진다.
'same'은 패딩을 적용하여 Conv2D 전후의 이미지 shape이 동일하게 만든다.

MaxPooling2D에서 풀링할 영역의 크기(pool_size)는 (2, 2)가 기본값이다.
strides 파라미터를 지정하여 몇 칸 단위로 움직이며 풀링을 할지 지정할 수 있다. 기본값은 None으로, 이대로 두면 pool_size와 동일하게 설정된다. 즉, 풀링 영역이 겹치지 않게 된다.

※ 한 개 층에 큰 필터 쓰기 vs. 여러 개 층에 작은 필터 쓰기

큰 필터 1개를 쓰기보다는 작은 필터 여러 개를 쓰는 것이 낫다.
학습되어야 할 가중치 파라미터의 수가 적어지고 일반적으로 더 나은 성능을 보이기 때문이다.

예를 들어 5X5와 3X3 크기 필터가 있다고 해보자.
입력 이미지의 크기가 5X5라고 했을 때 5X5 필터는 1개 층, 3X3 필터는 2개 층을 거치면 같은 결과를 얻게 된다.

5X5 vs. 3X3


그런데 5X5 필터는 학습되어야 할 가중치 파라미터의 수가 25개이고, 3X3 필터는 2개 층이므로 3 X 3 X 2 = 18개이다.
필터의 크기를 작게, 그리고 여러 층으로 만든 것이 큰 필터를 사용하는 것보다 적은 비용이 드는 것을 알 수 있다.

예외적으로 첫 번째 합성곱 층에서는 큰 크기의 필터(5X5 이상)과 2 이상의 스트라이드를 사용한다. 왜냐하면 필터 크기가 커져도 입력 이미지의 채널은 컬러인 경우 3개, 흑백인 경우 1개뿐이므로 비용이 크지 않다.
이렇게 하면 너무 많은 정보를 잃지 않고서 이미지의 차원을 줄일 수 있다.

model.summary()

model summary

Output Shape은 (행 수, 가로 픽셀 수, 세로 픽셀 수, 채널 수) 라고 생각하면 쉽다.
여기서 행(row) 수는 None으로 표시되어 있는데, 이는 특정한 숫자로 지정되지 않았음을 의미한다. 배치 사이즈처럼 32, 64 등 다양한 숫자가 올 수도 있기 때문이다.
채널 수는 각 Conv2D에서 지정한 filters의 값에 맞춰 나온 것을 볼 수 있다.

MaxPooling2D는 입력으로 들어온 이전의 Conv2D의 shape을 줄였다.
채널의 수는 입력 데이터와 동일하며, 학습되는 가중치가 없기 때문에 Param #도 0으로 표기되었다.

Flatten에서는 (4, 4, 32)의 데이터가 1차원으로 변환되었기 때문에 4 * 4 * 32=512의 shape을 갖게 되었다.

model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

10개 클래스의 다중 분류 문제이므로 'loss'를 'sparse_categorical_crossentropy'로 지정했다.

model.fit(X_train, y_train,
          batch_size=128,
          validation_data=(X_val, y_val),
          epochs=10)
  1. 모델 평가
model.evaluate(X_test, y_test, verbose=2)

313/313 - 4s - loss: 0.9526 - accuracy: 0.6713 - 4s/epoch - 13ms/step
[0.9525882601737976, 0.6712999939918518]


전이 학습(Transfer Learning)이란?

MMORPG 게임을 해본 사람이라면 알겠지만, 한 계정 내에 있는 캐릭터들 간에 아이템을 옮길 수 있는 기능이 있다(없는 게임도 있다).
이를 통해 무엇을 할 수 있는가? 일단 주 캐릭터를 초집중하여 강력하게 성장시킨다. 그 과정에서 다른 클래스의 장비나 잡화소모품 등을 상당수 얻게 된다.
이렇게 주 캐릭터를 충분히 성장시킨 후, 새로운 캐릭터를 시작한다고 해보자. 여기서 새로운 캐릭터는 제로(0)부터 시작할 필요가 없다. 왜냐하면 주 캐릭터를 키우면서 쌓아 놓은 수많은 물품들이 있기 때문이다.

전이 학습도 이와 같다.
기존에 훌륭하신 교수님들이나 연구원분들께서 연구 개발하여 발표하신 모델이 있으면(주 캐릭터), 우리는 이를 그대로 가져온 후에 우리에게 필요한 부분만 붙여서 쓰면 된다(부 캐릭터).

작동 방식

일반적으로 전이 학습은 대량의 데이터를 학습해놓은 사전 학습 모델(Pre-trained Model)의 가중치를 그대로 가져온 후, 분류기(완전 연결 신경망)만 필요에 따라 추가로 설계하여 붙이는 식으로 한다.

아래는 전이 학습을 나타낸 이미지이다.

전이 학습


위쪽이 사전 학습 모델이고 아래가 새롭게 구축한 모델이다.
새로운 모델을 보면 사전 학습 모델의 'Common inner layers'는 그대로 사용하되, 기존의 분류기 부분은 떼어낸 후 용도에 맞게 새로운 분류기를 붙여주었다.

특징

1) 어떠한 데이터에도 준수한 성능
사전 학습 모델의 가중치는 대량의 데이터를 학습하여 얻어진다.
즉, 여러 데이터의 일반적인 특징을 많이 학습하였으므로 아무 데이터나 넣어도 쓸만한 성능을 보인다.

2) 좋은 결과를 신속하게 도출
일반적으로 사전 학습 가중치는 학습되지 않고 고정(freeze)한 채로 진행되기 때문에 빠르게 좋은 결과를 얻을 수 있다.

이외에도 학습 데이터의 수가 적을 때도 효과적이며, 전이 학습 없이 학습하는 것보다 훨씬 높은 정확도를 제공한다는 장점도 있다.

이미지 분류를 위한 주요 사전 학습 모델

다음은 이미지 분류를 위해 사용할 수 있는 대표적인 사전 학습 모델들이다.

1. VGG

VGG는 2014년 ILSVRC 대회에서 2등을 한 모델이다.
VGG의 구조는 2개 또는 3개의 합성곱 층 뒤에 풀링 층이 나오고 다시 2개 또는 3개의 합성곱 층과 풀링 층이 등장하는 식이다.
VGG의 종류에 따라 총 16개 또는 19개의 합성곱 층이 있고, 이 갯수에 따라 VGG-16 또는 VGG-19라고 부른다.
마지막 분류기(완전 연결 신경망) 부분은 2개의 은닉층과 출력층으로 이루어진다.

VGG

VGG의 특징은 아래와 같다.

  • 모든 합성곱 층에서 3X3 크기 필터 사용
  • 활성화 함수로 ReLU 사용하고, 가중치 초기화는 He 초기화를 사용
  • 완전 연결 신경망에서 드롭아웃(Dropout)을 사용하여 과적합 방지 & 옵티마이저로 Adam 사용

2. ResNet

ResNet은 2015년 ILSVRC 대회에서 우승한 모델이다.
우승한 모델의 신경망 층 수는 152개였고, 이외에 34개, 50개, 101개 층 등의 변종도 있다.

ResNet

ResNet의 특징은 잔차 연결(Residual Connection; Skip Connection)이다. 이는 어떤 층을 거친 출력값에 그 층에 들어왔던 입력값을 그대로 더해주는 것을 말한다.
이를 통해 깊은 층의 신경망을 훈련시킬 수 있었다.

Residual Connection

3. Inception

Inception은 2014년 ILSVRC 대회에서 우승한 모델이다.
Inception 모델의 구조에서 주목할 부분은 Inception Module인데, 이는 가로 방향으로 신경망의 층을 넓게 구성한 구조를 말한다.
이를 활용하여 크기가 다른 필터와 풀링을 병렬적으로 적용한 뒤 결과를 조합한다.

Inception

이후 Inception-v3, Inception-v4 등 여러 변종이 나왔다.

4. EfficientNet

2019년에 발표된 모델로, Compound Scaling이란 방식을 통해서 기존의 모델보다 뛰어난 성능을 끌어냈다.

먼저 Scaling이란 합성곱 신경망 모델의 깊이나 너비, 또는 입력 이미지의 크기를 조절하는 것을 뜻한다.
이들 간의 균형을 맞추는 것이 모델 성능 향상에 중요하다는 생각을 바탕으로 이들 간의 균형을 나타내보니 간단한 상수비로 구할 수 있었다고 한다. 이를 토대로 나온 것이 Compound Scaling이다.

EfficientNet

코드 예시(Tensorflow & Keras)

위의 CNN 코드 예시에서 사용된 CIFAR-10 데이터셋을 그대로 사용하되, 모델은 사전 학습 모델(VGG16)을 이용한 전이 학습 모델로 구축하는 코드이다.

  1. 패키지 및 라이브러리 불러오기
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.layers import GlobalAveragePooling2D
  1. 사전 학습 모델 불러오기
pretrained_model = VGG16(weights='imagenet', include_top=False)
  1. 사전 학습 모델 위에 분류기 추가하기

아래에 추가된 GlobalAveragePooling2d() 층은 데이터 Shape을 (None, None, None, 512) 에서 (None, 512)로 변환하는 역할을 한다.

model = Sequential()
model.add(pretrained_model)
model.add(GlobalAveragePooling2D())
model.add(Dense(128,activation='relu'))
model.add(Dense(10,activation='softmax'))
model.summary()

transfer model summary

model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
model.fit(X_train, y_train,
          batch_size=128,
          validation_data=(X_val, y_val),
          epochs=10)
model.evaluate(X_test, y_test, verbose=2)

313/313 - 2s - loss: 0.7755 - accuracy: 0.7828 - 2s/epoch - 7ms/step
[0.7754948139190674, 0.782800018787384]


<참고 자료>

+ Recent posts