Lecture 09
The Four Subspaces & the Rank Theorem
One small matrix, four subspaces, four bases read straight off a single elimination, and the reason row rank always equals column rank.
102 slides / MIT 18.06, Lectures 8 to 10 / Strang §3.5

The idea in one sentence
Every matrix carries four subspaces, two in the space it reads from and two in the space it writes to, and a single run of elimination hands you a basis for all four.
One matrix, carried the whole way
Here is the matrix that will show you all four:
It is the doubled-columns matrix from Lecture 7.5 with one row added, and it earns its place by repeating itself in both directions. Read it down the columns: column 3 is twice column 1, and column 4 is twice column 2. Read it across the rows: row 3 is row 1 plus row 2. Nothing below uses any other matrix.
The same matrix, read twice. Two of the four columns are copies, and one of the three rows is the sum of the others. Both facts point at the same number, 2. Cyan marks the columns that will hand you the null space, purple the row that will hand you the left null space, and elimination is about to confirm both.
has 3 rows and 4 columns. It takes a list of four numbers and returns three, so it maps to . Four subspaces hang off it, two on each side.
On the input side, in , the row space is every combination of the rows of . The transpose sits in the name because the rows of are the columns of , and always means the space some columns span. Next to it lives the null space , every with . These are the directions is blind to, and the ring road machine from Lecture 2 was blind to .
On the output side, in , the column space is every combination of the columns, which by the column reading of is exactly the set of outputs can produce. Next to it lives the left null space , every with . Transpose that equation and it reads : the vector sits on the left of and kills it. In words, holds the coefficients of a combination of the rows of that comes out zero, and this matrix has one in plain sight, because row 1 plus row 2 minus row 3 is the zero row.
Four definitions, four sets of vectors to hunt down. One elimination finds them all.
Elimination, run once, paid four times
Reduce to its row reduced form , and carry the identity along so the record of what you did comes out on the right. Subtract 3 times row 1 from row 2 and 4 times row 1 from row 3, subtract the new row 2 from the new row 3, halve row 2, then subtract twice row 2 from row 1:
Two pivots, so the rank is . Columns 1 and 2 are the pivot columns, columns 3 and 4 are free. The third row of was never new information, and says so with a row of zeros.
One reduction, four answers. The yellow rings are the pivots, in columns 1 and 2. The cyan columns 3 and 4 are free. The row of zeros at the bottom is the left null space announcing itself.
Where the four bases come from
Each subspace takes its basis from a different place, and three of the four places are already on the page.
Row space: the pivot rows of
Every elimination step replaces a row by a combination of rows, so each row of is a combination of rows of . Every step is reversible, since has an inverse, so each row of is a combination of rows of . Two matrices whose rows are combinations of each other span the same space. So and have the same row space, and the zero row of contributes nothing. The pivot rows are a basis:
Both directions of that argument are on the page already. says how to build ‘s rows out of ‘s, and , which for this matrix comes out in whole numbers, says how to build ‘s rows back out of ‘s.
Elimination is a two-way street. The top arrow builds a row of from rows of , using the first row of . The bottom arrow builds a row of back from rows of , using the first row of , which here is . Neither trip leaves the row space.
Column space: the pivot columns of itself
Here the pattern breaks, and this is the one place readers get burned. Elimination does not preserve the column space. What it preserves is which columns depend on which, because and have the same solutions, and a dependence among columns is exactly a solution of . Column 3 of is twice column 1 of , so column 3 of is twice column 1 of . The recipe carries over. The vectors do not.
So take the columns in the pivot positions from the original matrix:
The columns of in those same positions are and , and they span a different plane in . Figure 4 shows how different.
Elimination moves the column space. The pink plane is spanned by the pivot columns of , the green one by the pivot columns of . The vector lies in the first and misses the second by one full unit, which is why a column space basis has to be read off .
Null space: the special solutions
Lecture 7.5 built these. Set one free variable to 1, the rest to 0, and solve upward. With the first row gives , so . With the second row gives , so :
One special solution per free column, so the count is forced to be .
Left null space: the last rows of
This is the one people never remember, and it is the easiest of the four once you know where to look. says that row of times gives row of . The bottom rows of are zero. So the bottom rows of , applied to , produce zero rows. Those rows are precisely the combinations of ‘s rows that vanish, which is what the left null space is.
Row 3 of is , and sure enough :
Why the left null space sits in . A row of times gives that row of , and ‘s last row is zero, so ‘s last row is a recipe for cancelling the rows of against each other. Reading it out loud gives the dependence you spotted in Figure 1.
The whole chapter on one card, and the color key for the rest of the course: green is the column space, orange the row space, cyan the null space, purple the left null space, yellow the pivots. Three matrices come out of one elimination, and each hands over a different basis. Only the column space is read off ; the other three are read off the leftovers.
The big picture
Now put the four in one frame. On the input side, holds the row space of dimension and the null space of dimension , and . On the output side, holds the column space of dimension and the left null space of dimension , and . The counts close on both sides, which is the first sign that nothing has been left out.
Every vector in splits into a row space piece and a null space piece :
The null piece dies on the way through. Whatever produces, it produces from the row space alone. Take . Its two pieces are
and you can check by hand that is of the first row space basis vector plus of the second, that is , and that while .
The big picture. Green is the column space, orange the row space, cyan the null space, purple the left null space, and blue is what does. Solid regions have dimension . On the left , on the right . A vector splits into and , the null piece dies, and the row piece carries the whole answer, so and land on the same .
Every later chapter of this course points back at this frame. Projections land inside the column space. Least squares takes a that is out of reach and moves it onto that space. Eigenvectors are found by asking which null space they sit in. Learn the four rooms once and the rest of the course has an address system.
Row space C(Aᵀ)
Lives in R⁴, dimension 2.
(1, 0, 2, 0)
(0, 1, 0, 2)
Basis read off the pivot rows of R.
Every attention head in a transformer carries these four spaces, and the numbers are lopsided enough to change how you picture a layer.
One attention head, drawn to scale: a unit of height is 16 dimensions. carries the 4,096 numbers of the residual stream down to the head’s own 128, so 3,968 input directions land on zero and never reach this head. The orange cap is the row space, the cyan below it is the null space, and the two add to 4,096. On the output side the head fills all 128 dimensions it has, so its left null space is a single point.
Why the two dimensions match
Look at the two dimension labels in Figure 7 again. Both say 2, and that is no accident of this matrix. For every matrix of every shape, the row space and the column space have the same dimension, and that shared number is what the word rank means.
It sounds like it should be hard to prove. A matrix has rows of length 4 and columns of length 3, so the two spaces do not even live in the same place. The proof takes two paragraphs and one factorization.
Start with the column space, since a basis for it is already sitting inside . Call its dimension . Put a basis for into the columns of a matrix . Every column of is some combination of those vectors, and if you write the coefficients for column down the -th column of a new matrix , the column reading of matrix multiplication says exactly . For our matrix, and both factors are already computed:
is and holds the pivot columns of . is and holds the pivot rows of , which is the same thing as the recipe for rebuilding every column. Multiply them and comes back.
Now read the same product the other way. The row reading of says row of is a combination of the rows of , with row of supplying the coefficients. Row 3 of is , so
Every row of is a combination of the rows of . So the row space sits inside the span of vectors, and such a space has dimension at most :
One product, two readings, and the rank theorem falls out. The green boxes are the column reading, the orange boxes the row reading. Because has only 2 columns and has only 2 rows, both spaces are trapped at dimension 2.
That is one inequality. For the other, run the identical argument on . A basis for its column space sits in its first two columns, so
and the same reading gives , because the row space of is the column space of and the column space of is the row space of . Two inequalities pointing at each other leave one possibility:
Checking it in code
Everything above is small enough to check by hand and small enough to check in four lines. The first block confirms the four bases. Watch do two jobs at once: it certifies the reduction, and its last row is the left null space.
import numpy as np
A = np.array([[1., 2., 2., 4.],
[3., 8., 6., 16.],
[4., 10., 8., 20.]])
R = np.array([[1., 0., 2., 0.],
[0., 1., 0., 2.],
[0., 0., 0., 0.]])
E = np.array([[4., -1., 0.],
[-1.5, 0.5, 0.],
[-1., -1., 1.]])
print(np.linalg.matrix_rank(A)) # 2
print(np.allclose(E @ A, R)) # True the record checks out
s1 = np.array([-2., 0., 1., 0.]) # special solutions -> null space
s2 = np.array([0., -2., 0., 1.])
print(A @ s1, A @ s2) # [0. 0. 0.] [0. 0. 0.]
y = np.array([-1., -1., 1.]) # last row of E -> left null space
print(A.T @ y) # [0. 0. 0. 0.]import torch
A = torch.tensor([[1., 2., 2., 4.],
[3., 8., 6., 16.],
[4., 10., 8., 20.]])
R = torch.tensor([[1., 0., 2., 0.],
[0., 1., 0., 2.],
[0., 0., 0., 0.]])
E = torch.tensor([[4., -1., 0.],
[-1.5, 0.5, 0.],
[-1., -1., 1.]])
print(torch.linalg.matrix_rank(A)) # tensor(2)
print(torch.allclose(E @ A, R)) # True the record checks out
s1 = torch.tensor([-2., 0., 1., 0.]) # special solutions -> null space
s2 = torch.tensor([0., -2., 0., 1.])
print(A @ s1, A @ s2) # tensor([0., 0., 0.]) tensor([0., 0., 0.])
y = torch.tensor([-1., -1., 1.]) # last row of E -> left null space
print(A.T @ y) # tensor([0., 0., 0., 0.])import jax.numpy as jnp
A = jnp.array([[1., 2., 2., 4.],
[3., 8., 6., 16.],
[4., 10., 8., 20.]])
R = jnp.array([[1., 0., 2., 0.],
[0., 1., 0., 2.],
[0., 0., 0., 0.]])
E = jnp.array([[4., -1., 0.],
[-1.5, 0.5, 0.],
[-1., -1., 1.]])
print(jnp.linalg.matrix_rank(A)) # 2
print(jnp.allclose(E @ A, R)) # True the record checks out
s1 = jnp.array([-2., 0., 1., 0.]) # special solutions -> null space
s2 = jnp.array([0., -2., 0., 1.])
print(A @ s1, A @ s2) # [0. 0. 0.] [0. 0. 0.]
y = jnp.array([-1., -1., 1.]) # last row of E -> left null space
print(A.T @ y) # [0. 0. 0. 0.]import tensorflow as tf
A = tf.constant([[1., 2., 2., 4.],
[3., 8., 6., 16.],
[4., 10., 8., 20.]])
R = tf.constant([[1., 0., 2., 0.],
[0., 1., 0., 2.],
[0., 0., 0., 0.]])
E = tf.constant([[4., -1., 0.],
[-1.5, 0.5, 0.],
[-1., -1., 1.]])
print(tf.linalg.matrix_rank(A)) # tf.Tensor(2, ...)
print(tf.reduce_all(tf.abs(E @ A - R) < 1e-6)) # True
s1 = tf.constant([[-2.], [0.], [1.], [0.]]) # special solutions
s2 = tf.constant([[0.], [-2.], [0.], [1.]])
print(A @ s1, A @ s2) # [[0.] [0.] [0.]] [[0.] [0.] [0.]]
y = tf.constant([[-1.], [-1.], [1.]]) # last row of E
print(tf.transpose(A) @ y) # [[0.] [0.] [0.] [0.]]The second block is the rank theorem itself. There is almost nothing to it once is on the table, because the shapes of and are the whole argument.
import numpy as np
A = np.array([[1., 2., 2., 4.], [3., 8., 6., 16.], [4., 10., 8., 20.]])
B = A[:, :2] # pivot columns of A
C = np.array([[1., 0., 2., 0.], [0., 1., 0., 2.]]) # pivot rows of R
print(np.allclose(B @ C, A)) # True
print(B.shape, C.shape) # (3, 2) (2, 4)
print(2 * B[:, 0] + 0 * B[:, 1]) # [2. 6. 8.] = column 3 of A
print(4 * C[0] + 10 * C[1]) # [ 4. 10. 8. 20.] = row 3 of A
print(np.linalg.matrix_rank(A), np.linalg.matrix_rank(A.T)) # 2 2
print(A.shape[1] - np.linalg.matrix_rank(A)) # 2 = dim N(A)
print(A.shape[0] - np.linalg.matrix_rank(A)) # 1 = dim N(A.T)import torch
A = torch.tensor([[1., 2., 2., 4.], [3., 8., 6., 16.], [4., 10., 8., 20.]])
B = A[:, :2] # pivot columns of A
C = torch.tensor([[1., 0., 2., 0.], [0., 1., 0., 2.]]) # pivot rows of R
print(torch.allclose(B @ C, A)) # True
print(B.shape, C.shape) # torch.Size([3, 2]) torch.Size([2, 4])
print(2 * B[:, 0] + 0 * B[:, 1]) # tensor([2., 6., 8.])
print(4 * C[0] + 10 * C[1]) # tensor([ 4., 10., 8., 20.])
print(torch.linalg.matrix_rank(A), torch.linalg.matrix_rank(A.T)) # 2 2
print(A.shape[1] - torch.linalg.matrix_rank(A)) # 2 = dim N(A)
print(A.shape[0] - torch.linalg.matrix_rank(A)) # 1 = dim N(A.T)import jax.numpy as jnp
A = jnp.array([[1., 2., 2., 4.], [3., 8., 6., 16.], [4., 10., 8., 20.]])
B = A[:, :2] # pivot columns of A
C = jnp.array([[1., 0., 2., 0.], [0., 1., 0., 2.]]) # pivot rows of R
print(jnp.allclose(B @ C, A)) # True
print(B.shape, C.shape) # (3, 2) (2, 4)
print(2 * B[:, 0] + 0 * B[:, 1]) # [2. 6. 8.]
print(4 * C[0] + 10 * C[1]) # [ 4. 10. 8. 20.]
print(jnp.linalg.matrix_rank(A), jnp.linalg.matrix_rank(A.T)) # 2 2
print(A.shape[1] - jnp.linalg.matrix_rank(A)) # 2 = dim N(A)
print(A.shape[0] - jnp.linalg.matrix_rank(A)) # 1 = dim N(A.T)import tensorflow as tf
A = tf.constant([[1., 2., 2., 4.], [3., 8., 6., 16.], [4., 10., 8., 20.]])
B = A[:, :2] # pivot columns of A
C = tf.constant([[1., 0., 2., 0.], [0., 1., 0., 2.]]) # pivot rows of R
print(tf.reduce_all(tf.abs(B @ C - A) < 1e-6)) # True
print(B.shape, C.shape) # (3, 2) (2, 4)
print(2 * B[:, 0] + 0 * B[:, 1]) # [2. 6. 8.]
print(4 * C[0] + 10 * C[1]) # [ 4. 10. 8. 20.]
print(tf.linalg.matrix_rank(A), tf.linalg.matrix_rank(tf.transpose(A))) # 2 2
print(A.shape[1] - tf.linalg.matrix_rank(A)) # 2 = dim N(A)
print(A.shape[0] - tf.linalg.matrix_rank(A)) # 1 = dim N(A.T)Where this is going
You now have the four subspaces, their dimensions, and a basis for each, all out of a single elimination. What you do not have is the geometry. Figure 7 drew the row space above the null space as though they were two halves of , and it split into two pieces as though there were only one way to do it. Both of those are true, and both are true for the same reason: the row space and the null space meet at a right angle, and so do the column space and the left null space. That right angle is the next chapter, and it turns this filing cabinet into the fundamental theorem of linear algebra.