1use scirs2_core::ndarray::{s, Array1, Array2};
14use scirs2_linalg::compat::ArrayLinalgExt;
15
16use crate::kernels::{create_kernel, Kernel, KernelType};
17use sklears_core::error::{Result, SklearsError};
18
19fn solve_linear_system(a: &Array2<f64>, b: &Array1<f64>, reg: f64) -> Array1<f64> {
28 if let Ok(x) = a.solve(b) {
29 if x.iter().all(|v| v.is_finite()) {
30 return x;
31 }
32 }
33
34 let n = a.nrows();
36 let mut a_reg = a.clone();
37 for i in 0..n {
38 a_reg[[i, i]] += reg.max(1e-8);
39 }
40 if let Ok(x) = a_reg.solve(b) {
41 if x.iter().all(|v| v.is_finite()) {
42 return x;
43 }
44 }
45
46 b.clone()
49}
50
51#[derive(Debug, Clone)]
53pub struct AdvancedOptimizationConfig {
54 pub c: f64,
56 pub kernel: KernelType,
58 pub tol: f64,
60 pub max_iter: usize,
62 pub rho: f64,
64 pub trust_radius: f64,
66 pub line_search_c1: f64,
68 pub line_search_c2: f64,
69 pub newton_reg: f64,
71 pub verbose: bool,
73}
74
75impl Default for AdvancedOptimizationConfig {
76 fn default() -> Self {
77 Self {
78 c: 1.0,
79 kernel: KernelType::Rbf { gamma: 1.0 },
80 tol: 1e-6,
81 max_iter: 1000,
82 rho: 1.0,
83 trust_radius: 1.0,
84 line_search_c1: 1e-4,
85 line_search_c2: 0.9,
86 newton_reg: 1e-8,
87 verbose: false,
88 }
89 }
90}
91
92#[derive(Debug, Clone)]
94pub struct OptimizationResult {
95 pub dual_coef: Array1<f64>,
97 pub intercept: f64,
99 pub support_indices: Vec<usize>,
101 pub n_iterations: usize,
103 pub objective_value: f64,
105 pub converged: bool,
107 pub history: Vec<f64>,
109}
110
111#[derive(Debug, Clone)]
128pub struct ADMMSVM {
129 config: AdvancedOptimizationConfig,
130 kernel: Option<KernelType>,
131 is_fitted: bool,
132}
133
134impl Default for ADMMSVM {
135 fn default() -> Self {
137 Self::new(AdvancedOptimizationConfig::default())
138 }
139}
140
141impl ADMMSVM {
142 pub fn new(config: AdvancedOptimizationConfig) -> Self {
144 Self {
145 config,
146 kernel: None,
147 is_fitted: false,
148 }
149 }
150
151 pub fn fit(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<OptimizationResult> {
153 if x.nrows() != y.len() {
155 return Err(SklearsError::InvalidInput(
156 "Number of samples must match number of labels".to_string(),
157 ));
158 }
159
160 let n_samples = x.nrows();
161
162 let kernel = self.config.kernel.clone();
164 self.kernel = Some(kernel);
165
166 let k_matrix = self.compute_kernel_matrix(x)?;
168
169 let mut alpha = Array1::zeros(n_samples);
171 let mut z = Array1::zeros(n_samples);
172 let mut u: Array1<f64> = Array1::zeros(n_samples); let mut history = Vec::new();
174
175 for iteration in 0..self.config.max_iter {
177 let z_prev = z.clone();
179
180 alpha = self.update_alpha(&k_matrix, &z, &u)?;
182
183 let w = self.update_w(x, y, &alpha)?;
185
186 z = self.update_z(&alpha, &u)?;
188
189 u = &u + &((&alpha - &z) * self.config.rho);
191
192 let objective = self.calculate_objective(&k_matrix, &alpha, &w)?;
194 history.push(objective);
195
196 if self.config.verbose && iteration % 10 == 0 {
197 println!("ADMM Iteration {}: Objective = {:.6}", iteration, objective);
198 }
199
200 let primal_diff = &alpha - &z;
202 let primal_residual = primal_diff.dot(&primal_diff).sqrt();
203 let z_diff = &z - &z_prev;
205 let dual_residual = self.config.rho * z_diff.dot(&z_diff).sqrt();
206
207 if primal_residual < self.config.tol && dual_residual < self.config.tol {
208 if self.config.verbose {
209 println!("ADMM converged after {} iterations", iteration + 1);
210 }
211
212 self.is_fitted = true;
213
214 let support_indices = self.find_support_vectors(&alpha)?;
215 let intercept = self.calculate_intercept(x, y, &alpha, &support_indices)?;
216
217 return Ok(OptimizationResult {
218 dual_coef: alpha,
219 intercept,
220 support_indices,
221 n_iterations: iteration + 1,
222 objective_value: objective,
223 converged: true,
224 history,
225 });
226 }
227 }
228
229 self.is_fitted = true;
230
231 let support_indices = self.find_support_vectors(&alpha)?;
233 let intercept = self.calculate_intercept(x, y, &alpha, &support_indices)?;
234
235 Ok(OptimizationResult {
236 dual_coef: alpha,
237 intercept,
238 support_indices,
239 n_iterations: self.config.max_iter,
240 objective_value: history.last().copied().unwrap_or(0.0),
241 converged: false,
242 history,
243 })
244 }
245
246 fn update_alpha(
248 &self,
249 k_matrix: &Array2<f64>,
250 z: &Array1<f64>,
251 u: &Array1<f64>,
252 ) -> Result<Array1<f64>> {
253 let n = k_matrix.nrows();
254
255 let mut q_matrix = k_matrix.clone();
259 for i in 0..n {
260 q_matrix[[i, i]] += self.config.rho;
261 }
262
263 let p = &Array1::from_elem(n, -1.0) + &((z - u) * self.config.rho);
264
265 let neg_p = p.mapv(|v| -v);
267 let mut alpha = solve_linear_system(&q_matrix, &neg_p, self.config.rho.max(1e-8));
268
269 for i in 0..n {
271 alpha[i] = alpha[i].max(0.0).min(self.config.c);
272 }
273
274 Ok(alpha)
275 }
276
277 fn update_w(
279 &self,
280 x: &Array2<f64>,
281 y: &Array1<f64>,
282 alpha: &Array1<f64>,
283 ) -> Result<Array1<f64>> {
284 let n_features = x.ncols();
285 let mut w = Array1::zeros(n_features);
286
287 for i in 0..alpha.len() {
289 if alpha[i] > 0.0 {
290 let coeff = alpha[i] * y[i];
291 for k in 0..n_features {
292 w[k] += coeff * x[[i, k]];
293 }
294 }
295 }
296
297 Ok(w)
298 }
299
300 fn update_z(&self, alpha: &Array1<f64>, u: &Array1<f64>) -> Result<Array1<f64>> {
302 let mut z = Array1::zeros(alpha.len());
303
304 for i in 0..alpha.len() {
306 let temp = alpha[i] + u[i];
307 z[i] = if temp > self.config.c / self.config.rho {
308 temp - self.config.c / self.config.rho
309 } else if temp < 0.0 {
310 temp
311 } else {
312 0.0
313 };
314 }
315
316 Ok(z)
317 }
318
319 fn compute_kernel_matrix(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
321 let kernel_type = self
322 .kernel
323 .as_ref()
324 .ok_or_else(|| SklearsError::NotFitted {
325 operation: "compute_kernel_matrix".to_string(),
326 })?;
327 let kernel = create_kernel(kernel_type.clone())?;
328 let n = x.nrows();
329 let mut k_matrix = Array2::zeros((n, n));
330
331 for i in 0..n {
332 for j in 0..n {
333 k_matrix[[i, j]] = kernel.compute(x.row(i), x.row(j));
334 }
335 }
336
337 Ok(k_matrix)
338 }
339
340 fn calculate_objective(
342 &self,
343 k_matrix: &Array2<f64>,
344 alpha: &Array1<f64>,
345 w: &Array1<f64>,
346 ) -> Result<f64> {
347 let dual_obj = alpha.sum() - 0.5 * alpha.dot(&k_matrix.dot(alpha));
348 let primal_obj = 0.5 * w.dot(w);
349
350 Ok(dual_obj.max(primal_obj))
351 }
352
353 fn find_support_vectors(&self, alpha: &Array1<f64>) -> Result<Vec<usize>> {
355 let support_indices: Vec<usize> = alpha
356 .iter()
357 .enumerate()
358 .filter(|(_, &val)| val > self.config.tol)
359 .map(|(i, _)| i)
360 .collect();
361
362 Ok(support_indices)
363 }
364
365 fn calculate_intercept(
367 &self,
368 x: &Array2<f64>,
369 y: &Array1<f64>,
370 alpha: &Array1<f64>,
371 support_indices: &[usize],
372 ) -> Result<f64> {
373 if support_indices.is_empty() {
374 return Ok(0.0);
375 }
376
377 let kernel_type = self
378 .kernel
379 .as_ref()
380 .ok_or_else(|| SklearsError::NotFitted {
381 operation: "calculate_intercept".to_string(),
382 })?;
383 let kernel = create_kernel(kernel_type.clone())?;
384 let mut intercept_sum = 0.0;
385 let mut count = 0;
386
387 for &i in support_indices {
388 if alpha[i] > self.config.tol && alpha[i] < self.config.c - self.config.tol {
389 let mut decision_value = 0.0;
390 for &j in support_indices {
391 decision_value += alpha[j] * y[j] * kernel.compute(x.row(i), x.row(j));
392 }
393 intercept_sum += y[i] - decision_value;
394 count += 1;
395 }
396 }
397
398 Ok(if count > 0 {
399 intercept_sum / count as f64
400 } else {
401 0.0
402 })
403 }
404
405 pub fn predict(&self, x: &Array2<f64>, result: &OptimizationResult) -> Result<Array1<f64>> {
407 if !self.is_fitted {
408 return Err(SklearsError::NotFitted {
409 operation: "prediction".to_string(),
410 });
411 }
412
413 let decision_values = self.decision_function(x, result)?;
414 Ok(Array1::from_vec(
415 decision_values
416 .iter()
417 .map(|&val| if val > 0.0 { 1.0 } else { -1.0 })
418 .collect(),
419 ))
420 }
421
422 pub fn decision_function(
424 &self,
425 x: &Array2<f64>,
426 result: &OptimizationResult,
427 ) -> Result<Array1<f64>> {
428 if !self.is_fitted {
429 return Err(SklearsError::NotFitted {
430 operation: "prediction".to_string(),
431 });
432 }
433
434 let kernel_type = self
435 .kernel
436 .as_ref()
437 .ok_or_else(|| SklearsError::NotFitted {
438 operation: "decision_function".to_string(),
439 })?;
440 let kernel = create_kernel(kernel_type.clone())?;
441 let mut decision_values = Array1::zeros(x.nrows());
442
443 for i in 0..x.nrows() {
444 let mut sum = 0.0;
445 for &j in &result.support_indices {
446 sum += result.dual_coef[j] * kernel.compute(x.row(i), x.row(j));
447 }
448 decision_values[i] = sum + result.intercept;
449 }
450
451 Ok(decision_values)
452 }
453}
454
455#[derive(Debug, Clone)]
461pub struct NewtonSVM {
462 config: AdvancedOptimizationConfig,
463 kernel: Option<KernelType>,
464 is_fitted: bool,
465}
466
467impl Default for NewtonSVM {
468 fn default() -> Self {
470 Self::new(AdvancedOptimizationConfig::default())
471 }
472}
473
474impl NewtonSVM {
475 pub fn new(config: AdvancedOptimizationConfig) -> Self {
477 Self {
478 config,
479 kernel: None,
480 is_fitted: false,
481 }
482 }
483
484 pub fn fit(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<OptimizationResult> {
486 if x.nrows() != y.len() {
488 return Err(SklearsError::InvalidInput(
489 "Number of samples must match number of labels".to_string(),
490 ));
491 }
492
493 let kernel = self.config.kernel.clone();
495 self.kernel = Some(kernel);
496
497 if matches!(self.config.kernel, KernelType::Linear) {
499 self.fit_primal_newton(x, y)
500 } else {
501 self.fit_dual_newton(x, y)
503 }
504 }
505
506 fn fit_primal_newton(
508 &mut self,
509 x: &Array2<f64>,
510 y: &Array1<f64>,
511 ) -> Result<OptimizationResult> {
512 let n_samples = x.nrows();
513 let n_features = x.ncols();
514
515 let mut w: Array1<f64> = Array1::zeros(n_features);
517 let mut b = 0.0;
518 let mut history = Vec::new();
519
520 for iteration in 0..self.config.max_iter {
521 let margins = self.calculate_margins(x, y, &w, b);
523
524 let active_indices: Vec<usize> = margins
526 .iter()
527 .enumerate()
528 .filter(|(_, &margin)| margin < 1.0)
529 .map(|(i, _)| i)
530 .collect();
531
532 if active_indices.is_empty() {
533 break; }
535
536 let hessian = self.build_hessian(x, &active_indices)?;
538
539 let gradient = self.build_gradient(x, y, &w, b, &active_indices, &margins)?;
541
542 let neg_gradient = gradient.mapv(|v| -v);
544 let direction = solve_linear_system(&hessian, &neg_gradient, self.config.newton_reg);
545
546 let step_size = self.line_search(x, y, &w, b, &direction, &margins)?;
548
549 for k in 0..n_features {
551 w[k] += step_size * direction[k];
552 }
553 b += step_size * direction[n_features];
554
555 let objective = self.calculate_primal_objective(&w, &margins);
557 history.push(objective);
558
559 if self.config.verbose && iteration % 10 == 0 {
560 println!(
561 "Newton Iteration {}: Objective = {:.6}",
562 iteration, objective
563 );
564 }
565
566 if gradient.dot(&gradient).sqrt() < self.config.tol {
568 if self.config.verbose {
569 println!("Newton method converged after {} iterations", iteration + 1);
570 }
571
572 self.is_fitted = true;
573
574 return Ok(OptimizationResult {
575 dual_coef: Array1::zeros(n_samples), intercept: b,
577 support_indices: active_indices,
578 n_iterations: iteration + 1,
579 objective_value: objective,
580 converged: true,
581 history,
582 });
583 }
584 }
585
586 self.is_fitted = true;
587
588 let margins = self.calculate_margins(x, y, &w, b);
590 let active_indices: Vec<usize> = margins
591 .iter()
592 .enumerate()
593 .filter(|(_, &margin)| margin < 1.0)
594 .map(|(i, _)| i)
595 .collect();
596
597 Ok(OptimizationResult {
598 dual_coef: Array1::zeros(n_samples),
599 intercept: b,
600 support_indices: active_indices,
601 n_iterations: self.config.max_iter,
602 objective_value: history.last().copied().unwrap_or(0.0),
603 converged: false,
604 history,
605 })
606 }
607
608 fn fit_dual_newton(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<OptimizationResult> {
610 let n_samples = x.nrows();
611
612 let mut alpha: Array1<f64> = Array1::zeros(n_samples);
614 let mut history = Vec::new();
615
616 let k_matrix = self.compute_kernel_matrix(x)?;
618
619 for iteration in 0..self.config.max_iter {
620 let gradient = self.calculate_dual_gradient(&k_matrix, &alpha);
622
623 let hessian = self.calculate_dual_hessian(&k_matrix, &alpha)?;
625
626 let neg_gradient = gradient.mapv(|v| -v);
628 let direction = solve_linear_system(&hessian, &neg_gradient, self.config.newton_reg);
629
630 let step_size = self.dual_line_search(&k_matrix, &alpha, &direction)?;
632
633 alpha = &alpha + &(&direction * step_size);
635
636 for i in 0..n_samples {
638 alpha[i] = alpha[i].max(0.0).min(self.config.c);
639 }
640
641 let objective = self.calculate_dual_objective(&k_matrix, &alpha);
643 history.push(objective);
644
645 if self.config.verbose && iteration % 10 == 0 {
646 println!(
647 "Dual Newton Iteration {}: Objective = {:.6}",
648 iteration, objective
649 );
650 }
651
652 if gradient.dot(&gradient).sqrt() < self.config.tol {
654 if self.config.verbose {
655 println!(
656 "Dual Newton method converged after {} iterations",
657 iteration + 1
658 );
659 }
660
661 self.is_fitted = true;
662
663 let support_indices = self.find_support_vectors(&alpha)?;
664 let intercept = self.calculate_intercept(x, y, &alpha, &support_indices)?;
665
666 return Ok(OptimizationResult {
667 dual_coef: alpha,
668 intercept,
669 support_indices,
670 n_iterations: iteration + 1,
671 objective_value: objective,
672 converged: true,
673 history,
674 });
675 }
676 }
677
678 self.is_fitted = true;
679
680 let support_indices = self.find_support_vectors(&alpha)?;
682 let intercept = self.calculate_intercept(x, y, &alpha, &support_indices)?;
683
684 Ok(OptimizationResult {
685 dual_coef: alpha,
686 intercept,
687 support_indices,
688 n_iterations: self.config.max_iter,
689 objective_value: history.last().copied().unwrap_or(0.0),
690 converged: false,
691 history,
692 })
693 }
694
695 fn calculate_margins(
697 &self,
698 x: &Array2<f64>,
699 y: &Array1<f64>,
700 w: &Array1<f64>,
701 b: f64,
702 ) -> Vec<f64> {
703 let mut margins = Vec::with_capacity(x.nrows());
704 for i in 0..x.nrows() {
705 let decision_value = x.row(i).dot(w) + b;
706 margins.push(y[i] * decision_value);
707 }
708 margins
709 }
710
711 fn build_hessian(&self, x: &Array2<f64>, active_indices: &[usize]) -> Result<Array2<f64>> {
713 let n_features = x.ncols();
714 let mut hessian = Array2::zeros((n_features + 1, n_features + 1));
715
716 for i in 0..n_features {
718 hessian[[i, i]] = 1.0;
719 }
720
721 for &idx in active_indices {
723 let x_i = x.row(idx);
724
725 for i in 0..n_features {
727 for j in 0..n_features {
728 hessian[[i, j]] += x_i[i] * x_i[j];
729 }
730 }
731
732 for i in 0..n_features {
734 hessian[[i, n_features]] += x_i[i];
735 hessian[[n_features, i]] += x_i[i];
736 }
737
738 hessian[[n_features, n_features]] += 1.0;
740 }
741
742 hessian.mapv_inplace(|v| v * self.config.c);
743
744 Ok(hessian)
745 }
746
747 fn build_gradient(
749 &self,
750 x: &Array2<f64>,
751 y: &Array1<f64>,
752 w: &Array1<f64>,
753 _b: f64,
754 active_indices: &[usize],
755 margins: &[f64],
756 ) -> Result<Array1<f64>> {
757 let n_features = x.ncols();
758 let mut gradient = Array1::zeros(n_features + 1);
759
760 gradient.slice_mut(s![0..n_features]).assign(w);
762
763 for &idx in active_indices {
765 let violation = 1.0 - margins[idx];
766 if violation > 0.0 {
767 let x_i = x.row(idx);
768
769 for i in 0..n_features {
771 gradient[i] -= self.config.c * y[idx] * x_i[i];
772 }
773
774 gradient[n_features] -= self.config.c * y[idx];
776 }
777 }
778
779 Ok(gradient)
780 }
781
782 fn line_search(
784 &self,
785 x: &Array2<f64>,
786 y: &Array1<f64>,
787 w: &Array1<f64>,
788 b: f64,
789 direction: &Array1<f64>,
790 margins: &[f64],
791 ) -> Result<f64> {
792 let n_features = x.ncols();
793 let mut step_size = 1.0;
794 let current_obj = self.calculate_primal_objective(w, margins);
795
796 for _ in 0..20 {
797 let mut new_w = w.clone();
799 for k in 0..n_features {
800 new_w[k] += step_size * direction[k];
801 }
802 let new_b = b + step_size * direction[n_features];
803 let new_margins = self.calculate_margins(x, y, &new_w, new_b);
804 let new_obj = self.calculate_primal_objective(&new_w, &new_margins);
805
806 if new_obj < current_obj {
807 return Ok(step_size);
808 }
809
810 step_size *= 0.5;
811 }
812
813 Ok(step_size)
814 }
815
816 fn calculate_primal_objective(&self, w: &Array1<f64>, margins: &[f64]) -> f64 {
818 let regularization = 0.5 * w.dot(w);
819 let hinge_loss: f64 = margins.iter().map(|&margin| (1.0 - margin).max(0.0)).sum();
820
821 regularization + self.config.c * hinge_loss
822 }
823
824 fn compute_kernel_matrix(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
826 let kernel_type = self
827 .kernel
828 .as_ref()
829 .ok_or_else(|| SklearsError::NotFitted {
830 operation: "compute_kernel_matrix".to_string(),
831 })?;
832 let kernel = create_kernel(kernel_type.clone())?;
833 let n = x.nrows();
834 let mut k_matrix = Array2::zeros((n, n));
835
836 for i in 0..n {
837 for j in 0..n {
838 k_matrix[[i, j]] = kernel.compute(x.row(i), x.row(j));
839 }
840 }
841
842 Ok(k_matrix)
843 }
844
845 fn calculate_dual_gradient(&self, k_matrix: &Array2<f64>, alpha: &Array1<f64>) -> Array1<f64> {
846 &Array1::from_elem(alpha.len(), 1.0) - &k_matrix.dot(alpha)
847 }
848
849 fn calculate_dual_hessian(
850 &self,
851 k_matrix: &Array2<f64>,
852 _alpha: &Array1<f64>,
853 ) -> Result<Array2<f64>> {
854 Ok(k_matrix.clone())
856 }
857
858 fn dual_line_search(
859 &self,
860 k_matrix: &Array2<f64>,
861 alpha: &Array1<f64>,
862 direction: &Array1<f64>,
863 ) -> Result<f64> {
864 let mut step_size = 1.0;
865 let current_obj = self.calculate_dual_objective(k_matrix, alpha);
866
867 for _ in 0..20 {
868 let new_alpha = alpha + &(direction * step_size);
869 let new_obj = self.calculate_dual_objective(k_matrix, &new_alpha);
870
871 if new_obj > current_obj {
872 return Ok(step_size);
873 }
874
875 step_size *= 0.5;
876 }
877
878 Ok(step_size)
879 }
880
881 fn calculate_dual_objective(&self, k_matrix: &Array2<f64>, alpha: &Array1<f64>) -> f64 {
882 alpha.sum() - 0.5 * alpha.dot(&k_matrix.dot(alpha))
883 }
884
885 fn find_support_vectors(&self, alpha: &Array1<f64>) -> Result<Vec<usize>> {
886 let support_indices: Vec<usize> = alpha
887 .iter()
888 .enumerate()
889 .filter(|(_, &val)| val > self.config.tol)
890 .map(|(i, _)| i)
891 .collect();
892
893 Ok(support_indices)
894 }
895
896 fn calculate_intercept(
897 &self,
898 x: &Array2<f64>,
899 y: &Array1<f64>,
900 alpha: &Array1<f64>,
901 support_indices: &[usize],
902 ) -> Result<f64> {
903 if support_indices.is_empty() {
904 return Ok(0.0);
905 }
906
907 let kernel_type = self
908 .kernel
909 .as_ref()
910 .ok_or_else(|| SklearsError::NotFitted {
911 operation: "calculate_intercept".to_string(),
912 })?;
913 let kernel = create_kernel(kernel_type.clone())?;
914 let mut intercept_sum = 0.0;
915 let mut count = 0;
916
917 for &i in support_indices {
918 if alpha[i] > self.config.tol && alpha[i] < self.config.c - self.config.tol {
919 let mut decision_value = 0.0;
920 for &j in support_indices {
921 decision_value += alpha[j] * y[j] * kernel.compute(x.row(i), x.row(j));
922 }
923 intercept_sum += y[i] - decision_value;
924 count += 1;
925 }
926 }
927
928 Ok(if count > 0 {
929 intercept_sum / count as f64
930 } else {
931 0.0
932 })
933 }
934
935 pub fn predict(&self, x: &Array2<f64>, result: &OptimizationResult) -> Result<Array1<f64>> {
937 if !self.is_fitted {
938 return Err(SklearsError::NotFitted {
939 operation: "prediction".to_string(),
940 });
941 }
942
943 let decision_values = self.decision_function(x, result)?;
944 Ok(Array1::from_vec(
945 decision_values
946 .iter()
947 .map(|&val| if val > 0.0 { 1.0 } else { -1.0 })
948 .collect(),
949 ))
950 }
951
952 pub fn decision_function(
954 &self,
955 x: &Array2<f64>,
956 result: &OptimizationResult,
957 ) -> Result<Array1<f64>> {
958 if !self.is_fitted {
959 return Err(SklearsError::NotFitted {
960 operation: "prediction".to_string(),
961 });
962 }
963
964 let kernel_type = self
965 .kernel
966 .as_ref()
967 .ok_or_else(|| SklearsError::NotFitted {
968 operation: "decision_function".to_string(),
969 })?;
970 let kernel = create_kernel(kernel_type.clone())?;
971 let mut decision_values = Array1::zeros(x.nrows());
972
973 for i in 0..x.nrows() {
974 let mut sum = 0.0;
975 for &j in &result.support_indices {
976 sum += result.dual_coef[j] * kernel.compute(x.row(i), x.row(j));
977 }
978 decision_values[i] = sum + result.intercept;
979 }
980
981 Ok(decision_values)
982 }
983}
984
985#[derive(Debug, Clone)]
997pub struct TrustRegionSVM {
998 config: AdvancedOptimizationConfig,
999 kernel: Option<KernelType>,
1000 is_fitted: bool,
1001}
1002
1003impl Default for TrustRegionSVM {
1004 fn default() -> Self {
1006 Self::new(AdvancedOptimizationConfig::default())
1007 }
1008}
1009
1010impl TrustRegionSVM {
1011 pub fn new(config: AdvancedOptimizationConfig) -> Self {
1013 Self {
1014 config,
1015 kernel: None,
1016 is_fitted: false,
1017 }
1018 }
1019
1020 pub fn fit(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<OptimizationResult> {
1022 if x.nrows() != y.len() {
1024 return Err(SklearsError::InvalidInput(
1025 "Number of samples must match number of labels".to_string(),
1026 ));
1027 }
1028
1029 let n_samples = x.nrows();
1030 self.kernel = Some(self.config.kernel.clone());
1031
1032 let k_matrix = self.compute_kernel_matrix(x)?;
1034
1035 let mut alpha = Array1::zeros(n_samples);
1037 let mut trust_radius = self.config.trust_radius;
1038 let mut history = Vec::new();
1039
1040 for iteration in 0..self.config.max_iter {
1042 let gradient = self.compute_dual_gradient(&k_matrix, &alpha);
1044 let hessian = self.compute_dual_hessian(&k_matrix);
1045
1046 let step = self.solve_trust_region_subproblem(&gradient, &hessian, trust_radius)?;
1048
1049 let current_obj = self.calculate_dual_objective(&k_matrix, &alpha);
1054 let new_alpha = self.project_onto_constraints(&(&alpha + &step));
1055 let new_obj = self.calculate_dual_objective(&k_matrix, &new_alpha);
1056
1057 let actual_reduction = new_obj - current_obj;
1058 let predicted_reduction = self.compute_predicted_reduction(&gradient, &hessian, &step);
1059
1060 let ratio = if predicted_reduction.abs() < 1e-12 {
1062 0.0
1063 } else {
1064 actual_reduction / predicted_reduction
1065 };
1066
1067 if self.config.verbose && iteration % 10 == 0 {
1068 println!(
1069 "Trust Region Iter {}: Obj = {:.6}, Trust Radius = {:.6}, Ratio = {:.3}",
1070 iteration, current_obj, trust_radius, ratio
1071 );
1072 }
1073
1074 let step_norm = step.dot(&step).sqrt();
1076 if ratio > 0.75 && (step_norm - trust_radius).abs() < 1e-6 {
1077 trust_radius = (2.0 * trust_radius).min(10.0);
1079 alpha = new_alpha;
1080 } else if ratio > 0.25 {
1081 alpha = new_alpha;
1083 } else if ratio > 0.0 {
1084 trust_radius *= 0.5;
1086 alpha = new_alpha;
1087 } else {
1088 trust_radius *= 0.25;
1090 }
1091
1092 trust_radius = trust_radius.max(1e-8);
1094
1095 history.push(current_obj);
1096
1097 if gradient.dot(&gradient).sqrt() < self.config.tol || trust_radius < 1e-8 {
1099 if self.config.verbose {
1100 println!("Trust Region converged after {} iterations", iteration + 1);
1101 }
1102
1103 let support_indices = self.find_support_vectors(&alpha)?;
1104 let intercept = self.calculate_intercept(x, y, &alpha, &support_indices)?;
1105
1106 self.is_fitted = true;
1107
1108 return Ok(OptimizationResult {
1109 dual_coef: alpha,
1110 intercept,
1111 support_indices,
1112 n_iterations: iteration + 1,
1113 objective_value: current_obj,
1114 converged: true,
1115 history,
1116 });
1117 }
1118 }
1119
1120 let support_indices = self.find_support_vectors(&alpha)?;
1122 let intercept = self.calculate_intercept(x, y, &alpha, &support_indices)?;
1123
1124 self.is_fitted = true;
1125
1126 Ok(OptimizationResult {
1127 dual_coef: alpha,
1128 intercept,
1129 support_indices,
1130 n_iterations: self.config.max_iter,
1131 objective_value: history.last().copied().unwrap_or(0.0),
1132 converged: false,
1133 history,
1134 })
1135 }
1136
1137 fn solve_trust_region_subproblem(
1139 &self,
1140 gradient: &Array1<f64>,
1141 hessian: &Array2<f64>,
1142 trust_radius: f64,
1143 ) -> Result<Array1<f64>> {
1144 let cauchy_step = self.compute_cauchy_point(gradient, hessian, trust_radius);
1146
1147 let neg_gradient = gradient.mapv(|v| -v);
1149 let newton_step = solve_linear_system(hessian, &neg_gradient, self.config.newton_reg);
1150 let newton_norm = newton_step.dot(&newton_step).sqrt();
1151
1152 if newton_norm <= trust_radius {
1153 return Ok(newton_step);
1155 }
1156
1157 Ok(self.dogleg_method(&newton_step, &cauchy_step, trust_radius))
1159 }
1160
1161 fn compute_cauchy_point(
1163 &self,
1164 gradient: &Array1<f64>,
1165 hessian: &Array2<f64>,
1166 trust_radius: f64,
1167 ) -> Array1<f64> {
1168 let grad_norm = gradient.dot(gradient).sqrt();
1169
1170 if grad_norm < 1e-12 {
1171 return Array1::zeros(gradient.len());
1172 }
1173
1174 let unit_grad = gradient / grad_norm;
1175 let hess_grad = hessian.dot(&unit_grad);
1176 let curvature = unit_grad.dot(&hess_grad);
1177
1178 if curvature <= 0.0 {
1179 &unit_grad * (-trust_radius)
1181 } else {
1182 let optimal_step = grad_norm / curvature;
1184 let step_length = optimal_step.min(trust_radius);
1185 &unit_grad * (-step_length)
1186 }
1187 }
1188
1189 fn dogleg_method(
1191 &self,
1192 newton_step: &Array1<f64>,
1193 cauchy_step: &Array1<f64>,
1194 trust_radius: f64,
1195 ) -> Array1<f64> {
1196 let cauchy_norm = cauchy_step.dot(cauchy_step).sqrt();
1197
1198 if cauchy_norm >= trust_radius {
1199 return cauchy_step * (trust_radius / cauchy_norm);
1201 }
1202
1203 let dogleg_direction = newton_step - cauchy_step;
1205 let a = dogleg_direction.dot(&dogleg_direction);
1206 let b = 2.0 * cauchy_step.dot(&dogleg_direction);
1207 let c = cauchy_norm * cauchy_norm - trust_radius * trust_radius;
1208
1209 if a < 1e-12 {
1210 return cauchy_step.clone();
1211 }
1212
1213 let discriminant = b * b - 4.0 * a * c;
1214 if discriminant < 0.0 {
1215 return cauchy_step.clone();
1216 }
1217
1218 let tau = (-b + discriminant.sqrt()) / (2.0 * a);
1219 let tau = tau.clamp(0.0, 1.0);
1220
1221 cauchy_step + &(&dogleg_direction * tau)
1222 }
1223
1224 fn compute_predicted_reduction(
1226 &self,
1227 gradient: &Array1<f64>,
1228 hessian: &Array2<f64>,
1229 step: &Array1<f64>,
1230 ) -> f64 {
1231 let linear_term = gradient.dot(step);
1232 let quadratic_term = 0.5 * step.dot(&hessian.dot(step));
1233 -(linear_term + quadratic_term)
1234 }
1235
1236 fn project_onto_constraints(&self, alpha: &Array1<f64>) -> Array1<f64> {
1238 alpha.mapv(|val| val.max(0.0).min(self.config.c))
1239 }
1240
1241 fn compute_dual_gradient(&self, k_matrix: &Array2<f64>, alpha: &Array1<f64>) -> Array1<f64> {
1248 &k_matrix.dot(alpha) - &Array1::from_elem(alpha.len(), 1.0)
1249 }
1250
1251 fn compute_dual_hessian(&self, k_matrix: &Array2<f64>) -> Array2<f64> {
1256 k_matrix.clone()
1257 }
1258
1259 fn compute_kernel_matrix(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
1261 let kernel_type = self
1262 .kernel
1263 .as_ref()
1264 .ok_or_else(|| SklearsError::NotFitted {
1265 operation: "compute_kernel_matrix".to_string(),
1266 })?;
1267 let kernel = create_kernel(kernel_type.clone())?;
1268 let n = x.nrows();
1269 let mut k_matrix = Array2::zeros((n, n));
1270
1271 for i in 0..n {
1272 for j in 0..n {
1273 k_matrix[[i, j]] = kernel.compute(x.row(i), x.row(j));
1274 }
1275 }
1276
1277 Ok(k_matrix)
1278 }
1279
1280 fn calculate_dual_objective(&self, k_matrix: &Array2<f64>, alpha: &Array1<f64>) -> f64 {
1282 alpha.sum() - 0.5 * alpha.dot(&k_matrix.dot(alpha))
1283 }
1284
1285 fn find_support_vectors(&self, alpha: &Array1<f64>) -> Result<Vec<usize>> {
1287 let support_indices: Vec<usize> = alpha
1288 .iter()
1289 .enumerate()
1290 .filter(|(_, &val)| val > self.config.tol)
1291 .map(|(i, _)| i)
1292 .collect();
1293
1294 Ok(support_indices)
1295 }
1296
1297 fn calculate_intercept(
1299 &self,
1300 x: &Array2<f64>,
1301 y: &Array1<f64>,
1302 alpha: &Array1<f64>,
1303 support_indices: &[usize],
1304 ) -> Result<f64> {
1305 if support_indices.is_empty() {
1306 return Ok(0.0);
1307 }
1308
1309 let kernel_type = self
1310 .kernel
1311 .as_ref()
1312 .ok_or_else(|| SklearsError::NotFitted {
1313 operation: "calculate_intercept".to_string(),
1314 })?;
1315 let kernel = create_kernel(kernel_type.clone())?;
1316 let mut intercept_sum = 0.0;
1317 let mut count = 0;
1318
1319 for &i in support_indices {
1320 if alpha[i] > self.config.tol && alpha[i] < self.config.c - self.config.tol {
1321 let mut decision_value = 0.0;
1322 for &j in support_indices {
1323 decision_value += alpha[j] * y[j] * kernel.compute(x.row(i), x.row(j));
1324 }
1325 intercept_sum += y[i] - decision_value;
1326 count += 1;
1327 }
1328 }
1329
1330 Ok(if count > 0 {
1331 intercept_sum / count as f64
1332 } else {
1333 0.0
1334 })
1335 }
1336
1337 pub fn predict(&self, x: &Array2<f64>, result: &OptimizationResult) -> Result<Array1<f64>> {
1339 if !self.is_fitted {
1340 return Err(SklearsError::NotFitted {
1341 operation: "prediction".to_string(),
1342 });
1343 }
1344
1345 let decision_values = self.decision_function(x, result)?;
1346 Ok(Array1::from_vec(
1347 decision_values
1348 .iter()
1349 .map(|&val| if val > 0.0 { 1.0 } else { -1.0 })
1350 .collect(),
1351 ))
1352 }
1353
1354 pub fn decision_function(
1356 &self,
1357 x: &Array2<f64>,
1358 result: &OptimizationResult,
1359 ) -> Result<Array1<f64>> {
1360 if !self.is_fitted {
1361 return Err(SklearsError::NotFitted {
1362 operation: "prediction".to_string(),
1363 });
1364 }
1365
1366 let kernel_type = self
1367 .kernel
1368 .as_ref()
1369 .ok_or_else(|| SklearsError::NotFitted {
1370 operation: "decision_function".to_string(),
1371 })?;
1372 let kernel = create_kernel(kernel_type.clone())?;
1373 let mut decision_values = Array1::zeros(x.nrows());
1374
1375 for i in 0..x.nrows() {
1376 let mut sum = 0.0;
1377 for &j in &result.support_indices {
1378 sum += result.dual_coef[j] * kernel.compute(x.row(i), x.row(j));
1379 }
1380 decision_values[i] = sum + result.intercept;
1381 }
1382
1383 Ok(decision_values)
1384 }
1385}
1386
1387#[derive(Debug, Clone)]
1399pub struct AcceleratedGradientSVM {
1400 config: AdvancedOptimizationConfig,
1401 kernel: Option<KernelType>,
1402 is_fitted: bool,
1403 pub momentum: f64,
1405 pub learning_rate: f64,
1407 pub method: AcceleratedMethod,
1409}
1410
1411#[derive(Debug, Clone)]
1413pub enum AcceleratedMethod {
1414 Nesterov,
1416 FISTA,
1418 HeavyBall,
1420}
1421
1422impl AcceleratedGradientSVM {
1423 pub fn new(config: AdvancedOptimizationConfig) -> Self {
1425 Self {
1426 config,
1427 kernel: None,
1428 is_fitted: false,
1429 momentum: 0.9,
1430 learning_rate: 0.01,
1431 method: AcceleratedMethod::Nesterov,
1432 }
1433 }
1434
1435 pub fn with_momentum(mut self, momentum: f64) -> Self {
1437 self.momentum = momentum;
1438 self
1439 }
1440
1441 pub fn with_learning_rate(mut self, learning_rate: f64) -> Self {
1443 self.learning_rate = learning_rate;
1444 self
1445 }
1446
1447 pub fn with_method(mut self, method: AcceleratedMethod) -> Self {
1449 self.method = method;
1450 self
1451 }
1452
1453 pub fn fit(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<OptimizationResult> {
1455 if x.nrows() != y.len() {
1457 return Err(SklearsError::InvalidInput(
1458 "Number of samples must match number of labels".to_string(),
1459 ));
1460 }
1461
1462 let n_samples = x.nrows();
1463 self.kernel = Some(self.config.kernel.clone());
1464
1465 let k_matrix = self.compute_kernel_matrix(x)?;
1467
1468 let mut alpha: Array1<f64> = Array1::zeros(n_samples);
1470 let mut t = 1.0; let mut history = Vec::new();
1472
1473 let mut current_lr = self.learning_rate;
1475
1476 for iteration in 0..self.config.max_iter {
1478 let gradient = self.compute_dual_gradient(&k_matrix, &alpha);
1480
1481 let objective = self.calculate_objective(&k_matrix, &alpha)?;
1483 history.push(objective);
1484
1485 if self.config.verbose && iteration % 10 == 0 {
1486 println!(
1487 "Accelerated Gradient Iteration {}: Objective = {:.6}",
1488 iteration, objective
1489 );
1490 }
1491
1492 let alpha_prev = alpha.clone();
1494
1495 match self.method {
1497 AcceleratedMethod::Nesterov => {
1498 let momentum_coeff = if iteration == 0 { 0.0 } else { self.momentum };
1500
1501 let momentum_term = (&alpha - &alpha_prev) * momentum_coeff;
1503
1504 let y_k = &alpha + &momentum_term;
1506
1507 let gradient_at_y = self.compute_dual_gradient(&k_matrix, &y_k);
1509 alpha = &y_k - &(&gradient_at_y * current_lr);
1510 }
1511 AcceleratedMethod::FISTA => {
1512 let gradient_step = &alpha - &(&gradient * current_lr);
1514
1515 let alpha_new = self.proximal_operator(&gradient_step)?;
1517
1518 let t_new = (1.0_f64 + (1.0_f64 + 4.0_f64 * t * t).sqrt()) / 2.0_f64;
1520 let beta = (t - 1.0) / t_new;
1521
1522 let _y_k = &alpha_new + &((&alpha_new - &alpha) * beta);
1524 alpha = alpha_new;
1525 t = t_new;
1526 }
1527 AcceleratedMethod::HeavyBall => {
1528 let momentum_coeff = if iteration == 0 { 0.0 } else { self.momentum };
1530
1531 let alpha_new = &(&alpha - &(&gradient * current_lr))
1533 + &((&alpha - &alpha_prev) * momentum_coeff);
1534 alpha = alpha_new;
1535 }
1536 }
1537
1538 for i in 0..n_samples {
1540 alpha[i] = alpha[i].max(0.0).min(self.config.c);
1541 }
1542
1543 let gradient_norm = gradient.dot(&gradient).sqrt();
1545 if gradient_norm < self.config.tol {
1546 if self.config.verbose {
1547 println!(
1548 "Accelerated Gradient converged after {} iterations",
1549 iteration + 1
1550 );
1551 }
1552
1553 self.is_fitted = true;
1554
1555 let support_indices = self.find_support_vectors(&alpha)?;
1556 let intercept = self.calculate_intercept(x, y, &alpha, &support_indices)?;
1557
1558 return Ok(OptimizationResult {
1559 dual_coef: alpha,
1560 intercept,
1561 support_indices,
1562 n_iterations: iteration + 1,
1563 objective_value: objective,
1564 converged: true,
1565 history,
1566 });
1567 }
1568
1569 if iteration > 0 && history.len() >= 2 {
1571 let prev_obj = history[history.len() - 2];
1572 let curr_obj = history[history.len() - 1];
1573
1574 if curr_obj > prev_obj {
1576 current_lr *= 0.8;
1577 } else if curr_obj < prev_obj && (prev_obj - curr_obj) / prev_obj.abs() > 0.01 {
1578 current_lr *= 1.05;
1580 }
1581
1582 current_lr = current_lr.clamp(1e-6, 1.0);
1584 }
1585 }
1586
1587 self.is_fitted = true;
1588
1589 let support_indices = self.find_support_vectors(&alpha)?;
1591 let intercept = self.calculate_intercept(x, y, &alpha, &support_indices)?;
1592
1593 Ok(OptimizationResult {
1594 dual_coef: alpha,
1595 intercept,
1596 support_indices,
1597 n_iterations: self.config.max_iter,
1598 objective_value: history.last().copied().unwrap_or(0.0),
1599 converged: false,
1600 history,
1601 })
1602 }
1603
1604 fn proximal_operator(&self, x: &Array1<f64>) -> Result<Array1<f64>> {
1606 let mut result = x.clone();
1607
1608 for i in 0..result.len() {
1610 result[i] = result[i].max(0.0).min(self.config.c);
1611 }
1612
1613 Ok(result)
1614 }
1615
1616 fn compute_dual_gradient(&self, k_matrix: &Array2<f64>, alpha: &Array1<f64>) -> Array1<f64> {
1618 let n = alpha.len();
1619 let mut gradient = Array1::from_elem(n, -1.0); for i in 0..n {
1623 for j in 0..n {
1624 gradient[i] += k_matrix[[i, j]] * alpha[j];
1625 }
1626 }
1627
1628 gradient
1629 }
1630
1631 fn compute_kernel_matrix(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
1633 let kernel_type = self
1634 .kernel
1635 .as_ref()
1636 .ok_or_else(|| SklearsError::NotFitted {
1637 operation: "compute_kernel_matrix".to_string(),
1638 })?;
1639 let kernel = create_kernel(kernel_type.clone())?;
1640 let n = x.nrows();
1641 let mut k_matrix = Array2::zeros((n, n));
1642
1643 for i in 0..n {
1644 for j in 0..n {
1645 k_matrix[[i, j]] = kernel.compute(x.row(i), x.row(j));
1646 }
1647 }
1648
1649 Ok(k_matrix)
1650 }
1651
1652 fn calculate_objective(&self, k_matrix: &Array2<f64>, alpha: &Array1<f64>) -> Result<f64> {
1654 let n = alpha.len();
1655 let mut objective = 0.0;
1656
1657 for i in 0..n {
1659 objective += alpha[i]; for j in 0..n {
1661 objective -= 0.5 * alpha[i] * alpha[j] * k_matrix[[i, j]]; }
1663 }
1664
1665 Ok(objective)
1666 }
1667
1668 fn find_support_vectors(&self, alpha: &Array1<f64>) -> Result<Vec<usize>> {
1670 let mut support_indices = Vec::new();
1671 let tol = 1e-6;
1672
1673 for (i, &alpha_i) in alpha.iter().enumerate() {
1674 if alpha_i > tol && alpha_i < self.config.c - tol {
1675 support_indices.push(i);
1676 }
1677 }
1678
1679 Ok(support_indices)
1680 }
1681
1682 fn calculate_intercept(
1684 &self,
1685 x: &Array2<f64>,
1686 y: &Array1<f64>,
1687 alpha: &Array1<f64>,
1688 support_indices: &[usize],
1689 ) -> Result<f64> {
1690 if support_indices.is_empty() {
1691 return Ok(0.0);
1692 }
1693
1694 let kernel_type = self
1695 .kernel
1696 .as_ref()
1697 .ok_or_else(|| SklearsError::NotFitted {
1698 operation: "calculate_intercept".to_string(),
1699 })?;
1700 let kernel = create_kernel(kernel_type.clone())?;
1701 let mut intercept_sum = 0.0;
1702
1703 for &sv_idx in support_indices {
1704 let mut kernel_sum = 0.0;
1705 for (j, &alpha_j) in alpha.iter().enumerate() {
1706 if alpha_j > 0.0 {
1707 kernel_sum += alpha_j * y[j] * kernel.compute(x.row(sv_idx), x.row(j));
1708 }
1709 }
1710 intercept_sum += y[sv_idx] - kernel_sum;
1711 }
1712
1713 Ok(intercept_sum / support_indices.len() as f64)
1714 }
1715
1716 pub fn predict(&self, x: &Array2<f64>, result: &OptimizationResult) -> Result<Array1<f64>> {
1718 let decision_values = self.decision_function(x, result)?;
1719 let mut predictions = Array1::zeros(decision_values.len());
1720
1721 for (i, &val) in decision_values.iter().enumerate() {
1722 predictions[i] = if val >= 0.0 { 1.0 } else { -1.0 };
1723 }
1724
1725 Ok(predictions)
1726 }
1727
1728 pub fn decision_function(
1730 &self,
1731 x: &Array2<f64>,
1732 result: &OptimizationResult,
1733 ) -> Result<Array1<f64>> {
1734 let kernel_type = self
1735 .kernel
1736 .as_ref()
1737 .ok_or_else(|| SklearsError::NotFitted {
1738 operation: "decision_function".to_string(),
1739 })?;
1740 let kernel = create_kernel(kernel_type.clone())?;
1741 let n_test = x.nrows();
1742 let mut decision_values = Array1::zeros(n_test);
1743
1744 for i in 0..n_test {
1745 let mut sum = 0.0;
1746 for &j in &result.support_indices {
1747 sum += result.dual_coef[j] * kernel.compute(x.row(i), x.row(j));
1748 }
1749 decision_values[i] = sum + result.intercept;
1750 }
1751
1752 Ok(decision_values)
1753 }
1754}
1755
1756#[allow(non_snake_case)]
1757#[cfg(test)]
1758#[path = "advanced_optimization_tests.rs"]
1759mod tests;