3. 一次元モンテカルロ積分
Ray Tracing: The Rest of Your Life (v3.2.3): 3 One Dimensional MC Integration / 3.3 一次元モンテカルロ積分
3.2 では
扱うのは 2 つのトピックです。
- 一様サンプリングによるモンテカルロ推定量の一般形
- PDF を工夫した重点サンプリング(importance sampling)による分散低減
モンテカルロ積分の一般形
確率変数
この期待値を標本平均で近似するのがモンテカルロ推定量です。
一様サンプリングによる推定
を推定します。厳密値は
となります。
重点サンプリング
分散を下げるには,
逆関数法により,
でサンプルできます。推定量は
となります。
Rust 実装
r303-one-dimensional-mc クレートも common に依存しないスタンドアローン実装です。XorShift64 は 3.2 と同じものをローカルに持ちます。
PDF と逆関数法によるサンプリング関数は次の通りです。
fn uniform_pdf(_x: f64) -> f64 {
0.5
}
fn linear_pdf(x: f64) -> f64 {
0.5 * x
}
fn sample_uniform(rng: &mut XorShift64) -> f64 {
rng.range_f64(0.0, 2.0)
}
fn sample_linear_pdf(rng: &mut XorShift64) -> f64 {
let u = rng.next_f64();
(4.0 * u).sqrt()
}推定関数はそれぞれ
fn estimate_integral_uniform(samples: usize, rng: &mut XorShift64) -> f64 {
let mut sum = 0.0;
for _ in 0..samples {
let x = sample_uniform(rng);
sum += (x * x) / uniform_pdf(x);
}
sum / samples as f64
}
fn estimate_integral_importance(samples: usize, rng: &mut XorShift64) -> f64 {
let mut sum = 0.0;
for _ in 0..samples {
let x = sample_linear_pdf(rng);
sum += (x * x) / linear_pdf(x);
}
sum / samples as f64
}1 回の実行では乱数の偶然に左右されるため,summarize_trials で同じ条件を複数回繰り返して平均絶対誤差を計算します。uniform と importance それぞれに独立した RNG インスタンスを使うことで,一方の試行結果が他方に影響しません。
fn summarize_trials(samples: usize, trials: usize, seed: u64) -> (f64, f64) {
let mut rng_u = XorShift64::new(seed ^ 0x3030_a1u64);
let mut rng_i = XorShift64::new(seed ^ 0x3030_b2u64);
let mut sum_abs_error_uniform = 0.0;
let mut sum_abs_error_importance = 0.0;
for _ in 0..trials {
let estimate_u = estimate_integral_uniform(samples, &mut rng_u);
let estimate_i = estimate_integral_importance(samples, &mut rng_i);
sum_abs_error_uniform += (estimate_u - EXACT_INTEGRAL).abs();
sum_abs_error_importance += (estimate_i - EXACT_INTEGRAL).abs();
}
(
sum_abs_error_uniform / trials as f64,
sum_abs_error_importance / trials as f64,
)
}generate_report は 2 段階の出力を構成します。まず
C++ と Rust の違い
C++ 原典では rand() によるグローバルな乱数状態を使います。
inline double random_double() {
return rand() / (RAND_MAX + 1.0);
}Rust では rng を引数として明示的に渡します。summarize_trials が rng_u と rng_i を独立したインスタンスで保持することで,2 手法の試行系列が互いに干渉せず,再現性が保証されます。
r303-one-dimensional-mc/src/lib.rs
const EXACT_INTEGRAL: f64 = 8.0 / 3.0;
struct XorShift64 {
state: u64,
}
impl XorShift64 {
fn new(seed: u64) -> Self {
let state = if seed == 0 { 0x9e3779b97f4a7c15 } else { seed };
Self { state }
}
fn next_u64(&mut self) -> u64 {
let mut x = self.state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.state = x;
x
}
fn next_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 * (1.0 / (1u64 << 53) as f64)
}
fn range_f64(&mut self, min: f64, max: f64) -> f64 {
min + (max - min) * self.next_f64()
}
}
fn uniform_pdf(_x: f64) -> f64 {
0.5
}
fn linear_pdf(x: f64) -> f64 {
0.5 * x
}
fn sample_uniform(rng: &mut XorShift64) -> f64 {
rng.range_f64(0.0, 2.0)
}
fn sample_linear_pdf(rng: &mut XorShift64) -> f64 {
let u = rng.next_f64();
(4.0 * u).sqrt()
}
fn estimate_integral_uniform(samples: usize, rng: &mut XorShift64) -> f64 {
let mut sum = 0.0;
for _ in 0..samples {
let x = sample_uniform(rng);
sum += (x * x) / uniform_pdf(x);
}
sum / samples as f64
}
fn estimate_integral_importance(samples: usize, rng: &mut XorShift64) -> f64 {
let mut sum = 0.0;
for _ in 0..samples {
let x = sample_linear_pdf(rng);
sum += (x * x) / linear_pdf(x);
}
sum / samples as f64
}
fn summarize_trials(samples: usize, trials: usize, seed: u64) -> (f64, f64) {
let mut rng_u = XorShift64::new(seed ^ 0x3030_a1u64);
let mut rng_i = XorShift64::new(seed ^ 0x3030_b2u64);
let mut sum_abs_error_uniform = 0.0;
let mut sum_abs_error_importance = 0.0;
for _ in 0..trials {
let estimate_u = estimate_integral_uniform(samples, &mut rng_u);
let estimate_i = estimate_integral_importance(samples, &mut rng_i);
sum_abs_error_uniform += (estimate_u - EXACT_INTEGRAL).abs();
sum_abs_error_importance += (estimate_i - EXACT_INTEGRAL).abs();
}
(
sum_abs_error_uniform / trials as f64,
sum_abs_error_importance / trials as f64,
)
}
pub fn generate_report() -> String {
let mut output = String::new();
output.push_str("One Dimensional Monte Carlo Integration\n");
output.push_str("=====================================\n\n");
output.push_str("Target integral: I = integral_0^2 x^2 dx = 8/3\n");
output.push_str(&format!("Exact value: {:.12}\n\n", EXACT_INTEGRAL));
let samples = 1_000_000usize;
let mut rng_uniform = XorShift64::new(0x3030_0001u64);
let mut rng_importance = XorShift64::new(0x3030_0002u64);
let uniform_est = estimate_integral_uniform(samples, &mut rng_uniform);
let importance_est = estimate_integral_importance(samples, &mut rng_importance);
output.push_str("Single run (N=1,000,000):\n");
output.push_str(&format!(
"method,estimate,abs_error\nuniform,{:.12},{:.12}\nimportance_x_over_2,{:.12},{:.12}\n\n",
uniform_est,
(uniform_est - EXACT_INTEGRAL).abs(),
importance_est,
(importance_est - EXACT_INTEGRAL).abs()
));
output.push_str("Average absolute error across repeated trials:\n");
output.push_str("samples,trials,uniform,importance_x_over_2\n");
let settings = [(100usize, 200usize), (1_000usize, 200usize), (10_000usize, 100usize)];
for (trial_samples, trials) in settings {
let (err_u, err_i) = summarize_trials(trial_samples, trials, 0x3030_9000u64 + trial_samples as u64);
output.push_str(&format!(
"{},{},{:.12},{:.12}\n",
trial_samples, trials, err_u, err_i
));
}
output
}ブラウザ上での実行結果
method,estimate,abs_error の行では samples,trials,uniform,importance_x_over_2 の行では,左 2 列が試行条件,右 2 列が平均絶対誤差です。小サンプルほど差が大きく現れ,通常は importance_x_over_2 が小さくなります。
まとめ
というモンテカルロ推定量の一般形を導いた。 が何であれ不偏推定量であり, の選び方が分散に影響する。 - 一様サンプリング
で を推定した。 による重点サンプリングは逆関数法 でサンプルでき,同じサンプル数で分散が下がる。 summarize_trialsによる複数試行の平均絶対誤差で,1 回の試行では見えにくい分散の差を実測した。
次章では,この