use crate::error::Result;
use crate::primitives::{Matrix, Vector};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Kernel {
Rbf {
gamma: f32,
},
Poly {
gamma: f32,
coef0: f32,
degree: u32,
},
}
impl Kernel {
#[must_use]
pub fn eval(self, a: &[f32], b: &[f32]) -> f32 {
match self {
Kernel::Rbf { gamma } => {
let mut sq = 0.0_f32;
for (&ai, &bi) in a.iter().zip(b.iter()) {
let d = ai - bi;
sq += d * d;
}
(-gamma * sq).exp()
}
Kernel::Poly {
gamma,
coef0,
degree,
} => {
let mut dot = 0.0_f32;
for (&ai, &bi) in a.iter().zip(b.iter()) {
dot += ai * bi;
}
(gamma * dot + coef0).powi(degree as i32)
}
}
}
}
#[derive(Debug, Clone)]
pub struct SVCRbf {
kernel: Kernel,
c: f32,
tol: f32,
max_iter: usize,
support_vectors: Option<Matrix<f32>>,
dual_coef: Option<Vec<f32>>,
intercept: f32,
classes: Option<[usize; 2]>,
}
impl SVCRbf {
#[must_use]
pub fn new() -> Self {
Self {
kernel: Kernel::Rbf { gamma: 0.5 },
c: 1.0,
tol: 1e-3,
max_iter: 1000,
support_vectors: None,
dual_coef: None,
intercept: 0.0,
classes: None,
}
}
#[must_use]
pub fn with_gamma(mut self, gamma: f32) -> Self {
self.kernel = Kernel::Rbf { gamma };
self
}
#[must_use]
pub fn with_kernel(mut self, kernel: Kernel) -> Self {
self.kernel = kernel;
self
}
#[must_use]
pub fn with_poly(mut self, gamma: f32, coef0: f32, degree: u32) -> Self {
self.kernel = Kernel::Poly {
gamma,
coef0,
degree,
};
self
}
#[must_use]
pub fn kernel(&self) -> Kernel {
self.kernel
}
#[must_use]
pub fn with_c(mut self, c: f32) -> Self {
self.c = c;
self
}
#[must_use]
pub fn with_tolerance(mut self, tol: f32) -> Self {
self.tol = tol;
self
}
#[must_use]
pub fn with_max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
pub fn fit(&mut self, x: &Matrix<f32>, y: &[usize]) -> Result<()> {
let (n_samples, n_features) = x.shape();
if n_samples == 0 {
return Err("Cannot fit with zero samples".into());
}
if n_samples != y.len() {
return Err("Number of samples in X and y must match".into());
}
let mut classes: Vec<usize> = y.to_vec();
classes.sort_unstable();
classes.dedup();
if classes.len() != 2 {
return Err("SVCRbf requires exactly 2 classes".into());
}
let neg_label = classes[0];
let pos_label = classes[1];
let signs: Vec<f32> = y
.iter()
.map(|&l| if l == pos_label { 1.0 } else { -1.0 })
.collect();
let rows: Vec<Vec<f32>> = (0..n_samples)
.map(|i| (0..n_features).map(|j| x.get(i, j)).collect())
.collect();
let mut kernel = vec![0.0_f32; n_samples * n_samples];
for i in 0..n_samples {
for j in i..n_samples {
let k = self.kernel.eval(&rows[i], &rows[j]);
kernel[i * n_samples + j] = k;
kernel[j * n_samples + i] = k;
}
}
let mut alpha = vec![0.0_f32; n_samples];
let mut bias = 0.0_f32;
let decision = |alpha: &[f32], bias: f32, i: usize| -> f32 {
let mut s = bias;
for j in 0..n_samples {
if alpha[j] != 0.0 {
s += alpha[j] * signs[j] * kernel[j * n_samples + i];
}
}
s
};
let c = self.c;
let tol = self.tol;
let mut passes = 0;
let max_passes = self.max_iter;
let mut iter_count = 0usize;
let hard_cap = self.max_iter.saturating_mul(n_samples).max(n_samples);
while passes < max_passes && iter_count < hard_cap {
let mut num_changed = 0;
for i in 0..n_samples {
iter_count += 1;
let e_i = decision(&alpha, bias, i) - signs[i];
let yi = signs[i];
let r_i = e_i * yi;
if (r_i < -tol && alpha[i] < c) || (r_i > tol && alpha[i] > 0.0) {
let mut best_j = usize::MAX;
let mut best_delta = 0.0_f32;
for j in 0..n_samples {
if j == i {
continue;
}
let e_j = decision(&alpha, bias, j) - signs[j];
let delta = (e_i - e_j).abs();
if delta > best_delta {
best_delta = delta;
best_j = j;
}
}
if best_j == usize::MAX {
continue;
}
let j = best_j;
let e_j = decision(&alpha, bias, j) - signs[j];
let yj = signs[j];
let alpha_i_old = alpha[i];
let alpha_j_old = alpha[j];
let (low, high) = if (yi - yj).abs() > 0.5 {
(
(alpha_j_old - alpha_i_old).max(0.0),
c + (alpha_j_old - alpha_i_old).min(0.0),
)
} else {
(
(alpha_i_old + alpha_j_old - c).max(0.0),
(alpha_i_old + alpha_j_old).min(c),
)
};
if (high - low).abs() < 1e-12 {
continue;
}
let k_ii = kernel[i * n_samples + i];
let k_jj = kernel[j * n_samples + j];
let k_ij = kernel[i * n_samples + j];
let eta = 2.0 * k_ij - k_ii - k_jj;
if eta >= 0.0 {
continue;
}
let mut alpha_j_new = alpha_j_old - yj * (e_i - e_j) / eta;
alpha_j_new = alpha_j_new.clamp(low, high);
if (alpha_j_new - alpha_j_old).abs() < 1e-6 {
continue;
}
let alpha_i_new = alpha_i_old + yi * yj * (alpha_j_old - alpha_j_new);
let b1 = bias
- e_i
- yi * (alpha_i_new - alpha_i_old) * k_ii
- yj * (alpha_j_new - alpha_j_old) * k_ij;
let b2 = bias
- e_j
- yi * (alpha_i_new - alpha_i_old) * k_ij
- yj * (alpha_j_new - alpha_j_old) * k_jj;
alpha[i] = alpha_i_new;
alpha[j] = alpha_j_new;
bias = if alpha_i_new > 0.0 && alpha_i_new < c {
b1
} else if alpha_j_new > 0.0 && alpha_j_new < c {
b2
} else {
0.5 * (b1 + b2)
};
num_changed += 1;
}
}
if num_changed == 0 {
passes += 1;
} else {
passes = 0;
}
}
{
let mut b_sum = 0.0_f32;
let mut b_cnt = 0usize;
for i in 0..n_samples {
if alpha[i] > 1e-8 && alpha[i] < c - 1e-8 {
b_sum += signs[i] - decision(&alpha, 0.0, i);
b_cnt += 1;
}
}
if b_cnt > 0 {
bias = b_sum / b_cnt as f32;
}
}
let mut sv_rows: Vec<f32> = Vec::new();
let mut dual_coef: Vec<f32> = Vec::new();
let mut n_sv = 0;
for i in 0..n_samples {
if alpha[i] > 1e-8 {
for j in 0..n_features {
sv_rows.push(rows[i][j]);
}
dual_coef.push(alpha[i] * signs[i]);
n_sv += 1;
}
}
if n_sv == 0 {
self.support_vectors = Some(Matrix::from_vec(1, n_features, rows[0].clone())?);
self.dual_coef = Some(vec![0.0]);
} else {
self.support_vectors = Some(Matrix::from_vec(n_sv, n_features, sv_rows)?);
self.dual_coef = Some(dual_coef);
}
self.intercept = bias;
self.classes = Some([neg_label, pos_label]);
Ok(())
}
fn decision_value(&self, sv: &Matrix<f32>, coef: &[f32], row: &[f32]) -> f32 {
let (n_sv, n_features) = sv.shape();
let mut s = self.intercept;
let mut sv_row = vec![0.0_f32; n_features];
for i in 0..n_sv {
for (j, slot) in sv_row.iter_mut().enumerate() {
*slot = sv.get(i, j);
}
s += coef[i] * self.kernel.eval(&sv_row, row);
}
s
}
pub fn decision_function(&self, x: &Matrix<f32>) -> Result<Vector<f32>> {
let sv = self.support_vectors.as_ref().ok_or("Model not fitted")?;
let coef = self.dual_coef.as_ref().ok_or("Model not fitted")?;
let (n_samples, n_features) = x.shape();
if n_features != sv.shape().1 {
return Err("Feature dimension mismatch".into());
}
let mut out = Vec::with_capacity(n_samples);
for i in 0..n_samples {
let row: Vec<f32> = (0..n_features).map(|j| x.get(i, j)).collect();
out.push(self.decision_value(sv, coef, &row));
}
Ok(Vector::from_vec(out))
}
pub fn predict(&self, x: &Matrix<f32>) -> Result<Vec<usize>> {
let classes = self.classes.as_ref().ok_or("Model not fitted")?;
let scores = self.decision_function(x)?;
Ok(scores
.as_slice()
.iter()
.map(|&s| if s > 0.0 { classes[1] } else { classes[0] })
.collect())
}
pub fn score(&self, x: &Matrix<f32>, y: &[usize]) -> Result<f32> {
let preds = self.predict(x)?;
if y.is_empty() {
return Ok(0.0);
}
let correct = preds.iter().zip(y.iter()).filter(|(p, t)| p == t).count();
Ok(correct as f32 / y.len() as f32)
}
#[must_use]
pub fn n_support_vectors(&self) -> usize {
self.support_vectors.as_ref().map_or(0, |sv| sv.shape().0)
}
}
impl Default for SVCRbf {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct MultiClassSVC {
kernel: Kernel,
c: f32,
tol: f32,
max_iter: usize,
classes: Vec<usize>,
models: Vec<(usize, usize, SVCRbf)>,
}
impl MultiClassSVC {
#[must_use]
pub fn new() -> Self {
Self {
kernel: Kernel::Rbf { gamma: 0.5 },
c: 1.0,
tol: 1e-3,
max_iter: 1000,
classes: Vec::new(),
models: Vec::new(),
}
}
#[must_use]
pub fn with_gamma(mut self, gamma: f32) -> Self {
self.kernel = Kernel::Rbf { gamma };
self
}
#[must_use]
pub fn with_kernel(mut self, kernel: Kernel) -> Self {
self.kernel = kernel;
self
}
#[must_use]
pub fn with_poly(mut self, gamma: f32, coef0: f32, degree: u32) -> Self {
self.kernel = Kernel::Poly {
gamma,
coef0,
degree,
};
self
}
#[must_use]
pub fn with_c(mut self, c: f32) -> Self {
self.c = c;
self
}
#[must_use]
pub fn with_max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
#[must_use]
pub fn classes(&self) -> &[usize] {
&self.classes
}
pub fn fit(&mut self, x: &Matrix<f32>, y: &[usize]) -> Result<()> {
let (n_samples, n_features) = x.shape();
if n_samples == 0 {
return Err("Cannot fit with zero samples".into());
}
if n_samples != y.len() {
return Err("Number of samples in X and y must match".into());
}
let mut classes: Vec<usize> = y.to_vec();
classes.sort_unstable();
classes.dedup();
if classes.len() < 2 {
return Err("MultiClassSVC requires at least 2 classes".into());
}
let mut models = Vec::with_capacity(classes.len() * (classes.len() - 1) / 2);
for a_pos in 0..classes.len() {
for b_pos in (a_pos + 1)..classes.len() {
let (a, b) = (classes[a_pos], classes[b_pos]);
let (mut sub_x, mut sub_y) = (Vec::new(), Vec::new());
for i in 0..n_samples {
if y[i] == a || y[i] == b {
for j in 0..n_features {
sub_x.push(x.get(i, j));
}
sub_y.push(y[i]);
}
}
let n_sub = sub_y.len();
let sub_x = Matrix::from_vec(n_sub, n_features, sub_x)?;
let mut model = SVCRbf::new()
.with_kernel(self.kernel)
.with_c(self.c)
.with_tolerance(self.tol)
.with_max_iter(self.max_iter);
model.fit(&sub_x, &sub_y)?;
models.push((a, b, model));
}
}
self.classes = classes;
self.models = models;
Ok(())
}
pub fn predict(&self, x: &Matrix<f32>) -> Result<Vec<usize>> {
if self.models.is_empty() {
return Err("Model not fitted".into());
}
let (n_samples, _) = x.shape();
let n_classes = self.classes.len();
let mut pair_preds: Vec<(usize, usize, Vec<usize>, Vector<f32>)> =
Vec::with_capacity(self.models.len());
for (a, b, model) in &self.models {
let preds = model.predict(x)?;
let margins = model.decision_function(x)?;
pair_preds.push((*a, *b, preds, margins));
}
let class_index: std::collections::HashMap<usize, usize> = self
.classes
.iter()
.enumerate()
.map(|(idx, &c)| (c, idx))
.collect();
let mut out = Vec::with_capacity(n_samples);
for i in 0..n_samples {
let mut votes = vec![0u32; n_classes];
let mut confidence = vec![0.0f32; n_classes];
for (_, _, preds, margins) in &pair_preds {
let winner = preds[i];
votes[class_index[&winner]] += 1;
confidence[class_index[&winner]] += margins.as_slice()[i].abs();
}
let mut best = 0usize;
for k in 1..n_classes {
let better = votes[k] > votes[best]
|| (votes[k] == votes[best] && confidence[k] > confidence[best]);
if better {
best = k;
}
}
out.push(self.classes[best]);
}
Ok(out)
}
pub fn score(&self, x: &Matrix<f32>, y: &[usize]) -> Result<f32> {
let preds = self.predict(x)?;
if y.is_empty() {
return Ok(0.0);
}
let correct = preds.iter().zip(y.iter()).filter(|(p, t)| p == t).count();
Ok(correct as f32 / y.len() as f32)
}
}
impl Default for MultiClassSVC {
fn default() -> Self {
Self::new()
}
}
impl crate::traits::Estimator for SVCRbf {
fn fit(&mut self, x: &Matrix<f32>, y: &Vector<f32>) -> Result<()> {
let labels: Vec<usize> = y.as_slice().iter().map(|&v| v.round() as usize).collect();
SVCRbf::fit(self, x, &labels)
}
fn predict(&self, x: &Matrix<f32>) -> Vector<f32> {
let fallback = self.classes.map_or(0, |c| c[0]);
let labels: Vec<usize> =
SVCRbf::predict(self, x).unwrap_or_else(|_| vec![fallback; x.shape().0]);
Vector::from_vec(labels.into_iter().map(|l| l as f32).collect())
}
fn score(&self, x: &Matrix<f32>, y: &Vector<f32>) -> f32 {
let fallback = self.classes.map_or(0, |c| c[0]);
let preds: Vec<usize> =
SVCRbf::predict(self, x).unwrap_or_else(|_| vec![fallback; x.shape().0]);
let n = y.len();
if n == 0 {
return 0.0;
}
let correct = preds
.iter()
.zip(y.as_slice())
.filter(|(&p, &t)| p == t.round() as usize)
.count();
correct as f32 / n as f32
}
}
#[cfg(test)]
#[path = "tests_svc_rbf.rs"]
mod tests_svc_rbf;