Lecture 11
Projections
When the target is out of reach, the best you can do is its shadow. The projection formula, the projection matrix, and why the miss is always perpendicular.
73 slides / MIT 18.06, Lecture 15 / Strang §4.2

The idea in one sentence
When a target is out of reach, the closest thing you can reach is its shadow, and you can find that shadow because the miss it leaves behind stands at a right angle to everything reachable.
A target with no answer
Five rows of data, two knobs to turn, and no setting of the knobs that works. That is the measurements table from Lecture 8: each row carries a weight and a height , and each row wants a target number built from them with the same two coefficients and :
Two knobs, five demands. Take the first two rows on their own and they pin the knobs down: and give and . Now look at row three. It wants , and those knob settings deliver , which is about . The first two rows and the third row want different things, and no setting of two knobs can satisfy all five.
Write the two columns side by side as a matrix , and the target as . The reachable outputs are the combinations , which is the column space : a flat two-dimensional plane sitting inside five-dimensional space. The target is not on that plane. The code at the end of this chapter checks it in one line: has rank 2, and with glued on as a third column has rank 3, so adds a direction the columns never had.
That is the ordinary situation. Real data almost never lands in the column space. Lecture 10 showed which directions are perpendicular to which. This chapter uses that to answer the question the failure raises: if you cannot reach , what is the closest point you can reach?
Start with one column
Shrink the problem until it cannot get smaller. One column , one target . Every reachable point is a multiple , so the reachable set is the line through . Which multiple sits closest to ?
You already know the answer as a picture. Drop a perpendicular from onto the line, and the foot of that perpendicular is the nearest point. Every other point on the line is the far corner of a right triangle whose short side is that perpendicular, so it has to be farther away.
The closest point on the line to is the foot of the perpendicular, and the arrow that is left over meets the line at a right angle.
Now turn the picture into algebra. Call the closest point and write it as , where is the multiple you are looking for. Call the miss . The picture says is perpendicular to , and perpendicular means the dot product is zero:
That is the whole derivation. Two numbers go in, and , and the closest point comes out. With the numbers in the figure, and , so and , giving and . The miss is , and , exactly as promised.
One question is still open. The picture asserted that the foot of the perpendicular is the closest point, and the algebra only ever used the right angle. Take any other point on the line. Then , , and sit at the corners of a right triangle whose right angle is at , so . The last term is never negative, and it is zero only when is . No other point on the line can tie.
Three points on the line, three joins from . Only the join to arrives at a right angle, so each of the others is the hypotenuse of a right triangle and has to be longer.
Two cases you can check in your head
A formula you cannot sanity check is a formula you do not own. There are two cases where you already know the answer.
Two cases the formula has to get right. On the left and the shadow is itself. On the right and the shadow collapses to the origin.
If lies along , say , then and , so . The shadow of something already on the line is itself. If is perpendicular to , then and , so . Nothing of lies along the line, so the closest reachable point is the origin.
Look at the second picture again next to the first. The vector is perpendicular to , and the miss computed a moment ago was , which is exactly half of . So splits as : the part the line can see, and the part it is blind to. Projection is that split.
a = (2.00, 0.00)
b = (2.00, 1.40)
x̂ = 1.00
length of p = 2.00
length of e = 1.40
P = [1.00 0.00;0.00 0.00]
Drag anywhere in the picture to move b. Warm is the target you chose, green is the shadow the line can reach, blue is what is left over. The right-angle mark never breaks, and the two diagonal entries of P always add to 1.
The matrix that casts the shadow
The formula for hides a matrix. In the quantity is a single number, so it can be moved around freely. Slide it to the right of and group differently:
Read the two products carefully, because they look alike and are nothing alike. A row times a column, , is one number. A column times a row, , is a full square matrix.
A column times a row. Both rows of are multiples of , which is what rank one looks like. The projection matrix is this matrix divided by 26.
For the outer product is the matrix with rows and , and is that matrix divided by 26. Every row of is a multiple of the same row , so all rows sit on one line and the rank is 1. That has to be true: maps everything onto a one-dimensional line, so its column space is that line.
Three properties fall out.
is symmetric, since .
applied twice is applied once:
The middle step is the whole trick: the two inner factors and meet and collapse into the number , which then cancels one copy from the denominator. The geometry says the same thing. already sits on the line, and the shadow of a point on the line is itself, so pressing the button again changes nothing.
One press of carries to . A second press does nothing, because is already on the line, and that is with no algebra in sight.
And the trace of is , which matches the rank. For projections that is a rule: the trace counts how many dimensions survive.
The simplest projection matrix in the world
Here is a projection you have used without calling it one. Take a picture six pixels wide, two rows of three, and stack the pixels into a vector in . Now redact the middle column by multiplying with a diagonal matrix of ones and zeros.
Six pixels and one diagonal matrix of ones and zeros. It keeps four coordinates and destroys two, and running it a second time does no more damage than the first.
That diagonal matrix is symmetric because diagonal matrices are. It satisfies because and , and because redacting an already redacted picture does nothing new. Its rank is 4, its trace is 4, and its column space is the four-dimensional set of pictures whose middle column is blank. It is a projection onto that subspace, and it is not rank one.
So rank one came from projecting onto a line, and the real definition of a projection matrix is the pair of properties: and . The matrix is a projection too, the one that keeps exactly what throws away. In this picture keeps the middle column and blanks the rest.
Many columns at once
Back to the real problem. Now has independent columns instead of one, so the reachable set grows from a single line to the whole column space. Everything else survives the upgrade.
Write the closest reachable point as , where now holds one coefficient per column, and write the miss as . Closest still means the miss is perpendicular to everything reachable, and everything reachable is built from the columns, so it is enough for to be perpendicular to each column separately. Stacking those conditions is exactly what does, since the rows of are the columns of :
Projection onto a plane, with , , and . The shadow is and the miss is , which leans on neither column. That single sentence is the equation .
One rearrangement finishes it:
The picture uses columns and and target . Then has rows and , and , so and . The miss is , and it dots to zero against both columns: and .
Two checks tell you the formula is the right one.
Set , so is the single column . Then is the number , its inverse is division, and . The subspace formula contains the line formula.
Now let be square and invertible. Then every is reachable and the projection should do nothing, and indeed .
Why you are allowed to invert that
The formula leans on , so it owes you a reason. Here it is, in two directions.
If then . That direction is free.
Going back, suppose . Multiply on the left by :
A squared length is zero only for the zero vector, so . The two null spaces are the same set: .
Now the conclusion writes itself. If the columns of are independent, then holds only the zero vector, so holds only the zero vector, and is a square matrix whose null space is trivial. That is exactly what invertible means. Independent columns are the only condition the projection formula ever needs.
One vector, two pieces
Put the pieces together. Any splits as , where lives in and satisfies , which is the definition of the left null space . Lecture 10 proved those two subspaces are perpendicular complements inside , so every splits this way, and only this way.
The column space and the left null space meet at a right angle, so every splits exactly one way. keeps the reachable piece, keeps the leftover, and the two lengths obey Pythagoras.
Since and meet at a right angle, the lengths obey Pythagoras: . For the plane above that reads .
The two machines are and . For that same plane, turns out to be the matrix with , the direction perpendicular to the plane. That is the rank-one formula from the start of the chapter, doing the complementary job. Projecting onto a plane in and projecting onto the line perpendicular to it are the same calculation seen from two sides.
import numpy as np
a = np.array([5., 1.])
b = np.array([2., 3.])
x_hat = (a @ b) / (a @ a)
p = x_hat * a
e = b - p
print(x_hat) # 0.5
print(p) # [2.5 0.5]
print(e) # [-0.5 2.5]
print(a @ e) # 0.0 the miss is perpendicular to a
print(b @ b, p @ p + e @ e) # 13.0 13.0 Pythagoras
P = np.outer(a, a) / (a @ a) # a column times a row, over a number
print(bool(abs(P @ b - p).max() < 1e-6)) # True P finds the same shadow
print(bool(abs(P @ P - P).max() < 1e-6)) # True P applied twice is P
print(bool(abs(P - P.T).max() < 1e-6)) # True P is symmetric
print(round(float(np.trace(P)), 6)) # 1.0 the trace counts what is kept
print(int(np.linalg.matrix_rank(P))) # 1
print(bool(abs(P @ a - a).max() < 1e-6)) # True a projects to itself
print(bool(abs(P @ np.array([-1., 5.])).max() < 1e-6)) # Trueimport torch
a = torch.tensor([5., 1.])
b = torch.tensor([2., 3.])
x_hat = (a @ b) / (a @ a)
p = x_hat * a
e = b - p
print(x_hat) # 0.5
print(p) # [2.5 0.5]
print(e) # [-0.5 2.5]
print(a @ e) # 0.0 the miss is perpendicular to a
print(b @ b, p @ p + e @ e) # 13.0 13.0 Pythagoras
P = torch.outer(a, a) / (a @ a) # a column times a row, over a number
print(bool(abs(P @ b - p).max() < 1e-6)) # True P finds the same shadow
print(bool(abs(P @ P - P).max() < 1e-6)) # True P applied twice is P
print(bool(abs(P - P.T).max() < 1e-6)) # True P is symmetric
print(round(float(torch.trace(P)), 6)) # 1.0 the trace counts what is kept
print(int(torch.linalg.matrix_rank(P))) # 1
print(bool(abs(P @ a - a).max() < 1e-6)) # True a projects to itself
print(bool(abs(P @ torch.tensor([-1., 5.])).max() < 1e-6)) # Trueimport jax.numpy as jnp
a = jnp.array([5., 1.])
b = jnp.array([2., 3.])
x_hat = (a @ b) / (a @ a)
p = x_hat * a
e = b - p
print(x_hat) # 0.5
print(p) # [2.5 0.5]
print(e) # [-0.5 2.5]
print(a @ e) # 0.0 the miss is perpendicular to a
print(b @ b, p @ p + e @ e) # 13.0 13.0 Pythagoras
P = jnp.outer(a, a) / (a @ a) # a column times a row, over a number
print(bool(abs(P @ b - p).max() < 1e-6)) # True P finds the same shadow
print(bool(abs(P @ P - P).max() < 1e-6)) # True P applied twice is P
print(bool(abs(P - P.T).max() < 1e-6)) # True P is symmetric
print(round(float(jnp.trace(P)), 6)) # 1.0 the trace counts what is kept
print(int(jnp.linalg.matrix_rank(P))) # 1
print(bool(abs(P @ a - a).max() < 1e-6)) # True a projects to itself
print(bool(abs(P @ jnp.array([-1., 5.])).max() < 1e-6)) # Trueimport tensorflow as tf
a = tf.constant([5., 1.])
b = tf.constant([2., 3.])
dot = lambda u, v: tf.tensordot(u, v, 1)
mv = tf.linalg.matvec
gap = lambda z: float(tf.reduce_max(tf.abs(z)))
x_hat = dot(a, b) / dot(a, a)
p = x_hat * a
e = b - p
print(x_hat) # 0.5
print(p) # [2.5 0.5]
print(e) # [-0.5 2.5]
print(dot(a, e)) # 0.0 the miss is perpendicular to a
print(dot(b, b), dot(p, p) + dot(e, e)) # 13.0 13.0 Pythagoras
P = tf.einsum('i,j->ij', a, a) / dot(a, a) # a column times a row
print(gap(mv(P, b) - p) < 1e-6) # True P finds the same shadow
print(gap(P @ P - P) < 1e-6) # True P applied twice is P
print(gap(P - tf.transpose(P)) < 1e-6) # True P is symmetric
print(round(float(tf.linalg.trace(P)), 6)) # 1.0 the trace counts
print(int(tf.linalg.matrix_rank(P))) # 1
print(gap(mv(P, a) - a) < 1e-6) # True a projects to itself
print(gap(mv(P, tf.constant([-1., 5.]))) < 1e-6) # True blind directionThe second block moves up to a subspace. It confirms that the measurements table really is unreachable, then projects onto a plane and checks every claim this chapter made about .
import numpy as np
w = np.array([5., 3., 2., 1., 4.])
h = np.array([2., 4., 8., 5., 7.])
target = np.array([3., 8., 7., 9., 2.])
A = np.column_stack([w, h])
print(int(np.linalg.matrix_rank(A))) # 2
print(int(np.linalg.matrix_rank(np.column_stack([A, target])))) # 3
B = np.array([[1., 0.], [1., 1.], [0., 1.]]) # columns (1,1,0) and (0,1,1)
b = np.array([2., 3., 4.])
print(B.T @ B) # [[2. 1.] [1. 2.]]
x_hat = np.linalg.solve(B.T @ B, B.T @ b)
print(x_hat) # [1. 3.]
p = B @ x_hat
e = b - p
print(p) # [1. 4. 3.]
print(e) # [ 1. -1. 1.]
print(B.T @ e) # [0. 0.] perpendicular to both columns
print(b @ b, p @ p + e @ e) # 29.0 29.0 Pythagoras again
P = B @ np.linalg.inv(B.T @ B) @ B.T
print(bool(abs(P @ b - p).max() < 1e-6)) # True
print(bool(abs(P @ P - P).max() < 1e-6)) # True still idempotent
print(bool(abs(P - P.T).max() < 1e-6)) # True still symmetric
print(round(float(np.trace(P)), 6)) # 2.0 two dimensions kept this time
n = np.array([1., -1., 1.]) # the direction perpendicular to the plane
print(bool(abs(np.eye(3) - P - np.outer(n, n) / (n @ n)).max() < 1e-6))import torch
w = torch.tensor([5., 3., 2., 1., 4.])
h = torch.tensor([2., 4., 8., 5., 7.])
target = torch.tensor([3., 8., 7., 9., 2.])
A = torch.stack([w, h], dim=1)
print(int(torch.linalg.matrix_rank(A))) # 2
print(int(torch.linalg.matrix_rank(torch.cat([A, target[:, None]], 1)))) # 3
B = torch.tensor([[1., 0.], [1., 1.], [0., 1.]])
b = torch.tensor([2., 3., 4.])
print(B.T @ B) # [[2. 1.] [1. 2.]]
x_hat = torch.linalg.solve(B.T @ B, B.T @ b)
print(x_hat) # [1. 3.]
p = B @ x_hat
e = b - p
print(p) # [1. 4. 3.]
print(e) # [ 1. -1. 1.]
print(B.T @ e) # [0. 0.] perpendicular to both columns
print(b @ b, p @ p + e @ e) # 29.0 29.0 Pythagoras again
P = B @ torch.linalg.inv(B.T @ B) @ B.T
print(bool(abs(P @ b - p).max() < 1e-6)) # True
print(bool(abs(P @ P - P).max() < 1e-6)) # True still idempotent
print(bool(abs(P - P.T).max() < 1e-6)) # True still symmetric
print(round(float(torch.trace(P)), 6)) # 2.0 two dimensions kept this time
n = torch.tensor([1., -1., 1.]) # the direction perpendicular to the plane
print(bool(abs(torch.eye(3) - P - torch.outer(n, n) / (n @ n)).max() < 1e-6))import jax.numpy as jnp
w = jnp.array([5., 3., 2., 1., 4.])
h = jnp.array([2., 4., 8., 5., 7.])
target = jnp.array([3., 8., 7., 9., 2.])
A = jnp.column_stack([w, h])
print(int(jnp.linalg.matrix_rank(A))) # 2
print(int(jnp.linalg.matrix_rank(jnp.column_stack([A, target])))) # 3
B = jnp.array([[1., 0.], [1., 1.], [0., 1.]])
b = jnp.array([2., 3., 4.])
print(B.T @ B) # [[2. 1.] [1. 2.]]
x_hat = jnp.linalg.solve(B.T @ B, B.T @ b)
print(x_hat) # [1. 3.]
p = B @ x_hat
e = b - p
print(p) # [1. 4. 3.]
print(e) # [ 1. -1. 1.]
print(B.T @ e) # [0. 0.] perpendicular to both columns
print(b @ b, p @ p + e @ e) # 29.0 29.0 Pythagoras again
P = B @ jnp.linalg.inv(B.T @ B) @ B.T
print(bool(abs(P @ b - p).max() < 1e-6)) # True
print(bool(abs(P @ P - P).max() < 1e-6)) # True still idempotent
print(bool(abs(P - P.T).max() < 1e-6)) # True still symmetric
print(round(float(jnp.trace(P)), 6)) # 2.0 two dimensions kept this time
n = jnp.array([1., -1., 1.]) # the direction perpendicular to the plane
print(bool(abs(jnp.eye(3) - P - jnp.outer(n, n) / (n @ n)).max() < 1e-6))import tensorflow as tf
w = tf.constant([5., 3., 2., 1., 4.])
h = tf.constant([2., 4., 8., 5., 7.])
target = tf.constant([3., 8., 7., 9., 2.])
mv, dot = tf.linalg.matvec, lambda u, v: tf.tensordot(u, v, 1)
gap = lambda z: float(tf.reduce_max(tf.abs(z)))
A = tf.stack([w, h], axis=1)
print(int(tf.linalg.matrix_rank(A))) # 2
print(int(tf.linalg.matrix_rank(tf.concat([A, target[:, None]], 1)))) # 3
B = tf.constant([[1., 0.], [1., 1.], [0., 1.]])
b = tf.constant([2., 3., 4.])
Bt = tf.transpose(B)
print(Bt @ B) # [[2. 1.] [1. 2.]]
x_hat = tf.linalg.solve(Bt @ B, mv(Bt, b)[:, None])[:, 0]
print(x_hat) # [1. 3.]
p = mv(B, x_hat)
e = b - p
print(p) # [1. 4. 3.]
print(e) # [ 1. -1. 1.]
print(mv(Bt, e)) # [0. 0.] perpendicular to both columns
print(dot(b, b), dot(p, p) + dot(e, e)) # 29.0 29.0 Pythagoras again
P = B @ tf.linalg.inv(Bt @ B) @ Bt
print(gap(mv(P, b) - p) < 1e-6) # True
print(gap(P @ P - P) < 1e-6) # True still idempotent
print(gap(P - tf.transpose(P)) < 1e-6) # True still symmetric
print(round(float(tf.linalg.trace(P)), 6)) # 2.0 two dimensions kept
n = tf.constant([1., -1., 1.]) # the direction perpendicular to the plane
print(gap(tf.eye(3) - P - tf.einsum('i,j->ij', n, n) / dot(n, n)) < 1e-6)Deleting a direction on purpose
The redaction matrix threw away coordinates. A direction a model has learned is almost never one of your coordinates, and you can throw it away anyway.
Take the word embeddings inside a language model and train a small linear classifier to read some property off them. The classifier’s weight vector is a direction in embedding space, and how much of the property a word carries is how far that word reaches along . To remove the property, project every embedding onto the perpendicular complement of . That is with , the same rank-one matrix from the start of this chapter, now used for what it destroys instead of what it keeps.
Four embeddings drawn in a frame where the learned direction runs along the horizontal, so the projection is a slide straight onto the line that survives. The readings along start at 6.26, 4.92, 3.13 and 1.79 and all four land on 0. The words stay apart vertically, so one direction is gone and the rest of the space is untouched.
The readings are not shrunk. They are exactly zero, because , and a classifier whose weight vector is now reads the same number off every word in the language. What the words keep is everything perpendicular to , which in a real model is all but one of several hundred directions.
One pass rarely finishes the job. Train a fresh classifier on what survived and it often finds another direction that still carries the property, so the method is run in a loop: learn a direction, project it away, learn the next one. Each pass subtracts one more rank-one , and each pass costs the model one more direction of the space it had to work in.
Where this is going
You now have the closest reachable point and the matrix that finds it. What you have not been told is what the coefficients mean. For the measurements table they are the numbers a model would learn, and the miss is the error it has to live with. Naming that properly, and fitting a line to real data with it, is the next chapter: least squares.
One warning to carry forward. The formula is how you think about projection. A good program computes it another way, because forming squares how badly conditioned the problem is. Lecture 13 replaces the inverse with orthonormal columns and turns the whole calculation into back substitution.