Skip to main content

scirs2_optimize/learned_optimizers/
learned_hyperparameter_tuner.rs

1//! Learned Hyperparameter Tuner
2//!
3//! Implementation of machine learning-based hyperparameter tuning that learns
4//! optimal hyperparameter configurations across different optimization problems.
5
6use 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/// Learned hyperparameter tuner with adaptive configuration
18#[derive(Debug, Clone)]
19pub struct LearnedHyperparameterTuner {
20    /// Configuration
21    config: LearnedOptimizationConfig,
22    /// Hyperparameter space
23    hyperparameter_space: HyperparameterSpace,
24    /// Performance database
25    performance_database: PerformanceDatabase,
26    /// Bayesian optimizer for hyperparameter search
27    bayesian_optimizer: BayesianOptimizer,
28    /// Multi-fidelity evaluator
29    multi_fidelity_evaluator: MultiFidelityEvaluator,
30    /// Meta-optimizer state
31    meta_state: MetaOptimizerState,
32    /// Tuning statistics
33    tuning_stats: HyperparameterTuningStats,
34}
35
36/// Hyperparameter space definition
37#[derive(Debug, Clone)]
38pub struct HyperparameterSpace {
39    /// Continuous hyperparameters
40    continuous_params: Vec<ContinuousHyperparameter>,
41    /// Discrete hyperparameters
42    discrete_params: Vec<DiscreteHyperparameter>,
43    /// Categorical hyperparameters
44    categorical_params: Vec<CategoricalHyperparameter>,
45    /// Conditional dependencies
46    conditional_dependencies: Vec<ConditionalDependency>,
47    /// Parameter bounds
48    parameter_bounds: HashMap<String, (f64, f64)>,
49}
50
51/// Continuous hyperparameter
52#[derive(Debug, Clone)]
53pub struct ContinuousHyperparameter {
54    /// Parameter name
55    name: String,
56    /// Lower bound
57    lower_bound: f64,
58    /// Upper bound
59    upper_bound: f64,
60    /// Scale (linear, log, etc.)
61    scale: ParameterScale,
62    /// Default value
63    default_value: f64,
64    /// Importance score
65    importance_score: f64,
66}
67
68/// Discrete hyperparameter
69#[derive(Debug, Clone)]
70pub struct DiscreteHyperparameter {
71    /// Parameter name
72    name: String,
73    /// Possible values
74    values: Vec<i64>,
75    /// Default value
76    default_value: i64,
77    /// Importance score
78    importance_score: f64,
79}
80
81/// Categorical hyperparameter
82#[derive(Debug, Clone)]
83pub struct CategoricalHyperparameter {
84    /// Parameter name
85    name: String,
86    /// Possible categories
87    categories: Vec<String>,
88    /// Default category
89    default_category: String,
90    /// Category embeddings
91    category_embeddings: HashMap<String, Array1<f64>>,
92    /// Importance score
93    importance_score: f64,
94}
95
96/// Parameter scale types
97#[derive(Debug, Clone)]
98pub enum ParameterScale {
99    Linear,
100    Logarithmic,
101    Exponential,
102    Sigmoid,
103}
104
105/// Conditional dependency between parameters
106#[derive(Debug, Clone)]
107pub struct ConditionalDependency {
108    /// Parent parameter
109    parent_param: String,
110    /// Child parameter
111    child_param: String,
112    /// Condition
113    condition: DependencyCondition,
114}
115
116/// Dependency condition types
117#[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/// Performance database for storing evaluation results
127#[derive(Debug, Clone)]
128pub struct PerformanceDatabase {
129    /// Evaluation records
130    records: Vec<EvaluationRecord>,
131    /// Indexing for fast retrieval
132    index: HashMap<String, Vec<usize>>,
133    /// Performance trends
134    performance_trends: HashMap<String, PerformanceTrend>,
135    /// Correlation matrix
136    correlation_matrix: Array2<f64>,
137}
138
139/// Evaluation record
140#[derive(Debug, Clone)]
141pub struct EvaluationRecord {
142    /// Hyperparameter configuration
143    config: HyperparameterConfig,
144    /// Performance metric
145    performance: f64,
146    /// Evaluation cost
147    cost: f64,
148    /// Timestamp
149    timestamp: u64,
150    /// Problem characteristics
151    problem_features: Array1<f64>,
152    /// Fidelity level
153    fidelity: f64,
154    /// Additional metrics
155    additional_metrics: HashMap<String, f64>,
156}
157
158/// Hyperparameter configuration
159#[derive(Debug, Clone)]
160pub struct HyperparameterConfig {
161    /// Parameter values
162    parameters: HashMap<String, ParameterValue>,
163    /// Configuration hash
164    config_hash: u64,
165    /// Configuration embedding
166    embedding: Array1<f64>,
167}
168
169/// Parameter value types
170#[derive(Debug, Clone)]
171pub enum ParameterValue {
172    Continuous(f64),
173    Discrete(i64),
174    Categorical(String),
175}
176
177/// Performance trend analysis
178#[derive(Debug, Clone)]
179pub struct PerformanceTrend {
180    /// Trend direction
181    trend_direction: f64,
182    /// Trend strength
183    trend_strength: f64,
184    /// Seasonal patterns
185    seasonal_patterns: Array1<f64>,
186    /// Volatility measure
187    volatility: f64,
188}
189
190/// Bayesian optimizer for hyperparameter search
191#[derive(Debug, Clone)]
192pub struct BayesianOptimizer {
193    /// Gaussian process surrogate model
194    gaussian_process: GaussianProcess,
195    /// Acquisition function
196    acquisition_function: AcquisitionFunction,
197    /// Optimization strategy
198    optimization_strategy: OptimizationStrategy,
199    /// Exploration-exploitation balance
200    exploration_factor: f64,
201}
202
203/// Gaussian process surrogate model
204#[derive(Debug, Clone)]
205pub struct GaussianProcess {
206    /// Training inputs
207    training_inputs: Array2<f64>,
208    /// Training outputs
209    training_outputs: Array1<f64>,
210    /// Kernel function
211    kernel: KernelFunction,
212    /// Kernel hyperparameters
213    kernel_params: Array1<f64>,
214    /// Noise variance
215    noise_variance: f64,
216    /// Mean function
217    mean_function: MeanFunction,
218}
219
220/// Kernel function types
221#[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/// Mean function for GP
243#[derive(Debug, Clone)]
244pub enum MeanFunction {
245    Zero,
246    Constant(f64),
247    Linear(Array1<f64>),
248    Quadratic(Array2<f64>),
249}
250
251/// Acquisition function types
252#[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/// Optimization strategy for acquisition function
262#[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/// Multi-fidelity evaluator
272#[derive(Debug, Clone)]
273pub struct MultiFidelityEvaluator {
274    /// Available fidelity levels
275    fidelity_levels: Vec<FidelityLevel>,
276    /// Cost model
277    cost_model: CostModel,
278    /// Fidelity selection strategy
279    selection_strategy: FidelitySelectionStrategy,
280    /// Correlation estimator
281    correlation_estimator: FidelityCorrelationEstimator,
282}
283
284/// Fidelity level definition
285#[derive(Debug, Clone)]
286pub struct FidelityLevel {
287    /// Fidelity value (0.0 to 1.0)
288    fidelity: f64,
289    /// Cost multiplier
290    cost_multiplier: f64,
291    /// Accuracy estimate
292    accuracy: f64,
293    /// Resource requirements
294    resource_requirements: ResourceRequirements,
295}
296
297/// Resource requirements for evaluation
298#[derive(Debug, Clone)]
299pub struct ResourceRequirements {
300    /// Computational time
301    computation_time: f64,
302    /// Memory usage
303    memory_usage: f64,
304    /// CPU cores
305    cpu_cores: usize,
306    /// GPU requirements
307    gpu_required: bool,
308}
309
310/// Cost model for evaluations
311#[derive(Debug, Clone)]
312pub struct CostModel {
313    /// Cost prediction network
314    cost_network: Array2<f64>,
315    /// Base cost parameters
316    base_cost: f64,
317    /// Scaling factors
318    scaling_factors: Array1<f64>,
319    /// Historical cost data
320    cost_history: VecDeque<(f64, f64)>, // (fidelity, cost)
321}
322
323/// Fidelity selection strategy
324#[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/// Correlation estimator between fidelities
340#[derive(Debug, Clone)]
341pub struct FidelityCorrelationEstimator {
342    /// Correlation matrix
343    correlation_matrix: Array2<f64>,
344    /// Estimation method
345    estimation_method: CorrelationMethod,
346    /// Confidence intervals
347    confidence_intervals: Array2<f64>,
348}
349
350/// Correlation estimation methods
351#[derive(Debug, Clone)]
352pub enum CorrelationMethod {
353    Pearson,
354    Spearman,
355    Kendall,
356    MutualInformation,
357}
358
359/// Hyperparameter tuning statistics
360#[derive(Debug, Clone)]
361pub struct HyperparameterTuningStats {
362    /// Total evaluations performed
363    total_evaluations: usize,
364    /// Best performance found
365    best_performance: f64,
366    /// Total cost spent
367    total_cost: f64,
368    /// Convergence rate
369    convergence_rate: f64,
370    /// Exploration efficiency
371    exploration_efficiency: f64,
372    /// Multi-fidelity savings
373    multi_fidelity_savings: f64,
374}
375
376impl LearnedHyperparameterTuner {
377    /// Create new learned hyperparameter tuner
378    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    /// Tune hyperparameters for optimization problem
403    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        // Extract problem features
418        let problem_features =
419            self.extract_problem_features(&objective, initial_params, problem)?;
420
421        // Initialize with promising configurations from database
422        let promising_configs = self.get_promising_configurations(&problem_features)?;
423
424        // Evaluate promising configurations
425        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            // Update database
435            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        // Bayesian optimization loop
444        while remaining_budget > 0.0 {
445            // Update Gaussian process
446            self.update_gaussian_process()?;
447
448            // Select next configuration to evaluate
449            let next_config = self.select_next_configuration(&problem_features)?;
450
451            // Select fidelity level
452            let fidelity = self.select_fidelity_level(&next_config, remaining_budget)?;
453
454            // Evaluate configuration
455            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            // Update database
465            self.add_evaluation_record(next_config.clone(), performance, cost, &problem_features)?;
466
467            // Update best configuration
468            if performance < best_performance {
469                best_performance = performance;
470                best_config = next_config;
471            }
472
473            // Update statistics
474            self.update_tuning_stats(performance, cost)?;
475
476            // Check convergence
477            if self.check_convergence() {
478                break;
479            }
480        }
481
482        Ok(best_config)
483    }
484
485    /// Extract problem features for configuration selection
486    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        // Problem dimension
498        features[0] = (problem.dimension as f64).ln();
499
500        // Objective landscape features
501        let f0 = objective(initial_params);
502        features[1] = f0.abs().ln();
503
504        // Gradient features
505        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(&params_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        // Parameter statistics
518        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        // Problem class encoding
524        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        // Budget and accuracy requirements
532        features[11] = (problem.max_evaluations as f64).ln();
533        features[12] = problem.target_accuracy.ln().abs();
534
535        // Add metadata features
536        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    /// Get promising configurations from database
546    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        // Find similar problems in database
554        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        // Sort by similarity and performance
561        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        // Select top configurations
570        for (record, similarity) in similarities.into_iter().take(5) {
571            configs.push(record.config.clone());
572        }
573
574        // Add some random configurations for exploration
575        for _ in 0..3 {
576            configs.push(self.sample_random_configuration()?);
577        }
578
579        Ok(configs)
580    }
581
582    /// Compute similarity between problem features
583    fn compute_problem_similarity(
584        &self,
585        features1: &Array1<f64>,
586        features2: &Array1<f64>,
587    ) -> OptimizeResult<f64> {
588        // Cosine similarity
589        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    /// Sample random configuration from hyperparameter space
606    fn sample_random_configuration(&self) -> OptimizeResult<HyperparameterConfig> {
607        let mut parameters = HashMap::new();
608
609        // Sample continuous parameters
610        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        // Sample discrete parameters
631        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        // Sample categorical parameters
638        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    /// Get default configuration
648    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    /// Evaluate configuration
676    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    /// Evaluate configuration with specified fidelity
689    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        // Create optimizer with specified configuration
700        let optimizer_result =
701            self.create_optimizer_from_config(config, objective, initial_params, fidelity)?;
702
703        // Compute cost based on fidelity
704        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    /// Create optimizer from configuration
711    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        // Extract optimization parameters from config
722        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        // Simple optimization with extracted parameters
733        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            // Compute gradient
739            let h = 1e-6;
740            let f0 = objective(&current_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(&params_plus.view());
747                gradient[i] = (f_plus - f0) / h;
748            }
749
750            // Update parameters
751            for i in 0..current_params.len() {
752                current_params[i] -= learning_rate * gradient[i];
753            }
754
755            let current_value = objective(&current_params.view());
756            if current_value < best_value {
757                best_value = current_value;
758                best_params = current_params.clone();
759            }
760
761            // Early stopping for low fidelity
762            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    /// Add evaluation record to database
785    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    /// Update Gaussian process with new data
810    fn update_gaussian_process(&mut self) -> OptimizeResult<()> {
811        // Extract training data from database
812        let (inputs, outputs) = self.extract_training_data()?;
813
814        // Update GP
815        self.bayesian_optimizer
816            .gaussian_process
817            .update_training_data(inputs, outputs)?;
818
819        // Optimize hyperparameters
820        self.bayesian_optimizer
821            .gaussian_process
822            .optimize_hyperparameters()?;
823
824        Ok(())
825    }
826
827    /// Extract training data from database
828    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    /// Select next configuration to evaluate
849    fn select_next_configuration(
850        &self,
851        _problem_features: &Array1<f64>,
852    ) -> OptimizeResult<HyperparameterConfig> {
853        // Use acquisition function to select next point
854        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    /// Generate candidate configurations
870    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    /// Evaluate acquisition function
884    fn evaluate_acquisition_function(&self, config: &HyperparameterConfig) -> OptimizeResult<f64> {
885        // Predict mean and variance using GP
886        let (mean, variance) = self
887            .bayesian_optimizer
888            .gaussian_process
889            .predict(&config.embedding)?;
890
891        // Compute acquisition function value
892        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(), // Default UCB
907        };
908
909        Ok(acquisition_value)
910    }
911
912    /// Normal CDF approximation
913    fn normal_cdf(&self, x: f64) -> f64 {
914        // Approximation of error function for Gaussian CDF
915        // Using tanh approximation: erf(x) ≈ tanh(√(π/2) * x)
916        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    /// Normal PDF
921    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    /// Get best performance from database
926    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    /// Select fidelity level for evaluation
935    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                // Simple adaptive strategy based on remaining _budget
947                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), // Default medium fidelity
951        }
952    }
953
954    /// Update tuning statistics
955    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        // Update convergence rate (simplified)
964        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    /// Check convergence criteria
974    fn check_convergence(&self) -> bool {
975        // Simple convergence check
976        self.tuning_stats.total_evaluations > 50 && self.tuning_stats.convergence_rate < 1e-6
977    }
978
979    /// Get tuning statistics
980    pub fn get_tuning_stats(&self) -> &HyperparameterTuningStats {
981        &self.tuning_stats
982    }
983}
984
985impl HyperparameterSpace {
986    /// Create default hyperparameter space for optimization
987    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    /// Create new hyperparameter configuration
1050    pub fn new(parameters: HashMap<String, ParameterValue>) -> Self {
1051        let config_hash = Self::compute_hash(&parameters);
1052        let embedding = Self::compute_embedding(&parameters);
1053
1054        Self {
1055            parameters,
1056            config_hash,
1057            embedding,
1058        }
1059    }
1060
1061    /// Compute hash for configuration
1062    fn compute_hash(parameters: &HashMap<String, ParameterValue>) -> u64 {
1063        // Simplified hash computation
1064        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    /// Hash string
1073    fn hash_string(s: &str) -> u64 {
1074        // Simple string hash
1075        s.bytes().fold(0u64, |hash, byte| {
1076            hash.wrapping_mul(31).wrapping_add(byte as u64)
1077        })
1078    }
1079
1080    /// Hash parameter value
1081    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    /// Compute embedding for configuration
1090    fn compute_embedding(parameters: &HashMap<String, ParameterValue>) -> Array1<f64> {
1091        let mut embedding = Array1::zeros(32); // Fixed embedding size
1092
1093        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                    // Simple categorical encoding
1110                    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    /// Create new performance database
1129    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    /// Add evaluation record
1139    pub fn add_record(&mut self, record: EvaluationRecord) {
1140        self.records.push(record);
1141
1142        // Update index (simplified)
1143        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    /// Create new Bayesian optimizer
1159    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    /// Create new Gaussian process
1179    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    /// Update training data
1194    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    /// Optimize hyperparameters
1205    pub fn optimize_hyperparameters(&mut self) -> OptimizeResult<()> {
1206        // Simplified hyperparameter optimization
1207        // In practice, would use marginal likelihood optimization
1208        Ok(())
1209    }
1210
1211    /// Predict mean and variance
1212    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        // Simplified GP prediction
1218        let mean = 0.0; // Would compute proper posterior mean
1219        let variance = 1.0; // Would compute proper posterior variance
1220
1221        Ok((mean, variance))
1222    }
1223}
1224
1225impl Default for MultiFidelityEvaluator {
1226    fn default() -> Self {
1227        Self::new()
1228    }
1229}
1230
1231impl MultiFidelityEvaluator {
1232    /// Create new multi-fidelity evaluator
1233    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    /// Create new cost model
1290    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    /// Create new correlation estimator
1310    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            // Create simple objective for training
1336            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            // Tune hyperparameters for this task
1341            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        // Extract problem features for future configuration selection
1358        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        // Create default problem for hyperparameter tuning
1374        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        // Tune hyperparameters
1384        let best_config =
1385            self.tune_hyperparameters(&objective, initial_params, &default_problem, 20.0)?;
1386
1387        // Use best configuration for final optimization
1388        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/// Convenience function for learned hyperparameter tuning
1402#[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); // features3 should be more similar to features1
1475    }
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}