1use 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#[derive(Debug, Clone)]
21pub enum SearchStrategy {
23 GridSearch {
25 n_points: usize,
27 },
28 RandomSearch {
30 n_samples: usize,
32 },
33 BayesianOptimization {
35 n_initial: usize,
37 n_iterations: usize,
39 exploration_factor: f64,
41 },
42 CoordinateDescent {
44 max_iterations: usize,
46 tolerance: f64,
48 },
49}
50
51#[derive(Debug, Clone)]
53pub struct ParameterBounds {
55 pub gamma_bounds: (f64, f64),
57 pub n_components_bounds: (usize, usize),
59 pub degree_bounds: Option<(i32, i32)>,
61 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#[derive(Debug, Clone)]
78pub enum ObjectiveFunction {
80 KernelAlignment,
82 CrossValidationError { n_folds: usize },
84 ApproximationQuality,
86 EffectiveRank,
88 Custom,
90}
91
92#[derive(Debug, Clone)]
94pub struct ParameterLearningConfig {
96 pub search_strategy: SearchStrategy,
98 pub parameter_bounds: ParameterBounds,
100 pub objective_function: ObjectiveFunction,
102 pub validation_fraction: f64,
104 pub random_seed: Option<u64>,
106 pub n_jobs: usize,
108 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#[derive(Debug, Clone)]
128pub struct ParameterSet {
130 pub gamma: f64,
132 pub n_components: usize,
134 pub degree: Option<i32>,
136 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 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#[derive(Debug, Clone)]
169pub struct OptimizationResult {
171 pub best_parameters: ParameterSet,
173 pub best_score: f64,
175 pub parameter_history: Vec<(ParameterSet, f64)>,
177 pub converged: bool,
179 pub n_evaluations: usize,
181}
182
183pub struct ParameterLearner {
185 config: ParameterLearningConfig,
186}
187
188impl ParameterLearner {
189 pub fn new(config: ParameterLearningConfig) -> Self {
191 Self { config }
192 }
193
194 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 pub fn optimize_nystroem_parameters(
219 &self,
220 x: &Array2<f64>,
221 y: Option<&Array1<f64>>,
222 ) -> Result<OptimizationResult> {
223 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 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, );
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, )
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 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 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 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, ¶ms)?;
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 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 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, ¶ms)?;
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 for iteration in 0..n_iterations {
435 let next_params =
437 self.acquisition_function_rbf(¶meter_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, ¤t_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 current_params = self.optimize_gamma_coordinate(x, y, ¤t_params)?;
492 current_score = self.evaluate_rbf_parameters(x, y, ¤t_params)?;
493 parameter_history.push((current_params.clone(), current_score));
494
495 current_params = self.optimize_n_components_coordinate(x, y, ¤t_params)?;
497 current_score = self.evaluate_rbf_parameters(x, y, ¤t_params)?;
498 parameter_history.push((current_params.clone(), current_score));
499
500 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 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, ¶ms)?;
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, ¶ms)?;
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 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 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); let x_subset = x.slice(s![..n_samples, ..]);
724
725 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 let x_transformed_subset = x_transformed.slice(s![..n_samples, ..]);
737 let k_approx = x_transformed_subset.dot(&x_transformed_subset.t());
738
739 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 let val_features = x_transformed.slice(s![start..end, ..]);
768 let val_targets = y.slice(s![start..end]);
769
770 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 let n_samples = x.nrows().min(50); let x_subset = x.slice(s![..n_samples, ..]);
795 let x_transformed_subset = x_transformed.slice(s![..n_samples, ..]);
796
797 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 let k_approx = x_transformed_subset.dot(&x_transformed_subset.t());
809
810 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)) }
818
819 fn compute_effective_rank(&self, x_transformed: &Array2<f64>) -> Result<f64> {
820 let (_, s, _) = x_transformed
822 .svd(false)
823 .map_err(|_| SklearsError::InvalidInput("SVD computation failed".to_string()))?;
824
825 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 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 let _best_score = parameter_history
911 .iter()
912 .map(|(_, score)| *score)
913 .fold(f64::NEG_INFINITY, f64::max);
914
915 let mut best_acquisition = f64::NEG_INFINITY;
917 let mut best_candidate = parameter_history[0].0.clone();
918
919 for _ in 0..100 {
920 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 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 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 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); 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); 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 }
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); }
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), ..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 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 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 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 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}