Orthonormal Basis
A basis gives you coordinates. Chapter 2 showed how: to express in a basis , solve . That solve is the price of a general basis, and it is a real price — it costs , it needs the basis to be well conditioned, and it has to be redone from scratch every time you add a basis vector.
An orthonormal basis removes the solve entirely. Coordinates become inner products, one per basis vector, computed independently. That is the whole point of this page, and it is why every serious numerical routine in the rest of the book — QR, eigendecomposition, SVD, PCA — hands you an orthonormal basis rather than any other kind.
What you’ll learn
Section titled “What you’ll learn”- Definition 3.9, and the two conditions in it: pairwise orthogonal and unit length.
- Why is the compact form of both conditions at once.
- Coordinates as inner products, and Parseval’s identity — the squared coordinates add up to the squared length.
- The Gram-Schmidt construction, and the book’s alternative route through Gaussian elimination on .
- The measured reason nobody implements textbook Gram-Schmidt: at condition number it loses orthogonality completely, while Householder QR does not budge from machine precision.
Intuition: a coordinate system where the axes do not lean on each other
Section titled “Intuition: a coordinate system where the axes do not lean on each other”Think about reading coordinates off a graph. If the axes are perpendicular and marked in equal units, reading a point’s -coordinate is a single glance: drop a line straight down. Neither axis tells you anything about the other, so the two readings are independent.
Now skew the axes so they meet at . Dropping a line “straight down” no longer gives the -coordinate, because moving along the second axis also changes your first coordinate. You have to solve for both at once, and the closer the axes get to parallel the more the two readings interfere — which is exactly what an ill-conditioned basis is.
An orthonormal basis is the first picture, in dimensions.
flowchart TD B["a basis (b1, …, bn)"] B --> GEN["general basis:
coordinates need a SOLVE
Bλ = x, O(n³)"] B --> ORTH["pairwise orthogonal:
⟨bi, bj⟩ = 0 for i ≠ j"] ORTH --> UNIT["plus unit length:
⟨bi, bi⟩ = 1"] UNIT --> ONB["ORTHONORMAL BASIS
Def 3.9"] ONB --> GRAM["BᵀB = I
both conditions in one equation"] GRAM --> COORD["coordinates are inner products:
λ = Bᵀx, O(n²), no solve"] GRAM --> PARS["Parseval: Σ λi² = ‖x‖²"] GRAM --> INV["B⁻¹ = Bᵀ, so nothing is ever inverted"] COORD --> PROJ["projection collapses to BBᵀx
§3.8"]
The rightmost consequence is the one §3.8 will cash in: the general projection formula collapses to when the basis is orthonormal, because the middle factor becomes the identity.
The math
Section titled “The math”Equation 3.34 says every basis vector has unit length, since .
One equation instead of two
Section titled “One equation instead of two”Stack the basis vectors as the columns of . Then the entry of is exactly , so Equations 3.33 and 3.34 together say
That matrix is called the Gram matrix of the basis, and “the basis is orthonormal” and “its Gram matrix is the identity” are the same statement. For a square this also makes it an orthogonal matrix in the sense of Definition 3.8, so .
Coordinates become inner products
Section titled “Coordinates become inner products”For a general basis, the coordinates of satisfy , so . When the basis is orthonormal the middle factor is and this collapses to
Read the second form carefully. Each coordinate is computed from its own basis vector alone. No solve, no interference between coordinates, and — the part that matters for truncation — adding an -th orthonormal direction does not change any of the first coordinates. That is why Fourier series, wavelets and PCA all truncate gracefully and a general basis expansion does not.
Parseval’s identity
Section titled “Parseval’s identity”Expand and there it is. Under a non-orthonormal basis the middle factor is not the identity and the identity simply fails — measured below, the gap is on a vector of squared length , which is a error rather than a rounding difference.
Gram-Schmidt
Section titled “Gram-Schmidt”The construction is one projection per step, so it belongs to §3.8; the book states it there and this page uses it. Two properties worth naming:
- The result depends on the order. is untouched, so whichever vector you list first survives unchanged. Different orders give different orthonormal bases of the same subspace.
- The span never changes. Every is minus a combination of earlier vectors, so nothing leaves the subspace and nothing new enters it.
The book’s other route
Section titled “The book’s other route”The book also mentions obtaining an ONB by applying Gaussian elimination to the augmented matrix , where has the spanning vectors as columns. It is the same computation with the projections done all at once rather than one at a time, and it inherits the same numerical weakness — forming squares the condition number before any elimination happens.
Worked example by hand
Section titled “Worked example by hand”Example 3.8 from the book
Section titled “Example 3.8 from the book”| check | working | result |
|---|---|---|
| — orthogonal | ||
| — unit length | ||
| — unit length |
So and this is an ONB. Now use it: take .
Two independent inner products, no system solved. Reconstruct to check: . ✓
And Parseval: , while . ✓
The book’s Exercise 3.8, worked by Gram-Schmidt
Section titled “The book’s Exercise 3.8, worked by Gram-Schmidt”Turn , into an ONB of the plane they span.
Step 1. .
Step 2. Subtract the projection of onto :
Confirm orthogonality: . ✓
Step 3. Normalise. and , so
The cancels: .
import numpy as np
# Example 3.8
b1 = np.array([1.0, 1.0]) / np.sqrt(2)
b2 = np.array([1.0, -1.0]) / np.sqrt(2)
B = np.stack([b1, b2], axis=1)
print("B^T B =\n", np.round(B.T @ B, 15))
x = np.array([3.0, -1.0])
lam = B.T @ x # coordinates: two inner products, no solve
print("coordinates:", np.round(lam, 6))
print("reconstruct:", np.round(B @ lam, 6))
print("Parseval: |x|^2 =", float(x @ x), " sum lam^2 =", float((lam ** 2).sum()))
# Exercise 3.8: Gram-Schmidt
p1 = np.array([1.0, 1.0, 1.0])
p2 = np.array([-1.0, 2.0, 0.0])
u1 = p1.copy()
coeff = float(u1 @ p2) / float(u1 @ u1)
u2 = p2 - coeff * u1
print("coefficient:", coeff, " u2 =", u2, " <u1,u2> =", float(u1 @ u2))
c1, c2 = u1 / np.linalg.norm(u1), u2 / np.linalg.norm(u2)
print("||u2|| =", np.linalg.norm(u2), " sqrt(42)/3 =", np.sqrt(42) / 3)
print("c2 * sqrt(42) =", np.round(c2 * np.sqrt(42), 6))
C = np.stack([c1, c2], axis=1)
print("C^T C =\n", np.round(C.T @ C, 15))B^T B =
[[ 1. -0.]
[-0. 1.]]
coordinates: [1.414214 2.828427]
reconstruct: [ 3. -1.]
Parseval: |x|^2 = 10.0 sum lam^2 = 9.999999999999998
coefficient: 0.3333333333333333 u2 = [-1.33333333 1.66666667 -0.33333333] <u1,u2> = 1.6653345369377348e-16
||u2|| = 2.1602468994692865 sqrt(42)/3 = 2.160246899469287
c2 * sqrt(42) = [-4. 5. -1.]
C^T C =
[[1. 0.]
[0. 1.]]Note the two residues of floating point. Parseval gives rather than , and is rather than . Both are one or two units in the last place — the correct amount of disagreement for a computation involving a square root, not an error to chase.
See it move
Section titled “See it move”The first sketch is the skewed-axes intuition, made numeric. Drag the second basis vector towards the first and watch the coordinate readings interfere; drag it perpendicular and they stop.
The second sketch is Gram-Schmidt with a draggable input. The grey shadow is the part being subtracted.
And the stepped version, on the book’s own Exercise 3.8:
Two vectors spanning a plane in three dimensions, turned into an orthonormal pair. The final frame shows the Gram matrix, which is the identity.
From scratch
Section titled “From scratch”Two implementations that differ by one word, and the difference decides whether the output is usable:
import numpy as np
def classical_gs(A):
"""Every coefficient taken against the ORIGINAL column."""
n, m = A.shape
Q = np.zeros((n, m))
for j in range(m):
v = A[:, j].copy()
for i in range(j):
v = v - (Q[:, i] @ A[:, j]) * Q[:, i] # <- A[:, j]
Q[:, j] = v / np.linalg.norm(v)
return Q
def modified_gs(A):
"""Every coefficient taken against the RUNNING remainder."""
n, m = A.shape
Q = np.zeros((n, m))
for j in range(m):
v = A[:, j].copy()
for i in range(j):
v = v - (Q[:, i] @ v) * Q[:, i] # <- v
Q[:, j] = v / np.linalg.norm(v)
return Q
def conditioned(n, kappa, seed=5):
"""A matrix with exactly the requested condition number."""
rng = np.random.default_rng(seed)
U, _ = np.linalg.qr(rng.normal(size=(n, n)))
V, _ = np.linalg.qr(rng.normal(size=(n, n)))
return U @ np.diag(np.logspace(0.0, -np.log10(kappa), n)) @ V.T
n = 8
print(f"{'kappa':>8} {'classical':>12} {'modified':>12} {'Householder':>12}")
for kappa in (1e2, 1e6, 1e10, 1e14):
A = conditioned(n, kappa)
row = []
for Q in (classical_gs(A), modified_gs(A), np.linalg.qr(A)[0]):
row.append(np.linalg.norm(Q.T @ Q - np.eye(n), 2))
print(f"{kappa:8.0e} {row[0]:12.3e} {row[1]:12.3e} {row[2]:12.3e}")
print("machine epsilon:", np.finfo(float).eps) kappa classical modified Householder
1e+02 1.152e-13 9.622e-15 7.239e-16
1e+06 3.063e-06 9.114e-11 7.334e-16
1e+10 1.013e+00 2.507e-07 5.774e-16
1e+14 2.993e+00 2.906e-03 1.052e-15
machine epsilon: 2.220446049250313e-16Read the third row. At condition number , classical Gram-Schmidt returns a matrix whose is — the columns are not approximately orthonormal, they are not orthonormal at all. At it reaches , and since that means the error is three times the size of the thing it was supposed to be.
Modified Gram-Schmidt — the same algorithm with the projection coefficient taken against the running
remainder rather than the original column — is seven orders of magnitude better at .
Householder QR, which is what np.linalg.qr runs, never leaves machine precision at all: its worst
value across the whole sweep is , under five units in the last place.
The mathematics of §3.8.3 is exact. The arithmetic is not, and this is the gap.
On real data
Section titled “On real data”Reading the plot
Section titled “Reading the plot”From the orthogonality plot. Three things.
The classical curve tracks — the dashed reference — rather than . Squaring the condition number is what forming inner products against the original columns costs you, and it is the same squaring that makes the book’s route inadvisable in floating point: has the square of ‘s condition number before elimination even starts.
The classical curve then saturates near and stops rising. That is not the error levelling off, it is the error running out of room: cannot grow much past a small constant once the columns bear no relation to orthonormality. A flat tail here means total failure, not stability.
Householder is flat at the bottom, and flat for a different reason: it never forms an inner product between two nearly-parallel columns. It builds as a product of reflections, each of which is exactly orthogonal by construction, so the orthogonality of the result does not depend on the conditioning of the input at all.
From the Parseval bars. Both panels report a reconstruction error at machine precision, which is the control: both bases genuinely represent the vector, and neither expansion is wrong. What differs is whether the coordinates are interpretable. Under the ONB the squared coordinates sum to , which is to within . Under the skewed basis they sum to — nearly double — so any statement of the form “this component accounts for of the energy” is meaningless in that basis. The skewed basis here has condition number only , which is mild; the failure is not a numerical one, it is that Parseval simply is not true off an ONB.
That is the property PCA’s variance-explained percentages rest on, and it is why they are quoted for principal components and never for the columns of an arbitrary factor loading matrix.
Pitfalls
Section titled “Pitfalls”Compare
Section titled “Compare”| basis type | Gram matrix | coordinates of | cost | Parseval? |
|---|---|---|---|---|
| general | full, symmetric positive definite | solve | no | |
| orthogonal | diagonal | , no solve | with weights | |
| orthonormal | , no solve | yes |
-
What does an orthonormal basis buy you that a general basis does not?
It is not unique — Gram-Schmidt's answer depends on the input order, and QR's column signs are arbitrary. The span is identical by construction. What changes is that B-transpose B is the identity, so the middle factor of the coordinate formula disappears.
pch.quizShowAnswer
B — Coordinates become independent inner products rather than the solution of a system, so adding a new orthonormal direction leaves every earlier coordinate untouched — which is what makes truncation graceful — It is not unique — Gram-Schmidt's answer depends on the input order, and QR's column signs are arbitrary. The span is identical by construction. What changes is that B-transpose B is the identity, so the middle factor of the coordinate formula disappears.
-
Classical Gram-Schmidt at condition number 1e10 gives ||Q-transpose Q minus I|| = 1.013. What does that number mean?
At 1e14 it reaches 2.993, three times the norm of the identity. The curve saturating near 1 is the error running out of room rather than levelling off. Householder QR on the same matrices never exceeds 1.052e-15.
pch.quizShowAnswer
B — Orthogonality is completely gone — the identity matrix itself has 2-norm 1, so an error of 1.013 is the same size as the thing being approximated — At 1e14 it reaches 2.993, three times the norm of the identity. The curve saturating near 1 is the error running out of room rather than levelling off. Householder QR on the same matrices never exceeds 1.052e-15.
-
Parseval's identity fails for the skewed basis in the figure, but that basis reconstructs the vector to machine precision. What is the lesson?
Its condition number is 6.79, which is mild, and the reconstruction error is 2.16e-15. The failure is structural rather than numerical: the squared coordinates sum to 11.53 against a squared length of 5.85.
pch.quizShowAnswer
B — Both expansions are correct; only the orthonormal one has coordinates whose squares are interpretable as shares of the total energy — which is why variance-explained percentages are quoted for principal components and not for arbitrary loadings — Its condition number is 6.79, which is mild, and the reconstruction error is 2.16e-15. The failure is structural rather than numerical: the squared coordinates sum to 11.53 against a squared length of 5.85.
-
A basis satisfies only Equation 3.33 — pairwise orthogonal, but not unit length. What breaks?
The Gram matrix is diagonal rather than the identity, so the middle factor of the coordinate formula is a diagonal inverse rather than nothing. The division appears explicitly in the Gram-Schmidt formula for exactly this reason.
pch.quizShowAnswer
B — The coordinate formula: lambda-i is the inner product divided by the squared length of the basis vector, not the plain inner product, so B-transpose x is wrong by a per-coordinate factor — The Gram matrix is diagonal rather than the identity, so the middle factor of the coordinate formula is a diagonal inverse rather than nothing. The division appears explicitly in the Gram-Schmidt formula for exactly this reason.
🧪 Try It Yourself
Section titled “🧪 Try It Yourself”Exercise 1 – Verify Example 3.8 and use it
Section titled “Exercise 1 – Verify Example 3.8 and use it”Exercise 2 – Gram-Schmidt on the book’s Exercise 3.8
Section titled “Exercise 2 – Gram-Schmidt on the book’s Exercise 3.8”Exercise 3 – Watch classical Gram-Schmidt fail
Section titled “Exercise 3 – Watch classical Gram-Schmidt fail”Exercise 4 – Break Parseval
Section titled “Exercise 4 – Break Parseval”Exercise 5 – Order matters
Section titled “Exercise 5 – Order matters”Recall card
Section titled “Recall card”- An orthonormal basis is pairwise orthogonal and unit length — Definition 3.9’s two equations, which together say the Gram matrix B-transpose B is the identity.
- Coordinates become inner products: lambda equals B-transpose x, one independent inner product per basis vector, with no system to solve.
- Adding a new orthonormal direction leaves every earlier coordinate unchanged, which is what makes truncation graceful in Fourier series, wavelets and PCA.
- Parseval’s identity holds only for an orthonormal basis: the squared coordinates add up to the squared length. Measured off an ONB the sum was 11.53 against a squared length of 5.85, on a well-conditioned basis.
- Gram-Schmidt subtracts the projection onto everything already accepted and normalises. Its output depends on the input order, because the first vector survives unchanged.
- Do not implement classical Gram-Schmidt. At condition number ten to the tenth it returns columns that are not orthonormal at all; modified Gram-Schmidt is seven orders better and Householder QR never leaves machine precision.
- Forming B-transpose B squares the condition number, which is why both classical Gram-Schmidt and the augmented-matrix route lose accuracy where a Householder factorisation does not.
- The projection formula collapses to B B-transpose for an orthonormal basis, and that collapse is what section 3.8 spends most of its time earning.
Next: Orthogonal Complement — what happens to everything the basis misses.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading