qualia_core_db/solvers/learning/classification/
svm_multiclass.rs1use crate::solvers::learning::classification::svm::{self, Kernel, Svm};
6use crate::solvers::learning::LearningError;
7
8#[derive(Debug, Clone)]
10pub struct MulticlassSvm {
11 classes: Vec<usize>,
12 svms: Vec<Svm>,
13 p: usize,
14}
15
16impl MulticlassSvm {
17 pub fn fit_one_vs_rest(
20 x: &[f64],
21 y: &[usize],
22 n: usize,
23 p: usize,
24 c: f64,
25 kernel: Kernel,
26 max_passes: usize,
27 tol: f64,
28 ) -> Result<Self, LearningError> {
29 if n == 0 || p == 0 || x.len() != n * p || y.len() != n {
30 return Err(LearningError::InvalidDimension);
31 }
32 let mut classes: Vec<usize> = y.to_vec();
33 classes.sort_unstable();
34 classes.dedup();
35 if classes.len() < 2 {
36 return Err(LearningError::InsufficientData);
37 }
38 let mut svms = Vec::with_capacity(classes.len());
39 for &cls in &classes {
40 let binary: Vec<bool> = y.iter().map(|&yi| yi == cls).collect();
41 let svm = svm::fit(x, &binary, n, p, c, kernel, max_passes, tol)?;
42 svms.push(svm);
43 }
44 Ok(Self { classes, svms, p })
45 }
46
47 pub fn predict_row(&self, q: &[f64]) -> usize {
49 let mut best = 0;
50 let mut best_d = f64::NEG_INFINITY;
51 for (i, svm) in self.svms.iter().enumerate() {
52 let d = svm.decision_row(q);
53 if d > best_d {
54 best_d = d;
55 best = i;
56 }
57 }
58 self.classes[best]
59 }
60
61 pub fn predict(&self, x: &[f64], m: usize) -> Vec<usize> {
62 (0..m)
63 .map(|i| self.predict_row(&x[i * self.p..(i + 1) * self.p]))
64 .collect()
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn separates_three_classes() {
74 let mut x = Vec::new();
76 let mut y = Vec::new();
77 for &(cx, cy, lbl) in &[(0.0, 0.0, 0usize), (10.0, 0.0, 1), (5.0, 10.0, 2)] {
78 for d in 0..5 {
79 x.push(cx + (d as f64 - 2.0) * 0.2);
80 x.push(cy + (d as f64 - 2.0) * 0.2);
81 y.push(lbl);
82 }
83 }
84 let n = 15;
85 let m =
86 MulticlassSvm::fit_one_vs_rest(&x, &y, n, 2, 1.0, Kernel::Linear, 30, 1e-3).unwrap();
87 assert_eq!(m.predict_row(&[0.0, 0.0]), 0);
88 assert_eq!(m.predict_row(&[10.0, 0.0]), 1);
89 assert_eq!(m.predict_row(&[5.0, 10.0]), 2);
90 }
91
92 #[test]
93 fn guards() {
94 assert_eq!(
95 MulticlassSvm::fit_one_vs_rest(
96 &[0.0, 0.0, 1.0, 1.0],
97 &[0, 0],
98 2,
99 2,
100 1.0,
101 Kernel::Linear,
102 5,
103 1e-3
104 )
105 .unwrap_err(),
106 LearningError::InsufficientData
107 );
108 }
109}