Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d88280c152 | |||
| 0e8fc82201 | |||
| d7ad3f60db | |||
| 5dc81fa0ab | |||
| 55e50e6afb | |||
| 2fef1ab3c3 | |||
| aa1718fed7 | |||
| f9227df96e | |||
| 02ec9498c4 | |||
| 183a78c09e | |||
| ce817c1f38 |
+13
-3
@@ -1,6 +1,9 @@
|
|||||||
|
[workspace]
|
||||||
|
members = ["optimizer-derive"]
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "optimizer"
|
name = "optimizer"
|
||||||
version = "0.4.0"
|
version = "0.5.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.88"
|
rust-version = "1.88"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -13,18 +16,20 @@ categories = ["algorithm", "science", "data-structures"]
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4"
|
rand = "0.10"
|
||||||
rand = "0.9"
|
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
tokio = { version = "1", features = ["sync", "rt-multi-thread"], optional = true }
|
tokio = { version = "1", features = ["sync", "rt-multi-thread"], optional = true }
|
||||||
|
optimizer-derive = { version = "0.1.0", path = "optimizer-derive", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
async = ["dep:tokio"]
|
async = ["dep:tokio"]
|
||||||
|
derive = ["dep:optimizer-derive"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
|
||||||
|
optimizer-derive = { version = "0.1.0", path = "optimizer-derive" }
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "async_api_optimization"
|
name = "async_api_optimization"
|
||||||
@@ -34,3 +39,8 @@ required-features = ["async"]
|
|||||||
[[example]]
|
[[example]]
|
||||||
name = "ml_hyperparameter_tuning"
|
name = "ml_hyperparameter_tuning"
|
||||||
path = "examples/ml_hyperparameter_tuning.rs"
|
path = "examples/ml_hyperparameter_tuning.rs"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "parameter_api"
|
||||||
|
path = "examples/parameter_api.rs"
|
||||||
|
required-features = ["derive"]
|
||||||
|
|||||||
@@ -13,28 +13,39 @@ A Rust library for black-box optimization with multiple sampling strategies.
|
|||||||
- **Random Search** - Simple random sampling for baseline comparisons
|
- **Random Search** - Simple random sampling for baseline comparisons
|
||||||
- **TPE (Tree-Parzen Estimator)** - Bayesian optimization for efficient search
|
- **TPE (Tree-Parzen Estimator)** - Bayesian optimization for efficient search
|
||||||
- **Grid Search** - Exhaustive search over a specified parameter grid
|
- **Grid Search** - Exhaustive search over a specified parameter grid
|
||||||
- Float, integer, and categorical parameter types
|
- Float, integer, categorical, boolean, and enum parameter types
|
||||||
- Log-scale and stepped parameter sampling
|
- Log-scale and stepped parameter sampling
|
||||||
- Sync and async optimization with parallel trial evaluation
|
- Sync and async optimization with parallel trial evaluation
|
||||||
|
- `#[derive(Categorical)]` for enum parameters
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use optimizer::{Direction, Study};
|
use optimizer::parameter::{FloatParam, Parameter};
|
||||||
use optimizer::sampler::tpe::TpeSampler;
|
use optimizer::sampler::tpe::TpeSampler;
|
||||||
|
use optimizer::{Direction, Study};
|
||||||
|
|
||||||
|
// Create a study with TPE sampler
|
||||||
let sampler = TpeSampler::builder().seed(42).build().unwrap();
|
let sampler = TpeSampler::builder().seed(42).build().unwrap();
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
// Define parameter search space
|
||||||
|
let x_param = FloatParam::new(-10.0, 10.0);
|
||||||
|
|
||||||
|
// Optimize x^2 for 20 trials
|
||||||
study
|
study
|
||||||
.optimize_with_sampler(20, |trial| {
|
.optimize_with_sampler(20, |trial| {
|
||||||
let x = trial.suggest_float("x", -10.0, 10.0)?;
|
let x = x_param.suggest(trial)?;
|
||||||
Ok::<_, optimizer::Error>(x * x)
|
Ok::<_, optimizer::Error>(x * x)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
// Get the best result
|
||||||
let best = study.best_trial().unwrap();
|
let best = study.best_trial().unwrap();
|
||||||
println!("Best value: {} at x={:?}", best.value, best.params);
|
println!("Best value: {}", best.value);
|
||||||
|
for (id, label) in &best.param_labels {
|
||||||
|
println!(" {}: {:?}", label, best.params[id]);
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Samplers
|
## Samplers
|
||||||
@@ -134,6 +145,7 @@ let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
|||||||
## Feature Flags
|
## Feature Flags
|
||||||
|
|
||||||
- `async` - Enable async optimization methods (requires tokio)
|
- `async` - Enable async optimization methods (requires tokio)
|
||||||
|
- `derive` - Enable `#[derive(Categorical)]` for enum parameters
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
//!
|
//!
|
||||||
//! # Key Concepts Demonstrated
|
//! # Key Concepts Demonstrated
|
||||||
//!
|
//!
|
||||||
//! - Async optimization with `optimize_parallel_with_sampler`
|
//! - Async optimization with `optimize_parallel`
|
||||||
//! - Running multiple trials concurrently for faster optimization
|
//! - Running multiple trials concurrently for faster optimization
|
||||||
//! - Boolean and categorical parameter types
|
//! - Boolean and categorical parameter types
|
||||||
//! - Measuring speedup from parallelism
|
//! - Measuring speedup from parallelism
|
||||||
@@ -26,8 +26,7 @@
|
|||||||
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use optimizer::sampler::tpe::TpeSampler;
|
use optimizer::prelude::*;
|
||||||
use optimizer::{Direction, ParamValue, Study, Trial};
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Configuration: Service parameters we want to tune
|
// Configuration: Service parameters we want to tune
|
||||||
@@ -133,32 +132,27 @@ async fn evaluate_service(config: &ServiceConfig) -> f64 {
|
|||||||
/// 3. Return both the Trial and the result value as a tuple
|
/// 3. Return both the Trial and the result value as a tuple
|
||||||
///
|
///
|
||||||
/// This ownership pattern allows the trial to be used across await points.
|
/// This ownership pattern allows the trial to be used across await points.
|
||||||
async fn objective(mut trial: Trial) -> optimizer::Result<(Trial, f64)> {
|
#[allow(clippy::too_many_arguments)]
|
||||||
// Sample configuration parameters
|
async fn objective(
|
||||||
|
mut trial: Trial,
|
||||||
// Stepped integers: only sample multiples of the step value
|
cache_size_mb_param: &IntParam,
|
||||||
let cache_size_mb = trial.suggest_int_step("cache_size_mb", 64, 1024, 64)?;
|
connection_pool_size_param: &IntParam,
|
||||||
let connection_pool_size = trial.suggest_int_step("connection_pool_size", 10, 200, 10)?;
|
request_timeout_ms_param: &IntParam,
|
||||||
let request_timeout_ms = trial.suggest_int_step("request_timeout_ms", 1000, 10000, 500)?;
|
retry_count_param: &IntParam,
|
||||||
|
batch_size_param: &IntParam,
|
||||||
// Regular integer
|
compression_level_param: &IntParam,
|
||||||
let retry_count = trial.suggest_int("retry_count", 0, 5)?;
|
use_http2_param: &BoolParam,
|
||||||
|
load_balancing_param: &CategoricalParam<&str>,
|
||||||
// Log-scale integer: good for parameters like batch sizes
|
) -> optimizer::Result<(Trial, f64)> {
|
||||||
// that might vary from 1 to 256
|
// Sample configuration parameters using parameter definitions
|
||||||
let batch_size = trial.suggest_int_log("batch_size", 1, 256)?;
|
let cache_size_mb = cache_size_mb_param.suggest(&mut trial)?;
|
||||||
|
let connection_pool_size = connection_pool_size_param.suggest(&mut trial)?;
|
||||||
// Regular integer for compression level
|
let request_timeout_ms = request_timeout_ms_param.suggest(&mut trial)?;
|
||||||
let compression_level = trial.suggest_int("compression_level", 0, 9)?;
|
let retry_count = retry_count_param.suggest(&mut trial)?;
|
||||||
|
let batch_size = batch_size_param.suggest(&mut trial)?;
|
||||||
// Boolean: internally uses categorical with [false, true]
|
let compression_level = compression_level_param.suggest(&mut trial)?;
|
||||||
let use_http2 = trial.suggest_bool("use_http2")?;
|
let use_http2 = use_http2_param.suggest(&mut trial)?;
|
||||||
|
let load_balancing = load_balancing_param.suggest(&mut trial)?;
|
||||||
// Categorical: choose from a list of options
|
|
||||||
let load_balancing = trial.suggest_categorical(
|
|
||||||
"load_balancing",
|
|
||||||
&["round_robin", "least_connections", "random", "ip_hash"],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Build configuration
|
// Build configuration
|
||||||
let config = ServiceConfig {
|
let config = ServiceConfig {
|
||||||
@@ -183,24 +177,6 @@ async fn objective(mut trial: Trial) -> optimizer::Result<(Trial, f64)> {
|
|||||||
// Helper Functions
|
// Helper Functions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Formats a parameter value for display.
|
|
||||||
fn format_param(name: &str, value: &ParamValue) -> String {
|
|
||||||
match (name, value) {
|
|
||||||
(_, ParamValue::Float(v)) => format!("{v:.4}"),
|
|
||||||
(_, ParamValue::Int(v)) => format!("{v}"),
|
|
||||||
("use_http2", ParamValue::Categorical(idx)) => {
|
|
||||||
if *idx == 1 { "true" } else { "false" }.to_string()
|
|
||||||
}
|
|
||||||
("load_balancing", ParamValue::Categorical(idx)) => {
|
|
||||||
["round_robin", "least_connections", "random", "ip_hash"]
|
|
||||||
.get(*idx)
|
|
||||||
.unwrap_or(&"unknown")
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
(_, ParamValue::Categorical(idx)) => format!("category_{idx}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prints the results of the optimization.
|
/// Prints the results of the optimization.
|
||||||
fn print_results(study: &Study<f64>, elapsed: Duration, n_trials: usize) {
|
fn print_results(study: &Study<f64>, elapsed: Duration, n_trials: usize) {
|
||||||
println!("\n{}", "=".repeat(60));
|
println!("\n{}", "=".repeat(60));
|
||||||
@@ -219,31 +195,46 @@ fn print_results(study: &Study<f64>, elapsed: Duration, n_trials: usize) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Prints the best configuration found.
|
/// Prints the best configuration found.
|
||||||
fn print_best_config(study: &Study<f64>) -> optimizer::Result<()> {
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn print_best_config(
|
||||||
|
study: &Study<f64>,
|
||||||
|
cache_size_mb_param: &IntParam,
|
||||||
|
connection_pool_size_param: &IntParam,
|
||||||
|
request_timeout_ms_param: &IntParam,
|
||||||
|
retry_count_param: &IntParam,
|
||||||
|
batch_size_param: &IntParam,
|
||||||
|
compression_level_param: &IntParam,
|
||||||
|
use_http2_param: &BoolParam,
|
||||||
|
load_balancing_param: &CategoricalParam<&str>,
|
||||||
|
) -> optimizer::Result<()> {
|
||||||
let best = study.best_trial()?;
|
let best = study.best_trial()?;
|
||||||
|
|
||||||
println!("\nBest configuration found:");
|
println!("\nBest configuration found:");
|
||||||
println!(" Score: {:.6}", best.value);
|
println!(" Score: {:.6}", best.value);
|
||||||
println!("\n Parameters:");
|
println!("\n Parameters:");
|
||||||
|
println!(
|
||||||
// Print parameters in a logical order
|
" cache_size_mb: {}",
|
||||||
let param_order = [
|
best.get(cache_size_mb_param).unwrap()
|
||||||
"cache_size_mb",
|
);
|
||||||
"connection_pool_size",
|
println!(
|
||||||
"request_timeout_ms",
|
" connection_pool_size: {}",
|
||||||
"retry_count",
|
best.get(connection_pool_size_param).unwrap()
|
||||||
"batch_size",
|
);
|
||||||
"compression_level",
|
println!(
|
||||||
"use_http2",
|
" request_timeout_ms: {}",
|
||||||
"load_balancing",
|
best.get(request_timeout_ms_param).unwrap()
|
||||||
];
|
);
|
||||||
|
println!(" retry_count: {}", best.get(retry_count_param).unwrap());
|
||||||
for name in param_order {
|
println!(" batch_size: {}", best.get(batch_size_param).unwrap());
|
||||||
if let Some(value) = best.params.get(name) {
|
println!(
|
||||||
let display = format_param(name, value);
|
" compression_level: {}",
|
||||||
println!(" {name}: {display}");
|
best.get(compression_level_param).unwrap()
|
||||||
}
|
);
|
||||||
}
|
println!(" use_http2: {}", best.get(use_http2_param).unwrap());
|
||||||
|
println!(
|
||||||
|
" load_balancing: {}",
|
||||||
|
best.get(load_balancing_param).unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -284,7 +275,35 @@ async fn main() -> optimizer::Result<()> {
|
|||||||
// Step 2: Create a study to minimize the score
|
// Step 2: Create a study to minimize the score
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
// Step 3: Configure optimization
|
// Step 3: Define parameter search spaces
|
||||||
|
let cache_size_mb_param = IntParam::new(64, 1024).name("cache_size_mb").step(64);
|
||||||
|
let connection_pool_size_param = IntParam::new(10, 200).name("connection_pool_size").step(10);
|
||||||
|
let request_timeout_ms_param = IntParam::new(1000, 10000)
|
||||||
|
.name("request_timeout_ms")
|
||||||
|
.step(500);
|
||||||
|
let retry_count_param = IntParam::new(0, 5).name("retry_count");
|
||||||
|
let batch_size_param = IntParam::new(1, 256).name("batch_size").log_scale();
|
||||||
|
let compression_level_param = IntParam::new(0, 9).name("compression_level");
|
||||||
|
let use_http2_param = BoolParam::new().name("use_http2");
|
||||||
|
let load_balancing_param = CategoricalParam::new(vec![
|
||||||
|
"round_robin",
|
||||||
|
"least_connections",
|
||||||
|
"random",
|
||||||
|
"ip_hash",
|
||||||
|
])
|
||||||
|
.name("load_balancing");
|
||||||
|
|
||||||
|
// Clone params for use after the closure moves them
|
||||||
|
let cache_size_mb_p = cache_size_mb_param.clone();
|
||||||
|
let connection_pool_size_p = connection_pool_size_param.clone();
|
||||||
|
let request_timeout_ms_p = request_timeout_ms_param.clone();
|
||||||
|
let retry_count_p = retry_count_param.clone();
|
||||||
|
let batch_size_p = batch_size_param.clone();
|
||||||
|
let compression_level_p = compression_level_param.clone();
|
||||||
|
let use_http2_p = use_http2_param.clone();
|
||||||
|
let load_balancing_p = load_balancing_param.clone();
|
||||||
|
|
||||||
|
// Step 4: Configure optimization
|
||||||
let n_trials = 40;
|
let n_trials = 40;
|
||||||
let concurrency = 4; // Run 4 trials in parallel
|
let concurrency = 4; // Run 4 trials in parallel
|
||||||
|
|
||||||
@@ -292,25 +311,57 @@ async fn main() -> optimizer::Result<()> {
|
|||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
||||||
// Step 4: Run parallel async optimization
|
// Step 5: Run parallel async optimization
|
||||||
//
|
//
|
||||||
// optimize_parallel_with_sampler:
|
// optimize_parallel:
|
||||||
// - Runs up to `concurrency` trials simultaneously
|
// - Runs up to `concurrency` trials simultaneously
|
||||||
// - Each trial calls the objective function
|
// - Each trial calls the objective function
|
||||||
// - Uses a semaphore to limit concurrent evaluations
|
// - Uses a semaphore to limit concurrent evaluations
|
||||||
// - Collects results as trials complete
|
// - Collects results as trials complete
|
||||||
//
|
//
|
||||||
// The "_with_sampler" suffix means the TPE sampler gets access to
|
// The sampler gets access to trial history for informed sampling.
|
||||||
// trial history for informed sampling.
|
|
||||||
study
|
study
|
||||||
.optimize_parallel_with_sampler(n_trials, concurrency, objective)
|
.optimize_parallel(n_trials, concurrency, move |trial| {
|
||||||
|
let cache_size_mb_param = cache_size_mb_param.clone();
|
||||||
|
let connection_pool_size_param = connection_pool_size_param.clone();
|
||||||
|
let request_timeout_ms_param = request_timeout_ms_param.clone();
|
||||||
|
let retry_count_param = retry_count_param.clone();
|
||||||
|
let batch_size_param = batch_size_param.clone();
|
||||||
|
let compression_level_param = compression_level_param.clone();
|
||||||
|
let use_http2_param = use_http2_param.clone();
|
||||||
|
let load_balancing_param = load_balancing_param.clone();
|
||||||
|
async move {
|
||||||
|
objective(
|
||||||
|
trial,
|
||||||
|
&cache_size_mb_param,
|
||||||
|
&connection_pool_size_param,
|
||||||
|
&request_timeout_ms_param,
|
||||||
|
&retry_count_param,
|
||||||
|
&batch_size_param,
|
||||||
|
&compression_level_param,
|
||||||
|
&use_http2_param,
|
||||||
|
&load_balancing_param,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
|
|
||||||
// Step 5: Print results
|
// Step 5: Print results
|
||||||
print_results(&study, elapsed, n_trials);
|
print_results(&study, elapsed, n_trials);
|
||||||
print_best_config(&study)?;
|
print_best_config(
|
||||||
|
&study,
|
||||||
|
&cache_size_mb_p,
|
||||||
|
&connection_pool_size_p,
|
||||||
|
&request_timeout_ms_p,
|
||||||
|
&retry_count_p,
|
||||||
|
&batch_size_p,
|
||||||
|
&compression_level_p,
|
||||||
|
&use_http2_p,
|
||||||
|
&load_balancing_p,
|
||||||
|
)?;
|
||||||
print_top_trials(&study, 5);
|
print_top_trials(&study, 5);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -23,9 +23,7 @@
|
|||||||
|
|
||||||
use std::ops::ControlFlow;
|
use std::ops::ControlFlow;
|
||||||
|
|
||||||
use optimizer::sampler::CompletedTrial;
|
use optimizer::prelude::*;
|
||||||
use optimizer::sampler::tpe::TpeSampler;
|
|
||||||
use optimizer::{Direction, ParamValue, Study, Trial};
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Configuration: Hyperparameters we want to tune
|
// Configuration: Hyperparameters we want to tune
|
||||||
@@ -87,34 +85,32 @@ fn evaluate_model(config: &ModelConfig) -> f64 {
|
|||||||
/// The objective function that the optimizer calls for each trial.
|
/// The objective function that the optimizer calls for each trial.
|
||||||
///
|
///
|
||||||
/// This function:
|
/// This function:
|
||||||
/// 1. Uses `trial.suggest_*()` methods to sample hyperparameter values
|
/// 1. Uses parameter definitions passed as arguments
|
||||||
/// 2. Builds a model configuration from those values
|
/// 2. Builds a model configuration from the suggested values
|
||||||
/// 3. Evaluates the model and returns the loss
|
/// 3. Evaluates the model and returns the loss
|
||||||
///
|
///
|
||||||
/// The optimizer learns from the results to suggest better parameters
|
/// The optimizer learns from the results to suggest better parameters
|
||||||
/// in future trials.
|
/// in future trials.
|
||||||
fn objective(trial: &mut Trial) -> optimizer::Result<f64> {
|
#[allow(clippy::too_many_arguments)]
|
||||||
// Sample hyperparameters using different strategies:
|
fn objective(
|
||||||
|
trial: &mut Trial,
|
||||||
// Log-scale: Good for parameters spanning multiple orders of magnitude
|
learning_rate_param: &FloatParam,
|
||||||
// The learning rate might be 0.001, 0.01, or 0.1 - log-scale samples evenly across these
|
max_depth_param: &IntParam,
|
||||||
let learning_rate = trial.suggest_float_log("learning_rate", 0.001, 0.3)?;
|
n_estimators_param: &IntParam,
|
||||||
|
subsample_param: &FloatParam,
|
||||||
// Regular integer: Uniformly samples from the range [3, 12]
|
colsample_bytree_param: &FloatParam,
|
||||||
let max_depth = trial.suggest_int("max_depth", 3, 12)?;
|
min_child_weight_param: &IntParam,
|
||||||
|
reg_alpha_param: &FloatParam,
|
||||||
// Stepped integer: Only samples multiples of 50 (50, 100, 150, ..., 500)
|
reg_lambda_param: &FloatParam,
|
||||||
// Useful when you only want to test specific values
|
) -> optimizer::Result<f64> {
|
||||||
let n_estimators = trial.suggest_int_step("n_estimators", 50, 500, 50)?;
|
let learning_rate = learning_rate_param.suggest(trial)?;
|
||||||
|
let max_depth = max_depth_param.suggest(trial)?;
|
||||||
// Regular float: Uniformly samples from [0.5, 1.0]
|
let n_estimators = n_estimators_param.suggest(trial)?;
|
||||||
let subsample = trial.suggest_float("subsample", 0.5, 1.0)?;
|
let subsample = subsample_param.suggest(trial)?;
|
||||||
let colsample_bytree = trial.suggest_float("colsample_bytree", 0.5, 1.0)?;
|
let colsample_bytree = colsample_bytree_param.suggest(trial)?;
|
||||||
|
let min_child_weight = min_child_weight_param.suggest(trial)?;
|
||||||
// More parameters
|
let reg_alpha = reg_alpha_param.suggest(trial)?;
|
||||||
let min_child_weight = trial.suggest_int("min_child_weight", 1, 10)?;
|
let reg_lambda = reg_lambda_param.suggest(trial)?;
|
||||||
let reg_alpha = trial.suggest_float_log("reg_alpha", 1e-3, 10.0)?;
|
|
||||||
let reg_lambda = trial.suggest_float_log("reg_lambda", 1e-3, 10.0)?;
|
|
||||||
|
|
||||||
// Build configuration and evaluate
|
// Build configuration and evaluate
|
||||||
let config = ModelConfig {
|
let config = ModelConfig {
|
||||||
@@ -148,35 +144,12 @@ fn objective(trial: &mut Trial) -> optimizer::Result<f64> {
|
|||||||
/// Return `ControlFlow::Continue(())` to keep optimizing.
|
/// Return `ControlFlow::Continue(())` to keep optimizing.
|
||||||
/// Return `ControlFlow::Break(())` to stop early.
|
/// Return `ControlFlow::Break(())` to stop early.
|
||||||
fn on_trial_complete(study: &Study<f64>, trial: &CompletedTrial<f64>) -> ControlFlow<()> {
|
fn on_trial_complete(study: &Study<f64>, trial: &CompletedTrial<f64>) -> ControlFlow<()> {
|
||||||
// Helper to extract parameter values
|
// Print trial number and objective value
|
||||||
let get_float = |name: &str| -> f64 {
|
print!("{:>5} ", study.n_trials());
|
||||||
match trial.params.get(name) {
|
for value in trial.params.values() {
|
||||||
Some(ParamValue::Float(v)) => *v,
|
print!("{value:>12} ");
|
||||||
_ => 0.0,
|
}
|
||||||
}
|
println!("{:>12.6}", trial.value);
|
||||||
};
|
|
||||||
|
|
||||||
let get_int = |name: &str| -> i64 {
|
|
||||||
match trial.params.get(name) {
|
|
||||||
Some(ParamValue::Int(v)) => *v,
|
|
||||||
_ => 0,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Print progress
|
|
||||||
println!(
|
|
||||||
"{:>5} {:>10.5} {:>10} {:>12} {:>10.3} {:>12.3} {:>8} {:>10.4} {:>10.4} {:>12.6}",
|
|
||||||
study.n_trials(),
|
|
||||||
get_float("learning_rate"),
|
|
||||||
get_int("max_depth"),
|
|
||||||
get_int("n_estimators"),
|
|
||||||
get_float("subsample"),
|
|
||||||
get_float("colsample_bytree"),
|
|
||||||
get_int("min_child_weight"),
|
|
||||||
get_float("reg_alpha"),
|
|
||||||
get_float("reg_lambda"),
|
|
||||||
trial.value,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Early stopping: if we find an excellent solution, stop early
|
// Early stopping: if we find an excellent solution, stop early
|
||||||
if trial.value < 0.16 {
|
if trial.value < 0.16 {
|
||||||
@@ -218,29 +191,47 @@ fn main() -> optimizer::Result<()> {
|
|||||||
// Print header
|
// Print header
|
||||||
println!("Starting hyperparameter optimization...\n");
|
println!("Starting hyperparameter optimization...\n");
|
||||||
println!(
|
println!(
|
||||||
"{:>5} {:>10} {:>10} {:>12} {:>10} {:>12} {:>8} {:>10} {:>10} {:>12}",
|
"{:>5} {:>12} (parameters...) {:>12}",
|
||||||
"Trial",
|
"Trial", "Params", "Loss"
|
||||||
"LR",
|
|
||||||
"MaxDepth",
|
|
||||||
"Estimators",
|
|
||||||
"Subsample",
|
|
||||||
"ColSample",
|
|
||||||
"MinCW",
|
|
||||||
"Alpha",
|
|
||||||
"Lambda",
|
|
||||||
"Loss"
|
|
||||||
);
|
);
|
||||||
println!("{}", "-".repeat(110));
|
println!("{}", "-".repeat(60));
|
||||||
|
|
||||||
// Step 3: Run optimization
|
// Step 3: Define parameter search spaces
|
||||||
|
let learning_rate_param = FloatParam::new(0.001, 0.3)
|
||||||
|
.name("learning_rate")
|
||||||
|
.log_scale();
|
||||||
|
let max_depth_param = IntParam::new(3, 12).name("max_depth");
|
||||||
|
let n_estimators_param = IntParam::new(50, 500).name("n_estimators").step(50);
|
||||||
|
let subsample_param = FloatParam::new(0.5, 1.0).name("subsample");
|
||||||
|
let colsample_bytree_param = FloatParam::new(0.5, 1.0).name("colsample_bytree");
|
||||||
|
let min_child_weight_param = IntParam::new(1, 10).name("min_child_weight");
|
||||||
|
let reg_alpha_param = FloatParam::new(1e-3, 10.0).name("reg_alpha").log_scale();
|
||||||
|
let reg_lambda_param = FloatParam::new(1e-3, 10.0).name("reg_lambda").log_scale();
|
||||||
|
|
||||||
|
// Step 4: Run optimization
|
||||||
//
|
//
|
||||||
// optimize_with_callback_sampler runs the objective function for up to
|
// optimize_with_callback runs the objective function for up to
|
||||||
// n_trials iterations. After each trial, it calls the callback.
|
// n_trials iterations. After each trial, it calls the callback.
|
||||||
// The "_sampler" suffix means the TPE sampler gets access to trial
|
// The sampler gets access to trial history for informed sampling.
|
||||||
// history for informed sampling.
|
|
||||||
let n_trials = 50;
|
let n_trials = 50;
|
||||||
|
|
||||||
study.optimize_with_callback_sampler(n_trials, objective, on_trial_complete)?;
|
study.optimize_with_callback(
|
||||||
|
n_trials,
|
||||||
|
|trial| {
|
||||||
|
objective(
|
||||||
|
trial,
|
||||||
|
&learning_rate_param,
|
||||||
|
&max_depth_param,
|
||||||
|
&n_estimators_param,
|
||||||
|
&subsample_param,
|
||||||
|
&colsample_bytree_param,
|
||||||
|
&min_child_weight_param,
|
||||||
|
®_alpha_param,
|
||||||
|
®_lambda_param,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
on_trial_complete,
|
||||||
|
)?;
|
||||||
|
|
||||||
// Step 4: Get the best result
|
// Step 4: Get the best result
|
||||||
println!("\n{}", "=".repeat(110));
|
println!("\n{}", "=".repeat(110));
|
||||||
@@ -251,14 +242,29 @@ fn main() -> optimizer::Result<()> {
|
|||||||
println!("\nBest trial:");
|
println!("\nBest trial:");
|
||||||
println!(" Loss: {:.6}", best.value);
|
println!(" Loss: {:.6}", best.value);
|
||||||
println!(" Parameters:");
|
println!(" Parameters:");
|
||||||
|
println!(
|
||||||
for (name, value) in &best.params {
|
" learning_rate: {:.6}",
|
||||||
match value {
|
best.get(&learning_rate_param).unwrap()
|
||||||
ParamValue::Float(v) => println!(" {name}: {v:.6}"),
|
);
|
||||||
ParamValue::Int(v) => println!(" {name}: {v}"),
|
println!(" max_depth: {}", best.get(&max_depth_param).unwrap());
|
||||||
ParamValue::Categorical(v) => println!(" {name}: category {v}"),
|
println!(
|
||||||
}
|
" n_estimators: {}",
|
||||||
}
|
best.get(&n_estimators_param).unwrap()
|
||||||
|
);
|
||||||
|
println!(" subsample: {:.6}", best.get(&subsample_param).unwrap());
|
||||||
|
println!(
|
||||||
|
" colsample_bytree: {:.6}",
|
||||||
|
best.get(&colsample_bytree_param).unwrap()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" min_child_weight: {}",
|
||||||
|
best.get(&min_child_weight_param).unwrap()
|
||||||
|
);
|
||||||
|
println!(" reg_alpha: {:.6}", best.get(®_alpha_param).unwrap());
|
||||||
|
println!(
|
||||||
|
" reg_lambda: {:.6}",
|
||||||
|
best.get(®_lambda_param).unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
// Step 5: Use the best parameters (in a real app)
|
// Step 5: Use the best parameters (in a real app)
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
use optimizer::prelude::*;
|
||||||
|
use optimizer_derive::Categorical;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Categorical)]
|
||||||
|
enum Activation {
|
||||||
|
Relu,
|
||||||
|
Sigmoid,
|
||||||
|
Tanh,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let study: Study<f64> = Study::new(Direction::Minimize);
|
||||||
|
|
||||||
|
// Define parameters outside the objective function
|
||||||
|
let lr_param = FloatParam::new(1e-5, 1e-1).name("lr").log_scale();
|
||||||
|
let n_layers_param = IntParam::new(1, 5).name("n_layers");
|
||||||
|
let units_param = IntParam::new(32, 512).name("units").step(32);
|
||||||
|
let optimizer_param = CategoricalParam::new(vec!["sgd", "adam", "rmsprop"]).name("optimizer");
|
||||||
|
let activation_param = EnumParam::<Activation>::new().name("activation");
|
||||||
|
let batch_size_param = IntParam::new(16, 256).name("batch_size").log_scale();
|
||||||
|
let use_dropout_param = BoolParam::new().name("use_dropout");
|
||||||
|
|
||||||
|
study
|
||||||
|
.optimize(20, |trial| {
|
||||||
|
let lr = lr_param.suggest(trial)?;
|
||||||
|
let n_layers = n_layers_param.suggest(trial)?;
|
||||||
|
let units = units_param.suggest(trial)?;
|
||||||
|
let optimizer = optimizer_param.suggest(trial)?;
|
||||||
|
let use_dropout = use_dropout_param.suggest(trial)?;
|
||||||
|
let activation = activation_param.suggest(trial)?;
|
||||||
|
let batch_size = batch_size_param.suggest(trial)?;
|
||||||
|
|
||||||
|
// Simulate a loss function
|
||||||
|
let loss = lr * (n_layers as f64) + (units as f64) * 0.001
|
||||||
|
- if use_dropout { 0.1 } else { 0.0 };
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Trial {}: lr={lr:.6}, layers={n_layers}, units={units}, opt={optimizer}, \
|
||||||
|
dropout={use_dropout}, activation={activation:?}, batch={batch_size} -> loss={loss:.4}",
|
||||||
|
trial.id()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok::<_, Error>(loss)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let best = study.best_trial().unwrap();
|
||||||
|
println!("\nBest trial: value={:.4}", best.value);
|
||||||
|
println!(" lr: {:.6}", best.get(&lr_param).unwrap());
|
||||||
|
println!(" n_layers: {}", best.get(&n_layers_param).unwrap());
|
||||||
|
println!(" units: {}", best.get(&units_param).unwrap());
|
||||||
|
println!(" optimizer: {}", best.get(&optimizer_param).unwrap());
|
||||||
|
println!(" activation: {:?}", best.get(&activation_param).unwrap());
|
||||||
|
println!(" batch_size: {}", best.get(&batch_size_param).unwrap());
|
||||||
|
println!(" use_dropout: {}", best.get(&use_dropout_param).unwrap());
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "optimizer-derive"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
rust-version = "1.88"
|
||||||
|
license = "MIT"
|
||||||
|
description = "Derive macros for the optimizer crate"
|
||||||
|
repository = "https://github.com/raimannma/rust-optimizer"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
syn = { version = "2", features = ["full"] }
|
||||||
|
quote = "1"
|
||||||
|
proc-macro2 = "1"
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{Data, DeriveInput, Fields, parse_macro_input};
|
||||||
|
|
||||||
|
/// Derive macro for the `Categorical` trait on fieldless enums.
|
||||||
|
///
|
||||||
|
/// Generates an implementation of `optimizer::Categorical` that maps
|
||||||
|
/// enum variants to/from sequential indices.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// use optimizer::Categorical;
|
||||||
|
///
|
||||||
|
/// #[derive(Clone, Categorical)]
|
||||||
|
/// enum Color {
|
||||||
|
/// Red,
|
||||||
|
/// Green,
|
||||||
|
/// Blue,
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[proc_macro_derive(Categorical)]
|
||||||
|
pub fn derive_categorical(input: TokenStream) -> TokenStream {
|
||||||
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
let name = &input.ident;
|
||||||
|
|
||||||
|
let Data::Enum(data_enum) = &input.data else {
|
||||||
|
return syn::Error::new_spanned(&input, "Categorical can only be derived for enums")
|
||||||
|
.to_compile_error()
|
||||||
|
.into();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate all variants are fieldless
|
||||||
|
for variant in &data_enum.variants {
|
||||||
|
if !matches!(variant.fields, Fields::Unit) {
|
||||||
|
return syn::Error::new_spanned(
|
||||||
|
variant,
|
||||||
|
"Categorical can only be derived for enums with unit variants (no fields)",
|
||||||
|
)
|
||||||
|
.to_compile_error()
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let n_choices = data_enum.variants.len();
|
||||||
|
let variant_names: Vec<_> = data_enum.variants.iter().map(|v| &v.ident).collect();
|
||||||
|
let indices: Vec<usize> = (0..n_choices).collect();
|
||||||
|
|
||||||
|
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
|
||||||
|
|
||||||
|
let expanded = quote! {
|
||||||
|
impl #impl_generics optimizer::Categorical for #name #ty_generics #where_clause {
|
||||||
|
const N_CHOICES: usize = #n_choices;
|
||||||
|
|
||||||
|
fn from_index(index: usize) -> Self {
|
||||||
|
match index {
|
||||||
|
#(#indices => #name::#variant_names,)*
|
||||||
|
_ => panic!("invalid index {} for {} with {} variants", index, stringify!(#name), #n_choices),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_index(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
#(#name::#variant_names => #indices,)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
expanded.into()
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
//! parameter independently, the multivariate KDE models the joint distribution
|
//! parameter independently, the multivariate KDE models the joint distribution
|
||||||
//! to better capture correlations between parameters.
|
//! to better capture correlations between parameters.
|
||||||
|
|
||||||
use rand::Rng;
|
use rand::{Rng, RngExt};
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
//! This module provides a Gaussian kernel density estimator used by the TPE
|
//! This module provides a Gaussian kernel density estimator used by the TPE
|
||||||
//! sampler to model probability distributions over good and bad trial regions.
|
//! sampler to model probability distributions over good and bad trial regions.
|
||||||
|
|
||||||
use rand::Rng;
|
use rand::{Rng, RngExt};
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
|
|||||||
+78
-27
@@ -28,24 +28,26 @@
|
|||||||
//! # Quick Start
|
//! # Quick Start
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
//! use optimizer::sampler::tpe::TpeSampler;
|
//! use optimizer::prelude::*;
|
||||||
//! use optimizer::{Direction, Study};
|
|
||||||
//!
|
//!
|
||||||
//! // Create a study with TPE sampler
|
//! // Create a study with TPE sampler
|
||||||
//! let sampler = TpeSampler::builder().seed(42).build().unwrap();
|
//! let sampler = TpeSampler::builder().seed(42).build().unwrap();
|
||||||
//! let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
//! let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
//!
|
//!
|
||||||
|
//! // Define parameter search space
|
||||||
|
//! let x = FloatParam::new(-10.0, 10.0).name("x");
|
||||||
|
//!
|
||||||
//! // Optimize x^2 for 20 trials
|
//! // Optimize x^2 for 20 trials
|
||||||
//! study
|
//! study
|
||||||
//! .optimize_with_sampler(20, |trial| {
|
//! .optimize(20, |trial| {
|
||||||
//! let x = trial.suggest_float("x", -10.0, 10.0)?;
|
//! let x_val = x.suggest(trial)?;
|
||||||
//! Ok::<_, optimizer::Error>(x * x)
|
//! Ok::<_, Error>(x_val * x_val)
|
||||||
//! })
|
//! })
|
||||||
//! .unwrap();
|
//! .unwrap();
|
||||||
//!
|
//!
|
||||||
//! // Get the best result
|
//! // Get the best result
|
||||||
//! let best = study.best_trial().unwrap();
|
//! let best = study.best_trial().unwrap();
|
||||||
//! println!("Best value: {} at x={:?}", best.value, best.params);
|
//! println!("x = {}", best.get(&x).unwrap());
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! # Creating a Study
|
//! # Creating a Study
|
||||||
@@ -69,29 +71,35 @@
|
|||||||
//!
|
//!
|
||||||
//! # Suggesting Parameters
|
//! # Suggesting Parameters
|
||||||
//!
|
//!
|
||||||
//! Within the objective function, use [`Trial`] to suggest parameter values:
|
//! Within the objective function, use parameter types to suggest values:
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
|
//! use optimizer::parameter::{BoolParam, CategoricalParam, FloatParam, IntParam, Parameter};
|
||||||
//! use optimizer::{Direction, Study};
|
//! use optimizer::{Direction, Study};
|
||||||
//!
|
//!
|
||||||
//! let study: Study<f64> = Study::new(Direction::Minimize);
|
//! let study: Study<f64> = Study::new(Direction::Minimize);
|
||||||
//!
|
//!
|
||||||
|
//! // Define parameter search spaces
|
||||||
|
//! let x_param = FloatParam::new(0.0, 1.0);
|
||||||
|
//! let lr_param = FloatParam::new(1e-5, 1e-1).log_scale();
|
||||||
|
//! let step_param = FloatParam::new(0.0, 1.0).step(0.1);
|
||||||
|
//! let n_param = IntParam::new(1, 10);
|
||||||
|
//! let batch_param = IntParam::new(16, 256).log_scale();
|
||||||
|
//! let units_param = IntParam::new(32, 512).step(32);
|
||||||
|
//! let flag_param = BoolParam::new();
|
||||||
|
//! let optimizer_param = CategoricalParam::new(vec!["sgd", "adam", "rmsprop"]);
|
||||||
|
//!
|
||||||
//! study
|
//! study
|
||||||
//! .optimize(10, |trial| {
|
//! .optimize(10, |trial| {
|
||||||
//! // Float parameters
|
//! let x = x_param.suggest(trial)?;
|
||||||
//! let x = trial.suggest_float("x", 0.0, 1.0)?;
|
//! let lr = lr_param.suggest(trial)?;
|
||||||
//! let lr = trial.suggest_float_log("learning_rate", 1e-5, 1e-1)?;
|
//! let step = step_param.suggest(trial)?;
|
||||||
//! let step = trial.suggest_float_step("step", 0.0, 1.0, 0.1)?;
|
//! let n = n_param.suggest(trial)?;
|
||||||
|
//! let batch = batch_param.suggest(trial)?;
|
||||||
|
//! let units = units_param.suggest(trial)?;
|
||||||
|
//! let flag = flag_param.suggest(trial)?;
|
||||||
|
//! let optimizer = optimizer_param.suggest(trial)?;
|
||||||
//!
|
//!
|
||||||
//! // Integer parameters
|
|
||||||
//! let n = trial.suggest_int("n_layers", 1, 10)?;
|
|
||||||
//! let batch = trial.suggest_int_log("batch_size", 16, 256)?;
|
|
||||||
//! let units = trial.suggest_int_step("units", 32, 512, 32)?;
|
|
||||||
//!
|
|
||||||
//! // Categorical parameters
|
|
||||||
//! let optimizer = trial.suggest_categorical("optimizer", &["sgd", "adam", "rmsprop"])?;
|
|
||||||
//!
|
|
||||||
//! // Return objective value
|
|
||||||
//! Ok::<_, optimizer::Error>(x * n as f64)
|
//! Ok::<_, optimizer::Error>(x * n as f64)
|
||||||
//! })
|
//! })
|
||||||
//! .unwrap();
|
//! .unwrap();
|
||||||
@@ -147,35 +155,78 @@
|
|||||||
//!
|
//!
|
||||||
//! ```ignore
|
//! ```ignore
|
||||||
//! use optimizer::{Study, Direction};
|
//! use optimizer::{Study, Direction};
|
||||||
|
//! use optimizer::parameter::{FloatParam, Parameter};
|
||||||
|
//!
|
||||||
|
//! let x_param = FloatParam::new(0.0, 1.0);
|
||||||
//!
|
//!
|
||||||
//! // Sequential async
|
//! // Sequential async
|
||||||
//! study.optimize_async(10, |mut trial| async move {
|
//! study.optimize_async(10, |mut trial| {
|
||||||
//! let x = trial.suggest_float("x", 0.0, 1.0)?;
|
//! let x_param = x_param.clone();
|
||||||
//! Ok((trial, x * x))
|
//! async move {
|
||||||
|
//! let x = x_param.suggest(&mut trial)?;
|
||||||
|
//! Ok((trial, x * x))
|
||||||
|
//! }
|
||||||
//! }).await?;
|
//! }).await?;
|
||||||
//!
|
//!
|
||||||
//! // Parallel with bounded concurrency
|
//! // Parallel with bounded concurrency
|
||||||
//! study.optimize_parallel(10, 4, |mut trial| async move {
|
//! study.optimize_parallel(10, 4, |mut trial| {
|
||||||
//! let x = trial.suggest_float("x", 0.0, 1.0)?;
|
//! let x_param = x_param.clone();
|
||||||
//! Ok((trial, x * x))
|
//! async move {
|
||||||
|
//! let x = x_param.suggest(&mut trial)?;
|
||||||
|
//! Ok((trial, x * x))
|
||||||
|
//! }
|
||||||
//! }).await?;
|
//! }).await?;
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! # Feature Flags
|
//! # Feature Flags
|
||||||
//!
|
//!
|
||||||
//! - `async`: Enable async optimization methods (requires tokio)
|
//! - `async`: Enable async optimization methods (requires tokio)
|
||||||
|
//! - `derive`: Enable `#[derive(Categorical)]` for enum parameters
|
||||||
|
|
||||||
mod distribution;
|
mod distribution;
|
||||||
mod error;
|
mod error;
|
||||||
mod kde;
|
mod kde;
|
||||||
mod param;
|
mod param;
|
||||||
|
pub mod parameter;
|
||||||
pub mod sampler;
|
pub mod sampler;
|
||||||
mod study;
|
mod study;
|
||||||
mod trial;
|
mod trial;
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
pub use error::{Error, Result};
|
pub use error::{Error, Result};
|
||||||
|
#[cfg(feature = "derive")]
|
||||||
|
pub use optimizer_derive::Categorical;
|
||||||
pub use param::ParamValue;
|
pub use param::ParamValue;
|
||||||
|
pub use parameter::{
|
||||||
|
BoolParam, Categorical, CategoricalParam, EnumParam, FloatParam, IntParam, ParamId, Parameter,
|
||||||
|
};
|
||||||
|
pub use sampler::CompletedTrial;
|
||||||
|
pub use sampler::grid::GridSearchSampler;
|
||||||
|
pub use sampler::random::RandomSampler;
|
||||||
|
pub use sampler::tpe::TpeSampler;
|
||||||
pub use study::Study;
|
pub use study::Study;
|
||||||
pub use trial::{SuggestableRange, Trial};
|
pub use trial::Trial;
|
||||||
pub use types::{Direction, TrialState};
|
pub use types::{Direction, TrialState};
|
||||||
|
|
||||||
|
/// Convenient wildcard import for the most common types.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use optimizer::prelude::*;
|
||||||
|
/// ```
|
||||||
|
pub mod prelude {
|
||||||
|
#[cfg(feature = "derive")]
|
||||||
|
pub use optimizer_derive::Categorical as DeriveCategory;
|
||||||
|
|
||||||
|
pub use crate::error::{Error, Result};
|
||||||
|
pub use crate::param::ParamValue;
|
||||||
|
pub use crate::parameter::{
|
||||||
|
BoolParam, Categorical, CategoricalParam, EnumParam, FloatParam, IntParam, Parameter,
|
||||||
|
};
|
||||||
|
pub use crate::sampler::CompletedTrial;
|
||||||
|
pub use crate::sampler::grid::GridSearchSampler;
|
||||||
|
pub use crate::sampler::random::RandomSampler;
|
||||||
|
pub use crate::sampler::tpe::TpeSampler;
|
||||||
|
pub use crate::study::Study;
|
||||||
|
pub use crate::trial::Trial;
|
||||||
|
pub use crate::types::Direction;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,3 +14,13 @@ pub enum ParamValue {
|
|||||||
/// A categorical parameter value, stored as an index into the choices array.
|
/// A categorical parameter value, stored as an index into the choices array.
|
||||||
Categorical(usize),
|
Categorical(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for ParamValue {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Float(v) => write!(f, "{v}"),
|
||||||
|
Self::Int(v) => write!(f, "{v}"),
|
||||||
|
Self::Categorical(v) => write!(f, "category({v})"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,995 @@
|
|||||||
|
//! Central parameter trait and built-in parameter types.
|
||||||
|
//!
|
||||||
|
//! The [`Parameter`] trait provides a unified way to define parameter types
|
||||||
|
//! and suggest values from a [`Trial`]. Built-in implementations
|
||||||
|
//! cover floats, integers, categoricals, booleans, and enum types.
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use optimizer::Trial;
|
||||||
|
//! use optimizer::parameter::{BoolParam, FloatParam, IntParam, Parameter};
|
||||||
|
//!
|
||||||
|
//! let mut trial = Trial::new(0);
|
||||||
|
//!
|
||||||
|
//! let lr = FloatParam::new(1e-5, 1e-1)
|
||||||
|
//! .log_scale()
|
||||||
|
//! .suggest(&mut trial)
|
||||||
|
//! .unwrap();
|
||||||
|
//! let layers = IntParam::new(1, 10).suggest(&mut trial).unwrap();
|
||||||
|
//! let dropout = BoolParam::new().suggest(&mut trial).unwrap();
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use core::fmt::Debug;
|
||||||
|
use core::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
|
use crate::distribution::{
|
||||||
|
CategoricalDistribution, Distribution, FloatDistribution, IntDistribution,
|
||||||
|
};
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
use crate::param::ParamValue;
|
||||||
|
use crate::trial::Trial;
|
||||||
|
|
||||||
|
static NEXT_PARAM_ID: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
/// A unique identifier for a parameter instance.
|
||||||
|
///
|
||||||
|
/// Each parameter is assigned a unique `ParamId` at creation time. Cloning a parameter
|
||||||
|
/// copies its `ParamId`, so clones refer to the same logical parameter.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ParamId(u64);
|
||||||
|
|
||||||
|
impl ParamId {
|
||||||
|
/// Creates a new unique `ParamId`.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self(NEXT_PARAM_ID.fetch_add(1, Ordering::Relaxed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ParamId {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for ParamId {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
write!(f, "param_{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait for defining parameter types that can be suggested by a [`Trial`].
|
||||||
|
///
|
||||||
|
/// Implementors specify the distribution to sample from and how to convert
|
||||||
|
/// the raw [`ParamValue`] back into a typed value.
|
||||||
|
pub trait Parameter: Debug {
|
||||||
|
/// The typed value returned after sampling.
|
||||||
|
type Value;
|
||||||
|
|
||||||
|
/// Returns the unique identifier for this parameter.
|
||||||
|
fn id(&self) -> ParamId;
|
||||||
|
|
||||||
|
/// Returns the distribution that this parameter samples from.
|
||||||
|
fn distribution(&self) -> Distribution;
|
||||||
|
|
||||||
|
/// Converts a raw [`ParamValue`] into the typed value.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the `ParamValue` variant doesn't match what this parameter expects.
|
||||||
|
fn cast_param_value(&self, param_value: &ParamValue) -> Result<Self::Value>;
|
||||||
|
|
||||||
|
/// Validates the parameter configuration.
|
||||||
|
///
|
||||||
|
/// Called before sampling. The default implementation accepts all configurations.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the parameter configuration is invalid.
|
||||||
|
fn validate(&self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a human-readable label for this parameter.
|
||||||
|
///
|
||||||
|
/// Defaults to the `Debug` output of the parameter.
|
||||||
|
fn label(&self) -> String {
|
||||||
|
format!("{self:?}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Suggests a value for this parameter from the given trial.
|
||||||
|
///
|
||||||
|
/// This is a convenience method that delegates to [`Trial::suggest_param`].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if validation fails, the parameter conflicts with
|
||||||
|
/// a previously suggested parameter of the same id, or sampling fails.
|
||||||
|
fn suggest(&self, trial: &mut Trial) -> Result<Self::Value>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
trial.suggest_param(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A floating-point parameter with optional log-scale and step size.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use optimizer::Trial;
|
||||||
|
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||||
|
///
|
||||||
|
/// let mut trial = Trial::new(0);
|
||||||
|
///
|
||||||
|
/// // Simple range
|
||||||
|
/// let x = FloatParam::new(0.0, 1.0).suggest(&mut trial).unwrap();
|
||||||
|
///
|
||||||
|
/// // Log-scale
|
||||||
|
/// let lr = FloatParam::new(1e-5, 1e-1)
|
||||||
|
/// .log_scale()
|
||||||
|
/// .suggest(&mut trial)
|
||||||
|
/// .unwrap();
|
||||||
|
///
|
||||||
|
/// // Stepped
|
||||||
|
/// let step = FloatParam::new(0.0, 1.0)
|
||||||
|
/// .step(0.25)
|
||||||
|
/// .suggest(&mut trial)
|
||||||
|
/// .unwrap();
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct FloatParam {
|
||||||
|
id: ParamId,
|
||||||
|
low: f64,
|
||||||
|
high: f64,
|
||||||
|
log_scale: bool,
|
||||||
|
step: Option<f64>,
|
||||||
|
name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FloatParam {
|
||||||
|
/// Creates a new float parameter with the given bounds.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(low: f64, high: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
id: ParamId::new(),
|
||||||
|
low,
|
||||||
|
high,
|
||||||
|
log_scale: false,
|
||||||
|
step: None,
|
||||||
|
name: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enables log-scale sampling.
|
||||||
|
#[must_use]
|
||||||
|
pub fn log_scale(mut self) -> Self {
|
||||||
|
self.log_scale = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a step size for discretized sampling.
|
||||||
|
#[must_use]
|
||||||
|
pub fn step(mut self, step: f64) -> Self {
|
||||||
|
self.step = Some(step);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a human-readable name for this parameter.
|
||||||
|
///
|
||||||
|
/// When set, this name is used as the parameter's label instead of
|
||||||
|
/// the default `Debug` output.
|
||||||
|
#[must_use]
|
||||||
|
pub fn name(mut self, name: impl Into<String>) -> Self {
|
||||||
|
self.name = Some(name.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parameter for FloatParam {
|
||||||
|
type Value = f64;
|
||||||
|
|
||||||
|
fn id(&self) -> ParamId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn distribution(&self) -> Distribution {
|
||||||
|
Distribution::Float(FloatDistribution {
|
||||||
|
low: self.low,
|
||||||
|
high: self.high,
|
||||||
|
log_scale: self.log_scale,
|
||||||
|
step: self.step,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cast_param_value(&self, param_value: &ParamValue) -> Result<f64> {
|
||||||
|
match param_value {
|
||||||
|
ParamValue::Float(v) => Ok(*v),
|
||||||
|
_ => Err(Error::Internal(
|
||||||
|
"Float distribution should return Float value",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate(&self) -> Result<()> {
|
||||||
|
if self.low > self.high {
|
||||||
|
return Err(Error::InvalidBounds {
|
||||||
|
low: self.low,
|
||||||
|
high: self.high,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if self.log_scale && self.low <= 0.0 {
|
||||||
|
return Err(Error::InvalidLogBounds);
|
||||||
|
}
|
||||||
|
if let Some(step) = self.step
|
||||||
|
&& step <= 0.0
|
||||||
|
{
|
||||||
|
return Err(Error::InvalidStep);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(&self) -> String {
|
||||||
|
self.name.clone().unwrap_or_else(|| format!("{self:?}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An integer parameter with optional log-scale and step size.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use optimizer::Trial;
|
||||||
|
/// use optimizer::parameter::{IntParam, Parameter};
|
||||||
|
///
|
||||||
|
/// let mut trial = Trial::new(0);
|
||||||
|
///
|
||||||
|
/// // Simple range
|
||||||
|
/// let n = IntParam::new(1, 10).suggest(&mut trial).unwrap();
|
||||||
|
///
|
||||||
|
/// // Log-scale
|
||||||
|
/// let batch = IntParam::new(1, 1024)
|
||||||
|
/// .log_scale()
|
||||||
|
/// .suggest(&mut trial)
|
||||||
|
/// .unwrap();
|
||||||
|
///
|
||||||
|
/// // Stepped
|
||||||
|
/// let units = IntParam::new(32, 512).step(32).suggest(&mut trial).unwrap();
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct IntParam {
|
||||||
|
id: ParamId,
|
||||||
|
low: i64,
|
||||||
|
high: i64,
|
||||||
|
log_scale: bool,
|
||||||
|
step: Option<i64>,
|
||||||
|
name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntParam {
|
||||||
|
/// Creates a new integer parameter with the given bounds.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(low: i64, high: i64) -> Self {
|
||||||
|
Self {
|
||||||
|
id: ParamId::new(),
|
||||||
|
low,
|
||||||
|
high,
|
||||||
|
log_scale: false,
|
||||||
|
step: None,
|
||||||
|
name: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enables log-scale sampling.
|
||||||
|
#[must_use]
|
||||||
|
pub fn log_scale(mut self) -> Self {
|
||||||
|
self.log_scale = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a step size for discretized sampling.
|
||||||
|
#[must_use]
|
||||||
|
pub fn step(mut self, step: i64) -> Self {
|
||||||
|
self.step = Some(step);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a human-readable name for this parameter.
|
||||||
|
///
|
||||||
|
/// When set, this name is used as the parameter's label instead of
|
||||||
|
/// the default `Debug` output.
|
||||||
|
#[must_use]
|
||||||
|
pub fn name(mut self, name: impl Into<String>) -> Self {
|
||||||
|
self.name = Some(name.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parameter for IntParam {
|
||||||
|
type Value = i64;
|
||||||
|
|
||||||
|
fn id(&self) -> ParamId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn distribution(&self) -> Distribution {
|
||||||
|
Distribution::Int(IntDistribution {
|
||||||
|
low: self.low,
|
||||||
|
high: self.high,
|
||||||
|
log_scale: self.log_scale,
|
||||||
|
step: self.step,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::cast_precision_loss)]
|
||||||
|
fn cast_param_value(&self, param_value: &ParamValue) -> Result<i64> {
|
||||||
|
match param_value {
|
||||||
|
ParamValue::Int(v) => Ok(*v),
|
||||||
|
_ => Err(Error::Internal("Int distribution should return Int value")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::cast_precision_loss)]
|
||||||
|
fn validate(&self) -> Result<()> {
|
||||||
|
if self.low > self.high {
|
||||||
|
return Err(Error::InvalidBounds {
|
||||||
|
low: self.low as f64,
|
||||||
|
high: self.high as f64,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if self.log_scale && self.low < 1 {
|
||||||
|
return Err(Error::InvalidLogBounds);
|
||||||
|
}
|
||||||
|
if let Some(step) = self.step
|
||||||
|
&& step <= 0
|
||||||
|
{
|
||||||
|
return Err(Error::InvalidStep);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(&self) -> String {
|
||||||
|
self.name.clone().unwrap_or_else(|| format!("{self:?}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A categorical parameter that selects from a list of choices.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use optimizer::Trial;
|
||||||
|
/// use optimizer::parameter::{CategoricalParam, Parameter};
|
||||||
|
///
|
||||||
|
/// let mut trial = Trial::new(0);
|
||||||
|
/// let opt = CategoricalParam::new(vec!["sgd", "adam", "rmsprop"])
|
||||||
|
/// .suggest(&mut trial)
|
||||||
|
/// .unwrap();
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct CategoricalParam<T: Clone> {
|
||||||
|
id: ParamId,
|
||||||
|
choices: Vec<T>,
|
||||||
|
name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> CategoricalParam<T> {
|
||||||
|
/// Creates a new categorical parameter with the given choices.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(choices: Vec<T>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: ParamId::new(),
|
||||||
|
choices,
|
||||||
|
name: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a human-readable name for this parameter.
|
||||||
|
///
|
||||||
|
/// When set, this name is used as the parameter's label instead of
|
||||||
|
/// the default `Debug` output.
|
||||||
|
#[must_use]
|
||||||
|
pub fn name(mut self, name: impl Into<String>) -> Self {
|
||||||
|
self.name = Some(name.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + Debug> Parameter for CategoricalParam<T> {
|
||||||
|
type Value = T;
|
||||||
|
|
||||||
|
fn id(&self) -> ParamId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn distribution(&self) -> Distribution {
|
||||||
|
Distribution::Categorical(CategoricalDistribution {
|
||||||
|
n_choices: self.choices.len(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cast_param_value(&self, param_value: &ParamValue) -> Result<T> {
|
||||||
|
match param_value {
|
||||||
|
ParamValue::Categorical(index) => Ok(self.choices[*index].clone()),
|
||||||
|
_ => Err(Error::Internal(
|
||||||
|
"Categorical distribution should return Categorical value",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate(&self) -> Result<()> {
|
||||||
|
if self.choices.is_empty() {
|
||||||
|
return Err(Error::EmptyChoices);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(&self) -> String {
|
||||||
|
self.name.clone().unwrap_or_else(|| format!("{self:?}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A boolean parameter (equivalent to a categorical with `[false, true]`).
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use optimizer::Trial;
|
||||||
|
/// use optimizer::parameter::{BoolParam, Parameter};
|
||||||
|
///
|
||||||
|
/// let mut trial = Trial::new(0);
|
||||||
|
/// let dropout = BoolParam::new().suggest(&mut trial).unwrap();
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct BoolParam {
|
||||||
|
id: ParamId,
|
||||||
|
name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoolParam {
|
||||||
|
/// Creates a new boolean parameter.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
id: ParamId::new(),
|
||||||
|
name: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a human-readable name for this parameter.
|
||||||
|
///
|
||||||
|
/// When set, this name is used as the parameter's label instead of
|
||||||
|
/// the default `Debug` output.
|
||||||
|
#[must_use]
|
||||||
|
pub fn name(mut self, name: impl Into<String>) -> Self {
|
||||||
|
self.name = Some(name.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BoolParam {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parameter for BoolParam {
|
||||||
|
type Value = bool;
|
||||||
|
|
||||||
|
fn id(&self) -> ParamId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn distribution(&self) -> Distribution {
|
||||||
|
Distribution::Categorical(CategoricalDistribution { n_choices: 2 })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cast_param_value(&self, param_value: &ParamValue) -> Result<bool> {
|
||||||
|
match param_value {
|
||||||
|
ParamValue::Categorical(index) => Ok(*index != 0),
|
||||||
|
_ => Err(Error::Internal(
|
||||||
|
"Categorical distribution should return Categorical value",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(&self) -> String {
|
||||||
|
self.name.clone().unwrap_or_else(|| format!("{self:?}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait for enum types that can be used as categorical parameters.
|
||||||
|
///
|
||||||
|
/// This trait maps enum variants to sequential indices and back. It can be
|
||||||
|
/// derived automatically for fieldless enums using `#[derive(Categorical)]`
|
||||||
|
/// when the `derive` feature is enabled.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// Manual implementation:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use optimizer::parameter::Categorical;
|
||||||
|
///
|
||||||
|
/// #[derive(Clone)]
|
||||||
|
/// enum Activation {
|
||||||
|
/// Relu,
|
||||||
|
/// Sigmoid,
|
||||||
|
/// Tanh,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl Categorical for Activation {
|
||||||
|
/// const N_CHOICES: usize = 3;
|
||||||
|
///
|
||||||
|
/// fn from_index(index: usize) -> Self {
|
||||||
|
/// match index {
|
||||||
|
/// 0 => Activation::Relu,
|
||||||
|
/// 1 => Activation::Sigmoid,
|
||||||
|
/// 2 => Activation::Tanh,
|
||||||
|
/// _ => panic!("invalid index"),
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn to_index(&self) -> usize {
|
||||||
|
/// match self {
|
||||||
|
/// Activation::Relu => 0,
|
||||||
|
/// Activation::Sigmoid => 1,
|
||||||
|
/// Activation::Tanh => 2,
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub trait Categorical: Sized + Clone {
|
||||||
|
/// The number of variants in the enum.
|
||||||
|
const N_CHOICES: usize;
|
||||||
|
|
||||||
|
/// Creates an instance from a variant index.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if `index >= N_CHOICES`.
|
||||||
|
fn from_index(index: usize) -> Self;
|
||||||
|
|
||||||
|
/// Returns the index of this variant.
|
||||||
|
fn to_index(&self) -> usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A parameter that selects from the variants of an enum implementing [`Categorical`].
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use optimizer::Trial;
|
||||||
|
/// use optimizer::parameter::{Categorical, EnumParam, Parameter};
|
||||||
|
///
|
||||||
|
/// #[derive(Clone, Debug)]
|
||||||
|
/// enum Optimizer {
|
||||||
|
/// Sgd,
|
||||||
|
/// Adam,
|
||||||
|
/// Rmsprop,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl Categorical for Optimizer {
|
||||||
|
/// const N_CHOICES: usize = 3;
|
||||||
|
/// fn from_index(index: usize) -> Self {
|
||||||
|
/// match index {
|
||||||
|
/// 0 => Optimizer::Sgd,
|
||||||
|
/// 1 => Optimizer::Adam,
|
||||||
|
/// 2 => Optimizer::Rmsprop,
|
||||||
|
/// _ => panic!("invalid index"),
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// fn to_index(&self) -> usize {
|
||||||
|
/// match self {
|
||||||
|
/// Optimizer::Sgd => 0,
|
||||||
|
/// Optimizer::Adam => 1,
|
||||||
|
/// Optimizer::Rmsprop => 2,
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let mut trial = Trial::new(0);
|
||||||
|
/// let opt = EnumParam::<Optimizer>::new().suggest(&mut trial).unwrap();
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct EnumParam<T: Categorical> {
|
||||||
|
id: ParamId,
|
||||||
|
name: Option<String>,
|
||||||
|
_marker: core::marker::PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Categorical> EnumParam<T> {
|
||||||
|
/// Creates a new enum parameter.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
id: ParamId::new(),
|
||||||
|
name: None,
|
||||||
|
_marker: core::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a human-readable name for this parameter.
|
||||||
|
///
|
||||||
|
/// When set, this name is used as the parameter's label instead of
|
||||||
|
/// the default `Debug` output.
|
||||||
|
#[must_use]
|
||||||
|
pub fn name(mut self, name: impl Into<String>) -> Self {
|
||||||
|
self.name = Some(name.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Categorical> Default for EnumParam<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Categorical + Debug> Parameter for EnumParam<T> {
|
||||||
|
type Value = T;
|
||||||
|
|
||||||
|
fn id(&self) -> ParamId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn distribution(&self) -> Distribution {
|
||||||
|
Distribution::Categorical(CategoricalDistribution {
|
||||||
|
n_choices: T::N_CHOICES,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cast_param_value(&self, param_value: &ParamValue) -> Result<T> {
|
||||||
|
match param_value {
|
||||||
|
ParamValue::Categorical(index) => Ok(T::from_index(*index)),
|
||||||
|
_ => Err(Error::Internal(
|
||||||
|
"Categorical distribution should return Categorical value",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(&self) -> String {
|
||||||
|
self.name.clone().unwrap_or_else(|| format!("{self:?}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn float_param_distribution() {
|
||||||
|
let param = FloatParam::new(0.0, 1.0);
|
||||||
|
assert_eq!(
|
||||||
|
param.distribution(),
|
||||||
|
Distribution::Float(FloatDistribution {
|
||||||
|
low: 0.0,
|
||||||
|
high: 1.0,
|
||||||
|
log_scale: false,
|
||||||
|
step: None,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn float_param_log_scale() {
|
||||||
|
let param = FloatParam::new(1e-5, 1e-1).log_scale();
|
||||||
|
assert_eq!(
|
||||||
|
param.distribution(),
|
||||||
|
Distribution::Float(FloatDistribution {
|
||||||
|
low: 1e-5,
|
||||||
|
high: 1e-1,
|
||||||
|
log_scale: true,
|
||||||
|
step: None,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn float_param_step() {
|
||||||
|
let param = FloatParam::new(0.0, 1.0).step(0.25);
|
||||||
|
assert_eq!(
|
||||||
|
param.distribution(),
|
||||||
|
Distribution::Float(FloatDistribution {
|
||||||
|
low: 0.0,
|
||||||
|
high: 1.0,
|
||||||
|
log_scale: false,
|
||||||
|
step: Some(0.25),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn float_param_validate_invalid_bounds() {
|
||||||
|
let param = FloatParam::new(1.0, 0.0);
|
||||||
|
assert!(param.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn float_param_validate_invalid_log() {
|
||||||
|
let param = FloatParam::new(-1.0, 1.0).log_scale();
|
||||||
|
assert!(param.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn float_param_validate_invalid_step() {
|
||||||
|
let param = FloatParam::new(0.0, 1.0).step(-0.1);
|
||||||
|
assert!(param.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(clippy::float_cmp)]
|
||||||
|
fn float_param_cast_param_value() {
|
||||||
|
let param = FloatParam::new(0.0, 1.0);
|
||||||
|
assert_eq!(
|
||||||
|
param.cast_param_value(&ParamValue::Float(0.5)).unwrap(),
|
||||||
|
0.5
|
||||||
|
);
|
||||||
|
assert!(param.cast_param_value(&ParamValue::Int(1)).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn int_param_distribution() {
|
||||||
|
let param = IntParam::new(1, 100);
|
||||||
|
assert_eq!(
|
||||||
|
param.distribution(),
|
||||||
|
Distribution::Int(IntDistribution {
|
||||||
|
low: 1,
|
||||||
|
high: 100,
|
||||||
|
log_scale: false,
|
||||||
|
step: None,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn int_param_log_scale() {
|
||||||
|
let param = IntParam::new(1, 1024).log_scale();
|
||||||
|
assert_eq!(
|
||||||
|
param.distribution(),
|
||||||
|
Distribution::Int(IntDistribution {
|
||||||
|
low: 1,
|
||||||
|
high: 1024,
|
||||||
|
log_scale: true,
|
||||||
|
step: None,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn int_param_step() {
|
||||||
|
let param = IntParam::new(100, 500).step(50);
|
||||||
|
assert_eq!(
|
||||||
|
param.distribution(),
|
||||||
|
Distribution::Int(IntDistribution {
|
||||||
|
low: 100,
|
||||||
|
high: 500,
|
||||||
|
log_scale: false,
|
||||||
|
step: Some(50),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn int_param_validate_invalid_bounds() {
|
||||||
|
let param = IntParam::new(10, 1);
|
||||||
|
assert!(param.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn int_param_validate_invalid_log() {
|
||||||
|
let param = IntParam::new(0, 10).log_scale();
|
||||||
|
assert!(param.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn int_param_validate_invalid_step() {
|
||||||
|
let param = IntParam::new(0, 10).step(-1);
|
||||||
|
assert!(param.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn int_param_cast_param_value() {
|
||||||
|
let param = IntParam::new(1, 10);
|
||||||
|
assert_eq!(param.cast_param_value(&ParamValue::Int(5)).unwrap(), 5);
|
||||||
|
assert!(param.cast_param_value(&ParamValue::Float(1.0)).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn categorical_param_distribution() {
|
||||||
|
let param = CategoricalParam::new(vec!["a", "b", "c"]);
|
||||||
|
assert_eq!(
|
||||||
|
param.distribution(),
|
||||||
|
Distribution::Categorical(CategoricalDistribution { n_choices: 3 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn categorical_param_validate_empty() {
|
||||||
|
let param = CategoricalParam::<&str>::new(vec![]);
|
||||||
|
assert!(param.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn categorical_param_cast_param_value() {
|
||||||
|
let param = CategoricalParam::new(vec!["sgd", "adam", "rmsprop"]);
|
||||||
|
assert_eq!(
|
||||||
|
param.cast_param_value(&ParamValue::Categorical(1)).unwrap(),
|
||||||
|
"adam"
|
||||||
|
);
|
||||||
|
assert!(param.cast_param_value(&ParamValue::Float(1.0)).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bool_param_distribution() {
|
||||||
|
let param = BoolParam::new();
|
||||||
|
assert_eq!(
|
||||||
|
param.distribution(),
|
||||||
|
Distribution::Categorical(CategoricalDistribution { n_choices: 2 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bool_param_cast_param_value() {
|
||||||
|
let param = BoolParam::new();
|
||||||
|
assert!(!param.cast_param_value(&ParamValue::Categorical(0)).unwrap());
|
||||||
|
assert!(param.cast_param_value(&ParamValue::Categorical(1)).unwrap());
|
||||||
|
assert!(param.cast_param_value(&ParamValue::Float(1.0)).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
enum TestEnum {
|
||||||
|
A,
|
||||||
|
B,
|
||||||
|
C,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Categorical for TestEnum {
|
||||||
|
const N_CHOICES: usize = 3;
|
||||||
|
|
||||||
|
fn from_index(index: usize) -> Self {
|
||||||
|
match index {
|
||||||
|
0 => TestEnum::A,
|
||||||
|
1 => TestEnum::B,
|
||||||
|
2 => TestEnum::C,
|
||||||
|
_ => panic!("invalid index"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_index(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
TestEnum::A => 0,
|
||||||
|
TestEnum::B => 1,
|
||||||
|
TestEnum::C => 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enum_param_distribution() {
|
||||||
|
let param = EnumParam::<TestEnum>::new();
|
||||||
|
assert_eq!(
|
||||||
|
param.distribution(),
|
||||||
|
Distribution::Categorical(CategoricalDistribution { n_choices: 3 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enum_param_cast_param_value() {
|
||||||
|
let param = EnumParam::<TestEnum>::new();
|
||||||
|
assert_eq!(
|
||||||
|
param.cast_param_value(&ParamValue::Categorical(0)).unwrap(),
|
||||||
|
TestEnum::A
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
param.cast_param_value(&ParamValue::Categorical(2)).unwrap(),
|
||||||
|
TestEnum::C
|
||||||
|
);
|
||||||
|
assert!(param.cast_param_value(&ParamValue::Float(1.0)).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn float_param_suggest_via_trial() {
|
||||||
|
let param = FloatParam::new(0.0, 1.0);
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
let x = param.suggest(&mut trial).unwrap();
|
||||||
|
assert!((0.0..=1.0).contains(&x));
|
||||||
|
|
||||||
|
// Cached value (same param id) - exact equality expected for cached values
|
||||||
|
let x2 = param.suggest(&mut trial).unwrap();
|
||||||
|
assert!((x - x2).abs() < f64::EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn int_param_suggest_via_trial() {
|
||||||
|
let param = IntParam::new(1, 10);
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
let n = param.suggest(&mut trial).unwrap();
|
||||||
|
assert!((1..=10).contains(&n));
|
||||||
|
|
||||||
|
// Cached value
|
||||||
|
let n2 = param.suggest(&mut trial).unwrap();
|
||||||
|
assert_eq!(n, n2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn categorical_param_suggest_via_trial() {
|
||||||
|
let choices = vec!["sgd", "adam", "rmsprop"];
|
||||||
|
let param = CategoricalParam::new(choices.clone());
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
let opt = param.suggest(&mut trial).unwrap();
|
||||||
|
assert!(choices.contains(&opt));
|
||||||
|
|
||||||
|
// Cached value
|
||||||
|
let opt2 = param.suggest(&mut trial).unwrap();
|
||||||
|
assert_eq!(opt, opt2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bool_param_suggest_via_trial() {
|
||||||
|
let param = BoolParam::new();
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
let val = param.suggest(&mut trial).unwrap();
|
||||||
|
let _ = val; // just check it doesn't error
|
||||||
|
|
||||||
|
// Cached value
|
||||||
|
let val2 = param.suggest(&mut trial).unwrap();
|
||||||
|
assert_eq!(val, val2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enum_param_suggest_via_trial() {
|
||||||
|
let param = EnumParam::<TestEnum>::new();
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
let val = param.suggest(&mut trial).unwrap();
|
||||||
|
assert!([TestEnum::A, TestEnum::B, TestEnum::C].contains(&val));
|
||||||
|
|
||||||
|
// Cached value
|
||||||
|
let val2 = param.suggest(&mut trial).unwrap();
|
||||||
|
assert_eq!(val, val2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parameter_conflict_detection() {
|
||||||
|
let param_float = FloatParam::new(0.0, 1.0);
|
||||||
|
let param_int = IntParam::new(0, 10);
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
let _ = param_float.suggest(&mut trial).unwrap();
|
||||||
|
|
||||||
|
// Different param with different distribution but same id won't happen
|
||||||
|
// since each param gets a unique id. But different param object = different id = no conflict.
|
||||||
|
let result = param_int.suggest(&mut trial);
|
||||||
|
assert!(result.is_ok()); // Different id, no conflict
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn float_param_validation_prevents_suggest() {
|
||||||
|
let param = FloatParam::new(1.0, 0.0);
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
let result = param.suggest(&mut trial);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn categorical_trait_roundtrip() {
|
||||||
|
for i in 0..TestEnum::N_CHOICES {
|
||||||
|
let val = TestEnum::from_index(i);
|
||||||
|
assert_eq!(val.to_index(), i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn param_id_uniqueness() {
|
||||||
|
let id1 = ParamId::new();
|
||||||
|
let id2 = ParamId::new();
|
||||||
|
assert_ne!(id1, id2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn param_clone_preserves_id() {
|
||||||
|
let param = FloatParam::new(0.0, 1.0);
|
||||||
|
let cloned = param.clone();
|
||||||
|
assert_eq!(param.id(), cloned.id());
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
-59
@@ -1,18 +1,14 @@
|
|||||||
//! Sampler trait and implementations for parameter sampling.
|
//! Sampler trait and implementations for parameter sampling.
|
||||||
|
|
||||||
pub mod grid;
|
pub mod grid;
|
||||||
pub mod multivariate_tpe;
|
|
||||||
pub mod random;
|
pub mod random;
|
||||||
pub mod tpe;
|
pub mod tpe;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub use multivariate_tpe::{
|
|
||||||
ConstantLiarStrategy, MultivariateTpeSampler, MultivariateTpeSamplerBuilder,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::distribution::Distribution;
|
use crate::distribution::Distribution;
|
||||||
use crate::param::ParamValue;
|
use crate::param::ParamValue;
|
||||||
|
use crate::parameter::{ParamId, Parameter};
|
||||||
|
|
||||||
/// A completed trial with its parameters, distributions, and objective value.
|
/// A completed trial with its parameters, distributions, and objective value.
|
||||||
///
|
///
|
||||||
@@ -23,10 +19,12 @@ use crate::param::ParamValue;
|
|||||||
pub struct CompletedTrial<V = f64> {
|
pub struct CompletedTrial<V = f64> {
|
||||||
/// The unique identifier for this trial.
|
/// The unique identifier for this trial.
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
/// The sampled parameter values, keyed by parameter name.
|
/// The sampled parameter values, keyed by parameter id.
|
||||||
pub params: HashMap<String, ParamValue>,
|
pub params: HashMap<ParamId, ParamValue>,
|
||||||
/// The parameter distributions used, keyed by parameter name.
|
/// The parameter distributions used, keyed by parameter id.
|
||||||
pub distributions: HashMap<String, Distribution>,
|
pub distributions: HashMap<ParamId, Distribution>,
|
||||||
|
/// Human-readable labels for parameters, keyed by parameter id.
|
||||||
|
pub param_labels: HashMap<ParamId, String>,
|
||||||
/// The objective value returned by the objective function.
|
/// The objective value returned by the objective function.
|
||||||
pub value: V,
|
pub value: V,
|
||||||
}
|
}
|
||||||
@@ -35,17 +33,60 @@ impl<V> CompletedTrial<V> {
|
|||||||
/// Creates a new completed trial.
|
/// Creates a new completed trial.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
id: u64,
|
id: u64,
|
||||||
params: HashMap<String, ParamValue>,
|
params: HashMap<ParamId, ParamValue>,
|
||||||
distributions: HashMap<String, Distribution>,
|
distributions: HashMap<ParamId, Distribution>,
|
||||||
|
param_labels: HashMap<ParamId, String>,
|
||||||
value: V,
|
value: V,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
params,
|
params,
|
||||||
distributions,
|
distributions,
|
||||||
|
param_labels,
|
||||||
value,
|
value,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the typed value for the given parameter.
|
||||||
|
///
|
||||||
|
/// Looks up the parameter by its unique id and casts the stored
|
||||||
|
/// [`ParamValue`] to the parameter's typed value.
|
||||||
|
///
|
||||||
|
/// Returns `None` if the parameter was not used in this trial.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the stored value is incompatible with the parameter type
|
||||||
|
/// (e.g., a `Float` value stored for an `IntParam`). This indicates
|
||||||
|
/// a bug in the program, not a runtime error.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||||
|
/// use optimizer::{Direction, Study};
|
||||||
|
///
|
||||||
|
/// let study: Study<f64> = Study::new(Direction::Minimize);
|
||||||
|
/// let x = FloatParam::new(-10.0, 10.0);
|
||||||
|
///
|
||||||
|
/// study
|
||||||
|
/// .optimize(5, |trial| {
|
||||||
|
/// let val = x.suggest(trial)?;
|
||||||
|
/// Ok::<_, optimizer::Error>(val * val)
|
||||||
|
/// })
|
||||||
|
/// .unwrap();
|
||||||
|
///
|
||||||
|
/// let best = study.best_trial().unwrap();
|
||||||
|
/// let x_val: f64 = best.get(&x).unwrap();
|
||||||
|
/// assert!((-10.0..=10.0).contains(&x_val));
|
||||||
|
/// ```
|
||||||
|
pub fn get<P: Parameter>(&self, param: &P) -> Option<P::Value> {
|
||||||
|
self.params.get(¶m.id()).map(|v| {
|
||||||
|
param
|
||||||
|
.cast_param_value(v)
|
||||||
|
.expect("parameter type mismatch: stored value incompatible with parameter")
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A pending (running) trial with its parameters and distributions, but no objective value yet.
|
/// A pending (running) trial with its parameters and distributions, but no objective value yet.
|
||||||
@@ -53,34 +94,16 @@ impl<V> CompletedTrial<V> {
|
|||||||
/// This struct represents a trial that has been started and has sampled parameters,
|
/// This struct represents a trial that has been started and has sampled parameters,
|
||||||
/// but is still running and hasn't returned an objective value. It is used with the
|
/// but is still running and hasn't returned an objective value. It is used with the
|
||||||
/// constant liar strategy for parallel optimization.
|
/// constant liar strategy for parallel optimization.
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```ignore
|
|
||||||
/// use std::collections::HashMap;
|
|
||||||
/// use optimizer::sampler::PendingTrial;
|
|
||||||
/// use optimizer::param::ParamValue;
|
|
||||||
/// use optimizer::distribution::{Distribution, FloatDistribution};
|
|
||||||
///
|
|
||||||
/// let mut params = HashMap::new();
|
|
||||||
/// params.insert("x".to_string(), ParamValue::Float(0.5));
|
|
||||||
///
|
|
||||||
/// let mut distributions = HashMap::new();
|
|
||||||
/// distributions.insert("x".to_string(), Distribution::Float(FloatDistribution {
|
|
||||||
/// low: 0.0, high: 1.0, log_scale: false, step: None,
|
|
||||||
/// }));
|
|
||||||
///
|
|
||||||
/// let pending = PendingTrial::new(1, params, distributions);
|
|
||||||
/// assert_eq!(pending.id, 1);
|
|
||||||
/// ```
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct PendingTrial {
|
pub struct PendingTrial {
|
||||||
/// The unique identifier for this trial.
|
/// The unique identifier for this trial.
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
/// The sampled parameter values, keyed by parameter name.
|
/// The sampled parameter values, keyed by parameter id.
|
||||||
pub params: HashMap<String, ParamValue>,
|
pub params: HashMap<ParamId, ParamValue>,
|
||||||
/// The parameter distributions used, keyed by parameter name.
|
/// The parameter distributions used, keyed by parameter id.
|
||||||
pub distributions: HashMap<String, Distribution>,
|
pub distributions: HashMap<ParamId, Distribution>,
|
||||||
|
/// Human-readable labels for parameters, keyed by parameter id.
|
||||||
|
pub param_labels: HashMap<ParamId, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PendingTrial {
|
impl PendingTrial {
|
||||||
@@ -88,13 +111,15 @@ impl PendingTrial {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
id: u64,
|
id: u64,
|
||||||
params: HashMap<String, ParamValue>,
|
params: HashMap<ParamId, ParamValue>,
|
||||||
distributions: HashMap<String, Distribution>,
|
distributions: HashMap<ParamId, Distribution>,
|
||||||
|
param_labels: HashMap<ParamId, String>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
params,
|
params,
|
||||||
distributions,
|
distributions,
|
||||||
|
param_labels,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,28 +129,6 @@ impl PendingTrial {
|
|||||||
/// Samplers are responsible for generating parameter values based on
|
/// Samplers are responsible for generating parameter values based on
|
||||||
/// the distribution and historical trial data. The trait requires
|
/// the distribution and historical trial data. The trait requires
|
||||||
/// `Send + Sync` to support concurrent and async optimization.
|
/// `Send + Sync` to support concurrent and async optimization.
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// Implementing a custom sampler:
|
|
||||||
///
|
|
||||||
/// ```ignore
|
|
||||||
/// use optimizer::{Sampler, ParamValue, Distribution, CompletedTrial};
|
|
||||||
///
|
|
||||||
/// struct MySampler;
|
|
||||||
///
|
|
||||||
/// impl Sampler for MySampler {
|
|
||||||
/// fn sample(
|
|
||||||
/// &self,
|
|
||||||
/// distribution: &Distribution,
|
|
||||||
/// trial_id: u64,
|
|
||||||
/// history: &[CompletedTrial],
|
|
||||||
/// ) -> ParamValue {
|
|
||||||
/// // Custom sampling logic here
|
|
||||||
/// todo!()
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub trait Sampler: Send + Sync {
|
pub trait Sampler: Send + Sync {
|
||||||
/// Samples a parameter value from the given distribution.
|
/// Samples a parameter value from the given distribution.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rand::rngs::StdRng;
|
use rand::rngs::StdRng;
|
||||||
use rand::{Rng, SeedableRng};
|
use rand::{RngExt, SeedableRng};
|
||||||
|
|
||||||
use crate::distribution::Distribution;
|
use crate::distribution::Distribution;
|
||||||
use crate::param::ParamValue;
|
use crate::param::ParamValue;
|
||||||
@@ -34,7 +34,7 @@ impl RandomSampler {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
rng: Mutex::new(StdRng::from_os_rng()),
|
rng: Mutex::new(rand::make_rng()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,13 @@
|
|||||||
//! including support for intersection search space calculation.
|
//! including support for intersection search space calculation.
|
||||||
|
|
||||||
mod gamma;
|
mod gamma;
|
||||||
|
mod multivariate;
|
||||||
mod sampler;
|
mod sampler;
|
||||||
pub mod search_space;
|
pub mod search_space;
|
||||||
|
|
||||||
pub use gamma::{FixedGamma, GammaStrategy, HyperoptGamma, LinearGamma, SqrtGamma};
|
pub use gamma::{FixedGamma, GammaStrategy, HyperoptGamma, LinearGamma, SqrtGamma};
|
||||||
|
pub use multivariate::{
|
||||||
|
ConstantLiarStrategy, MultivariateTpeSampler, MultivariateTpeSamplerBuilder,
|
||||||
|
};
|
||||||
pub use sampler::{TpeSampler, TpeSamplerBuilder};
|
pub use sampler::{TpeSampler, TpeSamplerBuilder};
|
||||||
pub use search_space::{GroupDecomposedSearchSpace, IntersectionSearchSpace};
|
pub use search_space::{GroupDecomposedSearchSpace, IntersectionSearchSpace};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+23
-15
@@ -60,7 +60,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rand::rngs::StdRng;
|
use rand::rngs::StdRng;
|
||||||
use rand::{Rng, SeedableRng};
|
use rand::{RngExt, SeedableRng};
|
||||||
|
|
||||||
use crate::distribution::Distribution;
|
use crate::distribution::Distribution;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
@@ -149,7 +149,7 @@ impl TpeSampler {
|
|||||||
n_startup_trials: 10,
|
n_startup_trials: 10,
|
||||||
n_ei_candidates: 24,
|
n_ei_candidates: 24,
|
||||||
kde_bandwidth: None,
|
kde_bandwidth: None,
|
||||||
rng: Mutex::new(StdRng::from_os_rng()),
|
rng: Mutex::new(rand::make_rng()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ impl TpeSampler {
|
|||||||
|
|
||||||
let rng = match seed {
|
let rng = match seed {
|
||||||
Some(s) => StdRng::seed_from_u64(s),
|
Some(s) => StdRng::seed_from_u64(s),
|
||||||
None => StdRng::from_os_rng(),
|
None => rand::make_rng(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@@ -280,6 +280,7 @@ impl TpeSampler {
|
|||||||
clippy::cast_possible_truncation,
|
clippy::cast_possible_truncation,
|
||||||
clippy::cast_sign_loss
|
clippy::cast_sign_loss
|
||||||
)]
|
)]
|
||||||
|
#[must_use]
|
||||||
fn split_trials<'a>(
|
fn split_trials<'a>(
|
||||||
&self,
|
&self,
|
||||||
history: &'a [CompletedTrial],
|
history: &'a [CompletedTrial],
|
||||||
@@ -856,7 +857,7 @@ impl TpeSamplerBuilder {
|
|||||||
|
|
||||||
let rng = match self.seed {
|
let rng = match self.seed {
|
||||||
Some(s) => StdRng::seed_from_u64(s),
|
Some(s) => StdRng::seed_from_u64(s),
|
||||||
None => StdRng::from_os_rng(),
|
None => rand::make_rng(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(TpeSampler {
|
Ok(TpeSampler {
|
||||||
@@ -1023,19 +1024,20 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::distribution::{CategoricalDistribution, FloatDistribution, IntDistribution};
|
use crate::distribution::{CategoricalDistribution, FloatDistribution, IntDistribution};
|
||||||
|
use crate::parameter::ParamId;
|
||||||
|
|
||||||
fn create_trial(
|
fn create_trial(
|
||||||
id: u64,
|
id: u64,
|
||||||
value: f64,
|
value: f64,
|
||||||
params: Vec<(&str, ParamValue, Distribution)>,
|
params: Vec<(ParamId, ParamValue, Distribution)>,
|
||||||
) -> CompletedTrial {
|
) -> CompletedTrial {
|
||||||
let mut param_map = HashMap::new();
|
let mut param_map = HashMap::new();
|
||||||
let mut dist_map = HashMap::new();
|
let mut dist_map = HashMap::new();
|
||||||
for (name, pv, dist) in params {
|
for (param_id, pv, dist) in params {
|
||||||
param_map.insert(name.to_string(), pv);
|
param_map.insert(param_id, pv);
|
||||||
dist_map.insert(name.to_string(), dist);
|
dist_map.insert(param_id, dist);
|
||||||
}
|
}
|
||||||
CompletedTrial::new(id, param_map, dist_map, value)
|
CompletedTrial::new(id, param_map, dist_map, HashMap::new(), value)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1103,12 +1105,13 @@ mod tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create 20 trials with values 0..20
|
// Create 20 trials with values 0..20
|
||||||
|
let x_id = ParamId::new();
|
||||||
let history: Vec<CompletedTrial> = (0..20)
|
let history: Vec<CompletedTrial> = (0..20)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
create_trial(
|
create_trial(
|
||||||
i as u64,
|
i as u64,
|
||||||
f64::from(i),
|
f64::from(i),
|
||||||
vec![("x", ParamValue::Float(f64::from(i) / 20.0), dist.clone())],
|
vec![(x_id, ParamValue::Float(f64::from(i) / 20.0), dist.clone())],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -1137,6 +1140,7 @@ mod tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create history where low values (near 0.2) are "good"
|
// Create history where low values (near 0.2) are "good"
|
||||||
|
let x_id = ParamId::new();
|
||||||
let history: Vec<CompletedTrial> = (0..20)
|
let history: Vec<CompletedTrial> = (0..20)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let x = f64::from(i) / 20.0;
|
let x = f64::from(i) / 20.0;
|
||||||
@@ -1145,7 +1149,7 @@ mod tests {
|
|||||||
create_trial(
|
create_trial(
|
||||||
i as u64,
|
i as u64,
|
||||||
value,
|
value,
|
||||||
vec![("x", ParamValue::Float(x), dist.clone())],
|
vec![(x_id, ParamValue::Float(x), dist.clone())],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -1174,6 +1178,7 @@ mod tests {
|
|||||||
let dist = Distribution::Categorical(CategoricalDistribution { n_choices: 4 });
|
let dist = Distribution::Categorical(CategoricalDistribution { n_choices: 4 });
|
||||||
|
|
||||||
// Create history where category 1 is consistently good
|
// Create history where category 1 is consistently good
|
||||||
|
let cat_id = ParamId::new();
|
||||||
let history: Vec<CompletedTrial> = (0..20)
|
let history: Vec<CompletedTrial> = (0..20)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let category = i % 4;
|
let category = i % 4;
|
||||||
@@ -1183,7 +1188,7 @@ mod tests {
|
|||||||
i as u64,
|
i as u64,
|
||||||
value,
|
value,
|
||||||
vec![(
|
vec![(
|
||||||
"cat",
|
cat_id,
|
||||||
ParamValue::Categorical(category as usize),
|
ParamValue::Categorical(category as usize),
|
||||||
dist.clone(),
|
dist.clone(),
|
||||||
)],
|
)],
|
||||||
@@ -1219,6 +1224,7 @@ mod tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create history where values near 30 are good
|
// Create history where values near 30 are good
|
||||||
|
let x_id = ParamId::new();
|
||||||
let history: Vec<CompletedTrial> = (0..20)
|
let history: Vec<CompletedTrial> = (0..20)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let x = i * 5; // 0, 5, 10, ..., 95
|
let x = i * 5; // 0, 5, 10, ..., 95
|
||||||
@@ -1226,7 +1232,7 @@ mod tests {
|
|||||||
create_trial(
|
create_trial(
|
||||||
i as u64,
|
i as u64,
|
||||||
value,
|
value,
|
||||||
vec![("x", ParamValue::Int(x), dist.clone())],
|
vec![(x_id, ParamValue::Int(x), dist.clone())],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -1251,12 +1257,13 @@ mod tests {
|
|||||||
step: None,
|
step: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let x_id = ParamId::new();
|
||||||
let history: Vec<CompletedTrial> = (0..20)
|
let history: Vec<CompletedTrial> = (0..20)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
create_trial(
|
create_trial(
|
||||||
i as u64,
|
i as u64,
|
||||||
f64::from(i),
|
f64::from(i),
|
||||||
vec![("x", ParamValue::Float(f64::from(i) / 20.0), dist.clone())],
|
vec![(x_id, ParamValue::Float(f64::from(i) / 20.0), dist.clone())],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -1331,12 +1338,13 @@ mod tests {
|
|||||||
step: None,
|
step: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let x_id = ParamId::new();
|
||||||
let history: Vec<CompletedTrial> = (0..20u32)
|
let history: Vec<CompletedTrial> = (0..20u32)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
create_trial(
|
create_trial(
|
||||||
u64::from(i),
|
u64::from(i),
|
||||||
f64::from(i),
|
f64::from(i),
|
||||||
vec![("x", ParamValue::Float(f64::from(i) / 20.0), dist.clone())],
|
vec![(x_id, ParamValue::Float(f64::from(i) / 20.0), dist.clone())],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
+232
-191
@@ -11,6 +11,7 @@
|
|||||||
use std::collections::{HashMap, HashSet, VecDeque};
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
|
|
||||||
use crate::distribution::Distribution;
|
use crate::distribution::Distribution;
|
||||||
|
use crate::parameter::ParamId;
|
||||||
use crate::sampler::CompletedTrial;
|
use crate::sampler::CompletedTrial;
|
||||||
|
|
||||||
/// Computes the intersection of search spaces across completed trials.
|
/// Computes the intersection of search spaces across completed trials.
|
||||||
@@ -87,34 +88,30 @@ impl IntersectionSearchSpace {
|
|||||||
/// - For dynamic search spaces where not all parameters are sampled in every trial,
|
/// - For dynamic search spaces where not all parameters are sampled in every trial,
|
||||||
/// this helps identify the "stable" set of parameters that can be modeled jointly.
|
/// this helps identify the "stable" set of parameters that can be modeled jointly.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn calculate(trials: &[CompletedTrial]) -> HashMap<String, Distribution> {
|
pub fn calculate(trials: &[CompletedTrial]) -> HashMap<ParamId, Distribution> {
|
||||||
if trials.is_empty() {
|
if trials.is_empty() {
|
||||||
return HashMap::new();
|
return HashMap::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get parameter names from the first trial as the initial candidate set
|
// Get parameter ids from the first trial as the initial candidate set
|
||||||
let first_trial = &trials[0];
|
let first_trial = &trials[0];
|
||||||
let mut candidate_params: HashSet<&str> = first_trial
|
let mut candidate_params: HashSet<ParamId> =
|
||||||
.distributions
|
first_trial.distributions.keys().copied().collect();
|
||||||
.keys()
|
|
||||||
.map(String::as_str)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Intersect with parameter sets from all other trials
|
// Intersect with parameter sets from all other trials
|
||||||
for trial in trials.iter().skip(1) {
|
for trial in trials.iter().skip(1) {
|
||||||
let trial_params: HashSet<&str> =
|
let trial_params: HashSet<ParamId> = trial.distributions.keys().copied().collect();
|
||||||
trial.distributions.keys().map(String::as_str).collect();
|
|
||||||
candidate_params.retain(|param| trial_params.contains(param));
|
candidate_params.retain(|param| trial_params.contains(param));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the result map using distributions from the first trial
|
// Build the result map using distributions from the first trial
|
||||||
// that contains each parameter
|
// that contains each parameter
|
||||||
let mut result = HashMap::new();
|
let mut result = HashMap::new();
|
||||||
for param_name in candidate_params {
|
for param_id in candidate_params {
|
||||||
// Find the first trial that has this parameter and use its distribution
|
// Find the first trial that has this parameter and use its distribution
|
||||||
for trial in trials {
|
for trial in trials {
|
||||||
if let Some(dist) = trial.distributions.get(param_name) {
|
if let Some(dist) = trial.distributions.get(¶m_id) {
|
||||||
result.insert(param_name.to_string(), dist.clone());
|
result.insert(param_id, dist.clone());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,7 +190,7 @@ impl GroupDecomposedSearchSpace {
|
|||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// A `Vec<HashSet<String>>` where each `HashSet` represents an independent group
|
/// A `Vec<HashSet<ParamId>>` where each `HashSet` represents an independent group
|
||||||
/// of parameters that co-occur. Parameters within the same group have appeared
|
/// of parameters that co-occur. Parameters within the same group have appeared
|
||||||
/// together in at least one trial (directly or transitively). Parameters in
|
/// together in at least one trial (directly or transitively). Parameters in
|
||||||
/// different groups have never appeared in the same trial.
|
/// different groups have never appeared in the same trial.
|
||||||
@@ -212,16 +209,16 @@ impl GroupDecomposedSearchSpace {
|
|||||||
/// - This is useful for `MultivariateTpeSampler` when `group=true` to sample
|
/// - This is useful for `MultivariateTpeSampler` when `group=true` to sample
|
||||||
/// independent parameter groups separately.
|
/// independent parameter groups separately.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn calculate(trials: &[CompletedTrial]) -> Vec<HashSet<String>> {
|
pub fn calculate(trials: &[CompletedTrial]) -> Vec<HashSet<ParamId>> {
|
||||||
if trials.is_empty() {
|
if trials.is_empty() {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect all unique parameter names
|
// Collect all unique parameter ids
|
||||||
let mut all_params: HashSet<String> = HashSet::new();
|
let mut all_params: HashSet<ParamId> = HashSet::new();
|
||||||
for trial in trials {
|
for trial in trials {
|
||||||
for param_name in trial.distributions.keys() {
|
for ¶m_id in trial.distributions.keys() {
|
||||||
all_params.insert(param_name.clone());
|
all_params.insert(param_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,51 +228,51 @@ impl GroupDecomposedSearchSpace {
|
|||||||
|
|
||||||
// Build adjacency list for co-occurrence graph
|
// Build adjacency list for co-occurrence graph
|
||||||
// Two parameters are adjacent if they appear in the same trial
|
// Two parameters are adjacent if they appear in the same trial
|
||||||
let mut adjacency: HashMap<String, HashSet<String>> = HashMap::new();
|
let mut adjacency: HashMap<ParamId, HashSet<ParamId>> = HashMap::new();
|
||||||
for param in &all_params {
|
for ¶m in &all_params {
|
||||||
adjacency.insert(param.clone(), HashSet::new());
|
adjacency.insert(param, HashSet::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
for trial in trials {
|
for trial in trials {
|
||||||
let trial_params: Vec<&String> = trial.distributions.keys().collect();
|
let trial_params: Vec<ParamId> = trial.distributions.keys().copied().collect();
|
||||||
// Connect all pairs of parameters in this trial
|
// Connect all pairs of parameters in this trial
|
||||||
for (i, param1) in trial_params.iter().enumerate() {
|
for (i, ¶m1) in trial_params.iter().enumerate() {
|
||||||
for param2 in trial_params.iter().skip(i + 1) {
|
for ¶m2 in trial_params.iter().skip(i + 1) {
|
||||||
adjacency
|
adjacency
|
||||||
.get_mut(*param1)
|
.get_mut(¶m1)
|
||||||
.expect("param should exist in adjacency map")
|
.expect("param should exist in adjacency map")
|
||||||
.insert((*param2).clone());
|
.insert(param2);
|
||||||
adjacency
|
adjacency
|
||||||
.get_mut(*param2)
|
.get_mut(¶m2)
|
||||||
.expect("param should exist in adjacency map")
|
.expect("param should exist in adjacency map")
|
||||||
.insert((*param1).clone());
|
.insert(param1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find connected components using BFS
|
// Find connected components using BFS
|
||||||
let mut visited: HashSet<String> = HashSet::new();
|
let mut visited: HashSet<ParamId> = HashSet::new();
|
||||||
let mut groups: Vec<HashSet<String>> = Vec::new();
|
let mut groups: Vec<HashSet<ParamId>> = Vec::new();
|
||||||
|
|
||||||
for param in &all_params {
|
for ¶m in &all_params {
|
||||||
if visited.contains(param) {
|
if visited.contains(¶m) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// BFS to find all parameters in this component
|
// BFS to find all parameters in this component
|
||||||
let mut component: HashSet<String> = HashSet::new();
|
let mut component: HashSet<ParamId> = HashSet::new();
|
||||||
let mut queue: VecDeque<String> = VecDeque::new();
|
let mut queue: VecDeque<ParamId> = VecDeque::new();
|
||||||
queue.push_back(param.clone());
|
queue.push_back(param);
|
||||||
visited.insert(param.clone());
|
visited.insert(param);
|
||||||
|
|
||||||
while let Some(current) = queue.pop_front() {
|
while let Some(current) = queue.pop_front() {
|
||||||
component.insert(current.clone());
|
component.insert(current);
|
||||||
|
|
||||||
if let Some(neighbors) = adjacency.get(¤t) {
|
if let Some(neighbors) = adjacency.get(¤t) {
|
||||||
for neighbor in neighbors {
|
for &neighbor in neighbors {
|
||||||
if !visited.contains(neighbor) {
|
if !visited.contains(&neighbor) {
|
||||||
visited.insert(neighbor.clone());
|
visited.insert(neighbor);
|
||||||
queue.push_back(neighbor.clone());
|
queue.push_back(neighbor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -293,19 +290,20 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::distribution::{CategoricalDistribution, FloatDistribution, IntDistribution};
|
use crate::distribution::{CategoricalDistribution, FloatDistribution, IntDistribution};
|
||||||
use crate::param::ParamValue;
|
use crate::param::ParamValue;
|
||||||
|
use crate::parameter::ParamId;
|
||||||
|
|
||||||
fn create_trial(
|
fn create_trial(
|
||||||
id: u64,
|
id: u64,
|
||||||
params: Vec<(&str, ParamValue, Distribution)>,
|
params: Vec<(ParamId, ParamValue, Distribution)>,
|
||||||
value: f64,
|
value: f64,
|
||||||
) -> CompletedTrial {
|
) -> CompletedTrial {
|
||||||
let mut param_map = HashMap::new();
|
let mut param_map = HashMap::new();
|
||||||
let mut dist_map = HashMap::new();
|
let mut dist_map = HashMap::new();
|
||||||
for (name, pv, dist) in params {
|
for (param_id, pv, dist) in params {
|
||||||
param_map.insert(name.to_string(), pv);
|
param_map.insert(param_id, pv);
|
||||||
dist_map.insert(name.to_string(), dist);
|
dist_map.insert(param_id, dist);
|
||||||
}
|
}
|
||||||
CompletedTrial::new(id, param_map, dist_map, value)
|
CompletedTrial::new(id, param_map, dist_map, HashMap::new(), value)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -317,6 +315,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_single_trial() {
|
fn test_single_trial() {
|
||||||
|
let x_id = ParamId::new();
|
||||||
|
let y_id = ParamId::new();
|
||||||
let dist_x = Distribution::Float(FloatDistribution {
|
let dist_x = Distribution::Float(FloatDistribution {
|
||||||
low: 0.0,
|
low: 0.0,
|
||||||
high: 1.0,
|
high: 1.0,
|
||||||
@@ -333,22 +333,24 @@ mod tests {
|
|||||||
let trial = create_trial(
|
let trial = create_trial(
|
||||||
0,
|
0,
|
||||||
vec![
|
vec![
|
||||||
("x", ParamValue::Float(0.5), dist_x.clone()),
|
(x_id, ParamValue::Float(0.5), dist_x.clone()),
|
||||||
("y", ParamValue::Int(5), dist_y.clone()),
|
(y_id, ParamValue::Int(5), dist_y.clone()),
|
||||||
],
|
],
|
||||||
1.0,
|
1.0,
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = IntersectionSearchSpace::calculate(&[trial]);
|
let result = IntersectionSearchSpace::calculate(&[trial]);
|
||||||
assert_eq!(result.len(), 2);
|
assert_eq!(result.len(), 2);
|
||||||
assert!(result.contains_key("x"));
|
assert!(result.contains_key(&x_id));
|
||||||
assert!(result.contains_key("y"));
|
assert!(result.contains_key(&y_id));
|
||||||
assert_eq!(result.get("x"), Some(&dist_x));
|
assert_eq!(result.get(&x_id), Some(&dist_x));
|
||||||
assert_eq!(result.get("y"), Some(&dist_y));
|
assert_eq!(result.get(&y_id), Some(&dist_y));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_all_trials_same_params() {
|
fn test_all_trials_same_params() {
|
||||||
|
let x_id = ParamId::new();
|
||||||
|
let y_id = ParamId::new();
|
||||||
let dist_x = Distribution::Float(FloatDistribution {
|
let dist_x = Distribution::Float(FloatDistribution {
|
||||||
low: 0.0,
|
low: 0.0,
|
||||||
high: 1.0,
|
high: 1.0,
|
||||||
@@ -369,8 +371,8 @@ mod tests {
|
|||||||
create_trial(
|
create_trial(
|
||||||
i,
|
i,
|
||||||
vec![
|
vec![
|
||||||
("x", ParamValue::Float(val), dist_x.clone()),
|
(x_id, ParamValue::Float(val), dist_x.clone()),
|
||||||
("y", ParamValue::Float(val - 0.5), dist_y.clone()),
|
(y_id, ParamValue::Float(val - 0.5), dist_y.clone()),
|
||||||
],
|
],
|
||||||
val * val,
|
val * val,
|
||||||
)
|
)
|
||||||
@@ -379,12 +381,15 @@ mod tests {
|
|||||||
|
|
||||||
let result = IntersectionSearchSpace::calculate(&trials);
|
let result = IntersectionSearchSpace::calculate(&trials);
|
||||||
assert_eq!(result.len(), 2);
|
assert_eq!(result.len(), 2);
|
||||||
assert!(result.contains_key("x"));
|
assert!(result.contains_key(&x_id));
|
||||||
assert!(result.contains_key("y"));
|
assert!(result.contains_key(&y_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_partial_overlap() {
|
fn test_partial_overlap() {
|
||||||
|
let x_id = ParamId::new();
|
||||||
|
let y_id = ParamId::new();
|
||||||
|
let z_id = ParamId::new();
|
||||||
let dist_x = Distribution::Float(FloatDistribution {
|
let dist_x = Distribution::Float(FloatDistribution {
|
||||||
low: 0.0,
|
low: 0.0,
|
||||||
high: 1.0,
|
high: 1.0,
|
||||||
@@ -408,8 +413,8 @@ mod tests {
|
|||||||
let trial1 = create_trial(
|
let trial1 = create_trial(
|
||||||
0,
|
0,
|
||||||
vec![
|
vec![
|
||||||
("x", ParamValue::Float(0.5), dist_x.clone()),
|
(x_id, ParamValue::Float(0.5), dist_x.clone()),
|
||||||
("y", ParamValue::Float(0.3), dist_y.clone()),
|
(y_id, ParamValue::Float(0.3), dist_y.clone()),
|
||||||
],
|
],
|
||||||
1.0,
|
1.0,
|
||||||
);
|
);
|
||||||
@@ -418,8 +423,8 @@ mod tests {
|
|||||||
let trial2 = create_trial(
|
let trial2 = create_trial(
|
||||||
1,
|
1,
|
||||||
vec![
|
vec![
|
||||||
("x", ParamValue::Float(0.7), dist_x.clone()),
|
(x_id, ParamValue::Float(0.7), dist_x.clone()),
|
||||||
("z", ParamValue::Float(0.2), dist_z.clone()),
|
(z_id, ParamValue::Float(0.2), dist_z.clone()),
|
||||||
],
|
],
|
||||||
0.5,
|
0.5,
|
||||||
);
|
);
|
||||||
@@ -428,24 +433,26 @@ mod tests {
|
|||||||
let trial3 = create_trial(
|
let trial3 = create_trial(
|
||||||
2,
|
2,
|
||||||
vec![
|
vec![
|
||||||
("x", ParamValue::Float(0.6), dist_x.clone()),
|
(x_id, ParamValue::Float(0.6), dist_x.clone()),
|
||||||
("y", ParamValue::Float(0.4), dist_y.clone()),
|
(y_id, ParamValue::Float(0.4), dist_y.clone()),
|
||||||
("z", ParamValue::Float(0.1), dist_z.clone()),
|
(z_id, ParamValue::Float(0.1), dist_z.clone()),
|
||||||
],
|
],
|
||||||
0.8,
|
0.8,
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = IntersectionSearchSpace::calculate(&[trial1, trial2, trial3]);
|
let result = IntersectionSearchSpace::calculate(&[trial1, trial2, trial3]);
|
||||||
|
|
||||||
// Only "x" appears in all three trials
|
// Only x appears in all three trials
|
||||||
assert_eq!(result.len(), 1);
|
assert_eq!(result.len(), 1);
|
||||||
assert!(result.contains_key("x"));
|
assert!(result.contains_key(&x_id));
|
||||||
assert!(!result.contains_key("y"));
|
assert!(!result.contains_key(&y_id));
|
||||||
assert!(!result.contains_key("z"));
|
assert!(!result.contains_key(&z_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_no_common_params() {
|
fn test_no_common_params() {
|
||||||
|
let x_id = ParamId::new();
|
||||||
|
let y_id = ParamId::new();
|
||||||
let dist_x = Distribution::Float(FloatDistribution {
|
let dist_x = Distribution::Float(FloatDistribution {
|
||||||
low: 0.0,
|
low: 0.0,
|
||||||
high: 1.0,
|
high: 1.0,
|
||||||
@@ -460,10 +467,10 @@ mod tests {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Trial 1: only x
|
// Trial 1: only x
|
||||||
let trial1 = create_trial(0, vec![("x", ParamValue::Float(0.5), dist_x.clone())], 1.0);
|
let trial1 = create_trial(0, vec![(x_id, ParamValue::Float(0.5), dist_x.clone())], 1.0);
|
||||||
|
|
||||||
// Trial 2: only y
|
// Trial 2: only y
|
||||||
let trial2 = create_trial(1, vec![("y", ParamValue::Float(0.3), dist_y.clone())], 0.5);
|
let trial2 = create_trial(1, vec![(y_id, ParamValue::Float(0.3), dist_y.clone())], 0.5);
|
||||||
|
|
||||||
let result = IntersectionSearchSpace::calculate(&[trial1, trial2]);
|
let result = IntersectionSearchSpace::calculate(&[trial1, trial2]);
|
||||||
|
|
||||||
@@ -473,6 +480,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mixed_distribution_types() {
|
fn test_mixed_distribution_types() {
|
||||||
|
let lr_id = ParamId::new();
|
||||||
|
let n_layers_id = ParamId::new();
|
||||||
|
let optimizer_id = ParamId::new();
|
||||||
let dist_float = Distribution::Float(FloatDistribution {
|
let dist_float = Distribution::Float(FloatDistribution {
|
||||||
low: 0.0,
|
low: 0.0,
|
||||||
high: 1.0,
|
high: 1.0,
|
||||||
@@ -490,9 +500,9 @@ mod tests {
|
|||||||
let trial1 = create_trial(
|
let trial1 = create_trial(
|
||||||
0,
|
0,
|
||||||
vec![
|
vec![
|
||||||
("learning_rate", ParamValue::Float(0.01), dist_float.clone()),
|
(lr_id, ParamValue::Float(0.01), dist_float.clone()),
|
||||||
("n_layers", ParamValue::Int(3), dist_int.clone()),
|
(n_layers_id, ParamValue::Int(3), dist_int.clone()),
|
||||||
("optimizer", ParamValue::Categorical(0), dist_cat.clone()),
|
(optimizer_id, ParamValue::Categorical(0), dist_cat.clone()),
|
||||||
],
|
],
|
||||||
1.0,
|
1.0,
|
||||||
);
|
);
|
||||||
@@ -500,13 +510,9 @@ mod tests {
|
|||||||
let trial2 = create_trial(
|
let trial2 = create_trial(
|
||||||
1,
|
1,
|
||||||
vec![
|
vec![
|
||||||
(
|
(lr_id, ParamValue::Float(0.001), dist_float.clone()),
|
||||||
"learning_rate",
|
(n_layers_id, ParamValue::Int(5), dist_int.clone()),
|
||||||
ParamValue::Float(0.001),
|
(optimizer_id, ParamValue::Categorical(1), dist_cat.clone()),
|
||||||
dist_float.clone(),
|
|
||||||
),
|
|
||||||
("n_layers", ParamValue::Int(5), dist_int.clone()),
|
|
||||||
("optimizer", ParamValue::Categorical(1), dist_cat.clone()),
|
|
||||||
],
|
],
|
||||||
0.8,
|
0.8,
|
||||||
);
|
);
|
||||||
@@ -514,19 +520,20 @@ mod tests {
|
|||||||
let result = IntersectionSearchSpace::calculate(&[trial1, trial2]);
|
let result = IntersectionSearchSpace::calculate(&[trial1, trial2]);
|
||||||
|
|
||||||
assert_eq!(result.len(), 3);
|
assert_eq!(result.len(), 3);
|
||||||
|
assert!(matches!(result.get(&lr_id), Some(Distribution::Float(_))));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
result.get("learning_rate"),
|
result.get(&n_layers_id),
|
||||||
Some(Distribution::Float(_))
|
Some(Distribution::Int(_))
|
||||||
));
|
));
|
||||||
assert!(matches!(result.get("n_layers"), Some(Distribution::Int(_))));
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
result.get("optimizer"),
|
result.get(&optimizer_id),
|
||||||
Some(Distribution::Categorical(_))
|
Some(Distribution::Categorical(_))
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_distribution_from_first_trial() {
|
fn test_distribution_from_first_trial() {
|
||||||
|
let x_id = ParamId::new();
|
||||||
// Test that when distributions differ, the first trial's distribution is used
|
// Test that when distributions differ, the first trial's distribution is used
|
||||||
let dist_x_v1 = Distribution::Float(FloatDistribution {
|
let dist_x_v1 = Distribution::Float(FloatDistribution {
|
||||||
low: 0.0,
|
low: 0.0,
|
||||||
@@ -543,13 +550,13 @@ mod tests {
|
|||||||
|
|
||||||
let trial1 = create_trial(
|
let trial1 = create_trial(
|
||||||
0,
|
0,
|
||||||
vec![("x", ParamValue::Float(0.5), dist_x_v1.clone())],
|
vec![(x_id, ParamValue::Float(0.5), dist_x_v1.clone())],
|
||||||
1.0,
|
1.0,
|
||||||
);
|
);
|
||||||
|
|
||||||
let trial2 = create_trial(
|
let trial2 = create_trial(
|
||||||
1,
|
1,
|
||||||
vec![("x", ParamValue::Float(5.0), dist_x_v2.clone())],
|
vec![(x_id, ParamValue::Float(5.0), dist_x_v2.clone())],
|
||||||
0.5,
|
0.5,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -557,13 +564,15 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(result.len(), 1);
|
assert_eq!(result.len(), 1);
|
||||||
// Should use the distribution from the first trial
|
// Should use the distribution from the first trial
|
||||||
assert_eq!(result.get("x"), Some(&dist_x_v1));
|
assert_eq!(result.get(&x_id), Some(&dist_x_v1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_many_trials_with_conditional_params() {
|
fn test_many_trials_with_conditional_params() {
|
||||||
|
let lr_id = ParamId::new();
|
||||||
|
let use_dropout_id = ParamId::new();
|
||||||
|
let dropout_rate_id = ParamId::new();
|
||||||
// Simulate a scenario with conditional parameters
|
// Simulate a scenario with conditional parameters
|
||||||
// e.g., "use_dropout" is a bool, and "dropout_rate" only exists when use_dropout=true
|
|
||||||
let dist_lr = Distribution::Float(FloatDistribution {
|
let dist_lr = Distribution::Float(FloatDistribution {
|
||||||
low: 1e-5,
|
low: 1e-5,
|
||||||
high: 1e-1,
|
high: 1e-1,
|
||||||
@@ -582,14 +591,14 @@ mod tests {
|
|||||||
let trial1 = create_trial(
|
let trial1 = create_trial(
|
||||||
0,
|
0,
|
||||||
vec![
|
vec![
|
||||||
("lr", ParamValue::Float(0.01), dist_lr.clone()),
|
(lr_id, ParamValue::Float(0.01), dist_lr.clone()),
|
||||||
(
|
(
|
||||||
"use_dropout",
|
use_dropout_id,
|
||||||
ParamValue::Categorical(1),
|
ParamValue::Categorical(1),
|
||||||
dist_dropout.clone(),
|
dist_dropout.clone(),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"dropout_rate",
|
dropout_rate_id,
|
||||||
ParamValue::Float(0.2),
|
ParamValue::Float(0.2),
|
||||||
dist_dropout_rate.clone(),
|
dist_dropout_rate.clone(),
|
||||||
),
|
),
|
||||||
@@ -601,9 +610,9 @@ mod tests {
|
|||||||
let trial2 = create_trial(
|
let trial2 = create_trial(
|
||||||
1,
|
1,
|
||||||
vec![
|
vec![
|
||||||
("lr", ParamValue::Float(0.001), dist_lr.clone()),
|
(lr_id, ParamValue::Float(0.001), dist_lr.clone()),
|
||||||
(
|
(
|
||||||
"use_dropout",
|
use_dropout_id,
|
||||||
ParamValue::Categorical(0),
|
ParamValue::Categorical(0),
|
||||||
dist_dropout.clone(),
|
dist_dropout.clone(),
|
||||||
),
|
),
|
||||||
@@ -615,14 +624,14 @@ mod tests {
|
|||||||
let trial3 = create_trial(
|
let trial3 = create_trial(
|
||||||
2,
|
2,
|
||||||
vec![
|
vec![
|
||||||
("lr", ParamValue::Float(0.005), dist_lr.clone()),
|
(lr_id, ParamValue::Float(0.005), dist_lr.clone()),
|
||||||
(
|
(
|
||||||
"use_dropout",
|
use_dropout_id,
|
||||||
ParamValue::Categorical(1),
|
ParamValue::Categorical(1),
|
||||||
dist_dropout.clone(),
|
dist_dropout.clone(),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"dropout_rate",
|
dropout_rate_id,
|
||||||
ParamValue::Float(0.3),
|
ParamValue::Float(0.3),
|
||||||
dist_dropout_rate.clone(),
|
dist_dropout_rate.clone(),
|
||||||
),
|
),
|
||||||
@@ -632,11 +641,11 @@ mod tests {
|
|||||||
|
|
||||||
let result = IntersectionSearchSpace::calculate(&[trial1, trial2, trial3]);
|
let result = IntersectionSearchSpace::calculate(&[trial1, trial2, trial3]);
|
||||||
|
|
||||||
// Only "lr" and "use_dropout" appear in all trials
|
// Only lr and use_dropout appear in all trials
|
||||||
assert_eq!(result.len(), 2);
|
assert_eq!(result.len(), 2);
|
||||||
assert!(result.contains_key("lr"));
|
assert!(result.contains_key(&lr_id));
|
||||||
assert!(result.contains_key("use_dropout"));
|
assert!(result.contains_key(&use_dropout_id));
|
||||||
assert!(!result.contains_key("dropout_rate")); // Not in trial2
|
assert!(!result.contains_key(&dropout_rate_id)); // Not in trial2
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== GroupDecomposedSearchSpace Tests ====================
|
// ==================== GroupDecomposedSearchSpace Tests ====================
|
||||||
@@ -657,12 +666,13 @@ mod tests {
|
|||||||
step: None,
|
step: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
let trial = create_trial(0, vec![("x", ParamValue::Float(0.5), dist)], 1.0);
|
let x_id = ParamId::new();
|
||||||
|
let trial = create_trial(0, vec![(x_id, ParamValue::Float(0.5), dist)], 1.0);
|
||||||
|
|
||||||
let groups = GroupDecomposedSearchSpace::calculate(&[trial]);
|
let groups = GroupDecomposedSearchSpace::calculate(&[trial]);
|
||||||
|
|
||||||
assert_eq!(groups.len(), 1);
|
assert_eq!(groups.len(), 1);
|
||||||
assert!(groups[0].contains("x"));
|
assert!(groups[0].contains(&x_id));
|
||||||
assert_eq!(groups[0].len(), 1);
|
assert_eq!(groups[0].len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -675,12 +685,15 @@ mod tests {
|
|||||||
step: None,
|
step: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let x_id = ParamId::new();
|
||||||
|
let y_id = ParamId::new();
|
||||||
|
let z_id = ParamId::new();
|
||||||
let trial = create_trial(
|
let trial = create_trial(
|
||||||
0,
|
0,
|
||||||
vec![
|
vec![
|
||||||
("x", ParamValue::Float(0.5), dist.clone()),
|
(x_id, ParamValue::Float(0.5), dist.clone()),
|
||||||
("y", ParamValue::Float(0.3), dist.clone()),
|
(y_id, ParamValue::Float(0.3), dist.clone()),
|
||||||
("z", ParamValue::Float(0.7), dist),
|
(z_id, ParamValue::Float(0.7), dist),
|
||||||
],
|
],
|
||||||
1.0,
|
1.0,
|
||||||
);
|
);
|
||||||
@@ -690,9 +703,9 @@ mod tests {
|
|||||||
// All params appear together, so one group
|
// All params appear together, so one group
|
||||||
assert_eq!(groups.len(), 1);
|
assert_eq!(groups.len(), 1);
|
||||||
assert_eq!(groups[0].len(), 3);
|
assert_eq!(groups[0].len(), 3);
|
||||||
assert!(groups[0].contains("x"));
|
assert!(groups[0].contains(&x_id));
|
||||||
assert!(groups[0].contains("y"));
|
assert!(groups[0].contains(&y_id));
|
||||||
assert!(groups[0].contains("z"));
|
assert!(groups[0].contains(&z_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -704,12 +717,17 @@ mod tests {
|
|||||||
step: None,
|
step: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let x_id = ParamId::new();
|
||||||
|
let y_id = ParamId::new();
|
||||||
|
let a_id = ParamId::new();
|
||||||
|
let b_id = ParamId::new();
|
||||||
|
|
||||||
// Trial 1: x, y (group 1)
|
// Trial 1: x, y (group 1)
|
||||||
let trial1 = create_trial(
|
let trial1 = create_trial(
|
||||||
0,
|
0,
|
||||||
vec![
|
vec![
|
||||||
("x", ParamValue::Float(0.1), dist.clone()),
|
(x_id, ParamValue::Float(0.1), dist.clone()),
|
||||||
("y", ParamValue::Float(0.2), dist.clone()),
|
(y_id, ParamValue::Float(0.2), dist.clone()),
|
||||||
],
|
],
|
||||||
1.0,
|
1.0,
|
||||||
);
|
);
|
||||||
@@ -718,8 +736,8 @@ mod tests {
|
|||||||
let trial2 = create_trial(
|
let trial2 = create_trial(
|
||||||
1,
|
1,
|
||||||
vec![
|
vec![
|
||||||
("a", ParamValue::Float(0.3), dist.clone()),
|
(a_id, ParamValue::Float(0.3), dist.clone()),
|
||||||
("b", ParamValue::Float(0.4), dist),
|
(b_id, ParamValue::Float(0.4), dist),
|
||||||
],
|
],
|
||||||
0.5,
|
0.5,
|
||||||
);
|
);
|
||||||
@@ -730,8 +748,8 @@ mod tests {
|
|||||||
assert_eq!(groups.len(), 2);
|
assert_eq!(groups.len(), 2);
|
||||||
|
|
||||||
// Find which group has x/y and which has a/b
|
// Find which group has x/y and which has a/b
|
||||||
let group_xy = groups.iter().find(|g| g.contains("x"));
|
let group_xy = groups.iter().find(|g| g.contains(&x_id));
|
||||||
let group_ab = groups.iter().find(|g| g.contains("a"));
|
let group_ab = groups.iter().find(|g| g.contains(&a_id));
|
||||||
|
|
||||||
assert!(group_xy.is_some());
|
assert!(group_xy.is_some());
|
||||||
assert!(group_ab.is_some());
|
assert!(group_ab.is_some());
|
||||||
@@ -740,12 +758,12 @@ mod tests {
|
|||||||
let group_ab = group_ab.expect("group with a should exist");
|
let group_ab = group_ab.expect("group with a should exist");
|
||||||
|
|
||||||
assert_eq!(group_xy.len(), 2);
|
assert_eq!(group_xy.len(), 2);
|
||||||
assert!(group_xy.contains("x"));
|
assert!(group_xy.contains(&x_id));
|
||||||
assert!(group_xy.contains("y"));
|
assert!(group_xy.contains(&y_id));
|
||||||
|
|
||||||
assert_eq!(group_ab.len(), 2);
|
assert_eq!(group_ab.len(), 2);
|
||||||
assert!(group_ab.contains("a"));
|
assert!(group_ab.contains(&a_id));
|
||||||
assert!(group_ab.contains("b"));
|
assert!(group_ab.contains(&b_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -757,12 +775,16 @@ mod tests {
|
|||||||
step: None,
|
step: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let x_id = ParamId::new();
|
||||||
|
let y_id = ParamId::new();
|
||||||
|
let z_id = ParamId::new();
|
||||||
|
|
||||||
// Trial 1: x, y
|
// Trial 1: x, y
|
||||||
let trial1 = create_trial(
|
let trial1 = create_trial(
|
||||||
0,
|
0,
|
||||||
vec![
|
vec![
|
||||||
("x", ParamValue::Float(0.1), dist.clone()),
|
(x_id, ParamValue::Float(0.1), dist.clone()),
|
||||||
("y", ParamValue::Float(0.2), dist.clone()),
|
(y_id, ParamValue::Float(0.2), dist.clone()),
|
||||||
],
|
],
|
||||||
1.0,
|
1.0,
|
||||||
);
|
);
|
||||||
@@ -771,8 +793,8 @@ mod tests {
|
|||||||
let trial2 = create_trial(
|
let trial2 = create_trial(
|
||||||
1,
|
1,
|
||||||
vec![
|
vec![
|
||||||
("y", ParamValue::Float(0.3), dist.clone()),
|
(y_id, ParamValue::Float(0.3), dist.clone()),
|
||||||
("z", ParamValue::Float(0.4), dist),
|
(z_id, ParamValue::Float(0.4), dist),
|
||||||
],
|
],
|
||||||
0.5,
|
0.5,
|
||||||
);
|
);
|
||||||
@@ -782,9 +804,9 @@ mod tests {
|
|||||||
// All should be in one group due to transitive connection via y
|
// All should be in one group due to transitive connection via y
|
||||||
assert_eq!(groups.len(), 1);
|
assert_eq!(groups.len(), 1);
|
||||||
assert_eq!(groups[0].len(), 3);
|
assert_eq!(groups[0].len(), 3);
|
||||||
assert!(groups[0].contains("x"));
|
assert!(groups[0].contains(&x_id));
|
||||||
assert!(groups[0].contains("y"));
|
assert!(groups[0].contains(&y_id));
|
||||||
assert!(groups[0].contains("z"));
|
assert!(groups[0].contains(&z_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -796,33 +818,35 @@ mod tests {
|
|||||||
step: None,
|
step: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let a_id = ParamId::new();
|
||||||
|
let b_id = ParamId::new();
|
||||||
|
let c_id = ParamId::new();
|
||||||
|
let d_id = ParamId::new();
|
||||||
|
|
||||||
// Create a chain: a-b, b-c, c-d
|
// Create a chain: a-b, b-c, c-d
|
||||||
// Trial 1: a, b
|
|
||||||
let trial1 = create_trial(
|
let trial1 = create_trial(
|
||||||
0,
|
0,
|
||||||
vec![
|
vec![
|
||||||
("a", ParamValue::Float(0.1), dist.clone()),
|
(a_id, ParamValue::Float(0.1), dist.clone()),
|
||||||
("b", ParamValue::Float(0.2), dist.clone()),
|
(b_id, ParamValue::Float(0.2), dist.clone()),
|
||||||
],
|
],
|
||||||
1.0,
|
1.0,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Trial 2: b, c
|
|
||||||
let trial2 = create_trial(
|
let trial2 = create_trial(
|
||||||
1,
|
1,
|
||||||
vec![
|
vec![
|
||||||
("b", ParamValue::Float(0.3), dist.clone()),
|
(b_id, ParamValue::Float(0.3), dist.clone()),
|
||||||
("c", ParamValue::Float(0.4), dist.clone()),
|
(c_id, ParamValue::Float(0.4), dist.clone()),
|
||||||
],
|
],
|
||||||
0.5,
|
0.5,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Trial 3: c, d
|
|
||||||
let trial3 = create_trial(
|
let trial3 = create_trial(
|
||||||
2,
|
2,
|
||||||
vec![
|
vec![
|
||||||
("c", ParamValue::Float(0.5), dist.clone()),
|
(c_id, ParamValue::Float(0.5), dist.clone()),
|
||||||
("d", ParamValue::Float(0.6), dist),
|
(d_id, ParamValue::Float(0.6), dist),
|
||||||
],
|
],
|
||||||
0.3,
|
0.3,
|
||||||
);
|
);
|
||||||
@@ -832,10 +856,10 @@ mod tests {
|
|||||||
// All should be connected in one group
|
// All should be connected in one group
|
||||||
assert_eq!(groups.len(), 1);
|
assert_eq!(groups.len(), 1);
|
||||||
assert_eq!(groups[0].len(), 4);
|
assert_eq!(groups[0].len(), 4);
|
||||||
assert!(groups[0].contains("a"));
|
assert!(groups[0].contains(&a_id));
|
||||||
assert!(groups[0].contains("b"));
|
assert!(groups[0].contains(&b_id));
|
||||||
assert!(groups[0].contains("c"));
|
assert!(groups[0].contains(&c_id));
|
||||||
assert!(groups[0].contains("d"));
|
assert!(groups[0].contains(&d_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -847,10 +871,14 @@ mod tests {
|
|||||||
step: None,
|
step: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let x_id = ParamId::new();
|
||||||
|
let y_id = ParamId::new();
|
||||||
|
let z_id = ParamId::new();
|
||||||
|
|
||||||
// Each trial has exactly one parameter (all isolated)
|
// Each trial has exactly one parameter (all isolated)
|
||||||
let trial1 = create_trial(0, vec![("x", ParamValue::Float(0.1), dist.clone())], 1.0);
|
let trial1 = create_trial(0, vec![(x_id, ParamValue::Float(0.1), dist.clone())], 1.0);
|
||||||
let trial2 = create_trial(1, vec![("y", ParamValue::Float(0.2), dist.clone())], 0.5);
|
let trial2 = create_trial(1, vec![(y_id, ParamValue::Float(0.2), dist.clone())], 0.5);
|
||||||
let trial3 = create_trial(2, vec![("z", ParamValue::Float(0.3), dist)], 0.3);
|
let trial3 = create_trial(2, vec![(z_id, ParamValue::Float(0.3), dist)], 0.3);
|
||||||
|
|
||||||
let groups = GroupDecomposedSearchSpace::calculate(&[trial1, trial2, trial3]);
|
let groups = GroupDecomposedSearchSpace::calculate(&[trial1, trial2, trial3]);
|
||||||
|
|
||||||
@@ -861,10 +889,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify all params are covered
|
// Verify all params are covered
|
||||||
let all_params: HashSet<String> = groups.iter().flatten().cloned().collect();
|
let all_params: HashSet<ParamId> = groups.iter().flatten().copied().collect();
|
||||||
assert!(all_params.contains("x"));
|
assert!(all_params.contains(&x_id));
|
||||||
assert!(all_params.contains("y"));
|
assert!(all_params.contains(&y_id));
|
||||||
assert!(all_params.contains("z"));
|
assert!(all_params.contains(&z_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -876,51 +904,54 @@ mod tests {
|
|||||||
step: None,
|
step: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let x_id = ParamId::new();
|
||||||
|
let y_id = ParamId::new();
|
||||||
|
let z_id = ParamId::new();
|
||||||
|
let a_id = ParamId::new();
|
||||||
|
let b_id = ParamId::new();
|
||||||
|
let w_id = ParamId::new();
|
||||||
|
|
||||||
// Group 1: x, y, z connected
|
// Group 1: x, y, z connected
|
||||||
// Group 2: a, b connected
|
// Group 2: a, b connected
|
||||||
// w is isolated
|
// w is isolated
|
||||||
|
|
||||||
// Trial 1: x, y
|
|
||||||
let trial1 = create_trial(
|
let trial1 = create_trial(
|
||||||
0,
|
0,
|
||||||
vec![
|
vec![
|
||||||
("x", ParamValue::Float(0.1), dist.clone()),
|
(x_id, ParamValue::Float(0.1), dist.clone()),
|
||||||
("y", ParamValue::Float(0.2), dist.clone()),
|
(y_id, ParamValue::Float(0.2), dist.clone()),
|
||||||
],
|
],
|
||||||
1.0,
|
1.0,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Trial 2: y, z
|
|
||||||
let trial2 = create_trial(
|
let trial2 = create_trial(
|
||||||
1,
|
1,
|
||||||
vec![
|
vec![
|
||||||
("y", ParamValue::Float(0.3), dist.clone()),
|
(y_id, ParamValue::Float(0.3), dist.clone()),
|
||||||
("z", ParamValue::Float(0.4), dist.clone()),
|
(z_id, ParamValue::Float(0.4), dist.clone()),
|
||||||
],
|
],
|
||||||
0.5,
|
0.5,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Trial 3: a, b
|
|
||||||
let trial3 = create_trial(
|
let trial3 = create_trial(
|
||||||
2,
|
2,
|
||||||
vec![
|
vec![
|
||||||
("a", ParamValue::Float(0.5), dist.clone()),
|
(a_id, ParamValue::Float(0.5), dist.clone()),
|
||||||
("b", ParamValue::Float(0.6), dist.clone()),
|
(b_id, ParamValue::Float(0.6), dist.clone()),
|
||||||
],
|
],
|
||||||
0.3,
|
0.3,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Trial 4: w (isolated)
|
let trial4 = create_trial(3, vec![(w_id, ParamValue::Float(0.7), dist)], 0.2);
|
||||||
let trial4 = create_trial(3, vec![("w", ParamValue::Float(0.7), dist)], 0.2);
|
|
||||||
|
|
||||||
let groups = GroupDecomposedSearchSpace::calculate(&[trial1, trial2, trial3, trial4]);
|
let groups = GroupDecomposedSearchSpace::calculate(&[trial1, trial2, trial3, trial4]);
|
||||||
|
|
||||||
// Should have 3 groups: {x,y,z}, {a,b}, {w}
|
// Should have 3 groups: {x,y,z}, {a,b}, {w}
|
||||||
assert_eq!(groups.len(), 3);
|
assert_eq!(groups.len(), 3);
|
||||||
|
|
||||||
let group_xyz = groups.iter().find(|g| g.contains("x"));
|
let group_xyz = groups.iter().find(|g| g.contains(&x_id));
|
||||||
let group_ab = groups.iter().find(|g| g.contains("a"));
|
let group_ab = groups.iter().find(|g| g.contains(&a_id));
|
||||||
let group_w = groups.iter().find(|g| g.contains("w"));
|
let group_w = groups.iter().find(|g| g.contains(&w_id));
|
||||||
|
|
||||||
assert!(group_xyz.is_some());
|
assert!(group_xyz.is_some());
|
||||||
assert!(group_ab.is_some());
|
assert!(group_ab.is_some());
|
||||||
@@ -928,18 +959,18 @@ mod tests {
|
|||||||
|
|
||||||
let group_xyz = group_xyz.expect("group with x should exist");
|
let group_xyz = group_xyz.expect("group with x should exist");
|
||||||
assert_eq!(group_xyz.len(), 3);
|
assert_eq!(group_xyz.len(), 3);
|
||||||
assert!(group_xyz.contains("x"));
|
assert!(group_xyz.contains(&x_id));
|
||||||
assert!(group_xyz.contains("y"));
|
assert!(group_xyz.contains(&y_id));
|
||||||
assert!(group_xyz.contains("z"));
|
assert!(group_xyz.contains(&z_id));
|
||||||
|
|
||||||
let group_ab = group_ab.expect("group with a should exist");
|
let group_ab = group_ab.expect("group with a should exist");
|
||||||
assert_eq!(group_ab.len(), 2);
|
assert_eq!(group_ab.len(), 2);
|
||||||
assert!(group_ab.contains("a"));
|
assert!(group_ab.contains(&a_id));
|
||||||
assert!(group_ab.contains("b"));
|
assert!(group_ab.contains(&b_id));
|
||||||
|
|
||||||
let group_w = group_w.expect("group with w should exist");
|
let group_w = group_w.expect("group with w should exist");
|
||||||
assert_eq!(group_w.len(), 1);
|
assert_eq!(group_w.len(), 1);
|
||||||
assert!(group_w.contains("w"));
|
assert!(group_w.contains(&w_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -951,14 +982,19 @@ mod tests {
|
|||||||
step: None,
|
step: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let a_id = ParamId::new();
|
||||||
|
let b_id = ParamId::new();
|
||||||
|
let c_id = ParamId::new();
|
||||||
|
let d_id = ParamId::new();
|
||||||
|
|
||||||
// All params in single trial
|
// All params in single trial
|
||||||
let trial = create_trial(
|
let trial = create_trial(
|
||||||
0,
|
0,
|
||||||
vec![
|
vec![
|
||||||
("a", ParamValue::Float(0.1), dist.clone()),
|
(a_id, ParamValue::Float(0.1), dist.clone()),
|
||||||
("b", ParamValue::Float(0.2), dist.clone()),
|
(b_id, ParamValue::Float(0.2), dist.clone()),
|
||||||
("c", ParamValue::Float(0.3), dist.clone()),
|
(c_id, ParamValue::Float(0.3), dist.clone()),
|
||||||
("d", ParamValue::Float(0.4), dist),
|
(d_id, ParamValue::Float(0.4), dist),
|
||||||
],
|
],
|
||||||
1.0,
|
1.0,
|
||||||
);
|
);
|
||||||
@@ -972,6 +1008,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_group_with_mixed_distribution_types() {
|
fn test_group_with_mixed_distribution_types() {
|
||||||
|
let lr_id = ParamId::new();
|
||||||
|
let n_layers_id = ParamId::new();
|
||||||
|
let optimizer_id = ParamId::new();
|
||||||
let dist_float = Distribution::Float(FloatDistribution {
|
let dist_float = Distribution::Float(FloatDistribution {
|
||||||
low: 0.0,
|
low: 0.0,
|
||||||
high: 1.0,
|
high: 1.0,
|
||||||
@@ -991,23 +1030,23 @@ mod tests {
|
|||||||
let trial1 = create_trial(
|
let trial1 = create_trial(
|
||||||
0,
|
0,
|
||||||
vec![
|
vec![
|
||||||
("learning_rate", ParamValue::Float(0.01), dist_float.clone()),
|
(lr_id, ParamValue::Float(0.01), dist_float.clone()),
|
||||||
("n_layers", ParamValue::Int(3), dist_int.clone()),
|
(n_layers_id, ParamValue::Int(3), dist_int.clone()),
|
||||||
],
|
],
|
||||||
1.0,
|
1.0,
|
||||||
);
|
);
|
||||||
|
|
||||||
let trial2 = create_trial(
|
let trial2 = create_trial(
|
||||||
1,
|
1,
|
||||||
vec![("optimizer", ParamValue::Categorical(1), dist_cat)],
|
vec![(optimizer_id, ParamValue::Categorical(1), dist_cat)],
|
||||||
0.5,
|
0.5,
|
||||||
);
|
);
|
||||||
|
|
||||||
let trial3 = create_trial(
|
let trial3 = create_trial(
|
||||||
2,
|
2,
|
||||||
vec![
|
vec![
|
||||||
("learning_rate", ParamValue::Float(0.001), dist_float),
|
(lr_id, ParamValue::Float(0.001), dist_float),
|
||||||
("n_layers", ParamValue::Int(5), dist_int),
|
(n_layers_id, ParamValue::Int(5), dist_int),
|
||||||
],
|
],
|
||||||
0.8,
|
0.8,
|
||||||
);
|
);
|
||||||
@@ -1017,20 +1056,20 @@ mod tests {
|
|||||||
// Should have 2 groups: {learning_rate, n_layers} and {optimizer}
|
// Should have 2 groups: {learning_rate, n_layers} and {optimizer}
|
||||||
assert_eq!(groups.len(), 2);
|
assert_eq!(groups.len(), 2);
|
||||||
|
|
||||||
let group_lr = groups.iter().find(|g| g.contains("learning_rate"));
|
let group_lr = groups.iter().find(|g| g.contains(&lr_id));
|
||||||
let group_opt = groups.iter().find(|g| g.contains("optimizer"));
|
let group_opt = groups.iter().find(|g| g.contains(&optimizer_id));
|
||||||
|
|
||||||
assert!(group_lr.is_some());
|
assert!(group_lr.is_some());
|
||||||
assert!(group_opt.is_some());
|
assert!(group_opt.is_some());
|
||||||
|
|
||||||
let group_lr = group_lr.expect("group with learning_rate should exist");
|
let group_lr = group_lr.expect("group with learning_rate should exist");
|
||||||
assert_eq!(group_lr.len(), 2);
|
assert_eq!(group_lr.len(), 2);
|
||||||
assert!(group_lr.contains("learning_rate"));
|
assert!(group_lr.contains(&lr_id));
|
||||||
assert!(group_lr.contains("n_layers"));
|
assert!(group_lr.contains(&n_layers_id));
|
||||||
|
|
||||||
let group_opt = group_opt.expect("group with optimizer should exist");
|
let group_opt = group_opt.expect("group with optimizer should exist");
|
||||||
assert_eq!(group_opt.len(), 1);
|
assert_eq!(group_opt.len(), 1);
|
||||||
assert!(group_opt.contains("optimizer"));
|
assert!(group_opt.contains(&optimizer_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1042,15 +1081,17 @@ mod tests {
|
|||||||
step: None,
|
step: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let center_id = ParamId::new();
|
||||||
|
let a_id = ParamId::new();
|
||||||
|
let b_id = ParamId::new();
|
||||||
|
let c_id = ParamId::new();
|
||||||
|
|
||||||
// Star topology: center connects to all others
|
// Star topology: center connects to all others
|
||||||
// Trial 1: center, a
|
|
||||||
// Trial 2: center, b
|
|
||||||
// Trial 3: center, c
|
|
||||||
let trial1 = create_trial(
|
let trial1 = create_trial(
|
||||||
0,
|
0,
|
||||||
vec![
|
vec![
|
||||||
("center", ParamValue::Float(0.1), dist.clone()),
|
(center_id, ParamValue::Float(0.1), dist.clone()),
|
||||||
("a", ParamValue::Float(0.2), dist.clone()),
|
(a_id, ParamValue::Float(0.2), dist.clone()),
|
||||||
],
|
],
|
||||||
1.0,
|
1.0,
|
||||||
);
|
);
|
||||||
@@ -1058,8 +1099,8 @@ mod tests {
|
|||||||
let trial2 = create_trial(
|
let trial2 = create_trial(
|
||||||
1,
|
1,
|
||||||
vec![
|
vec![
|
||||||
("center", ParamValue::Float(0.3), dist.clone()),
|
(center_id, ParamValue::Float(0.3), dist.clone()),
|
||||||
("b", ParamValue::Float(0.4), dist.clone()),
|
(b_id, ParamValue::Float(0.4), dist.clone()),
|
||||||
],
|
],
|
||||||
0.5,
|
0.5,
|
||||||
);
|
);
|
||||||
@@ -1067,8 +1108,8 @@ mod tests {
|
|||||||
let trial3 = create_trial(
|
let trial3 = create_trial(
|
||||||
2,
|
2,
|
||||||
vec![
|
vec![
|
||||||
("center", ParamValue::Float(0.5), dist.clone()),
|
(center_id, ParamValue::Float(0.5), dist.clone()),
|
||||||
("c", ParamValue::Float(0.6), dist),
|
(c_id, ParamValue::Float(0.6), dist),
|
||||||
],
|
],
|
||||||
0.3,
|
0.3,
|
||||||
);
|
);
|
||||||
@@ -1078,9 +1119,9 @@ mod tests {
|
|||||||
// All connected via center
|
// All connected via center
|
||||||
assert_eq!(groups.len(), 1);
|
assert_eq!(groups.len(), 1);
|
||||||
assert_eq!(groups[0].len(), 4);
|
assert_eq!(groups[0].len(), 4);
|
||||||
assert!(groups[0].contains("center"));
|
assert!(groups[0].contains(¢er_id));
|
||||||
assert!(groups[0].contains("a"));
|
assert!(groups[0].contains(&a_id));
|
||||||
assert!(groups[0].contains("b"));
|
assert!(groups[0].contains(&b_id));
|
||||||
assert!(groups[0].contains("c"));
|
assert!(groups[0].contains(&c_id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+159
-366
@@ -1,5 +1,6 @@
|
|||||||
//! Study implementation for managing optimization trials.
|
//! Study implementation for managing optimization trials.
|
||||||
|
|
||||||
|
use core::any::Any;
|
||||||
#[cfg(feature = "async")]
|
#[cfg(feature = "async")]
|
||||||
use core::future::Future;
|
use core::future::Future;
|
||||||
use core::ops::ControlFlow;
|
use core::ops::ControlFlow;
|
||||||
@@ -43,6 +44,10 @@ where
|
|||||||
completed_trials: Arc<RwLock<Vec<CompletedTrial<V>>>>,
|
completed_trials: Arc<RwLock<Vec<CompletedTrial<V>>>>,
|
||||||
/// Counter for generating unique trial IDs.
|
/// Counter for generating unique trial IDs.
|
||||||
next_trial_id: AtomicU64,
|
next_trial_id: AtomicU64,
|
||||||
|
/// Optional factory for creating sampler-aware trials.
|
||||||
|
/// Set automatically for `Study<f64>` so that `create_trial()` and all
|
||||||
|
/// optimization methods use the sampler without requiring `_with_sampler` suffixes.
|
||||||
|
trial_factory: Option<Arc<dyn Fn(u64) -> Trial + Send + Sync>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V> Study<V>
|
impl<V> Study<V>
|
||||||
@@ -66,7 +71,10 @@ where
|
|||||||
/// assert_eq!(study.direction(), Direction::Minimize);
|
/// assert_eq!(study.direction(), Direction::Minimize);
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(direction: Direction) -> Self {
|
pub fn new(direction: Direction) -> Self
|
||||||
|
where
|
||||||
|
V: 'static,
|
||||||
|
{
|
||||||
Self::with_sampler(direction, RandomSampler::new())
|
Self::with_sampler(direction, RandomSampler::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,15 +95,49 @@ where
|
|||||||
/// let study: Study<f64> = Study::with_sampler(Direction::Maximize, sampler);
|
/// let study: Study<f64> = Study::with_sampler(Direction::Maximize, sampler);
|
||||||
/// assert_eq!(study.direction(), Direction::Maximize);
|
/// assert_eq!(study.direction(), Direction::Maximize);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn with_sampler(direction: Direction, sampler: impl Sampler + 'static) -> Self {
|
pub fn with_sampler(direction: Direction, sampler: impl Sampler + 'static) -> Self
|
||||||
|
where
|
||||||
|
V: 'static,
|
||||||
|
{
|
||||||
|
let sampler: Arc<dyn Sampler> = Arc::new(sampler);
|
||||||
|
let completed_trials = Arc::new(RwLock::new(Vec::new()));
|
||||||
|
|
||||||
|
// For Study<f64>, set up a trial factory that provides sampler integration.
|
||||||
|
// This uses Any downcasting to check at runtime whether V = f64.
|
||||||
|
let trial_factory = Self::make_trial_factory(&sampler, &completed_trials);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
direction,
|
direction,
|
||||||
sampler: Arc::new(sampler),
|
sampler,
|
||||||
completed_trials: Arc::new(RwLock::new(Vec::new())),
|
completed_trials,
|
||||||
next_trial_id: AtomicU64::new(0),
|
next_trial_id: AtomicU64::new(0),
|
||||||
|
trial_factory,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a trial factory for sampler integration when `V = f64`.
|
||||||
|
fn make_trial_factory(
|
||||||
|
sampler: &Arc<dyn Sampler>,
|
||||||
|
completed_trials: &Arc<RwLock<Vec<CompletedTrial<V>>>>,
|
||||||
|
) -> Option<Arc<dyn Fn(u64) -> Trial + Send + Sync>>
|
||||||
|
where
|
||||||
|
V: 'static,
|
||||||
|
{
|
||||||
|
// Try to downcast the completed_trials Arc to the f64 specialization.
|
||||||
|
// This succeeds only when V = f64, enabling automatic sampler integration.
|
||||||
|
let any_ref: &dyn Any = completed_trials;
|
||||||
|
let f64_trials: Option<&Arc<RwLock<Vec<CompletedTrial<f64>>>>> = any_ref.downcast_ref();
|
||||||
|
|
||||||
|
f64_trials.map(|trials| {
|
||||||
|
let sampler = Arc::clone(sampler);
|
||||||
|
let trials = Arc::clone(trials);
|
||||||
|
let factory: Arc<dyn Fn(u64) -> Trial + Send + Sync> = Arc::new(move |id| {
|
||||||
|
Trial::with_sampler(id, Arc::clone(&sampler), Arc::clone(&trials))
|
||||||
|
});
|
||||||
|
factory
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the optimization direction.
|
/// Returns the optimization direction.
|
||||||
pub fn direction(&self) -> Direction {
|
pub fn direction(&self) -> Direction {
|
||||||
self.direction
|
self.direction
|
||||||
@@ -116,8 +158,12 @@ where
|
|||||||
/// let mut study: Study<f64> = Study::new(Direction::Minimize);
|
/// let mut study: Study<f64> = Study::new(Direction::Minimize);
|
||||||
/// study.set_sampler(TpeSampler::new());
|
/// study.set_sampler(TpeSampler::new());
|
||||||
/// ```
|
/// ```
|
||||||
pub fn set_sampler(&mut self, sampler: impl Sampler + 'static) {
|
pub fn set_sampler(&mut self, sampler: impl Sampler + 'static)
|
||||||
|
where
|
||||||
|
V: 'static,
|
||||||
|
{
|
||||||
self.sampler = Arc::new(sampler);
|
self.sampler = Arc::new(sampler);
|
||||||
|
self.trial_factory = Self::make_trial_factory(&self.sampler, &self.completed_trials);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates the next unique trial ID.
|
/// Generates the next unique trial ID.
|
||||||
@@ -131,9 +177,9 @@ where
|
|||||||
/// parameter values. After the objective function is evaluated, call
|
/// parameter values. After the objective function is evaluated, call
|
||||||
/// `complete_trial` or `fail_trial` to record the result.
|
/// `complete_trial` or `fail_trial` to record the result.
|
||||||
///
|
///
|
||||||
/// Note: For `Study<f64>`, this method creates a trial without sampler
|
/// For `Study<f64>`, this method automatically integrates with the study's
|
||||||
/// integration. Use `create_trial_with_sampler()` to create trials that
|
/// sampler and trial history, so there is no need to call a separate
|
||||||
/// use the study's sampler and have access to trial history.
|
/// `create_trial_with_sampler()` method.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
@@ -149,7 +195,11 @@ where
|
|||||||
/// ```
|
/// ```
|
||||||
pub fn create_trial(&self) -> Trial {
|
pub fn create_trial(&self) -> Trial {
|
||||||
let id = self.next_trial_id();
|
let id = self.next_trial_id();
|
||||||
Trial::new(id)
|
if let Some(factory) = &self.trial_factory {
|
||||||
|
factory(id)
|
||||||
|
} else {
|
||||||
|
Trial::new(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Records a completed trial with its objective value.
|
/// Records a completed trial with its objective value.
|
||||||
@@ -166,11 +216,13 @@ where
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||||
/// use optimizer::{Direction, Study};
|
/// use optimizer::{Direction, Study};
|
||||||
///
|
///
|
||||||
/// let study: Study<f64> = Study::new(Direction::Minimize);
|
/// let study: Study<f64> = Study::new(Direction::Minimize);
|
||||||
|
/// let x_param = FloatParam::new(0.0, 1.0);
|
||||||
/// let mut trial = study.create_trial();
|
/// let mut trial = study.create_trial();
|
||||||
/// let x = trial.suggest_float("x", 0.0, 1.0).unwrap();
|
/// let x = x_param.suggest(&mut trial).unwrap();
|
||||||
/// let objective_value = x * x;
|
/// let objective_value = x * x;
|
||||||
/// study.complete_trial(trial, objective_value);
|
/// study.complete_trial(trial, objective_value);
|
||||||
///
|
///
|
||||||
@@ -182,6 +234,7 @@ where
|
|||||||
trial.id(),
|
trial.id(),
|
||||||
trial.params().clone(),
|
trial.params().clone(),
|
||||||
trial.distributions().clone(),
|
trial.distributions().clone(),
|
||||||
|
trial.param_labels().clone(),
|
||||||
value,
|
value,
|
||||||
);
|
);
|
||||||
self.completed_trials.write().push(completed);
|
self.completed_trials.write().push(completed);
|
||||||
@@ -228,11 +281,13 @@ where
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||||
/// use optimizer::{Direction, Study};
|
/// use optimizer::{Direction, Study};
|
||||||
///
|
///
|
||||||
/// let study: Study<f64> = Study::new(Direction::Minimize);
|
/// let study: Study<f64> = Study::new(Direction::Minimize);
|
||||||
|
/// let x_param = FloatParam::new(0.0, 1.0);
|
||||||
/// let mut trial = study.create_trial();
|
/// let mut trial = study.create_trial();
|
||||||
/// let _ = trial.suggest_float("x", 0.0, 1.0);
|
/// let _ = x_param.suggest(&mut trial);
|
||||||
/// study.complete_trial(trial, 0.5);
|
/// study.complete_trial(trial, 0.5);
|
||||||
///
|
///
|
||||||
/// for completed in study.trials() {
|
/// for completed in study.trials() {
|
||||||
@@ -253,13 +308,15 @@ where
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||||
/// use optimizer::{Direction, Study};
|
/// use optimizer::{Direction, Study};
|
||||||
///
|
///
|
||||||
/// let study: Study<f64> = Study::new(Direction::Minimize);
|
/// let study: Study<f64> = Study::new(Direction::Minimize);
|
||||||
/// assert_eq!(study.n_trials(), 0);
|
/// assert_eq!(study.n_trials(), 0);
|
||||||
///
|
///
|
||||||
|
/// let x_param = FloatParam::new(0.0, 1.0);
|
||||||
/// let mut trial = study.create_trial();
|
/// let mut trial = study.create_trial();
|
||||||
/// let _ = trial.suggest_float("x", 0.0, 1.0);
|
/// let _ = x_param.suggest(&mut trial);
|
||||||
/// study.complete_trial(trial, 0.5);
|
/// study.complete_trial(trial, 0.5);
|
||||||
/// assert_eq!(study.n_trials(), 1);
|
/// assert_eq!(study.n_trials(), 1);
|
||||||
/// ```
|
/// ```
|
||||||
@@ -280,6 +337,7 @@ where
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||||
/// use optimizer::{Direction, Study};
|
/// use optimizer::{Direction, Study};
|
||||||
///
|
///
|
||||||
/// let study: Study<f64> = Study::new(Direction::Minimize);
|
/// let study: Study<f64> = Study::new(Direction::Minimize);
|
||||||
@@ -287,12 +345,14 @@ where
|
|||||||
/// // Error when no trials completed
|
/// // Error when no trials completed
|
||||||
/// assert!(study.best_trial().is_err());
|
/// assert!(study.best_trial().is_err());
|
||||||
///
|
///
|
||||||
|
/// let x_param = FloatParam::new(0.0, 1.0);
|
||||||
|
///
|
||||||
/// let mut trial1 = study.create_trial();
|
/// let mut trial1 = study.create_trial();
|
||||||
/// let _ = trial1.suggest_float("x", 0.0, 1.0);
|
/// let _ = x_param.suggest(&mut trial1);
|
||||||
/// study.complete_trial(trial1, 0.8);
|
/// study.complete_trial(trial1, 0.8);
|
||||||
///
|
///
|
||||||
/// let mut trial2 = study.create_trial();
|
/// let mut trial2 = study.create_trial();
|
||||||
/// let _ = trial2.suggest_float("x", 0.0, 1.0);
|
/// let _ = x_param.suggest(&mut trial2);
|
||||||
/// study.complete_trial(trial2, 0.3);
|
/// study.complete_trial(trial2, 0.3);
|
||||||
///
|
///
|
||||||
/// let best = study.best_trial().unwrap();
|
/// let best = study.best_trial().unwrap();
|
||||||
@@ -343,6 +403,7 @@ where
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||||
/// use optimizer::{Direction, Study};
|
/// use optimizer::{Direction, Study};
|
||||||
///
|
///
|
||||||
/// let study: Study<f64> = Study::new(Direction::Maximize);
|
/// let study: Study<f64> = Study::new(Direction::Maximize);
|
||||||
@@ -350,12 +411,14 @@ where
|
|||||||
/// // Error when no trials completed
|
/// // Error when no trials completed
|
||||||
/// assert!(study.best_value().is_err());
|
/// assert!(study.best_value().is_err());
|
||||||
///
|
///
|
||||||
|
/// let x_param = FloatParam::new(0.0, 1.0);
|
||||||
|
///
|
||||||
/// let mut trial1 = study.create_trial();
|
/// let mut trial1 = study.create_trial();
|
||||||
/// let _ = trial1.suggest_float("x", 0.0, 1.0);
|
/// let _ = x_param.suggest(&mut trial1);
|
||||||
/// study.complete_trial(trial1, 0.3);
|
/// study.complete_trial(trial1, 0.3);
|
||||||
///
|
///
|
||||||
/// let mut trial2 = study.create_trial();
|
/// let mut trial2 = study.create_trial();
|
||||||
/// let _ = trial2.suggest_float("x", 0.0, 1.0);
|
/// let _ = x_param.suggest(&mut trial2);
|
||||||
/// study.complete_trial(trial2, 0.8);
|
/// study.complete_trial(trial2, 0.8);
|
||||||
///
|
///
|
||||||
/// let best = study.best_value().unwrap();
|
/// let best = study.best_value().unwrap();
|
||||||
@@ -392,6 +455,7 @@ where
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||||
/// use optimizer::sampler::random::RandomSampler;
|
/// use optimizer::sampler::random::RandomSampler;
|
||||||
/// use optimizer::{Direction, Study};
|
/// use optimizer::{Direction, Study};
|
||||||
///
|
///
|
||||||
@@ -399,9 +463,11 @@ where
|
|||||||
/// let sampler = RandomSampler::with_seed(42);
|
/// let sampler = RandomSampler::with_seed(42);
|
||||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
///
|
///
|
||||||
|
/// let x_param = FloatParam::new(-10.0, 10.0);
|
||||||
|
///
|
||||||
/// study
|
/// study
|
||||||
/// .optimize(10, |trial| {
|
/// .optimize(10, |trial| {
|
||||||
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
/// let x = x_param.suggest(trial)?;
|
||||||
/// Ok::<_, optimizer::Error>(x * x)
|
/// Ok::<_, optimizer::Error>(x * x)
|
||||||
/// })
|
/// })
|
||||||
/// .unwrap();
|
/// .unwrap();
|
||||||
@@ -460,6 +526,7 @@ where
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||||
/// use optimizer::sampler::random::RandomSampler;
|
/// use optimizer::sampler::random::RandomSampler;
|
||||||
/// use optimizer::{Direction, Study};
|
/// use optimizer::{Direction, Study};
|
||||||
///
|
///
|
||||||
@@ -469,12 +536,17 @@ where
|
|||||||
/// let sampler = RandomSampler::with_seed(42);
|
/// let sampler = RandomSampler::with_seed(42);
|
||||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
///
|
///
|
||||||
|
/// let x_param = FloatParam::new(-10.0, 10.0);
|
||||||
|
///
|
||||||
/// study
|
/// study
|
||||||
/// .optimize_async(10, |mut trial| async move {
|
/// .optimize_async(10, |mut trial| {
|
||||||
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
/// let x_param = x_param.clone();
|
||||||
/// // Simulate async work (e.g., network request)
|
/// async move {
|
||||||
/// let value = x * x;
|
/// let x = x_param.suggest(&mut trial)?;
|
||||||
/// Ok::<_, optimizer::Error>((trial, value))
|
/// // Simulate async work (e.g., network request)
|
||||||
|
/// let value = x * x;
|
||||||
|
/// Ok::<_, optimizer::Error>((trial, value))
|
||||||
|
/// }
|
||||||
/// })
|
/// })
|
||||||
/// .await?;
|
/// .await?;
|
||||||
///
|
///
|
||||||
@@ -542,6 +614,7 @@ where
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||||
/// use optimizer::sampler::random::RandomSampler;
|
/// use optimizer::sampler::random::RandomSampler;
|
||||||
/// use optimizer::{Direction, Study};
|
/// use optimizer::{Direction, Study};
|
||||||
///
|
///
|
||||||
@@ -551,12 +624,17 @@ where
|
|||||||
/// let sampler = RandomSampler::with_seed(42);
|
/// let sampler = RandomSampler::with_seed(42);
|
||||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
///
|
///
|
||||||
|
/// let x_param = FloatParam::new(-10.0, 10.0);
|
||||||
|
///
|
||||||
/// study
|
/// study
|
||||||
/// .optimize_parallel(10, 4, |mut trial| async move {
|
/// .optimize_parallel(10, 4, move |mut trial| {
|
||||||
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
/// let x_param = x_param.clone();
|
||||||
/// // Async objective function (e.g., network request)
|
/// async move {
|
||||||
/// let value = x * x;
|
/// let x = x_param.suggest(&mut trial)?;
|
||||||
/// Ok::<_, optimizer::Error>((trial, value))
|
/// // Async objective function (e.g., network request)
|
||||||
|
/// let value = x * x;
|
||||||
|
/// Ok::<_, optimizer::Error>((trial, value))
|
||||||
|
/// }
|
||||||
/// })
|
/// })
|
||||||
/// .await?;
|
/// .await?;
|
||||||
///
|
///
|
||||||
@@ -652,6 +730,7 @@ where
|
|||||||
/// ```
|
/// ```
|
||||||
/// use std::ops::ControlFlow;
|
/// use std::ops::ControlFlow;
|
||||||
///
|
///
|
||||||
|
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||||
/// use optimizer::sampler::random::RandomSampler;
|
/// use optimizer::sampler::random::RandomSampler;
|
||||||
/// use optimizer::{Direction, Study};
|
/// use optimizer::{Direction, Study};
|
||||||
///
|
///
|
||||||
@@ -659,11 +738,13 @@ where
|
|||||||
/// let sampler = RandomSampler::with_seed(42);
|
/// let sampler = RandomSampler::with_seed(42);
|
||||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
///
|
///
|
||||||
|
/// let x_param = FloatParam::new(-10.0, 10.0);
|
||||||
|
///
|
||||||
/// study
|
/// study
|
||||||
/// .optimize_with_callback(
|
/// .optimize_with_callback(
|
||||||
/// 100,
|
/// 100,
|
||||||
/// |trial| {
|
/// |trial| {
|
||||||
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
/// let x = x_param.suggest(trial)?;
|
||||||
/// Ok::<_, optimizer::Error>(x * x)
|
/// Ok::<_, optimizer::Error>(x * x)
|
||||||
/// },
|
/// },
|
||||||
/// |_study, completed_trial| {
|
/// |_study, completed_trial| {
|
||||||
@@ -732,256 +813,72 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Specialized implementation for Study<f64> that provides full sampler integration.
|
// Specialized implementation for Study<f64> that provides deprecated `_with_sampler` aliases.
|
||||||
|
//
|
||||||
|
// For Study<f64>, the generic methods from `impl<V> Study<V>` (like `optimize()`,
|
||||||
|
// `create_trial()`) now automatically use the sampler via the `trial_factory`.
|
||||||
|
// The `_with_sampler` method names are deprecated in favor of the generic names.
|
||||||
|
#[allow(clippy::missing_errors_doc)]
|
||||||
impl Study<f64> {
|
impl Study<f64> {
|
||||||
/// Creates a new trial with sampler integration.
|
/// Deprecated: use `create_trial()` instead.
|
||||||
///
|
///
|
||||||
/// This method creates a trial that uses the study's sampler and has access
|
/// The generic `create_trial()` now automatically integrates with the sampler
|
||||||
/// to the history of completed trials for informed parameter suggestions.
|
/// for `Study<f64>`.
|
||||||
/// This is the recommended way to create trials when using `Study<f64>`.
|
#[deprecated(
|
||||||
///
|
since = "0.2.0",
|
||||||
/// The trial's `suggest_*` methods will delegate to the sampler (e.g., TPE)
|
note = "use `create_trial()` instead — it now uses the sampler automatically for Study<f64>"
|
||||||
/// which can use historical trial data to make informed sampling decisions.
|
)]
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use optimizer::sampler::random::RandomSampler;
|
|
||||||
/// use optimizer::{Direction, Study};
|
|
||||||
///
|
|
||||||
/// // With a seeded sampler for reproducibility
|
|
||||||
/// let sampler = RandomSampler::with_seed(42);
|
|
||||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
|
||||||
/// let mut trial = study.create_trial_with_sampler();
|
|
||||||
///
|
|
||||||
/// // Parameter suggestions now use the study's sampler and history
|
|
||||||
/// let x = trial.suggest_float("x", 0.0, 1.0).unwrap();
|
|
||||||
/// ```
|
|
||||||
pub fn create_trial_with_sampler(&self) -> Trial {
|
pub fn create_trial_with_sampler(&self) -> Trial {
|
||||||
let id = self.next_trial_id();
|
self.create_trial()
|
||||||
Trial::with_sampler(
|
|
||||||
id,
|
|
||||||
Arc::clone(&self.sampler),
|
|
||||||
Arc::clone(&self.completed_trials),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs optimization with full sampler integration.
|
/// Deprecated: use `optimize()` instead.
|
||||||
///
|
///
|
||||||
/// This method is similar to the generic `optimizer` method but creates trials
|
/// The generic `optimize()` now automatically integrates with the sampler
|
||||||
/// using `create_trial_with_sampler()`, giving the sampler access to the history
|
/// for `Study<f64>`.
|
||||||
/// of completed trials for informed parameter suggestions.
|
#[deprecated(
|
||||||
///
|
since = "0.2.0",
|
||||||
/// This is the recommended way to run optimization when using `Study<f64>`
|
note = "use `optimize()` instead — it now uses the sampler automatically for Study<f64>"
|
||||||
/// with advanced samplers like TPE.
|
)]
|
||||||
///
|
pub fn optimize_with_sampler<F, E>(&self, n_trials: usize, objective: F) -> crate::Result<()>
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `n_trials` - The number of trials to run.
|
|
||||||
/// * `objective` - A closure that takes a mutable reference to a `Trial` and
|
|
||||||
/// returns the objective value or an error.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns `Error::NoCompletedTrials` if all trials failed (no successful trials).
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use optimizer::sampler::random::RandomSampler;
|
|
||||||
/// use optimizer::{Direction, Study};
|
|
||||||
///
|
|
||||||
/// // Minimize x^2 with sampler integration
|
|
||||||
/// let sampler = RandomSampler::with_seed(42);
|
|
||||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
|
||||||
///
|
|
||||||
/// study
|
|
||||||
/// .optimize_with_sampler(10, |trial| {
|
|
||||||
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
|
||||||
/// Ok::<_, optimizer::Error>(x * x)
|
|
||||||
/// })
|
|
||||||
/// .unwrap();
|
|
||||||
///
|
|
||||||
/// // At least one trial should have completed
|
|
||||||
/// assert!(study.n_trials() > 0);
|
|
||||||
/// ```
|
|
||||||
pub fn optimize_with_sampler<F, E>(
|
|
||||||
&self,
|
|
||||||
n_trials: usize,
|
|
||||||
mut objective: F,
|
|
||||||
) -> crate::Result<()>
|
|
||||||
where
|
where
|
||||||
F: FnMut(&mut Trial) -> core::result::Result<f64, E>,
|
F: FnMut(&mut Trial) -> core::result::Result<f64, E>,
|
||||||
E: ToString,
|
E: ToString,
|
||||||
{
|
{
|
||||||
for _ in 0..n_trials {
|
self.optimize(n_trials, objective)
|
||||||
let mut trial = self.create_trial_with_sampler();
|
|
||||||
|
|
||||||
match objective(&mut trial) {
|
|
||||||
Ok(value) => {
|
|
||||||
self.complete_trial(trial, value);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
self.fail_trial(trial, e.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return error if no trials succeeded
|
|
||||||
if self.n_trials() == 0 {
|
|
||||||
return Err(crate::Error::NoCompletedTrials);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs optimization with a callback and full sampler integration.
|
/// Deprecated: use `optimize_with_callback()` instead.
|
||||||
///
|
///
|
||||||
/// This method combines the benefits of `optimize_with_sampler` (sampler access
|
/// The generic `optimize_with_callback()` now automatically integrates with the
|
||||||
/// to trial history) with `optimize_with_callback` (progress monitoring and
|
/// sampler for `Study<f64>`.
|
||||||
/// early stopping).
|
#[deprecated(
|
||||||
///
|
since = "0.2.0",
|
||||||
/// # Arguments
|
note = "use `optimize_with_callback()` instead — it now uses the sampler automatically for Study<f64>"
|
||||||
///
|
)]
|
||||||
/// * `n_trials` - The maximum number of trials to run.
|
|
||||||
/// * `objective` - A closure that takes a mutable reference to a `Trial` and
|
|
||||||
/// returns the objective value or an error.
|
|
||||||
/// * `callback` - A closure called after each successful trial. Returns
|
|
||||||
/// `ControlFlow::Continue(())` to proceed or `ControlFlow::Break(())` to stop.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns `Error::NoCompletedTrials` if no trials completed successfully.
|
|
||||||
/// Returns `Error::Internal` if a completed trial is not found after adding (internal invariant violation).
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use std::ops::ControlFlow;
|
|
||||||
///
|
|
||||||
/// use optimizer::sampler::random::RandomSampler;
|
|
||||||
/// use optimizer::{Direction, Study};
|
|
||||||
///
|
|
||||||
/// // Optimize with sampler integration and early stopping
|
|
||||||
/// let sampler = RandomSampler::with_seed(42);
|
|
||||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
|
||||||
///
|
|
||||||
/// study
|
|
||||||
/// .optimize_with_callback_sampler(
|
|
||||||
/// 100,
|
|
||||||
/// |trial| {
|
|
||||||
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
|
||||||
/// Ok::<_, optimizer::Error>(x * x)
|
|
||||||
/// },
|
|
||||||
/// |study, _completed_trial| {
|
|
||||||
/// // Stop after finding 5 good trials
|
|
||||||
/// if study.n_trials() >= 5 {
|
|
||||||
/// ControlFlow::Break(())
|
|
||||||
/// } else {
|
|
||||||
/// ControlFlow::Continue(())
|
|
||||||
/// }
|
|
||||||
/// },
|
|
||||||
/// )
|
|
||||||
/// .unwrap();
|
|
||||||
///
|
|
||||||
/// assert!(study.n_trials() >= 5);
|
|
||||||
/// ```
|
|
||||||
pub fn optimize_with_callback_sampler<F, C, E>(
|
pub fn optimize_with_callback_sampler<F, C, E>(
|
||||||
&self,
|
&self,
|
||||||
n_trials: usize,
|
n_trials: usize,
|
||||||
mut objective: F,
|
objective: F,
|
||||||
mut callback: C,
|
callback: C,
|
||||||
) -> crate::Result<()>
|
) -> crate::Result<()>
|
||||||
where
|
where
|
||||||
F: FnMut(&mut Trial) -> core::result::Result<f64, E>,
|
F: FnMut(&mut Trial) -> core::result::Result<f64, E>,
|
||||||
C: FnMut(&Study<f64>, &CompletedTrial<f64>) -> ControlFlow<()>,
|
C: FnMut(&Study<f64>, &CompletedTrial<f64>) -> ControlFlow<()>,
|
||||||
E: ToString,
|
E: ToString,
|
||||||
{
|
{
|
||||||
for _ in 0..n_trials {
|
self.optimize_with_callback(n_trials, objective, callback)
|
||||||
let mut trial = self.create_trial_with_sampler();
|
|
||||||
|
|
||||||
match objective(&mut trial) {
|
|
||||||
Ok(value) => {
|
|
||||||
self.complete_trial(trial, value);
|
|
||||||
|
|
||||||
// Get the just-completed trial for the callback
|
|
||||||
let trials = self.completed_trials.read();
|
|
||||||
let Some(completed) = trials.last() else {
|
|
||||||
return Err(crate::Error::Internal(
|
|
||||||
"completed trial not found after adding",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Call the callback and check if we should stop
|
|
||||||
// Note: We need to drop the read lock before calling callback
|
|
||||||
// to avoid potential deadlock if callback accesses the study
|
|
||||||
let completed_clone = completed.clone();
|
|
||||||
drop(trials);
|
|
||||||
|
|
||||||
if let ControlFlow::Break(()) = callback(self, &completed_clone) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
self.fail_trial(trial, e.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return error if no trials succeeded
|
|
||||||
if self.n_trials() == 0 {
|
|
||||||
return Err(crate::Error::NoCompletedTrials);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs optimization asynchronously with full sampler integration.
|
/// Deprecated: use `optimize_async()` instead.
|
||||||
///
|
///
|
||||||
/// This method combines async execution with the TPE sampler's ability to use
|
/// The generic `optimize_async()` now automatically integrates with the sampler
|
||||||
/// historical trial data for informed parameter suggestions.
|
/// for `Study<f64>`.
|
||||||
///
|
|
||||||
/// The objective function takes ownership of the `Trial` and must return it
|
|
||||||
/// along with the result. This allows async operations to use the trial
|
|
||||||
/// across await points.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `n_trials` - The number of trials to run.
|
|
||||||
/// * `objective` - A function that takes a `Trial` and returns a `Future`
|
|
||||||
/// that resolves to a tuple of `(Trial, Result<f64, E>)`.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns `Error::NoCompletedTrials` if all trials failed (no successful trials).
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use optimizer::sampler::random::RandomSampler;
|
|
||||||
/// use optimizer::{Direction, Study};
|
|
||||||
///
|
|
||||||
/// # #[cfg(feature = "async")]
|
|
||||||
/// # async fn example() -> optimizer::Result<()> {
|
|
||||||
/// // Minimize x^2 with async objective and sampler integration
|
|
||||||
/// let sampler = RandomSampler::with_seed(42);
|
|
||||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
|
||||||
///
|
|
||||||
/// study
|
|
||||||
/// .optimize_async_with_sampler(10, |mut trial| async move {
|
|
||||||
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
|
||||||
/// // Simulate async work (e.g., network request)
|
|
||||||
/// let value = x * x;
|
|
||||||
/// Ok::<_, optimizer::Error>((trial, value))
|
|
||||||
/// })
|
|
||||||
/// .await?;
|
|
||||||
///
|
|
||||||
/// // At least one trial should have completed
|
|
||||||
/// assert!(study.n_trials() > 0);
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
#[cfg(feature = "async")]
|
#[cfg(feature = "async")]
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.2.0",
|
||||||
|
note = "use `optimize_async()` instead — it now uses the sampler automatically for Study<f64>"
|
||||||
|
)]
|
||||||
pub async fn optimize_async_with_sampler<F, Fut, E>(
|
pub async fn optimize_async_with_sampler<F, Fut, E>(
|
||||||
&self,
|
&self,
|
||||||
n_trials: usize,
|
n_trials: usize,
|
||||||
@@ -992,78 +889,18 @@ impl Study<f64> {
|
|||||||
Fut: Future<Output = core::result::Result<(Trial, f64), E>>,
|
Fut: Future<Output = core::result::Result<(Trial, f64), E>>,
|
||||||
E: ToString,
|
E: ToString,
|
||||||
{
|
{
|
||||||
for _ in 0..n_trials {
|
self.optimize_async(n_trials, objective).await
|
||||||
let trial = self.create_trial_with_sampler();
|
|
||||||
|
|
||||||
match objective(trial).await {
|
|
||||||
Ok((trial, value)) => {
|
|
||||||
self.complete_trial(trial, value);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
// For async, we don't have the trial back on error
|
|
||||||
// We'll just count this as a failed trial without recording it
|
|
||||||
let _ = e.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return error if no trials succeeded
|
|
||||||
if self.n_trials() == 0 {
|
|
||||||
return Err(crate::Error::NoCompletedTrials);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs optimization with bounded parallelism and full sampler integration.
|
/// Deprecated: use `optimize_parallel()` instead.
|
||||||
///
|
///
|
||||||
/// This method combines parallel async execution with the TPE sampler's ability
|
/// The generic `optimize_parallel()` now automatically integrates with the
|
||||||
/// to use historical trial data for informed parameter suggestions. Up to
|
/// sampler for `Study<f64>`.
|
||||||
/// `concurrency` trials run simultaneously.
|
|
||||||
///
|
|
||||||
/// The objective function takes ownership of the `Trial` and must return it
|
|
||||||
/// along with the result. This allows async operations to use the trial
|
|
||||||
/// across await points.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `n_trials` - The total number of trials to run.
|
|
||||||
/// * `concurrency` - The maximum number of trials to run simultaneously.
|
|
||||||
/// * `objective` - A function that takes a `Trial` and returns a `Future`
|
|
||||||
/// that resolves to a tuple of `(Trial, f64)` or an error.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns `Error::NoCompletedTrials` if all trials failed (no successful trials).
|
|
||||||
/// Returns `Error::TaskError` if the semaphore is closed or a spawned task panics.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use optimizer::sampler::random::RandomSampler;
|
|
||||||
/// use optimizer::{Direction, Study};
|
|
||||||
///
|
|
||||||
/// # #[cfg(feature = "async")]
|
|
||||||
/// # async fn example() -> optimizer::Result<()> {
|
|
||||||
/// // Minimize x^2 with parallel async evaluation and sampler integration
|
|
||||||
/// let sampler = RandomSampler::with_seed(42);
|
|
||||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
|
||||||
///
|
|
||||||
/// study
|
|
||||||
/// .optimize_parallel_with_sampler(10, 4, |mut trial| async move {
|
|
||||||
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
|
||||||
/// // Async objective function (e.g., network request)
|
|
||||||
/// let value = x * x;
|
|
||||||
/// Ok::<_, optimizer::Error>((trial, value))
|
|
||||||
/// })
|
|
||||||
/// .await?;
|
|
||||||
///
|
|
||||||
/// // All trials should have completed
|
|
||||||
/// assert_eq!(study.n_trials(), 10);
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
#[cfg(feature = "async")]
|
#[cfg(feature = "async")]
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.2.0",
|
||||||
|
note = "use `optimize_parallel()` instead — it now uses the sampler automatically for Study<f64>"
|
||||||
|
)]
|
||||||
pub async fn optimize_parallel_with_sampler<F, Fut, E>(
|
pub async fn optimize_parallel_with_sampler<F, Fut, E>(
|
||||||
&self,
|
&self,
|
||||||
n_trials: usize,
|
n_trials: usize,
|
||||||
@@ -1075,51 +912,7 @@ impl Study<f64> {
|
|||||||
Fut: Future<Output = core::result::Result<(Trial, f64), E>> + Send,
|
Fut: Future<Output = core::result::Result<(Trial, f64), E>> + Send,
|
||||||
E: ToString + Send + 'static,
|
E: ToString + Send + 'static,
|
||||||
{
|
{
|
||||||
use tokio::sync::Semaphore;
|
self.optimize_parallel(n_trials, concurrency, objective)
|
||||||
|
.await
|
||||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
|
||||||
let objective = Arc::new(objective);
|
|
||||||
|
|
||||||
let mut handles = Vec::with_capacity(n_trials);
|
|
||||||
|
|
||||||
for _ in 0..n_trials {
|
|
||||||
let permit = semaphore
|
|
||||||
.clone()
|
|
||||||
.acquire_owned()
|
|
||||||
.await
|
|
||||||
.map_err(|e| crate::Error::TaskError(e.to_string()))?;
|
|
||||||
let trial = self.create_trial_with_sampler();
|
|
||||||
let objective = Arc::clone(&objective);
|
|
||||||
|
|
||||||
let handle = tokio::spawn(async move {
|
|
||||||
let result = objective(trial).await;
|
|
||||||
drop(permit); // Release semaphore permit when done
|
|
||||||
result
|
|
||||||
});
|
|
||||||
|
|
||||||
handles.push(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all tasks and record results
|
|
||||||
for handle in handles {
|
|
||||||
match handle
|
|
||||||
.await
|
|
||||||
.map_err(|e| crate::Error::TaskError(e.to_string()))?
|
|
||||||
{
|
|
||||||
Ok((trial, value)) => {
|
|
||||||
self.complete_trial(trial, value);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
let _ = e.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return error if no trials succeeded
|
|
||||||
if self.n_trials() == 0 {
|
|
||||||
return Err(crate::Error::NoCompletedTrials);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-767
@@ -1,81 +1,17 @@
|
|||||||
//! Trial implementation for tracking sampled parameters and trial state.
|
//! Trial implementation for tracking sampled parameters and trial state.
|
||||||
|
|
||||||
use core::ops::{Range, RangeInclusive};
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
|
|
||||||
use crate::distribution::{
|
use crate::distribution::Distribution;
|
||||||
CategoricalDistribution, Distribution, FloatDistribution, IntDistribution,
|
|
||||||
};
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::param::ParamValue;
|
use crate::param::ParamValue;
|
||||||
|
use crate::parameter::{ParamId, Parameter};
|
||||||
use crate::sampler::{CompletedTrial, Sampler};
|
use crate::sampler::{CompletedTrial, Sampler};
|
||||||
use crate::types::TrialState;
|
use crate::types::TrialState;
|
||||||
|
|
||||||
/// A trait for types that can be used with [`Trial::suggest_range`].
|
|
||||||
///
|
|
||||||
/// This trait is implemented for [`Range`] and [`RangeInclusive`] over `f64` and `i64`.
|
|
||||||
/// It allows using Rust's range syntax directly with the optimizer.
|
|
||||||
///
|
|
||||||
/// # Supported Range Types
|
|
||||||
///
|
|
||||||
/// | Range Type | Example | Description |
|
|
||||||
/// |------------|---------|-------------|
|
|
||||||
/// | `Range<f64>` | `0.0..1.0` | Float range (end-exclusive, treated as inclusive for continuous sampling) |
|
|
||||||
/// | `RangeInclusive<f64>` | `0.0..=1.0` | Float range (end-inclusive) |
|
|
||||||
/// | `Range<i64>` | `1..10` | Integer range from 1 to 9 (end-exclusive) |
|
|
||||||
/// | `RangeInclusive<i64>` | `1..=10` | Integer range from 1 to 10 (end-inclusive) |
|
|
||||||
pub trait SuggestableRange {
|
|
||||||
/// The output type when suggesting from this range.
|
|
||||||
type Output;
|
|
||||||
|
|
||||||
/// Suggests a value from this range using the given trial.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns an error if the range is invalid (e.g., empty or low > high).
|
|
||||||
fn suggest(self, trial: &mut Trial, name: String) -> Result<Self::Output>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SuggestableRange for Range<f64> {
|
|
||||||
type Output = f64;
|
|
||||||
|
|
||||||
fn suggest(self, trial: &mut Trial, name: String) -> Result<f64> {
|
|
||||||
trial.suggest_float(name, self.start, self.end)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SuggestableRange for RangeInclusive<f64> {
|
|
||||||
type Output = f64;
|
|
||||||
|
|
||||||
fn suggest(self, trial: &mut Trial, name: String) -> Result<f64> {
|
|
||||||
trial.suggest_float(name, *self.start(), *self.end())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SuggestableRange for Range<i64> {
|
|
||||||
type Output = i64;
|
|
||||||
|
|
||||||
fn suggest(self, trial: &mut Trial, name: String) -> Result<i64> {
|
|
||||||
// Range is exclusive on the end, so subtract 1
|
|
||||||
trial.suggest_int(
|
|
||||||
name,
|
|
||||||
self.start,
|
|
||||||
self.end.checked_sub(1).unwrap_or(self.end),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SuggestableRange for RangeInclusive<i64> {
|
|
||||||
type Output = i64;
|
|
||||||
|
|
||||||
fn suggest(self, trial: &mut Trial, name: String) -> Result<i64> {
|
|
||||||
trial.suggest_int(name, *self.start(), *self.end())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A trial represents a single evaluation of the objective function.
|
/// A trial represents a single evaluation of the objective function.
|
||||||
///
|
///
|
||||||
/// Each trial has a unique ID and stores the sampled parameters along with
|
/// Each trial has a unique ID and stores the sampled parameters along with
|
||||||
@@ -90,10 +26,12 @@ pub struct Trial {
|
|||||||
id: u64,
|
id: u64,
|
||||||
/// Current state of the trial.
|
/// Current state of the trial.
|
||||||
state: TrialState,
|
state: TrialState,
|
||||||
/// Sampled parameter values, keyed by parameter name.
|
/// Sampled parameter values, keyed by parameter id.
|
||||||
params: HashMap<String, ParamValue>,
|
params: HashMap<ParamId, ParamValue>,
|
||||||
/// Parameter distributions, keyed by parameter name.
|
/// Parameter distributions, keyed by parameter id.
|
||||||
distributions: HashMap<String, Distribution>,
|
distributions: HashMap<ParamId, Distribution>,
|
||||||
|
/// Human-readable labels for parameters, keyed by parameter id.
|
||||||
|
param_labels: HashMap<ParamId, String>,
|
||||||
/// The sampler to use for generating parameter values.
|
/// The sampler to use for generating parameter values.
|
||||||
sampler: Option<Arc<dyn Sampler>>,
|
sampler: Option<Arc<dyn Sampler>>,
|
||||||
/// Access to the history of completed trials (shared with Study).
|
/// Access to the history of completed trials (shared with Study).
|
||||||
@@ -107,6 +45,7 @@ impl core::fmt::Debug for Trial {
|
|||||||
.field("state", &self.state)
|
.field("state", &self.state)
|
||||||
.field("params", &self.params)
|
.field("params", &self.params)
|
||||||
.field("distributions", &self.distributions)
|
.field("distributions", &self.distributions)
|
||||||
|
.field("param_labels", &self.param_labels)
|
||||||
.field("has_sampler", &self.sampler.is_some())
|
.field("has_sampler", &self.sampler.is_some())
|
||||||
.field("has_history", &self.history.is_some())
|
.field("has_history", &self.history.is_some())
|
||||||
.finish()
|
.finish()
|
||||||
@@ -118,7 +57,7 @@ impl Trial {
|
|||||||
///
|
///
|
||||||
/// The trial starts in the `Running` state with no parameters sampled.
|
/// The trial starts in the `Running` state with no parameters sampled.
|
||||||
/// This constructor creates a trial without a sampler, which will use
|
/// This constructor creates a trial without a sampler, which will use
|
||||||
/// local random sampling for suggest_* methods.
|
/// local random sampling for suggest methods.
|
||||||
///
|
///
|
||||||
/// For trials that use the study's sampler, use `Trial::with_sampler` instead.
|
/// For trials that use the study's sampler, use `Trial::with_sampler` instead.
|
||||||
///
|
///
|
||||||
@@ -141,6 +80,7 @@ impl Trial {
|
|||||||
state: TrialState::Running,
|
state: TrialState::Running,
|
||||||
params: HashMap::new(),
|
params: HashMap::new(),
|
||||||
distributions: HashMap::new(),
|
distributions: HashMap::new(),
|
||||||
|
param_labels: HashMap::new(),
|
||||||
sampler: None,
|
sampler: None,
|
||||||
history: None,
|
history: None,
|
||||||
}
|
}
|
||||||
@@ -166,6 +106,7 @@ impl Trial {
|
|||||||
state: TrialState::Running,
|
state: TrialState::Running,
|
||||||
params: HashMap::new(),
|
params: HashMap::new(),
|
||||||
distributions: HashMap::new(),
|
distributions: HashMap::new(),
|
||||||
|
param_labels: HashMap::new(),
|
||||||
sampler: Some(sampler),
|
sampler: Some(sampler),
|
||||||
history: Some(history),
|
history: Some(history),
|
||||||
}
|
}
|
||||||
@@ -202,16 +143,22 @@ impl Trial {
|
|||||||
|
|
||||||
/// Returns a reference to the sampled parameters.
|
/// Returns a reference to the sampled parameters.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn params(&self) -> &HashMap<String, ParamValue> {
|
pub fn params(&self) -> &HashMap<ParamId, ParamValue> {
|
||||||
&self.params
|
&self.params
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a reference to the parameter distributions.
|
/// Returns a reference to the parameter distributions.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn distributions(&self) -> &HashMap<String, Distribution> {
|
pub fn distributions(&self) -> &HashMap<ParamId, Distribution> {
|
||||||
&self.distributions
|
&self.distributions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the parameter labels.
|
||||||
|
#[must_use]
|
||||||
|
pub fn param_labels(&self) -> &HashMap<ParamId, String> {
|
||||||
|
&self.param_labels
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the trial state to Complete.
|
/// Sets the trial state to Complete.
|
||||||
pub(crate) fn set_complete(&mut self) {
|
pub(crate) fn set_complete(&mut self) {
|
||||||
self.state = TrialState::Complete;
|
self.state = TrialState::Complete;
|
||||||
@@ -222,730 +169,69 @@ impl Trial {
|
|||||||
self.state = TrialState::Failed;
|
self.state = TrialState::Failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Suggests a float parameter with the given bounds.
|
/// Suggests a parameter value using a [`Parameter`] definition.
|
||||||
///
|
///
|
||||||
/// If the parameter has already been sampled with the same bounds, the cached value is returned.
|
/// This is the primary entry point for sampling parameters. It handles
|
||||||
/// If the parameter was sampled with different bounds, a `ParameterConflict` error is returned.
|
/// validation, caching, conflict detection, sampling, and conversion.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `name` - The name of the parameter.
|
/// * `param` - The parameter definition.
|
||||||
/// * `low` - The lower bound (inclusive).
|
|
||||||
/// * `high` - The upper bound (inclusive).
|
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns `InvalidBounds` if `low > high`.
|
/// Returns an error if:
|
||||||
/// Returns `ParameterConflict` if the parameter was previously sampled with different bounds.
|
/// - The parameter fails validation
|
||||||
|
/// - The parameter conflicts with a previously suggested parameter of the same id
|
||||||
|
/// - Sampling or conversion fails
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use optimizer::Trial;
|
/// use optimizer::Trial;
|
||||||
|
/// use optimizer::parameter::{BoolParam, FloatParam, IntParam, Parameter};
|
||||||
|
///
|
||||||
|
/// let x_param = FloatParam::new(0.0, 1.0);
|
||||||
|
/// let n_param = IntParam::new(1, 10);
|
||||||
|
/// let flag_param = BoolParam::new();
|
||||||
///
|
///
|
||||||
/// let mut trial = Trial::new(0);
|
/// let mut trial = Trial::new(0);
|
||||||
/// let x = trial.suggest_float("x", 0.0, 1.0).unwrap();
|
|
||||||
/// assert!(x >= 0.0 && x <= 1.0);
|
|
||||||
///
|
///
|
||||||
/// // Calling again with same bounds returns cached value
|
/// let x = trial.suggest_param(&x_param).unwrap();
|
||||||
/// let x2 = trial.suggest_float("x", 0.0, 1.0).unwrap();
|
/// let n = trial.suggest_param(&n_param).unwrap();
|
||||||
/// assert_eq!(x, x2);
|
/// let flag = trial.suggest_param(&flag_param).unwrap();
|
||||||
/// ```
|
/// ```
|
||||||
pub fn suggest_float(&mut self, name: impl Into<String>, low: f64, high: f64) -> Result<f64> {
|
pub fn suggest_param<P: Parameter>(&mut self, param: &P) -> Result<P::Value> {
|
||||||
if low > high {
|
param.validate()?;
|
||||||
return Err(Error::InvalidBounds { low, high });
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = name.into();
|
let param_id = param.id();
|
||||||
let distribution = FloatDistribution {
|
let distribution = param.distribution();
|
||||||
low,
|
|
||||||
high,
|
|
||||||
log_scale: false,
|
|
||||||
step: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if parameter already exists
|
// Check if parameter already exists
|
||||||
if let Some(existing_dist) = self.distributions.get(&name) {
|
if let Some(existing_dist) = self.distributions.get(¶m_id) {
|
||||||
// Verify the distribution matches
|
if *existing_dist == distribution {
|
||||||
if let Distribution::Float(existing) = existing_dist
|
|
||||||
&& (existing.low - low).abs() < f64::EPSILON
|
|
||||||
&& (existing.high - high).abs() < f64::EPSILON
|
|
||||||
&& !existing.log_scale
|
|
||||||
&& existing.step.is_none()
|
|
||||||
{
|
|
||||||
// Same distribution, return cached value
|
// Same distribution, return cached value
|
||||||
if let Some(ParamValue::Float(value)) = self.params.get(&name) {
|
if let Some(value) = self.params.get(¶m_id) {
|
||||||
return Ok(*value);
|
return param.cast_param_value(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Distribution exists but doesn't match
|
// Distribution exists but doesn't match
|
||||||
return Err(Error::ParameterConflict {
|
return Err(Error::ParameterConflict {
|
||||||
name,
|
name: param.label(),
|
||||||
reason: "parameter was previously sampled with different bounds or type"
|
reason: "parameter was previously sampled with different configuration or type"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sample using the sampler
|
// Sample using the sampler
|
||||||
let dist = Distribution::Float(distribution);
|
let value = self.sample_value(&distribution);
|
||||||
let ParamValue::Float(value) = self.sample_value(&dist) else {
|
let result = param.cast_param_value(&value)?;
|
||||||
return Err(Error::Internal(
|
|
||||||
"Float distribution should return Float value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store distribution and value
|
// Store distribution, value, and label
|
||||||
self.distributions.insert(name.clone(), dist);
|
self.distributions.insert(param_id, distribution);
|
||||||
self.params.insert(name, ParamValue::Float(value));
|
self.params.insert(param_id, value);
|
||||||
|
self.param_labels.insert(param_id, param.label());
|
||||||
|
|
||||||
Ok(value)
|
Ok(result)
|
||||||
}
|
|
||||||
|
|
||||||
/// Suggests a float parameter sampled on a logarithmic scale.
|
|
||||||
///
|
|
||||||
/// The value is sampled uniformly in log space, which is useful for parameters
|
|
||||||
/// that span multiple orders of magnitude (e.g., learning rates).
|
|
||||||
///
|
|
||||||
/// If the parameter has already been sampled with the same bounds and `log_scale=true`,
|
|
||||||
/// the cached value is returned. If the parameter was sampled with different configuration,
|
|
||||||
/// a `ParameterConflict` error is returned.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `name` - The name of the parameter.
|
|
||||||
/// * `low` - The lower bound (inclusive, must be positive).
|
|
||||||
/// * `high` - The upper bound (inclusive).
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns `InvalidLogBounds` if `low <= 0`.
|
|
||||||
/// Returns `InvalidBounds` if `low > high`.
|
|
||||||
/// Returns `ParameterConflict` if the parameter was previously sampled with different configuration.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use optimizer::Trial;
|
|
||||||
///
|
|
||||||
/// let mut trial = Trial::new(0);
|
|
||||||
/// let lr = trial
|
|
||||||
/// .suggest_float_log("learning_rate", 1e-5, 1e-1)
|
|
||||||
/// .unwrap();
|
|
||||||
/// assert!(lr >= 1e-5 && lr <= 1e-1);
|
|
||||||
///
|
|
||||||
/// // Calling again with same bounds returns cached value
|
|
||||||
/// let lr2 = trial
|
|
||||||
/// .suggest_float_log("learning_rate", 1e-5, 1e-1)
|
|
||||||
/// .unwrap();
|
|
||||||
/// assert_eq!(lr, lr2);
|
|
||||||
/// ```
|
|
||||||
pub fn suggest_float_log(
|
|
||||||
&mut self,
|
|
||||||
name: impl Into<String>,
|
|
||||||
low: f64,
|
|
||||||
high: f64,
|
|
||||||
) -> Result<f64> {
|
|
||||||
if low <= 0.0 {
|
|
||||||
return Err(Error::InvalidLogBounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
if low > high {
|
|
||||||
return Err(Error::InvalidBounds { low, high });
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = name.into();
|
|
||||||
let distribution = FloatDistribution {
|
|
||||||
low,
|
|
||||||
high,
|
|
||||||
log_scale: true,
|
|
||||||
step: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if parameter already exists
|
|
||||||
if let Some(existing_dist) = self.distributions.get(&name) {
|
|
||||||
// Verify the distribution matches
|
|
||||||
if let Distribution::Float(existing) = existing_dist
|
|
||||||
&& (existing.low - low).abs() < f64::EPSILON
|
|
||||||
&& (existing.high - high).abs() < f64::EPSILON
|
|
||||||
&& existing.log_scale
|
|
||||||
&& existing.step.is_none()
|
|
||||||
{
|
|
||||||
// Same distribution, return cached value
|
|
||||||
if let Some(ParamValue::Float(value)) = self.params.get(&name) {
|
|
||||||
return Ok(*value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Distribution exists but doesn't match
|
|
||||||
return Err(Error::ParameterConflict {
|
|
||||||
name,
|
|
||||||
reason: "parameter was previously sampled with different bounds or type"
|
|
||||||
.to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sample using the sampler (sampler handles log-scale transformation)
|
|
||||||
let dist = Distribution::Float(distribution);
|
|
||||||
let ParamValue::Float(value) = self.sample_value(&dist) else {
|
|
||||||
return Err(Error::Internal(
|
|
||||||
"Float distribution should return Float value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store distribution and value
|
|
||||||
self.distributions.insert(name.clone(), dist);
|
|
||||||
self.params.insert(name, ParamValue::Float(value));
|
|
||||||
|
|
||||||
Ok(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Suggests a float parameter that snaps to a step grid.
|
|
||||||
///
|
|
||||||
/// The value is sampled from the discrete set {low, low + step, low + 2*step, ...}
|
|
||||||
/// where each value is <= high.
|
|
||||||
///
|
|
||||||
/// If the parameter has already been sampled with the same configuration,
|
|
||||||
/// the cached value is returned. If the parameter was sampled with different configuration,
|
|
||||||
/// a `ParameterConflict` error is returned.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `name` - The name of the parameter.
|
|
||||||
/// * `low` - The lower bound (inclusive).
|
|
||||||
/// * `high` - The upper bound (inclusive).
|
|
||||||
/// * `step` - The step size (must be positive).
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns `InvalidStep` if `step <= 0`.
|
|
||||||
/// Returns `InvalidBounds` if `low > high`.
|
|
||||||
/// Returns `ParameterConflict` if the parameter was previously sampled with different configuration.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use optimizer::Trial;
|
|
||||||
///
|
|
||||||
/// let mut trial = Trial::new(0);
|
|
||||||
/// let x = trial.suggest_float_step("x", 0.0, 1.0, 0.25).unwrap();
|
|
||||||
/// // x will be one of: 0.0, 0.25, 0.5, 0.75, 1.0
|
|
||||||
/// assert!(x >= 0.0 && x <= 1.0);
|
|
||||||
/// assert!((x / 0.25).fract().abs() < 1e-10 || (x / 0.25).fract().abs() > 1.0 - 1e-10);
|
|
||||||
///
|
|
||||||
/// // Calling again with same bounds returns cached value
|
|
||||||
/// let x2 = trial.suggest_float_step("x", 0.0, 1.0, 0.25).unwrap();
|
|
||||||
/// assert_eq!(x, x2);
|
|
||||||
/// ```
|
|
||||||
pub fn suggest_float_step(
|
|
||||||
&mut self,
|
|
||||||
name: impl Into<String>,
|
|
||||||
low: f64,
|
|
||||||
high: f64,
|
|
||||||
step: f64,
|
|
||||||
) -> Result<f64> {
|
|
||||||
if step <= 0.0 {
|
|
||||||
return Err(Error::InvalidStep);
|
|
||||||
}
|
|
||||||
|
|
||||||
if low > high {
|
|
||||||
return Err(Error::InvalidBounds { low, high });
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = name.into();
|
|
||||||
let distribution = FloatDistribution {
|
|
||||||
low,
|
|
||||||
high,
|
|
||||||
log_scale: false,
|
|
||||||
step: Some(step),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if parameter already exists
|
|
||||||
if let Some(existing_dist) = self.distributions.get(&name) {
|
|
||||||
// Verify the distribution matches
|
|
||||||
if let Distribution::Float(existing) = existing_dist
|
|
||||||
&& (existing.low - low).abs() < f64::EPSILON
|
|
||||||
&& (existing.high - high).abs() < f64::EPSILON
|
|
||||||
&& !existing.log_scale
|
|
||||||
&& existing.step == Some(step)
|
|
||||||
{
|
|
||||||
// Same distribution, return cached value
|
|
||||||
if let Some(ParamValue::Float(value)) = self.params.get(&name) {
|
|
||||||
return Ok(*value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Distribution exists but doesn't match
|
|
||||||
return Err(Error::ParameterConflict {
|
|
||||||
name,
|
|
||||||
reason: "parameter was previously sampled with different bounds or type"
|
|
||||||
.to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sample using the sampler (sampler handles step-grid)
|
|
||||||
let dist = Distribution::Float(distribution);
|
|
||||||
let ParamValue::Float(value) = self.sample_value(&dist) else {
|
|
||||||
return Err(Error::Internal(
|
|
||||||
"Float distribution should return Float value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store distribution and value
|
|
||||||
self.distributions.insert(name.clone(), dist);
|
|
||||||
self.params.insert(name, ParamValue::Float(value));
|
|
||||||
|
|
||||||
Ok(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Suggests an integer parameter with the given bounds.
|
|
||||||
///
|
|
||||||
/// The value is sampled uniformly from the range [low, high] inclusive.
|
|
||||||
///
|
|
||||||
/// If the parameter has already been sampled with the same bounds, the cached value is returned.
|
|
||||||
/// If the parameter was sampled with different bounds, a `ParameterConflict` error is returned.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `name` - The name of the parameter.
|
|
||||||
/// * `low` - The lower bound (inclusive).
|
|
||||||
/// * `high` - The upper bound (inclusive).
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns `InvalidBounds` if `low > high`.
|
|
||||||
/// Returns `ParameterConflict` if the parameter was previously sampled with different bounds.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use optimizer::Trial;
|
|
||||||
///
|
|
||||||
/// let mut trial = Trial::new(0);
|
|
||||||
/// let n = trial.suggest_int("n_layers", 1, 10).unwrap();
|
|
||||||
/// assert!(n >= 1 && n <= 10);
|
|
||||||
///
|
|
||||||
/// // Calling again with same bounds returns cached value
|
|
||||||
/// let n2 = trial.suggest_int("n_layers", 1, 10).unwrap();
|
|
||||||
/// assert_eq!(n, n2);
|
|
||||||
/// ```
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
|
||||||
pub fn suggest_int(&mut self, name: impl Into<String>, low: i64, high: i64) -> Result<i64> {
|
|
||||||
if low > high {
|
|
||||||
return Err(Error::InvalidBounds {
|
|
||||||
low: low as f64,
|
|
||||||
high: high as f64,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = name.into();
|
|
||||||
let distribution = IntDistribution {
|
|
||||||
low,
|
|
||||||
high,
|
|
||||||
log_scale: false,
|
|
||||||
step: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if parameter already exists
|
|
||||||
if let Some(existing_dist) = self.distributions.get(&name) {
|
|
||||||
// Verify the distribution matches
|
|
||||||
if let Distribution::Int(existing) = existing_dist
|
|
||||||
&& existing.low == low
|
|
||||||
&& existing.high == high
|
|
||||||
&& !existing.log_scale
|
|
||||||
&& existing.step.is_none()
|
|
||||||
{
|
|
||||||
// Same distribution, return cached value
|
|
||||||
if let Some(ParamValue::Int(value)) = self.params.get(&name) {
|
|
||||||
return Ok(*value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Distribution exists but doesn't match
|
|
||||||
return Err(Error::ParameterConflict {
|
|
||||||
name,
|
|
||||||
reason: "parameter was previously sampled with different bounds or type"
|
|
||||||
.to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sample using the sampler
|
|
||||||
let dist = Distribution::Int(distribution);
|
|
||||||
let ParamValue::Int(value) = self.sample_value(&dist) else {
|
|
||||||
return Err(Error::Internal("Int distribution should return Int value"));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store distribution and value
|
|
||||||
self.distributions.insert(name.clone(), dist);
|
|
||||||
self.params.insert(name, ParamValue::Int(value));
|
|
||||||
|
|
||||||
Ok(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Suggests an integer parameter sampled on a logarithmic scale.
|
|
||||||
///
|
|
||||||
/// The value is sampled uniformly in log space, which is useful for parameters
|
|
||||||
/// that span multiple orders of magnitude (e.g., batch sizes).
|
|
||||||
///
|
|
||||||
/// If the parameter has already been sampled with the same bounds and `log_scale=true`,
|
|
||||||
/// the cached value is returned. If the parameter was sampled with different configuration,
|
|
||||||
/// a `ParameterConflict` error is returned.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `name` - The name of the parameter.
|
|
||||||
/// * `low` - The lower bound (inclusive, must be >= 1).
|
|
||||||
/// * `high` - The upper bound (inclusive).
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns `InvalidLogBounds` if `low < 1`.
|
|
||||||
/// Returns `InvalidBounds` if `low > high`.
|
|
||||||
/// Returns `ParameterConflict` if the parameter was previously sampled with different configuration.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use optimizer::Trial;
|
|
||||||
///
|
|
||||||
/// let mut trial = Trial::new(0);
|
|
||||||
/// let batch_size = trial.suggest_int_log("batch_size", 1, 1024).unwrap();
|
|
||||||
/// assert!(batch_size >= 1 && batch_size <= 1024);
|
|
||||||
///
|
|
||||||
/// // Calling again with same bounds returns cached value
|
|
||||||
/// let batch_size2 = trial.suggest_int_log("batch_size", 1, 1024).unwrap();
|
|
||||||
/// assert_eq!(batch_size, batch_size2);
|
|
||||||
/// ```
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
|
||||||
pub fn suggest_int_log(&mut self, name: impl Into<String>, low: i64, high: i64) -> Result<i64> {
|
|
||||||
if low < 1 {
|
|
||||||
return Err(Error::InvalidLogBounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
if low > high {
|
|
||||||
return Err(Error::InvalidBounds {
|
|
||||||
low: low as f64,
|
|
||||||
high: high as f64,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = name.into();
|
|
||||||
let distribution = IntDistribution {
|
|
||||||
low,
|
|
||||||
high,
|
|
||||||
log_scale: true,
|
|
||||||
step: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if parameter already exists
|
|
||||||
if let Some(existing_dist) = self.distributions.get(&name) {
|
|
||||||
// Verify the distribution matches
|
|
||||||
if let Distribution::Int(existing) = existing_dist
|
|
||||||
&& existing.low == low
|
|
||||||
&& existing.high == high
|
|
||||||
&& existing.log_scale
|
|
||||||
&& existing.step.is_none()
|
|
||||||
{
|
|
||||||
// Same distribution, return cached value
|
|
||||||
if let Some(ParamValue::Int(value)) = self.params.get(&name) {
|
|
||||||
return Ok(*value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Distribution exists but doesn't match
|
|
||||||
return Err(Error::ParameterConflict {
|
|
||||||
name,
|
|
||||||
reason: "parameter was previously sampled with different bounds or type"
|
|
||||||
.to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sample using the sampler (sampler handles log-scale transformation)
|
|
||||||
let dist = Distribution::Int(distribution);
|
|
||||||
let ParamValue::Int(value) = self.sample_value(&dist) else {
|
|
||||||
return Err(Error::Internal("Int distribution should return Int value"));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store distribution and value
|
|
||||||
self.distributions.insert(name.clone(), dist);
|
|
||||||
self.params.insert(name, ParamValue::Int(value));
|
|
||||||
|
|
||||||
Ok(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Suggests an integer parameter that snaps to a step grid.
|
|
||||||
///
|
|
||||||
/// The value is sampled from the discrete set {low, low + step, low + 2*step, ...}
|
|
||||||
/// where each value is <= high.
|
|
||||||
///
|
|
||||||
/// If the parameter has already been sampled with the same configuration,
|
|
||||||
/// the cached value is returned. If the parameter was sampled with different configuration,
|
|
||||||
/// a `ParameterConflict` error is returned.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `name` - The name of the parameter.
|
|
||||||
/// * `low` - The lower bound (inclusive).
|
|
||||||
/// * `high` - The upper bound (inclusive).
|
|
||||||
/// * `step` - The step size (must be positive).
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns `InvalidStep` if `step <= 0`.
|
|
||||||
/// Returns `InvalidBounds` if `low > high`.
|
|
||||||
/// Returns `ParameterConflict` if the parameter was previously sampled with different configuration.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use optimizer::Trial;
|
|
||||||
///
|
|
||||||
/// let mut trial = Trial::new(0);
|
|
||||||
/// let n = trial
|
|
||||||
/// .suggest_int_step("n_estimators", 100, 500, 50)
|
|
||||||
/// .unwrap();
|
|
||||||
/// // n will be one of: 100, 150, 200, 250, 300, 350, 400, 450, 500
|
|
||||||
/// assert!(n >= 100 && n <= 500);
|
|
||||||
/// assert!((n - 100) % 50 == 0);
|
|
||||||
///
|
|
||||||
/// // Calling again with same bounds returns cached value
|
|
||||||
/// let n2 = trial
|
|
||||||
/// .suggest_int_step("n_estimators", 100, 500, 50)
|
|
||||||
/// .unwrap();
|
|
||||||
/// assert_eq!(n, n2);
|
|
||||||
/// ```
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
|
||||||
pub fn suggest_int_step(
|
|
||||||
&mut self,
|
|
||||||
name: impl Into<String>,
|
|
||||||
low: i64,
|
|
||||||
high: i64,
|
|
||||||
step: i64,
|
|
||||||
) -> Result<i64> {
|
|
||||||
if step <= 0 {
|
|
||||||
return Err(Error::InvalidStep);
|
|
||||||
}
|
|
||||||
|
|
||||||
if low > high {
|
|
||||||
return Err(Error::InvalidBounds {
|
|
||||||
low: low as f64,
|
|
||||||
high: high as f64,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = name.into();
|
|
||||||
let distribution = IntDistribution {
|
|
||||||
low,
|
|
||||||
high,
|
|
||||||
log_scale: false,
|
|
||||||
step: Some(step),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if parameter already exists
|
|
||||||
if let Some(existing_dist) = self.distributions.get(&name) {
|
|
||||||
// Verify the distribution matches
|
|
||||||
if let Distribution::Int(existing) = existing_dist
|
|
||||||
&& existing.low == low
|
|
||||||
&& existing.high == high
|
|
||||||
&& !existing.log_scale
|
|
||||||
&& existing.step == Some(step)
|
|
||||||
{
|
|
||||||
// Same distribution, return cached value
|
|
||||||
if let Some(ParamValue::Int(value)) = self.params.get(&name) {
|
|
||||||
return Ok(*value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Distribution exists but doesn't match
|
|
||||||
return Err(Error::ParameterConflict {
|
|
||||||
name,
|
|
||||||
reason: "parameter was previously sampled with different bounds or type"
|
|
||||||
.to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sample using the sampler (sampler handles step-grid)
|
|
||||||
let dist = Distribution::Int(distribution);
|
|
||||||
let ParamValue::Int(value) = self.sample_value(&dist) else {
|
|
||||||
return Err(Error::Internal("Int distribution should return Int value"));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store distribution and value
|
|
||||||
self.distributions.insert(name.clone(), dist);
|
|
||||||
self.params.insert(name, ParamValue::Int(value));
|
|
||||||
|
|
||||||
Ok(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Suggests a categorical parameter from the given choices.
|
|
||||||
///
|
|
||||||
/// The value is selected uniformly at random from the provided choices.
|
|
||||||
/// Internally, the index of the selected choice is stored.
|
|
||||||
///
|
|
||||||
/// If the parameter has already been sampled with the same number of choices,
|
|
||||||
/// the cached value (same index) is returned. If the parameter was sampled with
|
|
||||||
/// a different number of choices, a `ParameterConflict` error is returned.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `name` - The name of the parameter.
|
|
||||||
/// * `choices` - A slice of choices to select from.
|
|
||||||
///
|
|
||||||
/// # Type Parameters
|
|
||||||
///
|
|
||||||
/// * `T` - The type of the choices. Only requires `Clone`.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns `EmptyChoices` if `choices` is empty.
|
|
||||||
/// Returns `ParameterConflict` if the parameter was previously sampled with a different number of choices.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use optimizer::Trial;
|
|
||||||
///
|
|
||||||
/// let mut trial = Trial::new(0);
|
|
||||||
/// let optimizer = trial
|
|
||||||
/// .suggest_categorical("optimizer", &["sgd", "adam", "rmsprop"])
|
|
||||||
/// .unwrap();
|
|
||||||
/// assert!(["sgd", "adam", "rmsprop"].contains(&optimizer));
|
|
||||||
///
|
|
||||||
/// // Calling again with same choices returns cached value
|
|
||||||
/// let optimizer2 = trial
|
|
||||||
/// .suggest_categorical("optimizer", &["sgd", "adam", "rmsprop"])
|
|
||||||
/// .unwrap();
|
|
||||||
/// assert_eq!(optimizer, optimizer2);
|
|
||||||
/// ```
|
|
||||||
pub fn suggest_categorical<T: Clone>(
|
|
||||||
&mut self,
|
|
||||||
name: impl Into<String>,
|
|
||||||
choices: &[T],
|
|
||||||
) -> Result<T> {
|
|
||||||
if choices.is_empty() {
|
|
||||||
return Err(Error::EmptyChoices);
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = name.into();
|
|
||||||
let n_choices = choices.len();
|
|
||||||
let distribution = CategoricalDistribution { n_choices };
|
|
||||||
|
|
||||||
// Check if parameter already exists
|
|
||||||
if let Some(existing_dist) = self.distributions.get(&name) {
|
|
||||||
// Verify the distribution matches
|
|
||||||
if let Distribution::Categorical(existing) = existing_dist
|
|
||||||
&& existing.n_choices == n_choices
|
|
||||||
{
|
|
||||||
// Same distribution, return cached value
|
|
||||||
if let Some(ParamValue::Categorical(index)) = self.params.get(&name) {
|
|
||||||
return Ok(choices[*index].clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Distribution exists but doesn't match
|
|
||||||
return Err(Error::ParameterConflict {
|
|
||||||
name,
|
|
||||||
reason: "parameter was previously sampled with different number of choices or type"
|
|
||||||
.to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sample using the sampler
|
|
||||||
let dist = Distribution::Categorical(distribution);
|
|
||||||
let ParamValue::Categorical(index) = self.sample_value(&dist) else {
|
|
||||||
return Err(Error::Internal(
|
|
||||||
"Categorical distribution should return Categorical value",
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store distribution and value (store the index)
|
|
||||||
self.distributions.insert(name.clone(), dist);
|
|
||||||
self.params.insert(name, ParamValue::Categorical(index));
|
|
||||||
|
|
||||||
Ok(choices[index].clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Suggests a boolean parameter.
|
|
||||||
///
|
|
||||||
/// The value is selected uniformly at random from `{false, true}`.
|
|
||||||
/// This is equivalent to calling `suggest_categorical(name, &[false, true])`.
|
|
||||||
///
|
|
||||||
/// If the parameter has already been sampled, the cached value is returned.
|
|
||||||
/// If the parameter was sampled with a different type, a `ParameterConflict` error is returned.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `name` - The name of the parameter.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns `ParameterConflict` if the parameter was previously sampled with a different type.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use optimizer::Trial;
|
|
||||||
///
|
|
||||||
/// let mut trial = Trial::new(0);
|
|
||||||
/// let use_dropout = trial.suggest_bool("use_dropout").unwrap();
|
|
||||||
/// assert!(use_dropout == true || use_dropout == false);
|
|
||||||
///
|
|
||||||
/// // Calling again returns cached value
|
|
||||||
/// let use_dropout2 = trial.suggest_bool("use_dropout").unwrap();
|
|
||||||
/// assert_eq!(use_dropout, use_dropout2);
|
|
||||||
/// ```
|
|
||||||
pub fn suggest_bool(&mut self, name: impl Into<String>) -> Result<bool> {
|
|
||||||
self.suggest_categorical(name, &[false, true])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Suggests a parameter value from a range.
|
|
||||||
///
|
|
||||||
/// This method accepts both [`Range`] (`..`) and [`RangeInclusive`] (`..=`)
|
|
||||||
/// for both `f64` and `i64` types, allowing natural Rust range syntax.
|
|
||||||
///
|
|
||||||
/// For integer ranges, note that `Range` (`..`) is end-exclusive while
|
|
||||||
/// `RangeInclusive` (`..=`) is end-inclusive, matching Rust's semantics.
|
|
||||||
///
|
|
||||||
/// If the parameter has already been sampled with the same bounds, the cached value is returned.
|
|
||||||
/// If the parameter was sampled with different bounds, a `ParameterConflict` error is returned.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `name` - The name of the parameter.
|
|
||||||
/// * `range` - The range to sample from.
|
|
||||||
///
|
|
||||||
/// # Type Parameters
|
|
||||||
///
|
|
||||||
/// * `R` - A range type implementing [`SuggestableRange`].
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// Returns `InvalidBounds` if the range is invalid (e.g., low > high or empty integer range).
|
|
||||||
/// Returns `ParameterConflict` if the parameter was previously sampled with different bounds.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use optimizer::Trial;
|
|
||||||
///
|
|
||||||
/// let mut trial = Trial::new(0);
|
|
||||||
///
|
|
||||||
/// // Float ranges
|
|
||||||
/// let x = trial.suggest_range("x", 0.0..1.0).unwrap();
|
|
||||||
/// assert!(x >= 0.0 && x <= 1.0);
|
|
||||||
///
|
|
||||||
/// let y = trial.suggest_range("y", 0.0..=1.0).unwrap();
|
|
||||||
/// assert!(y >= 0.0 && y <= 1.0);
|
|
||||||
///
|
|
||||||
/// // Integer ranges
|
|
||||||
/// let n = trial.suggest_range("n", 1_i64..10).unwrap(); // 1 to 9 inclusive
|
|
||||||
/// assert!(n >= 1 && n <= 9);
|
|
||||||
///
|
|
||||||
/// let m = trial.suggest_range("m", 1_i64..=10).unwrap(); // 1 to 10 inclusive
|
|
||||||
/// assert!(m >= 1 && m <= 10);
|
|
||||||
///
|
|
||||||
/// // Calling again with same range returns cached value
|
|
||||||
/// let x2 = trial.suggest_range("x", 0.0..1.0).unwrap();
|
|
||||||
/// assert_eq!(x, x2);
|
|
||||||
/// ```
|
|
||||||
pub fn suggest_range<R: SuggestableRange>(
|
|
||||||
&mut self,
|
|
||||||
name: impl Into<String>,
|
|
||||||
range: R,
|
|
||||||
) -> Result<R::Output> {
|
|
||||||
range.suggest(self, name.into())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-23
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#![cfg(feature = "async")]
|
#![cfg(feature = "async")]
|
||||||
|
|
||||||
|
use optimizer::parameter::{FloatParam, Parameter};
|
||||||
use optimizer::sampler::random::RandomSampler;
|
use optimizer::sampler::random::RandomSampler;
|
||||||
use optimizer::sampler::tpe::TpeSampler;
|
use optimizer::sampler::tpe::TpeSampler;
|
||||||
use optimizer::{Direction, Error, Study};
|
use optimizer::{Direction, Error, Study};
|
||||||
@@ -13,10 +14,15 @@ async fn test_optimize_async_basic() {
|
|||||||
let sampler = RandomSampler::with_seed(42);
|
let sampler = RandomSampler::with_seed(42);
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x_param = FloatParam::new(-10.0, 10.0);
|
||||||
|
|
||||||
study
|
study
|
||||||
.optimize_async(10, |mut trial| async move {
|
.optimize_async(10, move |mut trial| {
|
||||||
let x = trial.suggest_float("x", -10.0, 10.0)?;
|
let x_param = x_param.clone();
|
||||||
Ok::<_, Error>((trial, x * x))
|
async move {
|
||||||
|
let x = x_param.suggest(&mut trial)?;
|
||||||
|
Ok::<_, Error>((trial, x * x))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("async optimization should succeed");
|
.expect("async optimization should succeed");
|
||||||
@@ -27,7 +33,7 @@ async fn test_optimize_async_basic() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_optimize_async_with_sampler() {
|
async fn test_optimize_async_with_tpe() {
|
||||||
let sampler = TpeSampler::builder()
|
let sampler = TpeSampler::builder()
|
||||||
.seed(42)
|
.seed(42)
|
||||||
.n_startup_trials(5)
|
.n_startup_trials(5)
|
||||||
@@ -36,10 +42,15 @@ async fn test_optimize_async_with_sampler() {
|
|||||||
|
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x_param = FloatParam::new(-5.0, 5.0);
|
||||||
|
|
||||||
study
|
study
|
||||||
.optimize_async_with_sampler(15, |mut trial| async move {
|
.optimize_async(15, move |mut trial| {
|
||||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
let x_param = x_param.clone();
|
||||||
Ok::<_, Error>((trial, x * x))
|
async move {
|
||||||
|
let x = x_param.suggest(&mut trial)?;
|
||||||
|
Ok::<_, Error>((trial, x * x))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("async optimization with sampler should succeed");
|
.expect("async optimization with sampler should succeed");
|
||||||
@@ -54,10 +65,15 @@ async fn test_optimize_parallel() {
|
|||||||
let sampler = RandomSampler::with_seed(42);
|
let sampler = RandomSampler::with_seed(42);
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x_param = FloatParam::new(-10.0, 10.0);
|
||||||
|
|
||||||
study
|
study
|
||||||
.optimize_parallel(20, 4, |mut trial| async move {
|
.optimize_parallel(20, 4, move |mut trial| {
|
||||||
let x = trial.suggest_float("x", -10.0, 10.0)?;
|
let x_param = x_param.clone();
|
||||||
Ok::<_, Error>((trial, x * x))
|
async move {
|
||||||
|
let x = x_param.suggest(&mut trial)?;
|
||||||
|
Ok::<_, Error>((trial, x * x))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("parallel optimization should succeed");
|
.expect("parallel optimization should succeed");
|
||||||
@@ -66,7 +82,7 @@ async fn test_optimize_parallel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_optimize_parallel_with_sampler() {
|
async fn test_optimize_parallel_with_tpe() {
|
||||||
let sampler = TpeSampler::builder()
|
let sampler = TpeSampler::builder()
|
||||||
.seed(42)
|
.seed(42)
|
||||||
.n_startup_trials(5)
|
.n_startup_trials(5)
|
||||||
@@ -75,11 +91,18 @@ async fn test_optimize_parallel_with_sampler() {
|
|||||||
|
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x_param = FloatParam::new(-5.0, 5.0);
|
||||||
|
let y_param = FloatParam::new(-5.0, 5.0);
|
||||||
|
|
||||||
study
|
study
|
||||||
.optimize_parallel_with_sampler(15, 3, |mut trial| async move {
|
.optimize_parallel(15, 3, move |mut trial| {
|
||||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
let x_param = x_param.clone();
|
||||||
let y = trial.suggest_float("y", -5.0, 5.0)?;
|
let y_param = y_param.clone();
|
||||||
Ok::<_, Error>((trial, x * x + y * y))
|
async move {
|
||||||
|
let x = x_param.suggest(&mut trial)?;
|
||||||
|
let y = y_param.suggest(&mut trial)?;
|
||||||
|
Ok::<_, Error>((trial, x * x + y * y))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("parallel optimization with sampler should succeed");
|
.expect("parallel optimization with sampler should succeed");
|
||||||
@@ -105,6 +128,7 @@ async fn test_optimize_async_all_failures() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[allow(deprecated)]
|
||||||
async fn test_optimize_async_with_sampler_all_failures() {
|
async fn test_optimize_async_with_sampler_all_failures() {
|
||||||
let study: Study<f64> = Study::new(Direction::Minimize);
|
let study: Study<f64> = Study::new(Direction::Minimize);
|
||||||
|
|
||||||
@@ -139,6 +163,7 @@ async fn test_optimize_parallel_all_failures() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
#[allow(deprecated)]
|
||||||
async fn test_optimize_parallel_with_sampler_all_failures() {
|
async fn test_optimize_parallel_with_sampler_all_failures() {
|
||||||
let study: Study<f64> = Study::new(Direction::Minimize);
|
let study: Study<f64> = Study::new(Direction::Minimize);
|
||||||
|
|
||||||
@@ -162,12 +187,15 @@ async fn test_optimize_async_partial_failures() {
|
|||||||
|
|
||||||
let counter = std::sync::atomic::AtomicUsize::new(0);
|
let counter = std::sync::atomic::AtomicUsize::new(0);
|
||||||
|
|
||||||
|
let x_param = FloatParam::new(0.0, 10.0);
|
||||||
|
|
||||||
study
|
study
|
||||||
.optimize_async(10, |mut trial| {
|
.optimize_async(10, move |mut trial| {
|
||||||
let count = counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
let count = counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
let x_param = x_param.clone();
|
||||||
async move {
|
async move {
|
||||||
if count.is_multiple_of(2) {
|
if count.is_multiple_of(2) {
|
||||||
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
let x = x_param.suggest(&mut trial)?;
|
||||||
Ok::<_, Error>((trial, x))
|
Ok::<_, Error>((trial, x))
|
||||||
} else {
|
} else {
|
||||||
Err(Error::NoCompletedTrials) // Use as error type
|
Err(Error::NoCompletedTrials) // Use as error type
|
||||||
@@ -186,11 +214,16 @@ async fn test_optimize_parallel_high_concurrency() {
|
|||||||
let sampler = RandomSampler::with_seed(42);
|
let sampler = RandomSampler::with_seed(42);
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x_param = FloatParam::new(0.0, 10.0);
|
||||||
|
|
||||||
// Run with concurrency higher than n_trials
|
// Run with concurrency higher than n_trials
|
||||||
study
|
study
|
||||||
.optimize_parallel(5, 10, |mut trial| async move {
|
.optimize_parallel(5, 10, move |mut trial| {
|
||||||
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
let x_param = x_param.clone();
|
||||||
Ok::<_, Error>((trial, x))
|
async move {
|
||||||
|
let x = x_param.suggest(&mut trial)?;
|
||||||
|
Ok::<_, Error>((trial, x))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("should handle high concurrency");
|
.expect("should handle high concurrency");
|
||||||
@@ -203,11 +236,16 @@ async fn test_optimize_parallel_single_concurrency() {
|
|||||||
let sampler = RandomSampler::with_seed(42);
|
let sampler = RandomSampler::with_seed(42);
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x_param = FloatParam::new(0.0, 10.0);
|
||||||
|
|
||||||
// Run with concurrency of 1 (sequential)
|
// Run with concurrency of 1 (sequential)
|
||||||
study
|
study
|
||||||
.optimize_parallel(10, 1, |mut trial| async move {
|
.optimize_parallel(10, 1, move |mut trial| {
|
||||||
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
let x_param = x_param.clone();
|
||||||
Ok::<_, Error>((trial, x))
|
async move {
|
||||||
|
let x = x_param.suggest(&mut trial)?;
|
||||||
|
Ok::<_, Error>((trial, x))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("should work with single concurrency");
|
.expect("should work with single concurrency");
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
use optimizer::Trial;
|
||||||
|
use optimizer::parameter::{EnumParam, Parameter};
|
||||||
|
use optimizer_derive::Categorical;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Categorical)]
|
||||||
|
enum Color {
|
||||||
|
Red,
|
||||||
|
Green,
|
||||||
|
Blue,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Categorical)]
|
||||||
|
enum SingleVariant {
|
||||||
|
Only,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derive_categorical_n_choices() {
|
||||||
|
use optimizer::parameter::Categorical;
|
||||||
|
assert_eq!(Color::N_CHOICES, 3);
|
||||||
|
assert_eq!(SingleVariant::N_CHOICES, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derive_categorical_roundtrip() {
|
||||||
|
use optimizer::parameter::Categorical;
|
||||||
|
for i in 0..Color::N_CHOICES {
|
||||||
|
let val = Color::from_index(i);
|
||||||
|
assert_eq!(val.to_index(), i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derive_categorical_values() {
|
||||||
|
use optimizer::parameter::Categorical;
|
||||||
|
assert_eq!(Color::from_index(0), Color::Red);
|
||||||
|
assert_eq!(Color::from_index(1), Color::Green);
|
||||||
|
assert_eq!(Color::from_index(2), Color::Blue);
|
||||||
|
assert_eq!(Color::Red.to_index(), 0);
|
||||||
|
assert_eq!(Color::Green.to_index(), 1);
|
||||||
|
assert_eq!(Color::Blue.to_index(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derive_categorical_with_enum_param() {
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
let param = EnumParam::<Color>::new();
|
||||||
|
let color = param.suggest(&mut trial).unwrap();
|
||||||
|
assert!([Color::Red, Color::Green, Color::Blue].contains(&color));
|
||||||
|
|
||||||
|
// Cached (same param id)
|
||||||
|
let color2 = param.suggest(&mut trial).unwrap();
|
||||||
|
assert_eq!(color, color2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derive_categorical_suggest_via_trial() {
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
let color = trial.suggest_param(&EnumParam::<Color>::new()).unwrap();
|
||||||
|
assert!([Color::Red, Color::Green, Color::Blue].contains(&color));
|
||||||
|
}
|
||||||
+300
-343
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,8 @@
|
|||||||
clippy::cast_possible_truncation
|
clippy::cast_possible_truncation
|
||||||
)]
|
)]
|
||||||
|
|
||||||
use optimizer::sampler::MultivariateTpeSampler;
|
use optimizer::parameter::{CategoricalParam, FloatParam, IntParam, Parameter};
|
||||||
use optimizer::sampler::tpe::TpeSampler;
|
use optimizer::sampler::tpe::{MultivariateTpeSampler, TpeSampler};
|
||||||
use optimizer::{Direction, Error, Study};
|
use optimizer::{Direction, Error, Study};
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -51,10 +51,13 @@ fn test_multivariate_tpe_rosenbrock_finds_good_solution() {
|
|||||||
|
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x_param = FloatParam::new(-2.0, 2.0);
|
||||||
|
let y_param = FloatParam::new(-2.0, 4.0);
|
||||||
|
|
||||||
study
|
study
|
||||||
.optimize_with_sampler(100, |trial| {
|
.optimize(100, |trial| {
|
||||||
let x = trial.suggest_float("x", -2.0, 2.0)?;
|
let x = x_param.suggest(trial)?;
|
||||||
let y = trial.suggest_float("y", -2.0, 4.0)?;
|
let y = y_param.suggest(trial)?;
|
||||||
Ok::<_, Error>(rosenbrock(x, y))
|
Ok::<_, Error>(rosenbrock(x, y))
|
||||||
})
|
})
|
||||||
.expect("optimization should succeed");
|
.expect("optimization should succeed");
|
||||||
@@ -83,10 +86,13 @@ fn test_independent_tpe_rosenbrock() {
|
|||||||
|
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x_param = FloatParam::new(-2.0, 2.0);
|
||||||
|
let y_param = FloatParam::new(-2.0, 4.0);
|
||||||
|
|
||||||
study
|
study
|
||||||
.optimize_with_sampler(100, |trial| {
|
.optimize(100, |trial| {
|
||||||
let x = trial.suggest_float("x", -2.0, 2.0)?;
|
let x = x_param.suggest(trial)?;
|
||||||
let y = trial.suggest_float("y", -2.0, 4.0)?;
|
let y = y_param.suggest(trial)?;
|
||||||
Ok::<_, Error>(rosenbrock(x, y))
|
Ok::<_, Error>(rosenbrock(x, y))
|
||||||
})
|
})
|
||||||
.expect("optimization should succeed");
|
.expect("optimization should succeed");
|
||||||
@@ -123,10 +129,13 @@ fn test_multivariate_tpe_outperforms_on_correlated_problem() {
|
|||||||
|
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, multivariate_sampler);
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, multivariate_sampler);
|
||||||
|
|
||||||
|
let x_param = FloatParam::new(-2.0, 2.0);
|
||||||
|
let y_param = FloatParam::new(-2.0, 4.0);
|
||||||
|
|
||||||
study
|
study
|
||||||
.optimize_with_sampler(n_trials, |trial| {
|
.optimize(n_trials, |trial| {
|
||||||
let x = trial.suggest_float("x", -2.0, 2.0)?;
|
let x = x_param.suggest(trial)?;
|
||||||
let y = trial.suggest_float("y", -2.0, 4.0)?;
|
let y = y_param.suggest(trial)?;
|
||||||
Ok::<_, Error>(rosenbrock(x, y))
|
Ok::<_, Error>(rosenbrock(x, y))
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -143,10 +152,13 @@ fn test_multivariate_tpe_outperforms_on_correlated_problem() {
|
|||||||
|
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, independent_sampler);
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, independent_sampler);
|
||||||
|
|
||||||
|
let x_param = FloatParam::new(-2.0, 2.0);
|
||||||
|
let y_param = FloatParam::new(-2.0, 4.0);
|
||||||
|
|
||||||
study
|
study
|
||||||
.optimize_with_sampler(n_trials, |trial| {
|
.optimize(n_trials, |trial| {
|
||||||
let x = trial.suggest_float("x", -2.0, 2.0)?;
|
let x = x_param.suggest(trial)?;
|
||||||
let y = trial.suggest_float("y", -2.0, 4.0)?;
|
let y = y_param.suggest(trial)?;
|
||||||
Ok::<_, Error>(rosenbrock(x, y))
|
Ok::<_, Error>(rosenbrock(x, y))
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -202,10 +214,13 @@ fn test_multivariate_tpe_independent_problem() {
|
|||||||
|
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x_param = FloatParam::new(-5.0, 5.0);
|
||||||
|
let y_param = FloatParam::new(-5.0, 5.0);
|
||||||
|
|
||||||
study
|
study
|
||||||
.optimize_with_sampler(50, |trial| {
|
.optimize(50, |trial| {
|
||||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
let x = x_param.suggest(trial)?;
|
||||||
let y = trial.suggest_float("y", -5.0, 5.0)?;
|
let y = y_param.suggest(trial)?;
|
||||||
Ok::<_, Error>(sphere(x, y))
|
Ok::<_, Error>(sphere(x, y))
|
||||||
})
|
})
|
||||||
.expect("optimization should succeed");
|
.expect("optimization should succeed");
|
||||||
@@ -231,10 +246,13 @@ fn test_independent_tpe_independent_problem() {
|
|||||||
|
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x_param = FloatParam::new(-5.0, 5.0);
|
||||||
|
let y_param = FloatParam::new(-5.0, 5.0);
|
||||||
|
|
||||||
study
|
study
|
||||||
.optimize_with_sampler(50, |trial| {
|
.optimize(50, |trial| {
|
||||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
let x = x_param.suggest(trial)?;
|
||||||
let y = trial.suggest_float("y", -5.0, 5.0)?;
|
let y = y_param.suggest(trial)?;
|
||||||
Ok::<_, Error>(sphere(x, y))
|
Ok::<_, Error>(sphere(x, y))
|
||||||
})
|
})
|
||||||
.expect("optimization should succeed");
|
.expect("optimization should succeed");
|
||||||
@@ -268,10 +286,13 @@ fn test_both_samplers_work_on_independent_problem() {
|
|||||||
|
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x_param = FloatParam::new(-5.0, 5.0);
|
||||||
|
let y_param = FloatParam::new(-5.0, 5.0);
|
||||||
|
|
||||||
study
|
study
|
||||||
.optimize_with_sampler(n_trials, |trial| {
|
.optimize(n_trials, |trial| {
|
||||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
let x = x_param.suggest(trial)?;
|
||||||
let y = trial.suggest_float("y", -5.0, 5.0)?;
|
let y = y_param.suggest(trial)?;
|
||||||
Ok::<_, Error>(sphere(x, y))
|
Ok::<_, Error>(sphere(x, y))
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -287,10 +308,13 @@ fn test_both_samplers_work_on_independent_problem() {
|
|||||||
|
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x_param = FloatParam::new(-5.0, 5.0);
|
||||||
|
let y_param = FloatParam::new(-5.0, 5.0);
|
||||||
|
|
||||||
study
|
study
|
||||||
.optimize_with_sampler(n_trials, |trial| {
|
.optimize(n_trials, |trial| {
|
||||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
let x = x_param.suggest(trial)?;
|
||||||
let y = trial.suggest_float("y", -5.0, 5.0)?;
|
let y = y_param.suggest(trial)?;
|
||||||
Ok::<_, Error>(sphere(x, y))
|
Ok::<_, Error>(sphere(x, y))
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -332,10 +356,13 @@ fn test_multivariate_tpe_with_group_decomposition() {
|
|||||||
|
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x_param = FloatParam::new(-5.0, 5.0);
|
||||||
|
let y_param = FloatParam::new(-5.0, 5.0);
|
||||||
|
|
||||||
study
|
study
|
||||||
.optimize_with_sampler(50, |trial| {
|
.optimize(50, |trial| {
|
||||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
let x = x_param.suggest(trial)?;
|
||||||
let y = trial.suggest_float("y", -5.0, 5.0)?;
|
let y = y_param.suggest(trial)?;
|
||||||
Ok::<_, Error>(sphere(x, y))
|
Ok::<_, Error>(sphere(x, y))
|
||||||
})
|
})
|
||||||
.expect("optimization should succeed");
|
.expect("optimization should succeed");
|
||||||
@@ -364,11 +391,15 @@ fn test_multivariate_tpe_mixed_parameter_types() {
|
|||||||
|
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
let x_param = FloatParam::new(-5.0, 5.0);
|
||||||
|
let n_param = IntParam::new(1, 10);
|
||||||
|
let mode_param = CategoricalParam::new(vec!["a", "b", "c"]);
|
||||||
|
|
||||||
study
|
study
|
||||||
.optimize_with_sampler(50, |trial| {
|
.optimize(50, |trial| {
|
||||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
let x = x_param.suggest(trial)?;
|
||||||
let n = trial.suggest_int("n", 1, 10)?;
|
let n = n_param.suggest(trial)?;
|
||||||
let mode = trial.suggest_categorical("mode", &["a", "b", "c"])?;
|
let mode = mode_param.suggest(trial)?;
|
||||||
|
|
||||||
// Objective depends on all parameters
|
// Objective depends on all parameters
|
||||||
let mode_factor = match mode {
|
let mode_factor = match mode {
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
use optimizer::parameter::{
|
||||||
|
BoolParam, Categorical, CategoricalParam, EnumParam, FloatParam, IntParam, Parameter,
|
||||||
|
};
|
||||||
|
use optimizer::{Direction, Study, Trial};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn suggest_float_param_via_trial() {
|
||||||
|
let param = FloatParam::new(0.0, 1.0);
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
let x = trial.suggest_param(¶m).unwrap();
|
||||||
|
assert!((0.0..=1.0).contains(&x));
|
||||||
|
|
||||||
|
// Cached
|
||||||
|
let x2 = trial.suggest_param(¶m).unwrap();
|
||||||
|
assert_eq!(x, x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn suggest_float_log_param_via_trial() {
|
||||||
|
let param = FloatParam::new(1e-5, 1e-1).log_scale();
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
let lr = trial.suggest_param(¶m).unwrap();
|
||||||
|
assert!((1e-5..=1e-1).contains(&lr));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn suggest_float_step_param_via_trial() {
|
||||||
|
let param = FloatParam::new(0.0, 1.0).step(0.25);
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
let x = trial.suggest_param(¶m).unwrap();
|
||||||
|
assert!((0.0..=1.0).contains(&x));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn suggest_int_param_via_trial() {
|
||||||
|
let param = IntParam::new(1, 10);
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
let n = trial.suggest_param(¶m).unwrap();
|
||||||
|
assert!((1..=10).contains(&n));
|
||||||
|
|
||||||
|
// Cached
|
||||||
|
let n2 = trial.suggest_param(¶m).unwrap();
|
||||||
|
assert_eq!(n, n2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn suggest_int_log_param_via_trial() {
|
||||||
|
let param = IntParam::new(1, 1024).log_scale();
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
let batch = trial.suggest_param(¶m).unwrap();
|
||||||
|
assert!((1..=1024).contains(&batch));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn suggest_int_step_param_via_trial() {
|
||||||
|
let param = IntParam::new(32, 512).step(32);
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
let units = trial.suggest_param(¶m).unwrap();
|
||||||
|
assert!((32..=512).contains(&units));
|
||||||
|
assert_eq!((units - 32) % 32, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn suggest_categorical_param_via_trial() {
|
||||||
|
let choices = vec!["sgd", "adam", "rmsprop"];
|
||||||
|
let param = CategoricalParam::new(choices.clone());
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
let opt = trial.suggest_param(¶m).unwrap();
|
||||||
|
assert!(choices.contains(&opt));
|
||||||
|
|
||||||
|
// Cached
|
||||||
|
let opt2 = trial.suggest_param(¶m).unwrap();
|
||||||
|
assert_eq!(opt, opt2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn suggest_bool_param_via_trial() {
|
||||||
|
let param = BoolParam::new();
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
let val = trial.suggest_param(¶m).unwrap();
|
||||||
|
let _ = val;
|
||||||
|
|
||||||
|
// Cached
|
||||||
|
let val2 = trial.suggest_param(¶m).unwrap();
|
||||||
|
assert_eq!(val, val2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
enum Activation {
|
||||||
|
Relu,
|
||||||
|
Sigmoid,
|
||||||
|
Tanh,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Categorical for Activation {
|
||||||
|
const N_CHOICES: usize = 3;
|
||||||
|
|
||||||
|
fn from_index(index: usize) -> Self {
|
||||||
|
match index {
|
||||||
|
0 => Activation::Relu,
|
||||||
|
1 => Activation::Sigmoid,
|
||||||
|
2 => Activation::Tanh,
|
||||||
|
_ => panic!("invalid index"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_index(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Activation::Relu => 0,
|
||||||
|
Activation::Sigmoid => 1,
|
||||||
|
Activation::Tanh => 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn suggest_enum_param_via_trial() {
|
||||||
|
let param = EnumParam::<Activation>::new();
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
let act = trial.suggest_param(¶m).unwrap();
|
||||||
|
assert!([Activation::Relu, Activation::Sigmoid, Activation::Tanh].contains(&act));
|
||||||
|
|
||||||
|
// Cached
|
||||||
|
let act2 = trial.suggest_param(¶m).unwrap();
|
||||||
|
assert_eq!(act, act2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parameter_conflict_detection() {
|
||||||
|
let float_param = FloatParam::new(0.0, 1.0);
|
||||||
|
let int_param = IntParam::new(0, 10);
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
let _ = trial.suggest_param(&float_param).unwrap();
|
||||||
|
|
||||||
|
// Different param type with different id - no conflict
|
||||||
|
let result = trial.suggest_param(&int_param);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
// Different bounds for same param type but different id - no conflict
|
||||||
|
let float_param2 = FloatParam::new(0.0, 2.0);
|
||||||
|
let result = trial.suggest_param(&float_param2);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validation_prevents_suggest() {
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
|
||||||
|
assert!(trial.suggest_param(&FloatParam::new(1.0, 0.0)).is_err());
|
||||||
|
assert!(
|
||||||
|
trial
|
||||||
|
.suggest_param(&FloatParam::new(-1.0, 1.0).log_scale())
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
trial
|
||||||
|
.suggest_param(&FloatParam::new(0.0, 1.0).step(-0.1))
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(trial.suggest_param(&IntParam::new(10, 1)).is_err());
|
||||||
|
assert!(
|
||||||
|
trial
|
||||||
|
.suggest_param(&IntParam::new(0, 10).log_scale())
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(trial.suggest_param(&IntParam::new(0, 10).step(-1)).is_err());
|
||||||
|
assert!(
|
||||||
|
trial
|
||||||
|
.suggest_param(&CategoricalParam::<&str>::new(vec![]))
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parameter_api_with_study() {
|
||||||
|
let x_param = FloatParam::new(-5.0, 5.0);
|
||||||
|
let n_param = IntParam::new(1, 10);
|
||||||
|
let dropout_param = BoolParam::new();
|
||||||
|
let opt_param = CategoricalParam::new(vec!["sgd", "adam"]);
|
||||||
|
|
||||||
|
let study: Study<f64> = Study::new(Direction::Minimize);
|
||||||
|
study
|
||||||
|
.optimize(5, |trial| {
|
||||||
|
let x = x_param.suggest(trial)?;
|
||||||
|
let n = n_param.suggest(trial)?;
|
||||||
|
let dropout = dropout_param.suggest(trial)?;
|
||||||
|
let opt = opt_param.suggest(trial)?;
|
||||||
|
let _ = (n, dropout, opt);
|
||||||
|
Ok::<_, optimizer::Error>(x * x)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let best = study.best_trial().unwrap();
|
||||||
|
assert!(best.value >= 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parameter_suggest_method() {
|
||||||
|
let param = FloatParam::new(0.0, 1.0);
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
let x = param.suggest(&mut trial).unwrap();
|
||||||
|
assert!((0.0..=1.0).contains(&x));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn existing_suggest_methods_still_work() {
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
|
||||||
|
let x_param = FloatParam::new(0.0, 1.0);
|
||||||
|
let x = x_param.suggest(&mut trial).unwrap();
|
||||||
|
assert!((0.0..=1.0).contains(&x));
|
||||||
|
|
||||||
|
let lr_param = FloatParam::new(1e-5, 1e-1).log_scale();
|
||||||
|
let lr = lr_param.suggest(&mut trial).unwrap();
|
||||||
|
assert!((1e-5..=1e-1).contains(&lr));
|
||||||
|
|
||||||
|
let step_param = FloatParam::new(0.0, 1.0).step(0.25);
|
||||||
|
let step = step_param.suggest(&mut trial).unwrap();
|
||||||
|
assert!((0.0..=1.0).contains(&step));
|
||||||
|
|
||||||
|
let n_param = IntParam::new(1, 10);
|
||||||
|
let n = n_param.suggest(&mut trial).unwrap();
|
||||||
|
assert!((1..=10).contains(&n));
|
||||||
|
|
||||||
|
let batch_param = IntParam::new(1, 1024).log_scale();
|
||||||
|
let batch = batch_param.suggest(&mut trial).unwrap();
|
||||||
|
assert!((1..=1024).contains(&batch));
|
||||||
|
|
||||||
|
let units_param = IntParam::new(32, 512).step(32);
|
||||||
|
let units = units_param.suggest(&mut trial).unwrap();
|
||||||
|
assert!((32..=512).contains(&units));
|
||||||
|
|
||||||
|
let opt_param = CategoricalParam::new(vec!["sgd", "adam", "rmsprop"]);
|
||||||
|
let opt = opt_param.suggest(&mut trial).unwrap();
|
||||||
|
assert!(["sgd", "adam", "rmsprop"].contains(&opt));
|
||||||
|
|
||||||
|
let flag_param = BoolParam::new();
|
||||||
|
let flag = flag_param.suggest(&mut trial).unwrap();
|
||||||
|
let _ = flag;
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
[default.extend-words]
|
[default.extend-words]
|
||||||
Tpe = "Tpe"
|
Tpe = "Tpe"
|
||||||
|
consts = "consts"
|
||||||
|
|||||||
Reference in New Issue
Block a user