합성곱 신경망

전체 구조

이전까지 신경망은 계층의 모든 뉴런과 결합되어 있습니다. 이것을 완전연결(fully connected) 계층이라 하며 어파인(Affine) 계층으로 구현했습니다.

_images/complete_layer.png

CNN은 합성곱(Convolution) 계층과 풀링(Pooling) 계층이 추가됩니다.

_images/complete_layer1.png

합성곱 계층

CNN에서는 패딩(padding), 스트라이드(stride) 등의 용어가 등장합니다.

완전연결 계층의 문제점

데이터의 형태가 무시됩니다.

공간적 정보가 무시됩니다.

합성곱 연산

합성곱 연산은 이미지 처리에서 필터(또는 커널) 연산에 해당합니다.

_images/cnn_op.png

합성곱 연산의 예

합성곱 연산은 필터의 윈도우(window)를 일정 간격으로 이동하며 입력과 필터에 대응하는 원소끼리 곱한 후 총합을 구하는 것입니다. 이러한 계산을 단일 곱셈-누산(fused multiply-add, FMA)이라 합니다.

_images/cnn_op_order.png

합성곱 연산 계산 순서

완전연결 신경망에 가중치 매개변수와 편향이 존재하는데 CNN에서는 가중치 매개변수에 해당하는 것이 필터의 매개변수입니다. 그리고 CNN에도 편향이 존재합니다.

합성곱 편향

합성곱 연산의 편향

패딩

합성곱 연산을 수행하기 전에 입력 데이터 주변에 특정 값을 채우는 것을 패딩(padding)이라고 합니다.

_images/cnn_padding.png

합성곱 연산의 패딩(점선 부분이 패딩입니다.)

패딩은 주로 출력 크기를 조정할 목적으로 사용합니다.

넘파이에서는 배열의 패딩을 처리할 수 있는 함수 np.pad를 제공합니다. 파이썬 기초의 패딩을 참조하기 바랍니다.

스트라이드

필터를 적용하는 위치의 간격을 스트라이드(stride)라고 합니다.

_images/cnn_stride.png

스트라이드가 2인 합성곱 연산

입력 크기를 (H, W), 필터 크기를 (FH, FW), 출력크기를 (OH, OW), 패딩을 P, 스트라이드를 S라 하면 출력크기는 다음과 같이 계산됩니다.

(1)\[\begin{split}OH = \frac{H + 2P - FH}{S} + 1 \\ OW = \frac{W + 2P - FW}{S} + 1\end{split}\]

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

일반적으로 이미지는 이미지의 크기인 세로, 가로 뿐아니라 색상에 대한 정보인 채널(channel)이 포함되는 3차원 데이터입니다.

채널이 여러 개 있을 때 입력 데이터와 필터의 합성곱 연산을 채널마다 수행하고, 그 결과를 더해서 하나의 출력을 얻습니다.

_images/cnn_3d.png

3차원 데이터 합성곱 연산의 예

_images/cnn_3d_order.png

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

3차원 데이터 합성곱 연산 데모

블록으로 생각하기

_images/cnn_block.png

합성곱 연산을 직육면체 블록으로 생각합니다.

여기서 배열의 형태가 (C, H, W)순서로 있는 것은 reshape 함수를 사용할 때 맨 먼저 너비(W) 읽고 높이(H), 채널(C) 순서로 읽어들이기 위한 것입니다.

_images/cnn_block_batch.png

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

_images/cnn_block_bias.png

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

배치 처리

신경망 처리에서 여러 개의 입력 데이터를 묶어 배치로 처리했습니다. 합성곱 연산도 마찬가지로 배치 처리를 합니다.

_images/cnn_batch.png

합성곱 연산의 배치 처리

풀링 계층

풀링은 세로, 가로 방향의 공간을 줄이는 연산입니다.

다음 그림은 2x2 최대 풀링(max pooling)을 스트라이드 2로 처리하는 순서입니다. 최대 풀링은 대상 영역에서 최댓값을 구하는 연산입니다. 풀링의 스트라이드는 윈도우 크기와 같은 값으로 설정하는 것이 일반적입니다.

_images/cnn_pooling.png

최대 풀링의 처리 순서

풀링은 최대 풀링 외에도 평균 풀링(average pooling) 등이 있습니다. 이미지 인식 분야에서는 주로 최대 풀링을 사용합니다. 여기서도 풀링 계층이라고 하면 최대 풀링을 의미합니다.

최대 풀링을 함수로 표현하고 편미분을 하면 다음과 같습니다.

\[\begin{split}g(\mathbf{x}) = \max(\mathbf{x}), \quad \frac{\partial g}{\partial x_i} = \begin{cases} 1, & \text{만일} ~ x_i = \max(\mathbf{x}) \\ 0, & \text{그렇지않으면} \end{cases}\end{split}\]

따라서 최대 풀링을 이용한 역전파를 할 때는 최대값에 해당하는 변수만 1 이고 나머지는 0이 됩니다.

평균 풀링을 함수로 표현하고 편미분을 하면 다음과 같습니다.

\[g(\mathbf{x}) = \frac{\sum_{k=1}^{m} x_{k}}{m}, \quad \frac{\partial g}{\partial x_{i}} = \frac{1}{m}\]

풀링 계층의 특징

학습해야 할 매개 변수가 없다.

합성곱 계층과 달리 풀링계층는 학습해야할 매개변수가 없습니다.

채널수가 변하지 않는다

풀링연산은 입력 데이터의 채널 수 그대로 출력데이터로 보냅니다.

_images/cnn_pooling_channel.png
입력의 변화에 영향을 적게 받는다

입력데이터가 조금 변해도 풀링의 결과는 잘 변하지 않습니다. 다음 그림에서 보는 바와 같이 입력 데이터가 오른쪽으로 1칸씩 이동을 해도 값이 변하지 않는 것을 알 수 있습니다.

_images/cnn_pooling_robust.png

합성곱/풀링 계층 구현

4차원 배열

CNN에서 데이터는 4차원입니다. 입력데이터는 (N, C, H, W)N은 입력데이터의 수, C는 채널의 수, H, W는 각각 높이와 너비입니다.

im2col으로 데이터 전개

합성곱 연산을 하려면 중첩 for 문이 필요합니다. 여기서는 for 문 대신 im2col 함수를 이용합니다. im2col 함수는 입력데이터를 필터링 계산을 편하게 4차원 데이터를 2차원으로 변환합니다.

다음 동영상은 아래 설명에서와 다르게 행과 열이 전치되어 있습니다.

_images/im2col.gif

출처: https://hackmd.io/@bouteille/B1Cmns09I

한 개의 입력데이터 변환

한 개의 입력데이터를 im2col 을 이용하여 2차원 데이터로 변환하는 것입니다.

_images/cnn_im2col_new.png

변환된 2차원 데이터의 열의 갯수는 필터를 일렬로 나열한 것의 갯수 C * FH * FW와 같도록 맞춥니다. 변환된 행렬의 한 행은 필터 하나를 적용하기 위해 만들어 집니다. 따라서 행의 갯수는 필터가 움직이는 횟수인 OH * OW 와 같습니다. 위의 동영상에서는 행과 열이 전치되어 있습니다.

N 개의 입력데이터 변환

N 개의 입력데이터에 대해서 모두 2차원 데이터 형태로 변경합니다. 2차원 데이터의 행의 갯수는 입력 데이터의 갯수가 N 이므로 N * OH * OW 이고 열의 갯수는 C * FH * FW 개 입니다.

_images/cnn_im2col_filter_new.png

필터 2차원 변환

각각의 필터를 하나의 열을 차지 하도록 2차원 데이터로 변환합니다. 필터의 크기는 행의 갯수는 C * FH * FW 이고 열의 갯수는 필터의 갯수인 FN이 됩니다. 그러므로 앞에서 변경된 2차원 입력데이터와 2차원 필터 데이터와 행렬곱을 해서 합성곱 연산 결과를 얻습니다. 합성곱해서 최종적으로 나오는 출력데이터의 크기는 (N, FN, OH, OW) 입니다.

_images/cnn_im2col_detail.png
common/util.py im2col 함수
 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 함수를 사용하여 되돌립니다.

common/util.py
 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 의 최대값은 yfilter_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)를 반환합니다.

합성곱 계층 구현

common/layers.py Convolution __init__()
 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

합성곱 순전파

common/layers.py Convolution forward()
 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 과정을 표현한 것입니다.

_images/cnn_transpose.png

위 그림에서 CFN으로 수정합니다.

CNN의 순전파는 어파인 계층과 같은 상태로 전파되는 것을 알 수 있습니다. col 은 어파인의 Xcol_W은 어파인에서 W와 대응됩니다. Affine/Softmax 계층 구현을 참조하세요.

합성곱 역전파

common/layers.py Convolution backward()
 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의 역전파는 순전파에서 어파인 계층과 비슷했던 것과 같이 어파인 역전파와 비슷한 과정으로 진행됩니다. 즉, colX에 대응되고, col_WW에 대응되므로 어파인에서 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차원 데이터로 변환합니다.

_images/cnn_pooling_imp.png

2차원 행렬에서 각 행의 최대값을 구하고 출력데이터의 크기 (N, C, OH, OW)에 맞게 변환합니다.

순전파

_images/cnn_pooling_flow.png
common/layers.py forward
 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로 저장합니다.

역전파

common/layers.py backward
 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
_images/cnn_imp.png

SimpleConvNet 초기화

ch07/simple_convnet.py, SimpleConvNet 클래스 __init__()
 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, b1Conv1 계층에 설정됩니다. W1의 크기는 (FN, C, FH, FW) 입니다. b1의 크기는 (FN,) 입니다.

  • 32 - 34줄: 가중치 매개변수 W2, b2Affine1 계층에 설정됩니다. W2의 크기는 (pool_output_size, hidden_size) 입니다. b2의 크기는 (hidden_size,) 입니다.

  • 35 - 37줄: 가중치 매개변수 W3, b3Affine2 계층에 설정됩니다. 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의 순전파, 역전파

ch07/simple_convnet.py; 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 학습을 하는 코드입니다.

ch07/train_convnet.py
 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 클래스는 주어진 신경망을 학습하는 과정이 들어 있습니다.

common/trainer.py
 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을 이용하여 학습한 결과 합성곱 계층의 가중치 필터에 대한 그림입니다.

_images/cnn_weight_visualization.png

합성곱 계층의 가중치 시각화

학습 하기전 필터는 무작위로 초기화 되어 있어서 흑백 규칙이 보이지 않지만 학습 후 필터는 규칙이 있는 필터로 바뀐 것을 알 수 있습니다.

층 깊이에 따른 정보 변화

_images/alexnet-layers-info.png

AlexNet 1, 3, 5층 일부

AlexNet을 이용한 일반 사물 인식을 수행한 8층의 CNN입니다. 첫번째 층은 에지 edge와 블롭 blob, 3번째 층은 텍스처, 5번째 층은 사물의 일부, 마지막 완전연결 계층은 사물의 클래스(개, 자동차 등)에 뉴런이 반응하는 것을 볼 수 있습니다.

참고 사이트

Convolutional Neural Network with Numpy (Fast)