1use crate::Rng;
17
18pub const SPAM: i8 = 1;
20pub const HAM: i8 = -1;
21
22#[derive(Debug, Clone, Copy)]
24pub struct Problem<'a> {
25 pub x: &'a [f32],
27 pub dim: usize,
28 pub y: &'a [i8],
30}
31
32impl Problem<'_> {
33 pub fn rows(&self) -> usize {
34 self.y.len()
35 }
36
37 pub fn row(&self, i: usize) -> &[f32] {
38 &self.x[i * self.dim..(i + 1) * self.dim]
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq)]
44pub struct Params {
45 pub c: f64,
47 pub ham_weight: f64,
49 pub spam_weight: f64,
51 pub bias: f64,
53 pub eps: f64,
55 pub max_passes: usize,
57 pub seed: u64,
59}
60
61impl Default for Params {
62 fn default() -> Self {
63 Params { c: 1.0, ham_weight: 5.0, spam_weight: 1.0, bias: 1.0, eps: 0.01, max_passes: 1000, seed: 1 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq)]
69pub struct Model {
70 pub w: Vec<f64>,
71 pub b: f64,
73 pub passes: usize,
75 pub converged: bool,
77}
78
79impl Model {
80 pub fn decision(&self, row: &[f32]) -> f64 {
81 dot(&self.w, row) + self.b
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub struct Cancelled;
88
89pub fn train(problem: &Problem, params: &Params, cancelled: &dyn Fn() -> bool) -> Result<Model, Cancelled> {
91 let n = problem.rows();
92 let dim = problem.dim;
93 let mut w = vec![0.0f64; dim];
94 let mut w_bias = 0.0f64;
95 let mut alpha = vec![0.0f64; n];
96 let diag = |i: usize| {
98 let weight = if problem.y[i] == SPAM { params.spam_weight } else { params.ham_weight };
99 0.5 / (params.c * weight)
100 };
101 let qd: Vec<f64> = (0..n)
102 .map(|i| {
103 let row = problem.row(i);
104 row.iter().map(|&v| f64::from(v) * f64::from(v)).sum::<f64>() + params.bias * params.bias + diag(i)
105 })
106 .collect();
107 let mut index: Vec<usize> = (0..n).collect();
108 let mut active = n;
109 let mut rng = Rng::new(params.seed);
110 let mut pg_max_old = f64::INFINITY;
112 let mut passes = 0;
113 let mut converged = false;
114 while passes < params.max_passes {
115 if cancelled() {
116 return Err(Cancelled);
117 }
118 let (mut pg_max, mut pg_min) = (f64::NEG_INFINITY, f64::INFINITY);
119 for i in 0..active {
120 let j = i + rng.below(active - i);
121 index.swap(i, j);
122 }
123 let mut s = 0;
124 while s < active {
125 let i = index[s];
126 let yi = f64::from(problem.y[i]);
127 let row = problem.row(i);
128 let g = yi * (dot(&w, row) + w_bias * params.bias) - 1.0 + alpha[i] * diag(i);
129 let mut pg = 0.0;
130 if alpha[i] == 0.0 {
131 if g > pg_max_old {
132 active -= 1;
134 index.swap(s, active);
135 continue;
136 } else if g < 0.0 {
137 pg = g;
138 }
139 } else {
140 pg = g;
141 }
142 pg_max = pg_max.max(pg);
143 pg_min = pg_min.min(pg);
144 if pg.abs() > 1e-12 {
145 let old = alpha[i];
146 alpha[i] = (old - g / qd[i]).max(0.0);
147 let step = (alpha[i] - old) * yi;
148 for (wk, &xk) in w.iter_mut().zip(row) {
149 *wk += step * f64::from(xk);
150 }
151 w_bias += step * params.bias;
152 }
153 s += 1;
154 }
155 passes += 1;
156 if pg_max - pg_min <= params.eps {
157 if active == n {
158 converged = true;
159 break;
160 }
161 active = n;
163 pg_max_old = f64::INFINITY;
164 continue;
165 }
166 pg_max_old = if pg_max <= 0.0 { f64::INFINITY } else { pg_max };
167 }
168 Ok(Model { w, b: w_bias * params.bias, passes, converged })
169}
170
171fn dot(w: &[f64], row: &[f32]) -> f64 {
172 w.iter().zip(row).map(|(&a, &b)| a * f64::from(b)).sum()
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 fn exact() -> Params {
180 Params { eps: 1e-10, max_passes: 100_000, ..Params::default() }
181 }
182
183 #[test]
185 fn two_points_without_bias() {
186 let x = [1.0f32, -1.0];
187 let y = [SPAM, HAM];
188 let problem = Problem { x: &x, dim: 1, y: &y };
189 let even = Params { bias: 0.0, ham_weight: 1.0, ..exact() };
191 let model = train(&problem, &even, &|| false).unwrap();
192 assert!((model.w[0] - 0.8).abs() < 1e-9, "{model:?}");
193 assert_eq!(model.b, 0.0);
194 let weighted = Params { bias: 0.0, ..exact() };
196 let model = train(&problem, &weighted, &|| false).unwrap();
197 assert!((model.w[0] - 12.0 / 13.0).abs() < 1e-9, "{model:?}");
198 assert!(model.converged);
199 }
200
201 #[test]
205 fn three_points_with_bias() {
206 let even = Params { ham_weight: 1.0, ..exact() };
207 for (x, y) in [(vec![2.0f32, 0.0], vec![SPAM, HAM]), (vec![2.0f32, 0.0, 3.0], vec![SPAM, HAM, SPAM])] {
208 let problem = Problem { x: &x, dim: 1, y: &y };
209 let model = train(&problem, &even, &|| false).unwrap();
210 assert!((model.w[0] - 20.0 / 29.0).abs() < 1e-9, "{model:?}");
211 assert!((model.b + 16.0 / 29.0).abs() < 1e-9, "{model:?}");
212 assert!((model.decision(&[2.0]) - 24.0 / 29.0).abs() < 1e-9);
213 assert!(model.decision(&[3.0]) > 1.0, "beyond the margin: no weight of its own");
214 }
215 }
216
217 #[test]
219 fn separates_what_can_be() {
220 let mut rng = Rng::new(7);
221 let (mut x, mut y) = (Vec::new(), Vec::new());
222 for i in 0..400 {
223 let spam = i % 2 == 0;
224 let centre = if spam { 2.0 } else { -2.0 };
225 x.push((centre + rng.unit() - 0.5) as f32);
226 x.push((rng.unit() * 4.0 - 2.0) as f32);
227 y.push(if spam { SPAM } else { HAM });
228 }
229 let problem = Problem { x: &x, dim: 2, y: &y };
230 let model = train(&problem, &Params::default(), &|| false).unwrap();
231 for i in 0..problem.rows() {
232 assert_eq!(model.decision(problem.row(i)) > 0.0, y[i] == SPAM, "row {i}");
233 }
234 assert_eq!(train(&problem, &Params::default(), &|| false).unwrap(), model);
236 }
237
238 #[test]
239 fn stops_when_asked() {
240 let x = [1.0f32, -1.0];
241 let y = [SPAM, HAM];
242 let problem = Problem { x: &x, dim: 1, y: &y };
243 assert_eq!(train(&problem, &Params::default(), &|| true), Err(Cancelled));
244 }
245}