[Deep Learning from Scratch] 7. 합성곱 신경망(CNN)
import numpy as np
합성곱 신경망 (CNN, Convolutional Neural Network)¶
- 합성곱 계층(Convolutional layers)과 풀링 계층(Pooling layer)이 추가됨
- CNN계층은 Conv - Relu -Pooling'의 흐름으로 연결됨(Pooling 계층은 생략되기도 함)
ex ) Conv → Relu → Pooling → Conv → Relu → Pooling → Conv → Relu → Affine → Relu → Affine → Softmax
합성곱 계층¶
- 완전 연결계층(Fully connected Layer)의 문제점
- 데이터의 형상이 무시됨, 완전연결 계층에 입력할 때에는 3차원의 데이터를 1차원 데이터로 평탄화 시켜줘야 함. 즉 공간적 정보를 무시하고 모든 입력데이터를 동등한 뉴런(같은 차원의 뉴런)으로 취급하여 형상에 담긴 정보를 살릴수 없음
합성곱 계층은 형상은 유지, 3차원 이미지 데이터를 3차원 데이터로 전달
특징 맵(Feature map): 합성곱 계층의 입출력 데이터
- 입력특징 맵(input feature map): 합성곱 계층의 입력 데이터
- 출력특징 맵(output feature map): 합성곱 계층의 출력 데이터
합성곱 연산¶
- 합성곱 연산 = 필터(커널) 연산
- 입력과 필터에서 대응하는 원소끼리 곱한 후 그 총합을 구함
Scipy에서는 합성곱 함수를 쓸때 scipy.signal.correlated2d를 사용해야 함
- 편향은 필터를 적용한 후 데이터에 더해짐 따라서 항상 하나(1X1) 만 존재 하면 그 하나의 값을 필터를 적용한 모든 원소에 더함
패딩(Padding)¶
- 패딩(padding): 합성곱을 수행하기 전 입력데이터 주변을 특정값 ('0')으로 채움
- 패딩은 주로 출력 크기를 조정할 목적으로 사용
합성곱 연산을 거칠때마다 출력 크기가 작아진다. 이러한 사태를 막기위해 패딩을 사용. 즉 입력 데이터의 공간적 크기를 고정한 채로 다음 계층에 전달 할수 있음
스트라이드(Stride)¶
스트라이드(stride): 필터를 적용하는 위치의 간격,(보폭)
패딩, 스트라이드 출력크기 계산
Output=Input+2∗padding−filterstride+1
- ouput은 정수로 나누어 떨어지는 값이어야 한다. 딥러닝 프레임 워크 중에는 값이 나누어떨어지지 않을때 가장 가까운 정수로 반올림 하는 등 에러를 내지 않고 진행하도록 구현하는 경우도 있음
3차원 데이터의 합성곱 연산¶
- 입력 데이터의 채널수와 필터의 채널수가 같아야 함
- 필터 자체의 크기는 원하는 값으로 설정할 수 있으나. 모든 채널의 필터가 같은 크키여야함
- 아래의 그림은 (H, W, C)순서
블록으로 생각¶
- 입력데이터(C, H, W), 필터(C, FH, Fw) → (1, OH, OW): 채널이 1개인 feature map
- 합성곱 연산으로 다수의 채널을 보내려면 다수의 필터를 사용하면 됨
입력데이터(C, H, W), 필터(FN,C, FH, Fw) → (FN, OH, OW)
- FN개의 필터를 적용하면 출력 맵이 FN개 생성됨
- 따라서 필터의 가중치 데이터는 4차원 데이터 (출력 채널수, 입력 채널수, 높이, 너비)
- 편향은 채널 하나에 값 하나씩으로 구성됨
배치처리¶
- 각 계층에 흐르는 데이터를 차원을 하나 늘려 배치처리 (데이터 수, 채널수, 높이, 너비)
- 각 데이터의 선두에 배치용 차원을 추가 하여 4차원 형상을 가진 채 각 계층을 타고 흐름 → N회 분의 처리를 한번에 수행
풀링계층(Pooling Layer)¶
- 풀링은 가로세로 방향의 공간을 줄여주는 연산
- max pooling(최대풀링)은 대상영역에 최대값을 구하는 연산
- 풀링 윈도우 크기와 스트라이드 값을 같은 값으로 설정 하는 것이 일반적
윈도우가 3x3이면 스트라이드도 3 으로 윈도우가 4x4이면 스트라이드도 4로 설정
- 평균풀링은 대상 영역의 평균을 계산하는 것으로 이미지 인식분야에서는 주로 최대풀링을 사용
풀링계층의 특징¶
- 학습해야할 매개변수가 없다
풀링은 대상영역에서 최댓값이나 평균을 취하는 처리이므로 학습할 것이 없다
- 채널수가 변하지 않는다
풀링연산은 입력데이터 채널수를 그대로 출력데이터로 내보낸다 (채널마타 독립적으로 계산하기 때문)
- 입력의 변화에 영향을 적게 받는다(강건하다)
입력데이터가 조금 변해도 풀링의 결과는 잘 변하지 않는다.
x = np.random.rand(10, 1, 28, 28)
print('전체 데이터의 크기')
print(x.shape)
print('첫번째 데이터의 크기')
print(x[0].shape)
print('두번째 데이터의 크기')
print(x[1].shape)
전체 데이터의 크기 (10, 1, 28, 28) 첫번째 데이터의 크기 (1, 28, 28) 두번째 데이터의 크기 (1, 28, 28)
im2col(Image to column)와 col2im전개¶
- 이미지에서 행렬로 전개 : 필터 적용영역을 앞에서 부터 순서대로 1줄로 펼침
- im2col로 입력데이터를 전개한 다음 합성곱 계층의 필터를 1개의 열로 전개하고 두 행렬의 곱을 계산하면 됨
im2col: 다수의 이미지를 입력받아 2차원 배열로 변환한다(평탄화)
Parameters
- input_data : 4차원 배열 형태의 입력 데이터(이미지 수, 채널 수, 높이, 너비)
- filter_h : 필터의 높이
- filter_w : 필터의 너비
- stride : 스트라이드
- pad : 패딩
col2im: 2차원 배열을 입력받아 다수의 이미지 묶음으로 변환
Parameters
- col: 2차원 배열
tmp =[1,2,3]
print('==왼쪽에 하나 오른쪽에 2개 패딩==')
print(np.pad(tmp, (1,2), 'constant', constant_values=0))
mat =np.array([[1, 2, 3],[4, 5, 6]])
print('==상하좌우 패딩==')
print(np.pad(mat, ((2,2),(2,2)), 'constant', constant_values=0)) #상하좌우 패딩
==왼쪽에 하나 오른쪽에 2개 패딩== [0 1 2 3 0 0] ==상하좌우 패딩== [[0 0 0 0 0 0 0] [0 0 0 0 0 0 0] [0 0 1 2 3 0 0] [0 0 4 5 6 0 0] [0 0 0 0 0 0 0] [0 0 0 0 0 0 0]]
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
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
x1 = np.random.rand(1, 3, 7, 7)
col1 = im2col(x1, 5, 5, stride=1, pad=0)
print(col1.shape)
x2 = np.random.rand(10, 3, 7, 7)
col2 = im2col(x2, 5, 5, stride=1, pad=0)
print(col2.shape)
(9, 75) (90, 75)
def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
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]
합성곱 계층 구현¶
class Convolution:
def __init__(self, W, b, stride =1, pad=0):
self.W = W
self.b = b
self.stride = stride
self.pad = pad
# 중간데이터 (backward 시 사용)
self.x = None
self.col = None
selff.col_W = None
# 가중치와 편향 매개변수 기울기
self.dW = None
self.db = None
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)
self.x = x
self.col = col
self.col_W = col_W
return out
def backward(self, dout):
FN, C, FH, FW = self.W.shape
dout = dout.transpose(0, 2, 3, 1).reshape(-1, FN)
self.db = np.sum(dout, axis=0)
self.dW = np.dot(self.col.T, dout)
self.dW = self.dW.transpose(1,0).reshape(FN, C, FH, FW)
dcol = np.dot(dout, self.col_W.T)
dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)
return dx
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)
# 전개
col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
col = col.reshape(-1, self.pool_h*self.pool_w)
# 최댓값
arg_max = np.argmax(col, axis=1)
out = np.max(col, axis=1)
# 성형
out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
self.x = x
self.arg_max= arg_max
return out
def backward(self, dout):
dout = dout.transpose(0, 2, 3, 1)
pool_size = self.pool_h * self.pool_w
dmax = np.zeros((dout.size, pool_size))
dmax[np.arange(self.arg_max.size),self.arg_max.flatten()] = dout.flatten()
dmax = dmax.reshape(doutt.shape+(pool_size,))
dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)
return dx
CNN시각화하기¶
학습전 필터는 무작위로 초기화되어 있어 규칙성이 없지만 학습을 마친 필터는 규칙성이 있는 이미지가 됨 → 합성곱 계층 필터는 에지나 블롭등 정보를 추출할 수 있음
합성공 계층을 여러 겹 쌓으면 층이 깊어지면서 더 복잡하고 추상화된 정보가 추출됨. → 처음 층은 단순 에지에 반응하고 이어서 텍스처, 더 복잡한 사물의 일부에 반응하도록 변함 → 층이 깊어지면서 뉴런이 반응하는 대상이 단순한 모양에서 고급정보로 변해감(사물의 의미를 이해하도록 변화하는 것)
AlexNet¶
- 합성곱 계층과 풀링 계층을 거듭하며 마지막에 완전연결 계층을 거쳐 결과를 출력
- 활성화 함수로 ReLU 이용
- LRN (Local Response Normalization)이라는 국소적 정규화를 실시하는 계층을 이용
- 드롭아웃 사용
Summary
- CNN은 지금까지의 완전연결 계층 네트워크에 합성곱 계층과 풀링 계층을 새로 추가한다.
- 합성곱 계층과 풀링 계층은 im2col(이미지를 행렬로 전개하는 함수)를 이용하면 간단하고 효율적으로 구현할 수 있다.
- CNN을 시각화해보면 계층이 깊어질수록 고급 정보가 추출되는 모습을 확인할 수 있다.
- 대표적인 CNN에는 LeNet과 AlexNet이 있다.
- 딥러닝의 발전에는 빅데이터와 GPU가 크게 기여했다.