use crate::error::Error;
pub use crate::machine_learning::RegularizationType;
use crate::machine_learning::validation::{
preliminary_check, validate_learning_rate, validate_max_iterations, validate_predict_input,
validate_tolerance,
};
use crate::math::hinge_loss;
use crate::math::matmul::gemv_par_auto;
use crate::{Deserialize, Serialize};
use ndarray::{Array1, ArrayBase, Data, Ix1, Ix2, s};
use ndarray_rand::rand::seq::SliceRandom;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum Loss {
Hinge,
SquaredHinge,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LinearSVC {
weights: Option<Array1<f64>>,
bias: Option<f64>,
max_iter: usize,
learning_rate: f64,
learning_rate_decay: f64,
penalty: RegularizationType,
fit_intercept: bool,
tol: f64,
loss: Loss,
random_state: Option<u64>,
n_iter: Option<usize>,
}
impl Default for LinearSVC {
fn default() -> Self {
LinearSVC {
weights: None,
bias: None,
max_iter: 1000,
learning_rate: 0.001,
learning_rate_decay: 0.0,
penalty: RegularizationType::L2(1.0),
fit_intercept: true,
tol: 1e-4,
loss: Loss::Hinge,
random_state: None,
n_iter: None,
}
}
}
impl LinearSVC {
pub fn new(
max_iter: usize,
learning_rate: f64,
penalty: RegularizationType,
fit_intercept: bool,
tol: f64,
) -> Result<Self, Error> {
validate_max_iterations(max_iter)?;
validate_learning_rate(learning_rate)?;
validate_tolerance(tol)?;
let reg_param = match penalty {
RegularizationType::L1(lambda) | RegularizationType::L2(lambda) => lambda,
};
if reg_param < 0.0 || !reg_param.is_finite() {
return Err(Error::invalid_parameter(
"penalty",
format!(
"regularization parameter must be non-negative and finite, got {reg_param}"
),
));
}
Ok(LinearSVC {
weights: None,
bias: None,
max_iter,
learning_rate,
learning_rate_decay: 0.0,
penalty,
fit_intercept,
tol,
loss: Loss::Hinge,
random_state: None,
n_iter: None,
})
}
pub fn with_loss(mut self, loss: Loss) -> Self {
self.loss = loss;
self
}
pub fn with_learning_rate_decay(mut self, decay: f64) -> Result<Self, Error> {
if decay < 0.0 || !decay.is_finite() {
return Err(Error::invalid_parameter(
"learning_rate_decay",
format!("must be non-negative and finite, got {decay}"),
));
}
self.learning_rate_decay = decay;
Ok(self)
}
pub fn with_random_state(mut self, seed: u64) -> Self {
self.random_state = Some(seed);
self
}
fn check_weights_validity(weights: &Array1<f64>, bias: f64) -> Result<(), Error> {
if weights.iter().any(|&w| !w.is_finite()) || !bias.is_finite() {
return Err(Error::non_finite(
"weights during training (try reducing learning_rate or regularization_param)",
));
}
Ok(())
}
fn calculate_batch_size(n_samples: usize) -> usize {
const MIN_BATCH_SIZE: usize = 32;
const MAX_BATCH_SIZE: usize = 512;
(n_samples / 10).clamp(MIN_BATCH_SIZE, MAX_BATCH_SIZE)
}
get_field!(get_fit_intercept, fit_intercept, bool);
get_field!(get_learning_rate, learning_rate, f64);
get_field!(get_learning_rate_decay, learning_rate_decay, f64);
get_field!(get_tolerance, tol, f64);
get_field!(get_max_iterations, max_iter, usize);
get_field!(get_actual_iterations, n_iter, Option<usize>);
get_field_as_ref!(get_weights, weights, Option<&Array1<f64>>);
get_field!(get_bias, bias, Option<f64>);
get_field!(get_penalty, penalty, RegularizationType);
get_field!(get_loss, loss, Loss);
get_field!(get_random_state, random_state, Option<u64>);
pub fn fit<S>(
&mut self,
x: &ArrayBase<S, Ix2>,
y: &ArrayBase<S, Ix1>,
) -> Result<&mut Self, Error>
where
S: Data<Elem = f64> + Send + Sync,
{
preliminary_check(x, Some(y))?;
let n_samples = x.nrows();
let n_features = x.ncols();
if !y.iter().all(|&yi| yi == 0.0 || yi == 1.0) {
return Err(Error::invalid_input(
"LinearSVC is a binary classifier; all labels must be either 0.0 or 1.0",
));
}
let mut weights = Array1::zeros(n_features);
let mut bias = 0.0;
let y_binary = y.mapv(|v| if v <= 0.0 { -1.0 } else { 1.0 });
let mut indices: Vec<usize> = (0..n_samples).collect();
let mut rng = crate::random::make_rng(self.random_state);
let mut prev_weights = weights.clone();
let mut prev_bias = bias;
let mut n_iter = 0;
let batch_size = Self::calculate_batch_size(n_samples);
#[allow(unused_variables)]
let calculate_cost = |x: &ArrayBase<S, Ix2>,
y: &Array1<f64>,
weights: &Array1<f64>,
bias: f64,
penalty: &RegularizationType|
-> f64 {
let margins: Array1<f64> = gemv_par_auto(x, weights) + bias;
let data_loss = match self.loss {
Loss::Hinge => hinge_loss(&margins, y),
Loss::SquaredHinge => {
margins
.iter()
.zip(y.iter())
.map(|(&m, &yi)| {
let s = (1.0 - yi * m).max(0.0);
s * s
})
.sum::<f64>()
/ margins.len() as f64
}
};
let regularization_term = match penalty {
RegularizationType::L2(lambda) => {
lambda * weights.iter().map(|&w| w * w).sum::<f64>() / 2.0
}
RegularizationType::L1(lambda) => {
lambda * weights.iter().map(|&w| w.abs()).sum::<f64>()
}
};
data_loss + regularization_term
};
#[cfg(feature = "show_progress")]
let progress_bar = {
let pb = crate::create_progress_bar(
self.max_iter as u64,
"[{elapsed_precise}] {bar:40} {pos}/{len} | Cost: {msg}",
);
pb.set_message("Initializing...");
pb
};
while n_iter < self.max_iter {
n_iter += 1;
#[cfg(feature = "show_progress")]
progress_bar.inc(1);
let lr = self.learning_rate / (1.0 + self.learning_rate_decay * (n_iter - 1) as f64);
indices.shuffle(&mut rng);
for batch_indices in indices.chunks(batch_size) {
let batch_len = batch_indices.len() as f64;
let loss = self.loss;
let accumulate = |idx: usize, w_grad: &mut Array1<f64>, b_grad: &mut f64| {
let xi = x.slice(s![idx, ..]);
let yi = y_binary[idx];
let margin = xi.dot(&weights) + bias;
let slack = 1.0 - yi * margin;
if slack > 0.0 {
let coef = match loss {
Loss::Hinge => yi,
Loss::SquaredHinge => 2.0 * slack * yi,
};
w_grad.scaled_add(coef, &xi);
*b_grad += coef;
}
};
let (weight_grad_sum, bias_grad_sum) = {
let mut acc = (Array1::<f64>::zeros(n_features), 0.0);
for &idx in batch_indices {
accumulate(idx, &mut acc.0, &mut acc.1);
}
acc
};
weights.scaled_add(lr / batch_len, &weight_grad_sum);
match self.penalty {
RegularizationType::L2(lambda) => {
weights = &weights * (1.0 - lr * lambda);
}
RegularizationType::L1(lambda) => {
let l1_grad = weights.mapv(|w| {
if w > 0.0 {
1.0
} else if w < 0.0 {
-1.0
} else {
0.0
}
});
weights = &weights - &(l1_grad * (lr * lambda));
}
}
if self.fit_intercept {
bias += lr * bias_grad_sum / batch_len;
}
Self::check_weights_validity(&weights, bias)?;
}
let weight_diff = (&weights - &prev_weights)
.iter()
.map(|&x| x * x)
.sum::<f64>()
/ n_features as f64;
let bias_diff = if self.fit_intercept {
(bias - prev_bias).powi(2)
} else {
0.0
};
let total_diff = (weight_diff + bias_diff).sqrt();
#[cfg(feature = "show_progress")]
if n_iter % 10 == 0 || total_diff < self.tol {
let current_cost = calculate_cost(x, &y_binary, &weights, bias, &self.penalty);
progress_bar.set_message(format!("{:.6}", current_cost));
}
if total_diff < self.tol {
break;
}
prev_weights.assign(&weights);
prev_bias = bias;
}
#[cfg(feature = "show_progress")]
{
let final_cost = calculate_cost(x, &y_binary, &weights, bias, &self.penalty);
let convergence_status = if n_iter < self.max_iter {
"Converged"
} else {
"Max iterations"
};
progress_bar.finish_with_message(format!(
"{:.6} | {} | Iterations: {}",
final_cost, convergence_status, n_iter
));
}
self.weights = Some(weights);
self.bias = Some(bias);
self.n_iter = Some(n_iter);
Ok(self)
}
pub fn predict<S>(&self, x: &ArrayBase<S, Ix2>) -> Result<Array1<f64>, Error>
where
S: Data<Elem = f64>,
{
let decision = self.decision_function(x)?;
Ok(decision.mapv(|v| if v > 0.0 { 1.0 } else { 0.0 }))
}
pub fn decision_function<S>(&self, x: &ArrayBase<S, Ix2>) -> Result<Array1<f64>, Error>
where
S: Data<Elem = f64>,
{
let weights = self
.weights
.as_ref()
.ok_or_else(|| Error::not_fitted("LinearSVC"))?;
let bias = self.bias.unwrap_or(0.0);
validate_predict_input(x, weights.len())?;
let decision = gemv_par_auto(x, weights) + bias;
if decision.iter().any(|&val| !val.is_finite()) {
return Err(Error::non_finite("decision function"));
}
Ok(decision)
}
pub fn fit_predict<S>(
&mut self,
x: &ArrayBase<S, Ix2>,
y: &ArrayBase<S, Ix1>,
) -> Result<Array1<f64>, Error>
where
S: Data<Elem = f64> + Send + Sync,
{
self.fit(x, y)?;
self.predict(x)
}
model_save_and_load_methods!(LinearSVC);
}