본문 바로가기
Computer Science/Deep Learning

[비전공자용] [Python] 직접 딥러닝으로 손글씨 숫자 인식하기 (정확도 99% 이상)

by 롱일스 2020. 7. 30.
반응형

이번 포스트에서는 드디어 심층 신경망이라고 불리는 딥러닝으로 손글씨 숫자를 인식하는 예제를 구현해보려고 합니다.

이전 포스트에서 아주 간단한 CNN 합성곱 신경망을 구현했었는데요. 이번에는 더 깊게 신경망을 구성해보려고 합니다. 사전 지식이 필요하니 이전 포스트를 먼저 읽고 와주세요.

2020/07/28 - [Computer Science/Deep Learning] - [비전공자용] [Python] CNN 합성곱 신경망 구현하기

 

딥러닝으로 손글씨 숫자 인식하기 

 

먼저 계층을 구성할 겁니다.

손글씨 숫자 인식하는 심층 CNN

신경망의 몇 가지 특징을 먼저 정하고 갑시다.

  • 3 X 3의 작은 필터를 사용한 합성곱 계층

  • 활성화 함수: ReLU

  • 완전연결 계층 뒤에 드롭아웃 계층 사용

  • Adam을 사용해 최적화

  • 가중치 초깃값: 'He 초깃값'

위와 같이 계층을 구성하고 이전 포스트에서 소개했던 여러 코드를 이용해서 손글씨 숫자 인식을 해보려 합니다.

(혹시 지금까지 말한 용어들에 대한 사전지식이 없으신 분들은 이전 포스트에 상세히 나와있으니 Deep Learning 카테고리의 포스트를 참고해서 봐주세요!)

CNN 신경망을 파이썬으로 DeepConvNet 클래스로 구현한 코드입니다. 
([출처] Deep Learning from Scratch, ゼロ から作る)

# 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 *


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

 신경망 클래스를 구현했으니 이제 MNIST 손글씨 데이터를 입력 데이터로 주고 얼마나 인식을 잘 하는지 테스트 해보러 가봅시다.

파이썬으로 테스트 코드를 구현하면 아래와 같습니다. 학습에 생각보다 오랜 시간이 걸리니 실행시켜놓고 다른 일을 하시는 걸 추천합니다. (저는 자고 담날에 확인했습니다...)

# 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 deep_convnet import DeepConvNet
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!")

 

결과를 보면, accuracy 정확도가 99.28%가 나옵니다. 손글씨로 쓴 숫자 이미지 100개를 보고 99개는 확실히 무슨 숫자인지 정확히 판단한다는 뜻이죠. 사실 사람에 따라서 다르겠지만, 어떤 사람들 보다는 더 정확하게 인식한다고 볼 수도 있겠습니다. 

신경망을 깊게 구성하면, 정확도가 확실히 높아지는 걸 알 수 있습니다. 이전 포스트에서 아주 간단하게 구성한 CNN 신경망에 비해서 정확도가 높은 걸 보면 이해가 됩니다.


  현재 CNN을 기초로 한 기법들 중에서 MNIST 데이터셋에 대한 정확도 1위는 99.79% 라고 합니다. 오차율이 0.21%밖에 안되는데요. 2013년에 나온 기법이 그 후에 나온 기법들보다 성능이 우수한 건 의외네요. 1위를 한 기법도 합성곱 계층 2개에 완전연결 계층 2개인 신경망인데 무작정 깊다고 해서 정확도가 높아지는 건 아닌 것 같습니다.  

https://rodrigob.github.io/are_we_there_yet/build/classification_datasets_results.html

아마도 단순하게 숫자를 인식하는 데에 그렇게까지 깊은 신경망을 사용하는 건 오히려 과한 것 같다고 볼 수 있습니다. 그래서 더 깊게 신경망을 구현해도 정확도가 더 높아지진 않았던 것 같습니다.

위의 캡처 화면을 보면, 정확도를 높이기 위해 사용한 기법 Method들을 엿볼 수 있습니다. DropConnect, Multi-column, Augmented pattern 등등이 보이는데요. 이 중에서 데이터 확장 Data Augmentation은 정확도 개선에 아주 효과적이라는 게 입증된 방법입니다.

데이터 확장 방법은 입력 이미지를 알고리즘을 이용해서 인위적으로 확장합니다. 

데이터 확장의 예

데이터가 몇 개 없는 상태에서 학습할 때 비슷비슷한 데이터를 임의로 만들어서 데이터 수를 늘릴 수 있으니 학습에 좋겠죠? 손글씨 데이터의 경우에는 원본을 회전하거나 이동시키거나 약간 잘라내기도 하고 좌우 반전을 하기도 하는 등의 변화를 줘서 데이터 수를 늘립니다.


그래도 층을 깊게 하는 것은 중요합니다.

  1. 신경망을 깊게 하면 학습해야 할 문제를 계층적으로 분해할 수 있고, 각 층이 학습해야할 문제를 더 단순한 문제로 대체할 수 있게 됩니다. 

  2. 층을 깊게 하면 정보를 계층적으로 전달할 수 있습니다.

예를 들어서, 처음 층은 edge 에지 학습에 전념하도록 학습할 수 있고, 다음 층은 패턴 학습에 전념하도록 학습하면 어떤 복잡한 이미지나 영상을 만났을 때 체계적으로 하나씩 간단한 구조로 학습할 수 있습니다.

 에지를 추출한 층의 다음 층에서는 에지 정보를 사용할 수 있으니 더 복잡한 패턴을 효과적으로 학습할 수 있습니다. 풀기 쉬운 단순한 문제로 분해해서 효율적인 학습을 가능하게 하니 층을 깊게 하는 게 복잡한 입력 데이터를 만났을 때는 더 유용하겠죠?

 

그럼 다음 포스트에서는 딥러닝 트렌드에 대해서 살펴보도록 하겠습니다!

2020/07/30 - [Computer Science/Deep Learning] - [비전공자용] 딥러닝이 몇 년 전부터 뜨고 있는 이유 (feat. AlexNet) VGG, GoogLeNet, ResNet은 무엇?

 

[비전공자용] 딥러닝이 몇 년 전부터 뜨고 있는 이유 (feat. AlexNet) VGG, GoogLeNet, ResNet은 무엇?

이번 포스트에서는 딥러닝이 어떻게 주목받게 되었고 왜 많은 사람들이 딥러닝에 열중하는 지 그 이유에 대해서 알아보려고 합니다. 추가로 핫한 딥러닝 기법에 대해서도 맛보기로 소개하겠습�

huangdi.tistory.com

 

모든 코드와 내용은 아래 출처를 기반으로 작성한 것임을 알립니다.
[출처] Deep Learning from Scratch, ゼロ から作る

728x90
반응형