이미지 인식 분야에서 딥러닝을 활용한 기법은 CNN을 기초로 한다
완전연결: 인접하는 계층의 모든 뉴런과 결합되어 있는 신경망
완전연결 계층: ‘Affine - ReLU’
CNN 계층: ‘Conv - ReLU - (Pooling)’
→ 출력에 가까운 층에서는 ‘Affine - ReLU’ 구성을 사용한다
데이터의 형상이 무시된다
ex) 입력 데이터가 이미지인 경우(세로, 가로, 채널) 완전연결 계층에 입력할 때는 평평한 1차원 데이터로 평탄화해줘야 한다
→ 3차원 형상에는 공간적 정보가 담겨있는데 이를 무시하고 모든 입력데이터를 동등한 뉴런으로 취급하는 것
합성곱 계층은 형상을 유지한다(이미지처럼 형상을 가진 데이터를 제대로 이해할 가능성이 있는 것)
각 계층 사이에 3차원 데이터와 같은 입체적인 데이터가 흐른다
필터의 윈도우를 일정 간격으로 이동해가며 입력데이터에 적용한다(단일 곱셈-누산)
편향(1 X 1)은 필터를 적용한 모든 원소에 더한다
출력 크기
딱 나눠떨어지지 않을 때는 가장 가까운 정수로 반올림한다
채널 쪽으로 특징 맵이 여러개 있다면 입력 데이터와 필터의 합성곱 연산을 채널마다 수행한 후 더한다
필터를 다수 사용하게 되면 출력 맵도 FN개가 생성된다 출력 데이터(FN, OH, OW)
→ 필터의 가중치 데이터는 4차원 데이터(출력 채널 수, 입력 채널 수, 높이, 너비)
신경망 처리에서 배치 처리 했던 것과 같이, 합성곱 연산에서도 배치 처리를 지원한다
각 계층을 흐르는 데이터의 차원을 하나 늘려 4차원 데이터로 저장(데이터 수, 채널 수, 높이, 너비)
가로, 세로 방향의 공간을 줄이는 연산이다
풀링의 윈도우 크기 = 스트라이드 크기
최대 풀링: 특정 영역 내에서 가장 큰 원소 하나를 꺼내기
이미지 인식 분야에서는 최대 풀링을 주로 사용
평균 풀링: 특정 영역 내에서의 평균 계산
💡 풀링 계층의 특징
CNN은 4차원 데이터를 다뤄서 for 문을 겹겹이 쌓아서 연산해야될 것 같지만, im2col이라는 트릭을 사용하면 단순해진다
입력 데이터를 필터링 하기 좋게 전개하는 함수이다
3차원 입력 데이터(배치 안의 데이터 수까지 포함하면 4차원) → 2차원 행렬
입력 데이터를 필터 크기만큼 3부분으로 나누어(3 X 3) 2차원 행렬로 만든다
→ 나눠진 영역이 겹치는 경우가 대부분
→ im2col로 전개한 후의 원소 수가 원래 블록의 원소 수보다 많아짐(메모리 더 많이 소비)
입력 데이터를 im2col로 전개하고, 필터를 세로로 1열로 전개한 후에는 Affine 계층에서 한 것과 같이 두 행렬을 계산하면 된다
출력한 결과는 2차원 행렬이지만 CNN은 데이터를 4차원 배열로 저장하므로 2차원 출력 데이터를 4차원으로 변형한다
im2col 함수의 인터페이스:
im2col(input_data, filter_h, filter_w, stride = 1, pad = 0)
# input_data: (데이터 수, 채널 수, 높이, 너비) 입력 데이터
# filter_h: 필터의 높이
# filter_w: 필터의 너비
# stride, pad: 스트라이드, 패딩
im2col을 사용한 합성곱 계층 구현:
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)
# 입력 데이터를 (FH, FW) 필터 사이즈만큼 9개로 나눈다
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
# 축 순서 변경하기 (0, 1, 2, 3) -> (0, 3, 1, 2)
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
return out
reshape에 -1을 지정하면 다차원 배열의 원소 수가 변환 후에도 똑같이 유지되도록 적절히 묶어준다
ex) (10, 3, 5, 5) → (10, 75)
마지막으로 transpose 함수를 사용해 출력 데이터를 적절한 형상으로 바꿔준다
im2col로 전개한 덕분에 완전연결 계층의 Affine 계층과 거의 비슷하게 구현 가능하다
합성곱 계층의 역전파에서는 col2im 함수를 사용하면 된다
im2col을 사용해 입력 데이터를 전개하지만, 채널 쪽이 독립적이라는 점에서 합성곱 계층 때와 다르다
입력 데이터 전개, 행별 최댓값 구하기, 적절한 모양으로 성형
풀링 계층 구현:
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) ex)풀링 크기 2x2일 때:
col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad) # 2x2로 나누고
col = col.reshape(-1, self.pool_h * self.pool_w) # 1x4로 변환
# 최댓값 (2)
out = np.max(col, axis=1) # np.max의 축 인수로 지정한 축마다 최댓값을 구할 수 있다
# 성형 (3)
out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
return out
CNN 네트워크 순서: ‘Convolution - ReLU - Pooling - Affine - ReLU - Affine - Softmax’
SimpleConvNet 1번째:
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_ize = 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))
SimpleConvNet 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(filter_num, input_dim[0], filter_size, filter_size)
self.params['b2'] = np.zeros(filter_num)
self.params['W3'] = weight_init_std * \
np.random.randn(filter_num, input_dim[0], filter_size, filter_size)
self.params['b3'] = np.zeros(filter_num)
SimpleConvNet 3번째:
self.layers = OrderedDict()
self.layers['Conv1'] = Convolution(self.params['W1'],
self.params['b1']
conv_params['stride']
conv_parmas['pad']
self.layers['Relu1'] = Relu()
self.layers['pool1'] = Pooling([ppl_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()
SimpleConvNet 4번째:
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.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'].dW
grads['W3'] = self.layers['Affine2'].dW
grads['b3'] = self.layers['Affine2'].dW
return grads
MNIST 데이터셋으로 간단한 CNN 학습을 해보았는데, 그때 1번째 층의 합성곱 계층의 가중치는 그 형상이 (30, 1, 5, 5)였다.(필터 30개, 채널 1개, 5 X 5 크기)
학습을 마친 필터는 규칙성 있는 이미지가 되었다
규칙성 있는 필터는 ‘에지’(색상이 바뀐 경계선)와 ‘블롭’(국소적으로 덩어리진 영역)을 보고 있다
1번째 층의 합성곱 계층에서는 에지나 블롭 등의 저수준 정보가 추출되지만 CNN의 각 계층이 깊어질수록 추출되는 정보는 더 추상화된다
→ 1번째 층은 에지와 블롭, 3번째 층은 텍스처, 5번째 층은 사물의 일부 등에 뉴런이 반응한다.
LeNet
AlexNet