채널 3개, 높이 너비가 7x7인 데이터를 im2col을 이용해서 바꾸어주었다. 3x5x5 짜리인 필터와 내적하기 위해서 다음과 같이 변하였다. 필터 역시 3차원에서 2차원 행렬로 변하기 때문에, col1의 shape는 (9, 75)가 되었다. ( ∵ 필터가 2차원이 되는데, 한 블럭이 한 줄이 되는 꼴이기 때문에 3x5x5=75개의 원소가 된다.)
x2는 단순히 데이터 갯수가 10배이기 때문에, 행렬 역시 (90, 75)의 꼴을 가지게 된다.
Convolution class
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 #filter
N,C,H,W = x.shape #input data
out_h = int(1 + (H + 2*self.pad - FH) / self.stride)
out_w = int(1 + (H + 2*self.pad - FW) / self.stride)
col = im2col(x,FH,FW,self.stride,self.pad)
col_W = self.W.reshape(FH,-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
W는 filter 이고, x 는 input_data이다. 입력 데이터와 필터를 적절하게 2차원으로 바꾸어주고, 내적한다. 코드가 순서대로의 과정이기 때문에, 크게 어려울 것은 없으나 transpose 부분은 한 번 볼 필요가 있다.
transpose와 함께 나오는 정수는 인덱스를 의미한다. 그리고 그 인덱스 순서로 변환해주는 transposing을 해준다.
합성곱 신경망 Convolutional neural network, CNN은 이미지 인식과 음성 인식 등 다양한 곳에서 사용된다. 특히 이미지 인식 분야에서 딥러닝을 활용한 기법은 거의 다 CNN을 기초로 한다.
7.1 전체 구조
CNN도 앞서 봤던 신경망들과 동일하게, 레고마냥 계층들을 조합해서 만들 수 있다. 다만 합성곱 계층 Convolutional layer와 풀링 계층 Pooling layer가 새롭게 등장한다.
지금까지의 신경망은 인접하는 모든 계층의 모든 뉴런과 결합되어 있었다. 이를 완전연결(fully-connected)라 하며, 완전히 연결된 계층을 Affine 계층이라는 이름으로 구현했다.
Affine 계층을 사용하면, 5개층 완전연결 신경망은 다음과 같이 구현 할 수 있다.
완전연결 신경망은 Affine 계층 뒤에 활성화 함수를 갖는 ReLU 계층 or Sigmoid 계층이 이어진다. 위 예시에서는 Affine-ReLU가 4개 쌓이고, 5번째 층에서 Softmax에 최종 확률을 출력한다.
CNN의 경우는 다음과 같이 구현한다.
CNN에서는 새롭게 합성곱 계층 (conv)과 풀링 계층(pooling)이 추가된다.
주목할 점은 출력에 가까운 층에서는 Affine-ReLU구성을 사용할 수 있고, 마지막 출력 계층에서는 Affine-Softmax 조합을 그대로 사용한다는 것이다.
7.2 합성곱 계층
CNN에서는 패딩(padding), 스트라이드(stride) 등 CNN 고유의 용어가 등장한다. 또, 각 계층 사이에 3차원 데이터같이 입체적인 데이터가 흐른다.
7.2.1 완전연결 계층의 문제점
'데이터의 형상이 무시'
ex) 이미지는 가로, 세로, 색상으로 구성된 3차원 데이터, MNIST에서는 (1, 28, 28)의 데이터를 평탄화해서 사용했음.
하지만 공간적으로 가까운 픽셀의 값이 비슷, RGB의 각 채널이 밀접하게 관련, 거리가 먼 픽셀끼리는 연관 없음 등의 3차원 속의 본질적인 패턴을 무시할 수 있음
합성곱 계층은 형상을 유지하기 때문에 3차원으로 받아서, 3차원 데이터로 전달한다. 그렇기에 CNN은 이미지처럼 형상을 가진 데이터를 제대로 이해할 가능성이 있다.
특징맵(feature map): 합성곱 계층의 입출력 데이터
입력 특징 맵(input feature map): 입력 데이터
출력 특징 맵(output feature map): 출력 데이터
7.2.2 합성곱 연산
합성곱 연산은 이미지 처리에서 말하는 필터 연산에 해당한다.
형상을 (height, width)로 표현하고, 필터를 커널이라고도 함
연산은 필터의 윈도우를 일정한 간격으로 이동해가며 입력 데이터에 적용하는데, 입력데이터와 필터에서 대응하는 원소끼리 곱한 후 총합을 구한다. (단일 곱셈-누산 fused multiply-add, FMA)
ex) 1*2+2*0+3*1+0*0+1*1+2*2+3*1+0*0+1*2=15
완전연결 신경망
CNN
가중치 매개변수
필터 매개변수
합성곱 연산의 편향: 필터를 적용한 원소에 편향을 더함
7.2.3 패딩
패딩: 합성곱 연산을 수행하기 전에 입력 데이터 주변을 특정 값(ex 0)으로 채우는 기법
패딩: 1, 패딩 값: 0
패딩은 주로 출력 크기를 조정할 목적으로 사용한다. 합성곱 연산을 몇 번이나 되풀이하게 되는데, 연산을 거칠 때마다 크기가 작아지면 어느 시점에는 크기가 1이 된다. 즉 합성곱 연산을 적용할 수가 없어지는 것이다. 이러한 사태를 막기 위해 패딩을 사용한다. 결론적으로 패딩은 입력 데이터의 공간적 크기를 고정한 채로 다음 계층에 전달하고자 사용한다.
7.2.4 스트라이드
스트라이드: 필터를 적용하는 위치의 간격
앞선 예시들은 스트라이드가 1이었다. 밑 그림은 스트라이드가 2인 예시이다.
한 칸이 아닌 두 칸을 이동한 것을 볼 수 있다.
스트라이드를 키우면 출력 크기가 작아진다. 한편, 패딩을 크게 하면 출력 크기가 커졌다. 이를 수식으로 옮기면 다음과 같다.
먼저 입력 크기를 (H,W), 필터 크기를 (FH, FW), 출력 크기를 (OH, OW), 패딩을 P, 스트라이드를 S 라고 한다.
7.2.5 3차원 데이터의 합성곱 연산
입력 데이터와 필터의 합성곱 연산을 '채널마다' 수행하고, 모두 합산한다.
**단 입력 데이터의 채널 수와 필터의 채널 수가 같아야 한다!
7.2.6 블록으로 생각하기
(channel, height, width) 순으로 쓰며, 약자로 C, H, W를 쓴다. 필터의 경우 F를 붙여, FH, FW로 사용한다(채널은 입력데이터와 동일해야 하므로 그냥 C).
자 그럼 하나의 데이터에 여러개의 필터를 적용한다면 어떤 출력을 얻게 될까? 다음과 같다.
이렇게 완성된 블록을 다음 계층으로 넘기겠다는 것이 CNN의 처리의 흐름이다.
합성곱 연산의 처리 흐름
##편향은 블록의 형상이 다른데, 이는 브로드캐스트 기능으로 쉽게 구현할 수 있다.
7.2.7 배치 처리
합성곱 연산에서도 입력 데이터를 한 덩어리로 묶어 배치로 처리할 수 있다. 그래서 각 계층을 흐르는 데이터의 차원을 하나 늘려 4차원 데이터로 저장한다. (데이터 수, 채널 수, 높이, 너비) 순
신경망에 4차원 데이터가 하나 흐를 때마다 데이터 N개에 대한 합성곱 연산이 이뤄진다. 즉 N회 분의 처리를 한 번에 수행한다.
신경망 모델이 복잡해지면 가중치 감소만으로 대응하기 어려워진다. 이때 드롭아웃 Dropout 이라는 기법을 이용한다.
드롭아웃은 뉴런을 임의로 삭제하면서 학습하는 방법이다. 훈련 때 은닉층의 뉴런을 무작위로 골라 삭제한다. 삭제된 뉴런은 신호를 전달하지 않게 된다. 훈련 때는 데이터를 흘릴 때마다 삭제할 뉴런을 무작위로 선택하고, 시험 때는 모든 뉴런에 신호를 전달한다. 단, 시험 때는 각 뉴런의 출력에 훈련 때 삭제한 비율을 곱하여 출력한다.
class Dropout:
def __init__(self, dropout_ratio=0.5):
self.dropout_ratio = dropout_ratio
self.mask = None
def forward(self, x, train_flg=True):
if train_flg:
self.mask = np.random.rand(*x.shape) > self.dropout_ratio
return x * self.mask
else:
return x * (1.0 - self.dropout_ratio)
def backward(self, dout):
return dout * self.mask
순전파:
train 중이라면, x와 동일한 모양의 bool type 행렬을 만들고, 곱해서 dropout_ratio 보다 큰 원소만 True로 설정한다.
역전파:
ReLU와 동일, 순전파 때 신호를 통과시키는 뉴런은 역전파 때도 신호를 그대로 통과시키고, 그렇지 않은 뉴런은 역전파때도 신호를 차단함
드롭아웃을 적용한 결과, 훈련 데이터와 시험 데이터에 대한 정확도 차이가 줄어들었으며, 훈련 데이터에 대한 정확도가 100%에 도달하지 않게 되었다. (train은 100%에 도달하였으나, test가 100%가 아니라면 사실 상 정확하지 않은 오버피팅 상태)
- 신경망이 훈련 데이터에만 지나치게 적응되어 그 외의 데이터에는 제대로 대응하지 못하는 상태
- 기계학습은 범용 성능을 지향하기 때문에, 아직 보지 못한 데이터가 주어져도 바르게 식별해내는 모델이 바람직함
6.4.1 오버피팅
오버피팅은 주로 두 경우에서 일어남
매개변수가 많고 표현력이 많은 모델
훈련데이터가 적음
다음은 일부러 두 요건을 충족하여 오버피팅을 일으켰을 때이다. 본래 60,000개인 MNIST 데이터넷의 훈련 데이터 중 100개 ,300개, 1000개만 사용하고, 7층 네트워크를 사용해 네트워크의 복잡성을 높였다. 각 층의 뉴런은 100개, 활성화 함수는 ReLU이다.
각각 앞에서부터 100개, 300개, 1000개이다. 데이터수가 적을수록 훈련 데이터와 시험 데이터의 간극이 커지는 것을 확인할 수 있다.
6.4.2 가중치 감소
오버피팅 억제용으로 가중치 감소(weight decay)라는 것이 있다.
학습 과정에서 큰 가중치에 대해서는 그에 상응하는 큰 페널티를 부과하여 오버피팅을 억제하는 방법이다. 원래 오버피팅은 가중치 매개변수의 값이 커서 발생하는 경우가 많기 때문이다.
신경망 학습은 손실 함수의 값을 줄이도록 되어있다. 그렇기 때문에 역으로 손실 함수가 커진다면 가중치를 줄일 수 있다.
x는 가중치, f는 손실함수
예를 들어 가중치의 제곱 norm (L2 norm)을 손실함수에 더한다. 가중치를 W라고 하면 L2 norm에 따른 가중치는
이 되고, 이를 손실 함수에 더한다. (λ는 정규화의 세기를 조절하는 하이퍼파라미터, λ를 크게 설정할 수록 큰 가중치에 대한 페널티가 커진다.)
def loss(self, x, t):
"""손실 함수를 구한다.
Parameters
----------
x : 입력 데이터
t : 정답 레이블
Returns
-------
손실 함수의 값
"""
y = self.predict(x)
weight_decay = 0
for idx in range(1, self.hidden_layer_num + 2):
W = self.params['W' + str(idx)]
weight_decay += 0.5 * self.weight_decay_lambda * np.sum(W ** 2)
return self.last_layer.forward(y, t) + weight_decay #손실 함수에 더함
가중치 감소는 모든 가중치 각각의 손실 함수에 위 값을 더한다. 따라서 가중치의 기울기를 구하는 계산에서는 그동안의 오차역전파법에 따른 결과의 정규화 항을 미분한 λW를 더한다.
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 = {}
for idx in range(1, self.hidden_layer_num+2):
#미분한 값을 더하는 부분
grads['W' + str(idx)] = self.layers['Affine' + str(idx)].dW + self.weight_decay_lambda * self.layers['Affine' + str(idx)].W
grads['b' + str(idx)] = self.layers['Affine' + str(idx)].db
return grads