use crate::core::address::Address;
use crate::core::distribution::*;
use crate::core::model::Model;
use crate::runtime::handler::run;
use crate::runtime::interpreters::{PriorHandler, ScoreGivenTrace};
use crate::runtime::trace::{Choice, ChoiceValue, Trace};
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use std::collections::HashMap;
use std::fmt;
const LOG_SCALE_MIN: f64 = -20.0;
const LOG_SCALE_MAX: f64 = 20.0;
const MU_ABS_MAX: f64 = 1.0e6;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Support {
Real,
Positive,
Unit,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum GuideError {
UnsupportedDiscreteLatent {
addr: Address,
value_type: &'static str,
},
}
impl fmt::Display for GuideError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GuideError::UnsupportedDiscreteLatent { addr, value_type } => write!(
f,
"mean-field VI does not support the discrete latent at {} (type {}): \
only continuous latents (Normal/LogNormal/Beta factors) can be approximated",
addr, value_type
),
}
}
}
impl std::error::Error for GuideError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ParamCoord {
Location,
Scale,
}
#[derive(Clone, Debug)]
pub enum VariationalParam {
Normal {
mu: f64,
log_sigma: f64,
},
LogNormal {
mu: f64,
log_sigma: f64,
},
Beta {
log_alpha: f64,
log_beta: f64,
},
}
impl VariationalParam {
pub fn for_support(support: Support, init_value: f64) -> Self {
match support {
Support::Real => VariationalParam::Normal {
mu: init_value,
log_sigma: init_log_sigma(init_value),
},
Support::Positive => {
let safe = if init_value.is_finite() && init_value > 0.0 {
init_value
} else {
1.0
};
VariationalParam::LogNormal {
mu: safe.ln(),
log_sigma: 0.5_f64.ln(),
}
}
Support::Unit => {
let m = if init_value.is_finite() {
init_value.clamp(1e-3, 1.0 - 1e-3)
} else {
0.5
};
let concentration = 2.0;
VariationalParam::Beta {
log_alpha: (concentration * m).ln(),
log_beta: (concentration * (1.0 - m)).ln(),
}
}
}
}
pub fn sample<R: Rng>(&self, rng: &mut R) -> f64 {
match self {
VariationalParam::Normal { mu, log_sigma } => {
let sigma = log_sigma.exp();
if !mu.is_finite() || !sigma.is_finite() || sigma <= 0.0 {
return f64::NAN;
}
Normal::new(*mu, sigma).unwrap().sample(rng)
}
VariationalParam::LogNormal { mu, log_sigma } => {
let sigma = log_sigma.exp();
if !mu.is_finite() || !sigma.is_finite() || sigma <= 0.0 {
return f64::NAN;
}
LogNormal::new(*mu, sigma).unwrap().sample(rng)
}
VariationalParam::Beta {
log_alpha,
log_beta,
} => {
let alpha = log_alpha.exp();
let beta = log_beta.exp();
if !alpha.is_finite() || !beta.is_finite() || alpha <= 0.0 || beta <= 0.0 {
return f64::NAN;
}
Beta::new(alpha, beta).unwrap().sample(rng)
}
}
}
pub fn sample_with_aux<R: Rng>(&self, rng: &mut R) -> (f64, f64) {
match self {
VariationalParam::Normal { mu, log_sigma } => {
let sigma = log_sigma.exp();
let z = standard_normal(rng);
let value = mu + sigma * z;
(value, z)
}
VariationalParam::LogNormal { mu, log_sigma } => {
let sigma = log_sigma.exp();
let z = standard_normal(rng);
let log_value = mu + sigma * z;
let value = log_value.exp();
(value, z)
}
VariationalParam::Beta {
log_alpha,
log_beta,
} => {
let value = self.sample(rng);
let _ = (log_alpha, log_beta);
(value, f64::NAN)
}
}
}
pub fn log_prob(&self, x: f64) -> f64 {
match self {
VariationalParam::Normal { mu, log_sigma } => {
let sigma = log_sigma.exp();
Normal::new(*mu, sigma).unwrap().log_prob(&x)
}
VariationalParam::LogNormal { mu, log_sigma } => {
let sigma = log_sigma.exp();
LogNormal::new(*mu, sigma).unwrap().log_prob(&x)
}
VariationalParam::Beta {
log_alpha,
log_beta,
} => {
let alpha = log_alpha.exp();
let beta = log_beta.exp();
Beta::new(alpha, beta).unwrap().log_prob(&x)
}
}
}
}
fn standard_normal<R: Rng>(rng: &mut R) -> f64 {
let u1: f64 = rng.gen::<f64>().max(1e-10);
let u2: f64 = rng.gen();
(-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
}
fn init_log_sigma(value: f64) -> f64 {
let scale = if value.is_finite() { value.abs() } else { 1.0 };
(0.1 * scale).max(0.1).ln()
}
fn shifted(param: &VariationalParam, coord: ParamCoord, delta: f64) -> VariationalParam {
match param {
VariationalParam::Normal { mu, log_sigma } => match coord {
ParamCoord::Location => VariationalParam::Normal {
mu: mu + delta,
log_sigma: *log_sigma,
},
ParamCoord::Scale => VariationalParam::Normal {
mu: *mu,
log_sigma: log_sigma + delta,
},
},
VariationalParam::LogNormal { mu, log_sigma } => match coord {
ParamCoord::Location => VariationalParam::LogNormal {
mu: mu + delta,
log_sigma: *log_sigma,
},
ParamCoord::Scale => VariationalParam::LogNormal {
mu: *mu,
log_sigma: log_sigma + delta,
},
},
VariationalParam::Beta {
log_alpha,
log_beta,
} => match coord {
ParamCoord::Location => VariationalParam::Beta {
log_alpha: log_alpha + delta,
log_beta: *log_beta,
},
ParamCoord::Scale => VariationalParam::Beta {
log_alpha: *log_alpha,
log_beta: log_beta + delta,
},
},
}
}
fn apply_update(param: &mut VariationalParam, coord: ParamCoord, delta: f64) {
match param {
VariationalParam::Normal { mu, log_sigma } => match coord {
ParamCoord::Location => *mu = (*mu + delta).clamp(-MU_ABS_MAX, MU_ABS_MAX),
ParamCoord::Scale => {
*log_sigma = (*log_sigma + delta).clamp(LOG_SCALE_MIN, LOG_SCALE_MAX)
}
},
VariationalParam::LogNormal { mu, log_sigma } => match coord {
ParamCoord::Location => *mu = (*mu + delta).clamp(-MU_ABS_MAX, MU_ABS_MAX),
ParamCoord::Scale => {
*log_sigma = (*log_sigma + delta).clamp(LOG_SCALE_MIN, LOG_SCALE_MAX)
}
},
VariationalParam::Beta {
log_alpha,
log_beta,
} => match coord {
ParamCoord::Location => {
*log_alpha = (*log_alpha + delta).clamp(LOG_SCALE_MIN, LOG_SCALE_MAX)
}
ParamCoord::Scale => {
*log_beta = (*log_beta + delta).clamp(LOG_SCALE_MIN, LOG_SCALE_MAX)
}
},
}
}
#[derive(Clone, Debug)]
pub struct MeanFieldGuide {
pub params: HashMap<Address, VariationalParam>,
}
impl Default for MeanFieldGuide {
fn default() -> Self {
Self::new()
}
}
impl MeanFieldGuide {
pub fn new() -> Self {
Self {
params: HashMap::new(),
}
}
pub fn add_latent(&mut self, addr: Address, support: Support, init_value: f64) {
self.params
.insert(addr, VariationalParam::for_support(support, init_value));
}
pub fn from_trace(trace: &Trace) -> Result<Self, GuideError> {
let mut guide = Self::new();
for (addr, choice) in &trace.choices {
let param = match choice.value {
ChoiceValue::F64(val) => VariationalParam::Normal {
mu: val,
log_sigma: init_log_sigma(val),
},
ChoiceValue::Bool(_)
| ChoiceValue::I64(_)
| ChoiceValue::U64(_)
| ChoiceValue::Usize(_) => {
return Err(GuideError::UnsupportedDiscreteLatent {
addr: addr.clone(),
value_type: choice.value.type_name(),
});
}
};
guide.params.insert(addr.clone(), param);
}
Ok(guide)
}
pub fn sample_trace<R: Rng>(&self, rng: &mut R) -> Trace {
let mut trace = Trace::default();
let mut entries: Vec<(&Address, &VariationalParam)> = self.params.iter().collect();
entries.sort_by(|a, b| a.0.cmp(b.0));
for (addr, param) in entries {
let value = param.sample(rng);
let log_prob = param.log_prob(value);
trace.choices.insert(
addr.clone(),
Choice {
addr: addr.clone(),
value: ChoiceValue::F64(value),
logp: log_prob,
},
);
trace.log_prior += log_prob;
}
trace
}
}
pub fn elbo_with_guide<A, R: Rng>(
rng: &mut R,
model_fn: impl Fn() -> Model<A>,
guide: &MeanFieldGuide,
num_samples: usize,
) -> f64 {
let mut total_elbo = 0.0;
for _ in 0..num_samples {
let guide_trace = guide.sample_trace(rng);
let (_a, model_trace) = run(
ScoreGivenTrace {
base: guide_trace.clone(),
trace: Trace::default(),
},
model_fn(),
);
let log_joint = model_trace.total_log_weight();
let log_guide: f64 = model_trace
.choices
.keys()
.filter_map(|addr| guide_trace.choices.get(addr).map(|c| c.logp))
.sum();
total_elbo += log_joint - log_guide;
}
total_elbo / num_samples as f64
}
pub fn elbo_gradient_fd<A>(
seed: u64,
model_fn: impl Fn() -> Model<A>,
guide: &MeanFieldGuide,
addr: &Address,
coord: ParamCoord,
eps: f64,
num_samples: usize,
) -> f64 {
let base = match guide.params.get(addr) {
Some(p) => p,
None => return 0.0,
};
let mut guide_plus = guide.clone();
guide_plus
.params
.insert(addr.clone(), shifted(base, coord, eps));
let mut guide_minus = guide.clone();
guide_minus
.params
.insert(addr.clone(), shifted(base, coord, -eps));
let elbo_plus = elbo_with_guide(
&mut StdRng::seed_from_u64(seed),
&model_fn,
&guide_plus,
num_samples,
);
let elbo_minus = elbo_with_guide(
&mut StdRng::seed_from_u64(seed),
&model_fn,
&guide_minus,
num_samples,
);
(elbo_plus - elbo_minus) / (2.0 * eps)
}
#[derive(Clone, Debug)]
pub struct VIConfig {
pub n_iterations: usize,
pub n_samples_per_iter: usize,
pub base_learning_rate: f64,
pub fd_eps: f64,
pub convergence_tol: f64,
pub convergence_window: usize,
pub step_decay_exponent: f64,
}
impl Default for VIConfig {
fn default() -> Self {
Self {
n_iterations: 1000,
n_samples_per_iter: 16,
base_learning_rate: 0.1,
fd_eps: 0.01,
convergence_tol: 1e-4,
convergence_window: 20,
step_decay_exponent: 0.6,
}
}
}
#[derive(Clone, Debug)]
pub struct VIResult {
pub guide: MeanFieldGuide,
pub elbo_history: Vec<f64>,
pub converged: bool,
pub iterations: usize,
}
pub fn optimize_meanfield_vi_with_config<A, R: Rng>(
rng: &mut R,
model_fn: impl Fn() -> Model<A>,
initial_guide: MeanFieldGuide,
config: &VIConfig,
) -> VIResult {
let mut guide = initial_guide;
let mut elbo_history: Vec<f64> = Vec::with_capacity(config.n_iterations);
let mut converged = false;
let mut iterations = 0;
for iter in 0..config.n_iterations {
iterations = iter + 1;
let monitor_seed: u64 = rng.gen();
let current_elbo = elbo_with_guide(
&mut StdRng::seed_from_u64(monitor_seed),
&model_fn,
&guide,
config.n_samples_per_iter,
);
elbo_history.push(current_elbo);
let w = config.convergence_window;
if w > 0 && elbo_history.len() >= 2 * w {
let n = elbo_history.len();
let recent: f64 = elbo_history[n - w..].iter().sum::<f64>() / w as f64;
let previous: f64 = elbo_history[n - 2 * w..n - w].iter().sum::<f64>() / w as f64;
let denom = previous.abs().max(1e-8);
if (recent - previous).abs() / denom < config.convergence_tol {
converged = true;
break;
}
}
let step =
config.base_learning_rate * ((iter + 1) as f64).powf(-config.step_decay_exponent);
let snapshot = guide.clone();
let mut addrs: Vec<Address> = snapshot.params.keys().cloned().collect();
addrs.sort();
for addr in &addrs {
for coord in [ParamCoord::Location, ParamCoord::Scale] {
let seed: u64 = rng.gen();
let grad = elbo_gradient_fd(
seed,
&model_fn,
&snapshot,
addr,
coord,
config.fd_eps,
config.n_samples_per_iter,
);
if grad.is_finite() {
let update = step * grad;
if update.is_finite() {
if let Some(param) = guide.params.get_mut(addr) {
apply_update(param, coord, update);
}
}
}
}
}
}
VIResult {
guide,
elbo_history,
converged,
iterations,
}
}
pub fn optimize_meanfield_vi<A, R: Rng>(
rng: &mut R,
model_fn: impl Fn() -> Model<A>,
initial_guide: MeanFieldGuide,
n_iterations: usize,
n_samples_per_iter: usize,
learning_rate: f64,
) -> MeanFieldGuide {
let config = VIConfig {
n_iterations,
n_samples_per_iter,
base_learning_rate: learning_rate,
..VIConfig::default()
};
optimize_meanfield_vi_with_config(rng, model_fn, initial_guide, &config).guide
}
pub fn estimate_elbo<A, R: Rng>(
rng: &mut R,
model_fn: impl Fn() -> Model<A>,
num_samples: usize,
) -> f64 {
let mut total = 0.0;
for _ in 0..num_samples {
let (_a, prior_t) = run(
PriorHandler {
rng,
trace: Trace::default(),
},
model_fn(),
);
total += prior_t.log_likelihood + prior_t.log_factors;
}
total / (num_samples as f64)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::addr;
use crate::core::model::{observe, sample, ModelExt};
use crate::runtime::trace::{Choice, ChoiceValue, Trace};
use rand::rngs::StdRng;
use rand::SeedableRng;
#[test]
fn variational_param_sampling_and_log_prob() {
let mut rng = StdRng::seed_from_u64(20);
let vp_n = VariationalParam::Normal {
mu: 0.0,
log_sigma: 0.0,
};
let x = vp_n.sample(&mut rng);
assert!(x.is_finite());
assert!(vp_n.log_prob(x).is_finite());
let vp_b = VariationalParam::Beta {
log_alpha: (2.0f64).ln(),
log_beta: (3.0f64).ln(),
};
let y = vp_b.sample(&mut rng);
assert!(y > 0.0 && y < 1.0);
assert!(vp_b.log_prob(y).is_finite());
}
#[test]
fn elbo_computation_is_finite() {
let model_fn = || {
sample(addr!("mu"), Normal::new(0.0, 1.0).unwrap()).and_then(|mu| {
observe(addr!("y"), Normal::new(mu, 1.0).unwrap(), 0.2).map(move |_| mu)
})
};
let mut guide = MeanFieldGuide::new();
guide.params.insert(
addr!("mu"),
VariationalParam::Normal {
mu: 0.0,
log_sigma: 0.0,
},
);
let mut rng = StdRng::seed_from_u64(21);
let elbo = elbo_with_guide(&mut rng, model_fn, &guide, 5);
assert!(elbo.is_finite());
}
#[test]
fn meanfield_from_trace_continuous_ok() {
let mut base = Trace::default();
base.choices.insert(
addr!("pos"),
Choice {
addr: addr!("pos"),
value: ChoiceValue::F64(-1.0),
logp: -0.1,
},
);
base.choices.insert(
addr!("z"),
Choice {
addr: addr!("z"),
value: ChoiceValue::F64(0.0),
logp: -0.2,
},
);
let guide = MeanFieldGuide::from_trace(&base).expect("continuous trace should build");
assert_eq!(guide.params.len(), 2);
if let VariationalParam::Normal { log_sigma, .. } = guide.params.get(&addr!("z")).unwrap() {
assert!(log_sigma.is_finite());
assert!(log_sigma.exp() > 0.0);
} else {
panic!("expected Normal factor");
}
let t = guide.sample_trace(&mut StdRng::seed_from_u64(22));
assert!(!t.choices.is_empty());
assert!(t.log_prior.is_finite());
}
#[test]
fn optimize_vi_updates_parameters_and_is_stable() {
let model_fn = || {
sample(addr!("mu"), Normal::new(0.0, 1.0).unwrap()).and_then(|mu| {
observe(addr!("y"), Normal::new(mu, 1.0).unwrap(), 0.3).map(move |_| mu)
})
};
let mut guide = MeanFieldGuide::new();
guide.params.insert(
addr!("mu"),
VariationalParam::Normal {
mu: 0.0,
log_sigma: 0.0,
},
);
let optimized = optimize_meanfield_vi(
&mut StdRng::seed_from_u64(23),
model_fn,
guide.clone(),
5, 4,
0.1,
);
if let VariationalParam::Normal { mu, log_sigma } =
optimized.params.get(&addr!("mu")).unwrap()
{
assert!(mu.is_finite() && mu.abs() <= MU_ABS_MAX);
assert!(log_sigma.is_finite());
} else {
panic!("expected Normal param");
}
}
}