Skip to main content

qualia_core_db/solvers/learning/classification/
svm_multiclass.rs

1//! Multiclass SVM by one-vs-rest (ISL ch 9.4.2) — train one binary SVM per class
2//! (that class vs. all others) and predict the class whose decision value is
3//! largest. Reuses the binary [`super::svm`] (no duplicated SMO).
4
5use crate::solvers::learning::classification::svm::{self, Kernel, Svm};
6use crate::solvers::learning::LearningError;
7
8/// A one-vs-rest multiclass SVM.
9#[derive(Debug, Clone)]
10pub struct MulticlassSvm {
11    classes: Vec<usize>,
12    svms: Vec<Svm>,
13    p: usize,
14}
15
16impl MulticlassSvm {
17    /// Fit one binary SVM per class (class vs rest). Fails closed if a class is
18    /// absent or the binary fit fails (e.g. a degenerate split).
19    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    /// Predict the class whose one-vs-rest decision value is largest.
48    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        // Three clusters: 0 near (0,0), 1 near (10,0), 2 near (5,10).
75        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}