星にゃーんのブログ

ほとんど無害。

Elixirで二重数を実装して自動微分するまで

最近プログラミングElixirを読んでいる。

実際にプロジェクトを作る流れを示した練習課題があったり、ライブラリ事情だったり、実用性の高いガイドブックのような形式で、リファレンスじみた退屈さがなくて読みやすい。 Elixir言語だけでなく、周りのエコシステムごと解説されていて、いざ手を動かそうとした時のハードルが非常に低かった。

Elixirでは、自分で型を定義したり、演算子オーバーロードしたりもできる、という内容が本の後半に登場する。といってもざっくりとした紹介があるだけなので少し物足りない。

そこで、練習がてら二重数で自動微分する - Qiitaを参考に、Elixirで二重数を実装してみた。


まずは演算子オーバーロードしたいので、デフォルトの定義をimportしないように設定する。

defmodule DualNumber do
  defstruct a: 0, b: 0

  import Kernel, except: [+: 2, -: 2, *: 2, /: 2]

Inspectプロトコルを実装して、 a + bεの形で表示することにする。

  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と同じく { \displaystyle
f(x) = 4 x^2 + 3 x + 2
} を使った。

  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.# f(2) = 4 * 2^2 + 3 * 2 + 2 = 24
iex(2)> DualNumber.df(%DualNumber{a: 2.0, b: 0})
19.0+0.# df(2) = 8 * 2 + 3 = 19
iex(3)> DualNumber.f(%DualNumber{a: 2.0, b: 1.0})
24.0+19.# f(2+ε) = 24 + 19ε = f(2) + df(2) * ε 

二重数の実部にf(x)が、虚部に微分結果が返っているのがわかる。 複雑な関数の微分をテストしてもおもしろそうだが、今日は時間がないのでここまで。