Orthonormal bases & Gram–Schmidt
Construct an orthonormal basis by Gram–Schmidt, prove orthogonal matrices are isometries, and connect the result to the QR factorization.
Working in an orthonormal basis turns inner products into dot products, projections into trivial formulas, and matrix inversions into transposes. Gram–Schmidt is the algorithm that manufactures one from any basis.
Orthonormal sets
A set of vectors is orthonormal if
Every orthonormal set is automatically linearly independent: if , take the inner product with : . So an orthonormal set of vectors in is a basis, called an orthonormal basis.
In an orthonormal basis, coordinates are inner products: if , then . No linear system to solve: the coordinate in each direction is just the projection onto that direction.
Projection onto a subspace
Given a subspace with orthonormal basis , the orthogonal projection of onto is
The residual is orthogonal to every (check: ), hence orthogonal to all of .
The Gram–Schmidt algorithm
Given a basis , produce an orthonormal basis spanning the same space by the following procedure[1]:
Step (for ):
- Subtract from its projection onto the previously built orthonormal vectors:
- Normalize: .
The residual is nonzero because is independent of , so it cannot lie entirely in their span.
After steps, is orthonormal and for every .
Connection to QR
Writing out the Gram–Schmidt process in matrix form: each is a combination of with coefficients (for ) and . Stack into matrices:
where , , and is upper-triangular with positive diagonal. This is the QR factorization[2].
Orthogonal matrices as isometries
A square matrix with is called orthogonal. Its columns are orthonormal. Three equivalent facts:
- : the inverse is free (just transpose).
- preserves inner products: .
- preserves lengths: .
Preserving lengths means is an isometry: it rotates or reflects, but never stretches or compresses. This is the reason orthogonal matrices appear in the SVD, where we want to separate pure stretching (diagonal ) from pure rotation ().
Check it on arrays
import numpy as np
def gram_schmidt(A):
"""Orthonormalize columns of A via Gram-Schmidt."""
Q = np.zeros_like(A, dtype=float)
for j in range(A.shape[1]):
q = A[:, j].astype(float)
for i in range(j):
q -= np.dot(q, Q[:, i]) * Q[:, i]
Q[:, j] = q / np.linalg.norm(q)
return Q
A = np.array([
[1.0, 1.0, 0.0],
[1.0, 0.0, 1.0],
[0.0, 1.0, 1.0],
])
Q = gram_schmidt(A)
# Check orthonormality: Q^T Q should be I
print("Q^T Q:\n", np.round(Q.T @ Q, 10))
# Check same span: each column of A in span of columns of Q
# (the change-of-basis matrix R should be upper triangular)
R = Q.T @ A
print("R (upper triangular):\n", np.round(R, 10))
# Isometry: lengths preserved
v = np.array([1.0, 2.0, 3.0])
print("||v|| =", np.linalg.norm(v))
print("||Qv|| =", np.linalg.norm(Q @ v)) # sameThe block comes out as the identity (up to float error), and is upper triangular.
Exercises
Exercise 1. Apply Gram–Schmidt by hand to and to produce an orthonormal basis of .
Solution
Step 1. Normalize : , so .
Step 2. Subtract from its projection onto : , so .
Step 3. Normalize: , so . The orthonormal basis is , and indeed .
Exercise 2. Prove that if is orthogonal, then . (Use the product and transpose rules for determinants from the determinants lesson.)
Solution
Step 1. Orthogonality means . Take determinants of both sides: .
Step 2. Apply the product rule and the transpose rule , giving .
Step 3. So . The sign distinguishes rotations (, orientation preserved) from reflections (), consistent with being a length-preserving isometry.
Exercise 3 (code). Modified Gram–Schmidt subtracts each projection immediately as it is computed rather than all at once, which is numerically more stable. Implement it for the columns of a matrix, run it on a random matrix, and assert the output columns are orthonormal.
Solution
import numpy as np
def modified_gram_schmidt(A):
A = A.astype(float).copy()
n = A.shape[1]
Q = np.zeros_like(A)
for j in range(n):
q = A[:, j].copy()
for i in range(j):
q -= np.dot(Q[:, i], q) * Q[:, i] # subtract as we go
Q[:, j] = q / np.linalg.norm(q)
return Q
rng = np.random.default_rng(0)
A = rng.normal(size=(5, 4))
Q = modified_gram_schmidt(A)
# Columns orthonormal: Q^T Q is the 4x4 identity.
assert np.allclose(Q.T @ Q, np.eye(4))
print("max off-diagonal:", round(np.abs(Q.T @ Q - np.eye(4)).max(), 12))comes out as the identity, so the four output columns are an orthonormal set spanning the same space as the input columns.
The orthonormal basis in hand, the next lesson turns to determinants, which measure how a linear map stretches or collapses volume.
References
- [1]Trefethen, L. N., & Bau, D. (1997). Numerical Linear Algebra. SIAM. Gram-Schmidt orthogonalization and its numerical stability (Lectures 7-8).↩
- [2]Golub, G. H., & Van Loan, C. F. (2013). Matrix Computations (4th ed.). Johns Hopkins University Press. The QR factorization and orthogonal matrices (doi:10.56021/9781421407944).↩