7.1 전체구조
7.2 합성곱 계층
7.3 풀링 계층
7.4 합성곱/풀링 계층 구현하기
7.5 CNN 구현하기
7.6 CNN 시각화하기
7.7 대표적인 CNN
7.8 정리
7.1 전체구조
이전의 신경망과 같이 계층을 조합하여 만들 수 있다.
CNN에서는 새로운 합성곱 계층과 풀링 계층이 추가된다.
완전연결: 인접하는 계층의 모든 뉴런과 결합된 신경망 (완전 연결된 계층을 Affine 계층)
7.2 합성곱 계층
1) 완전연결 계층의 문제점
- 완전연결 계층에 입력할 때는 3차원 데이터를 평평한 1차원 데이터로 평탄화 = 데이터의 형상 무시
- 완전연결 계층은 형상을 무시하고 모든 이력 데이터를 동등한 뉴런(같은 차원의 뉴런)으로 취급해 형상에 담긴 정보를 살릴 수 없다.
하지만 합성곱 계층은 형상을 유지 (이미지처럼 형상을 가진 데이터를 제대로 이해할 (가능성이 있는) 것이다.)
CNN에서는 합성곱 게층의 입출력 데이터를 특징맵이라고 함. (입력 데이터는 입력특징 맵, 출력 데이터는 출력특징 맵)
2) 합성곱 연산 (이미지 처리에서 말하는 필터 연산에 해당)
- 합성곱 연산은 입력 데이터에 필터(커널)를 적용한다.
- 필터의 윈도우를 일정 간격 이동해가며 입력데이터에 적용
- 입력과 필터에 대응하는 원소끼리 곱한 후 그 총합을 구한다.
- 완전연결 신경망에는 가중치 매개변수와 편향이 존재하는데, CNN에서는 필터의 매개변수가 그동안의 가중치에 해당한다.
3) 패딩
합성곱 연산을 수행하기 전 입력 데이터 주변을 특정값(예컨대 0)으로 채우는 것
패딩은 출력크기를 조정할 목적으로 사용한다.
- 연산을 거칠 때 마다 출력의 형상은 계속해서 줄어들음
- 심층 신경망에서 어느 시점에서는 출력 크기가 1이 될 수 있음
- 이를 막기 위해 패딩 사용 -> 입력 데이터의 공간적 크기를 고정한 채로 다음 계층에 전달할 수 있다.
4) 스트라이드
필터를 적용하는 위치의 간격
이전에 본 건 스트라이드 1
스트라이드를 키우면 출력 크기가 작아진다. 패딩을 크게하면 출력 크기가 커진다.
이러한 관계를 수식화해 볼 수 있다.
- (4,4) 입력 데이터에 (3,3) 필터를 패딩0, 스트라이드1로 적용하면 OFM은 (2,2)가 된다.
5) 3차원 데이터의 합성곱 연산
길이 방향(채널 방향)으로 특징 맵이 늘어난다.
- 3차원 합성곱 연산에서 주의할 점은 입력 데이터의 채널 수와 필터의 채널 수가 같아야 한다는 것이다.
6) 블록으로 생각하기
- 3차원의 합성곱 연산은 데이터와 필터를 직육면체 블록이라고 생각하면 쉽다.
- 3차원 데이터를 다차원 배열로 나타낼 때는 (채널, 높이, 너비) 순서로 쓴다. (C, H, W), 필터도 (C, FH, FW)
필터를 FN개 적용하면 출력 맵도 FN개가 생성된다.
FN개의 맵을 모으면 형상이 (FN, OH, OW)인 블록이 완성된다. 이 완성된 블록을 다음 계층으로 넘기겠다는 것이 CNN의 처리 흐름이다.
편향은 채널 하나에 값 하나씩 (FN,1,1), (FN, OH, OW) 블록의 대응 채널의 원소 모두에 더해진다.
7) 배치 처리
합성곱 연산도 배치처리를 지원한다.
각 계층에 흐르는 데이터의 차원을 하나 늘려 4차원 데이터로 저장한다. = (데이터 수, 채널 수, 높이, 너비) 순으로 저장
각 데이터의 선두에 배치용 자원을 추가했다.
주의할 점은 신경망에 4차원 데이터가 하나 흐를 때마다 데이터 N개에 대한 합성곱 연산이 이뤄진다.= 즉 N회 분의 처리를 한 번에 수행
7.3 풀링 계층
풀링은 가로, 세로 방향의 공간을 줄이는 연산
- 위 그림은 2x2 최대 풀링을 스트라이드 2로 처리하는 순서이다.
- 최대 풀링: 최댓값을 구하는 연산, (2x2 대상 영역에 대하여 가장 큰 원소를 꺼냄)
- 즉 2x2 최대 풀링은 그림과 같이 2x2 크기의 영역에서 가장 큰 원소 하나를 꺼낸다.
- (풀링의 윈도우 크기와 스트라이드는 같은 값으로 설정하는 것이 보통)
- 평균 풀링 : 대상 영역에서 평균을 계산, 이미지 인식 분야에서는 주로 최대 풀링을 사용함.
1) 풀링 계층의 특징
- 학습해야 할 매개변수가 없다
- 풀링은 대상 영역에서 최댓값이나 평균을 취하는 명확한 처리이므로 특별히 학습할 것이 없다.
- 채널 수가 변하지 않는다.
- 풀링 연산은 입력 데이터의 채널 수 그대로 출력 데이터로
- 입력의 변화에 영향을 적게 받는다. (강건하다)
- 입력 데이터가 조금 변해도 풀링의 결과는 잘 변하지 않는다.
7.4 합성곱/풀링 계층 구현하기
1) 4차원 배열
x = np.random.rand(10, 1, 28, 28) # 무작위로 데이터 생성
x.shape
>>> (10, 1, 28, 28)
데이터 인덱스를 통한 접근
print(x[0].shape)
print(x[1].shape)
>>>
(1, 28, 28)
(1, 28, 28)
2) im2col로 데이터 전개하기
합성곱 연산을 곧이곧대로 구현하려면 for문을 겹겹이 써야한다.
for문 대신 im2col이라는 편의 함수를 이용해 간단히 구현이 가능하다.
- im2col는 입력 데이터를 필터링하기 좋게 전개하는(펼치는) 함수
- 3차원 데이터에 im2col 적용하면 2차원 행렬로 바뀜
im2col은 필터링하기 좋게 입력 데이터를 전개한다.
- 실제 상황에서는 필터 적용 영역이 겹치는 경우가 대부분
- 필터 적용 영역이 겹치게 되면 im2col로 전개한 후의 원소 수가 원래보다 많아짐 (메모리 더 많이 소비한다는 단점)
- 그러나 컴퓨터는 큰 행렬을 만들어 계산하는 데 탁월해 효율 높일 수 있음
- im2col로 입력 데이터 전개 후 합성곱계층의 필터(가중치)를 1열로 전개하고 두 행렬곱 계산
- 결과도 2차원 행렬이니 출력 데이터를 4차원으로 변형(reshape)한다.
3) 합성곱 계층 구현하기
- input_data : 4차원 배열 형태의 입력 데이터(이미지 수, 채널 수, 높이, 너비)
- filter_h : 필터의 높이
- filter_w : 필터의 너비
- stride : 스트라이드
- pad : 패딩
x1 = np.random.rand(1, 3, 7, 7) # 7x7에 채널 3
col1 = im2col(x1, 5, 5, stride=1, pad=0)
print(col1.shape)
>>> (9, 75) # 2번째 차원의 원소수는 75로 필터의 원소수와 같다(3x5x5)
x2 = np.random.rand(10, 3, 7, 7) # 배치 크기 10
col2 = im2col(x2, 5, 5, stride=1, pad=0)
print(col2.shape)
>>> (90, 75)
첫 번째는 배치 크기가 1(데이터 1개), 채널은 3개, (높이, 너비)가 7x7의 데이터.
두 번째는 배치 크기만 10이고 나머지는 첫 번째와 같다.
im2col 함수를 적용한 두 경우 모두 2번째 차원의 원소는 75개이다. (채널 3개, 5x5 데이터)
또한 배치 크기가 1일 때는 im2col의 결과의 크기가 (9,75)이고 10일 때는 그 10배인 (90,75) 크기의 데이터가 저장.
im2col을 사용해 합성곱 계층 구현
class Convoultion:
def __init__(self, W, b, stride=1, pad=0):
self.W = W
self.b = b
self.stride = stride
self.pad = pad
def forware(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)
# IFM랑 Filter랑 계산 할 수 있게 각자 전개하고 dot으로 계산함
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).tanspose(0, 3, 1, 2)
return out
- eshape 두 번째 인수 -1로 지정하면 다차원 배열의 원소 수가 변환 후에도 똑같이 유지되도록 묶어준다.
- transpose함수를 이용해 출력데이터를 적절한 형상으로 바꾸어 준다.
- 인덱스를 지정하여 축의 순서 변경
- 역전파에서는 im2col 대신 col2im 함수 사용
4) 풀링 계층 구현하기
풀링의 경우에는 채널이 독립적이라는 점이 합성곱 계층과 다른 점이다.
전개 후 최댓값 구하고 적절한 형상으로 바꾸어준다.
# 풀링 계층 구현
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, self.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
풀링 계층 구현의 세 단계
- 입력데이터 전개
- 행별 최댓값을 구한다.
- 적절한 모양으로 성형
7.5 CNN 구현하기
초기화(__ init __)때 받는 인수
- input_dim : 입력 데이터(채널 수, 높이, 너비)의 차원
- conv_param : 합성곱 계층의 하이퍼파라미터, 딕셔너리의 키는 다음과 같음
- filter_num : 필터 수
- filter_size : 필터 크기
- stride : 스트라이드
- pad : 패딩
- hidden_size : 은닉층(완전연결)의 뉴런 수
- output_size : 출력층(완전연결)의 뉴런 수
- weight_init_std : 초기화 때의 가중치 표준편차
합성곱계층의 하이퍼파라미터는 딕셔너리 형태로 주어진다.
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 딕셔너리에 저장한다.
마지막으로 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에 계층들을 추가한다.
초기화 한 이후에는 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)
predict 메서드는 초기화 때 layers에 추가한 계층을 맨 앞에서부터 차례로 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라는 딕셔너리 변수에 각 가중치 매개변수의 기울기를 저장한다.
7.6 CNN 시각화하기
1) 1번째 층의 가중치 시각화하기
MNIST 데이터 셋으로 CNN 학습을 해보면 1번째 층의 합성곱 계층의 가중치는 그 형상이 (30, 1, 5, 5)이다.
필터의 크기가 5x5이고 채널이 1개라는 것은 이 필터를 1채널의 회색조 이미지로 시각화할 수 있다는 뜻이다.
학습 전 필터는 무작위로 초기화되고 있기 때문에 흑백의 정도에 규칙성이 없지만 학습 후에는 규칙성 있는 이미지가 되었다. 이렇게 규칙성 있는 필터는 에지(색상이 바뀐 경계선)과 블롭(국소적으로 덩어리진 영역)을 보고 있다.
2) 층 깊이에 따른 추출 정보 변화
계층이 깊어질 수록 추출되는 정보는 더 추상화 된다.
딥러닝의 흥미로운 점은 합성곱 계층을 여러 겹 쌓으면 층이 깊어지면서 더 복잡하고 추상화된 정보가 추출된다는 점이다. 처음 층은 단순한 에지에 반응하고, 이어서 텍스처에 반응하고, 더 복잡한 사물의 일부에 반응하도록 변화한다.
층이 깊어지면서 뉴런이 반응하는 대상이 단순한 모양에서 고급 정보로 변화해간다.
다시말해 사물의 의미를 이해하도록 변화하는 것이다.
7.7 대표적인 CNN
1) LeNet
손글씨 숫자를 인식하는 네트워크이다. 합성곱 계층과 풀링 계층을 반복하고 마지막으로 완전연결 계층을 거치면서 결과를 출력한다.
- LeNet과 현재 CNN 비교
- 활성화 함수 : LeNet(sigmoid) / 현재(ReLU)
- 데이터 크기 줄이기 : LeNet(서브샘플링, 중간데이터 크기 줄임) / 현재(MaxPooling)
2) AlexNet
AlexNet는 합성곱 계층과 풀링 계층을 거듭하며 마지막으로 완전연결 계층을 거쳐 결과를 출력한다.
- LeNet과 비교해 바뀐 점
- 활성화 함수로 ReLU 사용
- LRN이라는 국소적 정규화 실시하는 계층 이용
- 드롭아웃 사용
- GPU계산 위해 병렬적인 구조로 설계
7.8 정리
- CNN은 지금까지의 완전연결 계층 네트워크에 합성곱 계층과 풀링 계층을 새로 추가한다.
- 합성곱 계층과 풀링 계층은 im2col(이미지를 행렬로 전개하는 함수)을 이용하면 간단하고 효율적으로 구현할 수 있다.
- CNN을 시각화해보면 계층이 깊어질수록 고급정보가 추출되는 모습을 확인할 수 있다.
- 대표적인 CNN에는 LeNet과 AlexNet가 있다.
- 딥러닝의 발전에는 빅데이터와 GPU가 크게 기여했다.