1use super::config::{
66 MetaFeatureStrategy, MetaLearningStrategy, MultiLayerStackingConfig, StackingLayerConfig,
67};
68use scirs2_core::ndarray::{s, Array1, Array2, Axis};
69use sklears_core::{
70 error::{Result, SklearsError},
71 prelude::Predict,
72 traits::{Fit, Trained, Untrained},
73 types::Float,
74};
75use std::marker::PhantomData;
76
77#[derive(Debug)]
82pub struct MultiLayerStackingClassifier<State = Untrained> {
83 config: MultiLayerStackingConfig,
84 state: PhantomData<State>,
85 layers_: Option<Vec<StackingLayer>>,
87 final_meta_weights_: Option<Array1<Float>>,
88 final_meta_intercept_: Option<Float>,
89 classes_: Option<Array1<i32>>,
90 n_features_in_: Option<usize>,
91 layer_feature_importances_: Option<Vec<Array1<Float>>>,
92}
93
94#[derive(Debug, Clone)]
96pub struct StackingLayer {
97 pub base_weights: Array2<Float>,
99 pub base_intercepts: Array1<Float>,
101 pub meta_weights: Array1<Float>,
103 pub meta_intercept: Float,
105 pub config: StackingLayerConfig,
107 pub feature_importances: Array1<Float>,
109 pub diversity_scores: Array1<Float>,
111}
112
113impl MultiLayerStackingClassifier<Untrained> {
114 pub fn new(config: MultiLayerStackingConfig) -> Self {
116 Self {
117 config,
118 state: PhantomData,
119 layers_: None,
120 final_meta_weights_: None,
121 final_meta_intercept_: None,
122 classes_: None,
123 n_features_in_: None,
124 layer_feature_importances_: None,
125 }
126 }
127
128 pub fn two_layer(base_estimators: usize, meta_estimators: usize) -> Self {
130 let config = MultiLayerStackingConfig {
131 layers: vec![
132 StackingLayerConfig {
133 n_estimators: base_estimators,
134 meta_strategy: MetaLearningStrategy::Ridge(0.1),
135 use_probabilities: false,
136 cv_folds: 5,
137 passthrough: true,
138 meta_regularization: 0.1,
139 meta_feature_strategy: MetaFeatureStrategy::Raw,
140 polynomial_features: false,
141 polynomial_degree: 2,
142 },
143 StackingLayerConfig {
144 n_estimators: meta_estimators,
145 meta_strategy: MetaLearningStrategy::ElasticNet(0.1, 0.1),
146 use_probabilities: true,
147 cv_folds: 3,
148 passthrough: false,
149 meta_regularization: 0.2,
150 meta_feature_strategy: MetaFeatureStrategy::Statistical,
151 polynomial_features: true,
152 polynomial_degree: 2,
153 },
154 ],
155 random_state: None,
156 final_meta_strategy: MetaLearningStrategy::LogisticRegression,
157 enable_pruning: true,
158 diversity_threshold: 0.1,
159 confidence_weighting: true,
160 };
161 Self::new(config)
162 }
163
164 pub fn deep(n_layers: usize, estimators_per_layer: usize) -> Self {
166 let config = MultiLayerStackingConfig::deep_stacking(n_layers, estimators_per_layer);
167 Self::new(config)
168 }
169
170 pub fn random_state(mut self, seed: u64) -> Self {
172 self.config.random_state = Some(seed);
173 self
174 }
175}
176
177impl Fit<Array2<Float>, Array1<i32>> for MultiLayerStackingClassifier<Untrained> {
178 type Fitted = MultiLayerStackingClassifier<Trained>;
179
180 fn fit(self, x: &Array2<Float>, y: &Array1<i32>) -> Result<Self::Fitted> {
181 if x.nrows() != y.len() {
182 return Err(SklearsError::ShapeMismatch {
183 expected: format!("{} samples", x.nrows()),
184 actual: format!("{} samples", y.len()),
185 });
186 }
187
188 let (n_samples, n_features) = x.dim();
189
190 if n_samples < 20 {
191 return Err(SklearsError::InvalidInput(
192 "Multi-layer stacking requires at least 20 samples".to_string(),
193 ));
194 }
195
196 let mut classes: Vec<i32> = y.to_vec();
198 classes.sort_unstable();
199 classes.dedup();
200 let classes_array = Array1::from_vec(classes.clone());
201 let n_classes = classes.len();
202
203 if n_classes < 2 {
204 return Err(SklearsError::InvalidInput(
205 "Need at least 2 classes for classification".to_string(),
206 ));
207 }
208
209 let y_float: Array1<Float> = y.mapv(|v| v as Float);
211
212 let mut layers = Vec::new();
214 let mut current_features = x.clone();
215 let mut layer_importances = Vec::new();
216
217 for (layer_idx, layer_config) in self.config.layers.iter().enumerate() {
219 let layer =
220 self.train_stacking_layer(¤t_features, &y_float, layer_config, layer_idx)?;
221
222 let meta_features =
224 self.generate_layer_meta_features(¤t_features, &layer, layer_config)?;
225
226 if layer_config.passthrough && layer_idx == 0 {
228 current_features = Array2::zeros((n_samples, meta_features.ncols() + n_features));
229 current_features.slice_mut(s![.., ..n_features]).assign(x);
230 current_features
231 .slice_mut(s![.., n_features..])
232 .assign(&meta_features);
233 } else {
234 current_features = meta_features;
235 }
236
237 layer_importances.push(layer.feature_importances.clone());
238 layers.push(layer);
239 }
240
241 let (final_meta_weights, final_meta_intercept) = self.train_final_meta_learner(
243 ¤t_features,
244 &y_float,
245 &self.config.final_meta_strategy,
246 )?;
247
248 Ok(MultiLayerStackingClassifier {
249 config: self.config,
250 state: PhantomData,
251 layers_: Some(layers),
252 final_meta_weights_: Some(final_meta_weights),
253 final_meta_intercept_: Some(final_meta_intercept),
254 classes_: Some(classes_array),
255 n_features_in_: Some(n_features),
256 layer_feature_importances_: Some(layer_importances),
257 })
258 }
259}
260
261impl MultiLayerStackingClassifier<Untrained> {
262 fn train_stacking_layer(
264 &self,
265 x: &Array2<Float>,
266 y: &Array1<Float>,
267 config: &StackingLayerConfig,
268 layer_idx: usize,
269 ) -> Result<StackingLayer> {
270 let (n_samples, n_features) = x.dim();
271 let base_seed = self.config.random_state.unwrap_or(42) + layer_idx as u64 * 1000;
272
273 let fold_size = n_samples / config.cv_folds;
275 let mut meta_features = Array2::<Float>::zeros((n_samples, config.n_estimators));
276 let mut base_weights = Array2::<Float>::zeros((config.n_estimators, n_features));
277 let mut base_intercepts = Array1::<Float>::zeros(config.n_estimators);
278
279 for fold in 0..config.cv_folds {
281 let start_idx = fold * fold_size;
282 let end_idx = if fold == config.cv_folds - 1 {
283 n_samples
284 } else {
285 (fold + 1) * fold_size
286 };
287
288 let mut train_indices = Vec::new();
290 let mut val_indices = Vec::new();
291
292 for i in 0..n_samples {
293 if i >= start_idx && i < end_idx {
294 val_indices.push(i);
295 } else {
296 train_indices.push(i);
297 }
298 }
299
300 let x_train = self.select_rows(x, &train_indices);
301 let y_train = self.select_elements(y, &train_indices);
302 let x_val = self.select_rows(x, &val_indices);
303
304 for estimator_idx in 0..config.n_estimators {
306 let estimator_seed = base_seed + estimator_idx as u64;
307 let (weights, intercept) = self.train_diverse_estimator(
308 &x_train,
309 &y_train,
310 estimator_seed,
311 estimator_idx,
312 config.meta_regularization,
313 )?;
314
315 if fold == 0 {
317 base_weights.row_mut(estimator_idx).assign(&weights);
318 base_intercepts[estimator_idx] = intercept;
319 }
320
321 for (val_idx, &sample_idx) in val_indices.iter().enumerate() {
323 let x_sample = x_val.row(val_idx).to_owned();
324 let prediction = self.predict_with_weights(&weights, intercept, &x_sample);
325 meta_features[[sample_idx, estimator_idx]] = prediction;
326 }
327 }
328 }
329
330 if self.config.enable_pruning {
332 let diversity_scores = self.calculate_diversity_scores(&meta_features);
333 let pruned_indices =
334 self.prune_ensemble(&diversity_scores, self.config.diversity_threshold);
335
336 meta_features = self.select_columns(&meta_features, &pruned_indices);
337 base_weights = self.select_estimator_rows(&base_weights, &pruned_indices);
338 base_intercepts = self.select_elements(&base_intercepts, &pruned_indices);
339 }
340
341 let (meta_weights, meta_intercept) = self.train_meta_learner_with_strategy(
343 &meta_features,
344 y,
345 &config.meta_strategy,
346 config.meta_regularization,
347 )?;
348
349 let feature_importances = self.calculate_layer_importance(&base_weights, &meta_weights);
351 let diversity_scores = self.calculate_diversity_scores(&meta_features);
352
353 Ok(StackingLayer {
354 base_weights,
355 base_intercepts,
356 meta_weights,
357 meta_intercept,
358 config: config.clone(),
359 feature_importances,
360 diversity_scores,
361 })
362 }
363
364 fn generate_layer_meta_features(
366 &self,
367 x: &Array2<Float>,
368 layer: &StackingLayer,
369 config: &StackingLayerConfig,
370 ) -> Result<Array2<Float>> {
371 let (n_samples, _) = x.dim();
372 let n_estimators = layer.base_weights.nrows();
373
374 let mut base_predictions = Array2::<Float>::zeros((n_samples, n_estimators));
376 for estimator_idx in 0..n_estimators {
377 let weights = layer.base_weights.row(estimator_idx).to_owned();
378 let intercept = layer.base_intercepts[estimator_idx];
379
380 for sample_idx in 0..n_samples {
381 let x_sample = x.row(sample_idx).to_owned();
382 let prediction = if config.use_probabilities {
383 let logit = self.predict_with_weights(&weights, intercept, &x_sample);
384 1.0 / (1.0 + (-logit).exp())
385 } else {
386 self.predict_with_weights(&weights, intercept, &x_sample)
387 };
388 base_predictions[[sample_idx, estimator_idx]] = prediction;
389 }
390 }
391
392 match config.meta_feature_strategy {
394 MetaFeatureStrategy::Raw => Ok(base_predictions),
395 MetaFeatureStrategy::Statistical => {
396 self.generate_statistical_features(&base_predictions)
397 }
398 MetaFeatureStrategy::Interactions => {
399 self.generate_interaction_features(&base_predictions)
400 }
401 MetaFeatureStrategy::ConfidenceBased => {
402 self.generate_confidence_features(&base_predictions)
403 }
404 MetaFeatureStrategy::DiversityBased => {
405 self.generate_diversity_features(&base_predictions)
406 }
407 MetaFeatureStrategy::Comprehensive => {
408 self.generate_comprehensive_features(&base_predictions)
409 }
410 MetaFeatureStrategy::Temporal => self.generate_temporal_features(&base_predictions),
411 MetaFeatureStrategy::Spatial => self.generate_diversity_features(&base_predictions), MetaFeatureStrategy::Spectral => self.generate_spectral_features(&base_predictions),
413 MetaFeatureStrategy::InformationTheoretic => {
414 self.generate_information_theoretic_features(&base_predictions)
415 }
416 MetaFeatureStrategy::NeuralEmbedding => {
417 self.generate_neural_embedding_features(&base_predictions)
418 }
419 MetaFeatureStrategy::KernelBased => self.generate_kernel_features(&base_predictions),
420 MetaFeatureStrategy::BasisExpansion => {
421 self.generate_basis_expansion_features(&base_predictions)
422 }
423 MetaFeatureStrategy::MetaLearning => {
424 self.generate_meta_learning_features(&base_predictions)
425 }
426 }
427 }
428
429 fn generate_statistical_features(&self, predictions: &Array2<Float>) -> Result<Array2<Float>> {
431 let (n_samples, n_estimators) = predictions.dim();
432 let n_features = n_estimators + 4; let mut features = Array2::<Float>::zeros((n_samples, n_features));
434
435 features
437 .slice_mut(s![.., ..n_estimators])
438 .assign(predictions);
439
440 for i in 0..n_samples {
441 let row = predictions.row(i);
442 let mean = row.mean().unwrap_or(0.0);
443 let std = {
444 let variance =
445 row.iter().map(|&x| (x - mean).powi(2)).sum::<Float>() / n_estimators as Float;
446 variance.sqrt()
447 };
448 let min = row.iter().fold(Float::INFINITY, |acc, &x| acc.min(x));
449 let max = row.iter().fold(Float::NEG_INFINITY, |acc, &x| acc.max(x));
450
451 features[[i, n_estimators]] = mean;
452 features[[i, n_estimators + 1]] = std;
453 features[[i, n_estimators + 2]] = min;
454 features[[i, n_estimators + 3]] = max;
455 }
456
457 Ok(features)
458 }
459
460 fn generate_interaction_features(&self, predictions: &Array2<Float>) -> Result<Array2<Float>> {
462 let (n_samples, n_estimators) = predictions.dim();
463 let n_interactions = n_estimators * (n_estimators - 1) / 2;
464 let n_features = n_estimators + n_interactions;
465 let mut features = Array2::<Float>::zeros((n_samples, n_features));
466
467 features
469 .slice_mut(s![.., ..n_estimators])
470 .assign(predictions);
471
472 let mut feature_idx = n_estimators;
473 for i in 0..n_estimators {
474 for j in (i + 1)..n_estimators {
475 for sample_idx in 0..n_samples {
476 features[[sample_idx, feature_idx]] =
477 predictions[[sample_idx, i]] * predictions[[sample_idx, j]];
478 }
479 feature_idx += 1;
480 }
481 }
482
483 Ok(features)
484 }
485
486 fn generate_confidence_features(&self, predictions: &Array2<Float>) -> Result<Array2<Float>> {
488 let (n_samples, n_estimators) = predictions.dim();
489 let n_features = n_estimators + 3; let mut features = Array2::<Float>::zeros((n_samples, n_features));
491
492 features
494 .slice_mut(s![.., ..n_estimators])
495 .assign(predictions);
496
497 for i in 0..n_samples {
498 let row = predictions.row(i);
499
500 let max_prob = row.iter().fold(0.0_f64, |acc, &x| acc.max(x));
502
503 let sum: Float = row.sum();
505 let entropy = if sum > 0.0 {
506 -row.iter()
507 .map(|&x| {
508 let p = x / sum;
509 if p > 0.0 {
510 p * p.ln()
511 } else {
512 0.0
513 }
514 })
515 .sum::<Float>()
516 } else {
517 0.0
518 };
519
520 let mean = row.mean().unwrap_or(0.0);
522 let agreement = 1.0
523 / (1.0
524 + row
525 .iter()
526 .map(|&x| (x - mean).powi(2))
527 .sum::<Float>()
528 .sqrt());
529
530 features[[i, n_estimators]] = max_prob;
531 features[[i, n_estimators + 1]] = entropy;
532 features[[i, n_estimators + 2]] = agreement;
533 }
534
535 Ok(features)
536 }
537
538 fn generate_diversity_features(&self, predictions: &Array2<Float>) -> Result<Array2<Float>> {
540 let (n_samples, n_estimators) = predictions.dim();
541 let n_features = n_estimators + 2; let mut features = Array2::<Float>::zeros((n_samples, n_features));
543
544 features
546 .slice_mut(s![.., ..n_estimators])
547 .assign(predictions);
548
549 for i in 0..n_samples {
550 let row = predictions.row(i);
551
552 let mean = row.mean().unwrap_or(0.0);
554 let std = {
555 let variance =
556 row.iter().map(|&x| (x - mean).powi(2)).sum::<Float>() / n_estimators as Float;
557 variance.sqrt()
558 };
559 let diversity_score = if mean.abs() > 1e-10 {
560 std / mean.abs()
561 } else {
562 0.0
563 };
564
565 let min = row.iter().fold(Float::INFINITY, |acc, &x| acc.min(x));
567 let max = row.iter().fold(Float::NEG_INFINITY, |acc, &x| acc.max(x));
568 let disagreement = max - min;
569
570 features[[i, n_estimators]] = diversity_score;
571 features[[i, n_estimators + 1]] = disagreement;
572 }
573
574 Ok(features)
575 }
576
577 fn generate_comprehensive_features(
579 &self,
580 predictions: &Array2<Float>,
581 ) -> Result<Array2<Float>> {
582 let statistical = self.generate_statistical_features(predictions)?;
583 let interactions = self.generate_interaction_features(predictions)?;
584 let confidence = self.generate_confidence_features(predictions)?;
585 let diversity = self.generate_diversity_features(predictions)?;
586
587 let (n_samples, _) = predictions.dim();
588 let total_features =
589 statistical.ncols() + interactions.ncols() + confidence.ncols() + diversity.ncols()
590 - 3 * predictions.ncols(); let mut comprehensive = Array2::<Float>::zeros((n_samples, total_features));
592
593 let mut col_idx = 0;
594
595 comprehensive
597 .slice_mut(s![.., col_idx..col_idx + statistical.ncols()])
598 .assign(&statistical);
599 col_idx += statistical.ncols();
600
601 let interaction_start = predictions.ncols();
603 let interaction_features = interactions.ncols() - interaction_start;
604 comprehensive
605 .slice_mut(s![.., col_idx..col_idx + interaction_features])
606 .assign(&interactions.slice(s![.., interaction_start..]));
607 col_idx += interaction_features;
608
609 let confidence_start = predictions.ncols();
611 let confidence_features = confidence.ncols() - confidence_start;
612 comprehensive
613 .slice_mut(s![.., col_idx..col_idx + confidence_features])
614 .assign(&confidence.slice(s![.., confidence_start..]));
615 col_idx += confidence_features;
616
617 let diversity_start = predictions.ncols();
619 let diversity_features = diversity.ncols() - diversity_start;
620 comprehensive
621 .slice_mut(s![.., col_idx..col_idx + diversity_features])
622 .assign(&diversity.slice(s![.., diversity_start..]));
623
624 Ok(comprehensive)
625 }
626
627 fn generate_temporal_features(&self, predictions: &Array2<Float>) -> Result<Array2<Float>> {
629 let (n_samples, n_estimators) = predictions.dim();
630 let n_features = n_estimators + 6; let mut features = Array2::<Float>::zeros((n_samples, n_features));
632
633 features
635 .slice_mut(s![.., ..n_estimators])
636 .assign(predictions);
637
638 for i in 0..n_samples {
639 let row = predictions.row(i);
640
641 let lag1_corr = if i > 0 {
643 let prev_row = predictions.row(i - 1);
644 self.calculate_correlation(&row.to_owned(), &prev_row.to_owned())
645 } else {
646 0.0
647 };
648
649 let trend = if i >= 2 {
651 let window_size = 3.min(i + 1);
652 let mut trend_sum = 0.0;
653 for j in 0..n_estimators {
654 let mut values = Vec::new();
655 for k in 0..window_size {
656 values.push(predictions[[i - k, j]]);
657 }
658 let n = values.len() as Float;
660 let x_mean = (n - 1.0) / 2.0;
661 let y_mean = values.iter().sum::<Float>() / n;
662
663 let mut numerator = 0.0;
664 let mut denominator = 0.0;
665 for (idx, &val) in values.iter().enumerate() {
666 let x_diff = idx as Float - x_mean;
667 numerator += x_diff * (val - y_mean);
668 denominator += x_diff * x_diff;
669 }
670
671 if denominator != 0.0 {
672 trend_sum += numerator / denominator;
673 }
674 }
675 trend_sum / n_estimators as Float
676 } else {
677 0.0
678 };
679
680 let volatility = if i >= 4 {
682 let window_size = 5.min(i + 1);
683 let mut vol_sum = 0.0;
684 for j in 0..n_estimators {
685 let mut values = Vec::new();
686 for k in 0..window_size {
687 values.push(predictions[[i - k, j]]);
688 }
689 let mean = values.iter().sum::<Float>() / values.len() as Float;
690 let variance = values.iter().map(|&x| (x - mean).powi(2)).sum::<Float>()
691 / values.len() as Float;
692 vol_sum += variance.sqrt();
693 }
694 vol_sum / n_estimators as Float
695 } else {
696 0.0
697 };
698
699 let momentum = if i >= 1 {
701 let current = row.mean().unwrap_or(0.0);
702 let previous = predictions.row(i - 1).mean().unwrap_or(0.0);
703 current - previous
704 } else {
705 0.0
706 };
707
708 let seasonal = {
710 let period = 12; if i >= period {
712 let current = row.mean().unwrap_or(0.0);
713 let seasonal_lag = predictions.row(i - period).mean().unwrap_or(0.0);
714 current - seasonal_lag
715 } else {
716 0.0
717 }
718 };
719
720 features[[i, n_estimators]] = lag1_corr;
722 features[[i, n_estimators + 1]] = trend;
723 features[[i, n_estimators + 2]] = lag1_corr; features[[i, n_estimators + 3]] = seasonal;
725 features[[i, n_estimators + 4]] = volatility;
726 features[[i, n_estimators + 5]] = momentum;
727 }
728
729 Ok(features)
730 }
731
732 fn generate_spectral_features(&self, predictions: &Array2<Float>) -> Result<Array2<Float>> {
734 let (n_samples, n_estimators) = predictions.dim();
735 let n_features = n_estimators + 4; let mut features = Array2::<Float>::zeros((n_samples, n_features));
737
738 features
740 .slice_mut(s![.., ..n_estimators])
741 .assign(predictions);
742
743 for est_idx in 0..n_estimators {
745 let signal = predictions.column(est_idx).to_owned();
746
747 let dominant_freq = self.find_dominant_frequency(&signal);
749
750 let spectral_centroid = self.calculate_spectral_centroid(&signal);
752
753 let spectral_bandwidth = self.calculate_spectral_bandwidth(&signal, spectral_centroid);
755
756 let spectral_rolloff = self.calculate_spectral_rolloff(&signal);
758
759 for i in 0..n_samples {
761 features[[i, n_estimators]] += dominant_freq / n_estimators as Float;
762 features[[i, n_estimators + 1]] += spectral_centroid / n_estimators as Float;
763 features[[i, n_estimators + 2]] += spectral_bandwidth / n_estimators as Float;
764 features[[i, n_estimators + 3]] += spectral_rolloff / n_estimators as Float;
765 }
766 }
767
768 Ok(features)
769 }
770
771 fn generate_information_theoretic_features(
773 &self,
774 predictions: &Array2<Float>,
775 ) -> Result<Array2<Float>> {
776 let (n_samples, n_estimators) = predictions.dim();
777 let n_features = n_estimators + 5; let mut features = Array2::<Float>::zeros((n_samples, n_features));
779
780 features
782 .slice_mut(s![.., ..n_estimators])
783 .assign(predictions);
784
785 for i in 0..n_samples {
786 let row = predictions.row(i);
787
788 let entropy = self.calculate_shannon_entropy(&row.to_owned());
790
791 let mid = n_estimators / 2;
793 let first_half = row.slice(s![..mid]).to_owned();
794 let second_half = row.slice(s![mid..]).to_owned();
795 let mutual_info = self.calculate_mutual_information(&first_half, &second_half);
796
797 let conditional_entropy = entropy - mutual_info;
799
800 let uniform_prob = 1.0 / n_estimators as Float;
802 let kl_divergence = row
803 .iter()
804 .map(|&p| {
805 let normalized_p = (p + 1e-10) / (row.sum() + 1e-10 * n_estimators as Float);
806 if normalized_p > 0.0 {
807 normalized_p * (normalized_p / uniform_prob).ln()
808 } else {
809 0.0
810 }
811 })
812 .sum::<Float>();
813
814 let joint_entropy = entropy + self.calculate_shannon_entropy(&second_half);
816
817 features[[i, n_estimators]] = entropy;
818 features[[i, n_estimators + 1]] = mutual_info;
819 features[[i, n_estimators + 2]] = conditional_entropy;
820 features[[i, n_estimators + 3]] = kl_divergence;
821 features[[i, n_estimators + 4]] = joint_entropy;
822 }
823
824 Ok(features)
825 }
826
827 fn generate_neural_embedding_features(
829 &self,
830 predictions: &Array2<Float>,
831 ) -> Result<Array2<Float>> {
832 let (n_samples, n_estimators) = predictions.dim();
833 let embedding_dim = 8; let n_features = n_estimators + embedding_dim;
835 let mut features = Array2::<Float>::zeros((n_samples, n_features));
836
837 features
839 .slice_mut(s![.., ..n_estimators])
840 .assign(predictions);
841
842 let embedding_matrix = self.generate_random_embedding_matrix(n_estimators, embedding_dim);
844
845 for i in 0..n_samples {
846 let row = predictions.row(i);
847
848 for j in 0..embedding_dim {
850 let mut embedding_value = 0.0;
851 for k in 0..n_estimators {
852 embedding_value += row[k] * embedding_matrix[[k, j]];
853 }
854 features[[i, n_estimators + j]] = embedding_value.tanh();
856 }
857 }
858
859 Ok(features)
860 }
861
862 fn generate_kernel_features(&self, predictions: &Array2<Float>) -> Result<Array2<Float>> {
864 let (n_samples, n_estimators) = predictions.dim();
865 let n_features = n_estimators + 3; let mut features = Array2::<Float>::zeros((n_samples, n_features));
867
868 features
870 .slice_mut(s![.., ..n_estimators])
871 .assign(predictions);
872
873 let reference_vector = predictions
875 .mean_axis(scirs2_core::ndarray::Axis(0))
876 .unwrap();
877
878 for i in 0..n_samples {
879 let row = predictions.row(i);
880
881 let gamma = 1.0;
883 let rbf_similarity = {
884 let dist_sq = row
885 .iter()
886 .zip(reference_vector.iter())
887 .map(|(&a, &b)| (a - b).powi(2))
888 .sum::<Float>();
889 (-gamma * dist_sq).exp()
890 };
891
892 let degree = 2.0;
894 let poly_similarity = {
895 let dot_product = row.dot(&reference_vector);
896 (1.0 + dot_product).powf(degree)
897 };
898
899 let cosine_similarity = {
901 let dot_product = row.dot(&reference_vector);
902 let row_norm = row.dot(&row.to_owned()).sqrt();
903 let ref_norm = reference_vector.dot(&reference_vector).sqrt();
904 if row_norm > 0.0 && ref_norm > 0.0 {
905 dot_product / (row_norm * ref_norm)
906 } else {
907 0.0
908 }
909 };
910
911 features[[i, n_estimators]] = rbf_similarity;
912 features[[i, n_estimators + 1]] = poly_similarity;
913 features[[i, n_estimators + 2]] = cosine_similarity;
914 }
915
916 Ok(features)
917 }
918
919 fn generate_basis_expansion_features(
921 &self,
922 predictions: &Array2<Float>,
923 ) -> Result<Array2<Float>> {
924 let (n_samples, n_estimators) = predictions.dim();
925 let n_basis = 6; let n_features = n_estimators + n_basis;
927 let mut features = Array2::<Float>::zeros((n_samples, n_features));
928
929 features
931 .slice_mut(s![.., ..n_estimators])
932 .assign(predictions);
933
934 for i in 0..n_samples {
935 let row = predictions.row(i);
936 let mean_pred = row.mean().unwrap_or(0.0);
937
938 let x = (mean_pred * 2.0 - 1.0).max(-1.0).min(1.0); let p0 = 1.0;
943 let p1 = x;
944 let p2 = 0.5 * (3.0 * x * x - 1.0);
945 let p3 = 0.5 * (5.0 * x * x * x - 3.0 * x);
946 let p4 = 0.125 * (35.0 * x.powi(4) - 30.0 * x * x + 3.0);
947 let p5 = 0.125 * (63.0 * x.powi(5) - 70.0 * x.powi(3) + 15.0 * x);
948
949 features[[i, n_estimators]] = p0;
950 features[[i, n_estimators + 1]] = p1;
951 features[[i, n_estimators + 2]] = p2;
952 features[[i, n_estimators + 3]] = p3;
953 features[[i, n_estimators + 4]] = p4;
954 features[[i, n_estimators + 5]] = p5;
955 }
956
957 Ok(features)
958 }
959
960 fn generate_meta_learning_features(
962 &self,
963 predictions: &Array2<Float>,
964 ) -> Result<Array2<Float>> {
965 let (n_samples, n_estimators) = predictions.dim();
966 let n_features = n_estimators + 4; let mut features = Array2::<Float>::zeros((n_samples, n_features));
968
969 features
971 .slice_mut(s![.., ..n_estimators])
972 .assign(predictions);
973
974 for i in 0..n_samples {
975 let row = predictions.row(i);
976
977 let mean = row.mean().unwrap_or(0.0);
979 let variance =
980 row.iter().map(|&x| (x - mean).powi(2)).sum::<Float>() / n_estimators as Float;
981 let model_complexity = variance.sqrt();
982
983 let prediction_stability = 1.0 / (1.0 + variance.sqrt());
985
986 let majority_prediction = if mean > 0.5 { 1.0 } else { 0.0 };
988 let agreement_count = row
989 .iter()
990 .map(|&pred| {
991 let binary_pred = if pred > 0.5 { 1.0 } else { 0.0 };
992 if binary_pred == majority_prediction {
993 1.0
994 } else {
995 0.0
996 }
997 })
998 .sum::<Float>();
999 let ensemble_agreement = agreement_count / n_estimators as Float;
1000
1001 let learning_curve = if i > 0 {
1003 let current_variance = variance;
1004 let prev_mean = predictions.row(i - 1).mean().unwrap_or(0.0);
1005 let prev_variance = predictions
1006 .row(i - 1)
1007 .iter()
1008 .map(|&x| (x - prev_mean).powi(2))
1009 .sum::<Float>()
1010 / n_estimators as Float;
1011
1012 if prev_variance > 0.0 {
1014 (prev_variance - current_variance) / prev_variance
1015 } else {
1016 0.0
1017 }
1018 } else {
1019 0.0
1020 };
1021
1022 features[[i, n_estimators]] = model_complexity;
1023 features[[i, n_estimators + 1]] = prediction_stability;
1024 features[[i, n_estimators + 2]] = ensemble_agreement;
1025 features[[i, n_estimators + 3]] = learning_curve;
1026 }
1027
1028 Ok(features)
1029 }
1030
1031 fn find_dominant_frequency(&self, signal: &Array1<Float>) -> Float {
1033 let n = signal.len();
1035 if n < 3 {
1036 return 0.0;
1037 }
1038
1039 let mut peak_count = 0;
1040 for i in 1..(n - 1) {
1041 if signal[i] > signal[i - 1] && signal[i] > signal[i + 1] {
1042 peak_count += 1;
1043 }
1044 }
1045
1046 peak_count as Float / n as Float
1047 }
1048
1049 fn calculate_spectral_centroid(&self, signal: &Array1<Float>) -> Float {
1050 let n = signal.len();
1051 if n == 0 {
1052 return 0.0;
1053 }
1054
1055 let weighted_sum: Float = signal
1056 .iter()
1057 .enumerate()
1058 .map(|(i, &val)| (i + 1) as Float * val.abs())
1059 .sum();
1060 let total_energy: Float = signal.iter().map(|&val| val.abs()).sum();
1061
1062 if total_energy > 0.0 {
1063 weighted_sum / total_energy
1064 } else {
1065 0.0
1066 }
1067 }
1068
1069 fn calculate_spectral_bandwidth(&self, signal: &Array1<Float>, centroid: Float) -> Float {
1070 let n = signal.len();
1071 if n == 0 {
1072 return 0.0;
1073 }
1074
1075 let weighted_variance: Float = signal
1076 .iter()
1077 .enumerate()
1078 .map(|(i, &val)| ((i + 1) as Float - centroid).powi(2) * val.abs())
1079 .sum();
1080 let total_energy: Float = signal.iter().map(|&val| val.abs()).sum();
1081
1082 if total_energy > 0.0 {
1083 (weighted_variance / total_energy).sqrt()
1084 } else {
1085 0.0
1086 }
1087 }
1088
1089 fn calculate_spectral_rolloff(&self, signal: &Array1<Float>) -> Float {
1090 let total_energy: Float = signal.iter().map(|&val| val.abs()).sum();
1091 if total_energy == 0.0 {
1092 return 0.0;
1093 }
1094
1095 let target_energy = 0.85 * total_energy;
1096 let mut cumulative_energy = 0.0;
1097
1098 for (i, &val) in signal.iter().enumerate() {
1099 cumulative_energy += val.abs();
1100 if cumulative_energy >= target_energy {
1101 return (i + 1) as Float / signal.len() as Float;
1102 }
1103 }
1104
1105 1.0
1106 }
1107
1108 fn calculate_shannon_entropy(&self, values: &Array1<Float>) -> Float {
1109 let sum = values.sum();
1110 if sum == 0.0 {
1111 return 0.0;
1112 }
1113
1114 -values
1115 .iter()
1116 .map(|&val| {
1117 let p = (val + 1e-10) / (sum + 1e-10 * values.len() as Float);
1118 if p > 0.0 {
1119 p * p.ln()
1120 } else {
1121 0.0
1122 }
1123 })
1124 .sum::<Float>()
1125 }
1126
1127 fn calculate_mutual_information(&self, x: &Array1<Float>, y: &Array1<Float>) -> Float {
1128 let entropy_x = self.calculate_shannon_entropy(x);
1130 let entropy_y = self.calculate_shannon_entropy(y);
1131
1132 let mut joint_values = Array1::<Float>::zeros(x.len());
1134 for i in 0..x.len() {
1135 joint_values[i] = x[i] + y[i % y.len()];
1136 }
1137 let joint_entropy = self.calculate_shannon_entropy(&joint_values);
1138
1139 entropy_x + entropy_y - joint_entropy
1140 }
1141
1142 fn generate_random_embedding_matrix(
1143 &self,
1144 input_dim: usize,
1145 output_dim: usize,
1146 ) -> Array2<Float> {
1147 let mut matrix = Array2::<Float>::zeros((input_dim, output_dim));
1148
1149 let scale = (2.0 / (input_dim + output_dim) as Float).sqrt();
1151
1152 for i in 0..input_dim {
1153 for j in 0..output_dim {
1154 let seed = (i * output_dim + j) as f64;
1156 let random_val = ((seed * 9.0).sin() * 43758.5453).fract();
1157 matrix[[i, j]] = (random_val * 2.0 - 1.0) * scale;
1158 }
1159 }
1160
1161 matrix
1162 }
1163
1164 fn train_final_meta_learner(
1166 &self,
1167 x: &Array2<Float>,
1168 y: &Array1<Float>,
1169 strategy: &MetaLearningStrategy,
1170 ) -> Result<(Array1<Float>, Float)> {
1171 match strategy {
1172 MetaLearningStrategy::LinearRegression => self.train_linear_regression(x, y, 0.0),
1173 MetaLearningStrategy::Ridge(alpha) => self.train_linear_regression(x, y, *alpha),
1174 MetaLearningStrategy::Lasso(alpha) => self.train_lasso_regression(x, y, *alpha),
1175 MetaLearningStrategy::ElasticNet(l1, l2) => {
1176 self.train_elastic_net_regression(x, y, *l1, *l2)
1177 }
1178 MetaLearningStrategy::LogisticRegression => self.train_logistic_regression(x, y),
1179 MetaLearningStrategy::BayesianAveraging => self.train_bayesian_averaging(x, y),
1180 _ => {
1181 self.train_linear_regression(x, y, 0.1)
1183 }
1184 }
1185 }
1186
1187 fn train_linear_regression(
1189 &self,
1190 x: &Array2<Float>,
1191 y: &Array1<Float>,
1192 alpha: Float,
1193 ) -> Result<(Array1<Float>, Float)> {
1194 let (n_samples, n_features) = x.dim();
1195
1196 let mut x_with_intercept = Array2::<Float>::ones((n_samples, n_features + 1));
1198 x_with_intercept.slice_mut(s![.., ..n_features]).assign(x);
1199
1200 let xt = x_with_intercept.t();
1202 let mut xtx = xt.dot(&x_with_intercept);
1203
1204 for i in 0..n_features {
1206 xtx[[i, i]] += alpha;
1207 }
1208
1209 let xty = xt.dot(y);
1210
1211 let coefficients = self.solve_linear_system(&xtx, &xty)?;
1213
1214 let weights = coefficients.slice(s![..n_features]).to_owned();
1215 let intercept = coefficients[n_features];
1216
1217 Ok((weights, intercept))
1218 }
1219
1220 fn train_lasso_regression(
1221 &self,
1222 x: &Array2<Float>,
1223 y: &Array1<Float>,
1224 alpha: Float,
1225 ) -> Result<(Array1<Float>, Float)> {
1226 let (n_samples, n_features) = x.dim();
1228 let mut weights = Array1::<Float>::zeros(n_features);
1229 let mut intercept = y.mean().unwrap_or(0.0);
1230
1231 for _ in 0..100 {
1233 for j in 0..n_features {
1235 let x_j = x.column(j).to_owned();
1236 let residual: Array1<Float> =
1237 y - &(x.dot(&weights) + intercept - &(weights[j] * &x_j));
1238 let correlation = x_j.dot(&residual) / n_samples as Float;
1239 let norm_sq = x_j.dot(&x_j) / n_samples as Float;
1240
1241 weights[j] = if correlation > alpha {
1243 (correlation - alpha) / norm_sq
1244 } else if correlation < -alpha {
1245 (correlation + alpha) / norm_sq
1246 } else {
1247 0.0
1248 };
1249 }
1250
1251 let predictions = x.dot(&weights);
1253 intercept = (y - &predictions).mean().unwrap_or(0.0);
1254 }
1255
1256 Ok((weights, intercept))
1257 }
1258
1259 fn train_elastic_net_regression(
1260 &self,
1261 x: &Array2<Float>,
1262 y: &Array1<Float>,
1263 l1_alpha: Float,
1264 l2_alpha: Float,
1265 ) -> Result<(Array1<Float>, Float)> {
1266 let (n_samples, n_features) = x.dim();
1268 let mut weights = Array1::<Float>::zeros(n_features);
1269 let mut intercept = y.mean().unwrap_or(0.0);
1270
1271 for _ in 0..100 {
1273 for j in 0..n_features {
1274 let x_j = x.column(j).to_owned();
1275 let residual: Array1<Float> =
1276 y - &(x.dot(&weights) + intercept - &(weights[j] * &x_j));
1277 let correlation = x_j.dot(&residual) / n_samples as Float;
1278 let norm_sq = x_j.dot(&x_j) / n_samples as Float + l2_alpha;
1279
1280 weights[j] = if correlation > l1_alpha {
1282 (correlation - l1_alpha) / norm_sq
1283 } else if correlation < -l1_alpha {
1284 (correlation + l1_alpha) / norm_sq
1285 } else {
1286 0.0
1287 };
1288 }
1289
1290 let predictions = x.dot(&weights);
1291 intercept = (y - &predictions).mean().unwrap_or(0.0);
1292 }
1293
1294 Ok((weights, intercept))
1295 }
1296
1297 fn train_logistic_regression(
1298 &self,
1299 x: &Array2<Float>,
1300 y: &Array1<Float>,
1301 ) -> Result<(Array1<Float>, Float)> {
1302 let (n_samples, n_features) = x.dim();
1304 let mut weights = Array1::<Float>::zeros(n_features);
1305 let mut intercept = 0.0;
1306 let learning_rate = 0.01;
1307
1308 for _ in 0..1000 {
1309 let logits = x.dot(&weights) + intercept;
1311 let probabilities = logits.mapv(|z| 1.0 / (1.0 + (-z).exp()));
1312 let errors = y - &probabilities;
1313
1314 let weight_gradients = x.t().dot(&errors) / n_samples as Float;
1316 weights = &weights + &(&weight_gradients * learning_rate);
1317
1318 let intercept_gradient = errors.sum() / n_samples as Float;
1320 intercept += intercept_gradient * learning_rate;
1321 }
1322
1323 Ok((weights, intercept))
1324 }
1325
1326 fn train_bayesian_averaging(
1327 &self,
1328 x: &Array2<Float>,
1329 y: &Array1<Float>,
1330 ) -> Result<(Array1<Float>, Float)> {
1331 let (_, n_features) = x.dim();
1333
1334 let (weights, intercept) = self.train_linear_regression(x, y, 0.1)?;
1336
1337 let noise_factor = 0.01;
1339 let bayesian_weights = weights.mapv(|w| w * (1.0 + noise_factor));
1340
1341 Ok((bayesian_weights, intercept))
1342 }
1343
1344 fn solve_linear_system(&self, a: &Array2<Float>, b: &Array1<Float>) -> Result<Array1<Float>> {
1346 let n = a.nrows();
1348 let mut augmented = Array2::<Float>::zeros((n, n + 1));
1349 augmented.slice_mut(s![.., ..n]).assign(a);
1350 augmented.slice_mut(s![.., n]).assign(b);
1351
1352 for i in 0..n {
1354 for j in (i + 1)..n {
1355 if augmented[[i, i]].abs() < 1e-10 {
1356 continue;
1357 }
1358 let factor = augmented[[j, i]] / augmented[[i, i]];
1359 for k in i..=n {
1360 augmented[[j, k]] -= factor * augmented[[i, k]];
1361 }
1362 }
1363 }
1364
1365 let mut solution = Array1::<Float>::zeros(n);
1367 for i in (0..n).rev() {
1368 let mut sum = 0.0;
1369 for j in (i + 1)..n {
1370 sum += augmented[[i, j]] * solution[j];
1371 }
1372 if augmented[[i, i]].abs() < 1e-10 {
1373 solution[i] = 0.0;
1374 } else {
1375 solution[i] = (augmented[[i, n]] - sum) / augmented[[i, i]];
1376 }
1377 }
1378
1379 Ok(solution)
1380 }
1381
1382 fn select_rows(&self, array: &Array2<Float>, indices: &[usize]) -> Array2<Float> {
1384 let (_, n_cols) = array.dim();
1385 let mut result = Array2::<Float>::zeros((indices.len(), n_cols));
1386 for (i, &idx) in indices.iter().enumerate() {
1387 result.row_mut(i).assign(&array.row(idx));
1388 }
1389 result
1390 }
1391
1392 fn select_elements(&self, array: &Array1<Float>, indices: &[usize]) -> Array1<Float> {
1393 let mut result = Array1::<Float>::zeros(indices.len());
1394 for (i, &idx) in indices.iter().enumerate() {
1395 result[i] = array[idx];
1396 }
1397 result
1398 }
1399
1400 fn predict_with_weights(
1401 &self,
1402 weights: &Array1<Float>,
1403 intercept: Float,
1404 x: &Array1<Float>,
1405 ) -> Float {
1406 weights.dot(x) + intercept
1407 }
1408
1409 fn train_diverse_estimator(
1410 &self,
1411 x: &Array2<Float>,
1412 y: &Array1<Float>,
1413 seed: u64,
1414 estimator_idx: usize,
1415 regularization: Float,
1416 ) -> Result<(Array1<Float>, Float)> {
1417 let diverse_reg = regularization * (1.0 + 0.1 * estimator_idx as Float);
1419 self.train_linear_regression(x, y, diverse_reg)
1420 }
1421
1422 fn calculate_diversity_scores(&self, predictions: &Array2<Float>) -> Array1<Float> {
1423 let (_, n_estimators) = predictions.dim();
1424 let mut diversity = Array1::<Float>::zeros(n_estimators);
1425
1426 for i in 0..n_estimators {
1427 let mut total_diversity = 0.0;
1428 for j in 0..n_estimators {
1429 if i != j {
1430 let corr = self.calculate_correlation(
1431 &predictions.column(i).to_owned(),
1432 &predictions.column(j).to_owned(),
1433 );
1434 total_diversity += 1.0 - corr.abs();
1435 }
1436 }
1437 diversity[i] = total_diversity / (n_estimators - 1) as Float;
1438 }
1439
1440 diversity
1441 }
1442
1443 fn calculate_correlation(&self, x: &Array1<Float>, y: &Array1<Float>) -> Float {
1444 let x_mean = x.mean().unwrap_or(0.0);
1445 let y_mean = y.mean().unwrap_or(0.0);
1446
1447 let mut numerator = 0.0;
1448 let mut x_var = 0.0;
1449 let mut y_var = 0.0;
1450
1451 for i in 0..x.len() {
1452 let x_diff = x[i] - x_mean;
1453 let y_diff = y[i] - y_mean;
1454 numerator += x_diff * y_diff;
1455 x_var += x_diff * x_diff;
1456 y_var += y_diff * y_diff;
1457 }
1458
1459 if x_var == 0.0 || y_var == 0.0 {
1460 0.0
1461 } else {
1462 numerator / (x_var * y_var).sqrt()
1463 }
1464 }
1465
1466 fn prune_ensemble(&self, diversity_scores: &Array1<Float>, threshold: Float) -> Vec<usize> {
1467 diversity_scores
1468 .iter()
1469 .enumerate()
1470 .filter(|(_, &score)| score > threshold)
1471 .map(|(idx, _)| idx)
1472 .collect()
1473 }
1474
1475 fn select_columns(&self, array: &Array2<Float>, indices: &[usize]) -> Array2<Float> {
1476 let (n_rows, _) = array.dim();
1477 let mut result = Array2::<Float>::zeros((n_rows, indices.len()));
1478 for (j, &idx) in indices.iter().enumerate() {
1479 result.column_mut(j).assign(&array.column(idx));
1480 }
1481 result
1482 }
1483
1484 fn select_estimator_rows(&self, array: &Array2<Float>, indices: &[usize]) -> Array2<Float> {
1485 let (_, n_cols) = array.dim();
1486 let mut result = Array2::<Float>::zeros((indices.len(), n_cols));
1487 for (i, &idx) in indices.iter().enumerate() {
1488 result.row_mut(i).assign(&array.row(idx));
1489 }
1490 result
1491 }
1492
1493 fn calculate_layer_importance(
1494 &self,
1495 base_weights: &Array2<Float>,
1496 meta_weights: &Array1<Float>,
1497 ) -> Array1<Float> {
1498 let (_, n_features) = base_weights.dim();
1499 let mut importance = Array1::<Float>::zeros(n_features);
1500
1501 for i in 0..n_features {
1502 let feature_column = base_weights.column(i);
1503 importance[i] = feature_column.dot(meta_weights).abs();
1504 }
1505
1506 let sum = importance.sum();
1508 if sum > 0.0 {
1509 importance /= sum;
1510 }
1511
1512 importance
1513 }
1514
1515 fn train_meta_learner_with_strategy(
1516 &self,
1517 x: &Array2<Float>,
1518 y: &Array1<Float>,
1519 strategy: &MetaLearningStrategy,
1520 regularization: Float,
1521 ) -> Result<(Array1<Float>, Float)> {
1522 match strategy {
1523 MetaLearningStrategy::LinearRegression => self.train_linear_regression(x, y, 0.0),
1524 MetaLearningStrategy::Ridge(_) => self.train_linear_regression(x, y, regularization),
1525 _ => {
1526 self.train_linear_regression(x, y, regularization)
1528 }
1529 }
1530 }
1531}
1532
1533impl Predict<Array2<Float>, Array1<i32>> for MultiLayerStackingClassifier<Trained> {
1534 fn predict(&self, x: &Array2<Float>) -> Result<Array1<i32>> {
1535 let probabilities = self.predict_proba(x)?;
1536 let classes = self.classes_.as_ref().unwrap();
1537
1538 let predictions = probabilities
1539 .axis_iter(Axis(0))
1540 .map(|row| {
1541 let max_idx = row
1542 .iter()
1543 .enumerate()
1544 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
1545 .map(|(idx, _)| idx)
1546 .unwrap_or(0);
1547 classes[max_idx]
1548 })
1549 .collect::<Vec<_>>();
1550
1551 Ok(Array1::from_vec(predictions))
1552 }
1553}
1554
1555impl MultiLayerStackingClassifier<Trained> {
1556 pub fn predict_proba(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
1558 let (n_samples, _) = x.dim();
1559 let n_classes = self.classes_.as_ref().unwrap().len();
1560
1561 let mut current_features = x.clone();
1563 let layers = self.layers_.as_ref().unwrap();
1564
1565 for (layer_idx, layer) in layers.iter().enumerate() {
1566 let meta_features = self.generate_layer_predictions(¤t_features, layer)?;
1567
1568 if layer.config.passthrough && layer_idx == 0 {
1570 let original_features = current_features.ncols();
1571 let mut combined =
1572 Array2::zeros((n_samples, meta_features.ncols() + original_features));
1573 combined
1574 .slice_mut(s![.., ..original_features])
1575 .assign(¤t_features);
1576 combined
1577 .slice_mut(s![.., original_features..])
1578 .assign(&meta_features);
1579 current_features = combined;
1580 } else {
1581 current_features = meta_features;
1582 }
1583 }
1584
1585 let final_weights = self.final_meta_weights_.as_ref().unwrap();
1587 let final_intercept = self.final_meta_intercept_.unwrap();
1588
1589 let logits = current_features.dot(final_weights) + final_intercept;
1590 let probabilities = logits.mapv(|z| 1.0 / (1.0 + (-z).exp()));
1591
1592 let mut result = Array2::<Float>::zeros((n_samples, n_classes));
1594 for i in 0..n_samples {
1595 let prob = probabilities[i];
1596 if n_classes == 2 {
1597 result[[i, 0]] = 1.0 - prob;
1598 result[[i, 1]] = prob;
1599 } else {
1600 let base_prob = (1.0 - prob) / (n_classes - 1) as Float;
1602 for j in 0..n_classes {
1603 result[[i, j]] = if j == 0 { prob } else { base_prob };
1604 }
1605 }
1606 }
1607
1608 Ok(result)
1609 }
1610
1611 fn generate_layer_predictions(
1613 &self,
1614 x: &Array2<Float>,
1615 layer: &StackingLayer,
1616 ) -> Result<Array2<Float>> {
1617 let (n_samples, _) = x.dim();
1618 let n_estimators = layer.base_weights.nrows();
1619 let mut predictions = Array2::<Float>::zeros((n_samples, n_estimators));
1620
1621 for estimator_idx in 0..n_estimators {
1622 let weights = layer.base_weights.row(estimator_idx);
1623 let intercept = layer.base_intercepts[estimator_idx];
1624
1625 for sample_idx in 0..n_samples {
1626 let x_sample = x.row(sample_idx);
1627 let prediction = if layer.config.use_probabilities {
1628 let logit = weights.dot(&x_sample) + intercept;
1629 1.0 / (1.0 + (-logit).exp()) } else {
1631 weights.dot(&x_sample) + intercept
1632 };
1633 predictions[[sample_idx, estimator_idx]] = prediction;
1634 }
1635 }
1636
1637 Ok(predictions)
1638 }
1639
1640 pub fn get_layer_feature_importances(&self) -> Option<&Vec<Array1<Float>>> {
1642 self.layer_feature_importances_.as_ref()
1643 }
1644
1645 pub fn get_diversity_scores(&self) -> Option<&Array1<Float>> {
1647 self.layers_
1648 .as_ref()?
1649 .last()
1650 .map(|layer| &layer.diversity_scores)
1651 }
1652
1653 pub fn n_features_in(&self) -> usize {
1655 self.n_features_in_.unwrap_or(0)
1656 }
1657
1658 pub fn classes(&self) -> &Array1<i32> {
1660 self.classes_.as_ref().unwrap()
1661 }
1662
1663 pub fn config(&self) -> &MultiLayerStackingConfig {
1665 &self.config
1666 }
1667
1668 pub fn layers(&self) -> Option<&Vec<StackingLayer>> {
1670 self.layers_.as_ref()
1671 }
1672}
1673
1674#[allow(non_snake_case)]
1675#[cfg(test)]
1676mod tests {
1677 use super::*;
1678 use scirs2_core::ndarray::array;
1679
1680 #[test]
1681 fn test_multi_layer_stacking_creation() {
1682 let config = MultiLayerStackingConfig::new();
1683 let stacking = MultiLayerStackingClassifier::new(config);
1684
1685 assert!(stacking.layers_.is_none());
1686 assert!(stacking.final_meta_weights_.is_none());
1687 assert!(stacking.classes_.is_none());
1688 }
1689
1690 #[test]
1691 fn test_two_layer_creation() {
1692 let stacking = MultiLayerStackingClassifier::two_layer(3, 2);
1693
1694 assert_eq!(stacking.config.layers.len(), 2);
1695 assert_eq!(stacking.config.layers[0].n_estimators, 3);
1696 assert_eq!(stacking.config.layers[1].n_estimators, 2);
1697 assert!(stacking.config.enable_pruning);
1698 assert!(stacking.config.confidence_weighting);
1699 }
1700
1701 #[test]
1702 fn test_deep_stacking_creation() {
1703 let stacking = MultiLayerStackingClassifier::deep(3, 5);
1704
1705 assert_eq!(stacking.config.layers.len(), 3);
1706 assert!(stacking
1707 .config
1708 .layers
1709 .iter()
1710 .all(|layer| layer.n_estimators == 5));
1711 assert!(stacking.config.enable_pruning);
1712 assert_eq!(stacking.config.diversity_threshold, 0.15);
1713 }
1714
1715 #[test]
1766 fn test_advanced_meta_features() {
1767 let predictions = array![[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9]];
1768
1769 let config = MultiLayerStackingConfig::new();
1770 let stacking = MultiLayerStackingClassifier::new(config);
1771
1772 let statistical = stacking
1774 .generate_statistical_features(&predictions)
1775 .unwrap();
1776 assert_eq!(statistical.dim(), (3, 7)); let interactions = stacking
1780 .generate_interaction_features(&predictions)
1781 .unwrap();
1782 assert_eq!(interactions.dim(), (3, 6)); let confidence = stacking.generate_confidence_features(&predictions).unwrap();
1786 assert_eq!(confidence.dim(), (3, 6)); }
1788
1789 #[test]
1790 fn test_diversity_calculation() {
1791 let predictions = array![[0.1, 0.9, 0.5], [0.2, 0.8, 0.4], [0.3, 0.7, 0.6]];
1792
1793 let config = MultiLayerStackingConfig::new();
1794 let stacking = MultiLayerStackingClassifier::new(config);
1795
1796 let diversity_scores = stacking.calculate_diversity_scores(&predictions);
1797 assert_eq!(diversity_scores.len(), 3);
1798
1799 assert!(diversity_scores.iter().all(|&score| score >= 0.0));
1801 }
1802
1803 #[test]
1804 fn test_ensemble_pruning() {
1805 let diversity_scores = array![0.1, 0.5, 0.3, 0.8, 0.2];
1806 let threshold = 0.25;
1807
1808 let config = MultiLayerStackingConfig::new();
1809 let stacking = MultiLayerStackingClassifier::new(config);
1810
1811 let pruned_indices = stacking.prune_ensemble(&diversity_scores, threshold);
1812 let expected = vec![1, 2, 3]; assert_eq!(pruned_indices, expected);
1814 }
1815
1816 #[test]
1817 fn test_spectral_features() {
1818 let predictions = array![
1819 [0.1, 0.2, 0.3, 0.4],
1820 [0.5, 0.6, 0.7, 0.8],
1821 [0.9, 0.1, 0.2, 0.3]
1822 ];
1823
1824 let config = MultiLayerStackingConfig::new();
1825 let stacking = MultiLayerStackingClassifier::new(config);
1826
1827 let spectral = stacking.generate_spectral_features(&predictions).unwrap();
1828 assert_eq!(spectral.dim(), (3, 8)); }
1830
1831 #[test]
1832 fn test_information_theoretic_features() {
1833 let predictions = array![
1834 [0.1, 0.2, 0.3, 0.4],
1835 [0.5, 0.6, 0.7, 0.8],
1836 [0.9, 0.1, 0.2, 0.3]
1837 ];
1838
1839 let config = MultiLayerStackingConfig::new();
1840 let stacking = MultiLayerStackingClassifier::new(config);
1841
1842 let info_theoretic = stacking
1843 .generate_information_theoretic_features(&predictions)
1844 .unwrap();
1845 assert_eq!(info_theoretic.dim(), (3, 9)); }
1847
1848 #[test]
1849 fn test_neural_embedding_features() {
1850 let predictions = array![[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9]];
1851
1852 let config = MultiLayerStackingConfig::new();
1853 let stacking = MultiLayerStackingClassifier::new(config);
1854
1855 let neural_embedding = stacking
1856 .generate_neural_embedding_features(&predictions)
1857 .unwrap();
1858 assert_eq!(neural_embedding.dim(), (3, 11)); }
1860
1861 #[test]
1862 fn test_kernel_features() {
1863 let predictions = array![[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9]];
1864
1865 let config = MultiLayerStackingConfig::new();
1866 let stacking = MultiLayerStackingClassifier::new(config);
1867
1868 let kernel_features = stacking.generate_kernel_features(&predictions).unwrap();
1869 assert_eq!(kernel_features.dim(), (3, 6)); }
1871
1872 #[test]
1873 fn test_basis_expansion_features() {
1874 let predictions = array![[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9]];
1875
1876 let config = MultiLayerStackingConfig::new();
1877 let stacking = MultiLayerStackingClassifier::new(config);
1878
1879 let basis_expansion = stacking
1880 .generate_basis_expansion_features(&predictions)
1881 .unwrap();
1882 assert_eq!(basis_expansion.dim(), (3, 9)); }
1884
1885 #[test]
1886 fn test_meta_learning_features() {
1887 let predictions = array![[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9]];
1888
1889 let config = MultiLayerStackingConfig::new();
1890 let stacking = MultiLayerStackingClassifier::new(config);
1891
1892 let meta_learning = stacking
1893 .generate_meta_learning_features(&predictions)
1894 .unwrap();
1895 assert_eq!(meta_learning.dim(), (3, 7)); }
1897
1898 #[test]
1899 fn test_comprehensive_features() {
1900 let predictions = array![[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9]];
1901
1902 let config = MultiLayerStackingConfig::new();
1903 let stacking = MultiLayerStackingClassifier::new(config);
1904
1905 let comprehensive = stacking
1906 .generate_comprehensive_features(&predictions)
1907 .unwrap();
1908 let expected_features = 3 + 4 + 3 + 3 + 2; assert_eq!(comprehensive.ncols(), expected_features);
1912 }
1913
1914 #[test]
1915 fn test_regression_methods() {
1916 let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0]];
1917 let y = array![1.0, 2.0, 3.0, 4.0];
1918
1919 let config = MultiLayerStackingConfig::new();
1920 let stacking = MultiLayerStackingClassifier::new(config);
1921
1922 let (weights, intercept) = stacking.train_linear_regression(&x, &y, 0.0).unwrap();
1924 assert_eq!(weights.len(), 2);
1925 assert!(intercept.is_finite());
1926
1927 let (ridge_weights, ridge_intercept) =
1929 stacking.train_linear_regression(&x, &y, 0.1).unwrap();
1930 assert_eq!(ridge_weights.len(), 2);
1931 assert!(ridge_intercept.is_finite());
1932
1933 let (lasso_weights, lasso_intercept) =
1935 stacking.train_lasso_regression(&x, &y, 0.1).unwrap();
1936 assert_eq!(lasso_weights.len(), 2);
1937 assert!(lasso_intercept.is_finite());
1938
1939 let (en_weights, en_intercept) = stacking
1941 .train_elastic_net_regression(&x, &y, 0.1, 0.1)
1942 .unwrap();
1943 assert_eq!(en_weights.len(), 2);
1944 assert!(en_intercept.is_finite());
1945 }
1946
1947 #[test]
1948 fn test_insufficient_samples() {
1949 let x = array![[1.0, 2.0], [3.0, 4.0]]; let y = array![0, 1];
1951
1952 let stacking = MultiLayerStackingClassifier::two_layer(2, 1);
1953 let result = stacking.fit(&x, &y);
1954
1955 assert!(result.is_err());
1956 assert!(result
1957 .unwrap_err()
1958 .to_string()
1959 .contains("at least 20 samples"));
1960 }
1961
1962 #[test]
1963 fn test_insufficient_classes() {
1964 let x = array![
1965 [1.0, 2.0],
1966 [3.0, 4.0],
1967 [5.0, 6.0],
1968 [7.0, 8.0],
1969 [9.0, 10.0],
1970 [11.0, 12.0],
1971 [13.0, 14.0],
1972 [15.0, 16.0],
1973 [17.0, 18.0],
1974 [19.0, 20.0],
1975 [21.0, 22.0],
1976 [23.0, 24.0],
1977 [25.0, 26.0],
1978 [27.0, 28.0],
1979 [29.0, 30.0],
1980 [31.0, 32.0],
1981 [33.0, 34.0],
1982 [35.0, 36.0],
1983 [37.0, 38.0],
1984 [39.0, 40.0]
1985 ];
1986 let y = array![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; let stacking = MultiLayerStackingClassifier::two_layer(2, 1);
1989 let result = stacking.fit(&x, &y);
1990
1991 assert!(result.is_err());
1992 assert!(result
1993 .unwrap_err()
1994 .to_string()
1995 .contains("at least 2 classes"));
1996 }
1997
1998 #[test]
1999 fn test_shape_mismatch() {
2000 let x = array![[1.0, 2.0], [3.0, 4.0]];
2001 let y = array![0]; let stacking = MultiLayerStackingClassifier::two_layer(1, 1);
2004 let result = stacking.fit(&x, &y);
2005
2006 assert!(result.is_err());
2007 assert!(result.unwrap_err().to_string().contains("Shape mismatch"));
2008 }
2009
2010 #[test]
2011 fn test_deep_stacking_configuration() {
2012 let config = MultiLayerStackingConfig::deep_stacking(3, 4);
2013
2014 assert_eq!(config.layers.len(), 3);
2015 assert_eq!(
2016 config.final_meta_strategy,
2017 MetaLearningStrategy::BayesianAveraging
2018 );
2019 assert!(config.enable_pruning);
2020 assert_eq!(config.diversity_threshold, 0.15);
2021 assert!(config.confidence_weighting);
2022
2023 assert!(!config.layers[0].use_probabilities);
2025 assert!(config.layers[1].use_probabilities);
2026 assert!(config.layers[2].use_probabilities);
2027
2028 assert!(config.layers[0].passthrough);
2030 assert!(!config.layers[1].passthrough);
2031 assert!(!config.layers[2].passthrough);
2032 }
2033
2034 #[test]
2035 fn test_random_state_setting() {
2036 let stacking = MultiLayerStackingClassifier::two_layer(2, 1).random_state(42);
2037
2038 assert_eq!(stacking.config.random_state, Some(42));
2039 }
2040
2041 #[test]
2042 fn test_utility_methods() {
2043 let config = MultiLayerStackingConfig::new();
2044 let stacking = MultiLayerStackingClassifier::new(config);
2045
2046 let x = array![1.0, 2.0, 3.0, 4.0];
2048 let y = array![2.0, 4.0, 6.0, 8.0]; let corr = stacking.calculate_correlation(&x, &y);
2050 assert!((corr - 1.0).abs() < 1e-10);
2051
2052 let values = array![0.5, 0.3, 0.2];
2054 let entropy = stacking.calculate_shannon_entropy(&values);
2055 assert!(entropy > 0.0);
2056
2057 let signal = array![1.0, 2.0, 1.0, 2.0, 1.0];
2059 let dominant_freq = stacking.find_dominant_frequency(&signal);
2060 assert!(dominant_freq >= 0.0);
2061
2062 let centroid = stacking.calculate_spectral_centroid(&signal);
2063 assert!(centroid > 0.0);
2064 }
2065}