use candle_core::{Result, Tensor};
use candle_nn::VarBuilder;
use super::sgvb_util::{antithetic_epsilon, SGVBConfig};
use super::traits::{
AnalyticalKL, LocalReparamModel, LocalReparamSample, Prior, VariationalDistribution,
};
use super::variational_gaussian::GaussianVar;
use super::variational_susie::SusieVar;
pub struct RegressionSGVB<V, P> {
pub variational: V,
pub prior: P,
pub x_design: Tensor,
pub config: SGVBConfig,
}
impl<V: VariationalDistribution, P: Prior> RegressionSGVB<V, P> {
pub fn from_variational(
variational: V,
x_design: Tensor,
prior: P,
config: SGVBConfig,
) -> Self {
Self {
variational,
prior,
x_design,
config,
}
}
pub fn eta_mean(&self) -> Result<Tensor> {
let theta_mean = self.variational.mean()?;
self.x_design.matmul(&theta_mean)
}
pub fn coef_mean(&self) -> Result<Tensor> {
self.variational.mean()
}
pub fn coef_var(&self) -> Result<Tensor> {
self.variational.var()
}
}
impl<V: VariationalDistribution, P: Prior + AnalyticalKL> RegressionSGVB<V, P> {
pub fn forward(&self, num_samples: usize) -> Result<LocalReparamSample> {
let theta_mean = self.variational.mean()?; let theta_var = self.variational.var()?;
let eta_mean = self.x_design.matmul(&theta_mean)?;
let x_sq = self.x_design.sqr()?; let eta_var = x_sq.matmul(&theta_var)?;
let (n, k) = eta_mean.dims2()?;
let device = eta_mean.device();
let dtype = eta_mean.dtype();
let epsilon = antithetic_epsilon(num_samples, n, k, device, dtype)?;
let eta_std = (eta_var + 1e-8)?.sqrt()?; let eta = eta_mean
.unsqueeze(0)?
.broadcast_add(&epsilon.broadcast_mul(&eta_std.unsqueeze(0)?)?)?;
let kl = self.prior.kl_from_gaussian(&theta_mean, &theta_var)?;
Ok(LocalReparamSample { eta, kl })
}
}
impl<V: VariationalDistribution, P: Prior + AnalyticalKL> LocalReparamModel
for RegressionSGVB<V, P>
{
fn forward(&self, num_samples: usize) -> Result<LocalReparamSample> {
self.forward(num_samples)
}
}
pub type GaussianRegressionSGVB<P> = RegressionSGVB<GaussianVar, P>;
pub type SusieRegressionSGVB<P> = RegressionSGVB<SusieVar, P>;
impl<P: Prior> GaussianRegressionSGVB<P> {
pub fn new(
vb: VarBuilder,
x_design: Tensor,
k: usize,
prior: P,
config: SGVBConfig,
) -> Result<Self> {
let p = x_design.dim(1)?;
let variational = GaussianVar::new(vb, p, k)?;
Ok(Self::from_variational(variational, x_design, prior, config))
}
pub fn coef_std(&self) -> Result<Tensor> {
self.variational.std()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::candle::sgvb::traits::BlackBoxLikelihood;
use crate::candle::sgvb::{local_reparam_loss, GaussianPrior};
use candle_core::{DType, Device, Tensor};
use candle_nn::{Optimizer, VarBuilder, VarMap};
struct TestGaussianLikelihood {
y: Tensor,
sigma: f64,
}
impl TestGaussianLikelihood {
fn new(y: Tensor, sigma: f64) -> Self {
Self { y, sigma }
}
}
impl BlackBoxLikelihood for TestGaussianLikelihood {
fn log_likelihood(&self, etas: &[&Tensor]) -> Result<Tensor> {
let eta = etas[0];
let sigma_sq = self.sigma.powi(2);
let ln_2pi = (2.0 * std::f64::consts::PI).ln();
let ln_sigma = self.sigma.ln();
let const_term = 2.0 * ln_sigma + ln_2pi;
let diff_sq = eta.broadcast_sub(&self.y)?.sqr()?;
let log_prob = (((diff_sq / sigma_sq)? + const_term)? * (-0.5))?;
log_prob.sum(2)?.sum(1)
}
}
#[test]
fn test_linear_sgvb_construction() -> Result<()> {
let device = Device::Cpu;
let dtype = DType::F32;
let n = 50;
let p = 10;
let k = 3;
let x = Tensor::randn(0f32, 1f32, (n, p), &device)?;
let varmap = VarMap::new();
let vb = VarBuilder::from_varmap(&varmap, dtype, &device);
let prior = GaussianPrior::new(vb.pp("prior"), 1.0)?;
let config = SGVBConfig::default();
let model = GaussianRegressionSGVB::new(vb.pp("model"), x, k, prior, config)?;
assert_eq!(model.coef_mean()?.dims(), &[p, k]);
assert_eq!(model.coef_std()?.dims(), &[p, k]);
assert_eq!(model.eta_mean()?.dims(), &[n, k]);
Ok(())
}
#[test]
fn test_linear_sgvb_loss() -> Result<()> {
let device = Device::Cpu;
let dtype = DType::F32;
let n = 150;
let p = 30;
let k = 1;
let x = Tensor::randn(0f32, 1f32, (n, p), &device)?;
let true_coef = 2.0f64;
let x_first = x.narrow(1, 0, 1)?; let noise = Tensor::randn(0f32, 0.5f32, (n, k), &device)?;
let y = (x_first * true_coef)?.add(&noise)?;
let varmap = VarMap::new();
let vb = VarBuilder::from_varmap(&varmap, dtype, &device);
let likelihood = TestGaussianLikelihood::new(y, 0.5);
let prior = GaussianPrior::new(vb.pp("prior"), 1.0)?;
let config = SGVBConfig::new(50);
let model = GaussianRegressionSGVB::new(vb.pp("model"), x, k, prior, config)?;
let mut optimizer = candle_nn::AdamW::new_lr(varmap.all_vars(), 0.01)?;
for i in 0..200 {
let loss = local_reparam_loss(&model, &likelihood, 50, 1.0)?;
optimizer.backward_step(&loss)?;
if i % 20 == 0 {
let loss_val: f32 = loss.to_scalar()?;
let elbo_val = -loss_val;
println!("iter {}: loss = {:.4}, elbo = {:.4}", i, loss_val, elbo_val);
}
assert!(loss.dims().is_empty());
}
let coef_mean = model.coef_mean()?;
let coef_first: f32 = coef_mean.get(0)?.get(0)?.to_scalar()?;
let mut other_sum = 0.0f32;
for i in 1..p {
let val: f32 = coef_mean.get(i)?.get(0)?.to_scalar()?;
other_sum += val.abs();
}
let other_mean = other_sum / (p - 1) as f32;
println!("\nCoefficients:");
println!(" First (true={:.1}): {:.4}", true_coef, coef_first);
println!(" Others mean abs: {:.4}", other_mean);
assert!(
coef_first > 1.0,
"First coef should be > 1.0, got {}",
coef_first
);
assert!(
coef_first.abs() > other_mean * 2.0,
"First coef should dominate others"
);
Ok(())
}
#[test]
fn test_susie_linear_construction() -> Result<()> {
use crate::candle::sgvb::SusieVar;
let device = Device::Cpu;
let dtype = DType::F32;
let n = 50;
let p = 20;
let k = 2;
let l = 3;
let x = Tensor::randn(0f32, 1f32, (n, p), &device)?;
let varmap = VarMap::new();
let vb = VarBuilder::from_varmap(&varmap, dtype, &device);
let susie = SusieVar::new(vb.pp("susie"), l, p, k)?;
let prior = GaussianPrior::new(vb.pp("prior"), 1.0)?;
let config = SGVBConfig::default();
let model = RegressionSGVB::from_variational(susie, x, prior, config);
assert_eq!(model.coef_mean()?.dims(), &[p, k]);
assert_eq!(model.coef_var()?.dims(), &[p, k]);
assert_eq!(model.eta_mean()?.dims(), &[n, k]);
assert_eq!(model.variational.alpha()?.dims(), &[l, p, k]);
assert_eq!(model.variational.pip()?.dims(), &[p, k]);
Ok(())
}
#[test]
fn test_susie_linear_sparse_recovery() -> Result<()> {
use crate::candle::sgvb::SusieVar;
let device = Device::Cpu;
let dtype = DType::F32;
let n = 150;
let p = 50;
let k = 1;
let l = 2;
let x = Tensor::randn(0f32, 1f32, (n, p), &device)?;
let x_0 = x.narrow(1, 0, 1)?;
let x_5 = x.narrow(1, 5, 1)?;
let noise = Tensor::randn(0f32, 0.5f32, (n, k), &device)?;
let y = ((x_0 * 2.0)? + (x_5 * 1.5)? + noise)?;
let varmap = VarMap::new();
let vb = VarBuilder::from_varmap(&varmap, dtype, &device);
let likelihood = TestGaussianLikelihood::new(y, 0.5);
let susie = SusieVar::new(vb.pp("susie"), l, p, k)?;
let prior = GaussianPrior::new(vb.pp("prior"), 1.0)?;
let config = SGVBConfig::new(50);
let model = RegressionSGVB::from_variational(susie, x, prior, config);
let mut optimizer = candle_nn::AdamW::new_lr(varmap.all_vars(), 0.05)?;
for i in 0..500 {
let loss = local_reparam_loss(&model, &likelihood, 50, 1.0)?;
optimizer.backward_step(&loss)?;
if i % 5 == 0 {
let loss_val: f32 = loss.to_scalar()?;
let pip = model.variational.pip()?;
let pip_0: f32 = pip.get(0)?.get(0)?.to_scalar()?;
let pip_5: f32 = pip.get(5)?.get(0)?.to_scalar()?;
println!(
"iter {}: loss = {:.4}, PIP[0] = {:.4}, PIP[5] = {:.4}",
i, loss_val, pip_0, pip_5
);
}
}
let pip = model.variational.pip()?;
let pip_0: f32 = pip.get(0)?.get(0)?.to_scalar()?;
let pip_5: f32 = pip.get(5)?.get(0)?.to_scalar()?;
let mut other_sum = 0.0f32;
for j in 0..p {
if j != 0 && j != 5 {
let val: f32 = pip.get(j)?.get(0)?.to_scalar()?;
other_sum += val;
}
}
let other_mean = other_sum / (p - 2) as f32;
println!("\nPosterior Inclusion Probabilities:");
println!(" PIP[0] (true): {:.4}", pip_0);
println!(" PIP[5] (true): {:.4}", pip_5);
println!(" Others mean: {:.4}", other_mean);
assert!(
pip_0 > other_mean * 3.0,
"PIP[0] should be > 3x other mean, got {} vs {}",
pip_0,
other_mean
);
assert!(
pip_5 > other_mean * 3.0,
"PIP[5] should be > 3x other mean, got {} vs {}",
pip_5,
other_mean
);
Ok(())
}
}