solow_decomposition/
dictionary_learning.rs1use ndarray::{Array1, Array2, ArrayView2};
8use solow_core::{Error, Result};
9
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12#[derive(Clone, Debug, PartialEq)]
13pub struct DictionaryLearning {
14 pub components: Array2<f64>,
16 pub codes: Array2<f64>,
18 pub n_components: usize,
20 pub alpha: f64,
22 pub n_iter: usize,
24}
25
26impl DictionaryLearning {
27 pub fn fit(x: ArrayView2<'_, f64>, n_components: usize) -> Result<Self> {
29 Self::fit_with(x, n_components, 1.0, 100, 1e-6)
30 }
31
32 pub fn fit_with(
34 x: ArrayView2<'_, f64>,
35 n_components: usize,
36 alpha: f64,
37 max_iter: usize,
38 tol: f64,
39 ) -> Result<Self> {
40 let n = x.nrows();
41 let d = x.ncols();
42 if n_components == 0 {
43 return Err(Error::Value("DictionaryLearning: n_components must be ≥ 1".into()));
44 }
45 if alpha < 0.0 {
46 return Err(Error::Value("DictionaryLearning: 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 }
55 for k in 0..n_components {
56 let mut nrm = 0.0_f64;
57 for j in 0..d {
58 nrm += dict[[k, j]] * dict[[k, j]];
59 }
60 let nrm = nrm.sqrt().max(1e-30);
61 for j in 0..d {
62 dict[[k, j]] /= nrm;
63 }
64 }
65 let mut codes = Array2::<f64>::zeros((n, n_components));
66 let mut iters = 0_usize;
67 for it in 0..max_iter {
68 iters = it + 1;
69 for i in 0..n {
71 for _ in 0..50 {
72 let mut delta = 0.0_f64;
73 for k in 0..n_components {
74 let mut num = 0.0_f64;
77 for j in 0..d {
78 let mut recon = 0.0_f64;
79 for kk in 0..n_components {
80 if kk == k {
81 continue;
82 }
83 recon += codes[[i, kk]] * dict[[kk, j]];
84 }
85 num += dict[[k, j]] * (x[[i, j]] - recon);
86 }
87 let new = soft_threshold(num, alpha);
89 let dd = (new - codes[[i, k]]).abs();
90 if dd > delta {
91 delta = dd;
92 }
93 codes[[i, k]] = new;
94 }
95 if delta < tol {
96 break;
97 }
98 }
99 }
100 let mut new_dict = Array2::<f64>::zeros((n_components, d));
102 for k in 0..n_components {
103 let mut num = Array1::<f64>::zeros(d);
104 let mut denom = 0.0_f64;
105 for i in 0..n {
106 denom += codes[[i, k]] * codes[[i, k]];
107 for j in 0..d {
108 let mut recon = 0.0_f64;
110 for kk in 0..n_components {
111 recon += codes[[i, kk]] * dict[[kk, j]];
112 }
113 num[j] += codes[[i, k]] * (x[[i, j]] - recon + codes[[i, k]] * dict[[k, j]]);
114 }
115 }
116 if denom > 1e-30 {
117 let mut nrm2 = 0.0_f64;
118 for j in 0..d {
119 new_dict[[k, j]] = num[j] / denom;
120 nrm2 += new_dict[[k, j]] * new_dict[[k, j]];
121 }
122 let nrm = nrm2.sqrt().max(1e-30);
123 for j in 0..d {
124 new_dict[[k, j]] /= nrm;
125 }
126 } else {
127 for j in 0..d {
128 new_dict[[k, j]] = dict[[k, j]];
129 }
130 }
131 }
132 let mut delta = 0.0_f64;
133 for k in 0..n_components {
134 for j in 0..d {
135 let dd = new_dict[[k, j]] - dict[[k, j]];
136 delta += dd * dd;
137 }
138 }
139 dict = new_dict;
140 if delta.sqrt() < tol {
141 break;
142 }
143 }
144 Ok(Self {
145 components: dict,
146 codes,
147 n_components,
148 alpha,
149 n_iter: iters,
150 })
151 }
152}
153
154fn soft_threshold(z: f64, alpha: f64) -> f64 {
155 if z > alpha {
156 z - alpha
157 } else if z < -alpha {
158 z + alpha
159 } else {
160 0.0
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use ndarray::array;
168
169 #[test]
170 fn dict_learning_returns_dict_of_the_right_shape() {
171 let x = array![
172 [1.0_f64, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0],
173 [1.0, 1.0, 0.0], [0.0, 1.0, 1.0]
174 ];
175 let m = DictionaryLearning::fit_with(x.view(), 3, 0.1, 20, 1e-4).unwrap();
176 assert_eq!(m.components.shape(), &[3, 3]);
177 assert_eq!(m.codes.shape(), &[5, 3]);
178 }
179}