Try first

You have gradient and you have the update rule. Write the training loop yourself before reading on. It should start from some (m, b), repeat the update, and return the result.

The whole algorithm

function train(data, lr = 0.01, steps = 5000) {
  let m = 0, b = 0;
  for (let i = 0; i < steps; i++) {
    const [dm, db] = gradient(m, b, data);
    m -= lr * dm;
    b -= lr * db;
  }
  return [m, b];
}

const [m, b] = train(study);
log("m", m.toFixed(3), " b", b.toFixed(3), " loss", loss(m, b, study).toFixed(3));

Six lines inside the function. That is the whole of it, and it is the thing that spent this entire section being built up to.

Start at zero. Ask which way is downhill. Take a step. Repeat. Everything else in machine learning is a better gradient, a better step, or a better model to apply it to.

Watch it work

function trainVerbose(data, lr = 0.01, steps = 5000, every = 500) {
  let m = 0, b = 0;
  for (let i = 0; i <= steps; i++) {
    if (i % every === 0) {
      log(String(i).padStart(5), " m", m.toFixed(3),
          " b", b.toFixed(3), " loss", loss(m, b, data).toFixed(4));
    }
    const [dm, db] = gradient(m, b, data);
    m -= lr * dm;
    b -= lr * db;
  }
  return [m, b];
}

trainVerbose(study);

Read the log carefully, because the shape of it is the point.

The loss collapses in the first few hundred steps, then improves slowly for thousands more. That curve, steep then flat, is what training looks like for nearly every model you will ever run. Most of the gain arrives early. Most of the time is spent on the last little bit.

Watch the two parameters separately. m settles quickly. b drifts upward for a very long time. That is the diagonal valley from lesson 3050 showing up in the numbers: once the walker reaches the valley floor, the floor itself slopes gently, and progress along it is slow.

0 2500 5000 10 100 1000 step loss each gridline is ten times the one below collapses then crawls 0 2500 5000 0 0.5 1 step fraction of final value m settles fast b drifts for thousands
A real 5000-step run on the study data at a learning rate of 0.01. Most of the gain arrives early. Most of the time goes on the last little bit, and it is b that spends it.

Compare against your hand tuning

const flat = study.reduce((s, r) => s + r.y, 0) / study.length;
log("always average", loss(0, flat, study).toFixed(3));
log("your best     ", loss(8, 24, study).toFixed(3));
log("trained       ", loss(m, b, study).toFixed(3));

The trained line should beat both. It took no judgment, no sliders, and about a hundredth of a second.

Knowing when to stop

Running a fixed 5000 steps is crude. It might be far too many or not enough. The usual fix is to stop when progress stalls.

let last = Infinity;
for (let i = 0; i < steps; i++) {
  const [dm, db] = gradient(m, b, data);
  m -= lr * dm;
  b -= lr * db;
  const now = loss(m, b, data);
  if (Math.abs(last - now) < 1e-9) { log("stopped at step", i); break; }
  last = now;
}

Stop when the loss stops moving. Be careful with the threshold. Too loose and you quit while still improving. Too tight and you never trigger it, because floating point noise keeps the difference above zero forever.

What to watch

Starting at m = 0, b = 0 is a choice, and here it does not matter, because the bowl has one bottom and every path leads to it. For a neural network the starting point changes the answer you get, and lesson 3100 is about picking it properly.

Exercises

  1. Run with 100 steps, then 1000, then 50000. Plot the final loss against the step count.
  2. Start from m = 30, b = -50. Does it still reach the same answer? How many more steps did it need?
  3. Add the stopping rule and find the smallest threshold that still triggers.
  4. Time 5000 steps with performance.now(). Then work out how long the grid search from lesson 3050 took for a worse answer.