오뚝이개발자

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

AI/밑바닥딥러닝1

[CH7] 합성곱 신경망(CNN)

땅어 2020. 6. 23. 18:42
728x90
300x250

합성곱 신경망 CNN은 Convolutional Neural Network의 약자이다. 이미지 인식과 음성 인식 등 다양한 곳에서 활용되는데, 특히 이미지 인식 분야에서 딥러닝을 활용한 기법은 거의 CNN을 기초로 한다. 

CNN의 구조


- 완전연결 신경망(fully-conected) : 인접하는 계층의 모든 뉴련과 결합

  - 완전히 연결된 계층을 Affine 계층이라는 이름으로 구현

 

- CNN : 합성곱 계층(Conv)풀링 계층(Pooling)이 추가됨.

  - Conv -> ReLU -> (Pooling) 흐름으로 연결(Pooling은 생략되기도 함)

  - 지금까지의 Affine -> ReLU 연결이 Conv -> ReLU -> Pooling으로 바뀌었다고 생각하면 쉽다.

  - 마지막 출력 계층에선 Affine -> Softmax 조합을 그대로 사용

 

완전연결 계층으로 이뤄진 네트워크

 

CNN으로 이뤄진 네트워크

 

완전연결 계층의 문제점


- 데이터의 형상이 무시됨.

  - 이미지는 통상 3차원(채널, 세로, 가로)으로 3차원 속에서 의미를 갖는 패턴이 有

    - 공간적으로 가까운 픽셀은 값이 비슷하거나, 거리가 먼 픽셀끼리는 관련성이 떨어진다거나 등. 

  - BUT 완전연결 계층에 입력할 땐 평평한 1차원으로 바꿔준다.

  - MNIST 손글씨 사례에서도 형상이 (1, 28, 28)인 이미지를 1줄로 세운 784개의 데이터 입력해줬음.

 

- CNN은 형상을 유지

  - 입력도 3차원으로 받고, 다음 계층에도 3차원으로 전달

  - CNN에선 입출력 데이터를 특징 맵(feature map)이라 부름.

 

합성곱 연산


- 합성곱 연산은 이미지 처리에서 말하는 필터 연산에 해당

- 필터를 커널이라 칭하기도 함.

- 합성곱 연산은 필터의 윈도우(window)를 일정 간격 이동해가며 입력 데이터에 적용

- 아래 그림과 같이 입력과 필터에서 대응하는 원소끼리 곱한 후 그 총합을 구함

- CNN에선 필터의 매개변수가 지금까지의 가중치에 해당

- 편향(bias)는 항상 하나만 존재(1 x 1)

필터 연산
편향을 반영한 필터 연산(출처:je-d 티스토리)

 

패딩


- 패딩(padding)이란 합성곱 연산 수행 전 입력 데이터 주변을 0과 같은 특정 값으로 채우는 것

- 주로 출력 데이터의 크기를 조정할 목적으로 사용

  - 예컨대, (4,4) 입력 데이터에 (3,3) 필터를 적용하면 출력은 (2,2)가 되어 입력보다 줄어든다. 이는 합성곱 연산을 되풀이해        야  하는 심층 신경망에서 문제가 된다. 연산을 거칠 때마가 크기가 작아져 어느 시점에서는 출력 크기가 1이 되어 더 이상          합성곱 연산을 적용할 수 없게 된다.

 

스트라이드(stride)


- 스트라이드 : 필터를 적용하는 위치의 간격

스트라이드가 2인 경우

- 입력크기 (H,W), 필터크기 (FH,FW), 출력크기 (OH,OW), 패딩 P, 스트라이드 S일 때, 출력크기 계산(출력크기는 정수로    나눠 떨어지는 값이어야 한다!)

출력 데이터 크기 구하기(출처:je-d 티스토리)

 

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


- 3차원 데이터는 기존 2차원과 비교해 채널 방향으로 특징 맵이 늘어난 형태

- 입력 데이터와 필터의 합성곱 연산을 채널마다 수행하고, 그 결과를 더해서 하나의 출력을 얻어냄

- 고로, 입력 데이터와 필터의 채널 수 같아야 함.

 

블록으로 생각하기


- 3차원의 합성곱 연산은 데이터와 필터를 직육면체 블록이라고 생각하면 쉽다.

- 3차원 데이터를 다차원 배열로 나타낼 때는 (C, H, W)=(채널, 높이, 너비) 순으로 쓴다.

- 위의 예는 출력으로 1장의 특징 맵을 내보냄.

- 여러 장의 특징 맵을 출력으로 하려면? 필터를 FN개 쓰면 출력 맵도 FN개 생성됨.

  - 이 때, 필터의 가중치 데이터는 4차원

  - 편향은 채널 하나에 값 하나씩 대응

여러 필터를 사용한 합성곱 연산
합성곱 연산의 처리 흐름(편향 추가)

 

배치 처리


- 합성곱 연산에서 배치처리를 하려면, 데이터를 4차원으로 저장하면 됨.

  - (N, C, H, W) = (데이터 수, 채널 수, 높이, 너비)

- 신경망에 4차원 데이터가 하나 흐를 때마다 데이터 N개에 대한 합성곱 연산이 이루어지는 것

 

풀링 계층


- 풀링 : 가로, 세로 방향의 공간을 줄이는 연산(2x2이상되는 크기의 영역을 원소 하나로 집약)

- 풀링 연산은 전체 매개변수의 수를 크게 줄인다.

- 풀링의 이론적 측면은 계산된 특징이 이미지 내의 위치에 대한 변화에 영항을 덜 받기 때문이다. 예를 들어 이미지의 우측 상단에서 눈을 찾는 특징은, 눈이 이미지의 중앙에 위치하더라도 크게 영향을 받지 않아야 한다. 그렇기 때문에 풀링을 이용하여 불변성(invariance)을 찾아내서 공간적 변화를 극복할 수 있다.

- max pooling : 대상 영역에서 최댓값을 취함.

- average pooling : 대상 영역의 평균값을 취함.

- 일반적으로, 풀링의 윈도우 크기 = 스트라이드

  - ex) 윈도우가 3x3이면 스트라이드는 3

풀링 연산(출처:je-d 티스토리)

 

풀링 계층의 특징


- 학습해야 할 매개변수가 없다 : 대상영역에서 최댓값이나 평균을 취하는 명확한 처리이므로

- 채널 수가 변하지 않는다 : 채널마다 독립적으로 계산하므로

- 입력의 변화에 영향을 적게 받는다(강건하다) : 입력데이터가 조금 변해도 풀링이 이를 흡수해 사라지게 함.

풀링의 강건함

 

im2col로 데이터 전개하기


- 합성곱 연산을 그대로 구현하려면 다중 for문 써야함(매우 귀찬...성능 저조...) 그래서 im2col을 쓴다.

- im2col이란 입력 데이터를 필터링(가중치 계산0하기 좋게 전개하는 함수

- im2col => image to column으로 '이미지에서 행렬로'라는 뜻

- 아래 그림과 같이 입력 데이터에서 필터를 적용하는 영역(3차원 블록)을 한 줄로 늘어놓음. 이 전개를 모든 영역에서 수행.

- 합성곱 연산의 필터 처리 과정 : 필터를 세로로 1열로 전개하고, im2col이 전개한 데이터와 행렬 내적 계산

- 마지막으로 출력 데이터를 reshape(2차원->4차원)

 

풀링 계층 구현하기


- 합성곱 계층과 마찬가지로 im2col을 이용해 입력 데이터 전개

- 단, 채널 쪽이 독립적

입력 데이터에 풀링 적용 영역(2x2)을 전개
풀링 계층 구현의 흐름 : 풀링 적용 영역에서 가장 큰 원소는 회색으로 표시

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)

        # 전개 (1)
        col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
        col = col.reshape(-1, self.pool_h*self.pool_w)

        # 최댓값 (2)
        # axis=0 은 열방향, axis=1은 행방향
        out = np.max(col, axis=1)

        # 성형 (3)
        out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)

        return out

 

CNN 구현하기


1. SimpleConvNet

# coding: utf-8
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 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)]

 

2. SimpleConvNet 학습시키기

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from simple_convnet import SimpleConvNet
from common.trainer import Trainer

# 데이터 읽기
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=False)

# 시간이 오래 걸릴 경우 데이터를 줄인다.
#x_train, t_train = x_train[:5000], t_train[:5000]
#x_test, t_test = x_test[:1000], t_test[:1000]

max_epochs = 20

network = SimpleConvNet(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)
                        
trainer = Trainer(network, x_train, t_train, x_test, t_test,
                  epochs=max_epochs, mini_batch_size=100,
                  optimizer='Adam', optimizer_param={'lr': 0.001},
                  evaluate_sample_num_per_epoch=1000)
trainer.train()

# 매개변수 보존
network.save_params("params.pkl")
print("Saved Network Parameters!")

# 그래프 그리기
markers = {'train': 'o', 'test': 's'}
x = np.arange(max_epochs)
plt.plot(x, trainer.train_acc_list, marker='o', label='train', markevery=2)
plt.plot(x, trainer.test_acc_list, marker='s', label='test', markevery=2)
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()

 

CNN 시각화하기


학습 전과 후의 1번째 층의 합성곱 계층의 가중치 : 가중치의 원소는 실수지만, 이미지에서는 가장 작은 값(0)은 검은색, 가장 큰 값(255)은 흰색으로 정규화하여 표시함

- 위 사진은 MNIST 데이터셋으로 학습한 CNN의 1번째 층을 시각화하여 나타낸 것.

- 학습 전 필터는 무작위로 초기화되고 있어 흑백의 정도에 규칙성 無

- 학습 마친 필터는 검은색으로 점차 변화하는 필터와 덩어리(블롭 ; blob)가 진 필터 등 규칙을 띄는 필터로 변화

- 규칙성 있는 필터는 에지(색상이 바뀐 경계선)와 블롭(국소적으로 덩어리진 영역) 등을 보는 것

- 합성곱 계층의 필터는 이처럼 에지나 블롭 등의 원시적인 정보 추출해 뒷단에 전달

가로 에지와 세로 에지에 반응하는 필터 : 출력 이미지 1은 세로 에지에 흰 픽셀이 나타나고, 출력 이미지 2는 가로 에지에 흰 픽셀이 많이 나온다.

 

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


CNN의 합성곱 계층에서 추출되는 정보. 1번째 층은 에지와 블롭, 3번째 층은 텍스처, 5번째 층은 사물의 일부, 마지막 완전연결 계층은 사물의 클래스(개, 자동차 등)에 뉴런이 반응한다.

- 위의 이미지와 같이 층이 깊어지면서 뉴런이 반응하는 대상이 단순한 모양에서 '고급' 정보로 변화

- 사물의 '의미'를 이해하도록 변화

 

대표적인 CNN


1. LeNet(1998)

현재의 CNN과의 차이

- LeNet은 시그모이드 함수를 활성화 함수로 사용, 현재는 주로 ReLU 사용

- LeNet은 서브샘플링을 하여 중간 데이터의 크기가 작아지지만 현재는 최대 풀링이 주류

- 거의 20년 전에 제안된 '첫 CNN'

 

2. AlexNet(2012)

LeNet과 비교한 AlexNet 차이점

- 활성화 함수로 ReLU 사용

- 드롭아웃 사용

- LRN(Local Response Normalization)이라는 국소적 정규화를 실시하는 계층 사용

 

LeNet과 AlexNet 간에는 네트워크 구성 면에서 큰 차이가 없다. 그러나 이를 둘러싼 환경과 컴퓨터 기술이 큰 진보를 이룬 것. 대량의 데이터를 누구나 얻을 수 있게 되었고, 병렬 계산에 특화된 GPU가 보급되면서 대량의 연산을 고속으로 수행할 수 있게 됨. 즉, 빅 데이터와 GPU가 딥러닝 발전의 큰 원동력.

 

 

 

728x90
300x250

'AI > 밑바닥딥러닝1' 카테고리의 다른 글

오버피팅(overfitting) 방지법 정리  (0) 2020.06.27
[CH8] 딥러닝  (0) 2020.06.23
[CH6] 학습 관련 기술들  (0) 2020.06.21
[CH5] 오차역전파법  (0) 2020.06.20
[CH4] 신경망 학습  (0) 2020.06.19
Comments