LLM + RoboticsBuild a Robot Brain · Week 2 · Lesson 3

Learning from data: fit a line by shrinking the error

Instead of telling the robot a rule, give it measurements and let it work the rule out. You'll write a number that says "how wrong", then a loop that keeps nudging the rule to make that number smaller. That loop, gradient descent, is how every modern AI learns, ChatGPT included.

Time3–4 h
You'll needLesson 0 done, a calculator, a ball or toy car, some books, a tape measure
New wordsModel, parameter, mean squared error, gradient, learning rate
Project pieceLearns its own braking distance

Start here

Learning means: guess a rule, measure how wrong it is, nudge it to be a bit less wrong, and repeat.

The problem

Robo-Helper slides a bit after it brakes. The faster it goes, the further it slides. If it's driving towards a wall 1 metre away, how fast can it safely go? Nobody gave it a formula. It only has a list of 13 measurements: "at this speed, I slid this far".

The idea in one picture

Imagine you're on a hillside in thick fog, and you want to reach the bottom of the valley. You can't see it. But you can feel which way the ground slopes under your feet. So you take a small step downhill, feel again, take another step. Eventually you're at the bottom. In this lesson the "height" is how wrong the robot's rule is, and walking downhill makes the rule better.

Words to know

Model
A rule with adjustable numbers in it. Ours: distance = w × speed + b.
Parameter
One of the adjustable numbers. We have two: w (how steep) and b (where it starts).
Mean squared error (MSE)
One number that says how wrong the model is on the data. 0 means perfect.
Gradient
Which way, and how steeply, the error goes up when you change each parameter. You walk the other way.
Learning rate
How big a step you take each time. Too small is slow. Too big and you overshoot.

Going deeper: everything below is the full lesson. It has the most maths of the course so far, but nothing harder than multiplying and adding. Go slowly, with a calculator.

1. The problem

Here are the robot's real braking measurements, from the homework file. Speed is in metres per second, stopping distance in metres:

speed:     0.2   0.4   0.5   0.6   0.8   1.0   1.1   1.2   1.4   1.5   1.6   1.8   2.0
distance:  0.27  0.40  0.45  0.57  0.70  0.86  0.95  1.05  1.18  1.30  1.36  1.55  1.70

Obvious approach #1: a lookup table. At 1.0 m/s it stops in 0.86 m. Fine. But what about 0.7 m/s, or 0.95? They aren't in the table. And the measurements are a bit noisy (remember Lesson 2), so the table has little bumps that aren't real.

Obvious approach #2: guess a rule. The distance roughly grows with the speed, so maybe distance = speed? At 2.0 m/s that predicts 2.0, but the robot actually stopped in 1.70. Too pessimistic. We could keep guessing… but which guess is best, and how would we even know?

We need two things: a way to score a guess, and a way to improve it automatically.

2. First principles

Step 1: choose a model shape

If you plot the 13 points, they lie almost on a straight line. A straight line has the form:

distance = w × speed + b

w is the slope: how many extra metres of sliding for each extra 1 m/s of speed. b is the starting value: the distance at speed 0. These two numbers are the model's parameters. "Learning" will mean finding good values for them. The function predict(w, b, speed) just computes this line.

Step 2: score a guess with the mean squared error

Take a guess, w = 1, b = 0, and look at just two points:

speed 1.0: predict 1×1.0 + 0 = 1.00, real 0.86, miss = 1.00 - 0.86 = +0.14
speed 2.0: predict 1×2.0 + 0 = 2.00, real 1.70, miss = 2.00 - 1.70 = +0.30

To turn many misses into one score, we could just average them. But misses can be positive (too high) or negative (too low), and a +0.3 and a −0.3 would average to 0, a "perfect" score for a bad line. So we square each miss first. Squares are never negative, and squaring punishes big misses much more than small ones (0.3² = 0.09, but 0.1² = 0.01). Then we take the mean. That's the mean squared error:

MSE = average of (prediction - real)²

Let's do a full one by hand on a tiny data set, just two points: (1, 1) and (2, 3). Guess w = 0, b = 0, so every prediction is 0:

point (1, 1): predict 0, miss = 0 - 1 = -1, squared = 1
point (2, 3): predict 0, miss = 0 - 3 = -3, squared = 9
MSE = (1 + 9) / 2 = 5

On the robot's 13 real points, Python gives these scores:

w = 0,   b = 0     MSE = 1.0938     (terrible: predicts 0 for everything)
w = 1,   b = 0     MSE = 0.0293     (much better)
w = 0.8, b = 0.1   MSE = 0.00072    (very close)

Now we can compare guesses with one number. Lower is better.

Step 3: which way is downhill?

Picture every possible (w, b) as a spot on a landscape, with the MSE as the height. We want the lowest spot. We're in the fog: we can't see the whole landscape. But we can feel the slope where we stand.

Feeling the slope, the simple way: nudge w by a tiny amount and see how the error changes. On the robot data at (0, 0):

MSE at w = 0,    b = 0:  1.0938
MSE at w = 0.01, b = 0:  1.0686   (a bit lower)
change per unit of w = (1.0686 - 1.0938) / 0.01 ≈ -2.52

Negative means "increasing w makes the error go down". So increase w. That number is the gradient for w. Doing the same for b gives the gradient for b.

The shortcut: for the MSE there's an exact formula, and the homework file gives it to you:

dw = average of  2 × (prediction - real) × speed
db = average of  2 × (prediction - real)

Here's the intuition. (prediction − real) is the miss. If the line is too high (positive miss), the gradient is positive, so we'll make w and b smaller. If it's too low, the opposite. For w, each miss is multiplied by the speed, because changing the slope moves the line much more at high speed than at low speed. (The 2 comes from the squaring. You'll meet why in calculus class.) On the robot data, the formula gives dw = −2.53 at (0, 0), very close to our nudge estimate of −2.52.

Back to the tiny data set, (1, 1) and (2, 3), at w = 0, b = 0. The misses are −1 and −3:

dw = ( 2 × -1 × 1  +  2 × -3 × 2 ) / 2 = (-2 + -12) / 2 = -7
db = ( 2 × -1      +  2 × -3     ) / 2 = (-2 +  -6) / 2 = -4

Step 4: take a step, and repeat

Walk against the gradient (downhill), by a small amount called the learning rate, lr. With lr = 0.1:

w = w - lr × dw = 0 - 0.1 × (-7) = 0.7
b = b - lr × db = 0 - 0.1 × (-4) = 0.4
new MSE: predictions 1.1 and 1.8, misses 0.1 and -1.2
         (0.01 + 1.44) / 2 = 0.725           down from 5!

Repeat that 2,000 times and you reach w = 2, b = −1, the line that goes exactly through both points, with an error of practically 0. That loop is gradient descent. It's the whole algorithm.

Predict first: on the robot's data, starting at (0, 0) with lr = 0.1, the error after one step is 0.326. If you take 2,000 steps, where do you think w ends up? And what about with lr = 0.5?

With lr = 0.1 the real output is w = 0.810, b = 0.071, and the error drops from 0.3259 to 0.00036. With lr = 0.5 something surprising happens: the error goes up. Python measured 1.95, then 3.51, then 6.33, and after 30 steps it's in the millions. The steps are so big that each one jumps right over the valley to a higher point on the other side, and it gets worse every time. That's called diverging. Try it in the demo below.

Step 5: use what you learned

Now the robot has a rule: distance ≈ 0.810 × speed + 0.071. If there's 1.0 m of free space ahead, and we want a 0.2 m safety margin, what's the fastest safe speed? Try speeds 0.00, 0.05, 0.10… and keep the fastest one where predicted distance + 0.2 ≤ 1.0:

speed 0.90: 0.810 × 0.90 + 0.071 = 0.800, plus margin 0.2 = 1.000 ≤ 1.0  ✓
speed 0.95: 0.810 × 0.95 + 0.071 = 0.840, plus margin 0.2 = 1.040 > 1.0  ✗
max safe speed = 0.9 m/s

Why this is the big idea of AI

Swap "two numbers" for "a few billion numbers" and "a straight line" for "a giant neural network", and you have how large language models like ChatGPT and Claude are trained. Their "error" measures how badly they predict the next word. Gradient descent nudges every number a tiny bit downhill, trillions of times. Same loop as yours: predict, measure the error, find the gradient, step.

3. Play with it

Interactive: fit the line yourself, then let gradient descent do it

The 13 real STOP_DATA points. Drag the sliders to move the line, and watch the misses (thin lines) and the MSE. Then press Reset to (0, 0) and One gradient step a few times: the same maths as your fit_line.

MSE
–
dw
–
db
–
Steps taken
–
Safe speed, 1 m free
–

Challenges: can you beat MSE 0.0005 by hand with the sliders? Now reset, set the learning rate to 0.5, and take a few steps. Watch the MSE. Then try 0.01 and count how many steps it takes to get under 0.001.

4. Homework

Open homework/lesson03_learning.py. STOP_DATA is already there. Write five functions, in order.

Function 1: predict(w, b, speed)

What it does: the straight line.

Example: predict(2, 1, 3) → 2 × 3 + 1 = 7.

Hint 1

It's the formula from Step 1, in one line.

Hint 2

Python uses * for multiply.

Hint 3

return w times speed plus b.

Function 2: mse(w, b, data)

What it does: for every (x, y) in data, square the miss predict(w, b, x) − y, then average.

Example: mse(0, 0, [(1, 1), (2, 3)]) → (1 + 9) / 2 = 5. A line through every point, like mse(1, 0, [(1, 1), (2, 2)]), gives 0.

Hint 1

Loop over the pairs: for x, y in data: unpacks each tuple for you.

Hint 2

** 2 squares a number. Keep a running total, then divide by len(data).

Hint 3
total = 0
for each x, y in data:
    miss = predict(w, b, x) - y
    total += miss squared
return total / number of points

Function 3: gradients(w, b, data)

What it does: returns (dw, db) using the two formulas from Step 3.

Example: gradients(0, 0, [(1, 1), (2, 3)]) → (-7.0, -4.0), exactly the hand calculation above.

Hint 1

Same loop shape as mse, but keep two running totals at once.

Hint 2

Inside the loop, work out miss = predict(w, b, x) - y once, then add 2 * miss * x to one total and 2 * miss to the other.

Hint 3
dw_total = 0, db_total = 0
for each x, y:
    miss = ...
    dw_total += 2 * miss * x
    db_total += 2 * miss
return (dw_total / n, db_total / n)

Function 4: fit_line(data, lr=0.1, steps=2000)

What it does: gradient descent. Start at w = 0, b = 0. Repeat steps times: get the gradients, step both parameters downhill, then record the new MSE in a history list. Return (w, b, history).

Example: on STOP_DATA, w ends up about 0.81, and history[0] (after the first step) is 0.3259.

Hint 1

The docstring spells out the loop. Read it line by line.

Hint 2

Downhill means minus: w = w - lr * dw. Record the MSE after updating w and b, so history has exactly steps numbers.

Hint 3
w, b, history = 0.0, 0.0, []
repeat steps times:
    dw, db = gradients(w, b, data)
    update w and b
    append mse(w, b, data) to history
return w, b, history

Function 5: max_safe_speed(w, b, free_distance, margin=0.2)

What it does: tries speeds 0.00, 0.05, 0.10 … 3.00 and returns the fastest one where predict(w, b, speed) + margin ≤ free_distance, rounded to 2 decimal places. If none fit, 0.0.

Example: max_safe_speed(1, 0, 1.0) → 0.8 (0.8 + 0.2 = 1.0 just fits). max_safe_speed(1, 0, 0.1) → 0.0.

Hint 1

0.00 to 3.00 in steps of 0.05 is 61 speeds. Loop over a whole number i from 0 to 60 and make the speed i * 0.05. (Adding 0.05 over and over builds up tiny rounding errors.)

Hint 2

Keep a variable best = 0.0. Every time a speed fits, set best to it. Since speeds go up, the last one that fits is the fastest.

Hint 3

At the end, return round(best, 2). Without rounding you might return 0.9000000000000001.

Run it and see

cd homework
python3 lesson03_learning.py          # Windows: py lesson03_learning.py
cd ..

Real output from the finished file:

learned: distance = 0.810 * speed + 0.071
error went from 0.3259 to 0.00036
max safe speed with 1.0 m free: 0.9 m/s

The robot worked out its own braking rule from 13 measurements. Nobody told it 0.810.

Check it

python3 check.py 3                 # Windows: py check.py 3
Lesson 3: lesson03_learning.py
  ✓ predict
  ✓ mse
  ✓ gradients
  ✓ fit_line learns
  ✓ max_safe_speed
  5/5 passed  🎉 lesson complete!

Two of the hints the checker can give you:

  ✗ gradients: gradients(0, 0, [(1,1),(2,3)]) should be (-7.0, -4.0)
  ✗ fit_line learns: the learned w should be about 0.81

If gradients is wrong, fit_line will be too, so fix them in order. If the gradients pass but fit_line doesn't, check your signs: + lr * dw walks uphill.

Stretch

  1. Print history[0], history[9], history[99] and history[-1]. Real values: 0.3259, 0.0158, 0.00052, 0.00036. Where does most of the learning happen?
  2. Write fit_line_until(data, lr, tol) that stops as soon as the error improves by less than tol in one step, and returns how many steps it took. Compare lr = 0.01, 0.1 and 0.3.

5. Real-world challenge: the ramp

You'll collect your own data and fit your own line, first by hand and then with your code.

  1. Build a ramp: lean a stiff board, tray or big hardback book on a stack of books. Put it on a hard floor with space in front.
  2. Choose your input: the number of books in the stack (1, 2, 3, 4, 5). That's your "speed".
  3. Measure the output: let a ball or toy car roll down from the top (let go, don't push) and measure how far it travels across the floor from the bottom of the ramp, in metres. Do 2–3 rolls per height (Lesson 2: average them!).
  4. Plot it on squared paper: books across, distance up. Draw the straight line you think fits best with a ruler, and read off your w (slope) and b (where it crosses 0 books).
  5. Score your line by hand: for each point, the miss, squared, then average. That's your MSE.
  6. Let the computer do it: type your data into Python as a list of (books, metres) pairs and run fit_line(my_data, lr=0.01). With inputs as big as 5, lr = 0.1 may diverge. If history grows instead of shrinking, divide lr by 10.
  7. Record: your table of data, your hand-drawn w and b with its MSE, and the computer's w and b with its MSE.

What it teaches: real data is noisy and not perfectly straight, and that's fine. The best line isn't the one through every point; it's the one with the smallest total squared miss. You'll probably find the computer beats your ruler by a little. Then use your line to predict: how far will it roll from 6 books? Test it. Predicting something you never measured is the whole point of learning a model.

6. Your Robo-Helper so far

Lesson 0  ✓ greets you, counts steps
Lesson 1  ✓ moves without crashing
Lesson 2  ✓ measures distance to walls, knows about noise
Lesson 3  ✓ fit_line + max_safe_speed: learns its own braking and picks a safe speed

7. Common mistakes

8. Check yourself

Why do we square the misses instead of just averaging them?

Positive and negative misses would cancel, so a bad line could score 0. Squaring makes every miss count as positive, and punishes big misses more than small ones.

At some (w, b), dw = +3.0 and db = −0.5. Which way should each parameter move?

Against the gradient: w goes down (dw is positive, so the error rises as w rises) and b goes up (db is negative). With lr = 0.1: w changes by −0.3 and b by +0.05.

Predict: what does mse(2, -1, [(1, 1), (2, 3)]) return?

0. Predictions are 2×1−1 = 1 and 2×2−1 = 3, exactly the real values. That's the line gradient descent finds on this data.

Predict: with the learned w = 0.810 and b = 0.071, what's max_safe_speed(w, b, 1.5)? Work it out, then run it.

1.5. You need 0.810 × s + 0.071 + 0.2 ≤ 1.5, so s ≤ 1.517. The fastest speed on the 0.05 grid below that is 1.50.

How is this the same as how an LLM learns?

Same loop: a model with adjustable numbers makes predictions (next words instead of stopping distances), an error measures how wrong they are, gradients say which way to nudge every number, and small steps downhill repeat many times. The LLM just has billions of numbers instead of two.

9. Go deeper (optional)