3章【ニューラルネットワーク 〜出力層と手書き文字認識〜】
Introduction
環境はGoogle Colaboratoryを用いています。
そのためデータセットをGoogleDriveの中のMyDrive配置していることが前提になります。
必要なデータセットはここからダウンロード or Clone してください。
前回は実際に行列を用いてNNを設計してみました。
今回は前回放置してしまっていた出力層の設計を行います。
期待する出力に応じて(扱う問題に応じて)出力する値を調整できます。
またこれまでの仕上げとして手書き文字認識の問題をNNを使って解いてみましょう。
データセットは有名なMNISTを用います。
出力層の設計
ニューラルネットワークは回帰問題や分類問題両方で活用できます。
それぞれ問題に応じて出力層に恒等関数とソフトマックス関数を噛ませます。
ソフトマックス関数はのちにみていくように、添字での総和が1になるので確率のような格好をしています。
ソフトマックス関数
ソフトマックス関数は以下の定義式で与えられる関数です。
y_k = \frac{exp(a_k)}{\sum{exp(a_i)}} \\
$(k = 1...n$であり、総和の添字も$1...n$までを走るとする$)$
それでは実際に実装してみましょう。
いきなり関数として実装してみます。
import numpy as np
def softmax(a):
a = np.array([0.3,2.9,4.0])
exp_a = np.exp(a) #指数関数
sum_exp_a = np.sum(exp_a) #全ての総和
y = exp_a/sum_exp_a
return y
a = np.array([0.3,2.9,4.0])
print(softmax(a))
ソフトマックス関数の注意点
ソフトマックス関数には数学的な特徴として、次の性質が挙げられる。
つまり配列に関して平行移動性を持っているのである。
y_k = \frac{exp(a_k)}{\sum{exp(a_i)}} \\
= \frac{Cexp(a_k)}{C\sum{exp(a_i)}} \\
= \frac{exp(a_k + logC)}{\sum{exp(a_i + logC)}} \\
という性質がある。
これによってオーバーフローを防ぐことが可能になります。
つまりexpはとても大きい数になりますが、定数倍平行移動することによってより値の小さい配列に切り替えることが可能なのです。
具体的には配列の中で最大の数だけ引いてやれば良いです。
しかし配列の最大数値が、その他の数値に比べて大きすぎる場合には意味をなさない時もあります。
それでは実装していきましょう。
import numpy as np
def softmax(a):
c = np.max(a)
exp_a = np.exp(a-c)
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
またこのソフトマックス関数の特徴として、全体の総和(出力の総和)が1になるという性質があるので、確率分布のように見做すことが可能であり、分類問題などに主に用いられます。
しかし、ソフトマックス関数は単調増加であることから出力された数字の大小は変化しないためソフトマックス関数を省いて実装することも可能です。
手書き文字認識
今回は手書き文字認識を学習済みモデルを用いて推論の実装のみをしていきます。
このような処理をNNの「順方方向電播」というそうです。
MNISTについて
まず今回用いるデータセットであるMNISTについて簡単に復習します。
MNISTは手書き数字のデータセットで0~0までの手書き数字の画像データからなります。
この本によると訓練用画像が60,000枚、テスト用画像が10,000枚用意されているそうです。
また各画像データは28×28のグレー画像であり、各ピクセルが0~255までの値をとります。
今回はMNISTのデータダウンロードから画像データの配列への変換までをサポートする便利なpythonスクリプトであるmnist.py を用います。(この本のデータをオライリーからダウンロードして用いる必要がある)
以下がMNISTデータセットを読み取るのに必要になるコードです。
しかしそれに当たって、今回すべてのコードはGoogleColaboratoryというオンラインでjupyterが利用できる(あるいはそれ以上の機能を兼ね備えた)サービスがあるのでそちらを利用しています。
その際には0から作るDeepLearningのオライリーのコードをGoogleドライブに保存して、ドライブをマウントしなくてはいけません。
そのため以下ではコードをドライブに保存したあと、マウントするところから始めていきたいと思います。
from google.colab import drive
drive.mount('/content/drive')
上記コードによってマウントできます。表示されたURLにアクセスして出てたコードをコピーして打ち込みましょう。
そして以下がいよいよ本に乗っているコードの実行なのですが、少し工夫が必要でディレクトリ移動してからコードを実行していきます。
import sys, os
sys.path.append('/content/drive/deep-learning-from-scratch')
%cd {'/content/drive/My Drive/deep-learning-from-scratch'}
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, 784)
print(t_train.shape) # (60000,)
print(x_test.shape) # (10000, 784)
print(t_test.shape) # (10000,)
import numpy as np
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[0]
label = t_train[0]
print(label) # >>>5
print(img.shape)
img = img.reshape(28,28) # 形状を元の画像サイズに変換
print(img.shape)
img_show(img)
これでMNISTの画像が表示されれば成功です。
NNの実装
それではいよいよNNの推論処理の実装に移りましょう。
ネットワークの構成としては入力層784個、出力層を10個のニューロンで構成します。
このようにNNの実装ではニューロンの個数を入力側ではデータに合わせて、出力側は分類問題ならクラスの個数分用意します。
また簡単のため2つの隠れ層を持ち、1つめが50個、2つ目が100個のニューロンで構成されているものと仮定します。
またPythonには便利なpickleという機能が備わっていて、実行中のオブジェクトをファイルとして保存しておく機能です。この機能によって学習中の重みのデータを保存しておきます。
%cd {'/content/drive/My Drive/deep-learning-from-scratch/ch03'}
import pickle
def get_data():
(x_train,t_trainh) = load_mnist(normalize = True,flatten = True,one_hot_label = False)
return x_test,t_test
def init_network():
with open("/content/drive/My Drive/deep-learning-from-scratch/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(a1,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)))
※注意
本のまま実行するとpickle周りやディレクトリ周りでうまくいかないので
import やマウントなどの処理を追記して実装しています。
バッチ処理について
バッチ処理の大きな利点は計算にかかる負荷を軽減できることにあるようです。
これについては筆者はあまりよく理解できておらず、お恥ずかしいのですが今回はそういうものだということで処理の流れだけ確認していきたいと思います。
まずは先ほど実装したNNの重みや各データの形状をみていきます。
x,_ = get_data()
network = init_network()
W1,W2,W3 = network['W1'],network['W2'],network['W3']
print(x.shape,x[0].shape)
print(W1.shape,W2.shape,W3.shape)
バッチ処理をする際に注意が必要なのは入力と出力の層の形が変化する可能性があることです。
それではバッチ処理の実装をまず確認してみましょう。
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("精度は:"+str( float(accuracy_cnt)/len(x)))
>>>精度は:0.8453
このコードの内容を解説していきます。
range関数はstart,end,step数の順に記述します。
つまり初めから最後の数まで感覚がstep分ずつ増える等差数列のようになる感覚です。
あとは同様に正解のラベルをカウントしていって、最後に精度をだす形になっています。
おわり
次回はニューラルネットがどのようにして重みを学習するのかについてみていきます。
損失関数と、その偏微分についても触れます。