TwoLayerNetクラスの実装|Excel VBAでMNIST機械学習

本ページはゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装を参考にニューラルネットワークを作成していきます。参考書籍では「Affineレイヤ」「ReLUレイヤ」「Softmax-with-Lossレイヤ」という3つのレイヤを作成し、それらを組み合わせることでニューラルネットワークを構築していきます。本サイトでも同じように3つのレイヤをVBAで作成し実装していきます。

これまでに「Affineレイヤ」「ReLUレイヤ」「Softmax-with-Lossレイヤ」は実装しました。
ここでは書籍同様にそれらを組み合わせた「TwoLayerNetクラス」をVBAで実装していきます。

「TwoLayerNetクラス」と難しく言っていますが、簡単に言えばこのクラスこそがニューラルネットワークです。このクラスにはいくつかの関数がありますが、関数同士が繋がっているものあるので実際に呼び出して実行する関数は限られています。

かなり長い内容になっていますが、ニューラルネットワークそのものを実装していくのでじっくりと理解しながら見ていきましょう。

 

TwoLayerNetクラスの機能

TwoLayerNetクラスでは今までに実装した3層(レイヤ)を呼び出し、「順伝播」「逆伝播」を行っていきます。これまでに実装したレイヤでは「forward」(順伝播)と「backward」(逆伝播)の関数を作成しているので、ここではそれらを呼び出して実行するだけです。

最終的にはメインのモジュールのループの中にこのTwoLayerNetを呼び出し学習を繰り返し行う流れになっています。また「順伝播」「逆伝播」以外にも、精度や答えを返す関数や学習したパラメータをExcelシートに書き出す関数など様々なものがあるので各機能で何を行っているのか理解しておきましょう。

 
機械学習の流れ 

これまでに何回も出てきましたが、今回作成していくニューラルネットワークをレイヤで表すと下図の通りです。この流れを取り仕切っているのが今回実装していく「TwoLayerNet」です。

再三になりますが「TwoLayerNet」の流れは以下のようになっています。(以下では上図の各レイヤを左から「Affine1」「ReLU」「Affine2」「Softmax-with-Loss」として説明していきます)

1. 入力値を「Affine1」に入れ、入力値と重みの総和を計算する。
2.「Affine1」の出力を「ReLU」に入れて活性化させる。
3.「ReLU」の出力を「Affine2」に入れ、入力値と重みの総和を計算する。
4.「Affine2」の出力を「Softmax-with-Loss」に入れ、活性化の結果/損失関数を求める。
5.「Softmax-with-Loss」から「Affine2」に逆伝播(Affine2のパラメータを更新)
6.「Affine2」から「ReLU」に逆伝播
7.「ReLU」から「Affine1」に逆伝播(Affine1のパラメータを更新)

上記の1~7を何百、何千回とループすることで「Affine1」と「Affine2」のパラメータ(重み/バイアス)が正解に近づくように徐々に更新されていきます。あとは損失関数をもとにループを止めれば学習終了といった流れになってます。

 

TwoLayerNetクラスの実装

まずはクラスモジュールで「TwoLayerNet」というモジュールを作成します。

「TwoLayerNet」では「Option Base 1」を使い配列を「1」スタートにします。
これは要素数とインデックスを合わせ配列同士の計算をわかりやすくするためです。

以下はTwoLayerNetの全コードです。

 
いままで通りPythonのコード(書籍のコード)を記載するとかなり長くなるなるので「TwoLayerNet」については割愛します。(Pythonコードは書籍を参照下さい)

※このクラスでは標準モジュールで作成した「Functions」の関数をいくつか呼び出して使用するので、「Functions」も合わせて読み進めて下さい。
 

Sub Initialize

まずは関数「Initialize」の機能を解説していきます。
ここでは今まで作成した「Affineレイヤ」「ReLUレイヤ」「Softmax-with-Lossレイヤ」のインスタンスを作成します。また、それと同時に重みパラメータとバイアスパラメータを初期化し、2つのAffineレイヤそれぞれに渡します。

引数としては「input_size」「hidden_size」「output_size」「Sheet」の4つがあります。
「input_size」「hidden_size」「output_size」はそれぞれ入力層、隠れ層、出力層のニューロン数です。この数によって重みパラメータの数とバイアスパラメータの数が決まります。

「Sheet」は学習済みの重み/バイアスパラメータが出力されているシートです。
このマクロでは学習を終えるたびに学習結果である重みとバイアスパラメータの値をシートに書き出します。加えてこのマクロは、その出力した値を再度ニューラルネットワークに取り込み続きから学習を始めることもできるようになっています。
つまり「Sheet」は以前出力したパラメータを再度ニューラルネットワークに取り込むための引数だということです。

関数「Initialize」では「重みパラメータの初期化」「バイアスパラメータの初期化」「レイヤの作成」の3つの処理を行います。

重みパラメータ/バイアスパラメータの初期化

重みパラメータとバイアスパラメータの初期化では「ParamsCheck」というグローバル変数を使い処理を2つに分岐させています。1つの処理は「はじめからの学習」の場合、もう1つの処理は「続きからの学習の場合」となっています。

「はじめからの学習」の場合、重みパラメータの初期値は「-1~1」の間の少数をランダムで設定、バイアスパラメータの初期値はすべて「0」で設定します。
「Affine1」と「Affine2」の2つのレイヤ分、「Functions」モジュールで作成した関数「RandomWeight」と「zeros」を使って重みパラメータとバイアスパラメータの値をそれぞれ設定します。

「続きからの学習」の場合は引数の「Sheet」に書かれている値をすべて読み込み、重みパラメータとバイアスパラメータの初期値としてそのまま利用します。

 
レイヤの作成

これまでにクラスモジュールで実装してきた「Affineレイヤ」「ReLUレイヤ」「Softmax-with-Lossレイヤ」のインスタンスを作成していきます。「Affineレイヤ」は2つ、その他のレイヤは1つずつ作成します。

また、これと同時に2つのAffineレイヤには(Affineレイヤの)関数「init」を使い、先ほど設定した重みパラメータとバイアスパラメータの初期値をそのまま受け渡します。これにより”Affineレイヤの中だけ”で重みパラメータとバイアスパラメータの値を更新させていくことができるようになります。

 

Function predict

関数「predict」はSoftmax関数で活性化する前までの重み付き入力値の総和を求める関数です。
簡単に言えば「Affine1」「ReLU」「Affine2」の3層での順伝播の最終出力値です。

この関数の引数としては「x()」があります。
これはニューラルネットワークへの入力値、つまりはMNIST画像データ(784個の数値)が入った1次元配列です。

この関数は引数「x()」を入力値として「「Affine1」「ReLU」「Affine2」の順に順伝播(forward)を行い、その最終的な計算結果を1次元配列としてそのまま返します。

 

Function loss

関数「loss」は損失関数「交差エントロピー誤差」(Loss値)を求める関数です。

この関数の引数としては「x()」と「t()」があります。
「x()」はニューラルネットワークへの入力値、つまりはMNIST画像データ(784個の数値)が入った1次元配列で、「t()」はone-hot表現化した正解ラベルが入った1次元配列です。

ここでの処理は非常にシンプルで前項の関数「predict」「Softmax-with-Lossレイヤ」の関数「forward」を実行しているだけです。実行すると交差エントロピー誤差の値(Loss値)を求めることができます。

また、引数として入力された正解ラベルと関数「predict」の出力値から、ニューラルネットワークの導き出した答えが合っているかを確認します。正解の場合は「True」を、不正解の場合は「False」を変数「class_accuracy」に格納します。
これを次々項のプロパティ「accuracy」を使ってどこからでも呼び出せるようにします。これはメインモジュールで学習の回数に対する正解数を求め、ニューラルネットワークの精度を測るためです。

 

Property Get E/accuracy

プロパティの「E」と「accuracy」はメインモジュールから前項の関数「loss」で求めた値を取得するためのものです。学習中に現在の「Loss値」と「学習精度」を表示するために使います。

 

Function gradient

関数「gradient」は今までに実装してきた関数を使い「順電波」「逆伝播」を行う関数です。
実際に学習の処理としてメインモジュールから実行する関数はこの「gradient」です。(コードを見ればわかりますが上記までに出てきた関数「predict」「loss」はこのコードに包含されています)

この関数の引数としては「x()」と「t()」があります。
「x()」はニューラルネットワークへの入力値、つまりはMNIST画像データ(784個の数値)が入った1次元配列で、「t()」はone-hot表現化した正解ラベルが入った1次元配列です。

この関数では今まで作成してきたレイヤ/関数を使って「順伝播(forward)」「逆伝播(backward)」を1回ずつ行います。メインモジュールではこの関数をループ内で呼び出すことで何百、何千回といった学習を行なっていきます。

「順伝播」は関数「loss」を呼び出すだけです。
順伝播では「交差エントロピー誤差(Loss値)」「ニューラルネットワークの最終出力値(導き出した答え)」「出力値が正解か不正解か」の3点を求める事ができます。

「逆伝播」はこれまでに作成したレイヤの関数「backward」を後ろのレイヤから順に行うだけです。
逆伝播により「Affine1」「Affine2」が持っている重みとバイアスパラメータの勾配を求めることができます。各パラメータの勾配を求める事でそのパラメータの値を更新することができます。
ここでいう「更新」とは、順伝播でのLoss値が「0」に近づくように重みパラメータの値とバイアスパラメータの値を微小に変更することを意味します。つまり順伝播と逆伝播を繰り返せば繰り返すほどLoss値が「0」に近づいていき、結果としてどのような画像が入力されても描かれている数字を当てることのできるパラメータ値にすることができるということです。

この関数では「Affine1」「Affine2」が持っている重みとバイアスパラメータの勾配を求めるだけで更新自体は行いません。(勾配の求め方は「Affineレイヤの実装」を参照下さい)

これらパラメータ別の勾配の値は各レイヤで保存されているため、Affineレイヤの関数「ParamsUpdate」を実行するだけでパラメータを更新することができます。Affineレイヤの関数「ParamsUpdate」は次項の関数「ParamsUpdate」でまとめて実行します。

 

Function ParamsUpdate

関数「ParamsUpdate」はその名の通り、パラメータの更新を行うための関数です。

引数としては学習率を表す「LearningRate」があります。

この関数では「Affineレイヤ」で作成した関数「ParamsUpdate」を実行するだけです。
「Affine1」「Affine2」はそれぞれの重みパラメータとバイアスパラメータを持っているので、どちらも関数「ParamsUpdate」でパラメータを更新します。この時、引数との「LearningRate」をそのまま関数「ParamsUpdate」の引数として使います。

 

Function ExportParameters

関数「ExportParameters」では現在の重みパラメータの値とバイアスパラメータの値を指定のシートに全て書き出す関数です。

引数としては「ExportSheet」があります。
これは現在の重み/バイアスパラメータの値を書き出すシートです。

この関数では引数「ExportSheet」の1枚のシートに、現在「Affiner1」と「Affine2」が持っている重みパラメータとバイアスパラメータを全て書き出します。この書き出されたパラメータの値は、学習済みモデルとしてニューラルネットワークの推論(簡単に言えば入力された画像にどの数字が書かれていうかを当てること)に使われます。

また、本ページの初めに出てきた関数「Initialize」でも軽く触れた通り、この書き出された値をそのままニューラルネットワークに取り込み学習を続きから始めることも可能です。

書き出される値は入力層、隠れ層、出力層のニューロンの数によって変化します。メインページの構成(入力層:784 ,隠れ層:64 ,出力層:10)の場合は計50890個の値が書き出されます。

 

Function test_accuracy

関数「test_accuracy」は入力されたテストデータに対して、ニューラルネットワークが導き出した答えが正解「True」か不正解「False」かを返す関数です。

引数としては「x()」と「t()」があります。
「x()」はニューラルネットワークへの入力値、つまりはMNIST画像データ(784個の数値)が入った1次元配列で、「t()」はone-hot表現化した正解ラベルが入った1次元配列です。

ここで注意する点としては入力する画像データと正解ラベルはテスト用データの画像データであるという点です。テスト用データはニューラルネットワークの学習に全く関与していない未知のデータです。

ニューラルネットワークは学習の量が多すぎると、学習に使ったデータのみに特化したニューラルネットワークが出来上がり、少しでも学習の画像データと違うものには全く対応できないという状態になってしまいます。(これを過学習といいます)

そのため学習の合間合間にテスト用データを読み込むことで過学習が起きていないかの確認を行う必要があります。

この関数では、この「学習の合間合間にテスト用データを読み込む」という処理を行っています。
入力されたテスト用データに対し正解か不正解かを求めて「True」「False」を返すだけで、パラメータの値を更新したり、何かの値を変更することは行いません。

 

Function answer

関数「answer」はその名の通り、入力された画像データに対して、ニューラルネットワークが導き出した答え(つまりは「何の数字が描かれているか」)を返す関数です。

引数としては「x()」があります。
これはニューラルネットワークへの入力値、つまりはMNIST画像データ(784個の数値)が入った1次元配列です。これまでの関数と違い、ここでは実際に”自分で描いた28×28(px)の画像データを1次元配列にしたもの”を入力します。(自分で描いた画像を1次元配列に変換するのは別のモジュールで行います)

ニューラルネットワークはSoftmax関数で出力された値をパーセンテージと考え、1番確率の高いもの(正確にいうとそれに紐づく正解ラベル)を最終的な答えとします。

ここで考えるべきは、Softmax関数は全ての入力値の割合から合計が「1」になるような値に変換する関数であるということです。これはSoftmax関数適用前の値の大小関係と、適用後の大小関係は同じであるという考えもできます。

つまりはSoftmax関数を適用しなくても答えを導き出すことができるということです。

よって、学習を行わないこの関数では「Softmax-with-Lossレイヤ」は使わずに、関数「predict」の出力値のうち1番値の大きい出力の正解ラベルを「最終的に導き出した答え」として返します。(Softmax関数を適用しても問題はありませんが、その分 計算時間が増えます)

 

まとめ

ここではゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装を参考にVBAでTwoLayerNetクラスの実装を行いました。

今回実装したTwoLayerNetクラスの機能をまとめると以下の通りです。

Sub Initialize:重み/バイアスパラメータの初期設定 & レイヤの作成
Function predict:順伝播(重み付き入力値の総和にバイアスを加算したものを取得)
Function loss:損失関数とニューラルネットワークが導き出した答えを求める
Property Get E/accuracy:Loss値/学習精度の取得
Function gradient:逆伝播(Affineレイヤの重みとバイアスの勾配を求める)
Function ParamsUpdate:勾配を使って重みとバイアスを更新する
Function ExportParameters:学習終了時に重みとバイアスをExcelシートに書き出す
Function test_accuracy:入力したテスト用データが正解不正解を確認する
Function answer:入力値に対するニューラルネットワークの導き出した答えを返す

このクラスではこれまでに実装したレイヤや関数を1つにまとめています。
複数のモジュール間を行き来するので、初めは戸惑うかも知れませんが実際にやっていることは単純なので順を追ってゆっくり理解してください。

また、このクラスだけを見てもどのように扱うのかがわからないと思うので、ある程度の機能を理解したら「メインモジュールの実装」に移ることをオススメします。

というのもこれまでのページと同様に、本ページだけを見てもどの関数をどのタイミングで呼び出せば学習ができるのか/推論ができるのかのイメージがつかないと思います。

メインモージュールの流れと合わせてみることでこのクラスの中身は深く理解できるので、このページだけをガッツリやるのは避けたほうがいいです。

むしろ、これまでに実装したレイヤ/関数のページや、この後で実装していくメインモージュールの実装ページを行き来しながら、ちょこちょこ理解していく方がイメージがつきやすいと思います。

 

メインページ
 

 icon-book 参考書籍

AI, Deep Learning, VBA

Posted by Lic