Go
ニューラルネットワーク

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

More than 1 year has passed since last update.

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情報はここ)