1use crate::solvers::learning::LearningError;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
11pub enum Kernel {
12 Linear,
14 Rbf { gamma: f64 },
16}
17
18impl Kernel {
19 fn eval(self, a: &[f64], b: &[f64]) -> f64 {
20 match self {
21 Kernel::Linear => a.iter().zip(b).map(|(x, y)| x * y).sum(),
22 Kernel::Rbf { gamma } => {
23 let d2: f64 = a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum();
24 (-gamma * d2).exp()
25 }
26 }
27 }
28}
29
30#[derive(Debug, Clone)]
33pub struct Svm {
34 sv_x: Vec<f64>, sv_alpha_y: Vec<f64>, b: f64,
37 kernel: Kernel,
38 p: usize,
39}
40
41struct Lcg(u64);
42impl Lcg {
43 fn below(&mut self, bound: usize) -> usize {
44 self.0 = self
45 .0
46 .wrapping_mul(6364136223846793005)
47 .wrapping_add(1442695040888963407);
48 ((self.0 >> 33) as usize) % bound.max(1)
49 }
50}
51
52fn fill_kernel_cpu(k: &mut [f64], x: &[f64], n: usize, p: usize, kernel: Kernel) {
57 for i in 0..n {
58 for j in i..n {
59 let v = kernel.eval(&x[i * p..(i + 1) * p], &x[j * p..(j + 1) * p]);
60 k[i * n + j] = v;
61 k[j * n + i] = v;
62 }
63 }
64}
65
66pub fn fit(
70 x: &[f64],
71 y: &[bool],
72 n: usize,
73 p: usize,
74 c: f64,
75 kernel: Kernel,
76 max_passes: usize,
77 tol: f64,
78) -> Result<Svm, LearningError> {
79 if n == 0 || p == 0 || x.len() != n * p || y.len() != n {
80 return Err(LearningError::InvalidDimension);
81 }
82 if !(c > 0.0) {
83 return Err(LearningError::InsufficientData);
84 }
85 let yi: Vec<f64> = y.iter().map(|&b| if b { 1.0 } else { -1.0 }).collect();
86 let n_pos = y.iter().filter(|&&b| b).count();
87 if n_pos == 0 || n_pos == n {
88 return Err(LearningError::InsufficientData); }
90
91 let mut k = vec![0.0; n * n];
100 #[cfg(all(not(target_arch = "wasm32"), feature = "wgsl-forge"))]
103 {
104 let work = n.saturating_mul(n).saturating_mul(p);
105 let caps = crate::wgsl_forge::dispatch::caps();
106 let accelerated =
107 (caps.cuda || caps.wgpu) && work >= crate::wgsl_forge::dispatch::GEMM_GPU_THRESHOLD;
108 if accelerated {
109 match kernel {
110 Kernel::Linear => {
111 use crate::solvers::linear_algebra::gemm::{gemm, Transpose};
112 if gemm(
114 Transpose::No,
115 Transpose::Yes,
116 n,
117 n,
118 p,
119 1.0,
120 x,
121 x,
122 0.0,
123 &mut k,
124 )
125 .is_err()
126 {
127 fill_kernel_cpu(&mut k, x, n, p, kernel);
128 }
129 }
130 Kernel::Rbf { gamma } => {
131 let d = crate::wgsl_forge::dispatch::pairwise_sq_dist_f64(x, x, n, n, p);
132 for (kij, &dij) in k.iter_mut().zip(d.iter()) {
133 *kij = (-gamma * dij).exp();
134 }
135 }
136 }
137 } else {
138 fill_kernel_cpu(&mut k, x, n, p, kernel);
139 }
140 }
141 #[cfg(not(all(not(target_arch = "wasm32"), feature = "wgsl-forge")))]
142 fill_kernel_cpu(&mut k, x, n, p, kernel);
143
144 let mut alpha = vec![0.0; n];
145 let mut b = 0.0;
146 let mut rng = Lcg(0x9E3779B97F4A7C15);
147
148 let f = |alpha: &[f64], b: f64, i: usize, k: &[f64]| -> f64 {
150 let mut s = b;
151 for m in 0..n {
152 if alpha[m] != 0.0 {
153 s += alpha[m] * yi[m] * k[m * n + i];
154 }
155 }
156 s
157 };
158
159 let mut passes = 0;
160 let max_iter = max_passes.max(1);
161 let hard_cap = 10_000; let mut sweeps = 0;
163 while passes < max_iter && sweeps < hard_cap {
164 sweeps += 1;
165 let mut num_changed = 0;
166 for i in 0..n {
167 let ei = f(&alpha, b, i, &k) - yi[i];
168 if (yi[i] * ei < -tol && alpha[i] < c) || (yi[i] * ei > tol && alpha[i] > 0.0) {
169 let mut j = rng.below(n);
171 if j == i {
172 j = (j + 1) % n;
173 }
174 let ej = f(&alpha, b, j, &k) - yi[j];
175 let (ai_old, aj_old) = (alpha[i], alpha[j]);
176 let (lo, hi) = if yi[i] != yi[j] {
178 ((aj_old - ai_old).max(0.0), c + (aj_old - ai_old).min(0.0))
179 } else {
180 ((ai_old + aj_old - c).max(0.0), (ai_old + aj_old).min(c))
181 };
182 if (hi - lo).abs() < 1e-12 {
183 continue;
184 }
185 let eta = 2.0 * k[i * n + j] - k[i * n + i] - k[j * n + j];
186 if eta >= 0.0 {
187 continue;
188 }
189 let mut aj = aj_old - yi[j] * (ei - ej) / eta;
190 aj = aj.clamp(lo, hi);
191 if (aj - aj_old).abs() < 1e-9 {
192 continue;
193 }
194 let ai = ai_old + yi[i] * yi[j] * (aj_old - aj);
195 let b1 = b
197 - ei
198 - yi[i] * (ai - ai_old) * k[i * n + i]
199 - yi[j] * (aj - aj_old) * k[i * n + j];
200 let b2 = b
201 - ej
202 - yi[i] * (ai - ai_old) * k[i * n + j]
203 - yi[j] * (aj - aj_old) * k[j * n + j];
204 alpha[i] = ai;
205 alpha[j] = aj;
206 b = if ai > 0.0 && ai < c {
207 b1
208 } else if aj > 0.0 && aj < c {
209 b2
210 } else {
211 0.5 * (b1 + b2)
212 };
213 num_changed += 1;
214 }
215 }
216 if num_changed == 0 {
217 passes += 1;
218 } else {
219 passes = 0;
220 }
221 }
222
223 let mut sv_x = Vec::new();
225 let mut sv_alpha_y = Vec::new();
226 for i in 0..n {
227 if alpha[i] > 1e-8 {
228 sv_x.extend_from_slice(&x[i * p..(i + 1) * p]);
229 sv_alpha_y.push(alpha[i] * yi[i]);
230 }
231 }
232 Ok(Svm {
233 sv_x,
234 sv_alpha_y,
235 b,
236 kernel,
237 p,
238 })
239}
240
241impl Svm {
242 pub fn decision_row(&self, q: &[f64]) -> f64 {
244 let mut s = self.b;
245 for (k, &ay) in self.sv_alpha_y.iter().enumerate() {
246 s += ay
247 * self
248 .kernel
249 .eval(&self.sv_x[k * self.p..(k + 1) * self.p], q);
250 }
251 s
252 }
253
254 pub fn predict_row(&self, q: &[f64]) -> bool {
256 self.decision_row(q) >= 0.0
257 }
258
259 pub fn n_support_vectors(&self) -> usize {
260 self.sv_alpha_y.len()
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn linear_svm_separates_linearly_separable() {
270 let x = [
272 0.0, 0.0, 1.0, 0.5, 0.5, 1.0, -0.5, 0.2, 6.0, 6.0, 5.5, 6.5, 6.5, 5.5, 5.0, 6.0,
273 ];
274 let y = [false, false, false, false, true, true, true, true];
275 let svm = fit(&x, &y, 8, 2, 1.0, Kernel::Linear, 20, 1e-3).unwrap();
276 assert!(!svm.predict_row(&[0.2, 0.2]));
277 assert!(svm.predict_row(&[6.0, 6.0]));
278 assert!(svm.n_support_vectors() >= 1);
279 for i in 0..8 {
281 assert_eq!(svm.predict_row(&x[i * 2..i * 2 + 2]), y[i]);
282 }
283 }
284
285 #[test]
286 fn rbf_svm_handles_nonlinear_boundary() {
287 let mut x = Vec::new();
290 let mut y = Vec::new();
291 for &(a, b) in &[(0.0, 0.0), (0.3, 0.0), (0.0, 0.3), (-0.3, 0.0), (0.0, -0.3)] {
293 x.push(a);
294 x.push(b);
295 y.push(false);
296 }
297 for &(a, b) in &[
299 (3.0, 0.0),
300 (-3.0, 0.0),
301 (0.0, 3.0),
302 (0.0, -3.0),
303 (2.1, 2.1),
304 (-2.1, -2.1),
305 ] {
306 x.push(a);
307 x.push(b);
308 y.push(true);
309 }
310 let n = 11;
311 let svm = fit(&x, &y, n, 2, 1.0, Kernel::Rbf { gamma: 0.5 }, 50, 1e-3).unwrap();
312 assert!(
313 !svm.predict_row(&[0.1, 0.1]),
314 "inner point should be class 0"
315 );
316 assert!(
317 svm.predict_row(&[3.0, 0.0]),
318 "outer point should be class 1"
319 );
320 assert!(svm.predict_row(&[0.0, -3.0]));
321 }
322
323 #[test]
324 fn guards() {
325 assert_eq!(
326 fit(
327 &[1.0, 2.0],
328 &[true, true][..1],
329 1,
330 2,
331 1.0,
332 Kernel::Linear,
333 5,
334 1e-3
335 )
336 .unwrap_err(),
337 LearningError::InsufficientData
338 );
339 let x = [0.0, 0.0, 1.0, 1.0];
340 assert_eq!(
341 fit(&x, &[true, true], 2, 2, 1.0, Kernel::Linear, 5, 1e-3).unwrap_err(),
342 LearningError::InsufficientData
343 );
344 }
345}