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::matvec;
use crate::math::reduction::det_reduce;
use crate::parallel_gates::sum_f64_parallel_min_elems;
use crate::{Deserialize, Serialize};
use gemmkit_ndarray::Parallelism;
use ndarray::{Array1, Array2, ArrayBase, Axis, Data, Ix1, Ix2};
#[derive(Debug, Clone, Copy, PartialEq, Default, Deserialize, Serialize)]
pub enum LeastSquaresSolver {
#[default]
Normal,
GradientDescent {
learning_rate: f64,
max_iter: usize,
tol: f64,
},
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LinearRegression {
coefficients: Option<Array1<f64>>,
intercept: Option<f64>,
fit_intercept: bool,
n_iter: Option<usize>,
regularization_type: Option<RegularizationType>,
solver: LeastSquaresSolver,
}
impl Default for LinearRegression {
fn default() -> Self {
Self::new(true)
}
}
impl LinearRegression {
pub fn new(fit_intercept: bool) -> Self {
LinearRegression {
coefficients: None,
intercept: None,
fit_intercept,
n_iter: None,
regularization_type: None,
solver: LeastSquaresSolver::Normal,
}
}
pub fn with_solver(mut self, solver: LeastSquaresSolver) -> Result<Self, Error> {
if let LeastSquaresSolver::GradientDescent {
learning_rate,
max_iter,
tol,
} = solver
{
validate_learning_rate(learning_rate)?;
validate_max_iterations(max_iter)?;
validate_tolerance(tol)?;
}
self.solver = solver;
Ok(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_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, LeastSquaresSolver);
pub fn fit<S1, S2>(
&mut self,
x: &ArrayBase<S1, Ix2>,
y: &ArrayBase<S2, Ix1>,
) -> Result<&mut Self, Error>
where
S1: Data<Elem = f64>,
S2: Data<Elem = f64>,
{
preliminary_check(x, Some(y.len()))?;
let LeastSquaresSolver::GradientDescent {
learning_rate,
max_iter,
tol,
} = self.solver
else {
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(
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 < max_iter {
n_iter += 1;
predictions.assign(&matvec(x, &weights, Parallelism::Rayon(0)));
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 =
matvec(&x.t(), &error_vec, Parallelism::Rayon(0)) / (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(_)) => {}
Some(RegularizationType::L2(alpha)) => {
weight_gradients.scaled_add(*alpha, &weights);
}
}
weights.scaled_add(-learning_rate, &weight_gradients);
if self.fit_intercept {
intercept -= learning_rate * intercept_gradient;
}
if let Some(RegularizationType::L1(alpha)) = &self.regularization_type {
let shrink = learning_rate * alpha;
weights.mapv_inplace(|w| {
if w > shrink {
w - shrink
} else if w < -shrink {
w + shrink
} else {
0.0
}
});
}
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 < 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 < 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<S1, S2>(
&mut self,
x: &ArrayBase<S1, Ix2>,
y: &ArrayBase<S2, Ix1>,
) -> Result<&mut Self, Error>
where
S1: Data<Elem = f64>,
S2: 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 LeastSquaresSolver::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 = matvec(x, coeffs, Parallelism::Rayon(0));
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<S1, S2>(
&mut self,
x: &ArrayBase<S1, Ix2>,
y: &ArrayBase<S2, Ix1>,
) -> Result<Array1<f64>, Error>
where
S1: Data<Elem = f64>,
S2: Data<Elem = f64>,
{
self.fit(x, y)?;
self.predict(x)
}
pub fn score<S1, S2>(
&self,
x: &ArrayBase<S1, Ix2>,
y: &ArrayBase<S2, Ix1>,
) -> Result<f64, Error>
where
S1: Data<Elem = f64>,
S2: 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 = Array2::<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 = Array1::<f64>::zeros(total_rows);
for (i, &yi) in y.iter().enumerate() {
t[i] = yi;
}
crate::machine_learning::linalg::svd(&d, true, true).solve(&t, 1e-12)
}