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

Is it any good? Honest testing, precision and recall

Your stop-detector says it's 96% accurate. On what? Against which mistakes? This lesson is about testing a model honestly, counting the right kinds of mistakes, and choosing a threshold so the robot almost never fails to stop when it should.

Time2–3 hours, plus the challenge
You'll needPython 3, Lesson 4 finished, a ball, tape, a friend
New wordstrain/test split, confusion matrix, false positive, false negative, precision, recall, threshold
Project pieceSafe, tested stop decisions

Start here

Test a model on examples it has never seen, count each kind of mistake separately, and when one kind of mistake is dangerous, move the threshold to make that one rare.

The problem

In Lesson 4 the neuron got 96.2% of its examples right. Sounds great. But those were the same examples it learned from. That's like a teacher handing out the exam answers, then setting the same exam. Also, "96% right" hides which 4% were wrong. A robot that stops for no reason is annoying. A robot that drives into the cat is a disaster. Those can't count the same.

The idea in one picture

Think of a smoke alarm. If it goes off when you burn toast, that's a false alarm: irritating, but nobody gets hurt. If it stays quiet during a real fire, that's a miss, and that's the one that matters. So smoke alarms are set to be jumpy on purpose. They trade lots of false alarms for almost no misses. We'll do the same with Robo-Helper's brakes.

Words to know

Training set / test set
Examples the model learns from, and different examples kept hidden until the end to check it.
Confusion matrix
A 2×2 table counting the four outcomes: right stop, false alarm, missed stop, right go.
False positive (false alarm)
Said "stop" but there was no need.
False negative (miss)
Said "go" but should have stopped. The dangerous one.
Precision
Of all the times the robot said stop, what fraction was right?
Recall
Of all the times the robot should have stopped, what fraction did it catch?
Threshold
The probability at which we switch from "go" to "stop". Lesson 4 used 0.5.

The rest is the full lesson: the problem in detail, the ideas built from nothing with real numbers, a demo, the homework and a challenge you do with a friend.

1. The problem

Two questions decide whether Robo-Helper is safe to let loose:

  1. Will it work on situations it hasn't seen? In the real room, every reading is new. Scoring the model on its own training examples tells you how well it memorised, not how well it works.
  2. Which mistakes does it make? One number, accuracy, mixes harmless mistakes with dangerous ones.

The obvious approach, "count how many it gets right on the data we have", fails both questions. Let's fix them one at a time.

2. First principles

Step 1: hide some examples (the train/test split)

Before training, set aside some examples and don't let the model see them. Train on the rest. Then score on the hidden ones. That's a fair exam.

600 examples
  shuffle them (in case they're in some order, e.g. all "stop" ones first)
  first 25% → TEST  (150 examples, locked in a drawer)
  the rest  → TRAIN (450 examples, the model learns from these)

Two details matter. Shuffle, because if the data were sorted, the test set might be all "go" examples. And shuffle a copy, because the caller might still need their list in its original order.

Step 2: why accuracy alone lies

Predict first: the test set has 150 examples, and 37 of them need a stop. A lazy robot that always says "go" (never stops) gets what accuracy?

It's right on all 113 "go" examples and wrong on all 37 "stop" ones: 113 / 150 = 75.3%. Three quarters right, while crashing into every single obstacle. Accuracy alone can't tell you if a robot is safe.

Step 3: count the four outcomes

Every prediction falls into one of four boxes. 1 means "stop":

                       truth: stop (1)      truth: go (0)
  robot said stop (1)   TP  true positive    FP  false positive (false alarm)
  robot said go   (0)   FN  false negative   TN  true negative
                            (MISSED STOP!)

A tiny worked example. Predictions [1, 1, 0, 0, 1], true labels [1, 0, 1, 0, 1]. Go pair by pair:

pred 1, label 1 → TP
pred 1, label 0 → FP
pred 0, label 1 → FN
pred 0, label 0 → TN
pred 1, label 1 → TP
(tp, fp, fn, tn) = (2, 1, 1, 1)

Step 4: precision and recall

From those four counts we get three questions:

accuracy  = (tp + tn) / all       how often right overall?          (2+1)/5 = 0.6
precision = tp / (tp + fp)        when it says stop, is it right?    2/3     = 0.667
recall    = tp / (tp + fn)        of the needed stops, how many?     2/3     = 0.667

For brakes, recall is the safety number. Recall 0.95 means 1 in 20 needed stops is missed. One edge case: if the robot never says stop, tp + fp = 0 and precision would divide by zero. Define it as 0 instead of crashing.

Step 5: the real numbers

Here's the Lesson 4 neuron, trained on the 450 TRAIN examples and tested on the 150 TEST examples it never saw, at threshold 0.5:

TEST, threshold 0.5           truth: stop   truth: go
  said stop                       35            2
  said go                          2          111

accuracy 0.973   precision 0.946   recall 0.946

Two needed stops were missed. One of them, reading (2.51 m, 1.51 m/s), got a stop-probability of 0.499. It missed the 0.5 threshold by a thousandth. On a real robot, that's a bump into something.

Step 6: move the threshold

The neuron outputs a probability. The threshold is our choice. Lower it, and the robot stops more often: fewer misses, more false alarms. Here's the trade on TEST:

threshold   precision   recall   accuracy
   0.9        1.000      0.784     0.947    never a false alarm, misses 8 stops
   0.7        1.000      0.946     0.987
   0.5        0.946      0.946     0.973
   0.3        0.878      0.973     0.960
   0.22       0.837      0.973     0.947
   0.1        0.804      1.000     0.940    catches every stop, lots of false alarms

A missed stop and a false alarm don't cost the same. A false alarm: the robot pauses for a second. A miss: it hits the cat. So we pick a rule: recall must be at least 0.97, and among thresholds that manage that, take the highest one (the fewest false alarms while staying safe). Try 0.99, 0.98, 0.97 … downwards, and stop at the first that passes.

Step 7: choose on TRAIN, judge on TEST

Look at the table again: 0.7 has the best test accuracy. It's tempting to just pick that. Don't. If you choose anything by looking at the test set, the test set isn't hidden any more, and its score stops being honest. So safest_threshold searches using the training data. On TRAIN, 0.5 misses 11 of 110 stops (recall 0.900). The search lands on 0.22, where TRAIN recall is 107/110 = 0.973.

Predict first: at threshold 0.22, will TEST accuracy go up or down compared to 0.5 (0.973)? And recall?

Accuracy goes down to 0.947, and recall goes up to 0.973:

TEST, threshold 0.22          truth: stop   truth: go
  said stop                       36            7
  said go                          1          106

accuracy 0.947   precision 0.837   recall 0.973

The robot makes more mistakes overall (8 instead of 4), but now they are mostly harmless false alarms. Only one miss is left: reading (2.79, 1.38), probability 0.186. The sensor noise made a dangerous situation look calm. No threshold above 0.186 can catch it, which is why real robots also have bumpers. An honest report says that out loud.

Honest surprise: at 0.5, the TEST accuracy (0.973) is higher than the TRAIN accuracy (0.953). Usually it's the other way round. With only 150 test examples, luck in which ones landed in the drawer moves the score by a couple of percent. Small test sets give wobbly numbers. That's a reason to collect more test data, not to celebrate.

3. Play with it

Interactive: slide the stop threshold

Every dot is one example, placed by the stop-probability the neuron gave it. Top row: situations that truly needed a stop. Bottom row: fine to go. Everything right of the line, the robot stops for. Red-ringed dots in the top row are missed stops. This uses the exact neuron and the exact TRAIN/TEST split from lesson05_evaluate.py.

truth: stoptruth: go
said stop––
said go––
Accuracy
–
Precision
–
Recall
–
Missed stops
–

Things to try: find the threshold where TEST recall hits 1.000. How many false alarms does that cost? Then switch to TRAIN and press the button: it should land on 0.22, just like the Python.

4. Homework

Open project/homework/lesson05_evaluate.py. It imports your Lesson 4 code, so finish Lesson 4 first. Write these, in order:

4.1 train_test_split(data, test_fraction, rng)

Shuffle a copy of data with rng. The first test_fraction of the shuffled copy is the test set, the rest is train. Return (train, test), train first. Example: 100 items with 0.25 → 75 train, 25 test, and the original list is unchanged.

Hint 1

list(data) makes a copy. rng.shuffle(items) shuffles a list in place (it returns None, so don't assign its result).

Hint 2

The number of test items is int(len(items) * test_fraction). Call it k. Slicing gives you the two parts.

Hint 3

Copy, shuffle the copy, compute k, then return (the slice from k to the end, the slice from the start up to k).

4.2 confusion(predictions, labels)

Count (tp, fp, fn, tn). Example: confusion([1, 1, 0, 0, 1], [1, 0, 1, 0, 1]) → (2, 1, 1, 1), the worked example in Step 3.

Hint 1

Walk through both lists together with zip(predictions, labels).

Hint 2

Keep four counters starting at 0. Each pair (p, y) adds 1 to exactly one of them.

Hint 3

If p is 1 and y is 1 → tp. p 1, y 0 → fp. p 0, y 1 → fn. Otherwise → tn. Return the four counts in that order.

4.3 accuracy, precision, recall

Each takes (tp, fp, fn, tn) and returns a number from 0 to 1, using the formulas in Step 4. Precision and recall return 0.0 when their bottom line would be 0. Example: precision(0, 0, 3, 5) → 0.0, not a crash.

Hint 1

Each is a one-liner. Write the formula from Step 4.

Hint 2

For precision and recall, check the bottom first: if tp + fp == 0: return 0.0.

Hint 3

Python's one-line form works too: return (top / bottom) if bottom else 0.0.

4.4 evaluate(weights, bias, data, threshold)

For each (x, label) in data, predict 1 if neuron(weights, bias, x) >= threshold, else 0. Build the confusion counts and return {"accuracy": ..., "precision": ..., "recall": ...}.

Hint 1

You need two lists of the same length: your predictions and the true labels.

Hint 2

neuron is already imported from Lesson 4. Pass the four counts to your metric functions. The * trick, accuracy(*c), unpacks a tuple c into four arguments.

Hint 3

preds = for each (x, _) in data, 1 if neuron(...) ≥ threshold else 0. labels = each y. c = confusion(preds, labels). Return a dict with the three metrics of c.

4.5 safest_threshold(weights, bias, data, min_recall=0.97)

Try thresholds 0.99, 0.98, … 0.01. Return the first (highest) one whose recall on data is at least min_recall. If none passes, return 0.01.

Hint 1

Avoid adding 0.01 over and over: floating-point drift gives you 0.21999999. Loop over whole numbers and divide.

Hint 2

range(99, 0, -1) counts 99, 98, … 1. Divide by 100 to get the threshold.

Hint 3

For i from 99 down to 1: t = i / 100; if evaluate(weights, bias, data, t)["recall"] ≥ min_recall, return t. After the loop, return 0.01.

Run it

cd homework
python3 lesson05_evaluate.py

What you should see (real output from the reference solution):

at 0.5 on TEST: {'accuracy': 0.973, 'precision': 0.946, 'recall': 0.946}
safest threshold (chosen on TRAIN): 0.22
at that threshold on TEST: {'accuracy': 0.947, 'precision': 0.837, 'recall': 0.973}

Line 1: the honest score at the default threshold. Line 2: the threshold picked for safety, using only training data. Line 3: the honest score at that threshold. Recall went up, precision went down.

Check it

cd ..
python3 check.py 5

Hints you might meet:

  ✗ train_test_split: don't shuffle the original list: shuffle a copy
  ✗ metrics: precision with no 'stop' predictions should be 0, not a crash

If safest_threshold shows a yellow … saying "Lesson 4: write this function", your Lesson 4 isn't finished. This lesson stands on it. 4/4 passed 🎉 lesson complete! means you're done.

Stretch

5. Real-world challenge: test a friend's "will it cross?" calls

You'll measure a human detector's precision and recall, then change its threshold by changing the instructions.

  1. Set up. Clear a stretch of floor, away from stairs and anything breakable. Put a strip of tape across the floor about 2 metres from where you'll roll from. Use a soft ball or a rolled-up pair of socks.
  2. Round 1 (20 rolls). Roll the ball toward the line, varying how hard you push. As soon as it leaves your hand, your friend calls "crosses!" (1) or "stops!" (0). Then watch: did it actually cross the tape? Write down both, for example call 1, truth 0.
  3. Score round 1. Fill in a confusion matrix (TP, FP, FN, TN). Work out accuracy, precision and recall by hand. Here "crosses" plays the role of "must stop".
  4. Round 2 (20 rolls). Change the instructions: "Pretend a missed 'crosses' means the ball hits a baby. Only say 'stops!' if you're really sure." You've just lowered their threshold.
  5. Compare. Did recall go up? Did precision go down? Did accuracy change much?

What to record: two confusion matrices and three numbers for each round. What it teaches: precision and recall aren't computer things. They describe any yes/no judge, including people. And the threshold is a choice about which mistake you'd rather make.

6. Your Robo-Helper so far

The obstacle detector from Lesson 4 now has a tested, safety-tuned threshold. Robo-Helper no longer brakes when it's 50% sure. It brakes when it's 22% sure, because missing a stop is worse than a pause.

7. Common mistakes

8. Check yourself

A model has 0 false positives. What's its precision, and does that make it safe?

Precision is 1.0 (every stop it called was right), but it could still be missing lots of needed stops. Safety is recall. At threshold 0.9, precision is 1.000 but 8 stops are missed.

Predict: confusion([0, 0, 0], [1, 1, 0])?

(0, 0, 2, 1): two missed stops, one correct go. Then precision is 0.0 (no stop predictions at all), recall is 0.0.

Why does lowering the threshold raise recall but lower precision?

A lower threshold means more examples get called "stop". More of the real stops get caught (recall up), but more harmless ones get called stop too (more false alarms, so precision down).

Why must the threshold be chosen on TRAIN, not TEST?

The test set is our only stand-in for brand-new situations. If we tune anything to it, our score is no longer a fair prediction of how the robot will do on truly new data.

Accuracy dropped from 0.973 to 0.947 when we moved to 0.22. Why is that the better robot?

It traded 1 missed stop for 5 extra false alarms. A missed stop can hurt something. A false alarm costs a pause. Accuracy counts both the same, so it can't see the difference.

9. Go deeper (optional)