use std::sync::Arc;
use crate::error::{KernelError, Result};
use crate::types::Kernel;
use super::smo::{smo_svc, SmoConfig};
pub(super) struct SvcFittedBinary {
pub(super) support_vectors: Vec<Vec<f64>>,
pub(super) support_alphas: Vec<f64>,
pub(super) bias: f64,
kernel: Arc<dyn Kernel>,
pub(super) positive_label: i32,
}
impl SvcFittedBinary {
pub(super) fn decision_function(&self, x: &[f64]) -> Result<f64> {
let mut score = 0.0_f64;
for (sv, &coef) in self.support_vectors.iter().zip(self.support_alphas.iter()) {
score += coef * self.kernel.compute(sv, x)?;
}
Ok(score - self.bias)
}
#[allow(dead_code)]
pub(super) fn predict(&self, x: &[f64]) -> Result<i32> {
let df = self.decision_function(x)?;
Ok(if df >= 0.0 { 1 } else { -1 })
}
pub(super) fn num_support_vectors(&self) -> usize {
self.support_vectors.len()
}
}
pub struct SvcModel {
kernel: Arc<dyn Kernel>,
config: SmoConfig,
}
impl std::fmt::Debug for SvcModel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SvcModel")
.field("kernel", &self.kernel.name())
.field("config", &self.config)
.finish()
}
}
impl SvcModel {
pub fn new(kernel: Arc<dyn Kernel>, c: f64) -> Result<Self> {
if c <= 0.0 {
return Err(KernelError::InvalidParameter {
parameter: "C".to_string(),
value: c.to_string(),
reason: "regularization parameter C must be strictly positive".to_string(),
});
}
Ok(Self {
kernel,
config: SmoConfig {
c,
..SmoConfig::default()
},
})
}
pub fn new_with_config(kernel: Arc<dyn Kernel>, config: SmoConfig) -> Result<Self> {
if config.c <= 0.0 {
return Err(KernelError::InvalidParameter {
parameter: "C".to_string(),
value: config.c.to_string(),
reason: "regularization parameter C must be strictly positive".to_string(),
});
}
Ok(Self { kernel, config })
}
pub fn fit(&self, x: &[Vec<f64>], y: &[i32]) -> Result<SvcFitted> {
let n = x.len();
if n == 0 {
return Err(KernelError::DimensionMismatch {
expected: vec![1],
got: vec![0],
context: "SvcModel::fit: training set cannot be empty".to_string(),
});
}
if y.len() != n {
return Err(KernelError::DimensionMismatch {
expected: vec![n],
got: vec![y.len()],
context: "SvcModel::fit: y must have same length as x".to_string(),
});
}
let mut classes: Vec<i32> = y.to_vec();
classes.sort_unstable();
classes.dedup();
if classes.len() < 2 {
return Err(KernelError::InvalidParameter {
parameter: "y".to_string(),
value: format!("{:?}", classes),
reason: "at least 2 distinct class labels are required for SVC".to_string(),
});
}
if classes.len() == 2 {
let neg_class = classes[0];
let pos_class = classes[1];
let y_binary: Vec<f64> = y
.iter()
.map(|&yi| if yi == pos_class { 1.0 } else { -1.0 })
.collect();
let binary = self.fit_binary(x, &y_binary, pos_class)?;
Ok(SvcFitted {
support_vectors: binary.support_vectors.clone(),
support_alphas: binary.support_alphas.clone(),
bias: binary.bias,
kernel: Arc::clone(&self.kernel),
mode: SvcMode::Binary {
classifier: binary,
neg_class,
pos_class,
},
})
} else {
let mut classifiers = Vec::with_capacity(classes.len());
for &pos_class in &classes {
let y_ovr: Vec<f64> = y
.iter()
.map(|&yi| if yi == pos_class { 1.0 } else { -1.0 })
.collect();
let binary = self.fit_binary(x, &y_ovr, pos_class)?;
classifiers.push(binary);
}
let all_sv: Vec<Vec<f64>> = classifiers
.iter()
.flat_map(|c| c.support_vectors.iter().cloned())
.collect();
let all_alphas: Vec<f64> = classifiers
.iter()
.flat_map(|c| c.support_alphas.iter().copied())
.collect();
Ok(SvcFitted {
support_vectors: all_sv,
support_alphas: all_alphas,
bias: 0.0, kernel: Arc::clone(&self.kernel),
mode: SvcMode::MultiClass { classifiers },
})
}
}
fn fit_binary(
&self,
x: &[Vec<f64>],
y_binary: &[f64],
positive_label: i32,
) -> Result<SvcFittedBinary> {
let (alpha, b) = smo_svc(x, y_binary, &self.kernel, &self.config)?;
let sv_threshold = 1e-8 * self.config.c;
let mut support_vectors = Vec::new();
let mut support_alphas = Vec::new();
for (i, &a) in alpha.iter().enumerate() {
if a > sv_threshold {
support_vectors.push(x[i].clone());
support_alphas.push(a * y_binary[i]);
}
}
Ok(SvcFittedBinary {
support_vectors,
support_alphas,
bias: b,
kernel: Arc::clone(&self.kernel),
positive_label,
})
}
}
enum SvcMode {
Binary {
classifier: SvcFittedBinary,
neg_class: i32,
pos_class: i32,
},
MultiClass {
classifiers: Vec<SvcFittedBinary>,
},
}
pub struct SvcFitted {
pub support_vectors: Vec<Vec<f64>>,
pub support_alphas: Vec<f64>,
pub bias: f64,
#[allow(dead_code)]
kernel: Arc<dyn Kernel>,
mode: SvcMode,
}
impl std::fmt::Debug for SvcFitted {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SvcFitted")
.field("num_support_vectors", &self.support_vectors.len())
.field("bias", &self.bias)
.finish()
}
}
impl SvcFitted {
pub fn decision_function(&self, x: &[f64]) -> Result<f64> {
match &self.mode {
SvcMode::Binary { classifier, .. } => classifier.decision_function(x),
SvcMode::MultiClass { .. } => Err(KernelError::ComputationError(
"decision_function is not defined for multi-class SVC; \
use decision_scores() to get per-class OvR scores"
.to_string(),
)),
}
}
pub fn decision_scores(&self, x: &[f64]) -> Result<Vec<(i32, f64)>> {
match &self.mode {
SvcMode::Binary {
classifier,
pos_class,
neg_class,
} => {
let df = classifier.decision_function(x)?;
Ok(vec![(*neg_class, -df), (*pos_class, df)])
}
SvcMode::MultiClass { classifiers } => {
let mut scores = Vec::with_capacity(classifiers.len());
for clf in classifiers {
let df = clf.decision_function(x)?;
scores.push((clf.positive_label, df));
}
Ok(scores)
}
}
}
pub fn predict(&self, x: &[f64]) -> Result<i32> {
match &self.mode {
SvcMode::Binary {
classifier,
neg_class,
pos_class,
} => {
let df = classifier.decision_function(x)?;
Ok(if df >= 0.0 { *pos_class } else { *neg_class })
}
SvcMode::MultiClass { classifiers } => {
let mut best_label = classifiers[0].positive_label;
let mut best_score = f64::NEG_INFINITY;
for clf in classifiers {
let df = clf.decision_function(x)?;
if df > best_score {
best_score = df;
best_label = clf.positive_label;
}
}
Ok(best_label)
}
}
}
pub fn predict_batch(&self, x: &[Vec<f64>]) -> Result<Vec<i32>> {
x.iter().map(|xi| self.predict(xi)).collect()
}
pub fn num_support_vectors(&self) -> usize {
match &self.mode {
SvcMode::Binary { classifier, .. } => classifier.num_support_vectors(),
SvcMode::MultiClass { classifiers } => {
classifiers.iter().map(|c| c.num_support_vectors()).sum()
}
}
}
pub fn is_binary(&self) -> bool {
matches!(&self.mode, SvcMode::Binary { .. })
}
pub fn binary_support_vectors(&self) -> Option<&[Vec<f64>]> {
match &self.mode {
SvcMode::Binary { classifier, .. } => Some(&classifier.support_vectors),
_ => None,
}
}
}