The conjugate gradient method
Solve symmetric positive-definite systems with conjugate gradient and understand its Krylov structure.
For a large symmetric positive-definite system , direct factorization costs and stores a dense factor. Conjugate gradient (CG) solves it iteratively using only matrix-vector products, finishing in at most steps in exact arithmetic and far fewer in practice[1]. It is the workhorse for sparse SPD systems and the template for Krylov methods.
CG as minimizing a quadratic
For SPD , solving is equivalent to minimizing the quadratic
whose gradient is (zero exactly at the solution). CG generates iterates each of which is the exact minimizer of over a growing Krylov subspace, using only the gradients (residuals) .
-conjugate directions
Two directions and are conjugate with respect to if , the -inner-product orthogonality. CG builds an -conjugate set of search directions: each is -orthogonal to all previous ones.
The payoff: minimizing along -conjugate directions one at a time is the same as minimizing it over their span. There is no need to revisit a previous direction once you have stepped along it. After steps, is the exact minimizer in the -dimensional subspace spanned by the first directions.
The CG update
Starting from with and , each step does:
- Step length along the current direction that minimizes :
- Update and .
- New direction -conjugate to all previous:
The cost per iteration is one matrix-vector product and a handful of vector operations: memory plus the cost of multiplying by , which for sparse scales with the number of nonzeros.
Krylov subspaces and finite termination
The space spanned by is the Krylov subspace . CG's minimizes over . In exact arithmetic, the dimension of the Krylov subspace grows by one each step until it contains the true solution, so CG terminates in at most steps with [2].
Convergence rate
In practice CG converges much faster than when the eigenvalues of cluster. With condition number , the error satisfies
a rate of like Nesterov's acceleration. Preconditioning (solving for a cheap ) shrinks the effective condition number and is what makes CG practical for ill-conditioned systems[3].
Check it on arrays
import numpy as np
rng = np.random.default_rng(0)
# Random SPD system of moderate condition number.
n = 200
B = rng.normal(size=(n, n))
A = B @ B.T + 0.5 * np.eye(n) # SPD
b = rng.normal(size=n)
def cg(A, b, tol=1e-10, max_iter=None):
x = np.zeros_like(b)
r = b - A @ x
p = r.copy()
rs_old = r @ r
history = []
for k in range(max_iter or len(b) + 5):
Ap = A @ p
alpha = rs_old / (p @ Ap)
x = x + alpha * p
r = r - alpha * Ap
rs_new = r @ r
history.append(np.sqrt(rs_new))
if rs_new < tol**2:
break
beta = rs_new / rs_old
p = r + beta * p
rs_old = rs_new
return x, history
x_cg, hist = cg(A, b)
x_true = np.linalg.solve(A, b)
print(f"CG converged in {len(hist)} iterations")
print(f"final residual: {hist[-1]:.2e}")
print(f"error vs direct solve: {np.linalg.norm(x_cg - x_true):.2e}")
print(f"finite-termination bound: <= n = {n}")CG reaches the solution in fewer iterations than the dimension when the spectrum is favorable, with residual decaying geometrically.
Exercises
Exercise 1. For the SPD system with , , and , compute the first CG step length and the iterate by hand.
Solution
Step 1. With , the initial residual is , and the first direction is .
Step 2. The step length is . The numerator is . For the denominator,
So .
Step 3. The new iterate is
This is the exact minimizer of along the line .
Exercise 2. With condition number , evaluate the CG error-reduction factor per step, and find how many steps drive the -norm error below of its start (ignore the leading factor of ).
Solution
Step 1. With , , so the per-step factor is
Step 2. The error bound shrinks like . Requiring and taking logs:
Step 3. The smallest integer is . A well-conditioned spectrum () gives a -rate of per step, so a handful of iterations suffices, far below the dimension .
Exercise 3 (code). Implement CG for a random SPD system and verify two properties: the residuals are mutually orthogonal, and the solution matches a direct solve.
Solution
import numpy as np
rng = np.random.default_rng(0)
n = 40
# Well-conditioned SPD matrix: eigenvalues spread evenly in [1, 10].
Q = np.linalg.qr(rng.normal(size=(n, n)))[0]
A = Q @ np.diag(np.linspace(1.0, 10.0, n)) @ Q.T
b = rng.normal(size=n)
x = np.zeros(n)
r = b - A @ x
p = r.copy()
rs_old = r @ r
residual_vecs = [r.copy()]
for _ in range(n):
Ap = A @ p
alpha = rs_old / (p @ Ap)
x = x + alpha * p
r = r - alpha * Ap
rs_new = r @ r
residual_vecs.append(r.copy())
if rs_new < 1e-20:
break
p = r + (rs_new / rs_old) * p
rs_old = rs_new
# Residuals are mutually orthogonal (a defining CG property). Check the early
# iterates, before the residuals shrink to the rounding floor.
R = np.array(residual_vecs[:10])
R = R / np.linalg.norm(R, axis=1, keepdims=True) # unit-normalize each
G = R @ R.T
off_diag = G - np.diag(np.diag(G))
assert np.max(np.abs(off_diag)) < 1e-8
x_true = np.linalg.solve(A, b)
assert np.linalg.norm(x - x_true) < 1e-6
print("max off-diagonal residual inner product:", np.max(np.abs(off_diag)))The off-diagonal Gram entries are negligible, confirming the residuals form an orthogonal set, which is the discrete reason CG never needs to revisit a direction and terminates in at most steps.
The next lesson generalizes Krylov solvers to nonsymmetric systems with GMRES.
References
- [1]Hestenes, M. R., & Stiefel, E. (1952). "Methods of conjugate gradients for solving linear systems". Journal of Research of the National Bureau of Standards, 49(6), 409-436. The original conjugate-gradient algorithm.↩
- [2]Trefethen, L. N., & Bau, D. (1997). Numerical Linear Algebra. SIAM. CG, Krylov subspaces, and finite termination (Lectures 38-40).↩
- [3]Saad, Y. (2003). Iterative Methods for Sparse Linear Systems (2nd ed.). SIAM. Preconditioned CG and the convergence bound (doi:10.1137/1.9780898718003).↩