CNN
0116. 전체 구조
![]()

- 위의 신경망은 인접하는 계층의 모든 뉴런과 결합되어 있었고 이를 완전연결 fully-connected라고 하며, 완전히 연결된 계층을 Affine계층이라 함.
- 지금까지의 ‘Affine-ReLU’연결이 아래 그림의 ‘Conv-ReLU-(Pooling)’으로 바뀜. (풀링 계층은 생략하기도 함.)
- 출력에 가까운 층에서는 지금까지의 ‘Affine-ReLU’구성을 사용할 수 있고 마지막 출력 계층에서는 ‘Affine-Softmax’조합을 그대로 사용.
0117. 완전연결 계층의 문제점: 데이터의 형상이 무시됨.
- 이미지는 세로, 가로, 채널(색상)으로 구성된 3차원 데이터, 그러나 완전연결 계층에 입력할 때는 3차원 데이터를 평평한 1차원 데이터로 평탄화해줘야 함.
- 반면 합성곱 계층은 형상을 유지함. 이미지를 3차원 데이터로 입력받으며, 마찬가지로 다음 계층에도 3차원 데이터로 전달.
0118. 합성곱 연산.

- 합성곱 연산은 필터의 윈도우window를 일정 간격으로 이동해가며 입력 데이터에 적용.
- 입력과 필터에 대응하는 원소끼리 곱한 후 그 총합을 구함.(단일 곱셈-누산, fused multiply-add, FMA)
- CNN에서는 필터의 매개변수가 ‘가중치’에 해당. 편향까지 포함하면 아래와 같음.
![]()
- 편향은 항상 하나(1x1)만 존재. 그 하나의 값을 필터를 적용한 모든 원소에 더함.
0119. 패딩padding: 합성곱 연산을 수행하기 전에 입력 데이터 주변을 특정 값(예컨대 0)으로 채우기도 함.
![]()
- 출력 크기를 조정할 목적으로 사용.
- 입력 데이터의 공간적 크기를 고정한 채로 다음 계층에 전달할 수 있음.
0120. 스트라이드stride: 필터를 적용하는 위치의 간격.
- 스트라이드를 2로 하면 필터를 적용하는 윈도우가 두 칸씩 이동.
0121. 출력크기 계산: 입력크기를 (H, W), 필터 크기를 (FH, FW), 출력 크기를 (OH, OW), 패딩을 P, 스트라이드를 S라 하면
![]()
- (W+2P-FW)/S 와 (H+2P-FH)/S가 정수로 나누어떨어지는 값이어야 함.
0122. 3차원 데이터 합성곱 연산
![]()
- 3차원 데이터를 2차원 데이터와 비교하면, 길이 방향(채널 방향)으로 특징 맵이 늘어났음.
- 채널쪽으로 특징 맵이 여러 개 있다면 입력 데이터와 필터의 합성곱 연산을 채널마다 수행하고, 그 결과를 더해서 하나의 출력을 얻음.
- 3차원 합성곱 연산 주의할 점: 입력 데이터의 채널 수와 필터의 채널 수가 같아야 함.
- 필터 자체의 크기는 원하는 값으로 설정할 수 있음.(단, 모든 채널의 필터가 같은 크기여야 함.)
0123. 3차원의 합성곱 연한은 데이터와 필터를 직육면체 블록이라고 생각하면 쉬움.
![]()
- 필터의 가중치 데이터는 4차원 데이터이며 (출력 채널 수, 입력 채널 수, 높이, 너비).
- 편향의 형상은 (FN, 1, 1)이고, 필터의 출력 결과의 형상은 (FN, OH, OW)인데 이 두 블록을 더하면 편향의 각 값이 필터의 출력인 (FN, OH, OW) 블록의 대응 채널의 원소 모두에 더해짐.
0124. 합성곱 연산을 배치 처리하기 위해 각 계층을 흐르는 데이터의 차원 수를 하나 늘려 4차원 데이터로 저장. (데이터 수, 채널 수, 높이, 너비)
![]()
- 신경망에 4차원 데이터가 하나 흐를 때마다 데이터 N개에 대한 합성곱 연산이 이루어짐. 즉, N회 분의 처리를 한 번에 수행.
0125. 풀링: 가로, 세로 방향의 공간을 줄이는 연산. 최대 풀링max pooling, 평균 풀링average pooling. 풀링의 윈도우 크기와 스트라이드는 같은 값으로 설정하는 것이 보통.
- 풀링 계층의 특징
- 학습해야 할 매개변수 없음.
- 채널 수가 변하지 않음. 채널마다 독립적으로 계산.
- 입력의 변화에 영향을 적게 받음.
- 학습해야 할 매개변수 없음.
0126.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
"""(im2col과 반대) 2차원 배열을 입력받아 다수의 이미지 묶음으로 변환한다.
Parameters
----------
col : 2차원 배열(입력 데이터)
input_shape : 원래 이미지 데이터의 형상(예:(10, 1, 28, 28))
filter_h : 필터의 높이
filter_w : 필터의 너비
stride : 스트라이드
pad : 패딩
Returns
-------
img : 변환된 이미지들
"""
N, C, H, W = input_shape
out_h = (H + 2*pad - filter_h)//stride + 1
out_w = (W + 2*pad - filter_w)//stride + 1
col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 4, 5, 1, 2)
img = np.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1))
for y in range(filter_h):
y_max = y + stride*out_h
for x in range(filter_w):
x_max = x + stride*out_w
img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]
return img[:, :, pad:H + pad, pad:W + pad]
0127.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
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
- reshape에 -1을 지정하면 다차원 배열의 원소 수가 변환 후에도 똑같이 유지되도록 적절하게 묶어줌.
- (10, 3, 5, 5) 형상을 한 다차원 배열의 원소 수는 총 750개임. 이 배열에 reshape(10, -1)을 호출하면 750개의 원소를 10묶음으로, 즉 형상이 (10, 75)인 배열로 만들어줌.
- (10, 3, 5, 5) 형상을 한 다차원 배열의 원소 수는 총 750개임. 이 배열에 reshape(10, -1)을 호출하면 750개의 원소를 10묶음으로, 즉 형상이 (10, 75)인 배열로 만들어줌.
- transpose: 다차원 배열의 축 순서를 바꿔주는 함수.
- (N, H, W, C) -> (N, C, H, W)
- (0, 1, 2, 3) -> (0, 3, 1, 2)
0128.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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) # 2차원 배열, 즉 행렬이라면 axis=0은 열 방향, axis=1은 행 방향을 뜻함.
# 적절한 성형 (3)
out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
0129.
초기화 때 받는 인수
- input_dim: 입력 데이터(채널 수, 높이, 너비)의 차원
- conv_param: 합성곱 계층의 하이퍼파라미터(딕셔너리), 딕셔너리의 키는 다음과 같음.
- filter_num: 필터 수
- filter_size: 필터 크기
- stride: 스트라이드
- pad: 패딩
- filter_num: 필터 수
- hidden_size: 은닉층(완전연결)의 뉴런 수
- output_size: 출력층(완전연결)의 뉴런 수
- weight_init_std: 초기화 때의 가중치 표준편차
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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_strite = 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 predic(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.last_layer.forward(y, t)
def gradient(self, x, t):
# 순전파
self.loss(x, t)
# 역전파
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'] = 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
0130. 층 깊이에 따른 추출 정보 변화
- 계층이 깊어질수록 추출되는 정보(정확히는 강하게 반응하는 뉴런)는 더 추상화됨.
- 처음 층은 단순한 에지에 반응하고, 이어서 텍스처에 반응하고, 더 복잡한 사물의 일부에 반응하도록 변화.
- 층이 깊어지면서 뉴런이 반응하는 대상이 단순한 모야에서 ‘고급’ 정보로 변화해감. = 사물의 ‘의미’를 이해하도록 변화함.