numpy에 for문을 사용하면 성능이 떨어지는 단점이 있습니다.
(numpy에서는 원소에 접근할 때 for문을 사용하지 않는 것이 바람직합니다.)
for문을 대신하여 im2col 함수를 이용합니다.
im2col 함수를 이용한 합공곱 계층의 구현 흐름
im2col 함수는 필터링하기 좋게 입력 데이터를 전개합니다.
3차원 입력 데이터에 im2col을 적용하면 2차원 행렬로 바뀝니다.
정확히는, 배치 안의 데이터 수까지 포함한 4차원 데이터를 2차원으로 변환합니다.
위 그림은 스트라이드를 크게 필터의 적용 영역이 겹치지 않도록 했지만, 실제 상황에서는 영역이 겹치는 경우가 대부분입니다.
필터 적용 영역이 겹치게 되면 im2col로 전개한 후의 원소 수가 원래 블록의 수보다 많아집니다.
그래서 im2col을 사용해 구현하면 메모리를 더 많이 소비한다는 단점이 생깁니다. 하지만 컴퓨터는 큰 행렬을 묶어서 계산하는 데에는 탁월합니다. 예를 들어 행렬 계산 라이브러리(선형 대수 라이브러리) 등은 행렬 계산에 고도로 최적화 되어 큰 행렬의 곱셈을 빠르게 계산할 수 있습니다. 그래서 문제를 행렬 계산으로 만들면 선형 대수 라이브러리를 활용해 효율을 높일 수 있습니다.
im2col로 입력 데이터를 전개한 다음에는 합성곱 계층의 필터(가중치)를 1열로 전개하고, 두 행렬의 곱을 계산하면 됩니다. 이는 완전연결 계층의 Affine 계층에서 한 것과 거의 동일합니다.
im2col 방식으로 출력한 결과는 2차원 행렬 입니다. CNN은 데이터를 4차원 배열로 저장하므로 2차원인 출력 데이터를 4차원으로 변형 합니다.
합성곱 계층 구현하기
import sys, os
import numpy as np
sys.path.append(os.pardir)
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
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)
출력결과
과정 시각화
- X1
- X2
합성곱 계층 클래스
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
합성곱 계층은 필터(가중치), 편향, 스트라이드, 패딩을 인수로 받아 초기화합니다.
필터는 (FN, C, FH, FW)의 4차원 형상입니다. (FN: 필터 개수, C: 채널, FH: 필터 높이, FW: 필터 너비)
표시한 중요부분은 필터를 전개하는 부분으로써, 입력 데이터를 im2col로 전개하고 필터도 reshape을 사용해 2차원 배열로 전개합니다. 이렇게 전개한 두 행렬의 곱을 구합니다.
self.W.reshape(FN, -1).T에서 -1은 배열의 원소 개수에 맞게 자동으로 묶어주는 기능을 합니다.
예를 들어 (10, 3, 5, 5)를 (10, -1)로 지정한다면
10, 3, 5, 5)의 총 원수 개수가 750이니, (10, 75)를 뜻할 것 입니다.
forward 구현의 마지막에선 출력 데이터를 적절한 형상으로 바꿔줍니다. 이때 넘파이의 transpose 함수를 사용하는데, 이는 다차원 배열의 축 순서를 바꿔주는 함수 입니다.
합성곱 계층의 역전파에서는 im2col함수를 역으로 구현한 col2im 함수를 이용하면 됩니다.
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]
풀링 계층 구현
풀링 계층 구현도 합성곱 계층과 마찬가지로 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)
col = im2col(x, self.pool_h, self.pool_w, stride, self.pad)
col = col.reshape(-1, self.pool_h * self.pool_w)
#최댓값 (2)
out = np.max(col, axis=1)
#성형 (3)
out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
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
CNN 구현하기
다음과 같은 CNN 네트워크는
"Convolution - ReLU - Pooling - Affine - ReLU - Affine - Softmax" 순으로 흐릅니다.
이를 SimpleConvNet이라는 이름의 클래스로 구현해보겠습니다.
class SimpleConvNet:
def __init__(self, intput_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_size = input_dim[1]
conv_output_size = (input_size - filter_size + 2 * filter_pad) / filiter_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'] = wieght_init_std * np.random.randn(hidden_size, output_size)
self.params['b3'] = np.zeros(output_size)
가중치 매개변수를 초기화하는 부분 입니다.
학습에 필요한 매개변수는 1번째 층의 합성곱 계층과 나머지 두 완전연결 계층의 가중치와 편향입니다. 이 매개변수들은 인스턴스 변수 params 딕셔너리에 저장합니다.
1번째 층의 합성곱 계층의 가중치를 W1, 편향을 b1이라는 키로 저장합니다.
마찬가지로 2번째 층의 완전연결 계층의 가중치와 편향을 W2와 b2,
마지막 3번째 층의 완전연결 계층의 가중치와 평향을 W3와 b3라는 키로 각각 저장합니다.
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()
순서가 있는 딕셔너리인 layers에 계층들을 차례로 추가합니다. 마지막 SoftmaxWithLoss 계층만큼은 last_layer라는 별도 변수에 저장해둡니다.
이상이 SimpleConvNet의 초기화 과정이었습니다. 이렇게 초기화를 마친 다음에는 추론을 수행하는 predict 메서드와 손실 함수의 값을 구하는 loss 메서드를 다음과 같이 구현할 수 있습니다.
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)
이 코드에서 인수 x는 입력 데이터, t는 정답 레이블 입니다.
추론을 수행하는 predict 메서드는 초기화 때 layers에 추가한 계층을 맨 앞에서부터 차례로 forward 메서드를 호출하며 그 결과를 다음 계층에 전달합니다. 손실 함수를 구하는 loss 메서드는 predict 메서드의 결과를 인수로 마지막 층의 forward 메서들르 호출합니다.
즉, 첫 계층부터 마지막 계층까지 forward를 처리합니다.
이어서 오차역전파법으로 기울기를 구하는 구현은 다음과 같습니다.
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
매개변수의 기울기는 오차역전파법으로 구합니다. 이 과정은 순전파와 역전파를 반복합니다.
마지막으로 grads라는 딕셔너리 변수에 각 가중치 매개변수의 기울기를 저장합니다.
딥러닝의 흥미로운 점은 위 그림과 같이 합성곱 계층을 여러 겹을 쌓으면, 층이 깊어지면서 더 복잡하고 추상화된 정보가 추출된다는 것 입니다. 처음 층은 단순한 에지에 반응하고, 이어서 텍스처에 반응하고, 더 복잡한 사물의 일부에 반응하도록 변화합니다.
즉, 층이 깊어지면서 뉴런이 반응하는 대상이 단순한 모양에서 '고급' 정보로 변화해갑니다. 다시 말하면 사물의 '의미'를 이해하도록 변화하는 것 입니다.
대표적인 CNN
- 원조 CNN, LeNet
- 딥러닝이 주목받도록 이끈, AlexNet
LeNet
LeNet은 손글씨 숫자를 인식하는 네트워크로 1998년에 제안되었습니다.
위 그림과 같이 합성곱 계층과 풀링 계층(정확히는 단순히 '원소를 줄이기'만 하는 서브샘플링 계층)을 반복하고, 마지막으로 완전연결 계층을 거치며 결과를 출력합니다.
LeNet과 현재 CNN 차이점
- 활성화 함수
LeNet은 sigmoid 함수 사용
현재 CNN은 ReLU tkdyd
- 풀링 계층
LeNet은 서브샘플링을 하여 중간 데이터의 크기를 줄여나감
현재 CNN은 최대 풀링이 주류
AlexNet
AlexNet은 2012년에 발표되었고 딥러닝 열풍을 일으키는 데 큰 역할을 했습니다.
AlexNet은 기본적으로 LeNet과 크게 다르지 않는데요,
합성곱 계층과 풀링 계층을 거듭하며 마지막으로 완전연결 계층을 거쳐 결과를 출력합니다.
LeNet과의 차별성은 다음과 같습니다.
- 활성화 함수로 ReLU를 이용한다
- LRN(Local Response Normalization)일는 국소적 정규화를 실시하는 계층을 이용한다
- 드롭아웃을 사용한다
마침 빅테이터 발전과 병렬 계산에 특화된 GPU가 보급되며 대량의 연산을 고속으로 수행할 수 있게 되었고 이것이 딥러닝 발전의 큰 원동력이 되었습니다.
'Paper > Deep Learning' 카테고리의 다른 글
[리뷰]Densecap: Fully Convolutional Localization Networks for Dense Captioning 번역 (1) | 2022.10.11 |
---|---|
Chainer: A Deep Learning Framework for Accelerating theResearch Cycle 리뷰 (0) | 2022.08.30 |