Skip to main content

solow_decomposition/
minibatch_dict.rs

1//! MiniBatchDictionaryLearning — online update of the (D, α) pair on
2//! random mini-batches (Mairal-Bach-Ponce-Sapiro 2010).
3
4use ndarray::{Array2, ArrayView2};
5use solow_core::{Error, Result};
6
7/// Fitted MiniBatchDictionaryLearning.
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[derive(Clone, Debug, PartialEq)]
10pub struct MiniBatchDictionaryLearning {
11    /// Dictionary `(n_components × d)`.
12    pub components: Array2<f64>,
13    /// Kept rank.
14    pub n_components: usize,
15    /// L1 penalty α used.
16    pub alpha: f64,
17    /// Batch size.
18    pub batch_size: usize,
19    /// Iterations run.
20    pub n_iter: usize,
21}
22
23impl MiniBatchDictionaryLearning {
24    /// Fit with the reference defaults `alpha = 1.0`, `batch_size = 3`,
25    /// `max_iter = 100`, `tol = 1e-3`.
26    pub fn fit(x: ArrayView2<'_, f64>, n_components: usize) -> Result<Self> {
27        Self::fit_with(x, n_components, 1.0, 3, 100, 1e-3, 0)
28    }
29
30    /// Full-configuration fit.
31    pub fn fit_with(
32        x: ArrayView2<'_, f64>,
33        n_components: usize,
34        alpha: f64,
35        batch_size: usize,
36        max_iter: usize,
37        tol: f64,
38        seed: u64,
39    ) -> Result<Self> {
40        let n = x.nrows();
41        let d = x.ncols();
42        if n_components == 0 {
43            return Err(Error::Value("MiniBatchDictionaryLearning: n_components must be ≥ 1".into()));
44        }
45        if alpha < 0.0 {
46            return Err(Error::Value("MiniBatchDictionaryLearning: alpha must be ≥ 0".into()));
47        }
48        // Init from the first `n_components` rows, normalised.
49        let mut dict = Array2::<f64>::zeros((n_components, d));
50        for k in 0..n_components.min(n) {
51            for j in 0..d {
52                dict[[k, j]] = x[[k, j]];
53            }
54            let nrm = (0..d).map(|j| dict[[k, j]] * dict[[k, j]]).sum::<f64>().sqrt().max(1e-30);
55            for j in 0..d {
56                dict[[k, j]] /= nrm;
57            }
58        }
59        let mut a_stat = Array2::<f64>::zeros((n_components, n_components));
60        let mut b_stat = Array2::<f64>::zeros((n_components, d));
61        let mut state = seed.wrapping_add(0xF00D_C0DE);
62        let mut iters = 0_usize;
63        let mut prev_dict = dict.clone();
64        for it in 0..max_iter {
65            iters = it + 1;
66            let batch = batch_size.min(n).max(1);
67            for _ in 0..batch {
68                let i = uniform_index(&mut state, n as u64);
69                // Sparse code x_i under current dictionary via coordinate-descent LASSO.
70                let alpha_i = sparse_code(x.row(i), &dict, alpha);
71                // Update running statistics A += αα^T, B += x α^T.
72                for k in 0..n_components {
73                    for l in 0..n_components {
74                        a_stat[[k, l]] += alpha_i[k] * alpha_i[l];
75                    }
76                    for j in 0..d {
77                        b_stat[[k, j]] += x[[i, j]] * alpha_i[k];
78                    }
79                }
80            }
81            // Dictionary update — block coordinate descent on rows.
82            for k in 0..n_components {
83                let akk = a_stat[[k, k]].max(1e-30);
84                let mut u = vec![0.0_f64; d];
85                for j in 0..d {
86                    let mut acc = b_stat[[k, j]];
87                    for l in 0..n_components {
88                        if l == k {
89                            continue;
90                        }
91                        acc -= a_stat[[l, k]] * dict[[l, j]];
92                    }
93                    u[j] = dict[[k, j]] + (acc - dict[[k, j]] * akk) / akk;
94                }
95                let nrm = u.iter().map(|v| v * v).sum::<f64>().sqrt().max(1e-30);
96                for j in 0..d {
97                    dict[[k, j]] = u[j] / nrm.max(1.0);
98                }
99            }
100            let mut delta = 0.0_f64;
101            for k in 0..n_components {
102                for j in 0..d {
103                    delta += (dict[[k, j]] - prev_dict[[k, j]]).powi(2);
104                }
105            }
106            prev_dict = dict.clone();
107            if delta.sqrt() < tol {
108                break;
109            }
110        }
111        Ok(Self {
112            components: dict,
113            n_components,
114            alpha,
115            batch_size,
116            n_iter: iters,
117        })
118    }
119}
120
121fn sparse_code(x: ndarray::ArrayView1<'_, f64>, dict: &Array2<f64>, alpha: f64) -> Vec<f64> {
122    let n_components = dict.nrows();
123    let d = dict.ncols();
124    let mut a = vec![0.0_f64; n_components];
125    for _ in 0..50 {
126        let mut delta = 0.0_f64;
127        for k in 0..n_components {
128            let mut num = 0.0_f64;
129            let mut denom = 0.0_f64;
130            for j in 0..d {
131                let mut resid = x[j];
132                for l in 0..n_components {
133                    if l == k {
134                        continue;
135                    }
136                    resid -= a[l] * dict[[l, j]];
137                }
138                num += dict[[k, j]] * resid;
139                denom += dict[[k, j]] * dict[[k, j]];
140            }
141            let z = if denom > 1e-30 {
142                soft_threshold(num, alpha) / denom
143            } else {
144                0.0
145            };
146            let d_val = (z - a[k]).abs();
147            if d_val > delta {
148                delta = d_val;
149            }
150            a[k] = z;
151        }
152        if delta < 1e-6 {
153            break;
154        }
155    }
156    a
157}
158
159fn soft_threshold(z: f64, alpha: f64) -> f64 {
160    if z > alpha {
161        z - alpha
162    } else if z < -alpha {
163        z + alpha
164    } else {
165        0.0
166    }
167}
168
169fn uniform_index(state: &mut u64, n: u64) -> usize {
170    *state = state
171        .wrapping_mul(6_364_136_223_846_793_005)
172        .wrapping_add(1_442_695_040_888_963_407);
173    let max = u64::MAX - (u64::MAX % n);
174    if *state < max {
175        (*state % n) as usize
176    } else {
177        (state.wrapping_mul(3) % n) as usize
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use ndarray::array;
185
186    #[test]
187    fn minibatch_dict_learning_returns_dict_of_the_right_shape() {
188        let x = array![
189            [1.0_f64, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0],
190            [1.0, 1.0, 0.0], [0.0, 1.0, 1.0]
191        ];
192        let m = MiniBatchDictionaryLearning::fit_with(x.view(), 3, 0.1, 3, 20, 1e-4, 42).unwrap();
193        assert_eq!(m.components.shape(), &[3, 3]);
194    }
195}