Least squares & the Moore–Penrose pseudo-inverse
Derive the normal equations from the residual, interpret the solution as a projection, and build the pseudo-inverse from the SVD.
Most real systems are overdetermined: more equations than unknowns, and no exact solution exists. The least-squares criterion picks the that makes as close to as possible, and the geometry is a projection.
The least-squares problem
Given with and , find
The objective is a quadratic in (it opens up: is PSD). Its minimum is where the gradient is zero.
Derivation via the gradient
Expand:
Taking the gradient with respect to and setting it to zero:
giving the normal equations:[1]
If has full column rank (), is invertible, and the unique solution is .
Geometric interpretation
is the projection of onto : it is the point in the column space closest to . The residual is orthogonal to every column of :
which is exactly the normal equations rewritten. The projection matrix onto is
Any symmetric idempotent matrix is a projection; these two properties characterize projections completely.
Numerical route: QR is preferable
Forming squares the condition number: . The QR route avoids this. Write (thin QR, , upper triangular). The normal equations become
a triangular system solved by backward substitution.
The Moore–Penrose pseudo-inverse
When does not have full column rank, the normal equations have infinitely many solutions. The minimum-norm solution (smallest among all minimizers of ) is given by the Moore–Penrose pseudo-inverse [2]:
Construction from SVD. Let with rank . Define
where replaces each nonzero singular value by and leaves zeros alone.
Properties:
- If has full column rank: (the standard formula).
- If has full row rank: (a right inverse).
- , , and both and are symmetric.
Check it on arrays
import numpy as np
rng = np.random.default_rng(7)
m, n = 50, 5
A = rng.normal(size=(m, n))
b = rng.normal(size=m)
# Normal equations (full column rank here)
ATA = A.T @ A
ATb = A.T @ b
x_normal = np.linalg.solve(ATA, ATb)
# QR route (numerically preferable)
Q, R = np.linalg.qr(A)
x_qr = np.linalg.solve(R, Q.T @ b)
# Built-in lstsq (uses SVD internally)
x_lstsq, _, _, _ = np.linalg.lstsq(A, b, rcond=None)
print("normal eq residual:", np.linalg.norm(A @ x_normal - b))
print("QR residual:", np.linalg.norm(A @ x_qr - b))
print("lstsq residual:", np.linalg.norm(A @ x_lstsq - b)) # same
print("solutions agree:", np.allclose(x_qr, x_lstsq, atol=1e-10))
# Pseudo-inverse via SVD: also handles rank-deficient A
U, s, Vt = np.linalg.svd(A, full_matrices=False)
A_plus = Vt.T @ np.diag(1.0 / s) @ U.T
x_pinv = A_plus @ b
print("pinv agrees with lstsq:", np.allclose(x_pinv, x_lstsq, atol=1e-10))
# Residual is orthogonal to col(A):
r = b - A @ x_lstsq
print("A^T r (should be ~0):", np.round(A.T @ r, 8))All four routes agree on the solution; the residual is orthogonal to every column of .
Exercises
Exercise 1. Solve the least-squares problem with and by the normal equations, and state what the scalar represents geometrically.
Solution
Step 1. Form the normal equation . Here and .
Step 2. Solve the scalar equation:
Step 3. Since the single column of is the all-ones vector, is the constant vector closest to . Projecting onto the all-ones direction returns the mean of the entries of , and indeed . The least-squares fit of a constant to data is the sample mean.
Exercise 2. Show directly from the definition that the projection matrix (with of full column rank) is idempotent () and symmetric ().
Solution
Step 1 (idempotent). Multiply by itself and cancel the inner :
Step 2 (symmetric). Transpose, using and the fact that is symmetric, so its inverse is too:
Both properties hold, so is an orthogonal projection: applying it once lands in , and applying it again changes nothing.
Exercise 3 (code). Build a rank-deficient (repeat a column so the column rank is below the number of columns). The normal equations are then singular. Verify that the SVD pseudo-inverse still returns a least-squares solution and that, among all minimizers, it has the smallest norm.
Solution
import numpy as np
rng = np.random.default_rng(3)
m = 30
c = rng.normal(size=m)
A = np.column_stack([c, 2 * c, rng.normal(size=m)]) # col 2 = 2*col 1: rank 2
b = rng.normal(size=m)
# Pseudo-inverse via SVD (drops the zero singular value).
U, s, Vt = np.linalg.svd(A, full_matrices=False)
tol = 1e-10
s_inv = np.where(s > tol, 1.0 / s, 0.0)
A_plus = Vt.T @ np.diag(s_inv) @ U.T
x_pinv = A_plus @ b
# It minimizes the residual: the residual is orthogonal to col(A).
r = b - A @ x_pinv
assert np.allclose(A.T @ r, 0.0, atol=1e-8)
# Any other minimizer adds a null-space vector; that only grows the norm.
null = Vt[-1] # right singular vector with sigma ~ 0
x_other = x_pinv + 0.7 * null
assert np.allclose(A @ x_other, A @ x_pinv, atol=1e-8) # same fit
assert np.linalg.norm(x_pinv) < np.linalg.norm(x_other) # pinv is minimum-norm
print("pinv norm:", round(float(np.linalg.norm(x_pinv)), 4),
"other:", round(float(np.linalg.norm(x_other)), 4))Adding any multiple of the null-space direction leaves the fit untouched but strictly increases , so the pseudo-inverse solution is the unique minimum-norm minimizer.
With overdetermined systems handled, the next lesson builds the calculus for differentiating expressions involving matrices, the prerequisite for understanding backpropagation.
References
- [1]Trefethen, L. N., & Bau, D. (1997). Numerical Linear Algebra. SIAM. The normal equations, the projection view, and the QR route to least squares (Lectures 11, 18-19).↩
- [2]Golub, G. H., & Van Loan, C. F. (2013). Matrix Computations (4th ed.). Johns Hopkins University Press. The Moore-Penrose pseudo-inverse and its SVD construction (doi:10.56021/9781421407944).↩