
Convolutional Neural Network의 약자로, 합성곱(convolution)을 수행하는 인공 신경망을 의미합니다.
합성곱의 수행 과정과 신경망의 기본 원리를 각각 알고 있으면 두 개념을 합친 CNN을 이해하는 건 어렵지 않은데요, 무엇보다 합성곱 필터 = 신경망 가중치라는 개념을 이해하는 것이 중요합니다.
아래 글을 통해 자세히 알아보도록 합시다.
CNN: 계층을 조립하여 만드는 것은 기존 신경망과 같지만, 합성곱 계층과 풀링 계층이 등장한다.
완전연결, Affine 계층: 인접하는 계층의 모든 뉴런과 결합. affine계층 뒤에 활성화 함수 ReLU 계층이 이어짐.
CNN에서는 합성곱 계층과 풀링 계층 추가, 출력 직전 은닉층과 출력층에서는 똑같이 Affine+ReLU, Affine+Softmax 계층을 쓸 수 있음,
완전연결 계층(Affine 계층)으로 이뤄진 네트워크

CNN으로 이뤄진 네트워크 예

참고
합성곱 연산: 이미지 처리에서 말하는 필터 연산.
커널: 필터의 다른 말
필터의 윈도우를 일정 간격으로 이동해가며 입력 데이터에 적용. (윈도우는 아래 그림의 회색 영역)
CNN에서는 필터의 매개변수가 그동안의 ‘가중치’에 해당.
CNN에서의 편향: 편향은 1by1으로 하나만 존재하며, 이를 필터 적용한 데이터 모든 원소에 더함.
합성곱 연산의 예



패딩: 합성곱 연산 수행 전 입력 데이터 주변을 0으로 채움.
패딩 크기를 원하는 정수로 설정할 수 있음.
패딩의 목적: 출력 크기 조정할 목적으로 사용. 합성곱 연산을 되풀이하면 출력이 입력보다 줄어들게 되어 심층신경망에서 문제가 될 수 있음. 패딩을 통해 이를 방지.
합성곱 연산의 패딩 처리

스트라이드: 필터를 적용하는 간격(윈도우 이동 간격)
스트라이드를 키우면 출력 크기는 작아짐.(패딩을 크기 하면 출력 크기도 커지므로, 둘은 반대 작용을 함.)
지금까지 본 입력 크기, 필터 크기, 패딩, 스트라이드를 통해 출력 크기를 계산할 수 있음. 아래 수식을 참고
주의 사항: 출력 크기가 정수가 아니면 오류를 내는 등의 대응 필요. 하지만, 딥러닝 프레임워크에서는 그냥 가까운 정수로 반올림 시켜버리기도 함.
텐서플로우에서는? TensorFlow(특히 Keras Conv2D, tf.nn.conv2d 등)에서는 공식에 의해 나온 출력 크기가 정수가 아닐 경우 자동으로 내림(floor, 즉 버림) 하여 정수로 만든다고 한다.
스트라이드 적용 예시

위에서 본 합성곱은 모두 2차원. 하지만 이미지만 해도 3차원 데이터(세로, 가로, 채널(RGB))임.
3차원 데이터의 특징 맵: 채널 방향으로 특징 맵 개수가 늘어남. → 특징 맵 개수만큼 필터가 필요
합성곱 연산을 채널마다 수행 후 그 결과를 하나로 더함.
당연히, 특징 맵의 채널 수와 필터의 채널 수가 같아야 함.
3차원 데이터 합성곱 연산 예시

3차원 데이터 합성곱 연산의 계산 순서

3차원 합성곱 연산 쉽게 생각하는 법: 데이터와 필터를 직육면체 블록이라고 생각하기.
출력 맵이 한 장의 특징맵임에 주목. 만약 다수의 특징 맵을 출력으로 보내고 싶다면? 필터를 다수 사용하기.
여러 장의 출력 특징 맵을 다음 계층으로 넘기는 것이 CNN의 처리 흐름.
필터의 가중치 데이터: (출력 채널 수, 입력 채널 수, 높이, 너비)
예를 들어 채널 수 3, 크기 5by5, 필터가 20개인 경우 (20, 3, 5, 5)로 표기
편향은 채널 하나에 값 하나씩으로 구성. 아래 그림에서 편향의 형상은 (FN, 1, 1)임. 편향 블록과 출력 특징 맵 블록을 더하면 편향의 각 값이 필터의 출력인 (FN, OH, OW) 블록의 대응 채널의 원소 모두에 더해짐.
직육면체 블록으로 보는 합성곱 연산

여러 필터를 사용한 합성곱 연산의 예

합성곱 연산의 처리 흐름(편향 추가)

CNN에서의 배치 처리: 각 계층을 흐르는 데이터의 차원을 하나 늘려 4차원 데이터로 저장.(맨 앞에 배치 처리를 위한 ‘데이터 수’차원 추가
신경망에 4차원 데이터가 하나 흐를 때마다 데이터 N(위에서 말한 데이터 수 차원)개에 대한 합성곱 연산이 이뤄짐.
합성곱 연산의 처리 흐름

풀링: 세로, 가로 방향의 공간을 줄이는 연산. 특정 사이즈의 영역을 원소 하나로 집약
최대 풀링(max pooling): 특정 영역, 예를 들어 2x2 영역이라 하면 그 영역의 최대 값만을 하나 꺼냄.
풀링 윈도우: 풀링 수행할 이동하는 특정 영역.
보통 풀링 윈도우와 스트라이드 값을 동일하게 설정
2x2 최대 풀링을 스트라이드 2로 처리하는 순서

풀링 계층은 합성곱 계층과 달리 학습해야 할 매개변수가 없음.
채널 수가 변하지 않는다.(합성곱이 필터 채널 수 만큼 출력 특징 맵 채널을 만드는 것과 대조)
입력의 변화에 영향을 적게 받음(robustness): 최대 풀링을 한다 치면, 풀링 영역 내에서 최대값 요소가 위치를 약간 바꾸거나, 그 외 요소의 값이 약간 바뀌는 것이 출력 특징 맵에 반영되지 않는다.ㄴ
풀링은 채널 수를 바꾸지 않는다

풀링 계층의 강건함

필터 적용 영역이 겹치게 되면 im2col로 전개한 후의 원소 수가 원래 블록의 원소 수보다 많아집니다. 그래서 im2col을 사용해 구현하면 메모리를 더 많이 소비하는 단점이 있습니다. 하지만 컴퓨터는 큰 행렬을 묶어서 계산하는데 탁월합니다. 예를 들어 행렬 계산 라이브러리(선형 대수 라이브러리) 등은 행렬 계산에 고도로 최적화되어 큰 행렬의 곱셈을 빠르게 계산할 수 있습니다. 그래서 문제를 행렬 계산으로 만들면 선형 대수 라이브러리를 활용해 효율을 높일 수 있습니다.



합성곱 계층은 필터(가중치), 편향, 스트라이드, 패딩을 인수로 받아 초기화.
필터는 (FN, C, FH, FW)의 4차원 형상
im2col 함수 덕분에 완전 연결층과 거의 같이 구현 가능.
합성곱 역시 col2im을 쓴다는 점을 제외하면 완전 연결층과 동일.
im2col 함수의 인터페이스
im2col 사용 예시
import sys, os
sys.path.append(os.pardir)
from common.util import im2col
# 배치 크기가 1, 채널은 3개, 7x7 데이터
x1 = np.random.rand(1, 3, 7, 7) # 데이터 수, 채널 수, 높이, 너비
col1 = im2col(x1, 5, 5, stride=1, pad=0)
print(col1.shape) # (9, 75)
# 배치 크기가 10, 나머지는 첫째와 같음.
x2 = np.random.rand(10, 3, 7, 7) # 데이터 10개
col2 = im2col(x2, 5, 5, stride=1, pad=0)
print(col2.shape) # (90, 75)
# 두 경우 모두 im2col함수 적용 후 2번째 차원 원소 75 = 필터의 원소 수와 같음(3 * 5 * 5)
합성곱 계층 구현
class Convolution:
def __init__(self, W, b, stride=1, pad=0):
self.W = W
self.b = b
self.stride = stride
self.pad = pad
def forward(self, x):
FN, C, FH, FW = self.W.shape
N, C, H, W = x.shape
out_h = int(1 + (H + 2*self.pad - FH) / self.stride)
out_w = int(1 + (W + 2*self.pad - FW) / self.stride)
# 입력 데이터를 im2col로 전개하고 필터도 reshape을 사용해 2차원 배열로 전개
col = im2col(x, FH, FW, self.stride, self.pad)
# reshape의 두 번째 인수가 -1: 다차원 배열의 원소 수가 변환 후에도 똑같이 유지되도록 적절히 묶어주는 편의기능
# 즉, 앞의 코드에서 (10, 3, 5, 5)형상을 한 다차원 배열 W의 원소 수는 750이고, 이 배열에
# reshape(10, -1)을 호출하면 750개 원소를 10묶음으로, 형상이 (10, 75)인 배열로 만들어줌.
col_w = self.W.reshape(FN, -1).T # 필터 전개.
# 전개한 두 행렬의 곱을 구함
out = np.dot(col, col_W) + self.b
# 출력 데이터를 적절한 형상으로 바꿔줌. (N, C, H, W 순으로 형상 순서를 바꿔 주기 위함)
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
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
CNN에서는 필터가 곧 가중치라는 사실을 절대 잊으면 안 됨!
입력 데이터에 풀링 적용 영역을 전개(2x2 풀링의 예)

일단 이렇게 전개 후 전개한 행렬에서 행별 최댓값 구하고 적절한 형상으로 성형.

파이썬 구현
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
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)
# 전개 (1)
col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
col = col.reshape(-1, self.pool_h*self.pool_w)
# 최댓값 (2)
out = np.max(col, axis=1)
# 성형 (3)
out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
return out
풀링 계층의 세 단계
풀링 계층은 역전파 안 하는 것 아니었나? 분명 학습해야 할 매개변수가 없다고 알고 있는데
최대 풀링(MaxPooling)의 역전파
예를 들어, 2x2 최대 풀링이면, 각 2x2 영역에서 뽑힌 최대값 위치에만 오차가 할당되고 나머지 값엔 0이 할당됩니다.
즉, "기울기 분배 방식"만 다를 뿐, 풀링 계층 자체적으로 학습하는 것은 없음
아래는 풀링 계층의 역전파.
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 dx

초기화 인수로 주어진 합성곱 계층의 하이퍼파라미터를 딕셔너리에서 꺼냄.
class SimpleConvNet:
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))
학습에 필요한 매개변수는 1번째 층의 합성곱 계층과 나머지 두 완전연결 계층의 가중치와 편향.
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.parmas['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)
마지막으로 CNN을 구성하는 계층들을 생성
self.layers = OrderDict()
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()
추론을 수행하는 predict 메서드와 손실함수의 값을 구하는 loss 메서드를 다음과 같이 구현 가능.
# x: 인수, t: 정답 레이블
def predict(self, x):
for layer in self.layers.values():
x = layer.forward(x)
return x
def loss(self, x, t):
y = self.predict(x)
return self.lat_layer.forward(y, t)
오차 역전파법으로 기울기 구하는 구현
def gradient(self, x, t):
# forward propagation
self.loss(x, t)
# back propagation
dout = 1
dout = self.last_layer.backward(dout)
layers = list(self.layers.values())
layers.reverse()
for layer in layers:
dout = layer.backward(dout)
# save results
grads = {}
grads['W1'] = self.layers['Conv1'].dW
grads['b1'] = self.layers['Conv1'].db
grads['W2'] = self.layers['Affine1'].dW
grads['b2'] = self.layers['Affine1'].db
grads['W3'] = self.layers['Affine2'].dW
grads['b3'] = self.layers['Affine2'].db
return grads
위 코드를 토대로 직접 훈련을 시켜보도록 하자.
훈련 결과는 아래와 같다.

=============== Final Test Accuracy ===============
test acc:0.9886
Saved Network Parameters!
합성곱 계층은 입력으로 받은 이미지 데이터에서 무엇을 볼까?
위에서 시행한 CNN 훈련은 합성곱 계층의 가중치 형상이 (30, 1, 5, 5)이었음.
필터의 크기가 5x5이고 채널이 1개이므로, 1채널의 흑백 이미지로 시각화 가능.
필터: 1개, 5x5 사이즈 필터 시각화


CNN의 필터(가중치)는 시각적으로 볼 수 있다. 가중치를 시각화한다는 개념이 매우 흥미롭다.
딥러닝의 흥미로운 점은 합성곱 계층을 여러 겹 쌓으면, 층이 깊어지면서 더 복잡하고 추상화된 정보가 추출된다는 것. 처음 층은 단순한 에지에 반응하고, 이어서 텍스처에 반응하고, 더 복잡한 사물의 일부에 반응하도록 변화함. 층이 깊어질 수록 뉴런이 반응하는 대상이 단순한 모양에서 ‘고급’정보로 변화. → 사물의 의미를 이해하도록 변화.
CNN의 합성곱 계층에서 추출되는 정보

이건 진짜 신비롭다. 층이 깊어질수록 필터(가중치)가 더 시각적으로 고급화 된다니.
이것의 더 자세한 설명은 아래 글을 참조.
왜 층이 깊어질수록 추출 정보가 더 추상적이 될까?
합성곱 계층과 풀링 계층(단순히 원소를 줄이기만 하는 서브샘플링 계층)을 반복한 후 완전연결 계층을 거쳐 결과 출력.
1997년에 제안됨.
LeNet의 구성

2012년 발표
LeNet과의 차이
LeNet, AlexNet, 현대 CNN 모두 큰 차이는 없지만 이를 둘러싼 컴퓨터 기술과 데이터가 진보를 이룸.
AlexNet의 구성
