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_regularization_type, validate_tolerance,
};
use crate::math::matmul::gemv_internal;
use crate::math::reduction::det_reduce;
use crate::parallel_gates::{CHEAP_MAP_F64_PARALLEL_THRESHOLD, SUM_F64_PARALLEL_MIN_ELEMS};
use crate::{Deserialize, Serialize};
use ndarray::{Array1, ArrayBase, Data, Ix1, Ix2};
use rayon::prelude::{
IndexedParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator,
};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LinearRegression {
coefficients: Option<Array1<f64>>,
intercept: Option<f64>,
fit_intercept: bool,
learning_rate: f64,
max_iter: usize,
tol: f64,
n_iter: Option<usize>,
regularization_type: Option<RegularizationType>,
}
impl Default for LinearRegression {
fn default() -> Self {
Self {
coefficients: None,
intercept: None,
fit_intercept: true,
learning_rate: 0.01,
max_iter: 1000,
tol: 1e-5,
n_iter: None,
regularization_type: None,
}
}
}
impl LinearRegression {
pub fn new(
fit_intercept: bool,
learning_rate: f64,
max_iterations: usize,
tolerance: f64,
) -> Result<Self, Error> {
validate_learning_rate(learning_rate)?;
validate_max_iterations(max_iterations)?;
validate_tolerance(tolerance)?;
Ok(LinearRegression {
coefficients: None,
intercept: None,
fit_intercept,
learning_rate,
max_iter: max_iterations,
tol: tolerance,
n_iter: None,
regularization_type: None,
})
}
pub fn with_regularization(
mut self,
regularization: RegularizationType,
) -> Result<Self, Error> {
validate_regularization_type(Some(regularization))?;
self.regularization_type = Some(regularization);
Ok(self)
}
get_field!(get_fit_intercept, fit_intercept, bool);
get_field!(get_learning_rate, learning_rate, 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!(
get_regularization_type,
regularization_type,
Option<RegularizationType>
);
get_field_as_ref!(get_coefficients, coefficients, Option<&Array1<f64>>);
get_field!(get_intercept, intercept, Option<f64>);
pub fn fit<S>(
&mut self,
x: &ArrayBase<S, Ix2>,
y: &ArrayBase<S, Ix1>,
) -> Result<&mut Self, Error>
where
S: Data<Elem = f64>,
{
preliminary_check(x, Some(y))?;
let n_samples = x.nrows();
let n_features = x.ncols();
let mut weights = Array1::<f64>::zeros(n_features);
let mut intercept = 0.0;
let mut prev_cost = f64::INFINITY;
let mut convergence_count = 0;
const CONVERGENCE_THRESHOLD: usize = 3;
let mut n_iter = 0;
let mut predictions = Array1::<f64>::zeros(n_samples);
let mut error_vec = Array1::<f64>::zeros(n_samples);
#[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(format!(
"{:.6} | Convergence: 0/{}",
f64::INFINITY,
CONVERGENCE_THRESHOLD
));
pb
};
while n_iter < self.max_iter {
n_iter += 1;
predictions.assign(&gemv_internal(x, &weights));
if self.fit_intercept {
predictions += intercept;
}
error_vec.assign(&(&predictions - y));
let sse = match error_vec.as_slice() {
Some(slice) => det_reduce(
slice,
slice.len() >= SUM_F64_PARALLEL_MIN_ELEMS,
|block| block.iter().map(|v| v * v).sum::<f64>(),
|a, b| a + b,
0.0,
),
_ => error_vec.dot(&error_vec),
};
let regularization_term = match &self.regularization_type {
None => 0.0,
Some(RegularizationType::L1(alpha)) => {
alpha * weights.iter().map(|w| w.abs()).sum::<f64>()
}
Some(RegularizationType::L2(alpha)) => 0.5 * alpha * weights.dot(&weights),
};
let cost = sse / (2.0 * n_samples as f64) + regularization_term;
#[cfg(feature = "show_progress")]
progress_bar.set_message(format!(
"{:.6} | Convergence: {}/{}",
cost, convergence_count, CONVERGENCE_THRESHOLD
));
#[cfg(feature = "show_progress")]
progress_bar.inc(1);
if !cost.is_finite() {
#[cfg(feature = "show_progress")]
progress_bar.finish_with_message("Error: NaN or infinite cost");
return Err(Error::non_finite("cost calculation"));
}
let mut weight_gradients = gemv_internal(&x.t(), &error_vec) / (n_samples as f64);
let intercept_gradient = if self.fit_intercept {
let error_sum = match error_vec.as_slice() {
Some(slice) => det_reduce(
slice,
slice.len() >= SUM_F64_PARALLEL_MIN_ELEMS,
|block| block.iter().sum::<f64>(),
|a, b| a + b,
0.0,
),
_ => error_vec.sum(),
};
error_sum / (n_samples as f64)
} else {
0.0
};
if weight_gradients.iter().any(|&val| !val.is_finite())
|| !intercept_gradient.is_finite()
{
#[cfg(feature = "show_progress")]
progress_bar.finish_with_message("Error: NaN or infinite gradients");
return Err(Error::non_finite("gradient calculation"));
}
match &self.regularization_type {
None => {}
Some(RegularizationType::L1(alpha)) => {
let alpha_val = *alpha;
if n_features >= CHEAP_MAP_F64_PARALLEL_THRESHOLD {
let weights_slice = weights.as_slice().unwrap();
let gradients_slice = weight_gradients.as_slice_mut().unwrap();
gradients_slice
.par_iter_mut()
.zip(weights_slice.par_iter())
.for_each(|(grad, w)| {
*grad += alpha_val * w.signum();
});
} else {
weight_gradients
.iter_mut()
.zip(weights.iter())
.for_each(|(grad, w)| {
*grad += alpha_val * w.signum();
});
}
}
Some(RegularizationType::L2(alpha)) => {
weight_gradients.scaled_add(*alpha, &weights);
}
}
weights.scaled_add(-self.learning_rate, &weight_gradients);
if self.fit_intercept {
intercept -= self.learning_rate * intercept_gradient;
}
if weights.iter().any(|&val| !val.is_finite()) || !intercept.is_finite() {
#[cfg(feature = "show_progress")]
progress_bar.finish_with_message("Error: NaN or infinite parameters");
return Err(Error::non_finite("parameter update"));
}
let cost_change = (prev_cost - cost).abs();
if cost_change < self.tol {
convergence_count += 1;
if convergence_count >= CONVERGENCE_THRESHOLD {
break;
}
} else {
convergence_count = 0;
}
prev_cost = cost;
}
#[cfg(feature = "show_progress")]
let convergence_status = if n_iter < self.max_iter {
"Converged"
} else {
"Max iterations"
};
#[cfg(feature = "show_progress")]
progress_bar.finish_with_message(format!(
"{:.6} | {} | Iterations: {}",
prev_cost, convergence_status, n_iter
));
self.coefficients = Some(weights);
self.intercept = Some(if self.fit_intercept { intercept } else { 0.0 });
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 coeffs = self
.coefficients
.as_ref()
.ok_or_else(|| Error::not_fitted("LinearRegression"))?;
let intercept = self.intercept.unwrap_or(0.0);
validate_predict_input(x, coeffs.len())?;
let mut predictions = gemv_internal(x, coeffs);
if self.fit_intercept {
predictions += intercept;
}
if predictions.iter().any(|&val| !val.is_finite()) {
return Err(Error::non_finite("prediction calculation"));
}
Ok(predictions)
}
pub fn fit_predict<S>(
&mut self,
x: &ArrayBase<S, Ix2>,
y: &ArrayBase<S, Ix1>,
) -> Result<Array1<f64>, Error>
where
S: Data<Elem = f64>,
{
self.fit(x, y)?;
self.predict(x)
}
model_save_and_load_methods!(LinearRegression);
}