?부분 알아보기, YOLO ~ YOLOv3 논문 읽어보기
이미지 인식 문제에서 완전 심층 연결망이 아닌 CNN을 쓰는 이유는 완전 연결층의 경우 아주 많은 파라미터를 만들기 때문입니다. 예를 들어, 100x100 이미지는 10,000개의 픽셀로 이루어져 있는데, 이를 첫 은닉층의 뉴런 1,000개에 연결하면 10,000,000개의 연결이 만들어지기 때문입니다. CNN은 층을 부분적으로 연결하고 가중치를 공유해 이 문제를 해결합니다.
합성곱 층
CNN은 합성곱(convolution)을 이용합니다. 합성곱은 한 함수가 다른 함수 위를 이동하며 원소별 곱셈의 적분을 계산하는 수학 연산입니다. 합성곱 층에서는 합성곱 연산 중 반전을 제외한 교차 상관(cross-correlation)을 사용합니다.
위 그림의 CNN은 이미지 중 수용장 안에 있는 픽셀에만 합성곱 층1 뉴런이 연결됩니다. 합성곱 층2의 경우, 합성곱 층1 중 수용장 안에 있는 픽셀에만 합성곱 층2의 뉴런이 연결됩니다. 이런 구조는 첫 번째 은닉층이 저수준 특성에 집중하고, 그 다음 은닉층에서 더 고수준 특성으로 조합해나가도록 돕습니다. 이런 계층적 구조는 실제 이미지에서 흔히 볼 수 있으며, 이는 CNN이 이미지 인식에 잘 작동하는 이유 중 하나입니다.
NOTE
지금까지 다층 신경망은 한 줄로 길게 늘어선 뉴런으로 구성되어 있어서 입력 이미지를 신경망에 주입하기 전에, 1D 배열로 펼쳐야 했습니다. CNN은 2D로 표현되므로 뉴런을 그에 상응하는 입력과 연결하기에 더 직관적입니다.
$f_h$, $f_w$는 수용장의 높이와 너비입니다. 한 층의 $i$행, $j$열에 있는 한 뉴런은 이전 층(제로 패딩된)의 $i$에서 $i+f_h-1$까지의 행과 $j$에서 $i+f_w-1$까지의 열에 있는 뉴런의 출력에 연결됩니다.
모델의 계산 복잡도를 낮추기 위해 수용장 사이에 간격을 두어 큰 입력층을 훨씬 작은 층에 연결할 수 있습니다. 한 수용장과 다음 수용장 사이의 간격을 stride라고 합니다.
상위층의 i행, j열의 뉴런이 이전 층의 $i\cdot s_h$에서 $i\cdot s_h+f_h-1$까지의 행과 $j\cdot s_w$에서 $j\cdot s_w+f_w-1$까지의 열에 위치한 뉴런과 연결됩니다. $s_h$, $s_w$는 스트라이드의 수직 값과 수평 값입니다. 위 그림에서는 (제로 패딩이 적용된) 5x7 입력층이 3x3 수용장과 stride=2를 사용해 3x4층에 연결되었습니다.
필터
뉴런의 가중치는 수용장 크기의 작은 이미지로 표현될 수 있습니다. 아래 그림은 filter(convolution kernel)이라고 부르는 두 개의 가중치 세트를 보여줍니다. 수직 필터는 가운데 열만 1이고 나머지는 0인 7x7 행렬입니다. 수평 필터는 가운데 행만 1이고 나머지는 0인 7x7 행렬입니다.
한 층의 모든 뉴런에 수직 필터를 (그리고 같은 편향을) 적용하면 왼쪽 이미지를, 수평 필터를 적용하면 오른쪽 이미지를 얻을 수 있습니다. 이를 통해 층의 전체 뉴런에 적용된 하나의 필터는 하나의 feature map(특성 맵)을 만듭니다.
같은 필터(가중치)를 모든 층의 뉴런이 공유한다는 것은 모델 전체의 파라미터 수를 급격히 줄여줍니다.
특성 맵은 필터를 가장 크게 활성화 시키는 이미지 영역을 강조하며 훈련하는 동안 자동으로 가장 유용한 필터를 찾고 상위 층들은 이들을 연결해 더 복잡한 패턴을 학습합니다.
여러 가지 특성 맵 쌓기
필터 하나가 특성 맵 하나를 출력합니다. 실제 합성곱 층은 여러 개의 필터를 가지고 있어서, 합성곱 층의 출력을 3D로 표현하는 것이 더 정확합니다. 한 뉴런의 수용장은 이전 층에 있는 모든 특성맵(입력층이라면 컬러 채널)에 걸쳐 확장됩니다.
NOTE
CNN은 한 지점에서 패턴을 인식하도록 학습되었다면 다른 어느 위치에 있는 패턴도 인식할 수 있습니다. 반면 DNN은 한 지점에 있는 패턴을 인식하도록 학습되었다면 오직 패턴이 그 위치에 있을 때만 감지할 수 있습니다.
입력 이미지는 컬러 채널(channel)마다 서브층으로 구성되기도 합니다. 컬러 채널은 RGB 세 가지 입니다. 흑백 이미지는 하나의 채널만 가집니다. 위성 이미지는 적외선 등 다른 빛의 파장을 채널로 갖습니다.
합성곱 층$l$에 있는 특성 맵$k$의 $i$행, $j$열에 위치한 뉴런은 이전 $l-1$층에 있는 모든 특성 맵의 $j\cdot s_h$에서 $j\cdot s_h+f_h-1$의 행, $j\cdot s_w$에서 $j\cdot s_w+f_w-1$의 열에 있는 뉴런의 출력에 연결됩니다.
이 과정을 수식으로 나타내면 다음과 같습니다. 단지 입력에 대한 가중치 합을 계산하고 편향을 더하는 것이 전부입니다.
$$z_{i,\ j,\ k}\ =\ b_k+\sum _{u=0}^{f_h-1}\sum _{v=0}^{f_w-1}\sum _{k'=0}^{f_{n'}-1}x_{i',\ j',\ k'}\cdot w_{u,v,k',k}$$
$$\begin{cases}i'=i\cdot s_h+u\\j'=j\cdot s_w+v\end{cases}$$
$z_{i,\,j,\ k}$는 합성곱 층($l$층)의 특성 맵$k$의 $i$행, $j$열에 위치한 뉴런의 출력입니다. $f_{n'}$은 이전 층($l-1$층)에 있는 특성 맵의 수입니다.
$x_{i',j',\ k'}$는 $l-1$층의 $i'$행, $j'$열, $k'$ 특성 맵(입력층일시 $k'$채널)에 있는 출력의 뉴런입니다.
$b_k$는 ($l$층에 있는) 특성 맵$k$의 편향입니다. 특성 맵마다 하나의 편향이 있어서 이 값을 조절하면 특성 맵에 있는 전체 뉴런값이 커지거나 작아집니다. 이는 특성 맵$k$의 전체 밝기를 조정하는 다이얼로 생각할 수 있습니다.
$w_{u,v,k',k}$는 $l$층의 특성 맵$k$에 있는 모든 뉴런과 뉴런 수용장의 $u$행, $v$열, 특성맵 $k'$에 해당하는 수용장 연결 가중치입니다.
텐서플로 구현
텐서플로에서 이미지는 [높이, 너비, 채널]의 3D 텐서로 표현됩니다. 하나의 미니배치는 [미니배치 크기, 높이, 너비, 채널] 형태의 4D 텐서로 표현됩니다.
합성곱 층의 가중치는 [$f_h$, $f_w$, $f_n$, $f_{n'}$]의 4D 텐서로 표현됩니다. 합성곱 층의 편향은 [$f_n$]의 1D 텐서로 나타냅니다.
load_sample_images()를 사용해 샘플 이미지를 로드합니다. load_sample_images()를 사용하기 위해 Pillow 패키지를 설치합니다.
pip install --upgrade Pillow
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from sklearn.datasets import load_sample_image
# 샘플 이미지를 로드합니다.
china = load_sample_image('china.jpg') / 255
flower = load_sample_image('flower.jpg') / 255
images = np.array([china, flower])
batch_size, height, width, channels = images.shape
# 필터 2개를 만듭니다.
filters = np.zeros(shape=(7, 7, channels, 2), dtype=np.float32)
filters[:, 3, :, 0] = 1 # 수직선
filters[3, :, :, 1] = 1 # 수평선
outputs = tf.nn.conv2d(images, filters, strides=1, padding='SAME')
plt.imshow(outputs[0, :, :, 1], cmap='gray') # 첫 번째 그림의 두 번째 특성맵을 그립니다.
plt.show()
tf.nn.conv2d()는 텐서플로 저수준 딥러닝 API로 필터를 적용할 수 있습니다.
- 제로 패딩은 padding='SAME'입니다.
- images는 입력의 미니배치(4D 텐서)입니다.
- strides는 1이나 4개의 원소를 갖는 1D 배열로 지정할 수 있습니다. 1D 배열은 [1, $s_h$, $s_w$, 1]로 지정합니다. 현재는 배치 스트라이드, 채널 스트라이드를 건너뛸수 없습니다.
- padding은 'VALID', 'SAME' 중 하나를 지정합니다.
'VALID'는 수용장이 입력 안쪽 영역에만 놓인다는 의미로, 크기가 맞지 않은 부분은 수용 영역이 무시합니다.
keras.layers.Conv2D 층을 사용하면 CNN층의 필터를 학습할 수 있습니다. kernel_size는 필터의 높이와 너비입니다.
conv = keras.layers.Conv2D(filters=32, kernel_size=3, strides=1, \
padding='same', activation='relu')
정확한 하이퍼파라미터 값을 찾으려면 교차 검증을 사용해야하지만, 시간이 많이 걸리므로, 자주 사용되는 CNN 구조를 살펴보면서 어떤 하이퍼파라미터 값이 실전에서 잘 작동하는지 아이디어를 얻게 될 것입니다.
메모리 요구 사항
CNN의 단점은 합성곱 층이 많은 양의 RAM을 요구한다는 점입니다. 특히 훈련하는 동안 역전파 알고리즘이 역방향 계산시 정방향에서 계산했던 모든 중간값을 필요로 하기 때문입니다.
5x5 필터로 stride=1, 'same' 패딩을 사용해 150x100 크기의 특성 맵 200개를 만드는 합성곱 층을 생각해봅시다. 입력이 150x100 RGB 이미지라면 파라미터 수는 (5*5*3 + 1)*200 = 15,200개입니다. 완전 연결 층보다는 적지만 계산량이 매우 많습니다.
200개의 특성 맵마다 150x100개의 뉴런을 포함하고 각 뉴런은 5*5*3개의 입력에 대한 가중치 합을 계산해야합니다. 즉, 실수 곱셈을 5*5*3(필터 크기)*150*100(특성 맵의 크기)*200(특성 맵 갯수) = 2억 2천 5백만번 해야합니다.
특성 맵이 32비트 부동소수로 표현된다면 합성곱 층의 출력은 RAM의 200*150*100*32 = 9천 6백만 비트(약 12MB)를 점유할 것입니다. 이는 한 샘플에 대해서입니다. 훈련 배치가 100개의 샘플로 이루어져 있다면, 이 층은 1.2GB RAM을 사용할 것입니다.
추론(새로운 샘플에 대해 예측)을 할 때 하나의 층이 점유하고 있는 RAM은 다음 층의 계산이 완료되자마자 해제할 수 있습니다. 그러므로 연속된 두 개의 층에서 필요로 하는 만큼의 RAM만 있으면 됩니다.
반면에, 훈련하는 동안에는 정방향에서 계산했던 모든 값이 역방향을 위해 보존되어야합니다. 그래서 각 층에서 필요한 RAM 양의 전체 합만큼 필요합니다.
TIP
메모리 부족으로 훈련이 실패한다면, 미니배치 크기를 줄여봅니다. stride를 크게 해서 차원을 줄이거나 몇 개 층을 제거할 수도 있습니다. 32비트 부동 소수 대신 16비트 부동소수를 사용할 수도 있고 여러 장치에 CNN을 분산시킬 수 있습니다.
풀링 층
풀링 층의 목적은 계산량, 메모리 사용량, 결과적으로 과대적합의 위험을 줄여주는 파라미터 수를 줄이기 위해 입력 이미지의 부표본(subsample) 즉, 축소본을 만드는 것입니다.
풀링 층은 합성곱 층에서와 마찬가지로, 풀링 층의 뉴런은 이전 층의 작은 사격 영역의 수용장 안에 있는 뉴런의 출력과 연결되어 있습니다. 크기, 스트라이드, 패딩 유형을 지정해야합니다. max pooling layer는 수용장 안에 있는 값들 중 최대값을 풀링 층에 전달합니다. 합성곱 층과 큰 차이점은 가중치가 없다는 점입니다.
아래 그림에서는 2x2 풀링 커널(pooling kernel, 가중치가 없어서 pooling function이라고도 부름)과 stride 2를 사용하고 패딩은 사용하지 않습니다.
NOTE
풀링층은 모든 입력 채널에 독립적으로 적용되므로 출력의 깊이가 입력의 깊이와 동일합니다.
최대 풀링층은 작은 변화에도 일정 수준의 불변성(invariance)을 만들어줍니다. 2x2 커널, stride=2인 최대 풀링 층을 통과할 경우, 그림이 약간 이동해도 A, B 그림의 경우 같은 출력을 내고 있습니다. 이처럼 최대 풀링층을 추가하면 전체적으로 일정 수준의 이동 불변성(translation invariance)을 얻을 수 있습니다. 이는 분류작업처럼 예측이 이런 작은 부분에서 영향을 받지 않는 경우 유용할 수 있습니다.
최대 풀링의 단점은 입력값을 많이 잃습니다. 2x2 커널과 stride=2를 사용하면 면적이 $\frac{1}{4}$로 줄어듭니다. 뒷장에서 배울 시맨틱 분할의 경우 불변성이 아닌 등변성(equivariance)가 목표가 되는데, 이런 경우는 풀링층이 필요하지 않습니다.
텐서플로 구현
max_pool = keras.layers.MaxPool2D(pool_size=2)
최대 풀링 층은 기본적으로 'valid' 패딩을 사용합니다. 즉, 어떤 패딩도 하지 않습니다.
max_pool = keras.layers.AvgPool2D(pool_size=2)
이 층은 최댓값이 아닌 평균을 계산합니다. 조금 놀랍게도 평균을 계산하면 최댓값을 계산하는 것보다 정보 손실이 적습니다.
일반적으로 최대 풀링 층이 성능이 더 좋습니다. 조금 더 명확한 신호로 작업할 수 있으며 평균 풀링보다 강력한 이동 불변성을 제공하고 연산 비용이 조금 덜 듭니다.
최대, 평균 풀링은 공간 차원이 아닌 깊이 차원으로 수행될 수 있습니다. 동일한 패턴을 회전한 여러 필터로 학습했을 때, 깊이 방향 최대 풀링 층은 회전에 상관없이 동일한 출력을 만듭니다. 두께, 밝기, 왜곡 등 어떤 것에 대해서도 불변성을 학습할 수 있습니다.
케라스는 깊이 방향(depthwise) 풀링 층을 제공하지 않습니다. 따라서 텐서플로 저수준 딥러닝 API를 사용할 수 있습니다.
output = tf.nn.max_pool(images,
ksize=(1, 1, 1, 3),
strides=(1, 1, 1, 3),
padding='valid')
깊이 방향 풀링층을 정의할 때 ksize, strides가 1이어야 합니다. 배치, 높이, 너비 차원을 따라 커널 크기, 스트라이드가 1이라는 뜻입니다. 예를 들어 입력 깊이와 커널, 스트라이드 깊이가 나누어 떨어저야 합니다. 이전 층이 20개의 특성맵을 갖는다면 3의 배수가 아니므로 동작하지 않습니다.
케라스 모델의 층으로 사용하려면 Lambda 층으로 감싸거나 사용자 정의 케라스 층을 만듭니다.
depth_pool = keras.layers.Lambda(
lambda X: tf.nn.max_pool(X, ksize=(1, 1, 1, 3), strides=(1, 1, 1, 3),
padding='valid')
)
마지막 풀링 층의 종류로 전역 평균 풀링 층(global average pooling layer)이 있습니다. 이 층은 다른 풀링 층과 다르게 각 특성 맵의 평균을 계산합니다. 각 샘플의 특성 맵마다 하나의 숫자를 출력한다는 뜻입니다. 이는 매우 파괴적인 연산이지만, 출력층에는 유용할 수 있습니다.
global_avg_pool = keras.layers.GlobalAvgPool2D()
입력과 같은 크기(차원)의 커널을 사용하는 풀링 층과 같습니다.
global_avg_pool = keras.layers.Lambda(lambda X: tf.reduce_mean(X, axis=[1, 2]))
# axis 0은 배치 차원인 듯
합성곱 신경망을 만들기 위한 모든 구성요소를 배웠으니 이제 조합하는 방법을 알아봅시다.
x = tf.constant([[1., 1.],
[2., 2.]])
tf.reduce_mean(x)
# <tf.Tensor: shape=(), dtype=float32, numpy=1.5>
tf.reduce_mean(x, 0)
# <tf.Tensor: shape=(2,), dtype=float32, numpy=array([1.5, 1.5], dtype=float32)>
tf.reduce_mean(x, 1)
# <tf.Tensor: shape=(2,), dtype=float32, numpy=array([1., 2.], dtype=float32)>
CNN 구조
전형적인 CNN 구조는 아래처럼 합성곱 층(+ReLU) + 풀링층 + 합성곱 층(+ReLU) + 풀링층 + ... + 완전 연결층(+ReLU) + .. + 마지막 층으로 구성됩니다. 네트워크를 통과할 수록 이미지는 작아지지만, 합성곱 층의 특성 맵이 깊어지기 때문에 괜찮습니다. 아래 그림에서 두 번째 합성곱은 더 두꺼운 것을 확인할 수 있습니다.
Tip
합성곱 층에 너무 큰 커널을 사용하는 것은 실수입니다. 5x5 커널의 합성곱 층 보다 3x3 커널 합성곱 층 두 개를 쌓습니다. 파라미터, 계산량이 더 적고 일반적으로 더 나은 성능을 냅니다. 5x5 커널은 25+1개의 파라미터, 3x3 커널은 18+2개의 파라미터를 갖습니다.
단, 첫 번째 합성곱 층은 큰 크기의 커널(5x5 이상 커널, stride=2)을 사용합니다. 정보 손실을 최소화하면서 공간 방향 차원을 줄일 수 있기 때문입니다. 커널 크기를 늘려도 입력 이미지는 3개의 커널만 가지므로 비용이 작습니다.
아래 코드는 Fashion MNIST 데이터셋 문재 해결을 위한 간단한 CNN입니다.
model = keras.models.Sequential([
keras.layers.Conv2D(64, 7, activation='relu', padding='same',
input_shape=[28, 28, 1]),
keras.layers.MaxPooling2D(2),
keras.layers.Conv2D(128, 3, activation='relu', padding='same'),
keras.layers.Conv2D(128, 3, activation='relu', padding='same'),
keras.layers.MaxPooling2D(2),
keras.layers.Conv2D(256, 3, activation='relu', padding='same'),
keras.layers.Conv2D(256, 3, activation='relu', padding='same'),
keras.layers.MaxPooling2D(2),
keras.layers.Flatten(),
keras.layers.Dense(128, activation='relu'),
keras.layers.Dropout(0.5),
keras.layers.Dense(64, activation='relu'),
keras.layers.Dropout(0.5),
keras.layers.Dense(10, activation='softmax')
])
- 이미지가 아주 크지 않아서 첫 번째 층은 큰 필터 7x7과 stride=1을 사용합니다.
- 최대 풀링 층을 추가해 공간 방향 차원을 절반으로 줄입니다.
- 이와 같은 구조를 반복하는데, 이미지가 클 수록 합성곱 층을 반복해 쌓습니다. 반복 횟수는 튜닝할 하이퍼파라미터입니다.
- 출력층에 다다를 수록 필터 개수를 늘립니다. 저수준 특성(작은 동심원, 수평선 등)은 개수가 적지만 이를 연결해 고수준 특성을 만들 방법은 많기 때문에 이런 구조가 합리적입니다. 보통 풀링층 다음 필터 개수를 2배로 늘립니다. 풀링층이 공간 방향을 2배로 줄여주어서 이 방법은 메모리, 파라미터, 계산 비용을 크게 늘리지 않습니다.
- 밀집 네트워크와 연결하기 위해 1D 배열로 펼쳐주고 밀집 층 사이 과대적합을 줄이기 위해 Dropout 층을 추가합니다.
위와 같은 기본적인 구조에서 많은 변종이 개발되었습니다. ILSVRC 이미지넷 대회와 같은 경연 대회의 오차율을 보면서 우승한 모델들의 발전 과정을 살펴보면 CNN을 이해하는데 도움이 될 것입니다.
고전적인 모델인 LeNet-5를 살펴보겠습니다.
LeNet-5
얀 르쿤이 만들고 MNIST에 널리 사용됐습니다.
층 | 종류 | 특성 맵 | 크기 | 커널 크기 | 스트라이드 | 활성화 함수 |
출력 | 완전 연결 | - | 10 | - | - | RBF |
F6 | 완전 연결 | - | 84 | - | - | tanh |
C5 | 합성곱 | 120 | 1x1 | 5x5 | 1 | tanh |
S4 | 평균 풀링 | 16 | 5x5 | 2x2 | 2 | tanh |
C3 | 합성곱 | 16 | 10x10 | 5x5 | 1 | tanh |
S2 | 평균 풀링 | 6 | 14x14 | 2x2 | 2 | tanh |
C1 | 합성곱 | 6 | 28x28 | 5x5 | 1 | tanh |
입력 | 입력 | 1 | 32x32 | - | - | - |
MNIST는 28x28이지만 입력할 때 제로 패딩을 넣어 32x32가 됩니다. 그리고 주입 전 정규화합니다. 네트워크 나머지 부분은 패딩을 사용하지 않아서 네트워크를 진행하며 점차 크기가 줄어듭니다.
논문에서는 평균 풀링층에 학습되는 값인 편향이 특성 맵마다 있지만, 현재는 평균 풀링에 학습되는 계수, 편향이 없으며 최대 풀링을 주로 사용합니다.
C3에 있는 대부분의 뉴런은 S2의 6개 맵 전체가 아니라 3~4개 맵에 있는 뉴런에만 연결됩니다. 더 자세한 사항은 논문 8쪽 'TABLE 1'을 참고하세요.
출력층에서는 입력과 가중치 벡터를 행렬곱하지 않고 입력 벡터와 가중치 벡터 사이의 유클리드 거리를 출력합니다. 각 출력은 이미지가 얼마나 특정 숫자 클래스에 속하는지 측정합니다. 요즘엔 크로스 엔트로피 비용 함수를 선호합니다. 이는 잘못된 예측을 많이 줄여주고 gradient가 빠르게 수렴하게 도와줍니다.
http://yann.lecun.com/exdb/lenet/index.html 에서 LeNet-5 모델이 숫자를 분류하는 멋진 데모가 있습니다.
AlexNet
AlexNet CNN 구조는 17% 에러율로 큰 차이로 우승했습니다. 2위는 26%였습니다. 구조는 LeNet-5와 비슷하며 처음으로 합성곱층 위에 풀링층을 쌓지 않고, 합성곱 층끼리 쌓았습니다.
층 | 종류 | 특성 맵 | 크기 | 커널 크기 | 패딩 | 활성화 함수 |
출력 | 완전 연결 | - | 1,000 | - | - | Softmax |
F10 | 완전 연결 | - | 4,096 | - | - | ReLU |
F9 | 완전 연결 | - | 4,096 | - | - | ReLU |
F8 | 최대 풀링 | 256 | 6x6 | 3x3 | valid | - |
C7 | 합성곱 | 256 | 13x13 | 3x3 | same | ReLU |
C6 | 합성곱 | 384 | 13x13 | 3x3 | same | ReLU |
C5 | 합성곱 | 384 | 13x13 | 3x3 | same | ReLU |
S4 | 최대 풀링 | 256 | 13x13 | 3x3 | valid | - |
C3 | 합성곱 | 256 | 27x27 | 5x5 | same | ReLU |
S2 | 최대 풀링 | 96 | 27x27 | 3x3 | valid | - |
C1 | 합성곱 | 96 | 55x55 | 11x11 | valid | ReLU |
입력 | 입력 | 3 (RGB) | 227x227 | - | - | - |
과대적합을 줄이기 위해 두 가지 규제 기법을 사용했습니다.
- F9과 F10의 출력에 드롭아웃을 50% 비율로 적용했습니다.
- 훈련 이미지를 랜덤하게 여러 간격으로 이동하거나 뒤집는 데이터 증식(data agumentation)을 수행했습니다.
data agumentation로 생성한 샘플은 진짜에 가까워야 합니다. 단순한 백색 잡음(white noise)를 추가하는 방식은 도움이 되지 않습니다. 즉, 증식 방식이 학습 가능해야 합니다. (데이터 증식에 설정한 특성이 분류에 필요한 것이어야 한다.) 만약 물체의 위치, 방향, 크기를 바꾸면 해당 특성의 변화인 위치, 방향, 크기의 변화에 덜 민감해집니다.
AlexNet은 C1과 C3 층의 ReLU 단계 후 바로 LRN(local response normalization)이라는 경쟁적인 정규화 단계를 사용했습니다. LRN은 가장 강하게 활성화된 뉴런이 다른 특성맵에 있는 같은 뉴런을 억제합니다. 이는 생물학적 뉴런에서도 관측되었습니다.
이러한 특성은 각각의 특성맵을 다른 것과 구별되게 도와주어 (한 뉴런이 가진 뚜렷한 특징을 약화시키므로) 일반화 성능을 향상시킵니다.
$$b_i=a_i{\left(k+\alpha \sum _{j=j_{low}}^{j_{high}}a_j^2\right)}^{^{-\beta }}$$
$$\begin{cases}j_{high}=\min _{\ }^{ }\left(i+\frac{r}{2},\ f_n-1\right)\\j_{low}=\max _{\ }^{ }\left(0,\ i-\frac{r}{2}\right)\end{cases}$$
- $b_i$는 특성맵$i$, $u$행, $v$열에 위치한 뉴런의 정규화된 출력입니다. 현재의 행, 열만 고려해서 $u$, $v$는 없습니다.
- $a_i$는 ReLU 단계를 지나고 정규화 단계는 거치기 전인 뉴런의 활성화 값입니다.
- $k,\ \alpha ,\ \beta ,\ r$는 하이퍼파라미터입니다. $k$는 편향, $r$은 깊이 반경(depth radius)입니다.
- $f_n$은 특성 맵의 수입니다.
예를 들어 $r=2$이고 한 뉴런이 강하게 활성화되면, 자신의 위와 아래의 특성맴에 위치한 뉴런의 활성화를 억제합니다. AlexNet의 하이퍼파라미터는 $r=2$, $\alpha=0.00002$, $\beta=0.75$, $k=1$로 설정되었습니다. $k=1$이면, 괄호 안이 항상 1보다 커서 주변 특성 맵에 있는 뉴런의 활성값($a_j$)이 커질 수록 뉴런의 출력($b_i$)이 줄어듭니다.
이를 tf.nn.local_response_normalization() 연산을 사용하여 구현할 수 있습니다. 케라스 모델에 사용하고 싶다면, Lambda 층으로 감쌉니다.
GoogLeNet
에러율을 7%로 낮춘 모델로 네트워크가 이전 CNN보다 훨씬 깊지만, 인셉션 모듈(inception module)이라는 서브 네트워크를 가지고 있어서 이전의 구조보다 훨씬 효과적으로 파라미터를 사용합니다. GoogLeNet은 AlexNet보다 10배나 적은 파라미터를 가집니다.
'3x3+1(s)'는 3x3 커널, stride=1, 'same' 패딩을 사용한다는 뜻입니다.
inception module의 과정은 아래와 같습니다. 모든 합성곱 층은 ReLU를 사용합니다. 그리고 stride=1, 'same'패딩을 사용합니다. 즉, 모든 층의 출력 너비와 높이가 입력과 같습니다.
- 처음 입력신호가 복사되어 4개의 다른 층에 주입됩니다.
- 두 번째 합성곱 층은 각기 다른 커널 크기(1x1, 3x3, 5x5)를 사용해 다른 크기의 패턴을 잡습니다.
각기 다른 패턴들인 4개의 합성곱 층에서 만든 특성 맵을 깊이 방향(axis=3)으로 쌓습니다. 텐서플로의 axis=3의 매개변수로 tf.concat() 연산을 사용하여 구현할 수 있습니다.
인셉션 모듈은 1x1 커널의 합성곱 층을 사용하는 이유는 아래와 같습니다.
- 이는 한 번에 한 픽셀만 처리해 공간 상의 패턴을 잡을 수 없지만, 깊이 차원을 따라 놓인 패턴을 잡을 수 있습니다.?
- 입력보다 더 적은 특성맵을 출력해 (커널 개수를 조절해서 깊이 차원을 늘리거나 줄일 수 있음) 차원을 줄이는 병목 층(bottleneck layer)의 역할을 합니다. 이는 연산 비용과 파라미터 개수를 줄여 훈련 속도를 높이고 일반화 성능을 향상시킵니다.
- 합성곱 층의 쌍([1x1, 3x3]과 [1x1, 5x5])이 더 복잡한 패턴을 감지할 수 있는 한 개의 강력한 합성곱 층처럼 동작합니다.? 하나의 합성곱 층이 하는 것처럼 단순한 선형 분류기 하나로 이미지를 훑는 것이 아니라 두 개의 층을 가진 신경망으로 이미지를 훑는 것입니다.
요약하면, 인셉션 모듈은 여러 크기의 복잡한 패턴이 담긴 특성맵을 출력하는 합성곱 층입니다. 각 합성곱 층의 커널 개수는 하이퍼파라미터입니다. 즉, 안타깝게도 인셉션 모듈을 추가할 때마다 하이퍼파라미터 6개가 늘어난다는 것입니다.
GoogLeNet의 CNN 구조는 아래와 같습니다. 인셉션 모듈에 있는 6개 숫자는 각 합성곱 층에서 출력하는 특성 맵의 수를 나타냅니다. 모든 합성곱 층은 ReLU 활성화 함수를 사용합니다.
- 첫 두 층(합성곱, 최대 풀링)은 계산양 감소를 위해 이미지의 높이, 너비를 4배로 줄입니다. (면적은 16배 줄어듭니다.) 많은 입력을 유지하기 위해 합성곱 층은 큰 커널도 사용합니다.
- LRN 층은 앞서 설명한 대로 이전 층의 다양한 특성을 학습합니다.
- 이어지는 두 개의 합성곱 층 중에서 첫 번째 합성곱 층이 병목 층처럼 동작합니다. 이 합성곱 쌍을 하나의 똑똑한 합성곱 층으로 생각할 수 있습니다.
- 다시 LRN 층으로 다양한 패턴을 학습합니다.
- 그 다음 최대 풀링층을 사용해 이미지의 높이, 너비를 2배로 줄여서 계산 속도를 늘립니다.
- 9개 인셉션 모듈이 길게 이어지고, 차원 감소와 속도 향상을 위해 몇 개의 최대 풀링을 끼워 넣습니다.
- 그 다음 전역 평균 풀링층이 각 특성 맵의 평균을 출력합니다. 여기서 공간의 방향에 대한 정보를 모두 잃습니다. 이 지점에서 남은 공간 정보(이미지 크기가 224x224 → 7x7로 됨)가 많지 않고, 분류 문제라서 물체의 위치가 중요하지 않기 때문에 괜찮습니다. 이 층에서 수행된 차원 축소 덕택에 완전 연결층을 사용하지 않아서 파라미터 수, 과대적합 위험을 줄일 수 있습니다.
- 마지막 층은 dropout 규제를 적용하고 softmax를 활성화 함수로 가진 완전연결층을 사용해 1000개의 클레스 확률을 구합니다.
위 그림은 약간 간소화한 버전이고 실제 GoogLeNet은 3번째, 6번째 인셉션 모듈 위에 연속된 두 개의 부가적인 분류기를 포함하고 있습니다. 분류기는 모두 하나의 평균 풀링층, 하나의 합성곱 층, 두 개의 완전 연결층, 소프트맥스 활성화층으로 구성되어 있습니다. 훈련하는 동안 여기에서의 손실이 (70% 정도 감해서) 전체 손실에 더해집니다. 이는 그레디언트 소실 문제, 네트워크를 규제하기 위해 만들었지만 효과는 적다고 알려져 있습니다.
VGGNet
VGG는 ILSVRC 2014 대회 2등입니다. VGGNet은 단순하고 고전적인 구조입니다. 2개 또는 3개의 합성곱 층 뒤에 풀링 층이 나오고 다시 2개 또는 3개의 합성곱 층과 풀링 층이 등장하는 식입니다. (VGGNet 종류에 따라 총 16~19개의 합성곱 층이 있습니다.) 마지막 밀집 네트워크는 2개의 은닉층과 출력층으로 구성됩니다. VGGNet은 필터 개수는 많이 사용하지만 모두 3x3 필터만 사용합니다.
ResNet
ResNet(잔차 네트워크, residual network)은 3.6% 이하의 놀라운 에러율을 기록했습니다. 2015 ILSVRC에서 우승한 네트워크는 152개의 층으로 구성된 매우 깊은 CNN을 사용했습니다. 더 적은 파라미터를 사용해 더 깊은 네트워크로 모델을 구성하는 일반적인 트렌드를 만들었습니다. 이런 깊은 네트워크를 훈련시킬 수 있는 핵심 요소는 스킵 연결(skip connection) 또는 숏컷 연결(shortcut connection)입니다. 즉, 어떤 층에 주입되는 신호가 상위 층의 출력에도 더해집니다.
신경망을 훈련할 때 $ h\left(X\right) $를 모델링 하는 것이 목표입니다. 목적 함수 $ h\left(X\right)$ 에 $X$를 스킵 연결하면, 아래 네트워크를 훈련할 때 $ f\left(X\right)\ =h\left(X\right)-X $에 근사한 값을 내기 위해 학습할 것입니다. 이를 잔차 학습(residual learning)이라 부릅니다.
스킵 연결을 추가하면 초기에 (층들의 초기 가중치가 0에 아까워서 영향X) 항등 함수를 모델링해서(출력해서) 목적 함수가 항등 함수에 가깝다면 일반적으로 훈련 속도가 매우 빨라질 것입니다.
또, 스킵 연결을 많이 추가하면 입력 신호가 아직 학습되지 않은 층에(사실, 전체 네트워크에) 영향을 쉽게 줄 수 있습니다. 심층 잔차 네트워크는 잔차 유닛 RU(Residual Unit)을 쌓은 것으로 볼 수 있습니다.
ResNet의 구조를 살펴보겠습니다. 이 구조의 시작과 끝 부분은 GoogLeNet과 같습니다. 중간 부분은 단순한 잔차 유닛을 매우 깊게 쌓았습니다.
각 잔차 유닛은 BN(배치 정규화), ReLU, 3x3, 공간 정보를 유지(stride=1, 'same' 패딩)하는 두 개의 합성곱 층으로 이루어져 있습니다. 특성 맵의 수는 일정 부분 이후로 2배로 늘어납니다. 대신, 일정 부분에서 stride=2로 주어 높이와 너비는 절반이 됩니다.
이렇게 될 경우, 입력을 다음 합성곱 층의 출력에 더할 때 크기가 달라 바로 더할 수 없습니다. 그래서 아래와 같이 stride=2로 주고 특성 맵의 수도 출력의 수와 맞출 수 있는 1x1 합성곱 층으로 입력을 통과시킵니다.
ResNet의 변종인 ResNet-34는 34개의 층으로 이루어진 ResNet입니다. 층을 셀 때는 파라미터를 가진 층만 셉니다. 64개의 특성 맵을 출력하는 3개 RU, 128개 맵의 4개 RU, 256개 맵의 6개 RU, 512개 맵의 3개 RU를 포함합니다.
이보다 깊은 ResNet-152는 256개 맵을 출력하는 3개 RU, 512개 맵의 8개 RU, 1024개 맵의 36개 RU, 마지막으로 2048개 맵의 3개 RU를 포함합니다. ResNet-152의 RU는 다른 ResNet과는 조금 다른데, 다른 ResNet처럼 256개의 특성맵으로 된 두 개의 3x3 합성곱 층 대신에, 64개 특성 맵 1x1 합성곱 층(병목 역할), 64개 특성 맵의 3x3 합성곱 층, 256개 특성 맵 1x1 합성곱 층(원본 깊이를 맞춰줌)을 사용합니다.
NOTE
구글의 Inception-v4는 GoogLeNet과 ResNet을 합쳐 이미지넷 분류에서 3%에 가까운 에러를 달성했습니다.
Xception
Inception-v4와 마찬가지로 GoogLeNet과 ResNet의 아이디어를 합친 것입니다. 다른 점은 인셉션 모듈이 깊이별 분리 합성곱 층(depthwise separable convolution layer 또는 분리 합성곱 층)으로 대체되었습니다.
공간 상의 패턴의 예시로는 타원이 있고, 채널 사이의 패턴을 동시에 잡으면 눈+코+입=얼굴을 인식할 수 있습니다.
일반 합성곱 층이 공간 상의 패턴과 채널 사이의 패턴을 동시에 잡기 위해 필터를 사용하지만, 분리 합성곱 층은 공간 패턴과 채널 사이 패턴을 분리하여 모델링할 수 있다고 가정합니다.
분리 합성곱 층은 두 개의 부분으로 구성됩니다. 한 부분은 하나의 공간 필터를 각 입력 특성맵에 적용합니다. 다른 부분은 1x1 필터를 사용한 일반적인 합성곱 층으로 채널 사이의 패턴만 조사합니다.
분리 합성곱 층은 채널이 너무 적은 층 다음에 사용하는 걸 피해야합니다. 예시로 3개의 채널을 가진 층 다음에 사용했지만, 이는 예시를 위한 목적입니다. 이런 이유로 Xception은 2개의 일반 합성곱 층으로 깊이를 깊게 늘려서 시작합니다. 나머지는 분리 합성곱 (총 34개)을 사용하고 몇 개의 최대 풀링 층과 전형적인 마지막 층들(전역 평균 풀링 층, 밀집 출력 층)을 사용합니다.
인셉션 모듈은 1x1 일반 합성곱 층을 포함하는데, 이는 채널 사이의 패턴만 감지합니다. Xception을 GoogLeNet의 변종으로 간주하는 이유는 1x1 일반 합성곱 층을 사용하기 때문입니다. 일반 합성곱 층(공간 패턴, 채널 패턴을 함께 고려하는)과 분리 합성곱 층의 중간 형태가 인셉션 모듈입니다.
분리 합성곱 층은 (채널 수가 적은 층 다음을 제외하고) 일반 합성곱 층보다 파라미터, 메모리, 연산, 성능 면에서 더 우수합니다. 기본으로 분리 합성곱 층을 사용하는 것을 고려해봐야 합니다.
SENet
ILSVRC 2017 대회 우승자는 SENet(squeeze-and-excitation network)입니다. SENet은 2.25% 에러율을 달성했습니다. 인셉션 네트워크와 ResNet을 확장한 버전을 각각 SE-Inception, SE-ResNet이라고 부릅니다.
SENet은 모든 유닛(즉, 모든 인셉션 모듈, 모든 잔차 유닛)에 SE 블록이라는 작은 신경망을 추가하여 성능을 향상했습니다.
SE 블록은 아래 입력값을 주는 모듈의 특성 맵 중에 활성화 되지 않은 특성 맵의 출력을 높입니다.(정확히 말하면 약한 출력의 특성 맵과 관계 없는 특성 맵의 값을 줄입니다.)
가령 눈, 코, 입을 SE 블럭이 학습한다고 할 때, 우리는 입, 코를 보았다면 눈도 볼 수 있다고 기대할 수 있습니다. 따라서 입, 코가 크게 활성화 되고 눈 특성 맵이 약하게 활성화 되어있다면, 눈 특성 맵의 출력을 높입니다. 눈이 다른 요소와 혼동된다면 이러한 특성 맵 보정이 효과적일 것입니다.
SE 블록은 전역 평균 풀링층, ReLU를 사용한 밀집 은닉층, Sigmoid를 사용한 밀집 출력 층으로 구성됩니다.
전역 평균 풀링 층이 한 특성 맵 당 평균 활성화 값을 계산합니다. 256개의 특성 맵이 입력되면 각 특성맵의 공간에 대한 평균값을 담은 256개의 숫자를 반환합니다. 이 값을 ReLU를 사용한 밀집 은닉층에 전달합니다. 이 층은 일반적으로 특성 맵 개수보다 16배 적게 해서 특성을 압축시킵니다. 만약, 256개를 입력 받았으면 16개의 원소를 가진 하나의 벡터(임베딩)로 압축시킵니다. 이 병목층을 통해 특성 조합에 대한 일반적인 표현을 학습합니다. 그 다음 Sigmoid를 사용한 밀집 은닉층은 0~1 사이값을 갖는 원소를 특성 맵 수만큼 갖는 벡터를 출력합니다. 예시에서는 256개의 벡터를 출력합니다. 그 다음, 이 벡터를 특성 맵과 곱해서 벡터 원소가 1에 가까운 특성 맵은 유지하고, 낮은 값을 갖는 특성맵은 출력이 약해집니다.
케라스를 사용해 ResNet-34 CNN 구현하기
ResNet-34 CNN을 구현해보겠습니다. 먼저 ResidualUnit 층을 만듭니다.
class ResidualUnit(keras.layers.Layer):
def __init__(self, filters, strides=1, activation='relu', **kwargs):
super().__init__(**kwargs)
self.activation = keras.activations.get(activation)
self.main_layers = [
keras.layers.Conv2D(filters, 3, strides=strides,
padding='same', use_bias=False),
keras.layers.BatchNormalization(),
self.activation,
keras.layers.Conv2D(filters, 3, strides=1,
padding='same', use_bias=False),
keras.layers.BatchNormalization()
]
self.skip_layers=[]
if strides > 1:
self.skip_layers=[
keras.layers.Conv2D(filters, 1, strides=strides,
padding='same', use_bias=False),
keras.layers.BatchNormalization()
]
def call(self, inputs):
Z = inputs
for layer in self.main_layers:
Z = layer(Z)
skip_Z = inputs
for layer in self.skip_layers:
skip_Z = layer(skip_Z)
return self.activation(Z + skip_Z)
위 코드는 아래 그림을 구현한 것입니다. skip_Z는 스킵 모듈이 있다면, 즉 합성곱 층의 stride가 커져서 입력이 메인 모듈의 출력과 크기가 다르다면 스킵 모듈을 만듭니다.
Sequential API를 사용해 ResNet-34 모델을 만들어보겠습니다.
model = keras.models.Sequential()
model.add(keras.layers.Conv2D(64, 7, strides=2, input_shape=[224, 224, 3],
padding='same', use_bias=False))
model.add(keras.layers.BatchNormalization())
model.add(keras.layers.Activation('relu'))
model.add(keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same'))
prev_filters=64
for filters in [64]*3 + [128]*4 + [256]*6 + [512]*3:
strides = 1 if filters == prev_filters else 2
model.add(ResidualUnit(filters, strides=strides))
prev_filters = filters
model.add(keras.layers.GlobalAvgPool2D())
model.add(keras.layers.Flatten())
model.add(keras.layers.Dense(10, activation='softmax'))
40줄 미만의 코드로 ILSVRC 2015년 대회 우승 모델을 만들 수 있습니다! 다른 CNN 구조를 구현하는 것도 어렵지 않지만, 사실 이런 구조는 케라스에 기본으로 포함되어 있으니 이를 사용하는 것이 좋습니다.
케라스에서 제공하는 사전 훈련된 모델 사용하기
keras.applications 패키지를 사용해 GoogLeNet이나 ResNet 같은 표준 모델의 사전 훈련된 모델을 한 줄로 불러올 수 있습니다. ResNet-50 모델을 로드해보겠습니다.
model = keras.applications.resnet50.ResNet50(weights='imagenet')
ResNet-50 모델은 224x224 픽셀의 이미지를 기대합니다. 텐서플로의 tf.image.resize() 함수로 적재한 이미지의 크기를 바꿀 수 있습니다. 이 함수는 가로-세로 비율을 유지하지 않습니다. 이것이 문제가 된다면 이미지를 가로-세로 비율이 되도록 잘라야합니다. tf.image.crop_and_resize() 함수를 사용하면 두 작업을 동시에 수행할 수 있습니다.
images_resized = tf.image.resize(images, [224, 224])
사전 훈련된 모델은 이미지가 이미 적절하게 전처리되었다고 가정하므로, 0~1 또는 -1~1 사이의 입력값을 기대합니다. 이를 모델마다 적절하게 처리해주는 preprocess_input() 함수를 제공합니다. 이 함수는 픽셀값이 0~255 사이라고 가정합니다.
inputs = keras.applications.resnet50.preprocess_input(images_resized)
이제 사전 훈련된 모델을 사용해 예측을 수행해보겠습니다.
Y_proba = model.predict(inputs)
Y_proba는 통상적인 구조대로 행 - 이미지 1개, 열 - 클래스 1개(여기서는 1,000개의 클래스가 있습니다.)인 행렬입니다. 최상위 K개의 예측을 클래스 이름과 예측 클래스의 추정 확률을 출력하려면 decode_predictions() 함수를 사용합니다. 각 이미지에 대해 최상위 K개의 예측을 담은 리스트를 반환합니다. 예측은 (클래스 ID, 이름, 확률)의 튜플을 반환합니다.
이미지넷 데이터셋에서 각 이미지는 WordNet 데이터셋에 있는 한 단어에 해당합니다. 클래스 ID는 WordNet ID가 됩니다.
top_K = keras.applications.resnet50.decode_predictions(Y_proba, top=3)
for image_index in range(len(images_resized)):
print(f'이미지 {image_index}')
for class_id, name, y_proba in top_K[image_index]:
print(f'{class_id} - {name:12s} {y_proba*100:.2f}% ')
print()
'''
이미지 0
n02825657 - bell_cote 82.35%
n03877845 - palace 6.75%
n03781244 - monastery 4.19%
이미지 1
n03530642 - honeycomb 49.83%
n13040303 - stinkhorn 33.96%
n02206856 - bee 4.35%
'''
두 이미지의 정답 클래스는 monastery와 daisy인데, 이미지 0의 경우 상위 결과 3개 안에 포함되어 있습니다. 모델이 1000개의 클레스 중에서 찾는다는 걸 고려하면 꽤 괜찮은 결과입니다.
keras.applications 패키지에 사전훈련된 모델을 사용해 좋은 이미지 분류기를 만들 수 있습니다. 다른 vision 모델들을 중에는 ResNet 모델, Inception-v3, Xception과 같은 GoogLeNet 변종 모델, 여러 가지 VGGNet 모델, 모바일 앱을 위한 경량 모델인 MobileNet과 MobileNetV2 모델이 있습니다.
사전훈련된 모델을 사용한 전이 학습
이미지넷에 없는 이미지 클래스를 분류려면, 분류기에 사전 훈련된 모델을 전이 학습 시키면 됩니다. 또한, 충분하지 않은 데이터로 이미지 분류기를 훈련할 때 사전 훈련된 모델의 하위층을 사용하면 좋습니다.
사전 훈련된 Xception 모델을 사용해 꽃 이미지를 분류하는 모델을 훈련해보겠습니다.
먼저, 텐서플로 데이터셋을 사용해 데이터를 적재합니다.
import tensorflow_datasets as tfds
dataset, info = tfds.load("tf_flowers", as_supervised=True, with_info=True)
dataset_size = info.splits['train'].num_examples # 3670
class_names = info.features['label'].names
# ['dandelion', 'daisy', 'tulips', 'sunflowers', 'roses']
n_classes = info.features['label'].num_classes # 5
with_info=True로 하면 데이터셋에 대한 정보를 얻을 수 있습니다. 이 데이터셋은 'train' 셋만 있고 test, valid 데이터 셋은 없습니다. 따라서 훈련 세트를 test, valid 등으로 나눠야합니다.
test_set, valid_set, train_set = tfds.load('tf_flowers', \
split=['train[:10%]', 'train[10%:25%]', 'train[25%:]'],\
as_supervised=True)
xception은 224x224 크기의 이미지를 기대하므로 크기를 조정하고 정규화합니다.
def preprocess(image, label):
resized_image = tf.image.resize(image, [224, 224])
final_image = keras.applications.xception.preprocess_input(resized_image)
return final_image, label
훈련 데이터를 섞고 각 데이터들을 전처리한 후 배치 크기를 지정하고 프리페치를 적용합니다.
batch_size=32
train_set = train_set.shuffle(1000)
train_set = train_set.map(preprocess).batch(batch_size).prefetch(1)
valid_set = valid_set.map(preprocess).batch(batch_size).prefetch(1)
test_set = test_set.map(preprocess).batch(batch_size).prefetch(1)
데이터 증식을 하고 싶다면 tf.image.random_crop() 함수로 이미지를 랜덤하게 자르거나 tf.image.random_filp_left_right() 함수로 이미지를 수평으로 랜덤하게 뒤집을 수 있습니다.
Tip
keras.preprocessing.image.ImageDataGenerator 클래스를 사용하면 손쉽게 디스크에서 이미지를 적재하여 여러 방식으로 데이터를 증식할 수 있습니다. 회전, 이동, 수평, 수직 뒤집기 등도 할 수 있고 원하는 어떤 변환 함수도 적용할 수 있습니다.
tf.image 연산을 기반으로 전처리 함수를 작성하면 이 함수는 tf.data 파이프라인과 상용 환경에 배포하는 모델에 모두 사용할 수 있습니다. tf.data 파이프라인을 만드는 것은 디스크뿐만 아니라 어떤 소스에서도 이미지를 효율적으로(예를 들면, 병렬로) 읽습니다.
이미지넷에서 사전훈련된 Xception 모델을 로드하고 include_top=False로 지정해 네트워크의 최상층에 해당하는 전역 풀링 층, 밀집 출력 층을 제외시킵니다. 최상위 층은 새로운 전역 풀링 층과 클래스 갯수를 맞춘 softmax를 사용하는 밀집 출력 층을 놓습니다.
base_model = keras.applications.xception.Xception(weights='imagenet',
include_top=False)
avg = keras.layers.GlobalAvgPool2D()(base_model.output)
output = keras.layers.Dense(n_classes, activation='softmax')(avg)
model = keras.Model(inputs=base_model.input, outputs=output)
훈련 초기에는 사전 훈련된 층의 가중치를 동결하는 것이 좋습니다.
for layer in base_model.layers:
layer.trainable = False
NOTE
base_model 객체 자체를 사용하는게 아니라 기반 모델 층을 직접 사용하기 때문에 base_model.trainable=False로 지정해도 효과가 없습니다.
마지막으로 모델을 컴파일하고 훈련을 시작합니다.
optimizer = keras.optimizers.SGD(lr=0.2, momentum=0.9, decay=0.01)
model.compile(loss='sparse_categorical_crossentropy', optimizer=optimizer,
metrics=['accuracy'])
history = model.fit(train_set, epochs=5, validation_data=valid_set)
CAUTION_
이 훈련은 GPU를 사용하지 않으면 매우 느리므로 Colab의 GPU 런타임을 사용해 실행하세요
몇 번의 에포크 동안 훈련하면 검증 정확도가 75~80%에 도달하는데, 이제는 더 나아지지 않을 것입니다. 이는 새로 추가한 최상위 층이 잘 훈련되었다는 것을 의미합니다. 이제 모든 층(또는 상위 층 일부)의 동결을 해제하고 훈련을 계속합니다. (층을 동결하거나 해제한 후에 모델을 다시 컴파일 해야합니다.) 이때는 사전 훈련된 가중치가 훼손되는 것을 피하기 위해 훨씬 작은 학습률로 학습합니다.
for layer in base_model.layers:
layer.trainable = True
optimizer = keras.optimizers.SGD(lr=0.01, momentum=0.9, decay=0.001)
model.compile(loss='sparse_categorical_crossentropy', optimizer=optimizer,
metrics=['accuracy'])
history = model.fit(train_set, epochs=5, validation_data=valid_set)
잠시 후 이 모델이 테스트 데이터에서 95%의 정확도를 달성할 것입니다. 이런 식으로 훌륭한 이미지 분류기를 만들 수 있습니다.
분류와 위치 추정
물체의 위치를 추정하는 것은 회귀 작업입니다. 물체 주위의 바운딩 박스(bounding box)를 예측하는 일반적인 방법은 물체 중심의 수평, 수직 좌표와 높이, 너비를 예측하는 것입니다. 모델을 크게 바꿀 필요는 없고, (일반적으로 전역 풀링 층 위에) 4개의 유닛을 가진 밀집 출력층을 추가하고 MSE 손실을 사용해 훈련합니다.
base_model = keras.applications.xception.Xception(weights='imagenet',
include_top=False)
avg = keras.layers.GlobalAvgPool2D()(base_model.output)
output = keras.layers.Dense(n_classes, activation='softmax')(avg)
loc_output = keras.layers.Dense(4, activation='sigmoid')(avg)
model = keras.Model(inputs=base_model.input, outputs=[output, loc_output])
model.compile(loss=['sparse_categorical_crossentropy', 'mse'], optimizer=optimizer,
loss_weights=[0.8, 0.2] # 어떤 것을 중요하게 생각하느냐에 따라
, metrics=['accuracy'])
history = model.fit(train_set, epochs=5, validation_data=valid_set)
꽃 데이터셋은 꽃 주위에 바운딩 박스에 대한 정보(좌표, 높이, 너비)를 가지고 있지 않습니다. 이미지 바운딩 박스를 추가하는 오픈 소스 이미지 레이블 도구로는 VGG Image Annotator, LabelImg, OpenLabeler, ImgLab이 있습니다.
이미지 마다 하나의 바운딩 박스가 있다고 생각하면, (images, (class_labels, bounding_boxes)) 형태의 튜플이 됩니다.
Tip
바운딩 박스의 높이, 너비, 수직·수평 좌표를 모두 0~1 사이로 정규화해야 합니다. 또한, 일반적으로 높이·너비는 높이·너비의 제곱근을 예측합니다. 따라서 작은 바운딩 박스의 10px 오차가 큰 바운딩 박스의 10px 오차보다 더 많은 패널티를 받습니다.
MSE는 바운딩 박스를 예측하는 좋은 지표가 아닙니다. 바운딩 박스에 널리 사용되는 지표는 IoU(intersection over union)입니다. tf.keras.metrics.MeanIoU에 구현되어 있습니다. $IoU = \frac{교집합}{합집합}$
하나의 물체를 분류하고 위치를 잘 추정할 수 있게 되었습니다. 이미지에 여러 물체가 들어 있는 경우를 생각해봅시다.
객체 탐지
한 이미지에 여러 물체를 분류하고 위치를 추정하는 작업을 객체 탐지(object detection)라고 부릅니다. 과거에는 하나의 물체를 분류하고 위치를 찾는 분류기로 이미지를 모두 훑는 방법을 사용했습니다. 이미지를 6x8 격자(grid)로 나누고 하나의 CNN(굵은 검정 사각형)이 3x3 영역을 슬라이딩 하며 전체 이미지를 훑습니다. 객체 크기가 서로 다를 수 있어 다른 크기의 CNN을 추가로 사용할 수 있습니다. 4x4 영역을 추가로 슬라이딩할 수 있습니다.
이 방법은 조금씩 다른 위치에서 동일한 물체를 여러 번 감지합니다. 불필요한 바운딩 박스를 처리할 필요가 있습니다. 흔히 사용하는 방법은 NMS(Non-Max Supperssion)입니다.
- 먼저 CNN이 꽃 이미지의 존재 여부 확률을 추정합니다, sigmoid를 가진 1개 유닛의 밀집 출력층을 CNN에 추가합니다. loss로 이진 크로스 엔트로피 손실을 사용합니다. 존재 여부 점수가 임계값 이하인 바운딩 박스를 모두 삭제합니다.
- 존재 여부 점수가 가장 높은 바운딩 박스를 찾고, 이 바운딩 박스와 많이 중첩된( ex. IoU가 60% 이상) 다른 바운딩 박스를 제거합니다.
- 제거할 바운딩 박스가 없을 때까지 2단계를 반복합니다.
이런 객체 탐지 방식은 잘 작동하지만 CNN을 여러 번 실행시켜야 해 많이 느립니다.(출력이 많아지면 CNN을 여러 번 실행하는 듯) 다행히 완전 합성곱 신경망 (Fully Convolutional Network) FCN을 사용하면 CNN을 훨씬 빠르게 이미지에 슬라이딩시킬 수 있습니다.
완전 합성곱 신경망 FCN
FCN은 시맨틱 분할을 위한 논문에서 처음 소개되었습니다. 저자들은 객체 탐지를 위한 CNN 맨 위 밀집 층을 합성곱 층으로 대체할 수 있다고 설명합니다. 밀집 층이 만드는 출력과 이를 대체할 합성곱 층이 만드는 출력이 같기 때문입니다.
7x7 크기의 특성 맵(커널 크기 아님) 을 100개 출력하는 합성곱 층 위에 뉴런이 200개 있는 밀집 층이 있습니다. 각 뉴런은 출력된 100x7x7 크기의 활성화 값(그리고 편향)에 대해 가중치 합을 계산합니다. 이 밀집 층을 7x7 크기의 필터(커널) 200개를 갖고 'valid' 패딩을 사용하는 합성곱 층으로 대체하면, 이 층은 1x1 크기의 특성맵 200개를 출력할 것입니다. 이는 밀집층이 만드는 숫자와 완전히 동일합니다.
밀집 층은 입력 특성마다 1개의 가중치를 두므로 특정 입력 크기를 기대합니다. 반면에 합성곱 층은 어떤 크기의 이미지도 처리할 수 있습니다. 따라서 FCN은 어떤 크기의 이미지에서도 훈련하고 실행할 수 있습니다.
한 가지 예외가 있는데, 'valid' 패딩을 사용하는 합성곱 층은 입력 크기가 커널 크기보다 작으면 문제가 됩니다.
예를 들어 꽃 분류와 위치 추정을 위해 하나의 CNN을 훈련했다고 해봅시다. 이 모델은 224x224 이미지에서 훈련했고, 10개의 숫자를 출력합니다.
- 0~4번째 출력은 softmax - 밀집층으로 전달되어 클래스 확률을 만듭니다.
- 5번째 출력은 logistic - 밀집층으로 전달되어 존재여부 확률을 출력합니다.
- 6~9번째 출력은 어떤 활성화 함수도 통과하지 않는데, 이 출력은 바운딩 박스 좌표, 높이·너비를 나타냅니다.
이 밀집 층을 합성곱 층으로 바꿀 수 있습니다. 훈련하기 전 CNN을 FCN으로 바꿀 수도 있지만, 이미 훈련된 밀집층이 있다면 밀집층의 가중치를 합성곱 층으로 복사할 수 있습니다.
FCN에 224x224 크기의 이미지가 주입되면 출력 직전 층의 합성곱 층(병목 층)은 7x7 크기의 특성 맵을 출력합니다. FCN에 448x448 이미지를 주입하면 병목층은 14x14 특성 맵을 출력할 것입니다. (합성곱 커널이 2배로 더 훑으므로)
예제의 네트워크가 same 패딩을 사용한다고 가정하고 448은 2로 나누어 떨어지므로 stride=2여도, 반올림 오차 없이 7이 될 때까지 나누어집니다. 'valid' 패딩은 특성 맵의 크기를 감소 시키고 stride=1, 2가 아닌 값을 사용해도 특성 맵은 작아질 것입니다.
FCN의 출력은 8x8 크기의 특성 맵 10개가 됩니다. stride=1이므로 14-7+1=8 입니다. 각 셀은 10개의 숫자(5개 클래스 확률, 1개의 존재여부 점수, 4개의 바운딩 박스 좌표)를 담고 있습니다.
FCN은 전체 이미지를 딱 한 번 처리하여 8x8 크기의 배열을 출력합니다. 이를 직관적으로 설명하자면, 원본 448x448 이미지를 14x14 격자로 나누고 이 격자 위에 7x7 윈도가 슬라이딩한다고 생각하면 가능한 윈도우 위치는 8x8 = 64개 이므로 64개의 예측이 됩니다. 하지만, FCN 방식은 이미지를 딱 한 번(YOLO, you only look once)만 처리하기 때문에 효율적입니다.
YOLO
2015년 발표된 매우 빠르고 정확한 객체 탐지 구조입니다. YOLOv2, YOLOv3에서 성능이 더 향상되었습니다. YOLOv3 알고리즘은 너무 빨라서 실시간으로 비디오에 적용할 수 있습니다. https://www.youtube.com/watch?v=MPU2HistivI
YOLOv3 구조는 앞서 설명한 구조와 매우 비슷하지만 몇 가지 중요한 차이점이 있습니다.
- 각 격자 셀마다 1개가 아닌 5개의 바운딩 박스를 출력합니다. 바운딩 박스마다 1개의 존재여부 점수가 부여됩니다. 이 모델은 PASCAL VOC 데이터셋에서 훈련되어서 20개의 클래스를 갖습니다. 따라서 각 격자 셀마다 20개의 클래스 확률을 출력합니다. 따라서 셀마다 출력은 총 45개 입니다.(5[바운드 박스]x4[박스 위치] + 5[존재 여부] + 20[클래스 확률])
- YOLOv3는 바운딩 박스 위치를 격자 셀에 대한 상대 위치로 예측합니다. YOLOv3는 박스 중심이 격자 셀 안에 놓인 것만 예측하도록 훈련합니다. (박스 자체는 셀을 넘길 수 있습니다.) YOLOv3는 logistic 함수를 사용해 박스 좌표가 0~1 사이가 되도록 만듭니다.
- 신경망을 훈련하기 전에 YOLOv3는 anchor box(bounding box prior)라 부르는 5개의 대표 바운딩 박스 크기를 찾습니다. 이 크기들을 구하기 위해 훈련 세트의 바운딩 박스 세트의 박스, 높이에 k-평균 알고리즘을 적용해 군집화합니다. 신경망이 각 셀마다 5개의 바운딩 박스를 예측할 때 각 앵커 박스의 크기를 조정할 스케일을 예측합니다. 이는 적합한 바운딩 박스를 빠르게 학습해서 훈련 속도를 높입니다.
가령 한 앵커 박스가 100x50 일때, 수직 방향 스케일 조정 비율이 1.5, 수평 방향 스케일 조정 비율이 0.9로 예측한다면 예측되는 바운딩 박스 크기는 150x45가 됩니다. 더 정확히 말하면 스케일 조정 비율의 로그값을 예측합니다.
- 네트워크가 훈련하는 동안 몇 번의 배치마다 랜덤하게 새로운 이미지 차원(330x330 ~ 608x608)을 선택해 다른 스케일의 이미지를 학습합니다. 이를 통해 네트워크가 다른 스케일의 객체를 탐지하는 방법을 학습합니다.
YOLOv3는 다른 스케일에도 사용 가능한데, 작은 스케일에서는 정확도가 떨어지는 대신 큰 스케일보다 예측 속도가 빠릅니다. 문제에 따라 적절히 trade-off를 선택할 수 있습니다.
mAP
객체 탐지에서 널리 사용하는 평가 지표는 mAP(mean average precision)입니다. 이 지표는 재현율-정밀도 trade-off에서 ROC 곡선 아래 면적인 AUC를 계산할 때 재현율이 증가할 때 정밀도가 상승하는 영역이 포함될 수 있어서 만들어졌습니다. 특히 낮은 재현율에서 그런 현상이 빈번히 발생합니다.
만약 재현율-정밀도가 0.1-0.9 인데, 0.2-0.96 이 된다면 0.2 재현율의 분류기를 사용하는 것이 당연합니다. 따라서 특정 재현율에서 최대 정밀도를 찾아야 합니다. 따라서 0에서의 최대 정밀도, 0.1에서의 최대 정밀도, 0.2에서의 최대 정밀도, 1에서의 최대 정밀도를 계산합니다. 그 다움 이 최대 정밀도를 평균합니다. 이 값을 AP(average precision) 평균 정밀도라고 부릅니다. 여러 클래스가 있을 때는 각 클래스에 대해 AP를 계산한 다음 평균 AP를 계산합니다. 이것이 mAP입니다.
객체 탐지 시스템에서는 클레스 분류 뿐만 아니라 바운딩 박스 위치도 잘 예측해야합니다. 객체가 바운딩 박스 밖에 있다면 당연히 올바른 예측에 포함시키면 안됩니다. 이를 해결하는 방법은 IOU 임계점을 정의하는 것입니다. 예를 들어 IOU가 0.5보다 크고 예측 클래스가 맞다면 올바른 예측 클래스로 간주합니다. 이에 해당하는 mAP는 mAP@0.5라고 씁니다.
https://github.com/zzh8829/yolov3-tf2에는 텐서플로를 사용한 YOLO 구현이 있습니다. SSD(싱글 샷 탐지 모델), R-CNN 같은 모델은 TF-hub에 포팅되어 있습니다.
Faster R-CNN은 이미지가 먼저 하나의 CNN을 통과해 그 출력을 RPN(Region Proposal Network)로 전달합니다. RPN은 객체가 들어 있을 가능성이 높은 바운딩 박스를 추출합니다. 이 CNN의 출력을 기반으로 바운딩 박스마다 분류기를 실행합니다.?
탐지 시스템은 속도, 정확도, 복잡도 등 여러 요인에 기반해 선택됩니다. 이제 우리는 바운딩 박스를 그려 위치를 추정할 수 있게 되었습니다. 픽셀 단위로 구분하는 방법도 알아봅시다.
시맨틱 분할 semantic segmentation
semantic segmentation(시맨틱 분할)은 각 픽셀이 객체의 클래스(도로, 사람 ..)로 분류됩니다. 이 작업에서 가장 어려운 점은 이미지가 stride가 1보다 큰 층을 지나며 공간(위치) 정보를 잃기 때문입니다. 따라서 보통 CNN은 이미지 왼쪽 아래에 사람이 있다는 정도만 알지 그 이상은 알지 못합니다.
2015년 조너선 롱이 발표한 논문에서 이 문제를 CNN을 FCN으로 변환해 해결했습니다. 논문 속 CNN이 입력 이미지에 적용하는 전체 stride는 32입니다.(1보다 큰 stride를 모두 더한 것입니다.) 이는 마지막 층이 입력 이미지보다 32배 작은 특성맵을 출력한다는 의미입니다. 해상도를 32배 늘리는 upsampling layer(업샘플링 층)를 하나 추가해서 이 문제를 해결합니다.
이중 선형 보간(bilinear interpolation)은 이미지 사이즈를 4배나 8배로 늘리는 데 적합합니다. 저자들은 대신 전치 합성곱 층(transposed convolutional layer)을 사용합니다. 이 층은 이미지에 0으로 채워진 행과 열을 삽입하여 늘린 다음 일반적인 합성곱을 수행합니다.
tf.keras.Conv2DTranspose로 구현할 수 있습니다.
NOTE
전치 합성곱 층에서 stride는 입력이 얼마나 늘어나는지를 정의합니다. 즉, stride가 클수록 출력이 커집니다.
텐서플로 합성곱 연산
텐서플로는 몇 가지 다른 종류의 합성곱 층도 제공합니다.
- keras.layers.Conv1D
1D 입력에 대한 합성곱 층을 만듭니다. 시계열, 텍스트(문자, 단어의 시퀀스)에서 사용합니다.
- keras.layers.Conv3D
3D PET(양전자를 방출하는 약품을 체내에 주입해 인체를 촬영) 스캔같은 3D 입력을 위한 합성곱 층을 만듭니다.
- dilation_rate
tf.keras의 합성곱 층에 있는 dilation_rate 매개변수를 2 이상으로 지정하면 아트루스 합성곱 층(atrous convolutional layer)이 됩니다. 이는 0으로 된 행과 열을 추가하여 늘린 필터로 보통의 합성곱 층을 사용하는 것과 동일합니다. [[1, 2, 3]]의 1x3 필터를 팽창 비율(dilation_rate) 4로 늘리면 [[1, 0, 0, 0, 2, 0, 0, 0, 3]]이 됩니다. 이는 추가적인 계산, 파라미터 없이 합성곱의 필터 크기(수용장의 크기)를 늘리게 해줍니다.
- tf.nn.depthwise_conv2d()
이 함수는 깊이방향 합성곱 층(depthwise convolutional layer)을 만듭니다. 모든 필터를 개개의 입력 채널에 독립적으로 적용합니다. $f_n$개의 필터와 $f'_n$개의 채널이 있다면 $f_n*f'_n$개의 특성 맵을 출력합니다.
전치 합성곱 층을 사용해 이미지 해상도를 늘려도 여전히 정확도가 떨어집니다. 이를 개선하기 위해 저자들은 아래쪽 층에서 부터 스킵 연결을 추가했습니다.
(32배가 아니라) 합성곱으로 작아진 출력 이미지를 먼저 2배로 업샘플링하고 아래쪽 층의 출력을 더합니다. 그리고 이 더한 결과를 16배로 업샘플링해 총 32배의 업샘플링을 달성합니다.
이러한 방식은 풀링 층에서 잃은 공간 정보를 복원합니다. 비슷하게 2배 업샘플링 + 아래쪽 층의 출력 + 2배 업샘플링 + 아래쪽 층의 출력 + 8배 업샘플링으로 총 32배 업샘플링을 했을 때 최상의 결과를 얻을 수 있었습니다.
원본 이미지보다 더 크게 업샘플링하는 것도 가능한데 이는 이미지 해상도를 증가시킨다고해서 초해상도(super-resolution)라고 부릅니다.
인스턴스 분할을 시맨틱 분할과 비슷하지만, 클래스 물체를 하나의 덩어리로 합치는 것이 아니라 각 물체를 구분하여 표시합니다. 예를 들면 자전거를 개별로 인식합니다.
텐서플로 모델 프로젝트에 포함된 인스턴스 분할 모델로 2017년 Mask R-CNN이 있습니다. 이 모델은 Faster R-CNN 모델을 확장하여 각 바운딩 박스에 대해 픽셀 마스크를 추가로 생성했습니다. 따라서 각 물체마다 클래스 추정확률, 바운딩 박스 뿐만 아니라 바운딩 박스 속 물체의 픽셀을 구분하는 픽셀 마스크도 얻을 수 있습니다.
adversarial learning(적대적 학습), explainabilty(설명 가능성), image generation(이미지 생성), single-shot learning(싱글-샷 학습) 등 컴퓨턴 비전 분야는 매우 빠르게 발전하고 있습니다.
싱글-샷 학습은 객체를 한 번 본 후에 인식할 수 있는 시스템입니다.
capsule networks(캡슐 네트워크)와 같은 새로운 구조를 연구하기도 합니다. https://homl.info/capsnetvideos
출처: 핸즈온 머신러닝 2판
사진 출처: 핸즈온 머신러닝 2판
'핸즈온 머신러닝 2판' 카테고리의 다른 글
16장 RNN과 어텐션을 사용한 자연어 처리 (1) | 2022.01.26 |
---|---|
15장 RNN과 CNN을 사용해 시퀀스 처리하기 (0) | 2022.01.21 |
13장 텐서플로에서 데이터 적재와 전처리하기 (0) | 2022.01.10 |
12장 텐서플로를 사용한 사용자 정의 모델과 훈련 (0) | 2022.01.04 |
11장 심층 신경망 훈련하기 (0) | 2021.12.30 |