1use crate::heteroscedastic::{LearnableNoiseFunction, NoiseFunction};
40use crate::kernels::Kernel;
41use scirs2_core::ndarray::{Array1, Array2, Axis};
42use sklears_core::error::{Result as SklResult, SklearsError};
44
45#[derive(Debug, Clone, Copy)]
47pub enum InformationCriterion {
48 AIC,
50 BIC,
52 HQC,
54}
55
56#[derive(Debug, Clone)]
58pub struct NoiseFunctionEvaluation {
59 pub noise_function: LearnableNoiseFunction,
60 pub log_likelihood: f64,
61 pub aic: f64,
62 pub bic: f64,
63 pub hqc: f64,
64 pub rmse: f64,
65 pub n_parameters: usize,
66}
67
68#[derive(Debug, Clone)]
70pub struct AutomaticNoiseFunctionSelector {
71 candidates: Vec<LearnableNoiseFunction>,
72 criterion: InformationCriterion,
73 max_iter: usize,
74 learning_rate: f64,
75 cross_validation_folds: Option<usize>,
76 regularization_strength: f64,
77}
78
79impl AutomaticNoiseFunctionSelector {
80 pub fn new() -> Self {
82 Self {
83 candidates: Vec::new(),
84 criterion: InformationCriterion::BIC,
85 max_iter: 100,
86 learning_rate: 0.01,
87 cross_validation_folds: None,
88 regularization_strength: 1e-6,
89 }
90 }
91
92 pub fn criterion(mut self, criterion: InformationCriterion) -> Self {
94 self.criterion = criterion;
95 self
96 }
97
98 pub fn max_iter(mut self, max_iter: usize) -> Self {
100 self.max_iter = max_iter;
101 self
102 }
103
104 pub fn learning_rate(mut self, learning_rate: f64) -> Self {
106 self.learning_rate = learning_rate;
107 self
108 }
109
110 pub fn cross_validation_folds(mut self, folds: usize) -> Self {
112 self.cross_validation_folds = Some(folds);
113 self
114 }
115
116 pub fn regularization_strength(mut self, strength: f64) -> Self {
118 self.regularization_strength = strength;
119 self
120 }
121
122 pub fn add_candidate_constant(mut self) -> Self {
124 self.candidates.push(LearnableNoiseFunction::constant(0.1));
125 self
126 }
127
128 pub fn add_candidate_linear(mut self) -> Self {
130 self.candidates
131 .push(LearnableNoiseFunction::linear(0.1, 0.01));
132 self
133 }
134
135 pub fn add_candidate_polynomial(mut self, degree: usize) -> Self {
137 let mut coeffs = vec![0.1]; for _ in 1..=degree {
139 coeffs.push(0.01); }
141 self.candidates
142 .push(LearnableNoiseFunction::polynomial(coeffs));
143 self
144 }
145
146 pub fn add_candidate_gaussian_process(
148 mut self,
149 kernel: Box<dyn Kernel>,
150 n_inducing: usize,
151 ) -> Self {
152 self.candidates
153 .push(LearnableNoiseFunction::gaussian_process(kernel, n_inducing));
154 self
155 }
156
157 pub fn add_candidate_neural_network(
159 mut self,
160 input_dim: usize,
161 hidden_sizes: Vec<usize>,
162 ) -> Self {
163 self.candidates.push(LearnableNoiseFunction::neural_network(
164 input_dim,
165 hidden_sizes,
166 ));
167 self
168 }
169
170 pub fn select_best(
172 &self,
173 x: &Array2<f64>,
174 y: &Array1<f64>,
175 ) -> SklResult<LearnableNoiseFunction> {
176 if self.candidates.is_empty() {
177 return Err(SklearsError::InvalidInput(
178 "No candidate noise functions specified".to_string(),
179 ));
180 }
181
182 let mut best_evaluation: Option<NoiseFunctionEvaluation> = None;
183
184 for candidate in &self.candidates {
185 let evaluation = if let Some(folds) = self.cross_validation_folds {
186 self.evaluate_with_cross_validation(candidate.clone(), x, y, folds)?
187 } else {
188 self.evaluate_noise_function(candidate.clone(), x, y)?
189 };
190
191 if let Some(ref best) = best_evaluation {
192 let is_better = match self.criterion {
193 InformationCriterion::AIC => evaluation.aic < best.aic,
194 InformationCriterion::BIC => evaluation.bic < best.bic,
195 InformationCriterion::HQC => evaluation.hqc < best.hqc,
196 };
197
198 if is_better {
199 best_evaluation = Some(evaluation);
200 }
201 } else {
202 best_evaluation = Some(evaluation);
203 }
204 }
205
206 Ok(best_evaluation.unwrap().noise_function)
207 }
208
209 fn evaluate_noise_function(
211 &self,
212 mut noise_fn: LearnableNoiseFunction,
213 x: &Array2<f64>,
214 y: &Array1<f64>,
215 ) -> SklResult<NoiseFunctionEvaluation> {
216 for _iter in 0..self.max_iter {
218 let residuals = y.clone();
220 noise_fn.update_parameters(x, &residuals, self.learning_rate)?;
221 }
222
223 let noise_predictions = noise_fn.compute_noise(x)?;
225
226 let residuals: Array1<f64> = y.iter().map(|&yi| yi * yi).collect(); let mut log_likelihood = 0.0;
229
230 for i in 0..y.len() {
231 let noise_var = noise_predictions[i].max(1e-12);
232 log_likelihood += -0.5
233 * (residuals[i] / noise_var + noise_var.ln() + (2.0 * std::f64::consts::PI).ln());
234 }
235
236 let params = noise_fn.get_parameters();
238 let regularization_penalty =
239 self.regularization_strength * params.iter().map(|&p| p * p).sum::<f64>();
240 log_likelihood -= regularization_penalty;
241
242 let n_data = y.len() as f64;
243 let n_params = params.len() as f64;
244
245 let aic = -2.0 * log_likelihood + 2.0 * n_params;
247 let bic = -2.0 * log_likelihood + n_params * n_data.ln();
248 let hqc = -2.0 * log_likelihood + 2.0 * n_params * n_data.ln().ln();
249
250 let mut rmse = 0.0;
252 for i in 0..y.len() {
253 let predicted_noise = noise_predictions[i].sqrt();
254 let actual_abs_residual = y[i].abs();
255 rmse += (predicted_noise - actual_abs_residual).powi(2);
256 }
257 rmse = (rmse / n_data).sqrt();
258
259 Ok(NoiseFunctionEvaluation {
260 noise_function: noise_fn,
261 log_likelihood,
262 aic,
263 bic,
264 hqc,
265 rmse,
266 n_parameters: params.len(),
267 })
268 }
269
270 fn evaluate_with_cross_validation(
272 &self,
273 noise_fn: LearnableNoiseFunction,
274 x: &Array2<f64>,
275 y: &Array1<f64>,
276 folds: usize,
277 ) -> SklResult<NoiseFunctionEvaluation> {
278 let n_data = x.nrows();
279 let fold_size = n_data / folds;
280 let mut total_log_likelihood = 0.0;
281 let mut total_rmse = 0.0;
282
283 for fold in 0..folds {
284 let start_idx = fold * fold_size;
285 let end_idx = if fold == folds - 1 {
286 n_data
287 } else {
288 (fold + 1) * fold_size
289 };
290
291 let mut train_indices = Vec::new();
293 let mut test_indices = Vec::new();
294
295 for i in 0..n_data {
296 if i >= start_idx && i < end_idx {
297 test_indices.push(i);
298 } else {
299 train_indices.push(i);
300 }
301 }
302
303 let x_train = x.select(Axis(0), &train_indices);
305 let y_train = y.select(Axis(0), &train_indices);
306 let x_test = x.select(Axis(0), &test_indices);
307 let y_test = y.select(Axis(0), &test_indices);
308
309 let mut fold_noise_fn = noise_fn.clone();
311 for _iter in 0..self.max_iter {
312 let residuals = y_train.clone();
313 fold_noise_fn.update_parameters(&x_train, &residuals, self.learning_rate)?;
314 }
315
316 let test_noise_predictions = fold_noise_fn.compute_noise(&x_test)?;
318
319 for i in 0..y_test.len() {
320 let noise_var = test_noise_predictions[i].max(1e-12);
321 let residual_sq = y_test[i] * y_test[i];
322 total_log_likelihood += -0.5
323 * (residual_sq / noise_var
324 + noise_var.ln()
325 + (2.0 * std::f64::consts::PI).ln());
326
327 let predicted_noise = noise_var.sqrt();
328 let actual_abs_residual = y_test[i].abs();
329 total_rmse += (predicted_noise - actual_abs_residual).powi(2);
330 }
331 }
332
333 total_log_likelihood /= n_data as f64;
335 total_rmse = (total_rmse / n_data as f64).sqrt();
336
337 let mut final_noise_fn = noise_fn;
339 for _iter in 0..self.max_iter {
340 let residuals = y.clone();
341 final_noise_fn.update_parameters(x, &residuals, self.learning_rate)?;
342 }
343
344 let params = final_noise_fn.get_parameters();
345 let n_data = y.len() as f64;
346 let n_params = params.len() as f64;
347
348 let aic = -2.0 * total_log_likelihood + 2.0 * n_params;
350 let bic = -2.0 * total_log_likelihood + n_params * n_data.ln();
351 let hqc = -2.0 * total_log_likelihood + 2.0 * n_params * n_data.ln().ln();
352
353 Ok(NoiseFunctionEvaluation {
354 noise_function: final_noise_fn,
355 log_likelihood: total_log_likelihood,
356 aic,
357 bic,
358 hqc,
359 rmse: total_rmse,
360 n_parameters: params.len(),
361 })
362 }
363
364 pub fn evaluate_all(
366 &self,
367 x: &Array2<f64>,
368 y: &Array1<f64>,
369 ) -> SklResult<Vec<NoiseFunctionEvaluation>> {
370 let mut evaluations = Vec::new();
371
372 for candidate in &self.candidates {
373 let evaluation = if let Some(folds) = self.cross_validation_folds {
374 self.evaluate_with_cross_validation(candidate.clone(), x, y, folds)?
375 } else {
376 self.evaluate_noise_function(candidate.clone(), x, y)?
377 };
378 evaluations.push(evaluation);
379 }
380
381 Ok(evaluations)
382 }
383}
384
385#[derive(Debug, Clone)]
387pub struct EnsembleNoiseFunction {
388 noise_functions: Vec<LearnableNoiseFunction>,
389 weights: Array1<f64>,
390 combination_method: CombinationMethod,
391}
392
393#[derive(Debug, Clone, Copy)]
395pub enum CombinationMethod {
396 WeightedAverage,
398 WeightedGeometricMean,
400 MinimumVariance,
402}
403
404impl EnsembleNoiseFunction {
405 pub fn new(noise_functions: Vec<LearnableNoiseFunction>) -> Self {
407 let n_functions = noise_functions.len();
408 let weights = Array1::from_elem(n_functions, 1.0 / n_functions as f64);
409
410 Self {
411 noise_functions,
412 weights,
413 combination_method: CombinationMethod::WeightedAverage,
414 }
415 }
416
417 pub fn combination_method(mut self, method: CombinationMethod) -> Self {
419 self.combination_method = method;
420 self
421 }
422
423 pub fn weights(mut self, weights: Array1<f64>) -> Self {
425 let sum = weights.sum();
427 self.weights = weights / sum;
428 self
429 }
430
431 pub fn learn_weights(
433 &mut self,
434 x: &Array2<f64>,
435 y: &Array1<f64>,
436 max_iter: usize,
437 ) -> SklResult<()> {
438 let n_functions = self.noise_functions.len();
439
440 self.weights = Array1::from_elem(n_functions, 1.0 / n_functions as f64);
442
443 let learning_rate = 0.01;
444
445 for _iter in 0..max_iter {
446 let mut predictions = Vec::new();
448 for noise_fn in &self.noise_functions {
449 predictions.push(noise_fn.compute_noise(x)?);
450 }
451
452 let ensemble_pred = self.combine_predictions(&predictions)?;
454
455 let target_noise: Array1<f64> = y.iter().map(|&yi| yi * yi).collect();
457
458 let mut weight_gradients = Array1::zeros(n_functions);
460
461 for i in 0..x.nrows() {
462 let error = ensemble_pred[i] - target_noise[i];
463
464 for j in 0..n_functions {
465 let pred_diff = predictions[j][i] - ensemble_pred[i];
466 weight_gradients[j] += error * pred_diff;
467 }
468 }
469
470 weight_gradients /= x.nrows() as f64;
472
473 for j in 0..n_functions {
475 self.weights[j] -= learning_rate * weight_gradients[j];
476 }
477
478 for weight in self.weights.iter_mut() {
480 *weight = weight.max(1e-8);
481 }
482 let sum = self.weights.sum();
483 self.weights /= sum;
484 }
485
486 Ok(())
487 }
488
489 fn combine_predictions(&self, predictions: &[Array1<f64>]) -> SklResult<Array1<f64>> {
491 if predictions.is_empty() {
492 return Err(SklearsError::InvalidInput(
493 "No predictions to combine".to_string(),
494 ));
495 }
496
497 let n_points = predictions[0].len();
498 let mut combined = Array1::zeros(n_points);
499
500 match self.combination_method {
501 CombinationMethod::WeightedAverage => {
502 for i in 0..n_points {
503 for (j, pred) in predictions.iter().enumerate() {
504 combined[i] += self.weights[j] * pred[i];
505 }
506 }
507 }
508 CombinationMethod::WeightedGeometricMean => {
509 for i in 0..n_points {
510 let mut log_sum = 0.0;
511 for (j, pred) in predictions.iter().enumerate() {
512 log_sum += self.weights[j] * pred[i].max(1e-12).ln();
513 }
514 combined[i] = log_sum.exp();
515 }
516 }
517 CombinationMethod::MinimumVariance => {
518 for i in 0..n_points {
520 let mut weighted_sum = 0.0;
521 let mut weight_sum = 0.0;
522
523 for (_j, pred) in predictions.iter().enumerate() {
524 let variance = pred[i].max(1e-12);
525 let inv_var_weight = 1.0 / variance;
526 weighted_sum += inv_var_weight * pred[i];
527 weight_sum += inv_var_weight;
528 }
529
530 combined[i] = weighted_sum / weight_sum;
531 }
532 }
533 }
534
535 Ok(combined)
536 }
537}
538
539impl NoiseFunction for EnsembleNoiseFunction {
540 fn compute_noise(&self, x: &Array2<f64>) -> SklResult<Array1<f64>> {
541 let mut predictions = Vec::new();
542 for noise_fn in &self.noise_functions {
543 predictions.push(noise_fn.compute_noise(x)?);
544 }
545 self.combine_predictions(&predictions)
546 }
547
548 fn update_parameters(
549 &mut self,
550 x: &Array2<f64>,
551 residuals: &Array1<f64>,
552 learning_rate: f64,
553 ) -> SklResult<()> {
554 for noise_fn in &mut self.noise_functions {
556 noise_fn.update_parameters(x, residuals, learning_rate)?;
557 }
558
559 let target_noise: Array1<f64> = residuals.iter().map(|&r| r * r).collect();
561 self.learn_weights(x, &target_noise, 10)?;
562
563 Ok(())
564 }
565
566 fn get_parameters(&self) -> Vec<f64> {
567 let mut all_params = Vec::new();
568
569 all_params.extend(self.weights.iter().cloned());
571
572 for noise_fn in &self.noise_functions {
574 all_params.extend(noise_fn.get_parameters());
575 }
576
577 all_params
578 }
579
580 fn set_parameters(&mut self, params: &[f64]) -> SklResult<()> {
581 let n_functions = self.noise_functions.len();
582
583 if params.len() < n_functions {
584 return Err(SklearsError::InvalidInput(
585 "Not enough parameters for ensemble".to_string(),
586 ));
587 }
588
589 for i in 0..n_functions {
591 self.weights[i] = params[i];
592 }
593
594 let sum = self.weights.sum();
596 self.weights /= sum;
597
598 let mut param_offset = n_functions;
600 for noise_fn in &mut self.noise_functions {
601 let fn_params = noise_fn.get_parameters();
602 let n_fn_params = fn_params.len();
603
604 if param_offset + n_fn_params > params.len() {
605 return Err(SklearsError::InvalidInput(
606 "Not enough parameters for all noise functions".to_string(),
607 ));
608 }
609
610 noise_fn.set_parameters(¶ms[param_offset..param_offset + n_fn_params])?;
611 param_offset += n_fn_params;
612 }
613
614 Ok(())
615 }
616
617 fn clone_box(&self) -> Box<dyn NoiseFunction> {
618 Box::new(self.clone())
619 }
620}
621
622#[derive(Debug, Clone)]
624pub struct AdaptiveRegularization {
625 base_strength: f64,
626 adaptation_rate: f64,
627 complexity_penalty: f64,
628}
629
630impl AdaptiveRegularization {
631 pub fn new(base_strength: f64) -> Self {
633 Self {
634 base_strength,
635 adaptation_rate: 0.1,
636 complexity_penalty: 1.0,
637 }
638 }
639
640 pub fn adaptation_rate(mut self, rate: f64) -> Self {
642 self.adaptation_rate = rate;
643 self
644 }
645
646 pub fn complexity_penalty(mut self, penalty: f64) -> Self {
648 self.complexity_penalty = penalty;
649 self
650 }
651
652 pub fn compute_regularization_strength(
654 &self,
655 x: &Array2<f64>,
656 y: &Array1<f64>,
657 n_parameters: usize,
658 ) -> f64 {
659 let n_data = y.len() as f64;
660 let n_features = x.ncols() as f64;
661
662 let data_complexity = (n_features / n_data).sqrt();
664
665 let param_complexity = (n_parameters as f64 / n_data).sqrt();
667
668 let y_mean = y.mean().unwrap_or(0.0);
670 let y_var = y.iter().map(|&yi| (yi - y_mean).powi(2)).sum::<f64>() / n_data;
671 let noise_level = y_var.sqrt();
672
673 let adaptive_factor = 1.0
675 + self.adaptation_rate * (data_complexity + self.complexity_penalty * param_complexity);
676 let strength = self.base_strength * adaptive_factor / noise_level.max(1e-6);
677
678 strength.max(1e-12).min(1e-1) }
680}
681
682#[allow(non_snake_case)]
683#[cfg(test)]
684mod tests {
685 use super::*;
686
687 use approx::assert_abs_diff_eq;
688 use scirs2_core::ndarray::array;
689
690 #[test]
691 fn test_automatic_noise_function_selector() {
692 let x = array![[0.0], [1.0], [2.0], [3.0], [4.0]];
693 let y = array![0.1, 0.2, 0.4, 0.8, 1.6]; let selector = AutomaticNoiseFunctionSelector::new()
696 .add_candidate_constant()
697 .add_candidate_linear()
698 .add_candidate_polynomial(2)
699 .criterion(InformationCriterion::BIC)
700 .max_iter(50);
701
702 let best_noise_fn = selector.select_best(&x, &y).unwrap();
703
704 let noise_pred = best_noise_fn.compute_noise(&x).unwrap();
706 assert_eq!(noise_pred.len(), x.nrows());
707 assert!(noise_pred.iter().all(|&n| n > 0.0));
708 }
709
710 #[test]
711 fn test_ensemble_noise_function() {
712 let x = array![[0.0], [1.0], [2.0], [3.0]];
713 let y = array![0.1, 0.2, 0.3, 0.4];
714
715 let noise_functions = vec![
716 LearnableNoiseFunction::constant(0.1),
717 LearnableNoiseFunction::linear(0.1, 0.05),
718 ];
719
720 let mut ensemble = EnsembleNoiseFunction::new(noise_functions)
721 .combination_method(CombinationMethod::WeightedAverage);
722
723 let pred = ensemble.compute_noise(&x).unwrap();
725 assert_eq!(pred.len(), x.nrows());
726
727 ensemble.learn_weights(&x, &y, 10).unwrap();
729
730 let weight_sum: f64 = ensemble.weights.sum();
732 assert_abs_diff_eq!(weight_sum, 1.0, epsilon = 1e-10);
733 }
734
735 #[test]
736 fn test_information_criteria() {
737 let x = array![[0.0], [1.0], [2.0]];
738 let y = array![0.1, 0.2, 0.3];
739
740 let selector = AutomaticNoiseFunctionSelector::new()
741 .add_candidate_constant()
742 .add_candidate_linear()
743 .max_iter(10);
744
745 let evaluations = selector.evaluate_all(&x, &y).unwrap();
746 assert_eq!(evaluations.len(), 2);
747
748 for eval in &evaluations {
749 assert!(eval.aic.is_finite());
750 assert!(eval.bic.is_finite());
751 assert!(eval.hqc.is_finite());
752 assert!(eval.rmse >= 0.0);
753 assert!(eval.n_parameters > 0);
754 }
755 }
756
757 #[test]
758 fn test_cross_validation_evaluation() {
759 let x = array![[0.0], [1.0], [2.0], [3.0], [4.0], [5.0]];
760 let y = array![0.1, 0.2, 0.3, 0.4, 0.5, 0.6];
761
762 let selector = AutomaticNoiseFunctionSelector::new()
763 .add_candidate_constant()
764 .add_candidate_linear()
765 .cross_validation_folds(3)
766 .max_iter(10);
767
768 let best_noise_fn = selector.select_best(&x, &y).unwrap();
769 let noise_pred = best_noise_fn.compute_noise(&x).unwrap();
770 assert_eq!(noise_pred.len(), x.nrows());
771 }
772
773 #[test]
774 fn test_adaptive_regularization() {
775 let x = array![[0.0], [1.0], [2.0], [3.0]];
776 let y = array![0.1, 0.2, 0.3, 0.4];
777
778 let adaptive_reg = AdaptiveRegularization::new(1e-3)
779 .adaptation_rate(0.1)
780 .complexity_penalty(1.5);
781
782 let reg_strength = adaptive_reg.compute_regularization_strength(&x, &y, 3);
783 assert!(reg_strength > 0.0);
784 assert!(reg_strength < 1.0);
785 }
786
787 #[test]
788 fn test_ensemble_combination_methods() {
789 let x = array![[0.0], [1.0], [2.0]];
790
791 let noise_functions = vec![
792 LearnableNoiseFunction::constant(0.1),
793 LearnableNoiseFunction::constant(0.2),
794 ];
795
796 let methods = [
797 CombinationMethod::WeightedAverage,
798 CombinationMethod::WeightedGeometricMean,
799 CombinationMethod::MinimumVariance,
800 ];
801
802 for method in &methods {
803 let ensemble =
804 EnsembleNoiseFunction::new(noise_functions.clone()).combination_method(*method);
805
806 let pred = ensemble.compute_noise(&x).unwrap();
807 assert_eq!(pred.len(), x.nrows());
808 assert!(pred.iter().all(|&p| p > 0.0));
809 }
810 }
811
812 #[test]
813 fn test_noise_function_parameter_management() {
814 let noise_functions = vec![
815 LearnableNoiseFunction::constant(0.1),
816 LearnableNoiseFunction::linear(0.1, 0.05),
817 ];
818
819 let mut ensemble = EnsembleNoiseFunction::new(noise_functions);
820
821 let params = ensemble.get_parameters();
823 assert!(params.len() >= 2); let new_params = vec![0.3, 0.7, 0.15, 0.12, 0.03]; let result = ensemble.set_parameters(&new_params);
828 assert!(result.is_ok());
829
830 let weight_sum: f64 = ensemble.weights.sum();
832 assert_abs_diff_eq!(weight_sum, 1.0, epsilon = 1e-10);
833 }
834}