딥러닝(DL)/딥러닝 기초

[DL] MNIST ( Modified National Institute of Standards and Technology database )

Song 컴퓨터공학 2023. 7. 31. 14:59

 

 

이번 포스팅은 MNIST 데이터셋을 이용해 직접 신경망의 순전파를 확인해보고, 일종의 실습을 하는 포스팅입니다. MNIST 는 대표적인 기계학습 데이터셋으로 0부터 9까지의 숫자 이미지로 구성된 데이터셋입니다.

MNIST의 이미지 데이터는 $28 \times 28$ 크기의 흑백 이미지이며, 각 픽셀은 $0$ ~ $255$ 까지의 밝기 값을 가집니다. 또한 각 이미지에는 그 숫자에 대한 레이블이 붙어있습니다. 이 데이터셋 내부에 이미 훈련 이미지가 $60,000$장, 시험 이미자가 $10,000$장 준비되어 있습니다. 훈련 이미지들을 사용해 학습된 모델로 시험 이미지를 분류해보는 것이 오늘의 목적입니다. 오늘은 학습된 매개변수로 사용해 분류가 진행되는 과정(순전파, Forward propagation)만 살펴보고, 학습에 대한 내용은 다다음 포스팅 쯤 다룰 예정입니다.

 

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
import pickle
from dataset.mnist import load_mnist
from PIL import Image


def img_show(img):
    pil_img = Image.fromarray(np.uint8(img))
    pil_img.show()

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

img = x_train[3]
label = t_train[3]
print(label)  # 5

print(img.shape)  # (784,)
img = img.reshape(28, 28)  # 형상을 원래 이미지의 크기로 변형
print(img.shape)  # (28, 28)

img_show(img)

'''
출력 :
1
(784,)
(28, 28)
'''

만약 위 코드를 실행하는데 다음과 같은 오류가 뜬다면 Python Imaging Library(PIL) 모듈이 설치되지 않았기 때문입니다. 따라서 최신 버전의 pip를 설치하고 Pillow 라이브러리를 설치하면 오류가 발생하지 않습니다.

모듈 에러 해결 과정

 

그리고 ime_show() 함수를 따로 정의한 이유는, 우리가 이미지를 보려면 784개의 1차원 배열로 저장된 데이터를 다시금 $28 \times $28$ 형태로 바꾸고, 넘파이로 저장된 이미지 데이터를 PIL용 데이터 객체로 변환해야하기 때문입니다.

 

matplotlib 을 사용해서 데이터를 확인할 수도 있습니다.

import matplotlib.pyplot as plt

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

plt.figure()
plt.imshow(x_train[0][0])
plt.colorbar()
plt.show()

plt.figure(figsize = (10,10))
for i in range(25):
    plt.subplot(5, 5, i+1)
    plt.xticks([])
    plt.yticks([])
    plt.imshow(x_train[i][0],  cmap=plt.cm.binary)
    plt.xlabel(t_train[i])

plt.show()

 

 

 

컴퓨터는 저 1이라는 숫자를 아래와 같은 형태로 저장합니다. 이를 확인하기 위한 코드를 보면

np.set_printoptions(linewidth = 116, threshold = 1000)
print(x_train[3])
print(t_train[3])

컴퓨터는 왼쪽의 이미지를 오른쪽과 같은 형태로 저장을 한다는 것을 확인할 수 있습니다. 이 때 데이터에 정규화를 하게 되면 모든 값이 0과 1 사이로 변하게 되는데, 그러면 아래처럼 나타나게 됩니다.

이 데이터를 통해 신경망을 학습시키고, MNIST 분류기를 만들 수 있습니다. 그러나 위에서 말했듯 이번 포스팅은 학습까지 진행하진 않고 이미 계산되어진 매개변수를 사용할 예정입니다.

'

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

# Accuracy:0.9352

MNIST 데이터셋의 데이터를 가져오는 get_data() 함수, init_network() 는 초기값을 설정해주는 함수인데, 이번에는 가중치와 바이어스 매개변수가 저장되어 있는 .pkl 파일을 읽어와서 사용하게 됩니다. predict 함수는 지난 포스팅에서 살펴본 3중 신경망의 코드입니다. 이 신경망은 입력층 뉴런은 784개($28\times 28$), 출력 뉴런은 10개(0~9 구분)로 구성됩니다. predict 함수는 각 레이블의 확률을 numpy 배열의 형태로 반환하며, 가장 확률값이 큰 원소의 인덱스를 예측 결과로 결정합니다.

위 사진은 2중 신경망이지만 코드상 구현은 3중 신경망입니다. 첫 번째 은닉층에는 50개, 두 번째 은닉층에는 100개의 뉴런이 배치되어 있습니다. 위 코드에서는 반복문을 돌면서 신경망이 예측한 답변과 정답 레이블을 비교하여 맞힌 숫자를 세고, 전체 이미지 숫자로 나눠 정확도를 나타냅니다. 93.52% 의 정확도를 나타내는 것을 확인할 수 있습니다.

 


 배치 처리

위 코드에서의 가중치 형상을 확인해보면 아래와 같습니다.

x, _ = get_data()
network = init_network()
W1, W2, W3 = network['W1'], network['W2'], network['W3']

print(x.shape)
print(x[0].shape)
print(W1.shape)
print(W2.shape)
print(W3.shape)

최종 결과로는 원소가 10개인 1차원 배열 Y가 나옵니다. 이는 이미지 1장인 데이터를 입력으로 넣었을 때의 연산입니다.

 

그렇다면 한번에 여러 개의 데이터를 묶어서 pridict() 함수에 넣는다면?

입력은 $100\times 784$ (100장의 이미지), 출력은 $100\times 10$ (100개의 예측 결과) 가 나오게 됩니다. 즉 100장 분량의 입력 데이터 결과가 한 번에 출력된다는 것이죠. x[0]과 y[0]에는 0번째 이미지와 그 추론 결과가, x[1]과 y[1]에는 1번째 이미지와 그 추론 결과가 대응되는 것입니다. 이처럼 하나로 묶은 입력 데이터를 배치(Batch) 라고 부릅니다.

 

x, _ = 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)))
# Accuracy:0.9352

위의 코드를 배치 처리로 구현한 코드입니다. 달라진 부분은 for 문에 step 인수를 추가하여 배치 사이즈 단위로 반복이 실행되고, 반복문의 내부에서는 batch_size 만큼 데이터를 묶어서 예측하고, 최댓값의 인덱스를 가져오는 argmax 함수를 그대로 사용하면서 각 원소에서 최댓값의 인덱스를 찾도록 axis = 1 을 추가해줍니다. ( 100 * 10 이라는 배열에서 1번째 차원인 10 (100은 0번째 차원)의 원에서 탐색을 하도록 ) 

 

그리고 if 문 또한 사라진 것처럼 보이지만 == 연산자를 통해 bool 배열을 만들고 True 의 개수를 세서 정확도를 반환합니다. 위 내용에 대한 간단한 동작 예시 코드를 보면 이해가 쉽습니다.

 

# for 문 3번째 인수 step
print(list(range(0, 10)))
print(list(range(0, 10, 3)))
print()

# argamx axis = 1
x = np.array([[0.1, 0.8, 0.1], [0.3, 0.1, 0.6], [0.5, 0.5, 0.3], [0.8, 0.2, 0.1]])
y = np.argmax(x, axis = 1)
print(x)
print(y)
print()

# Bool 배열 만들어 accuracy 측정
y = np.array([1,2,1,1])
t = np.array([1,2,0,1])
print(y==t)
print(np.sum(y==t))

 

배치 처리를 사용해 학습을 돌리면 배치 처리를 하지 않았을 때보다 훨씬 속도가 빠릅니다. 위에서 배운 2개의 예시에 대해 각각 수행 시간을 측정해보면 바로 알 수 있습니다.

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
import time


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

start = time.time()
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("배치 처리 O :Accuracy:" + str(float(accuracy_cnt) / len(x)))
print(time.time() - start)


start = time.time()
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("배치 처리 O : Accuracy:" + str(float(accuracy_cnt) / len(x)))
print(time.time() - start)

'''
출력 :
배치 처리 O :Accuracy:0.9352
0.7629587650299072
배치 처리 O : Accuracy:0.9352
0.1975116729736328
'''

 

 

또한 코드를 종합하여 학습이 잘못된 케이스에 대해서 확인할 수도 있습니다.

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
import matplotlib.pyplot as plt


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

error = []

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

plt.figure(figsize = (10,10))
for i in range(25):
    plt.subplot(5, 5, i+1)
    plt.xticks([])
    plt.yticks([])
    plt.imshow(x[error[i]].reshape(28, 28),  cmap=plt.cm.binary)
    plt.xlabel(t[error[i]])
    
plt.show()

확실히 결과가 틀린 것에 악필이 많네요.. 아무튼 여기까지 MNIST 데이터셋을 이용한 실습 코드들이었습니다. 감사합니다.

 

 

 

Reference :

밑바닥부터 시작하는 딥러닝 - 사이토 고키

https://ko.wikipedia.org/wiki/MNIST_%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4

https://github.com/WegraLee/deep-learning-from-scratch