ハミルトニアンモンテカルロ (あるいはハイブリッドモンテカルロ) を F# で実装してみたというお話。ハミルトニアンというのはポテンシャルエネルギーと運動エネルギーの和としてあらわされる物理量のことだそうですが,よくわかりません。わからなくても実装はできるから問題ない(キリッ
実用上で重要なのは,確率密度関数の対数をとってマイナスにしたものと,その偏導関数があれば良いということです。確率密度関数の正規化定数は不要です。したがってベイズ推定で事後分布からのサンプリングが必要な場合も ( は事前分布, は尤度関数) とその偏導関数があれば良いことになります。
open RecycleBin.Random let inline updateWith f (ys : float []) (xs : float []) = for index = 0 to Array.length xs - 1 do xs.[index] <- xs.[index] + f ys.[index] let inline updateBy f (xs : float []) = for index = 0 to Array.length xs - 1 do xs.[index] <- f xs.[index] let inline cross xs ys = Array.fold2 (fun acc x y -> acc + x * y) 0.0 xs ys let rec hmc minusLogP gradMinusLogP epsilon step (currentQ, seed) = xorshift seed { let d = Array.length currentQ let q = Array.copy currentQ let p = Array.zeroCreate d for index = 0 to d - 1 do let! z = normal (0.0, 1.0) p.[index] <- z let currentP = Array.copy p updateWith (fun x -> 0.5 * epsilon * x) (gradMinusLogP q) p for i = 1 to step do updateWith (fun x -> epsilon * x) p q if i <> step then updateWith (fun x -> -epsilon * x) (gradMinusLogP q) p updateWith (fun x -> -0.5 * epsilon * x) (gradMinusLogP q) p updateBy (fun x -> -x) p let currentH = let potential = minusLogP currentQ let kinetic = cross currentP currentP / 2.0 potential + kinetic let proposedH = let potential = minusLogP q let kinetic = cross p p / 2.0 potential + kinetic let! r = ``[0, 1)`` if r < exp (currentH - proposedH) then return q else return currentQ }
ハミルトニアンモンテカルロ hmc
の引数は順に,サンプルしたい密度関数の対数をとってマイナスにしたもの minusLogP
,その偏導関数 gradMinusLogP
,リープフロッグ積分のステップサイズ epsilon
,リープフロッグ積分の長さ step
と,現在の状態 currentQ
および乱数シード seed
です。最初の 2 つは密度関数なので勝手に決まります。最後の 2 つ (1 組) は状態なので,初期値さえ定めれば次以降は後は勝手に決まります。問題はリープフロッグ積分に関わるパラメーターで,複雑なケースではここの調節がなかなか難しいみたいです。
なお,関数中で使用する乱数生成には自作乱数生成フレームワークを使用しています。
例として各変数の分散が 1 で共分散が 0.5 の 2 変量正規分布からのサンプリングを考えます。確率密度関数は ( は正規化定数) で与えられるので, です。最初に述べた通り正規化定数は無視して良いので minusLogP
はこの関数の最初の項だけ与えてやれば良いことになります。実際に F# でサンプルを得る書いてみると次のようになります。
let minusLogNormal (xs : float []) = let x, y = xs.[0], xs.[1] (2.0 / 3.0) * (x * x - x * y + y * y) let gradMinusLogNormal (xs : float []) = let x, y = xs.[0], xs.[1] [|(2.0 / 3.0) * (2.0 * x - y); (2.0 / 3.0) * (-x + 2.0 * y)|] let h = hmc minusLogNormal gradMinusLogNormal 1.0e-2 1000 let rec sample current = seq { let next = h current yield fst next yield! sample next } sample ([|0.0; 0.0|], (123456789u, 362436069u, 521288629u, 88675123u)) |> Seq.take 2000 |> Seq.iter (fun x -> printfn "%f,%f" x.[0] x.[1])
epsilon
に 1.0e-2
, step
に 1000
を与えていますが特に根拠はありません。
ナイーブに実装していますが,例えばハミルトニアンの計算結果を毎回計算しているのを現在の状態として一緒に渡してやるなどすると多少高速化できるかもしれません。