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モジュールの全コードです。

'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を返すという機能だけ作成します。

'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つの配列に入れて返す関数です。

'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次元配列として返す関数です。

'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次元配列を返す関数です。

'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次元配列の積を配列として返す関数です。

'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次元配列として返す関数です。

'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次元配列として返す関数です。

'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関数を適応する関数です。

'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」は損失関数である交差エントロピー誤差を求めるための関数です。

'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次元配列として返します。

'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度コピペでもいいのでニューラルネットワークをすべて構築して実際に機械学習を行ってみることをオススメします。

メインページ
 

 icon-book 参考書籍

 

2021年1月3日AI,Deep Learning,Excel,VBA