Decomposition nmf fit - CyrilB1531/lodestar GitHub Wiki

Development build. This page describes main, not a released package. The latest published Lodestar.Decomposition is 0.2.0 — read its documentation.

HomeDecompositionFactorization

Nmf.Fit

Factorizes a sparse non-negative matrix into two non-negative factors, by Lee and Seung's multiplicative updates.

public static Nmf Fit(CsrMatrix matrix, int componentCount, NmfOptions options = null)
public static Nmf Fit(CsrMatrix matrix, double[] initialWeights, double[] initialComponents, NmfOptions options = null)

Parametersmatrix is the matrix to factorize, rows as samples and columns as features; it is read, never modified, and it must hold no negative value. componentCount is the rank to keep, at least 1 and no greater than min(rows, columns), which is scikit-learn's own bound. initialWeights is W₀, row-major matrix.RowCount × componentCount, and initialComponents is H₀, row-major componentCount × matrix.ColumnCount; both are non-negative, both are copied, and the rank is read off their lengths rather than passed again — so that overload, and only that one, reaches a rank above min(rows, columns). options carries the solver's settings, or is left out for scikit-learn's defaults: the Frobenius loss, NNDSVD, 200 iterations and a tolerance of 1e-4.

Returns — an Nmf holding W, H, the iteration count and the reconstruction error. Every property is populated; there is no second call to make.

ExceptionsArgumentNullException when matrix, initialWeights or initialComponents is null. ArgumentOutOfRangeException when componentCount is below 1 or above the smaller of the two dimensions, or when MaxIterations is below one or Tolerance is negative or NaN; the options are checked first, before the matrix is read or initialised. ArgumentException when matrix holds a negative value, a NaN or an infinity, on either overload — the precondition is checked rather than assumed, since a negative entry does not fail the loop, it returns signed factors under the Frobenius loss and NaN under Kullback–Leibler. ArgumentException too when matrix has no row or no column on the overload handed the blocks, when the two blocks do not agree on a component count, do not fit the matrix, or hold a negative number, a NaN or an infinity — and when RandomMatrix is given and is not matrix.ColumnCount × (componentCount + 10) values long.

Example — four documents over three terms, factorized at rank 2.

using Lodestar.Abstractions;
using Lodestar.Decomposition;

// Row-major CSR: values, the column each sits in, and where each row starts.
CsrMatrix matrix = new(
    4, 3,
    [3.0, 1.0, 2.0, 1.0, 1.0, 4.0, 2.0, 3.0],
    [0, 1, 0, 2, 1, 2, 0, 2],
    [0, 2, 4, 6, 8]);

Nmf fitted = Nmf.Fit(matrix, 2);

int rounds = fitted.Iterations;                             // => 50
double error = Math.Round(fitted.ReconstructionError, 3);   // => 1.066
double firstTerm = Math.Round(fitted.Components[0], 3);     // => 0.049

Example — the same matrix from an initialisation written down rather than computed, with the early stop disabled so the iteration count is an input.

using Lodestar.Abstractions;
using Lodestar.Decomposition;

CsrMatrix corpus = new(
    4, 3,
    [3.0, 1.0, 2.0, 1.0, 1.0, 4.0, 2.0, 3.0],
    [0, 1, 0, 2, 1, 2, 0, 2],
    [0, 2, 4, 6, 8]);

// W₀ is 4 × 2 and H₀ is 2 × 3, both row-major and neither holding a negative number.
double[] initialWeights = [1.0, 0.5, 0.5, 1.0, 0.8, 0.8, 1.0, 0.2];
double[] initialComponents = [1.0, 0.5, 1.0, 0.5, 1.0, 1.0];

Nmf model = Nmf.Fit(
    corpus, initialWeights, initialComponents,
    new NmfOptions { MaxIterations = 100, Tolerance = 0.0 });

int ran = model.Iterations;                                  // => 100
double reached = Math.Round(model.ReconstructionError, 3);   // => 1.066

Remarks — the two overloads are one algorithm. The first computes W₀ and H₀ with NmfInitialization and then calls the second, so anything the initialisation decides — most of all which entries are zero, because a multiplicative update can never revive one — is decided before the loop starts.

Each iteration updates W first and H second, against the already-updated W. Updating both against the old pair is a different algorithm that converges more slowly, and it is why an implementation that looks right can be close and never equal.

The stopping rule is scikit-learn's: the relative improvement is measured every tenth iteration and never on the others, against the divergence at the initialisation. A Tolerance of zero disables it, which turns MaxIterations into an exact iteration count — the form the oracle corpus freezes, and the form to use when two runs must be compared step for step.

The answer is a local minimum, not the minimum. Two initialisations reach two different factorizations of the same rank, both non-negative and both valid; NndSvd is deterministic once Ω is fixed, which is what makes a run repeatable at all.

Applies to — net10.0, netstandard2.0.

See alsoNmf, NmfOptions, NmfBetaLoss, NmfInitialization, the Python equivalence table.