완전연결층
CNN이전의 구조 - 완전연결층
* 우리가 지금까지 살펴본 network는 완전연결층(Fully-connected layer)으로 이뤄져있었다. 완전 연결층은 말 그대로 이전 층의 뉴런들과 다음층의 뉴런들이 하나도 빠짐없이 서로 연결되어있는 구조를 띠는 층을 뜻한다. 조금 생소할수도 있으니 다음 자료를 통해 이해해보자.
* 일반적인 DNN구조이다. 잘 보면 이전 층의 뉴런들과 다음 층의 뉴런들이 모두 각각 연결되어있는 것을 확인할 수 있다.
* 그리고 이 완전연결층을 구현할 때 우리는 지금까지 Affine layer와 ReLU layer를 사용하였다. 이렇게 말이다.
* 앞서 mnist dataset을 통해 이미지를 분류하는 과정을 생각해보자. 이미지가 network로 입력될 때 flatten되어서 입력된다. 이 때문에 2차원 이미지(흑백) 혹은 3차원 이미지(컬러)가 network에 입력될 때 공간적인 정보들을 모두 잃게 된다. 이미지데이터는 데이터의 특성상 공간적인 정보가 많은 것을 담고 있는 경우가 많다. 가까운 픽셀끼리의 관계, RGB채널의 동일 위치 값들간의 관계 등 이미지 데이터의 원본 형식이 담고있는 정보가 적지 않다. 이 이미지 픽셀들을 1차원 벡터로 만드는 과정에서 공간적인 정보는 모두 소실되는 것이다.
* 이러한 이유 때문에 완전연결층을 사용하는 DNN은 이미지 처리에는 효율적이지 않다. 그렇다면 이미지는 어떤 network를 통해 처리 가능할까?
CNN의 구조
* CNN에서는 새롭게 Convolution layer, Pooling layer가 추가된다. CNN의 layer는 Conv-ReLU-Pooling의 흐름으로 연결된다. 이 때 Pooling은 생략되기도 한다.
* 이러한 구조는 완전연결층과는 달리 이미지를 원본 형식 그대로 처리하고 다음 layer에 넘겨주기 때문에 공간적인 정보가 소실될 위험이 없다.
합성곱 연산(Convolution operation)
* Convolution layer에서의 Convolution operation은 이미지 처리에서 말하는 필터 연산과 같다. 필터는 참고로 kernel이라고 부르기도 한다.
* 입력 데이터에 필터를 통한 연산을 시행하는 과정은 아래와 같다.
* 필터에 대응하는 원소와 필터의 원소끼리 곱하여 총합을 계산하는 방식을 단일 곱셈-누산(Fused Multiply-Add, FMA) 라고 한다.
* 완전연결신경망에서 매개변수는 가중치와 편향으로 이뤄져있었다. CNN에서의 가중치는 필터의 값들이 가중치가 되고, 편향은 1x1의 단일 값으로 FMA 이후 출력된 feature map의 모든 원소에 더해진다.
패딩(padding)
* 패딩은 데이터 주위에 특정 width의 특정 값(예를 들어 zero padding)을 채워 출력 feature map의 크기를 조절하는 방식이다. 만약 정말 deep한 CNN이라면, layer를 거칠수록 이미지의 크기가 작아져서 결국엔 1이 될 것이다. 이렇게 되면 더 이상 Convolution 연산을 진행할 수 없다. 패딩은 이를 방지하기 위해 사용한다. 만약 이미지에 width가 1인 패딩을 적용하면 출력 feature map의 크기는 입력 feature map의 크기와 동일해진다.
스트라이드(Stride)
* stride는 필터를 적용하는 위치의 간격을 뜻한다. 지금까지는 stride가 1인 경우만 살펴봤지만, stride는 사용자가 임의로 조정할 수 있는 parameter이다.
* stride가 커질수록 출력 feature map의 크기는 작아진다. stride에 따른 feature map의 크기를 수식으로 나타내면 아래와 같다.
* 입력 크기를 (H, W), 필터 크기를 (FH, FW), 출력 크기를 (OH, OW), 패딩을 P, 스트라이드를 S라고 하면,
$OH = {H + 2P - FH \over S } + 1$
$OW = {W + 2P - FW \over S } + 1$
* 주의할 점은 ${H + 2P - FH \over S }$, ${W + 2P - FW \over S }$ 가 정수로 나눠떨어지는 값이어야 한다는 점이다.
3차원 데이터의 합성곱 연산
* 3차원 데이터의 합성곱 연산은 2차원 데이터의 합성곱 연산과 크게 다르지 않다. 주의할 점은 필터의 개수와 입력 데이터의 마지막 차원, 즉 채널의 수가 같아야 한다는 점이다.
블록으로 생각하기
* 3차원 합성곱 연산은 데이터와 필터를 직육면체 블록이라고 생각하면 이해하기 쉬워진다.
* 블록의 크기는 (Channel, Height, Width) 순서로 나타낸다.
* 이 예에서 출력 데이터는 한 장의 feature map이다. 이는 channel이 1 개인 feature map이라는 것인데, 만약 여러 장의 feature map을 추출하고 싶다면 다음과 같은 과정을 거치면 된다.
* 필터를 FN개를 사용하면, 출력되는 feature map도 FN개가 된다.
* 또한 편향은 feature map 개수만큼의 단일값들로 이뤄져있기 때문에 크기가 (FN, 1, 1)이 된다.
배치 처리
* CNN에서도 배치 처리가 가능하다. 위와 같이 (C, H, W) 크기의 데이터 N개를 한 번에 계산하는 것이 가능하다.
풀링 계층(Pooling layer)
* pooling layer는 세로, 가로 방향의 공간을 줄이는 연산을 뜻한다.
* pooling의 종류는 윈도우에 속한 원소 중 가장 큰 값만 추출하는 max pooling, 윈도우의 원소들의 평균을 추출하는 mean pooling 등이 있지만, 이미지 인식 분야에서는 주로 max pooling을 사용한다. 앞으로도 pooling이라 하면 max pooling으로 생각하면 될 것이다.
* pooling의 stride는 보통 window size와 동일하게 설정하는 것이 일반적이다(예를 들어 2x2 window면 stride 또한 2).
Pooling layer의 특징
1) 학습해야 할 매개변수가 없다.
* 대표적으로 max pooling을 예로 들면 가장 큰 값 만을 추출하면 되기 때문에 학습할 매개변수가 존재하지 않는다.
2) 채널 수가 변하지 않는다.
* pooilng layer의 output이 되는 feature map의 개수는 동일하다. channel마다 독립적으로 계산하기 때문이다.
3) 입력의 변화에 영향을 적게 받는다(강건하다)
* 위와 같이 입력 데이터가 1 칸씩 밀려도 결과는 달라지지 않는 것을 볼 수 있다. 물론 모든 경우에 출력이 동일하지는 않는다.
합성곱/풀링 계층 구현하기
4차원 배열
X = np.random.rand(10, 1, 28, 28)
X.shape
# (10, 1, 28, 28)
* 먼저 데이터로 사용할 4차원 이미지 데이터를 생성하였다. 배치처리를 위해 3차원 데이터가 아닌 4차원 데이터를 생성하였다.
im2col로 데이터 전개하기
* CNN의 연산 과정을 구현하기 위해서는 for 문을 사용해야 하는데, numpy에서 for문을 사용하는 것은 성능 면에서 좋지 못하다. 이를 대체하기 위해서 우리는 im2col 함수를 사용한다.
* 3차원 입력 데이터에 im2col을 적용하면 2차원 행렬로 바뀐다(정확히는 배치 안의 데이터수까지 포함한 4차원 데이터를 2차원 데이터로 변환한다). im2col은 필터링하기 좋게 입력 데이터를 전개한다.
* 위 예시 자료에서는 stride를 크게 잡아서 일부러 필터의 적용 영역이 겹치지 않게 했지만, 실제로는 겹치는 경우가 대부분이다. 만약 영역이 겹치면 출력한 후 원소 개수가 원래 블록의 원소 수보다 많아진다. 이는 메모리 소비라는 단점이 있지만, 행렬 계산이 for문 등의 계산을 사용하는 것 보다 효율적이기 때문에 이 방법을 사용한다.
* 조금 더 im2col함수를 살펴보면 만약 원본 데이터가 7x7xC 크기라고 할 때, 먼저 filter size (5,5,3) 대로 가로 3번, 세로 3번 총 9개로 나뉜다. 각 나뉜 구간의 데이터의 크기는 filter size와 같기 때문에 (5, 5, C)가 되고, 데이터를 모두 전개하면 (9, 5 x 5 x C) 크기의 2차원 행렬이 도출된다.
* im2col을 통해 데이터를 2차원으로 변환한 다음 filter 또한 각 필터를 1개의 열로 하는 2차원 행렬로 변환하여 계산을 진행한다.
* 마지막에 출력된 데이터를 reshape해주면 출력 데이터가 완성된다.
합성곱계층 구현하기
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)
# (9, 75)
* im2col 함수를 정의한 후 x1에 적용을 해봤다. filter size를 (5, 5)로 설정하였기 때문에 2번째 차원 원소의 수가 3x5x5 = 75인 것을 확인할 수 있다.
* 이제 이 im2col을 사용하여 합성곱 층을 구현해보자.
class Convolution:
def __init__(self, W, b, stride=1, pad=0):
self.W = W # filter
self.b = b
self.stride = stride
self.pad = pad
def forward(self, x):
FN, C, FH, FW = self.W.shape #filter shape
N, C, H, W = x.shape
out_h = int(1+(H+2*self.pad-FH)/self.stride) #output의 height
out_w = int(1+(W+2*self.pad-FW)/self.stride) #output의 width
# 전개
col = im2col(x, FH, FW, self.stride, self.pad)
col_W = self.W.reshape(FN, -1).T # filter to column
# 계산
out = np.dot(col, col_W) + self.b
# reshape
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2) #축 순서 변경하기(N, H, W, C) to (N, C, H, W)
return out
* 이렇게 im2col함수를 사용해 마치 이미지를 flatten해서 처리하는 DNN에서의 Affine 계층과 거의 똑같이 구현할 수 있었다.
* 합성곱의 역전파는 결국 계산 과정은 Affine 계층과 유사하기 때문에 다루지 않지만, 하나 주의할 점은 아래 col2lim함수를 통해 im2col을 역으로 처리해야 한다는 점이다.
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]
Pooling계층 구현하기
* pooling계층 구현 또한 합성곱 계층과 마찬가지로 im2col을 사용해 입력 데이터를 전개하지만, 채널마다 독립적인 pooling을 해준다는 점이 이라는 점이 합성곱 계층 때와 다르다. 위 그림은 pooling계층의 forward 처리 과정이다.
* pooling 계층의 forward과정을 python code로 구현하면 아래와 같다.
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)
out=np.max(col, axis=1) #행별 최댓값
out=out.reshape(n, out_h, out_w, c).transpose(0, 3, 1, 2)
return out
CNN 구현하기
* convolution 계층과 pooling 계층을 구현했으니, 이를 조합해 손글씨 숫자를 인식하는 CNN을 조립해보자.
* CNN 네트워크는 Convolution-ReLU-Pooling-Affine-ReLU-Affine-Softmax 순으로 흐른다.
import sys
import os
sys.path.append(os.pardir) # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
import matplotlib.pyplot as plt
from collections import OrderedDict
from common.layers import *
from common.gradient import numerical_gradient
from dataset.mnist import load_mnist
from common.trainer import Trainer
class SimpleConvNet:
"""
다음과 같은 CNN을 구성한다.
→ Conv → ReLU → Pooling → Affine → ReLU → Affine → Softmax →
전체 구현은 simple_convnet.py 참고
"""
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))
# 1층의 합성곱 계층과 2, 3층의 완전연결 계층의 가중치와 편향 생성
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)
# 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()
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)
def accuracy(self, x, t, batch_size=100):
if t.ndim != 1:
t = np.argmax(t, axis=1)
acc = 0.0
for i in range(int(x.shape[0] / batch_size)):
tx = x[i*batch_size:(i+1)*batch_size]
tt = t[i*batch_size:(i+1)*batch_size]
y = self.predict(tx)
y = np.argmax(y, axis=1)
acc += np.sum(y == tt)
return acc / x.shape[0]
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
CNN 시각화하기
1번째 층의 가중치 시각화하기
* CNN의 가중치 갱신 과정을 이해하기 위해 가중치를 시각화 해보자.
* 방금 구현한 합성곱 계층의 필터(가중치)를 이미지로 시각화해보면 다음과 같다.
* 학습 전의 무작위적인 패턴에서 학습 후 어느정도의 규칙성있는 패턴으로 변화한 것을 확인할 수 있다.
* 흰색에서 검은색으로 점차 변화하는 필터와 덩어리가 진 필터 등, 규칙을 띄는 필터로 바뀌었다.
* 오른쪽에 규칙성 있는 필터는 edge와 blob을 보고있는 것이라고 할 수 있다.
* filter의 모양에 따라 다르지만 대표적으로 세로 edge를 detect하는 filter 1과 가로 edge를 detect하는 filter2를 보자. filter1의 왼쪽 부분에 세로로 길게 이어진 흰색 픽셀이 보이는 것을 알 수 있는데, 이 때문에 filter1과 곱해진 원본 이미지는 세로 edge에 흰 픽셀이 많이 나타나게 된다. 반대로 filter 2의 출력본에서는 가로 edge에 흰 픽셀이 많이 나타나는 것을 확인할 수 있다.
* 첫 번째 convolution layer(저수준 layer)를 시각화한 결과이기 때문에 매우 원시적인 수준의 정보를 추출하고 있는 것을 볼 수 있다.
층 깊이에 따른 추출 정보 변화
* 앞서 저수준의 layer에서 어떤 정보를 추출하는지 시각화를 해 보았다. 그렇다면 깊이가 깊어질수록 어떤 정보를 추출하게 될까? 위 네트워크는 AlexNet이고 밑에 있는 그림들은 각 layer에서 어떤 feature map을 추출하는지를 시각화해본 결과이다.
* 네트워크는 층이 깊어질수록 더 추상화되고 복잡한 feature map을 추출하면서 패턴을 파악하는 것으로 보인다.
대표적인 CNN
LeNet
* LeNet은 1998년에 등장한 손글씨 인식을 위한 네트워크이다.
* 최근 CNN에서는 activation function으로 ReLU를 많이 사용하는 반면, LeNet에서는 sigmoid를 사용한다.
* max pooling layer가 아닌 sub sampling layer를 사용한다.
AlexNet
* 2012년에 발표된 네트워크이다. 활성화 함수로 ReLU를 사용한다.
* LRN(Local Response Normalization)이라는 국소적 정규화를 실시하는 계층을 이용한다.
* 드롭아웃을 사용한다.
'Deep Learning > 밑바닥부터 시작하는 딥러닝' 카테고리의 다른 글
Word2Vec (0) | 2022.02.03 |
---|---|
딥러닝 등장 이전의 자연어와 단어의 분산 표현 (0) | 2022.02.01 |
Optimizer (0) | 2022.01.27 |
Backpropagation (0) | 2022.01.27 |
경사하강법(Gradient Descent method) (0) | 2022.01.25 |