refactor(examples): split multi-concept examples into focused single-topic files
- Split pruning_and_callbacks into pruning and early_stopping - Split advanced_features into async_parallel, journal_storage, ask_and_tell, multi_objective - Each example now requires only its own feature flag - Trim sampler_comparison winner logic and verbose header - Update CI workflow and README to match new example names
This commit is contained in:
@@ -66,14 +66,22 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Run basic_optimization
|
||||
run: cargo run --example basic_optimization
|
||||
- name: Run sampler_comparison
|
||||
run: cargo run --example sampler_comparison
|
||||
- name: Run pruning_and_callbacks
|
||||
run: cargo run --example pruning_and_callbacks
|
||||
- name: Run parameter_types
|
||||
run: cargo run --example parameter_types --features derive
|
||||
- name: Run advanced_features
|
||||
run: cargo run --example advanced_features --features "async,journal"
|
||||
- name: Run sampler_comparison
|
||||
run: cargo run --example sampler_comparison
|
||||
- name: Run pruning
|
||||
run: cargo run --example pruning
|
||||
- name: Run early_stopping
|
||||
run: cargo run --example early_stopping
|
||||
- name: Run async_parallel
|
||||
run: cargo run --example async_parallel --features async
|
||||
- name: Run journal_storage
|
||||
run: cargo run --example journal_storage --features journal
|
||||
- name: Run ask_and_tell
|
||||
run: cargo run --example ask_and_tell
|
||||
- name: Run multi_objective
|
||||
run: cargo run --example multi_objective
|
||||
|
||||
docs:
|
||||
name: Docs
|
||||
|
||||
+22
-5
@@ -67,13 +67,30 @@ name = "sampler_comparison"
|
||||
path = "examples/sampler_comparison.rs"
|
||||
|
||||
[[example]]
|
||||
name = "pruning_and_callbacks"
|
||||
path = "examples/pruning_and_callbacks.rs"
|
||||
name = "pruning"
|
||||
path = "examples/pruning.rs"
|
||||
|
||||
[[example]]
|
||||
name = "advanced_features"
|
||||
path = "examples/advanced_features.rs"
|
||||
required-features = ["async", "journal"]
|
||||
name = "early_stopping"
|
||||
path = "examples/early_stopping.rs"
|
||||
|
||||
[[example]]
|
||||
name = "async_parallel"
|
||||
path = "examples/async_parallel.rs"
|
||||
required-features = ["async"]
|
||||
|
||||
[[example]]
|
||||
name = "journal_storage"
|
||||
path = "examples/journal_storage.rs"
|
||||
required-features = ["journal"]
|
||||
|
||||
[[example]]
|
||||
name = "ask_and_tell"
|
||||
path = "examples/ask_and_tell.rs"
|
||||
|
||||
[[example]]
|
||||
name = "multi_objective"
|
||||
path = "examples/multi_objective.rs"
|
||||
|
||||
[[test]]
|
||||
name = "journal_tests"
|
||||
|
||||
@@ -52,11 +52,15 @@ println!("Best x = {:.4}, f(x) = {:.4}", best.get(&x).unwrap(), best.value);
|
||||
## Examples
|
||||
|
||||
```sh
|
||||
cargo run --example basic_optimization # Minimize a quadratic — simplest possible usage
|
||||
cargo run --example sampler_comparison # Compare Random, TPE, and Grid on the same problem
|
||||
cargo run --example pruning_and_callbacks # Trial pruning with MedianPruner + early stopping
|
||||
cargo run --example parameter_types --features derive # All 5 param types + #[derive(Categorical)]
|
||||
cargo run --example advanced_features --features async,journal # Async, journal storage, ask-and-tell, multi-objective
|
||||
cargo run --example basic_optimization # Minimize a quadratic — simplest possible usage
|
||||
cargo run --example parameter_types --features derive # All 5 param types + #[derive(Categorical)]
|
||||
cargo run --example sampler_comparison # Compare Random, TPE, and Grid on the same problem
|
||||
cargo run --example pruning # Trial pruning with MedianPruner
|
||||
cargo run --example early_stopping # Halt a study when a target is reached
|
||||
cargo run --example async_parallel --features async # Evaluate trials concurrently with tokio
|
||||
cargo run --example journal_storage --features journal # Persist trials to disk and resume later
|
||||
cargo run --example ask_and_tell # Decouple sampling from evaluation
|
||||
cargo run --example multi_objective # Optimize competing objectives + Pareto front
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
@@ -1,277 +0,0 @@
|
||||
//! Advanced Features Example
|
||||
//!
|
||||
//! This example demonstrates four advanced capabilities of the optimizer crate:
|
||||
//!
|
||||
//! 1. **Async parallel optimization** — evaluate multiple trials concurrently
|
||||
//! 2. **Journal storage** — persist trials to disk and resume studies later
|
||||
//! 3. **Ask-and-tell interface** — decouple sampling from evaluation
|
||||
//! 4. **Multi-objective optimization** — optimize competing objectives simultaneously
|
||||
//!
|
||||
//! Run with: `cargo run --example advanced_features --features "async,journal"`
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use optimizer::multi_objective::MultiObjectiveStudy;
|
||||
use optimizer::prelude::*;
|
||||
|
||||
// ============================================================================
|
||||
// Section 1: Async Parallel Optimization
|
||||
// ============================================================================
|
||||
|
||||
/// Runs multiple trials concurrently using tokio, reducing wall-clock time
|
||||
/// when the objective function involves I/O or other async work.
|
||||
async fn async_parallel_optimization() -> optimizer::Result<()> {
|
||||
println!("=== Section 1: Async Parallel Optimization ===\n");
|
||||
|
||||
let sampler = TpeSampler::builder()
|
||||
.n_startup_trials(5)
|
||||
.seed(42)
|
||||
.build()
|
||||
.expect("Failed to build TPE sampler");
|
||||
|
||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
|
||||
let x = FloatParam::new(-5.0, 5.0).name("x");
|
||||
let y = FloatParam::new(-5.0, 5.0).name("y");
|
||||
|
||||
let n_trials = 30;
|
||||
let concurrency = 4;
|
||||
|
||||
println!("Running {n_trials} trials with {concurrency} concurrent workers...");
|
||||
let start = Instant::now();
|
||||
|
||||
// optimize_parallel spawns up to `concurrency` trials at once.
|
||||
// The closure must take ownership of Trial and return (Trial, value).
|
||||
study
|
||||
.optimize_parallel(n_trials, concurrency, {
|
||||
let x = x.clone();
|
||||
let y = y.clone();
|
||||
move |mut trial| {
|
||||
let x = x.clone();
|
||||
let y = y.clone();
|
||||
async move {
|
||||
let xv = x.suggest(&mut trial)?;
|
||||
let yv = y.suggest(&mut trial)?;
|
||||
|
||||
// Simulate async I/O (e.g., calling an external service)
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
|
||||
// Sphere function: minimum at origin
|
||||
let value = xv * xv + yv * yv;
|
||||
Ok::<_, optimizer::Error>((trial, value))
|
||||
}
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
let best = study.best_trial()?;
|
||||
|
||||
println!(
|
||||
"Completed in {elapsed:.2?} (vs ~{:.0?} sequential)",
|
||||
std::time::Duration::from_millis(10 * n_trials as u64)
|
||||
);
|
||||
println!(
|
||||
"Best: f({:.3}, {:.3}) = {:.6}\n",
|
||||
best.get(&x).unwrap(),
|
||||
best.get(&y).unwrap(),
|
||||
best.value
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Section 2: Journal Storage
|
||||
// ============================================================================
|
||||
|
||||
/// Persists trials to a JSONL file so that a study can be resumed later.
|
||||
/// Useful for long-running experiments or crash recovery.
|
||||
fn journal_storage_demo() -> optimizer::Result<()> {
|
||||
println!("=== Section 2: Journal Storage ===\n");
|
||||
|
||||
let path = std::env::temp_dir().join("optimizer_advanced_example.jsonl");
|
||||
|
||||
// Clean up from any previous run
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
let x = FloatParam::new(-5.0, 5.0).name("x");
|
||||
|
||||
// --- First run: optimize 20 trials and persist to disk ---
|
||||
{
|
||||
let storage = JournalStorage::<f64>::new(&path);
|
||||
let study: Study<f64> = Study::builder()
|
||||
.minimize()
|
||||
.sampler(TpeSampler::new())
|
||||
.storage(storage)
|
||||
.build();
|
||||
|
||||
study.optimize(20, |trial| {
|
||||
let xv = x.suggest(trial)?;
|
||||
Ok::<_, optimizer::Error>(xv * xv)
|
||||
})?;
|
||||
|
||||
println!(
|
||||
"First run: {} trials saved to {}",
|
||||
study.n_trials(),
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
|
||||
// --- Second run: resume from the journal file ---
|
||||
{
|
||||
// JournalStorage::open loads existing trials from disk
|
||||
let storage = JournalStorage::<f64>::open(&path)?;
|
||||
let study: Study<f64> = Study::builder()
|
||||
.minimize()
|
||||
.sampler(TpeSampler::new())
|
||||
.storage(storage)
|
||||
.build();
|
||||
|
||||
// The sampler sees the prior 20 trials, so it starts informed
|
||||
let before = study.n_trials();
|
||||
study.optimize(10, |trial| {
|
||||
let xv = x.suggest(trial)?;
|
||||
Ok::<_, optimizer::Error>(xv * xv)
|
||||
})?;
|
||||
|
||||
let best = study.best_trial()?;
|
||||
println!(
|
||||
"Resumed: {} → {} trials, best f({:.4}) = {:.6}",
|
||||
before,
|
||||
study.n_trials(),
|
||||
best.get(&x).unwrap(),
|
||||
best.value
|
||||
);
|
||||
}
|
||||
|
||||
// Clean up the temporary file
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Section 3: Ask-and-Tell Interface
|
||||
// ============================================================================
|
||||
|
||||
/// Decouples trial creation from evaluation. Useful when:
|
||||
/// - Evaluations happen outside the optimizer (e.g., in a separate process)
|
||||
/// - You want to batch evaluations before reporting results
|
||||
/// - You need custom scheduling logic
|
||||
fn ask_and_tell_demo() -> optimizer::Result<()> {
|
||||
println!("=== Section 3: Ask-and-Tell Interface ===\n");
|
||||
|
||||
let study: Study<f64> = Study::new(Direction::Minimize);
|
||||
|
||||
let x = FloatParam::new(-5.0, 5.0).name("x");
|
||||
let y = FloatParam::new(-5.0, 5.0).name("y");
|
||||
|
||||
// Ask for a batch of trials, evaluate externally, then tell results
|
||||
for batch in 0..3 {
|
||||
let batch_size = 5;
|
||||
let mut trials = Vec::with_capacity(batch_size);
|
||||
|
||||
// ask() creates trials with sampled parameters
|
||||
for _ in 0..batch_size {
|
||||
let mut trial = study.ask();
|
||||
let xv = x.suggest(&mut trial)?;
|
||||
let yv = y.suggest(&mut trial)?;
|
||||
|
||||
// Store values alongside the trial for later evaluation
|
||||
trials.push((trial, xv, yv));
|
||||
}
|
||||
|
||||
// Evaluate the batch (could be sent to workers, GPUs, etc.)
|
||||
for (trial, xv, yv) in trials {
|
||||
let value = xv * xv + yv * yv;
|
||||
// tell() reports the result back to the study
|
||||
study.tell(trial, Ok::<_, &str>(value));
|
||||
}
|
||||
|
||||
println!(
|
||||
"Batch {}: evaluated {} trials (total: {})",
|
||||
batch + 1,
|
||||
batch_size,
|
||||
study.n_trials()
|
||||
);
|
||||
}
|
||||
|
||||
let best = study.best_trial()?;
|
||||
println!(
|
||||
"Best: f({:.3}, {:.3}) = {:.6}\n",
|
||||
best.get(&x).unwrap(),
|
||||
best.get(&y).unwrap(),
|
||||
best.value
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Section 4: Multi-Objective Optimization
|
||||
// ============================================================================
|
||||
|
||||
/// Optimizes two competing objectives simultaneously.
|
||||
/// Returns the Pareto front — the set of solutions where no objective can
|
||||
/// be improved without worsening the other.
|
||||
fn multi_objective_demo() -> optimizer::Result<()> {
|
||||
println!("=== Section 4: Multi-Objective Optimization ===\n");
|
||||
|
||||
// Two objectives, both minimized
|
||||
let study = MultiObjectiveStudy::new(vec![Direction::Minimize, Direction::Minimize]);
|
||||
|
||||
let x = FloatParam::new(0.0, 1.0).name("x");
|
||||
|
||||
// Classic bi-objective problem: f1(x) = x², f2(x) = (x - 1)²
|
||||
// The Pareto front is the curve where improving f1 worsens f2 and vice versa.
|
||||
study.optimize(50, |trial| {
|
||||
let xv = x.suggest(trial)?;
|
||||
let f1 = xv * xv;
|
||||
let f2 = (xv - 1.0) * (xv - 1.0);
|
||||
Ok::<_, optimizer::Error>(vec![f1, f2])
|
||||
})?;
|
||||
|
||||
let front = study.pareto_front();
|
||||
println!(
|
||||
"Ran {} trials, Pareto front has {} solutions:",
|
||||
study.n_trials(),
|
||||
front.len()
|
||||
);
|
||||
|
||||
// Show a few Pareto-optimal trade-offs
|
||||
let mut sorted_front = front.clone();
|
||||
sorted_front.sort_by(|a, b| a.values[0].partial_cmp(&b.values[0]).unwrap());
|
||||
|
||||
for (i, trial) in sorted_front.iter().take(5).enumerate() {
|
||||
println!(
|
||||
" {}: x={:.3}, f1={:.4}, f2={:.4}",
|
||||
i + 1,
|
||||
trial.get(&x).unwrap(),
|
||||
trial.values[0],
|
||||
trial.values[1]
|
||||
);
|
||||
}
|
||||
if sorted_front.len() > 5 {
|
||||
println!(" ... and {} more", sorted_front.len() - 5);
|
||||
}
|
||||
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> optimizer::Result<()> {
|
||||
async_parallel_optimization().await?;
|
||||
journal_storage_demo()?;
|
||||
ask_and_tell_demo()?;
|
||||
multi_objective_demo()?;
|
||||
|
||||
println!("All sections completed successfully!");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
//! Ask-and-tell interface — decouple sampling from evaluation.
|
||||
//!
|
||||
//! Use `ask()` to get a trial with sampled parameters, evaluate it however
|
||||
//! you like (workers, GPUs, external processes), then `tell()` the result.
|
||||
//! This is useful for batch evaluation or custom scheduling.
|
||||
//!
|
||||
//! Run with: `cargo run --example ask_and_tell`
|
||||
|
||||
use optimizer::prelude::*;
|
||||
|
||||
fn main() -> optimizer::Result<()> {
|
||||
let study: Study<f64> = Study::new(Direction::Minimize);
|
||||
|
||||
let x = FloatParam::new(-5.0, 5.0).name("x");
|
||||
let y = FloatParam::new(-5.0, 5.0).name("y");
|
||||
|
||||
for batch in 0..3 {
|
||||
let batch_size = 5;
|
||||
let mut trials = Vec::with_capacity(batch_size);
|
||||
|
||||
// ask() creates trials with sampled parameters
|
||||
for _ in 0..batch_size {
|
||||
let mut trial = study.ask();
|
||||
let xv = x.suggest(&mut trial)?;
|
||||
let yv = y.suggest(&mut trial)?;
|
||||
trials.push((trial, xv, yv));
|
||||
}
|
||||
|
||||
// Evaluate the batch (could be sent to workers, GPUs, etc.)
|
||||
for (trial, xv, yv) in trials {
|
||||
let value = xv * xv + yv * yv;
|
||||
study.tell(trial, Ok::<_, &str>(value));
|
||||
}
|
||||
|
||||
println!(
|
||||
"Batch {}: evaluated {batch_size} trials (total: {})",
|
||||
batch + 1,
|
||||
study.n_trials(),
|
||||
);
|
||||
}
|
||||
|
||||
let best = study.best_trial()?;
|
||||
println!(
|
||||
"Best: f({:.3}, {:.3}) = {:.6}",
|
||||
best.get(&x).unwrap(),
|
||||
best.get(&y).unwrap(),
|
||||
best.value,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//! Async parallel optimization — evaluate multiple trials concurrently.
|
||||
//!
|
||||
//! Uses `optimize_parallel` with tokio to run several trials at once,
|
||||
//! reducing wall-clock time when the objective involves I/O or async work.
|
||||
//!
|
||||
//! Run with: `cargo run --example async_parallel --features async`
|
||||
|
||||
use optimizer::prelude::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> optimizer::Result<()> {
|
||||
let study: Study<f64> = Study::minimize(TpeSampler::new());
|
||||
|
||||
let x = FloatParam::new(-5.0, 5.0).name("x");
|
||||
let y = FloatParam::new(-5.0, 5.0).name("y");
|
||||
|
||||
let n_trials = 30;
|
||||
let concurrency = 4;
|
||||
|
||||
println!("Running {n_trials} trials with {concurrency} concurrent workers...");
|
||||
|
||||
study
|
||||
.optimize_parallel(n_trials, concurrency, {
|
||||
let x = x.clone();
|
||||
let y = y.clone();
|
||||
move |mut trial| {
|
||||
let x = x.clone();
|
||||
let y = y.clone();
|
||||
async move {
|
||||
let xv = x.suggest(&mut trial)?;
|
||||
let yv = y.suggest(&mut trial)?;
|
||||
|
||||
// Simulate async I/O (e.g. calling an external service)
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
|
||||
let value = xv * xv + yv * yv;
|
||||
Ok::<_, optimizer::Error>((trial, value))
|
||||
}
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
let best = study.best_trial()?;
|
||||
println!(
|
||||
"Best: f({:.3}, {:.3}) = {:.6}",
|
||||
best.get(&x).unwrap(),
|
||||
best.get(&y).unwrap(),
|
||||
best.value,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//! Early stopping — halt an entire study once a target is reached.
|
||||
//!
|
||||
//! Use `optimize_with_callback` to inspect each completed trial and return
|
||||
//! `ControlFlow::Break(())` when the study should stop (e.g. a quality
|
||||
//! threshold is met or a time budget is exhausted).
|
||||
//!
|
||||
//! Run with: `cargo run --example early_stopping`
|
||||
|
||||
use std::ops::ControlFlow;
|
||||
|
||||
use optimizer::prelude::*;
|
||||
|
||||
fn main() -> optimizer::Result<()> {
|
||||
let study: Study<f64> = Study::new(Direction::Minimize);
|
||||
let x = FloatParam::new(-10.0, 10.0).name("x");
|
||||
|
||||
let target = 0.01;
|
||||
|
||||
study.optimize_with_callback(
|
||||
100, // upper bound — we expect to stop much earlier
|
||||
|trial| {
|
||||
let xv = x.suggest(trial)?;
|
||||
Ok::<_, Error>((xv - 3.0).powi(2))
|
||||
},
|
||||
|_study, completed| {
|
||||
if completed.value < target {
|
||||
println!("Target {target} reached at trial #{}", completed.id);
|
||||
return ControlFlow::Break(());
|
||||
}
|
||||
ControlFlow::Continue(())
|
||||
},
|
||||
)?;
|
||||
|
||||
let best = study.best_trial()?;
|
||||
println!(
|
||||
"Stopped after {} trials — best f({:.4}) = {:.6}",
|
||||
study.n_trials(),
|
||||
best.get(&x).unwrap(),
|
||||
best.value,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//! Journal storage — persist trials to disk and resume later.
|
||||
//!
|
||||
//! `JournalStorage` writes every trial to a JSONL file so that a study can
|
||||
//! be resumed after a crash or across separate runs.
|
||||
//!
|
||||
//! Run with: `cargo run --example journal_storage --features journal`
|
||||
|
||||
use optimizer::prelude::*;
|
||||
|
||||
fn main() -> optimizer::Result<()> {
|
||||
let path = std::env::temp_dir().join("optimizer_journal_example.jsonl");
|
||||
|
||||
// Clean up from any previous run
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
let x = FloatParam::new(-5.0, 5.0).name("x");
|
||||
|
||||
// --- First run: optimize 20 trials and persist to disk ---
|
||||
{
|
||||
let storage = JournalStorage::<f64>::new(&path);
|
||||
let study: Study<f64> = Study::builder()
|
||||
.minimize()
|
||||
.sampler(TpeSampler::new())
|
||||
.storage(storage)
|
||||
.build();
|
||||
|
||||
study.optimize(20, |trial| {
|
||||
let xv = x.suggest(trial)?;
|
||||
Ok::<_, optimizer::Error>(xv * xv)
|
||||
})?;
|
||||
|
||||
println!(
|
||||
"First run: {} trials saved to {}",
|
||||
study.n_trials(),
|
||||
path.display(),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Second run: resume from the journal file ---
|
||||
{
|
||||
let storage = JournalStorage::<f64>::open(&path)?;
|
||||
let study: Study<f64> = Study::builder()
|
||||
.minimize()
|
||||
.sampler(TpeSampler::new())
|
||||
.storage(storage)
|
||||
.build();
|
||||
|
||||
let before = study.n_trials();
|
||||
study.optimize(10, |trial| {
|
||||
let xv = x.suggest(trial)?;
|
||||
Ok::<_, optimizer::Error>(xv * xv)
|
||||
})?;
|
||||
|
||||
let best = study.best_trial()?;
|
||||
println!(
|
||||
"Resumed: {} → {} trials, best f({:.4}) = {:.6}",
|
||||
before,
|
||||
study.n_trials(),
|
||||
best.get(&x).unwrap(),
|
||||
best.value,
|
||||
);
|
||||
}
|
||||
|
||||
// Clean up
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//! Multi-objective optimization — optimize competing objectives simultaneously.
|
||||
//!
|
||||
//! `MultiObjectiveStudy` returns the Pareto front: the set of solutions where
|
||||
//! no objective can be improved without worsening another.
|
||||
//!
|
||||
//! Run with: `cargo run --example multi_objective`
|
||||
|
||||
use optimizer::multi_objective::MultiObjectiveStudy;
|
||||
use optimizer::prelude::*;
|
||||
|
||||
fn main() -> optimizer::Result<()> {
|
||||
let study = MultiObjectiveStudy::new(vec![Direction::Minimize, Direction::Minimize]);
|
||||
|
||||
let x = FloatParam::new(0.0, 1.0).name("x");
|
||||
|
||||
// Classic bi-objective: f1(x) = x², f2(x) = (x-1)²
|
||||
// The Pareto front is the curve where improving f1 worsens f2.
|
||||
study.optimize(50, |trial| {
|
||||
let xv = x.suggest(trial)?;
|
||||
let f1 = xv * xv;
|
||||
let f2 = (xv - 1.0) * (xv - 1.0);
|
||||
Ok::<_, optimizer::Error>(vec![f1, f2])
|
||||
})?;
|
||||
|
||||
let front = study.pareto_front();
|
||||
println!(
|
||||
"Ran {} trials, Pareto front has {} solutions:",
|
||||
study.n_trials(),
|
||||
front.len(),
|
||||
);
|
||||
|
||||
let mut sorted = front.clone();
|
||||
sorted.sort_by(|a, b| a.values[0].partial_cmp(&b.values[0]).unwrap());
|
||||
|
||||
for (i, trial) in sorted.iter().take(5).enumerate() {
|
||||
println!(
|
||||
" {}: x={:.3}, f1={:.4}, f2={:.4}",
|
||||
i + 1,
|
||||
trial.get(&x).unwrap(),
|
||||
trial.values[0],
|
||||
trial.values[1],
|
||||
);
|
||||
}
|
||||
if sorted.len() > 5 {
|
||||
println!(" ... and {} more", sorted.len() - 5);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//! Trial pruning — stop unpromising trials early with `MedianPruner`.
|
||||
//!
|
||||
//! When your objective involves an iterative loop (e.g. training epochs),
|
||||
//! the pruner compares intermediate values across trials and kills the
|
||||
//! ones that fall below the median — saving compute on bad configurations.
|
||||
//!
|
||||
//! Run with: `cargo run --example pruning`
|
||||
|
||||
use optimizer::prelude::*;
|
||||
|
||||
fn main() -> optimizer::Result<()> {
|
||||
// MedianPruner prunes trials whose intermediate value falls below the
|
||||
// median of previously completed trials at the same step.
|
||||
let study: Study<f64> = Study::builder()
|
||||
.minimize()
|
||||
.sampler(RandomSampler::with_seed(42))
|
||||
.pruner(
|
||||
MedianPruner::new(Direction::Minimize)
|
||||
.n_warmup_steps(3) // run at least 3 epochs before pruning
|
||||
.n_min_trials(3), // need 3 completed trials before pruning kicks in
|
||||
)
|
||||
.build();
|
||||
|
||||
let lr = FloatParam::new(1e-4, 1.0).name("learning_rate");
|
||||
let momentum = FloatParam::new(0.0, 0.99).name("momentum");
|
||||
|
||||
let n_epochs: u64 = 20;
|
||||
|
||||
study.optimize(30, |trial| {
|
||||
let lr_val = lr.suggest(trial)?;
|
||||
let mom = momentum.suggest(trial)?;
|
||||
|
||||
// Simulated training loop — good hyperparameters converge to low loss,
|
||||
// bad ones plateau high, giving the pruner something to cut.
|
||||
let mut loss = 1.0;
|
||||
for epoch in 0..n_epochs {
|
||||
let lr_penalty = (lr_val.log10() - 0.01_f64.log10()).powi(2);
|
||||
let mom_penalty = (mom - 0.8).powi(2);
|
||||
let base_loss = 0.02 + 0.05 * lr_penalty + 1.5 * mom_penalty;
|
||||
let progress = (epoch as f64 + 1.0) / n_epochs as f64;
|
||||
loss = base_loss + (1.0 - base_loss) * (-3.5 * progress).exp();
|
||||
|
||||
// Report intermediate value so the pruner can evaluate this trial.
|
||||
trial.report(epoch, loss);
|
||||
|
||||
// Check whether the pruner recommends stopping early.
|
||||
if trial.should_prune() {
|
||||
Err(TrialPruned)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<_, Error>(loss)
|
||||
})?;
|
||||
|
||||
// --- Results ---
|
||||
let best = study.best_trial()?;
|
||||
println!(
|
||||
"Completed {} trials ({} pruned)",
|
||||
study.n_trials(),
|
||||
study.n_pruned_trials()
|
||||
);
|
||||
println!("Best trial #{}: loss = {:.6}", best.id, best.value);
|
||||
println!(" learning_rate = {:.6}", best.get(&lr).unwrap());
|
||||
println!(" momentum = {:.4}", best.get(&momentum).unwrap());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
//! Pruning and early-stopping example — demonstrates trial pruning with `MedianPruner`
|
||||
//! and early stopping via `optimize_with_callback`.
|
||||
//!
|
||||
//! Simulates a training loop where each trial trains for multiple "epochs". The pruner
|
||||
//! stops unpromising trials early, and a callback halts the entire study once a target
|
||||
//! loss is reached.
|
||||
//!
|
||||
//! Run with: `cargo run --example pruning_and_callbacks`
|
||||
|
||||
use std::ops::ControlFlow;
|
||||
|
||||
use optimizer::TrialState;
|
||||
use optimizer::prelude::*;
|
||||
|
||||
fn main() -> optimizer::Result<()> {
|
||||
let n_trials: usize = 30;
|
||||
let n_epochs: u64 = 20;
|
||||
let target_loss = 0.15;
|
||||
|
||||
// Build a study with a seeded random sampler and MedianPruner.
|
||||
// MedianPruner compares each trial's intermediate value against the median of
|
||||
// completed trials at the same step — trials performing below median are pruned.
|
||||
let study: Study<f64> = Study::builder()
|
||||
.minimize()
|
||||
.sampler(RandomSampler::with_seed(42))
|
||||
.pruner(
|
||||
MedianPruner::new(Direction::Minimize)
|
||||
.n_warmup_steps(3) // let every trial run at least 3 epochs before pruning
|
||||
.n_min_trials(3), // need 3 completed trials before pruning kicks in
|
||||
)
|
||||
.build();
|
||||
|
||||
let learning_rate = FloatParam::new(1e-4, 1.0).name("learning_rate");
|
||||
let momentum = FloatParam::new(0.0, 0.99).name("momentum");
|
||||
|
||||
// Use optimize_with_callback to get both pruning AND early stopping.
|
||||
// The callback fires after each completed (or pruned) trial and can halt the study.
|
||||
study.optimize_with_callback(
|
||||
n_trials,
|
||||
// --- Objective function: simulated training loop with pruning ---
|
||||
|trial| {
|
||||
let lr = learning_rate.suggest(trial)?;
|
||||
let mom = momentum.suggest(trial)?;
|
||||
|
||||
// Simulate training for n_epochs, reporting intermediate loss each epoch.
|
||||
// Good hyperparameters (lr ≈ 0.01, momentum ≈ 0.8) converge to low loss;
|
||||
// bad combos plateau high — giving the pruner something to cut.
|
||||
let mut loss = 1.0;
|
||||
for epoch in 0..n_epochs {
|
||||
let lr_penalty = (lr.log10() - 0.01_f64.log10()).powi(2); // 0 at lr=0.01
|
||||
let mom_penalty = (mom - 0.8).powi(2); // 0 at momentum=0.8
|
||||
let base_loss = 0.02 + 0.05 * lr_penalty + 1.5 * mom_penalty;
|
||||
let progress = (epoch as f64 + 1.0) / n_epochs as f64;
|
||||
// Loss decays from 1.0 toward base_loss over epochs.
|
||||
loss = base_loss + (1.0 - base_loss) * (-3.5 * progress).exp();
|
||||
|
||||
// Report the intermediate value so the pruner can evaluate this trial.
|
||||
trial.report(epoch, loss);
|
||||
|
||||
// Check whether the pruner recommends stopping this trial early.
|
||||
if trial.should_prune() {
|
||||
// Signal that this trial was pruned — the study records it as Pruned.
|
||||
Err(TrialPruned)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<_, Error>(loss)
|
||||
},
|
||||
// --- Callback: early stopping when we hit the target ---
|
||||
|study, completed_trial| {
|
||||
let n_complete = study.n_trials();
|
||||
let n_pruned = study
|
||||
.trials()
|
||||
.iter()
|
||||
.filter(|t| t.state == TrialState::Pruned)
|
||||
.count();
|
||||
|
||||
match completed_trial.state {
|
||||
TrialState::Pruned => {
|
||||
println!(
|
||||
" Trial {:>3} PRUNED at epoch {} (loss = {:.4}) \
|
||||
[{n_complete} done, {n_pruned} pruned]",
|
||||
completed_trial.id,
|
||||
completed_trial.intermediate_values.len(),
|
||||
completed_trial
|
||||
.intermediate_values
|
||||
.last()
|
||||
.map_or(f64::NAN, |v| v.1),
|
||||
);
|
||||
}
|
||||
TrialState::Complete => {
|
||||
println!(
|
||||
" Trial {:>3} complete: loss = {:.4} \
|
||||
[{n_complete} done, {n_pruned} pruned]",
|
||||
completed_trial.id, completed_trial.value,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Stop the entire study once we find a good enough result.
|
||||
if completed_trial.state == TrialState::Complete && completed_trial.value < target_loss
|
||||
{
|
||||
println!("\n Early stopping: reached target loss {target_loss}!");
|
||||
return ControlFlow::Break(());
|
||||
}
|
||||
|
||||
ControlFlow::Continue(())
|
||||
},
|
||||
)?;
|
||||
|
||||
// --- Results ---
|
||||
let best = study.best_trial().expect("at least one completed trial");
|
||||
let total = study.n_trials();
|
||||
let pruned = study
|
||||
.trials()
|
||||
.iter()
|
||||
.filter(|t| t.state == TrialState::Pruned)
|
||||
.count();
|
||||
|
||||
println!("\n--- Results ---");
|
||||
println!(" Total trials : {total}");
|
||||
println!(" Pruned : {pruned}");
|
||||
println!(" Completed : {}", total - pruned);
|
||||
println!(" Best trial #{}: loss = {:.6}", best.id, best.value);
|
||||
println!(
|
||||
" learning_rate = {:.6}",
|
||||
best.get(&learning_rate).unwrap()
|
||||
);
|
||||
println!(" momentum = {:.4}", best.get(&momentum).unwrap());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -40,10 +40,7 @@ fn run_study(study: Study<f64>, n_trials: usize) -> f64 {
|
||||
|
||||
fn main() {
|
||||
let n_trials: usize = 100;
|
||||
println!("Comparing samplers on Sphere(x, y) = x² + y²");
|
||||
println!(" Search space: x ∈ [-5, 5], y ∈ [-3, 3]");
|
||||
println!(" Known minimum: f(0, 0) = 0");
|
||||
println!(" Trials per sampler: {n_trials}");
|
||||
println!("Comparing samplers on Sphere(x, y) = x² + y² ({n_trials} trials each)");
|
||||
println!();
|
||||
|
||||
// --- Random sampler (baseline) ---
|
||||
@@ -78,17 +75,4 @@ fn main() {
|
||||
println!(" Random : {random_best:.6}");
|
||||
println!(" TPE : {tpe_best:.6}");
|
||||
println!(" Grid : {grid_best:.6}");
|
||||
println!();
|
||||
|
||||
// Find the winner
|
||||
let results = [
|
||||
("Random", random_best),
|
||||
("TPE", tpe_best),
|
||||
("Grid", grid_best),
|
||||
];
|
||||
let (winner, _) = results
|
||||
.iter()
|
||||
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
|
||||
.unwrap();
|
||||
println!("Winner: {winner} (closest to known minimum of 0.0)");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user