이번장의 주제는 합성곱 신경망(convolutional neural network, CNN)이다.
전체 구조
CNN도 지금까지 본 신경망과 같이 레고 블록처럼 계층을 조합하여 만들 수 있다.
다만, 합성곱 계층(convolution layer)과 풀링 계층(pooling layer)이 새롭게 등장한다.
지금까지 본 신경망은 인접하는 계층의 모든 뉴런과 결합되어 있다. 이를 완전연결(fully-connected,전결합)이라고 하며, 완전히 연결된 계층을 Affine계층이라는 이름으로 구현했다.
Affine계층을 사용하면, 가령 층이 5개인 완전연결 신경망은 아래 그림과 같이 구현할 수 있다.

그림과 같이 완전연결 신경망은 Affine 계층 뒤에 활성화 함수를 갖는 ReLU계층(혹은 Sigmoid계층)이 이어진다.
Affine-ReLU 조합이 4개가 쌓였고, 마지막 5번째 계층은 Affine계층에 이어 Softmax계층에서 최종 결과(확률)을 출력한다.
그렇다면 CNN의 구조는 어떻게 다를까?

그림과 같이 CNN에서는 합성곱 계층(Conv)과 풀링 계층(Pooling)이 추가된다.
CNN에서 주목할 또 다른 점은 출력에 가까운 층에서는 지금까지의 'Affine-ReLU'구성을 사용할 수 있다는 것이다.
마지막 출력 계층에서는 'Affine-Softmax' 조합을 그대로 사용한다.
이상은 일반적인 CNN에서 흔히 볼 수 있는 구성이다.
합성곱 계층
CNN에서는 패딩(padding), 스트라이드(stride)등 CNN 고유의 용어가 등장한다.
각 계층 사이에는 3차원 데이터같이 입체적인 데이터가 흐른다는 점에서 완전연결 신경망과 다르다.
완전연결 계층의 문제점
지금까지 본 완전연결 신경망에서는 완전연결 계층(Affine계층)을 사용했다.
완전연결 계층에서는 인접하는 계층의 뉴런이 모두 연결되고 출력의 수는 임의로 정할 수 있습니다.
완전연결 계층의 문제점은 무엇일까? '데이터의 형상이 무시'된다는 사실이다.
이미지가 데이터인 경우 세로,가로,채널(색상)으로 구성된 3차원 데이터이다. MNIST 데이터셋을 사용한 사례에서는 형상이 (1,28,28)인 이미지를 1줄로 세운 784개의 데이터를 Affine계층에 입력했다.
합성곱 계층은 형상을 유지한다. 이미지도 3차원 데이터로 입력받고, 다음 계층에도 3차원 데이터로 전달한다.
CNN에서는 합성곱 계층의 입출력 데이터를 특징 맵(feature map)이라고도 한다. 합성곱 계층의 입력 데이터를 입력 특징 맵(input feature map), 출력 데이터를 출력 특징 맵(output feature amp)이라고 하는 식이다.
합성곱 연산
합성곱 연산은 이미지 처리에서 말하는 필터 연산에 해당된다.

그림과 같이 합성곱 연산은 입력 데이터에 필터를 적용한다.
데이터와 필터의 형상을 (height, width)로 표기하며, 입력언 (4,4), 필터는 (3,3), 출력(2,2)가 된다.
문헌에 따라 필터를 커널이라 칭하기도 한다.
합성곱 연산은 필터의 Window를 일정 간격으로 이동해가며 입력 데이터에 적용한다.
아래 그림에서 보듯 입력과 필터에 대응하는 원소끼리 곱한 후 그 총합을 구한다(이 계산을 단일 곱셈-누산 fused multiply-add,FMA라 한다.) 그리고 그 결과를 해당 장소에 저장한다.
이 과정을 모든 장소에서 수행하면 합성곱 연산의 출력이 완성된다.

완전연결 신경망에서는 가중치 매개변수와 편향이 존재하는데, CNN에서는 필터의 매개변수가 그동안의 '가중치'에 해당한다. CNN에도 편향이 존재한다.
위 그림은 필터를 적용하는 단계까지만 진행한 것이고, 편향까지 포함한다면 다음과 같은 흐름이 된다.

편향은 항상 하나(1X1)만 존재한다. 하나의 값을 필터를 적용한 모든 원소에 더하는 것이다.
패딩(Padding)
합성곱 연산을 수행하기 전에 입력 데이터 주변을 특정값(0)으로 채우기도 한다. 이를 패딩(padding)이라고 하며, 합성곱 연산에서 자주 이용하는 기법이다.
다음 그림은 (4,4)크기의 입력 데이터에 폭이 1인 패딩을 적용한 모습이다.

스트라이드(Stride)
필터를 적용하는 위치의 간격을 스트라이드(stride)라고 한다.
지금까지 예는 스트라이드가 1이였지만, 스트라이드를 2로하면 윈도우가 두칸씩 이동한다.

이처럼 스트라이드를 키우면 출력 크기는 작아진다. 반편 패딩을 크게 하면 출력 크기가 커진다.
입력 크기를 (H,W), 필터 크기를 (FH,FW), 출력 크기를 (OH,OW), 패딩을 P, 스트라이드를 S라 하면 수식화하면 다음과 같다.

이 값은 각각 정수로 나눠떨어지는 값이여야 한다는 점에 주의하자.
딥러닝 프레임워크중에는 값이 딱 나눠떨어지지 않을 때는 가장 가까운 정수로 반올림하는 등, 에러를 내지 않고 진행하도록 구현하는 경우도 있다.
3차원 데이터의 합성곱 연산
3차원 데이터의 합성곱 연산은 2차원일때와 비교하면, 길이 방향(채널 방향)으로 특징 맵이 늘어났다.
채널 쪽으로 특징 맵이 여러개 있다면 입력 데이터와 필터의 합성곱 연산을 채널마다 수행하고, 그 결과를 더해서 하나의 출력을 얻는다.


여기서 주의할 점은 입력 데이터의 채널 수와 필터의 채널 수가 같아야 한다.
이 예에서는 모두 3개로 일치한다.
필터 자체의 크기는 원하는 값으로 설정할 수 있습니다.(단, 모든 채널의 필터가 같은 크기여야 한다.)
이 예에서는 필터의 크기가 (3,3)이지만 원한다면 (2,2), (5,5)등으로 설정해도 된다.
블록으로 생각하기
3차원 합성곱의 연산은 데이터와 필터를 직육면체 블록이라고 생각하면 쉽다.
채널수 C, 높이 H, 너비 W인 데이터의 형상은 (C,H,W)로 쓴다. 필터도 (C,FH,FW)로 쓴다.

필터를 FN개 적용하면 출력 맵도 FN개가 생성된다.
그리고 그 FN개의 맵을 모으면 형상이 (FN,OH,OW)인 블록이 완성된다.
이 완성된 블록을 다음 계층으로 넘기겠다는 것이 CNN의 처리 흐름이다.
합성곱 연산에서는 필터의 수도 고려해야 한다.
그런 이유로 필터의 가중치 데이터는 4차원 데이터이며 (출력 채널 수, 입력 채널 수, 높이, 너비) 순으로 쓴다.
이것에 편향을 더하면 다음과 같다.

그림에서 보듯 편향은 채널 하나에 값 하나씩으로 구성된다.
편향의 형상은 (FN,1,1)이고, 출력 결과의 형상은 (FN,OH,OW)이다.
형상이 다른 블록의 덧셈은 numpy의 브로드캐스트 기능으로 쉽게 구현할 수 있다.
배치 처리
합성곱 연산에서도 배치 처리가 가능하다.
데이터가 N개일때 배치처리한다면 아래 그림처럼 된다.

그림을 보면 각 데이터의 선두에 배치용 차원을 추가했다.
데이터는 4차원 형상을 가진 채 각 계층을 타고 흐른다.
주의할 점으로는 신경망에 4차원 데이터가 하나 흐를 때마다 데이터 N개에 대한 합성곱 연산이 이뤄진다.
즉, N회 분의 처리를 한꺼번에 수행하는 것이다.
풀링 계층
풀링은 세로,가로 방향의 공간을 줄이는 연산이다. 아래 그림과 같이 2X2영역을 원소 하나로 집약하여 공간 크기를 구한다.

위의 경우는 2X2 최대 풀링(max poioling)을 스트라이드 2로 처리하는 순서이다.
참고로, 풀링의 윈도우 크기와 스트라이드는 같은 값으로 설정하는 것이 보통이다.
풀링은 최대 풀링 외에도 평균 풀링(average pooling) 등이 있다.
풀링 계층의 특징
- 학습해야 할 매개변수가 없다
- 채널 수가 변하지 않는다.

- 입력의 변화에 영향을 저게 받는다.

합성곱/풀링 계층 구현하기
합성곱 계층과 풀링 계층은 복잡해 보이지만, 트릭을 이용하면 쉽게 구현할 수 있다.
4차원 배열
CNN에서 계층 사이를 흐르는 데이터는 4차원이다.
데이터의 형상이 (10,1,28,28)이라면 이는 높이 28, 너비 28, 채널 1개인 데이터가 10개란 소리이다.
코드로 구현하면 다음과 같다.
import numpy as np
x = np.random.rand(10,1,28,28) #무작위 데이터 생성
x.shape # (10,1,28,28)
#첫번째, 두번째 데이터에 접근할 때
x[0].shape # (1,28,28)
x[1].shape # (1,28,28)
#첫번째 데이터의 첫 채널의 공간 데이터에 접근할 때
x[0,0] # or x[0][0]
다음 절에서 설명하는 im2col이라는 트릭이 합성곱 연산의 구현문제를 단순하게 만들어준다.
im2col로 데이터 전개하기
합성곱 연산을 곧이곧대로 구현하려면 for문을 겹겹이 써야한다.
numpy에서는 원소에 접근할 때 for문을 사용하지 않는 것이 바람직하다.
im2col은 입력 데이터를 필터링(가중치 계산)하기 좋게 전개하는 함수이다.
다음 그림과 같이 3차원 입력 데이터에 im2col을 적용하면 2차원 행렬로 바뀐다.(정확히는 배치 안의 데이터 수까지 포함한 4차원 데이터를 2차원으로 변환한다.)

im2col은 필터링하기 좋게 입력 데이터를 전개한다.
구체적으로는 아래 그림과 같이 입력 데이터에서 필터를 적용하는 영역(3차원 블럭)을 한줄로 늘어놓는다.
이 전개를 필터를 적용하는 모든 영역에서 수행하는게 im2col이다.

위 그림은 보기 좋게 스트라이드를 크게 잡아 필터의 적용 영역이 겹치지 않도록 했지만, 실제 상황에서는 영역이 겹치는 경우가 대부분이다.
필터 적용 영역이 겹치게 되면 im2col로 전개한 후의 원소 수가 원래 블록의 원소 수보다 많아진다.
그래서 im2col을 사용해 구현하면 메모리를 더 많이 소비하는 단점이 있다.
하지만 컴퓨터는 큰 행렬을 묶어서 계산하는데 탁월하다.
그래서 문제를 행렬 계산으로 만들면 선형 대수 라이브러리를 활용해 효율을 높일 수 있다.
im2col은 'image to column', 즉 '이미지에서 행렬로'라는 뜻이다.
im2col로 입력 데이터를 전개한 다음에는 합성곱 계층의 필터(가중치)를 1열로 전개하고, 두 행렬의 곱을 계산하면 된다.
이는 완전연결 계층의 Affine계층에서 한 것과 거의 동일하다.

합성곱 계층 구현하기
im2col(input_data, filter_h, filter_w, stride = 1, pad = 0)
input_data는 (데이터 수, 채널 수, 높이, 너비)의 4차원 배열로 이뤄진 입력 데이터이다.
실제로 사용해보면 다음 코드와 같다.
import sys,os
sys.path.append(os.pardir)
from common.util import im2col
x1 = np.random.rand(1,3,7,7) #(데이터 수, 채널수, 높이, 너비)
col1 = im2col(x1,5,5,stride = 1, pad = 0)
print(col1.shape) #(9,75)
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 데이터).
다음 코드는 im2col를 이용해 합성곱 계층을 Convolution이라는 클래스로 구현했다.
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
필터를 전개하는 부분에서는 각 필터 블록을 1줄로 펼쳐 세운다.
이때 reshape의 두번째 인수를 -1로 지정했는데, 다차원 배열의 원소 수가 변환 후에도 똑같이 유지되도록 적절히 묶어준다.
이게 무슨 뜻이냐면, 앞의 코드에서 (10,3,5,5)형상을 한 다차원 배열 W의 원소 수는 총 750개이다.
이 배열에 reshape(10,-1)을 호출하면 750개의 원소를 10묶음, (10,75)인 배열로 만들어준다.
forward구현의 마지막 부분에서는 출력 데이터를 적절한 형상으로 바꿔준다.
이때 numpy의 transpose함수를 사용하는데, 이는 다차원 배열의 축 순서를 바꿔주는 함수이다.
다음 그림과 같이 인덱스(0부터 시작)를 지정하여 축의 순서를 변경한다.

풀링 계층 구현하기
푸링 계층 구현도 합성곱 계층과 마찬가지로 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,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
폴링 계층 구현은 세단계로 진행한다.
- 입력 데이터를 전개한다.
- 행벽 최댓값을 구한다.
- 적절한 모양으로 성형한다.
axis=0은 열방향 axis=1은 행방향을 뜻한다. (단, 2차원 배열에서)
CNN 구현하기
계층들을 조합하여 손글씨 숫자를 인식하는 CNN을 조립해보자. 다음 그림과 같은 CNN을 구현한다.

초기화할때 받는 인수
- input_dim: 입력 데이터(채널 수, 높이, 너비)의 차원
- conv_param: 합성곱 계층의 하이퍼파라미터(딕셔너리). 딕셔너리의 키는 다음과 같다.
- filer_num 필터수, filter_size 필터 크기, stride, pad, hidden_size, output_size, weight_init_std 초기화 때의 가중치 표준편차
class SimpleConvNet:
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_int_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.param = {}
self.params['W1'] = weight_int_std *\
np.random.rand(filter_num, input_din[0], filter_size, filter_size)
self.params['b1'] = np.zeros(filter_num)
self.params['W2'] = weight_int_std *\
np.random.rand(pool_output_size, hidden_size)
self.params['b2'] = np.zeros(hidden_size)
self.params['W3'] = weight_int_std *\
np.random.rand(hidden_size, output_size)
self.params['b3'] = np.zeros(output_size)
학습이 필요한 매개변수는 첫번째 층의 합성곱 계층과 남너지 두 완전연결 계층의 가중치와 편향이다.
이 매개변수들을 인수턴수 변수 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에 계층을 차례로 추가한다.
마지막 SoftmaxWithLoss계층은 last_layer에 따로 저장한다.
이렇게 초기화를 마친 다음에는 추론을 수행하는 predict 메서드와 손실함수의 값을 구하는 loss 메서드를 다음과 같이 구현할 수 있다.
def predict(self,x):
for layer in self.layers.values():
x = layer.forward(x)
return x
def lostt(self,x,t):
y = self.predict(x)
return self.last_layer.forward(y,t)
x는 입력 데이터, t는 정답 레이블이다.
predict메서드는 추론을 수행하며, layers에 추가한 계층을 맨 앞에서부터 차례로 forward를 호출하며 그 결과를 다음 계층으로 전달한다.
loss메서드는 손실함수를 구하며, predict메서드의 결과를 인수로 마지막 층의 forward메서드를 호출한다. 즉, 첫 계층부터 마지막 계층까지 forward를 처리한다.
CNN 시각화하기
CNN을 구성하는 합성곱 계층은 입력으로 받은 이미지 데이터에서 무엇을 보고 있는 것일까?
첫번째 층의 가중치 시각화하기
MNIST데이터셋으로 간단한 CNN 학습을 해보았는데, 이때 첫번째 층의 합성곱 계층의 가중치는 그 형상이 (30,1,5,5) 필터 30개, 채널 1개, 5x5크기였다.
필터의 크기와 채널이 1개라는 것은 이 필터를 1채널의 회색조 이미지로 시각화할 수 있다.
합성곱 계층의 첫번째 필터를 이미지로 나타내면 다음과 같다.
# coding: utf-8
import sys, os
sys.path.append(os.pardir) # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import pickle
import numpy as np
from collections import OrderedDict
from common.layers import *
from common.gradient import numerical_gradient
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)
# 계층 생성
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):
"""손실 함수를 구한다.
Parameters
----------
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 numerical_gradient(self, x, t):
"""기울기를 구한다(수치미분).
Parameters
----------
x : 입력 데이터
t : 정답 레이블
Returns
-------
각 층의 기울기를 담은 사전(dictionary) 변수
grads['W1']、grads['W2']、... 각 층의 가중치
grads['b1']、grads['b2']、... 각 층의 편향
"""
loss_w = lambda w: self.loss(x, t)
grads = {}
for idx in (1, 2, 3):
grads['W' + str(idx)] = numerical_gradient(loss_w, self.params['W' + str(idx)])
grads['b' + str(idx)] = numerical_gradient(loss_w, self.params['b' + str(idx)])
return grads
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
def save_params(self, file_name="params.pkl"):
params = {}
for key, val in self.params.items():
params[key] = val
with open(file_name, 'wb') as f:
pickle.dump(params, f)
def load_params(self, file_name="params.pkl"):
with open(file_name, 'rb') as f:
params = pickle.load(f)
for key, val in params.items():
self.params[key] = val
for i, key in enumerate(['Conv1', 'Affine1', 'Affine2']):
self.layers[key].W = self.params['W' + str(i+1)]
self.layers[key].b = self.params['b' + str(i+1)]


위의 그림과 같이 학습 전 필터는 무작위로 초기화되고 있어 흑백의 정도에 규칙성이 없다.
한편 학습을 마친 필터는 규칙성 있는 이미지가 되었다. 흰색에서 검은색으로 점차 변화하는 필터와 덩어리(blob)가 진 필터 등, 규칙을 띈느 필터로 바뀌었다.
그규칙성 있는 필터는 무엇을 보고 있는 걸까? 그것은 edge(색상이 바뀐 경계선)과 blob(국소적으로 덩어리진 영역) 등을 보고 있다.
가령 왼쪽 절반이 흰색이고 오른쪽 절반이 검은색인 필터는 다음 그림과 같이 세로 방향의 edge에 반응하는 필터이다.

이처럼 합성곱 계층의 필터는 edge나 blob등의 원시적인 정보를 추출할 수 있다. 이런 원시적인 정보가 뒷단 계층에 전달된다는 것이 앞에서 구현한 CNN에서 일어나는 일이다.
층 깊이에 따른 추출 정보 변환
앞절의 결과는 첫번째 층의 합성곱 계층을 대상으로 한 것이였다.
첫번째 층의 합성곱 계층에서는 edge나 blob등의 저수준 정보가 추출된다 치자. 그럼 CNN의 각 계층에서는 어떤 정보가 추출될까?
딥러닝 시각화에 관한 연구에 따르면, 계층이 깊어질수록 추출되는 정보(강하게 반응하는 뉴런)는 더 추상화된다는 것을 알 수 있다.
아래 그림은 일반 사물 인식(자동차나 개 등)을 수행한 8층의 CNN이다.
이 네트워크 구조를 AlexNet이라 하는데, 합성곱 계층과 풀링 계층을 여러겹 쌓고, 마지막으로 완전연결 계층을 거쳐 결과를 출력하는 구조이다.
블록으로 나타낸 것은 중간 데이터이며, 그 중간 데이터에 합성곱 연산을 연속해서 적용한다.

딥러닝의 흥미로운 점은 합성곱 계층을 여러 겹 쌓으면, 층이 깊어지면서 더 복잡하고 추상화된 정보가 추출된다.
즉, 층이 깊어지면서 뉴런이 반응하는 대상이 단순한 모양에서 '고급'정보로 변화해간다.
대표적인 CNN
LeNet과 AlexNet이 있다.
LeNet
손글씨 숫자를 인식하는 네트워크로, 1998년에 제안되었다.
아래 그림과 같이 합성곱 계층과 풀링 계층(정확히는 단순히 원소를 줄이기만 하는 서브샘플링 계층)을 반복하고, 마지막으로 완전연결 계층을 거치면서 결과를 출력한다.

LeNet과 현재의 CNN을 비교하면 몇가지 차이가 있다.
LeNet은 Sigmoid를 사용하는데 반해, 현재는 주로 ReLU를 사용한다.
LeNet은 서브샘플링을 하여 중간 데이터의 크기를 줄이지만, 현재는 최대 풀링이 주류이다.
AlexNet
2012년에 발표된 AlexNet은 기본적으로 LeNet과 크게 다르지 않다.

합성곱 계층과 풀링 계층을 거듭하여 마지막으로 완전연결 계층을 거쳐 결과를 출력한다.
LeNet과 비교하면 AlexNet은 다음과 같은 변화를 주었다.
- 활성화함수로 ReLU를 이용한다.
- LRN(Local Response Normalization)이라는 국소적 정규화를 실시하는 계층을 이용한다.
- 드롭아웃을 사용한다.
네트워크 구성면에서는 LeNet과 AlexNet에는 큰 차이가 없다.
빅데이터와 GPU, 이것이 딥러닝 발전의 큰 원동력이다.
정리
- CNN 은 완전연결 계층 네트워크에 합성곱 계층과 풀링 계층을 새로 추가한다.
- 합성곱 계층과 풀링 계층은 im2col을 이용하면 간단하고 효율적으로 구할 수 있다.
- CNN을 시각화해보면 계층이 깊어질수록 고급 정보가 추출되는 모습을 확인할 수 있다.
- 대표적 CNN으로는 LeNet,AlexNet이 있다.
- 딥러닝의 발전에는 빅데이터와 GPU가 큰 기여를 했다.
※dataset 및 코드 제공 - 밑바닥부터 시작하는 딥러닝 GitHub: https://github.com/kchcoo/WegraLee-deep-learning-from-
GitHub - kchcoo/WegraLee-deep-learning-from-scratch
Contribute to kchcoo/WegraLee-deep-learning-from-scratch development by creating an account on GitHub.
github.com
'공부 > 밑바닥부터 시작하는 딥러닝' 카테고리의 다른 글
밑바닥부터 시작하는 딥러닝2 - Chap1. 신경망 복습 (2) | 2024.12.14 |
---|---|
밑바닥부터 시작하는 딥러닝1 - Chap8. 딥러닝 (1) | 2024.12.10 |
밑바닥부터 시작하는 딥러닝1 - Chap6. 학습 관련 기술들 (3) | 2024.11.29 |
밑바닥부터 시작하는 딥러닝1 - Chap5. 오차역전파법 (1) | 2024.11.27 |
밑바닥부터 시작하는 딥러닝1 - Chap4. 신경망 학습 (2) | 2024.11.20 |