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

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

## モチベーション
* 今関わっている案件でGo言語を使っている（なのでGoで何か作ろうと思った）
* [ゼロから作るDeepLearning](https://www.oreilly.co.jp/books/9784873117584/)に触発されて、一からニューラルネットワークを組んでみたかった

## 前提
* 行列計算はOSSを使う(今回は[gonum](https://github.com/gonum/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

```Go
// 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

```Go
// 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

```Go
// 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の式は修正したい・・。

```Go
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を持てば良い。
※以下のような感じ。

```Go
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の正規分布で初期化している）

```Go
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情報は[ここ](https://godoc.org/gonum.org/v1/gonum/mat))