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
|
- uses: Swatinem/rust-cache@v2
|
||||||
- name: Run basic_optimization
|
- name: Run basic_optimization
|
||||||
run: cargo run --example 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
|
- name: Run parameter_types
|
||||||
run: cargo run --example parameter_types --features derive
|
run: cargo run --example parameter_types --features derive
|
||||||
- name: Run advanced_features
|
- name: Run sampler_comparison
|
||||||
run: cargo run --example advanced_features --features "async,journal"
|
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:
|
docs:
|
||||||
name: Docs
|
name: Docs
|
||||||
|
|||||||
+22
-5
@@ -67,13 +67,30 @@ name = "sampler_comparison"
|
|||||||
path = "examples/sampler_comparison.rs"
|
path = "examples/sampler_comparison.rs"
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "pruning_and_callbacks"
|
name = "pruning"
|
||||||
path = "examples/pruning_and_callbacks.rs"
|
path = "examples/pruning.rs"
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "advanced_features"
|
name = "early_stopping"
|
||||||
path = "examples/advanced_features.rs"
|
path = "examples/early_stopping.rs"
|
||||||
required-features = ["async", "journal"]
|
|
||||||
|
[[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]]
|
[[test]]
|
||||||
name = "journal_tests"
|
name = "journal_tests"
|
||||||
|
|||||||
@@ -52,11 +52,15 @@ println!("Best x = {:.4}, f(x) = {:.4}", best.get(&x).unwrap(), best.value);
|
|||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo run --example basic_optimization # Minimize a quadratic — simplest possible usage
|
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 parameter_types --features derive # All 5 param types + #[derive(Categorical)]
|
||||||
cargo run --example pruning_and_callbacks # Trial pruning with MedianPruner + early stopping
|
cargo run --example sampler_comparison # Compare Random, TPE, and Grid on the same problem
|
||||||
cargo run --example parameter_types --features derive # All 5 param types + #[derive(Categorical)]
|
cargo run --example pruning # Trial pruning with MedianPruner
|
||||||
cargo run --example advanced_features --features async,journal # Async, journal storage, ask-and-tell, multi-objective
|
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
|
## 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() {
|
fn main() {
|
||||||
let n_trials: usize = 100;
|
let n_trials: usize = 100;
|
||||||
println!("Comparing samplers on Sphere(x, y) = x² + y²");
|
println!("Comparing samplers on Sphere(x, y) = x² + y² ({n_trials} trials each)");
|
||||||
println!(" Search space: x ∈ [-5, 5], y ∈ [-3, 3]");
|
|
||||||
println!(" Known minimum: f(0, 0) = 0");
|
|
||||||
println!(" Trials per sampler: {n_trials}");
|
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
// --- Random sampler (baseline) ---
|
// --- Random sampler (baseline) ---
|
||||||
@@ -78,17 +75,4 @@ fn main() {
|
|||||||
println!(" Random : {random_best:.6}");
|
println!(" Random : {random_best:.6}");
|
||||||
println!(" TPE : {tpe_best:.6}");
|
println!(" TPE : {tpe_best:.6}");
|
||||||
println!(" Grid : {grid_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