CNN 구현하기
참고문헌: 책 『밑바닥부터 시작하는 딥러닝』 Chapter7. 합성곱 신경망(CNN)
~ 목차 ~
0. 서론- 왜 갑자기 CNN구현을 하게 되었는가
- 합성곱 계층(Conv)을 사용하는 이유
- 합성곱 계층 구현하기
2.1 im2col 함수 구현
2.1.1 im2col 함수 사용 예시1
2.1.2 im2col 함수 사용 예시2
2.2 합성곱 계층 구현 - 풀링 계층 구현하기
- CNN 구현하기
- 소감
0. 서론- 왜 갑자기 CNN구현을 하게 되었는가
- 코드를 만지면 만질수록 기본기가 부족함을 느낀다. 대회에 참여하거나 프로젝트 할 때는 대부분 당장의 결과물 산출을 우선으로 생각하니 있는 코드를 그대로 가져와서 변형하면서 사용하곤 하는데, 제대로 이해를 하지 않고 사용하니 겉도는 느낌이 계속 들었다. 지금 조금의 짬이 나서 기본기를 하나하나 다져보려고 한다.
- 어떤 자료를 보면 좋을까 떠올리던 도중 딥러닝 기초 국룰 책인 『밑바닥부터 시작하는 딥러닝』이 생각났다. 목차를 쭉 보면서 공부하고 싶은 것들만 추려서 공부해볼 예정이다.
1. 합성곱 계층(Conv)을 사용하는 이유
- Affine 층을 사용하면 3차원 데이터를 1차원으로 평탄화해줘야 하는데, 이 과정에서 데이터의 공간적 정보가 없어진다. 즉, 이미지 데이터는 옆에 있는 픽셀끼리 관계가 의미있는 경우가 많은데, 이런 것들이 아예 반영되지 못하게 된다.
- 이를 보완하기 위해 합성곱 계층을 사용하면 3차원 데이터를 그대로 3차원 데이터로 입력받을 수 있고, 다음 계층에도 3차원 데이터로 전달하게 된다. 그래서 데이터의 공간적 정보가 반영될 수 게 된다.
- 기존 네트워크의 Affine->ReLU 부분이 CNN에서는 Conv->ReLU->Pooling 모양이 된다.
- 그래서 이제 새롭게 등장한 합성곱계층(Conv)과 풀링계층(Pooling)을 직접 구현해볼 것이다.
2. 합성곱 계층 구현하기
import numpy as np
- 새삼 진짜 필요한 라이브러리만 임포트해오고 나머지는 구현한다는 점이 기본기 다지기의 목적에 부합해서 마음에 든다.
2.1 im2col 함수 구현
- Image to Column, 즉 '이미지에서 행렬로'
- 3차원 입력데이터를 필터링(가중치계산)하기 좋게 2차원으로 펼치는 함수
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
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), # (0,0): 배치 크기와 채널 수에 대해 패딩이 없음
(pad, pad), (pad, pad)], # (pad, pad): 높이(H)와 너비(W)에 패딩을 적용
'constant' # 'constant': 패딩을 0으로 채움
)
# 필터가 적용된 각 패치를 저장할 배열 초기화
col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))
# for문: 필터의 높이와 너비에 맞춰서 이미지에서 패치를 추출
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에서 필터 크기에 맞는 작은 패치를 추출해서 col배열에 저장
col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]
# reshape~: 모든 패치를 한 행으로 나열
col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
return col
2.1.1 im2col 함수 사용 예시1
x1 = np.random.rand(1,3,7,7) # 배치크기1, 채널수3, 높이7, 너비7
col1 = im2col(x1,5,5,stride=1,pad=0) # 필터크기 5x5
print(col1.shape)
(9, 75)
# 9: 추출할 수 있는 5x5 패치의 총 개수
# 75: 각 패치에서 추출된 값들의 총 개수
- 필터크기, stride, pad를 봤을 때 출력 이미지의 크기는 3 x 3 = 9이다.
- 배치크기가 1이므로, 필터가 적용되는 패치의 수는 1 x 9 = 9이다.
- 각 패치는 3개의 채널에서 5x5필터로 추출되므로, 패치 하나의 크기는 3 x 5 x 5 = 75이다.
2.1.2 im2col 함수 사용 예시2
x2 = np.random.rand(10,3,7,7)
col2 = im2col(x2,5,5,stride=1,pad=0)
print(col2.shape)
(90, 75)
- 위의 예시와 다른점은 배치크기가 10이므로, 필터가 적용되는 패치의 수는 10 x 9 = 90이다.
2.2 합성곱 계층 구현
class Convolution:
def __init__(self, W, b, stride=1, pad=0):
self.W = W # 필터 가중치(4차원배열-FN,C,FH,FW)
self.b = 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)
# 입력 데이터를 2차원 배열로 변환
col = im2col(x,FH,FW,self.stride, self.pad)
# 필터를 2차원 행렬로 변환
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
- 차원 모양 설명
- im2col을 사용하면:
- 입력데이터는 (N, C, H, W)에서 col = (N x out_h x out_w, C x FH x FW)이 되고,
- 필터는 (FN, C, FH, FW)에서 col_W = (C x FH x FW, FN)이 되는데,
- 이 상태에서 np.dot(col, col_W)를 하면 (N x out_h x out_w, FN)과 같은 2차원 배열이 나옴
- 이거를 reshape해서 (N, out_h, out_w, FN)과 같은 4차원 배열로 만든 뒤,
- 마지막에 transpose를 통해서 원하는 모양인 (N, FN, out_h, out_w)로 바꾸는 것임
- im2col을 사용하면:
3. 풀링 계층 구현하기
- 설명할 건 위에서 다 설명해서 패쓰
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)
# 요 부분에서 최댓값(max pooling) 구함
out = np.max(col, axis=1)
out = out.reshape(N, out_h, out_w, C).transpose(0,3,1,2)
return out
4. CNN 구현하기
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_nit_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)
# CNN 계층 구성
# 순서가 있는 딕셔너리인 layers에 계층들을 차례로 추가
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'])
# 마지막 SoftmaxWithLoss 계층은 last_layer라는 별도 변수에 저장
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): # x: 입력데이터, t: 정답레이블
# predict를 통해 예측한 값과 정답 레이블을
y = self.predict(x)
# SoftmaxWithLoss 계층의 forward메서드로 전달하여 손실값을 반환함
return self.last_layer.forward(y,t)
# 오차 역전파법으로 기울기를 구하는 구현
def gradient(self, x, t):
# 순전파
self.loss(x,t)
# 역전파
dout = 1 # 초기 기울기를 1로 설정. 마지막 softmax 계층의 출력에 대한 손실함수의 기울기가 여기서 시작됨
dout = self.last_layer.backward(dout) # 마지막 softmax 계층에서부터 역전파를 수행함
layers = list(self.layers.values())
layers.reverse() # 계층들을 역순으로 배치
for layer in layers:
dout = layer.backward(dout) # 현재 계층의 역전파를 수행하여 새로운 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
5. 소감
- 바위에 계란치기를 몇 번 경험한 다음 기본기를 다지러 돌아오니 예전보다 이론 내용이 와닿는다. 그동안 궁금했던 것들을 하나하나 공부해야겠다.
- 공식문서들을 보면서 공부하는 습관도 들이고 싶다. 10월 안에는 공식문서 보면서 공부도 해봐야겠다.
- 파이팅
... - 24.10.05.: Transformer 논문을 보고 와서 다시 보니까 느낌이 또 다르다. 신기하다.
'기본기 다지기 > CNN부터 Attention까지 구현' 카테고리의 다른 글
[기본이론] Attention 구현(2/3) (1) | 2024.09.27 |
---|---|
[기본이론] Attention 구현(1/3) (1) | 2024.09.26 |
[기본이론] seq2seq 구현 (0) | 2024.09.23 |
[밑시딥] LSTM 구현하기 (0) | 2024.09.23 |
[밑시딥] RNN 구현하기 (0) | 2024.09.23 |