0cae3b4227
Compute per-parameter importance scores by measuring the absolute Spearman rank correlation between each parameter's values and the objective across completed trials. Scores are normalized to sum to 1.0 and returned sorted by descending importance.
167 lines
5.2 KiB
Rust
167 lines
5.2 KiB
Rust
use optimizer::parameter::{CategoricalParam, FloatParam, IntParam, Parameter};
|
|
use optimizer::{Direction, Study};
|
|
|
|
#[test]
|
|
fn known_perfect_correlation() {
|
|
let study: Study<f64> = Study::new(Direction::Minimize);
|
|
let x = FloatParam::new(0.0, 100.0).name("x");
|
|
|
|
// Objective = x, so perfect correlation.
|
|
for _ in 0..30 {
|
|
let mut trial = study.ask();
|
|
let xv = x.suggest(&mut trial).unwrap();
|
|
study.tell(trial, Ok::<_, &str>(xv));
|
|
}
|
|
|
|
let importance = study.param_importance();
|
|
assert_eq!(importance.len(), 1);
|
|
assert_eq!(importance[0].0, "x");
|
|
assert!(
|
|
(importance[0].1 - 1.0).abs() < 1e-10,
|
|
"single param should be 1.0"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_effect_parameter() {
|
|
let study: Study<f64> = Study::new(Direction::Minimize);
|
|
let x = FloatParam::new(0.0, 100.0).name("x");
|
|
let noise = FloatParam::new(0.0, 100.0).name("noise");
|
|
|
|
// Objective depends only on x; noise is unused in objective.
|
|
for _ in 0..50 {
|
|
let mut trial = study.ask();
|
|
let xv = x.suggest(&mut trial).unwrap();
|
|
let _nv = noise.suggest(&mut trial).unwrap();
|
|
study.tell(trial, Ok::<_, &str>(xv));
|
|
}
|
|
|
|
let importance = study.param_importance();
|
|
assert_eq!(importance.len(), 2);
|
|
// x should have much higher importance than noise.
|
|
let x_score = importance.iter().find(|(l, _)| l == "x").unwrap().1;
|
|
let noise_score = importance.iter().find(|(l, _)| l == "noise").unwrap().1;
|
|
assert!(
|
|
x_score > noise_score,
|
|
"x ({x_score}) should outrank noise ({noise_score})"
|
|
);
|
|
// x should dominate
|
|
assert!(x_score > 0.7, "x importance {x_score} should be dominant");
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_parameters_varying_importance() {
|
|
let study: Study<f64> = Study::new(Direction::Minimize);
|
|
let x = FloatParam::new(0.0, 10.0).name("x");
|
|
let y = FloatParam::new(0.0, 10.0).name("y");
|
|
|
|
// Objective = 10*x + 0.01*y → x should be far more important.
|
|
for _ in 0..50 {
|
|
let mut trial = study.ask();
|
|
let xv = x.suggest(&mut trial).unwrap();
|
|
let yv = y.suggest(&mut trial).unwrap();
|
|
study.tell(trial, Ok::<_, &str>(10.0 * xv + 0.01 * yv));
|
|
}
|
|
|
|
let importance = study.param_importance();
|
|
assert_eq!(importance.len(), 2);
|
|
assert_eq!(importance[0].0, "x", "x should rank first");
|
|
assert!(importance[0].1 > importance[1].1);
|
|
}
|
|
|
|
#[test]
|
|
fn fewer_than_two_trials_returns_empty() {
|
|
let study: Study<f64> = Study::new(Direction::Minimize);
|
|
|
|
// 0 trials
|
|
assert!(study.param_importance().is_empty());
|
|
|
|
// 1 trial
|
|
let x = FloatParam::new(0.0, 1.0).name("x");
|
|
let mut trial = study.ask();
|
|
let xv = x.suggest(&mut trial).unwrap();
|
|
study.tell(trial, Ok::<_, &str>(xv));
|
|
assert!(study.param_importance().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn int_parameter() {
|
|
let study: Study<f64> = Study::new(Direction::Minimize);
|
|
let n = IntParam::new(1, 100).name("n");
|
|
|
|
for _ in 0..30 {
|
|
let mut trial = study.ask();
|
|
let nv = n.suggest(&mut trial).unwrap();
|
|
study.tell(trial, Ok::<_, &str>(nv as f64));
|
|
}
|
|
|
|
let importance = study.param_importance();
|
|
assert_eq!(importance.len(), 1);
|
|
assert_eq!(importance[0].0, "n");
|
|
assert!(importance[0].1 > 0.9);
|
|
}
|
|
|
|
#[test]
|
|
fn categorical_parameter() {
|
|
let study: Study<f64> = Study::new(Direction::Minimize);
|
|
let cat = CategoricalParam::new(vec!["a", "b", "c"]).name("cat");
|
|
let x = FloatParam::new(0.0, 100.0).name("x");
|
|
|
|
// Objective depends only on x; categorical is random noise.
|
|
for _ in 0..50 {
|
|
let mut trial = study.ask();
|
|
let _c = cat.suggest(&mut trial).unwrap();
|
|
let xv = x.suggest(&mut trial).unwrap();
|
|
study.tell(trial, Ok::<_, &str>(xv));
|
|
}
|
|
|
|
let importance = study.param_importance();
|
|
assert_eq!(importance.len(), 2);
|
|
let x_score = importance.iter().find(|(l, _)| l == "x").unwrap().1;
|
|
assert!(x_score > 0.5, "x should dominate over categorical noise");
|
|
}
|
|
|
|
#[test]
|
|
fn normalization_sums_to_one() {
|
|
let study: Study<f64> = Study::new(Direction::Minimize);
|
|
let x = FloatParam::new(0.0, 10.0).name("x");
|
|
let y = FloatParam::new(0.0, 10.0).name("y");
|
|
let z = FloatParam::new(0.0, 10.0).name("z");
|
|
|
|
for _ in 0..50 {
|
|
let mut trial = study.ask();
|
|
let xv = x.suggest(&mut trial).unwrap();
|
|
let yv = y.suggest(&mut trial).unwrap();
|
|
let zv = z.suggest(&mut trial).unwrap();
|
|
study.tell(trial, Ok::<_, &str>(xv + 0.5 * yv + 0.1 * zv));
|
|
}
|
|
|
|
let importance = study.param_importance();
|
|
let sum: f64 = importance.iter().map(|(_, s)| *s).sum();
|
|
assert!(
|
|
(sum - 1.0).abs() < 1e-10,
|
|
"scores should sum to 1.0, got {sum}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn label_when_unnamed_uses_debug() {
|
|
let study: Study<f64> = Study::new(Direction::Minimize);
|
|
// No .name() call → label defaults to Debug representation.
|
|
let x = FloatParam::new(0.0, 10.0);
|
|
|
|
for _ in 0..10 {
|
|
let mut trial = study.ask();
|
|
let xv = x.suggest(&mut trial).unwrap();
|
|
study.tell(trial, Ok::<_, &str>(xv));
|
|
}
|
|
|
|
let importance = study.param_importance();
|
|
assert_eq!(importance.len(), 1);
|
|
assert!(
|
|
importance[0].0.starts_with("FloatParam"),
|
|
"expected Debug label, got {:?}",
|
|
importance[0].0
|
|
);
|
|
}
|