Affineレイヤの実装|Excel VBAでIris分類問題
本ページは「ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装」を参考にニューラルネットワークを作成していきます。参考書籍では「Affineレイヤ」「ReLUレイヤ」「Softmax-with-Lossレイヤ」という3つのレイヤを作成し、それらを組み合わせることでニューラルネットワークを構築していきます。本サイトでも同じように3つのレイヤをVBAで作成し実装していきます。(ReLUレイヤの代わりとなるSigmoidレイヤというものも実装します)
本ページではその中の「Affineレイヤ」をVBAで実装していきます。
Affineレイヤの機能
よく機械学習の初学者の方は「最終的に何を求めたいのか」がわからないという人が多いですが、ここでその解答を一言で答えるとすれば「Affineレイヤが持つ重み/バイアスパラメータをより正解に近い値にした時の値」です。
ここでいう”正解”とは、入力された4つの数字とSoftmax関数で出力する値の最大値を取るラベル(花の種類)がイコールとなることを意味しています。もう少し噛み砕いていうと、入力さた4つの数字が何であれ、しっかりと花の種類を識別することのできるパラメータの値を”正解”と表現しています。
このレイヤには順伝播、逆伝播、パラメータ更新の機能がすべて入っているため単純にこのレイヤを呼び出すだけで簡単にニューラルネットワークを構築することができるようになっています。また、インスタンスを作成するだけで簡単に隠れ層の数を増やすこともできます。(本サイトのコードそのままではできませんが少し書き換えればすぐに増やすことができるようになっています)
機械学習の流れ
今回作成していくニューラルネットワークをレイヤで表すと下図の通りです。
このレイヤの流れは以下のようになっています。(以下では上図の各レイヤを左から「Affine1」「ReLU」「Affine2」「Softmax-with-Loss」として説明していきます)
1. 入力値(4つの数字)を「Affine1」に入れ、入力値と重みの総和を計算する。
2.「Affine1」の出力を「ReLU」に入れて活性化させる。(Sigmoidにも変更可能)
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 |
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 |
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 |
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 |
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()」があります。これは入力値であるガクの長さ、ガクの幅、花弁の長さ、花弁の幅の4つの数字が入った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」のパラメータ(Wとb)と同様にコードの途中で本レイヤ内の配列「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 |
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種類を求めます。
「この層の微分の値」は次の層へ逆伝播するために、「重みパラメータの勾配」「バイアスパラメータの勾配」はそれぞれのパラメータの値を更新するために求めます。(「Excel関数で学ぶニューラルネットワーク(順伝播編) / (逆伝播編)」を先にやっておくとここのイメージがつかみやすくなります)
逆伝播の本来の目的は重みパラメータとバイアスパラメータの勾配を求めることにあります。
これは勾配を求めることで、重みとバイアスをより正解に近い値に更新することができるためです。
(パラメータの更新自体は次項の関数「ParamsUpdate」で行います)
Function ParamsUpdate
関数「ParamsUpdate」は名前のとおりパラメータの更新を行う関数です。
この関数を実行することで、この層の重み「W()」とバイアス「b()」の値を正解(正解を導き出す可能性の高い値)に近づけるように”微小”に更新することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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:パラメータの取得
ニューラルネットワーク内の計算の流れがつかめない人は先に「Excel関数で学ぶニューラルネットワーク(順伝播編) / (逆伝播編)」の2つのページで手動によるニューラルネットワークの学習をすることをお勧めします。
正直なお話、このレイヤのコードだけ見ても、おそらく何をやっているのかわからない人が大半だと思います。先に全体の流れをつかむことで、本レイヤの各機能で何をしているかを理解しやすくなります。
次回は活性化関数であるランプ関数を使うReLUレイヤの実装です。
【次回】ReLUレイヤの実装
【前回】Functionsモジュールの実装
メインページ