1use crate::kernels::{Kernel, KernelType};
9use scirs2_core::ndarray::{Array1, Array2};
10use scirs2_linalg::compat::ArrayLinalgExt;
11use sklears_core::{
13 error::{Result, SklearsError},
14 traits::{Fit, Predict, Trained, Untrained},
15 types::Float,
16};
17use std::marker::PhantomData;
18
19#[derive(Debug, Clone)]
21pub struct LSVMConfig {
22 pub gamma: Float,
24 pub kernel: KernelType,
26 pub fit_intercept: bool,
28 pub tol: Float,
30 pub regularized_kernel: bool,
32}
33
34impl Default for LSVMConfig {
35 fn default() -> Self {
36 Self {
37 gamma: 1.0,
38 kernel: KernelType::Rbf { gamma: 1.0 },
39 fit_intercept: true,
40 tol: 1e-12,
41 regularized_kernel: true,
42 }
43 }
44}
45
46#[derive(Debug)]
48pub struct LSSVM<State = Untrained> {
49 config: LSVMConfig,
50 state: PhantomData<State>,
51 support_vectors_: Option<Array2<Float>>,
53 alpha_: Option<Array1<Float>>,
54 intercept_: Option<Float>,
55 training_labels_: Option<Array1<Float>>,
56 n_features_in_: Option<usize>,
57}
58
59impl LSSVM<Untrained> {
60 pub fn new() -> Self {
62 Self {
63 config: LSVMConfig::default(),
64 state: PhantomData,
65 support_vectors_: None,
66 alpha_: None,
67 intercept_: None,
68 training_labels_: None,
69 n_features_in_: None,
70 }
71 }
72
73 pub fn gamma(mut self, gamma: Float) -> Self {
75 self.config.gamma = gamma;
76 self
77 }
78
79 pub fn kernel(mut self, kernel: KernelType) -> Self {
81 self.config.kernel = kernel;
82 self
83 }
84
85 pub fn fit_intercept(mut self, fit_intercept: bool) -> Self {
87 self.config.fit_intercept = fit_intercept;
88 self
89 }
90
91 pub fn tol(mut self, tol: Float) -> Self {
93 self.config.tol = tol;
94 self
95 }
96
97 pub fn regularized_kernel(mut self, regularized: bool) -> Self {
99 self.config.regularized_kernel = regularized;
100 self
101 }
102}
103
104impl Default for LSSVM<Untrained> {
105 fn default() -> Self {
106 Self::new()
107 }
108}
109
110impl Fit<Array2<Float>, Array1<Float>> for LSSVM<Untrained> {
111 type Fitted = LSSVM<Trained>;
112
113 fn fit(self, x: &Array2<Float>, y: &Array1<Float>) -> Result<Self::Fitted> {
114 let n_samples = x.nrows();
115 let n_features = x.ncols();
116
117 if n_samples == 0 {
118 return Err(SklearsError::InvalidInput("Empty dataset".to_string()));
119 }
120
121 if n_samples != y.len() {
122 return Err(SklearsError::InvalidInput(
123 "Shape mismatch: X and y must have the same number of samples".to_string(),
124 ));
125 }
126
127 let kernel = match &self.config.kernel {
129 KernelType::Linear => Box::new(crate::kernels::LinearKernel) as Box<dyn Kernel>,
130 KernelType::Rbf { gamma } => {
131 Box::new(crate::kernels::RbfKernel::new(*gamma)) as Box<dyn Kernel>
132 }
133 _ => Box::new(crate::kernels::RbfKernel::new(1.0)) as Box<dyn Kernel>, };
135
136 let mut k_matrix = Array2::zeros((n_samples, n_samples));
138 for i in 0..n_samples {
139 for j in 0..n_samples {
140 let k_val = kernel.compute(x.row(i), x.row(j));
141 k_matrix[[i, j]] = k_val;
142 }
143 }
144
145 if self.config.regularized_kernel {
147 for i in 0..n_samples {
148 k_matrix[[i, i]] += 1.0 / self.config.gamma;
149 }
150 }
151
152 let (alpha, intercept) = if self.config.fit_intercept {
154 self.solve_with_intercept(&k_matrix, y)?
155 } else {
156 let alpha = self.solve_without_intercept(&k_matrix, y)?;
157 (alpha, 0.0)
158 };
159
160 Ok(LSSVM {
161 config: self.config,
162 state: PhantomData,
163 support_vectors_: Some(x.clone()),
164 alpha_: Some(alpha),
165 intercept_: Some(intercept),
166 training_labels_: Some(y.clone()),
167 n_features_in_: Some(n_features),
168 })
169 }
170}
171
172impl LSSVM<Untrained> {
173 fn solve_with_intercept(
175 &self,
176 k_matrix: &Array2<Float>,
177 y: &Array1<Float>,
178 ) -> Result<(Array1<Float>, Float)> {
179 let n = k_matrix.nrows();
180
181 let mut a_matrix = Array2::zeros((n + 1, n + 1));
183 let mut b_vector = Array1::zeros(n + 1);
184
185 for i in 0..n {
187 for j in 0..n {
188 a_matrix[[i, j]] = k_matrix[[i, j]];
189 }
190 }
191
192 if !self.config.regularized_kernel {
194 for i in 0..n {
195 a_matrix[[i, i]] += 1.0 / self.config.gamma;
196 }
197 }
198
199 for i in 0..n {
201 a_matrix[[i, n]] = 1.0;
202 a_matrix[[n, i]] = 1.0;
203 }
204
205 for i in 0..n {
207 b_vector[i] = y[i];
208 }
209
210 let solution = a_matrix.solve(&b_vector).map_err(|e| {
212 SklearsError::NumericalError(format!("Failed to solve LS-SVM linear system: {}", e))
213 })?;
214
215 let alpha = solution.slice(scirs2_core::ndarray::s![..n]).to_owned();
216 let intercept = solution[n];
217
218 Ok((alpha, intercept))
219 }
220
221 fn solve_without_intercept(
223 &self,
224 k_matrix: &Array2<Float>,
225 y: &Array1<Float>,
226 ) -> Result<Array1<Float>> {
227 let n = k_matrix.nrows();
228 let mut a_matrix = k_matrix.clone();
229
230 if !self.config.regularized_kernel {
232 for i in 0..n {
233 a_matrix[[i, i]] += 1.0 / self.config.gamma;
234 }
235 }
236
237 let alpha = a_matrix.solve(y).map_err(|e| {
239 SklearsError::NumericalError(format!("Failed to solve LS-SVM linear system: {}", e))
240 })?;
241
242 Ok(alpha)
243 }
244}
245
246impl Predict<Array2<Float>, Array1<Float>> for LSSVM<Trained> {
247 fn predict(&self, x: &Array2<Float>) -> Result<Array1<Float>> {
248 if x.ncols()
249 != self
250 .n_features_in_
251 .expect("n_features_in_ not available - model not fitted")
252 {
253 return Err(SklearsError::InvalidInput(
254 "Feature mismatch: X has different number of features than training data"
255 .to_string(),
256 ));
257 }
258
259 let support_vectors = self
260 .support_vectors_
261 .as_ref()
262 .expect("support_vectors_ not available - model not fitted");
263 let alpha = self
264 .alpha_
265 .as_ref()
266 .expect("alpha_ not available - model not fitted");
267 let intercept = self
268 .intercept_
269 .expect("intercept_ not available - model not fitted");
270 let kernel = match &self.config.kernel {
271 KernelType::Linear => Box::new(crate::kernels::LinearKernel) as Box<dyn Kernel>,
272 KernelType::Rbf { gamma } => {
273 Box::new(crate::kernels::RbfKernel::new(*gamma)) as Box<dyn Kernel>
274 }
275 _ => Box::new(crate::kernels::RbfKernel::new(1.0)) as Box<dyn Kernel>, };
277
278 let mut predictions = Array1::zeros(x.nrows());
279
280 for i in 0..x.nrows() {
281 let mut prediction = if self.config.fit_intercept {
282 intercept
283 } else {
284 0.0
285 };
286
287 for j in 0..support_vectors.nrows() {
288 let k_val = kernel.compute(x.row(i), support_vectors.row(j));
289 prediction += alpha[j] * k_val;
290 }
291
292 predictions[i] = prediction;
293 }
294
295 Ok(predictions)
296 }
297}
298
299impl LSSVM<Trained> {
300 pub fn support_vectors(&self) -> &Array2<Float> {
302 self.support_vectors_
303 .as_ref()
304 .expect("support_vectors_ not available - model not fitted")
305 }
306
307 pub fn alpha(&self) -> &Array1<Float> {
309 self.alpha_
310 .as_ref()
311 .expect("alpha_ not available - model not fitted")
312 }
313
314 pub fn intercept(&self) -> Float {
316 self.intercept_
317 .expect("intercept_ not available - model not fitted")
318 }
319
320 pub fn training_labels(&self) -> &Array1<Float> {
322 self.training_labels_
323 .as_ref()
324 .expect("training_labels_ not available - model not fitted")
325 }
326
327 pub fn n_features_in(&self) -> usize {
329 self.n_features_in_
330 .expect("n_features_in_ not available - model not fitted")
331 }
332
333 pub fn decision_function(&self, x: &Array2<Float>) -> Result<Array1<Float>> {
335 self.predict(x)
336 }
337
338 pub fn kernel_matrix(&self) -> Result<Array2<Float>> {
340 let support_vectors = self.support_vectors();
341 let n_samples = support_vectors.nrows();
342 let kernel = match &self.config.kernel {
343 KernelType::Linear => Box::new(crate::kernels::LinearKernel) as Box<dyn Kernel>,
344 KernelType::Rbf { gamma } => {
345 Box::new(crate::kernels::RbfKernel::new(*gamma)) as Box<dyn Kernel>
346 }
347 _ => Box::new(crate::kernels::RbfKernel::new(1.0)) as Box<dyn Kernel>, };
349
350 let mut k_matrix = Array2::zeros((n_samples, n_samples));
351 for i in 0..n_samples {
352 for j in 0..n_samples {
353 let k_val = kernel.compute(support_vectors.row(i), support_vectors.row(j));
354 k_matrix[[i, j]] = k_val;
355 }
356 }
357
358 Ok(k_matrix)
359 }
360}
361
362#[derive(Debug)]
364pub struct LSVMClassifier<State = Untrained> {
365 lssvm: LSSVM<State>,
366}
367
368impl LSVMClassifier<Untrained> {
369 pub fn new() -> Self {
371 Self {
372 lssvm: LSSVM::new(),
373 }
374 }
375
376 pub fn gamma(mut self, gamma: Float) -> Self {
378 self.lssvm = self.lssvm.gamma(gamma);
379 self
380 }
381
382 pub fn kernel(mut self, kernel: KernelType) -> Self {
384 self.lssvm = self.lssvm.kernel(kernel);
385 self
386 }
387
388 pub fn fit_intercept(mut self, fit_intercept: bool) -> Self {
390 self.lssvm = self.lssvm.fit_intercept(fit_intercept);
391 self
392 }
393}
394
395impl Default for LSVMClassifier<Untrained> {
396 fn default() -> Self {
397 Self::new()
398 }
399}
400
401impl Fit<Array2<Float>, Array1<i32>> for LSVMClassifier<Untrained> {
402 type Fitted = LSVMClassifier<Trained>;
403
404 fn fit(self, x: &Array2<Float>, y: &Array1<i32>) -> Result<Self::Fitted> {
405 let unique_classes: Vec<i32> = {
407 let mut classes = y.to_vec();
408 classes.sort_unstable();
409 classes.dedup();
410 classes
411 };
412
413 if unique_classes.len() != 2 {
414 return Err(SklearsError::InvalidInput(
415 "LS-SVM classifier currently supports only binary classification".to_string(),
416 ));
417 }
418
419 let y_float = Array1::from_vec(
420 y.iter()
421 .map(|&label| {
422 if label == unique_classes[0] {
423 -1.0
424 } else {
425 1.0
426 }
427 })
428 .collect(),
429 );
430
431 let fitted_lssvm = self.lssvm.fit(x, &y_float)?;
432
433 Ok(LSVMClassifier {
434 lssvm: fitted_lssvm,
435 })
436 }
437}
438
439impl Predict<Array2<Float>, Array1<i32>> for LSVMClassifier<Trained> {
440 fn predict(&self, x: &Array2<Float>) -> Result<Array1<i32>> {
441 let decision_values = self.lssvm.predict(x)?;
442
443 let predictions = Array1::from_vec(
444 decision_values
445 .iter()
446 .map(|&val| if val >= 0.0 { 1 } else { 0 })
447 .collect(),
448 );
449
450 Ok(predictions)
451 }
452}
453
454impl LSVMClassifier<Trained> {
455 pub fn decision_function(&self, x: &Array2<Float>) -> Result<Array1<Float>> {
457 self.lssvm.decision_function(x)
458 }
459
460 pub fn lssvm(&self) -> &LSSVM<Trained> {
462 &self.lssvm
463 }
464}
465
466#[allow(non_snake_case)]
467#[cfg(test)]
468mod tests {
469 use super::*;
470 use scirs2_core::ndarray::array;
471
472 #[test]
473 fn test_lssvm_creation() {
474 let lssvm = LSSVM::new()
475 .gamma(2.0)
476 .kernel(KernelType::Linear)
477 .fit_intercept(false)
478 .tol(1e-10)
479 .regularized_kernel(false);
480
481 assert_eq!(lssvm.config.gamma, 2.0);
482 assert_eq!(lssvm.config.kernel, KernelType::Linear);
483 assert!(!lssvm.config.fit_intercept);
484 assert_eq!(lssvm.config.tol, 1e-10);
485 assert!(!lssvm.config.regularized_kernel);
486 }
487
488 #[test]
489 fn test_lssvm_classifier_creation() {
490 let classifier = LSVMClassifier::new()
491 .gamma(1.5)
492 .kernel(KernelType::Rbf { gamma: 0.5 })
493 .fit_intercept(true);
494
495 assert_eq!(classifier.lssvm.config.gamma, 1.5);
496 assert_eq!(
497 classifier.lssvm.config.kernel,
498 KernelType::Rbf { gamma: 0.5 }
499 );
500 assert!(classifier.lssvm.config.fit_intercept);
501 }
502
503 #[test]
504 #[ignore = "Slow test: trains LS-SVM. Run with --ignored flag"]
505 fn test_lssvm_regression() {
506 let x = array![[1.0], [2.0], [3.0], [4.0], [5.0],];
507 let y = array![2.0, 4.0, 6.0, 8.0, 10.0]; let lssvm = LSSVM::new()
510 .gamma(1.0)
511 .kernel(KernelType::Linear)
512 .fit_intercept(true);
513
514 let fitted_model = lssvm.fit(&x, &y).expect("model fitting should succeed");
515
516 assert_eq!(fitted_model.n_features_in(), 1);
517
518 let predictions = fitted_model.predict(&x).expect("prediction should succeed");
519 assert_eq!(predictions.len(), 5);
520
521 for (i, &pred) in predictions.iter().enumerate() {
523 let expected = 2.0 * (i + 1) as Float;
524 assert!(
525 (pred - expected).abs() < 1.0,
526 "Prediction {} should be close to {}",
527 pred,
528 expected
529 );
530 }
531 }
532
533 #[test]
534 #[ignore = "Slow test: trains LS-SVM classifier. Run with --ignored flag"]
535 fn test_lssvm_binary_classification() {
536 let x = array![
537 [1.0, 2.0],
538 [2.0, 3.0],
539 [3.0, 4.0],
540 [6.0, 7.0],
541 [7.0, 8.0],
542 [8.0, 9.0],
543 ];
544 let y = array![0, 0, 0, 1, 1, 1];
545
546 let classifier = LSVMClassifier::new()
547 .gamma(1.0)
548 .kernel(KernelType::Linear)
549 .fit_intercept(true);
550
551 let fitted_model = classifier
552 .fit(&x, &y)
553 .expect("model fitting should succeed");
554
555 let predictions = fitted_model.predict(&x).expect("prediction should succeed");
556 assert_eq!(predictions.len(), 6);
557
558 for &pred in predictions.iter() {
560 assert!(pred == 0 || pred == 1);
561 }
562
563 let decision_values = fitted_model
564 .decision_function(&x)
565 .expect("decision function should succeed");
566 assert_eq!(decision_values.len(), 6);
567
568 for &val in decision_values.iter() {
570 assert!(val.is_finite());
571 }
572 }
573
574 #[test]
575 fn test_lssvm_shape_mismatch() {
576 let x = array![[1.0, 2.0], [3.0, 4.0]];
577 let y = array![1.0]; let lssvm = LSSVM::new();
580 let result = lssvm.fit(&x, &y);
581
582 assert!(result.is_err());
583 assert!(result.unwrap_err().to_string().contains("Shape mismatch"));
584 }
585
586 #[test]
587 fn test_lssvm_classifier_multiclass_error() {
588 let x = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
589 let y = array![0, 1, 2]; let classifier = LSVMClassifier::new();
592 let result = classifier.fit(&x, &y);
593
594 assert!(result.is_err());
595 assert!(result
596 .unwrap_err()
597 .to_string()
598 .contains("binary classification"));
599 }
600
601 #[test]
602 fn test_lssvm_feature_mismatch() {
603 let x_train = array![[1.0, 2.0], [3.0, 4.0]];
604 let y_train = array![1.0, 2.0];
605 let x_test = array![[1.0, 2.0, 3.0]]; let lssvm = LSSVM::new();
608 let fitted_model = lssvm
609 .fit(&x_train, &y_train)
610 .expect("model fitting should succeed");
611 let result = fitted_model.predict(&x_test);
612
613 assert!(result.is_err());
614 assert!(result.unwrap_err().to_string().contains("Feature mismatch"));
615 }
616}