Monocurl Essays
Series Download

What "learning" means: fitting a line to data

A model is a formula with adjustable numbers, a loss scores its misses, and training lowers the loss. All three, worked by hand on four house prices.

23 min·Early college·

AssumesCorrelation and the line of best fit, derivedWhat slope means, and why y = mx + b is a line

Four house sales and a line that predicts price from size. Each miss is drawn as a square. As the slope and intercept change, the average area of the squares, which is the mean squared error, falls from 1.25 to its smallest possible value, 0.2.
Monocurl source
let fd = DataFrame([-1.4, 0.2, 0], 3.5, 3.0)
let Fit = |w, b| [
    Squares(fd, w, b),
    LineOn(fd, w, b),
    Houses(fd),
    center{[2.4, 1.2, 0]} scale{1.2} color{EMBER} Number(w, 2),
    center{[2.4, 0.1, 0]} scale{1.2} color{EMBER} Number(b, 2),
    center{[2.4, -1.2, 0]} scale{1.55} color{ROSE} Number(loss(w, b), 2)
]
mesh plot = DataPlot(fd)
mesh side = [
    center{[2.4, 1.65, 0]} Note("slope w", 0.74),
    center{[2.4, 0.55, 0]} Note("intercept b", 0.74),
    center{[2.4, -0.6, 0]} Cap("mean squared error", 0.76, ROSE)
]
mesh fit = Fit(w: 1, b: 0)

slide "fit"
    play Wait(0.9)
    fit.w = 1.37
    play Lerp(1.8, [&fit])
    play Wait(0.4)
    fit.b = 1.4
    fit.w = 0.84
    play Lerp(2, [&fit])
    fit.b = 0.5
    fit.w = 1.2
    play Lerp(2, [&fit])
    play Wait(1.8)

Uses the shared definitions for this lesson's figures.

A house of 2500 square feet comes up for sale in a neighborhood where four houses have sold recently. What will it sell for?

HouseSize (thousands of sq ft)Price (hundreds of thousands of $)
111.5
223.5
333.5
445.5

No house of that size is in the table, so you cannot look the answer up. You would probably reason that bigger houses cost more and that 2500 square feet sits between houses 2 and 3, which both sold for $350,000, so something near that. That reasoning is a prediction rule, and it stops working once the table has forty houses and no two neighbors agree. Machine learning makes such rules explicit enough that a computer can find a good one. The phrase “learning from data” sounds mysterious, but for this problem every part of it can be written down and checked with a pencil.

A rule with two dials

The simplest rule that says “bigger costs more” is a straight line. Write $x$ for the size of a house in thousands of square feet and $\hat y$ (read “y hat”) for the predicted price in hundreds of thousands of dollars, and let

$$ \hat y = wx + b. $$

The two numbers $w$ and $b$ are not fixed yet. Think of them as dials. The dial $w$ is the slope: how much each extra thousand square feet adds to the price. The dial $b$ is the intercept, a baseline that shifts the whole line up or down. Every setting of the two dials is a different rule.

A common rule of thumb is “a hundred dollars per square foot”. A thousand square feet then costs $100,000, which in our units is $w = 1$ and $b = 0$: the rule $\hat y = x$. It predicts prices of $1, 2, 3, 4$ for the four houses. They actually sold for $1.5, 3.5, 3.5, 5.5$. The rule is too low every time, by $0.5, 1.5, 0.5$ and $1.5$.

Is that good or bad? Compared with what? We cannot improve a rule until we can say, in one number, how wrong it is.

Scoring the misses

For each house, call the prediction minus the actual price the error of the rule on that house:

$$ e_i = \hat y_i - y_i = wx_i + b - y_i. $$

For the rule of thumb the four errors are $-0.5, -1.5, -0.5, -1.5$. A natural first idea is to add them up, and it fails: a rule that is $100,000 too high on one house and $100,000 too low on another would score zero, the same as a rule that is exactly right on both. Misses in opposite directions must not cancel. The repair used throughout statistics, as in the lesson on the line of best fit, is to square each error first. Squares are never negative, so nothing cancels, and a miss twice as large counts four times as much. Then average, so that the score does not grow just because the table got longer.

For the rule of thumb the squared errors are $0.25, 2.25, 0.25, 2.25$, and their average is $5/4 = 1.25$.

The rule of thumb \hat y = x (slope 1, intercept 0) against the four sales. Each error is the vertical gap between a dot and the line, and the square drawn on it has the squared error as its area. The mean of the four squared errors is 1.25.
The rule of thumb $\hat y = x$ (slope 1, intercept 0) against the four sales. Each error is the vertical gap between a dot and the line, and the square drawn on it has the squared error as its area. The mean of the four squared errors is 1.25.
Monocurl source
let fd = DataFrame([-1.4, 0.2, 0], 3.5, 3.0)
mesh plot = DataPlot(fd)
mesh squares = Squares(fd, 1, 0)
mesh line = LineOn(fd, 1, 0)
mesh dots = Houses(fd)
mesh side = [
    center{[2.4, 1.7, 0]} Math("\hat y = 1 \cdot x + 0", 0.9, EMBER),
    center{[2.4, 1.05, 0]} Note("errors", 0.74),
    map(range(0, 4), |i| center{[1.35 + i * 0.7, 0.62, 0]} Math(to_string(HX[i] - HY[i], 1), 0.72, INK)),
    center{[2.4, 0.05, 0]} Note("squared errors", 0.74),
    map(range(0, 4), |i| center{[1.35 + i * 0.7, -0.38, 0]} Math(to_string((HX[i] - HY[i]) ^ 2, 2), 0.72, ROSE)),
    center{[2.4, -1.0, 0]} Cap("their mean", 0.76, ROSE),
    center{[2.4, -1.5, 0]} Math(to_string(loss(1, 0), 2), 1.15, ROSE)
]

Uses the shared definitions for this lesson's figures.

Pause and try

Since the rule of thumb is too low on every house, raise the whole line by 1: keep $w = 1$ and set $b = 1$. Compute the four errors and the mean of their squares.

Show answerHide answer

The predictions are $2, 3, 4, 5$, and the errors are $2 - 1.5 = 0.5$, $3 - 3.5 = -0.5$, $4 - 3.5 = 0.5$, $5 - 5.5 = -0.5$. Every squared error is $0.25$, so the mean is $0.25$. One turn of one dial cut the score from $1.25$ to a fifth of that.

We now have the three objects that every problem of this kind is made of, and they have standard names.

Read this against what we did. The model is the rule with its dials, and the parameters are the dials. The loss is the score, and note what its inputs are: the data are fixed, so $L$ is a function of $w$ and $b$ alone. We found $L(1, 0) = 1.25$ and $L(1, 1) = 0.25$. Training is the dial-turning, and the definition says nothing yet about how to do it. The word “learning” in machine learning refers to this search.

Turning one dial

The most cautious way to search is to leave $b = 0$ and turn only the slope. With $b = 0$ the four errors are $w - 1.5$, $2w - 3.5$, $3w - 3.5$ and $4w - 5.5$. Their squares add up to $(1 + 4 + 9 + 16)\,w^2 - (3 + 14 + 21 + 44)\,w + (2.25 + 12.25 + 12.25 + 30.25) = 30w^2 - 82w + 57$, and dividing by 4,

$$ L(w, 0) = 7.5w^2 - 20.5w + 14.25. $$

This is a quadratic in $w$ with a positive leading coefficient, so its graph is a parabola opening upward, and completing the square locates its lowest point: $L(w, 0) = 7.5\,(w - \tfrac{41}{30})^2 + \tfrac{29}{120}$. With the intercept held at zero, the best slope is $41/30 \approx 1.37$, where the loss is $29/120 \approx 0.242$.

Pause and try

Two lines now score about the same: $w = 1, b = 1$ scores $0.25$ and $w \approx 1.37, b = 0$ scores $0.242$. Try the line halfway between in both dials, roughly $w = 1.2$, $b = 0.5$. Does it beat both?

Show answerHide answer

Its predictions are $1.7, 2.9, 4.1, 5.3$, with errors $0.2, -0.6, 0.6, -0.2$ and a loss of $(0.04 + 0.36 + 0.36 + 0.04)/4 = 0.2$. It beats both. One line got close by lifting, the other by tilting, and doing some of each is better than either.

That exercise shows what is wrong with turning one dial. Repeat the calculation with the intercept held at $0.5$ instead of $0$ and you get a different parabola, $L(w, 0.5) = 7.5\,(w - 1.2)^2 + 0.2$, whose lowest point is at a different slope.

The loss as a function of the slope alone, for two fixed intercepts. With b = 0 the best slope is about 1.37, and with b = 0.5 it is 1.2. The best setting of one dial depends on where the other is.
The loss as a function of the slope alone, for two fixed intercepts. With $b = 0$ the best slope is about 1.37, and with $b = 0.5$ it is 1.2. The best setting of one dial depends on where the other is.
Monocurl source
let fq = Frame(0.8, 1.9, 0, 2.6, [0.1, 0.15, 0], 6.2, 3.0)
let TW = [0.8, 1, 1.2, 1.4, 1.6, 1.8]
let TL = [0.5, 1, 1.5, 2, 2.5]
let Slice = |b, col| Path(filter(map(sample(0.8, 1.9, 160), |w| at(fq, w, loss(w, b))), |p| p[1] < fy(fq, 2.6)), col, 4.5)
mesh frame = [
    GridLines(fq, TW, TL),
    Seg(at(fq, 0.8, 0), at(fq, 1.9, 0), SLATE, 2.5),
    Seg(at(fq, 0.8, 0), at(fq, 0.8, 2.6), SLATE, 2.5),
    map(TW, |v| center{[fx(fq, v), fy(fq, 0) - 0.3, 0]} Math(to_string(v, 1), 0.72, SLATE)),
    map(TL, |v| center{[fx(fq, 0.8) - 0.36, fy(fq, v), 0]} Math(to_string(v, 1), 0.72, SLATE)),
    center{[fx(fq, 1.35), fy(fq, 0) - 0.66, 0]} Note("slope w", 0.74),
    center{[fx(fq, 0.8) + 1.1, fy(fq, 2.6) + 0.28, 0]} Note("mean squared error", 0.74)
]
mesh curves = [Slice(0, AZURE), Slice(0.5, EMBER)]
mesh marks = [
    stroke{AZURE, 2.5} dashed{[0.1, 0.07]} Line(at(fq, 41 / 30, 0), at(fq, 41 / 30, loss(41 / 30, 0))),
    stroke{EMBER, 2.5} dashed{[0.1, 0.07]} Line(at(fq, 1.2, 0), at(fq, 1.2, loss(1.2, 0.5))),
    Pt(at(fq, 41 / 30, loss(41 / 30, 0)), AZURE, 0.11),
    Pt(at(fq, 1.2, loss(1.2, 0.5)), EMBER, 0.11),
    Pt(at(fq, 1, loss(1, 0)), INK, 0.1),
    center{at(fq, 1.13, 1.42)} Cap("rule of thumb", 0.72, INK),
    center{at(fq, 1.7, 0.42)} Math("b = 0", 0.9, AZURE),
    center{at(fq, 1.03, 2.2)} Math("b = 0.5", 0.9, EMBER)
]

Uses the shared definitions for this lesson's figures.

So the loss cannot be understood one dial at a time. It is a function of two variables, and to see it whole we need a picture with both dials in it.

The loss surface

Expand the mean squared error without fixing either dial. Each squared error $(wx_i + b - y_i)^2$ multiplies out to $x_i^2w^2 + 2x_iwb + b^2 - 2x_iy_iw - 2y_ib + y_i^2$, and averaging over the four houses replaces each coefficient by an average over the data: the mean of $x_i^2$ is $7.5$, of $x_i$ is $2.5$, of $x_iy_i$ is $10.25$, of $y_i$ is $3.5$ and of $y_i^2$ is $14.25$. Therefore

$$ L(w, b) = 7.5w^2 + 5wb + b^2 - 20.5w - 7b + 14.25. $$

Check it against what we know: $L(1, 0) = 7.5 - 20.5 + 14.25 = 1.25$, and $L(1, 1) = 7.5 + 5 + 1 - 20.5 - 7 + 14.25 = 0.25$.

A function of two variables can be drawn as a map. Put $w$ on the horizontal axis and $b$ on the vertical one, so that each point of the plane is one setting of the dials, which is to say one line. Shade each point by the loss there, and draw curves through points of equal loss, the way a hiking map draws curves through points of equal height.

The mean squared error of every line at once. Each point of this plane is one line \hat y = wx + b, shaded darker where the loss is larger, with curves of equal loss at 0.5, 1, 2, 4 and 8. The rule of thumb is point A, the raised line is B, and the lowest point is the green dot at (1.2, 0.5). Points C and D are two very different lines that both have loss 1.
The mean squared error of every line at once. Each point of this plane is one line $\hat y = wx + b$, shaded darker where the loss is larger, with curves of equal loss at 0.5, 1, 2, 4 and 8. The rule of thumb is point A, the raised line is B, and the lowest point is the green dot at $(1.2, 0.5)$. Points C and D are two very different lines that both have loss 1.
Monocurl source
let fm = MapFrame([-1.05, 0.22, 0], 4.3, 3.1)
let PTS = [[1, 0, EMBER, "A"], [1, 1, PLUM, "B"], [2, -1.5, INK, "C"], [0.4, 2.5, INK, "D"]]
mesh map_ = LossMap(fm, 1)
mesh tags = LevelTags(fm)
mesh pts = [
    map(PTS, |p| Pt(at(fm, p[0], p[1]), p[2], 0.09)),
    Pt(at(fm, WB, BB), LEAF, 0.11),
    center{at(fm, 0.86, -0.25)} Cap("A", 0.8, EMBER),
    center{at(fm, 0.86, 1.2)} Cap("B", 0.8, PLUM),
    center{at(fm, 2.14, -1.3)} Cap("C", 0.8, INK),
    center{at(fm, 0.27, 2.75)} Cap("D", 0.8, INK)
]
mesh legend = [
    center{[2.55, 1.5, 0]} Note("point (w, b): loss", 0.72),
    map(range(0, 4), |i| center{[2.55, 1.0 - 0.5 * i, 0]} Math(str_join(["\text{", PTS[i][3], " }(", to_string(PTS[i][0], 1), ",\ ", to_string(PTS[i][1], 1), "):\ ", to_string(loss(PTS[i][0], PTS[i][1]), 2)]), 0.78, PTS[i][2])),
    center{[2.55, -1.0, 0]} Math(str_join(["(1.2,\ 0.5):\ ", to_string(LMIN, 2)]), 0.78, LEAF)
]

Uses the shared definitions for this lesson's figures.

The curves of equal loss are ellipses, one inside the next, all centered on the same point. They are long and thin, and they lie along a diagonal. That shape says something concrete about the data. Moving along the diagonal means raising the slope while lowering the intercept, which pivots the line about the middle of the data and changes its predictions there very little. Moving across the diagonal lifts the whole line off the data. The loss is forgiving in the first direction and strict in the second.

Pause and try

The figure claims that points C and D have the same loss. Check it. C is the line $\hat y = 2x - 1.5$ and D is the line $\hat y = 0.4x + 2.5$.

Show answerHide answer

For C the predictions are $0.5, 2.5, 4.5, 6.5$ and the errors are $-1, -1, 1, 1$, so the loss is $1$. For D the predictions are $2.9, 3.3, 3.7, 4.1$ and the errors are $1.4, -0.2, 0.2, -1.4$, so the loss is $(1.96 + 0.04 + 0.04 + 1.96)/4 = 1$. One line is five times as steep as the other, and its intercept compensates.

In the next figure a point wanders over the map on the right while the line it stands for moves on the left.

Left: the line with its squared errors. Right: the same line as a point on the loss map. Sliding the point along the valley pivots the line and the loss changes slowly; stepping across the valley lifts the line off the data and the loss climbs quickly.
Monocurl source
let fd = DataFrame([-2.0, 0.1, 0], 2.7, 2.8)
let fm = MapFrame([2.0, 0.1, 0], 2.7, 2.8)
let Both = |w, b| [
    Squares(fd, w, b),
    LineOn(fd, w, b),
    Houses(fd),
    Ring(at(fm, w, b), EMBER, 0.11, 4),
    center{[-0.75, 1.9, 0]} scale{1.25} color{ROSE} Number(loss(w, b), 2)
]
mesh plots = [DataPlot(fd), LossMap(fm), Pt(at(fm, WB, BB), LEAF, 0.08)]
mesh name = center{[-1.6, 1.9, 0]} Cap("loss", 0.8, ROSE)
mesh both = Both(w: 1, b: 0)

slide "wander"
    play Wait(0.7)
    both.w = 0.5
    both.b = 2.25
    play Lerp(2, [&both])
    both.w = 1.9
    both.b = -1.25
    play Lerp(2.6, [&both])
    both.w = 1.2
    both.b = 0.5
    play Lerp(1.6, [&both])
    play Wait(0.5)
    both.b = 2.3
    play Lerp(1.3, [&both])
    both.b = 0.5
    play Lerp(1.3, [&both])
    play Wait(1.2)

Uses the shared definitions for this lesson's figures.

Take the loss as height. Closed curves nested around a single low point are then the contour lines of a basin. The loss surface of the four houses is a long bowl, and the green dot is its bottom.

Training now has a geometric meaning: find the bottom of the bowl. But a picture is not a proof. Is there always exactly one lowest point, or could some data set produce two pits, or a pit hidden outside the window we plotted? And where is the bottom?

Why the surface is a bowl

For our four houses, completing the square answers both questions at once. Group the terms of $L$ that contain $b$: $b^2 + 5wb - 7b = b^2 + 2b\,(2.5w - 3.5)$, which is $(b + 2.5w - 3.5)^2$ minus $(2.5w - 3.5)^2$. Subtracting $(2.5w - 3.5)^2 = 6.25w^2 - 17.5w + 12.25$ from the remaining terms $7.5w^2 - 20.5w + 14.25$ leaves $1.25w^2 - 3w + 2 = 1.25\,(w - 1.2)^2 + 0.2$. So

$$ L(w, b) = (b + 2.5w - 3.5)^2 + 1.25\,(w - 1.2)^2 + 0.2. $$

This is two squares plus a constant, and neither square can be negative, so $L \ge 0.2$ everywhere on the plane, with no window to worry about. Both squares vanish only when $w = 1.2$ and $b = 3.5 - 2.5 \times 1.2 = 0.5$. That is the single lowest point, and no other setting of the dials ties it.

The same computation works for any data. For data $(x_i, y_i)$ write $\bar x$ and $\bar y$ for the means, and

$$ S_{xx} = \sum (x_i - \bar x)^2, \qquad S_{xy} = \sum (x_i - \bar x)(y_i - \bar y), \qquad S_{yy} = \sum (y_i - \bar y)^2. $$

We also need the precise word for “bowl-shaped”.

The point $(1 - s)p + sq$ runs along the segment from $p$ to $q$ as $s$ goes from 0 to 1, and the right side is the height of the straight chord joining the surface above $p$ to the surface above $q$. So convexity says that between any two points the surface sags below the chord. A surface with two pits fails this, because the chord from one pit to the other passes under the ridge between them.

Part 3 has a visual meaning: cut the bowl along any straight line whatever, and the cross-section is a parabola that opens upward. The number $A$ is how sharply it curves.

Slicing the loss surface along straight lines through point A. Left: the slicing line on the map. Right: the loss along that line, plotted against the distance $t$ along it. Whatever the direction, the slice is an upward parabola; its curvature $A$ changes with direction but never reaches zero.
Monocurl source
let fm = MapFrame([-1.95, 0.1, 0], 2.7, 2.8)
let fs = Frame(-2, 2, 0, 6, [2.05, 0.1, 0], 2.7, 2.8)
let dir = |th| [0.5 * cos(th), 1.25 * sin(th)]
let along = |th, t| [1 + t * dir(th)[0], t * dir(th)[1]]
let curv = |th| mean(map(HX, |x| (dir(th)[0] * x + dir(th)[1]) ^ 2))
let Cut = |th| [
    Clipped(fm, map(sample(-2.6, 2.6, 80), |t| along(th, t)), EMBER, 4),
    Pt(at(fm, 1, 0), EMBER, 0.09),
    Path(filter(map(sample(-2, 2, 100), |t| at(fs, t, loss(along(th, t)[0], along(th, t)[1]))), |p| p[1] < fy(fs, 6)), EMBER, 4.5),
    Pt(at(fs, 0, loss(1, 0)), EMBER, 0.09),
    center{[2.75, 1.93, 0]} scale{1.1} color{EMBER} Number(curv(th), 2)
]
mesh plots = [
    LossMap(fm),
    GridLines(fs, [-2, -1, 0, 1, 2], [1, 2, 3, 4, 5, 6]),
    Seg(at(fs, -2, 0), at(fs, 2, 0), SLATE, 2.5),
    Seg(at(fs, -2, 0), at(fs, -2, 6), SLATE, 2.5),
    map([-2, -1, 0, 1, 2], |v| center{[fx(fs, v), fy(fs, 0) - 0.3, 0]} Math(to_string(v), 0.72, SLATE)),
    map([2, 4, 6], |v| center{[fx(fs, -2) - 0.28, fy(fs, v), 0]} Math(to_string(v), 0.72, SLATE)),
    center{[fx(fs, 0), fy(fs, 0) - 0.64, 0]} Note("distance t along the line", 0.72),
    center{[1.6, 1.93, 0]} Cap("curvature A =", 0.76, EMBER)
]
mesh cut = Cut(th: 0.15)

slide "turn"
    play Wait(0.6)
    cut.th = 0.15 + PI
    play Lerp(8, [&cut], linear)
    play Wait(0.8)

Uses the shared definitions for this lesson's figures.

Where the bottom is

Part 2 of the theorem is a formula for the trained model. For the houses, $\bar x = 2.5$ and $\bar y = 3.5$; the deviations are $u = (-1.5, -0.5, 0.5, 1.5)$ and $v = (-2, 0, 0, 2)$; so $S_{xx} = 5$ and $S_{xy} = 3 + 0 + 0 + 3 = 6$. Hence

$$ w^* = \frac{6}{5} = 1.2, \qquad b^* = 3.5 - 1.2 \times 2.5 = 0.5, $$

and with $S_{yy} = 8$ the minimum loss is $\frac14(8 - 36/5) = 0.2$, agreeing with the direct calculation. This is the least-squares line of statistics: the lesson on correlation and regression derives the same slope $S_{xy}/S_{xx}$ and intercept $\bar y - w^*\bar x$ by minimizing the sum of squared errors, which is $n$ times our mean and so has its minimum at the same place.

Pause and try

Use the trained model to answer the opening question: what does it predict for the house of 2500 square feet? And what price does it predict for 5000 square feet?

Show answerHide answer

In our units the size is $x = 2.5$, so $\hat y = 1.2 \times 2.5 + 0.5 = 3.5$, or $350,000. This is the mean price because $2.5$ is the mean size, and the best line always passes through $(\bar x, \bar y)$, since $b^* = \bar y - w^*\bar x$. For $x = 5$ the model says $6.5$, or $650,000. That prediction is an extrapolation beyond every house in the table, and nothing in the training guarantees that the straight-line pattern continues out there.

You might object that if a formula gives the answer, there is nothing left to train. But the formula belongs to this model and this loss. It exists because $L$ is quadratic in the parameters, and $L$ is quadratic because the model is linear in them and the loss squares the errors. Change either one, as we will when the model must output a probability or contains several layers, and no formula for the bottom is available. The picture survives: there is still a loss surface over the parameters, and training still means going downhill on it. Gradient descent, the subject of the next lesson, finds the bottom using only the local slope, and it will be tested first on this bowl, where we know the right answer.

Worked examples

When the bowl has no single bottom

The theorem assumed that the inputs are not all equal. Here is what happens without that. Suppose three houses of the same size, 2000 square feet, sold for $3$, $4$ and $5$. Every error is $2w + b - y_i$, which depends on the dials only through the combination $2w + b$, and

$$ L(w, b) = (2w + b - 4)^2 + \tfrac23. $$

The loss is as small as possible, $2/3$, at every point of the line $2w + b = 4$. The surface is a trough with a flat floor, and infinitely many lines tie for best: every line through the point $(2, 4)$. The data say what a 2000 square foot house costs on average and nothing about how price changes with size. The surface is still convex, but not strictly: along the floor of the trough, the chord lies in the surface.

Three houses of the same size. Left: three different lines through the mean price at that size, all with the same loss, 0.67. Right: the loss map is a trough with straight, parallel level curves at losses 2, 4 and 8, and the whole green line 2w + b = 4 is its floor. The three dots on it are the three lines on the left.
Three houses of the same size. Left: three different lines through the mean price at that size, all with the same loss, 0.67. Right: the loss map is a trough with straight, parallel level curves at losses 2, 4 and 8, and the whole green line $2w + b = 4$ is its floor. The three dots on it are the three lines on the left.
Monocurl source
let TX = [2, 2, 2]
let TY = [3, 4, 5]
let tl = |w, b| loss_on(TX, TY, w, b)
let TMIN = tl(1, 2)
let fd = DataFrame([-2.3, 0.08, 0], 2.1, 2.5)
let fm = MapFrame([0.6, 0.08, 0], 2.1, 2.5)
let CH = [[0.5, AZURE], [1, EMBER], [1.5, PLUM]]
# level lines of the trough at these losses; each number sits in a gap on the lower branch
let TL = [[2, 0.7], [4, 1.3], [8, 0.7]]
let branch = |c, sign| map(sample(W0, W1, 60), |w| [w, 4 - 2 * w + sign * sqrt(c - TMIN)])
let tag_on = |g| [g[1], 4 - 2 * g[1] - sqrt(g[0] - TMIN)]
let LX = 2.78
mesh left = [
    DataPlot(fd),
    map(CH, |c| LineOn(fd, c[0], 4 - 2 * c[0], c[1], 3.5)),
    Houses(fd, TX, TY)
]
mesh right = [
    Shaded(fm, tl),
    map(TL, |g| [
        Clipped(fm, branch(g[0], 1), AZURE, 2),
        Clipped(fm, branch(g[0], -1), AZURE, 2, [tag_on(g)]),
        center{at(fm, tag_on(g)[0], tag_on(g)[1])} Math(to_string(g[0]), 0.72, AZURE)
    ]),
    Clipped(fm, map(sample(W0, W1, 60), |w| [w, 4 - 2 * w]), LEAF, 4.5),
    MapTicks(fm),
    map(CH, |c| Pt(at(fm, c[0], 4 - 2 * c[0]), c[1], 0.1))
]
mesh legend = [
    center{[LX, 1.45, 0]} Note("line (w, b): loss", 0.72),
    map(range(0, 3), |i| center{[LX, 1.0 - 0.42 * i, 0]} Math(str_join(["(", to_string(CH[i][0], 1), ",\ ", to_string(4 - 2 * CH[i][0], 1), "):\ ", to_string(tl(CH[i][0], 4 - 2 * CH[i][0]), 2)]), 0.74, CH[i][1])),
    Seg([LX - 0.75, -0.45, 0], [LX - 0.25, -0.45, 0], LEAF, 4.5),
    center{[LX + 0.3, -0.45, 0]} Cap("floor", 0.74, LEAF),
    center{[LX, -0.87, 0]} Math("2w + b = 4", 0.78, LEAF),
    center{[LX, -1.29, 0]} Math(str_join(["\text{loss } ", to_string(TMIN, 2)]), 0.78, LEAF)
]

Uses the shared definitions for this lesson's figures.

The long thin valley on the map of the four real houses is a mild form of the same thing. Their sizes, 1 to 4, are all on one side of zero, so raising the intercept and lowering the slope nearly compensate. The valley will matter in the next lesson, because a method that feels its way downhill moves slowly along a nearly flat floor.

Stepping back

The hard step was a change in what we looked at. We started in the data plane, where the houses are points and the model is a line. The work happened in the plane of the parameters, where each line is a point and the data have been absorbed into a single function $L$. There, “which line is best?” becomes “where is this surface lowest?”, and for the linear model with squared error the surface is a convex bowl whose bottom has a formula.

Later lessons keep that structure and change its parts: a different model, a different loss, more parameters. The surface stops being a bowl and the formula disappears, but model, loss and training keep their meanings.

Check yourself

For the four houses, $L(1, 1) = 0.25$ and the minimum of the loss is $L(1.2, 0.5) = 0.2$. Which statement is correct?

Problems

The first three repeat the lesson’s computations. The next two ask you to explain or combine. The last one extends the lesson.

Problem 1

For the four houses, compute the mean squared error of the line $\hat y = 1.5x$ directly from the errors, and then again from the formula $L(w, b) = 7.5w^2 + 5wb + b^2 - 20.5w - 7b + 14.25$.

Show solutionHide solution

The predictions are $1.5, 3, 4.5, 6$ and the errors are $0, -0.5, 1, 0.5$. The squares are $0, 0.25, 1, 0.25$ with mean $1.5/4 = 0.375$. From the formula with $w = 1.5$, $b = 0$: $7.5 \times 2.25 - 20.5 \times 1.5 + 14.25 = 16.875 - 30.75 + 14.25 = 0.375$.

Problem 2

For the data $(1, 1), (2, 3), (3, 2)$, write $L(w, b)$ in expanded form, find the best line, and find the minimum loss.

Show solutionHide solution

The averages are: $x^2$: $14/3$; $x$: $2$; $xy$: $(1 + 6 + 6)/3 = 13/3$; $y$: $2$; $y^2$: $14/3$. So $L = \frac{14}{3}w^2 + 4wb + b^2 - \frac{26}{3}w - 4b + \frac{14}{3}$. For the minimum, $u = (-1, 0, 1)$ and $v = (-1, 1, 0)$, so $S_{xx} = 2$, $S_{xy} = 1$, $S_{yy} = 2$. Then $w^* = 0.5$, $b^* = 2 - 0.5 \times 2 = 1$, and the minimum loss is $\frac13(2 - \frac12) = 0.5$. Check: $\hat y = 0.5x + 1$ predicts $1.5, 2, 2.5$, with errors $0.5, -1, 0.5$ and mean square $(0.25 + 1 + 0.25)/3 = 0.5$.

Problem 3

Using $L(w, b) = (b + 2.5w - 3.5)^2 + 1.25(w - 1.2)^2 + 0.2$ for the houses: (a) recompute $L(1, 1)$. (b) Find every point on the valley floor $b = 3.5 - 2.5w$ whose loss is at most $0.25$. (c) What do those lines have in common on the data plot?

Show solutionHide solution

(a) $(1 + 2.5 - 3.5)^2 + 1.25 \times 0.04 + 0.2 = 0 + 0.05 + 0.2 = 0.25$. (b) On the valley floor the first square is zero, so the loss is $1.25(w - 1.2)^2 + 0.2 \le 0.25$, which gives $(w - 1.2)^2 \le 0.04$, that is $1 \le w \le 1.4$, with $b = 3.5 - 2.5w$ running from $1$ down to $0$. (c) The condition $b = 3.5 - 2.5w$ says $w \times 2.5 + b = 3.5$: each of these lines passes through the point of means $(2.5, 3.5)$. They are the lines through that point with slopes between 1 and 1.4. Point B of the lesson, $(1, 1)$, is one end of this range.

Problem 4

Let $e_i = w^*x_i + b^* - y_i$ be the errors of the best line. Show that $\sum e_i = 0$ and $\sum e_ix_i = 0$ for any data with the $x_i$ not all equal, and verify both for the houses.

Show solutionHide solution

In the notation of the proof, $e_i = (w^*u_i - v_i) + c$ with $c = b^* + w^*\bar x - \bar y = 0$. So $\sum e_i = w^*\sum u_i - \sum v_i = 0$. Next, $\sum e_ix_i = \sum e_i(u_i + \bar x) = \sum e_iu_i + \bar x\sum e_i = \sum (w^*u_i - v_i)u_i + 0 = w^*S_{xx} - S_{xy} = 0$, because $w^* = S_{xy}/S_{xx}$. For the houses the errors are $0.2, -0.6, 0.6, -0.2$: their sum is $0$, and $\sum e_ix_i = 0.2 - 1.2 + 1.8 - 0.8 = 0$. (With calculus: these two sums are, up to the factor $2/n$, the partial derivatives of $L$ with respect to $b$ and $w$, which vanish at the bottom of the bowl.)

Problem 5

A typing error records the price of house 4 as $9.5$ instead of $5.5$. Find the best line for the corrupted data and its prediction for the 2500 square foot house. Why does one bad entry move the line so much?

Show solutionHide solution

Now $\bar y = (1.5 + 3.5 + 3.5 + 9.5)/4 = 4.5$ and $v = (-3, -1, -1, 5)$, with $u = (-1.5, -0.5, 0.5, 1.5)$ as before. $S_{xy} = 4.5 + 0.5 - 0.5 + 7.5 = 12$ and $S_{xx} = 5$, so $w^* = 2.4$ and $b^* = 4.5 - 2.4 \times 2.5 = -1.5$. The slope has doubled. The prediction at $x = 2.5$ is $2.4 \times 2.5 - 1.5 = 4.5$, a full $100,000 above the earlier $350,000. Two things make the entry so influential. The loss squares errors, so the old line’s error of $-4.2$ on the bad point would contribute $17.64$ to the sum, far more than all other errors together, and the minimizer gives up accuracy on three houses to shrink that one term. And house 4 is at the end of the range of sizes, where pulling on the line tilts it most.

Problem 6

(Turning one dial at a time.) Start at the rule of thumb $(w, b) = (1, 0)$ and alternate two moves: (i) with $b$ fixed, set $w$ to the value that minimizes $L$; (ii) with $w$ fixed, set $b$ to the value that minimizes $L$. For the houses, find formulas for both moves, carry out two rounds, and prove that the slope converges to $1.2$.

Show solutionHide solution

Collect $L$ as a quadratic in $w$: $7.5w^2 + (5b - 20.5)w + \dots$, minimized at $w = (20.5 - 5b)/15$. As a quadratic in $b$: $b^2 + (5w - 7)b + \dots$, minimized at $b = (7 - 5w)/2 = 3.5 - 2.5w$. Round 1: $w = 20.5/15 \approx 1.3667$, then $b = 3.5 - 2.5 \times 1.3667 \approx 0.0833$; the loss is now about $0.2347$. Round 2: $w = (20.5 - 0.4167)/15 \approx 1.3389$, then $b \approx 0.1528$; the loss is about $0.2241$. For the proof, note that after move (ii) the point satisfies $b = 3.5 - 2.5w$. Substituting this into move (i) shows that each later round sends the slope $w$ to

$$ w' = \frac{20.5 - 5\,(3.5 - 2.5w)}{15} = \frac{3 + 12.5w}{15} = 0.2 + \tfrac56w. $$

So $w' - 1.2 = \frac56(w - 1.2)$: each round after the first multiplies the distance of the slope from $1.2$ by $5/6$. After round 1 that distance is $41/30 - 1.2 = \frac16$, so after $k$ rounds it is $\frac16(5/6)^{k-1}$, which tends to 0, and then $b = 3.5 - 2.5w$ tends to $0.5$. The method works, but slowly: it needs about 13 rounds to cut the distance by a factor of ten, since $(5/6)^{13} \approx 0.093$. Solving exactly for one block of unknowns at a time is also how k-means finds clusters.

Shared definitions used by this lesson's figures
# house kit: prepended to every essay figure after the std imports

# palette. AZURE and EMBER are the two actors, LEAF marks an answer, ROSE a problem
let PAPER = [1, 1, 1, 1]
let INK = [0.08, 0.09, 0.11, 1]
let SLATE = [0.40, 0.44, 0.50, 1]
let MIST = [0.85, 0.86, 0.88, 1]
let AZURE = [0.13, 0.42, 0.68, 1]
let EMBER = [0.89, 0.42, 0.12, 1]
let LEAF = [0.13, 0.55, 0.36, 1]
let ROSE = [0.78, 0.22, 0.24, 1]
let PLUM = [0.47, 0.30, 0.66, 1]
let GOLD = [0.85, 0.62, 0.08, 1]

background = PAPER

# an opaque pastel of col; use for fills so overlaps do not darken
let tint = |col, amount = 0.18| [
    1 - (1 - col[0]) * amount,
    1 - (1 - col[1]) * amount,
    1 - (1 - col[2]) * amount,
    1
]

# text: Cap for labels, Note for quiet labels, Math for typeset maths
let Cap = |txt, size = 0.75, col = INK| color{col} Text(txt, size, "Charter")
let Note = |txt, size = 0.65| color{SLATE} Text(txt, size, "Charter")
let Math = |tex, size = 0.9, col = INK| color{col} Tex(tex, size)

# a paper rectangle to sit behind text that crosses lines
let Plate = |pos, size| center{pos} fill{PAPER} stroke{alpha{0} PAPER, 0} Rect(size)

# shapes with fill and stroke both set, so no stray black appears
let Pt = |pos, col = INK, r = 0.075| center{pos} fill{col} stroke{col, 1} Circle(r, 48)
let Ring = |pos, col = INK, r = 0.1, w = 3| center{pos} fill{PAPER} stroke{col, w} Circle(r, 48)
let Seg = |a, b, col = INK, w = 3| stroke{col, w} Line(a, b)
let Path = |pts, col = INK, w = 3| stroke{col, w} Polyline(pts)
let Region = |pts, col| fill{col} stroke{alpha{0} col, 0} Polygon(pts)
let Outline = |pts, col = INK, w = 3| fill{alpha{0} PAPER} stroke{col, w} Polygon(pts)
let Shape = |pts, col, amount = 0.18, w = 3| fill{tint(col, amount)} stroke{col, w} Polygon(pts)
let Disc = |pos, r, col, amount = 0.18, w = 3| center{pos} fill{tint(col, amount)} stroke{col, w} Circle(r, 96)
let Box = |pos, size, col, amount = 0.18, w = 3| center{pos} fill{tint(col, amount)} stroke{col, w} Rect(size)
# a line from a to b ending in a small solid arrowhead
let Tip = |a, b, col = INK, w = 3, head = 0.17| {
    let d = normalize([b[0] - a[0], b[1] - a[1], 0])
    let n = [-d[1], d[0], 0]
    let base = [b[0] - d[0] * head, b[1] - d[1] * head, 0]
    return [
        stroke{col, w} Line(a, base),
        fill{col} stroke{col, 1} Polygon([
            b,
            [base[0] + n[0] * head * 0.42, base[1] + n[1] * head * 0.42, 0],
            [base[0] - n[0] * head * 0.42, base[1] - n[1] * head * 0.42, 0]
        ])
    ]
}

# plots. a frame is [x0, x1, y0, y1, centre, width, height]: a data window and the scene box it fills
let Frame = |x0, x1, y0, y1, c = [0, 0, 0], w = 6.4, h = 3.6| [x0, x1, y0, y1, c, w, h]
let fx = |fr, x| fr[4][0] + (x - (fr[0] + fr[1]) / 2) * fr[5] / (fr[1] - fr[0])
let fy = |fr, y| fr[4][1] + (y - (fr[2] + fr[3]) / 2) * fr[6] / (fr[3] - fr[2])
let at = |fr, x, y| [fx(fr, x), fy(fr, y), 0]

# axes through the data origin (or the nearest edge of the window), with optional names
let Axes = |fr, x_name = "", y_name = "", col = SLATE| {
    let ox = clamp(fr[0], 0, fr[1])
    let oy = clamp(fr[2], 0, fr[3])
    var parts = [
        Tip(at(fr, fr[0], oy), [fx(fr, fr[1]) + 0.25, fy(fr, oy), 0], col, 2.5, 0.15),
        Tip(at(fr, ox, fr[2]), [fx(fr, ox), fy(fr, fr[3]) + 0.25, 0], col, 2.5, 0.15)
    ]
    if (x_name != "") {
        parts .= center{[fx(fr, fr[1]) + 0.5, fy(fr, oy), 0]} Math(x_name, 0.8, col)
    }
    if (y_name != "") {
        parts .= center{[fx(fr, ox), fy(fr, fr[3]) + 0.5, 0]} Math(y_name, 0.8, col)
    }
    return parts
}

# tick marks with numbers at the listed data values
let XTicks = |fr, xs, col = SLATE, places = 0| block {
    let oy = fy(fr, clamp(fr[2], 0, fr[3]))
    for (x in xs) {
        . Seg([fx(fr, x), oy - 0.07, 0], [fx(fr, x), oy + 0.07, 0], col, 2)
        . center{[fx(fr, x), oy - 0.33, 0]} Math(to_string(x, places), 0.72, col)
    }
}
let YTicks = |fr, ys, col = SLATE, places = 0| block {
    let ox = fx(fr, clamp(fr[0], 0, fr[1]))
    for (y in ys) {
        . Seg([ox - 0.07, fy(fr, y), 0], [ox + 0.07, fy(fr, y), 0], col, 2)
        . center{[ox - 0.36, fy(fr, y), 0]} Math(to_string(y, places), 0.72, col)
    }
}

# faint grid lines at the listed data values
let GridLines = |fr, xs, ys, col = MIST| block {
    for (x in xs) {
        . Seg(at(fr, x, fr[2]), at(fr, x, fr[3]), col, 1.5)
    }
    for (y in ys) {
        . Seg(at(fr, fr[0], y), at(fr, fr[1], y), col, 1.5)
    }
}

# the graph of f over [a, b], drawn in the frame. values are clamped to the window
let Graph = |fr, f, a, b, col = AZURE, w = 4, n = 160|
    stroke{col, w} Polyline(map(sample(a, b, n), |x| at(fr, x, clamp(fr[2], f(x), fr[3]))))

# the region between the graph of f and the x axis over [a, b]
let AreaUnder = |fr, f, a, b, col = AZURE, amount = 0.2, n = 80| {
    var pts = [at(fr, a, 0)]
    for (x in sample(a, b, n)) {
        pts .= at(fr, x, clamp(fr[2], f(x), fr[3]))
    }
    pts .= at(fr, b, 0)
    return fill{tint(col, amount)} stroke{alpha{0} col, 0} Polygon(pts)
}

# four houses: size in thousands of square feet, sale price in hundreds of thousands of dollars
let HX = [1, 2, 3, 4]
let HY = [1.5, 3.5, 3.5, 5.5]
let loss_on = |xs, ys, w, b| mean(map(range(0, len(xs)), |i| (w * xs[i] + b - ys[i]) ^ 2))
let loss = |w, b| loss_on(HX, HY, w, b)
let WB = 1.2
let BB = 0.5
let LMIN = loss(WB, BB)
let DataFrame = |c, w, h| Frame(0, 5, 0, 7, c, w, h)
let DataPlot = |fr| [
    GridLines(fr, [1, 2, 3, 4, 5], [1, 2, 3, 4, 5, 6, 7]),
    Seg(at(fr, 0, 0), at(fr, 5, 0), SLATE, 2.5),
    Seg(at(fr, 0, 0), at(fr, 0, 7), SLATE, 2.5),
    map([1, 2, 3, 4], |v| center{[fx(fr, v), fy(fr, 0) - 0.3, 0]} Math(to_string(v), 0.72, SLATE)),
    map([2, 4, 6], |v| center{[fx(fr, 0) - 0.28, fy(fr, v), 0]} Math(to_string(v), 0.72, SLATE)),
    center{[fx(fr, 2.5), fy(fr, 0) - 0.66, 0]} Note("size (1000 sq ft)", 0.72),
    center{[fx(fr, 0) + 0.6, fy(fr, 7) + 0.28, 0]} Note("price ($100k)", 0.72)
]
let Houses = |fr, xs = HX, ys = HY| map(range(0, len(xs)), |i| Pt(at(fr, xs[i], ys[i]), INK, 0.09))
# the line y = w x + b, kept inside the data window (w >= 0)
let LineOn = |fr, w, b, col = EMBER, th = 4| {
    var lo = 0.05
    var hi = 4.95
    if (w > 0.01) {
        lo = max(lo, (0.05 - b) / w)
        hi = min(hi, (6.95 - b) / w)
    }
    return Seg(at(fr, lo, w * lo + b), at(fr, hi, w * hi + b), col, th)
}
# a square on each vertical miss, drawn square on screen
let Squares = |fr, w, b, xs = HX, ys = HY| block {
    for (i in range(0, len(xs))) {
        let p = at(fr, xs[i], w * xs[i] + b)
        let q = at(fr, xs[i], ys[i])
        let s = abs(p[1] - q[1]) * [1, -1][xs[i] > 3.5]
        if (abs(s) > 0.02) {
            . fill{alpha{0.3} ROSE} stroke{ROSE, 2.5} Polygon([p, [p[0] + s, p[1], 0], [q[0] + s, q[1], 0], q])
        }
    }
}
# the loss over the plane of the two dials: slope w across, intercept b up
let W0 = 0
let W1 = 2.4
let B0 = -2.5
let B1 = 3.5
let MapFrame = |c, w, h| Frame(W0, W1, B0, B1, c, w, h)
let shade = |v| {
    let t = clamp(ln(max(v, 0.2) / 0.2) / ln(80), 0, 1)
    return lerp(tint(AZURE, 0.04), tint(AZURE, 0.6), t)
}
let Shaded = |fr, f| stroke{alpha{0} PAPER, 0} point_map{|p| at(fr, p[0], p[1])}
    ColorGrid(|p, idx| shade(f(p[0], p[1])), [W0, W1, 61], [B0, B1, 61], 1)
# draw the parts of a list of [w, b] points that fall inside the map
let Clipped = |fr, pts, col, th, gaps = []| block {
    var cur = []
    for (p in pts) {
        let s = at(fr, p[0], p[1])
        let clear = len(filter(gaps, |g| abs(s[0] - fx(fr, g[0])) < 0.3 and abs(s[1] - fy(fr, g[1])) < 0.17)) == 0
        if (clear and p[0] >= W0 and p[0] <= W1 and p[1] >= B0 and p[1] <= B1) {
            cur .= at(fr, p[0], p[1])
        } else {
            if (len(cur) >= 2) { . Path(cur, col, th) }
            cur = []
        }
    }
    if (len(cur) >= 2) { . Path(cur, col, th) }
}
# the level curve loss = c of the house data: an ellipse around (1.2, 0.5)
let level = |c| map(sample(0, TAU, 240), |t| {
    let w = WB + sqrt((c - LMIN) / 1.25) * cos(t)
    return [w, 3.5 - 2.5 * w + sqrt(c - LMIN) * sin(t)]
})
let LEVELS = [0.5, 1, 2, 4, 8]
let MapTicks = |fr| [
    Outline([at(fr, W0, B0), at(fr, W1, B0), at(fr, W1, B1), at(fr, W0, B1)], SLATE, 2.5),
    map([0.5, 1, 1.5, 2], |v| [
        Seg(at(fr, v, B0), [fx(fr, v), fy(fr, B0) - 0.08, 0], SLATE, 2),
        center{[fx(fr, v), fy(fr, B0) - 0.3, 0]} Math(to_string(v, 1), 0.72, SLATE)
    ]),
    map([-2, -1, 0, 1, 2, 3], |v| [
        Seg(at(fr, W0, v), [fx(fr, W0) - 0.08, fy(fr, v), 0], SLATE, 2),
        center{[fx(fr, W0) - 0.36, fy(fr, v), 0]} Math(to_string(v), 0.72, SLATE)
    ]),
    center{[fx(fr, 1.2), fy(fr, B0) - 0.64, 0]} Note("slope w", 0.72),
    center{[fx(fr, W0) + 0.55, fy(fr, B1) + 0.28, 0]} Note("intercept b", 0.72)
]
# where the label of each level sits: above the minimum for some, below for others
let TAGS = [[0.5, 1, "0.5"], [1, -1, "1"], [2, 1, "2"], [4, -1, "4"], [8, 1, "8"]]
let tag_at = |g| [WB, BB + g[1] * sqrt(g[0] - LMIN)]
let LevelTags = |fr| map(TAGS, |g| center{at(fr, tag_at(g)[0], tag_at(g)[1])} Math(g[2], 0.72, AZURE))
let LossMap = |fr, tagged = 0| [
    Shaded(fr, loss),
    map(TAGS, |g| Clipped(fr, level(g[0]), AZURE, 2, [[], [tag_at(g)]][tagged])),
    MapTicks(fr)
]