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_par_auto;
use crate::math::reduction::det_reduce;
use crate::parallel_gates::sum_f64_parallel_min_elems;
use crate::{Deserialize, Serialize};
use ndarray::{Array1, Array2, ArrayBase, Axis, Data, Ix1, Ix2};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum Solver {
GradientDescent,
Normal,
}
#[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>,
solver: Solver,
}
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,
solver: Solver::GradientDescent,
}
}
}
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,
solver: Solver::GradientDescent,
})
}
pub fn with_solver(mut self, solver: Solver) -> Self {
self.solver = solver;
self
}
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>);
get_field!(get_solver, solver, Solver);
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))?;
if self.solver == Solver::Normal {
return self.fit_normal(x, 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_par_auto(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_par_auto(&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;
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)
}
fn fit_normal<S>(
&mut self,
x: &ArrayBase<S, Ix2>,
y: &ArrayBase<S, Ix1>,
) -> Result<&mut Self, Error>
where
S: Data<Elem = f64>,
{
let n_samples = x.nrows();
let ridge_lambda = match &self.regularization_type {
None => 0.0,
Some(RegularizationType::L2(alpha)) => *alpha * n_samples as f64,
Some(RegularizationType::L1(_)) => {
return Err(Error::invalid_input(
"the Normal solver does not support L1 regularization (no closed form); \
use Solver::GradientDescent",
));
}
};
let (x_design, y_target, x_means, y_mean) = if self.fit_intercept {
let x_means = x
.mean_axis(Axis(0))
.ok_or_else(|| Error::empty_input("feature matrix"))?;
let y_mean = y.sum() / n_samples as f64;
let xc = &x.to_owned() - &x_means;
let yc = y.mapv(|v| v - y_mean);
(xc, yc, Some(x_means), y_mean)
} else {
(x.to_owned(), y.to_owned(), None, 0.0)
};
let weights = solve_ridge_lstsq(&x_design, &y_target, ridge_lambda)?;
let intercept = match &x_means {
Some(x_means) => y_mean - x_means.dot(&weights),
None => 0.0,
};
if weights.iter().any(|v| !v.is_finite()) || !intercept.is_finite() {
return Err(Error::non_finite("closed-form solution"));
}
self.coefficients = Some(weights);
self.intercept = Some(intercept);
self.n_iter = Some(0);
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_par_auto(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)
}
pub fn score<S>(&self, x: &ArrayBase<S, Ix2>, y: &ArrayBase<S, Ix1>) -> Result<f64, Error>
where
S: Data<Elem = f64>,
{
let predictions = self.predict(x)?;
if y.len() != predictions.len() {
return Err(Error::dimension_mismatch(predictions.len(), y.len()));
}
if y.iter().any(|v| !v.is_finite()) {
return Err(Error::non_finite("target vector"));
}
let y_mean = y.sum() / y.len() as f64;
let mut ss_res = 0.0;
let mut ss_tot = 0.0;
for (yi, pi) in y.iter().zip(predictions.iter()) {
ss_res += (yi - pi).powi(2);
ss_tot += (yi - y_mean).powi(2);
}
let r2 = if ss_tot != 0.0 {
1.0 - ss_res / ss_tot
} else if ss_res == 0.0 {
1.0
} else {
0.0
};
Ok(r2)
}
model_save_and_load_methods!(LinearRegression);
}
fn solve_ridge_lstsq(
x: &Array2<f64>,
y: &Array1<f64>,
ridge_lambda: f64,
) -> Result<Array1<f64>, Error> {
let n = x.nrows();
let p = x.ncols();
let extra = if ridge_lambda > 0.0 { p } else { 0 };
let total_rows = n + extra;
let mut d = nalgebra::DMatrix::<f64>::zeros(total_rows, p);
for i in 0..n {
for j in 0..p {
d[(i, j)] = x[[i, j]];
}
}
if extra > 0 {
let s = ridge_lambda.sqrt();
for j in 0..p {
d[(n + j, j)] = s;
}
}
let mut t = nalgebra::DVector::<f64>::zeros(total_rows);
for (i, &yi) in y.iter().enumerate() {
t[i] = yi;
}
let svd = nalgebra::linalg::SVD::new(d, true, true);
let solution = svd
.solve(&t, 1e-12)
.map_err(|e| Error::computation(format!("closed-form least squares failed: {e}")))?;
Ok(Array1::from_iter(solution.iter().copied()))
}