Nimで順伝播型ニューラルネットワーク
プログラミング言語Nimは 静的型付け型推論なのにPython風の文法を持つ言語. ポストPythonを狙うべく, 入門がてら Nimで順伝播型ニューラルネットワークをやらせてみる.
題材はニューラルネットワークのWikipediaにある Pythonの実装例を使うが, Wikipediaの表現に気になるところがあるので 少し改変を加えつつ実装する.
目次
数式を使えるようになったことに調子乗っていたら, 本記事は長くなった. 目次の代わりを貼っておく.
最終的なソースコードはこれ.
理論の概説
Wikipediaの内容を踏襲しつつ, 慣れている形に改変する. 私は訓練データ全体を行列とは見ない派( でまとめたい派)である.
モデルの概要
ここでは, 3層順伝播型ニューラルネットワークで回帰を実装する. において, を学習する. 活性化関数はReLUを使用. 学習は, 確率的勾配降下法で誤差逆伝播法を行う.
予想モデル
予想モデルは以下. ただし, は入力, は出力 ( は予想値). は活性化関数. 隠れ層のノード数を とし, , を 重みパラメータ, , を 定数パラメータとする.
誤差関数
誤差関数は以下. ただし, を訓練データ数とし, は 訓練データである.
誤差関数の微分
誤差関数 をパラメータで偏微分した数式は以下. ただし, は の転置行列, は と のアダマール積.
も もスカラーであるから連鎖律で簡潔に解ける. なお, アダマール積になる理由は ReLU が要素ごとに適用される関数だから. また, という表現は, 英語版Wikipediaにあった. 堂々と使おう.
学習
誤差逆伝播は次のように行う. あるパラメータ に対する伝播は, 学習率を として,
を繰り返し計算すれば良い.
Nimの導入
GitHub - nim-lang/nim
gcc
等のCコンパイラが必須.
cd ~
git clone https://github.com/nim-lang/Nim.git
cd Nim
chmod 777 *
sh ./build_all.sh
export PATH=$PATH:$HOME/Nim/bin
# echo "export PATH=$PATH:$HOME/Nim/bin" >> ~/.bashrc
Hello, world!
hello.nim
で以下を保存.
echo "Hello, world!"
.nim
のコンパイル・実行は,
nim c -r hello.nim
#-> Hello, world!
ls
#-> hello.nim hello
コンパイルのみを行う場合は,
nim c -d:release hello.nim
実装の準備
以上の計算をするために必要なプロシージャを作る.
行列
今回のモデルはベクトル積を使っても計算できるが, 将来的な拡張性を考えて行列として扱う.
Nimで行列演算ができるNeoというライブラリもあるが,
つまらないので今回は使わない.
Nim by Exampleというサイトのarray[]
の項目にMatrix型を作る例があるのでこれを利用する.
Matrix型の定義
type
Matrix[T; M, N: static[int]] = array[1..M, array[1..N, T]]
type
で独自の型を作ることができる.
モダンな言語らしくて楽しい.
行列と定数の演算
# 加法
proc `+`[T, I, J](a: Matrix[T, I, J], b: T): Matrix[T, I, J] =
var c: Matrix[T, I, J]
for i in 1..I:
for j in 1..J:
c[i][j] = a[i][j] + b
result = c
# 減法
proc `-`[T, I, J](a: Matrix[T, I, J], b: T): Matrix[T, I, J] =
result = a + (-b)
# 乗法
proc `*`[T, I, J](a: Matrix[T, I, J], b: T): Matrix[T, I, J] =
var c: Matrix[T, I, J]
for i in 1..I:
for j in 1..J:
c[i][j] = a[i][j] * b
result = c
Nimでは独自の演算子を定義することができる. ただ, (定数, 行列) (行列, 定数)のどちらにも適応する方法がわからず, 必要であれば以下のように定義する.
# 加法
proc `+`[T, I, J](a: T, b: Matrix[T, I, J]): Matrix[T, I, J] =
result = b + a
# 減法
proc `-`[T, I, J](a: T, b: Matrix[T, I, J]): Matrix[T, I, J] =
result = b - a
# 乗法
proc `*`[T, I, J](a: T, b: Matrix[T, I, J]): Matrix[T, I, J] =
result = b * a
行列と行列の演算
# 加法
proc `+`[T, I, J](a, b: Matrix[T, I, J]): Matrix[T, I, J] =
var c: Matrix[T, I, J]
for i in 1..I:
for j in 1..J:
c[i][j] = a[i][j] + b[i][j]
result = c
# 減法
proc `-`[T, I, J](a, b: Matrix[T, I, J]): Matrix[T, I, J] =
var c: Matrix[T, I, J]
for i in 1..I:
for j in 1..J:
c[i][j] = a[i][j] - b[i][j]
result = c
# 行列の乗法
proc `*`[T, I, K, J](a: Matrix[T, I, K], b: Matrix[T, K, J]):
Matrix[T, I, J] =
var c: Matrix[T, I, J]
for i in 1..I:
for j in 1..J:
for k in 1..K:
c[i][j] += a[i][k] * b[k][j]
result = c
# アダマール積
proc `|*|`[T, I, J](a, b: Matrix[T, I, J]): Matrix[T, I, J] =
var c: Matrix[T, I, J]
for i in 1..I:
for j in 1..J:
c[i][j] = a[i][j] * b[i][j]
result = c
転置
proc t[T, I, J](a: Matrix[T, I, J]): Matrix[T, J, I] =
var c: Matrix[T, J, I]
for i in 1..I:
for j in 1..J:
c[j][i] = a[i][j]
result = c
行と列をいれかえるだけ.
使い方はA.t
.
その他の行列式などは今回使わないので定義しない.
乱数の行列
パラメータの初期化に必要.
与えられた行列を乱数(min
max
)にして返す.
import random
randomize()
proc toRandom[T, I, J](a: var Matrix[T, I, J], min: T = 0, max: T = 1):
Matrix[T, I, J] {.discardable.} =
for i in 1..I:
for j in 1..J:
a[i][j] = rand(max - min) + min
# 例
var mat: Matrix[float, 2, 2]
mat.toRandom(-1, 1)
echo mat
#-> [[0.5010605918633901, -0.08190786365339608], [-0.6507648825937791, 0.855403391145154]]
やはりPythonに似てる.
実装
以上のプロシージャを使って, 順伝播型ニューラルネットワークを組む. 基本的にはWikipediaの実装例に対応させて書いた.
import math, random, sugar
# Matrixのproc等は省略
const
dim_in = 1 # 入力は1次元
dim_out = 1 # 出力は1次元
hidden_count = 1024 # 隠れ層のノードは1024個
learn_rate = 0.005 # 学習率
# 訓練データは x は -1 ~ 1, y は 2 * x ^ 2 - 1
const train_count = 64 # 訓練データ数
let
train_x = collect(newSeq):
for i in 0..<train_count: 2 * i / (train_count - 1) - 1
train_y = collect(newSeq):
for x in train_x: 2 * x ^ 2 - 1
# 重みパラメータ. この行列の値を学習する.
var
w1: Matrix[float, hidden_count, dim_in]
w2: Matrix[float, dim_out, hidden_count]
b1: Matrix[float, hidden_count, 1]
b2: Matrix[float, dim_out, 1]
# -0.5 ~ 0.5 でランダムに初期化.
w1.toRandom(-0.5, 0.5)
w2.toRandom(-0.5, 0.5)
b1.toRandom(-0.5, 0.5)
b2.toRandom(-0.5, 0.5)
# 活性化関数は ReLU
proc activation[T](x: T): T =
x.max(0)
proc activation[T, I, J](x: Matrix[T, I, J]): Matrix[T, I, J] =
var c: Matrix[T, I, J]
for i in 1..I:
for j in 1..J:
c[i][j] = x[i][j].activation
result = c
# 活性化関数の微分
proc activation_dash[T](x: T): T =
(x.abs / x + 1) / 2
proc activation_dash[T, I, J](x: Matrix[T, I, J]): Matrix[T, I, J] =
var c: Matrix[T, I, J]
for i in 1..I:
for j in 1..J:
c[i][j] = x[i][j].activation_dash
result = c
# 順方向. 学習結果の利用.
proc forward(x: float): float =
(b2 + w2 * activation(w1 * x + b1))[1][1]
# 逆方向. 学習.
proc backward(x: float, diff: float) {.discardable.} =
let
v1 = (w2.t * diff) |*| activation_dash(w1 * x + b1)
v2 = activation(w1 * x + b1)
w1 = w1 - v1 * x * learn_rate
b1 = b1 - v1 * learn_rate
w2 = w2 - v2.t * diff * learn_rate
b2 = b2 - diff * learn_rate
# メイン処理
var
idxes = collect(newSeq):
for i in 0..<train_count: i # idxes は 0 ~ 63
error, y, diff: float
for epoc in 1..1000: # 1000 エポック
idxes.shuffle # 確率的勾配降下法のため, エポックごとにランダムにシャッフルする
error = 0 # 二乗和誤差
for idx in idxes:
y = forward(train_x[idx]) # 順方向で x から y を計算する
diff = y - train_y[idx] # 訓練データとの誤差
error += diff ^ 2 # 二乗和誤差に蓄積
backward(train_x[idx], diff) # 誤差を学習
echo error # エポックごとに二乗和誤差を出力
ソースコード全体はGistに貼ったのでそちらも参照されたい.
速度比較
WikipediaにあるPythonのコードとの比較.
Python
time -p python example.py
# real 9.00
# user 8.95
# sys 0.26
時間: 9秒
Nim
time -p nim c -d:release --opt:speed --hints:off example.nim
# real 2.53
# user 3.02
# sys 0.23
time -p ./example
# real 2.59
# user 2.55
# sys 0.01
時間: 5秒 実行ファイルだけなら 2.5秒 !!
感想
疲れた.
初めて書いたNimにしては,
ちょっと複雑すぎた気もする.
行列演算にNeoを使えばもっと手軽に実装できるだろうが,
自分で書くことで
type
とGenericsのいい練習になった.
あとこの題材は入門にもってこいである. 最初に機械学習を書く, これはモダンな言語にふさわしい入門かもしれない. しばらくは行列演算はライブラリを使いたいが.
参考
Nim関連
偉大なる先人達のおかげで日本語でもNimを学ぶ環境がある程度整っている.
公式のドキュメント系
行列演算プロシージャの例
機械学習
- 須山敦志(2019).『ベイズ深層学習』. 講談社. (2章)
表記方法等もこの本にある程度倣った. ただし, 予測値を とする表現は
- 持橋大地, 大羽成征(2019). 『ガウス過程と機械学習』. 講談社. (1章)
を真似している.