use std::sync::Arc;
use crate::error::{KernelError, Result};
use crate::types::Kernel;
#[derive(Debug, Clone)]
pub struct SmoConfig {
pub c: f64,
pub tol: f64,
pub epsilon: f64,
pub max_iter: usize,
}
impl Default for SmoConfig {
fn default() -> Self {
SmoConfig {
c: 1.0,
tol: 1e-3,
epsilon: 0.1,
max_iter: 10_000,
}
}
}
struct SmoState {
alpha: Vec<f64>,
b: f64,
error_cache: Vec<f64>,
n: usize,
c: f64,
tol: f64,
y: Vec<f64>,
kernel_matrix: Vec<Vec<f64>>,
}
impl SmoState {
fn decision_function(&self, i: usize) -> f64 {
let mut sum = 0.0;
for j in 0..self.n {
let aj = self.alpha[j];
if aj.abs() > 1e-12 {
sum += aj * self.y[j] * self.kernel_matrix[j][i];
}
}
sum - self.b
}
fn refresh_error(&mut self, i: usize) {
self.error_cache[i] = self.decision_function(i) - self.y[i];
}
fn is_non_bound(&self, i: usize) -> bool {
self.alpha[i] > 0.0 && self.alpha[i] < self.c
}
fn clip(val: f64, lo: f64, hi: f64) -> f64 {
val.max(lo).min(hi)
}
fn take_step(&mut self, i1: usize, i2: usize) -> Result<bool> {
if i1 == i2 {
return Ok(false);
}
let a1 = self.alpha[i1];
let a2 = self.alpha[i2];
let y1 = self.y[i1];
let y2 = self.y[i2];
let e1 = self.error_cache[i1];
let e2 = self.error_cache[i2];
let s = y1 * y2;
let (lo, hi) = if (s - 1.0).abs() > 1e-10 {
(f64::max(0.0, a2 - a1), f64::min(self.c, self.c + a2 - a1))
} else {
(f64::max(0.0, a1 + a2 - self.c), f64::min(self.c, a1 + a2))
};
if lo >= hi {
return Ok(false);
}
let k11 = self.kernel_matrix[i1][i1];
let k12 = self.kernel_matrix[i1][i2];
let k22 = self.kernel_matrix[i2][i2];
let eta = k11 + k22 - 2.0 * k12;
let a2_new = if eta > 1e-12 {
let a2_unc = a2 + y2 * (e1 - e2) / eta;
Self::clip(a2_unc, lo, hi)
} else {
let gamma = a1 + s * a2;
let a1_at_lo = gamma - s * lo;
let a1_at_hi = gamma - s * hi;
let f1 = e1 + y1;
let f2 = e2 + y2;
let a1_lo = a1_at_lo;
let a2_lo = lo;
let lobj = -0.5 * k11 * a1_lo * a1_lo
- 0.5 * k22 * a2_lo * a2_lo
- s * k12 * a1_lo * a2_lo
- y1 * a1_lo * f1
- y2 * a2_lo * f2
+ a1_lo
+ a2_lo;
let a1_hi = a1_at_hi;
let a2_hi = hi;
let hobj = -0.5 * k11 * a1_hi * a1_hi
- 0.5 * k22 * a2_hi * a2_hi
- s * k12 * a1_hi * a2_hi
- y1 * a1_hi * f1
- y2 * a2_hi * f2
+ a1_hi
+ a2_hi;
if lobj > hobj + 1e-12 {
lo
} else if hobj > lobj + 1e-12 {
hi
} else {
a2
}
};
if (a2_new - a2).abs() < 1e-5 * (a2_new + a2 + 1e-10) {
return Ok(false);
}
let a1_new = a1 + s * (a2 - a2_new);
let b_old = self.b;
let b1 = b_old + e1 + y1 * (a1_new - a1) * k11 + y2 * (a2_new - a2) * k12;
let b2 = b_old + e2 + y1 * (a1_new - a1) * k12 + y2 * (a2_new - a2) * k22;
let b_new = if a1_new > 1e-8 * self.c && a1_new < self.c * (1.0 - 1e-8) {
b1
} else if a2_new > 1e-8 * self.c && a2_new < self.c * (1.0 - 1e-8) {
b2
} else {
(b1 + b2) * 0.5
};
self.alpha[i1] = a1_new;
self.alpha[i2] = a2_new;
self.b = b_new;
let delta_alpha1 = a1_new - a1;
let delta_alpha2 = a2_new - a2;
let delta_b = b_new - b_old;
for j in 0..self.n {
if self.is_non_bound(j) {
self.error_cache[j] += y1 * delta_alpha1 * self.kernel_matrix[j][i1]
+ y2 * delta_alpha2 * self.kernel_matrix[j][i2]
- delta_b;
}
}
self.refresh_error(i1);
self.refresh_error(i2);
Ok(true)
}
fn examine_example(&mut self, i2: usize, random_offset: usize) -> Result<bool> {
if !self.is_non_bound(i2) {
self.refresh_error(i2);
}
let e2 = self.error_cache[i2];
let r2 = e2 * self.y[i2];
let kkt_violated =
(r2 < -self.tol && self.alpha[i2] < self.c) || (r2 > self.tol && self.alpha[i2] > 0.0);
if !kkt_violated {
return Ok(false);
}
let non_bound: Vec<usize> = (0..self.n).filter(|&j| self.is_non_bound(j)).collect();
if non_bound.len() > 1 {
let mut best_i1 = None;
let mut best_diff = 0.0;
for &j in &non_bound {
if j == i2 {
continue;
}
let diff = (self.error_cache[j] - e2).abs();
if diff > best_diff {
best_diff = diff;
best_i1 = Some(j);
}
}
if let Some(i1) = best_i1 {
if self.take_step(i1, i2)? {
return Ok(true);
}
}
}
if !non_bound.is_empty() {
let start = random_offset % non_bound.len();
for k in 0..non_bound.len() {
let i1 = non_bound[(start + k) % non_bound.len()];
if i1 == i2 {
continue;
}
if self.take_step(i1, i2)? {
return Ok(true);
}
}
}
let start = random_offset % self.n;
for k in 0..self.n {
let i1 = (start + k) % self.n;
if i1 == i2 {
continue;
}
if self.take_step(i1, i2)? {
return Ok(true);
}
}
Ok(false)
}
}
pub fn smo_svc(
x: &[Vec<f64>],
y: &[f64],
kernel: &Arc<dyn Kernel>,
config: &SmoConfig,
) -> Result<(Vec<f64>, f64)> {
let n = x.len();
if n == 0 {
return Err(KernelError::DimensionMismatch {
expected: vec![1],
got: vec![0],
context: "smo_svc: training set cannot be empty".to_string(),
});
}
if y.len() != n {
return Err(KernelError::DimensionMismatch {
expected: vec![n],
got: vec![y.len()],
context: "smo_svc: y must have the same length as x".to_string(),
});
}
if config.c <= 0.0 {
return Err(KernelError::InvalidParameter {
parameter: "C".to_string(),
value: config.c.to_string(),
reason: "C must be strictly positive".to_string(),
});
}
for (i, &yi) in y.iter().enumerate() {
if (yi - 1.0).abs() > 1e-9 && (yi + 1.0).abs() > 1e-9 {
return Err(KernelError::InvalidParameter {
parameter: "y".to_string(),
value: yi.to_string(),
reason: format!("label at index {} must be +1 or -1", i),
});
}
}
if n > 1 {
let d0 = x[0].len();
for (i, xi) in x.iter().enumerate().skip(1) {
if xi.len() != d0 {
return Err(KernelError::DimensionMismatch {
expected: vec![d0],
got: vec![xi.len()],
context: format!("smo_svc: x[{}] has wrong dimension", i),
});
}
}
}
let mut kernel_matrix = vec![vec![0.0_f64; n]; n];
for i in 0..n {
for j in i..n {
let k_val = kernel.compute(&x[i], &x[j])?;
kernel_matrix[i][j] = k_val;
kernel_matrix[j][i] = k_val;
}
}
let alpha = vec![0.0_f64; n];
let b = 0.0_f64;
let error_cache: Vec<f64> = y.iter().map(|&yi| -yi).collect();
let mut state = SmoState {
alpha,
b,
error_cache,
n,
c: config.c,
tol: config.tol,
y: y.to_vec(),
kernel_matrix,
};
let mut examine_all = true;
let mut total_passes = 0usize;
let mut random_offset: usize = 17;
loop {
let mut num_changed = 0usize;
let was_examine_all = examine_all;
if examine_all {
for i2 in 0..n {
if state.examine_example(i2, random_offset)? {
num_changed += 1;
}
random_offset = random_offset
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407)
& 0xFFFF;
}
} else {
let non_bound_indices: Vec<usize> = (0..n).filter(|&j| state.is_non_bound(j)).collect();
for i2 in non_bound_indices {
if state.examine_example(i2, random_offset)? {
num_changed += 1;
}
random_offset = random_offset
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407)
& 0xFFFF;
}
}
total_passes += 1;
if total_passes > config.max_iter {
return Err(KernelError::ComputationError(format!(
"SMO did not converge after {} passes (tol={}, C={}). \
Consider increasing max_iter, relaxing tol, or adjusting C.",
config.max_iter, config.tol, config.c
)));
}
if was_examine_all && num_changed == 0 {
break;
}
if was_examine_all {
examine_all = false;
} else if num_changed == 0 {
examine_all = true;
}
}
Ok((state.alpha, state.b))
}
#[cfg(test)]
mod unit_tests {
use super::*;
struct TestLinearKernel;
impl crate::types::Kernel for TestLinearKernel {
fn compute(&self, x: &[f64], y: &[f64]) -> crate::error::Result<f64> {
Ok(x.iter().zip(y.iter()).map(|(a, b)| a * b).sum())
}
fn name(&self) -> &str {
"TestLinear"
}
}
#[test]
fn test_smo_config_default() {
let cfg = SmoConfig::default();
assert_eq!(cfg.c, 1.0);
assert_eq!(cfg.tol, 1e-3);
assert_eq!(cfg.epsilon, 0.1);
assert_eq!(cfg.max_iter, 10_000);
}
#[test]
fn test_smo_empty_data() {
let kernel: Arc<dyn crate::types::Kernel> = Arc::new(TestLinearKernel);
let cfg = SmoConfig::default();
let result = smo_svc(&[], &[], &kernel, &cfg);
assert!(result.is_err());
}
#[test]
fn test_smo_invalid_c() {
let kernel: Arc<dyn crate::types::Kernel> = Arc::new(TestLinearKernel);
let cfg = SmoConfig {
c: -1.0,
..Default::default()
};
let x = vec![vec![1.0, 2.0]];
let y = vec![1.0];
let result = smo_svc(&x, &y, &kernel, &cfg);
assert!(matches!(result, Err(KernelError::InvalidParameter { .. })));
}
}