use burn::backend::Autodiff;
use burn::module::Module;
use burn::nn::loss::{MseLoss, Reduction};
use burn::nn::{Linear, LinearConfig};
use burn::optim::{AdamConfig, GradientsParams, Optimizer};
use burn::tensor::backend::Backend;
use burn::tensor::{activation, Distribution, Tensor, TensorData};
use burn_ndarray::NdArray;
use stableprop::burn_sdp::{propagate_linear, propagate_relu, Moments};
type Ad = Autodiff<NdArray<f32>>;
const D_IN: usize = 6;
const HIDDEN: usize = 64;
const N_TRAIN: usize = 3000;
const N_TEST: usize = 1000;
const TRAIN_STD: f64 = 0.2;
const TEST_STD: f64 = 0.3;
#[derive(Module, Debug)]
struct Mlp<B: Backend> {
lin1: Linear<B>,
lin2: Linear<B>,
}
impl<B: Backend> Mlp<B> {
fn init(device: &B::Device) -> Self {
Self {
lin1: LinearConfig::new(D_IN, HIDDEN).init(device),
lin2: LinearConfig::new(HIDDEN, 1).init(device),
}
}
fn forward(&self, x: Tensor<B, 2>) -> Tensor<B, 2> {
self.lin2.forward(activation::relu(self.lin1.forward(x)))
}
fn forward_with_var(&self, x: Tensor<B, 2>, std: f64) -> (Tensor<B, 2>, Tensor<B, 2>) {
let [n, d] = x.dims();
let var0 = Tensor::<B, 2>::full([n, d], std * std, &x.device());
let w1 = self.lin1.weight.val();
let b1 = self.lin1.bias.as_ref().map(|p| p.val());
let w2 = self.lin2.weight.val();
let b2 = self.lin2.bias.as_ref().map(|p| p.val());
let m1 = propagate_relu(&propagate_linear(&Moments::new(x.clone(), var0), w1, b1));
let m2 = propagate_linear(&m1, w2, b2);
(self.forward(x), m2.var)
}
}
fn target(x: &[f32]) -> f32 {
let s: f32 = x.iter().sum();
(s * 0.6).sin() + 0.5 * x[0] * x[1] - 0.3 * x[2] * x[2]
}
fn main() {
let dev = <Ad as Backend>::Device::default();
let make = |n: usize| -> (Tensor<Ad, 2>, Tensor<Ad, 2>) {
let xt = Tensor::<Ad, 2>::random([n, D_IN], Distribution::Normal(0.0, 1.0), &dev);
let xv = xt.to_data().to_vec::<f32>().unwrap();
let yv: Vec<f32> = (0..n)
.map(|i| target(&xv[i * D_IN..(i + 1) * D_IN]))
.collect();
(xt, Tensor::from_data(TensorData::new(yv, [n, 1]), &dev))
};
let (x_tr, y_tr) = make(N_TRAIN);
let (x_te, y_te) = make(N_TEST);
let init = Mlp::<Ad>::init(&dev);
let train = |mut model: Mlp<Ad>, lambda: f64| -> Mlp<Ad> {
let mut optim = AdamConfig::new().init();
for _ in 0..800 {
let (pred, var) = model.forward_with_var(x_tr.clone(), TRAIN_STD);
let mut loss = MseLoss::new().forward(pred, y_tr.clone(), Reduction::Mean);
if lambda > 0.0 {
loss = loss + var.mean().mul_scalar(lambda);
}
let grads = GradientsParams::from_grads(loss.backward(), &model);
model = optim.step(1e-3, model, grads);
}
model
};
let plain = train(init.clone(), 0.0);
let robust = train(init, 3.0);
let clean = vec![x_te.clone()];
let noisy: Vec<Tensor<Ad, 2>> = (0..20)
.map(|_| {
x_te.clone()
+ Tensor::<Ad, 2>::random([N_TEST, D_IN], Distribution::Normal(0.0, TEST_STD), &dev)
})
.collect();
let y = y_te.into_data().to_vec::<f32>().unwrap();
let rmse = |model: &Mlp<Ad>, inputs: &[Tensor<Ad, 2>]| -> f64 {
let mut total = 0.0;
for x in inputs {
let p = model
.forward(x.clone())
.into_data()
.to_vec::<f32>()
.unwrap();
total += (0..N_TEST)
.map(|i| (p[i] - y[i]).powi(2) as f64)
.sum::<f64>()
/ N_TEST as f64;
}
(total / inputs.len() as f64).sqrt()
};
println!("RMSE (lower = better), shared init + shared test noise:");
println!(" {:<28} {:>8} {:>8}", "net", "clean", "noisy");
println!(
" {:<28} {:>8.4} {:>8.4}",
"plain MSE",
rmse(&plain, &clean),
rmse(&plain, &noisy)
);
println!(
" {:<28} {:>8.4} {:>8.4}",
"MSE + variance penalty",
rmse(&robust, &clean),
rmse(&robust, &noisy)
);
println!("\nthe penalized net trades clean accuracy for lower error under input noise.");
}