dml_core/learner/
lasso.rs1use ndarray::{Array1, Array2, ArrayView1, ArrayView2, ShapeBuilder};
2
3use super::{center, Centered, Regressor};
4use crate::error::{DmlError, Result};
5
6#[derive(Debug, Clone)]
13pub struct Lasso {
14 alpha: f64,
15 tol: f64,
16 max_iter: usize,
17 coef: Option<Array1<f64>>,
18 intercept: f64,
19}
20
21impl Lasso {
22 pub fn new(alpha: f64) -> Self {
23 Self {
24 alpha: alpha.max(0.0),
25 tol: 1e-12,
26 max_iter: 100_000,
27 coef: None,
28 intercept: 0.0,
29 }
30 }
31
32 pub fn tol(mut self, tol: f64) -> Self {
34 self.tol = tol;
35 self
36 }
37
38 pub fn max_iter(mut self, max_iter: usize) -> Self {
39 self.max_iter = max_iter;
40 self
41 }
42
43 pub fn coef(&self) -> Option<&Array1<f64>> {
44 self.coef.as_ref()
45 }
46
47 pub fn intercept(&self) -> f64 {
48 self.intercept
49 }
50}
51
52fn soft_threshold(z: f64, gamma: f64) -> f64 {
53 if z > gamma {
54 z - gamma
55 } else if z < -gamma {
56 z + gamma
57 } else {
58 0.0
59 }
60}
61
62impl Lasso {
63 fn fit_impl(
64 &mut self,
65 x: ArrayView2<'_, f64>,
66 y: ArrayView1<'_, f64>,
67 w: Option<ArrayView1<'_, f64>>,
68 ) -> Result<()> {
69 let (n, p) = x.dim();
70 let Centered {
71 x_mean,
72 y_mean,
73 xc: centered,
74 yc: mut resid,
75 } = center(x, y, w, true)?;
76 let mut xc = Array2::<f64>::zeros((n, p).f());
78 xc.assign(¢ered);
79 let col_sq: Vec<f64> = xc.columns().into_iter().map(|c| c.dot(&c)).collect();
80 let n_alpha = n as f64 * self.alpha;
81 let mut w = Array1::<f64>::zeros(p);
82
83 let mut converged = p == 0;
84 for _ in 0..self.max_iter {
85 if converged {
86 break;
87 }
88 let mut max_change = 0.0_f64;
89 let mut max_w = 0.0_f64;
90 for j in 0..p {
91 if col_sq[j] == 0.0 {
92 continue;
93 }
94 let col = xc.column(j);
95 let old = w[j];
96 let rho = col.dot(&resid) + old * col_sq[j];
97 let new = soft_threshold(rho, n_alpha) / col_sq[j];
98 if new != old {
99 resid.scaled_add(old - new, &col);
100 w[j] = new;
101 }
102 max_change = max_change.max((new - old).abs());
103 max_w = max_w.max(new.abs());
104 }
105 converged = max_w == 0.0 || max_change <= self.tol * max_w;
106 }
107 if !converged {
108 return Err(DmlError::Convergence(format!(
109 "Lasso did not converge in {} sweeps",
110 self.max_iter
111 )));
112 }
113 self.intercept = y_mean - x_mean.dot(&w);
114 self.coef = Some(w);
115 Ok(())
116 }
117}
118
119impl Regressor for Lasso {
120 fn fit(&mut self, x: ArrayView2<'_, f64>, y: ArrayView1<'_, f64>) -> Result<()> {
121 self.fit_impl(x, y, None)
122 }
123
124 fn fit_weighted(
127 &mut self,
128 x: ArrayView2<'_, f64>,
129 y: ArrayView1<'_, f64>,
130 w: ArrayView1<'_, f64>,
131 ) -> Result<()> {
132 self.fit_impl(x, y, Some(w))
133 }
134
135 fn predict(&self, x: ArrayView2<'_, f64>) -> Result<Array1<f64>> {
136 let coef = self.coef.as_ref().ok_or(DmlError::NotFitted)?;
137 if x.ncols() != coef.len() {
138 return Err(DmlError::DimensionMismatch(
139 "Lasso::predict feature count".into(),
140 ));
141 }
142 Ok(x.dot(coef) + self.intercept)
143 }
144
145 fn fresh(&self) -> Box<dyn Regressor> {
146 Box::new(Self::new(self.alpha).tol(self.tol).max_iter(self.max_iter))
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::learner::Ols;
154 use ndarray::array;
155
156 fn data() -> (Array2<f64>, Array1<f64>) {
157 let x = Array2::from_shape_fn((40, 3), |(i, j)| {
158 (((i + 1) * (j + 2) * 7) % 13) as f64 - 6.0
159 });
160 let y = x
161 .rows()
162 .into_iter()
163 .map(|r| 2.0 + 1.5 * r[0] - 0.2 * r[2])
164 .collect();
165 (x, y)
166 }
167
168 #[test]
169 fn zero_alpha_equals_ols() {
170 let (x, y) = data();
171 let mut lasso = Lasso::new(0.0);
172 lasso.fit(x.view(), y.view()).unwrap();
173 let mut ols = Ols::new();
174 ols.fit(x.view(), y.view()).unwrap();
175 let diff = lasso.coef().unwrap() - ols.coef().unwrap();
176 assert!(diff.iter().all(|v| v.abs() < 1e-8));
177 }
178
179 #[test]
180 fn satisfies_kkt_conditions() {
181 let (x, y) = data();
182 let alpha = 0.5;
183 let mut m = Lasso::new(alpha);
184 m.fit(x.view(), y.view()).unwrap();
185 let resid = &y - &m.predict(x.view()).unwrap();
186 let n = x.nrows() as f64;
187 for (j, &wj) in m.coef().unwrap().iter().enumerate() {
188 let g = x.column(j).dot(&resid) / n;
189 if wj != 0.0 {
190 assert!((g - alpha * wj.signum()).abs() < 1e-8, "active {j}: {g}");
191 } else {
192 assert!(g.abs() <= alpha + 1e-8, "inactive {j}: {g}");
193 }
194 }
195 }
196
197 #[test]
198 fn large_alpha_gives_intercept_only() {
199 let (x, y) = data();
200 let mut m = Lasso::new(1e6);
201 m.fit(x.view(), y.view()).unwrap();
202 assert!(m.coef().unwrap().iter().all(|&v| v == 0.0));
203 assert!((m.intercept() - y.mean().unwrap()).abs() < 1e-12);
204 assert_eq!(
205 m.predict(array![[0.0, 0.0, 0.0]].view()).unwrap()[0],
206 m.intercept()
207 );
208 }
209}