딥러닝

딥러닝 기초 부수기 - 합성곱 신경망(CNN)

삶과계란사이 2022. 3. 19. 23:07

딥러닝 기초 부수기 - 합성곱 신경망(CNN)

본 게시글은 한빛미디어 출판사의 '밑바닥부터 시작하는 딥러닝(저자: 사이토 고키)' 도서 내용을 바탕으로 작성하였습니다.

 

1. 서론

이미지 인식 분야에서 딥러닝을 활용한 기법은 합성곱 신경망(convolutional neural network, CNN)을 기초로 한다. CNN은 이미지 인식과 음성 인식 등 다양한 곳에 사용된다.

 

2. CNN 전체 구조

지금까지 본 신경망은 계층의 모든 뉴런과 결합되어 있는 완전연결(fully-connected) 형태이다. 이렇게 완전하게 연결된 계층을 Affine 계층으로 구현할 수 있다. CNN은 합성곱 계층(convolutional layer)풀링 계층(pooling layer)이 추가된다. 그리고 CNN으로 이미지 등의 3차원 데이터를 효과적으로 다룰 수 있다.

완전연결 계층 신경망의 예시
CNN 신경망의 예시

 

3. 합성곱 계층

완전연결 계층 신경망은 입력 데이터의 형상이 무시된다. 즉 형상에 담긴 정보를 살릴 수 없다는 문제점이 있다. 하지만 합성곱 계층은 입력 데이터의 형상을 유지할 수 있다. CNN에서 합성곱 계층의 입출력 데이터를 특징 맵(feature map)이라 한다.

(1) 합성곱 연산

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

필터의 윈도우(window)를 일정 간격으로 이동하면서 입력 데이터에 적용한다. 윈도우는 필터 사이즈를 의미한다. 입력 데이터와 필터에서 대응하는 원소끼리 곱하고 그 총합을 결과에 저장한다.

CNN에서는 필터의 매개변수가 가중치에 해당한다. 또한 편향도 존재한다.

 

(2) 패딩

합성곱 연산 수행 전, 입력 데이터 주변을 0 같은 특정 값으로 채우는 것을 패딩(padding)이라 한다. 패딩은 출력 크기를 조정할 목적으로 주로 사용한다.

 

(3) 스트라이드

필터를 적용하는 위치 간격을 스트라이드(stride)라고 한다. 스트라이드를 3으로 설정하면 필터 윈도우가 세 칸씩 이동한다.

 

(4) 3차원 데이터 합성곱 연산

위에서는 2차원 데이터의 합성곱 연산을 알아보았다. 이번에는 이미지 데이터 형상인 3차원 데이터의 합성곱 연산을 살펴보겠다. 3차원 이미지 데이터는 가로, 세로, 채널이 존재한다. 채널 방향으로 특징 맵이 여러 개 있다면 입력 데이터와 필터의 합성곱 연산을 채널마다 수행하고, 그 결과를 더해 하나의 출력을 저장한다. 여기서 주의할 점은, 입력 데이터의 채널 수와 필터의 채널 수가 같아야 한다는 것이다.

 

(5) 배치 처리

CNN의 배치 처리는 데이터의 차원을 하나 늘려 4차원 데이터로 저장한다. (데이터 수, 채널 수, 높이, 너비) 순으로 저장한다.

 

4. 풀링 계층

풀링은 세로, 가로 방향의 공간을 줄이는 연산이다. 대상 영역에서 최댓값을 구하는 연산인 최대 풀링(max pooling), 대상 영역의 평균을 구하는 평균 풀링(average pooling) 등이 있다. 이미지 인식 쪽에서는 최대 풀링을 주로 사용한다고 한다.

최대 풀링 처리 순서

위 그림은 2x2 크기의 영역에서 가장 큰 원소 하나를 꺼내는 최대 풀링을 스트라이드 2로 처리하는 모습이다. 보통 윈도우 크기와 스트라이드는 같은 값으로 설정한다. 이러한 풀링 계층은 3가지 특징이 있다.

  1. 학습해야 할 매개변수가 존재하지 않는다.
  2. 채널 수가 변하지 않는다.
  3. 입력의 변화에 영향을 적게 받는다.

 

5. 합성곱, 풀링 계층 구현

(1) im2col로 데이터 전개

입력 데이터를 필터링(가중치 계산)하기 좋게 펼치는 im2col 함수를 사용하면 for문을 굳이 사용하지 않아도 합성곱 연산을 간단하게 구현할 수 있다. 3차원 입력 데이터에 im2col 함수를 적용하면 2차원 행렬로 바뀐다. 입력 데이터에 배치 처리를 한다면, 데이터 수까지 포함한 4차원 데이터가 2차원 행렬로 변환된다.

입력 데이터는 배치 처리가 된 N개의 데이터

 

(2) 합성곱 계층 구현

# 합성곱 계층

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 = 1 + int((H + 2*self.pad - FH) / self.stride)
        out_w = 1 + int((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

 

(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)
        
        # 최대 풀링
        out = np.max(col, axis=1)
        
        # 변형
        out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)

        return out

 

6. CNN 구현

합성곱 계층과 풀링 계층을 사용하여 단순한 CNN 네트워크를 구현할 수 있다.

단순한 CNN 네트워크

# 단순한 CNN 네트워크

class SimpleConvNet:
    """초기화 메소드
    input_dim : 입력 데이터(채널 수, 높이, 너비) 차원
    conv_param : 합성곱 계층의 하이퍼파라미터
    hidden_size : 은닉층 뉴런 수
    output_size : 출력층 뉴런 수
    weight_init_std : 초기화 때의 가중치 표준편차
    """
    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 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'], 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

 

7. CNN 정리

CNN에서 합성곱 계층을 깊게 쌓으면 쌓을수록 뉴런이 반응하는 대상이 단순한 모양에서 고급 정보로 변화한다. 이미지 속에 존재하는 사물의 의미를 이해하도록 변화하는 것이라고 볼 수 있다. 1번째 층은 에지와 블롭(blob, 덩어리), 3번째 층은 텍스처, 5번째 층은 사물의 일부, 마지막 완전연결 계층은 사물의 클래스에 뉴런이 반응하는 것처럼 점점 더 복잡하고 추상화된 정보를 추출할 수 있다.