use crate::types::{KernelType, SvmNode, SvmParameter};
#[inline]
pub fn powi(base: f64, times: i32) -> f64 {
let mut tmp = base;
let mut ret = 1.0;
let mut t = times;
while t > 0 {
if t % 2 == 1 {
ret *= tmp;
}
tmp *= tmp;
t /= 2;
}
ret
}
#[inline]
pub fn dot(x: &[SvmNode], y: &[SvmNode]) -> f64 {
let mut sum = 0.0;
let mut ix = 0;
let mut iy = 0;
while ix < x.len() && iy < y.len() {
if x[ix].index == y[iy].index {
sum += x[ix].value * y[iy].value;
ix += 1;
iy += 1;
} else if x[ix].index > y[iy].index {
iy += 1;
} else {
ix += 1;
}
}
sum
}
#[inline]
fn sparse_sq_dist(x: &[SvmNode], y: &[SvmNode]) -> f64 {
let mut sum = 0.0;
let mut ix = 0;
let mut iy = 0;
while ix < x.len() && iy < y.len() {
if x[ix].index == y[iy].index {
let d = x[ix].value - y[iy].value;
sum += d * d;
ix += 1;
iy += 1;
} else if x[ix].index > y[iy].index {
sum += y[iy].value * y[iy].value;
iy += 1;
} else {
sum += x[ix].value * x[ix].value;
ix += 1;
}
}
while ix < x.len() {
sum += x[ix].value * x[ix].value;
ix += 1;
}
while iy < y.len() {
sum += y[iy].value * y[iy].value;
iy += 1;
}
sum
}
pub fn k_function(x: &[SvmNode], y: &[SvmNode], param: &SvmParameter) -> f64 {
match param.kernel_type {
KernelType::Linear => dot(x, y),
KernelType::Polynomial => {
powi(param.gamma * dot(x, y) + param.coef0, param.degree)
}
KernelType::Rbf => {
(-param.gamma * sparse_sq_dist(x, y)).exp()
}
KernelType::Sigmoid => {
(param.gamma * dot(x, y) + param.coef0).tanh()
}
KernelType::Precomputed => {
let col = y[0].value as usize;
x.get(col).map_or(0.0, |n| n.value)
}
}
}
pub struct Kernel<'a> {
x: Vec<&'a [SvmNode]>,
x_square: Option<Vec<f64>>,
kernel_type: KernelType,
degree: i32,
gamma: f64,
coef0: f64,
}
impl<'a> Kernel<'a> {
pub fn new(x: &'a [Vec<SvmNode>], param: &SvmParameter) -> Self {
let x_refs: Vec<&'a [SvmNode]> = x.iter().map(|xi| xi.as_slice()).collect();
let x_square = if param.kernel_type == KernelType::Rbf {
Some(x_refs.iter().map(|xi| dot(xi, xi)).collect())
} else {
None
};
Self {
x: x_refs,
x_square,
kernel_type: param.kernel_type,
degree: param.degree,
gamma: param.gamma,
coef0: param.coef0,
}
}
#[inline]
pub fn evaluate(&self, i: usize, j: usize) -> f64 {
match self.kernel_type {
KernelType::Linear => dot(self.x[i], self.x[j]),
KernelType::Polynomial => {
powi(self.gamma * dot(self.x[i], self.x[j]) + self.coef0, self.degree)
}
KernelType::Rbf => {
let sq = self.x_square.as_ref().unwrap();
let val = sq[i] + sq[j] - 2.0 * dot(self.x[i], self.x[j]);
(-self.gamma * val).exp()
}
KernelType::Sigmoid => {
(self.gamma * dot(self.x[i], self.x[j]) + self.coef0).tanh()
}
KernelType::Precomputed => {
let col = self.x[j][0].value as usize;
self.x[i].get(col).map_or(0.0, |n| n.value)
}
}
}
pub fn swap_index(&mut self, i: usize, j: usize) {
self.x.swap(i, j);
if let Some(ref mut sq) = self.x_square {
sq.swap(i, j);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::SvmParameter;
fn make_nodes(pairs: &[(i32, f64)]) -> Vec<SvmNode> {
pairs
.iter()
.map(|&(index, value)| SvmNode { index, value })
.collect()
}
#[test]
fn powi_basic() {
assert_eq!(powi(2.0, 10), 1024.0);
assert_eq!(powi(3.0, 0), 1.0);
assert_eq!(powi(5.0, 1), 5.0);
assert!((powi(2.0, 3) - 8.0).abs() < 1e-15);
assert_eq!(powi(2.0, -1), 1.0);
}
#[test]
fn dot_product() {
let x = make_nodes(&[(1, 1.0), (3, 2.0), (5, 3.0)]);
let y = make_nodes(&[(1, 4.0), (2, 5.0), (5, 6.0)]);
assert!((dot(&x, &y) - 22.0).abs() < 1e-15);
}
#[test]
fn dot_disjoint() {
let x = make_nodes(&[(1, 1.0), (3, 2.0)]);
let y = make_nodes(&[(2, 5.0), (4, 6.0)]);
assert_eq!(dot(&x, &y), 0.0);
}
#[test]
fn dot_empty() {
let x = make_nodes(&[]);
let y = make_nodes(&[(1, 1.0)]);
assert_eq!(dot(&x, &y), 0.0);
}
#[test]
fn kernel_linear() {
let x = make_nodes(&[(1, 1.0), (2, 2.0)]);
let y = make_nodes(&[(1, 3.0), (2, 4.0)]);
let param = SvmParameter {
kernel_type: KernelType::Linear,
..Default::default()
};
assert!((k_function(&x, &y, ¶m) - 11.0).abs() < 1e-15);
}
#[test]
fn kernel_rbf() {
let x = make_nodes(&[(1, 1.0), (2, 0.0)]);
let y = make_nodes(&[(1, 0.0), (2, 1.0)]);
let param = SvmParameter {
kernel_type: KernelType::Rbf,
gamma: 0.5,
..Default::default()
};
let expected = (-1.0_f64).exp();
assert!((k_function(&x, &y, ¶m) - expected).abs() < 1e-15);
}
#[test]
fn kernel_poly() {
let x = make_nodes(&[(1, 1.0), (2, 2.0)]);
let y = make_nodes(&[(1, 3.0), (2, 4.0)]);
let param = SvmParameter {
kernel_type: KernelType::Polynomial,
gamma: 1.0,
coef0: 1.0,
degree: 2,
..Default::default()
};
assert!((k_function(&x, &y, ¶m) - 144.0).abs() < 1e-15);
}
#[test]
fn kernel_sigmoid() {
let x = make_nodes(&[(1, 1.0)]);
let y = make_nodes(&[(1, 1.0)]);
let param = SvmParameter {
kernel_type: KernelType::Sigmoid,
gamma: 1.0,
coef0: 0.0,
..Default::default()
};
let expected = 1.0_f64.tanh();
assert!((k_function(&x, &y, ¶m) - expected).abs() < 1e-15);
}
#[test]
fn kernel_struct_matches_standalone() {
let data = vec![
make_nodes(&[(1, 0.5), (3, -1.0)]),
make_nodes(&[(1, -0.25), (2, 0.75)]),
make_nodes(&[(2, 1.0), (3, 0.5)]),
];
let param = SvmParameter {
kernel_type: KernelType::Rbf,
gamma: 0.5,
..Default::default()
};
let kern = Kernel::new(&data, ¶m);
for i in 0..data.len() {
for j in 0..data.len() {
let via_struct = kern.evaluate(i, j);
let via_func = k_function(&data[i], &data[j], ¶m);
assert!(
(via_struct - via_func).abs() < 1e-15,
"mismatch at ({},{}): {} vs {}",
i, j, via_struct, via_func
);
}
}
}
#[test]
fn rbf_self_kernel_is_one() {
let x = make_nodes(&[(1, 3.0), (5, -2.0), (10, 0.7)]);
let param = SvmParameter {
kernel_type: KernelType::Rbf,
gamma: 1.0,
..Default::default()
};
assert!((k_function(&x, &x, ¶m) - 1.0).abs() < 1e-15);
}
}