AI Knowledge Tree

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 {q1,…,qk}\{q_1, \dots, q_k\} is orthonormal if

⟨qi,qj⟩=δij={1i=j0i≠j.\langle q_i, q_j\rangle = \delta_{ij} = \begin{cases} 1 & i = j \\ 0 & i \neq j \end{cases}.

Every orthonormal set is automatically linearly independent: if ∑ciqi=0\sum c_i q_i = 0, take the inner product with qjq_j: cj=⟨∑ciqi,qj⟩=0c_j = \langle \sum c_i q_i, q_j\rangle = 0. So an orthonormal set of nn vectors in Rn\mathbb{R}^n is a basis, called an orthonormal basis.

In an orthonormal basis, coordinates are inner products: if v=∑ixiqiv = \sum_i x_i q_i, then xj=⟨v,qj⟩x_j = \langle v, q_j\rangle. No linear system to solve: the coordinate in each direction is just the projection onto that direction.

Projection onto a subspace

Given a subspace W⊆VW \subseteq V with orthonormal basis {q1,…,qk}\{q_1, \dots, q_k\}, the orthogonal projection of vv onto WW is

proj⁡Wv=∑i=1k⟨v,qi⟩ qi.\operatorname{proj}_W v = \sum_{i=1}^k \langle v, q_i\rangle\, q_i.

The residual v−proj⁡Wvv - \operatorname{proj}_W v is orthogonal to every qiq_i (check: ⟨v−proj⁡Wv,qj⟩=⟨v,qj⟩−⟨v,qj⟩=0\langle v - \operatorname{proj}_W v, q_j\rangle = \langle v, q_j\rangle - \langle v, q_j\rangle = 0), hence orthogonal to all of WW.

The Gram–Schmidt algorithm

Given a basis {v1,…,vn}\{v_1, \dots, v_n\}, produce an orthonormal basis {q1,…,qn}\{q_1, \dots, q_n\} spanning the same space by the following procedure[1]:

Step kk (for k=1,2,…,nk = 1, 2, \dots, n):

  1. Subtract from vkv_k its projection onto the previously built orthonormal vectors: q~k=vk−∑i=1k−1⟨vk,qi⟩ qi.\tilde{q}_k = v_k - \sum_{i=1}^{k-1} \langle v_k, q_i\rangle\, q_i.
  2. Normalize: qk=q~k/∥q~k∥q_k = \tilde{q}_k / \lVert \tilde{q}_k\rVert.

The residual q~k\tilde{q}_k is nonzero because vkv_k is independent of {v1,…,vk−1}\{v_1, \dots, v_{k-1}\}, so it cannot lie entirely in their span.

After nn steps, {q1,…,qn}\{q_1, \dots, q_n\} is orthonormal and span⁡(q1,…,qk)=span⁡(v1,…,vk)\operatorname{span}(q_1, \dots, q_k) = \operatorname{span}(v_1, \dots, v_k) for every kk.

Connection to QR

Writing out the Gram–Schmidt process in matrix form: each vjv_j is a combination of q1,…,qjq_1, \dots, q_j with coefficients rij=⟨vj,qi⟩r_{ij} = \langle v_j, q_i\rangle (for i<ji < j) and rjj=∥q~j∥r_{jj} = \lVert \tilde{q}_j\rVert. Stack into matrices:

A=QR,A = Q R,

where A=[v1∣⋯∣vn]A = [v_1 \mid \cdots \mid v_n], Q=[q1∣⋯∣qn]Q = [q_1 \mid \cdots \mid q_n], and RR is upper-triangular with positive diagonal. This is the QR factorization[2].

Orthogonal matrices as isometries

A square matrix QQ with Q⊤Q=IQ^\top Q = I is called orthogonal. Its columns are orthonormal. Three equivalent facts:

  1. Q⊤=Q−1Q^\top = Q^{-1}: the inverse is free (just transpose).
  2. QQ preserves inner products: ⟨Qu,Qv⟩=(Qu)⊤(Qv)=u⊤Q⊤Qv=u⊤v\langle Qu, Qv\rangle = (Qu)^\top(Qv) = u^\top Q^\top Q v = u^\top v.
  3. QQ preserves lengths: ∥Qu∥=∥u∥\lVert Qu\rVert = \lVert u\rVert.

Preserving lengths means QQ 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 Σ\Sigma) from pure rotation (U,V⊤U, V^\top).

Gram–Schmidt orthogonalizes a basis by subtracting from each vector its projection onto the previous ones, leaving a perpendicular remainder.
Gram–Schmidt orthogonalizes a basis by subtracting from each vector its projection onto the previous ones, leaving a perpendicular remainder.

Check it on arrays

python · numpy
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))   # same

The Q⊤QQ^\top Q block comes out as the identity (up to float error), and RR is upper triangular.

Exercises

Exercise 1. Apply Gram–Schmidt by hand to v1=(3,0)v_1 = (3, 0) and v2=(1,2)v_2 = (1, 2) to produce an orthonormal basis {q1,q2}\{q_1, q_2\} of R2\mathbb{R}^2.

Solution

Step 1. Normalize v1v_1: ∥v1∥=3\lVert v_1\rVert = 3, so q1=(1,0)q_1 = (1, 0).

Step 2. Subtract from v2v_2 its projection onto q1q_1: ⟨v2,q1⟩=1\langle v_2, q_1\rangle = 1, so q~2=v2−1⋅q1=(1,2)−(1,0)=(0,2)\tilde q_2 = v_2 - 1\cdot q_1 = (1, 2) - (1, 0) = (0, 2).

Step 3. Normalize: ∥q~2∥=2\lVert \tilde q_2\rVert = 2, so q2=(0,1)q_2 = (0, 1). The orthonormal basis is {(1,0), (0,1)}\{(1, 0),\ (0, 1)\}, and indeed ⟨q1,q2⟩=0\langle q_1, q_2\rangle = 0.

Exercise 2. Prove that if QQ is orthogonal, then det⁡Q=±1\det Q = \pm 1. (Use the product and transpose rules for determinants from the determinants lesson.)

Solution

Step 1. Orthogonality means Q⊤Q=IQ^\top Q = I. Take determinants of both sides: det⁡(Q⊤Q)=det⁡I=1\det(Q^\top Q) = \det I = 1.

Step 2. Apply the product rule det⁡(Q⊤Q)=det⁡(Q⊤)det⁡(Q)\det(Q^\top Q) = \det(Q^\top)\det(Q) and the transpose rule det⁡(Q⊤)=det⁡(Q)\det(Q^\top) = \det(Q), giving (det⁡Q)2=1(\det Q)^2 = 1.

Step 3. So det⁡Q=±1\det Q = \pm 1. The sign distinguishes rotations (det⁡Q=+1\det Q = +1, orientation preserved) from reflections (det⁡Q=−1\det Q = -1), consistent with QQ 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 5×45\times 4 matrix, and assert the output columns are orthonormal.

Solution
python · numpy
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))

Q⊤QQ^\top Q 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. [1]Trefethen, L. N., & Bau, D. (1997). Numerical Linear Algebra. SIAM. Gram-Schmidt orthogonalization and its numerical stability (Lectures 7-8).↩
  2. [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).↩