1use super::{
7 LearnedOptimizationConfig, LearnedOptimizer, MetaOptimizerState, OptimizationProblem,
8 TrainingTask,
9};
10use crate::error::OptimizeResult;
11use crate::result::OptimizeResults;
12use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
13use scirs2_core::random::{Rng, RngExt};
14use statrs::statistics::Statistics;
15use std::collections::{HashMap, VecDeque};
16
17#[derive(Debug, Clone)]
19pub struct LearnedHyperparameterTuner {
20 config: LearnedOptimizationConfig,
22 hyperparameter_space: HyperparameterSpace,
24 performance_database: PerformanceDatabase,
26 bayesian_optimizer: BayesianOptimizer,
28 multi_fidelity_evaluator: MultiFidelityEvaluator,
30 meta_state: MetaOptimizerState,
32 tuning_stats: HyperparameterTuningStats,
34}
35
36#[derive(Debug, Clone)]
38pub struct HyperparameterSpace {
39 continuous_params: Vec<ContinuousHyperparameter>,
41 discrete_params: Vec<DiscreteHyperparameter>,
43 categorical_params: Vec<CategoricalHyperparameter>,
45 conditional_dependencies: Vec<ConditionalDependency>,
47 parameter_bounds: HashMap<String, (f64, f64)>,
49}
50
51#[derive(Debug, Clone)]
53pub struct ContinuousHyperparameter {
54 name: String,
56 lower_bound: f64,
58 upper_bound: f64,
60 scale: ParameterScale,
62 default_value: f64,
64 importance_score: f64,
66}
67
68#[derive(Debug, Clone)]
70pub struct DiscreteHyperparameter {
71 name: String,
73 values: Vec<i64>,
75 default_value: i64,
77 importance_score: f64,
79}
80
81#[derive(Debug, Clone)]
83pub struct CategoricalHyperparameter {
84 name: String,
86 categories: Vec<String>,
88 default_category: String,
90 category_embeddings: HashMap<String, Array1<f64>>,
92 importance_score: f64,
94}
95
96#[derive(Debug, Clone)]
98pub enum ParameterScale {
99 Linear,
100 Logarithmic,
101 Exponential,
102 Sigmoid,
103}
104
105#[derive(Debug, Clone)]
107pub struct ConditionalDependency {
108 parent_param: String,
110 child_param: String,
112 condition: DependencyCondition,
114}
115
116#[derive(Debug, Clone)]
118pub enum DependencyCondition {
119 Equals(String),
120 GreaterThan(f64),
121 LessThan(f64),
122 InRange(f64, f64),
123 OneOf(Vec<String>),
124}
125
126#[derive(Debug, Clone)]
128pub struct PerformanceDatabase {
129 records: Vec<EvaluationRecord>,
131 index: HashMap<String, Vec<usize>>,
133 performance_trends: HashMap<String, PerformanceTrend>,
135 correlation_matrix: Array2<f64>,
137}
138
139#[derive(Debug, Clone)]
141pub struct EvaluationRecord {
142 config: HyperparameterConfig,
144 performance: f64,
146 cost: f64,
148 timestamp: u64,
150 problem_features: Array1<f64>,
152 fidelity: f64,
154 additional_metrics: HashMap<String, f64>,
156}
157
158#[derive(Debug, Clone)]
160pub struct HyperparameterConfig {
161 parameters: HashMap<String, ParameterValue>,
163 config_hash: u64,
165 embedding: Array1<f64>,
167}
168
169#[derive(Debug, Clone)]
171pub enum ParameterValue {
172 Continuous(f64),
173 Discrete(i64),
174 Categorical(String),
175}
176
177#[derive(Debug, Clone)]
179pub struct PerformanceTrend {
180 trend_direction: f64,
182 trend_strength: f64,
184 seasonal_patterns: Array1<f64>,
186 volatility: f64,
188}
189
190#[derive(Debug, Clone)]
192pub struct BayesianOptimizer {
193 gaussian_process: GaussianProcess,
195 acquisition_function: AcquisitionFunction,
197 optimization_strategy: OptimizationStrategy,
199 exploration_factor: f64,
201}
202
203#[derive(Debug, Clone)]
205pub struct GaussianProcess {
206 training_inputs: Array2<f64>,
208 training_outputs: Array1<f64>,
210 kernel: KernelFunction,
212 kernel_params: Array1<f64>,
214 noise_variance: f64,
216 mean_function: MeanFunction,
218}
219
220#[derive(Debug, Clone)]
222pub enum KernelFunction {
223 RBF {
224 length_scale: f64,
225 variance: f64,
226 },
227 Matern {
228 nu: f64,
229 length_scale: f64,
230 variance: f64,
231 },
232 Polynomial {
233 degree: i32,
234 variance: f64,
235 },
236 Composite {
237 kernels: Vec<KernelFunction>,
238 weights: Array1<f64>,
239 },
240}
241
242#[derive(Debug, Clone)]
244pub enum MeanFunction {
245 Zero,
246 Constant(f64),
247 Linear(Array1<f64>),
248 Quadratic(Array2<f64>),
249}
250
251#[derive(Debug, Clone)]
253pub enum AcquisitionFunction {
254 ExpectedImprovement { xi: f64 },
255 ProbabilityOfImprovement { xi: f64 },
256 UpperConfidenceBound { beta: f64 },
257 EntropySearch { num_samples: usize },
258 MultiFidelity { alpha: f64, beta: f64 },
259}
260
261#[derive(Debug, Clone)]
263pub enum OptimizationStrategy {
264 RandomSearch { num_candidates: usize },
265 GridSearch { grid_resolution: usize },
266 GradientBased { num_restarts: usize },
267 EvolutionarySearch { population_size: usize },
268 DIRECT { max_nit: usize },
269}
270
271#[derive(Debug, Clone)]
273pub struct MultiFidelityEvaluator {
274 fidelity_levels: Vec<FidelityLevel>,
276 cost_model: CostModel,
278 selection_strategy: FidelitySelectionStrategy,
280 correlation_estimator: FidelityCorrelationEstimator,
282}
283
284#[derive(Debug, Clone)]
286pub struct FidelityLevel {
287 fidelity: f64,
289 cost_multiplier: f64,
291 accuracy: f64,
293 resource_requirements: ResourceRequirements,
295}
296
297#[derive(Debug, Clone)]
299pub struct ResourceRequirements {
300 computation_time: f64,
302 memory_usage: f64,
304 cpu_cores: usize,
306 gpu_required: bool,
308}
309
310#[derive(Debug, Clone)]
312pub struct CostModel {
313 cost_network: Array2<f64>,
315 base_cost: f64,
317 scaling_factors: Array1<f64>,
319 cost_history: VecDeque<(f64, f64)>, }
322
323#[derive(Debug, Clone)]
325pub enum FidelitySelectionStrategy {
326 Static(f64),
327 Adaptive {
328 initial_fidelity: f64,
329 adaptation_rate: f64,
330 },
331 BanditBased {
332 epsilon: f64,
333 },
334 Predictive {
335 prediction_horizon: usize,
336 },
337}
338
339#[derive(Debug, Clone)]
341pub struct FidelityCorrelationEstimator {
342 correlation_matrix: Array2<f64>,
344 estimation_method: CorrelationMethod,
346 confidence_intervals: Array2<f64>,
348}
349
350#[derive(Debug, Clone)]
352pub enum CorrelationMethod {
353 Pearson,
354 Spearman,
355 Kendall,
356 MutualInformation,
357}
358
359#[derive(Debug, Clone)]
361pub struct HyperparameterTuningStats {
362 total_evaluations: usize,
364 best_performance: f64,
366 total_cost: f64,
368 convergence_rate: f64,
370 exploration_efficiency: f64,
372 multi_fidelity_savings: f64,
374}
375
376impl LearnedHyperparameterTuner {
377 pub fn new(config: LearnedOptimizationConfig) -> Self {
379 let hyperparameter_space = HyperparameterSpace::create_default_space();
380 let performance_database = PerformanceDatabase::new();
381 let bayesian_optimizer = BayesianOptimizer::new();
382 let multi_fidelity_evaluator = MultiFidelityEvaluator::new();
383 let hidden_size = config.hidden_size;
384
385 Self {
386 config,
387 hyperparameter_space,
388 performance_database,
389 bayesian_optimizer,
390 multi_fidelity_evaluator,
391 meta_state: MetaOptimizerState {
392 meta_params: Array1::zeros(hidden_size),
393 network_weights: Array2::zeros((hidden_size, hidden_size)),
394 performance_history: Vec::new(),
395 adaptation_stats: super::AdaptationStatistics::default(),
396 episode: 0,
397 },
398 tuning_stats: HyperparameterTuningStats::default(),
399 }
400 }
401
402 pub fn tune_hyperparameters<F>(
404 &mut self,
405 objective: F,
406 initial_params: &ArrayView1<f64>,
407 problem: &OptimizationProblem,
408 budget: f64,
409 ) -> OptimizeResult<HyperparameterConfig>
410 where
411 F: Fn(&ArrayView1<f64>) -> f64,
412 {
413 let mut remaining_budget = budget;
414 let mut best_config = self.get_default_config()?;
415 let mut best_performance = f64::INFINITY;
416
417 let problem_features =
419 self.extract_problem_features(&objective, initial_params, problem)?;
420
421 let promising_configs = self.get_promising_configurations(&problem_features)?;
423
424 for config in promising_configs {
426 if remaining_budget <= 0.0 {
427 break;
428 }
429
430 let (performance, cost) =
431 self.evaluate_configuration(&objective, initial_params, &config)?;
432 remaining_budget -= cost;
433
434 self.add_evaluation_record(config.clone(), performance, cost, &problem_features)?;
436
437 if performance < best_performance {
438 best_performance = performance;
439 best_config = config;
440 }
441 }
442
443 while remaining_budget > 0.0 {
445 self.update_gaussian_process()?;
447
448 let next_config = self.select_next_configuration(&problem_features)?;
450
451 let fidelity = self.select_fidelity_level(&next_config, remaining_budget)?;
453
454 let (performance, cost) = self.evaluate_configuration_with_fidelity(
456 &objective,
457 initial_params,
458 &next_config,
459 fidelity,
460 )?;
461
462 remaining_budget -= cost;
463
464 self.add_evaluation_record(next_config.clone(), performance, cost, &problem_features)?;
466
467 if performance < best_performance {
469 best_performance = performance;
470 best_config = next_config;
471 }
472
473 self.update_tuning_stats(performance, cost)?;
475
476 if self.check_convergence() {
478 break;
479 }
480 }
481
482 Ok(best_config)
483 }
484
485 fn extract_problem_features<F>(
487 &self,
488 objective: &F,
489 initial_params: &ArrayView1<f64>,
490 problem: &OptimizationProblem,
491 ) -> OptimizeResult<Array1<f64>>
492 where
493 F: Fn(&ArrayView1<f64>) -> f64,
494 {
495 let mut features = Array1::zeros(20);
496
497 features[0] = (problem.dimension as f64).ln();
499
500 let f0 = objective(initial_params);
502 features[1] = f0.abs().ln();
503
504 let h = 1e-6;
506 let mut gradient_norm = 0.0;
507 for i in 0..initial_params.len().min(10) {
508 let mut params_plus = initial_params.to_owned();
509 params_plus[i] += h;
510 let f_plus = objective(¶ms_plus.view());
511 let grad_i = (f_plus - f0) / h;
512 gradient_norm += grad_i * grad_i;
513 }
514 gradient_norm = gradient_norm.sqrt();
515 features[2] = gradient_norm.ln();
516
517 features[3] = initial_params.view().mean();
519 features[4] = initial_params.variance().sqrt();
520 features[5] = initial_params.fold(-f64::INFINITY, |a, &b| a.max(b));
521 features[6] = initial_params.fold(f64::INFINITY, |a, &b| a.min(b));
522
523 match problem.problem_class.as_str() {
525 "quadratic" => features[7] = 1.0,
526 "neural_network" => features[8] = 1.0,
527 "sparse" => features[9] = 1.0,
528 _ => features[10] = 1.0,
529 }
530
531 features[11] = (problem.max_evaluations as f64).ln();
533 features[12] = problem.target_accuracy.ln().abs();
534
535 for (i, (_, &value)) in problem.metadata.iter().enumerate() {
537 if 13 + i < features.len() {
538 features[13 + i] = value.tanh();
539 }
540 }
541
542 Ok(features)
543 }
544
545 fn get_promising_configurations(
547 &self,
548 problem_features: &Array1<f64>,
549 ) -> OptimizeResult<Vec<HyperparameterConfig>> {
550 let mut configs = Vec::new();
551 let mut similarities = Vec::new();
552
553 for record in &self.performance_database.records {
555 let similarity =
556 self.compute_problem_similarity(problem_features, &record.problem_features)?;
557 similarities.push((record, similarity));
558 }
559
560 similarities.sort_by(|a, b| {
562 let combined_score_a = a.1 * (1.0 / (1.0 + a.0.performance));
563 let combined_score_b = b.1 * (1.0 / (1.0 + b.0.performance));
564 combined_score_b
565 .partial_cmp(&combined_score_a)
566 .unwrap_or(std::cmp::Ordering::Equal)
567 });
568
569 for (record, similarity) in similarities.into_iter().take(5) {
571 configs.push(record.config.clone());
572 }
573
574 for _ in 0..3 {
576 configs.push(self.sample_random_configuration()?);
577 }
578
579 Ok(configs)
580 }
581
582 fn compute_problem_similarity(
584 &self,
585 features1: &Array1<f64>,
586 features2: &Array1<f64>,
587 ) -> OptimizeResult<f64> {
588 let dot_product = features1
590 .iter()
591 .zip(features2.iter())
592 .map(|(&a, &b)| a * b)
593 .sum::<f64>();
594
595 let norm1 = (features1.iter().map(|&x| x * x).sum::<f64>()).sqrt();
596 let norm2 = (features2.iter().map(|&x| x * x).sum::<f64>()).sqrt();
597
598 if norm1 > 0.0 && norm2 > 0.0 {
599 Ok(dot_product / (norm1 * norm2))
600 } else {
601 Ok(0.0)
602 }
603 }
604
605 fn sample_random_configuration(&self) -> OptimizeResult<HyperparameterConfig> {
607 let mut parameters = HashMap::new();
608
609 for param in &self.hyperparameter_space.continuous_params {
611 let value = match param.scale {
612 ParameterScale::Linear => {
613 param.lower_bound
614 + scirs2_core::random::rng().random::<f64>()
615 * (param.upper_bound - param.lower_bound)
616 }
617 ParameterScale::Logarithmic => {
618 let log_lower = param.lower_bound.ln();
619 let log_upper = param.upper_bound.ln();
620 (log_lower
621 + scirs2_core::random::rng().random::<f64>() * (log_upper - log_lower))
622 .exp()
623 }
624 _ => param.default_value,
625 };
626
627 parameters.insert(param.name.clone(), ParameterValue::Continuous(value));
628 }
629
630 for param in &self.hyperparameter_space.discrete_params {
632 let idx = scirs2_core::random::rng().random_range(0..param.values.len());
633 let value = param.values[idx];
634 parameters.insert(param.name.clone(), ParameterValue::Discrete(value));
635 }
636
637 for param in &self.hyperparameter_space.categorical_params {
639 let idx = scirs2_core::random::rng().random_range(0..param.categories.len());
640 let value = param.categories[idx].clone();
641 parameters.insert(param.name.clone(), ParameterValue::Categorical(value));
642 }
643
644 Ok(HyperparameterConfig::new(parameters))
645 }
646
647 fn get_default_config(&self) -> OptimizeResult<HyperparameterConfig> {
649 let mut parameters = HashMap::new();
650
651 for param in &self.hyperparameter_space.continuous_params {
652 parameters.insert(
653 param.name.clone(),
654 ParameterValue::Continuous(param.default_value),
655 );
656 }
657
658 for param in &self.hyperparameter_space.discrete_params {
659 parameters.insert(
660 param.name.clone(),
661 ParameterValue::Discrete(param.default_value),
662 );
663 }
664
665 for param in &self.hyperparameter_space.categorical_params {
666 parameters.insert(
667 param.name.clone(),
668 ParameterValue::Categorical(param.default_category.clone()),
669 );
670 }
671
672 Ok(HyperparameterConfig::new(parameters))
673 }
674
675 fn evaluate_configuration<F>(
677 &self,
678 objective: &F,
679 initial_params: &ArrayView1<f64>,
680 config: &HyperparameterConfig,
681 ) -> OptimizeResult<(f64, f64)>
682 where
683 F: Fn(&ArrayView1<f64>) -> f64,
684 {
685 self.evaluate_configuration_with_fidelity(objective, initial_params, config, 1.0)
686 }
687
688 fn evaluate_configuration_with_fidelity<F>(
690 &self,
691 objective: &F,
692 initial_params: &ArrayView1<f64>,
693 config: &HyperparameterConfig,
694 fidelity: f64,
695 ) -> OptimizeResult<(f64, f64)>
696 where
697 F: Fn(&ArrayView1<f64>) -> f64,
698 {
699 let optimizer_result =
701 self.create_optimizer_from_config(config, objective, initial_params, fidelity)?;
702
703 let base_cost = 1.0;
705 let cost = base_cost * self.multi_fidelity_evaluator.cost_model.base_cost * fidelity;
706
707 Ok((optimizer_result.fun, cost))
708 }
709
710 fn create_optimizer_from_config<F>(
712 &self,
713 config: &HyperparameterConfig,
714 objective: &F,
715 initial_params: &ArrayView1<f64>,
716 fidelity: f64,
717 ) -> OptimizeResult<OptimizeResults<f64>>
718 where
719 F: Fn(&ArrayView1<f64>) -> f64,
720 {
721 let learning_rate = match config.parameters.get("learning_rate") {
723 Some(ParameterValue::Continuous(lr)) => *lr,
724 _ => 0.01,
725 };
726
727 let max_nit = match config.parameters.get("max_nit") {
728 Some(ParameterValue::Discrete(iters)) => (*iters as f64 * fidelity) as usize,
729 _ => (100.0 * fidelity) as usize,
730 };
731
732 let mut current_params = initial_params.to_owned();
734 let mut best_value = objective(initial_params);
735 let mut best_params = current_params.clone();
736
737 for iter in 0..max_nit {
738 let h = 1e-6;
740 let f0 = objective(¤t_params.view());
741 let mut gradient = Array1::zeros(current_params.len());
742
743 for i in 0..current_params.len() {
744 let mut params_plus = current_params.clone();
745 params_plus[i] += h;
746 let f_plus = objective(¶ms_plus.view());
747 gradient[i] = (f_plus - f0) / h;
748 }
749
750 for i in 0..current_params.len() {
752 current_params[i] -= learning_rate * gradient[i];
753 }
754
755 let current_value = objective(¤t_params.view());
756 if current_value < best_value {
757 best_value = current_value;
758 best_params = current_params.clone();
759 }
760
761 if fidelity < 1.0 && iter > (max_nit / 2) {
763 break;
764 }
765 }
766
767 Ok(OptimizeResults::<f64> {
768 x: best_params,
769 fun: best_value,
770 success: true,
771 nit: max_nit,
772 message: "Hyperparameter evaluation completed".to_string(),
773 jac: None,
774 hess: None,
775 constr: None,
776 nfev: max_nit,
777 njev: 0,
778 nhev: 0,
779 maxcv: 0,
780 status: 0,
781 })
782 }
783
784 fn add_evaluation_record(
786 &mut self,
787 config: HyperparameterConfig,
788 performance: f64,
789 cost: f64,
790 problem_features: &Array1<f64>,
791 ) -> OptimizeResult<()> {
792 let record = EvaluationRecord {
793 config,
794 performance,
795 cost,
796 timestamp: std::time::SystemTime::now()
797 .duration_since(std::time::UNIX_EPOCH)
798 .unwrap_or_default()
799 .as_secs(),
800 problem_features: problem_features.clone(),
801 fidelity: 1.0,
802 additional_metrics: HashMap::new(),
803 };
804
805 self.performance_database.add_record(record);
806 Ok(())
807 }
808
809 fn update_gaussian_process(&mut self) -> OptimizeResult<()> {
811 let (inputs, outputs) = self.extract_training_data()?;
813
814 self.bayesian_optimizer
816 .gaussian_process
817 .update_training_data(inputs, outputs)?;
818
819 self.bayesian_optimizer
821 .gaussian_process
822 .optimize_hyperparameters()?;
823
824 Ok(())
825 }
826
827 fn extract_training_data(&self) -> OptimizeResult<(Array2<f64>, Array1<f64>)> {
829 let num_records = self.performance_database.records.len();
830 if num_records == 0 {
831 return Ok((Array2::zeros((0, 10)), Array1::zeros(0)));
832 }
833
834 let input_dim = self.performance_database.records[0].config.embedding.len();
835 let mut inputs = Array2::zeros((num_records, input_dim));
836 let mut outputs = Array1::zeros(num_records);
837
838 for (i, record) in self.performance_database.records.iter().enumerate() {
839 for j in 0..input_dim.min(record.config.embedding.len()) {
840 inputs[[i, j]] = record.config.embedding[j];
841 }
842 outputs[i] = record.performance;
843 }
844
845 Ok((inputs, outputs))
846 }
847
848 fn select_next_configuration(
850 &self,
851 _problem_features: &Array1<f64>,
852 ) -> OptimizeResult<HyperparameterConfig> {
853 let candidate_configs = self.generate_candidate_configurations(100)?;
855 let mut best_config = candidate_configs[0].clone();
856 let mut best_acquisition = f64::NEG_INFINITY;
857
858 for config in candidate_configs {
859 let acquisition_value = self.evaluate_acquisition_function(&config)?;
860 if acquisition_value > best_acquisition {
861 best_acquisition = acquisition_value;
862 best_config = config;
863 }
864 }
865
866 Ok(best_config)
867 }
868
869 fn generate_candidate_configurations(
871 &self,
872 num_candidates: usize,
873 ) -> OptimizeResult<Vec<HyperparameterConfig>> {
874 let mut candidates = Vec::new();
875
876 for _ in 0..num_candidates {
877 candidates.push(self.sample_random_configuration()?);
878 }
879
880 Ok(candidates)
881 }
882
883 fn evaluate_acquisition_function(&self, config: &HyperparameterConfig) -> OptimizeResult<f64> {
885 let (mean, variance) = self
887 .bayesian_optimizer
888 .gaussian_process
889 .predict(&config.embedding)?;
890
891 let acquisition_value = match &self.bayesian_optimizer.acquisition_function {
893 AcquisitionFunction::ExpectedImprovement { xi } => {
894 let best_value = self.get_best_performance();
895 let improvement = best_value - mean;
896 let std_dev = variance.sqrt();
897
898 if std_dev > 1e-8 {
899 let z = (improvement + xi) / std_dev;
900 improvement * self.normal_cdf(z) + std_dev * self.normal_pdf(z)
901 } else {
902 0.0
903 }
904 }
905 AcquisitionFunction::UpperConfidenceBound { beta } => mean + beta * variance.sqrt(),
906 _ => mean + variance.sqrt(), };
908
909 Ok(acquisition_value)
910 }
911
912 fn normal_cdf(&self, x: f64) -> f64 {
914 let sqrt_pi_over_2 = (std::f64::consts::PI / 2.0).sqrt();
917 0.5 * (1.0 + (sqrt_pi_over_2 * x / 2.0_f64.sqrt()).tanh())
918 }
919
920 fn normal_pdf(&self, x: f64) -> f64 {
922 (1.0 / (2.0 * std::f64::consts::PI).sqrt()) * (-0.5 * x * x).exp()
923 }
924
925 fn get_best_performance(&self) -> f64 {
927 self.performance_database
928 .records
929 .iter()
930 .map(|r| r.performance)
931 .fold(f64::INFINITY, |a, b| a.min(b))
932 }
933
934 fn select_fidelity_level(
936 &self,
937 _config: &HyperparameterConfig,
938 remaining_budget: f64,
939 ) -> OptimizeResult<f64> {
940 match &self.multi_fidelity_evaluator.selection_strategy {
941 FidelitySelectionStrategy::Static(fidelity) => Ok(*fidelity),
942 FidelitySelectionStrategy::Adaptive {
943 initial_fidelity,
944 adaptation_rate: _,
945 } => {
946 let budget_ratio = remaining_budget / self.tuning_stats.total_cost.max(1.0);
948 Ok(initial_fidelity * budget_ratio.max(0.1).min(1.0))
949 }
950 _ => Ok(0.5), }
952 }
953
954 fn update_tuning_stats(&mut self, performance: f64, cost: f64) -> OptimizeResult<()> {
956 self.tuning_stats.total_evaluations += 1;
957 self.tuning_stats.total_cost += cost;
958
959 if performance < self.tuning_stats.best_performance {
960 self.tuning_stats.best_performance = performance;
961 }
962
963 if self.tuning_stats.total_evaluations > 1 {
965 let improvement_rate = (self.tuning_stats.best_performance - performance)
966 / self.tuning_stats.total_evaluations as f64;
967 self.tuning_stats.convergence_rate = improvement_rate.max(0.0);
968 }
969
970 Ok(())
971 }
972
973 fn check_convergence(&self) -> bool {
975 self.tuning_stats.total_evaluations > 50 && self.tuning_stats.convergence_rate < 1e-6
977 }
978
979 pub fn get_tuning_stats(&self) -> &HyperparameterTuningStats {
981 &self.tuning_stats
982 }
983}
984
985impl HyperparameterSpace {
986 pub fn create_default_space() -> Self {
988 let continuous_params = vec![
989 ContinuousHyperparameter {
990 name: "learning_rate".to_string(),
991 lower_bound: 1e-5,
992 upper_bound: 1.0,
993 scale: ParameterScale::Logarithmic,
994 default_value: 0.01,
995 importance_score: 1.0,
996 },
997 ContinuousHyperparameter {
998 name: "momentum".to_string(),
999 lower_bound: 0.0,
1000 upper_bound: 0.99,
1001 scale: ParameterScale::Linear,
1002 default_value: 0.9,
1003 importance_score: 0.8,
1004 },
1005 ContinuousHyperparameter {
1006 name: "weight_decay".to_string(),
1007 lower_bound: 1e-8,
1008 upper_bound: 1e-2,
1009 scale: ParameterScale::Logarithmic,
1010 default_value: 1e-4,
1011 importance_score: 0.6,
1012 },
1013 ];
1014
1015 let discrete_params = vec![
1016 DiscreteHyperparameter {
1017 name: "max_nit".to_string(),
1018 values: vec![10, 50, 100, 500, 1000],
1019 default_value: 100,
1020 importance_score: 0.9,
1021 },
1022 DiscreteHyperparameter {
1023 name: "batch_size".to_string(),
1024 values: vec![1, 8, 16, 32, 64, 128],
1025 default_value: 32,
1026 importance_score: 0.7,
1027 },
1028 ];
1029
1030 let categorical_params = vec![CategoricalHyperparameter {
1031 name: "optimizer_type".to_string(),
1032 categories: vec!["sgd".to_string(), "adam".to_string(), "lbfgs".to_string()],
1033 default_category: "adam".to_string(),
1034 category_embeddings: HashMap::new(),
1035 importance_score: 1.0,
1036 }];
1037
1038 Self {
1039 continuous_params,
1040 discrete_params,
1041 categorical_params,
1042 conditional_dependencies: Vec::new(),
1043 parameter_bounds: HashMap::new(),
1044 }
1045 }
1046}
1047
1048impl HyperparameterConfig {
1049 pub fn new(parameters: HashMap<String, ParameterValue>) -> Self {
1051 let config_hash = Self::compute_hash(¶meters);
1052 let embedding = Self::compute_embedding(¶meters);
1053
1054 Self {
1055 parameters,
1056 config_hash,
1057 embedding,
1058 }
1059 }
1060
1061 fn compute_hash(parameters: &HashMap<String, ParameterValue>) -> u64 {
1063 let mut hash = 0u64;
1065 for (key, value) in parameters {
1066 hash ^= Self::hash_string(key);
1067 hash ^= Self::hash_parameter_value(value);
1068 }
1069 hash
1070 }
1071
1072 fn hash_string(s: &str) -> u64 {
1074 s.bytes().fold(0u64, |hash, byte| {
1076 hash.wrapping_mul(31).wrapping_add(byte as u64)
1077 })
1078 }
1079
1080 fn hash_parameter_value(value: &ParameterValue) -> u64 {
1082 match value {
1083 ParameterValue::Continuous(v) => v.to_bits(),
1084 ParameterValue::Discrete(v) => *v as u64,
1085 ParameterValue::Categorical(s) => Self::hash_string(s),
1086 }
1087 }
1088
1089 fn compute_embedding(parameters: &HashMap<String, ParameterValue>) -> Array1<f64> {
1091 let mut embedding = Array1::zeros(32); let mut idx = 0;
1094 for value in parameters.values() {
1095 if idx >= embedding.len() {
1096 break;
1097 }
1098
1099 match value {
1100 ParameterValue::Continuous(v) => {
1101 embedding[idx] = v.tanh();
1102 idx += 1;
1103 }
1104 ParameterValue::Discrete(v) => {
1105 embedding[idx] = (*v as f64 / 100.0).tanh();
1106 idx += 1;
1107 }
1108 ParameterValue::Categorical(s) => {
1109 let hash_val = Self::hash_string(s) as f64 / u64::MAX as f64;
1111 embedding[idx] = (hash_val * 2.0 - 1.0).tanh();
1112 idx += 1;
1113 }
1114 }
1115 }
1116
1117 embedding
1118 }
1119}
1120
1121impl Default for PerformanceDatabase {
1122 fn default() -> Self {
1123 Self::new()
1124 }
1125}
1126
1127impl PerformanceDatabase {
1128 pub fn new() -> Self {
1130 Self {
1131 records: Vec::new(),
1132 index: HashMap::new(),
1133 performance_trends: HashMap::new(),
1134 correlation_matrix: Array2::zeros((0, 0)),
1135 }
1136 }
1137
1138 pub fn add_record(&mut self, record: EvaluationRecord) {
1140 self.records.push(record);
1141
1142 let record_idx = self.records.len() - 1;
1144 self.index
1145 .entry("all".to_string())
1146 .or_default()
1147 .push(record_idx);
1148 }
1149}
1150
1151impl Default for BayesianOptimizer {
1152 fn default() -> Self {
1153 Self::new()
1154 }
1155}
1156
1157impl BayesianOptimizer {
1158 pub fn new() -> Self {
1160 Self {
1161 gaussian_process: GaussianProcess::new(),
1162 acquisition_function: AcquisitionFunction::ExpectedImprovement { xi: 0.01 },
1163 optimization_strategy: OptimizationStrategy::RandomSearch {
1164 num_candidates: 100,
1165 },
1166 exploration_factor: 0.1,
1167 }
1168 }
1169}
1170
1171impl Default for GaussianProcess {
1172 fn default() -> Self {
1173 Self::new()
1174 }
1175}
1176
1177impl GaussianProcess {
1178 pub fn new() -> Self {
1180 Self {
1181 training_inputs: Array2::zeros((0, 0)),
1182 training_outputs: Array1::zeros(0),
1183 kernel: KernelFunction::RBF {
1184 length_scale: 1.0,
1185 variance: 1.0,
1186 },
1187 kernel_params: Array1::from(vec![1.0, 1.0]),
1188 noise_variance: 0.1,
1189 mean_function: MeanFunction::Zero,
1190 }
1191 }
1192
1193 pub fn update_training_data(
1195 &mut self,
1196 inputs: Array2<f64>,
1197 outputs: Array1<f64>,
1198 ) -> OptimizeResult<()> {
1199 self.training_inputs = inputs;
1200 self.training_outputs = outputs;
1201 Ok(())
1202 }
1203
1204 pub fn optimize_hyperparameters(&mut self) -> OptimizeResult<()> {
1206 Ok(())
1209 }
1210
1211 pub fn predict(&self, input: &Array1<f64>) -> OptimizeResult<(f64, f64)> {
1213 if self.training_inputs.is_empty() {
1214 return Ok((0.0, 1.0));
1215 }
1216
1217 let mean = 0.0; let variance = 1.0; Ok((mean, variance))
1222 }
1223}
1224
1225impl Default for MultiFidelityEvaluator {
1226 fn default() -> Self {
1227 Self::new()
1228 }
1229}
1230
1231impl MultiFidelityEvaluator {
1232 pub fn new() -> Self {
1234 let fidelity_levels = vec![
1235 FidelityLevel {
1236 fidelity: 0.1,
1237 cost_multiplier: 0.1,
1238 accuracy: 0.7,
1239 resource_requirements: ResourceRequirements {
1240 computation_time: 1.0,
1241 memory_usage: 0.5,
1242 cpu_cores: 1,
1243 gpu_required: false,
1244 },
1245 },
1246 FidelityLevel {
1247 fidelity: 0.5,
1248 cost_multiplier: 0.5,
1249 accuracy: 0.9,
1250 resource_requirements: ResourceRequirements {
1251 computation_time: 5.0,
1252 memory_usage: 1.0,
1253 cpu_cores: 2,
1254 gpu_required: false,
1255 },
1256 },
1257 FidelityLevel {
1258 fidelity: 1.0,
1259 cost_multiplier: 1.0,
1260 accuracy: 1.0,
1261 resource_requirements: ResourceRequirements {
1262 computation_time: 10.0,
1263 memory_usage: 2.0,
1264 cpu_cores: 4,
1265 gpu_required: true,
1266 },
1267 },
1268 ];
1269
1270 Self {
1271 fidelity_levels,
1272 cost_model: CostModel::new(),
1273 selection_strategy: FidelitySelectionStrategy::Adaptive {
1274 initial_fidelity: 0.5,
1275 adaptation_rate: 0.1,
1276 },
1277 correlation_estimator: FidelityCorrelationEstimator::new(),
1278 }
1279 }
1280}
1281
1282impl Default for CostModel {
1283 fn default() -> Self {
1284 Self::new()
1285 }
1286}
1287
1288impl CostModel {
1289 pub fn new() -> Self {
1291 Self {
1292 cost_network: Array2::from_shape_fn((1, 10), |_| {
1293 (scirs2_core::random::rng().random::<f64>() - 0.5) * 0.1
1294 }),
1295 base_cost: 1.0,
1296 scaling_factors: Array1::ones(5),
1297 cost_history: VecDeque::with_capacity(1000),
1298 }
1299 }
1300}
1301
1302impl Default for FidelityCorrelationEstimator {
1303 fn default() -> Self {
1304 Self::new()
1305 }
1306}
1307
1308impl FidelityCorrelationEstimator {
1309 pub fn new() -> Self {
1311 Self {
1312 correlation_matrix: Array2::eye(3),
1313 estimation_method: CorrelationMethod::Pearson,
1314 confidence_intervals: Array2::zeros((3, 2)),
1315 }
1316 }
1317}
1318
1319impl Default for HyperparameterTuningStats {
1320 fn default() -> Self {
1321 Self {
1322 total_evaluations: 0,
1323 best_performance: f64::INFINITY,
1324 total_cost: 0.0,
1325 convergence_rate: 0.0,
1326 exploration_efficiency: 0.0,
1327 multi_fidelity_savings: 0.0,
1328 }
1329 }
1330}
1331
1332impl LearnedOptimizer for LearnedHyperparameterTuner {
1333 fn meta_train(&mut self, training_tasks: &[TrainingTask]) -> OptimizeResult<()> {
1334 for task in training_tasks {
1335 let training_objective = |x: &ArrayView1<f64>| x.iter().map(|&xi| xi * xi).sum::<f64>();
1337
1338 let initial_params = Array1::zeros(task.problem.dimension);
1339
1340 let _best_config = self.tune_hyperparameters(
1342 training_objective,
1343 &initial_params.view(),
1344 &task.problem,
1345 10.0,
1346 )?;
1347 }
1348
1349 Ok(())
1350 }
1351
1352 fn adapt_to_problem(
1353 &mut self,
1354 problem: &OptimizationProblem,
1355 initial_params: &ArrayView1<f64>,
1356 ) -> OptimizeResult<()> {
1357 let simple_objective = |_x: &ArrayView1<f64>| 0.0;
1359 let _problem_features =
1360 self.extract_problem_features(&simple_objective, initial_params, problem)?;
1361
1362 Ok(())
1363 }
1364
1365 fn optimize<F>(
1366 &mut self,
1367 objective: F,
1368 initial_params: &ArrayView1<f64>,
1369 ) -> OptimizeResult<OptimizeResults<f64>>
1370 where
1371 F: Fn(&ArrayView1<f64>) -> f64,
1372 {
1373 let default_problem = OptimizationProblem {
1375 name: "hyperparameter_tuning".to_string(),
1376 dimension: initial_params.len(),
1377 problem_class: "general".to_string(),
1378 metadata: HashMap::new(),
1379 max_evaluations: 1000,
1380 target_accuracy: 1e-6,
1381 };
1382
1383 let best_config =
1385 self.tune_hyperparameters(&objective, initial_params, &default_problem, 20.0)?;
1386
1387 self.create_optimizer_from_config(&best_config, &objective, initial_params, 1.0)
1389 }
1390
1391 fn get_state(&self) -> &MetaOptimizerState {
1392 &self.meta_state
1393 }
1394
1395 fn reset(&mut self) {
1396 self.performance_database = PerformanceDatabase::new();
1397 self.tuning_stats = HyperparameterTuningStats::default();
1398 }
1399}
1400
1401#[allow(dead_code)]
1403pub fn hyperparameter_tuning_optimize<F>(
1404 objective: F,
1405 initial_params: &ArrayView1<f64>,
1406 config: Option<LearnedOptimizationConfig>,
1407) -> super::OptimizeResult<OptimizeResults<f64>>
1408where
1409 F: Fn(&ArrayView1<f64>) -> f64,
1410{
1411 let config = config.unwrap_or_default();
1412 let mut tuner = LearnedHyperparameterTuner::new(config);
1413 tuner.optimize(objective, initial_params)
1414}
1415
1416#[cfg(test)]
1417mod tests {
1418 use super::*;
1419
1420 #[test]
1421 fn test_hyperparameter_tuner_creation() {
1422 let config = LearnedOptimizationConfig::default();
1423 let tuner = LearnedHyperparameterTuner::new(config);
1424
1425 assert_eq!(tuner.tuning_stats.total_evaluations, 0);
1426 assert!(!tuner.hyperparameter_space.continuous_params.is_empty());
1427 }
1428
1429 #[test]
1430 fn test_hyperparameter_space() {
1431 let space = HyperparameterSpace::create_default_space();
1432
1433 assert!(!space.continuous_params.is_empty());
1434 assert!(!space.discrete_params.is_empty());
1435 assert!(!space.categorical_params.is_empty());
1436 }
1437
1438 #[test]
1439 fn test_hyperparameter_config() {
1440 let mut parameters = HashMap::new();
1441 parameters.insert(
1442 "learning_rate".to_string(),
1443 ParameterValue::Continuous(0.01),
1444 );
1445 parameters.insert("max_nit".to_string(), ParameterValue::Discrete(100));
1446 parameters.insert(
1447 "optimizer_type".to_string(),
1448 ParameterValue::Categorical("adam".to_string()),
1449 );
1450
1451 let config = HyperparameterConfig::new(parameters);
1452
1453 assert!(config.config_hash != 0);
1454 assert_eq!(config.embedding.len(), 32);
1455 assert!(config.embedding.iter().all(|&x| x.is_finite()));
1456 }
1457
1458 #[test]
1459 fn test_problem_similarity() {
1460 let config = LearnedOptimizationConfig::default();
1461 let tuner = LearnedHyperparameterTuner::new(config);
1462
1463 let features1 = Array1::from(vec![1.0, 0.0, 0.0]);
1464 let features2 = Array1::from(vec![0.0, 1.0, 0.0]);
1465 let features3 = Array1::from(vec![1.0, 0.1, 0.1]);
1466
1467 let sim1 = tuner
1468 .compute_problem_similarity(&features1, &features2)
1469 .expect("Operation failed");
1470 let sim2 = tuner
1471 .compute_problem_similarity(&features1, &features3)
1472 .expect("Operation failed");
1473
1474 assert!(sim2 > sim1); }
1476
1477 #[test]
1478 fn test_gaussian_process() {
1479 let mut gp = GaussianProcess::new();
1480
1481 let inputs = Array2::from_shape_fn((3, 2), |_| scirs2_core::random::rng().random::<f64>());
1482 let outputs = Array1::from(vec![1.0, 2.0, 3.0]);
1483
1484 gp.update_training_data(inputs, outputs)
1485 .expect("Operation failed");
1486
1487 let test_input = Array1::from(vec![0.5, 0.5]);
1488 let (mean, variance) = gp.predict(&test_input).expect("Operation failed");
1489
1490 assert!(mean.is_finite());
1491 assert!(variance >= 0.0);
1492 }
1493
1494 #[test]
1495 fn test_hyperparameter_tuning_optimization() {
1496 let objective = |x: &ArrayView1<f64>| x[0].powi(2) + x[1].powi(2);
1497 let initial = Array1::from(vec![2.0, 2.0]);
1498
1499 let config = LearnedOptimizationConfig {
1500 hidden_size: 32,
1501 ..Default::default()
1502 };
1503
1504 let result = hyperparameter_tuning_optimize(objective, &initial.view(), Some(config))
1505 .expect("Operation failed");
1506
1507 assert!(result.fun >= 0.0);
1508 assert_eq!(result.x.len(), 2);
1509 assert!(result.success);
1510 }
1511}