해당 포스팅은 '밑바닥부터 시작하는 딥러닝1'을 공부하고 정리, 요한 글입니다. 모든 내용은 해당 도서를 기준으로 합니다.
◼️ 7.1 전체구조
지금까지 본 신경망은 인접하는 계층의 모든 뉴런과 결합되어 있다. 이를 완전 연결(fully-connected)이라고 하며 완전히 연결된 게층을 affine 계층이라는 이름으로 구현했다.
CNN도 지금까지 본 신경망과 같이 레고 블록처럼 계층을 조합하여 만들 수 있다. 합성곱 계층(convolutional layer)과 풀링 계층(pooling layer)이 새롭게 등장한다.
CNN에서는 새로운 합성곱 계층(conv)과 풀링 계층(pooling)이 추가 되며 CNN 계층은 Conv-FeLU-(Pooling) 흐름으로 연결된다.
지금까지의 Affine-ReLU 연결이 Conv-FeLU-(Pooling) 흐름으로 변경되었다고 보면 된다.
CNN에서 출력에 가까운 층에서는 지금까지의 Affine-ReLU 구성을 사용할 수 있다. 또 마지막 출력 계층에서는 affine-softmax 조합을 그대로 사용한다.
◼️ 7.2 합성곱 계층
◾ 7.2.1 완전 연결 계층의 문제점
완전 연결 계층의 문제점은 데이터의 형상이 무시 된다는 것이다.
입력 데이터가 이미지인 경우를 예로 들면, 이미지는 세로, 가로, 채널(색상)로 구성된 3차원 데이터이다. 완전 연결 계층에 입력할 때는 3차원 데이터를 평평한 1차원 데이터로 평탄화 해 줘야 한다.
이미지는 이 3차원 형상에 공간적 정보가 담겨 있는데, 완전 연결 계층은 형상을 무시하고 모든 입력 데이터를 동등한 뉴런으로 취급하여 형상에 담긴 정보를 살릴 수 없다.
반면에, 합성곱 계층은 형상을 유지한다. 이미지도 3차원 데이터로 입력 받으며 다음 계층에도 3차원 데이터로 전달한다.
CNN에서는 합성곱 계층의 입출력 데이터를 feature map이라고 한다. 입력 데이터를 입력 feature map, 출력 데이터를 출력 feature map이라고 한다.
◾ 7.2.2 합성곱 연산
합성곱 계층에서는 합성곱 연산을 처리한다. 합성곱 연산은 이미지 처리에서 말하는 필터 연산에 해당한다.
그림 7-3과 같이 합성곱 연산은 입력 데이터에 필터를 적용한다. 데이터와 필터의 형상을 (높이 height, 너비 width)로 표기한다. 문헌에 따라 필터를 커널이라 칭하기도 한다.
합성곱 연산은 필터의 윈도우를 일정 간격으로 이동해 가며 입력 데이터에 적용한다. 윈도우는 그림 7-4의 회색 3x3 부분을 가리킨다.
입력과 필터에서 대응하는 원소끼리 곱한 후 그 총합을 구한다. 이 계산을 단일 곱셉-누산(fused multiply-add, FMA)라고 한다.
이 과정을 모든 장소에서 수행하면 합성곱 연산의 출력이 완성된다.
CNN에서는 필터의 매개변수가 그동안의 가중치에 해당한다.
또, CNN에서도 편향이 존재한다.
편향까지 포함하면 그림 7-5와 같은 흐름이 된다.
편향은 필터를 적용한 후의 데이터에 더해지며 편향은 항상 하나만 존재한다. 이 하나의 값을 필터를 적용한 모든 원소에 더하는 것이다.
◾ 7.2.3 패딩
합성곱 연산을 수행하기 전에 입력 데이터 주변을 특정 값으로 채우기도 한다. 이를 패딩(padding)이라고 하며 합성곱 연산에서 자주 이용하는 기법이다.
위 예시에서는 패딩을 1로 설정했지만 2나 3등 원하는 정수로 설정할 수 있다.
합성곱 연산을 몇번이나 되풀이 하는 심층 신경망에서는 입력 데이터의 크기가 작아지는 문제가 있다. 때문에 어느 시점에서는 출력 크기가 1이 되어 더이상 합성곱 연산을 적용할 수 없게 된다. 이를 방지하기 위해 패딩을 사용하여 출력 크기를 조정한다.
◾ 7.2.4 스트라이드
필터를 적용하는 위치의 간격을 스트라이드(strid)라고 한다.
스트라이드를 2로 하면 필터를 적용하는 윈도우가 두 칸씩 이동하게 된다.
스트라이드를 키우면 출력 크기는 작아지며 패딩을 크게 하면 출력 크기가 커진다. 이러한 관계를 수식화 하면 다음과 같다.
◾ 7.2.5 3차원 데이터의 합성곱 연산
그림 7-8은 3차원 데이터의 합성곱 연산의 예이다.
그림 7-9는 계산 순서를 나타낸 그림이다.
2차원일 때와 비교하면 길이 방향(채널 방향)으로 feature map이 늘어났다. 채널 쪽으로 feature map이 여러 개 있다면 입력 데이터와 필터의 합성곱 연산을 채널마다 수행하고 그 결과를 더해서 하나의 출력을 얻게 된다.
3차원 합성곱 연산에서 주의해야 할 점은 입력 데이터의 채널 수와 필터의 채널 수가 같아야 한다는 것이다.
◾ 7.2.6 블록으로 생각하기
3차원 합성곱 연산은 데이터와 필터를 직육면체 블록이라고 생각하면 쉽다. 3차원 데이터를 다차원 배열로 나타낼 때는 (채널, 높이, 너비) 순으로 쓰며 필터도 같은 순서로 쓴다.
위 예시에서는 출력 데이터는 한장의 feature map이 된다. 한 장의 feature map을 다른 말로 하면 채널이 1개인 feature map이 된다.
합성곱 연산의 출력으로 다수의 채널을 내보내려면 필터(가중치)를 다수 사용해야 한다.
그림 7-11과 같이 필터를 FN개 적용하면 출력 맵도 FN개 생성된다. 이 FN개의 맵을 모으면 형상이 (FN, OH, OW)인 블록이 완성된다. 이 완성된 블록을 다음 계층으로 넘기겠다는 것이 CNN의 처리 흐름이다.
합성곱 연산에서는 필터의 수도 고려해야 한다. 따라서 필터의 가중치 데이터는 4차원 데이터이며 (출력 채널 수, 입력 채널 수, 높이, 너비) 순으로 쓴다.
합성곱 연산에도 편향이 쓰인다.
형상이 다른 블록의 덧셈은 넘파이의 브로드캐스트 기능으로 쉽게 구현할 수 있다.
◾ 7.2.7 배치처리
합성곱 연산도 배치 처리를 지원한다.
그래서 각 계층을 흐르는 데이터의 차원을 하나 늘려 4차원 데이터(데이터 수, 채널 수, 높이, 너비)로 저장한다.
그림 7-13을 보면 각 데이터의 선두에 배치용 차원을 추가했다.
주의해야 할 점은 신경망에 4차원 데이터가 하나 흐를 때마다 데이터 N개에 대한 합성곱 연산이 이뤄진다는 것이다. 즉, N회 분의 처리를 한번에 수행하는 것이다.
◼️ 7.3 풀링 계층
풀링은 세로, 가로 방향의 공간을 줄이는 연산이다
위는 2x2 최대 풀링(max pooling)을 스트라이드 2로 처리하는 순서이다. 최대 풀링은 최댓값을 구하는 연산으로 2x2는 대상 영역의 크기를 의미한다.
위 예시에서는 스트라이드를 2로 설정했으므로 2x2 윈도우가 원소 2칸 간격으로 이동한다.
보통 풀링의 윈도우 크기와 스트라이드는 같은 값으로 설정한다.
◾ 7.3.1 풀링 계층의 특징
- 학습 해야 할 매개변수가 없다.
- 채널 수가 변하지 않는다.
-입력의 변화에 영향을 적게 받는다.
◼️ 7.4 합성곱/풀링 계층 구현하기
◾ 7.4.2 im2col로 데이터 전개하기
for문 대신 in2col이라는 편의 함수를 사용해 간단하게 구현 해 볼 수 있다.
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
"""다수의 이미지를 입력받아 2차원 배열로 변환(평탄화).
Parameters
----------
input_data : 4차원 배열 형태의 입력 데이터(이미지 수, 채널 수, 높이, 너비)
filter_h : 필터의 높이
filter_w : 필터의 너비
stride : 스트라이드
pad : 패딩
Returns
-------
col : 2차원 배열
"""
N, C, H, W = input_data.shape
out_h = (H + 2*pad - filter_h)//stride + 1
out_w = (W + 2*pad - filter_w)//stride + 1
img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))
for y in range(filter_h):
y_max = y + stride*out_h
for x in range(filter_w):
x_max = x + stride*out_w
col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]
col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
return col
im2col은 필터링 하기 좋게 입력 데이터를 전개한다.
그림 7-18과 같이 입력 데이터에서 필터를 적용하는 영역을 한 줄로 늘어놓고 이 전개를 필터를 적용하는 모든 영역에서 수행하는 게 im2col 함수이다.
그림 7-19와 같이 im2col 방식으로 출력한 결과는 2차원 행렬이다. CNN은 데이터를 4차원 배열로 저장하기 때문에 2차원 출력의 데이터를 4차원으로 reshape 한다.
◾ 7.4.3 합성곱 계층 구현하기
class Convolution:
def __init__(self, W, b, stride=1, pad=0):
self.W = W
self.b = b
self.stride = stride
self.pad = pad
# 중간 데이터(backward 시 사용)
self.x = None
self.col = None
self.col_W = None
# 가중치와 편향 매개변수의 기울기
self.dW = None
self.db = None
def forward(self, x):
FN, C, FH, FW = self.W.shape
N, C, H, W = x.shape
out_h = 1 + int((H + 2*self.pad - FH) / self.stride)
out_w = 1 + int((W + 2*self.pad - FW) / self.stride)
col = im2col(x, FH, FW, self.stride, self.pad)
col_W = self.W.reshape(FN, -1).T
out = np.dot(col, col_W) + self.b
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
self.x = x
self.col = col
self.col_W = col_W
return out
def backward(self, dout):
FN, C, FH, FW = self.W.shape
dout = dout.transpose(0,2,3,1).reshape(-1, FN)
self.db = np.sum(dout, axis=0)
self.dW = np.dot(self.col.T, dout)
self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)
dcol = np.dot(dout, self.col_W.T)
dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)
return dx
◾ 7.4.4 풀링 계층 구현하기
입력 데이터에 풀링 적용 영역을 전개하고, 전개한 행렬에서 행 별 최댓값을 구하고 적절한 형상으로 성형하기만 하면 된다.
풀링 계층 구현은 그림 7-22와 같이 세 단계로 진행한다.
class Pooling:
def __init__(self, pool_h, pool_w, stride=1, pad=0):
self.pool_h = pool_h
self.pool_w = pool_w
self.stride = stride
self.pad = pad
self.x = None
self.arg_max = None
def forward(self, x):
N, C, H, W = x.shape
out_h = int(1 + (H - self.pool_h) / self.stride)
out_w = int(1 + (W - self.pool_w) / self.stride)
col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
col = col.reshape(-1, self.pool_h*self.pool_w)
arg_max = np.argmax(col, axis=1)
out = np.max(col, axis=1)
out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
self.x = x
self.arg_max = arg_max
return out
def backward(self, dout):
dout = dout.transpose(0, 2, 3, 1)
pool_size = self.pool_h * self.pool_w
dmax = np.zeros((dout.size, pool_size))
dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
dmax = dmax.reshape(dout.shape + (pool_size,))
dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)
return
◼️ 7.5 CNN 구현하기
위 그림의 CNN 네트워크는 “convolution-ReLU-Pooling-Affien-ReLU-Affine-Softmax” 순으로 흐른다.
import sys, os
import pickle
import numpy as np
from collections import OrderedDict
from common.layers import *
from common.gradient import numerical_gradient
class SimpleConvNet:
"""단순한 합성곱 신경망
conv - relu - pool - affine - relu - affine - softmax
Parameters
----------
input_size : 입력 크기(MNIST의 경우엔 784)
hidden_size_list : 각 은닉층의 뉴런 수를 담은 리스트(e.g. [100, 100, 100])
output_size : 출력 크기(MNIST의 경우엔 10)
activation : 활성화 함수 - 'relu' 혹은 'sigmoid'
weight_init_std : 가중치의 표준편차 지정(e.g. 0.01)
'relu'나 'he'로 지정하면 'He 초깃값'으로 설정
'sigmoid'나 'xavier'로 지정하면 'Xavier 초깃값'으로 설정
"""
def __init__(self, input_dim=(1, 28, 28),
conv_param={'filter_num':30, 'filter_size':5, 'pad':0, 'stride':1},
hidden_size=100, output_size=10, weight_init_std=0.01):
filter_num = conv_param['filter_num']
filter_size = conv_param['filter_size']
filter_pad = conv_param['pad']
filter_stride = conv_param['stride']
input_size = input_dim[1]
conv_output_size = (input_size - filter_size + 2*filter_pad) / filter_stride + 1
pool_output_size = int(filter_num * (conv_output_size/2) * (conv_output_size/2))
# 가중치 초기화
self.params = {}
self.params['W1'] = weight_init_std * \
np.random.randn(filter_num, input_dim[0], filter_size, filter_size)
self.params['b1'] = np.zeros(filter_num)
self.params['W2'] = weight_init_std * \
np.random.randn(pool_output_size, hidden_size)
self.params['b2'] = np.zeros(hidden_size)
self.params['W3'] = weight_init_std * \
np.random.randn(hidden_size, output_size)
self.params['b3'] = np.zeros(output_size)
# 계층 생성
self.layers = OrderedDict()
self.layers['Conv1'] = Convolution(self.params['W1'], self.params['b1'],
conv_param['stride'], conv_param['pad'])
self.layers['Relu1'] = Relu()
self.layers['Pool1'] = Pooling(pool_h=2, pool_w=2, stride=2)
self.layers['Affine1'] = Affine(self.params['W2'], self.params['b2'])
self.layers['Relu2'] = Relu()
self.layers['Affine2'] = Affine(self.params['W3'], self.params['b3'])
self.last_layer = SoftmaxWithLoss()
def predict(self, x):
for layer in self.layers.values():
x = layer.forward(x)
return x
def loss(self, x, t):
"""
Parameters
----------
x : 입력 데이터
t : 정답 레이블
"""
y = self.predict(x)
return self.last_layer.forward(y, t)
def accuracy(self, x, t, batch_size=100):
if t.ndim != 1 : t = np.argmax(t, axis=1)
acc = 0.0
for i in range(int(x.shape[0] / batch_size)):
tx = x[i*batch_size:(i+1)*batch_size]
tt = t[i*batch_size:(i+1)*batch_size]
y = self.predict(tx)
y = np.argmax(y, axis=1)
acc += np.sum(y == tt)
return acc / x.shape[0]
def numerical_gradient(self, x, t):
"""기울기(수치미분) 구함.
Parameters
----------
x : 입력 데이터
t : 정답 레이블
Returns
-------
각 층의 기울기를 담은 dictionary 변수
grads['W1']、grads['W2']、... 각 층의 가중치
grads['b1']、grads['b2']、... 각 층의 편향
"""
loss_w = lambda w: self.loss(x, t)
grads = {}
for idx in (1, 2, 3):
grads['W' + str(idx)] = numerical_gradient(loss_w, self.params['W' + str(idx)])
grads['b' + str(idx)] = numerical_gradient(loss_w, self.params['b' + str(idx)])
return grads
def gradient(self, x, t):
"""기울기(오차역전파법)구함.
Parameters
----------
x : 입력 데이터
t : 정답 레이블
Returns
-------
각 층의 기울기를 담은 사전(dictionary) 변수
grads['W1']、grads['W2']、... 각 층의 가중치
grads['b1']、grads['b2']、... 각 층의 편향
"""
# forward
self.loss(x, t)
# backward
dout = 1
dout = self.last_layer.backward(dout)
layers = list(self.layers.values())
layers.reverse()
for layer in layers:
dout = layer.backward(dout)
# 결과 저장
grads = {}
grads['W1'], grads['b1'] = self.layers['Conv1'].dW, self.layers['Conv1'].db
grads['W2'], grads['b2'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
grads['W3'], grads['b3'] = self.layers['Affine2'].dW, self.layers['Affine2'].db
return grads
def save_params(self, file_name="params.pkl"):
params = {}
for key, val in self.params.items():
params[key] = val
with open(file_name, 'wb') as f:
pickle.dump(params, f)
def load_params(self, file_name="params.pkl"):
with open(file_name, 'rb') as f:
params = pickle.load(f)
for key, val in params.items():
self.params[key] = val
for i, key in enumerate(['Conv1', 'Affine1', 'Affine2']):
self.layers[key].W = self.params['W' + str(i+1)]
self.layers[key].b = self.params['b' + str(i+1)]
◼️ 7.6 CNN 시각화 하기
MNIST 데이터셋으로 간단한 CNN을 학습하면 필터를 시각화 할 수 있다.
그림 7-24와 같이 학습 전 필터는 무작위로 초기화 되고 있지만 학습 후 필터는 엣지(색상이 바뀐 경계선)와 블롭(국소적으로 덩어리진 영역)을 보고 있다.
이처럼 합성곱 계층의 필터는 엣지나 블롭 등의 원시적인 정보를 추출할 수 있다.
겹겹이 쌓인 CNN의 각 계층에서는 계층이 깊어질 수록 추출되는 정보가 더 추상화 된다.
처음 층은 단순한 엣지에 반응하고 이어서 텍스쳐에 반응하고, 더 복잡한 사물의 일부에 반응하도록 변화한다.
층이 깊어지면서 뉴런이 반응하는 대상이 단순한 모양에서 고급 정보로 변화해 간다.
◼️ 정리
CNN은 지금까지의 완전 연결 계층 네트워크에 합성곱 계층과 풀링 계층을 새로 추가한다.
CNN을 시각화 하면 계층이 깊어질 수록 고급 정보가 추출되는 모습을 확인할 수 있다.