Skip to main content

incremental_rs/
linear_regression.rs

1use crate::error::IncrementalError;
2use crate::learning_rate::LearningRateSchedule;
3use crate::IncrementalSupervisedEstimator;
4use ndarray::{Array1, Array2};
5
6#[derive(Debug)]
7pub struct IncrementalLinearRegression {
8    weights: Option<Array1<f64>>,
9    bias: f64,
10    schedule: LearningRateSchedule,
11    step_count: usize,
12    l2_penalty: f64,
13}
14
15impl IncrementalLinearRegression {
16    pub fn new(schedule: LearningRateSchedule, l2_penalty: f64) -> Self {
17        Self {
18            weights: None,
19            bias: 0.0,
20            schedule,
21            step_count: 0,
22            l2_penalty,
23        }
24    }
25
26    fn validate_batch(
27        &self,
28        batch_x: &Array2<f64>,
29        batch_y: &Array1<f64>,
30    ) -> Result<(), IncrementalError> {
31        if batch_x.nrows() == 0 {
32            return Err(IncrementalError::EmptyBatch);
33        }
34        if batch_x.nrows() != batch_y.len() {
35            return Err(IncrementalError::TargetDimensionMismatch {
36                target_len: batch_y.len(),
37                feature_rows: batch_x.nrows(),
38            });
39        }
40        if batch_x.iter().any(|v| !v.is_finite()) || batch_y.iter().any(|v| !v.is_finite()) {
41            return Err(IncrementalError::NonFiniteInput);
42        }
43        if let Some(ref w) = self.weights {
44            if batch_x.ncols() != w.len() {
45                return Err(IncrementalError::DimensionMismatch {
46                    expected: w.len(),
47                    actual: batch_x.ncols(),
48                });
49            }
50        }
51        Ok(())
52    }
53}
54
55impl IncrementalSupervisedEstimator for IncrementalLinearRegression {
56    fn partial_fit(
57        &mut self,
58        batch_x: &Array2<f64>,
59        batch_y: &Array1<f64>,
60    ) -> Result<(), IncrementalError> {
61        self.validate_batch(batch_x, batch_y)?;
62
63        let n_samples = batch_x.nrows() as f64;
64        let n_features = batch_x.ncols();
65
66        // Lazy-initialize weights on the first batch[cite: 1]
67        let weights = self
68            .weights
69            .get_or_insert_with(|| Array1::zeros(n_features));
70
71        // Compute predictions: y_hat = X * w + b
72        let predictions = batch_x.dot(weights) + self.bias;
73        let errors = &predictions - batch_y;
74
75        // Compute gradients
76        let mut weight_grad = batch_x.t().dot(&errors) / n_samples;
77        if self.l2_penalty > 0.0 {
78            weight_grad += &(weights.mapv(|w| w * self.l2_penalty)); // Ridge Regularization[cite: 1]
79        }
80        let bias_grad = errors.sum() / n_samples;
81
82        // Update parameters using current schedule
83        let eta = self.schedule.calculate(self.step_count);
84        *weights -= &(weight_grad * eta);
85        self.bias -= bias_grad * eta;
86
87        self.step_count += 1;
88        Ok(())
89    }
90
91    fn predict(&self, x: &Array2<f64>) -> Result<Array1<f64>, IncrementalError> {
92        if x.nrows() == 0 {
93            return Err(IncrementalError::EmptyBatch);
94        }
95        if x.iter().any(|v| !v.is_finite()) {
96            return Err(IncrementalError::NonFiniteInput);
97        }
98        let weights = match &self.weights {
99            Some(w) => w,
100            None => return Ok(Array1::zeros(x.nrows())),
101        };
102
103        if x.ncols() != weights.len() {
104            return Err(IncrementalError::DimensionMismatch {
105                expected: weights.len(),
106                actual: x.ncols(),
107            });
108        }
109
110        Ok(x.dot(weights) + self.bias)
111    }
112}