Purpose
The SVD is the decomposition that works for every matrix: no squareness, no symmetry, no full-rank requirement. This note derives it from the eigendecomposition of , defines the pseudoinverse through it, and states Eckart-Young, which is the license behind every truncated-SVD compression and PCA plot. It leans on Eigenvalues and Diagonalization and finishes the least-squares story from Orthogonality, Projections, and Least Squares.
Deriving the SVD from
Let . The matrix is symmetric and positive semidefinite, so by the spectral theorem it has an orthonormal eigenbasis with real eigenvalues . Define the singular values , and for each define
These are orthonormal: , which is 1 when and 0 otherwise. Completing to an orthonormal basis of and collecting everything in matrices gives
with and orthogonal and diagonal with . This is Strang’s construction in 18.06 lecture 29. The rank of is the number of nonzero singular values, and the compact form writes as a sum of rank-one pieces, largest first.
Sanity check both eigenproblems: and , so the right singular vectors diagonalize and the left ones diagonalize , sharing the nonzero spectrum .
Geometry
The SVD says every linear map is rotate, stretch, rotate. rotates the input so the axes line up with , stretches axis by (killing axes past rank ), and rotates into output space. The image of the unit sphere under any is a hyperellipse whose semi-axes have lengths pointing along (Trefethen and Bau open the whole subject with this picture in lecture 4). is the largest stretch factor any unit vector experiences, and the smallest nonzero one; their ratio is the condition number.
flowchart LR X["input x"] --> VT["V transpose: rotate input axes onto right singular vectors"] VT --> SIG["Sigma: stretch axis i by sigma i, drop axes past rank r"] SIG --> U["U: rotate onto left singular vectors in output space"] U --> Y["output Ax"] style VT fill:#e3f2fd style SIG fill:#e8f5e9 style U fill:#e3f2fd
The Moore-Penrose pseudoinverse
Invert what is invertible and leave the rest alone:
is the unique matrix satisfying the four Penrose conditions: , , and both and symmetric (Golub and Van Loan, ch. 5). When is square and invertible, . When has full column rank, , the least-squares operator from the normal equations.
The general statement: is always a least-squares solution of , and among all least-squares solutions it is the one with minimum norm. Rank deficiency makes the minimizer non-unique, adding any null-space component leaves the residual unchanged, and is the choice with zero null-space component. This is exactly what np.linalg.lstsq returns, since its SVD-based driver applies a truncated pseudoinverse (numpy.linalg.lstsq docs).
SVD, pseudoinverse, and least squares are one package
Undo the three-step geometry in reverse: rotate with , divide by the nonzero (zero out the rest), rotate back with . That is , and it solves least squares in every rank case at once:
- Square invertible : , the exact solution.
- Full column rank: , the normal-equations solution.
- Rank deficient: is the minimum-norm least-squares solution, the unique minimizer with no null-space component.
The normal equations only cover the middle case; the SVD route degrades gracefully through all three.
Eckart-Young: best low-rank approximation
Truncate the rank-one sum after terms: . The Eckart-Young-Mirsky theorem says no rank- matrix does better:
with attaining both (Mirsky extended the result to every unitarily invariant norm). The singular value spectrum is therefore a complete answer to “how compressible is this matrix”: if the decay fast, a few rank-one terms capture almost everything, and tells you exactly the error of stopping at .
PCA is this theorem applied to data. Center the data matrix (subtract column means), take its SVD, and the right singular vectors are the principal components; the variance along component is , and the fraction of variance explained by the top components is (Stanford STATS 305C). The repo has a small image-compression demo of exactly this truncation in content/math/linear-algebra/pca_image_compression.py.
NumPy
import numpy as np
rng = np.random.default_rng(0)
A = rng.standard_normal((6, 4)) @ np.diag([10.0, 3.0, 0.5, 0.01])
U, s, Vh = np.linalg.svd(A, full_matrices=False)
print(s) # descending: [25.57 5.64 0.95 0.01]
print(np.allclose(U @ np.diag(s) @ Vh, A)) # True
k = 2 # rank-2 truncation
Ak = U[:, :k] @ np.diag(s[:k]) @ Vh[:k]
print(np.linalg.norm(A - Ak, 2), s[k]) # equal, per Eckart-Young
Apinv = Vh.T @ np.diag(1/s) @ U.T # pseudoinverse by hand
print(np.allclose(Apinv, np.linalg.pinv(A))) # TrueConventions from the numpy.linalg.svd docs that trip people up: the function returns Vh , not ; singular values come back sorted descending as a 1-D array, so reconstruction needs np.diag; and full_matrices=True (the default) returns square and padded with basis vectors for the null spaces, while full_matrices=False gives the compact economy shapes. np.linalg.pinv applies an rcond cutoff before inverting singular values, because dividing by a tiny that is really numerical noise amplifies error by ; treating near-zero singular values as exactly zero is the stable choice for ill-conditioned systems.
Sources
- MIT 18.06 Linear Algebra, Spring 2010 (Strang), lecture 29
- Interactive Linear Algebra (Margalit and Rabinoff)
- numpy.linalg.svd documentation
- Stanford STATS 305C, PCA lecture
- Trefethen and Bau, Numerical Linear Algebra (SIAM, 1997), lectures 4-5
- Golub and Van Loan, Matrix Computations (4th ed.), chapter 5