Functionsモジュールの実装|Excel VBAでIris分類問題

今回は今後必要になる機能(関数)をまとめた「Functions」モジュールを実装していきます。
前回やったランダムで行数を取得する関数「GetRandomRow」のような、ニューラルネットワークで機械学習する際にあると便利な関数をまとめておくことで、メインのコードではこれらの関数を呼び出すだけで簡単に使用することができます

たとえば機械学習に優れた言語であるPythonの場合は「NumPy」の「dot」を使うことで簡単に行列計算を行うことができます。
しかしVBAにはそのような機能がないためこのモジュールに「NumPy」の「dot」と同じ機能を持つ関数「dot」を作成します。これにより、このFunctionsモジュールの関数「dot」を呼び出すことで「NumPy」の「dot」と同じように行列計算(VBAでは配列計算ですが)を行うことが可能にな流というわけです。

このようにPythonでは用意されていてVBAに無い機能をはじめ、いくつかの機能をここでまとめていきます。初めはいつ使う機能かわからないものも多いと思いうので、単純にコピペだけでもOKです。

 

unctionsモジュールの実装

まずは標準モジュールで「Functions」というモジュールを作成します。

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

以下はFunctionsモジュールの全コードです。

Option Explicit
Option Base 1
'――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
'************************************************************************************
'   関数名 :GetRandomRow
'   機能   :指定範囲内でランダムで生成した行数(自然数)を返す
'   引数   :DataCount      生成する値の数
'          :MaxDataCount  生成する値の上限値
'          :MinDataCount   生成する値の下限値(省略可/省略した場合は「1」となる)
'************************************************************************************

Public Function GetRandomRow(DataCount As Long, MaxDataCount As Long, Optional MinDataCount As Long) As Long()
    Dim i As Long, Num As Long
    Dim flag() As Boolean
    Dim RndRows() As Long
    
    ReDim flag(MaxDataCount)
    ReDim RndRows(MaxDataCount)

    Dim startDataCount As Long
    If MinDataCount < 1 Then
        startDataCount = 1
    Else
        startDataCount = MinDataCount
    End If
    
    For i = 1 To DataCount
    
        Randomize
        
        Do
            Num = Int((MaxDataCount - startDataCount + 1) * Rnd + startDataCount)
        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
'――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
'************************************************************************************
'   関数名 :RandomWeight
'   機能   :ランダム生成した数値の入った2次配列を返す
'   引数   :size1      2次配列のサイズ(1次元目)
'          :size2      2次配列のサイズ(2次元目)
'************************************************************************************

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
'――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
'************************************************************************************
'   関数名 :Zeros
'   機能   :0の入った1次配列を返す
'   引数   :size1      1次配列のサイズ
'************************************************************************************

Public Function Zeros(ByRef size1 As Long) As Double()

    Dim i As Long
    Dim List() As Double
    
    ReDim List(size1)

    For i = 1 To size1
        List(i) = 0
    Next i
    
    Zeros = List

End Function
'――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
'************************************************************************************
'   関数名 :dot
'   機能   :2つの配列のドット積(内積)の計算結果が入った1次配列を返す
'   引数   :x()      1次元配列
'          :W()      2次元配列
'************************************************************************************

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
'――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
'************************************************************************************
'   関数名 :dot2
'   機能   :2つの配列のドット積(内積)の計算結果が入った2次配列を返す
'   引数   :xT()      1次元配列
'          :dout()    1次元配列
'************************************************************************************

Public Function dot2(ByRef xT() As Double, ByRef dout() As Double) As Double()
    
    Dim i As Long
    Dim j As Long
    Dim dW() As Double
    
    ReDim dW(UBound(xT), UBound(dout))
    

    For i = 1 To UBound(dout)
        For j = 1 To UBound(xT)
    
            dW(j, i) = xT(j) * dout(i)

        Next j
    Next i
    
    dot2 = dW

End Function
'――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
'************************************************************************************
'   関数名 :add
'   機能   :配列内の要素同士の和が入った2次配列を返す
'   引数   :xT()      1次元配列
'          :dout()    1次元配列
'************************************************************************************

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
'――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
'************************************************************************************
'   関数名 :Sigmoid
'   機能   :入力した1次元配列に対してSigmoid関数を適用する
'   引数   :A()      1次元配列
'************************************************************************************

Public Function Sigmoid(ByRef A() As Double) As Double()

    Dim i As Long
    Dim z() As Double
    
    ReDim z(UBound(A))
    
    For i = 1 To UBound(A)
    
        z(i) = 1 / (1 + Exp(A(i) * -1))

    Next i
    
    Sigmoid = z
    
End Function
'――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
'************************************************************************************
'   関数名 :Softmax
'   機能   :入力した1次元配列に対してSoftmax関数を適用する
'   引数   :x()      1次元配列
'************************************************************************************

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
'――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
'************************************************************************************
'   関数名 :cross_entropy_error
'   機能   :交差エントロピー誤差より損失(Loss)を求める
'   引数   :y()    1次元配列(Softmax関数の出力値)
'          :t()    1次元配列(正解ラベル)
'************************************************************************************

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
'――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
'************************************************************************************
'   関数名 :one_hot_t
'   機能   :正解ラベルをone-hot表現にした1次元配列を返す
'   引数   :t_size      正解ラベルの総数(Iris分類の場合は3)
'          :label       正解ラベルのインデックス
'************************************************************************************

Public Function one_hot_t(ByRef t_size As Long, ByRef label As Integer) As Long()
    
    Dim labels() As Long
    ReDim labels(t_size)

    labels(label) = 1
    
    one_hot_t = labels
    
End Function

以下では各関数で何を行っているかを簡単に解説していきます。

 

Function GetRandomRow

関数「GetRandomRow」は入力された範囲内で、入力された個数分の整数をランダムで生成し、1つの配列に入れて返す関数です。

'************************************************************************************
'   関数名 :GetRandomRow
'   機能   :指定範囲内でランダムで生成した行数(自然数)を返す
'   引数   :DataCount      生成する値の数
'          :MaxDataCount  生成する値の上限値
'          :MinDataCount   生成する値の下限値(省略可/省略した場合は「1」となる)
'************************************************************************************

Public Function GetRandomRow(DataCount As Long, MaxDataCount As Long, Optional MinDataCount As Long) As Long()
    Dim i As Long, Num As Long
    Dim flag() As Boolean
    Dim RndRows() As Long
    
    ReDim flag(MaxDataCount)
    ReDim RndRows(MaxDataCount)

    Dim startDataCount As Long
    If MinDataCount < 1 Then
        startDataCount = 1
    Else
        startDataCount = MinDataCount
    End If
    
    For i = 1 To DataCount
    
        Randomize
        
        Do
            Num = Int((MaxDataCount - startDataCount + 1) * Rnd + startDataCount)
        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」と「MaxDataCount」、省略可能な「MinDataCount」があります。
どれも整数型で個数(DataCount)と範囲(MinDataCount〜MaxDataCount)を表しています。

やっていることは単純で、
たとえばDataCount=10、MinDataCount=100、MinDataCount=25とします。
この場合、返ってくるのは25以上100以下の範囲での10個の整数が入った配列です。
このとき整数は毎回ランダムで重複無しになっています。
(MinDataCountを省略した場合は「1〜」の範囲となります)

前回、Irisデータセットの前処理を行う際に使った関数と同じなので、
 前回作成した「preprocess」モジュール内の関数「GetRandomRow」は削除しておきましょう。

 

Function RandomWeight

関数「RandomWeight」は入力された値の数だけランダムで重みパラメータを生成し、2次元配列として返す関数です。

'************************************************************************************
'   関数名 :RandomWeight
'   機能   :ランダム生成した数値の入った2次配列を返す
'   引数   :size1      2次配列のサイズ(1次元目)
'          :size2      2次配列のサイズ(2次元目)
'************************************************************************************

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
'――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
'************************************************************************************
'   関数名 :Zeros
'   機能   :0の入った1次配列を返す
'   引数   :size1      1次配列のサイズ
'************************************************************************************

Public Function Zeros(ByRef size1 As Long) As Double()

    Dim i As Long
    Dim List() As Double
    
    ReDim List(size1)

    For i = 1 To size1
        List(i) = 0
    Next i
    
    Zeros = List

End Function

引数としては「size1」「size2」があります。
これらは返り値である2次元配列の”かたち”を決めるもので、「size1」×「size2」個の重みパラメータが作成されます。重みパラメータの値は-1以上1以下の小数でランダムに生成されます。生成した値は全て2次元配列の「List()」に格納し、この2次元配列を返します。

重みパラメータのランダム生成は、学習の1番初めに行う初期設定のようなものです。これは重みパラメータの初期値がすべて「0」の状態で学習を始めると、学習がうまく進まなくなるための仮の値です。今後ニューラルネットワークの学習を進めることでこの重みパラメータが適正なアタイへと徐々に変化していきます。

 

Function zeros

関数「zeros」は入力された値の数だけ「0」の要素を持つ1次元配列を返す関数です。

'************************************************************************************
'   関数名 :Zeros
'   機能   :0の入った1次配列を返す
'   引数   :size1      1次配列のサイズ
'************************************************************************************

Public Function Zeros(ByRef size1 As Long) As Double()

    Dim i As Long
    Dim List() As Double
    
    ReDim List(size1)

    For i = 1 To size1
        List(i) = 0
    Next i
    
    Zeros = List

End Function

引数としては「size1」があります。
これは返り値である1次元配列の要素数を決めるものです。

機能自体は非常にシンプルで、「size1」個の要素を持つ配列を返すだけです。
このとき、要素の値は全て「0」で返します。

たとえば「size1=5」の場合、返ってくる1次元配列は「zeros(0,0,0,0,0)」となります。

前項の関数「RandomWeight」は重みパラメータの初期化に使いましたが、この関数はバイアスパラメータの初期化の際に使います。重みパラメータは「0」であると学習が進みませんがバイアスパラメータの値は「0」でも問題がないためです。こちらも同様にニューラルネットワークの学習が進むにつれ値が適正なものに変化していきます。

 

Function dot

関数「dot」は入力された1次元配列と2次元配列の積を配列として返す関数です。

'************************************************************************************
'   関数名 :dot
'   機能   :2つの配列のドット積(内積)の計算結果が入った1次配列を返す
'   引数   :x()      1次元配列
'          :W()      2次元配列
'************************************************************************************

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

'************************************************************************************
'   関数名 :dot2
'   機能   :2つの配列のドット積(内積)の計算結果が入った2次配列を返す
'   引数   :xT()      1次元配列
'          :dout()    1次元配列
'************************************************************************************

Public Function dot2(ByRef xT() As Double, ByRef dout() As Double) As Double()
    
    Dim i As Long
    Dim j As Long
    Dim dW() As Double
    
    ReDim dW(UBound(xT), UBound(dout))
    

    For i = 1 To UBound(dout)
        For j = 1 To UBound(xT)
    
            dW(j, i) = xT(j) * dout(i)

        Next j
    Next i
    
    dot2 = dW

End Function

引数としては「xT()」と「dout()」があります。
「xT()」と「dout()」はともに1次元配列である必要があります。
この関数では2つの引数の積を1つの2次元配列として返します。

1次元配列同士の掛け算ですが、行列のかたちとしては「n×1」と「1×m」になります。
(つまり計算後の行列のかたちは「n×m」となります)

たとえば「xT()」の要素数が「3」、「dout()」の要素数が「3」の場合
以下のような計算が行われ、最終的には要素数が「3×3」の2次元配列が出来上がります。

 

Function add

関数「add」は入力された1次元配列と1次元配列の要素ごとの和を1次元配列として返す関数です。

'************************************************************************************
'   関数名 :add
'   機能   :配列内の要素同士の和が入った2次配列を返す
'   引数   :xT()      1次元配列
'          :dout()    1次元配列
'************************************************************************************

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

引数としては「xT()」と「dout()」があります。
「xT()」と「dout()」はともに1次元配列である必要があります。
このとき2つの引数の要素数は同じである必要があります。

この関数では2つの引数の各要素ごとの和を1次元配列として返します。

たとえば「xT(1,2,3)」と「dout(4,5,6)」というような値を持っていた時
返ってくる配列は「add(5,7,9)」となります。

 

Function Sigmoid

関数「Sigmoid」は入力された値にSigmoid関数を適応する関数です。

'************************************************************************************
'   関数名 :Sigmoid
'   機能   :入力した1次元配列に対してSigmoid関数を適用する
'   引数   :A()      1次元配列
'************************************************************************************

Public Function Sigmoid(ByRef A() As Double) As Double()

    Dim i As Long
    Dim z() As Double
    
    ReDim z(UBound(A))
    
    For i = 1 To UBound(A)
    
        z(i) = 1 / (1 + Exp(A(i) * -1))

    Next i
    
    Sigmoid = z
    
End Function

引数としては「A()」があります。
これは重み付き入力値の総和にバイアスを加算した値が入った1次元配列です。

今後実装していく「Affineレイヤ」というレイヤで活性化関数として呼び出して使うことができるようにしておきます。基本的には活性化関数として「ReLU関数」を使う予定ですが、Sigmoid関数にも切り替えられるようにここで関数を作成しておきます。

 

Function softmax

関数「softmax」は入力された値にSoftmax関数を適応する関数です。

'************************************************************************************
'   関数名 :Softmax
'   機能   :入力した1次元配列に対してSoftmax関数を適用する
'   引数   :x()      1次元配列
'************************************************************************************

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

'************************************************************************************
'   関数名 :cross_entropy_error
'   機能   :交差エントロピー誤差より損失(Loss)を求める
'   引数   :y()    1次元配列(Softmax関数の出力値)
'          :t()    1次元配列(正解ラベル)
'************************************************************************************

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

'************************************************************************************
'   関数名 :one_hot_t
'   機能   :正解ラベルをone-hot表現にした1次元配列を返す
'   引数   :t_size      正解ラベルの総数(Iris分類の場合は3)
'          :label       正解ラベルのインデックス
'************************************************************************************

Public Function one_hot_t(ByRef t_size As Long, ByRef label As Integer) As Long()
    
    Dim labels() As Long
    ReDim labels(t_size)

    labels(label) = 1
    
    one_hot_t = labels
    
End Function

引数としては「t_size」と「label」があります。
「t_size」は返り値の1次元配列の要素数、「label」は「1〜3」のいずれかの値です。
(Iris学習の場合、分類する花の種類の3種類あるので「t_size」は「3」で固定です)

「one-hot表現」とは1つの値が「1」でそれ以外の値が「0」で表現されたベクトル(ここでいう配列)のことを指します。

この関数では引数「label」(Irisの正解ラベル)に応じて以下のように変換し、変換後の値を1次元配列として返します。

「label=1 (Setosa)」の場合は「1,0,0」
「label=2 (Versicolor)」の場合は「0,1,0」
「label=3 (Virginica)」の場合は「0,0,1」

上記のように正解の花の種類を表す文字列にラベル(番号)をつけることで、ニューラルネットワーク内で数字として扱うことが可能になります。最終的にニューラルネットワークが導き出した答えが「1」の場合は「Setosa」、「2」の場合は「Versicolor」、「3」の場合は「Virginica」というようにここで指定したラベルで出力されます。

 

まとめ

ここでは今後作成していくレイヤやモジュールで扱うための様々な関数をまとめて実装しました。
今回実装したFunctionsモジュールの機能をまとめると以下の通りです。

Function GetRandomRow:ランダムで生成した行数を返す
Function RandomWeight:ランダムで生成した重みパラメータを返す(重みの初期化)
Function zeros:要素がすべて0の配列を返す(バイアスの初期化)
Function dot:配列同士の積を返す(1次元配列×2次元配列)
Function dot2:配列同士の積を返す(1次元配列×1次元配列)
Function add:配列同士の和を返す(1次元配列+1次元配列)
Function Sigmoid:Sigmoid関数を適応した値を返す
Function softmax:Softmax関数を適応した値を返す
Function cross_entropy_error:交差エントロピー誤差を返す
Function one_hot_t:入力値をone-hot表現に変換した値を返す

おそらく関数だけ見ても何のためにあるのか理解できないと思うので、今後作成していく内容を見ながら適宜本ページに戻ってきて確認して下さい。
逆にいえばはじめのうちはあまり理解していなくても問題ありません。

次回は今回実装したいくつかの関数も使用してAffineレイヤを実装していきます。
以降からはニューラルネットワークの「層(レイヤ)」を実装していく内容になってきます。
つまりはニューラルネットワークを作り始めるということですが、ニューラルネットワークが全て完成しないとわからない部分もあるので初めのうちはあまり難しく考えずに進めてみましょう。

【次回】Affineレイヤの実装
【前回】Irisデータセットの前処理
メインページ
 

 icon-book 参考書籍

2021年2月2日AI,Deep Learning,Excel,VBA