1use ndarray::{Array2, ArrayView2};
5use solow_core::{Error, Result};
6
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[derive(Clone, Debug, PartialEq)]
10pub struct MiniBatchDictionaryLearning {
11 pub components: Array2<f64>,
13 pub n_components: usize,
15 pub alpha: f64,
17 pub batch_size: usize,
19 pub n_iter: usize,
21}
22
23impl MiniBatchDictionaryLearning {
24 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 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 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 let alpha_i = sparse_code(x.row(i), &dict, alpha);
71 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 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}