use rand::SeedableRng;
use rill_ml::{
Metric, OnlineRegressor,
diagnostics::{
ModelHealthReport, OnlineModelSelector, PredictionReporter, SelectorConfig, TrainingSummary,
},
metrics::Mae,
models::{BaselineConfig, LinearRegression, LinearRegressionConfig, MeanRegressor},
optim::{Optimizer, SgdConfig},
pipeline::RegressionPipeline,
preprocessing::StandardScaler,
};
fn main() {
println!("=== RillML v0.2 Diagnostics Demo ===\n");
let feature_count = 2;
let scaler = StandardScaler::new(feature_count).unwrap();
let optimizer = Optimizer::sgd(
feature_count,
SgdConfig {
learning_rate: 0.05,
l2: 0.0,
},
)
.unwrap();
let regression = LinearRegression::new(
feature_count,
LinearRegressionConfig {
optimizer,
loss: Default::default(),
},
)
.unwrap();
let mut linear_pipeline: RegressionPipeline<StandardScaler, LinearRegression> =
RegressionPipeline::new(scaler, regression).unwrap();
let mut mean_baseline = MeanRegressor::new(BaselineConfig::default()).unwrap();
let mut selector = OnlineModelSelector::new(
&["MeanBaseline", "LinearRegression"],
SelectorConfig::default(),
)
.unwrap();
let mut reporter = PredictionReporter::default();
let mut summary = TrainingSummary::default();
let mut mae = Mae::default();
reporter.set_baseline(0.5).unwrap();
summary.set_baseline_error(0.5).unwrap();
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(42);
let n = 200;
for i in 0..n {
let x1 = rand::Rng::gen_range(&mut rng, 0.0..1.0);
let x2 = rand::Rng::gen_range(&mut rng, 0.0..1.0);
let noise = rand::Rng::gen_range(&mut rng, -0.05..0.05);
let y = 3.0 * x1 + 2.0 * x2 + noise;
let mean_pred = mean_baseline.predict(&[x1, x2]).unwrap();
let linear_pred = linear_pipeline.predict(&[x1, x2]).unwrap();
let linear_abs_err = (y - linear_pred).abs();
mae.update(y, linear_pred).unwrap();
summary.record_sample().unwrap();
summary.record_error(linear_abs_err).unwrap();
selector
.record(0, y, mean_pred)
.expect("record mean baseline");
selector
.record(1, y, linear_pred)
.expect("record linear model");
reporter.observe(linear_pred, y).unwrap();
mean_baseline.learn(&[x1, x2], y).unwrap();
linear_pipeline.learn(&[x1, x2], y).unwrap();
if (i + 1) % 50 == 0 {
selector.select();
let report = reporter.report(linear_pred).unwrap();
println!("--- step {} ---", i + 1);
println!(
" best model: {} (switches: {})",
selector.current_best_name().unwrap_or("(none yet)"),
selector.switch_count()
);
println!(
" warmup state: {:?}, confidence: {:?}",
report.warmup_state(),
report.confidence()
);
if let (Some(lo), Some(hi)) = (report.lower_bound(), report.upper_bound()) {
println!(
" prediction: {:.4}, interval: [{:.4}, {:.4}]",
report.prediction(),
lo,
hi
);
} else {
println!(
" prediction: {:.4}, interval: (insufficient data)",
report.prediction()
);
}
println!(
" recent error: {:?}, beats baseline: {:?}",
report.recent_error(),
report.beats_baseline()
);
}
}
println!("\n=== Final Summary ===");
println!("Total samples: {}", summary.total_samples());
println!("Final MAE: {:.6}", mae.value().unwrap());
println!(
"Best model: {} (switches: {})",
selector.current_best_name().unwrap_or("(none)"),
selector.switch_count()
);
println!(
"Recent error: {:?}, best error: {:?}",
summary.recent_error(),
summary.best_error()
);
let linear = linear_pipeline.model();
let health = ModelHealthReport::from_parameters(linear.weights(), Some(linear.intercept()));
println!("\n=== Linear Model Health ===");
println!("Parameter count: {}", health.parameter_count());
println!(
"Weight range: [{:?}, {:?}]",
health.weight_min(),
health.weight_max()
);
println!(
"Has NaN: {}, Has Infinity: {}",
health.has_nan(),
health.has_infinity()
);
println!("Healthy: {}", health.is_healthy());
println!("State size: {} bytes", health.state_size_bytes());
println!(
"\nLearned weights: [{:.4}, {:.4}], intercept: {:.4}",
linear.weights()[0],
linear.weights()[1],
linear.intercept()
);
}