Skip to main content

Module nmf

Module nmf 

Source
Expand description

Non-negative Matrix Factorization (NMF).

NMF decomposes a non-negative matrix X into two non-negative factors W and H such that X ~ W * H, where:

  • X has shape (n_samples, n_features)
  • W has shape (n_samples, n_components)
  • H has shape (n_components, n_features)

§Algorithm

Two solvers are supported:

  • Multiplicative Update (Lee & Seung, 2001): iteratively update W and H using multiplicative rules that guarantee non-negativity.
  • Coordinate Descent: iteratively solve for each element of W and H using closed-form coordinate-wise updates.

§Initialization

  • Random: initialize W and H with random non-negative values.
  • NNDSVD: Non-Negative Double SVD, initializes W and H from a truncated SVD of X, setting negative entries to zero.

§Examples

use ferrolearn_decomp::NMF;
use ferrolearn_core::traits::{Fit, Transform};
use ndarray::array;

let nmf = NMF::<f64>::new(2);
let x = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
let fitted = nmf.fit(&x, &()).unwrap();
let projected = fitted.transform(&x).unwrap();
assert_eq!(projected.ncols(), 2);

§REQ status

Design: .design/decomp/nmf.md. Tracking: #1608. Each REQ is BINARY — SHIPPED (impl + non-test consumer + tests + green verification) or NOT-STARTED (concrete open blocker). Non-test consumers: crate re-export (lib.rs:97), the PyO3 _RsNMF binding (ferrolearn-python/src/extras.rs:1116, registered lib.rs:75), PipelineTransformer. Oracle = live sklearn 1.5.2 (_nmf.py, class NMF), run from /tmp (R-CHAR-3). ferrolearn’s ctor still DEFAULTS to MU + Random init; exact component VALUES on the RANDOM/MU path are a carve-out (numpy RNG vs Rust RNG; NMF identifiable only up to permutation/scaling). BUT the DETERMINISTIC init='nndsvd', solver='cd' path is now BIT-EXACT to sklearn (#2398/#2394/#2395/ #2396/#2397): the real SVD-based NNDSVD init (init_nndsvd via ferray::linalg::svd_lapack + svd_flip_u_based), the violation-ratio CD convergence (solve_coordinate_descent/update_cd_sweep), and the CD transform W-solve all reproduce sklearn to ~1e-9 (tests/divergence_nmf_cd_nndsvd_2393.rs).

REQScopeStatusEvidence / Blocker
REQ-1Structural: components_ shape (n_components,n_features), transform W shape, finite + decreasing reconstruction_err_, n_iter_, seed-determinismSHIPPED (scoped)fit (nmf.rs:617); green-guards + in-module tests. STRUCTURAL, NOT values (REQ-5)
REQ-2Non-negativity of components_ (H) + transform (W)SHIPPEDMU multiplicative + CD clamp; test_nmf_components_non_negative/_transform_non_negative
REQ-3Both solvers (MU/CD) × both inits (Random/NNDSVD) runSHIPPEDfit dispatch (:657-670); 4-combo tests
REQ-4Reconstruction QUALITY (‖X−WH‖ small/decreasing — “did NMF work”)SHIPPEDreconstruction_error (:247) monotone-decreasing + small residual; test_nmf_*
REQ-5EXACT components_ value paritySHIPPED (deterministic cd+nndsvd path) / NOT-STARTED (random/MU)The DETERMINISTIC init='nndsvd', solver='cd' components_ is bit-exact to sklearn — divergence_components_cd_nndsvd (tests/divergence_nmf_cd_nndsvd_2393.rs) matches components_[0] to 1e-6 (was #2394). The random-init/MU path stays a CARVE-OUT (numpy vs Rust RNG, perm/scale) — blocker #1609
REQ-6real SVD init='nndsvd' (+ nndsvda/nndsvdar/custom, nndsvda default)SHIPPED (nndsvd) / NOT-STARTED (nndsvda default + nndsvdar/custom)init_nndsvd (nmf.rs symbol init_nndsvd) now does the REAL SVD-based NNDSVD = sklearn _initialize_nmf (_nmf.py:320-358): svd_lapack (LAPACK gesdd, the SAME driver randomized_svd/scipy use) → svd_flip_u_based (extmath.py svd_flip(u_based_decision=True)) → leading-triplet sqrt(S[0])·|U|/sqrt(S[0])·|Vt| + per-component pos/neg-part split lbd=sqrt(S[j]·sigma)<eps-zero. test_nmf_nndsvd_init_matches_sklearn matches sklearn _initialize_nmf(X,3,'nndsvd') W/H to 1e-9. STILL NOT-STARTED: the nndsvda/nndsvdar zero-fill + custom + init=None→nndsvda default — blocker #1610
REQ-7solver='cd' matching _fit_coordinate_descent (+ cd DEFAULT)SHIPPED (cd algorithm) / NOT-STARTED (cd ctor DEFAULT)solve_coordinate_descent/update_cd_sweep now match sklearn’s Cython _update_cdnmf_fast (Gram-based per-coordinate W[i,t]=max(0,W[i,t]−grad/hess), in-place sweep) + the VIOLATION-RATIO stop violation/violation_init<=tol (_nmf.py:500-525), reproducing sklearn n_iter_ (divergence_n_iter_cd_nndsvd → 151) + reconstruction_err_ (divergence_reconstruction_err_cd_nndsvd → 5.5136, 1e-6). STILL NOT-STARTED: NMF::new still defaults to MultiplicativeUpdate not cd — blocker #1611
REQ-8beta_loss (kullback-leibler/itakura-saito) + _gammaNOT-STARTEDsklearn _nmf.py:89,:919; ferrolearn Frobenius-only — blocker #1612
REQ-9transform NNLS-W VALUESHIPPEDtransform (impl Transform for FittedNMF) now solves W via the CD with H fixed = _fit_transform(X, H=components_, update_H=False) (_nmf.py:1213): W=zeros (_nmf.py:1254), violation-ratio CD. divergence_transform_w_cd_nndsvd matches sklearn m.transform(X)[0] to 1e-6 (was #2397)
REQ-10inverse_transform = W·HSHIPPEDnmf.rs:229 (= _nmf.py:1238); exact algebra + col-mismatch ShapeMismatch
REQ-11Error/parameter contracts (incl. NON-FINITE rejection, finiteness-before-nonnegative)SHIPPED (scoped)fit/transform guards. FLAG: sklearn raises InvalidParameterError, accepts n_components=None, doesn’t pre-reject >min(n,p). NON-FINITE: fit+transform call reject_non_finite (nmf.rs symbol reject_non_finite) BEFORE the non-negativity check and factorization, returning InvalidParameter{name:"X", reason:"Input X contains NaN or infinity."} = sklearn _validate_data(force_all_finite=True) (_nmf.py:1652) which runs BEFORE check_non_negative (_nmf.py:1706), so a NaN+negative input rejects for finiteness first (utils/validation.py:147-154). tests/divergence_nonfinite.rs::divergence_nmf_fit_nan_/_fit_nan_and_negative_finiteness_fires_first match the live sklearn 1.5.2 oracle. Was #2288/#2289, fixed. Consumer: re-export lib.rs + NMF fit/transform
REQ-12PyO3 _RsNMF binding (thin n_components ctor + fit + transform)SHIPPED (scoped)extras.rs:1116, registered lib.rs:75; NO params/getters/inverse_transform
REQ-13n_components=None defaultNOT-STARTEDsklearn _nmf.py:914 → min(n,p); ferrolearn requires explicit usize — blocker #1614
REQ-14alpha_W/alpha_H/l1_ratio regularizationNOT-STARTEDsklearn _nmf.py:921-923,:1275 — blocker #1615
REQ-15shuffle (CD) + fitted attrs n_components_/n_features_in_NOT-STARTEDsklearn _nmf.py:924 — blocker #1616
REQ-16ferray substrateNOT-STARTEDndarray + rand + hand-rolled Jacobi — blocker #1617

Count: 9 SHIPPED (REQ-1,2,3,4,9,10,11,12 + REQ-6/7 cd+nndsvd algorithm) / 7 NOT-STARTED (REQ-8,13,14,15,16 + REQ-6 nndsvda-default/nndsvdar/custom + REQ-7 cd-ctor-default). REQ-5 is SHIPPED on the deterministic cd+nndsvd path, NOT-STARTED on random/MU (carve-out #1609).

Structs§

FittedNMF
A fitted NMF model holding the learned components and reconstruction error.
NMF
Non-negative Matrix Factorization configuration.

Enums§

NMFInit
The initialization strategy for NMF.
NMFSolver
The solver algorithm for NMF.