use burn::tensor::backend::Backend;
use burn::tensor::{Tensor, TensorData};
use core::f64::consts::{FRAC_1_SQRT_2, PI};
#[derive(Clone, Debug)]
pub struct Moments<B: Backend> {
pub mean: Tensor<B, 2>,
pub var: Tensor<B, 2>,
}
impl<B: Backend> Moments<B> {
pub fn new(mean: Tensor<B, 2>, var: Tensor<B, 2>) -> Self {
Self { mean, var }
}
}
pub fn propagate_linear<B: Backend>(
m: &Moments<B>,
weight: Tensor<B, 2>,
bias: Option<Tensor<B, 1>>,
) -> Moments<B> {
let mut mean = m.mean.clone().matmul(weight.clone());
if let Some(b) = bias {
let d = b.dims()[0];
mean = mean + b.reshape([1, d]);
}
let w2 = weight.clone() * weight;
let var = m.var.clone().matmul(w2);
Moments { mean, var }
}
pub fn propagate_linear_bayes<B: Backend>(
m: &Moments<B>,
w_mean: Tensor<B, 2>,
w_var: Tensor<B, 2>,
bias: Option<(Tensor<B, 1>, Tensor<B, 1>)>,
) -> Moments<B> {
let mut mean = m.mean.clone().matmul(w_mean.clone());
let wm2 = w_mean.clone() * w_mean;
let mx2 = m.mean.clone() * m.mean.clone();
let mut var =
mx2.matmul(w_var.clone()) + m.var.clone().matmul(wm2) + m.var.clone().matmul(w_var);
if let Some((bm, bv)) = bias {
let d = bm.dims()[0];
mean = mean + bm.reshape([1, d]);
var = var + bv.reshape([1, d]);
}
Moments { mean, var }
}
pub fn propagate_matmul_left<B: Backend>(a: Tensor<B, 2>, m: &Moments<B>) -> Moments<B> {
let mean = a.clone().matmul(m.mean.clone());
let a2 = a.clone() * a;
let var = a2.matmul(m.var.clone());
Moments { mean, var }
}
pub fn propagate_relu<B: Backend>(m: &Moments<B>) -> Moments<B> {
let eps = 1e-12;
let var = m.var.clone().clamp_min(eps);
let sigma = var.clone().sqrt();
let mu = m.mean.clone();
let alpha = mu.clone() / sigma.clone();
let big_phi = alpha
.clone()
.mul_scalar(FRAC_1_SQRT_2)
.erf()
.add_scalar(1.0)
.mul_scalar(0.5);
let phi = (alpha.clone() * alpha.clone())
.mul_scalar(-0.5)
.exp()
.mul_scalar(1.0 / (2.0 * PI).sqrt());
let mu_out = mu.clone() * big_phi.clone() + sigma.clone() * phi.clone();
let var_out = (mu.clone() * mu.clone() + var) * big_phi + mu * sigma * phi
- mu_out.clone() * mu_out.clone();
Moments {
mean: mu_out,
var: var_out.clamp_min(0.0),
}
}
pub fn propagate_leaky_relu<B: Backend>(m: &Moments<B>, alpha: f64) -> Moments<B> {
let var = m.var.clone().clamp_min(1e-12);
let sigma = var.clone().sqrt();
let mu = m.mean.clone();
let a = mu.clone() / sigma.clone();
let big_phi = a
.clone()
.mul_scalar(FRAC_1_SQRT_2)
.erf()
.add_scalar(1.0)
.mul_scalar(0.5);
let phi = (a.clone() * a.clone())
.mul_scalar(-0.5)
.exp()
.mul_scalar(1.0 / (2.0 * PI).sqrt());
let mu2_plus_var = mu.clone() * mu.clone() + var.clone();
let e_r = mu.clone() * big_phi.clone() + sigma.clone() * phi.clone();
let e_r2 = mu2_plus_var.clone() * big_phi + mu * sigma * phi;
let mean = e_r.mul_scalar(1.0 - alpha) + m.mean.clone().mul_scalar(alpha);
let e_y2 = mu2_plus_var.mul_scalar(alpha * alpha) + e_r2.mul_scalar(1.0 - alpha * alpha);
let var_out = (e_y2 - mean.clone() * mean.clone()).clamp_min(0.0);
Moments { mean, var: var_out }
}
pub fn propagate_residual_add<B: Backend>(skip: &Moments<B>, branch: &Moments<B>) -> Moments<B> {
Moments {
mean: skip.mean.clone() + branch.mean.clone(),
var: skip.var.clone() + branch.var.clone(),
}
}
pub fn propagate_conv2d<B: Backend>(
mean: Tensor<B, 4>,
var: Tensor<B, 4>,
weight: Tensor<B, 4>,
bias: Option<Tensor<B, 1>>,
options: burn::tensor::ops::ConvOptions<2>,
) -> (Tensor<B, 4>, Tensor<B, 4>) {
let mean_out = burn::tensor::module::conv2d(mean, weight.clone(), bias, options.clone());
let var_out = burn::tensor::module::conv2d(var, weight.clone() * weight, None, options);
(mean_out, var_out)
}
fn eye<B: Backend>(d: usize, device: &B::Device) -> Tensor<B, 2> {
let mut v = vec![0.0f32; d * d];
for i in 0..d {
v[i * d + i] = 1.0;
}
Tensor::<B, 2>::from_data(TensorData::new(v, [d, d]), device)
}
#[derive(Clone, Debug)]
pub struct MomentsFull<B: Backend> {
pub mean: Tensor<B, 2>,
pub cov: Tensor<B, 3>,
}
impl<B: Backend> MomentsFull<B> {
pub fn new(mean: Tensor<B, 2>, cov: Tensor<B, 3>) -> Self {
Self { mean, cov }
}
pub fn from_diagonal(mean: Tensor<B, 2>, var: Tensor<B, 2>) -> Self {
let [n, d] = var.dims();
let eye_d = eye::<B>(d, &var.device());
let cov = var.unsqueeze_dim::<3>(2).expand([n, d, d]) * eye_d.unsqueeze::<3>();
Self { mean, cov }
}
pub fn variance(&self) -> Tensor<B, 2> {
let [n, d, _] = self.cov.dims();
let eye_d = eye::<B>(d, &self.cov.device());
(self.cov.clone() * eye_d.unsqueeze::<3>())
.sum_dim(2)
.reshape([n, d])
}
}
pub fn propagate_linear_full<B: Backend>(
m: &MomentsFull<B>,
weight: Tensor<B, 2>,
bias: Option<Tensor<B, 1>>,
) -> MomentsFull<B> {
let [n, _] = m.mean.dims();
let [d_in, d_out] = weight.dims();
let mut mean = m.mean.clone().matmul(weight.clone());
if let Some(b) = bias {
mean = mean + b.reshape([1, d_out]);
}
let w3 = weight.clone().unsqueeze::<3>().expand([n, d_in, d_out]);
let wt3 = weight
.swap_dims(0, 1)
.unsqueeze::<3>()
.expand([n, d_out, d_in]);
let cov = wt3.matmul(m.cov.clone().matmul(w3));
MomentsFull { mean, cov }
}
pub fn propagate_relu_full<B: Backend>(m: &MomentsFull<B>) -> MomentsFull<B> {
let [n, d, _] = m.cov.dims();
let dev = m.cov.device();
let eye_d = eye::<B>(d, &dev);
let var = (m.cov.clone() * eye_d.clone().unsqueeze::<3>())
.sum_dim(2)
.reshape([n, d])
.clamp_min(1e-12);
let sigma = var.clone().sqrt();
let mu = m.mean.clone();
let alpha = mu.clone() / sigma.clone();
let big_phi = alpha
.clone()
.mul_scalar(FRAC_1_SQRT_2)
.erf()
.add_scalar(1.0)
.mul_scalar(0.5);
let phi = (alpha.clone() * alpha.clone())
.mul_scalar(-0.5)
.exp()
.mul_scalar(1.0 / (2.0 * PI).sqrt());
let mu_out = mu.clone() * big_phi.clone() + sigma.clone() * phi.clone();
let var_out = ((mu.clone() * mu.clone() + var) * big_phi.clone() + mu * sigma * phi
- mu_out.clone() * mu_out.clone())
.clamp_min(0.0);
let g = big_phi;
let gg = g.clone().unsqueeze_dim::<3>(2) * g.unsqueeze_dim::<3>(1);
let off_mask = eye_d
.clone()
.mul_scalar(-1.0)
.add_scalar(1.0)
.unsqueeze::<3>();
let off = m.cov.clone() * gg * off_mask;
let diag = var_out.unsqueeze_dim::<3>(2).expand([n, d, d]) * eye_d.unsqueeze::<3>();
MomentsFull {
mean: mu_out,
cov: off + diag,
}
}
#[derive(Clone, Debug)]
pub struct Cauchy<B: Backend> {
pub location: Tensor<B, 2>,
pub scale: Tensor<B, 2>,
}
impl<B: Backend> Cauchy<B> {
pub fn new(location: Tensor<B, 2>, scale: Tensor<B, 2>) -> Self {
Self { location, scale }
}
pub fn interval_halfwidth(&self, p: f64) -> Tensor<B, 2> {
self.scale.clone().mul_scalar((PI * p / 2.0).tan())
}
}
pub fn propagate_linear_cauchy<B: Backend>(
c: &Cauchy<B>,
weight: Tensor<B, 2>,
bias: Option<Tensor<B, 1>>,
) -> Cauchy<B> {
let d_out = weight.dims()[1];
let mut location = c.location.clone().matmul(weight.clone());
if let Some(b) = bias {
location = location + b.reshape([1, d_out]);
}
let scale = c.scale.clone().matmul(weight.abs());
Cauchy { location, scale }
}
pub fn propagate_relu_cauchy<B: Backend>(c: &Cauchy<B>) -> Cauchy<B> {
let gate = c.location.clone().clamp_min(0.0).sign();
Cauchy {
location: c.location.clone().clamp_min(0.0),
scale: c.scale.clone() * gate,
}
}
#[cfg(test)]
mod tests {
use super::*;
use burn::tensor::Distribution;
use burn_ndarray::NdArray;
type B = NdArray<f32>;
fn mc_moments(samples: &[Vec<f64>], len: usize) -> (Vec<f64>, Vec<f64>) {
let k = samples.len() as f64;
let mut mean = vec![0.0; len];
for s in samples {
for i in 0..len {
mean[i] += s[i];
}
}
for m in mean.iter_mut() {
*m /= k;
}
let mut var = vec![0.0; len];
for s in samples {
for i in 0..len {
var[i] += (s[i] - mean[i]).powi(2);
}
}
for v in var.iter_mut() {
*v /= k - 1.0;
}
(mean, var)
}
#[test]
fn linear_variance_matches_monte_carlo() {
let dev = <B as Backend>::Device::default();
let (n, d_in, d_out, k) = (3usize, 5usize, 4usize, 40_000usize);
let std = 0.25f64;
let w = Tensor::<B, 2>::random([d_in, d_out], Distribution::Normal(0.0, 1.0), &dev);
let mean = Tensor::<B, 2>::random([n, d_in], Distribution::Normal(0.0, 1.0), &dev);
let var = Tensor::<B, 2>::full([n, d_in], std * std, &dev);
let out = propagate_linear(&Moments::new(mean.clone(), var), w.clone(), None);
let sdp_var = out.var.to_data().to_vec::<f32>().unwrap();
let len = n * d_out;
let mut samples = Vec::with_capacity(k);
for _ in 0..k {
let noise = Tensor::<B, 2>::random([n, d_in], Distribution::Normal(0.0, std), &dev);
let y = (mean.clone() + noise).matmul(w.clone());
samples.push(
y.to_data()
.to_vec::<f32>()
.unwrap()
.iter()
.map(|x| *x as f64)
.collect(),
);
}
let (_, mc_var) = mc_moments(&samples, len);
for i in 0..len {
let rel = (sdp_var[i] as f64 - mc_var[i]).abs() / mc_var[i].max(1e-9);
assert!(rel < 0.10, "output {i}: sdp={} mc={mc_var:?}", sdp_var[i]);
}
}
#[test]
fn relu_moments_match_monte_carlo() {
let dev = <B as Backend>::Device::default();
let (n, d, k) = (2usize, 3usize, 60_000usize);
let std = 0.8f64;
let mean = Tensor::<B, 2>::random([n, d], Distribution::Normal(0.0, 0.5), &dev);
let var = Tensor::<B, 2>::full([n, d], std * std, &dev);
let out = propagate_relu(&Moments::new(mean.clone(), var));
let sdp_mean = out.mean.to_data().to_vec::<f32>().unwrap();
let sdp_var = out.var.to_data().to_vec::<f32>().unwrap();
let len = n * d;
let mut samples = Vec::with_capacity(k);
for _ in 0..k {
let noise = Tensor::<B, 2>::random([n, d], Distribution::Normal(0.0, std), &dev);
let y = (mean.clone() + noise).clamp_min(0.0);
samples.push(
y.to_data()
.to_vec::<f32>()
.unwrap()
.iter()
.map(|x| *x as f64)
.collect(),
);
}
let (mc_mean, mc_var) = mc_moments(&samples, len);
for i in 0..len {
assert!(
(sdp_mean[i] as f64 - mc_mean[i]).abs() < 0.02,
"mean {i}: sdp={} mc={}",
sdp_mean[i],
mc_mean[i]
);
let rel = (sdp_var[i] as f64 - mc_var[i]).abs() / mc_var[i].max(1e-9);
assert!(rel < 0.08, "var {i}: sdp={} mc={}", sdp_var[i], mc_var[i]);
}
}
#[test]
fn full_cov_beats_diagonal_vs_monte_carlo() {
let dev = <B as Backend>::Device::default();
let (n, d_in, h, d_out, k) = (4usize, 5usize, 8usize, 3usize, 80_000usize);
let sig = 0.3f64;
let w1 = Tensor::<B, 2>::random([d_in, h], Distribution::Normal(0.0, 1.0), &dev);
let b1 = Tensor::<B, 1>::random([h], Distribution::Normal(0.0, 0.3), &dev);
let w2 = Tensor::<B, 2>::random([h, d_out], Distribution::Normal(0.0, 1.0), &dev);
let b2 = Tensor::<B, 1>::random([d_out], Distribution::Normal(0.0, 0.3), &dev);
let mean = Tensor::<B, 2>::random([n, d_in], Distribution::Normal(0.0, 1.0), &dev);
let var = Tensor::<B, 2>::full([n, d_in], sig * sig, &dev);
let m0 = MomentsFull::from_diagonal(mean.clone(), var.clone());
let m1 = propagate_relu_full(&propagate_linear_full(&m0, w1.clone(), Some(b1.clone())));
let m2 = propagate_linear_full(&m1, w2.clone(), Some(b2.clone()));
let f_var = m2.variance().to_data().to_vec::<f32>().unwrap();
let d0 = Moments::new(mean.clone(), var.clone());
let d1 = propagate_relu(&propagate_linear(&d0, w1.clone(), Some(b1.clone())));
let d2 = propagate_linear(&d1, w2.clone(), Some(b2.clone()));
let d_var = d2.var.to_data().to_vec::<f32>().unwrap();
let len = n * d_out;
let mut samples = Vec::with_capacity(k);
for _ in 0..k {
let noise = Tensor::<B, 2>::random([n, d_in], Distribution::Normal(0.0, sig), &dev);
let xk = mean.clone() + noise;
let hk = (xk.matmul(w1.clone()) + b1.clone().reshape([1, h])).clamp_min(0.0);
let yk = hk.matmul(w2.clone()) + b2.clone().reshape([1, d_out]);
samples.push(
yk.to_data()
.to_vec::<f32>()
.unwrap()
.iter()
.map(|x| *x as f64)
.collect(),
);
}
let (_, mc_var) = mc_moments(&samples, len);
let rel_err = |est: &[f32]| -> f64 {
(0..len)
.map(|i| (est[i] as f64 - mc_var[i]).abs() / mc_var[i].max(1e-9))
.sum::<f64>()
/ len as f64
};
let (fe, de) = (rel_err(&f_var), rel_err(&d_var));
assert!(fe < 0.12, "full-cov mean rel err {fe} vs MC too high");
assert!(
fe < de,
"full-cov ({fe}) should beat diagonal ({de}) against MC"
);
}
#[test]
fn cauchy_linear_exact_vs_monte_carlo() {
let dev = <B as Backend>::Device::default();
let (n, d_in, d_out, k) = (3usize, 4usize, 3usize, 40_000usize);
let loc = Tensor::<B, 2>::random([n, d_in], Distribution::Normal(0.0, 1.0), &dev);
let scale = Tensor::<B, 2>::full([n, d_in], 0.5, &dev);
let w = Tensor::<B, 2>::random([d_in, d_out], Distribution::Normal(0.0, 1.0), &dev);
let b = Tensor::<B, 1>::random([d_out], Distribution::Normal(0.0, 0.2), &dev);
let out = propagate_linear_cauchy(
&Cauchy::new(loc.clone(), scale.clone()),
w.clone(),
Some(b.clone()),
);
let p_loc = out.location.to_data().to_vec::<f32>().unwrap();
let p_scale = out.scale.to_data().to_vec::<f32>().unwrap();
let loc_v = loc.to_data().to_vec::<f32>().unwrap();
let scale_v = scale.to_data().to_vec::<f32>().unwrap();
let w_v = w.to_data().to_vec::<f32>().unwrap();
let b_v = b.to_data().to_vec::<f32>().unwrap();
let mut rng = 0x00C0_FFEE_u64;
let mut next = || {
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
((rng >> 11) as f64 + 1.0) / ((1u64 << 53) as f64 + 2.0)
};
let mut samples: Vec<Vec<f64>> = vec![Vec::with_capacity(k); n * d_out];
for _ in 0..k {
for i in 0..n {
let x: Vec<f64> = (0..d_in)
.map(|c| {
loc_v[i * d_in + c] as f64
+ scale_v[i * d_in + c] as f64 * (PI * (next() - 0.5)).tan()
})
.collect();
for j in 0..d_out {
let y = b_v[j] as f64
+ (0..d_in)
.map(|c| x[c] * w_v[c * d_out + j] as f64)
.sum::<f64>();
samples[i * d_out + j].push(y);
}
}
}
for idx in 0..n * d_out {
let s = &mut samples[idx];
s.sort_by(|a, b| a.partial_cmp(b).unwrap());
let med = s[k / 2];
let mc_scale = (s[3 * k / 4] - s[k / 4]) / 2.0;
assert!(
(p_loc[idx] as f64 - med).abs() < 0.08,
"loc {idx}: {} vs median {med}",
p_loc[idx]
);
let rel = (p_scale[idx] as f64 - mc_scale).abs() / mc_scale.max(1e-6);
assert!(
rel < 0.10,
"scale {idx}: {} vs half-IQR {mc_scale}",
p_scale[idx]
);
}
}
#[test]
fn leaky_relu_matches_monte_carlo() {
let dev = <B as Backend>::Device::default();
let (n, d, k) = (2usize, 4usize, 80_000usize);
let (alpha, std) = (0.1f64, 0.7f64);
let mean = Tensor::<B, 2>::random([n, d], Distribution::Normal(0.0, 0.5), &dev);
let var = Tensor::<B, 2>::full([n, d], std * std, &dev);
let out = propagate_leaky_relu(&Moments::new(mean.clone(), var.clone()), alpha);
let sm = out.mean.to_data().to_vec::<f32>().unwrap();
let sv = out.var.to_data().to_vec::<f32>().unwrap();
let len = n * d;
let mut samples = Vec::with_capacity(k);
for _ in 0..k {
let noise = Tensor::<B, 2>::random([n, d], Distribution::Normal(0.0, std), &dev);
let x = mean.clone() + noise;
let y = x.clone().clamp_min(0.0) + x.clamp_max(0.0).mul_scalar(alpha);
samples.push(
y.to_data()
.to_vec::<f32>()
.unwrap()
.iter()
.map(|v| *v as f64)
.collect(),
);
}
let (mc_mean, mc_var) = mc_moments(&samples, len);
for i in 0..len {
assert!(
(sm[i] as f64 - mc_mean[i]).abs() < 0.02,
"mean {i}: {} vs {}",
sm[i],
mc_mean[i]
);
let rel = (sv[i] as f64 - mc_var[i]).abs() / mc_var[i].max(1e-9);
assert!(rel < 0.08, "var {i}: {} vs {}", sv[i], mc_var[i]);
}
}
#[test]
fn residual_add_matches_monte_carlo_small_branch() {
let dev = <B as Backend>::Device::default();
let (n, d, h, k) = (3usize, 4usize, 8usize, 80_000usize);
let std = 0.5f64;
let w1 = Tensor::<B, 2>::random([d, h], Distribution::Normal(0.0, 0.07), &dev);
let b1 = Tensor::<B, 1>::random([h], Distribution::Normal(0.0, 0.1), &dev);
let w2 = Tensor::<B, 2>::random([h, d], Distribution::Normal(0.0, 0.07), &dev);
let b2 = Tensor::<B, 1>::random([d], Distribution::Normal(0.0, 0.1), &dev);
let mean = Tensor::<B, 2>::random([n, d], Distribution::Normal(0.0, 1.0), &dev);
let var = Tensor::<B, 2>::full([n, d], std * std, &dev);
let skip = Moments::new(mean.clone(), var.clone());
let branch = propagate_linear(
&propagate_relu(&propagate_linear(&skip, w1.clone(), Some(b1.clone()))),
w2.clone(),
Some(b2.clone()),
);
let res = propagate_residual_add(&skip, &branch);
let r_mean = res.mean.to_data().to_vec::<f32>().unwrap();
let r_var = res.var.to_data().to_vec::<f32>().unwrap();
let len = n * d;
let mut samples = Vec::with_capacity(k);
for _ in 0..k {
let noise = Tensor::<B, 2>::random([n, d], Distribution::Normal(0.0, std), &dev);
let x = mean.clone() + noise;
let br = (x.clone().matmul(w1.clone()) + b1.clone().reshape([1, h]))
.clamp_min(0.0)
.matmul(w2.clone())
+ b2.clone().reshape([1, d]);
let y = x + br;
samples.push(
y.to_data()
.to_vec::<f32>()
.unwrap()
.iter()
.map(|v| *v as f64)
.collect(),
);
}
let (mc_mean, mc_var) = mc_moments(&samples, len);
for i in 0..len {
assert!(
(r_mean[i] as f64 - mc_mean[i]).abs() < 0.03,
"mean {i}: {} vs {}",
r_mean[i],
mc_mean[i]
);
let rel = (r_var[i] as f64 - mc_var[i]).abs() / mc_var[i].max(1e-9);
assert!(
rel < 0.15,
"var {i}: {} vs {} (rel {rel})",
r_var[i],
mc_var[i]
);
}
}
#[test]
fn conv2d_variance_matches_monte_carlo() {
let dev = <B as Backend>::Device::default();
let (n, cin, hw, cout, ksz, k) = (2usize, 3usize, 6usize, 4usize, 3usize, 20_000usize);
let std = 0.3f64;
let opts = burn::tensor::ops::ConvOptions::new([1, 1], [0, 0], [1, 1], 1);
let weight =
Tensor::<B, 4>::random([cout, cin, ksz, ksz], Distribution::Normal(0.0, 0.4), &dev);
let bias = Tensor::<B, 1>::random([cout], Distribution::Normal(0.0, 0.2), &dev);
let mean = Tensor::<B, 4>::random([n, cin, hw, hw], Distribution::Normal(0.0, 1.0), &dev);
let var = Tensor::<B, 4>::full([n, cin, hw, hw], std * std, &dev);
let (_, var_out) = propagate_conv2d(
mean.clone(),
var,
weight.clone(),
Some(bias.clone()),
opts.clone(),
);
let p_var = var_out.to_data().to_vec::<f32>().unwrap();
let len = p_var.len();
let mut samples = Vec::with_capacity(k);
for _ in 0..k {
let noise =
Tensor::<B, 4>::random([n, cin, hw, hw], Distribution::Normal(0.0, std), &dev);
let y = burn::tensor::module::conv2d(
mean.clone() + noise,
weight.clone(),
Some(bias.clone()),
opts.clone(),
);
samples.push(
y.to_data()
.to_vec::<f32>()
.unwrap()
.iter()
.map(|v| *v as f64)
.collect(),
);
}
let (_, mc_var) = mc_moments(&samples, len);
for i in 0..len {
let rel = (p_var[i] as f64 - mc_var[i]).abs() / mc_var[i].max(1e-9);
assert!(rel < 0.10, "var {i}: {} vs {}", p_var[i], mc_var[i]);
}
}
}