1use 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#[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#[derive(Debug, Clone)]
46pub enum AdaptiveStrategy {
47 CrossValidation {
49 folds: usize,
50 c_range: (f64, f64),
51 n_candidates: usize,
52 },
53 LearningCurve {
55 patience: usize,
56 min_improvement: f64,
57 c_factor: f64,
58 },
59 BayesianOptimization {
61 n_iterations: usize,
62 acquisition_function: AcquisitionFunction,
63 },
64 EarlyStopping {
66 patience: usize,
67 validation_fraction: f64,
68 min_delta: f64,
69 },
70}
71
72#[derive(Debug, Clone)]
74pub enum AcquisitionFunction {
75 ExpectedImprovement,
76 UpperConfidenceBound { beta: f64 },
77 ProbabilityOfImprovement,
78}
79
80#[derive(Debug, Clone)]
82pub struct AdaptiveRegularizationConfig {
83 pub strategy: AdaptiveStrategy,
85 pub initial_c: f64,
87 pub max_iter: usize,
89 pub tol: f64,
91 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#[derive(Debug, Clone)]
113pub struct AdaptiveSVM<State = Untrained> {
114 config: AdaptiveRegularizationConfig,
115 state: PhantomData<State>,
116 optimal_c: Option<f64>,
118 base_svm_config: Option<crate::svc::SvcConfig>,
120 trained_svm: Option<crate::svc::SVC<Trained>>,
122 optimization_history: Option<Vec<(f64, f64)>>, }
125
126impl Default for AdaptiveSVM<Untrained> {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132impl AdaptiveSVM<Untrained> {
133 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 pub fn with_strategy(mut self, strategy: AdaptiveStrategy) -> Self {
147 self.config.strategy = strategy;
148 self
149 }
150
151 pub fn with_initial_c(mut self, c: f64) -> Self {
153 self.config.initial_c = c;
154 self
155 }
156
157 pub fn with_max_iter(mut self, max_iter: usize) -> Self {
159 self.config.max_iter = max_iter;
160 self
161 }
162
163 pub fn with_tolerance(mut self, tol: f64) -> Self {
165 self.config.tol = tol;
166 self
167 }
168
169 pub fn verbose(mut self, verbose: bool) -> Self {
171 self.config.verbose = verbose;
172 self
173 }
174
175 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 let (optimal_c, optimization_history) = self.optimize_regularization(x, y)?;
202
203 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 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 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 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 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 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 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 let score = self.cross_validate(x, y, current_c, 3)?; 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 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 if patience_counter >= patience {
340 if self.config.verbose {
341 println!("Early stopping: no improvement for {patience} iterations");
342 }
343 break;
344 }
345
346 if score < best_score {
348 current_c *= c_factor; } else {
350 current_c /= c_factor; }
352
353 current_c = current_c.clamp(1e-6, 1e6);
355 }
356
357 Ok((best_c, history))
358 }
359
360 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 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 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 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 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 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 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 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 if patience_counter >= patience {
466 if self.config.verbose {
467 println!("Early stopping: no improvement for {patience} iterations");
468 }
469 break;
470 }
471
472 if iteration % 2 == 0 {
474 current_c *= 2.0; } else {
476 current_c /= 4.0; }
478
479 current_c = current_c.clamp(1e-6, 1e6);
480 }
481
482 Ok((best_c, history))
483 }
484
485 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 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 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 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 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 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 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 trained_svm.predict(x)
566 }
567}
568
569impl AdaptiveSVM<Trained> {
570 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 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 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); 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}