Affineレイヤの実装|Excel VBAでMNIST機械学習
本ページは「ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装」を参考にニューラルネットワークを作成していきます。参考書籍では「Affineレイヤ」「ReLUレイヤ」「Softmax-with-Lossレイヤ」という3つのレイヤを作成し、それらを組み合わせることでニューラルネットワークを構築していきます。本サイトでも同じように3つのレイヤをVBAで作成し実装していきます。
本ページではその中の「Affineレイヤ」をVBAで実装していきます。
Affineレイヤの機能
よく機械学習の初学者の方は「最終的に何を求めたいのか」がわからないという人が多いですが、ここでその解答を一言で答えるとすれば「Affineレイヤが持つ重み/バイアスパラメータをより正解に近い値にした時の値」です。
ここでいう”正解”とは、入力された画像に描かれている数字とSoftmax関数で出力する値の最大値を取るラベルがイコールとなることを意味しています。もう少し噛み砕いていうと、入力さた画像データに描かれている数字が何であれ、しっかりと識別することのできるパラメータの値を”正解”と表現しています。
このレイヤには順伝播、逆伝播、パラメータ更新の機能がすべて入っているため単純にこのレイヤを呼び出すだけで簡単にニューラルネットワークを構築することができるようになっています。また、インスタンスを作成するだけで簡単に隠れ層の数を増やすこともできます。(本サイトのコードそのままではできませんが少し書き換えればすぐに増やすことができるようになっています)
機械学習の流れ
今回作成していくニューラルネットワークをレイヤで表すと下図の通りです。
このレイヤの流れは以下のようになっています。(以下では上図の各レイヤを左から「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」のパラメータ(重み/バイアス)が正解に近づくように徐々に更新されていきます。あとは損失関数をもとにループを止めれば学習終了といった流れになってます。
Affineレイヤの実装
まずはクラスモジュールで「Affine_Layer」というモジュールを作成します。
これは逆伝播時に必要になる入力値や重み、バイアス、それらの勾配の値を保管しておくためです。
また本モジュール内では「Option Base 1」を使い配列を「1」スタートにします。
これは要素数とインデックスを合わせ配列同士の計算をわかりやすくするためです。
以下はAffineレイヤの全コードです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
'VBA Affine_Layer(class) Option Explicit Option Base 1 Dim W() As Double Dim b() As Double Dim x() As Double Dim dW() As Double Dim db() As Double '―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― Public Function init(ByRef input_W() As Double, ByRef input_b() As Double) Dim i As Long Dim j As Long ReDim W(UBound(input_W, 1), UBound(input_W, 2)) For i = 1 To UBound(input_W, 1) For j = 1 To UBound(input_W, 2) W(i, j) = input_W(i, j) Next j Next i ReDim b(UBound(input_b)) For i = 1 To UBound(input_b) b(i) = input_b(i) Next i End Function '―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― Public Function forward(ByRef input_x() As Double) As Double() Dim i As Long Dim out() As Double ReDim x(UBound(input_x)) ReDim out(UBound(input_x)) For i = 1 To UBound(input_x) x(i) = input_x(i) Next i out = Functions.dot(x, W) '重み付き入力値の総和 out = Functions.add(out, b) 'バイアス加算 forward = out End Function '―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― Public Function backward(ByRef dout() As Double) Dim i As Long Dim j As Long Dim WT() As Double Dim dx() As Double ReDim WT(UBound(W, 2), UBound(W, 1)) For i = 1 To UBound(W, 1) For j = 1 To UBound(W, 2) WT(j, i) = W(i, j) Next j Next i ReDim dx(UBound(W, 1)) dx = Functions.dot(dout, WT) ReDim dW(UBound(W, 1), UBound(W, 2)) dW = Functions.dot2(x, dout) ReDim db(UBound(W, 2)) db = dout backward = dx End Function '―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― Public Function ParamsUpdate(ByRef LearningRate As Double) 'Wの更新 Dim i As Long Dim j As Long For i = 1 To UBound(W, 1) For j = 1 To UBound(W, 2) W(i, j) = W(i, j) - (LearningRate * dW(i, j)) Next j Next i 'bの更新 For i = 1 To UBound(b) b(i) = b(i) - (LearningRate * db(i)) Next i End Function '―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― Public Property Get Weight() Weight = W End Property '―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― Public Property Get Bias() Bias = b End Property |
AffineレイヤはPythonで書くと以下のようなコードです。(書籍のコード)
引数や関数名などはできる限りは揃えているのでPythonコードが読める方は見比べてみて下さい。
この時点でPythonで書くのがどれだけ楽か見てわかると思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#Python Affineレイヤ class Affine: def __init__(self, W, b): self.W =W self.b = b self.x = None self.dW = None self.db = None def forward(self, x): self.x = x out = np.dot(self.x, self.W) + self.b return out def backward(self, dout): dx = np.dot(dout, self.W.T) self.dW = np.dot(self.x.T, dout) self.db = np.sum(dout, axis=0) return dx |
この層では特に顕著に表れますがニューラルネットワークの中では多くの行列計算が行われます。
Pythonでは行列計算を得意とする「NumPy」というライブラリがあるので非常に簡単に実装できますが、VBAには行列という概念がなく配列で再現していくので少し難しく感じると思います。
ただ、やっている内容を理解するのは少し難しいですが、計算自体は非常にシンプルです。
ここでは各配列の次元数と”かたち”を意識して見ていきましょう。
以下では各関数で何を行っているかを解説していきます。
※この層では標準モジュールで作成した「Functions」の関数をいくつか呼び出して使用するので、「Functions」も合わせて読み進めて下さい。
Function init
関数「init」はインスタンス作成時の初期設定を行う関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
'VBA Affine_Layer(class) Dim W() As Double Dim b() As Double Dim x() As Double Dim dW() As Double Dim db() As Double '―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― Public Function init(ByRef input_W() As Double, ByRef input_b() As Double) Dim i As Long Dim j As Long ReDim W(UBound(input_W, 1), UBound(input_W, 2)) For i = 1 To UBound(input_W, 1) For j = 1 To UBound(input_W, 2) W(i, j) = input_W(i, j) Next j Next i ReDim b(UBound(input_b)) For i = 1 To UBound(input_b) b(i) = input_b(i) Next i End Function |
この関数の引数としては「input_W()」と「input_b()」があります。
これらは重みパラメータが入った配列とバイアスパラメータが入った配列です。
この関数の処理は、これらの値をループ文を使ってAffineレイヤ内にある「W()」と「b()」に値を入れるだけです。このとき「W / input_W」は同じ要素数の2次元配列、「b / input_b」は同じ要素数の1次元配列ということに注意しておきましょう。
これにより、順伝播/逆伝播を行う際にここで入力された重み/バイアスを利用することが可能になります。クラスモジュール内に重み/バイアスを入れておくことで、他のモジュールに値を受け渡しすることなくこのモジュール内だけで学習を完結させることができます。(重み/バイアスの更新もこのモジュール内で行うため)
Function forward
関数「forward」は名前のとおり順伝播を行う関数です。
順伝播といっても、活性化関数を適用する前の入力値の重み付き総和を求めるまでです。
上図でいうと「a1」を求めるだけの単純な機能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
'VBA Affine_Layer(class) Public Function forward(ByRef input_x() As Double) As Double() Dim i As Long Dim out() As Double ReDim x(UBound(input_x)) ReDim out(UBound(input_x)) For i = 1 To UBound(input_x) x(i) = input_x(i) Next i out = Functions.dot(x, W) '重み付き入力値の総和 out = Functions.add(out, b) 'バイアス加算 forward = out End Function |
この関数の引数としては「input_x()」があります。
これはMNIST画像データの784個の数字の入った1次元配列です。
この入力値に対し関数「init」で入力された重みパラメータ「W()」とバイアスパラメータ「b()」を使って入力値の重み付き総和を求めます。
Pythonの場合は「np.dot(self.x, self.W)」と書くだけで行列計算が行われます。VBAには「dot」に当たる機能はありませんが「Functions」モジュールで関数「dot」を用意しておいたのでPythonと同じように「Functions.dot(x, W)」と書くことで配列の積を求めることができます。
「Functions.dot(x, W)」で入力値と重みの積の総和は求めることができたので、バイアスを加算していきます。関数「dot」と同じく「Functions」モジュールの関数「add」を使い入力値と重みの積の総和にバイアスを加算します。
最終的にこの入力値と重みの積の総和にバイアスを加算したもの(上図でいう「a1」)を戻り値として返します。
また関数「init」と同様にコードの途中で「x()」に「input_x()」の値を入れています。
これは逆伝播時にこの層に入力された値が必要になるためです。
Function backward
関数「backward」は名前のとおり逆伝播を行う関数です。
ここでは重みとバイアスの勾配を求めるとともに、次の層へ渡すための「微分の値」を出力します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
'VBA Affine_Layer(class) Public Function backward(ByRef dout() As Double) Dim i As Long Dim j As Long Dim WT() As Double Dim dx() As Double ReDim WT(UBound(W, 2), UBound(W, 1)) For i = 1 To UBound(W, 1) For j = 1 To UBound(W, 2) WT(j, i) = W(i, j) Next j Next i ReDim dx(UBound(W, 1)) dx = Functions.dot(dout, WT) ReDim dW(UBound(W, 1), UBound(W, 2)) dW = Functions.dot2(x, dout) ReDim db(UBound(W, 2)) db = dout backward = dx End Function |
この関数の引数としては「dout()」があります。
この層の逆伝播では前の層で求めた微分の値「dout()」を使って、「この層の微分の値」「重みパラメータの勾配」「バイアスパラメータの勾配」の3種類を求めます。
「この層の微分の値」は次の層へ逆伝播するために、「重みパラメータの勾配」「バイアスパラメータの勾配」はそれぞれのパラメータの値を更新するために求めます。
逆伝播の本来の目的は重みパラメータとバイアスパラメータの勾配を求めることにあります。
勾配を求めることで、重みとバイアスをより正解に近い値に更新することができるためです。
(パラメータの更新自体は次項の関数「ParamsUpdate」で行います)
微分の値を求める「dx()」
引数の「dout()」は逆伝播でいう前の層で求めた「微分の値」が入った配列です。
(この層が隠れ層だとしたら、前の層は出力層を指す)
この「前の層から伝わった微分の値」と「この層の重みパラメータ」をかけることで、「この層の微分値」を求めることができます。(これは連鎖律と偏微分による考えです。詳しい説明は書籍、もしくは他サイトを参照下さい)
このとき「dout()」と「W()」は行列(配列)のかたち上、そのまま計算することはできません。
たとえばこの層が隠れ層の場合、「dout()」は「(1×)10」の1次元配列ですが、「W()」は「64×10」の2次元配列です。これを計算するには「W()」のかたちを「10×64」に変換する必要があります。このように行列の要素(i,j)を(j,i)に入れ替えることを「転置」といいます。
Pythonでは「self.W.T」というように後ろに「.T」を付けるだけで転置を行うことができますが、VBAではそう簡単にできません。(ExcelのTRANSPOSE関数を使えば転置させることができますが、値をセルに書き出しておく必要があります。何百何千回と学習を行うことを考えるとセルへの書き出しは処理時間の無駄なのでここでは使用しません)
というわけでここではループ文を使い「W()」の転置の値を「WT()」に書き出しています。
あとは「Functions」モジュールの関数「dot」を使って「dout()」と「WT()」の積を求めるだけです。
この計算結果は「dx()」に格納し戻り値として返します。
この「dx()」を「dout()」として次の層の「backward」に入れることで逆伝播が連鎖していきます。
重みパラメータの勾配を求める「dW()」
重みの勾配を求めるには「順伝播時の入力値」と「微分の値」をかけることで求められます。
コードの変数名でいうと「x()」と「dout()」の積です。
実際はこの「X()」も転置させてかける必要がありますが、1次元配列のためここでは転置をしていません。そのかわり積を求める際には「Functions」モジュールの関数「dot」ではなく、専用の関数「dot2」を使います。(転置をしなくても積を求められるようにした関数)
ここで求めた重みパラメータの勾配は「dW()」に格納します。
バイアスパラメータの勾配を求める「db()」
バイアスの勾配はそのまま「微分の値」が入ります。コードの変数名でいうと「dout()」です。
実際は「dout()」が多次元配列の場合は1次元にまとめる必要がありますが、今回は「dout()」は必ず1次元になるようになっているのでそのままの値を入れることになります。(バッチ処理などを行うと多次元になります)
よって「dout()」はバイアスパラメータの勾配として「db()」にそのまま格納します。
Function ParamsUpdate
関数「ParamsUpdate」は名前のとおりパラメータの更新を行う関数です。
この機能はPython(書籍)では別の場所に合った機能をVBA向けに変換して持ってきたものです。
この関数を実行することで、この層の重み「W()」とバイアス「b()」の値を正解(正解を導き出す可能性の高い値)に近づけるように微小に更新することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
'VBA Affine_Layer(class) Public Function ParamsUpdate(ByRef LearningRate As Double) 'Wの更新 Dim i As Long Dim j As Long For i = 1 To UBound(W, 1) For j = 1 To UBound(W, 2) W(i, j) = W(i, j) - (LearningRate * dW(i, j)) Next j Next i 'bの更新 For i = 1 To UBound(b) b(i) = b(i) - (LearningRate * db(i)) Next i End Function |
この関数の引数としては「LearningRate」があります。これは学習率を表していてます。
基本的に学習率は「0.001~0.01」という値が設定されますが、学習の精度によって変化させることができます。(LearningRateはメインモジュールで定義/設定をします)
パラメータの更新は以下の式で行われます。(W(1,1)の更新の場合)
W(1,1) = W(1,1) – (LearningRate * dW(1, 1))
これはバイアスの場合も同じで、各パラメータから各パラメータの勾配と学習率の積を引いた値が更新後の値となります。
ここでは関数「backward」で求めた重みパラメータの勾配「dW()」とバイアスパラメータの勾配「db()」を使って、全パラメータの値を更新します。
更新後の値はそのまま「W()」「b()」に上書きしていきます。
そうすることで再び順伝播(関数「forward」)を行うときには更新済みのパラメータで順伝播が行われます。これにより順伝播と逆伝播を何回も繰り返していくと、徐々に重み「W()」とバイアス「b()」が正解(正解を導き出す可能性の高い値)に近づいていくという訳です。
Property Get Weight/Bias
プロパティの「Weight」と「Bias」は他のモジュールからこの層の重み「W()」とバイアス「b()」を取得するためのものです。学習が完了し重み/バイアスパラメータの値を書き出す際に使います。
1 2 3 4 5 6 7 8 9 10 11 |
Public Property Get Weight() Weight = W End Property '―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― Public Property Get Bias() Bias = b End Property |
まとめ
ここでは「ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装」を参考にVBAでAffineレイヤの実装を行いました。
今回実装したAffineレイヤの機能をまとめると以下の通りです。
Function init:Affineレイヤの初期化(インスタンス作成時に実行)
Function forward:順伝播(「重み付き入力値の総和+バイアス」の値を求める)
Function backward:逆伝播(微分の値 / 重みの勾配 / バイアスの勾配を求める)
Function ParamsUpdate:重み/バイアスパラメータの更新
Property Get Weight/Bias:パラメータの取得
逆伝播(backward)の部分はおそらく大半の人がつまづく部分なので、本ページだけでなく書籍や他サイトのページも参考にして理解を深めて下さい。
今回やったAffineレイヤはニューロン間の値を受け渡すニューラルネットワークの肝の部分となる部分です。また、最終的に求めたい重み「W()」とバイアス「b()」を持っているレイヤでもあり一番重要な部分といっても過言ではないので、何をやっているかは必ず理解しておきましょう。
ニューラルネットワークはいくつものレイヤが紐づくことで学習が可能になります。
つまり本ページの内容だけではニューラルネットワーク内で何が行われているのか理解できません。
メインページよりいくつかのページと関連付けて見ることでようやく理解ができるようになります。
ニューラルネットワークについてはぜひ、時間をかけてじっくりと学んでください。
最終的なイメージが全くつかない人は、1度コピペでもいいのでニューラルネットワークをすべて構築して実際に機械学習を行ってみることをオススメします。