Skip to content

Latest commit

 

History

History
346 lines (241 loc) · 22 KB

220216.md

File metadata and controls

346 lines (241 loc) · 22 KB

Day 124

Deep Learning from Scratch

8. 딥러닝

더 깊게

더 깊은 신경망으로

이젠 손글씨 숫자를 인식하는 심층 CNN을 제작할 것이고 구조는 다음과 같다.

fig 8-1

여기서 사용하는 합성곱 계층은 모두 3x3 크기의 작은 필터로, 층이 깊어지면서 채널 수가 더 늘어나는 것이 특징이다. 합성곱 계층의 채널 수는 앞 계층에서부터 순서대로 16, 16, 32, 32, 64, 64로 늘어간다는 것이다. 또한 그림과 같이 풀링 계층을 추가하여 중간 데이터의 공간 크기를 점차 줄여간다. 마지막 단의 완전연결 계층에서는 드롭아웃 계층을 사용한다.

가중치 초깃값으로 He 초깃값을 사용하고, 가중치 매개변수 갱신에는 Adam을 사용한다.

이 신경망의 특징

  • 3x3의 작은 필터를 사용한 합성곱 계층
  • 활성화 함수는 ReLU
  • 완전연결 계층 뒤에 드롭아웃 계층 사용
  • Adam을 사용해 최적화
  • 가중치 초깃값은 'He의 초깃값'

이를 구현하면 다음과 같다.

import pickle
import numpy as np
from collections import OrderedDict
from common.layers import *


class DeepConvNet:
    """정확도 99% 이상의 고정밀 합성곱 신경망
    네트워크 구성은 아래와 같음
        conv - relu - conv- relu - pool -
        conv - relu - conv- relu - pool -
        conv - relu - conv- relu - pool -
        affine - relu - dropout - affine - dropout - softmax
    """
    def __init__(self, input_dim=(1, 28, 28),
                 conv_param_1 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
                 conv_param_2 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
                 conv_param_3 = {'filter_num':32, 'filter_size':3, 'pad':1, 'stride':1},
                 conv_param_4 = {'filter_num':32, 'filter_size':3, 'pad':2, 'stride':1},
                 conv_param_5 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
                 conv_param_6 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
                 hidden_size=50, output_size=10):
        # 가중치 초기화===========
        # 각 층의 뉴런 하나당 앞 층의 몇 개 뉴런과 연결되는가(TODO: 자동 계산되게 바꿀 것)
        pre_node_nums = np.array([1*3*3, 16*3*3, 16*3*3, 32*3*3, 32*3*3, 64*3*3, 64*4*4, hidden_size])
        wight_init_scales = np.sqrt(2.0 / pre_node_nums)  # ReLU를 사용할 때의 권장 초깃값
        
        self.params = {}
        pre_channel_num = input_dim[0]
        for idx, conv_param in enumerate([conv_param_1, conv_param_2, conv_param_3, conv_param_4, conv_param_5, conv_param_6]):
            self.params['W' + str(idx+1)] = wight_init_scales[idx] * np.random.randn(conv_param['filter_num'], pre_channel_num, conv_param['filter_size'], conv_param['filter_size'])
            self.params['b' + str(idx+1)] = np.zeros(conv_param['filter_num'])
            pre_channel_num = conv_param['filter_num']
        self.params['W7'] = wight_init_scales[6] * np.random.randn(64*4*4, hidden_size)
        self.params['b7'] = np.zeros(hidden_size)
        self.params['W8'] = wight_init_scales[7] * np.random.randn(hidden_size, output_size)
        self.params['b8'] = np.zeros(output_size)

        # 계층 생성===========
        self.layers = []
        self.layers.append(Convolution(self.params['W1'], self.params['b1'], 
                           conv_param_1['stride'], conv_param_1['pad']))
        self.layers.append(Relu())
        self.layers.append(Convolution(self.params['W2'], self.params['b2'], 
                           conv_param_2['stride'], conv_param_2['pad']))
        self.layers.append(Relu())
        self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
        self.layers.append(Convolution(self.params['W3'], self.params['b3'], 
                           conv_param_3['stride'], conv_param_3['pad']))
        self.layers.append(Relu())
        self.layers.append(Convolution(self.params['W4'], self.params['b4'],
                           conv_param_4['stride'], conv_param_4['pad']))
        self.layers.append(Relu())
        self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
        self.layers.append(Convolution(self.params['W5'], self.params['b5'],
                           conv_param_5['stride'], conv_param_5['pad']))
        self.layers.append(Relu())
        self.layers.append(Convolution(self.params['W6'], self.params['b6'],
                           conv_param_6['stride'], conv_param_6['pad']))
        self.layers.append(Relu())
        self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
        self.layers.append(Affine(self.params['W7'], self.params['b7']))
        self.layers.append(Relu())
        self.layers.append(Dropout(0.5))
        self.layers.append(Affine(self.params['W8'], self.params['b8']))
        self.layers.append(Dropout(0.5))
        
        self.last_layer = SoftmaxWithLoss()

    def predict(self, x, train_flg=False):
        for layer in self.layers:
            if isinstance(layer, Dropout):
                x = layer.forward(x, train_flg)
            else:
                x = layer.forward(x)
        return x

    def loss(self, x, t):
        y = self.predict(x, train_flg=True)
        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, train_flg=False)
            y = np.argmax(y, axis=1)
            acc += np.sum(y == tt)

        return acc / x.shape[0]

    def gradient(self, x, t):
        # forward
        self.loss(x, t)

        # backward
        dout = 1
        dout = self.last_layer.backward(dout)

        tmp_layers = self.layers.copy()
        tmp_layers.reverse()
        for layer in tmp_layers:
            dout = layer.backward(dout)

        # 결과 저장
        grads = {}
        for i, layer_idx in enumerate((0, 2, 5, 7, 10, 12, 15, 18)):
            grads['W' + str(i+1)] = self.layers[layer_idx].dW
            grads['b' + str(i+1)] = self.layers[layer_idx].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, layer_idx in enumerate((0, 2, 5, 7, 10, 12, 15, 18)):
            self.layers[layer_idx].W = self.params['W' + str(i+1)]
            self.layers[layer_idx].b = self.params['b' + str(i+1)]

훈련은 아래의 코드를 실행시키면 된다.

import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from common.trainer import Trainer

(x_train, t_train), (x_test, t_test) = load_mnist(flatten=False)

network = DeepConvNet()  
trainer = Trainer(network, x_train, t_train, x_test, t_test,
                  epochs=20, mini_batch_size=100,
                  optimizer='Adam', optimizer_param={'lr':0.001},
                  evaluate_sample_num_per_epoch=1000)
trainer.train()

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

=============== Final Test Accuracy ===============
test acc:0.9949

정확도가 99.49%로 매우 높게 나온 것을 확인할 수 있다.

이 신경망이 잘못 인식할 확률 0.51%로은 매우 낮은데 인식에 실패한 예는 다음과 같다.

fig 8-2

사진의 왼쪽 위는 정답, 오른쪽 아래는 추론 결과이다.

보면 인간도 판단하기 어려운 이미지임을 알 수 있다.

정확도를 더 높이려면

데이터 확장(data augmentation)은 이미지(훈련 이미지)를 알고리즘을 동원해 '인위적'으로 확장한다. 아래의 그림과 같이 입력 이미지를 회전하거나 세로로 이동하는 등 미세한 변화를 주어 이미지의 개수를 늘리는 것으로 데이터가 몇 개 없을 때 특히 효과적인 수단이다.

fig 8-4

그 외에도 이미지 일부를 잘라내는 crop이나 좌우를 뒤집는 flip등의 방법으로 이미지를 확장할 수도 있다. 다만, flip은 이미지의 대칭성을 고려하지 않아도 되는 경우에만 쓸 수 있다.

일반적인 이미지에는 밝기 등의 외형 변화나 확대, 축소 등의 스케일 변화도 효과적이다.

깊게 하는 이유

층을 깊게할 경우 신경망의 매개변수 수가 줄어든다는 이점이 있다. 층을 깊게 한 신경망은 깊지 않은 경우보다 적은 매개변수로 같거나 그 이상의 수준의 표현력을 달성할 수 있다.

fig 8-5

위는 5x5 필터로 구성된 합성곱 계층이다. 위 그림에서는 각각의 출력 노드는 입력 데이터의 5x5 크기 영역에서 계산된다.

fig 8-6

위 그림은 3x3의 합성곱 연산을 2회 반복하는 경우이다. 이 경우 출력 도느 하나는 중간 데이터의 3x3 영역에서 계산된다. 이 중간 데이터의 3x3 크기의 영역은 5x5 크기의 영역에서 계산되어 나온다. 즉, 이 그림의 ㅣ출력 데이터는 입력 데이터의 5x5 영역을 '보고' 계산하게 된다.

위의 내용을 정리하면 5x5의 합성곱 연산 1회는 3x3의 합성곱 연산을 2회 수행하여 대체할 수 있다는 것이다. 전자는 매개변수 수가 25개(5x5)인 반면, 후자는 총 18개(2x3x3)이며, 매개변수 수는 층을 반복할수록 적어진다. 이 개수의 차이는 층이 깊어질수록 커진다.

작은 필터를 겹쳐 신경망을 깊게 할 때의 장점은 매개변수 수를 줄여 넓은 수용 영역(receptive field)을 소화할 수 있다는 데 있다. 수용 영역은 뉴런에 변화를 일으키는 국소적인 공간 영역을 말한다. 층을 거듭하면서 ReLU 등의 활성화 함수를 합성곱 계층 사이에 끼움으로써 신경망의 표현령이 개선된다.

학습의 효율성도 층을 깊게하는 것의 이점이다. 층을 깊게 함으로써 학습 데이터의 양을 줄여 학습을 고속으로 수행할 수 있다는 뜻이다.

예를 들어 개를 인식하는 문제라면 신경망이 얕으면 합성곱 계층은 특징 대부분을 한 번에 이해해야 한다. 그래서 변화가 풍부하고 많은 학습 데이터가 필요하게 되어 학습 시간이 오래 걸리게 된다. 반면, 신경망을 깊게하면 학습해야 할 문제를 계층적으로 분해할 수 있다. 각 층이 학습해야 할 문제를 더 단순한 문제로 대체할 수 있는 것이다.

또한, 층을 깊게하면 정보를 계층적으로 전달할 수 있다. 예를 들면 에지를 추출한 층의 다음 층은 에지 정보를 쓸 수 있고, 더 고도의 패턴을 효과적으로 학습하리라 기대할 수 있다.


딥러닝의 유명 신경망

VGG

VGG는 합성곱 계층과 풀링 계층으로 구성되는 CNN이다. 아래의 그림처럼 비중 있는 층(합성곱 계층, 완전연결 계층)을 모두 16층(혹은 19층)으로 심화한 것이 특징이다.

fig 8-9

주목할 점은 3x3의 작은 필터를 사용한 합성곱 계층을 연속으로 거친다는 것이다. 그림에서 보듯 합성곱 계층을 2~4회 연속으로 풀링 계층을 두어 크기를 절반으로 줄이는 처리를 반복하고, 마지막에는 완전연결 계층을 통과시켜 결과를 출력한다.

GoogLeNet

GoogLeNet의 구성은 다음과 같다. 그림의 사각형이 합성곱 계층과 풀링 계층 등의 계층을 나타낸다.

fig 8-10

GoogLeNet은 세로 방향 깊이뿐 아니라 가로 방향도 깊다는 점이 특징이다.

GoogLeNet에는 가로 방향에 '폭'이 있다. 이를 인셉션 구조라 하며, 그 기반 구조는 다음과 같다.

fig 8-11

인셉션 구조는 위 그림과 같이 크기가 다른 필터(와 풀링)를 여러 개 적용하여 그 결과를 결합한다. 이 인셉션 구조를 하나의 빌딩 블록(구성요소)으로 사용하는 것이 GoogLeNet의 특징이다. 또한, GoogLeNet에서는 1x1 크기의 필터를 사용한 합성곱 계층을 많은 곳에서 사용한다. 이 1x1의 합성곱 연산은 채널 쪽으로 크기를 줄이는 것으로, 매개변수 제거와 고속 처리에 기여한다.

ResNet

ResNet(Residual Network)은 지금까지보다 층을 더 깊게 할 수 있는 특별한 장치가 있다.

층을 깊게 하는 것이 성능 향상에 중요하지만 딥러닝의 학습에서는 층이 지나치게 깊으면 학습이 잘 되지 않고, 오히려 성능이 떨어지는 경우도 많다. ResNet에서는 이 문제를 해결하기 위해 스킵 연결(skip connection)을 도입한다. 이 구조가 층의 깊이에 비례해 성능을 향상시킬 수 있게 한 핵심이다.

스킵 연결이란 아래 그림과 같이 입력 데이터를 합성곱 계층을 건너뛰어 출력에 바로 더하는 구조를 말한다.

fig 8-12

weigth layer는 합성곱 계층을 말한다.

그림을 보면 입력 x를 연속한 합성곱 계층을 건너뛰어 출력에 바로 연결한다. 이 단축 경로가 없었다면 두 합성곱 계층의 출력이 F(x)가 되나, 스킵 연결로 인해 F(x) + x가 되는 게 핵심이다.

스킵 연결은 입력 데이터를 그대로 흘리는 것으로, 역전파 때도 상류의 기울기를 그대로 하류로 보낸다. 그래서 스킵 연결로 기울기가 작아지거나 지나치게 커질 걱정 없이 앞 층에 의미 있는 기울기가 전해지리라 기대할 수 있다. 층을 깊게 할수록 기울기가 작아지는 소실 문제를 스킵 연결이 줄여준다는 것이다.

ResNet은 VGG 신경망을 기반으로 스킵 연결을 도입하여 층을 깊게 한 것이다.

fig 8-13


딥러닝 고속화

처리 시간

아래 그림은 AlexNet의 순전파에서 각 층이 소비하는 시간을 그래프로 표현한 것이다.

fig 8-14

왼쪽은 GPU, 오른쪽은 CPU를 사용한 경우이고, conv는 합성곱, pool은 풀링, fc는 완전연결, norm은 정규화 계층이다.

보면 AlexNet에서는 합성곱 계층에서 오랜 시간을 소요한다.

GPU를 활용한 고속화

GPU로 범용 수치 연산을 수행하는 것을 GPU 컴퓨팅이라고 한다.

딥러닝에서는 대량의 단일 곱셈-누산(도는 큰 행렬의 곱)을 수행해야 한다. 이런 대량 병렬 연산은 GPU의 특기이다.

아래 그림은 AlexNet의 학습 시간을 CPU와 GPU에서 비교한 결과이다.

fig 8-15

CPU에서는 40여 일이 걸리는 것을 GPu로는 6일까지 단축한 것을 확인할 수 있다.

분산 학습

1회 학습에 걸리는 시간을 단축하고자 딥러닝 학습을 수평 확장(scale out)하자는 아이디어가 분산 학습이다.

아래는 텐스플로의 분산학습 성능을 그래프로 나타낸 것이다.

fig 8-16

GPU 수가 늘어남에 따라 학습이 빨라지는 것을 확인할 수 있다.

연산 정밀도와 비트 줄이기

메모리 용량과 버스 대역폭 등이 딥러닝 고속화에 병목이 될 수 있다. 메모리 용량 면에서는 대량의 가중치 매개변수와 중간 데이터를 메모리에 저장해야 한다는 것을 생각해야 한다. 버스 대역폭 면에서는 GPU나 CPU의 버스를 흐르는 데이터가 많아져 한계를 넘어서면 병목이 된다. 이를 고려해 네트워크로 주고받는 데이터의 비트 수는 최소로 만드는 것이 바람직하다.

컴퓨터에서는 64비트나 32비트 부동소수점 수를 사용해 실수를 표현한다. 많은 비트를 사용할 수록 계산 오차는 줄어드나, 계산에 드는 비용과 메모리 사용량이 늘고 버스 대역폭에 부담을 준다.

하지만 딥러닝은 높은 수치 정밀도(수치를 몇 비트로 표현하느냐)를 요구하지 않는다. 이는 신경망의 견고성에 따른 특성이다. 덕분에 신경망을 흐르는 데이터를 퇴화시켜도 출력에 주는 영향은 적다.


딥러닝의 활용

사물 검출

사물 검출은 이미지 속에 담긴 사물의 위치와 종류를 알아내는 기술이다.

fig 8-17

R-CNN(Regions with Convolutional Neural Network)가 유명하다. 처리 흐름은 다음과 같다.

fig 8-18

R-CNN은 2번과 3번을 주목해야 한다. 먼저 사물이 위치한 영역을 찾아내고, 추출한 각 영역에 CNN을 적용하여 클래스를 분류하는 것이다.

후보 영역 추출까지 CNN으로 처리하는 Faster R-CNN 기법도 있다. 이 기법은 모든 일을 하나의 CNN에서 처리하기 때문에 빠르다.

분할

분할(segmentation)이란 이미지를 픽셀 수준에서 분류하는 문제이다. 아래 그림과 같이 픽셀 단위로 객체마다 채색된 지도(supervised) 데이터를 사용해 학습한다. 그리고 추론할 때 입력 이미지의 모든 픽셀을 분류한다.

fig 8-19

좌측이 입력 이미지, 우측이 지도용 이미지이다.

신경망을 이용해 분할하는 가장 단순한 방법은 모든 픽셀 각각을 추론하는 것이다. 이 방식은 픽셀의 수만큼 forward 처리를 해야 하여 긴 시간이 걸리고, 합성곱 연산에서 많은 영역을 쓸데없이 다시 계산하는 것이 문제가 된다. 이런 낭비를 줄여주는 기법으로 FCN(Fully Convolutional Network)이 고안되었다. 이는 단 한 번의 forward 처리로 모든 픽셀의 클래스를 분류해주는 기법이다.

fig 8-20

위 그림이 FCN의 전체 그림이다.

일반적인 CNN이 완전연결 계층을 이용하는 반면, FCN은 이 완전연결 계층을 '같은 기능을 하는 합성곱 계층'으로 바꾼다. 사물 인식에서 사용한 신경망의 완전연결 계층에서는 중간 데이터의 다차원 형태를 1차원으로 변환하여 한 줄로 늘어선 노드들이 처리했지만, FCN에서는 다차원 형태를 유지한 채 마지막 출력까지 처리할 수 있다.

FCN은 마지막에 공간 크기를 확대하는 처리를 도입했다는 것도 특징이다. 이 확대 처리로 인해 줄어든 중간 데이터를 입력 이미지와 같은 크기까지 단번에 확대할 수 있다. 이 확대는 이중 선형 보간(gilinear interpolation)에 의한 선형 확대이다. FCN에서는 이 선형 확대를 역합성곱(deconvolution)연산으로 구현했다.

완전연결 계층과 같은 일을 합성곱 계층을 구현할 수 있다. 예를 들어 입력 크기가 32x10x10(채널 32개, 높이 10, 너비 10)인 데이터에 대한 완전연결 계층은 필터 크기가 32x10x10인 합성곱 계층으로 대체할 수 있다. 만약, 완전연결 계층의 출력 노드가 100개라면 합성곱 계층에서는 기존의 필터를 100개 준비하면 완전히 같은 처리를 할 수 있다.

사진 캡션 생성

사진을 주면 자동으로 사진 캡션을 생성하는 것이다.

fig 8-21

이 분야는 NIC(Nerural Image Caption) 모델이 있다. NIC는 아래의 그림처럼 심층 CNN과 자연어를 다루는 순환 신경망(Recurrent Neural Network, RNN)으로 구성된다. RNN은 순환적 관계를 갖는 신경망으로 자연어나 시계열 데이터 등의 연속된 데이터를 다룰 때 많이 활용한다.

fig 8-22

NIC는 CNN으로 사진에서 특징을 추출하고, 그 특징을 RNN에 넘긴다. RNN은 이 특징을 초깃값으로 해서 텍스트를 '순환적'으로 생성한다.