perf(preprocessing): whiten via the covariance matrix in Whitener::pca - #453
Open
mysma-9403 wants to merge 1 commit into
Open
perf(preprocessing): whiten via the covariance matrix in Whitener::pca#453mysma-9403 wants to merge 1 commit into
mysma-9403 wants to merge 1 commit into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #453 +/- ##
==========================================
+ Coverage 77.53% 77.92% +0.39%
==========================================
Files 106 104 -2
Lines 7585 7535 -50
==========================================
- Hits 5881 5872 -9
+ Misses 1704 1663 -41 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
`WhiteningMethod::Pca` took the SVD of the whole centered (nsamples x nfeatures) data matrix, while the `Zca` and `Cholesky` arms next to it first form the (nfeatures x nfeatures) covariance and decompose that. The two are equivalent for PCA whitening: the right singular vectors of the centered data are the eigenvectors of its covariance, and the singular values relate to the eigenvalues by `lambda = s^2 / (nsamples - 1)`. Forming the covariance first turns an SVD that grows with the sample count into a fixed-size one, leaving a single GEMM as the only work proportional to `nsamples`: n=2000 d=16 4.11 ms -> 0.15 ms (27x) n=8000 d=32 83.28 ms -> 1.19 ms (70x) n=32000 d=64 2051.71 ms -> 13.31 ms (154x) n=64000 d=64 5259.08 ms -> 33.16 ms (159x) n=8000 d=128 768.69 ms -> 22.92 ms (34x) The epsilon floor is applied to the reconstructed singular values rather than to the eigenvalues, so it keeps the meaning it had before, and the eigenvalues are clamped at zero first because rounding can push a numerically-zero one slightly negative and turn `sqrt` into a NaN. With at most as many samples as features the covariance is rank deficient, and the SVD of the data matrix yields a differently shaped factor. Routing that case to the original formulation keeps the shape of the whitening matrix unchanged. Which shape that is depends on the backend, and did so before this change too: `linfa_linalg::svd` is compact and returns `nsamples x nfeatures`, `ndarray_linalg::svd` is full and returns `nfeatures x nfeatures`. `test_pca_matrix_more_features_than_samples` asserts both, and `test_pca_matrix_square_input` covers the square case, where the two agree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C1YZvjZCFRV5jsW5wPAL74
mysma-9403
force-pushed
the
perf/whitening-pca
branch
from
August 6, 2026 21:52
70287fe to
7c6b880
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
WhiteningMethod::Pcatakes the SVD of the whole centered(nsamples x nfeatures)data matrix, while theZcaandCholeskyarms immediately next to it first form the(nfeatures x nfeatures)covariance and decompose that instead.For PCA whitening the two are equivalent: the right singular vectors of the centered data are the eigenvectors of its covariance, and the singular values relate to the eigenvalues by
lambda = s² / (nsamples - 1). Forming the covariance first turns an SVD whose cost grows with the sample count into a fixed-size one, leaving a single GEMM as the only work proportional tonsamples.Results
Accuracy, measured in the same run (
Wis the whitening matrix,Ythe whitened data):max‖WᵀW − Σ⁻¹‖before → aftermax‖cov(Y) − I‖before → afterWorth being explicit about the trade-off rather than reading too much into those numbers: forming
XᵀXsquares the condition number, so on badly conditioned inputs this formulation is in principle less accurate than an SVD ofX, even though it comes out slightly ahead on the well-conditioned uniform-random data above.ZcaandCholeskyalready accept exactly that trade-off, so this makesPcaconsistent with them rather than introducing a new compromise. If you would rather keep the data-matrix SVD forPcaspecifically on numerical grounds, that is a reasonable call and I am happy to close this.Details
max(s, 1e-8)keeps the meaning it had before.sqrt, because rounding can push a numerically-zero one slightly negative and produce aNaN.WᵀW.Fewer samples than features
With
nsamples <= nfeaturesthe covariance is rank deficient, and the SVD of the data matrix returns a differently shaped factor — so switching formulations there would change the shape of the whitening matrix. That case is routed to the original code path, and two new tests pin the shape:test_pca_matrix_more_features_than_samples(20 × 50)test_pca_matrix_square_input(16 × 16)Which shape that is turns out to depend on the backend, which I had missed on the first push — the first of those tests asserted only the default backend's answer and failed the BLAS CI jobs.
linfa_linalg::svdis a compact SVD and returns the(nsamples, nfeatures)factor;ndarray_linalg::svdis a full one and returns(nfeatures, nfeatures). The test now asserts both. This split is not introduced here: thensamples <= nfeaturesarm is byte-for-byte the code that was there before, so both shapes are exactly whatmasterproduces today. The new covariance path is unaffected either way, since it decomposes a square matrix, on which the two agree.While confirming that, one pre-existing wrinkle became visible, mentioned only for the record — it is untouched by this PR and I have not tried to fix it here. Under BLAS in that same regime,
shasmin(nsamples, nfeatures)entries whilev_thasnfeaturesrows, so the scaling loop leaves the trailing rows unscaled. They span the null space of the centered data, so the corresponding output columns come out identically zero rather than unit variance. The compact SVD has no trailing rows and so no such columns. Neither backend can genuinely whiten a rank-deficient input, so this is a difference in how the degenerate case is presented rather than in whether it works. Happy to open a separate issue if that is worth tracking.Benchmark context
lowpowermode 0)linfa-linalg) backend, best of 3Whitener::pca().fit(..)calls, measured with a throwaway harness that is not part of this PR; A/B taken on this branch with and without the change so the binary shape is identicalChecks
cargo fmt --all -- --check— cleancargo clippy --workspace --all-targets -- -D warnings— cleancargo test --release --workspace— no failurescargo test --release -p linfa-preprocessing --features blas,linfa/netlib-static --lib— 55 passed, 0 failed, andcargo clippyclean under the same features (MKL still will not link locally on macOS, and OpenBLAS trips over an unrelatedopenblas-build/ureqTLS mismatch, sonetlib-staticis what I could actually run; that is a real LAPACK, so it exercises the samendarray-linalgcode path CI does)🤖 Generated with Claude Code
https://claude.ai/code/session_01L4uUK6s1CWFwx6u7Rx28go