社畜エンジニア発掘戦線

駆けだしAIエンジニア

MNIST手書き数字の画像識別(隠れ層の追加)

手書き文字を識別するためのニューラルネットワークを構築するベースができてきました。前回は「入力」と「出力」だけのシンプルなネットワークでしたが、今回はこれに隠れ層を追加してみたいと思います。基礎問題では、隠れ層と言えば「非線形」がキーワードで、直線で分類できないような問題に適応しました。画像認識が直線で分類可能かどうか?、はよく分からないんですが、より複雑な構造の分類(特徴量の抽出)も可能になり、正解率アップを期待します。

CONTENTS

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

前回のニューラルネットワークに隠れ層を追加します。とりあえず、隠れ層のニューロン数は100で固定してみます。100という数字に意味はありません、なにか決めないと進まないので決めました。ここで使用する活性化関数はおなじみのシグモイド関数を使いましょう。パラメータも「隠れ層」と「出力層」にそれぞれWとBがあるので注意です。

f:id:sutokun:20190417101656j:plain:w500

いつもの層がひとつ増えるだけなので、そんなに複雑ではありません。前回のコードをベースに実装してみます。

ニューラルネットワークの実装(MNISTデータのロード、関数定義)

もろもろ実装していくための準備からです。基本的にほとんどコピペなのでサラッといきましょう。

まずはライブラリもインポートします。使用するライブラリも前回と同じです。

#library import
import numpy as np
import matplotlib.pyplot as plt
import 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

ロードしたデータセットの前処理(ピクセルの規格化、One-Hot変換)を行います。

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

#transform_OneHot
T_train = np.eye(10)[list(map(int,train_label))] 
T_test = np.eye(10)[list(map(int,test_label))]

「ctr + C & V」が捗ります。まぁMacなんでcmdなんですけどね。

次に使用する関数を定義しておきます。今回は隠れ層の活性化関数にシグモイド関数を使うので、こちらも合わせて記述しておきます。

#function
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

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

MNISTの問題からクロスエントロピーに(0で割り算系の)対策を施しています。なんやこれってなったら最初の問題を参照下さい。

さて、こんな感じで前準備が完了です。

ニューラルネットワークの実装(初期パラメータの設定)

次はニューラルネットワークのパラメータ:W、Bを設定します。隠れ層もあるのでWh、Bh、Wo、Boで区別します。

#initial setting
num_of_hidden=100
Wh = np.random.randn(784, num_of_hidden)
Bh = np.random.randn(1,num_of_hidden)
Wo = np.random.randn(num_of_hidden, 10)
Bo = np.random.randn(1,10)
learning_rate = 0.001

「num_of_hidden」で隠れ層のニューロン数を定義しています。行列の型にだけ注意して、それぞれランダムな値で設定します。ここもいつも通りですね。

あと、損失量と正解率(精度)を保存するリストも定義しておきます。学習にかかる時間を測定するtime関数もついでに入れています。

E_save = []
accuracy_save = []
start_time = time.time()

そんな感じで初期値の設定が完了です。

ニューラルネットワークの実装(イテレーション:順伝搬)

ではイテレーションの中身を書いていきましょう。

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

まずはミニバッチの設定からです。ミニバッチ学習はランダムに画像を100個抽出して、そのカタマリをニューラルネットワークに突っ込んで1回学習させる、というものでした。ちなみに100個という数字は状況に応じて変更するパラメータです、たまたま100にしているだけです。詳しくは最初の問題を参照下さい。

ではバッチを抽出するコードを書きます。

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

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

ここも前回と同じなので、コピペです。

では順伝搬計算を書いていきましょう。

    #forward prop
    H = sigmoid(np.dot(X_batch,Wh)+Bh)
    Y = softmax(np.dot(H, Wo)+Bo)
    E = loss(Y,T_batch)/len(Y)
    E_save = np.append(E_save, E)

隠れ層のHが増えていますが、ここも今まで通りの記述です。ここもコピペするんですが、出力Yの中身をX_batchからHに変えないといけません。たまにパラメータを変え忘れてよくエラー出すんですよね、ほんま凡ミス。

出力Yまで出せたので、正解率(精度)を計算するコードを書きます。

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

ここもコピペですね、いや〜コピペって素晴らしい。

隠れ層Hの計算が増えただけで、大きな変更点はありません。これで順伝搬計算が完了です。

ニューラルネットワークの実装(イテレーション:逆伝搬)

でわでわ、逆伝搬を進めていきます。ここもこれまでの計算と同じです。まずは出力層から微分式を求めていきます。

ここは前回と同じ、連鎖率を2段階構造にするパターン。

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

式も同じです、添字だけ調整すればオッケー。

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

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


続いて、隠れ層の微分式ですが、こちらは連鎖率を3段階構造にします。

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

基礎問題でもやりましたが、ひとつ微分が増えるだけでかなり長くなりますね、、まぁ根気よく。

Whの微分式:{\displaystyle 
\frac{\partial E}{\partial W_h} = \frac{\partial }{\partial W_h} sigmoid(X_{batch} \cdot W_h+B_h) \cdot \frac{\partial }{\partial H}(H \cdot W_o + B_o) \cdot \Delta
}

{\displaystyle 
\frac{\partial E}{\partial W_h} = X_{batch} \cdot H(1-H) \cdot W_o \cdot (Y-T_{batch})
}


Bhの微分式:{\displaystyle 
\frac{\partial E}{\partial B_h} = \frac{\partial }{\partial B_h} sigmoid(X_{batch} \cdot W_h+B_h) \cdot \frac{\partial }{\partial H}(H \cdot W_o + B_o) \cdot \Delta
}

{\displaystyle 
\frac{\partial E}{\partial B_h} = H(1-H) \cdot W_o \cdot (Y-T_{batch})
}

シグモイドの微分も忘れがちなのでご注意。

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

    #back prop
    dWo = np.dot(H.T,(Y-T_batch))
    dBo = np.reshape(np.sum(Y-T_batch, axis=0),(1,10))

    dWh = np.dot(X_batch.T,H*(1-H)*np.dot(Y-T_batch,Wo.T))
    dBh = np.sum(H*(1-H)*np.dot(Y-T_batch,Wo.T), axis=0, keepdims=True)

一気にパラメータ更新まで書いてしまいます。ちなみに勾配降下法です。

    Wo = Wo - learning_rate*dWo
    Bo = Bo - learning_rate*dBo
    Wh = Wh - learning_rate*dWh
    Bh = Bh - learning_rate*dBh

最後に学習時間を測定するコードを書いておきます。

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

これでイテレーションの中身が完成です、わーい。

グラフ化

グラフ化するコードを書きます。

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

plt.figure()
plt.plot(E_save, color='blue')
plt.show()

ここは前回と同じ、正解率と損失量をグラフ化します。

パラメータの保存

今回は隠れ層も追加したので全部で4つのパラメータが出てきました。それぞれをリストにまとめて、またpickle形式で保存します。

#Save parameters

parameters = [Wh,Bh,Wo,Bo]

dataset_dir = '/Users/FUTOSHI/Desktop/MNIST_test'

save_file = dataset_dir + '/hidden.pkl'

with open(save_file, 'wb') as f:
    pickle.dump(parameters, f) 

parametersの中身と保存する名前「hidden.pkl」だけ変更しています、後は前回と同じです。

プログラムの実行

やっと完成しました。ではイテレーション3000回でプログラムを走らせてみます。

出力は3つ、正解率と損失量のグラフと計算時間です。

f:id:sutokun:20190417102000p:plain:w300f:id:sutokun:20190417102027p:plain:w300

3000回のイテレーション(3000エポック)で正解率は80%なかばなので、前回のニューラルネットワークとそんなに変わらない感触ですが、このグラフをよく見ると、まだ伸びそう。。

これは、3000回じゃ少ないんじゃないか説が提唱されそうです。とりあえず、この条件でテストまでしてみましょう。

ちなみに学習にかかった時間ですが、

> 5.614243984222412

ということで、前回の2倍ぐらいですね。まぁ隠れ層を追加してるんでそんなもんか、って感じです。

まとめ

今回はシンプルなニューラルネットワークから隠れ層を追加して、少し工夫してみました。結果としては80%なかばで前回とそんなに変わらなそうですが、イテレーションをもっと回すことでまだまだ向上していきそうな雰囲気があります。とりあえず、次回はこの条件で保存したパラメータを使ってテストまでしてみます。ここではアルゴリズムとコーディングがメインなので、イテレーションの回数によって正解率がどう向上するのかは別途調べてみたいと思います。


全体コード
github.com

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

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