6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-03-26

はじめに

本記事は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 乗算レイヤの実装

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を第1引数、forward時に書き込んだ値を第2引数として受け取って計算していきます。
その際に更新する重みのパラメーターを保持するために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 誤差逆伝播法の勾配確認

2つの関数の誤差を検証します

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

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?