Skip to main content

NSA-Flow: Non-negative Stiefel-Approximating Flow

A calibration-free relaxation between reconstruction and feature clustering.

import torch
from nsa_flow import nsa_flow

X0 = torch.rand(120, 8)          # a loading matrix to refine, e.g. |PCA loadings|
r  = nsa_flow(X0, w=0.7)         # w is a genuine convex weight in [0, 1]

print(r)
# NSAResult(w=0.7, iters=61, energy=..., fidelity=..., defect=...,
#           eff_rank=7.41, stop=grad_map, |Gmap|=4.2e-13)
print(r.Y.shape)                 # (120, 8), non-negative, near-orthogonal columns

What it computes

Minimise, over Y >= 0,

E_w(Y) = (1 - w) * ||Y - X0||_F^2 / ||X0||_F^2   +   w * D(Y) / (1 - 1/k)

where, with G = Y'Y / tr(Y'Y) the trace-normalised Gram matrix,

D(Y) = || G - I/k ||_F^2 = ||Y'Y||_F^2 / ||Y||_F^4 - 1/k

Both terms are dimensionless and O(1), so w needs no calibration: no normalising constants estimated from the data, no warm-up phase, no dependence on p, k or the scale of X0. A reported w means the same thing on every problem.

Why this D

D is the squared distance from the normalised Gram matrix to isotropy. It

  • vanishes exactly on R_{>0} * St(p, k) — orthogonal columns of equal norm;
  • equals k * Var(eigenvalues of G) = 1/EffectiveRank - 1/k, so minimising it drives the effective rank to k;
  • is invariant under Y -> cY, Y -> UY and Y -> YV for orthogonal U, V — the same group that preserves the constraint being relaxed;
  • charges at least 1/r - 1/k for rank r < k, so rank collapse is penalised;
  • is bounded: 0 <= D <= 1 - 1/k;
  • satisfies <grad D, Y> = 0, so it cannot change ||Y|| — scale is pinned by the fidelity term alone, and no renormalisation step is needed.

It splits as

D  =  sum_{i != j} G_ij^2   +   sum_i (G_ii - 1/k)^2
      \_______________/         \___________________/
        decorrelation              norm balance

The first term alone is the "invariant orthogonality defect" used in v1 and elsewhere. Dropping the second is what made that functional blind to conditioning, basis-dependent, and minimised by rank-deficient matrices.

What w does

w = 0 returns max(0, X0). As w -> 1, D -> 0, and since Y >= 0 with Y'Y diagonal forces pairwise disjoint column supports, the limit is a hard clustering of the p features into k groups — the feasible set of orthogonal NMF, equivalent to k-means. In between, the overlap is bounded:

max_{i != j} <y_i, y_j>  <=  sqrt(D) * ||Y||_F^2

so "approximately disjoint factors" is a claim with a number attached.

Solver

Spectral projected gradient: Barzilai–Borwein steps with Armijo backtracking on the projected step. Every accumulation point is a stationary point of the constrained problem, and result.grad_map is a computable stationarity certificate (result.stop_reason says why it stopped — never a silent claim of convergence).

The inner loop forms one Gram product and two [p,k] x [k,k] products: O(p k^2), with no SVD, eigendecomposition or QR. Typical convergence is 15–300 deterministic iterations. Pass compile=True for a 3–4x speedup via torch.compile at moderate sizes.

Empirically E_w has a unique optimum for w < 1 — 24 random restarts agree to machine precision on every problem family tested — so there are no restarts, schedules or step-size heuristics to tune.

Torch layers

Two routes, both sound:

from nsa_flow import NSAFlowLinear, NSAFlowConv2d

# (preferred) penalty: a standard layer plus a regulariser
layer = NSAFlowLinear(256, 32)
loss  = task_loss(layer(x), y) + 0.1 * layer.defect()

# (parameterisation) effective weight is blended toward the projection
layer = NSAFlowLinear(256, 32, w=0.5)   # w is the true blend fraction

polar_factor and project_scaled_stiefel carry an explicit Sylvester-equation derivative. Differentiating torch.linalg.svd divides by sigma_i^2 - sigma_j^2 and returns NaN at repeated singular values — which is exactly what nn.init.orthogonal_ produces. The polar factor is smooth wherever Y has full column rank; its derivative divides by h_i + h_j > 0.

API

Class / Function Purpose
nsa_flow(data_or_target, k=..., w=...) Unified high-level entry point; auto-dispatches based on data signs and dimensions
NSAFlow(n_components=..., w=...) Scikit-learn compatible estimator with fit, transform, fit_transform
nsa_flow_data(X, k, w, ...) fit non-negative basis to data; matrix-free when p > n
nsa_flow_signed(X, k, w, consolidate=True) V = V⁺ − V⁻, signed contrast lifting with disjoint lobes
relax_into_nonneg(...) continuation in μ into the non-negative cone
stiefel_defect(Y) D(Y), orthoNORMality
angle_defect(Y, diagonal=) C(Y), orthogonality at any column norms
subspace_fidelity(Y, X0) sign-blind distance to range(X0)
negative_mass(X0) how much of a target is unreachable under Y ≥ 0
part_sparsity(W) / consolidate_supports(W) per-lobe support diagnostics and rounding
effective_rank(Y) k/(kD+1), in [1,k]
project_nonneg / project_scaled_stiefel / polar_factor projections
NSAFlowLinear / NSAFlowConv2d / NSAFlowLayer torch layers

High-Level Unified Interface

nsa_flow serves as the unified wrapper across all problem modes:

from nsa_flow import nsa_flow, NSAFlow

# 1. Non-negative data -> fits non-negative basis V >= 0 (auto data mode)
r_data = nsa_flow(X_positive, k=5, w=0.5)

# 2. Signed or centered data -> signed contrast lifting V = V+ - V- (auto signed mode)
r_signed = nsa_flow(X_centered, k=5, w=0.5, consolidate=True)

# 3. Target loadings matrix -> anchored flow refinement
r_anc = nsa_flow(PCA_loadings, w=0.5)

# 4. Scikit-learn Pipeline Integration
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression

pipe = Pipeline([
    ("nsa", NSAFlow(n_components=5, w=0.5, consolidate=True)),
    ("clf", LogisticRegression())
])
pipe.fit(X_train, y_train)

Optimizers & Performance

All solvers default to optimizer="torch_lbfgs", a 100% pure PyTorch native quasi-Newton optimizer using quadratic reparameterization ($V = Z^2$) and exact analytical chain rule gradients ($\nabla_Z E = 2 Z \odot \nabla_V E$). It achieves up to 23.2× speedup over spectral projected gradient (SPG) without boundary stalling or host-device transfers. SPG (optimizer="spg") and SciPy L-BFGS-B (optimizer="lbfgs") remain available.

nsa_flow_signed writes each component as a contrast of two non-negative parts, which restores a signed basis's representational capacity: at w = 0 it reproduces signed PCA's reconstruction to the digit. The relaxation alone leaves each part concentrated but not sparse, so pass consolidate=True to round to exactly disjoint supports and re-solve with the support fixed. On ADNI cortical thickness at w = 0.5 that takes the largest part from 39 of 66 features to 18, leaves V⁺ and V⁻ each about 10% dense with no component lost, and improves held-out prediction — the small tail was noise.

Leave w at its default. Across nine ADNI cognitive outcomes the lifting is the best of the four variants at w = 0.5 (mean ΔR² over PCA +0.011 linear, +0.044 forest) and worse than PCA on 0 of 9 at w = 0.75. Since a linear model sees only the span, that collapse is a loss of span quality: pushing w up rounds the contrasts toward a partition that no longer spans what the data needs. w = 0.5 is also the best setting for sparsity, so there is nothing to trade off.

For k > p, orthonormal columns are impossible and inf D = 1/p - 1/k > 0; this is reported rather than hidden behind a silently row-orthonormal answer.

Install, test, reproduce

make install       # editable install with experiment + test extras
make test          # 133 assertions
make theory        # just the property battery (70 assertions)
make experiments   # regenerate paper/results/ and paper/figs/
make paper         # build paper/nsaflow.pdf

tests/test_theory.py states every proposition in the paper executably — the bounds, the zero set, the invariances, the spectral identity, the decomposition, the collapse floor, the gradient identities, the disjoint-support equivalence, the overlap bound, and term calibration for every option pair. If a claim in the paper is weakened, one of those fails.

Migrating from 1.x

The 1.x API is gone; nsa_flow(target, w=...) replaces nsa_flow_orth(Y0, X0=..., ...) and the retraction, optimiser and learning-rate-strategy modules are removed. The appendix of the paper lists the substantive changes and why each was made; the 1.x code and tests are preserved under attic/ for reference.

Citation

Avants, B. NSA-Flow: Non-negative Stiefel-Approximating Flow --- a
calibration-free relaxation between reconstruction and feature clustering.

MIT licensed.

Metadata

Release files for nsa-flow 2.14.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nsa-flow 2.14.0
File Size Uploaded
nsa_flow-2.14.0.tar.gz 71.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nsa-flow 2.14.0
File Interpreter ABI Platform
nsa_flow-2.14.0-py3-none-any.whl Python 3 none any Details

Total release size: 121.7 kB

Release files / nsa_flow-2.14.0.tar.gz

Download URL nsa_flow-2.14.0.tar.gz
Size 71.5 kB
Tags Source
SHA-256 checksum
How to use checksums
75f4536d310f8d12666e5546b724dddd3e2c7ef31a7152d2bf8a5cedac6e5084
BLAKE2b-256 checksum
How to use checksums
2e4266caf36e0d73f1994d07d7086b535fc4493b9de4b2dfbd213b51e47915c0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.12

Release files / nsa_flow-2.14.0-py3-none-any.whl

Download URL nsa_flow-2.14.0-py3-none-any.whl
Size 50.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e3f22e99f58c72b16903255933c38d8945d6739d5a29c4328d73f80e2e40a003
BLAKE2b-256 checksum
How to use checksums
32aa81364258169d78ab62ea0471dce6bc8229153df99c9b01d31f7876cdbc41
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.12

Release history Release notifications | RSS feed

2.15.0

2 release files

This release

2.14.0 This release

2 release files

1.5.0

2 release files

1.4.0

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

0.9.0

2 release files

0.8.9

2 release files

0.8.8

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.7

2 release files

0.7.5

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page