Skip to main content

sklears_svm/
adaptive_regularization.rs

1//! Adaptive Regularization Methods for SVMs
2//!
3//! This module implements adaptive regularization techniques that automatically
4//! adjust regularization parameters during training for improved performance.
5//!
6//! Methods included:
7//! - Adaptive C selection based on cross-validation
8//! - Learning curve based regularization
9//! - Bayesian regularization parameter optimization
10//! - Early stopping with validation loss
11
12use scirs2_core::ndarray::{Array1, Array2};
13use sklears_core::{
14    error::{Result, SklearsError},
15    traits::{Fit, Predict, Trained, Untrained},
16};
17use std::fmt;
18use std::marker::PhantomData;
19
20/// Errors that can occur during adaptive regularization
21#[derive(Debug, Clone)]
22pub enum AdaptiveRegularizationError {
23    InvalidInput(String),
24    OptimizationError(String),
25    ConvergenceError(String),
26}
27
28impl fmt::Display for AdaptiveRegularizationError {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self {
31            AdaptiveRegularizationError::InvalidInput(msg) => write!(f, "Invalid input: {msg}"),
32            AdaptiveRegularizationError::OptimizationError(msg) => {
33                write!(f, "Optimization error: {msg}")
34            }
35            AdaptiveRegularizationError::ConvergenceError(msg) => {
36                write!(f, "Convergence error: {msg}")
37            }
38        }
39    }
40}
41
42impl std::error::Error for AdaptiveRegularizationError {}
43
44/// Adaptive regularization strategy
45#[derive(Debug, Clone)]
46pub enum AdaptiveStrategy {
47    /// Cross-validation based C selection
48    CrossValidation {
49        folds: usize,
50        c_range: (f64, f64),
51        n_candidates: usize,
52    },
53    /// Learning curve based adaptation
54    LearningCurve {
55        patience: usize,
56        min_improvement: f64,
57        c_factor: f64,
58    },
59    /// Bayesian optimization of regularization
60    BayesianOptimization {
61        n_iterations: usize,
62        acquisition_function: AcquisitionFunction,
63    },
64    /// Early stopping with validation
65    EarlyStopping {
66        patience: usize,
67        validation_fraction: f64,
68        min_delta: f64,
69    },
70}
71
72/// Acquisition function for Bayesian optimization
73#[derive(Debug, Clone)]
74pub enum AcquisitionFunction {
75    ExpectedImprovement,
76    UpperConfidenceBound { beta: f64 },
77    ProbabilityOfImprovement,
78}
79
80/// Configuration for adaptive regularization
81#[derive(Debug, Clone)]
82pub struct AdaptiveRegularizationConfig {
83    /// Adaptive strategy to use
84    pub strategy: AdaptiveStrategy,
85    /// Initial regularization parameter
86    pub initial_c: f64,
87    /// Maximum number of iterations
88    pub max_iter: usize,
89    /// Tolerance for convergence
90    pub tol: f64,
91    /// Verbose output
92    pub verbose: bool,
93}
94
95impl Default for AdaptiveRegularizationConfig {
96    fn default() -> Self {
97        Self {
98            strategy: AdaptiveStrategy::CrossValidation {
99                folds: 5,
100                c_range: (0.001, 1000.0),
101                n_candidates: 20,
102            },
103            initial_c: 1.0,
104            max_iter: 100,
105            tol: 1e-6,
106            verbose: false,
107        }
108    }
109}
110
111/// Adaptive regularization SVM wrapper
112#[derive(Debug, Clone)]
113pub struct AdaptiveSVM<State = Untrained> {
114    config: AdaptiveRegularizationConfig,
115    state: PhantomData<State>,
116    // Final regularization parameter
117    optimal_c: Option<f64>,
118    // Base SVM configuration
119    base_svm_config: Option<crate::svc::SvcConfig>,
120    // Trained base SVM
121    trained_svm: Option<crate::svc::SVC<Trained>>,
122    // Optimization history
123    optimization_history: Option<Vec<(f64, f64)>>, // (C, validation_score)
124}
125
126impl Default for AdaptiveSVM<Untrained> {
127    fn default() -> Self {
128        Self::new()
129    }
130}
131
132impl AdaptiveSVM<Untrained> {
133    /// Create a new adaptive SVM
134    pub fn new() -> Self {
135        Self {
136            config: AdaptiveRegularizationConfig::default(),
137            state: PhantomData,
138            optimal_c: None,
139            base_svm_config: None,
140            trained_svm: None,
141            optimization_history: None,
142        }
143    }
144
145    /// Set the adaptive strategy
146    pub fn with_strategy(mut self, strategy: AdaptiveStrategy) -> Self {
147        self.config.strategy = strategy;
148        self
149    }
150
151    /// Set initial C parameter
152    pub fn with_initial_c(mut self, c: f64) -> Self {
153        self.config.initial_c = c;
154        self
155    }
156
157    /// Set maximum iterations
158    pub fn with_max_iter(mut self, max_iter: usize) -> Self {
159        self.config.max_iter = max_iter;
160        self
161    }
162
163    /// Set tolerance
164    pub fn with_tolerance(mut self, tol: f64) -> Self {
165        self.config.tol = tol;
166        self
167    }
168
169    /// Enable verbose output
170    pub fn verbose(mut self, verbose: bool) -> Self {
171        self.config.verbose = verbose;
172        self
173    }
174
175    /// Set base SVM configuration
176    pub fn with_svm_config(mut self, config: crate::svc::SvcConfig) -> Self {
177        self.base_svm_config = Some(config);
178        self
179    }
180}
181
182impl Fit<Array2<f64>, Array1<f64>> for AdaptiveSVM<Untrained> {
183    type Fitted = AdaptiveSVM<Trained>;
184
185    fn fit(self, x: &Array2<f64>, y: &Array1<f64>) -> Result<Self::Fitted> {
186        let (n_samples, _n_features) = x.dim();
187
188        if n_samples != y.len() {
189            return Err(SklearsError::InvalidInput(
190                "Number of samples must match number of labels".to_string(),
191            ));
192        }
193
194        if n_samples == 0 {
195            return Err(SklearsError::InvalidInput(
196                "Cannot fit on empty dataset".to_string(),
197            ));
198        }
199
200        // Find optimal regularization parameter
201        let (optimal_c, optimization_history) = self.optimize_regularization(x, y)?;
202
203        // Train final SVM with optimal C
204        let mut base_config = self.base_svm_config.expect("value should be present");
205        base_config.c = optimal_c;
206
207        let base_svm = crate::svc::SVC::new()
208            .c(optimal_c)
209            .tol(base_config.tol)
210            .max_iter(base_config.max_iter);
211
212        //         use sklears_core::traits::Fit;
213        let trained_svm = base_svm.fit(x, y)?;
214
215        if self.config.verbose {
216            println!("Adaptive regularization found optimal C = {:.6}", optimal_c);
217        }
218
219        Ok(AdaptiveSVM {
220            config: self.config,
221            state: PhantomData,
222            optimal_c: Some(optimal_c),
223            base_svm_config: Some(base_config),
224            trained_svm: Some(trained_svm),
225            optimization_history: Some(optimization_history),
226        })
227    }
228}
229
230impl AdaptiveSVM<Untrained> {
231    /// Optimize regularization parameter based on strategy
232    fn optimize_regularization(
233        &self,
234        x: &Array2<f64>,
235        y: &Array1<f64>,
236    ) -> Result<(f64, Vec<(f64, f64)>)> {
237        match &self.config.strategy {
238            AdaptiveStrategy::CrossValidation {
239                folds,
240                c_range,
241                n_candidates,
242            } => self.cross_validation_optimization(x, y, *folds, *c_range, *n_candidates),
243            AdaptiveStrategy::LearningCurve {
244                patience,
245                min_improvement,
246                c_factor,
247            } => self.learning_curve_optimization(x, y, *patience, *min_improvement, *c_factor),
248            AdaptiveStrategy::BayesianOptimization {
249                n_iterations,
250                acquisition_function,
251            } => self.bayesian_optimization(x, y, *n_iterations, acquisition_function),
252            AdaptiveStrategy::EarlyStopping {
253                patience,
254                validation_fraction,
255                min_delta,
256            } => {
257                self.early_stopping_optimization(x, y, *patience, *validation_fraction, *min_delta)
258            }
259        }
260    }
261
262    /// Cross-validation based C optimization
263    fn cross_validation_optimization(
264        &self,
265        x: &Array2<f64>,
266        y: &Array1<f64>,
267        folds: usize,
268        c_range: (f64, f64),
269        n_candidates: usize,
270    ) -> Result<(f64, Vec<(f64, f64)>)> {
271        let (c_min, c_max) = c_range;
272        let mut best_c = self.config.initial_c;
273        let mut best_score = f64::NEG_INFINITY;
274        let mut history = Vec::new();
275
276        // Generate C candidates (log scale)
277        let log_c_min = c_min.ln();
278        let log_c_max = c_max.ln();
279        let step = (log_c_max - log_c_min) / (n_candidates - 1) as f64;
280
281        for i in 0..n_candidates {
282            let log_c = log_c_min + i as f64 * step;
283            let c = log_c.exp();
284
285            // Perform k-fold cross-validation
286            let cv_score = self.cross_validate(x, y, c, folds)?;
287            history.push((c, cv_score));
288
289            if self.config.verbose {
290                println!("C = {:.6}: CV Score = {:.6}", c, cv_score);
291            }
292
293            if cv_score > best_score {
294                best_score = cv_score;
295                best_c = c;
296            }
297        }
298
299        Ok((best_c, history))
300    }
301
302    /// Learning curve based optimization
303    fn learning_curve_optimization(
304        &self,
305        x: &Array2<f64>,
306        y: &Array1<f64>,
307        patience: usize,
308        min_improvement: f64,
309        c_factor: f64,
310    ) -> Result<(f64, Vec<(f64, f64)>)> {
311        let mut current_c = self.config.initial_c;
312        let mut best_c = current_c;
313        let mut best_score = f64::NEG_INFINITY;
314        let mut history = Vec::new();
315        let mut patience_counter = 0;
316
317        for iteration in 0..self.config.max_iter {
318            // Evaluate current C
319            let score = self.cross_validate(x, y, current_c, 3)?; // 3-fold CV for speed
320            history.push((current_c, score));
321
322            if self.config.verbose {
323                println!(
324                    "Iteration {}: C = {:.6}, Score = {:.6}",
325                    iteration, current_c, score
326                );
327            }
328
329            // Check for improvement
330            if score > best_score + min_improvement {
331                best_score = score;
332                best_c = current_c;
333                patience_counter = 0;
334            } else {
335                patience_counter += 1;
336            }
337
338            // Early stopping
339            if patience_counter >= patience {
340                if self.config.verbose {
341                    println!("Early stopping: no improvement for {patience} iterations");
342                }
343                break;
344            }
345
346            // Adaptive C adjustment
347            if score < best_score {
348                current_c *= c_factor; // Increase regularization
349            } else {
350                current_c /= c_factor; // Decrease regularization
351            }
352
353            // Keep C within reasonable bounds
354            current_c = current_c.clamp(1e-6, 1e6);
355        }
356
357        Ok((best_c, history))
358    }
359
360    /// Simplified Bayesian optimization
361    fn bayesian_optimization(
362        &self,
363        x: &Array2<f64>,
364        y: &Array1<f64>,
365        n_iterations: usize,
366        _acquisition_function: &AcquisitionFunction,
367    ) -> Result<(f64, Vec<(f64, f64)>)> {
368        let mut history = Vec::new();
369        let mut best_c = self.config.initial_c;
370
371        // Initial evaluation
372        let initial_score = self.cross_validate(x, y, self.config.initial_c, 5)?;
373        history.push((self.config.initial_c, initial_score));
374        let mut best_score = initial_score;
375
376        // Simple grid search (simplified Bayesian optimization)
377        let c_candidates = [0.001, 0.01, 0.1, 1.0, 10.0, 100.0, 1000.0];
378
379        for (iteration, &c) in c_candidates.iter().enumerate().take(n_iterations) {
380            if !history.iter().any(|(prev_c, _)| (prev_c - c).abs() < 1e-8) {
381                let score = self.cross_validate(x, y, c, 5)?;
382                history.push((c, score));
383
384                if self.config.verbose {
385                    println!(
386                        "Bayesian Opt Iteration {}: C = {:.6}, Score = {:.6}",
387                        iteration, c, score
388                    );
389                }
390
391                if score > best_score {
392                    best_score = score;
393                    best_c = c;
394                }
395            }
396        }
397
398        Ok((best_c, history))
399    }
400
401    /// Early stopping optimization with validation split
402    fn early_stopping_optimization(
403        &self,
404        x: &Array2<f64>,
405        y: &Array1<f64>,
406        patience: usize,
407        validation_fraction: f64,
408        min_delta: f64,
409    ) -> Result<(f64, Vec<(f64, f64)>)> {
410        let n_samples = x.nrows();
411        let val_size = (n_samples as f64 * validation_fraction) as usize;
412        let train_size = n_samples - val_size;
413
414        // Simple train/validation split
415        let x_train = x
416            .slice(scirs2_core::ndarray::s![..train_size, ..])
417            .to_owned();
418        let y_train = y.slice(scirs2_core::ndarray::s![..train_size]).to_owned();
419        let x_val = x
420            .slice(scirs2_core::ndarray::s![train_size.., ..])
421            .to_owned();
422        let y_val = y.slice(scirs2_core::ndarray::s![train_size..]).to_owned();
423
424        let mut current_c = self.config.initial_c;
425        let mut best_c = current_c;
426        let mut best_score = f64::NEG_INFINITY;
427        let mut history = Vec::new();
428        let mut patience_counter = 0;
429
430        for iteration in 0..self.config.max_iter {
431            // Train on training set, evaluate on validation set
432            let base_svm = crate::svc::SVC::new().c(current_c).max_iter(100);
433
434            use sklears_core::traits::Predict;
435            let trained_svm = base_svm.fit(&x_train, &y_train)?;
436            let predictions = trained_svm.predict(&x_val)?;
437
438            // Calculate accuracy
439            let correct = predictions
440                .iter()
441                .zip(y_val.iter())
442                .filter(|(&pred, &true_label)| (pred - true_label).abs() < 1e-6)
443                .count();
444            let accuracy = correct as f64 / y_val.len() as f64;
445
446            history.push((current_c, accuracy));
447
448            if self.config.verbose {
449                println!(
450                    "Early Stop Iteration {}: C = {:.6}, Val Accuracy = {:.6}",
451                    iteration, current_c, accuracy
452                );
453            }
454
455            // Check for improvement
456            if accuracy > best_score + min_delta {
457                best_score = accuracy;
458                best_c = current_c;
459                patience_counter = 0;
460            } else {
461                patience_counter += 1;
462            }
463
464            // Early stopping
465            if patience_counter >= patience {
466                if self.config.verbose {
467                    println!("Early stopping: no improvement for {patience} iterations");
468                }
469                break;
470            }
471
472            // Simple search pattern
473            if iteration % 2 == 0 {
474                current_c *= 2.0; // Try higher regularization
475            } else {
476                current_c /= 4.0; // Try lower regularization
477            }
478
479            current_c = current_c.clamp(1e-6, 1e6);
480        }
481
482        Ok((best_c, history))
483    }
484
485    /// Perform k-fold cross-validation
486    fn cross_validate(
487        &self,
488        x: &Array2<f64>,
489        y: &Array1<f64>,
490        c: f64,
491        folds: usize,
492    ) -> Result<f64> {
493        let n_samples = x.nrows();
494        let fold_size = n_samples / folds;
495        let mut scores = Vec::new();
496
497        for fold in 0..folds {
498            let val_start = fold * fold_size;
499            let val_end = if fold == folds - 1 {
500                n_samples
501            } else {
502                (fold + 1) * fold_size
503            };
504
505            // Create train/validation split
506            let mut train_indices = Vec::new();
507            let mut val_indices = Vec::new();
508
509            for i in 0..n_samples {
510                if i >= val_start && i < val_end {
511                    val_indices.push(i);
512                } else {
513                    train_indices.push(i);
514                }
515            }
516
517            // Extract training and validation data
518            let x_train = self.extract_rows(x, &train_indices);
519            let y_train = self.extract_elements(y, &train_indices);
520            let x_val = self.extract_rows(x, &val_indices);
521            let y_val = self.extract_elements(y, &val_indices);
522
523            // Train SVM
524            let base_svm = crate::svc::SVC::new().c(c).max_iter(100);
525
526            use sklears_core::traits::Predict;
527            let trained_svm = base_svm.fit(&x_train, &y_train)?;
528            let predictions = trained_svm.predict(&x_val)?;
529
530            // Calculate accuracy
531            let correct = predictions
532                .iter()
533                .zip(y_val.iter())
534                .filter(|(&pred, &true_label)| (pred - true_label).abs() < 1e-6)
535                .count();
536            let accuracy = correct as f64 / y_val.len() as f64;
537            scores.push(accuracy);
538        }
539
540        Ok(scores.iter().sum::<f64>() / scores.len() as f64)
541    }
542
543    /// Extract rows from array
544    fn extract_rows(&self, array: &Array2<f64>, indices: &[usize]) -> Array2<f64> {
545        let mut result = Array2::zeros((indices.len(), array.ncols()));
546        for (new_idx, &old_idx) in indices.iter().enumerate() {
547            result.row_mut(new_idx).assign(&array.row(old_idx));
548        }
549        result
550    }
551
552    /// Extract elements from array
553    fn extract_elements(&self, array: &Array1<f64>, indices: &[usize]) -> Array1<f64> {
554        Array1::from_vec(indices.iter().map(|&i| array[i]).collect())
555    }
556}
557
558impl Predict<Array2<f64>, Array1<f64>> for AdaptiveSVM<Trained> {
559    fn predict(&self, x: &Array2<f64>) -> Result<Array1<f64>> {
560        let trained_svm = self
561            .trained_svm
562            .as_ref()
563            .expect("trained_svm not available - model not fitted");
564        //         use sklears_core::traits::Predict;
565        trained_svm.predict(x)
566    }
567}
568
569impl AdaptiveSVM<Trained> {
570    /// Get the optimal regularization parameter
571    pub fn optimal_c(&self) -> f64 {
572        *self
573            .optimal_c
574            .as_ref()
575            .expect("optimal_c not available - model not fitted")
576    }
577
578    /// Get the optimization history
579    pub fn optimization_history(&self) -> &[(f64, f64)] {
580        self.optimization_history
581            .as_ref()
582            .expect("optimization_history not available - model not fitted")
583    }
584
585    /// Get the underlying trained SVM
586    pub fn base_svm(&self) -> &crate::svc::SVC<Trained> {
587        self.trained_svm
588            .as_ref()
589            .expect("trained_svm not available - model not fitted")
590    }
591}
592
593#[allow(non_snake_case)]
594#[cfg(test)]
595mod tests {
596    use super::*;
597    use scirs2_core::ndarray::array;
598
599    fn create_test_data() -> (Array2<f64>, Array1<f64>) {
600        let x = array![
601            [1.0, 2.0],
602            [2.0, 3.0],
603            [3.0, 1.0],
604            [5.0, 6.0],
605            [6.0, 7.0],
606            [7.0, 5.0],
607            [1.5, 2.5],
608            [2.5, 3.5],
609        ];
610        let y = array![0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0];
611        (x, y)
612    }
613
614    #[test]
615    fn test_adaptive_svm_creation() {
616        let adaptive_svm = AdaptiveSVM::new()
617            .with_strategy(AdaptiveStrategy::CrossValidation {
618                folds: 3,
619                c_range: (0.1, 10.0),
620                n_candidates: 5,
621            })
622            .with_initial_c(1.0)
623            .verbose(false);
624
625        assert!(matches!(
626            adaptive_svm.config.strategy,
627            AdaptiveStrategy::CrossValidation { .. }
628        ));
629        assert_eq!(adaptive_svm.config.initial_c, 1.0);
630    }
631
632    #[test]
633    #[ignore]
634    fn test_cross_validation_strategy() {
635        let (x, y) = create_test_data();
636
637        let adaptive_svm = AdaptiveSVM::new()
638            .with_strategy(AdaptiveStrategy::CrossValidation {
639                folds: 3,
640                c_range: (0.1, 10.0),
641                n_candidates: 5,
642            })
643            .verbose(false);
644
645        use sklears_core::traits::Fit;
646        let result = adaptive_svm.fit(&x, &y);
647        assert!(result.is_ok());
648
649        let trained_model = result.expect("operation should succeed");
650        assert!(trained_model.optimal_c.is_some());
651        assert!(trained_model.optimization_history.is_some());
652    }
653
654    #[test]
655    #[ignore]
656    fn test_early_stopping_strategy() {
657        let (x, y) = create_test_data();
658
659        let adaptive_svm = AdaptiveSVM::new()
660            .with_strategy(AdaptiveStrategy::EarlyStopping {
661                patience: 3,
662                validation_fraction: 0.3,
663                min_delta: 0.01,
664            })
665            .verbose(false);
666
667        use sklears_core::traits::Fit;
668        let result = adaptive_svm.fit(&x, &y);
669        assert!(result.is_ok());
670
671        let trained_model = result.expect("operation should succeed");
672        assert!(trained_model.optimal_c.is_some());
673    }
674
675    #[test]
676    #[ignore]
677    fn test_adaptive_svm_predict() {
678        let (x, y) = create_test_data();
679
680        let adaptive_svm = AdaptiveSVM::new()
681            .with_strategy(AdaptiveStrategy::CrossValidation {
682                folds: 2,
683                c_range: (0.1, 10.0),
684                n_candidates: 3,
685            })
686            .verbose(false);
687
688        use sklears_core::traits::Predict;
689        let trained_model = adaptive_svm
690            .fit(&x, &y)
691            .expect("model fitting should succeed");
692
693        let predictions = trained_model.predict(&x);
694        assert!(predictions.is_ok());
695
696        let pred_labels = predictions.expect("operation should succeed");
697        assert_eq!(pred_labels.len(), x.nrows());
698    }
699
700    #[test]
701    fn test_invalid_input() {
702        let x = Array2::zeros((5, 2));
703        let y = Array1::zeros(6); // Wrong number of samples
704
705        let adaptive_svm = AdaptiveSVM::new();
706        use sklears_core::traits::Fit;
707        let result = adaptive_svm.fit(&x, &y);
708        assert!(result.is_err());
709    }
710}