blob: e01d2b117c9aca9353d20c65e4b0517b6b915aa3 [file] [log] [blame]
//! Support Vector Machine Module
//!
//! Contains implementation of Support Vector Machine using the
//! [Pegasos training algorithm](http://ttic.uchicago.edu/~nati/Publications/PegasosMPB.pdf).
//!
//! The SVM models currently only support binary classification.
//! The model inputs should be a matrix and the training targets are
//! in the form of a vector of `-1`s and `1`s.
//!
//! # Examples
//!
//! ```
//! use rusty_machine::learning::svm::SVM;
//! use rusty_machine::learning::SupModel;
//!
//! use rusty_machine::linalg::Matrix;
//! use rusty_machine::linalg::Vector;
//!
//! let inputs = Matrix::new(4,1,vec![1.0,3.0,5.0,7.0]);
//! let targets = Vector::new(vec![-1.,-1.,1.,1.]);
//!
//! let mut svm_mod = SVM::default();
//!
//! // Train the model
//! svm_mod.train(&inputs, &targets).unwrap();
//!
//! // Now we'll predict a new point
//! let new_point = Matrix::new(1,1,vec![10.]);
//! let output = svm_mod.predict(&new_point).unwrap();
//!
//! // Hopefully we classified our new point correctly!
//! assert!(output[0] == 1f64, "Our classifier isn't very good!");
//! ```
use std::vec::*;
use linalg::{Matrix, BaseMatrix};
use linalg::Vector;
use learning::toolkit::kernel::{Kernel, SquaredExp};
use learning::{LearningResult, SupModel};
use learning::error::{Error, ErrorKind};
use rand;
use rand::Rng;
/// Support Vector Machine
#[derive(Debug)]
pub struct SVM<K: Kernel> {
ker: K,
alpha: Option<Vector<f64>>,
train_inputs: Option<Matrix<f64>>,
train_targets: Option<Vector<f64>>,
lambda: f64,
/// Number of iterations for training.
pub optim_iters: usize,
}
/// The default Support Vector Machine.
///
/// The defaults are:
///
/// - `ker` = `SquaredExp::default()`
/// - `lambda` = `0.3`
/// - `optim_iters` = `100`
impl Default for SVM<SquaredExp> {
fn default() -> SVM<SquaredExp> {
SVM {
ker: SquaredExp::default(),
alpha: None,
train_inputs: None,
train_targets: None,
lambda: 0.3f64,
optim_iters: 100,
}
}
}
impl<K: Kernel> SVM<K> {
/// Constructs an untrained SVM with specified
/// kernel and lambda which determins the hardness
/// of the margin.
///
/// # Examples
///
/// ```
/// use rusty_machine::learning::svm::SVM;
/// use rusty_machine::learning::toolkit::kernel::SquaredExp;
///
/// let _ = SVM::new(SquaredExp::default(), 0.3);
/// ```
pub fn new(ker: K, lambda: f64) -> SVM<K> {
SVM {
ker: ker,
alpha: None,
train_inputs: None,
train_targets: None,
lambda: lambda,
optim_iters: 100,
}
}
}
impl<K: Kernel> SVM<K> {
/// Construct a kernel matrix
fn ker_mat(&self, m1: &Matrix<f64>, m2: &Matrix<f64>) -> LearningResult<Matrix<f64>> {
if m1.cols() != m2.cols() {
Err(Error::new(ErrorKind::InvalidState,
"Inputs to kernel matrices have different column counts."))
} else {
let dim1 = m1.rows();
let dim2 = m2.rows();
let mut ker_data = Vec::with_capacity(dim1 * dim2);
ker_data.extend(m1.row_iter().flat_map(|row1| {
m2.row_iter()
.map(move |row2| self.ker.kernel(row1.raw_slice(), row2.raw_slice()))
}));
Ok(Matrix::new(dim1, dim2, ker_data))
}
}
}
/// Train the model using the Pegasos algorithm and
/// predict the model output from new data.
impl<K: Kernel> SupModel<Matrix<f64>, Vector<f64>> for SVM<K> {
fn predict(&self, inputs: &Matrix<f64>) -> LearningResult<Vector<f64>> {
let ones = Matrix::<f64>::ones(inputs.rows(), 1);
let full_inputs = ones.hcat(inputs);
if let (&Some(ref alpha), &Some(ref train_inputs), &Some(ref train_targets)) =
(&self.alpha, &self.train_inputs, &self.train_targets) {
let ker_mat = try!(self.ker_mat(&full_inputs, train_inputs));
let weight_vec = alpha.elemul(train_targets) / self.lambda;
let plane_dist = ker_mat * weight_vec;
Ok(plane_dist.apply(&|d| d.signum()))
} else {
Err(Error::new_untrained())
}
}
fn train(&mut self, inputs: &Matrix<f64>, targets: &Vector<f64>) -> LearningResult<()> {
let n = inputs.rows();
let mut rng = rand::thread_rng();
let mut alpha = vec![0f64; n];
let ones = Matrix::<f64>::ones(inputs.rows(), 1);
let full_inputs = ones.hcat(inputs);
for t in 0..self.optim_iters {
let i = rng.gen_range(0, n);
let row_i = full_inputs.select_rows(&[i]);
let sum = full_inputs.row_iter()
.fold(0f64, |sum, row| sum + self.ker.kernel(row_i.data(), row.raw_slice())) *
targets[i] / (self.lambda * (t as f64));
if sum < 1f64 {
alpha[i] += 1f64;
}
}
self.alpha = Some(Vector::new(alpha) / (self.optim_iters as f64));
self.train_inputs = Some(full_inputs);
self.train_targets = Some(targets.clone());
Ok(())
}
}