use hessboost::config::MultiStrategy;
use hessboost::conformal::ConformalizedQuantile;
use hessboost::objective::distributional::{Dist, DistFamily, Distributional};
use hessboost::prelude::*;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use std::num::NonZeroUsize;
mod common;
use common::{coverage, lcg};
fn dataset(n: usize, seed: u64) -> Result<DMatrix> {
let mut next = lcg(seed);
let mut rng = StdRng::seed_from_u64(seed);
let noise = Dist::Normal {
mu: 0.0,
sigma: 1.0,
};
let mut x = Vec::with_capacity(2 * n);
let mut y = Vec::with_capacity(n);
for _ in 0..n {
let (x0, x1) = (next(), next());
let eps = noise.sample(|| rng.next_u64()) as f32;
x.extend_from_slice(&[x0, x1]);
y.push(2.0 * (std::f32::consts::TAU * x0).sin() + (0.1 + x1) * eps);
}
DMatrix::from_dense(&x, n, 2)?.with_labels(&y)
}
fn mean_nll(dists: &[Dist], data: &DMatrix) -> f64 {
let labels = data.labels().unwrap_or_default();
let total: f64 = dists
.iter()
.zip(labels)
.map(|(d, &y)| -d.log_prob(f64::from(y)))
.sum();
total / labels.len() as f64
}
fn summarize(intervals: &[(f64, f64)], data: &DMatrix) -> (f64, f64) {
coverage(intervals.iter().copied(), data.labels().unwrap_or_default())
}
fn main() -> Result<()> {
let dtrain = dataset(6000, 1)?;
let dvalid = dataset(2000, 2)?;
let dcal = dataset(2000, 3)?;
let dtest = dataset(6000, 4)?;
let fit_with = |objective: Objective, strategy: MultiStrategy| -> Result<BoostedModel> {
let params = TrainingParams::builder()
.objective(objective)
.tree_method(TreeMethod::Hist)
.max_depth(3)
.eta(0.1)
.multi_strategy(strategy)
.build()?;
Ok(Trainer::new(¶ms, &dtrain, 1000)
.eval(&dvalid, "valid")
.early_stopping_rounds(NonZeroUsize::new(20).unwrap())
.train()?
.model)
};
let fit = |objective: Objective| fit_with(objective, MultiStrategy::OneOutputPerTree);
let normal = Objective::Dist(Distributional::new(DistFamily::Normal));
let model = fit(normal.clone())?;
let dists = model.predict_distribution(&dtest, Iterations::Best)?;
println!(
"dist:normal: {} rounds, first test rows:",
model.best_iteration().map_or(0, |b| b + 1)
);
for d in &dists[..3] {
let (lo, hi) = d.interval(0.9);
println!(
" mean {:+.3} sd {:.3} 90% interval [{lo:+.3}, {hi:+.3}]",
d.mean(),
d.std_dev()
);
}
let point = fit(Objective::SquaredError(RegLoss::default()))?;
let fitted = point.predict(&dtrain, Iterations::Best)?.into_vec();
let labels = dtrain.labels().unwrap_or_default();
let sigma = (fitted
.iter()
.zip(labels)
.map(|(&p, &y)| f64::from(y - p).powi(2))
.sum::<f64>()
/ labels.len() as f64)
.sqrt();
let baseline: Vec<Dist> = point
.predict(&dtest, Iterations::Best)?
.into_vec()
.into_iter()
.map(|mu| Dist::Normal {
mu: f64::from(mu),
sigma,
})
.collect();
println!("\nheld-out mean NLL (lower is better):");
println!(" dist:normal {:.4}", mean_nll(&dists, &dtest));
println!(
" homoscedastic baseline {:.4}",
mean_nll(&baseline, &dtest)
);
let shared = fit_with(normal, MultiStrategy::MultiOutputTree)?;
println!(
" shared trees (PGB) {:.4} ({} trees vs {} for one tree per parameter)",
mean_nll(
&shared.predict_distribution(&dtest, Iterations::Best)?,
&dtest
),
shared.best_iteration().map_or(0, |b| b + 1),
2 * model.best_iteration().map_or(0, |b| b + 1),
);
println!("\n90% intervals on the test set (coverage, mean width):");
let raw: Vec<(f64, f64)> = dists.iter().map(|d| d.interval(0.9)).collect();
let (c, w) = summarize(&raw, &dtest);
println!(" predicted distribution {c:.3} {w:.3}");
let flat: Vec<(f64, f64)> = baseline.iter().map(|d| d.interval(0.9)).collect();
let (c, w) = summarize(&flat, &dtest);
println!(" homoscedastic baseline {c:.3} {w:.3}");
let cqr = ConformalizedQuantile::calibrate_distribution(&model, &dcal, 0.1)?;
let conformal: Vec<(f64, f64)> = cqr
.predict_interval(&dtest)?
.into_iter()
.map(|i| (f64::from(i.lower), f64::from(i.upper)))
.collect();
let (c, w) = summarize(&conformal, &dtest);
println!(
" conformalized (CQR) {c:.3} {w:.3} (correction {:+.4})",
cqr.correction()
);
Ok(())
}