Functionsモジュールの実装|Excel VBAでMNIST機械学習
本ページは「ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装」を参考にニューラルネットワークを作成していきます。参考書籍では「Affineレイヤ」「ReLUレイヤ」「Softmax-with-Lossレイヤ」という3つのレイヤを作成し、それらを組み合わせることでニューラルネットワークを構築していきます。本サイトでも同じように3つのレイヤをVBAで作成し実装していきます。
ここでは今後必要になる機能を関数として1つの「Functions」モジュールにまとめていきます。ここに様々な機能をまとめておくことで、実際にその機能を使いたい場合は呼び出すだけで実行可能という状態にすることができます。
たとえばPythonの場合は「NumPy」の「dot」を使うことで簡単に行列計算を行うことができます。
しかしVBAにはそのような機能がないためこのモジュールに「NumPy」の「dot」と同じ機能を持つ関数「dot」を作成します。これにより、このFunctionsモジュールの関数「dot」を呼び出すことで「NumPy」の「dot」と同じように行列計算(VBAでは配列計算ですが)を行うことが可能になります。
このようにPythonでは用意されていてVBAに無い機能をはじめ、いくつかの機能をここでまとめていきます。
Functionsモジュールの実装
まずは標準モジュールで「Functions」というモジュールを作成します。
「Functions」では「Option Base 1」を使い配列を「1」スタートにします。
これは要素数とインデックスを合わせ配列同士の計算をわかりやすくするためです。
以下はFunctionsモジュールの全コードです。
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 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
'VBA Functions(module) Option Explicit Option Base 1 '―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― Public Function KeyPress() As Boolean Const KEY_PRESSED = -32768 KeyPress = (GetAsyncKeyState(vbKeyF7) And KEY_PRESSED) = KEY_PRESSED End Function '―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― Public Function GetRandomRow(ByRef DataCount As Long, MAX_DATA As Long) As Long() Dim i As Long, Num As Long Dim flag() As Boolean Dim RndRows() As Long ReDim flag(MAX_DATA) ReDim RndRows(MAX_DATA) For i = 1 To DataCount Randomize Do Num = Int((MAX_DATA - 1 + 1) * Rnd + 1) Loop Until flag(Num) = False RndRows(i) = Num flag(Num) = True Next i ReDim Preserve flag(DataCount) ReDim Preserve RndRows(DataCount) GetRandomRow = RndRows End Function '―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― Public Function RandomWeight(ByRef size1 As Long, ByRef size2 As Long) As Double() Dim i As Long Dim j As Long Dim W As Double Dim List() As Double ReDim List(size1, size2) For i = 1 To size1 For j = 1 To size2 Randomize Do W = (Rnd * 2) - 1 Loop Until W <> 0 List(i, j) = W Next j Next i RandomWeight = List End Function '―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― Public Function zeros(ByRef input_size As Long) As Double() Dim i As Long Dim ZerosList() As Double ReDim ZerosList(input_size) For i = 1 To input_size ZerosList(i) = 0 Next i zeros = ZerosList End Function '―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― Public Function dot(ByRef x() As Double, ByRef W() As Double) As Double() Dim i As Long Dim j As Long Dim A() As Double ReDim A(UBound(W, 2)) Dim sum As Double For i = 1 To UBound(W, 2) sum = 0 For j = 1 To UBound(x) sum = sum + (x(j) * W(j, i)) Next j A(i) = sum Next i dot = A End Function '―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― Public Function dot2(ByRef x() As Double, ByRef dout() As Double) As Double() Dim i As Long Dim j As Long Dim dW() As Double ReDim dW(UBound(x), UBound(dout)) For i = 1 To UBound(dout) For j = 1 To UBound(x) dW(j, i) = x(j) * dout(i) Next j Next i dot2 = dW End Function '―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― Public Function add(ByRef x() As Double, ByRef Bias() As Double) As Double() Dim i As Long Dim A() As Double ReDim A(UBound(x)) For i = 1 To UBound(x) A(i) = x(i) + Bias(i) Next i add = A End Function '―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― Public Function softmax(ByRef x() As Double) As Double() Dim i As Long Dim max As Double Dim sum As Double Dim z() As Double ReDim z(UBound(x)) max = x(1) For i = 1 To UBound(x) If x(i) > max Then max = x(i) End If Next i sum = 0 For i = 1 To UBound(x) sum = sum + Exp(x(i) - max) 'オーバーフロー対策 Next i For i = 1 To UBound(x) z(i) = Exp(x(i) - max) / sum Next i softmax = z End Function '―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― Public Function cross_entropy_error(ByRef y() As Double, ByRef t() As Long) Dim delta As Double delta = 0.0000001 'infを発生させないため微小な数字を加算する Dim i As Long Dim sum As Double sum = 0 For i = 1 To UBound(t) sum = sum + (t(i) * Log(y(i) + delta)) Next cross_entropy_error = sum * -1 End Function '―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― Public Function one_hot_t(ByRef t_size As Long, ByRef label As Integer) As Long() Dim labels() As Long ReDim labels(t_size) If label = 0 Then labels(10) = 1 '※Option Base 1でlabels(0)が存在しないためlabels(10)を使用 Else labels(label) = 1 End If one_hot_t = labels End Function |
以下では各関数で何を行っているかを解説していきます。
Function KeyPress
関数「KeyPress」は[F7]キーが押された時にTrueを返す関数です。
※これはWindows APIの「GetAsyncKeyState関数」を利用しています。
上記リンク先でGetAsyncKeyState関数の使い方を解説しているので併せて参照ください。
GetAsyncKeyState関数の宣言はメインモジュールで行うため、ここでは[F7]キーが押された場合にTrueを返すという機能だけ作成します。
1 2 3 4 5 |
'VBA Functions(module) Public Function KeyPress() As Boolean Const KEY_PRESSED = -32768 KeyPress = (GetAsyncKeyState(vbKeyF7) And KEY_PRESSED) = KEY_PRESSED End Function |
機械学習の最終目的は「適切な重みパラメータとバイアスパラメータを求めること」です。
しかし、実際のところ「適切なパラメータはコレです!」というように明確な答えは導き出せません。最終的にはこれ以上は精度が上がらないなという妥協点を見つけて学習を終了させます。
学習はメインモジュールで「Do~Loop」を使いループ状態にして行いますが、この関数はそのループの終了条件のための関数という訳です。(ループ内ではIf文を使いこの関数からTrueが返ってきたらループを終了させるようにします)
[F7]キーである理由はあまり作業の支障にならないキーだからです。
というのも、学習にはある程度時間がかかるので学習しながら他の作業をしたくなります。
その時に[Enter]キーなどのよく使うキーを設定していると、別作業中に意図しない場面で学習が終了してしまうため、あまり押されないキーとして[F7]キーを設定しています。([F7]キーをよく使う人は別のキーに変更しておきましょう)
Function GetRandomRow
関数「GetRandomRow」は入力された範囲内で、入力された個数分の整数をランダムで生成し、1つの配列に入れて返す関数です。
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 |
'VBA Functions(module) Public Function GetRandomRow(ByRef DataCount As Long, MAX_DATA As Long) As Long() Dim i As Long, Num As Long Dim flag() As Boolean Dim RndRows() As Long ReDim flag(MAX_DATA) ReDim RndRows(MAX_DATA) For i = 1 To DataCount Randomize Do Num = Int((MAX_DATA - 1 + 1) * Rnd + 1) Loop Until flag(Num) = False RndRows(i) = Num flag(Num) = True Next i ReDim Preserve flag(DataCount) ReDim Preserve RndRows(DataCount) GetRandomRow = RndRows End Function |
引数としては「DataCount」と「MAX_DATA」があります。
どちらも整数型で個数(DataCount)と範囲(MAX_DATA)を表しています。
やっていることは単純で、たとえばDataCount=100、MAX_DATA=60000とします。
この場合、返ってくるのは1以上60000以下の範囲での100個の整数が入った配列です。
このとき整数は毎回ランダムで重複無しになっています。
これはMNIST学習データを無作為で取り出すためのインデックス(行数)として使います。
MNISTのページでもやった通り、MNISTデータは1行が1画像を表しています。
つまりここで取得した値の行を取得すればDataCount個の画像をランダムで抜き出すことができるということです。
ランダムで抜き出す理由は、毎回60000枚分の画像データをすべて学習していると時間がかかるためです。無作為で選んだいくつかのデータのみを学習させることで学習時間を短縮しようという考えからきています。(ミニバッチ処理と同じ考えです)
Function RandomWeight
関数「RandomWeight」は入力された値の数だけランダムで重みパラメータを生成し、2次元配列として返す関数です。
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 |
'VBA Functions(module) Public Function RandomWeight(ByRef size1 As Long, ByRef size2 As Long) As Double() Dim i As Long Dim j As Long Dim W As Double Dim List() As Double ReDim List(size1, size2) For i = 1 To size1 For j = 1 To size2 Randomize Do W = (Rnd * 2) - 1 Loop Until W <> 0 List(i, j) = W Next j Next i RandomWeight = List End Function |
引数としては「size1」「size2」があります。
これらは返り値である2次元配列の”かたち”を決めるもので、「size1」×「size2」個の重みパラメータ「W」が作成されます。重みパラメータの値は-1以上1以下の小数でランダムに生成されます。生成した値は全て2次元配列の「List()」に格納し、この2次元配列を返します。
重みパラメータのランダム生成は、学習の1番初めに行う初期設定のようなものです。これは重みパラメータの初期値がすべて「0」の状態で学習を始めると、学習がうまく進まなくなるためです。
Function zeros
関数「zeros」は入力された値の数だけ「0」の要素を持つ1次元配列を返す関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
'VBA Functions(module) Public Function zeros(ByRef input_size As Long) As Double() Dim i As Long Dim ZerosList() As Double ReDim ZerosList(input_size) For i = 1 To input_size ZerosList(i) = 0 Next i zeros = ZerosList End Function |
引数としては「input_size」があります。
これは返り値である1次元配列の要素数を決めるものです。
機能自体は非常にシンプルで、「input_size」個の要素を持つ配列を返すだけです。
このとき、要素の値は全て「0」で返します。
たとえば「input_size=5」の場合、返ってくる1次元配列は「zeros(0,0,0,0,0)」となります。
前項の関数「RandomWeight」は重みパラメータの初期化に使いましたが、この関数はバイアスパラメータの初期化の際に使います。重みパラメータは「0」であると学習が進みませんがバイアスパラメータの値は「0」でも問題がないためです。
Function dot
関数「dot」は入力された1次元配列と2次元配列の積を配列として返す関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
'VBA Functions(module) Public Function dot(ByRef x() As Double, ByRef W() As Double) As Double() Dim i As Long Dim j As Long Dim A() As Double ReDim A(UBound(W, 2)) Dim sum As Double For i = 1 To UBound(W, 2) sum = 0 For j = 1 To UBound(x) sum = sum + (x(j) * W(j, i)) Next j A(i) = sum Next i dot = A End Function |
引数としては「x()」と「W()」があります。
「x()」は1次元配列、「W()」は2次元配列である必要があります。
この関数では2つの引数の積を1つの1次元配列として返します。
ここで行う計算は行列での掛け算と同じです。
VBAでは行列がないため配列を使って計算していますが中身は同じです。
たとえば「x()」の要素数が「3」、「W()」の要素数が「3×2」の場合
以下のような計算が行われ、最終的には要素数が「2」の1次元配列が出来上がります。
Function dot2
関数「dot2」は入力された1次元配列と1次元配列の積を2次元配列として返す関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
'VBA Functions(module) Public Function dot2(ByRef x() As Double, ByRef dout() As Double) As Double() Dim i As Long Dim j As Long Dim dW() As Double ReDim dW(UBound(x), UBound(dout)) For i = 1 To UBound(dout) For j = 1 To UBound(x) dW(j, i) = x(j) * dout(i) Next j Next i dot2 = dW End Function |
引数としては「x()」と「dout()」があります。
「x()」と「dout()」はともに1次元配列である必要があります。
この関数では2つの引数の積を1つの2次元配列として返します。
1次元配列同士の掛け算ですが、行列のかたちとしては「n×1」と「1×m」になります。
たとえば「x()」の要素数が「3」、「dout()」の要素数が「3」の場合
以下のような計算が行われ、最終的には要素数が「3×3」の2次元配列が出来上がります。
Function add
関数「add」は入力された1次元配列と1次元配列の和を1次元配列として返す関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
'VBA Functions(module) Public Function add(ByRef x() As Double, ByRef Bias() As Double) As Double() Dim i As Long Dim A() As Double ReDim A(UBound(x)) For i = 1 To UBound(x) A(i) = x(i) + Bias(i) Next i add = A End Function |
引数としては「x()」と「Bias()」があります。
「x()」と「Bias()」はともに1次元配列である必要があります。
このとき2つの引数の要素数は同じである必要があります。
この関数では2つの引数の各要素ごとの和を1次元配列として返します。
たとえば「x(1,2,3)」と「Bias(4,5,6)」というような値を持っていた時
返ってくる配列は「add(5,7,9)」となります。
Function softmax
関数「softmax」は入力された値にSoftmax関数を適応する関数です。
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 |
'VBA Functions(module) Public Function softmax(ByRef x() As Double) As Double() Dim i As Long Dim max As Double Dim sum As Double Dim z() As Double ReDim z(UBound(x)) max = x(1) For i = 1 To UBound(x) If x(i) > max Then max = x(i) End If Next i sum = 0 For i = 1 To UBound(x) sum = sum + Exp(x(i) - max) 'オーバーフロー対策 Next i For i = 1 To UBound(x) z(i) = Exp(x(i) - max) / sum Next i softmax = z End Function |
引数としては「x()」があります。
これは重み付き入力値の総和にバイアスを加算した値が入った1次元配列です。
今後、Softmax関数の機能と交差エントロピーの機能をまとめた「Softmax-with-Lossレイヤ」というものをクラスモジュールで実装していきます。
そのレイヤ内でSoftmax関数を実装してもいいのですが、レイヤ内はできる限りシンプルな構造にしたいのでここでSoftmax関数の機能を作っておき、あとは呼び出すだけという状態にしておきます。
Function cross_entropy_error
関数「cross_entropy_error」は損失関数である交差エントロピー誤差を求めるための関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
'VBA Functions(module) Public Function cross_entropy_error(ByRef y() As Double, ByRef t() As Long) Dim delta As Double delta = 0.0000001 'infを発生させないため微小な数字を加算する Dim i As Long Dim sum As Double sum = 0 For i = 1 To UBound(t) sum = sum + (t(i) * Log(y(i) + delta)) Next cross_entropy_error = sum * -1 End Function |
引数としては「y()」と「t()」があります。
「y()」はSoftmax関数の最終出力の入った1次元配列、
「t()」は正解ラベル(one-hot表現)の入った1次元配列です。
この関数で最終的に出力されるのが、いわゆる「Loss値」です。
このLoss値が0に近いほど、重み/バイアスパラメータが適切な値になっていることを表します。
前項の関数「softmax」と同じく、今後実装していく「Softmax-with-Lossレイヤ」で機能を呼び出すだけの状態にしておくため、この「Functions」モジュールで実装しています。
Function one_hot_t
関数「one_hot_t」は入力された値を「one-hot表現」に変換して1次元配列として返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
'VBA Functions(module) Public Function one_hot_t(ByRef t_size As Long, ByRef label As Integer) As Long() Dim labels() As Long ReDim labels(t_size) If label = 0 Then labels(10) = 1 '※Option Base 1でlabels(0)が存在しないためlabels(10)を使用 Else labels(label) = 1 End If one_hot_t = labels End Function |
引数としては「t_size」と「label」があります。
「t_size」は返り値の1次元配列の要素数、「label」は「0~9」のいずれかの値です。
(MNIST学習の場合「0~9」の10種類あるので「t_size」は「10」で固定です)
「one-hot表現」とは1つの値が「1」でそれ以外の値が「0」で表現されたベクトル(ここでいう配列)のことを指します。
この関数では引数「label」(MNISTの正解ラベル)に応じて以下のように変換し、変換後の値を1次元配列として返します。
「label=1」の場合は「1,0,0,0,0,0,0,0,0,0」
「label=2」の場合は「0,1,0,0,0,0,0,0,0,0」
「label=3」の場合は「0,0,1,0,0,0,0,0,0,0」
「label=4」の場合は「0,0,0,1,0,0,0,0,0,0」
「label=5」の場合は「0,0,0,0,1,0,0,0,0,0」
「label=6」の場合は「0,0,0,0,0,1,0,0,0,0」
「label=7」の場合は「0,0,0,0,0,0,1,0,0,0」
「label=8」の場合は「0,0,0,0,0,0,0,1,0,0」
「label=9」の場合は「0,0,0,0,0,0,0,0,1,0」
「label=0」の場合は「0,0,0,0,0,0,0,0,0,1」
まとめ
ここでは今後作成していくレイヤやモジュールで扱うための様々な関数をまとめて実装しました。
今回実装したFunctionsモジュールの機能をまとめると以下の通りです。
Function KeyPress:[F7]キーが押下されるとTrueを返す(学習終了の条件)
Function GetRandomRow:ランダムで生成した行数を返す
Function RandomWeight:ランダムで生成した重みパラメータを返す(重みの初期化)
Function zeros:要素がすべて0の配列を返す(バイアスの初期化)
Function dot:配列同士の積を返す(1次元配列×2次元配列)
Function dot2:配列同士の積を返す(1次元配列×1次元配列)
Function add:配列同士の和を返す(1次元配列+1次元配列)
Function softmax:Softmax関数を適応した値を返す
Function cross_entropy_error:交差エントロピー誤差を返す
Function one_hot_t:入力値をone-hot表現に変換した値を返す
おそらく関数だけ見ても何のためにあるのか理解できないと思うので、今後作成していく内容を見ながら適宜本ページに戻ってきて確認して下さい。
逆にいえばはじめのうちはあまり理解していなくても問題ありません。
ニーラルネットワークはいくつものレイヤが紐づくことで学習が可能になります。
つまり本ページの内容だけではニューラルネットワーク内で何が行われているのか理解できません。
メインページよりいくつかのページと関連付けて見ることでようやく理解ができるようになります。
ニューラルネットワークについてはぜひ、時間をかけてじっくりと学んでください。
最終的なイメージが全くつかない人は、1度コピペでもいいのでニューラルネットワークをすべて構築して実際に機械学習を行ってみることをオススメします。
参考書籍