KBKnowledge Base
Linear Algebra for ML · 1.14

QR Decomposition & Gram-Schmidt

Orthogonalizing vectors, and the numerically stable way to do least squares.

On this page
In plain English — beginner to advanced

Beginner: orthogonalization means turning a set of vectors into a set that all point perpendicular to each other, without changing what they span (section 1.9). The classic recipe for doing this is the Gram-Schmidt process: take each new vector, subtract off whatever part of it points along the directions you already have, and keep only what's left over — which is automatically perpendicular to everything before it.

Intermediate: QR decomposition packages this idea into a matrix factorization: any matrix A can be written as A = QR, where Q has orthonormal columns (the Gram-Schmidt-ed, unit-length version of A's columns) and R is upper-triangular (recording exactly how much of each original column was "redundant" with the earlier ones).

Advanced: QR gives a numerically superior way to solve least-squares regression compared to the normal equation from section 1.6 (w = (XᵀX)⁻¹Xᵀy). Forming XᵀX squares the condition number of X, amplifying numerical error; solving via QR avoids that squaring entirely, which is why serious statistical software defaults to a QR-based solver rather than the textbook formula.

Formula
A=QRQTQ=IA = QR \qquad Q^TQ = I

Gram-Schmidt, step by step, for vectors a₁, a₂: e₁ = a₁/‖a₁‖, then e₂ = (a₂ − (a₂·e₁)e₁), normalized. Each new vector only ever has the previous directions subtracted out.

Derivation: why the leftover piece is always perpendicular

Given a fixed direction e₁ (unit length) and a new vector a₂, define the projection coefficient c = a₂·e₁ and subtract it off:

u=a2ce1,c=a2e1\vec{u} = \vec{a}_2 - c\,\vec{e}_1, \qquad c = \vec{a}_2\cdot\vec{e}_1

Claim: u is exactly perpendicular to e₁. Proof: compute the dot product directly:

ue1=a2e1c(e1e1)=cc1=0\vec{u}\cdot\vec{e}_1 = \vec{a}_2\cdot\vec{e}_1 - c(\vec{e}_1\cdot\vec{e}_1) = c - c\cdot 1 = 0

(using e₁·e₁ = ‖e₁‖² = 1, since e₁ is a unit vector). This holds for any choice of a₂ — c is defined precisely so this cancellation always happens, which is exactly the live check shown in the diagram's readout.

Where this is used: repeating this subtraction against every previously built direction, one at a time, is the entire Gram-Schmidt algorithm — each new vector is only ever guaranteed perpendicular to what came immediately before it, which is exactly why the process must go through the vectors in order.

Watch Gram-Schmidt strip out the redundant part

Blue is the fixed reference direction. Drag orange — grey dashed is the projection being removed, green is what's left: always exactly perpendicular to blue.

Practical example — QR decomposition and stable least squares

np.linalg.lstsq is what you should reach for in practice — but knowing it's doing QR (not the textbook normal equation) explains why it's the more numerically trustworthy choice.

python
import numpy as np

X = np.array([[1., 1.], [1., 2.], [1., 3.], [1., 4.]])   # design matrix
y = np.array([2., 4., 5., 8.])

Q, R = np.linalg.qr(X)
print(np.allclose(Q.T @ Q, np.eye(2)))   # True -> Q's columns are orthonormal

# Solve the least-squares fit via QR instead of the fragile normal equation:
w = np.linalg.solve(R, Q.T @ y)
print(w)   # intercept, slope

# NumPy's own least-squares solver uses this same idea internally:
w2, *_ = np.linalg.lstsq(X, y, rcond=None)
print(np.allclose(w, w2))   # True
Real-world examples
  • Robotics and computer graphics use QR (or the related Householder reflections) to keep a sequence of rotation matrices numerically "clean" — repeated multiplication of rotation matrices slowly accumulates floating-point drift away from true orthogonality, and re-orthogonalizing via QR fixes it.
  • Every serious linear regression / least-squares solver (R's lm(), scikit-learn's LinearRegression, NumPy's lstsq) uses QR or SVD internally instead of the raw normal equation.
Common mistakes
  • Implementing "classical" Gram-Schmidt naively for many vectors — it's numerically unstable in practice; real libraries use modified Gram-Schmidt or Householder reflections, which are mathematically equivalent but far more stable in floating point.
  • Solving least squares via (XᵀX)⁻¹Xᵀy directly in code that matters — prefer np.linalg.lstsq or an explicit QR/SVD-based solve.
Going deeper

QR decomposition isn't just for least squares — the QR algorithm (repeatedly factoring a matrix as QR, then multiplying the factors back together in reverse order, and iterating) is the actual method general-purpose numerical libraries use to compute eigenvalues (section 1.7) for matrices larger than 2×2 or 3×3. The "solve the characteristic polynomial" method taught in school is essentially never used in real software — it's numerically unreliable for anything but the smallest matrices.

At the master level: the columns of Q form an orthonormal basis for the same column space as A — meaning QR is simultaneously an orthogonalization procedure, a rank-revealing factorization, and (via the QR algorithm) an eigenvalue solver, which is a lot of mileage from one relatively simple idea.

Newsletter

Stay in the loop

Subscribe to get new docs, diagrams, and engineering write-ups by Dharaneesh Boobalan delivered to your inbox.

  • Deep-dive write-ups on ML, inference, and systems.
  • New Draw.io diagrams & interactive canvases.
  • Agentic patterns and rocket-science notes.
  • No spam. One tasteful email when there's something new.

Crafted by Dharaneesh Boobalan

Newsletter

Get new docs, diagrams, and write-ups in your inbox.

We never share your details. Unsubscribe anytime.