無限不可能性ドライブ

『ニューラルネットワーク自作入門』に刺激されてExcelVBAでニューラルネットワークを作ってみたものの、やっぱり数学やらなきゃと思い少しずつやってきたのもあって、自分の知識の整理とかそういった感じです。

【VBA編】(順伝播)出力層の作成

f:id:celaeno42:20181201115558p:plain

出力層

前回は隠れ層のクラスを作成しました。今回は出力層用のクラスを作成します。なお、今回のコードは隠れ層とほとんど同じです。
celaeno42.hatenablog.com

出力層の初期化

隠れ層同様、初期化処理を作ります。コードは「classOutputLayer」に記述します。なお、内容は「classHiddenLayer」と同じです。

クラスモジュール【classOutputLayer】

'[classOutputLayer - 出力層クラス]
Option Explicit
Option Base 1

Dim mUnitList() As classUnit        'ユニット格納用リスト
Dim mUnitCount As Long              '自レイヤーのユニット数
Dim mAct As Long                    '活性関数の種類

'自レイヤーのユニットを作成する
'[引数] <- aUnitCount : Long / ユニットの数, aInputCount : Long / 入力データの数, aActivationFunction : Long / 活性化関数の種類
Public Sub Initialize(ByRef aUnitCount As Long, ByRef aInputCount As Long, ByRef aActivationFunction As Long)
    Dim i As Long
    
    ReDim mUnitList(aUnitCount)
    
    '必要な数だけユニットを作成しユニット格納用リストに格納する
    For i = 1 To aUnitCount
        Set mUnitList(i) = New classUnit
        'Newしたユニットを初期化する
        Call mUnitList(i).Initialize(aInputCount)
    Next
    
    mUnitCount = aUnitCount
    mAct = aActivationFunction
    
End Sub


順伝播

順伝播の処理も隠れ層と同様です。

クラスモジュール【classOutputLayer】

'順伝播:各ユニットのuとzを求める
'[引数] <- aInputDataList() : Double / 入力データのリスト
Public Sub Forward(ByRef aInputDataList() As Double)
    Dim i As Long
    
    'ユニットに入力値を渡して u を計算する
    For i = 1 To mUnitCount
        Call mUnitList(i).CalcU(aInputDataList)
    Next
    
    '活性化関数を適用して z を計算する
    Call activateU
    
End Sub


活性化関数

今回の出力層では活性化関数として Softmax を適用します。
例えば出力層1つめのユニットの出力値を求める Softmax の計算式は以下の通りです。


\displaystyle z_1^4 = Softmax(u_1^4) = \frac{\exp(u_1^4)}{\sum_{k=1}^3 \exp(u_k^4)} = \frac{\exp(u_1^4)}{\exp(u_1^4) + \exp(u_2^4) + \exp(u_3^4)}


これをもとに【ML】モジュールに実装します。
なお、忘れずに [ Option Base 1 ] を宣言しておいてください。

標準モジュール【ML】

Option Explicit
Option Base 1

'活性化関数 Softmax
'[引数] <- aUList() : Double / 出力層のすべての出力値のリスト, aIndex : Long / 対象のユニットのインデックス
'[戻り値] -> actSoftmax : Double / Softmax適用後の値
Public Function actSoftmax(ByRef aUList() As Double, ByRef aIndex As Long) As Double
    Dim i As Long
    Dim max As Double
    Dim sum As Double

    'オーバーフロー対策ですべての出力値から最大値を引く必要があるため最大値を求めておく
    max = WorksheetFunction.max(aUList)
    
    '分母部分の計算
    sum = 0
    For i = 1 To UBound(aUList)
        sum = sum + Exp(aUList(i) - max)
    Next

    '該当ユニットの出力値を全出力値で割る
    actSoftmax = Exp(aUList(aIndex) - max) / sum
    
End Function


計算式では、すべてのユニットの出力(を \exp() したもの)の合計を求めて、該当のユニットの出力(の \exp())をその合計で割っていますが、実装の際にはオーバーフローに気をつける必要があります。
\exp(u_1^4) e^{u_1^4} で e は約 2.71828… のため、u が大きいとすぐにオーバーフローしてしまいます。そこで、実装時には出力層のユニットの u の最大値をそれぞれの出力値から引いて値を小さくすることで、オーバーフローを防ぐようにします。
ちなみに、活性化関数の適用をレイヤークラスで行うようにしているのは、Softmax の計算でその層の全ユニットの出力値が必要になるためです。自ユニットは他のユニットの出力値を知る必要がないため、あえてユニットクラスでは実装しないようにしました。ReLUなどの自ユニットの出力値だけが引数になっているものであれば、ユニットクラスに実装してもよいかと思います。


では、【classOutputLayer】に戻って、いま実装した「actSoftmax()」を呼び出す処理を書きましょう。

クラスモジュール【classOutputLayer】

'ユニットの u に活性化関数を適用する
Private Sub activateU()

    If mAct = ACT.Softmax Then
        Call activationSoftmax
    End If

End Sub

'ユニットの u にSoftmaxを適用する
Private Sub activationSoftmax()
    Dim i As Long
    Dim uList() As Double
    
    'Softmaxの計算には全出力値が必要となるため、配列に格納
    ReDim uList(mUnitCount)
    
    For i = 1 To mUnitCount
        uList(i) = mUnitList(i).u
    Next
    
    For i = 1 To mUnitCount
        mUnitList(i).z = ML.actSoftmax(uList, i)
    Next
    
End Sub

「actSoftmax()」 の引数として渡すため、出力層のすべてのユニットの出力値をいったん配列に格納しています。

ニューラルネットワークの推測

出力層の各ユニットの z(最終出力値)が求まったので、出力値が最も大きいユニット(のインデックス)をニューラルネットワークが推測した答えとします。
今回のニューラルネットワークでは、最も出力値の大きいユニットが1番目のユニットであれば「Irsi-Setosa」、2番目のユニットであれば「Iris-Versicolor」、3番目のユニットであれば「Iris-Virginica」と推測したとします。最終的な判断は「mdlSupervisor」モジュールで行うため、ここではインデックスを返す処理のみを実装しています。

'最も大きい出力値のユニットのインデックスを返す
Public Function GetAnswerIndex() As Long
    Dim i As Long
    Dim ans As Long
    
    ans = 1
    For i = 2 To mUnitCount
        If mUnitList(i).z > mUnitList(ans).z Then
            ans = i
        End If
    Next
    
    GetAnswerIndex = ans
    
End Function


出力値リスト

各ユニットの z をリスト化(配列化)しておきます。隠れ層と違い次の層に渡すことはないのですが、損失関数の計算を行う際に必要となるので、隠れ層同様の処理をしておきます。
コードは隠れ層のものと同じです。

'次の層に渡すための出力リストを準備する
'[戻り値] -> OutputDataList() : Double / 自層の各ユニットの出力値のリスト
Public Function OutputDataList() As Double()
    Dim i As Long
    Dim outputDatas() As Double
    
    ReDim outputDatas(mUnitCount)
    
    For i = 1 To mUnitCount
        outputDatas(i) = mUnitList(i).z
    Next
    
    OutputDataList = outputDatas()
    
End Function


さて、これでニューラルネットワークの入力から推測までの実装ができました。ただ、パラメータを更新する機能は未実装なので、まだ学習はできません。この後は、いったん動作確認した後で、バックプロパゲーションによる学習機能を実装していく予定です。


f:id:celaeno42:20181212233850p:plain