use crate::bench::regression_metrics::RegressionMetrics;
use crate::builders::linear_regression::LinearRegressionBuilder;
use crate::core::error::ModelError;
use crate::core::types::{Matrix, Scalar, Vector};
use crate::model::core::base::{BaseModel, OptimizableModel};
use crate::model::core::param_collection::{GradientCollection, ParamCollection};
use crate::model::core::regression_model::RegressionModel;
use ndarray::{Array0, Array1, ArrayView, ArrayView0, ArrayView1};
#[derive(Debug)]
pub struct LinearRegression {
pub w: Vector,
pub b: Scalar,
dw: Vector,
db: Scalar,
}
impl LinearRegression {
pub fn new(n_x: usize) -> Result<Self, ModelError> {
let weights = Array1::<f64>::zeros(n_x);
let bias = Array0::<f64>::from_elem((), 0.0);
Ok(Self {
w: weights,
b: bias,
dw: Array1::<f64>::zeros(n_x),
db: Array0::<f64>::from_elem((), 0.0),
})
}
pub fn builder() -> LinearRegressionBuilder {
LinearRegressionBuilder::new()
}
}
impl ParamCollection for LinearRegression {
fn get<D: ndarray::Dimension>(&self, key: &str) -> Result<ArrayView<f64, D>, ModelError> {
match key {
"weights" => Ok(self.w.view().into_dimensionality::<D>().unwrap()),
"bias" => Ok(self.b.view().into_dimensionality::<D>().unwrap()),
_ => Err(ModelError::KeyError(key.to_string())),
}
}
fn get_mut<D: ndarray::Dimension>(
&mut self,
key: &str,
) -> Result<ndarray::ArrayViewMut<f64, D>, ModelError> {
match key {
"weights" => Ok(self.w.view_mut().into_dimensionality::<D>().unwrap()),
"bias" => Ok(self.b.view_mut().into_dimensionality::<D>().unwrap()),
_ => Err(ModelError::KeyError(key.to_string())),
}
}
fn set<D: ndarray::Dimension>(
&mut self,
key: &str,
value: ArrayView<f64, D>,
) -> Result<(), ModelError> {
match key {
"weights" => {
self.w
.assign(&value.into_dimensionality::<ndarray::Ix1>().unwrap());
Ok(())
}
"bias" => {
self.b
.assign(&value.into_dimensionality::<ndarray::Ix0>().unwrap());
Ok(())
}
_ => Err(ModelError::KeyError(key.to_string())),
}
}
fn param_iter(&self) -> Vec<(&str, ArrayView<f64, ndarray::IxDyn>)> {
vec![
("weights", self.w.view().into_dyn()),
("bias", self.b.view().into_dyn()),
]
}
}
impl GradientCollection for LinearRegression {
fn get_gradient<D: ndarray::Dimension>(
&self,
key: &str,
) -> Result<ArrayView<f64, D>, ModelError> {
match key {
"weights" => Ok(self.dw.view().into_dimensionality::<D>().unwrap()),
"bias" => Ok(self.db.view().into_dimensionality::<D>().unwrap()),
_ => Err(ModelError::KeyError(key.to_string())),
}
}
fn set_gradient<D: ndarray::Dimension>(
&mut self,
key: &str,
value: ArrayView<f64, D>,
) -> Result<(), ModelError> {
match key {
"weights" => {
self.dw
.assign(&value.into_dimensionality::<ndarray::Ix1>().unwrap());
Ok(())
}
"bias" => {
self.db
.assign(&value.into_dimensionality::<ndarray::Ix0>().unwrap());
Ok(())
}
_ => Err(ModelError::KeyError(key.to_string())),
}
}
}
impl BaseModel<Matrix, Vector> for LinearRegression {
fn predict(&self, x: &Matrix) -> Result<Vector, ModelError> {
let w: ArrayView1<f64> = self.get("weights")?;
let b: ArrayView0<f64> = self.get("bias")?;
let y_hat = w.t().dot(x) + b;
Ok(y_hat)
}
fn compute_cost(&self, x: &Matrix, y: &Vector) -> Result<f64, ModelError> {
let y_hat = self.predict(x)?;
let m = x.len() as f64;
let cost = (&y_hat - y).powi(2).sum() / (2.0 * m);
Ok(cost)
}
}
#[cfg(test)]
mod lr_base_model_tests {
use crate::model::core::base::BaseModel;
use crate::model::linear_regression::LinearRegression;
use ndarray::{arr0, arr1, arr2};
#[test]
fn test_predict() {
let mut lr = LinearRegression::new(2).unwrap();
let x = arr2(&[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
let weights = arr1(&[1.0, 2.0]);
let bias = arr0(0.0);
lr.w = weights;
lr.b = bias;
let y = arr1(&[9.0, 12.0, 15.0]);
let y_hat = lr.predict(&x).unwrap();
assert_eq!(&y, &y_hat);
}
}
impl OptimizableModel<Matrix, Vector> for LinearRegression {
fn forward(&self, input: &Matrix) -> Result<Vector, ModelError> {
let y_hat = &self.w.t().dot(input) + &self.b;
Ok(y_hat)
}
fn backward(&mut self, input: &Matrix, output_grad: &Vector) -> Result<(), ModelError> {
let m = input.shape()[1] as f64;
let dw: Vector = input.dot(&output_grad.t()) / m;
let dw: ArrayView1<f64> = dw.view();
let db = output_grad.sum() / m;
let binding = Scalar::from_elem((), db);
let db: ArrayView0<f64> = binding.view();
self.set_gradient("weights", dw)?;
self.set_gradient("bias", db)?;
Ok(())
}
fn compute_output_gradient(&self, x: &Matrix, y: &Vector) -> Result<Vector, ModelError> {
let y_hat = self.forward(x)?;
let dy = &y_hat - y;
Ok(dy)
}
}
#[cfg(test)]
mod lr_optimizable_model_tests {
use ndarray::{ArrayView0, ArrayView1, arr0, arr1, arr2};
use crate::{
builders::builder::Builder,
model::core::{base::OptimizableModel, param_collection::GradientCollection},
};
use super::LinearRegression;
#[test]
fn test_forward_propagation() {
let n_features = 2;
let mut model = LinearRegression::builder()
.n_input_features(n_features)
.build()
.unwrap();
let weights = arr1(&[1.0, 2.0]);
model.w.assign(&weights);
let bias = arr0(0.0);
model.b.assign(&bias);
let x = arr2(&[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
let y = arr1(&[9.0, 12.0, 15.0]);
let y_hat = model.forward(&x).unwrap();
assert_eq!(y_hat, y);
assert_eq!(model.w, weights);
assert_eq!(model.b, bias);
}
#[test]
fn test_compute_output_grad() {
let n_features = 2;
let mut model = LinearRegression::builder()
.n_input_features(n_features)
.build()
.unwrap();
let weights = arr1(&[1.0, 2.0]);
model.w.assign(&weights);
let bias = arr0(0.0);
model.b.assign(&bias);
let x = arr2(&[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
let y = arr1(&[8.0, 11.0, 14.0]);
let expected_grad = 1.0;
let output_grad = model.compute_output_gradient(&x, &y).unwrap();
assert_eq!(output_grad[0], expected_grad);
}
#[test]
fn test_backward_propagation() {
let n_features = 2;
let mut model = LinearRegression::builder()
.n_input_features(n_features)
.build()
.unwrap();
let weights = arr1(&[1.0, 2.0]);
model.w.assign(&weights);
let bias = arr0(0.0);
model.b.assign(&bias);
let x = arr2(&[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
let y = arr1(&[8.0, 11.0, 14.0]);
let output_grad = model.compute_output_gradient(&x, &y).unwrap();
model.backward(&x, &output_grad).unwrap();
let expected_dw = arr1(&[2.0, 5.0]);
let expected_db = arr0(1.0);
let dw: ArrayView1<f64> = model.get_gradient("weights").unwrap();
let db: ArrayView0<f64> = model.get_gradient("bias").unwrap();
assert_eq!(dw, expected_dw);
assert_eq!(db, expected_db);
}
}
impl RegressionModel<Matrix, Vector> for LinearRegression {
fn mse(&self, x: &Matrix, y: &Vector) -> Result<f64, ModelError> {
let y_hat = self.predict(x)?;
let m = x.len() as f64;
Ok((&y_hat - y).mapv(|v| v.powi(2)).sum() / m)
}
fn rmse(&self, x: &Matrix, y: &Vector) -> Result<f64, ModelError> {
let y_hat = self.predict(x)?;
let m = x.len() as f64;
let rmse = ((&y_hat - y).mapv(|v| v.powi(2)).sum() / m).sqrt();
Ok(rmse)
}
fn r2(&self, x: &Matrix, y: &Vector) -> Result<f64, ModelError> {
let y_hat = self.predict(x)?;
let numerator = self.mse(x, y)?;
let denominator = (y_hat - y.mean().unwrap()).powi(2).sum();
Ok(1.0 - numerator / denominator)
}
fn compute_metrics(&self, x: &Matrix, y: &Vector) -> Result<RegressionMetrics, ModelError> {
let mse = self.mse(x, y)?;
let rmse = self.rmse(x, y)?;
let r2 = self.r2(x, y)?;
Ok(RegressionMetrics { mse, rmse, r2 })
}
}