incremental_rs/
linear_regression.rs1use 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 let weights = self
68 .weights
69 .get_or_insert_with(|| Array1::zeros(n_features));
70
71 let predictions = batch_x.dot(weights) + self.bias;
73 let errors = &predictions - batch_y;
74
75 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)); }
80 let bias_grad = errors.sum() / n_samples;
81
82 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}