1use scirs2_core::ndarray::{Array1, Array2, ScalarOperand};
2use scirs2_core::numeric::{Float, ToPrimitive};
3use scirs2_core::random::{RngExt, SeedableRng};
4use std::fmt::Debug;
5
6use crate::activation::Activation;
7use crate::mlp_classifier::MLPClassifier;
8use crate::mlp_regressor::MLPRegressor;
9use sklears_core::error::SklearsError;
10use sklears_core::traits::{Fit, Predict};
11use sklears_core::types::FloatBounds;
12use std::collections::HashMap;
13
14pub type FoldIndices = (Vec<usize>, Vec<usize>);
16
17pub type LearningCurveStats<T> = (Vec<T>, Vec<T>, Vec<T>, Vec<T>);
19
20#[derive(Debug, Clone)]
26pub struct ModelMetrics<T: Float> {
27 pub model_name: String,
29 pub train_score: T,
31 pub validation_score: T,
33 pub test_score: Option<T>,
35 pub training_time: T,
37 pub num_parameters: usize,
39 pub complexity_score: T,
41 pub additional_metrics: HashMap<String, T>,
43}
44
45impl<T: Float> ModelMetrics<T> {
46 pub fn new(model_name: String) -> Self {
48 Self {
49 model_name,
50 train_score: T::zero(),
51 validation_score: T::zero(),
52 test_score: None,
53 training_time: T::zero(),
54 num_parameters: 0,
55 complexity_score: T::zero(),
56 additional_metrics: HashMap::new(),
57 }
58 }
59
60 pub fn add_metric(&mut self, name: String, value: T) {
62 self.additional_metrics.insert(name, value);
63 }
64
65 pub fn get_metric(&self, name: &str) -> Option<T> {
67 match name {
68 "train_score" => Some(self.train_score),
69 "validation_score" => Some(self.validation_score),
70 "test_score" => self.test_score,
71 "training_time" => Some(self.training_time),
72 "complexity_score" => Some(self.complexity_score),
73 _ => self.additional_metrics.get(name).copied(),
74 }
75 }
76}
77
78#[derive(Debug, Clone)]
80pub struct CrossValidationConfig {
81 pub n_folds: usize,
83 pub random_seed: Option<u64>,
85 pub shuffle: bool,
87 pub stratify: bool,
89}
90
91impl Default for CrossValidationConfig {
92 fn default() -> Self {
93 Self {
94 n_folds: 5,
95 random_seed: Some(42),
96 shuffle: true,
97 stratify: true,
98 }
99 }
100}
101
102#[derive(Debug, Clone)]
104pub struct CrossValidationResults<T: Float> {
105 pub fold_scores: Vec<T>,
107 pub mean_score: T,
109 pub std_score: T,
111 pub best_fold: usize,
113 pub worst_fold: usize,
115}
116
117impl<T: Float + std::iter::Sum> CrossValidationResults<T> {
118 pub fn from_scores(scores: Vec<T>) -> Self {
120 let mean_score =
121 scores.iter().cloned().sum::<T>() / T::from(scores.len()).unwrap_or_else(|| T::zero());
122
123 let variance = scores
124 .iter()
125 .map(|&score| {
126 let diff = score - mean_score;
127 diff * diff
128 })
129 .sum::<T>()
130 / T::from(scores.len()).unwrap_or_else(|| T::zero());
131
132 let std_score = variance.sqrt();
133
134 let best_fold = scores
135 .iter()
136 .enumerate()
137 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
138 .map(|(idx, _)| idx)
139 .unwrap_or(0);
140
141 let worst_fold = scores
142 .iter()
143 .enumerate()
144 .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
145 .map(|(idx, _)| idx)
146 .unwrap_or(0);
147
148 Self {
149 fold_scores: scores,
150 mean_score,
151 std_score,
152 best_fold,
153 worst_fold,
154 }
155 }
156}
157
158#[derive(Debug, Clone)]
160pub struct GridSearchConfig<T: Float> {
161 pub hidden_layer_sizes: Vec<Vec<usize>>,
163 pub learning_rates: Vec<T>,
165 pub alphas: Vec<T>,
167 pub activations: Vec<Activation>,
169 pub max_iters: Vec<usize>,
171 pub cv_config: CrossValidationConfig,
173 pub scoring: String,
175}
176
177impl<T: Float> Default for GridSearchConfig<T> {
178 fn default() -> Self {
179 Self {
180 hidden_layer_sizes: vec![vec![100], vec![50, 50], vec![100, 50], vec![100, 100]],
181 learning_rates: vec![
182 T::from(0.001).unwrap_or_else(|| T::zero()),
183 T::from(0.01).unwrap_or_else(|| T::zero()),
184 T::from(0.1).unwrap_or_else(|| T::zero()),
185 ],
186 alphas: vec![
187 T::from(0.0001).unwrap_or_else(|| T::zero()),
188 T::from(0.001).unwrap_or_else(|| T::zero()),
189 T::from(0.01).unwrap_or_else(|| T::zero()),
190 ],
191 activations: vec![Activation::Relu, Activation::Tanh, Activation::Logistic],
192 max_iters: vec![200, 500, 1000],
193 cv_config: CrossValidationConfig::default(),
194 scoring: "accuracy".to_string(),
195 }
196 }
197}
198
199#[derive(Debug, Clone)]
201pub struct HyperparameterSet<T: Float> {
202 pub hidden_layer_sizes: Vec<usize>,
204 pub learning_rate: T,
206 pub alpha: T,
208 pub activation: Activation,
210 pub max_iter: usize,
212}
213
214#[derive(Debug, Clone)]
216pub struct GridSearchResults<T: Float> {
217 pub best_params: HyperparameterSet<T>,
219 pub best_score: T,
221 pub all_results: Vec<(HyperparameterSet<T>, CrossValidationResults<T>)>,
223 pub best_estimator_metrics: ModelMetrics<T>,
225}
226
227#[derive(Debug)]
229pub struct ModelSelector<T: FloatBounds + ScalarOperand + ToPrimitive + std::iter::Sum> {
230 cv_config: CrossValidationConfig,
232 results: Vec<ModelMetrics<T>>,
234}
235
236impl<T: FloatBounds + ScalarOperand + ToPrimitive + std::iter::Sum> ModelSelector<T> {
237 pub fn new(cv_config: CrossValidationConfig) -> Self {
239 Self {
240 cv_config,
241 results: Vec::new(),
242 }
243 }
244
245 pub fn cross_validate_classifier(
247 &self,
248 X: &Array2<T>,
249 y: &[usize],
250 hidden_layer_sizes: &[usize],
251 activation: Activation,
252 learning_rate: T,
253 alpha: T,
254 max_iter: usize,
255 ) -> Result<CrossValidationResults<T>, SklearsError> {
256 let folds = self.create_folds(X, Some(y))?;
257 let mut scores = Vec::new();
258
259 for (train_indices, val_indices) in folds {
260 let X_train = self.select_rows(X, &train_indices)?;
262 let y_train: Vec<usize> = train_indices.iter().map(|&i| y[i]).collect();
263 let X_val = self.select_rows(X, &val_indices)?;
264 let y_val: Vec<usize> = val_indices.iter().map(|&i| y[i]).collect();
265
266 let classifier = MLPClassifier::new()
268 .hidden_layer_sizes(hidden_layer_sizes)
269 .activation(activation)
270 .learning_rate_init(learning_rate.to_f64().unwrap_or(0.0))
271 .alpha(alpha.to_f64().unwrap_or(0.0))
272 .max_iter(max_iter);
273
274 let X_train_f64 = X_train.mapv(|x| x.to_f64().unwrap_or(0.0));
276 let X_val_f64 = X_val.mapv(|x| x.to_f64().unwrap_or(0.0));
277
278 let trained_classifier = classifier.fit(&X_train_f64, &y_train)?;
279 let predictions = trained_classifier.predict(&X_val_f64)?;
280
281 let accuracy = self.compute_accuracy(&predictions, &y_val);
283 scores.push(T::from(accuracy).unwrap_or_else(|| T::zero()));
284 }
285
286 Ok(CrossValidationResults::from_scores(scores))
287 }
288
289 pub fn cross_validate_regressor(
291 &self,
292 X: &Array2<T>,
293 y: &Array1<T>,
294 hidden_layer_sizes: &[usize],
295 activation: Activation,
296 learning_rate: T,
297 alpha: T,
298 max_iter: usize,
299 ) -> Result<CrossValidationResults<T>, SklearsError> {
300 let folds = self.create_folds(X, None)?;
301 let mut scores = Vec::new();
302
303 for (train_indices, val_indices) in folds {
304 let X_train = self.select_rows(X, &train_indices)?;
306 let y_train = self.select_elements(y, &train_indices)?;
307 let X_val = self.select_rows(X, &val_indices)?;
308 let y_val = self.select_elements(y, &val_indices)?;
309
310 let regressor = MLPRegressor::new()
312 .hidden_layer_sizes(hidden_layer_sizes)
313 .activation(activation)
314 .learning_rate_init(learning_rate.to_f64().unwrap_or(0.0))
315 .alpha(alpha.to_f64().unwrap_or(0.0))
316 .max_iter(max_iter);
317
318 let X_train_f64 = X_train.mapv(|x| x.to_f64().unwrap_or(0.0));
320 let X_val_f64 = X_val.mapv(|x| x.to_f64().unwrap_or(0.0));
321 let y_train_f64 = y_train.mapv(|x| x.to_f64().unwrap_or(0.0));
322
323 let y_train_2d = y_train_f64.insert_axis(scirs2_core::ndarray::Axis(1));
325
326 let trained_regressor = regressor.fit(&X_train_f64, &y_train_2d)?;
327 let predictions = trained_regressor.predict(&X_val_f64)?;
328
329 let predictions_1d_f64 = predictions.column(0).to_owned();
331 let predictions_1d =
332 predictions_1d_f64.mapv(|x| T::from(x).unwrap_or_else(|| T::zero()));
333
334 let r2_score = self.compute_r2_score(&predictions_1d, &y_val);
336 scores.push(T::from(r2_score).unwrap_or_else(|| T::zero()));
337 }
338
339 Ok(CrossValidationResults::from_scores(scores))
340 }
341
342 pub fn grid_search_classifier(
344 &self,
345 X: &Array2<T>,
346 y: &[usize],
347 config: GridSearchConfig<T>,
348 ) -> Result<GridSearchResults<T>, SklearsError> {
349 let mut best_score = T::neg_infinity();
350 let mut best_params = None;
351 let mut all_results = Vec::new();
352
353 for hidden_layers in &config.hidden_layer_sizes {
355 for &learning_rate in &config.learning_rates {
356 for &alpha in &config.alphas {
357 for activation in &config.activations {
358 for &max_iter in &config.max_iters {
359 let params = HyperparameterSet {
360 hidden_layer_sizes: hidden_layers.clone(),
361 learning_rate,
362 alpha,
363 activation: *activation,
364 max_iter,
365 };
366
367 let cv_results = self.cross_validate_classifier(
368 X,
369 y,
370 hidden_layers,
371 *activation,
372 learning_rate,
373 alpha,
374 max_iter,
375 )?;
376
377 if cv_results.mean_score > best_score {
378 best_score = cv_results.mean_score;
379 best_params = Some(params.clone());
380 }
381
382 all_results.push((params, cv_results));
383 }
384 }
385 }
386 }
387 }
388
389 let best_params = best_params.ok_or_else(|| SklearsError::InvalidParameter {
390 name: "grid_search".to_string(),
391 reason: "No valid parameter combinations found".to_string(),
392 })?;
393
394 let final_classifier = MLPClassifier::new()
396 .hidden_layer_sizes(&best_params.hidden_layer_sizes)
397 .activation(best_params.activation)
398 .learning_rate_init(best_params.learning_rate.to_f64().unwrap_or(0.0))
399 .alpha(best_params.alpha.to_f64().unwrap_or(0.0))
400 .max_iter(best_params.max_iter);
401
402 let x_f64 = X.mapv(|x| x.to_f64().unwrap_or(0.0));
404 let y_vec = y.to_vec();
405 let _trained_final = final_classifier.fit(&x_f64, &y_vec)?;
406
407 let best_estimator_metrics = ModelMetrics {
408 model_name: "Best_MLP_Classifier".to_string(),
409 train_score: best_score,
410 validation_score: best_score,
411 test_score: None,
412 training_time: T::zero(),
413 num_parameters: 0,
414 complexity_score: T::zero(),
415 additional_metrics: HashMap::new(),
416 };
417
418 Ok(GridSearchResults {
419 best_params,
420 best_score,
421 all_results,
422 best_estimator_metrics,
423 })
424 }
425
426 pub fn compare_models(&mut self, models: Vec<ModelMetrics<T>>) -> Vec<ModelMetrics<T>> {
428 self.results.extend(models);
429
430 self.results.sort_by(|a, b| {
432 b.validation_score
433 .partial_cmp(&a.validation_score)
434 .unwrap_or(std::cmp::Ordering::Equal)
435 });
436
437 self.results.clone()
438 }
439
440 pub fn get_rankings(&self) -> Vec<(usize, &ModelMetrics<T>)> {
442 self.results.iter().enumerate().collect()
443 }
444
445 fn create_folds(
448 &self,
449 X: &Array2<T>,
450 _y: Option<&[usize]>,
451 ) -> Result<Vec<FoldIndices>, SklearsError> {
452 let n_samples = X.nrows();
453 let fold_size = n_samples / self.cv_config.n_folds;
454 let mut indices: Vec<usize> = (0..n_samples).collect();
455
456 if self.cv_config.shuffle {
458 if let Some(seed) = self.cv_config.random_seed {
459 let mut rng = scirs2_core::random::rngs::StdRng::seed_from_u64(seed);
460 for i in (1..indices.len()).rev() {
461 let j = rng.random_range(0..i + 1);
462 indices.swap(i, j);
463 }
464 }
465 }
466
467 let mut folds = Vec::new();
468
469 for fold in 0..self.cv_config.n_folds {
470 let start = fold * fold_size;
471 let end = if fold == self.cv_config.n_folds - 1 {
472 n_samples } else {
474 (fold + 1) * fold_size
475 };
476
477 let val_indices = indices[start..end].to_vec();
478 let train_indices: Vec<usize> = indices[0..start]
479 .iter()
480 .chain(indices[end..].iter())
481 .cloned()
482 .collect();
483
484 folds.push((train_indices, val_indices));
485 }
486
487 Ok(folds)
488 }
489
490 fn select_rows(&self, X: &Array2<T>, indices: &[usize]) -> Result<Array2<T>, SklearsError> {
492 let mut selected = Array2::zeros((indices.len(), X.ncols()));
493
494 for (i, &idx) in indices.iter().enumerate() {
495 if idx < X.nrows() {
496 selected.row_mut(i).assign(&X.row(idx));
497 }
498 }
499
500 Ok(selected)
501 }
502
503 fn select_elements(&self, y: &Array1<T>, indices: &[usize]) -> Result<Array1<T>, SklearsError> {
505 let mut selected = Array1::zeros(indices.len());
506
507 for (i, &idx) in indices.iter().enumerate() {
508 if idx < y.len() {
509 selected[i] = y[idx];
510 }
511 }
512
513 Ok(selected)
514 }
515
516 fn compute_accuracy(&self, predictions: &[usize], y_true: &[usize]) -> f64 {
518 if predictions.len() != y_true.len() {
519 return 0.0;
520 }
521
522 let correct = predictions
523 .iter()
524 .zip(y_true.iter())
525 .filter(|(pred, true_)| pred == true_)
526 .count();
527
528 correct as f64 / predictions.len() as f64
529 }
530
531 fn compute_r2_score(&self, predictions: &Array1<T>, y_true: &Array1<T>) -> f64 {
533 if predictions.len() != y_true.len() {
534 return 0.0;
535 }
536
537 let pred_f64: Vec<f64> = predictions
539 .iter()
540 .map(|&x| x.to_f64().unwrap_or(0.0))
541 .collect();
542 let true_f64: Vec<f64> = y_true.iter().map(|&x| x.to_f64().unwrap_or(0.0)).collect();
543
544 let mean_true = true_f64.iter().sum::<f64>() / true_f64.len() as f64;
545
546 let ss_res: f64 = pred_f64
547 .iter()
548 .zip(true_f64.iter())
549 .map(|(pred, true_)| {
550 let diff = true_ - pred;
551 diff * diff
552 })
553 .sum();
554
555 let ss_tot: f64 = true_f64
556 .iter()
557 .map(|true_| {
558 let diff = true_ - mean_true;
559 diff * diff
560 })
561 .sum();
562
563 if ss_tot == 0.0 {
564 0.0
565 } else {
566 1.0 - (ss_res / ss_tot)
567 }
568 }
569}
570
571#[derive(Debug, Clone)]
573pub struct LearningCurveAnalyzer<T: Float> {
574 pub train_sizes: Vec<usize>,
576 pub train_scores: Vec<Vec<T>>,
578 pub validation_scores: Vec<Vec<T>>,
580}
581
582impl<T: Float + std::iter::Sum> Default for LearningCurveAnalyzer<T> {
583 fn default() -> Self {
584 Self::new()
585 }
586}
587
588impl<T: Float + std::iter::Sum> LearningCurveAnalyzer<T> {
589 pub fn new() -> Self {
591 Self {
592 train_sizes: Vec::new(),
593 train_scores: Vec::new(),
594 validation_scores: Vec::new(),
595 }
596 }
597
598 pub fn analyze_learning_curve(
600 &mut self,
601 X: &Array2<T>,
602 y: &[usize],
603 hidden_layer_sizes: &[usize],
604 activation: Activation,
605 learning_rate: f64,
606 alpha: f64,
607 max_iter: usize,
608 train_sizes: Vec<usize>,
609 ) -> Result<(), SklearsError> {
610 self.train_sizes = train_sizes.clone();
611 self.train_scores.clear();
612 self.validation_scores.clear();
613
614 for &train_size in &train_sizes {
615 if train_size > X.nrows() {
616 continue;
617 }
618
619 let X_subset = X
621 .slice(scirs2_core::ndarray::s![..train_size, ..])
622 .to_owned();
623 let y_subset = &y[..train_size];
624
625 let val_size = train_size / 5; let train_end = train_size - val_size;
628
629 let X_train = X_subset
630 .slice(scirs2_core::ndarray::s![..train_end, ..])
631 .to_owned();
632 let y_train = &y_subset[..train_end];
633 let X_val = X_subset
634 .slice(scirs2_core::ndarray::s![train_end.., ..])
635 .to_owned();
636 let y_val = &y_subset[train_end..];
637
638 let classifier = MLPClassifier::new()
640 .hidden_layer_sizes(hidden_layer_sizes)
641 .activation(activation)
642 .learning_rate_init(learning_rate)
643 .alpha(alpha)
644 .max_iter(max_iter);
645
646 let X_train_f64 = X_train.mapv(|x| x.to_f64().unwrap_or(0.0));
648 let X_val_f64 = X_val.mapv(|x| x.to_f64().unwrap_or(0.0));
649 let y_train_vec = y_train.to_vec();
650
651 let trained = classifier.fit(&X_train_f64, &y_train_vec)?;
652
653 let train_pred = trained.predict(&X_train_f64)?;
655 let train_accuracy = self.compute_accuracy(&train_pred, y_train);
656
657 let val_pred = trained.predict(&X_val_f64)?;
659 let val_accuracy = self.compute_accuracy(&val_pred, y_val);
660
661 self.train_scores
662 .push(vec![T::from(train_accuracy).unwrap_or_else(|| T::zero())]);
663 self.validation_scores
664 .push(vec![T::from(val_accuracy).unwrap_or_else(|| T::zero())]);
665 }
666
667 Ok(())
668 }
669
670 pub fn get_statistics(&self) -> Result<LearningCurveStats<T>, SklearsError> {
672 let train_means: Vec<T> = self
673 .train_scores
674 .iter()
675 .map(|scores| {
676 scores.iter().cloned().sum::<T>()
677 / T::from(scores.len()).unwrap_or_else(|| T::zero())
678 })
679 .collect();
680
681 let train_stds: Vec<T> = self
682 .train_scores
683 .iter()
684 .zip(train_means.iter())
685 .map(|(scores, &mean)| {
686 let variance = scores
687 .iter()
688 .map(|&score| {
689 let diff = score - mean;
690 diff * diff
691 })
692 .sum::<T>()
693 / T::from(scores.len()).unwrap_or_else(|| T::zero());
694 variance.sqrt()
695 })
696 .collect();
697
698 let val_means: Vec<T> = self
699 .validation_scores
700 .iter()
701 .map(|scores| {
702 scores.iter().cloned().sum::<T>()
703 / T::from(scores.len()).unwrap_or_else(|| T::zero())
704 })
705 .collect();
706
707 let val_stds: Vec<T> = self
708 .validation_scores
709 .iter()
710 .zip(val_means.iter())
711 .map(|(scores, &mean)| {
712 let variance = scores
713 .iter()
714 .map(|&score| {
715 let diff = score - mean;
716 diff * diff
717 })
718 .sum::<T>()
719 / T::from(scores.len()).unwrap_or_else(|| T::zero());
720 variance.sqrt()
721 })
722 .collect();
723
724 Ok((train_means, train_stds, val_means, val_stds))
725 }
726
727 fn compute_accuracy(&self, predictions: &[usize], y_true: &[usize]) -> f64 {
729 if predictions.len() != y_true.len() {
730 return 0.0;
731 }
732
733 let correct = predictions
734 .iter()
735 .zip(y_true.iter())
736 .filter(|(pred, true_)| pred == true_)
737 .count();
738
739 correct as f64 / predictions.len() as f64
740 }
741}
742
743#[allow(non_snake_case)]
744#[cfg(test)]
745mod tests {
746 use super::*;
747 use approx::assert_abs_diff_eq;
748
749 #[test]
750 fn test_model_metrics_creation() {
751 let mut metrics = ModelMetrics::<f32>::new("TestModel".to_string());
752 metrics.train_score = 0.95;
753 metrics.validation_score = 0.90;
754 metrics.add_metric("precision".to_string(), 0.88);
755
756 assert_eq!(metrics.model_name, "TestModel");
757 assert_eq!(metrics.train_score, 0.95);
758 assert_eq!(metrics.get_metric("precision"), Some(0.88));
759 }
760
761 #[test]
762 fn test_cross_validation_results() {
763 let scores = vec![0.8, 0.85, 0.82, 0.87, 0.83];
764 let results = CrossValidationResults::from_scores(scores);
765
766 assert_abs_diff_eq!(results.mean_score, 0.834, epsilon = 1e-3);
767 assert!(results.std_score > 0.0);
768 assert_eq!(results.best_fold, 3); }
770
771 #[test]
772 fn test_model_selector_creation() {
773 let cv_config = CrossValidationConfig::default();
774 let selector = ModelSelector::<f32>::new(cv_config);
775 assert_eq!(selector.cv_config.n_folds, 5);
776 }
777
778 #[test]
779 fn test_fold_creation() {
780 let cv_config = CrossValidationConfig {
781 n_folds: 3,
782 random_seed: Some(42),
783 shuffle: false,
784 stratify: false,
785 };
786 let selector = ModelSelector::<f32>::new(cv_config);
787
788 let X = Array2::from_shape_vec((9, 2), vec![1.0; 18]).expect("array shape mismatch");
789 let folds = selector
790 .create_folds(&X, None)
791 .expect("operation should succeed");
792
793 assert_eq!(folds.len(), 3);
794 assert_eq!(folds[0].1.len(), 3); assert_eq!(folds[0].0.len(), 6); }
797
798 #[test]
799 fn test_accuracy_computation() {
800 let selector = ModelSelector::<f32>::new(CrossValidationConfig::default());
801
802 let predictions = vec![0, 1, 1, 0, 1];
803 let y_true = vec![0, 1, 0, 0, 1];
804
805 let accuracy = selector.compute_accuracy(&predictions, &y_true);
806 assert_abs_diff_eq!(accuracy, 0.8, epsilon = 1e-6);
807 }
808
809 #[test]
810 fn test_r2_score_computation() {
811 let selector = ModelSelector::<f32>::new(CrossValidationConfig::default());
812
813 let predictions = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
814 let y_true = Array1::from_vec(vec![1.1, 1.9, 3.1, 3.9]);
815
816 let r2 = selector.compute_r2_score(&predictions, &y_true);
817 assert!(r2 > 0.9); }
819
820 #[test]
821 fn test_grid_search_config_default() {
822 let config = GridSearchConfig::<f32>::default();
823 assert!(!config.hidden_layer_sizes.is_empty());
824 assert!(!config.learning_rates.is_empty());
825 assert!(!config.activations.is_empty());
826 }
827
828 #[test]
829 fn test_hyperparameter_set() {
830 let params = HyperparameterSet {
831 hidden_layer_sizes: vec![100, 50],
832 learning_rate: 0.01,
833 alpha: 0.001,
834 activation: Activation::Relu,
835 max_iter: 1000,
836 };
837
838 assert_eq!(params.hidden_layer_sizes, vec![100, 50]);
839 assert_eq!(params.learning_rate, 0.01);
840 }
841
842 #[test]
843 fn test_learning_curve_analyzer() {
844 let analyzer = LearningCurveAnalyzer::<f32>::new();
845 assert!(analyzer.train_sizes.is_empty());
846 assert!(analyzer.train_scores.is_empty());
847 }
848
849 #[test]
850 fn test_model_comparison() {
851 let cv_config = CrossValidationConfig::default();
852 let mut selector = ModelSelector::<f32>::new(cv_config);
853
854 let model1 = ModelMetrics {
855 model_name: "Model1".to_string(),
856 train_score: 0.90,
857 validation_score: 0.85,
858 test_score: None,
859 training_time: 10.0,
860 num_parameters: 1000,
861 complexity_score: 0.5,
862 additional_metrics: HashMap::new(),
863 };
864
865 let model2 = ModelMetrics {
866 model_name: "Model2".to_string(),
867 train_score: 0.88,
868 validation_score: 0.87,
869 test_score: None,
870 training_time: 15.0,
871 num_parameters: 1500,
872 complexity_score: 0.7,
873 additional_metrics: HashMap::new(),
874 };
875
876 let results = selector.compare_models(vec![model1, model2]);
877 assert_eq!(results.len(), 2);
878 assert_eq!(results[0].model_name, "Model2"); }
880}