본문 바로가기

인공지능/이론 적용

MNIST with multi-layer perceptron

MNIST with multi-layer perceptron

MNIST Dataset

multi-layer perceptron for multi-class classification

0~9 사이의 흑백 손글씨가 무슨 숫자인지 알아맞히는 알고리즘을 작성해보도록 하겠다.


mnist.load_data()에 커서를 옮긴 후 Shift+Tab를 쳐본다.
그러면 그 함수에 대한 설명이 나오는데 그 중 Returns 항목을 보면

<Tuple of Numpy arrays: (x_train, y_train), (x_test, y_test).>라고 쓰여있다.
그것을 토대로 mnist.load_data()의 반환형식을 알 수 있으며 해당하는 형태로 값을 받아주면 된다.

In [1]:
import numpy as np
from keras.datasets import mnist

(X_train, y_train), (X_test, y_test) = mnist.load_data()

print(X_train.shape, y_train.shape)
print(X_test.shape, y_test.shape)
Using TensorFlow backend.
(60000, 28, 28) (60000,)
(10000, 28, 28) (10000,)

28x28 size의 train 이미지가 6만개, test 이미지가 1만개 있음을 확인할 수 있다.
또한 정답이 들어있는 train과 test값들도 각각 6만개와 1만개씩 있음을 확인할 수 있다.


X에 들어있는 이미지와 y에 들어있는 label값이 잘 매칭되어 있는지 확인해 보겠다.
y_train과 X_train으로부터 각각 0번부터 9번째 index의 값들을 추출해 비교해본다.

In [2]:
import matplotlib.pyplot as plt
%matplotlib inline

plt.gray() # mnist 데이터가 흑백이란 것을 명시

print(y_train[:10])

figures, axes = plt.subplots(nrows=2, ncols=5)
figures.set_size_inches(18, 8)

for i in range(10):
    axes[i // 5][i % 5].matshow(X_train[i])
[5 0 4 1 9 2 1 3 1 4]
<Figure size 432x288 with 0 Axes>

확인결과 잘 일치하고 있음을 알 수 있다.


Preprocessing

본격적으로 알고리즘을 작성하기에 앞서 몇가지 전처리(Preprocessing) 작업을 해주겠다.

먼저 3차원 배열로 구성되어있는 X값들을 2차원 배열의 형태로 바꿔주겠다.
그럼 X_train의 경우를 살펴보자면 6만개의 데이터와 784개의 특성을 가지고 있는 데이터셋이 되는데 이는 2차원 배열형태라서 친숙하고 쉬워보인다.

In [3]:
X_train = X_train.reshape(60000, 28 * 28)
X_test = X_test.reshape(10000, 28 * 28)

print(X_train.shape, X_test.shape)
(60000, 784) (10000, 784)

다음으론 y값들을 One-hot encoding해주겠다.
앞으로 각각의 데이터 마다 가능한 모든 레이블에 대한 Squashing된 점수값들을 구하게 될텐데, 정답에 해당하는 y값들도 그것들과의 호환을 위해 형태를 맞춰주는 작업이다.

In [4]:
y_train_hot = np.eye(10)[y_train]
y_test_hot = np.eye(10)[y_test]

print(y_train_hot.shape)
print(y_test_hot.shape)
(60000, 10)
(10000, 10)

아래와 같이 비교해 One-hot encoding이 잘 되었음을 확인해 본다.

In [5]:
print(y_train[0:10])
print()
print(y_train_hot[0:10])
[5 0 4 1 9 2 1 3 1 4]

[[0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]
 [1. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 1. 0. 0. 0. 0. 0.]
 [0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]
 [0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
 [0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]
 [0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 1. 0. 0. 0. 0. 0.]]

Classification문제를 풀기 위한 수단으로 sigmoid function을 만들어준다. sigmoid의 역할은 값을 squashing하여 0~1사이로 그 범위를 좁혀주는 역할을 한다.

In [6]:
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

잘 동작하는지 확인해보겠다.

In [7]:
xx = np.linspace(-10, 10, num=41)
yy = sigmoid(xx)

plt.plot(xx, yy)
Out[7]:
[<matplotlib.lines.Line2D at 0x1db96dd1b08>]

전형적인 sigmoid 그래프가 그려짐을 확인할 수 있다.

또한 Gradient Descent를 통해 back propagation을 수행하는 기능 중 하나로써 sigmoid를 미분해 주는 함수도 만들어 두겠다.

In [8]:
# derivative of sigmoid function
def dsigmoid(s):
    return s * (1 - s)

예측결과가 실제 정답과 얼마나 맞아떨어졌는지를 체크하는 것만으로는 학습이 잘 이루어진 정도를 알 수 없는 부분이 있다.
비록 예측은 실패했더라도 굉장히 망설이다가 틀렸을 수도 있고(sigmoid값이 0.5근처) 화끈하게 정반대의 오답을 정답으로 확신하는 경우도 있을 것이다. 그리고 이는 예측이 성공한 경우 또한 마찬가지다.
그렇기에 결과론적인 평가외에 과정에 대한 평가도 같이 확인해보기 위해선 cross_entropy를 구해주는 함수 또한 필요할 것이다.

In [9]:
def cross_entropy(actual, predict, eps=1e-15):
    actual = np.array(actual)
    predict = np.array(predict)
    
    # log의 진수가 0이 되어 값이 무한대로 발산해버리는 경우를 방지한다.
    clipped_predict = np.minimum(np.maximum(predict, eps), 1 - eps)
    
    # 모든 정답값과 예측값 쌍들에 대해 cross_entropy방식으로 손실 정도를 구한다.
    loss = -actual * np.log(clipped_predict) + -(1 - actual) * np.log(1 - clipped_predict)
    
    # 구해진 모든 손실값들을 평균내어 반환한다.
    return loss.mean()

이제 본격적으로 MLP(Multi-Layer Perceptron) 알고리즘 작성을 시작해보자.

In [10]:
num_epoch = 1000
learning_rate = 2.5

# Input layer
w1 = np.random.uniform(low=-1, high=1, size=(1000, 784))
b1 = np.random.uniform(low=-1, high=1, size=(1000, 1))

# Hidden layer
w2 = np.random.uniform(low=-1, high=1, size=(10, 1000))
b2 = np.random.uniform(low=-1, high=1, size=(10, 1))

# 전체 데이터 개수
num_data = X_train.shape[0]

# 변하지 않아서 재활용이 가능한 대형 행렬에 대한 전치값
X_train_T = X_train.T
y_train_hot_T = y_train_hot.T

# 학습
for epoch in range(num_epoch):
    # Forward propagation
    z1 = np.dot(w1, X_train_T) + b1
    a1 = sigmoid(z1)
    z2 = np.dot(w2, a1) + b2
    a2 = sigmoid(z2)
    y_predict_hot = a2
    
    # 정확도 구하기
    y_predict = np.argmax(y_predict_hot, axis=0)
    accuracy = (y_predict == y_train).mean()   
    
    # 정확도가 95%에 도달할 때까지 학습
    if accuracy > 0.95:
        break

    # cross_entropy로 측정된 손실정도를 구한다
    loss = cross_entropy(y_train_hot_T, y_predict_hot)

    # 일정 주기마다 한 번씩 학습 경과를 알려준다
    if epoch % 10 == 0:
        print("{0:2} accuracy = {1:.5f}, loss = {2:.5f}".format(epoch, accuracy, loss))

    # Back propagation
    # cross_entropy를 각각의 weight와 bias에 대해 편미분한 값으로 다시 weight와 bias를 갱신하여 학습시킨다
    d2 = a2 - y_train_hot_T
    d1 = np.dot(w2.T, d2) * dsigmoid(a1)
    
    w2 = w2 - learning_rate * np.dot(d2, a1.T) / num_data
    w1 = w1 - learning_rate * np.dot(d1, X_train) / num_data

    b2 = b2 - learning_rate * d2.mean(axis=1, keepdims=True)
    b1 = b1 - learning_rate * d1.mean(axis=1, keepdims=True)

print("----" * 10)
print("{0:2} accuracy = {1:.5f}, loss = {2:.5f}".format(epoch, accuracy, loss))
 0 accuracy = 0.07262, loss = 6.17090
10 accuracy = 0.24955, loss = 1.98295
20 accuracy = 0.63080, loss = 0.71335
30 accuracy = 0.73200, loss = 0.36217
40 accuracy = 0.80655, loss = 0.21230
50 accuracy = 0.78852, loss = 0.20982
60 accuracy = 0.84108, loss = 0.15791
70 accuracy = 0.85290, loss = 0.13913
80 accuracy = 0.84027, loss = 0.15385
90 accuracy = 0.88630, loss = 0.10469
100 accuracy = 0.89658, loss = 0.09274
110 accuracy = 0.89763, loss = 0.09081
120 accuracy = 0.89962, loss = 0.08768
130 accuracy = 0.90762, loss = 0.08207
140 accuracy = 0.90835, loss = 0.08026
150 accuracy = 0.91043, loss = 0.07802
160 accuracy = 0.91448, loss = 0.07455
170 accuracy = 0.91655, loss = 0.07203
180 accuracy = 0.91982, loss = 0.06949
190 accuracy = 0.92073, loss = 0.06836
200 accuracy = 0.92465, loss = 0.06573
210 accuracy = 0.92593, loss = 0.06407
220 accuracy = 0.92838, loss = 0.06243
230 accuracy = 0.92942, loss = 0.06132
240 accuracy = 0.93168, loss = 0.06015
250 accuracy = 0.93132, loss = 0.05965
260 accuracy = 0.93258, loss = 0.05853
270 accuracy = 0.93375, loss = 0.05783
280 accuracy = 0.93568, loss = 0.05636
290 accuracy = 0.93587, loss = 0.05573
300 accuracy = 0.93655, loss = 0.05495
310 accuracy = 0.93737, loss = 0.05453
320 accuracy = 0.93835, loss = 0.05365
330 accuracy = 0.93840, loss = 0.05340
340 accuracy = 0.93942, loss = 0.05271
350 accuracy = 0.94057, loss = 0.05174
360 accuracy = 0.94122, loss = 0.05127
370 accuracy = 0.94087, loss = 0.05106
380 accuracy = 0.94220, loss = 0.05028
390 accuracy = 0.94222, loss = 0.04995
400 accuracy = 0.94295, loss = 0.04945
410 accuracy = 0.94258, loss = 0.04925
420 accuracy = 0.94370, loss = 0.04868
430 accuracy = 0.94468, loss = 0.04784
440 accuracy = 0.94487, loss = 0.04758
450 accuracy = 0.94492, loss = 0.04723
460 accuracy = 0.94570, loss = 0.04676
470 accuracy = 0.94660, loss = 0.04635
480 accuracy = 0.94628, loss = 0.04621
490 accuracy = 0.94737, loss = 0.04561
500 accuracy = 0.94712, loss = 0.04528
510 accuracy = 0.94762, loss = 0.04512
520 accuracy = 0.94778, loss = 0.04462
530 accuracy = 0.94903, loss = 0.04393
540 accuracy = 0.94920, loss = 0.04377
550 accuracy = 0.94960, loss = 0.04355
560 accuracy = 0.94910, loss = 0.04355
----------------------------------------
567 accuracy = 0.95013, loss = 0.04337

끝으로 알고리즘이 훈련과정에서는 접해보지 못했던 Test 데이터들에 대해서도 제대로 동작하는지 확인해 해보겠다.

In [11]:
import pandas as pd

# 구해진 weight와 bias로 예측값 구하기
z1 = np.dot(w1, X_test.T) + b1
a1 = sigmoid(z1)
z2 = np.dot(w2, a1) + b2
a2 = sigmoid(z2)

y_predict_hot = a2
y_predict = np.argmax(y_predict_hot, axis=0)

# 보기 좋게 pandas의 DataFrame으로 정리
test_result = pd.DataFrame({'actual': y_test, 'predict': y_predict})

# 예측 정확도 측정
test_accuracy = (test_result["actual"] == test_result["predict"]).mean()
print("Accuracy(test) = {0:.5f}\n".format(test_accuracy))

print(test_result.shape)
test_result.head(10)
Accuracy(test) = 0.92260

(10000, 2)
Out[11]:
actual predict
0 7 7
1 2 2
2 1 1
3 0 0
4 4 4
5 1 1
6 4 4
7 9 9
8 5 6
9 9 9

Test 데이터에 대해서도 비교적 잘 예측한다는 것을 확인할 수 있다.