Lecture 15
Eigenvalues & Positive Definite Matrices
Almost every direction gets turned when a matrix acts. A few survive and only stretch. Those directions run the long run, and their stretch factors shape every loss surface you will ever descend.
115 slides / MIT 18.06, Lectures 21, 22, 24, 25, 27 / Strang Ch. 6

The idea in one sentence
Almost every direction gets turned when a matrix acts. A few come back pointing exactly the way they went in, only longer or shorter, and those few decide everything that happens when you apply the matrix again and again.
The directions that survive
Lecture 2 left you with a matrix as a machine: a vector goes in, a moved vector comes out. Now stop watching the whole plane and watch one arrow at a time. Take
and feed it . Out comes . The arrow got longer, and it also swung around toward the horizontal. Output and input point different ways. That is the ordinary case, and it is what almost every arrow does.
Now feed the same machine . Out comes : the same direction, five times as long. Feed it . Out comes : the same direction, twice as long. Those two arrows did not turn at all. The machine that bends everything else leaves these two lines exactly where they are and slides points along them.
An arrow that survives like this is an eigenvector of . The number it gets multiplied by is its eigenvalue, written . The word eigen is German for own, so these are the matrix’s own directions. One line holds the whole subject:
The condition is not fussiness. The zero vector satisfies for every number , so allowing it would make the definition say nothing. Also, if is an eigenvector then so is and so is , with the same . What survives is a line through the origin, and is just a name for a point on it.
Three arrows, one machine. is turned. and stay on their own dotted lines and only change length. Those two lines are the eigendirections, and the length ratios 5 and 2 are the eigenvalues.
The eigenvalue is allowed to be zero. If for some nonzero , then is an eigenvector with , and sits in the null space . You have already met one. The ring road machine from Lecture 2 sends to , because raising all three cities by a meter changes no climb. That blind direction is an eigenvector with eigenvalue zero. Singular and “has eigenvalue zero” are the same news said twice.
A = [2.0 0.0; 0.0 0.5]
x = (1.00, 0.00)
Ax = (2.00, 0.00)
turned by 0.0°
Orange is the input, blue is where A sends it. Sweep slowly. Catch the two angles where the two arrows lie on one line: the picture turns green and the ratio of lengths is the eigenvalue. Rotate has none.
How to find them
Guessing works for small matrices and stops working fast. Here is the procedure, and it comes straight out of Lecture 14.
Move everything to one side of . You cannot subtract the number from the matrix , so put it on the diagonal first, as :
Read that as a question about one matrix. We want a nonzero in the null space of . A square matrix has a nonzero null vector exactly when it is singular, and Lecture 14 gave a one-number test for singular. So is an eigenvalue of precisely when
In words: the eigenvalues of are the numbers you can subtract from its diagonal to make it singular. That one sentence is the whole method.
Run it on the matrix from Figure 1:
The roots are and , which is what Figure 1 showed. Now put each root back and solve. For ,
whose null space is spanned by . For the matrix is and the null space is spanned by . Two lines, two numbers, done.
What looks like. The dashed square is the unit square and the blue shape is where sends it. For a that is not an eigenvalue the landing spot is a parallelogram, and the shaded patch is the area it still has. At and the plane collapses onto a line: no area, determinant zero, and some nonzero vector sent to the origin. That vector is the eigenvector.
Look again at the polynomial . The 7 is , the sum along the diagonal of . The 10 is , the determinant of . That is not a coincidence of this example. For any by matrix,
where is the sum of the diagonal entries, the trace. Both checks cost seconds and both catch arithmetic slips. Here and .
One more free result. If is triangular then is triangular too, and the determinant of a triangular matrix is the product of its diagonal entries. So , and the eigenvalues of a triangular matrix are exactly the numbers sitting on its diagonal. Nothing to compute.
A machine that settles down
You can find eigenvalues now. Here is why anyone bothers. Two towns swap residents every year. Of the people in town 1, eighty percent stay and twenty percent move to town 2. Of the people in town 2, seventy percent stay and thirty percent move to town 1. Write the fractions as columns:
Each column sums to 1, because everybody ends up somewhere. A matrix like this is called a Markov matrix. If holds the two population fractions in year , then , and the population in year 100 is . Nobody wants to multiply a hundred matrices.
Do the eigenvalue computation instead:
The trace check gives and the determinant check gives . Both agree. For , the matrix has rows and , so the null space is spanned by , which we rescale to so the entries sum to 1. For the null space of is spanned by .
Those two numbers say everything. Start from , everyone in town 1, and split it along the eigenvectors:
Each year, leaves the first piece alone and halves the second, because that is what eigenvalues 1 and 0.5 do. So
and the second piece dies out. The population settles at 60 percent and 40 percent no matter where it starts. The eigenvector for is the steady state, and the eigenvector for is the part that decays.
Every column of sums to 1, so the total population never changes and every state stays on the line . Along that line the gap to is multiplied by each year.
The code below runs the whole example. It checks the two eigenvectors by hand, checks the trace and the determinant, and then computes twice: once by a hundred honest multiplications, once from the eigenvalues alone.
import numpy as np
A = np.array([[0.8, 0.3], [0.2, 0.7]])
x1 = np.array([0.6, 0.4]) # null space of A - I
x2 = np.array([1.0, -1.0]) # null space of A - 0.5 I
print(A @ x1) # [0.6 0.4] unchanged, so lambda = 1
print(A @ x2) # [ 0.5 -0.5] halved, so lambda = 0.5
print(np.trace(A), np.linalg.det(A)) # 1.5 0.5 = 1 + 0.5 and 1 * 0.5
def power(M, k): # the expensive way
P = M
for _ in range(k - 1):
P = P @ M
return P
print(power(A, 100)) # [[0.6 0.6]
# [0.4 0.4]]
X = np.column_stack([x1, x2]) # eigenvectors in the columns
L = np.diag([1.0 ** 100, 0.5 ** 100])
print(X @ L @ np.linalg.inv(X)) # [[0.6 0.6]
# [0.4 0.4]] same answer, two numbers of workimport torch
A = torch.tensor([[0.8, 0.3], [0.2, 0.7]])
x1 = torch.tensor([0.6, 0.4]) # null space of A - I
x2 = torch.tensor([1.0, -1.0]) # null space of A - 0.5 I
print(A @ x1) # tensor([0.6000, 0.4000]) lambda = 1
print(A @ x2) # tensor([ 0.5000, -0.5000]) lambda = 0.5
print(torch.trace(A), torch.linalg.det(A)) # 1.5 0.5
def power(M, k):
P = M
for _ in range(k - 1):
P = P @ M
return P
print(power(A, 100)) # [[0.6 0.6]
# [0.4 0.4]]
X = torch.stack([x1, x2], dim=1) # torch spells column_stack as stack(dim=1)
L = torch.diag(torch.tensor([1.0 ** 100, 0.5 ** 100]))
print(X @ L @ torch.linalg.inv(X)) # [[0.6 0.6]
# [0.4 0.4]]import jax.numpy as jnp
A = jnp.array([[0.8, 0.3], [0.2, 0.7]])
x1 = jnp.array([0.6, 0.4]) # null space of A - I
x2 = jnp.array([1.0, -1.0]) # null space of A - 0.5 I
print(A @ x1) # [0.6 0.4] unchanged, so lambda = 1
print(A @ x2) # [ 0.5 -0.5] halved, so lambda = 0.5
print(jnp.trace(A), jnp.linalg.det(A)) # 1.5 0.5
def power(M, k):
P = M
for _ in range(k - 1):
P = P @ M
return P
print(power(A, 100)) # [[0.6 0.6]
# [0.4 0.4]]
X = jnp.column_stack([x1, x2])
L = jnp.diag(jnp.array([1.0 ** 100, 0.5 ** 100]))
print(X @ L @ jnp.linalg.inv(X)) # [[0.6 0.6]
# [0.4 0.4]]import tensorflow as tf
A = tf.constant([[0.8, 0.3], [0.2, 0.7]])
x1 = tf.constant([0.6, 0.4]) # null space of A - I
x2 = tf.constant([1.0, -1.0]) # null space of A - 0.5 I
print(tf.linalg.matvec(A, x1)) # [0.6 0.4] unchanged, so lambda = 1
print(tf.linalg.matvec(A, x2)) # [ 0.5 -0.5] halved, so lambda = 0.5
print(tf.linalg.trace(A), tf.linalg.det(A)) # 1.5 0.5
def power(M, k):
P = M
for _ in range(k - 1):
P = P @ M
return P
print(power(A, 100)) # [[0.6 0.6]
# [0.4 0.4]]
X = tf.stack([x1, x2], axis=1) # tensorflow has no column_stack
L = tf.linalg.diag(tf.constant([1.0 ** 100, 0.5 ** 100]))
print(X @ L @ tf.linalg.inv(X)) # [[0.6 0.6]
# [0.4 0.4]]Both columns of are the steady state. That is the promise of eigenvalues: a hundred matrix multiplications collapse into raising two numbers to the hundredth power, and one of those numbers is so small that it vanishes.
Matrices whose eigenvalues you can read off
Some machines wear their answers on the outside. Four of them are worth memorizing, because they show up inside everything else.
Start with the identity. for every , so every vector is an eigenvector and the only eigenvalue is 1. Nothing turns because nothing moves.
Next a projection. Lecture 11 built , the machine that drops every vector onto the line through . Take , so
A vector already on the line, like , is left alone: . A vector perpendicular to the line, like , is crushed to zero: . There is a reason those are the only two options. Projecting twice is the same as projecting once, so , and applying that to an eigenvector gives . The only numbers that survive are 0 and 1. The column space of is the eigenvectors for , and the null space is the eigenvectors for .
Double a projection and subtract the identity and you get a reflection, a mirror, . Vectors on the mirror line are fixed, so . Vectors perpendicular to it are sent to their opposites, so . Reflecting twice does nothing, so and . The same argument as before, one letter different.
Last, a rotation. Turn the plane a quarter circle, . Every direction moves. No line survives, and the algebra agrees: , which no real number solves.
Three machines and their surviving lines. The projection keeps one line and destroys the perpendicular one. The reflection keeps one and reverses the other. The rotation keeps none, and its characteristic equation says so.
Two shortcuts follow from the definition and cost nothing. Apply twice to an eigenvector and you get , so squaring a matrix squares its eigenvalues and keeps its eigenvectors. Shift the matrix by and every eigenvalue shifts by , since . The eigenvectors do not move at all.
The shortcut you might expect next is false. Eigenvalues of are not the sums of the eigenvalues, and eigenvalues of are not the products, because and generally have different eigenvectors. Take and . Every eigenvalue of each is 0. Yet has eigenvalues and , and has eigenvalues 1 and 0. Sums and products of eigenvalues only behave when the two matrices share eigenvectors, which happens exactly when .
Diagonalization
So far the eigenvectors have been handled one at a time. Take them all at once and the matrix comes apart into numbers.
Suppose an by matrix has independent eigenvectors. Put them in the columns of a matrix and put the eigenvalues on the diagonal of . Then look at what is, column by column: the -th column of is , which is . That is the -th column of scaled, which is exactly the -th column of . So
is invertible because its columns are independent, which is the only thing we assumed.
Read the middle formula from right to left and it becomes a sentence. rewrites a vector in eigen-coordinates, saying how much of each eigenvector it contains. scales each of those amounts by its own eigenvalue. writes the result back in ordinary coordinates. Change the language, do something trivial, change the language back.
Watch it on the matrix from Figure 1, whose eigenvectors are with and with . Take . In eigen-coordinates, , since . Now apply by scaling each amount:
which is what gives directly. The matrix never had to be multiplied.
Diagonalization, drawn. Split along the two eigenlines, stretch each piece by its own eigenvalue, add the pieces back. The parallelogram before and the parallelogram after have the same two sides, scaled by 5 and by 2.
Powers come out of this for free. Two copies give , since the inner pair cancels, and the same cancellation happens all the way up:
Raising a diagonal matrix to the hundredth power means raising numbers to the hundredth power. That is the entire trick behind the Markov example, and it also answers the stability question at a glance: goes to the zero matrix exactly when every .
It does not always work. Diagonalization needs independent eigenvectors, and some matrices do not have them. Take
It is triangular, so the eigenvalues are 1 and 1. The root appears twice in the characteristic polynomial, which we call its algebraic multiplicity. But has rank 1, so its null space is only one dimensional: there is a single eigendirection, , and no second one. The number of independent eigenvectors for a given is its geometric multiplicity, and it is never larger than the algebraic multiplicity. When it comes up short there is no eigenvector basis and no diagonalization. Distinct eigenvalues always give independent eigenvectors, so the trouble can only start when a root repeats.
Fibonacci and the golden ratio
Now a matrix where everything works, and where the answer turns out to be famous.
The numbers obey . That is a rule for getting the next number from the previous two, which sounds like nothing to do with matrices until you stack two consecutive terms into a vector. Let . Then
The top row adds the two entries, the bottom row remembers the old top. So with , and the Fibonacci numbers are the powers of one small matrix. Now diagonalize it:
The larger root is the golden ratio , and the smaller is . The trace check gives and the determinant check gives , both of which you can read straight off the polynomial. For the eigenvectors, the second row of says , so the eigenvector is .
Split along them. Solving gives and , and since we get . Multiply by a hundred times, which now means multiplying each piece by its own eigenvalue a hundred times, and read the bottom entry:
A sequence defined by addition has a closed form built from square roots. And because , the second term shrinks to nothing: is the nearest integer to . It also explains a fact people notice without knowing why. The ratio marches toward , because the piece grows and the piece dies, so after a while the vector is pointing almost exactly along the eigenvector .
The squares are Fibonacci numbers and the arcs join into a spiral. The eigenvalue is the growth factor: each square’s side is times the one before it in the limit, so each quarter turn of the spiral grows by very nearly .
import numpy as np
phi = (1 + 5 ** 0.5) / 2
psi = (1 - 5 ** 0.5) / 2
print(phi, psi) # 1.618033988749895 -0.6180339887498949
print(phi + psi, phi * psi) # 1.0 -1.0000000000000002 the trace and the determinant
def fib_closed(n):
return (phi ** n - psi ** n) / 5 ** 0.5
def fib_loop(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
for n in [10, 30, 50]:
print(n, fib_loop(n), fib_closed(n))
# 10 55 55.000000000000014
# 30 832040 832040.0000000008
# 50 12586269025 12586269025.00002
A = np.array([[1.0, 1.0], [1.0, 0.0]])
u = np.array([1.0, 0.0])
for _ in range(10):
u = A @ u
print(u) # [89. 55.] which is (F11, F10)
print(fib_loop(101) / fib_loop(100)) # 1.618033988749895import torch
phi = (1 + 5 ** 0.5) / 2
psi = (1 - 5 ** 0.5) / 2
print(phi, psi) # 1.618033988749895 -0.6180339887498949
print(phi + psi, phi * psi) # 1.0 -1.0000000000000002 the trace and the determinant
def fib_closed(n):
return (phi ** n - psi ** n) / 5 ** 0.5
def fib_loop(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
for n in [10, 30, 50]:
print(n, fib_loop(n), fib_closed(n))
# 10 55 55.000000000000014
# 30 832040 832040.0000000008
# 50 12586269025 12586269025.00002
A = torch.tensor([[1.0, 1.0], [1.0, 0.0]], dtype=torch.float64)
u = torch.tensor([1.0, 0.0], dtype=torch.float64)
for _ in range(10):
u = A @ u
print(u) # tensor([89., 55.]) which is (F11, F10)
print(fib_loop(101) / fib_loop(100)) # 1.618033988749895import jax.numpy as jnp
from jax import config
config.update("jax_enable_x64", True) # jax defaults to float32, so ask for doubles
phi = (1 + 5 ** 0.5) / 2
psi = (1 - 5 ** 0.5) / 2
print(phi, psi) # 1.618033988749895 -0.6180339887498949
print(phi + psi, phi * psi) # 1.0 -1.0000000000000002 the trace and the determinant
def fib_closed(n):
return (phi ** n - psi ** n) / 5 ** 0.5
def fib_loop(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
for n in [10, 30, 50]:
print(n, fib_loop(n), fib_closed(n))
# 10 55 55.000000000000014
# 30 832040 832040.0000000008
# 50 12586269025 12586269025.00002
A = jnp.array([[1.0, 1.0], [1.0, 0.0]])
u = jnp.array([1.0, 0.0])
for _ in range(10):
u = A @ u
print(u) # [89. 55.] which is (F11, F10)
print(fib_loop(101) / fib_loop(100)) # 1.618033988749895import tensorflow as tf
phi = (1 + 5 ** 0.5) / 2
psi = (1 - 5 ** 0.5) / 2
print(phi, psi) # 1.618033988749895 -0.6180339887498949
print(phi + psi, phi * psi) # 1.0 -1.0000000000000002 the trace and the determinant
def fib_closed(n):
return (phi ** n - psi ** n) / 5 ** 0.5
def fib_loop(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
for n in [10, 30, 50]:
print(n, fib_loop(n), fib_closed(n))
# 10 55 55.000000000000014
# 30 832040 832040.0000000008
# 50 12586269025 12586269025.00002
A = tf.constant([[1.0, 1.0], [1.0, 0.0]], dtype=tf.float64)
u = tf.constant([1.0, 0.0], dtype=tf.float64)
for _ in range(10):
u = tf.linalg.matvec(A, u)
print(u) # [89. 55.] which is (F11, F10)
print(fib_loop(101) / fib_loop(100)) # 1.618033988749895The closed form drifts from the integers in the sixteenth significant digit, which is a double doing its best, and the loop stays exact. Both are worth having. The formula tells you the answer grows like ; the loop tells you the answer.
That last observation, that repeated multiplication drags any starting vector toward the largest eigenvalue’s direction, is an algorithm. It is called power iteration, and it is how the biggest eigenvalue of a huge matrix gets computed when nobody can afford a characteristic polynomial. Press the button and watch it happen.
A = [1.0 1.0; 1.0 0.0]
k = 0
u = (1.00, 0.00)
‖uₖ‖/‖uₖ₋₁‖ = -
still swinging
The blue arrow is redrawn at a fixed length, so only its direction moves; the orange trail is where it has been. Press a few times and it stops moving. That settled direction is the top eigenvector, and the printed ratio is its eigenvalue. Rotate never settles.
Symmetric matrices are the good case
Everything so far has worked for any matrix with enough eigenvectors. One family does much better than that, and it happens to be the family your data arrives in.
A matrix is symmetric when , so the entry in row column equals the entry in row column . This is not a rare accident. Covariance matrices are symmetric. So is every Gram matrix , and so is the second-derivative matrix of any smooth function. Symmetric matrices come with two gifts.
The first gift is that the eigenvalues are real. No rotations hide inside a symmetric matrix, so the complex case never arises.
The second is that the eigenvectors are perpendicular. Here is the whole proof for two different eigenvalues. Suppose and with . Then
where the middle step is where symmetry is used, since and . So , and since the dot product must be zero. Perpendicular.
Scale each eigenvector to length 1 and call them . Put them in the columns of . Perpendicular unit columns mean , so , and diagonalization loses its inverse:
This is the spectral theorem, and it holds for every symmetric matrix, even when eigenvalues repeat. Read the three factors right to left: rotates space so the eigenvectors become the coordinate axes, stretches along those axes, rotates back. Every symmetric matrix is a stretch in disguise, and the disguise is a rotation.
Take . The trace is 4 and the determinant is 3, so the eigenvalues are 3 and 1. The eigenvectors are and , which are perpendicular as promised. Feed the unit circle and it comes out as an ellipse with half-axes 3 and 1, pointing along and .
The symmetric matrix with rows and turns the unit circle into an ellipse. The axes of that ellipse are the eigenvectors and their half-lengths are the eigenvalues. Rotate, stretch, rotate back.
There is one more way to read , and it is the one that will matter later. Multiply it out as a sum of columns times rows:
Every piece is a familiar object. It is Lecture 11’s projection matrix with , and since has length 1 the denominator is 1. So each piece projects onto one eigendirection, and scales that projection by and adds the results. A symmetric matrix is a weighted sum of projections onto perpendicular lines. For the example, and , and times the first plus times the second rebuilds exactly.
Positive definite
Some symmetric matrices have all their eigenvalues on the positive side. That single condition turns out to be the difference between a surface with a bottom and a surface without one, which is why this section is really about optimization.
A symmetric matrix is positive definite when the number
is greater than zero for every nonzero . That number is called the energy. For a 2 by 2 matrix with rows and the energy works out to . So the condition says: this quadratic is positive everywhere except at the origin.
Why is that the same as “all eigenvalues positive”? Because of the spectral theorem. Write in the eigenvector basis, . The are perpendicular unit vectors, so the cross terms vanish and
The energy is a weighted sum of squares with the eigenvalues as weights. It is positive for every nonzero exactly when every weight is positive. Take one negative and the energy goes negative along .
For a symmetric matrix, four tests all say the same thing, and each is worth having because each is cheap in a different situation:
- every eigenvalue is positive;
- every pivot from elimination is positive;
- every leading top-left determinant is positive, the , the , and so on;
- for some matrix with independent columns.
The last one is the reason positive definite matrices are everywhere in machine learning. If holds your data, then , which is a squared length and so never negative, and it is zero only when , which independent columns forbid. Least squares and covariance both produce , so both produce positive definite matrices whenever the data has no redundant column.
Try all four on the tridiagonal matrix from Lecture 5:
Its pivots are , all positive. Its leading determinants are , all positive. Its eigenvalues are , then , then , all positive, and they multiply to 4, which is the determinant, and sum to 6, which is the trace. And Cholesky factors it as , which is the fourth test with . Four tests, one verdict.
Positive semidefinite relaxes every strict inequality to an inclusive one. The energy is allowed to reach zero: for every , some eigenvalue may be 0, some pivot may be 0, and the determinant may be 0. The two names get swapped constantly, so keep the distinction sharp: definite means the bowl has exactly one lowest point, semidefinite means the bowl may have a flat valley floor running through it.
is the small example. Its energy is , which is never negative and is zero all along the line . Its eigenvalues are 2 and 0. Semidefinite, not definite. And , with eigenvalues 1 and , has energy , which is positive in one direction and negative in another. That one is called indefinite, and its graph is a saddle.
The graph of the energy , for three symmetric matrices. Positive eigenvalues curve the surface up in every direction. A negative one opens a downhill escape. A zero one flattens a whole line, and the bottom stretches into a valley. That flat direction is a null direction of , which is why the trough carries the null-space color from the four-subspaces picture.
You have met this test before. In one variable you look at : positive means the curve holds water and you are sitting at the bottom of it. The Hessian is that same number with more directions to account for, and you read the verdict off the signs exactly as you always did.
The second derivative test, grown up. On the left one number settles it. On the right that number becomes a list, the eigenvalues of the Hessian, and the signs decide the shape: all positive gives the green bowl, opposite signs the pink saddle, a zero the cyan valley. Flip every sign and the bowl becomes a peak.
import numpy as np
K = np.array([[2.0, -1.0, 0.0], [-1.0, 2.0, -1.0], [0.0, -1.0, 2.0]])
print(np.linalg.eigvalsh(K)) # [0.58578644 2. 3.41421356] 2 - sqrt2, 2, 2 + sqrt2
L = np.linalg.cholesky(K) # succeeds only for positive definite input
print(np.diag(L) ** 2) # [2. 1.5 1.33333333] the pivots
print(np.round([np.linalg.det(K[:1, :1]),
np.linalg.det(K[:2, :2]),
np.linalg.det(K)], 6)) # [2. 3. 4.] the leading determinants
for x in [np.array([1.0, 2.0, 3.0]), np.array([1.0, 1.0, 1.0])]:
print(x @ K @ x) # 12.0 then 2.0 energy, positive away from the origin
lam = np.linalg.eigvalsh(K)
Q = np.linalg.eigh(K)[1]
S = sum(lam[i] * np.outer(Q[:, i], Q[:, i]) for i in range(3))
print(np.round(S, 10)) # rebuilds K from rank-one projections
N = np.array([[1.0, 1.0], [1.0, 1.0]])
print(np.linalg.eigvalsh(N)) # [0. 2.] semidefinite, not definite
d = np.array([1.0, -1.0])
print(d @ N @ d) # 0.0 the flat directionimport torch
K = torch.tensor([[2.0, -1.0, 0.0], [-1.0, 2.0, -1.0], [0.0, -1.0, 2.0]], dtype=torch.float64)
print(torch.linalg.eigvalsh(K)) # [0.58578644 2. 3.41421356] 2 - sqrt2, 2, 2 + sqrt2
L = torch.linalg.cholesky(K) # raises only for non positive definite input
print(torch.diag(L) ** 2) # [2. 1.5 1.33333333] the pivots
print(torch.round(torch.stack([torch.linalg.det(K[:1, :1]),
torch.linalg.det(K[:2, :2]),
torch.linalg.det(K)]), decimals=6)) # [2. 3. 4.]
for x in [torch.tensor([1.0, 2.0, 3.0], dtype=torch.float64),
torch.tensor([1.0, 1.0, 1.0], dtype=torch.float64)]:
print(x @ K @ x) # 12.0 then 2.0 energy, positive away from the origin
lam, Q = torch.linalg.eigh(K)
S = sum(lam[i] * torch.outer(Q[:, i], Q[:, i]) for i in range(3))
print(torch.round(S, decimals=10)) # rebuilds K from rank-one projections
N = torch.tensor([[1.0, 1.0], [1.0, 1.0]], dtype=torch.float64)
print(torch.linalg.eigvalsh(N)) # [0. 2.] semidefinite, not definite
d = torch.tensor([1.0, -1.0], dtype=torch.float64)
print(d @ N @ d) # 0.0 the flat directionimport jax.numpy as jnp
from jax import config
config.update("jax_enable_x64", True)
K = jnp.array([[2.0, -1.0, 0.0], [-1.0, 2.0, -1.0], [0.0, -1.0, 2.0]])
print(jnp.linalg.eigvalsh(K)) # [0.58578644 2. 3.41421356] 2 - sqrt2, 2, 2 + sqrt2
L = jnp.linalg.cholesky(K) # returns nan entries for non positive definite input
print(jnp.diag(L) ** 2) # [2. 1.5 1.33333333] the pivots
print(jnp.round(jnp.array([jnp.linalg.det(K[:1, :1]),
jnp.linalg.det(K[:2, :2]),
jnp.linalg.det(K)]), 6)) # [2. 3. 4.]
for x in [jnp.array([1.0, 2.0, 3.0]), jnp.array([1.0, 1.0, 1.0])]:
print(x @ K @ x) # 12.0 then 2.0 energy, positive away from the origin
lam, Q = jnp.linalg.eigh(K)
S = sum(lam[i] * jnp.outer(Q[:, i], Q[:, i]) for i in range(3))
print(jnp.round(S, 10)) # rebuilds K from rank-one projections
N = jnp.array([[1.0, 1.0], [1.0, 1.0]])
print(jnp.linalg.eigvalsh(N)) # [0. 2.] semidefinite, not definite
d = jnp.array([1.0, -1.0])
print(d @ N @ d) # 0.0 the flat directionimport tensorflow as tf
K = tf.constant([[2.0, -1.0, 0.0], [-1.0, 2.0, -1.0], [0.0, -1.0, 2.0]], dtype=tf.float64)
print(tf.linalg.eigvalsh(K)) # [0.58578644 2. 3.41421356] 2 - sqrt2, 2, 2 + sqrt2
L = tf.linalg.cholesky(K) # fills with nan for non positive definite input
print(tf.linalg.diag_part(L) ** 2) # [2. 1.5 1.33333333] the pivots
# tensorflow splits diag into diag and diag_part
print(tf.round(tf.stack([tf.linalg.det(K[:1, :1]),
tf.linalg.det(K[:2, :2]),
tf.linalg.det(K)]) * 1e6) / 1e6) # [2. 3. 4.]
for x in [tf.constant([1.0, 2.0, 3.0], dtype=tf.float64),
tf.constant([1.0, 1.0, 1.0], dtype=tf.float64)]:
print(tf.tensordot(x, tf.linalg.matvec(K, x), 1)) # 12.0 then 2.0 the energy
lam, Q = tf.linalg.eigh(K)
S = sum(lam[i] * tf.tensordot(Q[:, i], Q[:, i], axes=0) for i in range(3))
print(S) # rebuilds K from rank-one projections
# tensorflow has no outer, so tensordot with axes=0
N = tf.constant([[1.0, 1.0], [1.0, 1.0]], dtype=tf.float64)
print(tf.linalg.eigvalsh(N)) # [0. 2.] semidefinite, not definite
d = tf.constant([1.0, -1.0], dtype=tf.float64)
print(tf.tensordot(d, tf.linalg.matvec(N, d), 1)) # 0.0 the flat directionWhy gradient descent zigzags
Now the payoff. Training a model means walking downhill on a loss surface. Near a minimum, every smooth loss looks like a quadratic, and the quadratic is written with a symmetric matrix, the matrix of second derivatives called the Hessian. So study the simplest possible loss, with symmetric and positive definite:
Its lowest point is and its gradient is . The level sets are ellipses, which Figure 7 already showed you: axes along the eigenvectors, and along the ellipse reaches out to . A big eigenvalue means a tight, steeply curved direction. A small eigenvalue means a long, nearly flat one.
Gradient descent takes the step . Write in the eigenvector basis again and the update falls apart into separate scalar updates:
One number per direction, no interaction. That one line explains everything people say about learning rates. For the method to converge you need in every direction, so : the steepest direction sets the speed limit. But the error along the flattest direction shrinks by , which sits close to 1 when is small. The steep direction caps your step; the flat direction decides how many steps you take. The ratio
is the condition number, and it is the number that matters. With the best possible constant step , the error shrinks by a factor each step, which is close to 1 whenever is large.
Two matrices make the point without changing anything else:
Both are symmetric with the same eigenvectors, and . Both have trace 10, so both take the same best step . The only difference is the spread: has eigenvalues 4 and 6, so , and has eigenvalues 1 and 9, so .
Those eigenvalues are curvatures, and that is the whole of it. Slice the bowl along an eigenvector and you get a parabola: putting into leaves , whose second derivative is itself. The eigenvalues of the Hessian are how hard the bowl bends in its two special directions.
The two eigenvalues of , drawn as what they are. Cut the bowl along its steep eigenvector and the cut is the orange parabola . Cut along the shallow one and it is the green parabola . Both are drawn to one scale, so the nine to one you see is the nine to one in the eigenvalues.
Starting from and running until the distance to the minimum is below , the nearly round bowl takes 9 steps and the stretched one takes 65.
Same starting point, same step size, same eigenvector directions. Only the eigenvalue spread differs. On the round bowl the gradient points nearly at the minimum. On the stretched one it points mostly across the valley, so the path bounces from wall to wall and inches forward.
The picture also shows what momentum fixes. The bouncing direction reverses sign every step, since is negative there, while the crawling direction keeps the same sign. Momentum averages the recent steps, so the reversals cancel each other and the steady ones add up. The update becomes then , and with tuned and the convergence factor improves from to about . On the stretched bowl that turns 65 steps into 25.
import numpy as np
round_bowl = np.array([[5.0, -1.0], [-1.0, 5.0]]) # eigenvalues 4 and 6
stretched = np.array([[5.0, -4.0], [-4.0, 5.0]]) # eigenvalues 1 and 9
print(np.linalg.eigvalsh(round_bowl), np.linalg.eigvalsh(stretched))
# [4. 6.] [1. 9.]
def descend(S, step=0.2, beta=0.0, tol=1e-6):
x = np.array([1.0, 1.5])
v = np.zeros(2)
n = 0
while np.sqrt(x @ x) > tol:
v = beta * v - step * (S @ x) # beta = 0 is plain gradient descent
x = x + v
n += 1
return n
print(descend(round_bowl)) # 9
print(descend(stretched)) # 65
print(descend(stretched, step=0.25, beta=0.25)) # 25 momentumimport torch
round_bowl = torch.tensor([[5.0, -1.0], [-1.0, 5.0]], dtype=torch.float64)
stretched = torch.tensor([[5.0, -4.0], [-4.0, 5.0]], dtype=torch.float64)
print(torch.linalg.eigvalsh(round_bowl), torch.linalg.eigvalsh(stretched))
# [4. 6.] [1. 9.]
def descend(S, step=0.2, beta=0.0, tol=1e-6):
x = torch.tensor([1.0, 1.5], dtype=torch.float64)
v = torch.zeros(2, dtype=torch.float64)
n = 0
while torch.sqrt(x @ x) > tol:
v = beta * v - step * (S @ x) # beta = 0 is plain gradient descent
x = x + v
n += 1
return n
print(descend(round_bowl)) # 9
print(descend(stretched)) # 65
print(descend(stretched, step=0.25, beta=0.25)) # 25 momentumimport jax.numpy as jnp
from jax import config
config.update("jax_enable_x64", True)
round_bowl = jnp.array([[5.0, -1.0], [-1.0, 5.0]]) # eigenvalues 4 and 6
stretched = jnp.array([[5.0, -4.0], [-4.0, 5.0]]) # eigenvalues 1 and 9
print(jnp.linalg.eigvalsh(round_bowl), jnp.linalg.eigvalsh(stretched))
# [4. 6.] [1. 9.]
def descend(S, step=0.2, beta=0.0, tol=1e-6):
x = jnp.array([1.0, 1.5])
v = jnp.zeros(2)
n = 0
while jnp.sqrt(x @ x) > tol:
v = beta * v - step * (S @ x) # beta = 0 is plain gradient descent
x = x + v
n += 1
return n
print(descend(round_bowl)) # 9
print(descend(stretched)) # 65
print(descend(stretched, step=0.25, beta=0.25)) # 25 momentumimport tensorflow as tf
round_bowl = tf.constant([[5.0, -1.0], [-1.0, 5.0]], dtype=tf.float64)
stretched = tf.constant([[5.0, -4.0], [-4.0, 5.0]], dtype=tf.float64)
print(tf.linalg.eigvalsh(round_bowl), tf.linalg.eigvalsh(stretched))
# [4. 6.] [1. 9.]
def descend(S, step=0.2, beta=0.0, tol=1e-6):
x = tf.constant([1.0, 1.5], dtype=tf.float64)
v = tf.zeros(2, dtype=tf.float64)
n = 0
while tf.sqrt(tf.tensordot(x, x, 1)) > tol:
v = beta * v - step * tf.linalg.matvec(S, x) # beta = 0 is plain descent
x = x + v
n += 1
return n
print(descend(round_bowl)) # 9
print(descend(stretched)) # 65
print(descend(stretched, step=0.25, beta=0.25)) # 25 momentumMomentum treats the symptom. Here is the cure. Gradient descent knows only the slope, so for a given steepness it takes the same step whether the valley is tight or wide. Newton’s method divides by the curvature: its step is , and on a quadratic bowl that lands on the minimum from wherever you start, in one move. From the gradient is , so the gradient step goes to , over on the far side of the valley floor. The Newton step is , which lands on exactly.
One start, two steps, on the stretched bowl of Figure 11. Blue is the gradient step: it leaves perpendicular to the contour and crosses the valley floor to land on the far side. Green is the Newton step , which lands on the minimum. Lecture 0 ran Newton on a curve to hunt for a root; the tangent line has become a tangent bowl, and dividing by has become multiplying by .
Where this is going
You now have the two facts that carry the rest of the course. Any symmetric matrix is a stretch along perpendicular axes, . Any matrix of the form is symmetric with nonnegative eigenvalues.
Put them together on a data matrix. Center your data so each column has mean zero, and the covariance matrix is . It is symmetric, so it has perpendicular eigenvectors, and it is at least positive semidefinite, so its eigenvalues are all nonnegative. Those eigenvectors are the directions along which the data spreads out most, and the eigenvalues measure how much spread sits in each one. That is principal component analysis, and it is the first thing Part VI does.
The same trick applied to a matrix that is not square gives the singular value decomposition, and with that the course finishes what it started in Lecture 1.