본문 바로가기
ML&DATA/밑바닥부터 시작하는 딥러닝

7 - CNN

by sun__ 2020. 10. 7.

이미지 인식 분야에서 딥러닝을 활용한 기법은 거의 다 CNN을 기초로 함.

 

7.1 전체 구조

 

CNN에서는 기존 네트워크에서 새로운 합성곱 계층과 풀링 계층이 추가된다. CNN 계층은 conv-relu-pooling 흐름으로 연결된다. 출력에 가까운 층에서는 affine-relu 구성을 사용할 수 있다.

 


 

 

7.2 합성곱 계층

이미지는 다차원의 형상이고, 이 형상엔 공간적 정보가 담겨있다. 예를들어 공간적으로 먼 픽셀끼리는 별 연관이 없는 등 공간 속에서 의미를 갖는 본질적인 패턴이 숨어있다. 여태까지 배웠던 완전연결 계층은 형상에 담긴 정보를 살릴 수 없다. 

 

반면, 합성곱 계층은 형상을 유지한다. 다차원 데이터를 입력받아 다차원 데이터를 출력받는다. 그래서 CNN에서는 이미지처럼 형상을 가진 데이터를 제대로 이해할 가능성이 있는 것이다.

 

합성곱 계층의 입출력 데이터를 특징 맵(feature map) 이라고 한다.

 

패딩 : 합성곱 연산을 수행하기 전에 입력 데이터 주변을 특정 값으로 채우는 것.

 

스트라이드: 필터 적용 간격

 

입력 크기를 $(H,W)$, 필터 크기를 $(FH, FW)$, 출력 크기를 $(OG, OW)$, 패딩을 $P$, 스트라이드를 $S$라고 하면 출력의 크기$(OH, OW)$는

$$OH=\frac{H+2P-FH}{S}+1$$

$$OW=\frac{W+2P-FW}{S}+1$$

 

이때 $OH$,$OW$는 정수여야 함. 프레임워크 중에선 반올림하는 등 에러를 내지 않고 진행하도록 구현하기도 함.

 

7.2.1 3차원 데이터에서 합성곱 연산

 

7.2.2 배치 처리

 


 

 

7.3 풀링계층

풀링은 세로, 가로 방향의 공간을 줄이는 연산이다. 최대 풀링은 대상 영역 중에서 최댓값을 구하는 것이다. 설정한 스트라이드의 크기 대로 윈도우를 설정하고 그 간격으로 이동한다.

풀링은 대상 영역에서 최댓값이나 평균을 취하는 명확한 처리이므로 학습할 것이 없다. 그리고 채널수가 변하지 않는다. 최대 풀링의 경우 입력의 변화에 영향을 적게 받는다.

 


 

7.4 합성곱/풀링 계층(layer) 구현하기

3차원 데이터의 배치를 input으로 하는 계층을 만드는 경우를 생각해보자. (총  4차원)

 

 

넘파이에 for 문을 사용하면 성능이 떨어지기 때문에 im2col이라는 편의 함수를 이용하면 편하다. im2col은 입력 데이터를 필터링하기 좋게 전개하는 함수이다.

 

im2col함수의 인터페이스는 다음과 같다.

 

input_data : (데이터 수, 채널 수, 높이, 너비)의 4차원 배열로 이뤄진 입력 데이터

filter_h, filter_w : 필터 높이, 너비

stride, pad : 스트라이드, 패딩

im2col(input_data, filter_h, filter_w, stride=1, pad=0)

 

위 im2col은 필터 차원, 스트라이드 및 패딩을 고려해서 입력 데이터를 2차원 배열로 전개한다.

 

<합성곱 계층>

class Convolution:
    def __init__(self, W, b, stride=1, pad=0):
        self.W = W
        self.b = b
        self.stride = stride
        self.pad = pad
        
        # 중간 데이터(backward 시 사용)
        self.x = None   
        self.col = None
        self.col_W = None
        
        # 가중치와 편향 매개변수의 기울기
        self.dW = None
        self.db = None

    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)

        self.x = x
        self.col = col
        self.col_W = col_W

        return out

    def backward(self, dout):
        FN, C, FH, FW = self.W.shape
        dout = dout.transpose(0,2,3,1).reshape(-1, FN)

        self.db = np.sum(dout, axis=0)
        self.dW = np.dot(self.col.T, dout)
        self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)

        dcol = np.dot(dout, self.col_W.T)
        dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)

        return dx

backward는 affine과 유사하게 이뤄진다.

 

 

<풀링 계층>

forward 흐름은 다음과 같다. 역시 전개할 땐 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
        
        self.x = None
        self.arg_max = None

    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)

        arg_max = np.argmax(col, axis=1)
        out = np.max(col, axis=1)
        out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)

        self.x = x
        self.arg_max = arg_max

        return out

    def backward(self, dout):
        dout = dout.transpose(0, 2, 3, 1)
        
        pool_size = self.pool_h * self.pool_w
        dmax = np.zeros((dout.size, pool_size))
        dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
        dmax = dmax.reshape(dout.shape + (pool_size,)) 
        
        dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
        dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)
        
        return dx

backward는 Relu와 유사하다.

 


 

7.5 CNN 구현하기

5장에서와 비슷하게 network클래스를 잘 만들어주면 된다. 위에서 만들어둔 convolution, pooling layer를 사용하면 된다.

 

흐름만 익히자.

더보기
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 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

 


 

7.6 CNN 시각적 해석

학습 전 필터는 무작위로 초기화되고 있어 흑백의 정도에 규칙성이 없다. 학습을 마친 필터는 규칙이 있는 이미지가 되었다.

  • 흰색에서 검은색으로 변화하고 덩어리를 가지기도 하는 규칙을 나타낸다. 
  • 이 있는 필터는 색상이 바뀐 경계선인 에지와 덩어리진 영역인 블롭을 보고 있다.
  • 이렇게 합성곱 계층의 필터는 에지나 블롭 등 원시적인 정보를 추출할 수 있다.

이런 원시적인 정보가 뒷단 계층에 전달되는 것이 CNN에서 일어나는 일이다.

 

AlexNet

계층이 깊어질수록 추출되는 정보는 더 추상화된다. 1층은 에지와 블롭, 3층은 텍스쳐, 5층은 사물의 일부, 마지막은 사물의 클래스에 뉴런이 반응한다.

 

층이 깊어질수록 뉴런이 고급 정보에 반응한다. 즉, 사물의 의미를 이해하도록 변화한다.

 

 


 

7.7 대표적인 CNN

<LeNet>

손글씨 숫자를 인식하는 네트워크이다. 합성곱 계층과 풀링 계층을 반복하고 마지막으로 완전연결 계층을 거치면서 결과를 출력한다.

LeNet과 현재의 CNN과의 차이점

  • LeNex는 시그모이드 함수를 사용하고 현재는 ReLU를 사용한다.

  • LeNex는 서브샘플링을 하여 중간데이터 크기가 작아지지만 현재는 최대 풀링이 주류다.

<AlexNet>

합성곱 계층과 풀링 계층을 거듭하며 마지막으로 완전연결 계층을 사용하지만, 다음과 같은 변화가 있다

  • 활성화 함수로 ReLU를 이용한다

  • LRN이라는 국소적 정규화를 실시하는 계층을 이용한다.

  • 드롭아웃을 사용한다.

'ML&DATA > 밑바닥부터 시작하는 딥러닝' 카테고리의 다른 글

5 - 오차역전파법  (0) 2020.09.18
4 - 신경망 학습  (0) 2020.09.10
3. 신경망  (0) 2020.09.08