LoginSignup
4

More than 3 years have passed since last update.

posted at

updated at

Goでゼロからニューラルネットワークを組んでみた

Go言語でニューラルネットワーク(フリーコネクト)を組んでみたので、記事を書いてみます。

モチベーション

  • 今関わっている案件でGo言語を使っている(なのでGoで何か作ろうと思った)
  • ゼロから作るDeepLearningに触発されて、一からニューラルネットワークを組んでみたかった

前提

  • 行列計算はOSSを使う(今回はgonumを使ってます)
  • NNの各レイヤーはゼロから実装

目標

  • フリーコネクト(多層パーセプトロン)なレイヤーを何層か重ねて、MNISTの画像データを0-9に分類可能であること(分類時の精度を可視化出来ると望ましい)

リポジトリ

Githubにあげてます。
リポジトリは以下。
https://github.com/kurama554101/goMLLibrary
※まだReadmeかけてないです・・涙

実装したレイヤー

活性化関数

  • Relu
  • Sigmoid
  • Tanh
  • SoftmaxWithLoss

重み付きの素子

  • Affine

最適化関数

  • SGD(確率的勾配降下)

MNISTのデータを読み込んで分類するサンプル

ソースコード

下記を参照。
https://github.com/kurama554101/goMLLibrary/blob/master/main/MnistSample.go

構造

3層構造

  • 1層目 : Affine+Relu
  • 2層目 : Affine+Relu
  • 3層目 : Affine+SoftmaxWithLoss

精度結果

MNISTテストデータ(1万件)に対しての分類精度は下記。

  • loss : 0.07程度
  • accuracy : 0.95 - 0.98程度

各レイヤーの詳細

ニューラルネットワークについて

活性化関数であっても、Affine変換なレイヤーであっても、
順伝搬+逆伝搬をするため以下のようなAPIが必要

  • Forward()
  • Backward()

活性化関数

まず、活性化関数を作った。
活性化関数は入力されたデータをどの程度次のレイヤーに伝搬させるか、を決める関数。
※内部にパラメーターを持たないため、ForwardとBackwardのみがあれば良い。

  • Relu
// Relu : Relu関数
type Relu struct {
    out mat.Matrix
}

func (relu *Relu) Forward(x mat.Matrix) mat.Matrix {
    r, c := x.Dims()
    dense := mat.NewDense(r, c, nil) // zero matrix
    dense.Apply(func(i, j int, v float64) float64 {
        if v > 0 {
            return x.At(i, j)
        } else {
            return 0
        }
    }, x)
    relu.out = dense
    return dense
}

func (relu *Relu) Backward(dout mat.Matrix) mat.Matrix {
    r, c := dout.Dims()
    dense := mat.NewDense(r, c, nil)
    dense.Apply(func(i, j int, v float64) float64 {
        if relu.out.At(i, j) > 0 {
            return v
        } else {
            return 0
        }
    }, dout)
    return dense
}
  • Sigmoid
// Sigmoid : シグモイド関数
type Sigmoid struct {
    out mat.Matrix
}

func (sigmoid *Sigmoid) Forward(x mat.Matrix) mat.Matrix {
    r, c := x.Dims()
    dense := mat.NewDense(r, c, nil) // zero dense
    dense.Apply(func(i, j int, v float64) float64 {
        return 1.0 / (1.0 + math.Exp(-v))
    }, x)
    sigmoid.out = dense
    return dense
}

func (sigmoid *Sigmoid) Backward(dout mat.Matrix) mat.Matrix {
    r, c := dout.Dims()
    dense := mat.NewDense(r, c, nil) // zero dense
    dense.Apply(func(i, j int, v float64) float64 {
        return v * (1.0 - sigmoid.out.At(i, j)) * sigmoid.out.At(i, j)
    }, dout)
    return dense
}
  • Tanh
// Tanh : Tanh関数
type Tanh struct {
    out mat.Matrix
}

func (tanh *Tanh) Forward(x mat.Matrix) mat.Matrix {
    r, c := x.Dims()
    dense := mat.NewDense(r, c, nil)
    dense.Apply(func(i, j int, v float64) float64 {
        return math.Tanh(v)
    }, x)
    tanh.out = dense
    return dense
}

func (tanh *Tanh) Backward(dout mat.Matrix) mat.Matrix {
    r, c := dout.Dims()
    dense := mat.NewDense(r, c, nil)
    dense.Apply(func(i, j int, v float64) float64 {
        return v * (1 - math.Pow(tanh.out.At(i, j), 2))
    }, dout)
    return dense
}
  • SoftmaxWithLoss

※softmaxやcrossEntropyの式は修正したい・・。

type SoftmaxWithLoss struct {
    out  mat.Matrix
    t    mat.Matrix
    loss float64
}

func (s *SoftmaxWithLoss) Forward(x mat.Matrix, t mat.Matrix) (loss float64, accuracy float64) {
    s.t = t
    s.out = s.softmax(x)
    s.loss = s.crossEntropyError(s.out, t)
    accuracy = calcAccuracy(s.out, t)
    return s.loss, accuracy
}

func (s *SoftmaxWithLoss) Backward() mat.Matrix {
    r, c := s.t.Dims()
    dense := mat.NewDense(r, c, nil)
    bs := r

    dense.Apply(func(i, j int, v float64) float64 {
        return (v - s.t.At(i, j)) / float64(bs)
    }, s.out)
    return dense
}

func (s *SoftmaxWithLoss) softmax(x mat.Matrix) mat.Matrix {
    r, c := x.Dims()
    tmp := mat.DenseCopyOf(x)
    dense := mat.NewDense(r, c, nil)
    // 行の計算
    for i := 0; i < r; i++ {
        // 1つのデータのベクトルを取得し、合計値と要素の最大値を計算
        // TODO : 処理に時間がかかるため、リファクタリングする
        v := tmp.RowView(i)
        max := mat.Max(v.T())
        sum := 0.0
        for k := 0; k < c; k++ {
            sum += math.Exp(v.At(k, 0) - max)
        }

        // 各列の値を算出
        for j := 0; j < c; j++ {
            val := math.Exp(v.At(j, 0)-max) / sum
            dense.Set(i, j, val)
        }
    }
    return dense
}

// crossEntropyError : 実値と正解データから交差エントロピー誤差を算出
// x : 入力値, t : 正解データ
func (s *SoftmaxWithLoss) crossEntropyError(x mat.Matrix, t mat.Matrix) float64 {
    xr, xc := x.Dims()
    tr, tc := t.Dims()

    // 実際のデータと正解データの行列の形が同じかを確認
    if xr != tr || xc != tc {
        // TODO エラー対応
        return 0.0
    }

    // バッチサイズを取得(行数)
    bs := xr

    // 各値の交差エントロピーを求め、バッチサイズを考慮して平均を出力
    dense := mat.NewDense(xr, xc, nil)
    for i := 0; i < xr; i++ {
        for j := 0; j < xc; j++ {
            val := -1 * t.At(i, j) * math.Log(x.At(i, j)+delta)
            dense.Set(i, j, val)
        }
    }
    return mat.Sum(dense) / float64(bs)
}

Optimizer

次にOptimizerを作った。
Optimizerは勾配情報からパラメーターをアップデートするようなAPIを持てば良い。
※以下のような感じ。

func (sgd *SGD) Update(params map[string]mat.Matrix, grads map[string]mat.Matrix) {
    for key, _ := range params {
        //r, c := params[key].Dims()
        dense := mat.DenseCopyOf(params[key])

        // 学習率分だけ勾配を拡縮
        dense.Apply(func(i, j int, v float64) float64 {
            return v * sgd.lr
        }, grads[key])

        // 重みから勾配分(学習率を考慮)だけ差をとる
        dense.Sub(params[key], dense)

        // paramに戻す
        params[key] = dense
    }
}

Affine変換

活性化関数と違ってパラメーター(重みとバイアス)を持つため、勾配情報から重みをアップデートするためのAPIが必要。

  • パラメーターと勾配情報があれば、Optimizerでアップデートが可能。
  • なぜかgonumの転置のメソッドが上手く動いていない気がして、自分で作った。
  • 最初作り終わった時にMNISTのデータで精度が全く出ず困ったが、理由は重みやバイアスの初期値を0リセットしていたためだった・・・涙(現状は標準偏差0.01、平均0の正規分布で初期化している)
type Affine struct {
    w  mat.Matrix
    b  mat.Vector
    x  mat.Matrix
    dw mat.Matrix
    db mat.Vector
}

func (aff *Affine) Forward(x mat.Matrix) mat.Matrix {
    aff.x = x
    batchSize, _ := aff.x.Dims()
    _, outputSize := aff.w.Dims()
    d := mat.NewDense(batchSize, outputSize, nil)
    d.Mul(aff.x, aff.w)
    d.Apply(func(i, j int, v float64) float64 {
        return aff.b.AtVec(j) + v
    }, d)
    return d
}

func (aff *Affine) Backward(dout mat.Matrix) mat.Matrix {
    // dxの計算
    // r, _ := dout.Dims()
    r, c := aff.x.Dims()
    dx := mat.NewDense(r, c, nil)
    dx.Mul(dout, util.Transpose(aff.w))

    // dwの計算
    r, c = aff.w.Dims()
    dw := mat.NewDense(r, c, nil)
    dw.Mul(util.Transpose(aff.x), dout)
    aff.dw = dw

    // dbの計算
    r, c = dout.Dims()
    db := mat.NewVecDense(aff.b.Len(), nil)
    for i := 0; i < r; i++ {
        for j := 0; j < c; j++ {
            tmpVal := db.AtVec(j)
            db.SetVec(j, tmpVal+dout.At(i, j))
        }
    }
    aff.db = db
    return dx
}

func (aff *Affine) UpdateParams(params map[string]mat.Matrix) {
    // パラメータのアップデート
    aff.w = params["w"]
    aff.b = mat.DenseCopyOf(params["b"]).ColView(0)

    // 勾配のリセット
    aff.dw = nil
    aff.db = nil
}

所感

  • 自分が勉強不足な感もあって、思ったより作るのに時間がかかった。
  • やはりpythonと比べると、コード量が絶対的に多いと思う。(numpyが優秀とか、pythonが書きやすいとかはあるかもしれない)
  • gonumを使いきれてない・・。API読み直す。(API情報はここ)

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
4