Lecture 06
Factorizations: A = CR and A = LU
Split a matrix into simpler matrices and it starts talking: how many independent columns it really has, and exactly what elimination did to it.
66 slides / MIT 18.06, Lecture 4 / Strang §2.6, §1.4

The idea in one sentence
A factorization writes as a product of simpler matrices, and the shape of the pieces answers a question the original block of numbers was keeping to itself.
Two questions a matrix will not answer directly
A matrix arrives as a rectangle of numbers. Two questions come up immediately, and neither one can be read off the rectangle.
The first: how much is actually in here? A table with four columns may only hold three columns worth of information, because one of them is a copy of the others in disguise. You cannot see that by looking.
The second: I need to solve a hundred times with a hundred different right-hand sides. Elimination worked the first time. Do I have to do all of it again?
Each question gets a factorization. answers the first by pulling out the columns that carry real information. answers the second by writing down what elimination did, so it never has to be done twice. Both are the same move: replace one hard matrix by a product of easy ones.
A = CR: the columns that matter, and the recipe
Here is a matrix with something to hide:
Walk left to right and ask each column one question: are you new, or can I already build you?
Column 1 is . Nothing came before it, so it is new. Column 2 is . To be a copy of column 1 it would have to be a single multiple of it, and , and disagree. New. Column 3 is , and , , . Column 3 is column 1 plus column 2. Old. Column 4 is . Building it from the first two would need together with , which force , and then the last entry says . New.
Three new columns. Stand them side by side and call that matrix :
Now every column of is a combination of the columns of , and each combination is three numbers. Write those three numbers as a column and collect them:
Read column by column. Column 1 of is : take one of the first ingredient and none of the others, which returns column 1 of . Column 3 is : one of the first, one of the second, which is the relation you found. Column 4 is : the third ingredient on its own.
holds the ingredients. holds the recipes. Their product is .
Green marks , the columns that carry the information. Yellow marks the column that was a copy and the recipe column of that rebuilds it from the other two.
The number of ingredients has a name. It is the rank of , written , and here it is 3. That is the point of this factorization. You never compute the rank from by a formula. You read it off as the width of .
Look at the shapes. is by . is by , where is the rank. is by . The product has to pass through a waist of width .
The rank is the waist of the factorization. Green is , holding columns that span the column space; orange is , holding rows that span the row space. The same on both sides, which is why the two counts can never disagree.
That waist pays a dividend right away. Read the product the other way: row of is row of applied to , so every row of is a combination of the rows of . That means independent columns force at most independent rows. Run the same argument on transposed and the inequality comes back the other way. The number of independent rows and the number of independent columns have to be the same. Lecture 9 proves it properly.
import numpy as np
A = np.array([[2., 1, 3, 0], [3, 4, 7, 0], [1, 2, 3, -1]])
print(A[:, 2] - A[:, 0] - A[:, 1]) # [0. 0. 0.] col 3 = col 1 + col 2
C = A[:, [0, 1, 3]] # keep the columns that were new
R = np.array([[1., 0, 1, 0], [0, 1, 1, 0], [0, 0, 0, 1]])
print(C @ R - A) # all zeros: A = CR
print(C.shape, R.shape) # (3, 3) (3, 4)
print(np.linalg.matrix_rank(A)) # 3 the width of Cimport torch
A = torch.tensor([[2., 1, 3, 0], [3, 4, 7, 0], [1, 2, 3, -1]])
print(A[:, 2] - A[:, 0] - A[:, 1]) # tensor([0., 0., 0.])
C = A[:, [0, 1, 3]] # keep the columns that were new
R = torch.tensor([[1., 0, 1, 0], [0, 1, 1, 0], [0, 0, 0, 1]])
print(C @ R - A) # all zeros: A = CR
print(C.shape, R.shape) # torch.Size([3, 3]) torch.Size([3, 4])
print(torch.linalg.matrix_rank(A)) # tensor(3) the width of Cimport jax.numpy as jnp
A = jnp.array([[2., 1, 3, 0], [3, 4, 7, 0], [1, 2, 3, -1]])
print(A[:, 2] - A[:, 0] - A[:, 1]) # [0. 0. 0.]
C = A[:, [0, 1, 3]] # keep the columns that were new
R = jnp.array([[1., 0, 1, 0], [0, 1, 1, 0], [0, 0, 0, 1]])
print(C @ R - A) # all zeros: A = CR
print(C.shape, R.shape) # (3, 3) (3, 4)
print(jnp.linalg.matrix_rank(A)) # 3 the width of Cimport tensorflow as tf
A = tf.constant([[2., 1, 3, 0], [3, 4, 7, 0], [1, 2, 3, -1]])
print(A[:, 2] - A[:, 0] - A[:, 1]) # [0. 0. 0.]
C = tf.gather(A, [0, 1, 3], axis=1) # tf spells column pick "gather"
R = tf.constant([[1., 0, 1, 0], [0, 1, 1, 0], [0, 0, 0, 1]])
print(C @ R - A) # all zeros: A = CR
print(C.shape, R.shape) # (3, 3) (3, 4)
print(tf.linalg.matrix_rank(A)) # 3 the width of CThis factorization pays for itself in a place you may already have used. When people fine-tune a large model they freeze the weight matrix and learn a thin and a wide beside it. The trick is called LoRA, and the thing they train is , whose waist is the whole point.
stays frozen and the learning happens in the two slivers beside it. The update has the same shape, so it can touch every entry of , yet its waist is 8: you train 65,536 numbers instead of 16,777,216. That is 0.39% of the matrix, and the rank is what sets the price.
A = LU: the receipt elimination leaves behind
Now the second question. Take the system from Lecture 4:
Elimination cleared the entries below the diagonal in three steps. Each step subtracted a multiple of one row from another, and that multiple is called a multiplier. Write for the number that multiplied row when it was subtracted from row :
and the result is the triangular matrix
It is tempting to keep and throw the multipliers away. That wastes the work. Put the three multipliers into a matrix instead, each one in the slot it came from, with ones down the diagonal:
The receipt. Each elimination step spends one multiplier, and that multiplier is filed at the address of the entry it cleared. Nothing is mixed and nothing is rescaled on the way in.
Then , exactly. Check the bottom row by hand. Row 3 of is times row 1 of , plus times row 2 of , plus times row 3 of :
which is row 3 of . That computation is elimination read backwards. Steps 2 and 3 said
The rows being subtracted are rows of , because a pivot row is finished the moment it becomes a pivot row and never changes again. Move the two terms across the equals sign and row 3 of is a combination of the rows of with the multipliers as weights. That is exactly what computes.
The proof, drawn. Orange holds the finished rows of , yellow holds the weights, and green is what comes out: row 3 of , entry for entry.
Now the payoff on the second question. To solve , solve in two easy stages. Set and solve going down, then solve going up. Both matrices are triangular, so each stage is a walk through the entries with no searching. With this gives and then . The expensive part, building and , happens once. Every right-hand side after that is cheap.
Why the factorization pays. A triangular system solves itself: the first row of gives outright, and every row after it has one new unknown. The orange walk goes down, the green walk comes back up, and yellow is , the answer the first walk hands to the second.
Why the multipliers land unmixed
Something in the last section deserves suspicion. Matrix multiplication normally scrambles everything. Multiply two matrices carrying a 5 and a 4 and you expect a 20 to appear somewhere. Yet the multipliers walked into untouched. Here is the smallest example that shows why.
Let subtract 5 times row 1 from row 2, and let subtract 4 times row 2 from row 3:
Elimination does first and second, so the combined step is the product :
There is the 20, sitting in the corner. It is not noise. replaced row 2 by row 2 minus 5 times row 1. Then subtracted 4 times that new row 2 from row 3, and the new row 2 had row 1 baked into it. So row 3 came out carrying times row 1. Going forward, row 3 feels row 1.
Now undo it. Undoing means adding 5 times row 1 back to row 2, and undoing means adding 4 times row 2 back to row 3. The last step taken has to be the first step undone, so is undone first and the product is :
Clean. The 20 is gone and the two multipliers sit in their own slots. In this order acts first, and at that moment row 2 is still a finished row of with no row 1 in it. Then repairs row 2 and never touches row 3. Row 3 never hears about row 1 at all.
The blue arc is the leak. Going forward, row 1 reaches row 3 through the row 2 that changed underneath it, and the product records that with a 20, marked pink. Going backward in the reversed order that path does not exist, so the corner stays zero and the multipliers survive.
Order is the whole trick. Multiply the inverses in the other order, , and the 20 comes straight back. Lecture 5 gave the rule: undoing a sequence reverses it, socks before shoes on the way in and shoes before socks on the way out. That is what makes readable.
step 0 of 6
Press Next. Three steps clear the matrix into U, and each one files its multiplier in L. Three more steps replay L times U and rebuild A.
Blue is the row being changed, yellow is the multiplier just filed, green rings the pivots.
import numpy as np
A = np.array([[2., 4, -2], [4, 9, -3], [-2, -3, 7]])
E21 = np.array([[1., 0, 0], [-2, 1, 0], [0, 0, 1]]) # 2 x row1 off row2
E31 = np.array([[1., 0, 0], [0, 1, 0], [1, 0, 1]]) # -1 x row1 off row3
E32 = np.array([[1., 0, 0], [0, 1, 0], [0, -1, 1]]) # 1 x row2 off row3
U = E32 @ E31 @ E21 @ A
print(U) # [[2. 4. -2.] [0. 1. 1.] [0. 0. 4.]]
L = np.array([[1., 0, 0], [2, 1, 0], [-1, 1, 1]]) # multipliers in place
print(L @ U - A) # all zeros
b = np.array([2., 8, 10])
c = np.linalg.solve(L, b) # [2. 4. 8.] forward through L
x = np.linalg.solve(U, c) # [-1. 2. 2.] back through U
print(c, x)import torch
A = torch.tensor([[2., 4, -2], [4, 9, -3], [-2, -3, 7]])
E21 = torch.tensor([[1., 0, 0], [-2, 1, 0], [0, 0, 1]]) # 2 x row1 off row2
E31 = torch.tensor([[1., 0, 0], [0, 1, 0], [1, 0, 1]]) # -1 x row1 off row3
E32 = torch.tensor([[1., 0, 0], [0, 1, 0], [0, -1, 1]]) # 1 x row2 off row3
U = E32 @ E31 @ E21 @ A
print(U) # [[2. 4. -2.] [0. 1. 1.] [0. 0. 4.]]
L = torch.tensor([[1., 0, 0], [2, 1, 0], [-1, 1, 1]]) # multipliers in place
print(L @ U - A) # all zeros
b = torch.tensor([2., 8, 10])
c = torch.linalg.solve(L, b) # tensor([2., 4., 8.]) forward through L
x = torch.linalg.solve(U, c) # tensor([-1., 2., 2.]) back through U
print(c, x)import jax.numpy as jnp
A = jnp.array([[2., 4, -2], [4, 9, -3], [-2, -3, 7]])
E21 = jnp.array([[1., 0, 0], [-2, 1, 0], [0, 0, 1]]) # 2 x row1 off row2
E31 = jnp.array([[1., 0, 0], [0, 1, 0], [1, 0, 1]]) # -1 x row1 off row3
E32 = jnp.array([[1., 0, 0], [0, 1, 0], [0, -1, 1]]) # 1 x row2 off row3
U = E32 @ E31 @ E21 @ A
print(U) # [[2. 4. -2.] [0. 1. 1.] [0. 0. 4.]]
L = jnp.array([[1., 0, 0], [2, 1, 0], [-1, 1, 1]]) # multipliers in place
print(L @ U - A) # all zeros
b = jnp.array([2., 8, 10])
c = jnp.linalg.solve(L, b) # [2. 4. 8.] forward through L
x = jnp.linalg.solve(U, c) # [-1. 2. 2.] back through U
print(c, x)import tensorflow as tf
A = tf.constant([[2., 4, -2], [4, 9, -3], [-2, -3, 7]])
E21 = tf.constant([[1., 0, 0], [-2, 1, 0], [0, 0, 1]]) # 2 x row1 off row2
E31 = tf.constant([[1., 0, 0], [0, 1, 0], [1, 0, 1]]) # -1 x row1 off row3
E32 = tf.constant([[1., 0, 0], [0, 1, 0], [0, -1, 1]]) # 1 x row2 off row3
U = E32 @ E31 @ E21 @ A
print(U) # [[2. 4. -2.] [0. 1. 1.] [0. 0. 4.]]
L = tf.constant([[1., 0, 0], [2, 1, 0], [-1, 1, 1]]) # multipliers in place
print(L @ U - A) # all zeros
b = tf.constant([[2.], [8.], [10.]]) # solve wants b as a column
c = tf.linalg.solve(L, b) # [[2.] [4.] [8.]] forward through L
x = tf.linalg.solve(U, c) # [[-1.] [2.] [2.]] back through U
print(c, x)LDU: pulling the pivots out
and look like a mismatched pair. has ones on its diagonal, and has the pivots on its. That asymmetry is cosmetic, and one more factor removes it. Divide each row of by its own pivot and park the pivots in a diagonal matrix :
So , with unit lower triangular, diagonal, and the new unit upper triangular. The two triangular factors are now built the same way and the pivots stand alone in the middle where you can read them. Their product is the determinant, which Lecture 14 collects.
This form is also the only one of its kind. Suppose , with all four triangular factors carrying unit diagonals. Push the s to the left and the s to the right: . The left side is lower triangular, the right side is upper triangular, and a matrix that is both is diagonal. Unit diagonals then force and . Uniqueness looks like bookkeeping. The next section cashes it in: a symmetric matrix has only one triangle to store.
Transposes, and what they are for
Flipping a matrix across its diagonal turns rows into columns. Written , the transpose obeys three rules:
The middle rule is the one worth deriving, because the reversal surprises people. Entry of is entry of , which is row of dotted with column of . Read that same dot product from the other side: it is row of dotted with column of , which is entry of . The order flips because transposing turns the outside indices into the inside ones. The third rule follows from the second: transpose to get .
Here is what those rules buy. Suppose is symmetric, so . Now transpose both sides of , using the product rule twice and because is diagonal:
Look at . It is a unit lower triangular matrix, times a diagonal, times a unit upper triangular matrix, which is an factorization of . And is unique. So , or equivalently , and every symmetric matrix factors as
Take the tridiagonal matrix from Lecture 5. Elimination gives multipliers and and pivots , , , and the factorization mirrors itself:
A symmetric matrix folds along its diagonal. Cyan is one multiplier and its mirror image, yellow is the pivots. Software stores one triangle and the pivots, then gets the other triangle free.
That is worth real money. A symmetric matrix needs half the storage and half the elimination work, because computing hands you for nothing. Covariance matrices are symmetric, and so are the normal equations of Lecture 12. That shortcut runs underneath much of what machine learning code does.
import numpy as np
K = np.array([[2., -1, 0], [-1, 2, -1], [0, -1, 2]])
L = np.array([[1., 0, 0], [-0.5, 1, 0], [0, -2 / 3, 1]])
D = np.diag([2., 1.5, 4 / 3])
print(L @ D @ L.T - K) # zeros: K = L D L transpose
print(np.prod(np.diag(D))) # 4.0 product of the pivots
print(np.linalg.det(K)) # 4.0 and the determinantimport torch
K = torch.tensor([[2., -1, 0], [-1, 2, -1], [0, -1, 2]])
L = torch.tensor([[1., 0, 0], [-0.5, 1, 0], [0, -2 / 3, 1]])
D = torch.diag(torch.tensor([2., 1.5, 4 / 3]))
print(L @ D @ L.T - K) # zeros: K = L D L transpose
print(torch.prod(torch.diag(D))) # tensor(4.) product of pivots
print(torch.linalg.det(K)) # tensor(4.) and determinantimport jax.numpy as jnp
K = jnp.array([[2., -1, 0], [-1, 2, -1], [0, -1, 2]])
L = jnp.array([[1., 0, 0], [-0.5, 1, 0], [0, -2 / 3, 1]])
D = jnp.diag(jnp.array([2., 1.5, 4 / 3]))
print(L @ D @ L.T - K) # zeros: K = L D L transpose
print(jnp.prod(jnp.diag(D))) # 4.0 product of the pivots
print(jnp.linalg.det(K)) # 4.0 and the determinantimport tensorflow as tf
K = tf.constant([[2., -1, 0], [-1, 2, -1], [0, -1, 2]])
L = tf.constant([[1., 0, 0], [-0.5, 1, 0], [0, -2 / 3, 1]])
D = tf.linalg.diag(tf.constant([2., 1.5, 4 / 3]))
print(L @ D @ tf.transpose(L) - K) # zeros: K = L D L transpose
print(tf.reduce_prod(tf.linalg.diag_part(D))) # 4.0 prod is reduce_prod
print(tf.linalg.det(K)) # 4.0 and the determinantWhen a pivot is zero: PA = LU
Everything above assumed elimination never got stuck. It gets stuck. Try this matrix:
Clear the first column. Row 2 minus 2 times row 1 gives . Row 3 minus row 1 gives . Now look at the second pivot position, the middle of the matrix. It holds a zero, and you cannot divide by zero. Elimination has nothing to work with.
Nothing is wrong with . Its rank is 3 and it is invertible. The rows arrived in an unlucky order. Exchange rows 2 and 3 and the problem disappears.
Exchanging rows is itself a matrix. A permutation matrix is the identity with its rows reordered, and multiplying by it reorders the rows of whatever it hits. To swap rows 2 and 3, take the identity and swap rows 2 and 3:
Permutation matrices have a pleasant property that falls out of the transpose rules. The columns of are the standard basis vectors in some order, so entry of is column of dotted with column of , which is 1 when and 0 otherwise. So , and
To undo a shuffle, transpose it. Now eliminate . Row 2 minus 1 times row 1 gives , and row 3 minus 2 times row 1 gives , which already has its zero in the right place, so the third multiplier is 0. The multipliers are , , :
The zero in the middle comes from the order the rows arrived in. The matrix itself is fine. Watch the orange and cyan rows trade places: after the swap, the same elimination runs to the end and lands three yellow pivots.
So the honest general statement is . For every square matrix there is a row order that makes elimination work, and records it.
import numpy as np
A = np.array([[1., 2, 1], [2, 4, 3], [1, 1, 2]])
E = np.array([[1., 0, 0], [-2, 1, 0], [-1, 0, 1]]) # clear column 1
print(E @ A) # [[1. 2. 1.] [0. 0. 1.] [0. -1. 1.]] pivot is 0
P = np.array([[1., 0, 0], [0, 0, 1], [0, 1, 0]]) # swap rows 2 and 3
print(P.T @ P) # identity: P inverse is P transpose
L = np.array([[1., 0, 0], [1, 1, 0], [2, 0, 1]])
U = np.array([[1., 2, 1], [0, -1, 1], [0, 0, 1]])
print(L @ U - P @ A) # all zeros: PA = LUimport torch
A = torch.tensor([[1., 2, 1], [2, 4, 3], [1, 1, 2]])
E = torch.tensor([[1., 0, 0], [-2, 1, 0], [-1, 0, 1]]) # clear column 1
print(E @ A) # [[1. 2. 1.] [0. 0. 1.] [0. -1. 1.]] pivot is 0
P = torch.tensor([[1., 0, 0], [0, 0, 1], [0, 1, 0]]) # swap rows 2 and 3
print(P.T @ P) # identity: P inverse is P transpose
L = torch.tensor([[1., 0, 0], [1, 1, 0], [2, 0, 1]])
U = torch.tensor([[1., 2, 1], [0, -1, 1], [0, 0, 1]])
print(L @ U - P @ A) # all zeros: PA = LUimport jax.numpy as jnp
A = jnp.array([[1., 2, 1], [2, 4, 3], [1, 1, 2]])
E = jnp.array([[1., 0, 0], [-2, 1, 0], [-1, 0, 1]]) # clear column 1
print(E @ A) # [[1. 2. 1.] [0. 0. 1.] [0. -1. 1.]] pivot is 0
P = jnp.array([[1., 0, 0], [0, 0, 1], [0, 1, 0]]) # swap rows 2 and 3
print(P.T @ P) # identity: P inverse is P transpose
L = jnp.array([[1., 0, 0], [1, 1, 0], [2, 0, 1]])
U = jnp.array([[1., 2, 1], [0, -1, 1], [0, 0, 1]])
print(L @ U - P @ A) # all zeros: PA = LUimport tensorflow as tf
A = tf.constant([[1., 2, 1], [2, 4, 3], [1, 1, 2]])
E = tf.constant([[1., 0, 0], [-2, 1, 0], [-1, 0, 1]]) # clear column 1
print(E @ A) # [[1. 2. 1.] [0. 0. 1.] [0. -1. 1.]] pivot is 0
P = tf.constant([[1., 0, 0], [0, 0, 1], [0, 1, 0]]) # swap rows 2 and 3
print(tf.transpose(P) @ P) # identity: P inverse is P transpose
L = tf.constant([[1., 0, 0], [1, 1, 0], [2, 0, 1]])
U = tf.constant([[1., 2, 1], [0, -1, 1], [0, 0, 1]])
print(L @ U - P @ A) # all zeros: PA = LUWhere this is going
Two factorizations, two answers. freezes elimination so it never has to run twice, and patches the one case where it stalls. found the columns that carry information and named their count the rank.
But leaned on a word that has not been defined. Column 3 was called a copy because it was a combination of two others, and column 4 was called new because it was not. Behind that test sits a bigger object: the set of every vector you can build out of the columns of . It has a shape and a dimension, and it is the reason the rank means anything at all. Naming it takes the next chapter.