1use scirs2_core::ndarray::Array2;
7use scirs2_core::random::thread_rng;
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10use sklears_core::{
11 error::{Result, SklearsError},
12 traits::{Estimator, Fit, Trained, Transform, Untrained},
13 types::Float,
14};
15
16#[derive(Debug, Clone, Copy)]
18#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
19pub enum DictionaryTransformAlgorithm {
20 OMP,
22 LARS,
24 CoordinateDescent,
26 Threshold,
28}
29
30#[derive(Debug, Clone)]
32#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
33pub struct DictionaryLearningConfig {
34 pub n_components: usize,
36 pub max_iter: usize,
38 pub tol: Float,
40 pub transform_algorithm: DictionaryTransformAlgorithm,
42 pub alpha: Float,
44 pub random_state: Option<u64>,
46}
47
48impl Default for DictionaryLearningConfig {
49 fn default() -> Self {
50 Self {
51 n_components: 100,
52 max_iter: 1000,
53 tol: 1e-8,
54 transform_algorithm: DictionaryTransformAlgorithm::OMP,
55 alpha: 1.0,
56 random_state: None,
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
63#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
64pub struct DictionaryLearning<State = Untrained> {
65 config: DictionaryLearningConfig,
66 state: std::marker::PhantomData<State>,
67 components_: Option<Array2<Float>>,
69 n_iter_: Option<usize>,
71}
72
73impl DictionaryLearning<Untrained> {
74 pub fn new(config: DictionaryLearningConfig) -> Self {
76 Self {
77 config,
78 state: std::marker::PhantomData,
79 components_: None,
80 n_iter_: None,
81 }
82 }
83
84 pub fn builder() -> DictionaryLearningBuilder {
86 DictionaryLearningBuilder::default()
87 }
88}
89
90impl DictionaryLearning<Trained> {
91 pub fn components(&self) -> &Array2<Float> {
93 self.components_.as_ref().expect("Model is trained")
94 }
95
96 pub fn n_iter(&self) -> usize {
98 self.n_iter_.expect("Model is trained")
99 }
100}
101
102impl Estimator for DictionaryLearning<Untrained> {
103 type Config = DictionaryLearningConfig;
104 type Error = SklearsError;
105 type Float = Float;
106
107 fn config(&self) -> &Self::Config {
108 &self.config
109 }
110}
111
112impl Fit<Array2<Float>, ()> for DictionaryLearning<Untrained> {
113 type Fitted = DictionaryLearning<Trained>;
114
115 fn fit(self, x: &Array2<Float>, _y: &()) -> Result<Self::Fitted> {
116 let (_n_samples, n_features) = x.dim();
117
118 if self.config.n_components > n_features {
119 return Err(SklearsError::InvalidInput(
120 "n_components cannot be larger than n_features".to_string(),
121 ));
122 }
123
124 let mut rng = thread_rng();
126 let mut components = Array2::zeros((self.config.n_components, n_features));
127
128 for i in 0..self.config.n_components {
129 for j in 0..n_features {
130 components[[i, j]] = rng.random::<Float>() - 0.5;
131 }
132 }
133
134 for mut row in components.rows_mut() {
136 let norm = row.mapv(|x| x * x).sum().sqrt();
137 if norm > 1e-10 {
138 row.mapv_inplace(|x| x / norm);
139 }
140 }
141
142 use super::omp_algorithms::{OMPConfig, OMPEncoder};
145
146 let n_samples = x.nrows();
147 let n_atoms = self.config.n_components;
148
149 let omp_config = OMPConfig {
151 n_nonzero_coefs: Some((self.config.alpha * n_atoms as Float).max(1.0) as usize),
152 tol: Some(self.config.tol * 0.1), };
154
155 let mut n_iter = 0;
156 let mut prev_error = Float::INFINITY;
157
158 for iter in 0..self.config.max_iter {
159 n_iter = iter + 1;
160
161 let mut codes = Array2::zeros((n_samples, n_atoms));
163 let encoder = OMPEncoder::new(omp_config.clone());
164 let dict_transposed = components.t().to_owned();
165
166 for i in 0..n_samples {
167 let signal = x.row(i).to_owned();
168 if let Ok(result) = encoder.encode(&dict_transposed, &signal) {
169 for j in 0..n_atoms {
170 codes[[i, j]] = result.coefficients[j];
171 }
172 }
173 }
174
175 for atom_idx in 0..n_atoms {
180 let mut residual = x.clone();
182 for i in 0..n_samples {
183 for k in 0..n_atoms {
184 if k != atom_idx {
185 let coef = codes[[i, k]];
186 for j in 0..n_features {
187 residual[[i, j]] -= coef * components[[k, j]];
188 }
189 }
190 }
191 }
192
193 let codes_col_norm_sq: Float = (0..n_samples)
195 .map(|i| codes[[i, atom_idx]] * codes[[i, atom_idx]])
196 .sum();
197
198 if codes_col_norm_sq > 1e-10 {
199 for j in 0..n_features {
200 let mut numerator = 0.0;
201 for i in 0..n_samples {
202 numerator += residual[[i, j]] * codes[[i, atom_idx]];
203 }
204 components[[atom_idx, j]] = numerator / codes_col_norm_sq;
205 }
206
207 let atom_norm: Float = (0..n_features)
209 .map(|j| components[[atom_idx, j]] * components[[atom_idx, j]])
210 .sum::<Float>()
211 .sqrt();
212
213 if atom_norm > 1e-10 {
214 for j in 0..n_features {
215 components[[atom_idx, j]] /= atom_norm;
216 }
217 }
218 }
219 }
220
221 let mut reconstruction_error = 0.0;
223 for i in 0..n_samples {
224 for j in 0..n_features {
225 let mut pred = 0.0;
226 for k in 0..n_atoms {
227 pred += codes[[i, k]] * components[[k, j]];
228 }
229 let diff = x[[i, j]] - pred;
230 reconstruction_error += diff * diff;
231 }
232 }
233 reconstruction_error =
234 (reconstruction_error / (n_samples * n_features) as Float).sqrt();
235
236 if (prev_error - reconstruction_error).abs() < self.config.tol {
238 break;
239 }
240 prev_error = reconstruction_error;
241 }
242
243 Ok(DictionaryLearning {
244 config: self.config,
245 state: std::marker::PhantomData,
246 components_: Some(components),
247 n_iter_: Some(n_iter),
248 })
249 }
250}
251
252impl Transform<Array2<Float>, Array2<Float>> for DictionaryLearning<Trained> {
253 fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
254 use super::omp_algorithms::{OMPConfig, OMPEncoder};
255
256 let (n_samples, n_features) = x.dim();
257 let components = self.components();
258 let (n_atoms, dict_features) = components.dim();
259
260 if n_features != dict_features {
262 return Err(SklearsError::InvalidInput(format!(
263 "Input features {} don't match dictionary features {}",
264 n_features, dict_features
265 )));
266 }
267
268 let omp_config = match self.config.transform_algorithm {
270 DictionaryTransformAlgorithm::OMP => OMPConfig {
271 n_nonzero_coefs: Some((self.config.alpha * n_atoms as Float) as usize),
272 tol: Some(self.config.tol),
273 },
274 _ => {
275 OMPConfig {
277 n_nonzero_coefs: None,
278 tol: Some(self.config.tol),
279 }
280 }
281 };
282
283 let encoder = OMPEncoder::new(omp_config);
284 let mut codes = Array2::zeros((n_samples, n_atoms));
285
286 let dict_transposed = components.t().to_owned();
290
291 for i in 0..n_samples {
292 let signal = x.row(i).to_owned();
293 let result = encoder.encode(&dict_transposed, &signal)?;
294
295 for j in 0..n_atoms {
297 codes[[i, j]] = result.coefficients[j];
298 }
299 }
300
301 Ok(codes)
302 }
303}
304
305#[derive(Debug, Clone, Default)]
307pub struct DictionaryLearningBuilder {
308 config: DictionaryLearningConfig,
309}
310
311impl DictionaryLearningBuilder {
312 pub fn n_components(mut self, n_components: usize) -> Self {
313 self.config.n_components = n_components;
314 self
315 }
316
317 pub fn max_iter(mut self, max_iter: usize) -> Self {
318 self.config.max_iter = max_iter;
319 self
320 }
321
322 pub fn tol(mut self, tol: Float) -> Self {
323 self.config.tol = tol;
324 self
325 }
326
327 pub fn transform_algorithm(mut self, algorithm: DictionaryTransformAlgorithm) -> Self {
328 self.config.transform_algorithm = algorithm;
329 self
330 }
331
332 pub fn alpha(mut self, alpha: Float) -> Self {
333 self.config.alpha = alpha;
334 self
335 }
336
337 pub fn random_state(mut self, random_state: Option<u64>) -> Self {
338 self.config.random_state = random_state;
339 self
340 }
341
342 pub fn build(self) -> DictionaryLearning<Untrained> {
343 DictionaryLearning::new(self.config)
344 }
345}
346
347pub type TrainedDictionaryLearning = DictionaryLearning<Trained>;
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn test_dictionary_learning_transform() {
356 let x = Array2::from_shape_vec(
358 (10, 5),
359 vec![
360 1.0, 2.0, 3.0, 4.0, 5.0, 2.0, 3.0, 4.0, 5.0, 6.0, 3.0, 4.0, 5.0, 6.0, 7.0, 1.5,
361 2.5, 3.5, 4.5, 5.5, 2.5, 3.5, 4.5, 5.5, 6.5, 1.2, 2.2, 3.2, 4.2, 5.2, 2.2, 3.2,
362 4.2, 5.2, 6.2, 1.8, 2.8, 3.8, 4.8, 5.8, 2.8, 3.8, 4.8, 5.8, 6.8, 1.1, 2.1, 3.1,
363 4.1, 5.1,
364 ],
365 )
366 .expect("operation should succeed");
367
368 let config = DictionaryLearningConfig {
370 n_components: 3,
371 max_iter: 50,
372 tol: 1e-4,
373 transform_algorithm: DictionaryTransformAlgorithm::OMP,
374 alpha: 0.5,
375 random_state: Some(42),
376 };
377
378 let model = DictionaryLearning::new(config);
379 let fitted_model = model.fit(&x, &()).expect("model fitting should succeed");
380
381 let codes = fitted_model
383 .transform(&x)
384 .expect("transformation should succeed");
385
386 assert_eq!(codes.nrows(), 10); assert_eq!(codes.ncols(), 3); let sum: Float = codes.iter().map(|&x| x.abs()).sum();
392 assert!(sum > 0.0, "Codes should not be all zeros");
393 }
394
395 #[test]
396 fn test_dictionary_learning_dimension_validation() {
397 let x_train = Array2::from_shape_vec((5, 3), vec![1.0; 15])
399 .expect("shape and data length should match");
400 let x_test = Array2::from_shape_vec((3, 4), vec![1.0; 12])
401 .expect("shape and data length should match");
402
403 let config = DictionaryLearningConfig {
404 n_components: 2, ..Default::default()
406 };
407 let model = DictionaryLearning::new(config);
408 let fitted_model = model
409 .fit(&x_train, &())
410 .expect("model fitting should succeed");
411
412 let result = fitted_model.transform(&x_test);
414 assert!(result.is_err());
415 }
416
417 #[test]
418 fn test_dictionary_learning_builder() {
419 let model = DictionaryLearning::builder()
420 .n_components(5)
421 .max_iter(100)
422 .tol(1e-5)
423 .alpha(0.3)
424 .random_state(Some(123))
425 .build();
426
427 assert_eq!(model.config.n_components, 5);
428 assert_eq!(model.config.max_iter, 100);
429 assert_eq!(model.config.tol, 1e-5);
430 assert_eq!(model.config.alpha, 0.3);
431 assert_eq!(model.config.random_state, Some(123));
432 }
433
434 #[test]
435 fn test_dictionary_learning_convergence() {
436 let x = Array2::from_shape_vec(
438 (20, 8),
439 vec![
440 5.0, 5.0, 5.0, 5.0, 1.0, 1.0, 1.0, 1.0, 5.5, 5.5, 5.5, 5.5, 0.5, 0.5, 0.5, 0.5, 4.5,
442 4.5, 4.5, 4.5, 1.5, 1.5, 1.5, 1.5, 5.2, 5.2, 5.2, 5.2, 0.8, 0.8, 0.8, 0.8, 4.8,
443 4.8, 4.8, 4.8, 1.2, 1.2, 1.2, 1.2,
444 1.0, 1.0, 1.0, 1.0, 5.0, 5.0, 5.0, 5.0, 0.5, 0.5, 0.5, 0.5, 5.5, 5.5, 5.5, 5.5, 1.5,
446 1.5, 1.5, 1.5, 4.5, 4.5, 4.5, 4.5, 0.8, 0.8, 0.8, 0.8, 5.2, 5.2, 5.2, 5.2, 1.2,
447 1.2, 1.2, 1.2, 4.8, 4.8, 4.8, 4.8, 5.0, 1.0, 5.0, 1.0, 5.0, 1.0, 5.0, 1.0, 5.5, 0.5, 5.5, 0.5, 5.5, 0.5, 5.5, 0.5,
449 4.5, 1.5, 4.5, 1.5, 4.5, 1.5, 4.5, 1.5, 5.2, 0.8, 5.2, 0.8, 5.2, 0.8, 5.2, 0.8,
450 4.8, 1.2, 4.8, 1.2, 4.8, 1.2, 4.8, 1.2, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.5, 2.5, 3.5, 2.5, 3.5, 2.5, 3.5, 2.5,
452 2.5, 3.5, 2.5, 3.5, 2.5, 3.5, 2.5, 3.5, 4.0, 2.0, 3.0, 3.0, 2.0, 4.0, 3.0, 3.0,
453 2.0, 4.0, 3.0, 3.0, 4.0, 2.0, 3.0, 3.0,
454 ],
455 )
456 .expect("operation should succeed");
457
458 let config = DictionaryLearningConfig {
460 n_components: 4,
461 max_iter: 30,
462 tol: 1e-3,
463 transform_algorithm: DictionaryTransformAlgorithm::OMP,
464 alpha: 0.3,
465 random_state: Some(42),
466 };
467
468 let model = DictionaryLearning::new(config);
469 let fitted_model = model.fit(&x, &()).expect("model fitting should succeed");
470
471 let components = fitted_model.components();
473 assert_eq!(components.nrows(), 4);
474 assert_eq!(components.ncols(), 8);
475
476 for atom_idx in 0..4 {
478 let norm: Float = (0..8)
479 .map(|j| components[[atom_idx, j]] * components[[atom_idx, j]])
480 .sum::<Float>()
481 .sqrt();
482 assert!(
483 (norm - 1.0).abs() < 1e-6,
484 "Atom {} not normalized: {}",
485 atom_idx,
486 norm
487 );
488 }
489
490 let codes = fitted_model
492 .transform(&x)
493 .expect("transformation should succeed");
494 assert_eq!(codes.nrows(), 20);
495 assert_eq!(codes.ncols(), 4);
496
497 let mut nonzero_count = 0;
499 for i in 0..codes.nrows() {
500 for j in 0..codes.ncols() {
501 if codes[[i, j]].abs() > 1e-10 {
502 nonzero_count += 1;
503 }
504 }
505 }
506 let sparsity_ratio = nonzero_count as f64 / (codes.nrows() * codes.ncols()) as f64;
507 assert!(
508 sparsity_ratio < 0.5,
509 "Codes should be sparse, but {}% are nonzero",
510 sparsity_ratio * 100.0
511 );
512
513 let mut reconstruction_error = 0.0;
515 for i in 0..x.nrows() {
516 for j in 0..x.ncols() {
517 let mut reconstruction = 0.0;
518 for k in 0..components.nrows() {
519 reconstruction += codes[[i, k]] * components[[k, j]];
520 }
521 let diff = x[[i, j]] - reconstruction;
522 reconstruction_error += diff * diff;
523 }
524 }
525 reconstruction_error = (reconstruction_error / (x.nrows() * x.ncols()) as Float).sqrt();
526
527 assert!(
529 reconstruction_error < 3.0,
530 "Reconstruction error too high: {}",
531 reconstruction_error
532 );
533 }
534}