Implement Parameters API
This commit is contained in:
+11
@@ -1,3 +1,6 @@
|
||||
[workspace]
|
||||
members = ["optimizer-derive"]
|
||||
|
||||
[package]
|
||||
name = "optimizer"
|
||||
version = "0.4.0"
|
||||
@@ -17,13 +20,16 @@ rand = "0.9"
|
||||
thiserror = "2"
|
||||
parking_lot = "0.12"
|
||||
tokio = { version = "1", features = ["sync", "rt-multi-thread"], optional = true }
|
||||
optimizer-derive = { version = "0.1.0", path = "optimizer-derive", optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
async = ["dep:tokio"]
|
||||
derive = ["dep:optimizer-derive"]
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
|
||||
optimizer-derive = { version = "0.1.0", path = "optimizer-derive" }
|
||||
|
||||
[[example]]
|
||||
name = "async_api_optimization"
|
||||
@@ -33,3 +39,8 @@ required-features = ["async"]
|
||||
[[example]]
|
||||
name = "ml_hyperparameter_tuning"
|
||||
path = "examples/ml_hyperparameter_tuning.rs"
|
||||
|
||||
[[example]]
|
||||
name = "parameter_api"
|
||||
path = "examples/parameter_api.rs"
|
||||
required-features = ["derive"]
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use optimizer::parameter::{BoolParam, CategoricalParam, IntParam, Parameter};
|
||||
use optimizer::sampler::tpe::TpeSampler;
|
||||
use optimizer::{Direction, ParamValue, Study, Trial};
|
||||
|
||||
@@ -133,32 +134,27 @@ async fn evaluate_service(config: &ServiceConfig) -> f64 {
|
||||
/// 3. Return both the Trial and the result value as a tuple
|
||||
///
|
||||
/// This ownership pattern allows the trial to be used across await points.
|
||||
async fn objective(mut trial: Trial) -> optimizer::Result<(Trial, f64)> {
|
||||
// Sample configuration parameters
|
||||
|
||||
// Stepped integers: only sample multiples of the step value
|
||||
let cache_size_mb = trial.suggest_int_step("cache_size_mb", 64, 1024, 64)?;
|
||||
let connection_pool_size = trial.suggest_int_step("connection_pool_size", 10, 200, 10)?;
|
||||
let request_timeout_ms = trial.suggest_int_step("request_timeout_ms", 1000, 10000, 500)?;
|
||||
|
||||
// Regular integer
|
||||
let retry_count = trial.suggest_int("retry_count", 0, 5)?;
|
||||
|
||||
// Log-scale integer: good for parameters like batch sizes
|
||||
// that might vary from 1 to 256
|
||||
let batch_size = trial.suggest_int_log("batch_size", 1, 256)?;
|
||||
|
||||
// Regular integer for compression level
|
||||
let compression_level = trial.suggest_int("compression_level", 0, 9)?;
|
||||
|
||||
// Boolean: internally uses categorical with [false, true]
|
||||
let use_http2 = trial.suggest_bool("use_http2")?;
|
||||
|
||||
// Categorical: choose from a list of options
|
||||
let load_balancing = trial.suggest_categorical(
|
||||
"load_balancing",
|
||||
&["round_robin", "least_connections", "random", "ip_hash"],
|
||||
)?;
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn objective(
|
||||
mut trial: Trial,
|
||||
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<(Trial, f64)> {
|
||||
// Sample configuration parameters using parameter definitions
|
||||
let cache_size_mb = cache_size_mb_param.suggest(&mut trial)?;
|
||||
let connection_pool_size = connection_pool_size_param.suggest(&mut trial)?;
|
||||
let request_timeout_ms = request_timeout_ms_param.suggest(&mut trial)?;
|
||||
let retry_count = retry_count_param.suggest(&mut trial)?;
|
||||
let batch_size = batch_size_param.suggest(&mut trial)?;
|
||||
let compression_level = compression_level_param.suggest(&mut trial)?;
|
||||
let use_http2 = use_http2_param.suggest(&mut trial)?;
|
||||
let load_balancing = load_balancing_param.suggest(&mut trial)?;
|
||||
|
||||
// Build configuration
|
||||
let config = ServiceConfig {
|
||||
@@ -184,20 +180,11 @@ async fn objective(mut trial: Trial) -> optimizer::Result<(Trial, f64)> {
|
||||
// ============================================================================
|
||||
|
||||
/// 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}"),
|
||||
fn format_param(value: &ParamValue) -> String {
|
||||
match value {
|
||||
ParamValue::Float(v) => format!("{v:.4}"),
|
||||
ParamValue::Int(v) => format!("{v}"),
|
||||
ParamValue::Categorical(idx) => format!("category_{idx}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,23 +213,13 @@ fn print_best_config(study: &Study<f64>) -> optimizer::Result<()> {
|
||||
println!(" Score: {:.6}", best.value);
|
||||
println!("\n Parameters:");
|
||||
|
||||
// Print parameters in a logical order
|
||||
let param_order = [
|
||||
"cache_size_mb",
|
||||
"connection_pool_size",
|
||||
"request_timeout_ms",
|
||||
"retry_count",
|
||||
"batch_size",
|
||||
"compression_level",
|
||||
"use_http2",
|
||||
"load_balancing",
|
||||
];
|
||||
|
||||
for name in param_order {
|
||||
if let Some(value) = best.params.get(name) {
|
||||
let display = format_param(name, value);
|
||||
println!(" {name}: {display}");
|
||||
}
|
||||
for (id, value) in &best.params {
|
||||
let label = best
|
||||
.param_labels
|
||||
.get(id)
|
||||
.map_or_else(|| format!("{id}"), |l| l.clone());
|
||||
let display = format_param(value);
|
||||
println!(" {label}: {display}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -284,7 +261,22 @@ async fn main() -> optimizer::Result<()> {
|
||||
// Step 2: Create a study to minimize the score
|
||||
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).step(64);
|
||||
let connection_pool_size_param = IntParam::new(10, 200).step(10);
|
||||
let request_timeout_ms_param = IntParam::new(1000, 10000).step(500);
|
||||
let retry_count_param = IntParam::new(0, 5);
|
||||
let batch_size_param = IntParam::new(1, 256).log_scale();
|
||||
let compression_level_param = IntParam::new(0, 9);
|
||||
let use_http2_param = BoolParam::new();
|
||||
let load_balancing_param = CategoricalParam::new(vec![
|
||||
"round_robin",
|
||||
"least_connections",
|
||||
"random",
|
||||
"ip_hash",
|
||||
]);
|
||||
|
||||
// Step 4: Configure optimization
|
||||
let n_trials = 40;
|
||||
let concurrency = 4; // Run 4 trials in parallel
|
||||
|
||||
@@ -292,7 +284,7 @@ async fn main() -> optimizer::Result<()> {
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
// Step 4: Run parallel async optimization
|
||||
// Step 5: Run parallel async optimization
|
||||
//
|
||||
// optimize_parallel_with_sampler:
|
||||
// - Runs up to `concurrency` trials simultaneously
|
||||
@@ -303,7 +295,30 @@ async fn main() -> optimizer::Result<()> {
|
||||
// The "_with_sampler" suffix means the TPE sampler gets access to
|
||||
// trial history for informed sampling.
|
||||
study
|
||||
.optimize_parallel_with_sampler(n_trials, concurrency, objective)
|
||||
.optimize_parallel_with_sampler(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?;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
use std::ops::ControlFlow;
|
||||
|
||||
use optimizer::parameter::{FloatParam, IntParam, Parameter};
|
||||
use optimizer::sampler::CompletedTrial;
|
||||
use optimizer::sampler::tpe::TpeSampler;
|
||||
use optimizer::{Direction, ParamValue, Study, Trial};
|
||||
@@ -87,34 +88,32 @@ fn evaluate_model(config: &ModelConfig) -> f64 {
|
||||
/// The objective function that the optimizer calls for each trial.
|
||||
///
|
||||
/// This function:
|
||||
/// 1. Uses `trial.suggest_*()` methods to sample hyperparameter values
|
||||
/// 2. Builds a model configuration from those values
|
||||
/// 1. Uses parameter definitions passed as arguments
|
||||
/// 2. Builds a model configuration from the suggested values
|
||||
/// 3. Evaluates the model and returns the loss
|
||||
///
|
||||
/// The optimizer learns from the results to suggest better parameters
|
||||
/// in future trials.
|
||||
fn objective(trial: &mut Trial) -> optimizer::Result<f64> {
|
||||
// Sample hyperparameters using different strategies:
|
||||
|
||||
// Log-scale: Good for parameters spanning multiple orders of magnitude
|
||||
// The learning rate might be 0.001, 0.01, or 0.1 - log-scale samples evenly across these
|
||||
let learning_rate = trial.suggest_float_log("learning_rate", 0.001, 0.3)?;
|
||||
|
||||
// Regular integer: Uniformly samples from the range [3, 12]
|
||||
let max_depth = trial.suggest_int("max_depth", 3, 12)?;
|
||||
|
||||
// Stepped integer: Only samples multiples of 50 (50, 100, 150, ..., 500)
|
||||
// Useful when you only want to test specific values
|
||||
let n_estimators = trial.suggest_int_step("n_estimators", 50, 500, 50)?;
|
||||
|
||||
// Regular float: Uniformly samples from [0.5, 1.0]
|
||||
let subsample = trial.suggest_float("subsample", 0.5, 1.0)?;
|
||||
let colsample_bytree = trial.suggest_float("colsample_bytree", 0.5, 1.0)?;
|
||||
|
||||
// More parameters
|
||||
let min_child_weight = trial.suggest_int("min_child_weight", 1, 10)?;
|
||||
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)?;
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn objective(
|
||||
trial: &mut Trial,
|
||||
learning_rate_param: &FloatParam,
|
||||
max_depth_param: &IntParam,
|
||||
n_estimators_param: &IntParam,
|
||||
subsample_param: &FloatParam,
|
||||
colsample_bytree_param: &FloatParam,
|
||||
min_child_weight_param: &IntParam,
|
||||
reg_alpha_param: &FloatParam,
|
||||
reg_lambda_param: &FloatParam,
|
||||
) -> optimizer::Result<f64> {
|
||||
let learning_rate = learning_rate_param.suggest(trial)?;
|
||||
let max_depth = max_depth_param.suggest(trial)?;
|
||||
let n_estimators = n_estimators_param.suggest(trial)?;
|
||||
let subsample = subsample_param.suggest(trial)?;
|
||||
let colsample_bytree = colsample_bytree_param.suggest(trial)?;
|
||||
let min_child_weight = min_child_weight_param.suggest(trial)?;
|
||||
let reg_alpha = reg_alpha_param.suggest(trial)?;
|
||||
let reg_lambda = reg_lambda_param.suggest(trial)?;
|
||||
|
||||
// Build configuration and evaluate
|
||||
let config = ModelConfig {
|
||||
@@ -148,35 +147,16 @@ fn objective(trial: &mut Trial) -> optimizer::Result<f64> {
|
||||
/// Return `ControlFlow::Continue(())` to keep optimizing.
|
||||
/// Return `ControlFlow::Break(())` to stop early.
|
||||
fn on_trial_complete(study: &Study<f64>, trial: &CompletedTrial<f64>) -> ControlFlow<()> {
|
||||
// Helper to extract parameter values
|
||||
let get_float = |name: &str| -> f64 {
|
||||
match trial.params.get(name) {
|
||||
Some(ParamValue::Float(v)) => *v,
|
||||
_ => 0.0,
|
||||
// Print trial number and objective value
|
||||
print!("{:>5} ", study.n_trials());
|
||||
for value in trial.params.values() {
|
||||
match value {
|
||||
ParamValue::Float(v) => print!("{v:>12.5} "),
|
||||
ParamValue::Int(v) => print!("{v:>12} "),
|
||||
ParamValue::Categorical(v) => print!("{v:>12} "),
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
println!("{:>12.6}", trial.value);
|
||||
|
||||
// Early stopping: if we find an excellent solution, stop early
|
||||
if trial.value < 0.16 {
|
||||
@@ -218,21 +198,22 @@ fn main() -> optimizer::Result<()> {
|
||||
// Print header
|
||||
println!("Starting hyperparameter optimization...\n");
|
||||
println!(
|
||||
"{:>5} {:>10} {:>10} {:>12} {:>10} {:>12} {:>8} {:>10} {:>10} {:>12}",
|
||||
"Trial",
|
||||
"LR",
|
||||
"MaxDepth",
|
||||
"Estimators",
|
||||
"Subsample",
|
||||
"ColSample",
|
||||
"MinCW",
|
||||
"Alpha",
|
||||
"Lambda",
|
||||
"Loss"
|
||||
"{:>5} {:>12} (parameters...) {:>12}",
|
||||
"Trial", "Params", "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).log_scale();
|
||||
let max_depth_param = IntParam::new(3, 12);
|
||||
let n_estimators_param = IntParam::new(50, 500).step(50);
|
||||
let subsample_param = FloatParam::new(0.5, 1.0);
|
||||
let colsample_bytree_param = FloatParam::new(0.5, 1.0);
|
||||
let min_child_weight_param = IntParam::new(1, 10);
|
||||
let reg_alpha_param = FloatParam::new(1e-3, 10.0).log_scale();
|
||||
let reg_lambda_param = FloatParam::new(1e-3, 10.0).log_scale();
|
||||
|
||||
// Step 4: Run optimization
|
||||
//
|
||||
// optimize_with_callback_sampler runs the objective function for up to
|
||||
// n_trials iterations. After each trial, it calls the callback.
|
||||
@@ -240,7 +221,23 @@ fn main() -> optimizer::Result<()> {
|
||||
// history for informed sampling.
|
||||
let n_trials = 50;
|
||||
|
||||
study.optimize_with_callback_sampler(n_trials, objective, on_trial_complete)?;
|
||||
study.optimize_with_callback_sampler(
|
||||
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
|
||||
println!("\n{}", "=".repeat(110));
|
||||
@@ -252,11 +249,15 @@ fn main() -> optimizer::Result<()> {
|
||||
println!(" Loss: {:.6}", best.value);
|
||||
println!(" Parameters:");
|
||||
|
||||
for (name, value) in &best.params {
|
||||
for (id, value) in &best.params {
|
||||
let label = best
|
||||
.param_labels
|
||||
.get(id)
|
||||
.map_or_else(|| format!("{id}"), |l| l.clone());
|
||||
match value {
|
||||
ParamValue::Float(v) => println!(" {name}: {v:.6}"),
|
||||
ParamValue::Int(v) => println!(" {name}: {v}"),
|
||||
ParamValue::Categorical(v) => println!(" {name}: category {v}"),
|
||||
ParamValue::Float(v) => println!(" {label}: {v:.6}"),
|
||||
ParamValue::Int(v) => println!(" {label}: {v}"),
|
||||
ParamValue::Categorical(v) => println!(" {label}: category {v}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
use optimizer::parameter::{
|
||||
BoolParam, CategoricalParam, EnumParam, FloatParam, IntParam, Parameter,
|
||||
};
|
||||
use optimizer::{Direction, Study};
|
||||
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).log_scale();
|
||||
let n_layers_param = IntParam::new(1, 5);
|
||||
let units_param = IntParam::new(32, 512).step(32);
|
||||
let optimizer_param = CategoricalParam::new(vec!["sgd", "adam", "rmsprop"]);
|
||||
let activation_param = EnumParam::<Activation>::new();
|
||||
let batch_size_param = IntParam::new(16, 256).log_scale();
|
||||
let use_dropout_param = BoolParam::new();
|
||||
|
||||
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::<_, optimizer::Error>(loss)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let best = study.best_trial().unwrap();
|
||||
println!("\nBest trial: value={:.4}", best.value);
|
||||
for (id, label) in &best.param_labels {
|
||||
println!(" {}: {:?}", label, best.params[id]);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
+52
-23
@@ -28,6 +28,7 @@
|
||||
//! # Quick Start
|
||||
//!
|
||||
//! ```
|
||||
//! use optimizer::parameter::{FloatParam, Parameter};
|
||||
//! use optimizer::sampler::tpe::TpeSampler;
|
||||
//! use optimizer::{Direction, Study};
|
||||
//!
|
||||
@@ -35,17 +36,23 @@
|
||||
//! let sampler = TpeSampler::builder().seed(42).build().unwrap();
|
||||
//! 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
|
||||
//! .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)
|
||||
//! })
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! // Get the best result
|
||||
//! 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]);
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! # Creating a Study
|
||||
@@ -69,29 +76,35 @@
|
||||
//!
|
||||
//! # 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};
|
||||
//!
|
||||
//! 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
|
||||
//! .optimize(10, |trial| {
|
||||
//! // Float parameters
|
||||
//! let x = trial.suggest_float("x", 0.0, 1.0)?;
|
||||
//! let lr = trial.suggest_float_log("learning_rate", 1e-5, 1e-1)?;
|
||||
//! let step = trial.suggest_float_step("step", 0.0, 1.0, 0.1)?;
|
||||
//! let x = x_param.suggest(trial)?;
|
||||
//! let lr = lr_param.suggest(trial)?;
|
||||
//! let step = step_param.suggest(trial)?;
|
||||
//! 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)
|
||||
//! })
|
||||
//! .unwrap();
|
||||
@@ -147,35 +160,51 @@
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use optimizer::{Study, Direction};
|
||||
//! use optimizer::parameter::{FloatParam, Parameter};
|
||||
//!
|
||||
//! let x_param = FloatParam::new(0.0, 1.0);
|
||||
//!
|
||||
//! // Sequential async
|
||||
//! study.optimize_async(10, |mut trial| async move {
|
||||
//! let x = trial.suggest_float("x", 0.0, 1.0)?;
|
||||
//! Ok((trial, x * x))
|
||||
//! study.optimize_async(10, |mut trial| {
|
||||
//! let x_param = x_param.clone();
|
||||
//! async move {
|
||||
//! let x = x_param.suggest(&mut trial)?;
|
||||
//! Ok((trial, x * x))
|
||||
//! }
|
||||
//! }).await?;
|
||||
//!
|
||||
//! // Parallel with bounded concurrency
|
||||
//! study.optimize_parallel(10, 4, |mut trial| async move {
|
||||
//! let x = trial.suggest_float("x", 0.0, 1.0)?;
|
||||
//! Ok((trial, x * x))
|
||||
//! study.optimize_parallel(10, 4, |mut trial| {
|
||||
//! let x_param = x_param.clone();
|
||||
//! async move {
|
||||
//! let x = x_param.suggest(&mut trial)?;
|
||||
//! Ok((trial, x * x))
|
||||
//! }
|
||||
//! }).await?;
|
||||
//! ```
|
||||
//!
|
||||
//! # Feature Flags
|
||||
//!
|
||||
//! - `async`: Enable async optimization methods (requires tokio)
|
||||
//! - `derive`: Enable `#[derive(Categorical)]` for enum parameters
|
||||
|
||||
mod distribution;
|
||||
mod error;
|
||||
mod kde;
|
||||
mod param;
|
||||
pub mod parameter;
|
||||
pub mod sampler;
|
||||
mod study;
|
||||
mod trial;
|
||||
mod types;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
#[cfg(feature = "derive")]
|
||||
pub use optimizer_derive::Categorical;
|
||||
pub use param::ParamValue;
|
||||
pub use parameter::{
|
||||
BoolParam, Categorical, CategoricalParam, EnumParam, FloatParam, IntParam, ParamId, Parameter,
|
||||
};
|
||||
pub use study::Study;
|
||||
pub use trial::{SuggestableRange, Trial};
|
||||
pub use trial::Trial;
|
||||
pub use types::{Direction, TrialState};
|
||||
|
||||
@@ -0,0 +1,913 @@
|
||||
//! 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>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl BoolParam {
|
||||
/// Creates a new boolean parameter.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self { id: ParamId::new() }
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
_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(),
|
||||
_marker: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
+21
-54
@@ -8,6 +8,7 @@ use std::collections::HashMap;
|
||||
|
||||
use crate::distribution::Distribution;
|
||||
use crate::param::ParamValue;
|
||||
use crate::parameter::ParamId;
|
||||
|
||||
/// A completed trial with its parameters, distributions, and objective value.
|
||||
///
|
||||
@@ -18,10 +19,12 @@ use crate::param::ParamValue;
|
||||
pub struct CompletedTrial<V = f64> {
|
||||
/// The unique identifier for this trial.
|
||||
pub id: u64,
|
||||
/// The sampled parameter values, keyed by parameter name.
|
||||
pub params: HashMap<String, ParamValue>,
|
||||
/// The parameter distributions used, keyed by parameter name.
|
||||
pub distributions: HashMap<String, Distribution>,
|
||||
/// The sampled parameter values, keyed by parameter id.
|
||||
pub params: HashMap<ParamId, ParamValue>,
|
||||
/// The parameter distributions used, keyed by parameter id.
|
||||
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.
|
||||
pub value: V,
|
||||
}
|
||||
@@ -30,14 +33,16 @@ impl<V> CompletedTrial<V> {
|
||||
/// Creates a new completed trial.
|
||||
pub fn new(
|
||||
id: u64,
|
||||
params: HashMap<String, ParamValue>,
|
||||
distributions: HashMap<String, Distribution>,
|
||||
params: HashMap<ParamId, ParamValue>,
|
||||
distributions: HashMap<ParamId, Distribution>,
|
||||
param_labels: HashMap<ParamId, String>,
|
||||
value: V,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
params,
|
||||
distributions,
|
||||
param_labels,
|
||||
value,
|
||||
}
|
||||
}
|
||||
@@ -48,34 +53,16 @@ impl<V> CompletedTrial<V> {
|
||||
/// 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
|
||||
/// 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)]
|
||||
pub struct PendingTrial {
|
||||
/// The unique identifier for this trial.
|
||||
pub id: u64,
|
||||
/// The sampled parameter values, keyed by parameter name.
|
||||
pub params: HashMap<String, ParamValue>,
|
||||
/// The parameter distributions used, keyed by parameter name.
|
||||
pub distributions: HashMap<String, Distribution>,
|
||||
/// The sampled parameter values, keyed by parameter id.
|
||||
pub params: HashMap<ParamId, ParamValue>,
|
||||
/// The parameter distributions used, keyed by parameter id.
|
||||
pub distributions: HashMap<ParamId, Distribution>,
|
||||
/// Human-readable labels for parameters, keyed by parameter id.
|
||||
pub param_labels: HashMap<ParamId, String>,
|
||||
}
|
||||
|
||||
impl PendingTrial {
|
||||
@@ -83,13 +70,15 @@ impl PendingTrial {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
id: u64,
|
||||
params: HashMap<String, ParamValue>,
|
||||
distributions: HashMap<String, Distribution>,
|
||||
params: HashMap<ParamId, ParamValue>,
|
||||
distributions: HashMap<ParamId, Distribution>,
|
||||
param_labels: HashMap<ParamId, String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
params,
|
||||
distributions,
|
||||
param_labels,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,28 +88,6 @@ impl PendingTrial {
|
||||
/// Samplers are responsible for generating parameter values based on
|
||||
/// the distribution and historical trial data. The trait requires
|
||||
/// `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 {
|
||||
/// Samples a parameter value from the given distribution.
|
||||
///
|
||||
|
||||
+701
-517
File diff suppressed because it is too large
Load Diff
+18
-11
@@ -1024,19 +1024,20 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::distribution::{CategoricalDistribution, FloatDistribution, IntDistribution};
|
||||
use crate::parameter::ParamId;
|
||||
|
||||
fn create_trial(
|
||||
id: u64,
|
||||
value: f64,
|
||||
params: Vec<(&str, ParamValue, Distribution)>,
|
||||
params: Vec<(ParamId, ParamValue, Distribution)>,
|
||||
) -> CompletedTrial {
|
||||
let mut param_map = HashMap::new();
|
||||
let mut dist_map = HashMap::new();
|
||||
for (name, pv, dist) in params {
|
||||
param_map.insert(name.to_string(), pv);
|
||||
dist_map.insert(name.to_string(), dist);
|
||||
for (param_id, pv, dist) in params {
|
||||
param_map.insert(param_id, pv);
|
||||
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]
|
||||
@@ -1104,12 +1105,13 @@ mod tests {
|
||||
});
|
||||
|
||||
// Create 20 trials with values 0..20
|
||||
let x_id = ParamId::new();
|
||||
let history: Vec<CompletedTrial> = (0..20)
|
||||
.map(|i| {
|
||||
create_trial(
|
||||
i as u64,
|
||||
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();
|
||||
@@ -1138,6 +1140,7 @@ mod tests {
|
||||
});
|
||||
|
||||
// Create history where low values (near 0.2) are "good"
|
||||
let x_id = ParamId::new();
|
||||
let history: Vec<CompletedTrial> = (0..20)
|
||||
.map(|i| {
|
||||
let x = f64::from(i) / 20.0;
|
||||
@@ -1146,7 +1149,7 @@ mod tests {
|
||||
create_trial(
|
||||
i as u64,
|
||||
value,
|
||||
vec![("x", ParamValue::Float(x), dist.clone())],
|
||||
vec![(x_id, ParamValue::Float(x), dist.clone())],
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
@@ -1175,6 +1178,7 @@ mod tests {
|
||||
let dist = Distribution::Categorical(CategoricalDistribution { n_choices: 4 });
|
||||
|
||||
// Create history where category 1 is consistently good
|
||||
let cat_id = ParamId::new();
|
||||
let history: Vec<CompletedTrial> = (0..20)
|
||||
.map(|i| {
|
||||
let category = i % 4;
|
||||
@@ -1184,7 +1188,7 @@ mod tests {
|
||||
i as u64,
|
||||
value,
|
||||
vec![(
|
||||
"cat",
|
||||
cat_id,
|
||||
ParamValue::Categorical(category as usize),
|
||||
dist.clone(),
|
||||
)],
|
||||
@@ -1220,6 +1224,7 @@ mod tests {
|
||||
});
|
||||
|
||||
// Create history where values near 30 are good
|
||||
let x_id = ParamId::new();
|
||||
let history: Vec<CompletedTrial> = (0..20)
|
||||
.map(|i| {
|
||||
let x = i * 5; // 0, 5, 10, ..., 95
|
||||
@@ -1227,7 +1232,7 @@ mod tests {
|
||||
create_trial(
|
||||
i as u64,
|
||||
value,
|
||||
vec![("x", ParamValue::Int(x), dist.clone())],
|
||||
vec![(x_id, ParamValue::Int(x), dist.clone())],
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
@@ -1252,12 +1257,13 @@ mod tests {
|
||||
step: None,
|
||||
});
|
||||
|
||||
let x_id = ParamId::new();
|
||||
let history: Vec<CompletedTrial> = (0..20)
|
||||
.map(|i| {
|
||||
create_trial(
|
||||
i as u64,
|
||||
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();
|
||||
@@ -1332,12 +1338,13 @@ mod tests {
|
||||
step: None,
|
||||
});
|
||||
|
||||
let x_id = ParamId::new();
|
||||
let history: Vec<CompletedTrial> = (0..20u32)
|
||||
.map(|i| {
|
||||
create_trial(
|
||||
u64::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();
|
||||
|
||||
+232
-191
@@ -11,6 +11,7 @@
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
|
||||
use crate::distribution::Distribution;
|
||||
use crate::parameter::ParamId;
|
||||
use crate::sampler::CompletedTrial;
|
||||
|
||||
/// 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,
|
||||
/// this helps identify the "stable" set of parameters that can be modeled jointly.
|
||||
#[must_use]
|
||||
pub fn calculate(trials: &[CompletedTrial]) -> HashMap<String, Distribution> {
|
||||
pub fn calculate(trials: &[CompletedTrial]) -> HashMap<ParamId, Distribution> {
|
||||
if trials.is_empty() {
|
||||
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 mut candidate_params: HashSet<&str> = first_trial
|
||||
.distributions
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect();
|
||||
let mut candidate_params: HashSet<ParamId> =
|
||||
first_trial.distributions.keys().copied().collect();
|
||||
|
||||
// Intersect with parameter sets from all other trials
|
||||
for trial in trials.iter().skip(1) {
|
||||
let trial_params: HashSet<&str> =
|
||||
trial.distributions.keys().map(String::as_str).collect();
|
||||
let trial_params: HashSet<ParamId> = trial.distributions.keys().copied().collect();
|
||||
candidate_params.retain(|param| trial_params.contains(param));
|
||||
}
|
||||
|
||||
// Build the result map using distributions from the first trial
|
||||
// that contains each parameter
|
||||
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
|
||||
for trial in trials {
|
||||
if let Some(dist) = trial.distributions.get(param_name) {
|
||||
result.insert(param_name.to_string(), dist.clone());
|
||||
if let Some(dist) = trial.distributions.get(¶m_id) {
|
||||
result.insert(param_id, dist.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -193,7 +190,7 @@ impl GroupDecomposedSearchSpace {
|
||||
///
|
||||
/// # 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
|
||||
/// together in at least one trial (directly or transitively). Parameters in
|
||||
/// 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
|
||||
/// independent parameter groups separately.
|
||||
#[must_use]
|
||||
pub fn calculate(trials: &[CompletedTrial]) -> Vec<HashSet<String>> {
|
||||
pub fn calculate(trials: &[CompletedTrial]) -> Vec<HashSet<ParamId>> {
|
||||
if trials.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Collect all unique parameter names
|
||||
let mut all_params: HashSet<String> = HashSet::new();
|
||||
// Collect all unique parameter ids
|
||||
let mut all_params: HashSet<ParamId> = HashSet::new();
|
||||
for trial in trials {
|
||||
for param_name in trial.distributions.keys() {
|
||||
all_params.insert(param_name.clone());
|
||||
for ¶m_id in trial.distributions.keys() {
|
||||
all_params.insert(param_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,51 +228,51 @@ impl GroupDecomposedSearchSpace {
|
||||
|
||||
// Build adjacency list for co-occurrence graph
|
||||
// Two parameters are adjacent if they appear in the same trial
|
||||
let mut adjacency: HashMap<String, HashSet<String>> = HashMap::new();
|
||||
for param in &all_params {
|
||||
adjacency.insert(param.clone(), HashSet::new());
|
||||
let mut adjacency: HashMap<ParamId, HashSet<ParamId>> = HashMap::new();
|
||||
for ¶m in &all_params {
|
||||
adjacency.insert(param, HashSet::new());
|
||||
}
|
||||
|
||||
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
|
||||
for (i, param1) in trial_params.iter().enumerate() {
|
||||
for param2 in trial_params.iter().skip(i + 1) {
|
||||
for (i, ¶m1) in trial_params.iter().enumerate() {
|
||||
for ¶m2 in trial_params.iter().skip(i + 1) {
|
||||
adjacency
|
||||
.get_mut(*param1)
|
||||
.get_mut(¶m1)
|
||||
.expect("param should exist in adjacency map")
|
||||
.insert((*param2).clone());
|
||||
.insert(param2);
|
||||
adjacency
|
||||
.get_mut(*param2)
|
||||
.get_mut(¶m2)
|
||||
.expect("param should exist in adjacency map")
|
||||
.insert((*param1).clone());
|
||||
.insert(param1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find connected components using BFS
|
||||
let mut visited: HashSet<String> = HashSet::new();
|
||||
let mut groups: Vec<HashSet<String>> = Vec::new();
|
||||
let mut visited: HashSet<ParamId> = HashSet::new();
|
||||
let mut groups: Vec<HashSet<ParamId>> = Vec::new();
|
||||
|
||||
for param in &all_params {
|
||||
if visited.contains(param) {
|
||||
for ¶m in &all_params {
|
||||
if visited.contains(¶m) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// BFS to find all parameters in this component
|
||||
let mut component: HashSet<String> = HashSet::new();
|
||||
let mut queue: VecDeque<String> = VecDeque::new();
|
||||
queue.push_back(param.clone());
|
||||
visited.insert(param.clone());
|
||||
let mut component: HashSet<ParamId> = HashSet::new();
|
||||
let mut queue: VecDeque<ParamId> = VecDeque::new();
|
||||
queue.push_back(param);
|
||||
visited.insert(param);
|
||||
|
||||
while let Some(current) = queue.pop_front() {
|
||||
component.insert(current.clone());
|
||||
component.insert(current);
|
||||
|
||||
if let Some(neighbors) = adjacency.get(¤t) {
|
||||
for neighbor in neighbors {
|
||||
if !visited.contains(neighbor) {
|
||||
visited.insert(neighbor.clone());
|
||||
queue.push_back(neighbor.clone());
|
||||
for &neighbor in neighbors {
|
||||
if !visited.contains(&neighbor) {
|
||||
visited.insert(neighbor);
|
||||
queue.push_back(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,19 +290,20 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::distribution::{CategoricalDistribution, FloatDistribution, IntDistribution};
|
||||
use crate::param::ParamValue;
|
||||
use crate::parameter::ParamId;
|
||||
|
||||
fn create_trial(
|
||||
id: u64,
|
||||
params: Vec<(&str, ParamValue, Distribution)>,
|
||||
params: Vec<(ParamId, ParamValue, Distribution)>,
|
||||
value: f64,
|
||||
) -> CompletedTrial {
|
||||
let mut param_map = HashMap::new();
|
||||
let mut dist_map = HashMap::new();
|
||||
for (name, pv, dist) in params {
|
||||
param_map.insert(name.to_string(), pv);
|
||||
dist_map.insert(name.to_string(), dist);
|
||||
for (param_id, pv, dist) in params {
|
||||
param_map.insert(param_id, pv);
|
||||
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]
|
||||
@@ -317,6 +315,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_single_trial() {
|
||||
let x_id = ParamId::new();
|
||||
let y_id = ParamId::new();
|
||||
let dist_x = Distribution::Float(FloatDistribution {
|
||||
low: 0.0,
|
||||
high: 1.0,
|
||||
@@ -333,22 +333,24 @@ mod tests {
|
||||
let trial = create_trial(
|
||||
0,
|
||||
vec![
|
||||
("x", ParamValue::Float(0.5), dist_x.clone()),
|
||||
("y", ParamValue::Int(5), dist_y.clone()),
|
||||
(x_id, ParamValue::Float(0.5), dist_x.clone()),
|
||||
(y_id, ParamValue::Int(5), dist_y.clone()),
|
||||
],
|
||||
1.0,
|
||||
);
|
||||
|
||||
let result = IntersectionSearchSpace::calculate(&[trial]);
|
||||
assert_eq!(result.len(), 2);
|
||||
assert!(result.contains_key("x"));
|
||||
assert!(result.contains_key("y"));
|
||||
assert_eq!(result.get("x"), Some(&dist_x));
|
||||
assert_eq!(result.get("y"), Some(&dist_y));
|
||||
assert!(result.contains_key(&x_id));
|
||||
assert!(result.contains_key(&y_id));
|
||||
assert_eq!(result.get(&x_id), Some(&dist_x));
|
||||
assert_eq!(result.get(&y_id), Some(&dist_y));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_trials_same_params() {
|
||||
let x_id = ParamId::new();
|
||||
let y_id = ParamId::new();
|
||||
let dist_x = Distribution::Float(FloatDistribution {
|
||||
low: 0.0,
|
||||
high: 1.0,
|
||||
@@ -369,8 +371,8 @@ mod tests {
|
||||
create_trial(
|
||||
i,
|
||||
vec![
|
||||
("x", ParamValue::Float(val), dist_x.clone()),
|
||||
("y", ParamValue::Float(val - 0.5), dist_y.clone()),
|
||||
(x_id, ParamValue::Float(val), dist_x.clone()),
|
||||
(y_id, ParamValue::Float(val - 0.5), dist_y.clone()),
|
||||
],
|
||||
val * val,
|
||||
)
|
||||
@@ -379,12 +381,15 @@ mod tests {
|
||||
|
||||
let result = IntersectionSearchSpace::calculate(&trials);
|
||||
assert_eq!(result.len(), 2);
|
||||
assert!(result.contains_key("x"));
|
||||
assert!(result.contains_key("y"));
|
||||
assert!(result.contains_key(&x_id));
|
||||
assert!(result.contains_key(&y_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
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 {
|
||||
low: 0.0,
|
||||
high: 1.0,
|
||||
@@ -408,8 +413,8 @@ mod tests {
|
||||
let trial1 = create_trial(
|
||||
0,
|
||||
vec![
|
||||
("x", ParamValue::Float(0.5), dist_x.clone()),
|
||||
("y", ParamValue::Float(0.3), dist_y.clone()),
|
||||
(x_id, ParamValue::Float(0.5), dist_x.clone()),
|
||||
(y_id, ParamValue::Float(0.3), dist_y.clone()),
|
||||
],
|
||||
1.0,
|
||||
);
|
||||
@@ -418,8 +423,8 @@ mod tests {
|
||||
let trial2 = create_trial(
|
||||
1,
|
||||
vec![
|
||||
("x", ParamValue::Float(0.7), dist_x.clone()),
|
||||
("z", ParamValue::Float(0.2), dist_z.clone()),
|
||||
(x_id, ParamValue::Float(0.7), dist_x.clone()),
|
||||
(z_id, ParamValue::Float(0.2), dist_z.clone()),
|
||||
],
|
||||
0.5,
|
||||
);
|
||||
@@ -428,24 +433,26 @@ mod tests {
|
||||
let trial3 = create_trial(
|
||||
2,
|
||||
vec![
|
||||
("x", ParamValue::Float(0.6), dist_x.clone()),
|
||||
("y", ParamValue::Float(0.4), dist_y.clone()),
|
||||
("z", ParamValue::Float(0.1), dist_z.clone()),
|
||||
(x_id, ParamValue::Float(0.6), dist_x.clone()),
|
||||
(y_id, ParamValue::Float(0.4), dist_y.clone()),
|
||||
(z_id, ParamValue::Float(0.1), dist_z.clone()),
|
||||
],
|
||||
0.8,
|
||||
);
|
||||
|
||||
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!(result.contains_key("x"));
|
||||
assert!(!result.contains_key("y"));
|
||||
assert!(!result.contains_key("z"));
|
||||
assert!(result.contains_key(&x_id));
|
||||
assert!(!result.contains_key(&y_id));
|
||||
assert!(!result.contains_key(&z_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_common_params() {
|
||||
let x_id = ParamId::new();
|
||||
let y_id = ParamId::new();
|
||||
let dist_x = Distribution::Float(FloatDistribution {
|
||||
low: 0.0,
|
||||
high: 1.0,
|
||||
@@ -460,10 +467,10 @@ mod tests {
|
||||
});
|
||||
|
||||
// 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
|
||||
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]);
|
||||
|
||||
@@ -473,6 +480,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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 {
|
||||
low: 0.0,
|
||||
high: 1.0,
|
||||
@@ -490,9 +500,9 @@ mod tests {
|
||||
let trial1 = create_trial(
|
||||
0,
|
||||
vec![
|
||||
("learning_rate", ParamValue::Float(0.01), dist_float.clone()),
|
||||
("n_layers", ParamValue::Int(3), dist_int.clone()),
|
||||
("optimizer", ParamValue::Categorical(0), dist_cat.clone()),
|
||||
(lr_id, ParamValue::Float(0.01), dist_float.clone()),
|
||||
(n_layers_id, ParamValue::Int(3), dist_int.clone()),
|
||||
(optimizer_id, ParamValue::Categorical(0), dist_cat.clone()),
|
||||
],
|
||||
1.0,
|
||||
);
|
||||
@@ -500,13 +510,9 @@ mod tests {
|
||||
let trial2 = create_trial(
|
||||
1,
|
||||
vec![
|
||||
(
|
||||
"learning_rate",
|
||||
ParamValue::Float(0.001),
|
||||
dist_float.clone(),
|
||||
),
|
||||
("n_layers", ParamValue::Int(5), dist_int.clone()),
|
||||
("optimizer", ParamValue::Categorical(1), dist_cat.clone()),
|
||||
(lr_id, ParamValue::Float(0.001), dist_float.clone()),
|
||||
(n_layers_id, ParamValue::Int(5), dist_int.clone()),
|
||||
(optimizer_id, ParamValue::Categorical(1), dist_cat.clone()),
|
||||
],
|
||||
0.8,
|
||||
);
|
||||
@@ -514,19 +520,20 @@ mod tests {
|
||||
let result = IntersectionSearchSpace::calculate(&[trial1, trial2]);
|
||||
|
||||
assert_eq!(result.len(), 3);
|
||||
assert!(matches!(result.get(&lr_id), Some(Distribution::Float(_))));
|
||||
assert!(matches!(
|
||||
result.get("learning_rate"),
|
||||
Some(Distribution::Float(_))
|
||||
result.get(&n_layers_id),
|
||||
Some(Distribution::Int(_))
|
||||
));
|
||||
assert!(matches!(result.get("n_layers"), Some(Distribution::Int(_))));
|
||||
assert!(matches!(
|
||||
result.get("optimizer"),
|
||||
result.get(&optimizer_id),
|
||||
Some(Distribution::Categorical(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_distribution_from_first_trial() {
|
||||
let x_id = ParamId::new();
|
||||
// Test that when distributions differ, the first trial's distribution is used
|
||||
let dist_x_v1 = Distribution::Float(FloatDistribution {
|
||||
low: 0.0,
|
||||
@@ -543,13 +550,13 @@ mod tests {
|
||||
|
||||
let trial1 = create_trial(
|
||||
0,
|
||||
vec![("x", ParamValue::Float(0.5), dist_x_v1.clone())],
|
||||
vec![(x_id, ParamValue::Float(0.5), dist_x_v1.clone())],
|
||||
1.0,
|
||||
);
|
||||
|
||||
let trial2 = create_trial(
|
||||
1,
|
||||
vec![("x", ParamValue::Float(5.0), dist_x_v2.clone())],
|
||||
vec![(x_id, ParamValue::Float(5.0), dist_x_v2.clone())],
|
||||
0.5,
|
||||
);
|
||||
|
||||
@@ -557,13 +564,15 @@ mod tests {
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
// 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]
|
||||
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
|
||||
// e.g., "use_dropout" is a bool, and "dropout_rate" only exists when use_dropout=true
|
||||
let dist_lr = Distribution::Float(FloatDistribution {
|
||||
low: 1e-5,
|
||||
high: 1e-1,
|
||||
@@ -582,14 +591,14 @@ mod tests {
|
||||
let trial1 = create_trial(
|
||||
0,
|
||||
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),
|
||||
dist_dropout.clone(),
|
||||
),
|
||||
(
|
||||
"dropout_rate",
|
||||
dropout_rate_id,
|
||||
ParamValue::Float(0.2),
|
||||
dist_dropout_rate.clone(),
|
||||
),
|
||||
@@ -601,9 +610,9 @@ mod tests {
|
||||
let trial2 = create_trial(
|
||||
1,
|
||||
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),
|
||||
dist_dropout.clone(),
|
||||
),
|
||||
@@ -615,14 +624,14 @@ mod tests {
|
||||
let trial3 = create_trial(
|
||||
2,
|
||||
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),
|
||||
dist_dropout.clone(),
|
||||
),
|
||||
(
|
||||
"dropout_rate",
|
||||
dropout_rate_id,
|
||||
ParamValue::Float(0.3),
|
||||
dist_dropout_rate.clone(),
|
||||
),
|
||||
@@ -632,11 +641,11 @@ mod tests {
|
||||
|
||||
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!(result.contains_key("lr"));
|
||||
assert!(result.contains_key("use_dropout"));
|
||||
assert!(!result.contains_key("dropout_rate")); // Not in trial2
|
||||
assert!(result.contains_key(&lr_id));
|
||||
assert!(result.contains_key(&use_dropout_id));
|
||||
assert!(!result.contains_key(&dropout_rate_id)); // Not in trial2
|
||||
}
|
||||
|
||||
// ==================== GroupDecomposedSearchSpace Tests ====================
|
||||
@@ -657,12 +666,13 @@ mod tests {
|
||||
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]);
|
||||
|
||||
assert_eq!(groups.len(), 1);
|
||||
assert!(groups[0].contains("x"));
|
||||
assert!(groups[0].contains(&x_id));
|
||||
assert_eq!(groups[0].len(), 1);
|
||||
}
|
||||
|
||||
@@ -675,12 +685,15 @@ mod tests {
|
||||
step: None,
|
||||
});
|
||||
|
||||
let x_id = ParamId::new();
|
||||
let y_id = ParamId::new();
|
||||
let z_id = ParamId::new();
|
||||
let trial = create_trial(
|
||||
0,
|
||||
vec![
|
||||
("x", ParamValue::Float(0.5), dist.clone()),
|
||||
("y", ParamValue::Float(0.3), dist.clone()),
|
||||
("z", ParamValue::Float(0.7), dist),
|
||||
(x_id, ParamValue::Float(0.5), dist.clone()),
|
||||
(y_id, ParamValue::Float(0.3), dist.clone()),
|
||||
(z_id, ParamValue::Float(0.7), dist),
|
||||
],
|
||||
1.0,
|
||||
);
|
||||
@@ -690,9 +703,9 @@ mod tests {
|
||||
// All params appear together, so one group
|
||||
assert_eq!(groups.len(), 1);
|
||||
assert_eq!(groups[0].len(), 3);
|
||||
assert!(groups[0].contains("x"));
|
||||
assert!(groups[0].contains("y"));
|
||||
assert!(groups[0].contains("z"));
|
||||
assert!(groups[0].contains(&x_id));
|
||||
assert!(groups[0].contains(&y_id));
|
||||
assert!(groups[0].contains(&z_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -704,12 +717,17 @@ mod tests {
|
||||
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)
|
||||
let trial1 = create_trial(
|
||||
0,
|
||||
vec![
|
||||
("x", ParamValue::Float(0.1), dist.clone()),
|
||||
("y", ParamValue::Float(0.2), dist.clone()),
|
||||
(x_id, ParamValue::Float(0.1), dist.clone()),
|
||||
(y_id, ParamValue::Float(0.2), dist.clone()),
|
||||
],
|
||||
1.0,
|
||||
);
|
||||
@@ -718,8 +736,8 @@ mod tests {
|
||||
let trial2 = create_trial(
|
||||
1,
|
||||
vec![
|
||||
("a", ParamValue::Float(0.3), dist.clone()),
|
||||
("b", ParamValue::Float(0.4), dist),
|
||||
(a_id, ParamValue::Float(0.3), dist.clone()),
|
||||
(b_id, ParamValue::Float(0.4), dist),
|
||||
],
|
||||
0.5,
|
||||
);
|
||||
@@ -730,8 +748,8 @@ mod tests {
|
||||
assert_eq!(groups.len(), 2);
|
||||
|
||||
// Find which group has x/y and which has a/b
|
||||
let group_xy = groups.iter().find(|g| g.contains("x"));
|
||||
let group_ab = groups.iter().find(|g| g.contains("a"));
|
||||
let group_xy = groups.iter().find(|g| g.contains(&x_id));
|
||||
let group_ab = groups.iter().find(|g| g.contains(&a_id));
|
||||
|
||||
assert!(group_xy.is_some());
|
||||
assert!(group_ab.is_some());
|
||||
@@ -740,12 +758,12 @@ mod tests {
|
||||
let group_ab = group_ab.expect("group with a should exist");
|
||||
|
||||
assert_eq!(group_xy.len(), 2);
|
||||
assert!(group_xy.contains("x"));
|
||||
assert!(group_xy.contains("y"));
|
||||
assert!(group_xy.contains(&x_id));
|
||||
assert!(group_xy.contains(&y_id));
|
||||
|
||||
assert_eq!(group_ab.len(), 2);
|
||||
assert!(group_ab.contains("a"));
|
||||
assert!(group_ab.contains("b"));
|
||||
assert!(group_ab.contains(&a_id));
|
||||
assert!(group_ab.contains(&b_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -757,12 +775,16 @@ mod tests {
|
||||
step: None,
|
||||
});
|
||||
|
||||
let x_id = ParamId::new();
|
||||
let y_id = ParamId::new();
|
||||
let z_id = ParamId::new();
|
||||
|
||||
// Trial 1: x, y
|
||||
let trial1 = create_trial(
|
||||
0,
|
||||
vec![
|
||||
("x", ParamValue::Float(0.1), dist.clone()),
|
||||
("y", ParamValue::Float(0.2), dist.clone()),
|
||||
(x_id, ParamValue::Float(0.1), dist.clone()),
|
||||
(y_id, ParamValue::Float(0.2), dist.clone()),
|
||||
],
|
||||
1.0,
|
||||
);
|
||||
@@ -771,8 +793,8 @@ mod tests {
|
||||
let trial2 = create_trial(
|
||||
1,
|
||||
vec![
|
||||
("y", ParamValue::Float(0.3), dist.clone()),
|
||||
("z", ParamValue::Float(0.4), dist),
|
||||
(y_id, ParamValue::Float(0.3), dist.clone()),
|
||||
(z_id, ParamValue::Float(0.4), dist),
|
||||
],
|
||||
0.5,
|
||||
);
|
||||
@@ -782,9 +804,9 @@ mod tests {
|
||||
// All should be in one group due to transitive connection via y
|
||||
assert_eq!(groups.len(), 1);
|
||||
assert_eq!(groups[0].len(), 3);
|
||||
assert!(groups[0].contains("x"));
|
||||
assert!(groups[0].contains("y"));
|
||||
assert!(groups[0].contains("z"));
|
||||
assert!(groups[0].contains(&x_id));
|
||||
assert!(groups[0].contains(&y_id));
|
||||
assert!(groups[0].contains(&z_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -796,33 +818,35 @@ mod tests {
|
||||
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
|
||||
// Trial 1: a, b
|
||||
let trial1 = create_trial(
|
||||
0,
|
||||
vec![
|
||||
("a", ParamValue::Float(0.1), dist.clone()),
|
||||
("b", ParamValue::Float(0.2), dist.clone()),
|
||||
(a_id, ParamValue::Float(0.1), dist.clone()),
|
||||
(b_id, ParamValue::Float(0.2), dist.clone()),
|
||||
],
|
||||
1.0,
|
||||
);
|
||||
|
||||
// Trial 2: b, c
|
||||
let trial2 = create_trial(
|
||||
1,
|
||||
vec![
|
||||
("b", ParamValue::Float(0.3), dist.clone()),
|
||||
("c", ParamValue::Float(0.4), dist.clone()),
|
||||
(b_id, ParamValue::Float(0.3), dist.clone()),
|
||||
(c_id, ParamValue::Float(0.4), dist.clone()),
|
||||
],
|
||||
0.5,
|
||||
);
|
||||
|
||||
// Trial 3: c, d
|
||||
let trial3 = create_trial(
|
||||
2,
|
||||
vec![
|
||||
("c", ParamValue::Float(0.5), dist.clone()),
|
||||
("d", ParamValue::Float(0.6), dist),
|
||||
(c_id, ParamValue::Float(0.5), dist.clone()),
|
||||
(d_id, ParamValue::Float(0.6), dist),
|
||||
],
|
||||
0.3,
|
||||
);
|
||||
@@ -832,10 +856,10 @@ mod tests {
|
||||
// All should be connected in one group
|
||||
assert_eq!(groups.len(), 1);
|
||||
assert_eq!(groups[0].len(), 4);
|
||||
assert!(groups[0].contains("a"));
|
||||
assert!(groups[0].contains("b"));
|
||||
assert!(groups[0].contains("c"));
|
||||
assert!(groups[0].contains("d"));
|
||||
assert!(groups[0].contains(&a_id));
|
||||
assert!(groups[0].contains(&b_id));
|
||||
assert!(groups[0].contains(&c_id));
|
||||
assert!(groups[0].contains(&d_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -847,10 +871,14 @@ mod tests {
|
||||
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)
|
||||
let trial1 = create_trial(0, vec![("x", ParamValue::Float(0.1), dist.clone())], 1.0);
|
||||
let trial2 = create_trial(1, vec![("y", ParamValue::Float(0.2), dist.clone())], 0.5);
|
||||
let trial3 = create_trial(2, vec![("z", ParamValue::Float(0.3), dist)], 0.3);
|
||||
let trial1 = create_trial(0, vec![(x_id, ParamValue::Float(0.1), dist.clone())], 1.0);
|
||||
let trial2 = create_trial(1, vec![(y_id, ParamValue::Float(0.2), dist.clone())], 0.5);
|
||||
let trial3 = create_trial(2, vec![(z_id, ParamValue::Float(0.3), dist)], 0.3);
|
||||
|
||||
let groups = GroupDecomposedSearchSpace::calculate(&[trial1, trial2, trial3]);
|
||||
|
||||
@@ -861,10 +889,10 @@ mod tests {
|
||||
}
|
||||
|
||||
// Verify all params are covered
|
||||
let all_params: HashSet<String> = groups.iter().flatten().cloned().collect();
|
||||
assert!(all_params.contains("x"));
|
||||
assert!(all_params.contains("y"));
|
||||
assert!(all_params.contains("z"));
|
||||
let all_params: HashSet<ParamId> = groups.iter().flatten().copied().collect();
|
||||
assert!(all_params.contains(&x_id));
|
||||
assert!(all_params.contains(&y_id));
|
||||
assert!(all_params.contains(&z_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -876,51 +904,54 @@ mod tests {
|
||||
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 2: a, b connected
|
||||
// w is isolated
|
||||
|
||||
// Trial 1: x, y
|
||||
let trial1 = create_trial(
|
||||
0,
|
||||
vec![
|
||||
("x", ParamValue::Float(0.1), dist.clone()),
|
||||
("y", ParamValue::Float(0.2), dist.clone()),
|
||||
(x_id, ParamValue::Float(0.1), dist.clone()),
|
||||
(y_id, ParamValue::Float(0.2), dist.clone()),
|
||||
],
|
||||
1.0,
|
||||
);
|
||||
|
||||
// Trial 2: y, z
|
||||
let trial2 = create_trial(
|
||||
1,
|
||||
vec![
|
||||
("y", ParamValue::Float(0.3), dist.clone()),
|
||||
("z", ParamValue::Float(0.4), dist.clone()),
|
||||
(y_id, ParamValue::Float(0.3), dist.clone()),
|
||||
(z_id, ParamValue::Float(0.4), dist.clone()),
|
||||
],
|
||||
0.5,
|
||||
);
|
||||
|
||||
// Trial 3: a, b
|
||||
let trial3 = create_trial(
|
||||
2,
|
||||
vec![
|
||||
("a", ParamValue::Float(0.5), dist.clone()),
|
||||
("b", ParamValue::Float(0.6), dist.clone()),
|
||||
(a_id, ParamValue::Float(0.5), dist.clone()),
|
||||
(b_id, ParamValue::Float(0.6), dist.clone()),
|
||||
],
|
||||
0.3,
|
||||
);
|
||||
|
||||
// Trial 4: w (isolated)
|
||||
let trial4 = create_trial(3, vec![("w", ParamValue::Float(0.7), dist)], 0.2);
|
||||
let trial4 = create_trial(3, vec![(w_id, ParamValue::Float(0.7), dist)], 0.2);
|
||||
|
||||
let groups = GroupDecomposedSearchSpace::calculate(&[trial1, trial2, trial3, trial4]);
|
||||
|
||||
// Should have 3 groups: {x,y,z}, {a,b}, {w}
|
||||
assert_eq!(groups.len(), 3);
|
||||
|
||||
let group_xyz = groups.iter().find(|g| g.contains("x"));
|
||||
let group_ab = groups.iter().find(|g| g.contains("a"));
|
||||
let group_w = groups.iter().find(|g| g.contains("w"));
|
||||
let group_xyz = groups.iter().find(|g| g.contains(&x_id));
|
||||
let group_ab = groups.iter().find(|g| g.contains(&a_id));
|
||||
let group_w = groups.iter().find(|g| g.contains(&w_id));
|
||||
|
||||
assert!(group_xyz.is_some());
|
||||
assert!(group_ab.is_some());
|
||||
@@ -928,18 +959,18 @@ mod tests {
|
||||
|
||||
let group_xyz = group_xyz.expect("group with x should exist");
|
||||
assert_eq!(group_xyz.len(), 3);
|
||||
assert!(group_xyz.contains("x"));
|
||||
assert!(group_xyz.contains("y"));
|
||||
assert!(group_xyz.contains("z"));
|
||||
assert!(group_xyz.contains(&x_id));
|
||||
assert!(group_xyz.contains(&y_id));
|
||||
assert!(group_xyz.contains(&z_id));
|
||||
|
||||
let group_ab = group_ab.expect("group with a should exist");
|
||||
assert_eq!(group_ab.len(), 2);
|
||||
assert!(group_ab.contains("a"));
|
||||
assert!(group_ab.contains("b"));
|
||||
assert!(group_ab.contains(&a_id));
|
||||
assert!(group_ab.contains(&b_id));
|
||||
|
||||
let group_w = group_w.expect("group with w should exist");
|
||||
assert_eq!(group_w.len(), 1);
|
||||
assert!(group_w.contains("w"));
|
||||
assert!(group_w.contains(&w_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -951,14 +982,19 @@ mod tests {
|
||||
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
|
||||
let trial = create_trial(
|
||||
0,
|
||||
vec![
|
||||
("a", ParamValue::Float(0.1), dist.clone()),
|
||||
("b", ParamValue::Float(0.2), dist.clone()),
|
||||
("c", ParamValue::Float(0.3), dist.clone()),
|
||||
("d", ParamValue::Float(0.4), dist),
|
||||
(a_id, ParamValue::Float(0.1), dist.clone()),
|
||||
(b_id, ParamValue::Float(0.2), dist.clone()),
|
||||
(c_id, ParamValue::Float(0.3), dist.clone()),
|
||||
(d_id, ParamValue::Float(0.4), dist),
|
||||
],
|
||||
1.0,
|
||||
);
|
||||
@@ -972,6 +1008,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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 {
|
||||
low: 0.0,
|
||||
high: 1.0,
|
||||
@@ -991,23 +1030,23 @@ mod tests {
|
||||
let trial1 = create_trial(
|
||||
0,
|
||||
vec![
|
||||
("learning_rate", ParamValue::Float(0.01), dist_float.clone()),
|
||||
("n_layers", ParamValue::Int(3), dist_int.clone()),
|
||||
(lr_id, ParamValue::Float(0.01), dist_float.clone()),
|
||||
(n_layers_id, ParamValue::Int(3), dist_int.clone()),
|
||||
],
|
||||
1.0,
|
||||
);
|
||||
|
||||
let trial2 = create_trial(
|
||||
1,
|
||||
vec![("optimizer", ParamValue::Categorical(1), dist_cat)],
|
||||
vec![(optimizer_id, ParamValue::Categorical(1), dist_cat)],
|
||||
0.5,
|
||||
);
|
||||
|
||||
let trial3 = create_trial(
|
||||
2,
|
||||
vec![
|
||||
("learning_rate", ParamValue::Float(0.001), dist_float),
|
||||
("n_layers", ParamValue::Int(5), dist_int),
|
||||
(lr_id, ParamValue::Float(0.001), dist_float),
|
||||
(n_layers_id, ParamValue::Int(5), dist_int),
|
||||
],
|
||||
0.8,
|
||||
);
|
||||
@@ -1017,20 +1056,20 @@ mod tests {
|
||||
// Should have 2 groups: {learning_rate, n_layers} and {optimizer}
|
||||
assert_eq!(groups.len(), 2);
|
||||
|
||||
let group_lr = groups.iter().find(|g| g.contains("learning_rate"));
|
||||
let group_opt = groups.iter().find(|g| g.contains("optimizer"));
|
||||
let group_lr = groups.iter().find(|g| g.contains(&lr_id));
|
||||
let group_opt = groups.iter().find(|g| g.contains(&optimizer_id));
|
||||
|
||||
assert!(group_lr.is_some());
|
||||
assert!(group_opt.is_some());
|
||||
|
||||
let group_lr = group_lr.expect("group with learning_rate should exist");
|
||||
assert_eq!(group_lr.len(), 2);
|
||||
assert!(group_lr.contains("learning_rate"));
|
||||
assert!(group_lr.contains("n_layers"));
|
||||
assert!(group_lr.contains(&lr_id));
|
||||
assert!(group_lr.contains(&n_layers_id));
|
||||
|
||||
let group_opt = group_opt.expect("group with optimizer should exist");
|
||||
assert_eq!(group_opt.len(), 1);
|
||||
assert!(group_opt.contains("optimizer"));
|
||||
assert!(group_opt.contains(&optimizer_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1042,15 +1081,17 @@ mod tests {
|
||||
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
|
||||
// Trial 1: center, a
|
||||
// Trial 2: center, b
|
||||
// Trial 3: center, c
|
||||
let trial1 = create_trial(
|
||||
0,
|
||||
vec![
|
||||
("center", ParamValue::Float(0.1), dist.clone()),
|
||||
("a", ParamValue::Float(0.2), dist.clone()),
|
||||
(center_id, ParamValue::Float(0.1), dist.clone()),
|
||||
(a_id, ParamValue::Float(0.2), dist.clone()),
|
||||
],
|
||||
1.0,
|
||||
);
|
||||
@@ -1058,8 +1099,8 @@ mod tests {
|
||||
let trial2 = create_trial(
|
||||
1,
|
||||
vec![
|
||||
("center", ParamValue::Float(0.3), dist.clone()),
|
||||
("b", ParamValue::Float(0.4), dist.clone()),
|
||||
(center_id, ParamValue::Float(0.3), dist.clone()),
|
||||
(b_id, ParamValue::Float(0.4), dist.clone()),
|
||||
],
|
||||
0.5,
|
||||
);
|
||||
@@ -1067,8 +1108,8 @@ mod tests {
|
||||
let trial3 = create_trial(
|
||||
2,
|
||||
vec![
|
||||
("center", ParamValue::Float(0.5), dist.clone()),
|
||||
("c", ParamValue::Float(0.6), dist),
|
||||
(center_id, ParamValue::Float(0.5), dist.clone()),
|
||||
(c_id, ParamValue::Float(0.6), dist),
|
||||
],
|
||||
0.3,
|
||||
);
|
||||
@@ -1078,9 +1119,9 @@ mod tests {
|
||||
// All connected via center
|
||||
assert_eq!(groups.len(), 1);
|
||||
assert_eq!(groups[0].len(), 4);
|
||||
assert!(groups[0].contains("center"));
|
||||
assert!(groups[0].contains("a"));
|
||||
assert!(groups[0].contains("b"));
|
||||
assert!(groups[0].contains("c"));
|
||||
assert!(groups[0].contains(¢er_id));
|
||||
assert!(groups[0].contains(&a_id));
|
||||
assert!(groups[0].contains(&b_id));
|
||||
assert!(groups[0].contains(&c_id));
|
||||
}
|
||||
}
|
||||
|
||||
+83
-32
@@ -166,11 +166,13 @@ where
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
/// let study: Study<f64> = Study::new(Direction::Minimize);
|
||||
/// let x_param = FloatParam::new(0.0, 1.0);
|
||||
/// 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;
|
||||
/// study.complete_trial(trial, objective_value);
|
||||
///
|
||||
@@ -182,6 +184,7 @@ where
|
||||
trial.id(),
|
||||
trial.params().clone(),
|
||||
trial.distributions().clone(),
|
||||
trial.param_labels().clone(),
|
||||
value,
|
||||
);
|
||||
self.completed_trials.write().push(completed);
|
||||
@@ -228,11 +231,13 @@ where
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
/// let study: Study<f64> = Study::new(Direction::Minimize);
|
||||
/// let x_param = FloatParam::new(0.0, 1.0);
|
||||
/// 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);
|
||||
///
|
||||
/// for completed in study.trials() {
|
||||
@@ -253,13 +258,15 @@ where
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
/// let study: Study<f64> = Study::new(Direction::Minimize);
|
||||
/// assert_eq!(study.n_trials(), 0);
|
||||
///
|
||||
/// let x_param = FloatParam::new(0.0, 1.0);
|
||||
/// 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);
|
||||
/// assert_eq!(study.n_trials(), 1);
|
||||
/// ```
|
||||
@@ -280,6 +287,7 @@ where
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
/// let study: Study<f64> = Study::new(Direction::Minimize);
|
||||
@@ -287,12 +295,14 @@ where
|
||||
/// // Error when no trials completed
|
||||
/// assert!(study.best_trial().is_err());
|
||||
///
|
||||
/// let x_param = FloatParam::new(0.0, 1.0);
|
||||
///
|
||||
/// 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);
|
||||
///
|
||||
/// 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);
|
||||
///
|
||||
/// let best = study.best_trial().unwrap();
|
||||
@@ -343,6 +353,7 @@ where
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
/// let study: Study<f64> = Study::new(Direction::Maximize);
|
||||
@@ -350,12 +361,14 @@ where
|
||||
/// // Error when no trials completed
|
||||
/// assert!(study.best_value().is_err());
|
||||
///
|
||||
/// let x_param = FloatParam::new(0.0, 1.0);
|
||||
///
|
||||
/// 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);
|
||||
///
|
||||
/// 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);
|
||||
///
|
||||
/// let best = study.best_value().unwrap();
|
||||
@@ -392,6 +405,7 @@ where
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
/// use optimizer::sampler::random::RandomSampler;
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
@@ -399,9 +413,11 @@ where
|
||||
/// let sampler = RandomSampler::with_seed(42);
|
||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
///
|
||||
/// let x_param = FloatParam::new(-10.0, 10.0);
|
||||
///
|
||||
/// study
|
||||
/// .optimize(10, |trial| {
|
||||
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
||||
/// let x = x_param.suggest(trial)?;
|
||||
/// Ok::<_, optimizer::Error>(x * x)
|
||||
/// })
|
||||
/// .unwrap();
|
||||
@@ -460,6 +476,7 @@ where
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
/// use optimizer::sampler::random::RandomSampler;
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
@@ -469,12 +486,17 @@ where
|
||||
/// let sampler = RandomSampler::with_seed(42);
|
||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
///
|
||||
/// let x_param = FloatParam::new(-10.0, 10.0);
|
||||
///
|
||||
/// study
|
||||
/// .optimize_async(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))
|
||||
/// .optimize_async(10, |mut trial| {
|
||||
/// let x_param = x_param.clone();
|
||||
/// async move {
|
||||
/// let x = x_param.suggest(&mut trial)?;
|
||||
/// // Simulate async work (e.g., network request)
|
||||
/// let value = x * x;
|
||||
/// Ok::<_, optimizer::Error>((trial, value))
|
||||
/// }
|
||||
/// })
|
||||
/// .await?;
|
||||
///
|
||||
@@ -542,6 +564,7 @@ where
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
/// use optimizer::sampler::random::RandomSampler;
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
@@ -551,12 +574,17 @@ where
|
||||
/// let sampler = RandomSampler::with_seed(42);
|
||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
///
|
||||
/// let x_param = FloatParam::new(-10.0, 10.0);
|
||||
///
|
||||
/// study
|
||||
/// .optimize_parallel(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))
|
||||
/// .optimize_parallel(10, 4, move |mut trial| {
|
||||
/// let x_param = x_param.clone();
|
||||
/// async move {
|
||||
/// let x = x_param.suggest(&mut trial)?;
|
||||
/// // Async objective function (e.g., network request)
|
||||
/// let value = x * x;
|
||||
/// Ok::<_, optimizer::Error>((trial, value))
|
||||
/// }
|
||||
/// })
|
||||
/// .await?;
|
||||
///
|
||||
@@ -652,6 +680,7 @@ where
|
||||
/// ```
|
||||
/// use std::ops::ControlFlow;
|
||||
///
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
/// use optimizer::sampler::random::RandomSampler;
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
@@ -659,11 +688,13 @@ where
|
||||
/// let sampler = RandomSampler::with_seed(42);
|
||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
///
|
||||
/// let x_param = FloatParam::new(-10.0, 10.0);
|
||||
///
|
||||
/// study
|
||||
/// .optimize_with_callback(
|
||||
/// 100,
|
||||
/// |trial| {
|
||||
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
||||
/// let x = x_param.suggest(trial)?;
|
||||
/// Ok::<_, optimizer::Error>(x * x)
|
||||
/// },
|
||||
/// |_study, completed_trial| {
|
||||
@@ -746,6 +777,7 @@ impl Study<f64> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
/// use optimizer::sampler::random::RandomSampler;
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
@@ -755,7 +787,8 @@ impl Study<f64> {
|
||||
/// 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();
|
||||
/// let x_param = FloatParam::new(0.0, 1.0);
|
||||
/// let x = x_param.suggest(&mut trial).unwrap();
|
||||
/// ```
|
||||
pub fn create_trial_with_sampler(&self) -> Trial {
|
||||
let id = self.next_trial_id();
|
||||
@@ -788,6 +821,7 @@ impl Study<f64> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
/// use optimizer::sampler::random::RandomSampler;
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
@@ -795,9 +829,11 @@ impl Study<f64> {
|
||||
/// let sampler = RandomSampler::with_seed(42);
|
||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
///
|
||||
/// let x_param = FloatParam::new(-10.0, 10.0);
|
||||
///
|
||||
/// study
|
||||
/// .optimize_with_sampler(10, |trial| {
|
||||
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
||||
/// let x = x_param.suggest(trial)?;
|
||||
/// Ok::<_, optimizer::Error>(x * x)
|
||||
/// })
|
||||
/// .unwrap();
|
||||
@@ -859,6 +895,7 @@ impl Study<f64> {
|
||||
/// ```
|
||||
/// use std::ops::ControlFlow;
|
||||
///
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
/// use optimizer::sampler::random::RandomSampler;
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
@@ -866,11 +903,13 @@ impl Study<f64> {
|
||||
/// let sampler = RandomSampler::with_seed(42);
|
||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
///
|
||||
/// let x_param = FloatParam::new(-10.0, 10.0);
|
||||
///
|
||||
/// study
|
||||
/// .optimize_with_callback_sampler(
|
||||
/// 100,
|
||||
/// |trial| {
|
||||
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
||||
/// let x = x_param.suggest(trial)?;
|
||||
/// Ok::<_, optimizer::Error>(x * x)
|
||||
/// },
|
||||
/// |study, _completed_trial| {
|
||||
@@ -958,6 +997,7 @@ impl Study<f64> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
/// use optimizer::sampler::random::RandomSampler;
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
@@ -967,12 +1007,17 @@ impl Study<f64> {
|
||||
/// let sampler = RandomSampler::with_seed(42);
|
||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
///
|
||||
/// let x_param = FloatParam::new(-10.0, 10.0);
|
||||
///
|
||||
/// 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))
|
||||
/// .optimize_async_with_sampler(10, |mut trial| {
|
||||
/// let x_param = x_param.clone();
|
||||
/// async move {
|
||||
/// let x = x_param.suggest(&mut trial)?;
|
||||
/// // Simulate async work (e.g., network request)
|
||||
/// let value = x * x;
|
||||
/// Ok::<_, optimizer::Error>((trial, value))
|
||||
/// }
|
||||
/// })
|
||||
/// .await?;
|
||||
///
|
||||
@@ -1040,6 +1085,7 @@ impl Study<f64> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use optimizer::parameter::{FloatParam, Parameter};
|
||||
/// use optimizer::sampler::random::RandomSampler;
|
||||
/// use optimizer::{Direction, Study};
|
||||
///
|
||||
@@ -1049,12 +1095,17 @@ impl Study<f64> {
|
||||
/// let sampler = RandomSampler::with_seed(42);
|
||||
/// let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
///
|
||||
/// let x_param = FloatParam::new(-10.0, 10.0);
|
||||
///
|
||||
/// 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))
|
||||
/// .optimize_parallel_with_sampler(10, 4, move |mut trial| {
|
||||
/// let x_param = x_param.clone();
|
||||
/// async move {
|
||||
/// let x = x_param.suggest(&mut trial)?;
|
||||
/// // Async objective function (e.g., network request)
|
||||
/// let value = x * x;
|
||||
/// Ok::<_, optimizer::Error>((trial, value))
|
||||
/// }
|
||||
/// })
|
||||
/// .await?;
|
||||
///
|
||||
|
||||
+53
-767
@@ -1,81 +1,17 @@
|
||||
//! Trial implementation for tracking sampled parameters and trial state.
|
||||
|
||||
use core::ops::{Range, RangeInclusive};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use crate::distribution::{
|
||||
CategoricalDistribution, Distribution, FloatDistribution, IntDistribution,
|
||||
};
|
||||
use crate::distribution::Distribution;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::param::ParamValue;
|
||||
use crate::parameter::{ParamId, Parameter};
|
||||
use crate::sampler::{CompletedTrial, Sampler};
|
||||
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.
|
||||
///
|
||||
/// Each trial has a unique ID and stores the sampled parameters along with
|
||||
@@ -90,10 +26,12 @@ pub struct Trial {
|
||||
id: u64,
|
||||
/// Current state of the trial.
|
||||
state: TrialState,
|
||||
/// Sampled parameter values, keyed by parameter name.
|
||||
params: HashMap<String, ParamValue>,
|
||||
/// Parameter distributions, keyed by parameter name.
|
||||
distributions: HashMap<String, Distribution>,
|
||||
/// Sampled parameter values, keyed by parameter id.
|
||||
params: HashMap<ParamId, ParamValue>,
|
||||
/// Parameter distributions, keyed by parameter id.
|
||||
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.
|
||||
sampler: Option<Arc<dyn Sampler>>,
|
||||
/// 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("params", &self.params)
|
||||
.field("distributions", &self.distributions)
|
||||
.field("param_labels", &self.param_labels)
|
||||
.field("has_sampler", &self.sampler.is_some())
|
||||
.field("has_history", &self.history.is_some())
|
||||
.finish()
|
||||
@@ -118,7 +57,7 @@ impl Trial {
|
||||
///
|
||||
/// The trial starts in the `Running` state with no parameters sampled.
|
||||
/// 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.
|
||||
///
|
||||
@@ -141,6 +80,7 @@ impl Trial {
|
||||
state: TrialState::Running,
|
||||
params: HashMap::new(),
|
||||
distributions: HashMap::new(),
|
||||
param_labels: HashMap::new(),
|
||||
sampler: None,
|
||||
history: None,
|
||||
}
|
||||
@@ -166,6 +106,7 @@ impl Trial {
|
||||
state: TrialState::Running,
|
||||
params: HashMap::new(),
|
||||
distributions: HashMap::new(),
|
||||
param_labels: HashMap::new(),
|
||||
sampler: Some(sampler),
|
||||
history: Some(history),
|
||||
}
|
||||
@@ -202,16 +143,22 @@ impl Trial {
|
||||
|
||||
/// Returns a reference to the sampled parameters.
|
||||
#[must_use]
|
||||
pub fn params(&self) -> &HashMap<String, ParamValue> {
|
||||
pub fn params(&self) -> &HashMap<ParamId, ParamValue> {
|
||||
&self.params
|
||||
}
|
||||
|
||||
/// Returns a reference to the parameter distributions.
|
||||
#[must_use]
|
||||
pub fn distributions(&self) -> &HashMap<String, Distribution> {
|
||||
pub fn distributions(&self) -> &HashMap<ParamId, Distribution> {
|
||||
&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.
|
||||
pub(crate) fn set_complete(&mut self) {
|
||||
self.state = TrialState::Complete;
|
||||
@@ -222,730 +169,69 @@ impl Trial {
|
||||
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.
|
||||
/// If the parameter was sampled with different bounds, a `ParameterConflict` error is returned.
|
||||
/// This is the primary entry point for sampling parameters. It handles
|
||||
/// validation, caching, conflict detection, sampling, and conversion.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the parameter.
|
||||
/// * `low` - The lower bound (inclusive).
|
||||
/// * `high` - The upper bound (inclusive).
|
||||
/// * `param` - The parameter definition.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `InvalidBounds` if `low > high`.
|
||||
/// Returns `ParameterConflict` if the parameter was previously sampled with different bounds.
|
||||
/// Returns an error if:
|
||||
/// - The parameter fails validation
|
||||
/// - The parameter conflicts with a previously suggested parameter of the same id
|
||||
/// - Sampling or conversion fails
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// 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 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 x2 = trial.suggest_float("x", 0.0, 1.0).unwrap();
|
||||
/// assert_eq!(x, x2);
|
||||
/// let x = trial.suggest_param(&x_param).unwrap();
|
||||
/// let n = trial.suggest_param(&n_param).unwrap();
|
||||
/// let flag = trial.suggest_param(&flag_param).unwrap();
|
||||
/// ```
|
||||
pub fn suggest_float(&mut self, name: impl Into<String>, low: f64, high: f64) -> Result<f64> {
|
||||
if low > high {
|
||||
return Err(Error::InvalidBounds { low, high });
|
||||
}
|
||||
pub fn suggest_param<P: Parameter>(&mut self, param: &P) -> Result<P::Value> {
|
||||
param.validate()?;
|
||||
|
||||
let name = name.into();
|
||||
let distribution = FloatDistribution {
|
||||
low,
|
||||
high,
|
||||
log_scale: false,
|
||||
step: None,
|
||||
};
|
||||
let param_id = param.id();
|
||||
let distribution = param.distribution();
|
||||
|
||||
// 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()
|
||||
{
|
||||
if let Some(existing_dist) = self.distributions.get(¶m_id) {
|
||||
if *existing_dist == distribution {
|
||||
// Same distribution, return cached value
|
||||
if let Some(ParamValue::Float(value)) = self.params.get(&name) {
|
||||
return Ok(*value);
|
||||
if let Some(value) = self.params.get(¶m_id) {
|
||||
return param.cast_param_value(value);
|
||||
}
|
||||
}
|
||||
// Distribution exists but doesn't match
|
||||
return Err(Error::ParameterConflict {
|
||||
name,
|
||||
reason: "parameter was previously sampled with different bounds or type"
|
||||
name: param.label(),
|
||||
reason: "parameter was previously sampled with different configuration or type"
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Sample using the sampler
|
||||
let dist = Distribution::Float(distribution);
|
||||
let ParamValue::Float(value) = self.sample_value(&dist) else {
|
||||
return Err(Error::Internal(
|
||||
"Float distribution should return Float value",
|
||||
));
|
||||
};
|
||||
let value = self.sample_value(&distribution);
|
||||
let result = param.cast_param_value(&value)?;
|
||||
|
||||
// Store distribution and value
|
||||
self.distributions.insert(name.clone(), dist);
|
||||
self.params.insert(name, ParamValue::Float(value));
|
||||
// Store distribution, value, and label
|
||||
self.distributions.insert(param_id, distribution);
|
||||
self.params.insert(param_id, value);
|
||||
self.param_labels.insert(param_id, param.label());
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// 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())
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
+57
-21
@@ -4,6 +4,7 @@
|
||||
|
||||
#![cfg(feature = "async")]
|
||||
|
||||
use optimizer::parameter::{FloatParam, Parameter};
|
||||
use optimizer::sampler::random::RandomSampler;
|
||||
use optimizer::sampler::tpe::TpeSampler;
|
||||
use optimizer::{Direction, Error, Study};
|
||||
@@ -13,10 +14,15 @@ async fn test_optimize_async_basic() {
|
||||
let sampler = RandomSampler::with_seed(42);
|
||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
|
||||
let x_param = FloatParam::new(-10.0, 10.0);
|
||||
|
||||
study
|
||||
.optimize_async(10, |mut trial| async move {
|
||||
let x = trial.suggest_float("x", -10.0, 10.0)?;
|
||||
Ok::<_, Error>((trial, x * x))
|
||||
.optimize_async(10, move |mut trial| {
|
||||
let x_param = x_param.clone();
|
||||
async move {
|
||||
let x = x_param.suggest(&mut trial)?;
|
||||
Ok::<_, Error>((trial, x * x))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("async optimization should succeed");
|
||||
@@ -36,10 +42,15 @@ async fn test_optimize_async_with_sampler() {
|
||||
|
||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
|
||||
let x_param = FloatParam::new(-5.0, 5.0);
|
||||
|
||||
study
|
||||
.optimize_async_with_sampler(15, |mut trial| async move {
|
||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
||||
Ok::<_, Error>((trial, x * x))
|
||||
.optimize_async_with_sampler(15, move |mut trial| {
|
||||
let x_param = x_param.clone();
|
||||
async move {
|
||||
let x = x_param.suggest(&mut trial)?;
|
||||
Ok::<_, Error>((trial, x * x))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("async optimization with sampler should succeed");
|
||||
@@ -54,10 +65,15 @@ async fn test_optimize_parallel() {
|
||||
let sampler = RandomSampler::with_seed(42);
|
||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||
|
||||
let x_param = FloatParam::new(-10.0, 10.0);
|
||||
|
||||
study
|
||||
.optimize_parallel(20, 4, |mut trial| async move {
|
||||
let x = trial.suggest_float("x", -10.0, 10.0)?;
|
||||
Ok::<_, Error>((trial, x * x))
|
||||
.optimize_parallel(20, 4, move |mut trial| {
|
||||
let x_param = x_param.clone();
|
||||
async move {
|
||||
let x = x_param.suggest(&mut trial)?;
|
||||
Ok::<_, Error>((trial, x * x))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("parallel optimization should succeed");
|
||||
@@ -75,11 +91,18 @@ async fn test_optimize_parallel_with_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
|
||||
.optimize_parallel_with_sampler(15, 3, |mut trial| async move {
|
||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
||||
let y = trial.suggest_float("y", -5.0, 5.0)?;
|
||||
Ok::<_, Error>((trial, x * x + y * y))
|
||||
.optimize_parallel_with_sampler(15, 3, move |mut trial| {
|
||||
let x_param = x_param.clone();
|
||||
let y_param = y_param.clone();
|
||||
async move {
|
||||
let x = x_param.suggest(&mut trial)?;
|
||||
let y = y_param.suggest(&mut trial)?;
|
||||
Ok::<_, Error>((trial, x * x + y * y))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("parallel optimization with sampler should succeed");
|
||||
@@ -162,12 +185,15 @@ async fn test_optimize_async_partial_failures() {
|
||||
|
||||
let counter = std::sync::atomic::AtomicUsize::new(0);
|
||||
|
||||
let x_param = FloatParam::new(0.0, 10.0);
|
||||
|
||||
study
|
||||
.optimize_async(10, |mut trial| {
|
||||
.optimize_async(10, move |mut trial| {
|
||||
let count = counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
let x_param = x_param.clone();
|
||||
async move {
|
||||
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))
|
||||
} else {
|
||||
Err(Error::NoCompletedTrials) // Use as error type
|
||||
@@ -186,11 +212,16 @@ async fn test_optimize_parallel_high_concurrency() {
|
||||
let sampler = RandomSampler::with_seed(42);
|
||||
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
|
||||
study
|
||||
.optimize_parallel(5, 10, |mut trial| async move {
|
||||
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
||||
Ok::<_, Error>((trial, x))
|
||||
.optimize_parallel(5, 10, move |mut trial| {
|
||||
let x_param = x_param.clone();
|
||||
async move {
|
||||
let x = x_param.suggest(&mut trial)?;
|
||||
Ok::<_, Error>((trial, x))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("should handle high concurrency");
|
||||
@@ -203,11 +234,16 @@ async fn test_optimize_parallel_single_concurrency() {
|
||||
let sampler = RandomSampler::with_seed(42);
|
||||
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)
|
||||
study
|
||||
.optimize_parallel(10, 1, |mut trial| async move {
|
||||
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
||||
Ok::<_, Error>((trial, x))
|
||||
.optimize_parallel(10, 1, move |mut trial| {
|
||||
let x_param = x_param.clone();
|
||||
async move {
|
||||
let x = x_param.suggest(&mut trial)?;
|
||||
Ok::<_, Error>((trial, x))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.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));
|
||||
}
|
||||
+240
-322
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@
|
||||
clippy::cast_possible_truncation
|
||||
)]
|
||||
|
||||
use optimizer::parameter::{CategoricalParam, FloatParam, IntParam, Parameter};
|
||||
use optimizer::sampler::tpe::{MultivariateTpeSampler, TpeSampler};
|
||||
use optimizer::{Direction, Error, Study};
|
||||
|
||||
@@ -50,10 +51,13 @@ fn test_multivariate_tpe_rosenbrock_finds_good_solution() {
|
||||
|
||||
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
|
||||
.optimize_with_sampler(100, |trial| {
|
||||
let x = trial.suggest_float("x", -2.0, 2.0)?;
|
||||
let y = trial.suggest_float("y", -2.0, 4.0)?;
|
||||
let x = x_param.suggest(trial)?;
|
||||
let y = y_param.suggest(trial)?;
|
||||
Ok::<_, Error>(rosenbrock(x, y))
|
||||
})
|
||||
.expect("optimization should succeed");
|
||||
@@ -82,10 +86,13 @@ fn test_independent_tpe_rosenbrock() {
|
||||
|
||||
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
|
||||
.optimize_with_sampler(100, |trial| {
|
||||
let x = trial.suggest_float("x", -2.0, 2.0)?;
|
||||
let y = trial.suggest_float("y", -2.0, 4.0)?;
|
||||
let x = x_param.suggest(trial)?;
|
||||
let y = y_param.suggest(trial)?;
|
||||
Ok::<_, Error>(rosenbrock(x, y))
|
||||
})
|
||||
.expect("optimization should succeed");
|
||||
@@ -122,10 +129,13 @@ fn test_multivariate_tpe_outperforms_on_correlated_problem() {
|
||||
|
||||
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
|
||||
.optimize_with_sampler(n_trials, |trial| {
|
||||
let x = trial.suggest_float("x", -2.0, 2.0)?;
|
||||
let y = trial.suggest_float("y", -2.0, 4.0)?;
|
||||
let x = x_param.suggest(trial)?;
|
||||
let y = y_param.suggest(trial)?;
|
||||
Ok::<_, Error>(rosenbrock(x, y))
|
||||
})
|
||||
.unwrap();
|
||||
@@ -142,10 +152,13 @@ fn test_multivariate_tpe_outperforms_on_correlated_problem() {
|
||||
|
||||
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
|
||||
.optimize_with_sampler(n_trials, |trial| {
|
||||
let x = trial.suggest_float("x", -2.0, 2.0)?;
|
||||
let y = trial.suggest_float("y", -2.0, 4.0)?;
|
||||
let x = x_param.suggest(trial)?;
|
||||
let y = y_param.suggest(trial)?;
|
||||
Ok::<_, Error>(rosenbrock(x, y))
|
||||
})
|
||||
.unwrap();
|
||||
@@ -201,10 +214,13 @@ fn test_multivariate_tpe_independent_problem() {
|
||||
|
||||
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
|
||||
.optimize_with_sampler(50, |trial| {
|
||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
||||
let y = trial.suggest_float("y", -5.0, 5.0)?;
|
||||
let x = x_param.suggest(trial)?;
|
||||
let y = y_param.suggest(trial)?;
|
||||
Ok::<_, Error>(sphere(x, y))
|
||||
})
|
||||
.expect("optimization should succeed");
|
||||
@@ -230,10 +246,13 @@ fn test_independent_tpe_independent_problem() {
|
||||
|
||||
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
|
||||
.optimize_with_sampler(50, |trial| {
|
||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
||||
let y = trial.suggest_float("y", -5.0, 5.0)?;
|
||||
let x = x_param.suggest(trial)?;
|
||||
let y = y_param.suggest(trial)?;
|
||||
Ok::<_, Error>(sphere(x, y))
|
||||
})
|
||||
.expect("optimization should succeed");
|
||||
@@ -267,10 +286,13 @@ fn test_both_samplers_work_on_independent_problem() {
|
||||
|
||||
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
|
||||
.optimize_with_sampler(n_trials, |trial| {
|
||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
||||
let y = trial.suggest_float("y", -5.0, 5.0)?;
|
||||
let x = x_param.suggest(trial)?;
|
||||
let y = y_param.suggest(trial)?;
|
||||
Ok::<_, Error>(sphere(x, y))
|
||||
})
|
||||
.unwrap();
|
||||
@@ -286,10 +308,13 @@ fn test_both_samplers_work_on_independent_problem() {
|
||||
|
||||
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
|
||||
.optimize_with_sampler(n_trials, |trial| {
|
||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
||||
let y = trial.suggest_float("y", -5.0, 5.0)?;
|
||||
let x = x_param.suggest(trial)?;
|
||||
let y = y_param.suggest(trial)?;
|
||||
Ok::<_, Error>(sphere(x, y))
|
||||
})
|
||||
.unwrap();
|
||||
@@ -331,10 +356,13 @@ fn test_multivariate_tpe_with_group_decomposition() {
|
||||
|
||||
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
|
||||
.optimize_with_sampler(50, |trial| {
|
||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
||||
let y = trial.suggest_float("y", -5.0, 5.0)?;
|
||||
let x = x_param.suggest(trial)?;
|
||||
let y = y_param.suggest(trial)?;
|
||||
Ok::<_, Error>(sphere(x, y))
|
||||
})
|
||||
.expect("optimization should succeed");
|
||||
@@ -363,11 +391,15 @@ fn test_multivariate_tpe_mixed_parameter_types() {
|
||||
|
||||
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
|
||||
.optimize_with_sampler(50, |trial| {
|
||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
||||
let n = trial.suggest_int("n", 1, 10)?;
|
||||
let mode = trial.suggest_categorical("mode", &["a", "b", "c"])?;
|
||||
let x = x_param.suggest(trial)?;
|
||||
let n = n_param.suggest(trial)?;
|
||||
let mode = mode_param.suggest(trial)?;
|
||||
|
||||
// Objective depends on all parameters
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user