use crate::{Feature, MatrixDRows};
use faer::Mat;
use faer::linalg::solvers::Solve;
use faer_ext::{IntoFaer, IntoNalgebra};
use nalgebra::{DVector, SMatrix, SVector};
use std::sync::Arc;
#[cfg_attr(doc, katexit::katexit)]
pub struct LinearModel<const D: usize> {
pub features: Vec<Arc<dyn Feature<D> + Send + Sync>>,
}
impl<const D: usize> LinearModel<D> {
pub fn new(features: Vec<Arc<dyn Feature<D> + Send + Sync>>) -> Self {
Self { features }
}
pub fn val(&self, x: &SVector<f64, D>, coefficients: &DVector<f64>) -> f64 {
self.features
.iter()
.enumerate()
.map(|(idx, f)| f.val(x) * coefficients[idx])
.sum()
}
#[inline(always)]
pub fn val_grad(
&self,
x: &SVector<f64, D>,
coefficients: &DVector<f64>,
) -> (f64, SVector<f64, D>) {
self.features
.iter()
.enumerate()
.map(|(idx, t)| {
let val_grad = t.val_grad(x);
(
val_grad.0 * coefficients[idx],
val_grad.1 * coefficients[idx],
)
})
.reduce(|(mut sum_val, mut sum_grad), (val, grad)| {
sum_val += val;
sum_grad += grad;
(sum_val, sum_grad)
})
.unwrap()
}
#[inline(always)]
pub fn val_grad_hes(
&self,
x: &SVector<f64, D>,
coefficients: &DVector<f64>,
) -> (f64, SVector<f64, D>, SMatrix<f64, D, D>) {
self.features
.iter()
.enumerate()
.map(|(idx, t)| {
let (val, grad, hes) = t.val_grad_hes(x);
let c = coefficients[idx];
(val * c, grad * c, hes * c)
})
.reduce(
|(mut sum_val, mut sum_grad, mut sum_hes), (val, grad, hes)| {
sum_val += val;
sum_grad += grad;
sum_hes += hes;
(sum_val, sum_grad, sum_hes)
},
)
.unwrap()
}
pub fn feature_vec(&self, x: &SVector<f64, D>) -> Mat<f64> {
let mut feature_vec = Mat::<f64>::zeros(self.features.len(), 1);
feature_vec
.col_mut(0)
.iter_mut()
.enumerate()
.for_each(|(idx, r)| {
*r = self.features[idx].val(x);
});
feature_vec
}
pub fn feature_vec_hessian(&self, x: &SVector<f64, D>) -> Vec<Mat<f64>> {
self.features
.iter()
.map(|f| {
f.val_grad_hes(x)
.2
.view_range(.., ..)
.into_faer()
.to_owned()
})
.collect::<Vec<Mat<f64>>>()
}
pub fn design_t(&self, data: &MatrixDRows<D>) -> Mat<f64> {
let no_features = self.features.len();
let mut design_t = Mat::<f64>::zeros(no_features, data.ncols());
design_t
.col_iter_mut()
.enumerate()
.for_each(|(j, mut col)| {
let x = data.column(j).into();
for i in 0..col.nrows() {
col[i] = self.features[i].val(&x);
}
});
design_t
}
pub fn fim(&self, data: &MatrixDRows<D>, weights: &Mat<f64>) -> Mat<f64> {
let design_t = self.design_t(data);
self.fim_from_design_t(&design_t, weights)
}
pub fn fim_from_design_t(&self, design_t: &Mat<f64>, weights: &Mat<f64>) -> Mat<f64> {
let mut scaled_design_t = design_t.to_owned();
scaled_design_t
.col_iter_mut()
.enumerate()
.for_each(|(c_idx, mut c)| {
let sqrt_w = weights[(c_idx, 0)].sqrt();
c *= sqrt_w;
});
&scaled_design_t * scaled_design_t.transpose()
}
pub fn jac_t(&self, x: &SVector<f64, D>) -> Mat<f64> {
let mut m = Mat::<f64>::zeros(D, self.features.len());
m.col_iter_mut().enumerate().for_each(|(idx, mut c)| {
c.copy_from(
self.features[idx]
.val_grad(x)
.1
.column(0)
.into_faer()
.col(0),
);
});
m
}
pub fn fit(&self, data: &MatrixDRows<D>, y: &DVector<f64>) -> (DVector<f64>, f64) {
let fy = y.view_range(.., ..).into_faer();
let dm_t = self.design_t(data);
let a = &dm_t * dm_t.transpose();
let b = &dm_t * fy;
let coeff = a.partial_piv_lu().solve(b);
let rmse: f64 =
(1. / data.shape().1 as f64).sqrt() * (dm_t.transpose() * &coeff - fy).norm_l2();
(coeff.as_ref().into_nalgebra().column(0).into(), rmse)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{FeatureFunction, Result};
use faer::mat;
use nalgebra::{Matrix2, Vector2};
use num_dual::DualNum;
const EQ_EPS: f64 = 1e-8;
const EQ_MAX_REL: f64 = 1e-8;
#[derive(Feature)]
#[dimension = 2]
struct Monomial {
i: i32,
j: i32,
}
impl FeatureFunction<2> for Monomial {
fn f<D: DualNum<f64>>(&self, x: &SVector<D, 2>) -> D {
x[0].powi(self.i) * x[1].powi(self.j)
}
}
fn get_polynomial() -> LinearModel<2> {
let monomial_a = Monomial { i: 0, j: 0 };
let monomial_b = Monomial { i: 1, j: 1 };
let monomial_c = Monomial { i: 2, j: 2 };
LinearModel::new(vec![
Arc::new(monomial_a),
Arc::new(monomial_b),
Arc::new(monomial_c),
])
}
#[test]
fn linear_model_value_gradient_hessian() -> Result<()> {
let coeff = DVector::from_vec(vec![1., 2., 1.]);
let x = Vector2::new(2., 1.);
let polynomial = get_polynomial();
let val_grad_hes_rslt = (9., Vector2::new(6., 12.), Matrix2::new(2., 10., 10., 8.));
assert_eq!(polynomial.val(&x, &coeff), val_grad_hes_rslt.0);
assert_eq!(
polynomial.val_grad(&x, &coeff),
(val_grad_hes_rslt.0, val_grad_hes_rslt.1)
);
assert_eq!(polynomial.val_grad_hes(&x, &coeff), val_grad_hes_rslt);
Ok(())
}
#[test]
fn linear_model_feature_vec() -> Result<()> {
let polynomial = get_polynomial();
let x = Vector2::new(2., 1.);
let feature_vec = polynomial.feature_vec(&x);
assert_eq!(feature_vec, mat![[1.], [2.], [4.]]);
let feature_vec_hessian = polynomial.feature_vec_hessian(&x);
assert_eq!(
feature_vec_hessian,
vec![
mat![[0., 0.], [0., 0.]],
mat![[0., 1.], [1., 0.]],
mat![[2., 8.], [8., 8.]]
]
);
Ok(())
}
#[test]
fn design_matrix() -> Result<()> {
let polynomial = get_polynomial();
let data = MatrixDRows::from_vec(vec![1., 1., 2., 2.]);
let design_t = polynomial.design_t(&data);
assert_eq!(design_t, mat![[1., 1.], [1., 4.], [1., 16.]]);
Ok(())
}
#[test]
fn fisher_information_matrix() -> Result<()> {
let polynomial = get_polynomial();
let data = MatrixDRows::from_vec(vec![1., 1., 2., 2.]);
let weights = mat![[2.], [2.]];
let fim = polynomial.fim(&data, &weights);
let expected = mat![[4., 10., 34.], [10., 34., 130.], [34., 130., 514.]];
assert!((&fim - &expected).norm_l2() < EQ_EPS);
Ok(())
}
#[test]
fn fisher_information_matrix_from_design() -> Result<()> {
let polynomial = get_polynomial();
let data = MatrixDRows::from_vec(vec![1., 1., 2., 2.]);
let design_t = polynomial.design_t(&data);
let weights = mat![[2.], [2.]];
let fim = polynomial.fim_from_design_t(&design_t, &weights);
let expected = mat![[4., 10., 34.], [10., 34., 130.], [34., 130., 514.]];
assert!((&fim - &expected).norm_l2() < EQ_EPS);
Ok(())
}
#[test]
fn jacobian() -> Result<()> {
let polynomial = get_polynomial();
let x = Vector2::new(2., 1.);
let jac_t = polynomial.jac_t(&x);
assert_eq!(jac_t, mat![[0., 1., 4.], [0., 2., 8.]]);
Ok(())
}
#[test]
fn linear_regression() -> Result<()> {
let polynomial = get_polynomial();
let data = MatrixDRows::from_vec(vec![0., 0., 1., 1., 2., 2.]);
let y = DVector::from_vec(vec![2., 4., 22.]);
let (coeff, rmse) = polynomial.fit(&data, &y);
assert!(rmse < 1e-10);
assert!(coeff.relative_eq(&DVector::from_vec(vec![2., 1., 1.]), EQ_EPS, EQ_MAX_REL));
Ok(())
}
}