F#で逆誤差伝播法

今回はニューラルネットワークで利用される、逆誤差伝播法をF#で実装してみたいと思います。

実装をするに際し、Math.NETライブラリを利用しています。

レイヤーの定義

今回は、全結合のAffine層、ReLU活性化層、Softmax最終活性化層を定義しました。

その他の層が欲しい場合は、ここに定義を追加していく形となります。

またAffine層の初期化として、He初期値を利用する関数も定義しました。

type Layer =
    | Affine of weight : Matrix<double> * bias : Vector<double>
    | ReLU

type LastLayer =
    | SoftmaxCrossEntropy        

type Network =
    { Layers    : Layer[]
      LastLayer : LastLayer
    }

let createAffineHe (inputCount : int) (outputCount : int) : Layer =
    let weight = Matrix<double>.Build.Random(inputCount, outputCount) * (sqrt (2.0 / double inputCount))
    let bias   = Vector<double>.Build.Dense(outputCount)
    Affine(weight, bias)


伝播&逆伝播関数

純粋な伝播を定義するforward関数と、伝播しながら逆伝播関数を生成するforwardAndCreateBackward関数を定義します。

let forward (input : Vector<double>) (layer : Layer) : Vector<double> =
    match layer with
    | Affine(weight, bias) -> 
        input * weight + bias            
    | ReLU ->
        input |> Vector.map (max 0.0)

let forwardAndCreateBackward 
    (rate : double) (input : Vector<double>) (layer : Layer) 
    : (Vector<double> -> Layer * Vector<double>) * Vector<double> =
    let output = forward input layer
    let backward =
        match layer with
        | Affine(weight, bias) ->
            (fun (dy : Vector<double>) ->
                let dx = dy * weight.Transpose()
                let dw = input.ToColumnMatrix() * dy.ToRowMatrix()
                Affine(weight - rate * dw, bias - rate * dy), dx
            )
        | ReLU ->
            (fun dy ->
                let dx = Vector.map2 (fun y dy -> if y = 0.0 then 0.0 else dy) output dy
                layer, dx
            )
    backward, output


学習関数

順伝播しながら逆伝播関数を生成し、最終層から逆伝播させ、更新された新しいNetworkを生成しています。

let softmax (x : Vector<double>) : Vector<double> =
    let c = Vector.max x
    let e = x |> Vector.map (fun x -> exp (x - c))
    e / (Vector.sum e)

let learn (rate : double) (network : Network) (input : Vector<double>) (teacher : Vector<double>) : Network =
    let backwards, y = network.Layers |> Array.mapFold (forwardAndCreateBackward rate) input
    let dy = 
        match network.LastLayer with
        | SoftmaxCrossEntropy -> (softmax y) - teacher
    let layers, _ = backwards |> Array.rev |> Array.mapFold (|>) dy
    { network with Layers = layers |> Array.rev }


評価関数

入力と教師データから正答率を算出しています。(教師データは、どれか一つの値が活性化するものと想定しています。)

let predict (network : Network) (input : Vector<double>) : Vector<double> =
    let y = network.Layers |> Array.fold forward input
    match network.LastLayer with
    | SoftmaxCrossEntropy -> softmax y        

let accuracy (network : Network) (inputs : Vector<double>[]) (teachers : Vector<double>[]) : double =
    let outputs = inputs |> Array.map (predict network)
    Seq.map2
        (=)
        (outputs  |> Seq.map Vector.maxIndex)
        (teachers |> Seq.map Vector.maxIndex)
    |> Seq.averageBy (fun x -> if x then 1.0 else 0.0)


MNISTを学習してみる

MNISTの読み込みに関しては、下記記事を参照してみてください。
MNISTの読み込み(F#) - 何でもプログラミング

10000データ学習ごとの正答率は下記のように推移しました。
[ 0.0931, 0.913, 0.9321, 0.9297, 0.9441, 0.9471, 0.9488 ]

let trainImages = Mnist.loadImageVectors "train-images.idx3-ubyte"
let trainLabels = Mnist.loadLabelVectors "train-labels.idx1-ubyte"
let testImages  = Mnist.loadImageVectors "t10k-images.idx3-ubyte"
let testLabels  = Mnist.loadLabelVectors "t10k-labels.idx1-ubyte"

let initialNetwork =
    { Layers =
        [| createAffineHe 784 50
            ReLU
            createAffineHe 50 10
        |]
        LastLayer = SoftmaxCrossEntropy
    }

Seq.zip trainImages trainLabels
|> Seq.scan (fun network (image, label) -> learn 0.01 network image label) initialNetwork
|> Seq.indexed
|> Seq.iter 
    (fun (i, network) -> 
        if i % 10000 = 0 then 
            printf "accuracy %f\n" (accuracy network testImages testLabels)
    )