Fitting a line and measuring error

How do you say, with one number, how wrong a model is?

Each dot is a student: how many hours they studied and their quiz score. Move the line with the two knobs and watch the loss, the single number that says how wrong the line is.

Hours studied vs quiz score

0 h2 h4 h6 h8 h10 h 020406080100
your linedashed: the best possible line misses (residuals), and their squares

The two knobs

1
60
score = 1.0 × hours + 60
146.9your MSE
30.9best possible MSE
12.1typical miss, in points

The exact best line is score = 5.22 × hours + 39.82. It still misses, because real scores are noisy.

Loss if you only turned one knob

106−51,52702,94954,370105,79115slope m (b kept at 60) 14301,106252,070503,033753,996100start b (m kept at 1.0)

Each curve is a valley, and the dashed line is where you are. Training means walking to the bottom of the valley. The next lesson does that automatically.

Why squared error?

Every miss counts as positive. Missing by +10 and −10 shouldn't cancel out to "perfect".

Big misses count much more. One miss of 20 costs 400, but four misses of 5 cost only 100. Turn on "Draw the squares" and the big red squares show you which students the line is failing hardest.

It's a smooth valley. Look at the MSE curve: a smooth bowl with one bottom. That smoothness is what lets gradient descent slide down it. MAE's valley has a sharp point at the bottom, which is harder to slide into.

LLMs use a different loss (cross-entropy, lesson 5 onward) because they predict a word, not a number. But the idea is identical: one number for "how wrong", and every knob gets turned to make it smaller.

Every residual, one by one

HoursReal scoreLine saysMissMiss²
1.045.161.0−15.9253
3.362.863.3−0.50
7.182.467.115.3234
3.254.163.2−9.183
5.075.965.010.9119
4.269.564.25.328
5.061.165.0−3.915
8.580.368.511.8139
3.965.863.91.94
6.667.966.61.32
8.985.468.916.5272
3.663.663.60.00
9.195.069.125.9671
7.476.367.48.979
9.286.369.217.1292
1.949.561.9−12.4154
8.885.268.816.4269
4.556.364.5−8.267
7.472.367.44.924
2.154.962.1−7.252
2.242.262.2−20.0400
6.177.866.111.7137
4.857.264.8−7.658
2.949.762.9−13.2174
Average of the last column = MSE146.9

The code that just ran

Everything on this page came from calling this code, which lives at the top of this same file.

Show or hide the source
final class Line
{
    public function __construct(public float $m, public float $b) {}

    // The model itself. That's it: one multiplication and one addition.
    public function predict(float $x): float
    {
        return $this->m * $x + $this->b;
    }

    // Residual = how far each real point is from the line (real − predicted).
    public function residuals(array $points): array
    {
        return array_map(fn($p) => $p['y'] - $this->predict($p['x']), $points);
    }

    // MEAN SQUARED ERROR: square every miss, then average.
    // Squaring makes every miss positive and punishes big misses much more.
    public function mse(array $points): float
    {
        $sum = 0.0;
        foreach ($this->residuals($points) as $r) $sum += $r * $r;
        return $sum / count($points);
    }

    // MEAN ABSOLUTE ERROR: the plain average size of a miss.
    public function mae(array $points): float
    {
        $sum = 0.0;
        foreach ($this->residuals($points) as $r) $sum += abs($r);
        return $sum / count($points);
    }

    // For a straight line there is an exact formula for the lowest-MSE line
    // ("least squares"). Most models have no such formula, which is why the
    // next lesson, gradient descent, exists.
    public static function bestFit(array $points): self
    {
        $n = count($points);
        $mx = array_sum(array_column($points, 'x')) / $n;
        $my = array_sum(array_column($points, 'y')) / $n;
        $num = 0.0; $den = 0.0;
        foreach ($points as $p) {
            $num += ($p['x'] - $mx) * ($p['y'] - $my);
            $den += ($p['x'] - $mx) ** 2;
        }
        $m = $den ? $num / $den : 0.0;
        return new self($m, $my - $m * $mx);
    }
}