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 は を返します。
日常的なタスク
値と導関数を同時に得る
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
は で、これは で 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
導関数は です。
ニュートン法で根を求める
value_and_diff は、ニュートン法の 1 ステップ に必要なものをちょうど与えます:
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]]に適用できません。
次のステップ
- forward API ではドライバーの正確な契約を述べています。
- dual チュートリアル では、方向微分を含め、手動で接成分をシードします。
- linalg チュートリアル では勾配とヤコビ行列を計算します。
- forward の設計 では手法と入れ子のコストを説明しています。