はじめに
本記事はElixirで機械学習/ディープラーニングができるようになるnumpy likeなライブラリ Nxを使って
「ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装」
をElixirで書いていこうという記事になります。
今回は5章の誤差逆伝播法をやっていきます、各項目の詳細は書籍を参照して頂いて、本記事ではelixirのコードを補足するにとどめます。
準備編
exla setup
1章 pythonの基本 -> とばします
2章 パーセプトロン -> とばします
3章 ニューラルネットワーク
with exla
4章 ニューラルネットワークの学習
5章 誤差逆伝播法
Nx.Defn.Kernel.grad
6章 学習に関するテクニック -> とばします
7章 畳み込みニューラルネットワーク
結論
結論から入りますが、
今回実装した誤差逆伝播法はLossがいつまで立っても減らないし、float 64にしたら200回まわして loss 7 -> 16と増えて欠陥品なので
おとなしく Nx gradを使いましょう backwardを実装する必要もないし、今回の誤差逆伝播法と速度はあまり変わりません
誤差逆伝播法の仕組みを学んだり、Elixirでの1つの実装のアイデアとしてみていただければ幸いです
5.4.1 乗算レイヤの実装
defmodule Ch5.MulLayer do
import Nx.Defn
defn forward(x,y) do
{ Nx.multiply(x, y), {x, y} }
end
defn backward({x, y},dout) do
dx = Nx.multiply(dout, y)
dy = Nx.multiply(dout, x)
{dx, dy}
end
end
5.4.2 加算レイヤの実装
defmodule Ch5.AddLayer do
import Nx.Defn
defn forward(x,y) do
{Nx.add(x, y), {x, y}}
end
defn backward({_x, _y}, dout) do
{dout * 1, dout * 1}
end
end
defmodule BuyAppleOrange do
alias Ch5.{ MulLayer, AddLayer }
def main do
apple = 100
apple_num = 2
orange = 150
orange_num = 3
tax = 1.1
{apple_price, mul_apple_layer} = MulLayer.forward(apple, apple_num)
{orange_price, mul_orange_layer}= MulLayer.forward(orange, orange_num)
{all_price, add_apple_orange_layer} = AddLayer.forward(apple_price, orange_price)
{price, mul_tax_layer} = MulLayer.forward(all_price, tax)
dprice = 1
{dall_price, dtax} = MulLayer.backward(mul_tax_layer, dprice)
{dapple_price, dorange_price} = AddLayer.backward(add_apple_orange_layer, dall_price)
{dorange, dorange_num} = MulLayer.backward(mul_orange_layer, dorange_price)
{dapple, dapple_num} = MulLayer.backward(mul_apple_layer, dapple_price)
IO.inspect(price)
IO.inspect({dapple_num, dapple, dorange, dorange_num, dtax})
end
end
5.5.1 ReLUレイヤ
ReLUはあとの方で使うので少し手を加えています
誤差逆伝播法で使う foward, backward
nx gradで使う forward_g
maskは Nx.greaterを使用して右辺が左辺より大きければ1(tue),小さければ0(false)にテンソルを変換しています
defmodule Ch5.ReluLayer do
import Nx.Defn
@default_defn_compiler {EXLA, max_float_type: {:f, 32}}
defn forward({x, _var}) do
mask = Nx.greater(x, 0)
{ Nx.multiply(x, mask), mask }
end
defn forward_g(x) do
mask = Nx.greater(x, 0)
Nx.multiply(x, mask)
end
defn backward(x, mask) do
Nx.multiply(x, mask)
end
end
5.5.2 Sigmoid レイヤ
defmodule Ch5.SigmoidLayer do
import Nx.Defn
defn forward(x) do
out = 1 / (1 + Nx.exp(-x))
{out, out}
end
defn backward(x, out) do
x * (1.0 - out) * out
end
end
5.6.1 Affineレイヤ
forwardでは backward時にbは使わないのでw,xのみ保持するようにしています
defmodule Ch5.AffineLayer do
import Nx.Defn
@default_defn_compiler {EXLA, max_float_type: {:f, 32}}
defn forward({x, _var}, w, b) do
out = Nx.dot(x, w) |> Nx.add(b)
{out, {w, x}}
end
defn forward_g(x, w, b) do
Nx.dot(x, w) |> Nx.add(b)
end
defn backward(dout, {w , x}) do
dx = Nx.dot(dout, Nx.transpose(w))
dw = Nx.dot(Nx.transpose(x), dout)
db = Nx.sum(dout, axes: [0])
{dx, {dw, db}}
end
end
5.6.3 Softmax-with-Loss レイヤ
defmodule Ch5.SoftmaxWithLossLayer do
import Nx.Defn
@default_defn_compiler {EXLA, max_float_type: {:f, 32}}
defn forward({x, _var}, t, batch_size \\ 1) do
y = Ch3.Activation.softmax(x)
loss = Ch4.Loss.cross_entropy_error(y, t, batch_size)
{loss, {y, t}}
end
defn forward_g(x, t, batch_size) do
Ch3.Activation.softmax(x)
|> Ch4.Loss.cross_entropy_error(t, batch_size)
end
defn backward(_, {y, t}) do
{batch_size, _} = Nx.shape(t)
Nx.subtract(y, t)
|> Nx.divide(batch_size)
end
end
5.7 誤差逆伝播法の実装
各レイヤの実装が終わったので、誤差逆伝播法を実装していきます
Elixirの変数はimmutableなので、pythonのクラス的な振る舞いができない(できるが推奨されない)のと、
Nxのdefn内ではtuple, tensor, number, listしか使用できないので
backwardに必要な計算結果をインメモリDB(ets)に保存しています。
そのため 各レイヤのforward出力は {out,{var1,var2}}のようにし、Cache.writeに渡しています
Cache.writeではvarをetsに書き込んだあとに出力を {out, 0}にしてvarを破棄して次のレイヤに渡します
backward時にはdoutを第1引数、forward時に書き込んだ値を第2引数として受け取って計算していきます。
その際に更新する重みのパラメーターを保持するためにaffineレイヤはdw,dbの値でCache.d_writeを挟んで上書きしています
d_writeは値を書き込んでoutだけを次のレイヤに渡しています
また nx gradですが関数とパラメータの渡し方が &loss(&1,...)と変わっているので注意が必要です
defmodule Ch5.TwoLayerNet do
import Nx.Defn
@default_defn_compiler {EXLA, max_float_type: {:f, 32}}
defn init_params(input_size \\ 784, hidden_size \\ 100, output_size \\ 10) do
w1 = Nx.random_normal({input_size, hidden_size}, 0.0, 0.1)
b1 = Nx.random_uniform({ hidden_size }, 0, 0, type: {:f, 64})
w2 = Nx.random_normal({ hidden_size, output_size }, 0.0, 0.1)
b2 = Nx.random_uniform({ output_size }, 0, 0, type: {:f, 64})
{w1, b1, w2, b2}
end
def forward(x, {w1,b1,w2,b2}, table) do
{x, 0}
|> Ch5.AffineLayer.forward(w1, b1)
|> Cache.write(:affine1, table)
|> Ch5.ReluLayer.forward()
|> Cache.write(:relu, table)
|> Ch5.AffineLayer.forward(w2, b2)
|> Cache.write(:affine2, table)
end
def backward(dout, table) do
dout
|> Ch5.SoftmaxWithLossLayer.backward(Cache.read(:last, table))
|> Ch5.AffineLayer.backward(Cache.read(:affine2, table))
|> Cache.d_write(:affine2, table)
|> Ch5.ReluLayer.backward(Cache.read(:relu, table))
|> Ch5.AffineLayer.backward(Cache.read(:affine1, table))
|> Cache.d_write(:affine1, table)
end
def predict(x,{_w1,_b1,_w2,_b2} = params,table) do
forward(x, params, table)
end
def loss(x, t, {_w1,_b1,_w2,_b2} = params, batch_size, table) do
predict(x,params,table)
|> Ch5.SoftmaxWithLossLayer.forward(t, batch_size)
|> Cache.write(:last, table)
end
def accuracy({ w1, b1, w2, b2 }, x, t, table) do
{out, _} = predict(x,{ w1, b1, w2, b2 }, table)
out
|> Nx.argmax(axis: 1)
|> Nx.equal(Nx.argmax(t, axis: 1))
|> Nx.mean
end
def gradient({_w1,_b1,_w2,_b2} = params, x, t, table) do
# forward
loss(x, t, params, 100, table)
# backward
dout = 1
backward(dout, table)
{w1, b1} = Cache.read(:affine1, table)
{w2, b2} = Cache.read(:affine2, table)
{w1, b1, w2, b2}
end
defn forward_g(x, {w1,b1,w2,b2}) do
x
|> Ch5.AffineLayer.forward_g(w1, b1)
|> Ch5.ReluLayer.forward_g()
|> Ch5.AffineLayer.forward_g(w2, b2)
end
defn loss_g({w1,b1,w2,b2}, x, t, batch_size) do
forward_g(x, {w1,b1,w2,b2})
|> Ch5.SoftmaxWithLossLayer.forward_g(t, batch_size)
end
defn numerical_gradient({ w1, b1, w2, b2 } = params, x, t, batch_size \\ 100) do
grad(params, &loss_g(&1, x, t, batch_size))
end
end
defmodule Cache do
def write({out, var}, key, table) do
:ets.insert(table, { key, var })
{out, 0}
end
def read(key, table) do
:ets.lookup(table, key)[key]
end
def d_write({dout, var}, key, table) do
:ets.insert(table, { key, var })
dout
end
end
5.7.3 誤差逆伝播法の勾配確認
2つの関数の誤差を検証します
defmodule GradientCheck do
def get_dataset do
x_train = Dataset.train_image |> Nx.tensor |> (& Nx.divide(&1, Nx.reduce_max(&1))).()
t_train = Dataset.train_label |> Dataset.to_one_hot |> Nx.tensor
{x_train, t_train}
end
def run do
{x_train, t_train} = get_dataset()
IO.puts("data load")
params = Ch5.TwoLayerNet.init_params
table = :ets.new(:grad, [:set, :public])
x_batch = x_train[0..99]
t_batch = t_train[0..99]
IO.puts("numerical grad")
grad = Ch5.TwoLayerNet.numerical_gradient(params, x_batch, t_batch)
IO.puts("gradient")
prop = Ch5.TwoLayerNet.gradient(params, x_batch, t_batch, table)
Enum.zip(Tuple.to_list(grad), Tuple.to_list(prop))
|> Enum.map(fn {g,p} -> Nx.subtract(g,p) |> Nx.abs |> Nx.mean end)
|> IO.inspect()
end
end
実行結果
[#Nx.Tensor<
f64
0.002207910791729087
>, #Nx.Tensor<
f64
0.016974970024421852
>, #Nx.Tensor<
f64
0.037842788674917735
>, #Nx.Tensor<
f64
0.09899999999999998
>]
結構誤差がありますね・・・・
おまけにベンチを貼っておきます
Name ips average deviation median 99th %
backpropagation 241.52 4.14 ms ±57.54% 3.63 ms 9.98 ms
nx grad 232.20 4.31 ms ±29.97% 3.89 ms 8.66 ms
5.7.4 誤差逆伝播法を使った学習
defmodule Train do
alias Ch5.TwoLayerNet, as: Net
def mini_batch(x_train, t_train, batch_size) do
row = Enum.count(x_train)
batch_mask = 0..(row - 1) |> Enum.take_random(batch_size)
x_batch = Enum.map(batch_mask, fn mask -> Enum.at(x_train, mask) end) |> Nx.tensor |> (& Nx.divide(&1, Nx.reduce_max(&1))).()
t_batch = Enum.map(batch_mask, fn mask -> Enum.at(t_train, mask) end) |> Nx.tensor
{ x_batch, t_batch }
end
def train() do
x_train = Dataset.train_image
t_train = Dataset.train_label |> Dataset.to_one_hot
x_test = Dataset.test_image |> Nx.tensor |> (& Nx.divide(&1, Nx.reduce_max(&1))).()
t_test = Dataset.test_label |> Dataset.to_one_hot |> Nx.tensor
IO.puts("data load")
params = Net.init_params
table = :ets.new(:grad, [:set, :public])
iteras_num = 1000
batch_size = 100
lr = 0.1
result = Enum.reduce(
1..iteras_num,
%{params: params, loss_list: [], train_acc: [], test_acc: []},
fn i, acc ->
IO.puts("#{i} epoch start")
{ x_batch, t_batch } = mini_batch(x_train, t_train, batch_size)
{grad_w1, grad_b1, grad_w2, grad_b2} = Net.gradient(acc.params, x_batch, t_batch, table)
{w1, b1, w2, b2} = acc.params
params = {
Nx.subtract(w1,Nx.multiply(grad_w1,lr)),
Nx.subtract(b1,Nx.multiply(grad_b1,lr)),
Nx.subtract(w2,Nx.multiply(grad_w2,lr)),
Nx.subtract(b2,Nx.multiply(grad_b2,lr)),
}
train_loss_list = [Net.loss_g(params, x_batch, t_batch, batch_size) |> Nx.to_scalar | acc.loss_list ]
IO.inspect(train_loss_list)
if (rem(i,batch_size) == 0) do
IO.inspect(acc.train_acc)
IO.inspect(acc.test_acc)
%{
params: params,
loss_list: train_loss_list,
train_acc: [Net.accuracy(params, x_batch, t_batch) |> Nx.to_scalar | acc.train_acc ],
test_acc: [Net.accuracy(params, x_test, t_test) |> Nx.to_scalar | acc.test_acc]
}
else
%{
params: params,
loss_list: train_loss_list,
train_acc: acc.train_acc,
test_acc: acc.test_acc
}
end
end)
IO.inspect(result.loss_list)
end
end
結論
みなさん Nx gradで実装しましょう!
コード
nx and exla update
https://github.com/thehaigo/nx_dl/commit/49b7bb454a74220a8524c7ea51136cb16277ad41
chapter 5
https://github.com/thehaigo/nx_dl/commit/5804f125f3c67c11e80b0124e4d0f5bd3ab109cc