sklears_kernel_approximation/
parameter_learning.rs

1//! Automatic kernel parameter learning and optimization
2//!
3//! This module provides automated methods for learning optimal kernel parameters
4//! using various optimization strategies like grid search and Bayesian optimization.
5
6use crate::{Nystroem, RBFSampler};
7use rayon::prelude::*;
8
9use scirs2_core::ndarray::{s, Array1, Array2};
10use scirs2_core::random::rngs::StdRng;
11use scirs2_core::random::Rng;
12use scirs2_core::random::{thread_rng, SeedableRng};
13use scirs2_linalg::compat::ArrayLinalgExt;
14use sklears_core::{
15    error::{Result, SklearsError},
16    traits::{Fit, Transform},
17};
18
19/// Parameter search strategies
20#[derive(Debug, Clone)]
21/// SearchStrategy
22pub enum SearchStrategy {
23    /// Grid search over parameter space
24    GridSearch {
25        /// Number of points per parameter
26        n_points: usize,
27    },
28    /// Random search
29    RandomSearch {
30        /// Number of random samples
31        n_samples: usize,
32    },
33    /// Bayesian optimization (simplified)
34    BayesianOptimization {
35        /// Number of initial random samples
36        n_initial: usize,
37        /// Number of optimization iterations
38        n_iterations: usize,
39        /// Acquisition function parameter
40        exploration_factor: f64,
41    },
42    /// Coordinate descent
43    CoordinateDescent {
44        /// Maximum iterations
45        max_iterations: usize,
46        /// Convergence tolerance
47        tolerance: f64,
48    },
49}
50
51/// Parameter bounds for optimization
52#[derive(Debug, Clone)]
53/// ParameterBounds
54pub struct ParameterBounds {
55    /// Gamma parameter bounds
56    pub gamma_bounds: (f64, f64),
57    /// Number of components bounds
58    pub n_components_bounds: (usize, usize),
59    /// Degree bounds (for polynomial kernels)
60    pub degree_bounds: Option<(i32, i32)>,
61    /// Coef0 bounds (for polynomial kernels)
62    pub coef0_bounds: Option<(f64, f64)>,
63}
64
65impl Default for ParameterBounds {
66    fn default() -> Self {
67        Self {
68            gamma_bounds: (1e-6, 1e2),
69            n_components_bounds: (10, 1000),
70            degree_bounds: Some((2, 5)),
71            coef0_bounds: Some((-1.0, 1.0)),
72        }
73    }
74}
75
76/// Objective functions for parameter optimization
77#[derive(Debug, Clone)]
78/// ObjectiveFunction
79pub enum ObjectiveFunction {
80    /// Kernel alignment score
81    KernelAlignment,
82    /// Cross-validation error
83    CrossValidationError { n_folds: usize },
84    /// Approximation quality (Frobenius norm)
85    ApproximationQuality,
86    /// Effective rank
87    EffectiveRank,
88    /// Custom objective function
89    Custom,
90}
91
92/// Configuration for parameter learning
93#[derive(Debug, Clone)]
94/// ParameterLearningConfig
95pub struct ParameterLearningConfig {
96    /// Search strategy
97    pub search_strategy: SearchStrategy,
98    /// Parameter bounds
99    pub parameter_bounds: ParameterBounds,
100    /// Objective function
101    pub objective_function: ObjectiveFunction,
102    /// Validation fraction for evaluation
103    pub validation_fraction: f64,
104    /// Random seed for reproducibility
105    pub random_seed: Option<u64>,
106    /// Parallel processing
107    pub n_jobs: usize,
108    /// Verbose output
109    pub verbose: bool,
110}
111
112impl Default for ParameterLearningConfig {
113    fn default() -> Self {
114        Self {
115            search_strategy: SearchStrategy::GridSearch { n_points: 10 },
116            parameter_bounds: ParameterBounds::default(),
117            objective_function: ObjectiveFunction::KernelAlignment,
118            validation_fraction: 0.2,
119            random_seed: None,
120            n_jobs: num_cpus::get(),
121            verbose: false,
122        }
123    }
124}
125
126/// Parameter set for optimization
127#[derive(Debug, Clone)]
128/// ParameterSet
129pub struct ParameterSet {
130    /// Gamma parameter
131    pub gamma: f64,
132    /// Number of components
133    pub n_components: usize,
134    /// Degree (for polynomial kernels)
135    pub degree: Option<i32>,
136    /// Coef0 (for polynomial kernels)
137    pub coef0: Option<f64>,
138}
139
140impl PartialEq for ParameterSet {
141    fn eq(&self, other: &Self) -> bool {
142        (self.gamma - other.gamma).abs() < f64::EPSILON
143            && self.n_components == other.n_components
144            && self.degree == other.degree
145            && match (self.coef0, other.coef0) {
146                (Some(a), Some(b)) => (a - b).abs() < f64::EPSILON,
147                (None, None) => true,
148                _ => false,
149            }
150    }
151}
152
153impl Eq for ParameterSet {}
154
155impl std::hash::Hash for ParameterSet {
156    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
157        // Convert f64 to bits for hashing
158        self.gamma.to_bits().hash(state);
159        self.n_components.hash(state);
160        self.degree.hash(state);
161        if let Some(coef0) = self.coef0 {
162            coef0.to_bits().hash(state);
163        }
164    }
165}
166
167/// Results from parameter optimization
168#[derive(Debug, Clone)]
169/// OptimizationResult
170pub struct OptimizationResult {
171    /// Best parameter set found
172    pub best_parameters: ParameterSet,
173    /// Best objective value
174    pub best_score: f64,
175    /// All evaluated parameter sets and scores
176    pub parameter_history: Vec<(ParameterSet, f64)>,
177    /// Convergence information
178    pub converged: bool,
179    /// Number of function evaluations
180    pub n_evaluations: usize,
181}
182
183/// Automatic parameter learning for RBF kernels
184pub struct ParameterLearner {
185    config: ParameterLearningConfig,
186}
187
188impl ParameterLearner {
189    /// Create a new parameter learner
190    pub fn new(config: ParameterLearningConfig) -> Self {
191        Self { config }
192    }
193
194    /// Optimize RBF sampler parameters
195    pub fn optimize_rbf_parameters(
196        &self,
197        x: &Array2<f64>,
198        y: Option<&Array1<f64>>,
199    ) -> Result<OptimizationResult> {
200        match &self.config.search_strategy {
201            SearchStrategy::GridSearch { n_points } => self.grid_search_rbf(x, y, *n_points),
202            SearchStrategy::RandomSearch { n_samples } => self.random_search_rbf(x, y, *n_samples),
203            SearchStrategy::BayesianOptimization {
204                n_initial,
205                n_iterations,
206                exploration_factor,
207            } => {
208                self.bayesian_optimization_rbf(x, y, *n_initial, *n_iterations, *exploration_factor)
209            }
210            SearchStrategy::CoordinateDescent {
211                max_iterations,
212                tolerance,
213            } => self.coordinate_descent_rbf(x, y, *max_iterations, *tolerance),
214        }
215    }
216
217    /// Optimize Nyström parameters
218    pub fn optimize_nystroem_parameters(
219        &self,
220        x: &Array2<f64>,
221        y: Option<&Array1<f64>>,
222    ) -> Result<OptimizationResult> {
223        // Similar to RBF optimization but for Nyström method
224        match &self.config.search_strategy {
225            SearchStrategy::GridSearch { n_points } => self.grid_search_nystroem(x, y, *n_points),
226            SearchStrategy::RandomSearch { n_samples } => {
227                self.random_search_nystroem(x, y, *n_samples)
228            }
229            _ => {
230                // Fallback to grid search for unsupported strategies
231                self.grid_search_nystroem(x, y, 10)
232            }
233        }
234    }
235
236    fn grid_search_rbf(
237        &self,
238        x: &Array2<f64>,
239        y: Option<&Array1<f64>>,
240        n_points: usize,
241    ) -> Result<OptimizationResult> {
242        let gamma_values = self.create_parameter_grid(
243            self.config.parameter_bounds.gamma_bounds.0,
244            self.config.parameter_bounds.gamma_bounds.1,
245            n_points,
246            true, // log scale
247        );
248
249        let n_components_values = self
250            .create_parameter_grid(
251                self.config.parameter_bounds.n_components_bounds.0 as f64,
252                self.config.parameter_bounds.n_components_bounds.1 as f64,
253                n_points,
254                false, // linear scale
255            )
256            .into_iter()
257            .map(|x| x as usize)
258            .collect::<Vec<_>>();
259
260        let mut parameter_history = Vec::new();
261        let mut best_score = f64::NEG_INFINITY;
262        let mut best_parameters = ParameterSet {
263            gamma: gamma_values[0],
264            n_components: n_components_values[0],
265            degree: None,
266            coef0: None,
267        };
268
269        // Generate all parameter combinations
270        let parameter_combinations: Vec<_> = gamma_values
271            .iter()
272            .flat_map(|&gamma| {
273                n_components_values
274                    .iter()
275                    .map(move |&n_components| ParameterSet {
276                        gamma,
277                        n_components,
278                        degree: None,
279                        coef0: None,
280                    })
281            })
282            .collect();
283
284        if self.config.verbose {
285            println!(
286                "Evaluating {} parameter combinations",
287                parameter_combinations.len()
288            );
289        }
290
291        // Parallel evaluation of parameter combinations
292        let scores: Result<Vec<_>> = parameter_combinations
293            .par_iter()
294            .map(|params| {
295                let score = self.evaluate_rbf_parameters(x, y, params)?;
296                Ok((params.clone(), score))
297            })
298            .collect();
299
300        let scores = scores?;
301
302        for (params, score) in scores {
303            parameter_history.push((params.clone(), score));
304
305            if score > best_score {
306                best_score = score;
307                best_parameters = params;
308            }
309        }
310
311        let n_evaluations = parameter_history.len();
312        Ok(OptimizationResult {
313            best_parameters,
314            best_score,
315            parameter_history,
316            converged: true,
317            n_evaluations,
318        })
319    }
320
321    fn random_search_rbf(
322        &self,
323        x: &Array2<f64>,
324        y: Option<&Array1<f64>>,
325        n_samples: usize,
326    ) -> Result<OptimizationResult> {
327        let mut rng = if let Some(seed) = self.config.random_seed {
328            StdRng::seed_from_u64(seed)
329        } else {
330            StdRng::from_seed(thread_rng().gen())
331        };
332
333        let mut parameter_history = Vec::new();
334        let mut best_score = f64::NEG_INFINITY;
335        let mut best_parameters = ParameterSet {
336            gamma: 1.0,
337            n_components: 100,
338            degree: None,
339            coef0: None,
340        };
341
342        for _ in 0..n_samples {
343            // Sample random parameters
344            let gamma = self.sample_log_uniform(
345                &mut rng,
346                self.config.parameter_bounds.gamma_bounds.0,
347                self.config.parameter_bounds.gamma_bounds.1,
348            );
349
350            let n_components = rng.gen_range(
351                self.config.parameter_bounds.n_components_bounds.0
352                    ..=self.config.parameter_bounds.n_components_bounds.1,
353            );
354
355            let params = ParameterSet {
356                gamma,
357                n_components,
358                degree: None,
359                coef0: None,
360            };
361
362            let score = self.evaluate_rbf_parameters(x, y, &params)?;
363            parameter_history.push((params.clone(), score));
364
365            if score > best_score {
366                best_score = score;
367                best_parameters = params;
368            }
369        }
370
371        Ok(OptimizationResult {
372            best_parameters,
373            best_score,
374            parameter_history,
375            converged: true,
376            n_evaluations: n_samples,
377        })
378    }
379
380    fn bayesian_optimization_rbf(
381        &self,
382        x: &Array2<f64>,
383        y: Option<&Array1<f64>>,
384        n_initial: usize,
385        n_iterations: usize,
386        exploration_factor: f64,
387    ) -> Result<OptimizationResult> {
388        // Simplified Bayesian optimization using Gaussian process surrogate
389        let mut rng = if let Some(seed) = self.config.random_seed {
390            StdRng::seed_from_u64(seed)
391        } else {
392            StdRng::from_seed(thread_rng().gen())
393        };
394
395        let mut parameter_history = Vec::new();
396        let mut best_score = f64::NEG_INFINITY;
397        let mut best_parameters = ParameterSet {
398            gamma: 1.0,
399            n_components: 100,
400            degree: None,
401            coef0: None,
402        };
403
404        // Initial random sampling
405        for _ in 0..n_initial {
406            let gamma = self.sample_log_uniform(
407                &mut rng,
408                self.config.parameter_bounds.gamma_bounds.0,
409                self.config.parameter_bounds.gamma_bounds.1,
410            );
411
412            let n_components = rng.gen_range(
413                self.config.parameter_bounds.n_components_bounds.0
414                    ..=self.config.parameter_bounds.n_components_bounds.1,
415            );
416
417            let params = ParameterSet {
418                gamma,
419                n_components,
420                degree: None,
421                coef0: None,
422            };
423
424            let score = self.evaluate_rbf_parameters(x, y, &params)?;
425            parameter_history.push((params.clone(), score));
426
427            if score > best_score {
428                best_score = score;
429                best_parameters = params;
430            }
431        }
432
433        // Bayesian optimization iterations
434        for iteration in 0..n_iterations {
435            // Simplified acquisition function (Upper Confidence Bound)
436            let next_params =
437                self.acquisition_function_rbf(&parameter_history, exploration_factor, &mut rng);
438
439            let score = self.evaluate_rbf_parameters(x, y, &next_params)?;
440            parameter_history.push((next_params.clone(), score));
441
442            if score > best_score {
443                best_score = score;
444                best_parameters = next_params;
445            }
446
447            if self.config.verbose {
448                println!(
449                    "Iteration {}: Best score = {:.6}",
450                    iteration + 1,
451                    best_score
452                );
453            }
454        }
455
456        Ok(OptimizationResult {
457            best_parameters,
458            best_score,
459            parameter_history,
460            converged: true,
461            n_evaluations: n_initial + n_iterations,
462        })
463    }
464
465    fn coordinate_descent_rbf(
466        &self,
467        x: &Array2<f64>,
468        y: Option<&Array1<f64>>,
469        max_iterations: usize,
470        tolerance: f64,
471    ) -> Result<OptimizationResult> {
472        let mut current_params = ParameterSet {
473            gamma: (self.config.parameter_bounds.gamma_bounds.0
474                * self.config.parameter_bounds.gamma_bounds.1)
475                .sqrt(),
476            n_components: (self.config.parameter_bounds.n_components_bounds.0
477                + self.config.parameter_bounds.n_components_bounds.1)
478                / 2,
479            degree: None,
480            coef0: None,
481        };
482
483        let mut current_score = self.evaluate_rbf_parameters(x, y, &current_params)?;
484        let mut parameter_history = vec![(current_params.clone(), current_score)];
485        let mut converged = false;
486
487        for iteration in 0..max_iterations {
488            let prev_score = current_score;
489
490            // Optimize gamma
491            current_params = self.optimize_gamma_coordinate(x, y, &current_params)?;
492            current_score = self.evaluate_rbf_parameters(x, y, &current_params)?;
493            parameter_history.push((current_params.clone(), current_score));
494
495            // Optimize n_components
496            current_params = self.optimize_n_components_coordinate(x, y, &current_params)?;
497            current_score = self.evaluate_rbf_parameters(x, y, &current_params)?;
498            parameter_history.push((current_params.clone(), current_score));
499
500            // Check convergence
501            if (current_score - prev_score).abs() < tolerance {
502                converged = true;
503                if self.config.verbose {
504                    println!(
505                        "Converged at iteration {} with score {:.6}",
506                        iteration + 1,
507                        current_score
508                    );
509                }
510                break;
511            }
512
513            if self.config.verbose {
514                println!("Iteration {}: Score = {:.6}", iteration + 1, current_score);
515            }
516        }
517
518        let n_evaluations = parameter_history.len();
519        Ok(OptimizationResult {
520            best_parameters: current_params,
521            best_score: current_score,
522            parameter_history,
523            converged,
524            n_evaluations,
525        })
526    }
527
528    fn grid_search_nystroem(
529        &self,
530        x: &Array2<f64>,
531        y: Option<&Array1<f64>>,
532        n_points: usize,
533    ) -> Result<OptimizationResult> {
534        // Similar to RBF grid search but for Nyström method
535        let gamma_values = self.create_parameter_grid(
536            self.config.parameter_bounds.gamma_bounds.0,
537            self.config.parameter_bounds.gamma_bounds.1,
538            n_points,
539            true,
540        );
541
542        let n_components_values = self
543            .create_parameter_grid(
544                self.config.parameter_bounds.n_components_bounds.0 as f64,
545                self.config.parameter_bounds.n_components_bounds.1 as f64,
546                n_points,
547                false,
548            )
549            .into_iter()
550            .map(|x| x as usize)
551            .collect::<Vec<_>>();
552
553        let mut parameter_history = Vec::new();
554        let mut best_score = f64::NEG_INFINITY;
555        let mut best_parameters = ParameterSet {
556            gamma: gamma_values[0],
557            n_components: n_components_values[0],
558            degree: None,
559            coef0: None,
560        };
561
562        for &gamma in &gamma_values {
563            for &n_components in &n_components_values {
564                let params = ParameterSet {
565                    gamma,
566                    n_components,
567                    degree: None,
568                    coef0: None,
569                };
570
571                let score = self.evaluate_nystroem_parameters(x, y, &params)?;
572                parameter_history.push((params.clone(), score));
573
574                if score > best_score {
575                    best_score = score;
576                    best_parameters = params;
577                }
578            }
579        }
580
581        let n_evaluations = parameter_history.len();
582        Ok(OptimizationResult {
583            best_parameters,
584            best_score,
585            parameter_history,
586            converged: true,
587            n_evaluations,
588        })
589    }
590
591    fn random_search_nystroem(
592        &self,
593        x: &Array2<f64>,
594        y: Option<&Array1<f64>>,
595        n_samples: usize,
596    ) -> Result<OptimizationResult> {
597        let mut rng = if let Some(seed) = self.config.random_seed {
598            StdRng::seed_from_u64(seed)
599        } else {
600            StdRng::from_seed(thread_rng().gen())
601        };
602
603        let mut parameter_history = Vec::new();
604        let mut best_score = f64::NEG_INFINITY;
605        let mut best_parameters = ParameterSet {
606            gamma: 1.0,
607            n_components: 100,
608            degree: None,
609            coef0: None,
610        };
611
612        for _ in 0..n_samples {
613            let gamma = self.sample_log_uniform(
614                &mut rng,
615                self.config.parameter_bounds.gamma_bounds.0,
616                self.config.parameter_bounds.gamma_bounds.1,
617            );
618
619            let n_components = rng.gen_range(
620                self.config.parameter_bounds.n_components_bounds.0
621                    ..=self.config.parameter_bounds.n_components_bounds.1,
622            );
623
624            let params = ParameterSet {
625                gamma,
626                n_components,
627                degree: None,
628                coef0: None,
629            };
630
631            let score = self.evaluate_nystroem_parameters(x, y, &params)?;
632            parameter_history.push((params.clone(), score));
633
634            if score > best_score {
635                best_score = score;
636                best_parameters = params;
637            }
638        }
639
640        Ok(OptimizationResult {
641            best_parameters,
642            best_score,
643            parameter_history,
644            converged: true,
645            n_evaluations: n_samples,
646        })
647    }
648
649    fn evaluate_rbf_parameters(
650        &self,
651        x: &Array2<f64>,
652        y: Option<&Array1<f64>>,
653        params: &ParameterSet,
654    ) -> Result<f64> {
655        let sampler = RBFSampler::new(params.n_components).gamma(params.gamma);
656        let fitted = sampler.fit(x, &())?;
657        let x_transformed = fitted.transform(x)?;
658
659        match &self.config.objective_function {
660            ObjectiveFunction::KernelAlignment => {
661                self.compute_kernel_alignment(x, &x_transformed, params.gamma)
662            }
663            ObjectiveFunction::CrossValidationError { n_folds } => {
664                if let Some(y_data) = y {
665                    self.compute_cross_validation_score(&x_transformed, y_data, *n_folds)
666                } else {
667                    // Fallback to kernel alignment
668                    self.compute_kernel_alignment(x, &x_transformed, params.gamma)
669                }
670            }
671            ObjectiveFunction::ApproximationQuality => {
672                self.compute_approximation_quality(x, &x_transformed, params.gamma)
673            }
674            ObjectiveFunction::EffectiveRank => self.compute_effective_rank(&x_transformed),
675            ObjectiveFunction::Custom => {
676                // Placeholder for custom objective function
677                Ok(0.0)
678            }
679        }
680    }
681
682    fn evaluate_nystroem_parameters(
683        &self,
684        x: &Array2<f64>,
685        y: Option<&Array1<f64>>,
686        params: &ParameterSet,
687    ) -> Result<f64> {
688        use crate::nystroem::Kernel;
689
690        let kernel = Kernel::Rbf {
691            gamma: params.gamma,
692        };
693        let nystroem = Nystroem::new(kernel, params.n_components);
694        let fitted = nystroem.fit(x, &())?;
695        let x_transformed = fitted.transform(x)?;
696
697        match &self.config.objective_function {
698            ObjectiveFunction::KernelAlignment => {
699                self.compute_kernel_alignment(x, &x_transformed, params.gamma)
700            }
701            ObjectiveFunction::CrossValidationError { n_folds } => {
702                if let Some(y_data) = y {
703                    self.compute_cross_validation_score(&x_transformed, y_data, *n_folds)
704                } else {
705                    self.compute_kernel_alignment(x, &x_transformed, params.gamma)
706                }
707            }
708            ObjectiveFunction::ApproximationQuality => {
709                self.compute_approximation_quality(x, &x_transformed, params.gamma)
710            }
711            ObjectiveFunction::EffectiveRank => self.compute_effective_rank(&x_transformed),
712            ObjectiveFunction::Custom => Ok(0.0),
713        }
714    }
715
716    fn compute_kernel_alignment(
717        &self,
718        x: &Array2<f64>,
719        x_transformed: &Array2<f64>,
720        gamma: f64,
721    ) -> Result<f64> {
722        let n_samples = x.nrows().min(100); // Limit for efficiency
723        let x_subset = x.slice(s![..n_samples, ..]);
724
725        // Compute exact kernel matrix
726        let mut k_exact = Array2::zeros((n_samples, n_samples));
727        for i in 0..n_samples {
728            for j in 0..n_samples {
729                let diff = &x_subset.row(i) - &x_subset.row(j);
730                let squared_norm = diff.dot(&diff);
731                k_exact[[i, j]] = (-gamma * squared_norm).exp();
732            }
733        }
734
735        // Compute approximate kernel matrix
736        let x_transformed_subset = x_transformed.slice(s![..n_samples, ..]);
737        let k_approx = x_transformed_subset.dot(&x_transformed_subset.t());
738
739        // Compute alignment
740        let k_exact_frobenius = k_exact.iter().map(|&x| x * x).sum::<f64>().sqrt();
741        let k_approx_frobenius = k_approx.iter().map(|&x| x * x).sum::<f64>().sqrt();
742        let k_product = (&k_exact * &k_approx).sum();
743
744        let alignment = k_product / (k_exact_frobenius * k_approx_frobenius);
745        Ok(alignment)
746    }
747
748    fn compute_cross_validation_score(
749        &self,
750        x_transformed: &Array2<f64>,
751        y: &Array1<f64>,
752        n_folds: usize,
753    ) -> Result<f64> {
754        let n_samples = x_transformed.nrows();
755        let fold_size = n_samples / n_folds;
756        let mut cv_scores = Vec::new();
757
758        for fold in 0..n_folds {
759            let start = fold * fold_size;
760            let end = if fold == n_folds - 1 {
761                n_samples
762            } else {
763                (fold + 1) * fold_size
764            };
765
766            // Simple correlation-based score (placeholder for more sophisticated CV)
767            let val_features = x_transformed.slice(s![start..end, ..]);
768            let val_targets = y.slice(s![start..end]);
769
770            // Compute mean correlation between features and targets
771            let mut correlations = Vec::new();
772            for j in 0..val_features.ncols() {
773                let feature_col = val_features.column(j);
774                let correlation =
775                    self.compute_correlation(feature_col.into_owned().view(), val_targets);
776                correlations.push(correlation.abs());
777            }
778
779            let mean_correlation = correlations.iter().sum::<f64>() / correlations.len() as f64;
780            cv_scores.push(mean_correlation);
781        }
782
783        Ok(cv_scores.iter().sum::<f64>() / cv_scores.len() as f64)
784    }
785
786    fn compute_approximation_quality(
787        &self,
788        x: &Array2<f64>,
789        x_transformed: &Array2<f64>,
790        gamma: f64,
791    ) -> Result<f64> {
792        // Compute reconstruction error
793        let n_samples = x.nrows().min(50); // Limit for efficiency
794        let x_subset = x.slice(s![..n_samples, ..]);
795        let x_transformed_subset = x_transformed.slice(s![..n_samples, ..]);
796
797        // Exact kernel matrix
798        let mut k_exact = Array2::zeros((n_samples, n_samples));
799        for i in 0..n_samples {
800            for j in 0..n_samples {
801                let diff = &x_subset.row(i) - &x_subset.row(j);
802                let squared_norm = diff.dot(&diff);
803                k_exact[[i, j]] = (-gamma * squared_norm).exp();
804            }
805        }
806
807        // Approximate kernel matrix
808        let k_approx = x_transformed_subset.dot(&x_transformed_subset.t());
809
810        // Frobenius norm error
811        let error_matrix = &k_exact - &k_approx;
812        let frobenius_error = error_matrix.iter().map(|&x| x * x).sum::<f64>().sqrt();
813        let exact_frobenius = k_exact.iter().map(|&x| x * x).sum::<f64>().sqrt();
814
815        let relative_error = frobenius_error / exact_frobenius;
816        Ok(1.0 / (1.0 + relative_error)) // Convert error to quality score
817    }
818
819    fn compute_effective_rank(&self, x_transformed: &Array2<f64>) -> Result<f64> {
820        // Compute SVD - only need singular values, not full U/V matrices
821        let (_, s, _) = x_transformed
822            .svd(false)
823            .map_err(|_| SklearsError::InvalidInput("SVD computation failed".to_string()))?;
824
825        // Compute effective rank using entropy
826        let s_sum = s.sum();
827        if s_sum == 0.0 {
828            return Ok(0.0);
829        }
830
831        let s_normalized = &s / s_sum;
832        let entropy = -s_normalized
833            .iter()
834            .filter(|&&x| x > 1e-12)
835            .map(|&x| x * x.ln())
836            .sum::<f64>();
837
838        Ok(entropy.exp())
839    }
840
841    fn compute_correlation(
842        &self,
843        x: scirs2_core::ndarray::ArrayView1<f64>,
844        y: scirs2_core::ndarray::ArrayView1<f64>,
845    ) -> f64 {
846        let x_mean = x.mean().unwrap_or(0.0);
847        let y_mean = y.mean().unwrap_or(0.0);
848
849        let numerator: f64 = x
850            .iter()
851            .zip(y.iter())
852            .map(|(&xi, &yi)| (xi - x_mean) * (yi - y_mean))
853            .sum();
854
855        let x_var: f64 = x.iter().map(|&xi| (xi - x_mean).powi(2)).sum();
856        let y_var: f64 = y.iter().map(|&yi| (yi - y_mean).powi(2)).sum();
857
858        let denominator = (x_var * y_var).sqrt();
859
860        if denominator == 0.0 {
861            0.0
862        } else {
863            numerator / denominator
864        }
865    }
866
867    // Helper methods
868    fn create_parameter_grid(
869        &self,
870        min_val: f64,
871        max_val: f64,
872        n_points: usize,
873        log_scale: bool,
874    ) -> Vec<f64> {
875        if log_scale {
876            let log_min = min_val.ln();
877            let log_max = max_val.ln();
878            (0..n_points)
879                .map(|i| {
880                    let t = i as f64 / (n_points - 1) as f64;
881                    (log_min + t * (log_max - log_min)).exp()
882                })
883                .collect()
884        } else {
885            (0..n_points)
886                .map(|i| {
887                    let t = i as f64 / (n_points - 1) as f64;
888                    min_val + t * (max_val - min_val)
889                })
890                .collect()
891        }
892    }
893
894    fn sample_log_uniform(&self, rng: &mut StdRng, min_val: f64, max_val: f64) -> f64 {
895        let log_min = min_val.ln();
896        let log_max = max_val.ln();
897        let log_val = rng.gen_range(log_min..log_max);
898        log_val.exp()
899    }
900
901    fn acquisition_function_rbf(
902        &self,
903        parameter_history: &[(ParameterSet, f64)],
904        exploration_factor: f64,
905        rng: &mut StdRng,
906    ) -> ParameterSet {
907        // Simplified Upper Confidence Bound acquisition function
908        // In practice, this would use a more sophisticated GP surrogate model
909
910        let _best_score = parameter_history
911            .iter()
912            .map(|(_, score)| *score)
913            .fold(f64::NEG_INFINITY, f64::max);
914
915        // Generate candidate points and evaluate acquisition function
916        let mut best_acquisition = f64::NEG_INFINITY;
917        let mut best_candidate = parameter_history[0].0.clone();
918
919        for _ in 0..100 {
920            // Sample 100 candidates
921            let gamma = self.sample_log_uniform(
922                rng,
923                self.config.parameter_bounds.gamma_bounds.0,
924                self.config.parameter_bounds.gamma_bounds.1,
925            );
926
927            let n_components = rng.gen_range(
928                self.config.parameter_bounds.n_components_bounds.0
929                    ..=self.config.parameter_bounds.n_components_bounds.1,
930            );
931
932            let candidate = ParameterSet {
933                gamma,
934                n_components,
935                degree: None,
936                coef0: None,
937            };
938
939            // Simple acquisition function: exploration bonus
940            let mean_score = self.predict_score_from_history(parameter_history, &candidate);
941            let uncertainty =
942                exploration_factor * self.estimate_uncertainty(parameter_history, &candidate);
943            let acquisition = mean_score + uncertainty;
944
945            if acquisition > best_acquisition {
946                best_acquisition = acquisition;
947                best_candidate = candidate;
948            }
949        }
950
951        best_candidate
952    }
953
954    fn predict_score_from_history(
955        &self,
956        parameter_history: &[(ParameterSet, f64)],
957        candidate: &ParameterSet,
958    ) -> f64 {
959        // Simple distance-weighted average
960        let mut weighted_sum = 0.0;
961        let mut weight_sum = 0.0;
962
963        for (params, score) in parameter_history {
964            let distance = self.parameter_distance(candidate, params);
965            let weight = (-distance).exp();
966            weighted_sum += weight * score;
967            weight_sum += weight;
968        }
969
970        if weight_sum > 0.0 {
971            weighted_sum / weight_sum
972        } else {
973            0.0
974        }
975    }
976
977    fn estimate_uncertainty(
978        &self,
979        parameter_history: &[(ParameterSet, f64)],
980        candidate: &ParameterSet,
981    ) -> f64 {
982        // Simple uncertainty estimation based on distance to nearest neighbor
983        let min_distance = parameter_history
984            .iter()
985            .map(|(params, _)| self.parameter_distance(candidate, params))
986            .fold(f64::INFINITY, f64::min);
987
988        min_distance
989    }
990
991    fn parameter_distance(&self, p1: &ParameterSet, p2: &ParameterSet) -> f64 {
992        let gamma_diff = (p1.gamma.ln() - p2.gamma.ln()).powi(2);
993        let n_components_diff =
994            ((p1.n_components as f64).ln() - (p2.n_components as f64).ln()).powi(2);
995        (gamma_diff + n_components_diff).sqrt()
996    }
997
998    fn optimize_gamma_coordinate(
999        &self,
1000        x: &Array2<f64>,
1001        y: Option<&Array1<f64>>,
1002        current_params: &ParameterSet,
1003    ) -> Result<ParameterSet> {
1004        let gamma_values = self.create_parameter_grid(
1005            self.config.parameter_bounds.gamma_bounds.0,
1006            self.config.parameter_bounds.gamma_bounds.1,
1007            10,
1008            true,
1009        );
1010
1011        let mut best_gamma = current_params.gamma;
1012        let mut best_score = f64::NEG_INFINITY;
1013
1014        for &gamma in &gamma_values {
1015            let test_params = ParameterSet {
1016                gamma,
1017                n_components: current_params.n_components,
1018                degree: current_params.degree,
1019                coef0: current_params.coef0,
1020            };
1021
1022            let score = self.evaluate_rbf_parameters(x, y, &test_params)?;
1023            if score > best_score {
1024                best_score = score;
1025                best_gamma = gamma;
1026            }
1027        }
1028
1029        Ok(ParameterSet {
1030            gamma: best_gamma,
1031            n_components: current_params.n_components,
1032            degree: current_params.degree,
1033            coef0: current_params.coef0,
1034        })
1035    }
1036
1037    fn optimize_n_components_coordinate(
1038        &self,
1039        x: &Array2<f64>,
1040        y: Option<&Array1<f64>>,
1041        current_params: &ParameterSet,
1042    ) -> Result<ParameterSet> {
1043        let n_components_values = self
1044            .create_parameter_grid(
1045                self.config.parameter_bounds.n_components_bounds.0 as f64,
1046                self.config.parameter_bounds.n_components_bounds.1 as f64,
1047                10,
1048                false,
1049            )
1050            .into_iter()
1051            .map(|x| x as usize)
1052            .collect::<Vec<_>>();
1053
1054        let mut best_n_components = current_params.n_components;
1055        let mut best_score = f64::NEG_INFINITY;
1056
1057        for &n_components in &n_components_values {
1058            let test_params = ParameterSet {
1059                gamma: current_params.gamma,
1060                n_components,
1061                degree: current_params.degree,
1062                coef0: current_params.coef0,
1063            };
1064
1065            let score = self.evaluate_rbf_parameters(x, y, &test_params)?;
1066            if score > best_score {
1067                best_score = score;
1068                best_n_components = n_components;
1069            }
1070        }
1071
1072        Ok(ParameterSet {
1073            gamma: current_params.gamma,
1074            n_components: best_n_components,
1075            degree: current_params.degree,
1076            coef0: current_params.coef0,
1077        })
1078    }
1079}
1080
1081#[allow(non_snake_case)]
1082#[cfg(test)]
1083mod tests {
1084    use super::*;
1085    use approx::assert_abs_diff_eq;
1086
1087    #[test]
1088    fn test_parameter_learner_grid_search() {
1089        let x =
1090            Array2::from_shape_vec((50, 5), (0..250).map(|i| i as f64 * 0.01).collect()).unwrap();
1091
1092        let config = ParameterLearningConfig {
1093            search_strategy: SearchStrategy::GridSearch { n_points: 3 },
1094            parameter_bounds: ParameterBounds {
1095                gamma_bounds: (0.1, 10.0),
1096                n_components_bounds: (10, 50),
1097                ..Default::default()
1098            },
1099            objective_function: ObjectiveFunction::KernelAlignment,
1100            ..Default::default()
1101        };
1102
1103        let learner = ParameterLearner::new(config);
1104        let result = learner.optimize_rbf_parameters(&x, None).unwrap();
1105
1106        assert!(result.best_score > 0.0);
1107        assert!(result.best_parameters.gamma >= 0.1);
1108        assert!(result.best_parameters.gamma <= 10.0);
1109        assert!(result.best_parameters.n_components >= 10);
1110        assert!(result.best_parameters.n_components <= 50);
1111        assert_eq!(result.parameter_history.len(), 9); // 3x3 grid
1112        assert!(result.converged);
1113    }
1114
1115    #[test]
1116    fn test_parameter_learner_random_search() {
1117        let x =
1118            Array2::from_shape_vec((30, 4), (0..120).map(|i| i as f64 * 0.05).collect()).unwrap();
1119
1120        let config = ParameterLearningConfig {
1121            search_strategy: SearchStrategy::RandomSearch { n_samples: 5 },
1122            parameter_bounds: ParameterBounds {
1123                gamma_bounds: (0.01, 1.0),
1124                n_components_bounds: (5, 25),
1125                ..Default::default()
1126            },
1127            random_seed: Some(42),
1128            ..Default::default()
1129        };
1130
1131        let learner = ParameterLearner::new(config);
1132        let result = learner.optimize_rbf_parameters(&x, None).unwrap();
1133
1134        assert!(result.best_score > 0.0);
1135        assert_eq!(result.parameter_history.len(), 5);
1136        assert_eq!(result.n_evaluations, 5);
1137    }
1138
1139    #[test]
1140    fn test_parameter_learner_bayesian_optimization() {
1141        let x = Array2::from_shape_vec((25, 3), (0..75).map(|i| i as f64 * 0.1).collect()).unwrap();
1142
1143        let config = ParameterLearningConfig {
1144            search_strategy: SearchStrategy::BayesianOptimization {
1145                n_initial: 3,
1146                n_iterations: 2,
1147                exploration_factor: 1.0,
1148            },
1149            parameter_bounds: ParameterBounds {
1150                gamma_bounds: (0.1, 5.0),
1151                n_components_bounds: (10, 30),
1152                ..Default::default()
1153            },
1154            random_seed: Some(123),
1155            ..Default::default()
1156        };
1157
1158        let learner = ParameterLearner::new(config);
1159        let result = learner.optimize_rbf_parameters(&x, None).unwrap();
1160
1161        assert!(result.best_score > 0.0);
1162        assert_eq!(result.parameter_history.len(), 5); // 3 initial + 2 iterations
1163        assert_eq!(result.n_evaluations, 5);
1164    }
1165
1166    #[test]
1167    fn test_parameter_learner_coordinate_descent() {
1168        let x =
1169            Array2::from_shape_vec((40, 6), (0..240).map(|i| i as f64 * 0.02).collect()).unwrap();
1170
1171        let config = ParameterLearningConfig {
1172            search_strategy: SearchStrategy::CoordinateDescent {
1173                max_iterations: 3,
1174                tolerance: 1e-6,
1175            },
1176            parameter_bounds: ParameterBounds {
1177                gamma_bounds: (0.1, 2.0),
1178                n_components_bounds: (15, 35),
1179                ..Default::default()
1180            },
1181            ..Default::default()
1182        };
1183
1184        let learner = ParameterLearner::new(config);
1185        let result = learner.optimize_rbf_parameters(&x, None).unwrap();
1186
1187        assert!(result.best_score > 0.0);
1188        assert!(result.parameter_history.len() >= 1);
1189        // Note: May converge early, so we don't check exact length
1190    }
1191
1192    #[test]
1193    fn test_parameter_learner_nystroem() {
1194        let x =
1195            Array2::from_shape_vec((35, 4), (0..140).map(|i| i as f64 * 0.03).collect()).unwrap();
1196
1197        let config = ParameterLearningConfig {
1198            search_strategy: SearchStrategy::GridSearch { n_points: 2 },
1199            parameter_bounds: ParameterBounds {
1200                gamma_bounds: (0.5, 2.0),
1201                n_components_bounds: (10, 20),
1202                ..Default::default()
1203            },
1204            ..Default::default()
1205        };
1206
1207        let learner = ParameterLearner::new(config);
1208        let result = learner.optimize_nystroem_parameters(&x, None).unwrap();
1209
1210        assert!(result.best_score > 0.0);
1211        assert_eq!(result.parameter_history.len(), 4); // 2x2 grid
1212    }
1213
1214    #[test]
1215    fn test_cross_validation_objective() {
1216        let x =
1217            Array2::from_shape_vec((50, 3), (0..150).map(|i| i as f64 * 0.01).collect()).unwrap();
1218        let y = Array1::from_shape_fn(50, |i| (i as f64 * 0.1).sin());
1219
1220        let config = ParameterLearningConfig {
1221            search_strategy: SearchStrategy::GridSearch { n_points: 2 },
1222            objective_function: ObjectiveFunction::CrossValidationError { n_folds: 3 },
1223            ..Default::default()
1224        };
1225
1226        let learner = ParameterLearner::new(config);
1227        let result = learner.optimize_rbf_parameters(&x, Some(&y)).unwrap();
1228
1229        assert!(result.best_score >= 0.0);
1230        assert!(result.parameter_history.len() > 0);
1231    }
1232
1233    #[test]
1234    fn test_effective_rank_objective() {
1235        let x =
1236            Array2::from_shape_vec((20, 3), (0..60).map(|i| i as f64 * 0.02).collect()).unwrap();
1237
1238        let config = ParameterLearningConfig {
1239            search_strategy: SearchStrategy::RandomSearch { n_samples: 2 },
1240            parameter_bounds: ParameterBounds {
1241                gamma_bounds: (0.1, 5.0),
1242                n_components_bounds: (10, 20), // Further reduced to ensure fast SVD on all platforms
1243                ..Default::default()
1244            },
1245            objective_function: ObjectiveFunction::EffectiveRank,
1246            random_seed: Some(456),
1247            ..Default::default()
1248        };
1249
1250        let learner = ParameterLearner::new(config);
1251        let result = learner.optimize_rbf_parameters(&x, None).unwrap();
1252
1253        assert!(result.best_score > 0.0);
1254        assert_eq!(result.parameter_history.len(), 2);
1255    }
1256
1257    #[test]
1258    fn test_parameter_grid_creation() {
1259        let learner = ParameterLearner::new(ParameterLearningConfig::default());
1260
1261        // Test linear scale
1262        let linear_grid = learner.create_parameter_grid(0.0, 10.0, 5, false);
1263        assert_eq!(linear_grid.len(), 5);
1264        assert_abs_diff_eq!(linear_grid[0], 0.0, epsilon = 1e-10);
1265        assert_abs_diff_eq!(linear_grid[4], 10.0, epsilon = 1e-10);
1266
1267        // Test log scale
1268        let log_grid = learner.create_parameter_grid(0.1, 10.0, 3, true);
1269        assert_eq!(log_grid.len(), 3);
1270        assert_abs_diff_eq!(log_grid[0], 0.1, epsilon = 1e-10);
1271        assert_abs_diff_eq!(log_grid[2], 10.0, epsilon = 1e-10);
1272        assert!(log_grid[1] > log_grid[0]);
1273        assert!(log_grid[1] < log_grid[2]);
1274    }
1275
1276    #[test]
1277    fn test_optimization_result() {
1278        let x = Array2::from_shape_vec((20, 3), (0..60).map(|i| i as f64 * 0.1).collect()).unwrap();
1279
1280        let config = ParameterLearningConfig {
1281            search_strategy: SearchStrategy::GridSearch { n_points: 2 },
1282            ..Default::default()
1283        };
1284
1285        let learner = ParameterLearner::new(config);
1286        let result = learner.optimize_rbf_parameters(&x, None).unwrap();
1287
1288        // Verify result structure
1289        assert!(result.best_score > 0.0);
1290        assert!(result.best_parameters.gamma > 0.0);
1291        assert!(result.best_parameters.n_components > 0);
1292        assert!(result.converged);
1293        assert_eq!(result.n_evaluations, result.parameter_history.len());
1294
1295        // Verify that best score is actually the best
1296        let max_score = result
1297            .parameter_history
1298            .iter()
1299            .map(|(_, score)| *score)
1300            .fold(f64::NEG_INFINITY, f64::max);
1301        assert_abs_diff_eq!(result.best_score, max_score, epsilon = 1e-10);
1302    }
1303}