본문 바로가기

딥러닝/밑바닥부터 배우는 딥러닝

7장. 합성곱 신경망(CNN)

7.1 전체 구조

신경망 합성곱 신경망
Affine 계층(완전연결층) 합성곱 계층(convolutional layer), 풀링 계층(pooling layer)

 

7.2 합성곱 계층

1) 완전 연결 계층의 문제점

- 데이터의 형상이 무시된다. 

- C, H, W이 무시되고 하나의 긴 벡터가 affine 계층에 입력된다.  

 

특징 맵 (feature map)

: CNN에서 합성곱 계층의 입출력 데이터, input feature map과 output feature map이 있음.

 

2) 합성곱 연산

- 필터 연산

입력 (4, 4) * 필터 (3, 3) -> 출력 (2,2)

필터의 윈도우를 옮겨가며 단일 곱셈-누산 연산(fused multiply-add, FMA)을 반복함.

필터의 매개변수를 가중치라고 생각할 수 있다.

 

편향은 필터를 적용한 후 더해진다. 

 

3) 패딩

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

폭이 1인 패딩을 입력 데이터에 0값으로 채운 결과이다.

 

4) 스트라이드

: 필터를 적용하는 위치의 간격, 필터를 적용하는 윈도우가 몇 칸씩 이동할 것인지

+) 입력 크기, 필터 크기, 패딩, 스트라이드에 따른 출력 크기

입력 크기: (H,W)

필터 크기: (FH, FW)

패딩: P

스트라이드: S

출력 크기: (OH, OW)

 

 

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

입력데이터의 차원이 (C, H, W)로 차원 수가 3차원으로 추가됐다. 채널마다 입력데이터와 필터의 합성곱 연산을 수행하고 합쳐준다. 

삼차원 합성곱 연산을 수행할 때 입력 데이터의 채널 수와 필터의 채널 수가 같아야 한다.

 

6) 블록으로 생각하기

입력이 삼차원일 때 합성곱 연산을 블록으로 생각하여 수행한다.

 

필터를 FN개 사용하는 경우: 출력이 FN개 생성된다.

 

필터를 FN개 사용하면서 편향도 추가한 경우: 입력데이터에 필터를 적용한 결과에 편향을 추가한다. 

편향은 채널별로 한개씩 있다.

 

7) 배치 처리

N개의 데이터를 배치 처리 하는 경우, 입력데이터와 출력데이터의 차원 수가 4차원으로, 데이터 수에 대한 차원 수가 추가된다. 신경망이 한 번 흐를 때마다 N개의 데이터에 대해 합성곱 연산이 수행된다.

 

7.3 풀링 계층

: 가로, 세로 방향의 공간을 줄이는 방법

2*2 최대 풀링(max pooling): 2*2 영역 안에서 최댓값만 가져오는 방식

c.f) 평균 풀링(average pooling)

 

풀링 계층의 특징 

  • 학습해야 할 매개변수가 없다: 최댓값, 평균을 구하는 방식은 매개변수 학습이 불필요하다.
  • 채널 수가 변하지 않는다: 채널마다 풀링을 독립적으로 계산한다.
  • 입력의 변화에 영향을 적게 받는다: 입력 데이터가 조금 변해도 풀링 결과는 크게 변하지 않는다.

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

1) 4차원 배열 구현하기

x=np.random.rand(10,1,28,28)
x.shape #(10,1,28,28)

4차원의 입력 데이터를 무작위로 구현한 것이다. N=10의 배치처리를 할 것이고, 채널 수는 C=1, 세로 H=28, 가로 W=28의 shape을 가지고 있다. 

 

2) im2col로 데이터 전개하기

im2col

  • 입력 데이터를 필터링 하기 좋게 펼치는 함수, 3차원/4차원 데이터를 2차원으로 변환한다.
  • 입력 데이터에 대해 필터의 적용 영역이 겹치게 된다.
  • 원래 블록의 원소 수 < im2col로 전개한 원소 수 
  • 메모리를 많이 소모하는 단점이 있지만 큰 행렬 묶어서 계산하기 좋다.
  • 합성곱 계층과 풀링 계층에 들어오는 input을 im2col로 구현한다.

입력 데이터를 im2col로 펼치고, FN개의 필터에 대해 하나의 열로 전개하여 둘을 행렬 곱하여 2차원의 출력 데이터를 얻고 이를 4차원으로 reshape 한다.

 

3) 합성곱 계층 구현하기

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

im2col은 input data의 (N, C, H, W), FH, FW, S, P를 받아서 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 = int(1 + (H + 2 * self.pad - FH) / self.stride)
        out_w = int(1 + (W + 2 * self.pad - FW) / self.stride)

        # 입력 데이터와 필터를 2차원 배열로 전개하고 내적한다.
        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

        # reshape에서 -1 : 원소 개수에 맞춰 적절하게 묶어줌.
        # transpose : 다차원 배열의 축 순서를 바꿔줌(N,H,W,C) -> (N,C,H,W)
        out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)

        return out

합성곱 계층에서 init에서 W, b, S, P를 받아온다.

forward에서 input에서 filter을 통해 output을 계산해야 한다.

W의 shape을 (FN, C, FH, FW)으로, x의 shape을 (N, C, H, W)으로 만들고 OH, OW도 공식을 통해 계산한다.

col은 input을 im2col로 펼친 2차원 데이터, col_W은 W의 필터들을 열로 펼친 2차원 데이터이다.

col은 im2col로 처리해주고, col_W는 FN이 열 수가 되도록 reshape 한다.

output은 col과 col_W의 행렬곱에 편향인 b를 더한 것이다.

이 때 out은 3차원이고 이를 4차원 데이터로 reshape 해준다. 

transpose는 다차원 배열의 축 순서를 바꿔주는 함수이다. 

c.f) 역전파에서 im2col을 역으로 처리할 때는 col2im을 사용한다.

 

4) 풀링 계층 구현하기

풀링 적용 영역을 채널마다 다르게 처리해야 한다. 입력 데이터에서 채널들을 전개하고, 풀링 적용 영역을 행으로 길게 만들고, 행별로 최댓값을 뽑아서 다시 reshape 한다.

  • 입력 데이터를 전개한다
  • 행별 최댓값을 구한다
  • 적절한 모양으로 reshape 한다.
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)

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

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

        return out

 init에서 pooling 영역이 지정될 FH, FW, S, P를 입력받는다.

forward에서 (N, C, H, W)로 x의 shape을 잡고, OH, OW를 계산한다. im2col으로 col을 전개해준다. PH*PW가 열 수가 되도록 reshape 해준다. 

col의 행 별 최댓값을 찾아준 뒤 4차원이 되도록 reshape 해준다. 

 

7.5 CNN 구현하기

위와 같이 합성곱 계층 1개, affine 계층 2개를 가지는 신경망으로 손글씨를 예측하는 model을 만들겠다.

 

패키지, 데이터 불러오기

import sys
import os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
import matplotlib.pyplot as plt
from collections import OrderedDict
from common.layers import *
from common.gradient import numerical_gradient
from dataset.mnist import load_mnist
from common.trainer import Trainer

 

단계별로 나누어 simpleconvnet 클래스를 구현할 것이다.

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))

conv_param은 합성곱 계층의 하이퍼 파라미터를 딕셔너리 안에 담아놓은 것으로 (conv 계층에서) FN, FS(FH, FW), S, P이 포함된다. conv 계층의 FN, FS, S, P를 filter_~ 변수에 꺼내놓고, conv_output_size(OH, OW)와 pool_output_size를 계산한다. 

        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)

 

  각 층에서 사용할 가중치의 매개변수를 초기화 하는 과정이다. 첫번째 층에서는 CNN을 위한 W1과 b1을, 두번째, 세번째 층에서는 affine 계층을 위한 W2, b2, W3, b3이 필요하다.

W1은 (FN, C, FH, FW)의 shape을 가지고, b1은 (FN, 1, 1)의 shape을 가진다.

 

        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()

layers에 순서가 있는 딕셔너리인 OrderedDict를 부여하고 층별로 layer를 설정한다. 

[Conv -> Relu -> Pool] -> [Affine1 -> Relu] -> [Affine2 -> Softmax] 

Conv: (W1, b1, S, P), Pool: (PH, PW, S), Affine1: (W2, b2), Affine2: (W3, b3)

    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 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]

predict: x가 각 layer을 거치면서 수행한 forward propagation의 결과

loss: 예측값 y와 정답값 t을 last_layer(softmax with loss)에서 수행한 결과

accuracy: batch에 따라 예측값 y와 정답값 tt를 비교했을 때의 정확도

tx: x에서 batch만큼 가져온 것, tt: 정답값 t에서 batch만큼 가져온 것

y: tx에 대해 softmax를 처리한 예측값

acc: 예측값과 정답값 tt를 비교했을 때 정확도

    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

각 층에서 가중치에 대한 역전파 값을 grads에 저장한다.

 

7.6 CNN 시각화 하기

1) 1번째 층의 가중치 시각화

W1은 conv 계층 안에 있는 가중치 값으로 (FN, C, FH, FW)의 shape을 가지고 있다. 7.5에서 다룬 예시는 (30, 1, 5, 5)에서 C=1으로 데이터가 흑백으로 들어온 경우에 대한 것이다. 학습 전, W1이 랜덤한 값으로 초기화 되었을 때와, 학습 후 W1:=W1-learning rate * dW1으로 업데이트 되었을 때를 둘 다 시각화 했다.

학습 전엔 흑백의 정도에 규칙성이 없지만 학습 후에는 필터가 에지(색상이 바뀐 경계선)와 블롭(국소적으로 덩어리 진 영역)을 보고 있다. 이렇게 학습한 매개변수는 input에서 에지와 블롭과 같은 원시적인 정보를 인식할 수 있다. 

 

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

계층이 깊어질 수록 더 복잡하고 추상화 된 정보가 추출된다. 

 

7.7 대표적인 CNN

1) LeNet

LeNet   현재의 CNN
sigmoid 활성화 함수 ReLU
subsampling 원소 수 줄이기 max pooling

2) AlexNet

[Conv -> Pooling] x n -> [Affine]

  • 활성화 함수로 ReLU를 이용한다.
  • LRN으로 국소적 정규화를 실시하는 계층을 이용한다.
  • drop out

'딥러닝 > 밑바닥부터 배우는 딥러닝' 카테고리의 다른 글

6장. 학습 관련 기술들  (0) 2021.02.23
5장. 오차역전파법  (1) 2021.02.10
4장 신경망 학습  (0) 2021.02.01
3장 신경망  (0) 2021.02.01