use scirs2_core::numeric::Float;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
#[derive(Debug)]
pub struct HyperparameterOptimizer<T: Float> {
search_space: HyperparameterSpace<T>,
strategy: OptimizationStrategy,
evaluation_history: Vec<HyperparameterEvaluation<T>>,
best_config: Option<HyperparameterConfiguration<T>>,
state: OptimizerState<T>,
}
#[derive(Debug, Clone)]
pub struct HyperparameterSpace<T: Float> {
parameters: HashMap<String, ParameterRange<T>>,
dependencies: Vec<ParameterDependency>,
constraints: Vec<HyperparameterConstraint<T>>,
categorical_parameters: HashMap<String, Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParameterRange<T: Float> {
pub name: String,
pub min_value: T,
pub max_value: T,
pub distribution: DistributionType,
pub log_scale: bool,
pub discrete_values: Option<Vec<T>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DistributionType {
Uniform,
Normal,
LogNormal,
Beta,
Exponential,
}
#[derive(Debug, Clone)]
pub struct ParameterDependency {
pub dependent: String,
pub parent: String,
pub dependency_type: DependencyType,
pub condition: DependencyCondition,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DependencyType {
Conditional,
Derived,
RangeDependent,
}
#[derive(Debug, Clone)]
pub enum DependencyCondition {
Equals(String),
GreaterThan(f64),
LessThan(f64),
InRange(f64, f64),
InSet(Vec<String>),
}
#[derive(Debug, Clone)]
pub struct HyperparameterConstraint<T: Float> {
pub name: String,
pub parameters: Vec<String>,
pub constraint_type: ConstraintType<T>,
pub penalty: T,
}
#[derive(Debug, Clone)]
pub enum ConstraintType<T: Float> {
Linear { coefficients: Vec<T>, bound: T },
NonLinear { function_name: String },
MutualExclusion,
Ordering,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HyperparameterConfiguration<T: Float> {
pub id: String,
pub parameters: HashMap<String, T>,
pub categorical_parameters: HashMap<String, String>,
pub score: Option<T>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct HyperparameterEvaluation<T: Float> {
pub configuration: HyperparameterConfiguration<T>,
pub metrics: EvaluationMetrics<T>,
pub duration_seconds: f64,
pub status: EvaluationStatus,
pub error_message: Option<String>,
}
#[derive(Debug, Clone)]
pub struct EvaluationMetrics<T: Float> {
pub primary_objective: T,
pub secondary_objectives: HashMap<String, T>,
pub validation_score: Option<T>,
pub training_time: f64,
pub memory_usage: f64,
pub converged: bool,
pub convergence_iterations: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EvaluationStatus {
Success,
Failed,
Terminated,
Running,
Timeout,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OptimizationStrategy {
Random,
Grid,
Bayesian,
Evolutionary,
ParticleSwarm,
TPE,
SuccessiveHalving,
Hyperband,
BOHB,
}
#[derive(Debug)]
pub struct OptimizerState<T: Float> {
pub iteration: u32,
pub num_evaluations: u32,
pub best_score: Option<T>,
pub population: Vec<HyperparameterConfiguration<T>>,
pub surrogate_model: Option<SurrogateModel<T>>,
pub early_stopping: EarlyStoppingState<T>,
}
#[derive(Debug)]
pub struct SurrogateModel<T: Float> {
pub model_type: SurrogateModelType,
pub training_data: Vec<(Vec<T>, T)>,
pub hyperparameters: HashMap<String, f64>,
pub acquisition_function: AcquisitionFunction,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SurrogateModelType {
GaussianProcess,
RandomForest,
NeuralNetwork,
Polynomial,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AcquisitionFunction {
ExpectedImprovement,
UpperConfidenceBound,
ProbabilityOfImprovement,
EntropySearch,
}
#[derive(Debug)]
pub struct EarlyStoppingState<T: Float> {
pub enabled: bool,
pub patience: u32,
pub patience_counter: u32,
pub min_improvement: T,
pub best_score_for_stopping: Option<T>,
}
impl<T: Float + Default + Clone + Send + Sync> HyperparameterOptimizer<T> {
pub fn new(search_space: HyperparameterSpace<T>, strategy: OptimizationStrategy) -> Self {
Self {
search_space,
strategy,
evaluation_history: Vec::new(),
best_config: None,
state: OptimizerState {
iteration: 0,
num_evaluations: 0,
best_score: None,
population: Vec::new(),
surrogate_model: None,
early_stopping: EarlyStoppingState {
enabled: false,
patience: 50,
patience_counter: 0,
min_improvement: T::from(0.001).unwrap_or_else(T::zero),
best_score_for_stopping: None,
},
},
}
}
pub fn suggest_configuration(&mut self) -> HyperparameterConfiguration<T> {
match self.strategy {
OptimizationStrategy::Random => self.random_search(),
OptimizationStrategy::Grid => self.grid_search(),
OptimizationStrategy::Bayesian => self.bayesian_optimization(),
OptimizationStrategy::Evolutionary => self.evolutionary_search(),
OptimizationStrategy::TPE => self.tpe_search(),
_ => self.random_search(), }
}
pub fn record_evaluation(&mut self, evaluation: HyperparameterEvaluation<T>) {
self.state.num_evaluations += 1;
if let Some(score) = evaluation.configuration.score {
if self.best_config.is_none()
|| self
.best_config
.as_ref()
.expect("unwrap failed")
.score
.unwrap_or_else(T::zero)
< score
{
self.best_config = Some(evaluation.configuration.clone());
self.state.best_score = Some(score);
}
}
if self.state.early_stopping.enabled {
self.update_early_stopping(&evaluation);
}
self.evaluation_history.push(evaluation);
self.update_strategy_state();
}
pub fn should_stop_early(&self) -> bool {
if !self.state.early_stopping.enabled {
return false;
}
self.state.early_stopping.patience_counter >= self.state.early_stopping.patience
}
pub fn get_best_configuration(&self) -> Option<&HyperparameterConfiguration<T>> {
self.best_config.as_ref()
}
pub fn get_statistics(&self) -> OptimizationStatistics<T> {
let scores: Vec<T> = self
.evaluation_history
.iter()
.filter_map(|eval| eval.configuration.score)
.collect();
let mean_score = if scores.is_empty() {
T::zero()
} else {
scores.iter().fold(T::zero(), |acc, &x| acc + x)
/ T::from(scores.len()).unwrap_or_else(|| T::one())
};
OptimizationStatistics {
num_evaluations: self.state.num_evaluations,
best_score: self.state.best_score,
mean_score: Some(mean_score),
num_successful_evaluations: self
.evaluation_history
.iter()
.filter(|eval| matches!(eval.status, EvaluationStatus::Success))
.count() as u32,
convergence_iteration: self.get_convergence_iteration(),
}
}
fn random_search(&self) -> HyperparameterConfiguration<T> {
use scirs2_core::random::Rng;
let mut rng = scirs2_core::random::Random::default();
let mut parameters = HashMap::new();
let mut categorical_parameters = HashMap::new();
for (name, range) in &self.search_space.parameters {
let value = match range.distribution {
DistributionType::Uniform => {
if range.log_scale {
let log_min = range.min_value.ln();
let log_max = range.max_value.ln();
let log_val = rng.gen_range(
log_min.to_f64().expect("unwrap failed")
..=log_max.to_f64().expect("unwrap failed"),
);
T::from(log_val.exp()).unwrap_or_else(T::zero)
} else {
let min_f64 = range.min_value.to_f64().expect("unwrap failed");
let max_f64 = range.max_value.to_f64().expect("unwrap failed");
T::from(rng.gen_range(min_f64..=max_f64)).unwrap_or_else(T::zero)
}
}
_ => {
let min_f64 = range.min_value.to_f64().expect("unwrap failed");
let max_f64 = range.max_value.to_f64().expect("unwrap failed");
T::from(rng.gen_range(min_f64..=max_f64)).unwrap_or_else(T::zero)
}
};
parameters.insert(name.clone(), value);
}
for (name, categories) in &self.search_space.categorical_parameters {
let idx = rng.gen_range(0..categories.len());
categorical_parameters.insert(name.clone(), categories[idx].clone());
}
HyperparameterConfiguration {
id: format!("config_{}", self.state.num_evaluations),
parameters,
categorical_parameters,
score: None,
metadata: HashMap::new(),
}
}
fn grid_search(&self) -> HyperparameterConfiguration<T> {
self.random_search()
}
fn bayesian_optimization(&self) -> HyperparameterConfiguration<T> {
self.random_search()
}
fn evolutionary_search(&mut self) -> HyperparameterConfiguration<T> {
if self.state.population.is_empty() {
for _ in 0..20 {
self.state.population.push(self.random_search());
}
}
if let Some(best) = self.best_config.as_ref() {
self.mutate_configuration(best)
} else {
self.random_search()
}
}
fn tpe_search(&self) -> HyperparameterConfiguration<T> {
self.random_search()
}
fn mutate_configuration(
&self,
config: &HyperparameterConfiguration<T>,
) -> HyperparameterConfiguration<T> {
use scirs2_core::random::Rng;
let mut rng = scirs2_core::random::Random::default();
let mut new_config = config.clone();
new_config.id = format!("mutated_{}", self.state.num_evaluations);
new_config.score = None;
if !new_config.parameters.is_empty() {
let param_names: Vec<_> = new_config.parameters.keys().cloned().collect();
let param_to_mutate = ¶m_names[rng.gen_range(0..param_names.len())];
if let Some(range) = self.search_space.parameters.get(param_to_mutate) {
let current_value = new_config.parameters[param_to_mutate];
let range_size = range.max_value - range.min_value;
let mutation_strength = range_size * T::from(0.1).unwrap_or_else(T::zero);
let mutation =
T::from(rng.gen_range(-0.1..=0.1)).unwrap_or_else(T::zero) * mutation_strength;
let new_value = (current_value + mutation)
.max(range.min_value)
.min(range.max_value);
new_config
.parameters
.insert(param_to_mutate.clone(), new_value);
}
}
new_config
}
fn update_early_stopping(&mut self, evaluation: &HyperparameterEvaluation<T>) {
if let Some(score) = evaluation.configuration.score {
if let Some(best_score) = self.state.early_stopping.best_score_for_stopping {
if score > best_score + self.state.early_stopping.min_improvement {
self.state.early_stopping.best_score_for_stopping = Some(score);
self.state.early_stopping.patience_counter = 0;
} else {
self.state.early_stopping.patience_counter += 1;
}
} else {
self.state.early_stopping.best_score_for_stopping = Some(score);
}
}
}
fn update_strategy_state(&mut self) {
self.state.iteration += 1;
}
fn get_convergence_iteration(&self) -> Option<u32> {
let window_size = 10;
if self.evaluation_history.len() < window_size * 2 {
return None;
}
let recent_scores: Vec<T> = self
.evaluation_history
.iter()
.rev()
.take(window_size)
.filter_map(|eval| eval.configuration.score)
.collect();
let older_scores: Vec<T> = self
.evaluation_history
.iter()
.rev()
.skip(window_size)
.take(window_size)
.filter_map(|eval| eval.configuration.score)
.collect();
if recent_scores.len() == window_size && older_scores.len() == window_size {
let recent_mean = recent_scores.iter().fold(T::zero(), |acc, &x| acc + x)
/ T::from(window_size).unwrap_or_else(|| T::one());
let older_mean = older_scores.iter().fold(T::zero(), |acc, &x| acc + x)
/ T::from(window_size).unwrap_or_else(|| T::one());
let improvement = recent_mean - older_mean;
let threshold = T::from(0.001).unwrap_or_else(T::zero);
if improvement < threshold {
return Some(self.state.iteration - window_size as u32);
}
}
None
}
}
#[derive(Debug, Clone)]
pub struct OptimizationStatistics<T: Float> {
pub num_evaluations: u32,
pub best_score: Option<T>,
pub mean_score: Option<T>,
pub num_successful_evaluations: u32,
pub convergence_iteration: Option<u32>,
}
impl<T: Float> HyperparameterSpace<T> {
pub fn new() -> Self {
Self {
parameters: HashMap::new(),
dependencies: Vec::new(),
constraints: Vec::new(),
categorical_parameters: HashMap::new(),
}
}
pub fn add_parameter(&mut self, name: String, range: ParameterRange<T>) {
self.parameters.insert(name, range);
}
pub fn add_categorical_parameter(&mut self, name: String, categories: Vec<String>) {
self.categorical_parameters.insert(name, categories);
}
pub fn add_dependency(&mut self, dependency: ParameterDependency) {
self.dependencies.push(dependency);
}
pub fn add_constraint(&mut self, constraint: HyperparameterConstraint<T>) {
self.constraints.push(constraint);
}
pub fn validate_configuration(&self, config: &HyperparameterConfiguration<T>) -> bool {
for (name, value) in &config.parameters {
if let Some(range) = self.parameters.get(name) {
if *value < range.min_value || *value > range.max_value {
return false;
}
}
}
for (name, value) in &config.categorical_parameters {
if let Some(categories) = self.categorical_parameters.get(name) {
if !categories.contains(value) {
return false;
}
}
}
for constraint in &self.constraints {
if !self.check_constraint(constraint, config) {
return false;
}
}
true
}
fn check_constraint(
&self,
constraint: &HyperparameterConstraint<T>,
config: &HyperparameterConfiguration<T>,
) -> bool {
match &constraint.constraint_type {
ConstraintType::Linear {
coefficients,
bound,
} => {
let mut sum = T::zero();
for (i, param_name) in constraint.parameters.iter().enumerate() {
if let Some(value) = config.parameters.get(param_name) {
sum = sum + coefficients[i] * *value;
}
}
sum <= *bound
}
ConstraintType::MutualExclusion => {
let active_count = constraint
.parameters
.iter()
.filter(|param| config.parameters.contains_key(*param))
.count();
active_count <= 1
}
ConstraintType::Ordering => {
let mut values = Vec::new();
for param_name in &constraint.parameters {
if let Some(value) = config.parameters.get(param_name) {
values.push(*value);
}
}
for i in 1..values.len() {
if values[i - 1] > values[i] {
return false;
}
}
true
}
ConstraintType::NonLinear { .. } => {
true
}
}
}
}
impl<T: Float> Default for HyperparameterSpace<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Float + fmt::Debug> fmt::Display for HyperparameterConfiguration<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Configuration: {}", self.id)?;
for (name, value) in &self.parameters {
writeln!(f, " {}: {:?}", name, value)?;
}
for (name, value) in &self.categorical_parameters {
writeln!(f, " {}: {}", name, value)?;
}
if let Some(score) = self.score {
writeln!(f, " Score: {:?}", score)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hyperparameter_space_creation() {
let mut space: HyperparameterSpace<f64> = HyperparameterSpace::new();
space.add_parameter(
"learning_rate".to_string(),
ParameterRange {
name: "learning_rate".to_string(),
min_value: 1e-5,
max_value: 1e-1,
distribution: DistributionType::Uniform,
log_scale: true,
discrete_values: None,
},
);
space.add_categorical_parameter(
"optimizer".to_string(),
vec!["adam".to_string(), "sgd".to_string()],
);
assert!(space.parameters.contains_key("learning_rate"));
assert!(space.categorical_parameters.contains_key("optimizer"));
}
#[test]
fn test_random_search() {
let mut space: HyperparameterSpace<f64> = HyperparameterSpace::new();
space.add_parameter(
"learning_rate".to_string(),
ParameterRange {
name: "learning_rate".to_string(),
min_value: 1e-5,
max_value: 1e-1,
distribution: DistributionType::Uniform,
log_scale: false,
discrete_values: None,
},
);
let mut optimizer = HyperparameterOptimizer::new(space, OptimizationStrategy::Random);
let config = optimizer.suggest_configuration();
assert!(config.parameters.contains_key("learning_rate"));
assert!(config.parameters["learning_rate"] >= 1e-5);
assert!(config.parameters["learning_rate"] <= 1e-1);
}
#[test]
fn test_evaluation_recording() {
let space: HyperparameterSpace<f64> = HyperparameterSpace::new();
let mut optimizer = HyperparameterOptimizer::new(space, OptimizationStrategy::Random);
let config = HyperparameterConfiguration {
id: "test_config".to_string(),
parameters: HashMap::new(),
categorical_parameters: HashMap::new(),
score: Some(0.85),
metadata: HashMap::new(),
};
let evaluation = HyperparameterEvaluation {
configuration: config,
metrics: EvaluationMetrics {
primary_objective: 0.85,
secondary_objectives: HashMap::new(),
validation_score: Some(0.82),
training_time: 120.0,
memory_usage: 1024.0,
converged: true,
convergence_iterations: Some(100),
},
duration_seconds: 125.0,
status: EvaluationStatus::Success,
error_message: None,
};
optimizer.record_evaluation(evaluation);
assert_eq!(optimizer.state.num_evaluations, 1);
assert!(optimizer.best_config.is_some());
assert_eq!(
optimizer.best_config.as_ref().expect("unwrap failed").score,
Some(0.85)
);
}
}