はじめに
本記事はElixirで機械学習/ディープラーニングができるようになるnumpy likeなライブラリ Nxを使って
「ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装」
をElixirで書いていこうという記事になります。
今回は4章ニューラルネットワークの学習をやっていきます、各項目はの詳細は書籍を参照して頂いて、本記事ではelixirのコードを補足するにとどめます。
量的、内容的に結構重いですが頑張っていきましょう。
準備編
exla setup
1章 pythonの基本 -> とばします
2章 パーセプトロン -> とばします
3章 ニューラルネットワーク
with exla
4章 ニューラルネットワークの学習
5章 誤差逆伝播法
Nx.Defn.Kernel.grad
6章 学習に関するテクニック -> とばします
7章 畳み込みニューラルネットワーク
4.2.1 2乗和誤差
pythonコードは以下で べき乗は Nx.powerになります
0.5 * np.sum((y-t)**2)
defmodule Ch4.Loss do
  import Nx.Defn
  # comment in exla cpu mode
  # @defn_compiler {EXLA, max_float_type: {:f, 64}}
  defn mean_squared_error(y, t) do
    Nx.power(y - t, 2)
    |> Nx.sum()
    |> Nx.multiply(0.5)
  end
end
iex(1)> t = Nx.tensor([0, 0, 1, 0, 0, 0, 0, 0, 0, 0])
# Nx.Tensor<
  s64[10]
  [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
>
iex(2)> y = Nx.tensor([0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0])
# Nx.Tensor<
  f64[10]
  [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
>
iex(3)> Ch4.Loss.mean_squared_error(y, t)
# Nx.Tensor<
  f64
  0.09750000000000003
>
4.2.2 交差エントロピー誤差
こちらは最終的なミニバッチ対応版でbatch_sizeの指定がない場合は1で割るので特に何も起こりません
pythonに比べるとだいぶスッキリしてますね
def cross_entropy_error(y, t): 
  if y.ndim == 1:
    t = t.reshape(1, t.size)
    y = y.reshape(1, y.size)
  batch_size = y.shape[0]
  return -np.sum(t * np.log(y + 1e-7)) / batch_size
defmodule Ch4.Loss do
  import Nx.Defn
  # comment in exla cpu mode
  # @defn_compiler {EXLA, max_float_type: {:f, 64}}
  defn cross_entropy_error(y, t, batch_size \\ 1) do
    Nx.add(y, 1.0e-7)
    |> Nx.log()
    |> Nx.multiply(t)
    |> Nx.sum()
    |> Nx.negate()
    |> Nx.divide(batch_size)
  end
end
iex(1)> t = Nx.tensor([0, 0, 1, 0, 0, 0, 0, 0, 0, 0])
# Nx.Tensor<
  s64[10]
  [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
>
iex(2)> y = Nx.tensor([0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0])
# Nx.Tensor<
  f64[10]
  [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
>
iex(3)> Ch4.Loss.cross_entropy_error(y, t)
# Nx.Tensor<
  f64
  0.510825457099338
>
4.2.3 ミニバッチ学習
Dataset moduleにone_hot行列に変換するものがなかったので実装します
テンソル化する前のListなので Nx.tensor()でテンソルするのをお忘れなく
  def to_one_hot(label) do
    list = label |> Enum.max |> (&(0..&1)).() |> Enum.to_list
    label |> Enum.map(fn t ->  Enum.map(list, fn l -> if t == l, do: 1, else: 0 end) end)
  end
np.random.choiceもないので以下で代用します
x_test = Dataset.test_image() 
t_test = Dataset.test_label() |> Dataset.to_one_hot
{test_size, _ } = Nx.shape(x_test)
batch_size = 10
batch_mask = 0..(test_size-1) |> Enum.take_random(batch_size)
x_batch = Enum.map(batch_mask, fn mask -> Enum.at(x_test, mask) end) |> Nx.tensor |> (& Nx.divide(&1, Nx.reduce_max(&1))).()
t_batch = Enum.map(batch_mask, fn mask -> Enum.at(t_test, mask) end) |> Nx.tensor
4.3.1 微分
これを参考に関数を引数に取れるようにします
https://qiita.com/piacerex/items/421753c033647b6cfb99
defmodule Ch4.Grad do
  import Nx.Defn
  # comment in exla cpu mode
  # @defn_compiler {EXLA, max_float_type: {:f, 64}}
  def numerical_diff(func \\ &nop/1, tensor) do
    h = 1.0e-4
    func.(Nx.add(tensor, h))
    |> Nx.subtract(func.(Nx.subtract(tensor, h)))
    |> Nx.divide(2 * h)
  end
  defp nop(enum), do: enum
end
4.3.2 数値微分の例
function1 = fn (x) -> x |> Nx.power(2) |> Nx.multiply(0.01) |> Nx.add(Nx.multiply(x, 0.1)) end
# Function<44.79398840/1 in :erl_eval.expr/5>
iex(47)> Ch4.Grad.numerical_diff(function1, 5)
# Nx.Tensor<
  f64
  0.1999999999990898
>
4.3.3 偏微分
iex(1)> function_tmp1 = fn x0 -> Nx.multiply(x0, x0) |> Nx.add(Nx.power(4.0,2)) end
# Function<44.79398840/1 in :erl_eval.expr/5>
iex(2)> Ch4.Grad.numerical_diff(function_tmp1,3.0)
# Nx.Tensor<
  f64
  6.00000000000378
>
iex(3)> function_tmp2 = fn x1 -> Nx.power(3.0,2) |> Nx.add(Nx.multiply(x1,x1)) end    
# Function<44.79398840/1 in :erl_eval.expr/5>
iex(4)> Ch4.Grad.numerical_diff(function_tmp2,4.0)                                
# Nx.Tensor<
  f64
  7.999999999999119
>
4.4 勾配
pythonコードは一時的に保持する変数を作成して、最後に戻す等していますが、
Elixirは基本イミュータブルなのでEnum.mapでまわします
特定箇所の重み変更するのが難しいので、一度リストにしてList.update_atで変更した後にNx.tensorで変換しています
def numerical_gradient(f, x): 
  h = 1e-4 # 0.0001
  grad = np.zeros like(x) # x と同じ形状の配列を生成 
  for idx in range(x.size):
    tmp_val = x[idx] 
    # f(x+h) の計算
    x[idx] = tmp_val + h
    fxh1 = f(x)
    # f(x-h) の計算
    x[idx] = tmp_val - h 
    fxh2 = f(x)
    grad[idx] = (fxh1 - fxh2) / (2*h)
    x[idx] = tmp_val # 値を元に戻す 
  return grad
defmodule Ch4.Grad do
  import Nx.Defn
...
  def numerical_gradient(func \\ &nop/1 , tensor) do
    h = 1.0e-4
    Enum.to_list(0..(Nx.size(tensor) - 1))
    |> Enum.map(fn i ->
      idx_p =
        tensor
        |> Nx.to_flat_list
        |> List.update_at(i, &(&1 + h))
        |> Nx.tensor()
        |> Nx.reshape(Nx.shape(tensor))
      idx_n = tensor
        |> Nx.to_flat_list
        |> List.update_at(i, &(&1 - h))
        |> Nx.tensor()
        |> Nx.reshape(Nx.shape(tensor))
      func.(idx_p)
      |> Nx.subtract(func.(idx_n))
      |> Nx.divide(2 * h)
      |> Nx.to_flat_list
    end)
    |> Nx.tensor()
    |> Nx.reshape(Nx.shape(tensor))
  end
end
iex(1)> function_2 = fn x -> Nx.power(x[0], 2) |> Nx.add(Nx.power(x[1], 2)) end
iex(2)> Ch4.Grad.numerical_gradient(function_2, Nx.tensor([3.0, 4.0]))
# Nx.Tensor<
  f32[2]
  [5.98907470703125, 8.001327514648438]
>
4.4.1 勾配法
先程のnumerical_gradientをstem_num回行って、
求められた勾配を学習率(lr)を掛けて引くだけなので特に難しい箇所はありません
defmodule Ch4.Grad do
  import Nx.Defn
...
  def gradient_descent(f, tensor, lr \\ 0.01, step_num \\ 100) do
    Enum.reduce(
      0..(step_num - 1),
      tensor,
      fn _, acc ->
        numerical_gradient(f, acc)
        |> Nx.multiply(lr)
        |> Nx.subtract(acc)
      end
    )
  end
end
iex(1)> function_2 = fn x -> Nx.power(x[0], 2) |> Nx.add(Nx.power(x[1], 2)) end
iex(2)> Ch4.Grad.gradient_descent(function_2, Nx.tensor([-3.0,4.0]), 0.1)
# Nx.Tensor<
  f32[2]
  [-6.106581906806241e-10, 8.152838404384966e-10]
>
4.4.2 ニューラルネットワークに対する勾配
numerical_gradientを実際にニューラルネットワークで使っていきます
defmodule Ch4.SimpleNet do
  def init do
    #Nx.random_normal({2,3})
    Nx.tensor(
      [
        [ 0.47355232, 0.9977393, 0.84668094],
        [ 0.85557411, 0.03563661, 0.69422093]
      ]
    )
  end
  def predict(x,w) do
    Nx.dot(x,w)
  end
  def loss(x,t,w) do
    predict(x,w)
    |> Ch3.Activation.softmax
    |> Ch4.Loss.cross_entropy_error(t)
  end
end
iex(1)> net = Ch4.SimpleNet.init
# Nx.Tensor<
  f32[2][3]
  [
    [0.47355231642723083, 0.997739315032959, 0.8466809391975403],
    [0.8555741310119629, 0.03563661128282547, 0.6942209005355835]
  ]
>
iex(2)> x = Nx.tensor([0.6,0.9])
# Nx.Tensor<
  f32[2]
  [0.6000000238418579, 0.8999999761581421]
>
iex(3)> p = Ch4.SimpleNet.predict(x,net)
# Nx.Tensor<
  f32[3]
  [1.0541480779647827, 0.6307165622711182, 1.1328073740005493]
>
iex(4)> Nx.argmax(p)    
# Nx.Tensor<
  s64
  2
>
iex(5)> t = Nx.tensor([0,0,1])
# Nx.Tensor<
  s64[3]
  [0, 0, 1]
>
iex(6)> Ch4.SimpleNet.loss(x,t,net)
# Nx.Tensor<
  f32
  0.9280683994293213
>
iex(7)> f = fn w -> Ch4.SimpleNet.loss(Nx.tensor([0.6,0.9]),Nx.tensor([0,0,1]),w) end
# Function<44.79398840/1 in :erl_eval.expr/5>
iex(8)> Ch4.Grad.numerical_gradient(f, net)
# Nx.Tensor<
  f32[2][3]
  [
    [0.2193450927734375, 0.14394521713256836, -0.36269426345825195],
    [0.32901763916015625, 0.2154707908630371, -0.5444884300231934]
  ]
>
4.5.1 2層ニューラルネットワークのクラス
実際に学習を行うNNを実装していきます
numerical_gradientはNxの方に勾配を求める関数 grad()があるのでそちらを使います!
grad()はdefnで定義した関数内でしか使用できないので注意してください
numerical_gradientの引数を{,,,,} = paramsとしているのは
paramsが4つの要素を持つタプルであるということを示す必要があるため、この形になっています
paramsだけですとエラーになりました
defmodule Ch4.TwoLayerNet do
  import Nx.Defn
  # @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
  defn predict({ w1, b1, w2, b2 }, x) do
    x
    |> Nx.dot(w1)
    |> Nx.add(b1)
    |> Ch3.Activation.sigmoid
    |> Nx.dot(w2)
    |> Nx.add(b2)
    |> Ch3.Activation.softmax
  end
  defn loss({ w1, b1, w2, b2 }, x, t, batch_size) do
    predict({w1, b1, w2, b2}, x)
    |> Ch4.Loss.cross_entropy_error(t, batch_size)
  end
  defn accuracy({ w1, b1, w2, b2 }, x, t) do
    predict({ w1, b1, w2, b2 }, x)
    |> Nx.argmax(axis: 1)
    |> Nx.equal(Nx.argmax(t, axis: 1))
    |> Nx.mean
  end
  defn numerical_gradient({ _w1, _b1, _w2, _b2 } = params, x, t, batch_size) do
    grad(params, loss(params, x, t, batch_size))
  end
end
iex(1)> params = Ch4.TwoLayerNet.params_init()
iex(2)> x = Nx.random_uniform({100, 784})
iex(3)> t = Enum.map(0..99, fn _ -> Enum.random(0..9)end)  |> Dataset.to_one_hot |> Nx.tensor
iex(4)> { grad_w1, grad_b1, grad_w2, grad_b2 } = Ch4.TwoLayerNet.numerical_gradient(params,x,t)
{#Nx.Tensor<
   f64[784][100]
...
>, #Nx.Tensor<
   f64[100]
...
>,
 #Nx.Tensor<
   f64[100][10]
...
>, #Nx.Tensor<
   f64[10]
...
>}
4.5.2 ミニバッチ学習の実装
grad関数で勾配が取得できることがわかったので、実際に重みとバイアスを更新していく関数を実装していきます
学習にはExlaのCPUを使っても時間がかかるので覚悟しておきましょう
Enum.reduceで複数の値を更新したい場合はMapを使えば問題なく行えます
defmodule Ch4.TwoLayerNet do
...
  defn update({ w1, b1, w2, b2 } = params, x, t, lr, batch_size) do
    {grad_w1, grad_b1, grad_w2, grad_b2} = grad(params, loss(params, x, t, batch_size))
    {
      w1 - (grad_w1 * lr),
      b1 - (grad_b1 * lr),
      w2 - (grad_w2 * lr),
      b2 - (grad_b2 * lr)
    }
  end
  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(params) do
    x_train = Dataset.train_image
    t_train = Dataset.train_label |> Dataset.to_one_hot
    IO.puts("data load")
    iteras_num = 1000
    batch_size = 100
    lr = 0.1
    result = Enum.reduce(1..iteras_num, %{params: params, loss_list: []}, fn i, acc ->
      IO.puts("#{i} epoch start")
      { x_batch, t_batch } = mini_batch(x_train, t_train, batch_size)
      params = update(acc.params, x_batch, t_batch, lr, batch_size)
      train_loss_list = [loss(acc.params, x_batch, t_batch, batch_size) |> Nx.to_number | acc.loss_list ]
      IO.inspect(train_loss_list |> Enum.reverse)
      %{params: params, loss_list: train_loss_list}
    end)
    IO.inspect(result.loss_list)
  end
end
4.5.3 テストデータで評価
1epoch = 100iterates として rem(i,batch_size)が0のときにaccuracyを計るようにしていきます
accuracyも時間がかかるので覚悟しておきましょう・・・
書籍にも、numerical_gradientを使用した学習に時間がかかるので、飛ばして次の高速版に行っても構いませんとあるので、
全体の流れだけ追って、特にエラーがないようでしたら途中で止めても大丈夫かと思います
defmodule Ch4.TwoLayerNet
...
  def train(params) 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")
    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)
      params = update(acc.params, x_batch, t_batch, lr, batch_size)
      train_loss_list = [loss(acc.params, x_batch, t_batch, batch_size) |> Nx.to_number | acc.loss_list ]
      IO.inspect(train_loss_list |> Enum.reverse)
      if (rem(i,2) == 0) do
        IO.inspect(acc.train_acc)
        IO.inspect(acc.test_acc)
        %{
          params: params,
          loss_list: train_loss_list,
          train_acc: [accuracy(params, x_batch, t_batch) |> Nx.to_number | acc.train_acc ],
          test_acc: [accuracy(params, x_test, t_test) |> Nx.to_number | 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
最後に
実装は以上になります、ありがとうございました
今回のコード
https://github.com/thehaigo/nx_dl/commit/39ea4711d4e35a8dbb2a4475466623ecb05e68c9
https://github.com/thehaigo/nx_dl/commit/0c0f1f3661637da6a7c9c2591413ea4489e6ace8
参考ページ
https://qiita.com/piacerex/items/421753c033647b6cfb99
https://github.com/elixir-nx/nx/blob/main/exla/examples/mnist.exs

