社畜エンジニア発掘戦線

駆けだしAIエンジニア

MNIST手書き数字の画像識別(シンプルニューラルネット)

データセットの準備が整ったところで、実際にMNISTの画像をニューラルネットワークを使って分類してみたいと思います。今回はあまり複雑なことをせずに、「画像の入力」と「数字の出力」の2層でネットワークを作成してみます。考え方、コード作成の流れはこれまでの分類問題とほとんど同じです。

CONTENTS

データセットの作成

基礎問題でもずっとそうでしたが、ニューラルネットワークによる分類問題の第一歩はデータセットの作成から始まります。前回の問題でMNISTの画像データと正解ラベルをオリジナルのホームページからダウンロードし、使いやすいpickle形で保存しました。まずはこのデータセットを確認してみます。


・ライブラリのインポート

import numpy as np
import matplotlib.pyplot as plt
import time

何ごともまずはライブラリのインポートからですね。今回は、計算にかかる時間も測定したいのでtimeというライブラリもインポートしておきます。


・データセットのロード
前回と同じコードを書いてデータセットをロードします。

#load dataset
import pickle

save_file = '/Users/FUTOSHI/Desktop/MNIST_test/mnist.pkl'

with open(save_file, 'rb') as f:
    dataset = pickle.load(f)

train_img,train_label,test_img,test_label = dataset

これで「train_img、train_label、test_img、test_label」にMNISTのデータが入りました。この時点でこれらのデータセットの型を確認しておきます。

print(train_img.shape) #(60000, 784)
print(train_label.shape) #(60000,)
print(test_img.shape) #(10000, 784)
print(test_label.shape) #(10000,)

「train_img」は28x28 = 784ピクセルの画像データが60000枚、「train_label」は画像に対応した60000個の正解ラベル。testデータは同じ形で10000枚、おっけーですね。

f:id:sutokun:20190331141102j:plain:w500


ピクセル値の規格化
次に、画像のピクセル値の加工です。一般的に画像データは「輝度値」といって、ピクセルごとの色(RGB)の濃さを0〜255段階で表現しています。今回も扱うデータは画像なので、ピクセルの値は0〜255となっています。いちおう最大値を確認してみます。

print(np.amax(train_img)) #255

確かに0〜255の値で構成されているようです。まあこのまま分類を進めてしまっても良いんですが、扱いやすいように1で正規化しておきます。

#pixel normalization
X_train, X_test = train_img/255, test_img/255

これでもう一度、最大値を確認してみます。変数をtrain_imgからX_trainに変更しているのでご注意。

print(np.amax(X_train)) #1.0

おっけーですね、色の濃さがmax:1のパーセンテージに変換されたので分かりやすくなりました。


・正解ラベルのOne-Hot表現
次に、正解ラベルの加工です。今回は0〜9の数字を識別するので、10クラス分類になります。多クラス分類といえば「One-Hot表現」が必要でした。しかし、正解ラベルは0〜9の値で構成されているので、これらを0と1の「One-Hot表現」に変換しなければいけません。

一度に変換するとややこしいので、ひとつずつトライ。

まず、train_labelのデータの型を確認してみます。0番目の値を抜き出してtype関数でprint。

print(type(train_label[0])) #<class 'numpy.uint8'>

'numpy.uint8'ってなんだ、よく分からんけどこのままだとエラーが出るので、int(整数)型に変換します。配列の全要素を変換するのでmap関数を使います。そしてlist関数で変換された値を配列として構築し直します。

list(map(int,train_label))

また0番目を抜き出して、printで型を確認してみます。

print(type(list(map(int,train_label))[0])) #<class 'int'>

無事にintへ変換されました、めでたし。できたとは言えブッサイクなコードやな…、たぶん他にいい書き方があると思います。

次に、10x10の単位行列を作成します。0〜9までの10クラスなので「10」という値になっています。単位行列は正方行列(縦横同じ要素数)の斜め要素だけが1でそれ以外は0という特殊な行列です。

T_train = np.eye(10)

そして、上で作成したint型の正解ラベルの行を抜き出すことで、その値に対応したOne-Hot表現を得ることができます。test_labelも合わせて変換しておきます。

#one-hot transform
T_train = np.eye(10)[list(map(int,train_label))] 
T_test = np.eye(10)[list(map(int,test_label))] 

f:id:sutokun:20190331142500j:plain:w500

これでようやく準備が整いました。正直なところ、この作業も「前処理」なので、前回の内容でやってしまっても良かった気がします。

ではうまく加工できているか確認してみます。前回の最後に確認した方法と同じです。train_labelとT_trainを2つ並べて、きちんと数字がOne-Hotに対応しているか確認します。nは自分で好きな好きな値を選択してください。あと、見やすいようにグレーにしました。

n = 1234
img = np.reshape(X_train[n],(28,28))

plt.figure()
plt.imshow(img, cmap='gray_r')
plt.show()

print(train_label[n])
print(T_train[n])

f:id:sutokun:20190331143327p:plain:w300

おお、これは3っぽい。そして正解ラベルは…

> 3
> [0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]

素晴らしい、成功です、念のためnの値を変えて他の順番でもできているか確認してみてください。

ニューラルネットワークの設計

今回は一番シンプルなネットワーク、「入力」と「出力」の2層で構築したいと思います。「入力」はX_train、784の値を1次元でつっこみます。「出力」は0〜9の10個です。

f:id:sutokun:20190331144821j:plain:w500

画像を扱いますが、分類問題であることに変わりはないので活性化関数にはソフトマックス関数を使います。

とてもシンプルですね。しかし、X_trainだと画像は全部で60000枚あります。まぁ1個ずつ全部つっこんでも良いんですが、ここでは「ミニバッチ学習」を導入します。ミニバッチ学習とは、60000枚の画像の中からランダムに100枚の画像を選び、まずはこの100枚に対して損失量を計算します。そして、100通りの損失量を平均し、1度学習を更新します。その後、再び100枚の画像を選び、学習を行う、という流れです。ミニバッチは必ずしも100枚じゃなくていいんですが、100枚ごとに計算するのがよく見るかな〜ってことで100という値をチョイスしています。

ニューラルネットワークの実装(計算準備)

では具体的にコードを書いていきたいと思います。ミニバッチのところ以外、今まで通りです。

まずは関数を定義しておきます。ソフトマックスとクロスエントロピーです。

#function
def softmax(z):
    return np.exp(z) / np.sum(np.exp(z), axis=1, keepdims=True)

def loss(y, t):
    delta = 1e-7
    return -np.sum(np.multiply(t, np.log(y+delta)) + np.multiply((1 - t), np.log(1 - y+delta)))

少し問題が発生したので、クロスエントロピーの関数を修正しました。データ数が多くなったせい?かクロスエントロピーで「0で割り算しちゃう系」のエラーが発生します。logの中なんで0になると無限小に発散しちゃうんですかね。そこで、0になりきらないように「delta = 1e-7」という小さい数字を足して対策しておきます。

なんか最近新しいことたくさんやってるんで、アレンジはしていますが、こういう見慣れたコード見ると安心します。


次に初期値の設定です。

#initial setting
W = np.random.randn(784, 10)
B = np.random.randn(1,10)
learning_rate = 0.001

E_save = []
accuracy_save = []

重みとバイアスは型にだけ注意、他は同じです。学習率は、とりあえず経験的に0.001を入れておきます。基礎問題もだいたいこれぐらいでうまくいってたし…。あと、パラメータの更新するときに正解率(精度)も計算します。その精度を保存するためにからの配列「accuracy_save」もここで定義しておきます。

ニューラルネットワークの実装(順伝搬計算)

ではイテレーションの中身を書いていきます。計算を始める前に、計算時間を計算するためのコードも足しておきます。「start_time」は計算が始まる時間を保存しておく変数です。

イテレーションはとりあえず3000回でトライしてみます。ちなみに、今回はミニバッチで学習するのでイテレーション1回の学習で100枚の計算が実行されます。このミニバッチ:100というカタマリを「EPOCH」という単位で扱うそうです。またグラフ化のときに使います。

#iteration
start_time = time.time()
num_of_itr=3000
for i in range(num_of_itr):

まずはミニバッチの設定から、

    #set batch
    train_size = X_train.shape[0] #60000
    batch_size = 100
    batch_mask = np.random.choice(train_size,batch_size)

    X_batch = X_train[batch_mask]
    T_batch = T_train[batch_mask]

60000枚(X_train.shape[0])の中から100枚(batch_size)をランダムに選択(np.random.choice)して「batch_mask」にまとめます。このbatch_maskを画像と正解ラベルに適応させて、100枚のミニバッチが完成です。ちなみにここからはイテレーション(for)の中なのでインテンドされているので、ご注意。

いちおうミニバッチの型も見ておきましょう。イテレーション値を3000のまま回すとめっちゃ出力されるんで、確認するときは1とかに変更して下さい。

   print(X_batch.shape) #(100, 784)
   print(T_batch.shape) #(100, 10)

ちゃんとミニバッチ100枚分が反映されていますね。100枚からまた1枚ずつ取り出して計算するんじゃなくて、ひとつの行列として一気に計算します。

続いて順伝搬計算、損失量Eまで計算します。

    #forward prop
    Y = softmax(np.dot(X_batch, W)+B)
    E = loss(Y,T_batch)/len(Y)
    E_save = np.append(E_save, E)

100枚あるので、Eも100個出てきます。なので、要素数:len(Y)で割って、平均化しています。E_saveにはバッチごとに平均された損失量が保存されていきます。

ちょっとイレギュラーですが、基本的には今までと同じ計算処理です。

ニューラルネットワークの実装(精度の計算)

これで順伝搬計算が終わりました、この時点で一度このニューラルネットワークの正解率(精度)を計算しておきます。

    #set accuracy
    Y_accuracy = np.argmax(Y, axis=1)
    T_accuracy = np.argmax(T_batch, axis=1)
    accuracy = 100*np.sum(Y_accuracy == T_accuracy)/batch_size

    accuracy_save = np.append(accuracy_save, accuracy)

「Y_accuracy」は10個の出力のうち一番確率の高いもの、「T_accuracy」は正解ラベルの最大値、つまり「1」になっているもの、batch_size:100枚のうち「Y_accuracy」と「T_accuracy」が一致しているものがいくつありますか、という計算です。

f:id:sutokun:20190401004628j:plain:w500

ちなみに、バッチ計算でYは100行になっているのでargmaxに「axis=1」が必要です。

ニューラルネットワークの実装(逆伝搬計算)

さてさて、順伝搬計算が終わったところで逆伝搬の計算に移ります。逆伝搬を計算するにはまず微分式を求めて、パラメータを更新する、という流れでした。入力が画像になりましたが、基本的に計算は同じです。

まずは微分式を求めてみます。今回、更新すべきパラメータは重みWとバイアスBです。2層しかないので連鎖率は2段階構造になっています。

{\displaystyle 
\frac{\partial E}{\partial P} = \frac{\partial Y}{\partial P} \frac{\partial E}{\partial Y} 
}

今回もソフトマックスとクロスエントロピーの組み合わせなのでΔを使って計算します。

Wの微分式:{\displaystyle 
\frac{\partial E}{\partial W} = \frac{\partial }{\partial W} (X_{batch} \cdot W + B) \cdot \Delta= X_{batch} \cdot (Y-T_{batch} )
}

Bの微分式:{\displaystyle 
\frac{\partial E}{\partial B} = \frac{\partial }{\partial B} (X_{batch} \cdot W + B) \cdot \Delta= Y-T_{batch} 
}


基礎問題で散々やったのでここはサラッと。


ではパラメータを更新します。更新方法は勾配降下法を用います、一番シンプルなんで。画像識別もいろいろできるようになってきたらmomentumとか試してみます。

Wのパラメータ更新:{\displaystyle 
W_{new} = W_{old} - \mu \frac{\partial E}{\partial W} 
}

Bのパラメータ更新:{\displaystyle 
W_{new} = W_{old} - \mu \frac{\partial E}{\partial W} 
}

ではコードに落としていきます。

    #back prop
    dW = np.dot(X_batch.T,(Y-T_batch))
    dB = np.reshape(np.sum(Y-T_batch, axis=0),(1,10))

    W = W - learning_rate*dW
    B = B - learning_rate*dB

なんか普通に計算するとdBの結果が転置?しちゃってBと型が合わなかったのでreshapeしています。特に新しいこともなく、今まで通りの流れ。


これでニューラルネットワークの計算は完了です。最後にこの計算にかかる時間を計測したいので下記のコードを足しておきます。

end_time = time.time()
time = end_time - start_time
print(time)

イテレーションの計算前にstart_timeを定義しておきました。イテレーションが終わった時間をend_timeで指定して、「end - start」でかかった時間を計測します。ちなみに、ここはもうfor文を抜けているのでインテンドはありません。

グラフを描画

ひと通りの計算が終わったので、グラフ化するコードを書いていきます。グラフ化したいのはイテレーションごとに変化する精度「accuracy」、あと損失量「E」です。

plt.figure()
plt.plot(accuracy_save, color='blue')

plt.figure()
plt.plot(E_save, color='blue')
plt.show()
プログラムを実行

これでコードは全て書き終えました。ドキドキしながらターミナルで実行してみます。出力される結果は「精度(グラフ)」「損失量(グラフ)」「計算時間(値)」です。

f:id:sutokun:20190401024045p:plain:w300f:id:sutokun:20190401024059p:plain:w300

おお、うまく計算できました。accuracyのグラフを見ると、3000エポック、つまり3000回のイテレーションを回したところで精度は80%を超えています。まじか、こんなシンプルなニューラルネットワークで80%超えるんだ。よく人間も汚い字で書いて読み間違えることもあるし、8割正解するなら上等じゃないの、と思うんですが、ちゃんとした専門家からすると99%とか超えないとダメなようです。

ただ、精度に幅があります。3000EPOCHあたりに、下は78%?、上は92%?ぐらいで学習ごとに差が見られます。間をとって、だいたい86%の正解率ですかね。次回、テストよう画像を使ってこの正解率を測定してみたいと思います。

損失量についても、こちらも幅がありますがよく収束してくれています。精度のグラフはもっとイテレーションを回すとまだ伸びそうな雰囲気がありますが、損失量は収束しきった感がありますね。

あと、この計算にかかった時間ですが、

> 2.679206132888794 (sec)

まあ悪くないんじゃないの、という感覚です。プログラムを走らせて一瞬フリーズした?と思ったら結果が出てくる、みたいな感じ。ただ、基本問題をトライしていたときはほぼノータイムで結果が出てきていたので、それと比べると確かにデータ量が増えただけ計算時間が増えたのかな、と思います。

まだまだこのPCのスペックでもやれそうです。

まとめ

今回はシンプルなニューラルネットワークを使って画像認識の問題にトライしてみました。入力と出力の2層だけですが、精度は80%を超えるぐらいの結果が得られました。個人的には、こんなシンプルな設計だと50%ぐらいなんちゃうかな、って思っていたんでけっこう驚きました。

次回は1万枚のテスト用画像を使って、このニューラルネットワークがどれぐらいの正解率を出すのか測定してみたいと思います。まぁ、だいたい86%ぐらいなんだろうけど。

全体コード
github.com

元の記事
週末のディープラーニング - 社畜エンジニア発掘戦線

Twitter
世界の社畜 (@sekai_syachiku) | Twitter