Chap.07 합성곱 신경망(CNN)

2022. 1. 10. 22:19

7-0. Intro

이번장의 목표는 합성곱 신경망(CNN)의 매커니즘을 자세히 설명하고 이를 파이썬으로 직접 구현하는 것이다. 

자 그럼 시작해보자!


7-1. 전체구조

합성곱 신경망의 전체 구조는 합성곱 계층풀링 계층 그리고 앞에서 배운 완전연결 계층(Affine 계층)을 연결한 구조이다.

먼저 지금까지 사용해왔던 Affine 계층을 다시 그림으로 살펴보자.

그림에서는 Affine 계층과 ReLU 계층을 4개를 연결하였고 마지막에는 Affine 계층과 Softmax 함수를 이용해 최종결과를 출력한다. 

이 그림은 합성곱 신경망을 표현한 그림이다. 

CNN의 특징은 합성곱 계층과 ReLU 그리고 Pooling 계층을 앞에 구현하였고 뒤에는 Affine, ReLU조합을 그대로 구현하였다는 것이다.


7-2. 합성곱 계층

그럼 본격적으로 합성곱 계층이 무엇인지 살펴보자.

Affine 계층 (완전연결 계층의 문제점)

Affine 계층의 가장 큰 문제점은 '데이터의 형상이 무시'된다는 것이다. 이것은 매우 중요한 문제이다. 입력 데이터가 이미지인 경우를 예를 들어 보자. 이미지는 (가로, 세로, 채널)로 구성된 3차원 데이터이다. 그런데 우리가 사용했던 Affine 계층에서는 이 데이터를 reshape해 3차원 데이터를 1차워 데이터로 바꿔주어서 계산한다. 

 

하지만 합성곱 계층에서는 3차원의 데이터를 입력으로 받으면 형상을 유지한다. 이것이 Affine 계층 대신 CNN을 사용하는 이유이다.

CNN에서의 용어
특징 맵(feature map) -> 합성곱 계층의 입출력 데이터 
입력 특징 맵(input feature map) -> 합성곱 계층의 입력 데이터 
출력 특징 맵(output feature map) -> 합성곱 계층의 출력 데이터

합성곱 연산 

CNN에서는 합성곱 연산이라는 것을 한다. 이것은 이미지 처리에서 말하는 필터 연산과 같은 말이다.

문헌에 따라 필터를 커널이라고도 한다.
단일 곱셈-누산 -> 입력과 필터에서 대응하는 원소끼리 곱한 후 그 총합을 구하는 방법

완전연결 신경망에서는 가중치와, 편향이 존재하는데 CNN에서는 필터가 가중치와 같은 역할을 한다. 

패딩

💡 패딩이란?
합성곱 연산을 수행하기 전에 입력 데이터 주변을 특정 값(보통 0)으로 채운는 방식

스트라이드 

💡 스트라이드란?
필터를 적용하는 위치의 간격

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

아까 CNN은 3차원 데이터가 입력 데이터로 들어와도 데이터의 형상(차원)이 유지된다고 했는데 이는 합성곱 연산은 입력되는 특징 맵에 대응하는 필터들을 사용하기 때문이다.

그렇기 때문에 항상 입력 데이터의 채널 수와 필터의 채널 수는 같아야 한다.

블록으로 이해하기

CNN은 다음과 같이 블록화 하여 나타낼 수 있는데 대부분의 논문에서는 블록화 설명을 많이 사용함으로 알아두는 것이 좋다.

위의 그림은 한 개의 특징 맵인 경우를 나타낸 것이다. 최종 표현은 다음과 같다. (필터 추가, 편향 추가, 배치처리)


7-3. 풀링 계층

CNN에서 합성곱 계층과 더불어 처음 등장하는 것이 풀링 계층이다.

💡 풀링 계층이란?
세로, 가로 방향의 공간을 줄이는 연산을 하는 계층

위의 그림은 풀링 방법중 최대 풀링(max pooling)을 표현한 것이다. 최대 풀링은 정해진 윈도우에서 최대값만 골라낸다.

 

풀링 계층의 특징 

  • 학습해야 할 매개변수가 없다.
  • 채널 수가 변하지 않는다.
  • 입력의 변화에 영향을 적게 받는다 (강건하다)

7-4. 합성곱, 풀링 계층 구현

그럼 지금 CNN에서 새롭게 배운 합성곱 계층과 풀링 계층을 구현해보자.

먼저 합성곱 계층을 구현해보자.

 

합성곱 계층 구현

합성곱 계층은 구현하려면 for문을 겹겹이 써야한다. 하지만 우리는 하나의 트릭을 이용해 구현할 것이다.

im2col이용

im2col은  3차원의 입력 데이터를 2차원의 행렬 데이터로 변환해 주는 것이다.

이러한 트릭을 사용하여 CNN을 구현하면 다음과 같은 과정을 거친다.

그럼 이제 진짜 코드로 구현해보자.

def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
    """다수의 이미지를 입력받아 2차원 배열로 변환한다(평탄화).
    
    Parameters
    ----------
    input_data : 4차원 배열 형태의 입력 데이터(이미지 수, 채널 수, 높이, 너비)
    filter_h : 필터의 높이
    filter_w : 필터의 너비
    stride : 스트라이드
    pad : 패딩
    
    Returns
    -------
    col : 2차원 배열
    """
    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), (pad, pad), (pad, pad)], 'constant')
    col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))

    for y in range(filter_h):
        y_max = y + stride*out_h
        for x in range(filter_w):
            x_max = x + stride*out_w
            col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]

    col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
    return col
    
    
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

풀링 계층 구현 

풀링 계층도 합성곱 계층과 마찬가지로 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

7-5. CNN 구현하기

우리는 합성곱 계층, 풀링 계층을 코드로 구현하였다. 이제 최종적으로 CNN을 구현해 볼 것이다. 우리가 구현할 CNN구조는 다음과 같다.

자 그럼 코드로 구현해보자.

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 accuracy(self, x, t, batch_size=100):
        if t.ndim != 1 : t = np.argmax(t, axis=1)
        
        acc = 0.0
        
        for i in range(int(x.shape[0] / batch_size)):
            tx = x[i*batch_size:(i+1)*batch_size]
            tt = t[i*batch_size:(i+1)*batch_size]
            y = self.predict(tx)
            y = np.argmax(y, axis=1)
            acc += np.sum(y == tt) 
        
        return acc / x.shape[0]

    def numerical_gradient(self, x, t):
        """기울기를 구한다(수치미분).

        Parameters
        ----------
        x : 입력 데이터
        t : 정답 레이블

        Returns
        -------
        각 층의 기울기를 담은 사전(dictionary) 변수
            grads['W1']、grads['W2']、... 각 층의 가중치
            grads['b1']、grads['b2']、... 각 층의 편향
        """
        loss_w = lambda w: self.loss(x, t)

        grads = {}
        for idx in (1, 2, 3):
            grads['W' + str(idx)] = numerical_gradient(loss_w, self.params['W' + str(idx)])
            grads['b' + str(idx)] = numerical_gradient(loss_w, self.params['b' + str(idx)])

        return grads

    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
        
    def save_params(self, file_name="params.pkl"):
        params = {}
        for key, val in self.params.items():
            params[key] = val
        with open(file_name, 'wb') as f:
            pickle.dump(params, f)

    def load_params(self, file_name="params.pkl"):
        with open(file_name, 'rb') as f:
            params = pickle.load(f)
        for key, val in params.items():
            self.params[key] = val

        for i, key in enumerate(['Conv1', 'Affine1', 'Affine2']):
            self.layers[key].W = self.params['W' + str(i+1)]
            self.layers[key].b = self.params['b' + str(i+1)]

7-6. CNN 시각화하기

CNN이든 완전연결 계층이든 중요한 것은 학습을 통해 가중치의 매개변수를 잘 갱신시키고 그것의 의미를 아는 것이다. 그렇다면 CNN의 가중치를 보면서 그것이 의미하는 것이 무엇인지 살펴보자.

가중치 시각화

위에 그림은 CNN의 학습전,후의 가중치를 나타낸 것이다. 학습 전에는 무작위로 초기화되었지만 학습 후에는 규칙을 띄는 필터로 변화하였다. 이것은 에지와 블롭의 정보를 담고 있는 것이다.

이처럼 CNN에서 필터는 에지나 블롭 등의 원시적인 정보를 추출할 수 있다. 

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

위에서 본 CNN에서 필터의 원시적인 정보 추출은 저수준의 정보이고 층이 깊어질수록 더 추상화된다.

다음은 AlexNet에서의 추출되는 정보이다.

CNN은 이처럼 여러 곂을 쌓으면서 사물의 '의미'를 이해하도록 변화한다.


7-7. 대표적인 CNN

CNN의 종류와 구성은 다양하다. 따라서 많은 네트워크가 존재하는데 그 중 CNN의 원조인 LeNet과 딥러닝이 주목받도록 이끈 AlexNet을 살펴보자.

LeNet

LeNet은 손글씨 순자를 인식하는 네트워크로 1998년에 제안되었다. 또한 기본 CNN구조인 합성곱 계층과 풀링계층을 반복하고 마지막에는 Affine 계층으로 구현하였다. 

LeNet은 1998년도에 만들어 졌기 때문에 지금의 CNN과는 조금 다른 점이 있다.

차이점

  • LeNet은 시그모이드 함수를 사용하지만 지금의 CNN은 ReLU를 사용한다.
  • LeNet은 서브샘플링을 하여 중간 데이터의 크기를 줄이지만 현제는 최대 풀링을 사용한다.

AlexNet

AlexNet은 딥러닝 열풍을 일으킨 주역이다. AlexNet의 기본적인 구조는 LeNet과 크게 다르지 않다.

AlexNet과 LeNet의 차이점은 다음과 같다.

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

Chapter. 7 정리

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

출처: 사이토 고키『밑바닥부터 시작하는 딥러닝』, 한빛미디어(2017)

'Deep Learning > deep learning from scratch' 카테고리의 다른 글

Chap.08 딥러닝  (0) 2022.01.10
Chap.06 학습 관련 기술들  (0) 2022.01.09
Chap.05 오차역전파법  (0) 2022.01.07
Chap.04 신경망 학습  (0) 2022.01.07
Chap.03 신경망  (0) 2022.01.05

BELATED ARTICLES

more