[밑바닥부터시작하는딥러닝1] Chapter 7. 합성곱 신경망(CNN)
7.1. 전체 구조
지금까지 본 신경망은 인접하는 계층의 모든 뉴런과 결합되어 있었다.
이를 완전연결(fully-connected)라고 하며, 완전히 연결된 계층을 Affine 계층이라고 불렀다.
완전연결 신경망은 Affine 계층 뒤에 활성화 함수를 갖는 ReLU 계층(or Sigmoid 계층)이 이어진다.
그림 7-1에서는 Affine-ReLU조합이 4개, 마지막 5번째 층은 Affine 계층에 이어 소프트맥스 계층에서 최종 결과(확률)를 출력한다.
CNN은 새로운 합성곱 계층(Conv)와 풀링 계층(Pooling)이 추가된다.
Conv-ReLU-(Pooling) 흐름으로 연결된다.(Pooling은 생략 가능)
7.2. 합성곱 계층
✔️ 완전연결 계층 문제점
완전연결 신경망에서는 완전연결 계층(Affine 계층)을 사용했다. 완전연결 계층에서는 인접하는 계층의 뉴런이 모두 연결되고 출력의 수는 임의로 정할 수 있다.
그러나 '데이터의 형상이 무시된다'는 단점이 있다.
입력데이터가 이미지인 경우, (가로, 세로 채널(색상))으로 구성된 3차원 데이터인데 이를 완전연결 계층에 입력할 때는 평평한 1차원 데이터로 평탄화 해줘야 한다.
이미지는 3차원 형상으로 공간적 정보가 담겨있다. 예를 들어 공간적으로 가까운 픽셀은 값이 비슷하거나 RGB의 각 채널은 서로 밀접하게 연결되어있는 등 3차원 속에서 의미를 갖는 본질적인 패턴이 숨어있을 것이다. 하지만 완전연결 계층은 형상을 무시하고 모든 입력 데이터를 동등한 뉴런(같은 차원의 뉴런)으로 취급하여 형상에 담긴 정보를 살릴 수 없다.
한편, 합성곱 계층은 형상을 유지한다. 이미지를 3차원 데이터로 입력받고 다음 계층에도 3차원 데이터로 전달한다. 따라서 이미지처럼 형상을 가진 데이터를 제대로 이해할 가능성이 크다.
특징맵(feature map) : 합성곱 계층의 입출력 데이터
입력특징맵(input feature map) : 합성곱 계층의 입력 데이터
출력특징맵(output feature map) : 합성곱 계층의 출력 데이터
✔️ 합성곱 연산
합성곱계층에서는 합성곱 연산을 처리하는데 이미지 처리에서 말하는 필터연산에 해당한다.
그림 7-3과 같이 합성곱 연산은 입력 데이터에 필터(커널)를 적용한다.
합성곱 연산은 필터의 윈도우(window)를 일정 간격으로 이동해가며 입력 데이터에 적용한다.
그림 7-4의 회색 3*3부분을 말하는데 입력과 필터에서 대응하는 원소끼리 곱한 후 그 총합을 구한다.
(단일 곱셈-누산 fused multiply-add, FMA)
이 과정을 모든 장소에서 수행하여 해당 장소에 저장하면 합성곱 연산의 출력이 완성된다.
완전 신경망에서 가중치 매개변수는 CNN에서의 필터의 매개변수에 해당한다.
그리고 완전 신경망의 편향처럼 CNN에도 편향이 존재한다.
그림 7-5와 같이 편향은 필터를 적용한 후의 데이터에 더한다.
또한 편향은 항상 하나(1*1)만 존재하고 그 하나의 값을 필터를 적용한 모든 원소에 더한다.
✔️ 패딩
패딩(padding) : 합성곱 연산을 수행하기전에 입력 데이터 주변을 특정값(예컨대 0)으로 채우는 것
그림 7-6은 (4, 4)크기의 입력 데이터에 폭이 1인 패딩을 적용한 모습이다.
패딩은 2나 3 등 원하는 정수로 설정할 수 있으며, 그림 7-5에 패딩 2로 설정하면 (8, 8), 3으로 설정하면 (10, 10)이 된다.
패딩은 주로 출력 크기를 조정할 목적으로 사용한다.
(4, 4) 입력데이터에 (3, 3) 필터를 적용하면 출력은 (2, 2)가 된다. 이처럼 합성곱 연산을 거칠 때마다 크기가 작아지면 어느 시점에서는 출력크기가 1이 되어 더이상 합성곱 연산을 적용할 수 없게된다. 이러한 사태를 막기 위해 패딩을 사용한다.
예를 들어 패딩의 폭을 1로 설정하면 (4, 4) 입력데이터에 대해 출력이 같은 크기인 (4, 4)로 유지된다.
즉, 입력데이터의 공간적 크기를 고정한 채로 다음 계층에 전달할 수 있다.
✔️ 스트라이드
스트라이드(stride) : 필터를 적용하는 위치의 간격
앞선 예시들은 모두 스트라이드가 1이었고 그림 7-7은 스트라이드가 2이다.
스트라이드를 키우면 출력 크기는 작아진다. 한편, 패딩을 크게하면 출력크기는 커졌다.
이를 수식화하면 식 7-1과 같다. 단, 식 7-1은 정수로 나눠떨어지는 값이어야 한다.
입력 크기를 (H, W), 필터 크기를 (FH, FW), 출력 크기를 (OH, OW), 패딩을 P, 스트라이드를 S
✔️ 3차원 데이터의 합성곱 연산
지금까지 2차원 형상을 다루는 합성곱 연산을 살펴보았으니 3차원도 알아보자.
2차원일 때(그림 7-3)와 비교하면 길이 방향(채널 방향)으로 특징맵이 늘어났다.
채널쪽으로 특징맵이 여러개라면 입력 데이터와 필터의 합성곱 연산을 채널마다 수행하고 그 결과를 더해서 하나의 출력을 얻는다.
3차원 합성곱 연산에서 주의할 점은 입력데이터의 채널 수와 필터의 채널 수가 같아야 한다는 것이다.
필터의 크기는 원하는 값으로 설정할 수 있지만 모든 채널의 필터가 같은 크기여야 한다.
✔️ 블록으로 생각하기
3차원 합성곱 연산은 데이터와 필터를 직육면체 블록이라고 생각하면 쉽다.
3차원 데이터는 (C, H, W) = (채널 Channel, 높이 Height, 너비 Width)
필터는 (C, FH, FW) = (채널 Channel, 필터 높이 Filter Height, 필터 너비 Filter Width)
그림 7-10에서 출력 데이터는 한 장의 특징 맵(채널이 1개인 특징맵)이다.
다수의 채널을 내보내려면 그림 7-11과 같이 필터(가중치)를 다수 사용하면 된다.
필터를 FN개 사용하면 출력맵도 FN개 생성된다.
이 FN개의 맵을 모아 형상이 (FN, OH, OW)인 블록이 완성된다.
이를 다음 계층으로 넘기는 것이 CNN의 처리 흐름
이와 같이 합성곱 연산에서는 필터의 수도 고려해야 하기 때문에 필터의 가중치 데이터는 4차원 데이터이다.
(출력 채널 수, 입력 채널 수, 높이, 너비)
(20, 3, 5, 5) = 채널 수 3, 크기 5*5, 필터 20개
그림 7-12와 같이 편향은 채널 하나에 값 하나씩으로 구성된다.
신경망 처리에서는 입력 데이터를 한 덩어리로 묶어 배치로 처리했다.
합성곱 연산도 마찬가지로 배치 처리를 지원하고자 각 계층의 데이터 차원을 하나 늘려 4차원 데이터(데이터 수, 채널 수, 높이, 너비)로 저장한다.
이처럼 데이터는 4차원 형상을 가진 채 각 계층을 타고 흐르는데 신경망에 4차원 데이터가 하나 흐를 때마다 데이터 N개에 대한 합성곱 연산이 이뤄진다. 즉, N회분의 처리를 한 번에 수행한다.
✔️ 배치 처리
7.3. 풀링 계층
풀링은 세로, 가로 방향의 공간을 줄이는 연산이다.
그림 7-14는 2*2 최대 풀링(max pooling)을 스트라이드 2로 처리하는 것이다.
최대 풀링은 최댓값(max)을 구하는 연산으로 2*2는 대상 영역의 크기를 뜻한다.
그림과 같이 2*2 크기의 영역에서 가장 큰 원소 하나를 꺼내고, 스트라이드가 2이므로 2칸 간격으로 이동한다.
보통 풀링의 윈도우 크기와 스트라이드는 같은 값으로 설정한다.
✔️ 풀링 계층의 특징
- 합성곱 계층과 달리 학습해야 할 매개변수가 없다.
- 채널 수가 변하지 않는다.
- 입력의 변화에 영향을 적게 받는다.(강건하다)
입력 데이터가 조금 변해도 풀링의 결과는 잘 변하지 않는다. 그림 7-16과 같이 입력 데이터의 차이(데이터가 오른쪽으로 1칸씩 이동)를 풀링이 흡수해 사라지게 된다.
7.4. 합성곱/풀링 계층 구현하기
✔️ im2col로 데이터 전개하기
합성곱 연산을 그대로 구현하려면 for문을 겹겹이 써야하지만 im2col이라는 편의함수를 사용하면 간단하게 구현할 수 있다.
(im2col은 'image to column'으로 '이미지에서 행렬로' 라는 뜻)
im2col은 입력 데이터를 필터링(가중치 계산)하기 좋게 전개하는(펼치는) 함수이다.
그림 7-17과 같이 배치 안의 데이터 수까지 포함한 4차원 데이터를 2차원으로 변환한다.
그림 7-18과 같이 필터 적용하는 영역(3차원 블록)을 한 줄로 늘어 놓는다.
이 과정에서 스트라이드를 크게 잡아 필터의 적용 영역이 겹치지 않도록 할 수 있지만 실제 상황에서는 영역이 겹치는 경우가 대부분이다.
따라서 필터 적용 영역이 겹치면서 im2col로 전개한 후의 원소 수가 원래 블록의 원소 수보다 많아져 메모리를 더 많이 소비한다는 단점이 있다. 하지만 컴퓨터는 큰 행렬을 묶어서 계산하는데 최적화 되어 있기 때문에 문제를 행렬 계산으로 만들면 효율을 높일 수 있다.
im2col로 입력데이터를 전개한 다음 합성곱 계층의 필터(가중치)를 1열로 전개하여 두 행렬의 내적을 계산한다.
이는 완전연결계층의 Affine 계층에서 한 것과 거의 같다.
출력한 결과는 2차원 행렬인데 CNN은 데이터를 4차원 배열로 저장하므로 2차원 출력데이터를 4차원으로 변형(reshape)한다.
✔️ 합성곱 계층 구현
합성곱 계층은 필터(가중치), 편향, 스트라이드, 패딩을 인수로 받아 초기화한다.
필터는 (FN, C, FH, FW)의 4차원 형상으로 FN은 필터 개수, C는 채널, FH는 필터 높이, FW는 필터 너비
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.strid)
out_w = int(1 + (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)
return out
합성곱 계층의 forward구현을 im2col로 전개한 덕분에 완전연결 계층의 Affine 계층과 거의 똑같이 구현할 수 있었다.
다음은 합성곱 계층의 backward구현할 차례인데 Affine 계층의 구현과 공통점이 많아 생략한다.
단, 합성곱 계층의 역전파에서는 im2col을 역으로 처리해야 한다.
✔️ 풀링 계층 구현
풀링계층구현도 마찬가지로 im2col을 사용하지만 채널 쪽이 독립적이라는 점이 다르다.
전개 후, 그림 7-22와 같이 전개 행렬에서 행별 최댓값을 구하고 적절한 형상으로 성형하기만 하면 된다.
1. 입력 데이터를 전개한다.
2. 행별 최댓값을 구한다.
3. 적절한 모양으로 성형한다.
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
7.5. CNN 구현하기
구현한 합성곱 계층과 풀링 계층을 조합하여 CNN을 구현해보자.
그림 7-23은 Convolution - ReLU - Pooling - Affine - ReLU - Affine - Softmax순으로 흐른다.
합성곱 계층의 하이퍼파라미터는 딕셔너리 형태로 주어진다.(conv_param)
하이퍼파라미터를 딕셔너리에서 꺼내고 합성곱 계층의 출력 크기를 계산한다.
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번째 층의 합성곱 계층과 나머지 두 완전연결 계층의 가중치와 편향이다.
이 매개변수를 param 딕셔너리에 저장한다.
# 가중치 초기화
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)
CNN을 구성하는 계층들을 생성한다.
순서가 있는 딕셔너리인 layer에 계층들을 차례로 추가한다.
마지막 SoftmaxWithLoss 계층은 last_layer로 따로 저장한다.
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()
인수 x는 입력 데이터, t는 정답 레이블
추론을 수행하는 predict는 초기화 때 layers에 추가한 계층을 앞에서부터 차례로 forward 메서드를 호출하며 그 결과를 다음 계층에 전달한다.
손실 함수를 구하는 loss 메서드는 predict의 결과를 인수로 마지막 층의 forward 메서드를 호출한다.
def predict(self, x):
for layer in self.layers.values():
x = layer.forward(x)
return x
다음 오차역전파법으로 기울기를 구한다.
이 때 순전파와 역전파를 반복하며 마지막 grad에 각 가중치 매개변수의 기울기를 저장한다.
def gradient(self, x, t):
# 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
7.6. CNN 시각화하기
합성곱 계층을 시각화해서 CNN이 보고 있는 것이 무엇인지 알아보자.
✔️ 1번째 층의 가중치 시각화
학습 전 필터는 무작위로 초기화되어 있어 흑백의 정도에 규칙성이 없다.
하지만 학습 후 필터는 흰색에서 검정색으로 점차 변화하는 필터, 덩어리(블롭)가 진 필터 등 규칙을 띄는 필터로 바뀌었다.
오른쪽과 같이 규칙성이 있는 필터는 엣지(색상이 바뀐 경계선)와 블롭(국소적으로 덩어리진 영역) 등을 보고 있다.
왼쪽 절반이 흰색이고 오른쪽 절반이 검은색인 필터는 그림 7-25와 같이 세로방향의 엣지에 반응하는 필터이다.
✔️ 층 깊이에 따른 추출 정보 변화
1번째 층의 합성곱 계층에서는 엣지나 블롭 등의 저수준 정보가 추출된 것이고 계층이 깊어질수록 추출되는 정보(강하게 반응하는 뉴런)는 더 추상화된다.
즉, 층이 깊어지면서 뉴런이 반응하는 대상이 단순한 모양에서 고급정보로, 사물의 의미를 이해하도록 변화하는 것이다.
그림 7-26에서 1번째 층은 엣지와 블롭, 3번째 층은 텍스처, 5번째 층은 사물의 일부, 마지막 완전연결 계층은 사물의 클래스(개, 자동차 등)에 뉴런이 반응한다.
7.7. 대표적인 CNN
✔️ LeNet
손글씨 숫자를 인식하는 네트워크
합성곱 계층과 풀링 계층(단순히 원소를 줄이기만 하는 서브샘플링 계층)을 반복하고 마지막으로 완전연결 계층을 거쳐 결과를 출력한다.
LeNet은 시그모이드 함수를 사용하고, 현재의 CNN은 주로 ReLU를 사용한다.
LeNet은 서브샘플링을 하여 중간 데이터의 크기가 작아지지만 현재의 CNN은 최대 풀링이 주류이다.
✔️ AlexNet
합성곱 계층과 풀링 계층을 거듭하며 마지막으로 완전연결 계층을 거쳐 결과를 출력한다.
LeNet과 구조는 크게 바뀌지 않았지만 다음과 같은 변화를 주었다.
- 활성화 함수로 ReLU 사용
- LRN(Local Response Normalization)이라는 국소적 정규화를 실시하는 계층 사용
- 드롭아웃 사용
7.8. 정리
- CNN은 지금까지의 완전연결 계층 네트워크에 합성곱 계층과 풀링 계층을 새로 추가한다.
- 합성곱 계층과 풀링 계층은 im2col(이미지를 행렬로 전개하는 함수)을 이용하면 간단하고 효율적으로 구현할 수 있다.
- CNN을 시각화해보면 계층이 깊어질수록 고급 정보가 추출되는 모습을 확인할 수 있다.
- 대표적인 CNN에는 LeNet과 AlexNet이 있다.
- 딥러닝의 발전에는 빅데이터와 GPU가 크게 기여했다.
참고 git : https://github.com/geonsangyoo/DeepLearning/tree/master