지금까지 공부한 신경망들은 인접하는 계층의 모든 뉴런과 결합이 되어있었다. 이를 완전연결이라고 하고, 완전히 연결된 계층을 Affine 계층이라는 이름으로 구현했다. 그 구조는 다음과 같았다.
완전연결 신경망은 Affine 계층 뒤에 활성화 함수를 갖는 ReLU 계층이 이어진다. 마지막 5번째 층은 Affine 계층에 이어 softmax 계층에서 최종겨롸를 출력한다.
CNN 구조는 다음과 같다.
CNN에는 합성곱 계층과 폴링 계층이 추가된다. CNN 계층은 Conv-ReLU 흐름으로 연결된다. Affine-ReLU 에서 Conv-Relu 로 바뀌었다고 생각하면 된다.
CNN의 특징 중 또 하나는 출력에 가까운 층에서는 Affine-ReLU 구성을 사용할 수 있다는 점이다. 마지막 출력 계층에서는 Affine-Softmax 조합을 그대로 사용한다.
CNN에서는 패딩, 스트라이트 등 CNN 고유의 용어가 있다. 각 계층 사이에는 3차원 데이터같이 입체적인 데이터가 흐른다는 점에서 완전연결 신경망과 다르다.
완전연결 계층에서는 인접하는 계층의 뉴런이 모두 연결되고 출력의 수는 임의로 정할 수 있다. 하지만 데이터의 형상이 무시된다는 문제점이 있다. 완전연결 계층에서 이미지를 예를 들면 가로 세로 채널(색상) 으로 구성된 3차원을 입력할 때에는 1차원 데이터로 평탄화해줘야한다. 이미지의 형상에는 소중한 공간적 정보가 있다. 그러나 완전연결 계층은 이것을 무시하고 모든 입력 데이터를 동등한 뉴런(같은 차원의 뉴런)으로 취급하여 형상에 담긴 정보를 살릴 수 없다.
합성곱 계층은 형상을 유지한다. 이미지도 3차원 데이터로 입력받고 전달역시 3차원 데이터로 전달한다. CNN에서는 합성곱 계층의 입출력 데이터를 특징 맵이라고도 한다. 입력데이터를 입력 특징 맵, 출력 데이터를 출력 특징 맵이라고 하는 식이다.
합성곱 연산은 이미지 처리에서 말하는 필터 연산에 해당한다. 예를 들면 다음과 같다.
합성곱 연산은 입력 데이터에 필터(커널)를 적용한다. 합성곱 연산은 필터의 윈도우를 일정 간격으로 이동해가며 입력 데이터에 적용한다. 입력과 필터에서 대응하는 원소끼리 곱한 후 그 총합을 출력의 해당 장소에 저장한다. 완전연결 신경망에서는 가중치 매개변수와 편향이 존재하는데 CNN에서는 필터의 매개변수가 그 동안의 가중치에 해당한다. 여기에 편향까지 포함하면 다음과 같은 흐름이 된다.
편향 하나의 값을 필터를 적용한 모든 원소에 더하는 것이다.
합성곱 연산을 수행하기 전에 입력 데이터 주변을 특정 값으로 채우기도 한다. 이를 패딩이라고 한다.
패딩은 주로 출력 크기를 조정할 목적으로 사용된다. (4,4) 입력 데이터에 (3,3) 필터를 적용하면 출력은 (2,2)가 되어 입력보다 2만큼 줄어든다. 계속 진행하다보면 출력 크기가 계속 줄어들어 더 이상은 합성곱 연산을 적용할 수 없을 때가 오는데 이를 방지하기 위해 사용한다.
필터를 적용하는 위치의 간격을 스트라이드라고 한다. 스트라이드를 2로 하면 필터를 적용하는 윈도우가 두 칸씩 이동하게 된다.
하지만 스트라이드를 크게 설정하면 출력 크기가 줄어든다. 반대로 패딩을 크게하면 출력의 크기가 커진다. 이런한 관계를 수식으로 나타내면 다음과 같다.
3차원의 합성곱 연산은 데이터와 필터를 직육면체 블록이라고 생각하면 된다. 블록은 3차원 직육면체이다. 예를 들어 채널 수C, 높이 H, 너비 W인 데이터의 형상은 (C,H,W)로 쓴다.
출력 데이터는 한 장의 특징 맵이다. 합성곱 연산의 출력으로 다수의 채널을 내보내려면 필터(가중치)를 다수 사용하면 된다.
필터를 FN개 적용하면 출력 맵도 FN개가 생성이 된다. 그리고 FN개의 맵을 모으면 형상이 (FN, OH, OW)인 블록이 완성된다. 이 완성된 블록을 다음 계층으로 넘기는 것이 CNN 처리 흐름이다. 필터의 가중치 데이터는 4차원 데이터이며 (출력 채널 수, 입력 채널 수, 높이, 너비) 순으로 쓴다. 합성곱 연산에서도 편향이 쓰인다.
합성곱 연산에서도 배치 처리를 지원한다. 각 계층을 흐르는 데이터의 차원을 하나 늘려 4차원 데이터로 저장한다. 구체적으로는 데이터를 (데이터 수, 채널 수, 높이, 너비) 순으로 저장한다. 데이터가 N개일 때 배치 처리하면 데이터의 형태가 다음과 같아진다.
각 데이터의 선두에 배치용 차원을 추가했다. 데이터는 4차원 형상을 가진 채 각 계층을 타고 흐르는데 주의할 점은 신경망에 4차원 데이터가 하나 흐를 때마다 데이터 N개에 대한 합성곱 연산이 이뤄진다는 것이다. 다시말해 N회 분의 처리를 한번에 수행한다는 것이다.
폴링은 세로,가로 방향의 공간을 줄이는 연산이다.
최대폴링을 스트라이드 2로 처리하는 순서이다. 최대 폴링은 최대값을 구하는 연산으로 2x2 크기의 영역에서 가장 큰 원소 하나를 꺼낸다. 폴링은 최대 폴링 외에도 평균 폴링 등이 있다. 평균 폴링은 대상 영역의 평균을 계산한다. 이미지 인식 분야에서는 주로 최대 폴링을 사용한다.
im2col은 입력 데이터를 필터링하기 좋개 전개하는 함수이다.
물론 실제 상황에서는 영역이 겹치는 경우가 대부분이다. 그렇기 때문에 im2col로 전개한 후의 원소 수가 원래 블록의 원소 수보다 많아진다. 그래서 메모리를 더 많이 소비하는 단점이 있다.
im2col로 입력 데이터를 전개한 다음에는 합성곱 계층의 필터를 1열로 전개하고 두 행렬의 곱을 계산하면 된다. 이는 완전연결 계층의 Affine 계층에서 한 것과 거의 같다. 상세 과정을 살펴보면 다음과 같다.
im2col 방식으로 출력한 결과는 2차원 행렬이다. CNN은 데이터를 4차원 배열로 저장하므로 2차원인 출력 데이터를 4차원으로 변형한다.
im2col을 사용하여 합성곱 계층을 구현한 코드는 다음과 같다.
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
합성곱 계층은 필터, 현향, 스트라이드, 패딩을 인수로 받아 초기화한다. 필터는 (FN, C, FH, FW)의 4차원 형상이다. FN은 필터 개수, C는 채널, FH는 필터 높이, FW는 필터 너비이다. 입력데이터를 im2col로 전개하고 필터도 reshape를 사용해 2차원 배열로 전개한다. 그리고 두 행렬의 곱을 구한다. reshape의 두 번째 인수를 -1로 지정하는 이유는 다차원 배열의 원소 수가 변환 후에도 똑같이 유지되도록 적절히 묶어준다. (10, 3, 5, 5) 형상을 한 다차원 W의 원소 수는 총 10x3x5x5 = 750 개이다. 이 배열에 reshape(10,-1)을 호출하면 형상이 (10,75)인 배열로 만들어준다. 다음으로 forward 구현의 마지막에는 출려 데이터를 적절한 형상으로 바꿔준다. 이때 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
폴링 계층은 3 단계로 진행된다.
1. 입력 데이터를 전개한다.
2. 행별 최대값을 구한다.
3. 적절한 모양으로 성형한다.
초기화할 때 받는 인수
input_dim - 입력 데이터(채널 수, 높이, 너비)의 차원
conv_param - 합성곱 계층의 하이퍼파라미터(딕셔너리), key는 다음과 같다
filter_num : 필터 수
filter_size : 필터 크기
stride : 스트라이드
pad : 패딩
hidden_size : 은닝층의 뉴런 수
output_size : 출력층의 뉴런 수
weight_init_std : 초기화 때의 가중치 표준편차
SimpleConvNet의 코드를 나타내면 다음과 같다
class SimpleConvNet:
"""단순한 합성곱 신경망
conv - relu - pool - affine - relu - affine - softmax
Parameters
----------
input_size : 입력 크기(MNIST의 경우엔 784)
hidden_size_list : 각 은닉층의 뉴런 수를 담은 리스트(e.g. [100, 100, 100])
output_size : 출력 크기(MNIST의 경우엔 10)
activation : 활성화 함수 - 'relu' 혹은 'sigmoid'
weight_init_std : 가중치의 표준편차 지정(e.g. 0.01)
'relu'나 'he'로 지정하면 'He 초깃값'으로 설정
'sigmoid'나 'xavier'로 지정하면 'Xavier 초깃값'으로 설정
"""
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_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)
학습에 필요한 매개변수는 1번째 층의 합성곱 계층과 나머지 두 완전연결 계층의 가중치 편향이다. 이 매개변수들을 인스턴수 변수 params 딕셔너리에 저장한다. 1번째 층의 합ㅂ성곱 계층의 가중치를 W1, 편향을 b1이라는 키로 저장한다. 마찬가지로 2번째 층의 완전연결 계층의 가중치와 편향을 W2와 b2, 마지막 3번째 층의 완전연결 계층의 가중치와 편향을 W3와 b3라는 키로 각각 저장한다. 마지막으로 CNN을 구성하는 계층들을 생성한다.
# 계층 생성
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라는 별도 변수에 저장해둔다. 초기화 한 이후에는 predict 메서드와 loss 메서드를 통해 추론을 수행하고 손실 함수의 값을 구한다.
def predict(self, x):
for layer in self.layers.values():
x = layer.forward(x)
return x
def loss(self, x, t):
"""손실 함수를 구한다.
Parameters
----------
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):
"""기울기를 구한다(오차역전파법).
Parameters
----------
x : 입력 데이터
t : 정답 레이블
Returns
-------
각 층의 기울기를 담은 사전(dictionary) 변수
grads['W1']、grads['W2']、... 각 층의 가중치
grads['b1']、grads['b2']、... 각 층의 편향
"""
# forward
self.loss(x, t)
# backward
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'], grads['b1'] = self.layers['Conv1'].dW, self.layers['Conv1'].db
grads['W2'], grads['b2'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
grads['W3'], grads['b3'] = self.layers['Affine2'].dW, self.layers['Affine2'].db
return grads
매개변수의 기울기는 오차역전파법으로 구한다. 이 과정은 순전파와 역전파를 반복한다. 마지막으로 grads라는 딕셔너리 변수에 각 가중치 매개변수의 기울기를 저장한다.
MNIST 데이터 셋으로 CNN 학습알 해보면 1번째 층의 합성곱 계층의 가중치는 그 형상이 (30, 1, 5, 5)이다. 필터의 크기가 5x5이고 채널이 1개라는 것은 이 필터를 1채널의 회색조 이미지로 시각화할 수 있다는 뜻이다. 합성곱 계층 필터를 이미지로 나타내면 다음과 같다.
학습 전 필터는 무작위로 초기화되고 있기 때문에 흑백의 정도에 규칙성이 없지만 학습 후에는 규칙성 있는 이미지가 되었다. 이렇게 규칙성 있는 필터는 에지(색상이 바뀐 경계선)과 블롭(국소적으로 덩어리진 영역)을 보고 있다.
계층이 깊어질 수록 추출되는 정보는 더 추상화 된다. 딥러닝의 흥미로운 점은 합성곱 계층을 여러 겹 쌓으면 층이 깊어지면서 더 복잡하고 추상화된 정보가 추출된다는 점이다. 처음 층은 단순한 에지에 반응하고, 이어서 텍스처에 반응하고, 더 복잡한 사물의 일부에 반응하도록 변화한다. 층이 깊어지면서 뉴런이 반응하는 대상이 단순한 모양에서 고급 정보로 변화해간다. 다시말해 사물의 의미를 이해하도록 변화하는 것이다.
LeNet은 손글씨 숫자를 인식하는 네트워크이다. 합성곱 계층과 폴링 계층을 반복하고 마지막으로 완전연결 계층을 거치면서 결과를 출력한다.
LeNet과 현재의 CNN은 차이가 있는데 첫 번째 차이는 활성화 함수이다. LeNet은 시그모이드 함수를 사용하는데 반해 현재는 주로 ReLU를 사용한다. 또 원래의 LeNet은 서브샘플링을 하여 중간 데이터의 크기를 줄이지만 현재는 최대 풀링이 주류이다.
구성을 살펴보면 다음과 같다.
AlexNet은 합성곱 계층과 풀링 계층을 거듭하여 마지막으로 완전연결 계층을 거쳐 결과를 출력한다. AlexNet에서는 다음과 같은 변화를 주었다.
활성화 함수로 ReLU를 이용한다
LRN이라는 국소적 정규화를 실시하는 계층을 이용한다
드롭아웃을 사용한다
출처 : 밑바닥부터 시작하는 딥러닝: 파이썬으로 익히는 딥러닝 이론과 구현 - 사이토 고키(2017)