Eigenvalues, power iteration, and the spectral radius
Overview
Section titled “Overview”| Module | M03.4 · build · Python · Pass 2 · 3 to 4 h |
| You build | python/tinyllm/linalg/eig.py: power_iteration(matvec, n, iters, rng) (the dominant eigenpair), inverse_iteration(A, shift, iters, rng) (the eigenpair nearest a shift, one LU and many solves), spectral_radius(W, iters) ( by the QR algorithm, complex eigenvalues included) |
| Contract | course/contracts/py/tinyllm/linalg/eig.pyi |
| Tests | course/tests/M03.4/test_eig.py (what they check: section 4) |
| Needs | M03.2 lu and lu_solve, M03.3 qr_householder (or --ref-deps) |
| Used by | later L0.5 (the training monitor reports of weight matrices), L3.1 (the exploding-gradient diagnostic), M10.5 (the top Hessian eigenvalue); M10.1 reads it for the condition number. No registered module calls it yet (section 6) |
| Milestone | MS-P2 (the foundations gate) |
| Optional depth | Trefethen and Bau, Numerical Linear Algebra, lectures 24 to 28 (eigenvalue problems, power and inverse iteration, the QR algorithm); Strang, Introduction to Linear Algebra, ch. 6; Pascanu, Mikolov, and Bengio, “On the difficulty of training recurrent neural networks” (2013) |
Key Takeaways
Section titled “Key Takeaways”- An eigenvector is a direction a matrix only stretches, ; repeated multiplication amplifies the eigenvalue of largest modulus fastest, so power iteration converges to it at the rate per step (
test_hand_example_power_iteration). - Return the Rayleigh quotient , not , and renormalize every step: the first loses the sign, the second overflows (
test_negative_dominant_eigenvalue_keeps_its_sign,test_renormalizes_every_step). - Inverse iteration runs power iteration on , whose dominant eigenvalue belongs to the nearest ; factor once with LU and solve at every step (
test_inverse_iteration_factors_once). - A real matrix’s dominant eigenvalues can be a complex pair (a rotation), where power iteration never converges. The QR algorithm exposes them as blocks, and the spectral radius reads off the blocks (
test_spectral_radius_of_a_rotation,test_spectral_radius_matches_numpy).
How to work this chapter
Section titled “How to work this chapter”ol start M03.4 # stubs python/tinyllm/linalg/eig.py into your repool tests M03.4 # read the test catalog first: rung R0, you write no tests hereol check M03.4 # exit code is the verdictol check M03.4 --ref-deps # only if your M03.2 or M03.3 is not passing yetol diff M03.4 # after passing: your code against the reference1. Why now
Section titled “1. Why now”Training a recurrent network (L3.1) multiplies the hidden state by the same weight matrix at every step, and backpropagation multiplies the gradient by the same number of times. Whether that product grows without bound or fades to nothing over 200 steps is decided by one number, the spectral radius : above 1 the gradients explode, below 1 they vanish. Your training monitor (L0.5) will print it for every weight matrix, and M10.5 uses the same machinery to find the largest curvature of the loss, which bounds the learning rate. numpy.linalg.eigvals would answer, but you are building the system yourself, and the method is simple: multiply, normalize, repeat. This module builds it, sees where it fails (rotations), and fixes that failure with the QR factorization from M03.3.
2. Principles
Section titled “2. Principles”| Symbol | Meaning | Type / shape |
|---|---|---|
| a real square matrix | float64[n, n] | |
| eigenvalues and eigenvectors: , | complex scalar, vector | |
| eigenvalues ordered by modulus | ||
| the spectral radius | scalar | |
| the unit vector after steps of an iteration | float64[n] | |
| the Rayleigh quotient of a unit vector | scalar | |
| a shift: a guess near the wanted eigenvalue | scalar | |
| the QR factorization at step of the QR algorithm | ||
| the imaginary unit, |
2.1 Eigenvalues and eigenvectors
Section titled “2.1 Eigenvalues and eigenvectors”A non-zero vector is an eigenvector of with eigenvalue when : maps onto its own line. The eigenvalues are the roots of the characteristic polynomial , a polynomial of degree , so there are of them counted with multiplicity, possibly complex (a real polynomial’s complex roots come in conjugate pairs ). For a matrix the polynomial is : trace and determinant, solved by the quadratic formula. A symmetric matrix has only real eigenvalues and an orthonormal basis of eigenvectors.
2.2 Power iteration
Section titled “2.2 Power iteration”Write a start vector in the eigenvector basis, . Then
If and , every other term shrinks like , so the direction of converges to . Three practical rules follow.
- Normalize every step, . The direction is all that matters, and overflows: is
infin float64, andinf/infis NaN. - Start at random. A start with never finds (in exact arithmetic): is exactly such a start for . Random entries make an event of probability zero. The contract draws from the caller’s generator, for uniforms, so runs are reproducible.
- Report the Rayleigh quotient . For an exact eigenvector it equals , with its sign; equals and turns into . For a symmetric matrix its error is the square of the direction’s error, so it converges twice as fast.
If some is exactly zero, lies in the null space: the eigenvalue is 0, and dividing would produce NaN, so return 0.
2.3 Inverse iteration
Section titled “2.3 Inverse iteration”The matrix has the same eigenvectors as , with eigenvalues . The largest of those belongs to the nearest , so power iteration on finds it, at the rate per step: very fast for a good shift. Each step needs , that is, a solve with . Never form the inverse, and never refactor: lu once ( operations, M03.2), then lu_solve per step (). The answer reported is , an eigenvalue of itself. If is exactly an eigenvalue, is singular and there is nothing to solve with: raise.
2.4 Complex pairs and the QR algorithm
Section titled “2.4 Complex pairs and the QR algorithm”The rotation has trace 0 and determinant 4, so and : equal moduli, no real eigenvector. Power iteration sends to , then , then : the direction turns by every step and never settles. Recurrent weight matrices are full of such rotations.
The QR algorithm handles them. Start from and repeat
Since , : a similarity transform, which never changes the eigenvalues. It is power iteration on all directions at once, kept orthonormal by the QR step: the first column follows , the first two span the dominant 2-dimensional invariant subspace, and so on. When the moduli separate, converges to block upper triangular form (the real Schur form): entry decays like , and what remains on the diagonal are blocks (real eigenvalues) and blocks (complex pairs, whose subdiagonal never vanishes). The spectral radius is the largest modulus over the blocks: for a block, the quadratic formula of 2.1 for a one. spectral_radius treats a subdiagonal entry below times the largest entry as zero.
2.5 Why the spectral radius decides explosion
Section titled “2.5 Why the spectral radius decides explosion”For any vector norm, as (Gelfand’s formula), so grows like when and shrinks like when , up to factors polynomial in . Backpropagation through steps of multiplies by , which has the same spectral radius. Note , with equality for symmetric (and orthogonal) : an orthogonal initialization (M03.3) starts every recurrent weight at exactly .
3. Worked example by hand
Section titled “3. Worked example by hand”Power iteration on , with eigenvalues 3 (eigenvector ) and 1 (eigenvector ). Take the start (the tests draw a random one) and skip the normalization to keep integers:
| Rayleigh quotient | ||
|---|---|---|
| 0 | ||
| 1 | ||
| 2 | ||
| 3 |
The wrong component stays while the right one triples, so the angle error shrinks by per step, and the Rayleigh quotient’s error by : , , . After 60 steps both are exact to rounding: test_hand_example_power_iteration checks to and .
The spectral radius of a scaled rotation, (test_spectral_radius_of_a_rotation). Its QR factorization is the rotation itself, , so : the QR algorithm leaves it unchanged, and the block never becomes triangular. That is the expected outcome for a complex pair. The block’s trace is and its determinant , so
4. The interface
Section titled “4. The interface”def power_iteration(matvec, n: int, iters: int, rng) -> tuple[float, NDArray]: """v0 = n draws of 2 * rng.uniform() - 1, normalized; iters steps of v = Av / ||Av||; returns (v . A v, v). (0.0, v) if some Av is exactly 0."""
def inverse_iteration(A, shift: float, iters: int, rng) -> tuple[float, NDArray]: """power_iteration on (A - shift I)^-1 via one lu and a lu_solve per step; returns (v . A v, v). ValueError if A - shift I is singular."""
def spectral_radius(W, iters: int = 100) -> float: """max |lambda| by the unshifted QR algorithm with qr_householder; 0.0 for 0 x 0."""power_iteration takes a function, not a matrix, so M10.5 can pass a Hessian-vector product without ever forming the Hessian.
What the tests check
Section titled “What the tests check”| Test | KIND | Checks | Why it matters downstream |
|---|---|---|---|
test_hand_example_power_iteration | unit, smoke | section 3: , | you and the tests agree on the method |
test_negative_dominant_eigenvalue_keeps_its_sign | boundary | gives | M10.5 tells a maximum from a saddle by the sign |
test_random_start_finds_the_dominant_direction | boundary | gives 3, not 1 | a fixed start can be orthogonal to |
test_start_vector_draws_from_rng_in_order | unit | with 0 steps, is the normalized draws | reproducible from the seed (P11) |
test_renormalizes_every_step | boundary | over 400 steps stays finite | long runs |
test_symmetric_matrices_match_numpy | differential | 20 symmetric matrices, dominant against , versus np.linalg.eigh | the sign and the vector, at a slow ratio |
test_zero_map_returns_zero | boundary | gives 0 without NaN; bad n and iters raise | rank-deficient weights |
test_inverse_iteration_finds_the_eigenvalue_nearest_the_shift | unit | spectrum : shift 2.9 finds 3, shift 0 finds 1; an exact eigenvalue as the shift raises | the smallest Hessian eigenvalue in M10.5 |
test_inverse_iteration_factors_once | unit | lu is called exactly once for 25 steps | the point of a factorization |
test_spectral_radius_of_a_rotation | unit, smoke | section 3: for a scaled rotation | recurrent weights rotate |
test_spectral_radius_matches_numpy | differential | five non-symmetric matrices with real, negative, and complex dominant eigenvalues, versus np.linalg.eigvals | the L3.1 diagnostic on realistic matrices |
test_spectral_radius_edge_cases | boundary | , , a nilpotent matrix (all eigenvalues 0), non-square input | |
test_input_not_modified | boundary | the caller’s is unchanged | the monitor reads live weights |
5. Pitfalls
Section titled “5. Pitfalls”| Pitfall | Symptom | Caught by |
|---|---|---|
| returning instead of | reported for eigenvalue | test_negative_dominant_eigenvalue_keeps_its_sign (mutant s01) |
| a fixed start such as | the wrong eigenvalue whenever is orthogonal to | test_random_start_finds_the_dominant_direction (mutant s02) |
| normalizing only at the end | inf, then NaN, after a few hundred steps | test_renormalizes_every_step (mutant s03) |
| dividing by | NaN for a vector in the null space | test_zero_map_returns_zero (mutant s04) |
| iterating with instead of its inverse | finds the eigenvalue farthest from | test_inverse_iteration_finds_the_eigenvalue_nearest_the_shift (mutant s05) |
| refactoring at every step | correct but iters times slower | test_inverse_iteration_factors_once (mutant s06) |
| spectral radius by power iteration | never converges on rotations; wrong for complex pairs | test_spectral_radius_of_a_rotation (mutant s07) |
| reading only the diagonal after the QR algorithm | complex pairs reported as their real parts | test_spectral_radius_matches_numpy (mutant s08) |
| iterating in the caller’s array | the weights being monitored change | test_input_not_modified (mutant s09) |
6. Where it’s used next
Section titled “6. Where it’s used next”| Direction | Module | How it uses this |
|---|---|---|
| Back | M03.2 | lu once and lu_solve per step in inverse_iteration |
| Back | M03.3 | qr_householder drives the QR algorithm in spectral_radius |
| Forward | L0.5 | the training monitor logs of each weight matrix |
| Forward | L3.1 | the exploding-gradient diagnostic: gradient norms grow when |
| Forward | M10.5 | the top Hessian eigenvalue by power_iteration on Hessian-vector products; the edge of stability |
| Forward | M10.1 | the condition number of a quadratic is (reading) |
None of the forward modules is in the registry with M03.4 in its deps yet, so ol verify course M03.4 reports no call site until one lands (course/DEVIATIONS.md, M034-02).
Going further
Section titled “Going further”| Your piece | Production equivalent | What it adds | Where to look |
|---|---|---|---|
| unshifted QR algorithm | LAPACK dhseqr (via numpy.linalg.eigvals) | reduction to Hessenberg form first ( per step instead of ), Wilkinson and Francis double shifts for quadratic convergence, deflation, aggressive early deflation | LAPACK SRC/dhseqr.f, dlahqr.f |
power iteration on a matvec | Lanczos and Arnoldi (ARPACK, scipy.sparse.linalg.eigsh) | keep every iterate and extract eigenvalues from the whole Krylov subspace: many eigenpairs, far fewer products | scipy/sparse/linalg/_eigen/arpack/ |
Hessian top eigenvalue (M10.5) | PyHessian | power iteration and Lanczos on Hessian-vector products of a real network | Yao et al., “PyHessian” (2020) |
| spectral radius monitoring | spectral normalization in GANs | one power-iteration step per training step to keep | torch.nn.utils.parametrizations.spectral_norm |