forward チュートリアル

このチュートリアルでは、このリポジトリから導関数を得る最短の方法である diff と value_and_diff を使って、1 変数関数を微分します。クロージャ、名前付きのジェネリック関数、チェック付き演算を呼び出す関数を微分し、最後に 2 階導関数を計算します。背景は forward の設計 にあります。

クイックスタート

moon add Luna-Flow/autodiff@0.2.0
import {
  "Luna-Flow/autodiff",
}
fn main {
  let d = @autodiff.diff(x => x * x + x.sin(), 2.0)
  println("d/dx (x^2 + sin x) at 2 = \{d}")
}
d/dx (x^2 + sin x) at 2 = 3.5838531634528574

クロージャは Dual[Double] を受け取るので、* と sin は双対数の演算であり、diff は 2⋅2+cos⁡22 \cdot 2 + \cos 2 を返します。

日常的なタスク

値と導関数を同時に得る

value_and_diff は 1 回の評価から両方を返します:

fn main {
  let (value, slope) = @autodiff.value_and_diff(x => x.exp() / (x * x), 2.0)
  println("f(2)  = \{value}")
  println("f'(2) = \{slope}")
}
f(2)  = 1.8472640247326626
f'(2) = 0

f(x)=ex/x2f(x) = e^x / x^2 は f′(x)=ex(x−2)/x3f'(x) = e^x (x - 2)/x^3 で、これは x=2x = 2 で 0 になります。

関数の中で定数を使う

キャプチャした数値は定数として入れなければなりません:

fn main {
  let k = 3.0
  let d = @autodiff.diff(
    x => @autodiff.Dual::constant(k) * x * x - x.cos(),
    1.0,
  )
  println("d/dx (3x^2 - cos x) at 1 = \{d}")
}
d/dx (3x^2 - cos x) at 1 = 6.841470984807897

名前付きのジェネリック関数を微分する

トレイトに対して書かれた関数は直接渡すことができ、Dual[Double] でインスタンス化されます:

fn[T : @autodiff.Ring + @autodiff.Logarithmic] x_ln_x(x : T) -> T {
  x * @autodiff.Logarithmic::ln(x)
}

fn main {
  for x in [0.5, 1.0, 2.0] {
    println("d/dx x ln x at \{x} = \{@autodiff.diff(x_ln_x, x)}")
  }
}
d/dx x ln x at 0.5 = 0.3068528194400547
d/dx x ln x at 1 = 1
d/dx x ln x at 2 = 1.6931471805599454

導関数は ln⁡x+1\ln x + 1 です。

ニュートン法で根を求める

value_and_diff は、ニュートン法の 1 ステップ x←x−f(x)/f′(x)x \leftarrow x - f(x)/f'(x) に必要なものをちょうど与えます:

fn main {
  let f = (x : @autodiff.Dual[Double]) => x * x * x - @autodiff.Dual::constant(2.0)
  let mut x = 1.0
  for _ in 0..<5 {
    let (fx, dfx) = @autodiff.value_and_diff(f, x)
    x = x - fx / dfx
  }
  println("cube root of 2 = \{x}")
}
cube root of 2 = 1.2599210498948732

チェック付き演算を通して微分する

f が div_checked や sqrt_checked を使う場合は、エラーが値としてクロージャから出ていけるようにドライバーの呼び出しを包みます:

fn safe_derivative(x : Double) -> Result[Double, @autodiff.ArithmeticError] {
  let ctx = @autodiff.ArithmeticContext::new(53)
  let mut failure = None
  let d = @autodiff.diff(
    v => match v.sqrt_checked(ctx) {
      Ok(r) => r
      Err(e) => {
        failure = Some(e)
        @autodiff.Dual::constant(0.0)
      }
    },
    x,
  )
  match failure {
    Some(e) => Err(e)
    None => Ok(d)
  }
}

fn main {
  for x in [4.0, -4.0] {
    match safe_derivative(x) {
      Ok(d) => println("sqrt'(\{x}) = \{d}")
      Err(e) => println("sqrt'(\{x}) failed: domain error \{e.is_domain_error()}")
    }
  }
}
sqrt'(4) = 0.25
sqrt'(-4) failed: domain error true

さらに進む

2 階導関数

それ自体が diff を呼び出す関数に diff を適用します。内側の関数は Dual[Dual[Double]] 上で実行できるようにジェネリックでなければなりません:

fn[T : @autodiff.Ring + @autodiff.Trigonometric] f(x : T) -> T {
  x * @autodiff.Trigonometric::sin(x)
}

fn main {
  let second = @autodiff.diff(x => @autodiff.diff(f, x), 1.0)
  let expected = 2.0 * @math.cos(1.0) - @math.sin(1.0)
  println("f''(1) = \{second}")
  println("2 cos 1 - sin 1 = \{expected}")
}
f''(1) = 0.23913362692838303
2 cos 1 - sin 1 = 0.23913362692838303

入れ子にした導関数は閉じた式と一致します。レベルごとに処理量が 2 倍になるので、低い階数にのみ使ってください。

その他のスカラー型

diff は One を持つすべての T で動作します。f が環演算だけを使うなら、Float や Int でも問題ありません:

fn main {
  let d : Int = @autodiff.diff(x => x * x * x * x, 3)
  println("d/dx x^4 at 3 = \{d}")
}
d/dx x^4 at 3 = 108

よくある落とし穴

  • クロージャの引数は双対数です。 x.sin() または @autodiff.Trigonometric::sin(x) と書き、@math.sin(x) は使わないでください。後者は Double を受け取るので、ここではコンパイルできません。
  • キャプチャした値には Dual::constant が必要です。 キャプチャした Dual がそれ自体 Dual::variable(…) だと、その接成分が加わり、結果が黙って変わってしまいます。
  • 分岐。 if x.value() < 0.0 { … } は選ばれた分岐を微分します。折れ目では片側導関数が得られます。
  • 入れ子にはジェネリックな内側関数が必要です。 diff を入れ子にするには内側の関数が T についてジェネリックである必要があります。Dual[Double] 上のクロージャは Dual[Dual[Double]] に適用できません。

次のステップ