기본기 다지기/CNN부터 Attention까지 구현

[밑시딥] CNN 구현하기

syveany 2024. 9. 23. 19:38

CNN 구현하기

참고문헌: 책 『밑바닥부터 시작하는 딥러닝』 Chapter7. 합성곱 신경망(CNN)

~ 목차 ~
0. 서론- 왜 갑자기 CNN구현을 하게 되었는가

  1. 합성곱 계층(Conv)을 사용하는 이유
  2. 합성곱 계층 구현하기
    2.1 im2col 함수 구현
    2.1.1 im2col 함수 사용 예시1
    2.1.2 im2col 함수 사용 예시2
    2.2 합성곱 계층 구현
  3. 풀링 계층 구현하기
  4. CNN 구현하기
  5. 소감

0. 서론- 왜 갑자기 CNN구현을 하게 되었는가

  • 코드를 만지면 만질수록 기본기가 부족함을 느낀다. 대회에 참여하거나 프로젝트 할 때는 대부분 당장의 결과물 산출을 우선으로 생각하니 있는 코드를 그대로 가져와서 변형하면서 사용하곤 하는데, 제대로 이해를 하지 않고 사용하니 겉도는 느낌이 계속 들었다. 지금 조금의 짬이 나서 기본기를 하나하나 다져보려고 한다.
  • 어떤 자료를 보면 좋을까 떠올리던 도중 딥러닝 기초 국룰 책인 『밑바닥부터 시작하는 딥러닝』이 생각났다. 목차를 쭉 보면서 공부하고 싶은 것들만 추려서 공부해볼 예정이다.

1. 합성곱 계층(Conv)을 사용하는 이유

image.png

  • Affine 층을 사용하면 3차원 데이터를 1차원으로 평탄화해줘야 하는데, 이 과정에서 데이터의 공간적 정보가 없어진다. 즉, 이미지 데이터는 옆에 있는 픽셀끼리 관계가 의미있는 경우가 많은데, 이런 것들이 아예 반영되지 못하게 된다.
  • 이를 보완하기 위해 합성곱 계층을 사용하면 3차원 데이터를 그대로 3차원 데이터로 입력받을 수 있고, 다음 계층에도 3차원 데이터로 전달하게 된다. 그래서 데이터의 공간적 정보가 반영될 수 게 된다.

image.png

  • 기존 네트워크의 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)로 바꾸는 것임

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 논문을 보고 와서 다시 보니까 느낌이 또 다르다. 신기하다.