본문 바로가기
딥러닝

06. 딥러닝 신경망 구현 기초 MNIST - 신경망 구성, 정확도 평가, 배치 처리

by Go! Jake 2021. 5. 23.

 

- 이전 글: 05. 딥러닝 신경망 구현의 기초 - 출력층, 항등함수, 소프트맥스함수, 분류, 회귀

 

 

  해당 포스팅은 '밑바닥부터 시작하는 딥러닝'과 기타 인터넷 자료를 요약한 자료입니다.

손글씨 숫자 인식

신경망의 구조를 실전 예인 손글씨 숫자 분류에 사용 해 본다.

 

MNIST 데이터셋

이번 예에서 사용되는 데이터셋은 mnist라는 손글씨 숫자 이미지 집합이다. MNIST는 아주 유명한 데이터셋으로, 간단한 실험부터 논문으로 발표되는 연구까지 다양한 곳에서 연구된다.

 

MNIST 데이터셋은 0부터 9까지 숫자 이미지로 구성된다. 예를 들어 훈련 이미지가 60000장, 시험 이미지가 10000장을 준비하고 이 훈련 이미지들을 사용하여 모델을 학습하고, 학습한 모델로 시험 이미지들을 얼마나 정확하게 분류하는지를 평가한다.

머신 러닝에서는 훈련데이터와 테스트 데이터로 나뉜다. 둘은 섞이지 않아야 한다. 추가적으로 검증 데이터로 구성된다. 

MNIST의 이미지는 28X28 크기의 회색조 이미지 (1채널)이며, 각 픽셀은 0에서 22까지의 값을 취한다.

 

https://github.com/oreilly-japan/deep-learning-from-scratch/tree/master/ch03

https://github.com/oreilly-japan/deep-learning-from-scratch/tree/master/dataset

위 Github에서 ch03, dataset을 다운받는다.

 

import sys, os
sys.path.append(os.pardir)
from dataset.mnist import load_mnist

(x_train, t_train), (x_test, t_test) =\
    load_mnist(flatten=True, normalize=False)
    
print(x_train,shape) # (60000, 1, 28, 28)
print(t_train,shape) # (60000,)
print(x_test.shape) # (10000,784)
print(t_test.shape) # (10000,)

파일 경로 관련 설명

우선, 현재도 앞으로도 각 파일/자료가 분산되어 있더라도 어떻게 매번 같은 디렉토리에 넣지 않고도 활용할 수 있는 지에 대한 고민이 필요하다. 따라서 위 코드 또한 경로 실행에 대한 이해가 좀 필요하다.

from dataset.mnist import load_mnist : "load_mnist라는 함수를 dataset 폴더 내에 mnist.py에서 가져오겠다."라는 의미이다. 이 의미는 기본적으로 탐색하는 경로 내에 "dataset"이 탐색 범위 내에 있어야 한다는 뜻이다.

 

현재 어느 경로가 탐색 범위인지 보고자 한다면, sys.path를 통해 볼 수 있다. 만약 탐색 범위 안에 dataset 폴더가 없어 추가를 하고 싶다면, sys.path.append("경로")를 넣을 수도 있다.

 

위 코드는 sys.path.append(os.pardir)와 같이, 현재 .py 파일을 RUN으로 실행하는 경우 파일이 실행되는 디렉토리가 기준이 된다. os.pardir를 선택하면, 이에 해당되는 한 단계 위인 부모 디렉토리 기준도 추가하겠다는 의미가 된다. 따라서 현재 .py를 저장한 부모 디렉토리에서 보았을 때 dataset 폴더가 탐색 범위 내에 있게 되므로, sys.path.append(os.pardir)를 사용한다.

 

내 경우에는 python console을 사용하기 때문에 .py 파일이 있는 경로가 기준이 되지 않으므로 부모 디렉토리를 직접 추가하였다.

 

훈련 이미지와 시험 이미지

(x_train, t_train), (x_test, t_test)를 불러왔다. 이 때 각각 x_train은 mnist를 훈련하게 될 이미지, t_train은 훈련 이미지의 레이블 (정답의 의미로 보면 된다.), x_test는 test용 이미지, t_test를 test용 이미지의 레이블이다. 각각은 numpy.ndarray 타입으로, n-dimensional array가 된다. 즉 각각은 numpy array인 것이다.

머신 러닝은 훈련데이터는 교과서와 같은 개념이며, 테스트 데이터는 시험을 보는 개념이다. 이 때 신경망의 성능은 시험을 통해 갈리게 된다.

 

load_mnist의 인수(factor)

load_mnist는 데이터를 다룰 때 몇 가지 대표적인 인수를 제공한다. normalize, flatten, one_hot_label 3가지이다.

첫 번째 normalize는 입력 이미지의 픽셀 값을 0.0 ~ 1.0까지 제공한다. 기존은 0~255까지 제공.

두 번째 flatten은 입력 이미지를 1차원 배열로 만든다. 예를 들어 현재는 60000장의 훈련 데이터가 있다고 한다면, 60000X1X28X28과 같이 4차원 텐서 형태를 가지게 된다. 이 구조는 아래에 설명하도록 하겠다. 이 구조를 flatten=True로 한다면 60000X784과 같이, 60000장을 784개의 1차원 배열로 평탄화하게 된다.

세 번째 원-핫 인코딩(one-hot encoding)은 예를 들어 [0,0,1,0,0,0,0,0]과 같이 정답을 뜻하는 원소만 1이고(hot) 나머지는 모두 0인 배열이다. 예를 들어 현재는 0부터 9까지의 10개의 원소가 있는 데, 정답을 뜻하는 원소만 1을 두고 나머지는 0을 사용하겠다는 의미이다. 5가 정답일 때 one_hot_label = True라면 t_train[0]은 array([0., 0., 0., 0., 0., 1., 0., 0., 0., 0.])을 표출한다. 반면 one_hot_label = False라면 5를 표출하게 된다.

 

 

 

신경망의 추론 처리

신경망 구성하기

MNIST 데이터셋을 가지고 추론을 수행하는 신경망을 구현한다. 신경망 생성을 위해서는 입력층과 은닉층, 그리고 출력층을 정의해야 한다. 입력층은 여기서 각 pixel이 된다. (장당 28x28=784개), 출력층은 10개가 된다. 이는 맞춰야 되는 숫자가 0에서부터 9까지이기 때문이다. 은닉층은 2개의 은닉층을 형성하고 각각 첫 번째 은닉층에는 50개의 뉴런, 두 번째 은닉층에는 100개의 뉴런을 배치한다. 이는 임의로 정한 값이다.

 

import sys, os
sys.path.append("D:\\deep-learning-from-scratch-master")  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
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("D:\\deep-learning-from-scratch-master\\ch03\\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

get_data(): get_data()함수는 우선적으로 우리가 사용할 x_train, t_train, x_test, t_test numpy.ndarray 형태로 가져오도록 한다. 인수(factor)를 이용해 normalize, flatten, one_hot_label을 설정한다. 각각 True, True, False로 설정한다.

 

입력층에 입력할 기초적인 데이터를 불러오는 것으로 이해하면 이해가 쉽다. 입력 데이터는 각 훈련 데이터와 레이블, 각 시험 데이터와 레이블 각각의 pixel 값이다. 우선 x_test, t_test만 return하도록 한다.

 

def init_network(): 신경망 구현에 기초 데이터를 불러온다. with open(path,"rb") as f 를 사용한다. pickle 파일을 "rb" 형태로 불러온다. 이 때 pickle은 binary이므로 "rb"로 불러온다. binary 파일을 read한다는 의미이다.

참고로, pickle은 특정 객체를 파일로 저장하는 기능이다. 일반 텍스트가 아닌 리스트나 클래스 등의 자료형은 파일 입출력이 불가능하다. 따라서 텍스트 이외에 자료를 저장하기 위해 pickle을 사용한다. 또한 list 또는 dictionary를 그대로 저장하면 용량이 크지만 pickle로 저장하면 binary 형태로 저장하기 때문에 용량이 줄어든다.

 

pickle 파일을 불러오고, network 변수에 넣는다. pickle 파일 내에는 각 신경망의 가중치와 편향이 key 형태로 저장되어 있다. 신경망 구현 시 이를 참조할 것이다.

   

def predict(network,x):

network['W1'] , network['b1'] 등으로, dictionary형인 network를 참조한다. 이 의미는 첫 번째 은닉층으로의 가중치와 편향이 된다.

a1=np.dot(x,W1)+b1

z1=sigmoid(a1)

np.dot을 활용하면 행렬 곱과 같은 것으로 보면 된다. 이 때 np.dot(x,W1)+b1와 sigmoid(a1)을 통해 입력값에 가중치를 곱하고, 활성화 함수 처리한다. 이와 같은 작업을 반복한다.

 

결괏값에 대한 정확도 평가

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

x, t에 각각 x_train, t_train 값을 받아온다. 또한 network 변수에 init_network()로 pickle 파일 값을 가져온다.

이후부터는 predict(network,x[i]) 함수를 사용하여 은닉층을 통과한 결과값인 y를 입력한다.

또한 y는 0부터 9에 대한 계산값이기 때문에 여기서 최댓값, 즉 정답으로 판단하는 값을 p값으로 저장하도록 한다.

이후 각 p값과 t 레이블 값을 맞추면서 정답일 때마다 accuracy_cnt를 1씩 올리고, 이 값을 전체 값으로 나누어 정확도를 구한다.

 

배치 처리

배치 처리는 입력 데이터를 하나로 묶어 처리하는 것을 의미하며, 배치는 하나로 묶은 입력 데이터이다. 

>>> x.shape
(10000, 784)

현재까지 진행 해 온 코드에서 x.shape을 하면, (10000, 784)임을 알 수 있다. 이는 28x28 픽셀의 사진이 10000장으로 구성되어 있다는 의미이며, 10000개의 행과 784개의 픽셀이 열로 구성되어 있다는 것을 알 수 있다.

 

지금까지의 코드에서는 결괏값 y는 1개의 행과 10개의 열을 가진 array를 for문을 통해 10000장을 일일이 대조하고 정답인 경우 accuracy_cnt를 올렸다. 여기서 배치 처리를 사용한다면 묶음 처리하여 계산을 한번에 처리하고, 10000개의 행과 10개의 열을 가진 추론 결과를 얻을 수 있다.

x, t = get_data()
network = init_network()

batch_size = 100 # 배치 크기
accuracy_cnt = 0

for i in range(0, len(x), batch_size):
    x_batch = x[i:i+batch_size]
    y_batch = predict(network, x_batch)
    p = np.argmax(y_batch, axis=1)
    accuracy_cnt += np.sum(p == t[i:i+batch_size])

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

 

 

for i in range(0, len(x), batch_size): 여기서 len(x)는 사진 장수가 된다. 따라서, for문 0에서 10000장까지 모두 탐색하되, i는 각각 batch_size만큼의 step을 가지게 된다. 따라서 i는 0, 100, 200, 300, ....., 10000이 된다.

 

이전 코드와 비교하였을 때 x는 단순히 x[i]였고, i는 0부터 10000을 모두 탐색하였으나,

배치에서는

- x_batch 함수를 정의한다.

- x_batch = x[i:i+batch_size]로 step만큼의 크기씩 묶는다.

 

p = np.argmax(y_batch, axis=1): predict(network, x_batch)를 통해 사진별 각 0~9까지 예측한 값을 y_batch에 넣고, np.argmax()를 실행한다.

묶은 x_batch를 p = np.argmax(y_batch, axis=1)로 정의한다. 이 때 참고로, axis = 1을 사용하는 이유는, '행'에서 비교했을 때 최댓값이 위치한 인덱스를 표출하기 위해서이다. axis = 0으로 설정한다면 같은 '열'에서 최댓값이 위치한 인덱스를 표출한다.

우리의 경우 100장에 대한 행을 만들고, 각 행마다 0~9에 해당하는 열이 있고, 확률 값이 들어 있으므로, 우리는 각 행, 즉 1장의 사진에서 어떤 숫자가 가장 높은 확률을 가지고 있는 지 알 필요가 있다. 따라서 axis = 1로 두면 된다.

 

accuracy_cnt += np.sum(p == t[i:i+batch_size]): t[i:i+batch_size]로 각 라벨별 값과 p값을 비교하여 레이블과 같은 값을 셀 수 있다. 한번에 연산하게 된다.

 

추가로, 위에서 설명한 np.argmax와 np.sum에 대해 예시를 살펴 보자.

 

np.argmax

>>> x = np.array([[0.1, 0.8, 0.1], [0.3, 0.1, 0.6], [0.2, 0.5, 0.3], [0.8, 0.1, 0.1]])
>>> y= np.argmax(x, axis=1)
>>> y    
array([1, 2, 1, 0], dtype=int64)

np.argmax에서 axis를 설정하지 않는 경우 평탄화(flatten)하여 최댓값이 있는 인덱스 위치를 반환한다.

axis = 0으로 설정한 경우 각 열에서 최댓값을 가지는 인덱스 위치를 반환한다.

axis = 1로 설정한 경우 각 행에서 최댓값을 가지는 인덱스 위치를 반환한다.

 

np.sum

>>> y = np.array([1,2,1,0])
>>> t = np.array([1,2,0,0])
>>> y==t
array([ True,  True, False,  True])
>>> np.sum(y==t)
3

위에서 쓰인 np.sum 활용은 == 연산자를 사용해 넘파이 배열끼리 비교하여 True/False로 구성된 bool 배열을 만들고, 이 결과에서 True의 개수를 세고 있다.

 

댓글