Lecture 12
Least Squares & Linear Regression
No line goes through all the data. Least squares picks the one that misses by the least, and it is the projection from the last chapter wearing different clothes.
65 slides / MIT 18.06, Lecture 16 / Strang §4.3

The idea in one sentence
When no exact answer exists, take the one whose leftover error is as small as it can be, and that answer is the projection from the last chapter read as a list of weights.
Five equations, two knobs
Here is the measurements table one last time, with all five rows asking for the same pair of coefficients and :
Lecture 11 showed there is no answer. Rank 2 for the matrix of coefficients, rank 3 once the target joins as a third column, so the target points somewhere the two columns cannot go. That is the normal situation for data, and a chapter that stops there has taught you nothing you can use. This one solves it anyway.
The same numbers, read two ways
Before solving, look at the failure from both sides, because the two views explain each other.
Two features span a plane, and a plane inside five-dimensional space is hard to draw. So for the pictures, use one feature and an intercept: keep the weight column and the target column. Write for the weight in row and for its target, and fit the two unknowns and in
Every argument below works for any number of columns. The two-feature answer arrives at the end of the next section.
Now the two readings. Read a row as a point. Row one says the pair was measured, and a candidate line either passes near it or does not. Five rows are five dots on a page, and the unknowns and pick out one line among all lines. That is data space.
Read the same row as a constraint instead. Row one demands , and the pairs that satisfy it form a straight line in a plane whose axes are the unknowns. Five rows are five lines, and a candidate line in data space is a single dot here. That is weight space.
The same five rows in two rooms. On the left each row is a dot and a candidate model is a line. On the right each row is a line and a candidate model is a dot. The bold line and the bold dot are the same candidate. No dot sits on all five lines, so no line passes through all five dots.
The two rooms hold the same information turned inside out. A line over there is a point over here. Fitting the data exactly would mean finding one point that sits on all five weight-space lines at once, and the picture says there is no such point. Every pair of lines crosses somewhere different, which is another way of saying the first two rows and the third row want different coefficients.
Choosing what best means
No point sits on all five lines, so pick a rule for scoring the ones that do not. Write the model as , where holds the columns, holds the unknown coefficients, and holds the targets. Any candidate leaves a leftover , one number per row, and the score is the total of their squares:
Squaring is a choice, and absolute values would also measure the leftovers. Two reasons pick squares. The first is geometric. That sum is the ordinary squared distance from the point to the point , so making it small means finding the reachable point closest to , which is the projection Lecture 11 already built. The second is practical: squares make the problem linear, so a formula exists. Absolute values give a perfectly sensible score with no closed form.
Squaring has a cost. A leftover of 4 counts as much as sixteen leftovers of 1, so one bad outlier can drag the whole fit toward itself. Every model fit with squares carries that sensitivity.
The score has a second reading, back in weight space. Write for row of , so row asks for and draws one line in the picture of unknowns. The distance from a candidate point to that line is , which is the leftover divided by the length of the row. Squaring and adding gives
where is that distance. So the answer settles at the point closest to all five lines at once, with each line pulling in proportion to the squared length of its row. A row of big numbers is a line that insists.
So the least-squares answer is the coefficient list of the projection, and is the shadow of the data on what the model can reach. Nothing new has happened yet. The coefficients now have a meaning, and the leftover has a name: the residual.
The normal equations, twice
Two roads lead to the same equation. Take both, because each answers a question the other leaves open.
By perpendicularity. This is the argument from Lecture 11, one line long. At the closest point, the residual is perpendicular to every column of , and stacking those conditions is what does:
That pair of equations is called the normal equations, from the old word normal meaning perpendicular. They are the whole of least squares.
By algebra. The first road shows that is a special point. It does not show that beats every other point, and it does not show that no second point ties with it. Expand the score instead. For any ,
Let be a solution of and write any other candidate as . Substitute and collect terms. The cross terms combine into a single bracket:
The bracket is zero, by the choice of . And . So
Every other candidate scores worse by exactly , the squared length of the change that move makes inside the column space. Equality needs , and with independent columns that forces . So is the winner and it is the only one. Lecture 11 proved , which is the same independence condition seen from the other end, and it is what lets you write when you want the formula spelled out.
The answer, in numbers
For the line fit, has a column of ones and the weight column. Then counts, sums, and sums of squares:
The 5 is the number of rows, the 15 is , the 55 is , the 29 is , and the 70 is . The determinant is , and solving gives and exactly.
The line that least squares picks, with every residual drawn. The circles on the line are what the model predicts, the filled dots are what was measured, and the dashed gaps between them are what the fit could not explain.
The five fitted values are , , , , , and the residuals are , , , , . Squared and added they come to . Try any other line and you get a bigger number, which the lab below lets you test for yourself.
Two facts about those residuals are not luck.
Perpendicularity in numbers. The first row of is the intercept column, so its dot product with the residual is the plain sum. The second row is the weight column, so its dot product is the weighted sum. Both come out zero.
They add to zero, and their weighted total is zero as well. Both are just read one row at a time. The first row of is the row of ones, so its dot product with is the plain sum. The second row is , so its dot product is the weighted sum. Any model with an intercept column has residuals summing to zero, and that comes from perpendicularity alone. The data has no say in it.
It also explains why no better line exists. To improve the fit you would tilt or shift the line, which changes the prediction by some combination of the two columns. But the residual leans on neither column, so every such change moves you away from .
Watch what one move costs. Hold the slope at and slide the intercept away from by an amount , which is the direction . The algebra above says the score becomes , and for this is the top left entry of , which is 5. So one full unit of shift costs exactly 5.
One straight walk away from the answer. Sliding the intercept by raises the score by , so the bottom of this parabola sits exactly where the normal equations put it.
Here is the whole story as one surface. Score every possible pair by its total squared error, and the result is a bowl. The algebra above already told you its shape: , so the level sets are ellipses centered on the answer.
The same story in two dimensions. Each ellipse collects the lines with one fixed total squared error, and the dot at the center is the winner. The long thin shape means the score hardly changes along one direction, which is the direction a fitted model is least sure about.
The ellipses are long and thin, and that ratio matters later. It says the score barely changes along one direction and changes fast along another, so a fit can be confidently wrong about the individual coefficients while being right about their combination.
best line: 10.9 −1.7w
its error: 9.90
your error: 21.30
Drag any dot to change the data. Green is the line the normal equations name, warm is yours, and the blue springs are the residuals it has to live with.
Now the original two-feature question, with and as the columns and no intercept:
Solving gives , which is about . The
fitted values are , , , , , and the squared residuals add to
. The code below computes it, checks it against np.linalg.lstsq, and confirms that the
residual dots to zero against both columns. The fit is poor, and it should be: the numbers in the
table were never generated by a rule. Least squares always returns an answer. Reading whether that
answer means anything is your job, and the formula will not do it for you.
import numpy as np
w = np.array([5., 3., 2., 1., 4.])
h = np.array([2., 4., 8., 5., 7.])
b = np.array([3., 8., 7., 9., 2.])
A = np.column_stack([w, h])
print(A.T @ A) # [[ 55. 71.] [ 71. 158.]]
print(A.T @ b) # [ 70. 153.]
x_hat = np.linalg.solve(A.T @ A, A.T @ b)
print(np.round(x_hat, 4)) # [0.054 0.9441]
print(np.round(np.linalg.lstsq(A, b, rcond=None)[0], 4)) # the same numbers
e = b - A @ x_hat
print(bool(abs(A.T @ e).max() < 1e-6)) # True e leans on neither column
print(round(float(e @ e), 4)) # 58.7745
M = np.column_stack([np.ones(5), w]) # intercept column, then one feature
print(M.T @ M) # [[ 5. 15.] [15. 55.]]
print(M.T @ b) # [29. 70.]
c, d = np.linalg.solve(M.T @ M, M.T @ b)
print(round(c, 10), round(d, 10)) # 10.9 -1.7
r = b - M @ np.array([c, d])
print(np.round(M @ np.array([c, d]), 4)) # [2.4 5.8 7.5 9.2 4.1]
print(np.round(r, 4)) # [ 0.6 2.2 -0.5 -0.2 -2.1]
print(bool(abs(r.sum()) < 1e-6), bool(abs(w @ r) < 1e-6)) # True True
print(round(float(r @ r), 4)) # 9.9
for C, D in [(11.0, -1.7), (10.9, -1.6), (10.0, -1.5)]:
print(C, D, round(float(((b - (C + D * w)) ** 2).sum()), 4))
# 11.0 -1.7 9.95 / 10.9 -1.6 10.45 / 10.0 -1.5 10.75: all worse than 9.9import torch
w = torch.tensor([5., 3., 2., 1., 4.])
h = torch.tensor([2., 4., 8., 5., 7.])
b = torch.tensor([3., 8., 7., 9., 2.])
A = torch.stack([w, h], dim=1)
print(A.T @ A) # [[ 55. 71.] [ 71. 158.]]
print(A.T @ b) # [ 70. 153.]
x_hat = torch.linalg.solve(A.T @ A, A.T @ b)
print(torch.round(x_hat, decimals=4)) # [0.054 0.9441]
print(torch.round(torch.linalg.lstsq(A, b).solution, decimals=4)) # the same
e = b - A @ x_hat
print(bool(abs(A.T @ e).max() < 1e-4)) # True e leans on neither column
print(round(float(e @ e), 4)) # 58.7745
M = torch.stack([torch.ones(5), w], dim=1) # intercept, then one feature
print(M.T @ M) # [[ 5. 15.] [15. 55.]]
print(M.T @ b) # [29. 70.]
cd = torch.linalg.solve(M.T @ M, M.T @ b)
print(round(float(cd[0]), 4), round(float(cd[1]), 4)) # 10.9 -1.7
r = b - M @ cd
print(torch.round(M @ cd, decimals=4)) # [2.4 5.8 7.5 9.2 4.1]
print(torch.round(r, decimals=4)) # [ 0.6 2.2 -0.5 -0.2 -2.1]
print(bool(abs(r.sum()) < 1e-4), bool(abs(w @ r) < 1e-4)) # True True
print(round(float(r @ r), 4)) # 9.9
for C, D in [(11.0, -1.7), (10.9, -1.6), (10.0, -1.5)]:
print(C, D, round(float(((b - (C + D * w)) ** 2).sum()), 4))
# 11.0 -1.7 9.95 / 10.9 -1.6 10.45 / 10.0 -1.5 10.75: all worse than 9.9import jax.numpy as jnp
w = jnp.array([5., 3., 2., 1., 4.])
h = jnp.array([2., 4., 8., 5., 7.])
b = jnp.array([3., 8., 7., 9., 2.])
A = jnp.column_stack([w, h])
print(A.T @ A) # [[ 55. 71.] [ 71. 158.]]
print(A.T @ b) # [ 70. 153.]
x_hat = jnp.linalg.solve(A.T @ A, A.T @ b)
print(jnp.round(x_hat, 4)) # [0.054 0.9441]
print(jnp.round(jnp.linalg.lstsq(A, b)[0], 4)) # the same numbers
e = b - A @ x_hat
print(bool(abs(A.T @ e).max() < 1e-4)) # True e leans on neither column
print(round(float(e @ e), 4)) # 58.7745
M = jnp.column_stack([jnp.ones(5), w]) # intercept column, then one feature
print(M.T @ M) # [[ 5. 15.] [15. 55.]]
print(M.T @ b) # [29. 70.]
cd = jnp.linalg.solve(M.T @ M, M.T @ b)
print(round(float(cd[0]), 4), round(float(cd[1]), 4)) # 10.9 -1.7
r = b - M @ cd
print(jnp.round(M @ cd, 4)) # [2.4 5.8 7.5 9.2 4.1]
print(jnp.round(r, 4)) # [ 0.6 2.2 -0.5 -0.2 -2.1]
print(bool(abs(r.sum()) < 1e-4), bool(abs(w @ r) < 1e-4)) # True True
print(round(float(r @ r), 4)) # 9.9
for C, D in [(11.0, -1.7), (10.9, -1.6), (10.0, -1.5)]:
print(C, D, round(float(((b - (C + D * w)) ** 2).sum()), 4))
# 11.0 -1.7 9.95 / 10.9 -1.6 10.45 / 10.0 -1.5 10.75: all worse than 9.9import tensorflow as tf
w = tf.constant([5., 3., 2., 1., 4.])
h = tf.constant([2., 4., 8., 5., 7.])
b = tf.constant([3., 8., 7., 9., 2.])
mv = tf.linalg.matvec
gap = lambda z: float(tf.reduce_max(tf.abs(z)))
solve = lambda G, v: tf.linalg.solve(G, v[:, None])[:, 0]
A = tf.stack([w, h], axis=1)
At = tf.transpose(A)
print(At @ A) # [[ 55. 71.] [ 71. 158.]]
print(mv(At, b)) # [ 70. 153.]
x_hat = solve(At @ A, mv(At, b))
print(tf.round(x_hat * 1e4) / 1e4) # [0.054 0.9441]
print(tf.round(tf.linalg.lstsq(A, b[:, None])[:, 0] * 1e4) / 1e4) # the same
e = b - mv(A, x_hat)
print(gap(mv(At, e)) < 1e-4) # True e leans on neither column
print(round(float(tf.tensordot(e, e, 1)), 4)) # 58.7745
M = tf.stack([tf.ones(5), w], axis=1) # intercept column, then one feature
Mt = tf.transpose(M)
print(Mt @ M) # [[ 5. 15.] [15. 55.]]
print(mv(Mt, b)) # [29. 70.]
cd = solve(Mt @ M, mv(Mt, b))
print(round(float(cd[0]), 4), round(float(cd[1]), 4)) # 10.9 -1.7
r = b - mv(M, cd)
print(tf.round(mv(M, cd) * 1e4) / 1e4) # [2.4 5.8 7.5 9.2 4.1]
print(tf.round(r * 1e4) / 1e4) # [ 0.6 2.2 -0.5 -0.2 -2.1]
print(gap(tf.reduce_sum(r)) < 1e-4, gap(tf.tensordot(w, r, 1)) < 1e-4)
print(round(float(tf.tensordot(r, r, 1)), 4)) # 9.9
for C, D in [(11.0, -1.7), (10.9, -1.6), (10.0, -1.5)]:
print(C, D, round(float(tf.reduce_sum((b - (C + D * w)) ** 2)), 4))
# 11.0 -1.7 9.95 / 10.9 -1.6 10.45 / 10.0 -1.5 10.75: all worse than 9.9More columns, and the trap that comes with them
Nothing in the derivation cared what the columns held. They can hold anything computed from the input, and the fit is still linear in the unknowns, which is all least squares ever needed. Put , , and in three columns and you are fitting a parabola with the same machinery.
Take five readings of something that bends: with values . With two columns the best line is and the error is . Add the column and the best parabola is , with error .
Five readings that bend. The dashed line is the best a straight fit can do, and the solid curve is the best parabola, found by the same normal equations with one more column.
A curve is the same projection as before, onto a column space that now happens to contain curved shapes.
Adding columns can only lower the error, since the old fit is still available with a zero coefficient on the new column. That sounds like good news until you follow it to the end. The measurements table has five rows and five distinct weights, so five columns of powers reach every possible target vector, and the error drops to exactly zero.
Five points and five columns of powers. The curve hits every point exactly, so the error is zero, and it tells you nothing about a weight it has not seen.
The curve through all five points is not a discovery. It is the data written in a different alphabet. It dips below between points that never left the range to , and asked about a weight of it answers with total confidence. The error went to zero because the model ran out of things to be wrong about.
This is the training step
Nothing in this chapter mentioned learning, and yet this is what fitting a linear model is. The columns are features, the unknowns are weights, the score is the loss, and the answer is the trained model. When a library fits a linear regression it forms the normal equations, or it does something numerically better that lands in the same place.
Gradient descent lands there too. The gradient of the score is
and setting it to zero is the normal equations again, rearranged. So descent walks downhill on the bowl from Figure 5 toward the point the normal equations name. It gets there slowly when the bowl is long and thin, because the step that suits one direction overshoots the other, and that zigzag has a name and a cure that arrive in Lecture 15.
Gradient descent on the same bowl, started at the open circle. Every step points straight downhill, and the steepest direction points across the valley, so the walk zigzags toward the point the normal equations name in one shot.
import numpy as np
t = np.array([-2., -1., 0., 1., 2.])
y = np.array([7., 1., 0., 3., 9.])
line = np.column_stack([np.ones(5), t])
para = np.column_stack([np.ones(5), t, t ** 2])
c_line = np.linalg.solve(line.T @ line, line.T @ y)
c_para = np.linalg.solve(para.T @ para, para.T @ y)
sse = lambda X, c: round(float(((y - X @ c) ** 2).sum()), 2)
print(np.round(c_line, 4), sse(line, c_line))
print(np.round(c_para, 4), sse(para, c_para))
# [4. 0.6] 56.4 a line cannot bend
# [0. 0.6 2. ] 0.4 one more column, and it can
w = np.array([5., 3., 2., 1., 4.])
b = np.array([3., 8., 7., 9., 2.])
for k in (2, 3, 5): # k columns of powers of w
V = np.vander(w, k)
c = np.linalg.lstsq(V, b, rcond=None)[0]
print(k, round(float(((b - V @ c) ** 2).sum()), 2))
# 2 9.9 / 3 9.83 / 5 0.0 five columns, five rows, nothing left to get wrong
print(f"{float(np.linalg.cond(np.vander(w, 5))):.1e}") # 2.6e+04
A = np.column_stack([np.ones(5), w]) # gradient descent finds the same point
x = np.zeros(2)
for step in range(3000):
x = x - 0.008 * (2 * A.T @ (A @ x - b))
print(np.round(x, 4)) # [10.9 -1.7]import torch
t = torch.tensor([-2., -1., 0., 1., 2.])
y = torch.tensor([7., 1., 0., 3., 9.])
line = torch.stack([torch.ones(5), t], dim=1)
para = torch.stack([torch.ones(5), t, t ** 2], dim=1)
c_line = torch.linalg.solve(line.T @ line, line.T @ y)
c_para = torch.linalg.solve(para.T @ para, para.T @ y)
sse = lambda X, c: round(float(((y - X @ c) ** 2).sum()), 2)
print(torch.round(c_line, decimals=4), sse(line, c_line))
print(torch.round(c_para, decimals=4), sse(para, c_para))
# [4. 0.6] 56.4 a line cannot bend
# [0. 0.6 2. ] 0.4 one more column, and it can
w = torch.tensor([5., 3., 2., 1., 4.])
b = torch.tensor([3., 8., 7., 9., 2.])
for k in (2, 3, 5): # k columns of powers of w
V = torch.stack([w ** j for j in range(k - 1, -1, -1)], dim=1)
c = torch.linalg.lstsq(V, b, driver='gelsd').solution
print(k, round(float(((b - V @ c) ** 2).sum()), 2))
# 2 9.9 / 3 9.83 / 5 0.0 five columns, five rows, nothing left to get wrong
V5 = torch.stack([w ** j for j in range(4, -1, -1)], dim=1)
print(f"{float(torch.linalg.cond(V5)):.1e}") # 2.6e+04
A = torch.stack([torch.ones(5), w], dim=1) # gradient descent, same point
x = torch.zeros(2)
for step in range(3000):
x = x - 0.008 * (2 * A.T @ (A @ x - b))
print(torch.round(x, decimals=4)) # [10.9 -1.7]import jax.numpy as jnp
t = jnp.array([-2., -1., 0., 1., 2.])
y = jnp.array([7., 1., 0., 3., 9.])
line = jnp.column_stack([jnp.ones(5), t])
para = jnp.column_stack([jnp.ones(5), t, t ** 2])
c_line = jnp.linalg.solve(line.T @ line, line.T @ y)
c_para = jnp.linalg.solve(para.T @ para, para.T @ y)
sse = lambda X, c: round(float(((y - X @ c) ** 2).sum()), 2)
print(jnp.round(c_line, 4), sse(line, c_line))
print(jnp.round(c_para, 4), sse(para, c_para))
# [4. 0.6] 56.4 a line cannot bend
# [0. 0.6 2. ] 0.4 one more column, and it can
w = jnp.array([5., 3., 2., 1., 4.])
b = jnp.array([3., 8., 7., 9., 2.])
for k in (2, 3, 5): # k columns of powers of w
V = jnp.vander(w, k)
c = jnp.linalg.lstsq(V, b)[0]
print(k, round(float(((b - V @ c) ** 2).sum()), 2))
# 2 9.9 / 3 9.83 / 5 0.0 five columns, five rows, nothing left to get wrong
print(f"{float(jnp.linalg.cond(jnp.vander(w, 5))):.1e}") # 2.6e+04
A = jnp.column_stack([jnp.ones(5), w]) # gradient descent, same point
x = jnp.zeros(2)
for step in range(3000):
x = x - 0.008 * (2 * A.T @ (A @ x - b))
print(jnp.round(x, 4)) # [10.9 -1.7]import tensorflow as tf
mv = tf.linalg.matvec
solve = lambda G, v: tf.linalg.solve(G, v[:, None])[:, 0]
fit = lambda X, v: solve(tf.transpose(X) @ X, mv(tf.transpose(X), v))
sse = lambda X, c, v: round(float(tf.reduce_sum((v - mv(X, c)) ** 2)), 2)
t = tf.constant([-2., -1., 0., 1., 2.])
y = tf.constant([7., 1., 0., 3., 9.])
line = tf.stack([tf.ones(5), t], axis=1)
para = tf.stack([tf.ones(5), t, t ** 2], axis=1)
c_line, c_para = fit(line, y), fit(para, y)
print(tf.round(c_line * 1e4) / 1e4, sse(line, c_line, y))
print(tf.round(c_para * 1e4) / 1e4, sse(para, c_para, y))
# [4. 0.6] 56.4 a line cannot bend
# [0. 0.6 2. ] 0.4 one more column, and it can
w = tf.constant([5., 3., 2., 1., 4.])
b = tf.constant([3., 8., 7., 9., 2.])
for k in (2, 3, 5): # k columns of powers of w
V = tf.stack([w ** float(j) for j in range(k - 1, -1, -1)], axis=1)
c = tf.linalg.lstsq(V, b[:, None], fast=False)[:, 0]
print(k, sse(V, c, b))
# 2 9.9 / 3 9.83 / 5 0.0 five columns, five rows, nothing left to get wrong
V5 = tf.stack([w ** float(j) for j in range(4, -1, -1)], axis=1)
s = tf.linalg.svd(V5, compute_uv=False)
print(f"{float(s[0] / s[-1]):.1e}") # 2.6e+04
A = tf.stack([tf.ones(5), w], axis=1) # gradient descent finds the same point
x = tf.zeros(2)
for step in range(3000):
x = x - 0.008 * (2 * mv(tf.transpose(A), mv(A, x) - b))
print(tf.round(x * 1e4) / 1e4) # [10.9 -1.7]Where this is going
Least squares is the first honest answer this course has given to a question with no answer. It came from one geometric fact, that the shortest distance to a subspace is measured along a perpendicular, and it produced a formula you can compute.
Two threads run onward. The first is numerical. Forming is the wrong way to compute , and Lecture 13 shows the right way by building orthonormal columns first, which turns the normal equations into back substitution and stops the condition number from being squared. The second is what happens when the columns are dependent, so has no inverse and the best answer is a whole flat set of them. Choosing sensibly among that set is what the pseudoinverse does, and Part VI cashes it in with the singular value decomposition.