오답노트

[밑딥] 합성곱 신경망 (CNN) 본문

Python/DL

[밑딥] 합성곱 신경망 (CNN)

권멋져 2023. 2. 27. 22:21

전체 구조

지금까지 본 신경망은 인접하는 계층의 모든 뉴런과 결합되어 있었다. 이를 완전연결(fully-connected)이라고 하며 완전히 연결된 계층을 Affine 계층이라는 이름으로 구현했다.

CNN에서는 새로운 '합성곱 계층'과 '풀링 계층'이 추가된다. CNN의 계층은 'Conv-ReLU-(Pooling)' 흐름으로 연결된다. 주목할 또 다른 점은 출력에 가까운 층에서는 지금까지의 'Affine-ReLU' 구성을 사용할 수 있다는 것이다. 도 마지막 출력 계층에서는 'Affine-Softmax' 조합을 그대로 사용한다.

 

합성곱 계층

완전연결 계층의 문제점

완전연결 계층에서는 인접하는 계층의 뉴런이 모두 연결되고 출력의 수는 임의로 정할 수 있다. 완전연결 계층의 문제점은 '데이터의 형상이 무시'된다는 사실이다. 완전연결 계층은 형상을 무시하고 모든 입력 데이터를 동등한 뉴런(같은 차원의 뉴런)으로 취급하여 형상에 담긴 정보를 살릴 수 없다.

한편, 합성곱 계층은 형상을 유지한다. 그래서 CNN에서는 이미지처럼 형상을 가진 데이터를 제대로 이해할 가능성이 있는 것이다.

CNN에서는 합성곱 계층의 입출력 데이터를 특징 맵이라고도 한다. 합성곱 계층의 입력 데이터를 입력 특징 맵, 출력 데이터를 출력 특징 맵이라고 하는 식이다.

 

합성곱 연산

합성곱 계층에서의 합성곱 연상을 처리한다. 합성곱 연산은 이미지 처리에서 말하는 필터 연산에 해당한다.

문헌에 따라 필터를 커널이라 칭하기도 한다.

합성곱 연산은 필터의 윈도우를 일정 간격으로 이동해가며 입력 데이터에 적용한다. 이 그림에서 보듯 입력과 필터에서 대응하는 원소끼리 곱한 후 그 총합을 구한다. 입력과 필터에서 대응하는 원소끼리 곱한 후 그 총합을 구한다. 이 계산을 단일 곱셈-누산이라한다. 그리고 그 결과를 출력의 해당 장소에 저장한다. 이 과정을 모든 장소에서 수행하면 합성곱 연산의 출력이 완성된다. CNN에서는 필터의 매개변수가 그동안의 '가중치'에 해당한다.

 

패딩

합성곱 연산을 수행하기 전에 입력 데이터 주변을 특정 값으로 채우기도 한다. 이를 패딩이라고 하며, 합성곱 연산에서 자주 이용하는 기법이다. 예를 들어 폭이 1짜리 패딩이라 하면 입력 데이터 사방 1픽셀을 특정 값으로 채우는 것이다.

패딩은 주로 출력 크기를 조정할 목적으로 사용된다. 입력 데이터의 공간적 크기를 고정한 채로 다음 계층에 전달할 수 있다.

 

스트라이드

필터를 적용하는 위치의 간격을 스트라이드라고 한다. 입력 크기를 (H,W), 필터 크기를 (FH, FW), 출력 크기를(OH,OW), 패딩을 P, 스트라이드를 S라 하면, 출력 크기는 다음 식으로 계산한다.

OH, OW는 정수로 나눠떨어지는 값이어야 한다는 점에 주의해야한다. 출력 크기가 정수가 아니면 오류를 내는 등의 대응을 해줘야한다.

 

3차원 데이터의 합성곱 연산

채널쪽으로 특징 맻이 여러 개 있다면 입력 데이터와 필터의 합성곱 연산을 채널마다 수행하고, 그 결과를 더해서 하나의 출력을 얻는다. 3차원의 합성곱 연산에서 주의할 점은 입력 데이터의 채널 수와 필터의 채널 수가 같아야한다는 것이다. 필터의 채널 수는 입력 데이터의 채널 수와 같도록 설정해야하 한다.

 

블록으로 생각하기

3차원의 합성곱 연산은 데이터와 필터를 직육면체 블록이라고 생각하면 쉽다. 한 장의 특징 맵을 다른 말로 하면 채널이 1개인 특징 맵이다. 합성곱 연산의 출력으로 다수의 채널을 내보내려면 필터를 다수 사용하면 된다.

이 완성된 블록을 다음 계층으로 넘기는 것이 CNN의 처리 흐름이다.

 

배치 처리

신경망 처리에서는 입력 데이터를 한 덩어리로 묶어 배치로 처리했다. 완전 연결 신경망을 구현하면서 이 방식을 지원하여 처리 효율을 높이고, 미니배치 방식의 학습도 지원하도록 했다.

각 계층을 흐르는 데이터의 차원을 하나 늘려 4차원 데이터로 저장한다. 여기서 주의할 점으로는 신경망에 4차원 데이터가 하나 흐를 때마다 데이터 N개에 대한 합성곱 연산이 이뤄진다는 것이다. 즉, N회 분의 처리를 한 번에 수행하는 것.

 

풀링 계층

풀링은 세로 가로 방향의 공간을 줄이는 연산이다.

 

풀링 계층의 특징

  • 학습해야 할 매개변수가 없다 : 풀링 계층은 합성곱 계층과 달리 학습해야 할 매개변수가 없다. 풀링은 대상 영역에서 최댓값이나 평균을 취하는 명확한 처리이므로 특별히 학습할 것이 없다.
  • 채널 수가 변하지 않는다 : 풀링 연산은 입력 데이터의 채널 수 그대로 출력 데이터로 내보낸다.
  • 입력의 변화에 영향을 적게 받는다 (강건하다) : 입력 데이터가 조금 변해도 풀링의 결과는 잘 변하지 않는다.

합성곱/풀링 계층 구현하기

합성곱 계층과 풀링 계층은 복잡해 보이지만, 사실 '트릭'을 사용하면 쉽게 구현할 수 있다.

 

4차원 배열

CNN에서 계층 사이를 흐르는 데이터는 4차원이다. 그래서 합성곱 연산의 구현은 복잡해질 것 같지만, im2col이 문제를 단순하게 만들어준다.

 

im2col로 데이터 전개하기

im2col은 입력 데이터를 필터링 (가중치 계산)하기 좋게 전개하는 함수다. 3차원 입력 데이터에 im2col을 적용하면 2차원 행렬로 바뀐다. 입력 데이터에서 필터를 적용하는 영역을 한 줄로 늘어놓는다. 이 전개를 필터를 적용하는 모든 영역에서 수행하는게 im2col이다.

필터 적용 영역이 겹치게 되면 im2col로 전개한 후의 원소 수가 원래 블록의 원소 수보다 많아진다. 그래서 im2col을 사용해 구현하면 메모리를 더 많이 소비하는 단점이 있다. 하지만 컴퓨터는 큰 행렬을 묶어서 계산하는데 탁월하다. 문제를 행렬 계산으로 만들면 선형 대수 라이브러리를 활용해 효율을 높일 수 있다.

 

합성곱 계층 구현하기

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)
    out = np.dot(col.col_W) + self.b

    out = out.reshape(N, out_h, out_w, -1).transpose(0,3,1,2)

    return out

 

풀링 계층 구현

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 + 2*self.pad - self.pool_h) / self.stride)
    out_w = int(1+(W + 2*self.pad - 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 구현하기

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_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):
    y = self.predict(x)
    return self.last_layer.forward(y,t)
  
  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에서 일어나는 일이다.

 

층 깊이에 따른 추출 정보 변화

겹겹이 쌓인 CNN의 각 계층에서는 계층이 깊어질수록 추출되는 정보 (정확히는 강하게 반응하는 뉴런)는 더 추상화된다는 것을 알 수 있다.

처음 층은 단순한 에지에 반응하고, 이어서 텍스처에 반응하고, 더 복잡한 사물의 일부에 반응하도록 변화한다. 즉, 층이 깊어지면서 뉴런이 반응하는 대상이 단순한 모양에서 '고급' 정보로 변화해간다. 다시 말하면 사물의 '의미'를 이해하도록 변화하는 것이다.

 

대표적인 CNN

LeNet

LeNet은 손글씨 숫자를 인식하는 네트워크로 1998년에 제안됐다. 합성곱 계측과 풀링 계층 (정확히는 단순히 '원소를 줄이기'만 하는 서브 샘플링 계층)을 반복하고 마지막으로 완전연결 계층을 거치면서 결과를 출력한다.

LeNet과 '현재의 CNN'을 비교하면 몇 가지 면에서 차이가 있다. 첫 번재 차이는 활설화 함수다. LeNet은 시그모이드 함수를 사용하는데, 현재는 주로 ReLU를 사용한다. 또 원래의 LeNet은 서브샘플링을 하여 중간 데이터의 크기를 줄이지만 현재는 최대 풀링이 주류다.

 

AlexNet

AlexNet은 합성곱 계층과 풀링 계층을 거듭하며 마지막으로 완전연결 계층을 거쳐 결과를 출력한다. LeNet에서 큰 구조는 바뀌지 않지만, AlexNet에서는 다음과 같은 변화를 주었다.

  • 활성화 함수로 ReLU를 사용한다.
  • LRN(Local Response Normalization)이라는 국소적 정규화를 실시하는 계층을 이용한다.
  • 드롭아웃을 사용한다.

대량의 데이터를 누구나 얻을 수 있게 되었고, 병렬 계산에 특화된 GPU가 보급되면서 대량의 연산을 고속으로 수행할 수 있게 되었다. 빅데이터와 GPU, 이것이 딥러닝 발전의 큰 원동력이다.

 

정리

  • CNN은 지금까지의 완전연결 계층 네트워크에 합성곱 계층과 풀링 계층을 새로 추가한다.
  • 합성곱 계층과 풀링 계층은 im2col (이미지를 행렬로 전개하는 함수)을 이용하면 간단하고 효율적으로 구현할 수 있다.
  • CNN을 시각화해보면 계층이 깊어질수록 고급 정보가 추출되는 모습을 확인할 수 있다.
  • 대표적인 CNN에는 LeNet과 AlexNet이 있다.
  • 딥러닝의 발전에는 빅데이터와 GPU가 크게 기여했다.

'Python > DL' 카테고리의 다른 글

[DL] Embedding  (0) 2023.03.04
[NLP/RS] Negative Sampling  (0) 2023.03.03
[밑딥] 학습 관련 기술들  (0) 2023.02.27
[밑딥] 오차역전파법  (0) 2023.02.27
[밑딥] 신경망 학습  (0) 2023.02.27