합성곱 연산을 그대로 구현하면 데이터 형상에따라 상당히 많은 for문을 요할 수 있다. 특히 numpy 연산에서는 for문을 사용하지 않는게 바람직하기 때문에 for 문대신 im2col이라는 함수를 사용한다.
im2col의 원리는 보다 편리한 계산을 위해 다차원 데이터를 2차원 행렬로 변환하는것을 의미한다.

필터 역시도 이 2차원 행렬과 matrix multiplication을 할 수 있도록 펼쳐준다.

물론 이렇게 펼치는 과정에서 입력데이터는 윈도우 하나하나를 펼치기 때문에 데이터 중복이 일어나 공간적으로 손해를 보는 경우가 생길 수 있다. 하지만 그만큼 시간적으로 큰 이득을 보기 때문에 im2col 함수를 사용한다. 이렇게 multiplication이 이루어지고 나면, 출력데이터 형식을 다시 reshape하는 과정을 통해 마무리해준다.
import sys,os
import numpy as np
sys.path.append(os.pardir)
from util import im2col
x1 = np.random.randn(1,3,7,7)
col1 = im2col(x1,5,5,1,0)
print(col1.shape) # (9, 75)
실제 im2col을 사용하는 예제이다. 7*7에 채널 3개짜리 입력데이터에 5*5 짜리 필터를 적용할때, im2col을 통해 입력데이터를 2차원으로 바꿀 수 있다.
과정을 생각해보면, 먼저 7*7 짜리 형상에 5*5 필터를 적용하게 되면 총 9개의 블록이 나오게된다. 이때 블록 하나에 5*5 즉 25개의 데이터에 채널이 3개이므로 이를 가로로 쭉 펼치면 75개의 데이터가 나오게된다. 따라서 입력데이터의 행렬은 (9, 75) 형상을 가지게 된다.
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
self.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 = 1 + int((H + 2*self.pad - FH) / self.stride)
out_w = 1 + int((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
앞서 설명한 im2col을 이용해 합성곱 계층을 구현한다. im2col을 통해 입력데이터를 2차원 행렬로 펼친다.
필터 역시 2차원 행렬로 펼친다. 필터는 FN개의 블록이 여러개있는데, 각 블록을 1줄로 펼치면 되니까 reshape를 이용한다.
펼친 두개의 데이터를 matrix multiplication을 진행한뒤, 원래의 형상에 맞게 다시또 reshape해준다.
이때 transpose를 사용하는 이유는 원래 데이터의 형태 x를 보면, N,C,H,W의 모습을 하는 것을 알 수 있는데, reshape를 할 경우 N,H,W,C의 결과를 얻을 수 있으므로, 다시 x형태로 바꾸기위해 transpose해준다.

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
self.x = None
self.arg_max = None
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(dout.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

풀링 계층은 그림을 통해 손쉽게 이해할 수 있다. 먼저 입력데이터를 그대로 전개한 다음, 행별 최댓값을 구해준다. 그다음 앞선 합성곱 계층과 같이 reshape를 통해 똑같은 형태로 형상을 바꿔준다.

필터수: 30
필터 사이즈 : 5
PAD : 0
STRIDE : 1
입력데이터의 형상은 (1,28,28)이다. (mnist dataset)
채널이 1개인 이유는 흑백이미지이기 때문 채널이 필요가없다.
하이퍼 파라미터와 입력데이터를 토대로 Conv-ReLu-Pooling의 과정에 따른 입력 데이터의 형상변화를 그림으로 표현하면 다음과같다.

이를 im2col을 포함한 연산과정으로 보면 다음과 같을 것이다.

위의 전개를 토대로 simplecnn을 python으로 구현하면 다음과 같다.
from layers import *
from functions import *
from collections import OrderedDict
class CNN:
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'] # 30
filter_size = conv_param['filter_size'] # 5
filter_pad = conv_param['pad'] # 0
filter_stride = conv_param['stride'] # 1
input_size = input_dim[1] # 28
conv_output_size = (input_size - filter_size + 2*filter_pad/filter_stride + 1) # 24
pool_output_size = int(filter_num * (conv_output_size/2)*(conv_output_size/2)) # 30 * 12 * 12
### 매개변수 생성 ###
self.params = {}
self.params['W1'] = weight_init_std * np.random.randn(filter_num,input_dim[0],filter_size,filter_size) # 30 * 1 * 5 * 5
self.params['b1'] = np.zeros(filter_num) # 30
self.params['W2'] = weight_init_std * np.random.randn(pool_output_size,hidden_size) # 30*12*12->4320 , 4320*100
self.params['b2'] = np.zeros(hidden_size) # 100
self.params['W3'] = weight_init_std * np.random.randn(hidden_size,output_size) # 100*10
self.params['b3'] = np.zeros(output_size) # 10
### 계층 생성 ###
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 predict(self,x): # 순전파 진행
for layer in self.layers.values():
x= layer.forward(x)
return x
def loss(self,x,t): # last layer의 forward -> forward + 손실함수 계산
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
import numpy as np
from mnist import load_mnist
from CNN import *
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=False)
train_loss_list = []
iters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1
network = CNN(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)
for i in range(iters_num):
batch_mask = np.random.choice(train_size,batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
grad = network.gradient(x_batch,t_batch)
for key in ('W1','b1','W2','b2','W3','b3'):
network.params[key] -= learning_rate*grad[key]
loss = network.loss(x_batch,t_batch)
print(i,": ",loss)
train_loss_list.append(loss)