Nxで始めるゼロから作るディープラーニング 5章誤差逆伝播法

# はじめに
本記事はElixirで機械学習/ディープラーニングができるようになるnumpy likeなライブラリ [Nx](https://github.com/elixir-nx/nx/tree/main/nx)を使って
「[ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装](https://www.oreilly.co.jp/books/9784873117584/)」
をElixirで書いていこうという記事になります。

今回は5章の誤差逆伝播法をやっていきます、各項目の詳細は書籍を参照して頂いて、本記事ではelixirのコードを補足するにとどめます。

[準備編](https://qiita.com/the_haigo/items/1a2f0b371a3644960251)
[exla setup](https://qiita.com/the_haigo/items/d1d1b62bfb9fd2013c17)
1章 pythonの基本 -> とばします
2章 パーセプトロン -> とばします
3章 [ニューラルネットワーク](https://qiita.com/the_haigo/items/bedd466142aaaf01641c)
[with exla](https://qiita.com/the_haigo/items/4ec791c878fd48b9dec1)
４章 [ニューラルネットワークの学習](https://qiita.com/the_haigo/items/2b7535bcff8b4df50d9d)
５章 [誤差逆伝播法](https://qiita.com/the_haigo/items/f273a9c687302507d79d)
[Nx.Defn.Kernel.grad](https://qiita.com/thehaigo/items/593926922a1d72591c3d)
6章 学習に関するテクニック -> とばします
7章 [畳み込みニューラルネットワーク](https://qiita.com/thehaigo/items/a4be7812b97bdddd6c1c/)

# 結論
結論から入りますが、
今回実装した誤差逆伝播法はLossがいつまで立っても減らないし、float 64にしたら200回まわして loss ７ -> 16と増えて欠陥品なので
おとなしく Nx gradを使いましょう backwardを実装する必要もないし、今回の誤差逆伝播法と速度はあまり変わりません
誤差逆伝播法の仕組みを学んだり、Elixirでの１つの実装のアイデアとしてみていただければ幸いです

# 5.4.1　乗算レイヤの実装

```lib/ch5/mul_layer.ex
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 加算レイヤの実装

```lib/ch5/add_layer.ex
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
```

```lib/ch5/buy_apple_orange.ex
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)にテンソルを変換しています

```lib/ch5/relu_layer.ex
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 レイヤ

```lib/ch5/sigmoid_layer.ex
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のみ保持するようにしています

```lib/ch5/affine_layer.ex
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 レイヤ

```lib/ch5/softmax_with_loss_layer.ex
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を第１引数、forward時に書き込んだ値を第２引数として受け取って計算していきます。
その際に更新する重みのパラメーターを保持するためにaffineレイヤはdw,dbの値でCache.d_writeを挟んで上書きしています
d_writeは値を書き込んでoutだけを次のレイヤに渡しています

また nx gradですが関数とパラメータの渡し方が &loss(&1,...)と変わっているので注意が必要です

```lib/ch5/two_layer_net.ex
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
```

```lib/ch5/cache.ex
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 誤差逆伝播法の勾配確認
２つの関数の誤差を検証します

```lib/ch5/gradient_check.ex
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 誤差逆伝播法を使った学習

```lib/ch5/train.ex
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