본문 바로가기
  • 비둘기다
  • 비둘기다
  • 비둘기다
AI/Deep learning from Scratch

[머신러닝, 딥러닝] 신경망 (4) - MNIST 손글씨 데이터 인식

by parzival56 2023. 1. 23.

MNIST 손글씨 숫자 인식

 

일반적으로 출력을 받을 때는 우리가 출력받기로 설정한 클래스의 개수만큼 출력층의 뉴런 수를 정합니다. 예를 들어 0부터 9까지의 수를 숫자에 맞게 분류하기로 한다면 출력층의 뉴런 수를 10개로 잡으면 됩니다.

 

이번 페이지에서 사용할 MNIST 데이터셋은 손글씨 숫자 이미지의 집합입니다. 

MNIST 손글씨 숫자 데이터는 위와 같은 형식으로 이루어져 있으며 누군가가 예쁘게 쓴 실제 손글씨입니다. 

위의 숫자들이 뭔가 흐릿하게 보이는 이유는 픽셀로 이루어져 있기 때문입니다. 사진들의 크기는 각각 28 x 28로 작은 크기라서 더욱 선명하게 픽셀자국이 보입니다. 각 픽셀은 0에서 255까지의 값을 취합니다. 

 

가장 먼저 해주는 작업은 이 픽셀 사진들을 넘파이 배열로 바꿔주는 것입니다. 위에서 말씀드렸듯이 각 픽셀은 0에서 255까지의 값을 가지기 때문에 이들을 넘파이 배열로 만들 수 있습니다. 

 

아래의 코드가 MNIST 이미지 데이터를 넘파이 배열로 변환해주는 코드입니다. 

try:
    import urllib.request
except ImportError:
    raise ImportError('You should use Python 3.x')
import os.path
import gzip
import pickle
import os
import numpy as np


url_base = 'http://yann.lecun.com/exdb/mnist/'
key_file = {
    'train_img':'train-images-idx3-ubyte.gz',
    'train_label':'train-labels-idx1-ubyte.gz',
    'test_img':'t10k-images-idx3-ubyte.gz',
    'test_label':'t10k-labels-idx1-ubyte.gz'
}

dataset_dir = os.path.dirname(os.path.abspath(__file__))
save_file = dataset_dir + "/mnist.pkl"

train_num = 60000
test_num = 10000
img_dim = (1, 28, 28)
img_size = 784


def _download(file_name):
    file_path = dataset_dir + "/" + file_name
    
    if os.path.exists(file_path):
        return

    print("Downloading " + file_name + " ... ")
    urllib.request.urlretrieve(url_base + file_name, file_path)
    print("Done")
    
def download_mnist():
    for v in key_file.values():
       _download(v)
        
def _load_label(file_name):
    file_path = dataset_dir + "/" + file_name
    
    print("Converting " + file_name + " to NumPy Array ...")
    with gzip.open(file_path, 'rb') as f:
            labels = np.frombuffer(f.read(), np.uint8, offset=8)
    print("Done")
    
    return labels

def _load_img(file_name):
    file_path = dataset_dir + "/" + file_name
    
    print("Converting " + file_name + " to NumPy Array ...")    
    with gzip.open(file_path, 'rb') as f:
            data = np.frombuffer(f.read(), np.uint8, offset=16)
    data = data.reshape(-1, img_size)
    print("Done")
    
    return data
    
def _convert_numpy():
    dataset = {}
    dataset['train_img'] =  _load_img(key_file['train_img'])
    dataset['train_label'] = _load_label(key_file['train_label'])    
    dataset['test_img'] = _load_img(key_file['test_img'])
    dataset['test_label'] = _load_label(key_file['test_label'])
    
    return dataset

def init_mnist():
    download_mnist()
    dataset = _convert_numpy()
    print("Creating pickle file ...")
    with open(save_file, 'wb') as f:
        pickle.dump(dataset, f, -1)
    print("Done!")

def _change_one_hot_label(X):
    T = np.zeros((X.size, 10))
    for idx, row in enumerate(T):
        row[X[idx]] = 1
        
    return T
    

def load_mnist(normalize=True, flatten=True, one_hot_label=False):
    """MNIST 데이터셋 읽기
    
    Parameters
    ----------
    normalize : 이미지의 픽셀 값을 0.0~1.0 사이의 값으로 정규화할지 정한다.
    one_hot_label : 
        one_hot_label이 True면、레이블을 원-핫(one-hot) 배열로 돌려준다.
        one-hot 배열은 예를 들어 [0,0,1,0,0,0,0,0,0,0]처럼 한 원소만 1인 배열이다.
    flatten : 입력 이미지를 1차원 배열로 만들지를 정한다. 
    
    Returns
    -------
    (훈련 이미지, 훈련 레이블), (시험 이미지, 시험 레이블)
    """
    if not os.path.exists(save_file):
        init_mnist()
        
    with open(save_file, 'rb') as f:
        dataset = pickle.load(f)
    
    if normalize:
        for key in ('train_img', 'test_img'):
            dataset[key] = dataset[key].astype(np.float32)
            dataset[key] /= 255.0
            
    if one_hot_label:
        dataset['train_label'] = _change_one_hot_label(dataset['train_label'])
        dataset['test_label'] = _change_one_hot_label(dataset['test_label'])    
    
    if not flatten:
         for key in ('train_img', 'test_img'):
            dataset[key] = dataset[key].reshape(-1, 1, 28, 28)

    return (dataset['train_img'], dataset['train_label']), (dataset['test_img'], dataset['test_label']) 


if __name__ == '__main__':
    init_mnist()

다음은 이미지 데이터를 넘파이 배열로 바꿔주는 위 코드 내의 load.mnist라는 함수를 import 하여 다른 작업 공간에 MNIST 데이터를 가져오는 코드입니다.

import sys, os
sys.path.append('C:\\~~~~~~~~~~')
from dataset.mnist import load_mnist

(x_train, t_train), (x_test, t_test) = \
    load_mnist(flatten=True, normalize=False)

print(x_train.shape)
print(t_train.shape)
print(x_test.shape)
print(t_test.shape)

밑에 출력문들은 들여온 데이터의 전체적인 크기를 출력하기 위한 목적입니다. 

위에 sys.path.append()라는 부분이 나오는데 이 부분의 활용법은 크게 두 가지입니다. 

첫 번째, (본인은 주피터 사용중) 만약 본인이 위의 load.mnist 함수가 있는 긴 코드를 담고 있는 부모 디렉터리 밑에 있는 작업 공간에서 작업 중이라면 괄호 안에 os.pardir라고 작성하시면 됩니다. 이는 내가 원하는 파일이 특정한 부모 디렉터리 밑에 있을 때 컴퓨터에서 딴 파일 뒤지지 말고 이 부모 파일 안부터 뒤져라는 명령을 하여 더욱 빠르게 파일을 가져오게 하도록 설정하는 것입니다. 

두 번째, 만약 지금 나는 작업하고 있는 스터디 모음 디렉토리 같은 게 존재해서 굳이 이 코드만을 위해서 다른 곳에 또 만들기 싫다 하신다면 저처럼 괄호 안에 내가 원하는 데이터가 있는 파일의 부모 디렉토리까지의 주소를 붙여넣기 하시면 됩니다. 예를 들어 MNIST 파일이 컴퓨터의 문서 내에 abc라는 파일 안에 있다고 치면

C:\\Users\\UserName\\Documents\\abc를 입력하면 됩니다.

 

load.mnist 함수는 읽은 MNIST 데이터를 (훈련 이미지, 훈련 레이블), (시험 이미지, 시험 레이블) 형식으로 반환합니다. 

인수로는 normalize, flatten, one_hot_label을 설정할 수 있습니다. 이 세 가지 모두 bool값입니다.

 

먼저 normalize는 입력 이미지의 픽셀 값을 0.0 ~ 1.0까지의 값으로 정규화할지를 결정합니다. False를 입력하면 원래대로 0 ~ 255까지로 나타나고 True라고 할 시 위와 같이 바뀝니다. 

다음 flatten은 입력 이미지를 평탄하게 즉, 1차원 배열로 만들지를 결정합니다. False를 입력하면 입력 이미지를  1 x 28 x 28의 3차원 배열로, True로 설정하면 784개의 원소로 이루어진 1차원 배열로 저장합니다. 

마지막 one_hot_label은 레이블을 원-핫 인코딩 형태로 저장할지를 결정합니다. 원-핫 인코딩이란, 예를 들어 [0,0,0,1,0,0]처럼 정답을 뜻하는 원소만 1로 표기하고 나머지는 모두 0으로 표시하는 배열입니다. False를 입력하면 원래의 값들을 넣고, True일 때는 원-핫 인코딩 된 값을 저장합니다.

 

위의 내용들을 고려하여 저장된 MNIST 데이터의 한 가지 값을 실제로 사진으로 불러오는 코드를 작성해보려합니다.

import sys 
import os
import numpy as np
sys.path.append('~~~~~~~~~') # 부모 디렉토리
from dataset.mnist import load_mnist # dataset폴더에 있는 mnist라는 파일에서 load_mnist라는 함수를 import 해라
from PIL import Image

def img_show(img):
    pil_img = Image.fromarray(np.uint8(img)) # 넘파이로 저장된 이미지 데이터를 PIL용 데이터 객체로 변환하는 함수
    pil_img.show()
    
(x_train, t_train), (x_test, t_test) = \
    load_mnist(flatten=True, normalize=False) # flattern = True로 설정해 읽어 들인 이미지는 1차원 넘파이 배열로 저장
    
img = x_train[0]
label = t_train[0]
print(label)

print(img.shape)
img = img.reshape(28,28) # 원래 이미지 모양으로 변형
print(img.shape)

img_show(img)

이 코드를 실행하게 되면 손글씨 사진으로 5라는 숫자의 사진이 나옵니다. 위에 img와 label을 0번째 인덱스라고 설정했기 때문에 데이터의 0번째 순서인 5가 나온 것이고 다른 인덱스를 입력하면 다른 숫자 사진이 나옵니다.

주석으로 작성했듯, Image.fromarray()는 넘파이로 저장된 이미지 데이터를 PIL용 데이터 객체로 변환해 주는 함수입니다.

 

이제 앞선 개념들을 바탕으로 MNIST 데이터셋으로 추론을 하는 신경망을 구현할 차례입니다.

먼저 코드입니다.

import sys, os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
import pickle
from dataset.mnist import load_mnist
from common.functions import sigmoid, softmax


def get_data():
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=False)
    return x_test, t_test


def init_network():
    with open("sample_weight.pkl", 'rb') as f:
        network = pickle.load(f)
    return network


def predict(network, x):
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']

    a1 = np.dot(x, W1) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(z1, W2) + b2
    z2 = sigmoid(a2)
    a3 = np.dot(z2, W3) + b3
    y = softmax(a3)

    return y


x, t = get_data()
network = init_network()
accuracy_cnt = 0
for i in range(len(x)):
    y = predict(network, x[i])
    p= np.argmax(y) # 확률이 가장 높은 원소의 인덱스를 얻는다.
    if p == t[i]:
        accuracy_cnt += 1

print("Accuracy:" + str(float(accuracy_cnt) / len(x)))

일단 함수의 정의 부분부터 보도록 하겠습니다.

get_data()에서는 앞에서 봤듯 laod.mnist를 bool형의 3가지 값을 설정해 주는 과정입니다.

init_network()에서는 sample_weight.pkl이라는 샘플 데이터를 들여오고 pickle 형태로 바꿔주는 작업을 진행합니다.

predict()에서는 딕셔너리의 형태로 저장되어 있는 network를 일반 변수로 바꿔주고 신경망 내의 계산을 맡아 실질적인 결괏값을 확률 변수로 도출하는 작업을 하는 함수입니다.

 

x, t = get_data()
network = init_network()
accuracy_cnt = 0
for i in range(len(x)):
    y = predict(network, x[i])
    p= np.argmax(y) # 확률이 가장 높은 원소의 인덱스를 얻는다.
    if p == t[i]:
        accuracy_cnt += 1

print("Accuracy:" + str(float(accuracy_cnt) / len(x)))

이 부분은 위의 함수들이 선언된 후 이들을 활용하여 정확도를 출력하는 코드입니다. 

함수와 관련된 부분은 생략하고 accuracy_cnt로 맞아떨어지는 경우의 수를 계산해 주고 for문을 통해 확률이 가장 높은 즉, 값이 가장 큰 원소의 인덱스를 얻음과 동시에 조건문으로 정확도 수를 올려주고, 마지막 출력문에서 정확도를 계산합니다.

 

정규화란, 데이터를 특정 범위러 변환하는 처리를 의미합니다.

전처리란, 신경망의 입력 데이터에 특정 변환을 가하는 것을 의미합니다.

 

지금까지 MNIST 데이터를 바탕으로 이를 추론하는 방법을 알아봤습니다. 저는 궁금해서 이미 있는 데이터가 아니라 제가 그림판에 글씨를 써서 테스트를 해봤는데 꽤 흥미가 있는 파트였습니다!

댓글