impl Ridge {
#[must_use]
pub fn new(alpha: f32) -> Self {
Self {
alpha,
coefficients: None,
intercept: 0.0,
fit_intercept: true,
}
}
#[must_use]
pub fn with_intercept(mut self, fit_intercept: bool) -> Self {
self.fit_intercept = fit_intercept;
self
}
#[must_use]
pub fn alpha(&self) -> f32 {
self.alpha
}
#[must_use]
pub fn coefficients(&self) -> &Vector<f32> {
self.coefficients
.as_ref()
.expect("Model not fitted. Call fit() first.")
}
#[must_use]
pub fn intercept(&self) -> f32 {
self.intercept
}
#[must_use]
pub fn is_fitted(&self) -> bool {
self.coefficients.is_some()
}
pub fn save<P: AsRef<Path>>(&self, path: P) -> std::result::Result<(), String> {
let bytes = bincode::serialize(self).map_err(|e| format!("Serialization failed: {e}"))?;
fs::write(path, bytes).map_err(|e| format!("File write failed: {e}"))?;
Ok(())
}
pub fn load<P: AsRef<Path>>(path: P) -> std::result::Result<Self, String> {
let bytes = fs::read(path).map_err(|e| format!("File read failed: {e}"))?;
let model =
bincode::deserialize(&bytes).map_err(|e| format!("Deserialization failed: {e}"))?;
Ok(model)
}
pub fn save_safetensors<P: AsRef<Path>>(&self, path: P) -> std::result::Result<(), String> {
use crate::serialization::safetensors;
use std::collections::BTreeMap;
let coefficients = self
.coefficients
.as_ref()
.ok_or("Cannot save unfitted model. Call fit() first.")?;
let mut tensors = BTreeMap::new();
let coef_data: Vec<f32> = (0..coefficients.len()).map(|i| coefficients[i]).collect();
let coef_shape = vec![coefficients.len()];
tensors.insert("coefficients".to_string(), (coef_data, coef_shape));
let intercept_data = vec![self.intercept];
let intercept_shape = vec![1];
tensors.insert("intercept".to_string(), (intercept_data, intercept_shape));
let alpha_data = vec![self.alpha];
let alpha_shape = vec![1];
tensors.insert("alpha".to_string(), (alpha_data, alpha_shape));
safetensors::save_safetensors(path, &tensors)?;
Ok(())
}
pub fn load_safetensors<P: AsRef<Path>>(path: P) -> std::result::Result<Self, String> {
use crate::serialization::safetensors;
let (metadata, raw_data) = safetensors::load_safetensors(path)?;
let coef_meta = metadata
.get("coefficients")
.ok_or("Missing 'coefficients' tensor in SafeTensors file")?;
let coef_data = safetensors::extract_tensor(&raw_data, coef_meta)?;
let intercept_meta = metadata
.get("intercept")
.ok_or("Missing 'intercept' tensor in SafeTensors file")?;
let intercept_data = safetensors::extract_tensor(&raw_data, intercept_meta)?;
let alpha_meta = metadata
.get("alpha")
.ok_or("Missing 'alpha' tensor in SafeTensors file")?;
let alpha_data = safetensors::extract_tensor(&raw_data, alpha_meta)?;
if intercept_data.len() != 1 {
return Err(format!(
"Expected intercept tensor to have 1 element, got {}",
intercept_data.len()
));
}
if alpha_data.len() != 1 {
return Err(format!(
"Expected alpha tensor to have 1 element, got {}",
alpha_data.len()
));
}
Ok(Self {
alpha: alpha_data[0],
coefficients: Some(Vector::from_vec(coef_data)),
intercept: intercept_data[0],
fit_intercept: true, })
}
}
impl Estimator for Ridge {
fn fit(&mut self, x: &Matrix<f32>, y: &Vector<f32>) -> Result<()> {
let (n_samples, n_features) = x.shape();
if n_samples != y.len() {
return Err("Number of samples must match target length".into());
}
if n_samples == 0 {
return Err("Cannot fit with zero samples".into());
}
let x_design = if self.fit_intercept {
LinearRegression::add_intercept_column(x)
} else {
x.clone()
};
let n_params = if self.fit_intercept {
n_features + 1
} else {
n_features
};
let xt = x_design.transpose();
let mut xtx = xt.matmul(&x_design)?;
for i in 0..n_params {
if self.fit_intercept && i == 0 {
continue;
}
let current = xtx.get(i, i);
xtx.set(i, i, current + self.alpha);
}
let xty = xt.matvec(y)?;
let beta = xtx.cholesky_solve(&xty)?;
if self.fit_intercept {
self.intercept = beta[0];
self.coefficients = Some(beta.slice(1, n_features + 1));
} else {
self.intercept = 0.0;
self.coefficients = Some(beta);
}
Ok(())
}
fn predict(&self, x: &Matrix<f32>) -> Vector<f32> {
let coefficients = self
.coefficients
.as_ref()
.expect("Model not fitted. Call fit() first.");
let result = x
.matvec(coefficients)
.expect("Matrix dimensions don't match coefficients");
result.add_scalar(self.intercept)
}
fn score(&self, x: &Matrix<f32>, y: &Vector<f32>) -> f32 {
let y_pred = self.predict(x);
r_squared(&y_pred, y)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Lasso {
alpha: f32,
coefficients: Option<Vector<f32>>,
intercept: f32,
fit_intercept: bool,
max_iter: usize,
tol: f32,
}