Elixirで二重数を実装して自動微分するまで
最近プログラミングElixirを読んでいる。
実際にプロジェクトを作る流れを示した練習課題があったり、ライブラリ事情だったり、実用性の高いガイドブックのような形式で、リファレンスじみた退屈さがなくて読みやすい。 Elixir言語だけでなく、周りのエコシステムごと解説されていて、いざ手を動かそうとした時のハードルが非常に低かった。
Elixirでは、自分で型を定義したり、演算子をオーバーロードしたりもできる、という内容が本の後半に登場する。といってもざっくりとした紹介があるだけなので少し物足りない。
そこで、練習がてら二重数で自動微分する - Qiitaを参考に、Elixirで二重数を実装してみた。
まずは演算子をオーバーロードしたいので、デフォルトの定義をimportしないように設定する。
defmodule DualNumber do defstruct a: 0, b: 0 import Kernel, except: [+: 2, -: 2, *: 2, /: 2]
Inspectプロトコルを実装して、の形で表示することにする。
defimpl Inspect do def inspect(%DualNumber{a: a, b: b}, _opts) do "#{a}+#{b}ε" end end
がりがりと算術処理を実装。中置演算子の定義について、引数名は特に宣言がないらしい。
Kernel.+
のように書くと、importしなかった関数も呼び出せる。
def left + right do %DualNumber{ a: Kernel.+(left.a, right.a), b: Kernel.+(left.b, right.b) } end def left - right do %DualNumber{ a: Kernel.-(left.a, right.a), b: Kernel.-(left.b, right.b) } end def left * right do %DualNumber{ a: Kernel.*(left.a, right.a), b: Kernel.+(Kernel.*(left.a, right.b), Kernel.*(left.b, right.a)) } end def conj(d) do %DualNumber{ a: d.a, b: (- d.b)} end def left / right do (left * (conj right)) / %DualNumber{a: Kernel.*(right.a, right.a), b: 0} end
実際に微分したい関数と、比較用にすでに微分してある関数を定義する。 今回は二重数で自動微分する - Qiitaと同じく を使った。
def f(x) do (%DualNumber{a: 4, b: 0} * x + %DualNumber{a: 3, b: 0}) * x + %DualNumber{a: 2, b: 0} end def df(x) do %DualNumber{a: 8, b: 0} * x + %DualNumber{a: 3, b: 0} end end
実行するとこうなる。
iex(1)> DualNumber.f(%DualNumber{a: 2.0, b: 0}) 24.0+0.0ε # f(2) = 4 * 2^2 + 3 * 2 + 2 = 24 iex(2)> DualNumber.df(%DualNumber{a: 2.0, b: 0}) 19.0+0.0ε # df(2) = 8 * 2 + 3 = 19 iex(3)> DualNumber.f(%DualNumber{a: 2.0, b: 1.0}) 24.0+19.0ε # f(2+ε) = 24 + 19ε = f(2) + df(2) * ε
二重数の実部にf(x)が、虚部に微分結果が返っているのがわかる。 複雑な関数の微分をテストしてもおもしろそうだが、今日は時間がないのでここまで。