1use scirs2_core::ndarray::Array2;
8use scirs2_core::random::rngs::StdRng;
9use scirs2_core::random::{thread_rng, RngExt, SeedableRng};
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12use sklears_core::{
13 error::{Result, SklearsError},
14 traits::{Fit, Transform, Untrained},
15};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
20pub enum NMFSolver {
21 MultiplicativeUpdate,
23 #[default]
25 CoordinateDescent,
26 ALS,
28 SemiNMF,
30 OnlineNMF,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
37pub enum NMFInit {
38 #[default]
40 Random,
41 Nndsvd,
43 NndsvdA,
45 NndsvdAr,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
52pub enum NMFRegularization {
53 #[default]
55 None,
56 L1,
58 L2,
60 Both,
62}
63
64#[derive(Debug, Clone)]
66pub struct NMF<State = Untrained> {
67 pub n_components: usize,
69 pub init: NMFInit,
71 pub solver: NMFSolver,
73 pub regularization: NMFRegularization,
75 pub alpha_w: f64,
77 pub alpha_h: f64,
79 pub l1_ratio: f64,
81 pub max_iter: usize,
83 pub tol: f64,
85 pub random_state: Option<u64>,
87
88 state: State,
90}
91
92#[derive(Debug, Clone)]
94#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
95pub struct TrainedNMF {
96 pub components: Array2<f64>,
98 pub w_matrix: Array2<f64>,
100 pub n_features_in: usize,
102 pub n_components: usize,
104 pub n_iter: usize,
106 pub reconstruction_err: f64,
108}
109
110impl NMF<Untrained> {
111 pub fn new(n_components: usize) -> Self {
113 Self {
114 n_components,
115 init: NMFInit::Random,
116 solver: NMFSolver::CoordinateDescent,
117 regularization: NMFRegularization::None,
118 alpha_w: 0.0,
119 alpha_h: 0.0,
120 l1_ratio: 0.0,
121 max_iter: 200,
122 tol: 1e-4,
123 random_state: None,
124 state: Untrained,
125 }
126 }
127
128 pub fn init(mut self, init: NMFInit) -> Self {
130 self.init = init;
131 self
132 }
133
134 pub fn solver(mut self, solver: NMFSolver) -> Self {
136 self.solver = solver;
137 self
138 }
139
140 pub fn regularization(mut self, regularization: NMFRegularization, alpha: f64) -> Self {
142 self.regularization = regularization;
143 match regularization {
144 NMFRegularization::L1 => {
145 self.alpha_w = alpha;
146 self.alpha_h = alpha;
147 }
148 NMFRegularization::L2 => {
149 self.alpha_w = alpha;
150 self.alpha_h = alpha;
151 }
152 NMFRegularization::Both => {
153 self.alpha_w = alpha;
154 self.alpha_h = alpha;
155 }
156 NMFRegularization::None => {}
157 }
158 self
159 }
160
161 pub fn l1_ratio(mut self, l1_ratio: f64) -> Self {
163 self.l1_ratio = l1_ratio;
164 self
165 }
166
167 pub fn max_iter(mut self, max_iter: usize) -> Self {
169 self.max_iter = max_iter;
170 self
171 }
172
173 pub fn tol(mut self, tol: f64) -> Self {
175 self.tol = tol;
176 self
177 }
178
179 pub fn random_state(mut self, random_state: u64) -> Self {
181 self.random_state = Some(random_state);
182 self
183 }
184}
185
186impl Fit<Array2<f64>, ()> for NMF<Untrained> {
187 type Fitted = NMF<TrainedNMF>;
188
189 fn fit(self, x: &Array2<f64>, _y: &()) -> Result<Self::Fitted> {
190 let (n_samples, n_features) = x.dim();
191
192 if self.solver != NMFSolver::SemiNMF {
194 for &val in x.iter() {
195 if val < 0.0 {
196 return Err(SklearsError::InvalidInput(
197 "NMF requires non-negative data (use Semi-NMF for mixed-sign data)"
198 .to_string(),
199 ));
200 }
201 }
202 }
203
204 if self.n_components > n_features.min(n_samples) {
205 return Err(SklearsError::InvalidInput(
206 "n_components cannot be larger than min(n_samples, n_features)".to_string(),
207 ));
208 }
209
210 let mut rng = if let Some(seed) = self.random_state {
212 StdRng::seed_from_u64(seed)
213 } else {
214 StdRng::from_rng(&mut thread_rng())
215 };
216
217 let (mut w, mut h) = self.initialize_matrices(x, &mut rng)?;
219
220 let (n_iter, reconstruction_err) = match self.solver {
222 NMFSolver::MultiplicativeUpdate => self.multiplicative_update(x, &mut w, &mut h)?,
223 NMFSolver::CoordinateDescent => self.coordinate_descent(x, &mut w, &mut h)?,
224 NMFSolver::ALS => self.alternating_least_squares(x, &mut w, &mut h)?,
225 NMFSolver::SemiNMF => self.semi_nmf_update(x, &mut w, &mut h)?,
226 NMFSolver::OnlineNMF => self.online_nmf_update(x, &mut w, &mut h)?,
227 };
228
229 Ok(NMF {
230 n_components: self.n_components,
231 init: self.init,
232 solver: self.solver,
233 regularization: self.regularization,
234 alpha_w: self.alpha_w,
235 alpha_h: self.alpha_h,
236 l1_ratio: self.l1_ratio,
237 max_iter: self.max_iter,
238 tol: self.tol,
239 random_state: self.random_state,
240 state: TrainedNMF {
241 components: h,
242 w_matrix: w,
243 n_features_in: n_features,
244 n_components: self.n_components,
245 n_iter,
246 reconstruction_err,
247 },
248 })
249 }
250}
251
252impl Transform<Array2<f64>, Array2<f64>> for NMF<TrainedNMF> {
253 fn transform(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
254 let (n_samples, n_features) = x.dim();
255
256 if n_features != self.state.n_features_in {
257 return Err(SklearsError::FeatureMismatch {
258 expected: self.state.n_features_in,
259 actual: n_features,
260 });
261 }
262
263 if self.solver != NMFSolver::SemiNMF {
265 for &val in x.iter() {
266 if val < 0.0 {
267 return Err(SklearsError::InvalidInput(
268 "NMF requires non-negative data (use Semi-NMF for mixed-sign data)"
269 .to_string(),
270 ));
271 }
272 }
273 }
274
275 let mut w = Array2::from_elem((n_samples, self.state.n_components), 0.1);
278
279 for _ in 0..100 {
281 for k in 0..self.state.n_components {
283 let mut residual = x.clone();
285 for j in 0..self.state.n_components {
286 if j != k {
287 let wj = w.column(j);
288 let hj = self.state.components.row(j);
289 for i in 0..residual.nrows() {
290 for l in 0..residual.ncols() {
291 residual[[i, l]] -= wj[i] * hj[l];
292 }
293 }
294 }
295 }
296
297 let hk = self.state.components.row(k);
299 let hk_norm_sq = hk.mapv(|x| x * x).sum();
300
301 if hk_norm_sq > 1e-12 {
302 for i in 0..w.nrows() {
303 let numerator = residual.row(i).dot(&hk);
304 w[[i, k]] = (numerator / hk_norm_sq).max(0.0);
305 }
306 }
307 }
308 }
309
310 Ok(w)
311 }
312}
313
314impl NMF<Untrained> {
315 fn initialize_matrices(
317 &self,
318 x: &Array2<f64>,
319 rng: &mut impl RngExt,
320 ) -> Result<(Array2<f64>, Array2<f64>)> {
321 let (n_samples, n_features) = x.dim();
322
323 match self.init {
324 NMFInit::Random => {
325 let w = self.random_init((n_samples, self.n_components), rng);
326 let h = self.random_init((self.n_components, n_features), rng);
327 Ok((w, h))
328 }
329 NMFInit::Nndsvd | NMFInit::NndsvdA | NMFInit::NndsvdAr => self.nndsvd_init(x, rng),
330 }
331 }
332
333 fn random_init(&self, shape: (usize, usize), rng: &mut impl RngExt) -> Array2<f64> {
335 let (rows, cols) = shape;
336 let mut matrix = Array2::zeros((rows, cols));
337
338 for i in 0..rows {
339 for j in 0..cols {
340 matrix[[i, j]] = rng.random::<f64>();
341 }
342 }
343
344 matrix
345 }
346
347 fn nndsvd_init(
349 &self,
350 x: &Array2<f64>,
351 rng: &mut impl RngExt,
352 ) -> Result<(Array2<f64>, Array2<f64>)> {
353 let (n_samples, n_features) = x.dim();
354
355 let avg = x.mean().unwrap_or(0.1);
359 let mut w = Array2::from_elem((n_samples, self.n_components), avg.sqrt());
360 let mut h = Array2::from_elem((self.n_components, n_features), avg.sqrt());
361
362 match self.init {
364 NMFInit::NndsvdA => {
365 for i in 0..n_samples {
367 for j in 0..self.n_components {
368 if w[[i, j]] == 0.0 {
369 w[[i, j]] = avg;
370 }
371 }
372 }
373 for i in 0..self.n_components {
374 for j in 0..n_features {
375 if h[[i, j]] == 0.0 {
376 h[[i, j]] = avg;
377 }
378 }
379 }
380 }
381 NMFInit::NndsvdAr => {
382 for i in 0..n_samples {
384 for j in 0..self.n_components {
385 if w[[i, j]] == 0.0 {
386 w[[i, j]] = avg * rng.random::<f64>() * 0.01;
387 }
388 }
389 }
390 for i in 0..self.n_components {
391 for j in 0..n_features {
392 if h[[i, j]] == 0.0 {
393 h[[i, j]] = avg * rng.random::<f64>() * 0.01;
394 }
395 }
396 }
397 }
398 _ => {}
399 }
400
401 Ok((w, h))
402 }
403
404 fn multiplicative_update(
406 &self,
407 x: &Array2<f64>,
408 w: &mut Array2<f64>,
409 h: &mut Array2<f64>,
410 ) -> Result<(usize, f64)> {
411 let mut prev_error = f64::INFINITY;
412 let mut n_iter = 0;
413
414 for iter in 0..self.max_iter {
415 n_iter = iter + 1;
416
417 self.update_h_multiplicative_regularized(x, w, h)?;
419
420 self.update_w_multiplicative_regularized(x, w, h)?;
422
423 let reconstruction = w.dot(h);
425 let mut error = self.frobenius_norm_squared(&(x - &reconstruction));
426
427 error += self.compute_regularization_penalty(w, h);
429
430 if (prev_error - error).abs() < self.tol {
432 break;
433 }
434
435 prev_error = error;
436 }
437
438 Ok((n_iter, prev_error))
439 }
440
441 fn update_h_multiplicative_regularized(
443 &self,
444 x: &Array2<f64>,
445 w: &Array2<f64>,
446 h: &mut Array2<f64>,
447 ) -> Result<()> {
448 let numerator_h = w.t().dot(x);
449 let base_denominator_h = w.t().dot(w).dot(h);
450
451 for i in 0..h.nrows() {
452 for j in 0..h.ncols() {
453 let mut denominator = base_denominator_h[[i, j]];
454
455 match self.regularization {
457 NMFRegularization::L1 => {
458 denominator += self.alpha_h;
459 }
460 NMFRegularization::L2 => {
461 denominator += 2.0 * self.alpha_h * h[[i, j]];
462 }
463 NMFRegularization::Both => {
464 denominator += self.l1_ratio * self.alpha_h
466 + (1.0 - self.l1_ratio) * 2.0 * self.alpha_h * h[[i, j]];
467 }
468 NMFRegularization::None => {}
469 }
470
471 if denominator > 1e-12 {
472 h[[i, j]] *= numerator_h[[i, j]] / denominator;
473 h[[i, j]] = h[[i, j]].max(1e-12); }
475 }
476 }
477 Ok(())
478 }
479
480 fn update_w_multiplicative_regularized(
482 &self,
483 x: &Array2<f64>,
484 w: &mut Array2<f64>,
485 h: &Array2<f64>,
486 ) -> Result<()> {
487 let numerator_w = x.dot(&h.t());
488 let base_denominator_w = w.dot(h).dot(&h.t());
489
490 for i in 0..w.nrows() {
491 for j in 0..w.ncols() {
492 let mut denominator = base_denominator_w[[i, j]];
493
494 match self.regularization {
496 NMFRegularization::L1 => {
497 denominator += self.alpha_w;
498 }
499 NMFRegularization::L2 => {
500 denominator += 2.0 * self.alpha_w * w[[i, j]];
501 }
502 NMFRegularization::Both => {
503 denominator += self.l1_ratio * self.alpha_w
505 + (1.0 - self.l1_ratio) * 2.0 * self.alpha_w * w[[i, j]];
506 }
507 NMFRegularization::None => {}
508 }
509
510 if denominator > 1e-12 {
511 w[[i, j]] *= numerator_w[[i, j]] / denominator;
512 w[[i, j]] = w[[i, j]].max(1e-12); }
514 }
515 }
516 Ok(())
517 }
518
519 fn compute_regularization_penalty(&self, w: &Array2<f64>, h: &Array2<f64>) -> f64 {
521 let mut penalty = 0.0;
522
523 match self.regularization {
524 NMFRegularization::L1 => {
525 penalty += self.alpha_w * w.mapv(|x| x.abs()).sum();
527 penalty += self.alpha_h * h.mapv(|x| x.abs()).sum();
528 }
529 NMFRegularization::L2 => {
530 penalty += self.alpha_w * w.mapv(|x| x * x).sum();
532 penalty += self.alpha_h * h.mapv(|x| x * x).sum();
533 }
534 NMFRegularization::Both => {
535 let l1_penalty = w.mapv(|x| x.abs()).sum() + h.mapv(|x| x.abs()).sum();
537 let l2_penalty = w.mapv(|x| x * x).sum() + h.mapv(|x| x * x).sum();
538 penalty += self.l1_ratio * (self.alpha_w + self.alpha_h) * l1_penalty
539 + (1.0 - self.l1_ratio) * (self.alpha_w + self.alpha_h) * l2_penalty;
540 }
541 NMFRegularization::None => {}
542 }
543
544 penalty
545 }
546
547 fn alternating_least_squares(
552 &self,
553 x: &Array2<f64>,
554 w: &mut Array2<f64>,
555 h: &mut Array2<f64>,
556 ) -> Result<(usize, f64)> {
557 let mut prev_error = f64::INFINITY;
558 let mut n_iter = 0;
559
560 for iter in 0..self.max_iter {
561 n_iter = iter + 1;
562
563 self.update_h_als(x, w, h)?;
565
566 self.update_w_als(x, w, h)?;
568
569 let reconstruction = w.dot(h);
571 let error = self.frobenius_norm_squared(&(x - &reconstruction));
572
573 if (prev_error - error).abs() < self.tol {
575 break;
576 }
577
578 prev_error = error;
579 }
580
581 Ok((n_iter, prev_error))
582 }
583
584 fn update_h_als(&self, x: &Array2<f64>, w: &Array2<f64>, h: &mut Array2<f64>) -> Result<()> {
590 let wtw = w.t().dot(w);
592
593 let wtx = w.t().dot(x);
595
596 for j in 0..h.ncols() {
598 let mut h_col = h.column(j).to_owned();
599 let rhs = wtx.column(j);
600
601 let step_size = 0.01; for _gd_iter in 0..50 {
604 let gradient = wtw.dot(&h_col) - rhs;
607
608 h_col = &h_col - step_size * &gradient;
610
611 h_col.mapv_inplace(|x| x.max(0.0));
613 }
614
615 h.column_mut(j).assign(&h_col);
617 }
618
619 Ok(())
620 }
621
622 fn update_w_als(&self, x: &Array2<f64>, w: &mut Array2<f64>, h: &Array2<f64>) -> Result<()> {
628 let hht = h.dot(&h.t());
630
631 let xht = x.dot(&h.t());
633
634 for i in 0..w.nrows() {
636 let mut w_row = w.row(i).to_owned();
637 let rhs = xht.row(i);
638
639 let step_size = 0.01; for _gd_iter in 0..50 {
642 let gradient = w_row.dot(&hht) - rhs;
645
646 w_row = &w_row - step_size * &gradient;
648
649 w_row.mapv_inplace(|x| x.max(0.0));
651 }
652
653 w.row_mut(i).assign(&w_row);
655 }
656
657 Ok(())
658 }
659
660 fn coordinate_descent(
662 &self,
663 x: &Array2<f64>,
664 w: &mut Array2<f64>,
665 h: &mut Array2<f64>,
666 ) -> Result<(usize, f64)> {
667 let mut prev_error = f64::INFINITY;
668 let mut n_iter = 0;
669
670 for iter in 0..self.max_iter {
671 n_iter = iter + 1;
672
673 for k in 0..self.n_components {
675 let mut residual = x.clone();
677 for j in 0..self.n_components {
678 if j != k {
679 let wj = w.column(j);
680 let hj = h.row(j);
681 for i in 0..residual.nrows() {
682 for l in 0..residual.ncols() {
683 residual[[i, l]] -= wj[i] * hj[l];
684 }
685 }
686 }
687 }
688
689 let hk = h.row(k);
691 let hk_norm_sq = hk.mapv(|x| x * x).sum();
692
693 if hk_norm_sq > 1e-12 {
694 for i in 0..w.nrows() {
695 let numerator = residual.row(i).dot(&hk);
696 let raw_update = numerator / hk_norm_sq;
697
698 w[[i, k]] = self.apply_soft_thresholding_w(raw_update);
700 }
701 }
702
703 let wk = w.column(k);
705 let wk_norm_sq = wk.mapv(|x| x * x).sum();
706
707 if wk_norm_sq > 1e-12 {
708 for j in 0..h.ncols() {
709 let numerator = wk.dot(&residual.column(j));
710 let raw_update = numerator / wk_norm_sq;
711
712 h[[k, j]] = self.apply_soft_thresholding_h(raw_update);
714 }
715 }
716 }
717
718 let reconstruction = w.dot(h);
720 let mut error = self.frobenius_norm_squared(&(x - &reconstruction));
721 error += self.compute_regularization_penalty(w, h);
722
723 if (prev_error - error).abs() < self.tol {
725 break;
726 }
727
728 prev_error = error;
729 }
730
731 Ok((n_iter, prev_error))
732 }
733
734 fn apply_soft_thresholding_w(&self, value: f64) -> f64 {
736 match self.regularization {
737 NMFRegularization::L1 => {
738 let threshold = self.alpha_w;
740 if value > threshold {
741 value - threshold
742 } else if value < -threshold {
743 value + threshold
744 } else {
745 0.0
746 }
747 }
748 NMFRegularization::L2 => {
749 value / (1.0 + self.alpha_w)
751 }
752 NMFRegularization::Both => {
753 let l1_threshold = self.l1_ratio * self.alpha_w;
755 let l2_factor = 1.0 + (1.0 - self.l1_ratio) * self.alpha_w;
756
757 let soft_thresholded = if value > l1_threshold {
758 value - l1_threshold
759 } else if value < -l1_threshold {
760 value + l1_threshold
761 } else {
762 0.0
763 };
764
765 soft_thresholded / l2_factor
766 }
767 NMFRegularization::None => value,
768 }
769 .max(0.0) }
771
772 fn apply_soft_thresholding_h(&self, value: f64) -> f64 {
774 match self.regularization {
775 NMFRegularization::L1 => {
776 let threshold = self.alpha_h;
778 if value > threshold {
779 value - threshold
780 } else if value < -threshold {
781 value + threshold
782 } else {
783 0.0
784 }
785 }
786 NMFRegularization::L2 => {
787 value / (1.0 + self.alpha_h)
789 }
790 NMFRegularization::Both => {
791 let l1_threshold = self.l1_ratio * self.alpha_h;
793 let l2_factor = 1.0 + (1.0 - self.l1_ratio) * self.alpha_h;
794
795 let soft_thresholded = if value > l1_threshold {
796 value - l1_threshold
797 } else if value < -l1_threshold {
798 value + l1_threshold
799 } else {
800 0.0
801 };
802
803 soft_thresholded / l2_factor
804 }
805 NMFRegularization::None => value,
806 }
807 .max(0.0) }
809
810 fn frobenius_norm_squared(&self, matrix: &Array2<f64>) -> f64 {
812 matrix.mapv(|x| x * x).sum()
813 }
814
815 fn semi_nmf_update(
821 &self,
822 x: &Array2<f64>,
823 w: &mut Array2<f64>,
824 h: &mut Array2<f64>,
825 ) -> Result<(usize, f64)> {
826 let mut prev_error = f64::INFINITY;
827 let mut n_iter = 0;
828
829 for iter in 0..self.max_iter {
830 n_iter = iter + 1;
831
832 self.update_h_semi_nmf(x, w, h)?;
834
835 self.update_w_semi_nmf(x, w, h)?;
837
838 let reconstruction = w.dot(h);
840 let error = (x - &reconstruction).mapv(|x| x * x).sum().sqrt();
841
842 if (prev_error - error).abs() < self.tol {
844 break;
845 }
846 prev_error = error;
847 }
848
849 Ok((n_iter, prev_error))
850 }
851
852 fn update_h_semi_nmf(
854 &self,
855 x: &Array2<f64>,
856 w: &Array2<f64>,
857 h: &mut Array2<f64>,
858 ) -> Result<()> {
859 let wt = w.t();
860 let wt_x = wt.dot(x);
861 let wt_w = wt.dot(w);
862 let wt_w_h = wt_w.dot(h);
863
864 let mut wt_x_pos = Array2::zeros(wt_x.dim());
866 let mut wt_x_neg = Array2::zeros(wt_x.dim());
867
868 for i in 0..wt_x.nrows() {
869 for j in 0..wt_x.ncols() {
870 if wt_x[[i, j]] >= 0.0 {
871 wt_x_pos[[i, j]] = wt_x[[i, j]];
872 } else {
873 wt_x_neg[[i, j]] = -wt_x[[i, j]];
874 }
875 }
876 }
877
878 for i in 0..h.nrows() {
880 for j in 0..h.ncols() {
881 let numerator = wt_x_pos[[i, j]];
882 let denominator = wt_w_h[[i, j]] + wt_x_neg[[i, j]] + 1e-10;
883 h[[i, j]] *= numerator / denominator;
884 h[[i, j]] = h[[i, j]].max(0.0); }
886 }
887
888 Ok(())
889 }
890
891 fn update_w_semi_nmf(
893 &self,
894 x: &Array2<f64>,
895 w: &mut Array2<f64>,
896 h: &Array2<f64>,
897 ) -> Result<()> {
898 let ht = h.t();
899 let x_ht = x.dot(&ht);
900 let h_ht = h.dot(&ht);
901 let w_h_ht = w.dot(&h_ht);
902
903 let mut x_ht_pos = Array2::zeros(x_ht.dim());
905 let mut x_ht_neg = Array2::zeros(x_ht.dim());
906
907 for i in 0..x_ht.nrows() {
908 for j in 0..x_ht.ncols() {
909 if x_ht[[i, j]] >= 0.0 {
910 x_ht_pos[[i, j]] = x_ht[[i, j]];
911 } else {
912 x_ht_neg[[i, j]] = -x_ht[[i, j]];
913 }
914 }
915 }
916
917 for i in 0..w.nrows() {
919 for j in 0..w.ncols() {
920 let numerator = x_ht_pos[[i, j]];
921 let denominator = w_h_ht[[i, j]] + x_ht_neg[[i, j]] + 1e-10;
922 w[[i, j]] *= numerator / denominator;
923 w[[i, j]] = w[[i, j]].max(0.0); }
925 }
926
927 Ok(())
928 }
929
930 fn online_nmf_update(
936 &self,
937 x: &Array2<f64>,
938 w: &mut Array2<f64>,
939 h: &mut Array2<f64>,
940 ) -> Result<(usize, f64)> {
941 let (n_samples, _n_features) = x.dim();
942 let batch_size = (n_samples / 10).max(1); let learning_rate = 0.01;
944 let momentum = 0.9;
945
946 let mut w_momentum = Array2::zeros(w.dim());
948 let mut h_momentum = Array2::zeros(h.dim());
949
950 let mut total_error = 0.0;
951 let mut n_iter = 0;
952
953 for batch_start in (0..n_samples).step_by(batch_size) {
955 let batch_end = (batch_start + batch_size).min(n_samples);
956 let batch_indices: Vec<usize> = (batch_start..batch_end).collect();
957 let x_batch = x.select(scirs2_core::ndarray::Axis(0), &batch_indices);
958
959 n_iter += 1;
960
961 self.update_h_online(&x_batch, w, h, &mut h_momentum, learning_rate, momentum)?;
963
964 self.update_w_online(&x_batch, w, h, &mut w_momentum, learning_rate, momentum)?;
966
967 let w_batch = w.select(scirs2_core::ndarray::Axis(0), &batch_indices);
969 let reconstruction = w_batch.dot(h);
970 let batch_error = (&x_batch - &reconstruction).mapv(|x| x * x).sum();
971 total_error += batch_error;
972 }
973
974 let avg_error = (total_error / n_samples as f64).sqrt();
975 Ok((n_iter, avg_error))
976 }
977
978 fn update_h_online(
980 &self,
981 x_batch: &Array2<f64>,
982 w: &Array2<f64>,
983 h: &mut Array2<f64>,
984 h_momentum: &mut Array2<f64>,
985 learning_rate: f64,
986 momentum_coeff: f64,
987 ) -> Result<()> {
988 let (batch_size, _) = x_batch.dim();
989 let _wt = w.t();
990
991 let wh = w.dot(h);
993 let wh_batch = wh.slice(scirs2_core::ndarray::s![0..batch_size, ..]);
994 let residual = &wh_batch.to_owned() - x_batch;
995 let w_batch = w.slice(scirs2_core::ndarray::s![0..batch_size, ..]);
996 let grad_h = w_batch.t().dot(&residual);
997
998 for i in 0..h.nrows() {
1000 for j in 0..h.ncols() {
1001 h_momentum[[i, j]] =
1003 momentum_coeff * h_momentum[[i, j]] - learning_rate * grad_h[[i, j]];
1004
1005 h[[i, j]] += h_momentum[[i, j]];
1007
1008 h[[i, j]] = h[[i, j]].max(0.0);
1010 }
1011 }
1012
1013 Ok(())
1014 }
1015
1016 fn update_w_online(
1018 &self,
1019 x_batch: &Array2<f64>,
1020 w: &mut Array2<f64>,
1021 h: &Array2<f64>,
1022 w_momentum: &mut Array2<f64>,
1023 learning_rate: f64,
1024 momentum_coeff: f64,
1025 ) -> Result<()> {
1026 let (batch_size, _) = x_batch.dim();
1027
1028 let wh = w.dot(h);
1030 let wh_batch = wh.slice(scirs2_core::ndarray::s![0..batch_size, ..]);
1031 let residual = &wh_batch.to_owned() - x_batch;
1032 let grad_w_batch = residual.dot(&h.t());
1033
1034 for i in 0..batch_size {
1036 let global_i = i; if global_i < w.nrows() {
1038 for j in 0..w.ncols() {
1039 w_momentum[[global_i, j]] = momentum_coeff * w_momentum[[global_i, j]]
1041 - learning_rate * grad_w_batch[[i, j]];
1042
1043 w[[global_i, j]] += w_momentum[[global_i, j]];
1045
1046 w[[global_i, j]] = w[[global_i, j]].max(0.0);
1048 }
1049 }
1050 }
1051
1052 Ok(())
1053 }
1054}
1055
1056impl NMF<TrainedNMF> {
1057 pub fn components(&self) -> Array2<f64> {
1059 self.state.components.t().to_owned()
1060 }
1061
1062 pub fn n_iter(&self) -> usize {
1064 self.state.n_iter
1065 }
1066
1067 pub fn reconstruction_err(&self) -> f64 {
1069 self.state.reconstruction_err
1070 }
1071
1072 pub fn inverse_transform(&self, h: &Array2<f64>) -> Result<Array2<f64>> {
1074 let (_n_samples, n_components) = h.dim();
1075
1076 if n_components != self.state.n_components {
1077 return Err(SklearsError::FeatureMismatch {
1078 expected: self.state.n_components,
1079 actual: n_components,
1080 });
1081 }
1082
1083 let reconstructed = h.dot(&self.state.components);
1085 Ok(reconstructed)
1086 }
1087}
1088
1089impl Default for NMF<Untrained> {
1090 fn default() -> Self {
1091 Self::new(2)
1092 }
1093}
1094
1095#[allow(non_snake_case)]
1096#[cfg(test)]
1097mod tests {
1098 use super::*;
1099 use approx::assert_abs_diff_eq;
1100 use scirs2_core::ndarray::array;
1101
1102 #[test]
1103 fn test_nmf_creation() {
1104 let nmf = NMF::new(3)
1105 .init(NMFInit::Random)
1106 .solver(NMFSolver::MultiplicativeUpdate)
1107 .max_iter(100)
1108 .tol(1e-6)
1109 .random_state(42);
1110
1111 assert_eq!(nmf.n_components, 3);
1112 assert_eq!(nmf.init, NMFInit::Random);
1113 assert_eq!(nmf.solver, NMFSolver::MultiplicativeUpdate);
1114 assert_eq!(nmf.max_iter, 100);
1115 assert_abs_diff_eq!(nmf.tol, 1e-6, epsilon = 1e-10);
1116 }
1117
1118 #[test]
1119 fn test_nmf_fit_transform() {
1120 let x = array![
1122 [1.0, 2.0, 3.0],
1123 [2.0, 4.0, 6.0],
1124 [1.0, 3.0, 5.0],
1125 [2.0, 5.0, 8.0],
1126 ];
1127
1128 let nmf = NMF::new(2).random_state(42);
1129 let trained_nmf = nmf.fit(&x, &()).expect("model fitting should succeed");
1130 let h = trained_nmf
1131 .transform(&x)
1132 .expect("transformation should succeed");
1133
1134 assert_eq!(h.dim(), (4, 2));
1135 assert_eq!(trained_nmf.state.n_features_in, 3);
1136 assert_eq!(trained_nmf.state.n_components, 2);
1137
1138 for &val in h.iter() {
1140 assert!(val >= 0.0);
1141 }
1142 }
1143
1144 #[test]
1145 fn test_nmf_inverse_transform() {
1146 let x = array![[1.0, 2.0], [2.0, 4.0], [3.0, 6.0],];
1147
1148 let nmf = NMF::new(2).random_state(123);
1149 let trained_nmf = nmf.fit(&x, &()).expect("model fitting should succeed");
1150 let h = trained_nmf
1151 .transform(&x)
1152 .expect("transformation should succeed");
1153 let x_reconstructed = trained_nmf
1154 .inverse_transform(&h)
1155 .expect("operation should succeed");
1156
1157 assert_eq!(x_reconstructed.dim(), x.dim());
1158
1159 for &val in x_reconstructed.iter() {
1161 assert!(val >= 0.0);
1162 }
1163 }
1164
1165 #[test]
1166 fn test_nmf_different_solvers() {
1167 let x = array![[1.0, 2.0, 3.0], [2.0, 3.0, 4.0], [3.0, 4.0, 5.0],];
1168
1169 let solvers = vec![
1170 NMFSolver::MultiplicativeUpdate,
1171 NMFSolver::CoordinateDescent,
1172 NMFSolver::ALS,
1173 ];
1174
1175 for solver in solvers {
1176 let nmf = NMF::new(2).solver(solver).random_state(42);
1177
1178 let trained_nmf = nmf.fit(&x, &()).expect("model fitting should succeed");
1179 let h = trained_nmf
1180 .transform(&x)
1181 .expect("transformation should succeed");
1182
1183 assert_eq!(h.dim(), (3, 2));
1184
1185 for &val in h.iter() {
1187 assert!(val >= 0.0);
1188 }
1189 }
1190 }
1191
1192 #[test]
1193 fn test_nmf_different_inits() {
1194 let x = array![[1.0, 2.0], [2.0, 4.0], [3.0, 6.0],];
1195
1196 let inits = vec![
1197 NMFInit::Random,
1198 NMFInit::Nndsvd,
1199 NMFInit::NndsvdA,
1200 NMFInit::NndsvdAr,
1201 ];
1202
1203 for init in inits {
1204 let nmf = NMF::new(2).init(init).random_state(42);
1205
1206 let trained_nmf = nmf.fit(&x, &()).expect("model fitting should succeed");
1207 let h = trained_nmf
1208 .transform(&x)
1209 .expect("transformation should succeed");
1210
1211 assert_eq!(h.dim(), (3, 2));
1212 }
1213 }
1214
1215 #[test]
1216 fn test_nmf_negative_data_error() {
1217 let x = array![
1218 [1.0, -2.0], [2.0, 4.0],
1220 ];
1221
1222 let nmf = NMF::new(2);
1223 let result = nmf.fit(&x, &());
1224 assert!(result.is_err());
1225 }
1226
1227 #[test]
1228 fn test_nmf_too_many_components() {
1229 let x = array![[1.0, 2.0], [2.0, 4.0],];
1230
1231 let nmf = NMF::new(5); let result = nmf.fit(&x, &());
1233 assert!(result.is_err());
1234 }
1235
1236 #[test]
1237 fn test_nmf_components_access() {
1238 let x = array![[1.0, 2.0, 3.0], [2.0, 4.0, 6.0], [1.0, 3.0, 5.0],];
1239
1240 let nmf = NMF::new(2).random_state(42);
1241 let trained_nmf = nmf.fit(&x, &()).expect("model fitting should succeed");
1242
1243 let components = trained_nmf.components();
1244 assert_eq!(components.dim(), (3, 2));
1245
1246 for &val in components.iter() {
1248 assert!(val >= 0.0);
1249 }
1250 }
1251
1252 #[test]
1253 fn test_sparse_nmf_l1_regularization() {
1254 let x = array![
1255 [1.0, 2.0, 0.0, 3.0],
1256 [0.0, 4.0, 5.0, 1.0],
1257 [2.0, 0.0, 6.0, 2.0],
1258 [3.0, 1.0, 0.0, 4.0],
1259 ];
1260
1261 let nmf_no_reg = NMF::new(2)
1263 .regularization(NMFRegularization::None, 0.0)
1264 .solver(NMFSolver::CoordinateDescent)
1265 .max_iter(100)
1266 .random_state(42);
1267
1268 let nmf_l1 = NMF::new(2)
1269 .regularization(NMFRegularization::L1, 0.5) .solver(NMFSolver::CoordinateDescent)
1271 .max_iter(100)
1272 .random_state(42);
1273
1274 let trained_no_reg = nmf_no_reg
1275 .fit(&x, &())
1276 .expect("model fitting should succeed");
1277 let trained_l1 = nmf_l1.fit(&x, &()).expect("model fitting should succeed");
1278
1279 let h_no_reg = trained_no_reg
1280 .transform(&x)
1281 .expect("transformation should succeed");
1282 let h_l1 = trained_l1
1283 .transform(&x)
1284 .expect("transformation should succeed");
1285
1286 for &val in h_no_reg.iter() {
1288 assert!(val >= 0.0);
1289 }
1290 for &val in h_l1.iter() {
1291 assert!(val >= 0.0);
1292 }
1293
1294 let threshold = 0.1; let sparse_count_no_reg = h_no_reg.iter().filter(|&&val| val < threshold).count();
1297 let sparse_count_l1 = h_l1.iter().filter(|&&val| val < threshold).count();
1298
1299 let mean_no_reg = h_no_reg
1302 .mean()
1303 .expect("array should have elements for mean computation");
1304 let mean_l1 = h_l1
1305 .mean()
1306 .expect("array should have elements for mean computation");
1307
1308 assert!(
1310 sparse_count_l1 >= sparse_count_no_reg || mean_l1 <= mean_no_reg,
1311 "L1 regularization should induce sparsity or smaller coefficients. No reg: sparse={}, mean={:.3}, L1: sparse={}, mean={:.3}",
1312 sparse_count_no_reg, mean_no_reg, sparse_count_l1, mean_l1
1313 );
1314 }
1315
1316 #[test]
1317 fn test_nmf_l2_regularization() {
1318 let x = array![[1.0, 2.0, 3.0], [2.0, 4.0, 6.0], [1.0, 3.0, 5.0],];
1319
1320 let nmf_l2 = NMF::new(2)
1321 .regularization(NMFRegularization::L2, 0.01)
1322 .solver(NMFSolver::MultiplicativeUpdate)
1323 .random_state(42);
1324
1325 let trained_l2 = nmf_l2.fit(&x, &()).expect("model fitting should succeed");
1326 let h_l2 = trained_l2
1327 .transform(&x)
1328 .expect("transformation should succeed");
1329
1330 for &val in h_l2.iter() {
1332 assert!(val >= 0.0);
1333 }
1334
1335 assert_eq!(h_l2.dim(), (3, 2));
1336 }
1337
1338 #[test]
1339 fn test_nmf_elastic_net_regularization() {
1340 let x = array![
1341 [1.0, 2.0, 3.0, 0.0],
1342 [0.0, 4.0, 1.0, 2.0],
1343 [2.0, 0.0, 5.0, 1.0],
1344 [1.0, 3.0, 0.0, 4.0],
1345 ];
1346
1347 let nmf_elastic = NMF::new(2)
1348 .regularization(NMFRegularization::Both, 0.05)
1349 .l1_ratio(0.5) .solver(NMFSolver::CoordinateDescent)
1351 .max_iter(50)
1352 .random_state(42);
1353
1354 let trained_elastic = nmf_elastic
1355 .fit(&x, &())
1356 .expect("model fitting should succeed");
1357 let h_elastic = trained_elastic
1358 .transform(&x)
1359 .expect("transformation should succeed");
1360
1361 for &val in h_elastic.iter() {
1363 assert!(val >= 0.0);
1364 }
1365
1366 assert_eq!(h_elastic.dim(), (4, 2));
1367 }
1368
1369 #[test]
1370 fn test_nmf_regularization_comparison() {
1371 let x = array![[2.0, 1.0, 0.0], [1.0, 3.0, 1.0], [0.0, 2.0, 2.0],];
1372
1373 let nmf_none = NMF::new(2)
1375 .regularization(NMFRegularization::None, 0.0)
1376 .random_state(42);
1377
1378 let nmf_l1 = NMF::new(2)
1379 .regularization(NMFRegularization::L1, 0.1)
1380 .random_state(42);
1381
1382 let trained_none = nmf_none.fit(&x, &()).expect("model fitting should succeed");
1383 let trained_l1 = nmf_l1.fit(&x, &()).expect("model fitting should succeed");
1384
1385 let h_none = trained_none
1386 .transform(&x)
1387 .expect("transformation should succeed");
1388 let h_l1 = trained_l1
1389 .transform(&x)
1390 .expect("transformation should succeed");
1391
1392 for &val in h_none.iter() {
1394 assert!(val >= 0.0);
1395 }
1396 for &val in h_l1.iter() {
1397 assert!(val >= 0.0);
1398 }
1399
1400 let l1_sum = h_l1.sum();
1402 let none_sum = h_none.sum();
1403
1404 assert!(l1_sum <= none_sum || (l1_sum - none_sum).abs() < 1e-2);
1406 }
1407
1408 #[test]
1409 fn test_nmf_regularization_parameters() {
1410 let nmf = NMF::new(2)
1411 .regularization(NMFRegularization::L1, 0.05)
1412 .l1_ratio(0.7);
1413
1414 assert_eq!(nmf.regularization, NMFRegularization::L1);
1415 assert_eq!(nmf.alpha_w, 0.05);
1416 assert_eq!(nmf.alpha_h, 0.05);
1417 assert_eq!(nmf.l1_ratio, 0.7);
1418 }
1419}