합성곱 신경망
전체 구조
이전까지 신경망은 계층의 모든 뉴런과 결합되어 있습니다. 이것을 완전연결(fully connected) 계층이라 하며 어파인(Affine) 계층으로 구현했습니다.
CNN은 합성곱(Convolution) 계층과 풀링(Pooling) 계층이 추가됩니다.
합성곱 계층
CNN에서는 패딩(padding), 스트라이드(stride) 등의 용어가 등장합니다.
완전연결 계층의 문제점
- 데이터의 형태가 무시됩니다.
공간적 정보가 무시됩니다.
합성곱 연산
합성곱 연산은 이미지 처리에서 필터(또는 커널) 연산에 해당합니다.
합성곱 연산은 필터의 윈도우(window)를 일정 간격으로 이동하며 입력과 필터에 대응하는 원소끼리 곱한 후 총합을 구하는 것입니다. 이러한 계산을 단일 곱셈-누산(fused multiply-add, FMA)이라 합니다.
완전연결 신경망에 가중치 매개변수와 편향이 존재하는데 CNN에서는 가중치 매개변수에 해당하는 것이 필터의 매개변수입니다. 그리고 CNN에도 편향이 존재합니다.
패딩
합성곱 연산을 수행하기 전에 입력 데이터 주변에 특정 값을 채우는 것을 패딩(padding)이라고 합니다.
패딩은 주로 출력 크기를 조정할 목적으로 사용합니다.
넘파이에서는 배열의 패딩을 처리할 수 있는 함수 np.pad
를 제공합니다. 파이썬 기초의 패딩을 참조하기 바랍니다.
스트라이드
필터를 적용하는 위치의 간격을 스트라이드(stride)라고 합니다.
입력 크기를 (H, W), 필터 크기를 (FH, FW), 출력크기를 (OH, OW), 패딩을 P, 스트라이드를 S라 하면 출력크기는 다음과 같이 계산됩니다.
3차원 데이터의 합성곱 연산
일반적으로 이미지는 이미지의 크기인 세로, 가로 뿐아니라 색상에 대한 정보인 채널(channel)이 포함되는 3차원 데이터입니다.
채널이 여러 개 있을 때 입력 데이터와 필터의 합성곱 연산을 채널마다 수행하고, 그 결과를 더해서 하나의 출력을 얻습니다.
블록으로 생각하기
여기서 배열의 형태가 (C, H, W)
순서로 있는 것은 reshape
함수를 사용할 때 맨 먼저 너비(W
) 읽고 높이(H
), 채널(C
) 순서로 읽어들이기 위한 것입니다.
배치 처리
신경망 처리에서 여러 개의 입력 데이터를 묶어 배치로 처리했습니다. 합성곱 연산도 마찬가지로 배치 처리를 합니다.
풀링 계층
풀링은 세로, 가로 방향의 공간을 줄이는 연산입니다.
다음 그림은 2x2 최대 풀링(max pooling)을 스트라이드 2로 처리하는 순서입니다. 최대 풀링은 대상 영역에서 최댓값을 구하는 연산입니다. 풀링의 스트라이드는 윈도우 크기와 같은 값으로 설정하는 것이 일반적입니다.
풀링은 최대 풀링 외에도 평균 풀링(average pooling) 등이 있습니다. 이미지 인식 분야에서는 주로 최대 풀링을 사용합니다. 여기서도 풀링 계층이라고 하면 최대 풀링을 의미합니다.
최대 풀링을 함수로 표현하고 편미분을 하면 다음과 같습니다.
따라서 최대 풀링을 이용한 역전파를 할 때는 최대값에 해당하는 변수만 1
이고 나머지는 0
이 됩니다.
평균 풀링을 함수로 표현하고 편미분을 하면 다음과 같습니다.
풀링 계층의 특징
- 학습해야 할 매개 변수가 없다.
합성곱 계층과 달리 풀링계층는 학습해야할 매개변수가 없습니다.
- 채널수가 변하지 않는다
풀링연산은 입력 데이터의 채널 수 그대로 출력데이터로 보냅니다.
- 입력의 변화에 영향을 적게 받는다
입력데이터가 조금 변해도 풀링의 결과는 잘 변하지 않습니다. 다음 그림에서 보는 바와 같이 입력 데이터가 오른쪽으로 1칸씩 이동을 해도 값이 변하지 않는 것을 알 수 있습니다.
합성곱/풀링 계층 구현
4차원 배열
CNN에서 데이터는 4차원입니다. 입력데이터는 (N, C, H, W)
로 N
은 입력데이터의 수, C
는 채널의 수, H
, W
는 각각 높이와 너비입니다.
im2col으로 데이터 전개
합성곱 연산을 하려면 중첩 for
문이 필요합니다. 여기서는 for
문 대신 im2col
함수를 이용합니다. im2col
함수는 입력데이터를 필터링 계산을 편하게 4차원 데이터를 2차원으로 변환합니다.
다음 동영상은 아래 설명에서와 다르게 행과 열이 전치되어 있습니다.
한 개의 입력데이터 변환
한 개의 입력데이터를 im2col
을 이용하여 2차원 데이터로 변환하는 것입니다.
변환된 2차원 데이터의 열의 갯수는 필터를 일렬로 나열한 것의 갯수 C * FH * FW
와 같도록 맞춥니다. 변환된 행렬의 한 행은 필터 하나를 적용하기 위해 만들어 집니다. 따라서 행의 갯수는 필터가 움직이는 횟수인 OH * OW
와 같습니다. 위의 동영상에서는 행과 열이 전치되어 있습니다.
N 개의 입력데이터 변환
N
개의 입력데이터에 대해서 모두 2차원 데이터 형태로 변경합니다. 2차원 데이터의 행의 갯수는 입력 데이터의 갯수가 N
이므로 N * OH * OW
이고 열의 갯수는 C * FH * FW
개 입니다.
필터 2차원 변환
각각의 필터를 하나의 열을 차지 하도록 2차원 데이터로 변환합니다. 필터의 크기는 행의 갯수는 C * FH * FW
이고 열의 갯수는 필터의 갯수인 FN
이 됩니다. 그러므로 앞에서 변경된 2차원 입력데이터와 2차원 필터 데이터와 행렬곱을 해서 합성곱 연산 결과를 얻습니다. 합성곱해서 최종적으로 나오는 출력데이터의 크기는 (N, FN, OH, OW)
입니다.
1def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
2 """다수의 이미지를 입력받아 2차원 배열로 변환한다(평탄화).
3
4 Parameters
5 ----------
6 input_data : 4차원 배열 형태의 입력 데이터(이미지 수, 채널 수, 높이, 너비)
7 filter_h : 필터의 높이
8 filter_w : 필터의 너비
9 stride : 스트라이드
10 pad : 패딩
11
12 Returns
13 -------
14 col : 2차원 배열
15 """
16 N, C, H, W = input_data.shape
17 out_h = (H + 2*pad - filter_h)//stride + 1
18 out_w = (W + 2*pad - filter_w)//stride + 1
19
20 img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
21 col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))
22
23 for y in range(filter_h):
24 y_max = y + stride*out_h
25 for x in range(filter_w):
26 x_max = x + stride*out_w
27 col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]
28
29 col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
30 return col
17 - 18줄: 공식 (1)을 이용하여
OH
,OW
를 계산합니다.20줄:
np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
에서input_data
배열에 패딩을 추가합니다.N
,C
차원에는 패딩을 넣지 않고H
,W
차원에 각각 앞, 뒤로pad
갯수 만큼의 기본값0
을 넣습니다.21줄: 필터 높이, 너비 및 출력 높이, 너비가 추가된 6차원 배열
col
을 초기화 합니다.27줄:
col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]
에서y, x
는 필터의 인덱스를 의미합니다. 필터y, x
부분과 대응하는 이미지 셀들img[:, :, y:y_max:stride, x:x_max:stride]
만 골라서 추가합니다.27줄:
y:y_max:stride
는 출력데이터 크기OH
만큼에 해당하는 이미지 셀들만 골라내는 겁니다.29줄:
col.transpose(0, 4, 5, 1, 2, 3)
으로N, OH, OW, C, FH, FW
축의 배열로 변경합니다.29줄:
col.transpose(0, 4, 5, 1, 2, 3).reshape(N * out_h * out_w, -1)
을 이용하여(N * OH * OW, C * FH * FW)
크기의 2차원 배열로 변경합니다.
col2im 이용 역전파 데이터 복원
역전파시 순전파 때의 입력데이터에 맞도록 데이터의 크기를 원래 크기에 맞도록 되돌릴 필요가 있습니다. 이때 col2im
함수를 사용하여 되돌립니다.
1def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
2 """(im2col과 반대) 2차원 배열을 입력받아 다수의 이미지 묶음으로 변환한다.
3
4 Parameters
5 ----------
6 col : 2차원 배열(입력 데이터)
7 input_shape : 원래 이미지 데이터의 형상(예:(10, 1, 28, 28))
8 filter_h : 필터의 높이
9 filter_w : 필터의 너비
10 stride : 스트라이드
11 pad : 패딩
12
13 Returns
14 -------
15 img : 변환된 이미지들
16 """
17 N, C, H, W = input_shape
18 out_h = (H + 2*pad - filter_h)//stride + 1
19 out_w = (W + 2*pad - filter_w)//stride + 1
20 col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 4, 5, 1, 2)
21
22 img = np.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1))
23 for y in range(filter_h):
24 y_max = y + stride*out_h
25 for x in range(filter_w):
26 x_max = x + stride*out_w
27 img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]
28
29 return img[:, :, pad:H + pad, pad:W + pad]
1줄:
col
은 합성곱 역전파에서 호출할 때,im2col
의 반환값(N * OH * OW, C * FH * FW)
의 형태로 입력됩니다.20줄:
col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 4, 5, 1, 2)
을 이용하여 2차원 데이터를 6차원 데이터(N, C, FH, FW, OH, OW)
로 변경합니다.22줄:
H + 2 * pad + stride - 1
가 나오는 이유는 다음과 같습니다.y_max
의 최대값은y
가filter_h - 1
일 때이므로img[:, :, y, x]
에서y
의 값의 법위는0
부터y_max = filter_h - 1 + stride * out_h
입니다. 그리고out_h = (H + 2 * pad - filter_h) // stride + 1
식으로부터filter_h - 1 + stride * out_h = H + 2 * pad + stride - 1
이 됩니다.27줄:
img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]
에서im2col
의 27줄에서 했던 것과 역으로 하는데+=
연산을 하여, 기존에 있던 원소에 더하는 과정을 실행합니다. 이것은 편미분할 때 같은 변수에 대해서 각각 여러 번 했던 것을 합치는 과정이 들어간 겁니다.im2col
에 의해서 변형되기 전의 원래 있던 변수로 복원하는 과정인데, 같은 변수에 대해서는 chain rule에 의해서 더해야 하는 것입니다.29줄:
img[:, :, pad:H + pad, pad:W + pad]
은 패딩 부분을 제외한 원래 이미지(N, C, H, W)
를 반환합니다.
합성곱 계층 구현
1class Convolution:
2 def __init__(self, W, b, stride=1, pad=0):
3 self.W = W
4 self.b = b
5 self.stride = stride
6 self.pad = pad
7
8 # 중간 데이터(backward 시 사용)
9 self.x = None
10 self.col = None
11 self.col_W = None
12
13 # 가중치와 편향 매개변수의 기울기
14 self.dW = None
15 self.db = None
합성곱 순전파
1 def forward(self, x):
2 FN, C, FH, FW = self.W.shape
3 N, C, H, W = x.shape
4 out_h = 1 + int((H + 2*self.pad - FH) / self.stride)
5 out_w = 1 + int((W + 2*self.pad - FW) / self.stride)
6
7 col = im2col(x, FH, FW, self.stride, self.pad)
8 col_W = self.W.reshape(FN, -1).T
9
10 out = np.dot(col, col_W) + self.b
11 out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
12
13 self.x = x
14 self.col = col
15 self.col_W = col_W
16
17 return out
7줄:
col = im2col(x, FH, FW, self.stride, self.pad)
을 이용하여 4차원 입력데이터x
를 2차원 배열col
으로 변경합니다.col
의 크기는(N * OH * OW, C * FH * FW)
입니다.8줄:
col_W = self.W.reshape(FN, -1).T
를 이용하여col_W
는(C * FH * FW, FN)
형태로 변환됩니다.10줄:
out = np.dot(col, col_W) + self.b
에서 행렬곱 연산을 합니다.out
의 크기는(N * OH * OW, FN)
이 됩니다.10줄: 편향
self.b
의 크기는(FN,)
이었는데out
배열의 크기만큼 브로드캐스팅을 하여 더합니다.11줄:
out = out.reshape(N, out_h, out_w, -1)
를 이용하여(N, OH, OW, FN)
배열로 만들고transpose(0, 3, 1, 2)
를 통하여(N, FN, OH, OW)
크기의 배열을 만들어 반환합니다.13-15줄: 역전파
backward()
과정에서 사용하기 위하여self.x
,self.col
,self.col_W
를 보존합니다.
다음 그림은 transpose
과정을 표현한 것입니다.
위 그림에서 C
는 FN
으로 수정합니다.
CNN의 순전파는 어파인 계층과 같은 상태로 전파되는 것을 알 수 있습니다. col
은 어파인의 X
와 col_W
은 어파인에서 W
와 대응됩니다. Affine/Softmax 계층 구현을 참조하세요.
합성곱 역전파
1 def backward(self, dout):
2 FN, C, FH, FW = self.W.shape
3 dout = dout.transpose(0,2,3,1).reshape(-1, FN)
4
5 self.db = np.sum(dout, axis=0)
6 self.dW = np.dot(self.col.T, dout)
7 self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)
8
9 dcol = np.dot(dout, self.col_W.T)
10 dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)
11
12 return dx
CNN의 역전파는 순전파에서 어파인 계층과 비슷했던 것과 같이 어파인 역전파와 비슷한 과정으로 진행됩니다. 즉, col
이 X
에 대응되고, col_W
가 W
에 대응되므로 어파인에서 dx
, dW
, db
를 계산하는 것과 같이 계산한 후에 원래 크기에 맞춰 변형만 해주면 됩니다. 마찬가지로 Affine 역전파를 참조하세요.
1줄: 역전파로 입력되는
dout
의 크기는 순전파 출력과 같은 크기인(N, FN, OH, OW)
입니다.3줄:
dout = dout.transpose(0, 2, 3, 1).reshape(-1, FN)
를 이용하여(N * OH * OW, FN)
형태로 변환합니다.5줄:
self.db = np.sum(dout, axis=0)
은 Affine의db
를 구하는 과정과 같습니다.6줄:
self.dW = np.dot(self.col.T, dout)
은 Affine의dW
를 구하는 과정과 같고self.col = (N * OH * OW, C * FH * FW)
은 Affine의X
와 같은 역할을 합니다.self.dW
의 크기는(C * FH * FW, FN)
이 됩니다.7줄:
self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)
를 이용하여self.dW
를(FN, C, FH, FW)
형태로 변환합니다.9줄:
dcol = np.dot(dout, self.col_W.T)
을 이용하여 Affine의dx
에 대응하는 연산을 수행합니다.col_W
가 Affine의W
와 대응됩니다.dout
의 크기는(N * OH * OW, FN)
이고col_W.T
는(FN, C * FH * FW)
이므로 행렬곱dcol
의 결과는(N * OH * OW, C * FH * FW)
입니다.10줄:
col2im
를 통해서 순전파 때 입력되었던 입력데이터의 크기(N, C, H, W)
로 역전파dx
가 반환됩니다. col2im에서 설명했듯이 이 과정에서 중복된 편미분들이 합쳐지게 됩니다.
풀링 계층 구현
합성곱 계층에서와 마찬가지로 4차원 데이터를 im2col
을 이용하여 2차원 데이터로 변환합니다.
2차원 행렬에서 각 행의 최대값을 구하고 출력데이터의 크기 (N, C, OH, OW)
에 맞게 변환합니다.
순전파
1class Pooling:
2 def __init__(self, pool_h, pool_w, stride=1, pad=0):
3 self.pool_h = pool_h
4 self.pool_w = pool_w
5 self.stride = stride
6 self.pad = pad
7
8 self.x = None
9 self.arg_max = None
10
11 def forward(self, x):
12 N, C, H, W = x.shape
13 out_h = int(1 + (H - self.pool_h) / self.stride)
14 out_w = int(1 + (W - self.pool_w) / self.stride)
15
16 col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
17 col = col.reshape(-1, self.pool_h*self.pool_w)
18
19 arg_max = np.argmax(col, axis=1)
20 out = np.max(col, axis=1)
21 out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
22
23 self.x = x
24 self.arg_max = arg_max
25
26 return out
13-14줄: 풀링할 때는 패딩이 필요없습니다.
16줄:
im2col
을 이용하여 4차원 데이터x
를 2차원 데이터col = (N * OH * OW, C * PH * PW)
로 변경합니다.17줄:
col = col.reshape(-1, self.pool_h * self.pool_w)
를 이용하여(N * OH * OW * C, PH * PW)
크기로 변경합니다.19줄:
arg_max = np.argmax(col, axis=1)
는col
배열의 각 행의 최대값을 갖는 인덱스를 구합니다.(N * OH * OW * C,)
크기의 1차원 배열입니다.20줄:
out = np.max(col, axis=1)
를 이용하여 각 행의 최대값을 구하여out
배열에 저장합니다. 이 부분이 max 풀링을 적용한 것과 같습니다.21줄:
reshape, transpose
를 이용하여(N, C, OH, OW)
크기로 변환하여 반환합니다.24줄: 역전파할 때 필요하기 때문에
self.arg_max
로 저장합니다.
역전파
1 def backward(self, dout):
2 dout = dout.transpose(0, 2, 3, 1)
3
4 pool_size = self.pool_h * self.pool_w
5 dmax = np.zeros((dout.size, pool_size))
6 dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
7 dmax = dmax.reshape(dout.shape + (pool_size,))
8
9 dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
10 dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)
11
12 return dx
1줄:
dout
은 순전파에서 출력되었던 형태인(N, C, OH, OW)
크기로 입력됩니다.2줄:
dout = dout.transpose(0, 2, 3, 1)
를 통해서(N, OH, OW, C)
크기로 변환합니다.5줄:
dmax = np.zeros((dout.size, pool_size))
에서 순전파 때 생성되었던col
배열과 같은 크기인(N * OH * OW * C, PH * PW)
크기의 영배열을 만듭니다.6줄:
dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
를 이용하여 최대값을 가졌던 인덱스에 역전파된 미분값을 대입하고 나머지는0
으로 놓습니다.7줄:
dmax = dmax.reshape(dout.shape + (pool_size,))
를 이용하여dmax
의 크기를(N, OH, OW, C, PH * PW)
크기로 변환합니다.9줄:
dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
를 이용하여(N * OH * OW, C * PH * PW)
형태로 변환합니다.10줄:
dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)
를 이용하여 순전파 때 입력되었던 원래 데이터의 크기self.x.shape
로 변환하여 최대 풀링의 역전파를 반환합니다. 이 부분에서 col2im에서 설명한 것과 같이 같은 변수에 대한 미분값이 더해집니다.
CNN 구현
다음은 단순한 합성곱 네트워크를 구현한 것입니다.
Convolution -> Relu -> Pool(윈도우 크기는 2x2) -> Affine -> Relu -> Affine -> SoftmaxWithLoss
SimpleConvNet 초기화
1class SimpleConvNet:
2 """단순한 합성곱 신경망
3
4 conv - relu - pool - affine - relu - affine - softmax
5
6 Parameters
7 ----------
8 input_size : 입력 크기(MNIST의 경우엔 784)
9 hidden_size_list : 각 은닉층의 뉴런 수를 담은 리스트(e.g. [100, 100, 100])
10 output_size : 출력 크기(MNIST의 경우엔 10)
11 activation : 활성화 함수 - 'relu' 혹은 'sigmoid'
12 weight_init_std : 가중치의 표준편차 지정(e.g. 0.01)
13 'relu'나 'he'로 지정하면 'He 초깃값'으로 설정
14 'sigmoid'나 'xavier'로 지정하면 'Xavier 초깃값'으로 설정
15 """
16 def __init__(self, input_dim=(1, 28, 28),
17 conv_param={'filter_num':30, 'filter_size':5, 'pad':0, 'stride':1},
18 hidden_size=100, output_size=10, weight_init_std=0.01):
19 filter_num = conv_param['filter_num']
20 filter_size = conv_param['filter_size']
21 filter_pad = conv_param['pad']
22 filter_stride = conv_param['stride']
23 input_size = input_dim[1]
24 conv_output_size = (input_size - filter_size + 2*filter_pad) / filter_stride + 1
25 pool_output_size = int(filter_num * (conv_output_size/2) * (conv_output_size/2))
26
27 # 가중치 초기화
28 self.params = {}
29 self.params['W1'] = weight_init_std * \
30 np.random.randn(filter_num, input_dim[0], filter_size, filter_size)
31 self.params['b1'] = np.zeros(filter_num)
32 self.params['W2'] = weight_init_std * \
33 np.random.randn(pool_output_size, hidden_size)
34 self.params['b2'] = np.zeros(hidden_size)
35 self.params['W3'] = weight_init_std * \
36 np.random.randn(hidden_size, output_size)
37 self.params['b3'] = np.zeros(output_size)
38
39 # 계층 생성
40 self.layers = OrderedDict()
41 self.layers['Conv1'] = Convolution(self.params['W1'], self.params['b1'],
42 conv_param['stride'], conv_param['pad'])
43 self.layers['Relu1'] = Relu()
44 self.layers['Pool1'] = Pooling(pool_h=2, pool_w=2, stride=2)
45 self.layers['Affine1'] = Affine(self.params['W2'], self.params['b2'])
46 self.layers['Relu2'] = Relu()
47 self.layers['Affine2'] = Affine(self.params['W3'], self.params['b3'])
48
49 self.last_layer = SoftmaxWithLoss()
29 - 31줄: 가중치 매개변수
W1
,b1
은 Conv1 계층에 설정됩니다.W1
의 크기는(FN, C, FH, FW)
입니다.b1
의 크기는(FN,)
입니다.32 - 34줄: 가중치 매개변수
W2
,b2
은 Affine1 계층에 설정됩니다.W2
의 크기는(pool_output_size, hidden_size)
입니다.b2
의 크기는(hidden_size,)
입니다.35 - 37줄: 가중치 매개변수
W3
,b3
은 Affine2 계층에 설정됩니다.W3
의 크기는(hidden_size, output_size)
입니다.b3
의 크기는(output_size,)
입니다. 여기서output_size = 10
입니다.
Conv1
41 - 42줄: 입력데이터 MNIST
(1, 28, 28)
크기가 입력되고 출력은 필터의 갯수FN
와 필터의 크기(FH, FW)
에 의해 결정됩니다. 출력의 크기는(N, FN, OH, OW)
입니다. 합성곱의 매개변수W1
는 필터의 크기에 의해 결정되므로(FN, C, FH, FW)
가 됩니다. 편향은(FN,)
이 됩니다.
Relu1
43줄: 입력데이터는 앞 층의 합성곱 계층의 출력데이터
(N, FN, OH, OW)
이고 Relu1의 출력 크기도 마찬가지로 변하지 않습니다.
Pooling
44줄: 풀링의 입력은 앞 층의 Relu1의 크기인
(N, FN, OH, OW)
이고 출력데이터의 크기는(N, FN, OH, OW)
입니다. 그런데 다음 계층이 완전 연결 계층인 어파인 이므로 어파인에 연결될 때는(N, FN * OH * OW)
가 됩니다.
Affine1
45줄: 풀링 출력데이터가 4차원 데이터인데 어파인에 입력되어 2차원으로 바뀌어 처리가 됩니다. 즉,
(N, FN * OH * OW)
로 처리됩니다. 따라서 어파인의 매개변수W2
의 크기는(FN * OH * OW, 은닉 노드의 갯수)
가 됩니다. 어파인의 편향은(은닉 노드의 갯수,)
가 됩니다. Affine1의 출력데이터 크기는(N, 은닉 노드의 갯수)
가 됩니다.
Relu2
46줄: Relu1과 마찬가지로 입력데이터의 크기만큼 출력데이터로 나옵니다. 입/출력데이터의 크기는
(N, 은닉 노드의 갯수)
가 됩니다.
Affine2
47줄: Affine2의 출력은 최종 출력층이 되므로 매개변수
W3
의 MNIST의 분류 갯수인 10이 되므로 앞 층의 입력데이터의 크기와 함께(은닉 노드의 갯수, 10)
이 됩니다. 편향은(10,)
이 됩니다.
SoftmaxWithLoss
49줄: 출력층의 활성화함수 소프트맥스와 오차가 함께 처리되는 층입니다.
SimpleConvNet의 순전파, 역전파
1 def predict(self, x):
2 for layer in self.layers.values():
3 x = layer.forward(x)
4
5 return x
6
7 def loss(self, x, t):
8 """손실 함수를 구한다.
9
10 Parameters
11 ----------
12 x : 입력 데이터
13 t : 정답 레이블
14 """
15 y = self.predict(x)
16 return self.last_layer.forward(y, t)
17
18 def gradient(self, x, t):
19 """기울기를 구한다(오차역전파법).
20
21 Parameters
22 ----------
23 x : 입력 데이터
24 t : 정답 레이블
25
26 Returns
27 -------
28 각 층의 기울기를 담은 사전(dictionary) 변수
29 grads['W1']、grads['W2']、... 각 층의 가중치
30 grads['b1']、grads['b2']、... 각 층의 편향
31 """
32 # forward
33 self.loss(x, t)
34
35 # backward
36 dout = 1
37 dout = self.last_layer.backward(dout)
38
39 layers = list(self.layers.values())
40 layers.reverse()
41 for layer in layers:
42 dout = layer.backward(dout)
43
44 # 결과 저장
45 grads = {}
46 grads['W1'], grads['b1'] = self.layers['Conv1'].dW, self.layers['Conv1'].db
47 grads['W2'], grads['b2'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
48 grads['W3'], grads['b3'] = self.layers['Affine2'].dW, self.layers['Affine2'].db
49
50 return grads
2 - 3줄: Softmax-with-Loss 계층을 제외한 계층의 순전파를 계산합니다.
15줄: SoftmaxWithLoss 계층을 제외한 순전파를 호출합니다.
16줄: SoftmaxWithLoss 계층을 계산하여 손실을 구합니다.
33줄: 순전파와 손실을 계산합니다.
36 - 42줄: 역전파가 이루어집니다.
45 - 48줄: 역전파로 계산된 가중치들을 반환합니다.
CNN 학습
다음은 SimpleConvNet
을 이용해 MNIST 학습을 하는 코드입니다.
1from dataset.mnist import load_mnist
2from simple_convnet import SimpleConvNet
3from common.trainer import Trainer
4
5# 데이터 읽기
6(x_train, t_train), (x_test, t_test) = load_mnist(flatten=False)
7
8# 시간이 오래 걸릴 경우 데이터를 줄인다.
9#x_train, t_train = x_train[:5000], t_train[:5000]
10#x_test, t_test = x_test[:1000], t_test[:1000]
11
12max_epochs = 20
13
14network = SimpleConvNet(input_dim=(1,28,28),
15 conv_param = {'filter_num': 30, 'filter_size': 5, 'pad': 0, 'stride': 1},
16 hidden_size=100, output_size=10, weight_init_std=0.01)
17
18trainer = Trainer(network, x_train, t_train, x_test, t_test,
19 epochs=max_epochs, mini_batch_size=100,
20 optimizer='Adam', optimizer_param={'lr': 0.001},
21 evaluate_sample_num_per_epoch=1000)
22trainer.train()
23
24# 매개변수 보존
25network.save_params("params.pkl")
26print("Saved Network Parameters!")
14-16줄: SimpleConvNet 초기화
18-21줄: Trainer 초기화
22줄: 학습 시작
25줄: 학습된 매개변수 저장
다음은 Trainer
클래스를 나타냅니다. Trainer
클래스는 주어진 신경망을 학습하는 과정이 들어 있습니다.
1from common.optimizer import *
2
3class Trainer:
4 """신경망 훈련을 대신 해주는 클래스
5 """
6 def __init__(self, network, x_train, t_train, x_test, t_test,
7 epochs=20, mini_batch_size=100,
8 optimizer='SGD', optimizer_param={'lr':0.01},
9 evaluate_sample_num_per_epoch=None, verbose=True):
10 self.network = network
11 self.verbose = verbose
12 self.x_train = x_train
13 self.t_train = t_train
14 self.x_test = x_test
15 self.t_test = t_test
16 self.epochs = epochs
17 self.batch_size = mini_batch_size
18 self.evaluate_sample_num_per_epoch = evaluate_sample_num_per_epoch
19
20 # optimzer
21 optimizer_class_dict = {'sgd':SGD, 'momentum':Momentum, 'nesterov':Nesterov,
22 'adagrad':AdaGrad, 'rmsprpo':RMSprop, 'adam':Adam}
23 self.optimizer = optimizer_class_dict[optimizer.lower()](**optimizer_param)
24
25 self.train_size = x_train.shape[0]
26 self.iter_per_epoch = max(self.train_size / mini_batch_size, 1)
27 self.max_iter = int(epochs * self.iter_per_epoch)
28 self.current_iter = 0
29 self.current_epoch = 0
30
31 self.train_loss_list = []
32 self.train_acc_list = []
33 self.test_acc_list = []
34
35 def train_step(self):
36 batch_mask = np.random.choice(self.train_size, self.batch_size)
37 x_batch = self.x_train[batch_mask]
38 t_batch = self.t_train[batch_mask]
39
40 grads = self.network.gradient(x_batch, t_batch)
41 self.optimizer.update(self.network.params, grads)
42
43 loss = self.network.loss(x_batch, t_batch)
44 self.train_loss_list.append(loss)
45 if self.verbose: print("train loss:" + str(loss))
46
47 if self.current_iter % self.iter_per_epoch == 0:
48 self.current_epoch += 1
49
50 x_train_sample, t_train_sample = self.x_train, self.t_train
51 x_test_sample, t_test_sample = self.x_test, self.t_test
52 if not self.evaluate_sample_num_per_epoch is None:
53 t = self.evaluate_sample_num_per_epoch
54 x_train_sample, t_train_sample = self.x_train[:t], self.t_train[:t]
55 x_test_sample, t_test_sample = self.x_test[:t], self.t_test[:t]
56
57 train_acc = self.network.accuracy(x_train_sample, t_train_sample)
58 test_acc = self.network.accuracy(x_test_sample, t_test_sample)
59 self.train_acc_list.append(train_acc)
60 self.test_acc_list.append(test_acc)
61
62 if self.verbose: print("=== epoch:" + str(self.current_epoch) + ", train acc:" + str(train_acc) + ", test acc:" + str(test_acc) + " ===")
63 self.current_iter += 1
64
65 def train(self):
66 for i in range(self.max_iter):
67 self.train_step()
68
69 test_acc = self.network.accuracy(self.x_test, self.t_test)
70
71 if self.verbose:
72 print("=============== Final Test Accuracy ===============")
73 print("test acc:" + str(test_acc))
21-23줄:
common.optimizer
모듈로부터 최적화 함수를 설정합니다.26줄: 에폭당 반복 횟수를 설정합니다.(학습데이터 크기 / 미니 배치 크기)
27줄:
Trainer.train_step()
반복 횟수를 설정합니다.28-29줄: 현재 반복 수와 현재 에폭 수를 저장하는 변수입니다
40줄: 주어진 네트워크의 순전파/역전파를 계산합니다.
41줄: 각 계층의 가중치들을 업데이트 합니다.
43-45줄: 업데이트된 가중치를 이용해서 미니 배치 데이터의 손실을 계산/저장하고 출력합니다.
47-62줄: 에폭당 샘플 학습데이터와 시험데이터의 정확도를 계산/저장하고 출력합니다.
CNN 시각화
첫번째 층 가중치 시각화
다음은 SimpleConvNet을 이용하여 학습한 결과 합성곱 계층의 가중치 필터에 대한 그림입니다.
학습 하기전 필터는 무작위로 초기화 되어 있어서 흑백 규칙이 보이지 않지만 학습 후 필터는 규칙이 있는 필터로 바뀐 것을 알 수 있습니다.
층 깊이에 따른 정보 변화
AlexNet을 이용한 일반 사물 인식을 수행한 8층의 CNN입니다. 첫번째 층은 에지 edge와 블롭 blob, 3번째 층은 텍스처, 5번째 층은 사물의 일부, 마지막 완전연결 계층은 사물의 클래스(개, 자동차 등)에 뉴런이 반응하는 것을 볼 수 있습니다.