Singular Value Decomposition
Every matrix, no matter its shape, splits into a rotation, a set of independent stretches, and another rotation — and the stretches come ranked, so you can keep the ones that matter and drop the rest.
Why Does This Exist?
SVD is the closest thing linear algebra has to a universal tool, and it earns that by being the one decomposition that works on any matrix — rectangular, singular, rank-deficient, whatever you have. Eigendecomposition needs a square matrix and can hand back complex numbers. SVD always exists and is always real for real input.
What it buys you, concretely:
- The best possible low-rank approximation, with a proof that nothing better exists at that rank. This is LoRA's mathematical justification and the basis of model compression.
- PCA. Running SVD on centred data gives the principal components directly, and it is numerically better than forming the covariance matrix first.
- Least squares that never fails. The pseudo-inverse handles rank-deficient systems where the normal equations blow up.
- The spectral norm, the largest singular value, which is what spectral normalisation bounds and what condition numbers are built from.
If you learn one decomposition, learn this one.
Think of It Like This
Any deformation is three simple moves
You have a lump of dough and you press it into some shape. It looks like one complicated action.
SVD says it was always three simple actions in sequence. First you turned the dough to line it up. Then you squashed and stretched it along fixed perpendicular directions — no turning, just independent scaling on each axis. Then you turned the result again.
That is the whole theorem: rotate, scale, rotate. Any matrix, however messy, is exactly those three steps.
The useful part is the middle one. The stretch factors come out sorted, largest first, and they tell you how much each direction actually contributes. If the first two are large and the rest are nearly zero, then the deformation is essentially two-dimensional — and you can throw away everything else and lose almost nothing. You know precisely how much you lose, too: it is the size of what you discarded.
How It Actually Works
Every real matrix of shape factors as
with three parts, each doing one job:
- (, orthogonal) — the input rotation. Its rows are the right singular vectors.
- (, diagonal, non-negative, sorted descending) — the stretches. Its diagonal entries are the singular values.
- (, orthogonal) — the output rotation. Its columns are the left singular vectors.
Orthogonal matrices preserve length and angle, so they only rotate or reflect. All the actual distortion is in .
Relation to eigenvalues
Singular values are not eigenvalues in general, but they are closely tied:
In plain words: the singular values of are the square roots of the eigenvalues of . Since is symmetric and positive semi-definite, those eigenvalues are real and non-negative, so the square root is always defined. That is why SVD always exists and is always real.
For a symmetric positive definite matrix the two coincide: singular values equal eigenvalues. That is the case in the worked example below, chosen deliberately so you can check it by hand.
The number of nonzero singular values is exactly the rank, which turns rank from a yes/no algebraic property into a measurable spectrum.
Truncation, and the guarantee
Write as a sum of rank-1 pieces, one per singular value:
In plain words: the matrix is a weighted sum of simple outer products, each weighted by its own importance. Keep the first terms and you get , the truncated SVD.
The Eckart–Young theorem is the guarantee that makes this useful: is the best rank- approximation of in both the Frobenius and spectral norms. No other rank- matrix is closer. And the error is exactly what you left out:
So you can decide how much to discard before discarding it, by looking at the spectrum. The proportion of "energy" retained is , which is precisely the "explained variance" figure PCA reports.
The four uses worth knowing
- PCA. Centre your data matrix, take its SVD, and the right singular vectors are the principal components while are the explained variances. Doing it this way avoids forming , which squares the condition number and loses precision.
- Low-rank compression. Replace a weight matrix by its rank- truncation: storage drops from to . LoRA is the same idea applied to the update rather than the weights.
- Pseudo-inverse. , inverting only the nonzero singular values. This solves least squares even when the system is rank-deficient, which is what
np.linalg.lstsquses. - Conditioning. . Large means numerically fragile, and it is the honest way to check invertibility rather than a determinant.
Worked example
Take — symmetric and positive definite, so its singular values equal its eigenvalues and everything is checkable by hand.
The characteristic polynomial is , giving and . So and . Trace check: ✓. Determinant: ✓.
The eigenvectors are and , normalised to and .
Now truncate to rank 1:
The error:
And 2 is exactly , the singular value discarded — the Eckart–Young formula confirmed by hand.
Energy retained: . So one of two directions carries 80% of this matrix, which is the diagram's annotation.
Show Me the Code
import numpy as np
A = np.array([[3.0, 1.0], [1.0, 3.0]])
U, s, Vt = np.linalg.svd(A)print(s) # -> [4. 2.] sorted descendingprint(np.allclose(A, U @ np.diag(s) @ Vt)) # -> True reconstruction
A1 = s[0] * np.outer(U[:, 0], Vt[0]) # rank-1 truncation, shape (2, 2)print(np.round(A1, 6).tolist()) # -> [[2, 2], [2, 2]]print(round(float(np.linalg.norm(A - A1)), 6)) # -> 2.0 == the discarded sigmaprint(round(float(s[0] ** 2 / (s**2).sum()), 4)) # -> 0.8 energy kept
print(np.linalg.matrix_rank(A), round(s[0] / s[-1], 4)) # -> 2 2.0 rank, conditionSingular values 4 and 2, the rank-1 truncation [[2,2],[2,2]], error exactly 2, and 80% energy — every number matches the hand calculation.
Watch Out For
Running a full SVD when you only need the top few components
np.linalg.svd computes all singular values and vectors, at a cost of roughly . For a matrix that is enormous, and if you wanted 50 components you have thrown almost all of it away.
Worse, the memory does not fit. A full for is — 40 GB in float32. This is the failure people hit first, and it is avoidable in one line: full_matrices=False returns the economy form, which is all anyone needs.
Beyond that, use a truncated solver. scipy.sparse.linalg.svds and sklearn.decomposition.TruncatedSVD compute only the top , and randomised SVD gets there faster still with accuracy that is fine for approximation work. For a sparse matrix this is not an optimisation but a requirement — a dense SVD densifies your sparse matrix first and exhausts memory.
The habit: decide before you decompose, then pick a routine that computes only .
Calling it PCA without centring the data first
PCA is the SVD of centred data. Skip the centring and the first component points toward the mean instead of along the direction of greatest variance, and every downstream number is quietly wrong.
The mechanism is easy to see. If your data sits in a tight cluster far from the origin — say all points near — then the dominant direction in the uncentred matrix is the direction of that offset, because every row is roughly the same large vector. The genuine variation within the cluster is a small perturbation on top and shows up in later components, or not at all.
The symptom is a suspiciously dominant first component, often explaining 99%+ of "variance", followed by projections that all look the same. People interpret this as the data being one-dimensional when it is really just off-centre.
sklearn.decomposition.PCA centres for you; TruncatedSVD deliberately does not, because it is designed for sparse data where centring would destroy sparsity. Those two are not interchangeable, and swapping one for the other is a real and common bug. If you are calling np.linalg.svd directly, subtract the column means yourself, and keep them — you need them to project new data consistently.
The Quick Version
- : rotate, scale, rotate. Works for any real matrix, and is always real.
- Singular values are non-negative and sorted descending. All the distortion is in .
- , which is why SVD always exists. For symmetric positive definite matrices, singular values equal eigenvalues.
- The count of nonzero singular values is the rank, so rank becomes a spectrum rather than a yes/no.
- — a sum of rank-1 pieces ranked by importance.
- Eckart–Young: the truncation is the best rank- approximation, and the error is .
- Energy retained — this is PCA's explained variance.
- Uses: PCA, low-rank compression and LoRA, the pseudo-inverse for rank-deficient least squares, the spectral norm, condition number .
- Pass
full_matrices=False, and use a truncated solver when you only want the top . - PCA requires centring.
PCAcentres,TruncatedSVDdoes not.
What to Read Next
- Eigenvalues and Eigenvectors is what singular values reduce to for symmetric matrices.
- Vector Spaces and Rank is what the nonzero singular values count, and where the LoRA saving comes from.
- Norms supplies the Frobenius and spectral norms the approximation guarantee is stated in.
- Matrix Multiplication is the operation a low-rank factorisation makes cheaper.
- Definitions worth a look: Matrix Rank, Transpose, Orthogonal Vectors, and L2 Norm.
- Related interview question: Implement matrix multiplication and analyse its complexity