Build with AI

How do you know your model actually improved?

Written as of · Published · Updated · By Robert Scales, with AI tools · 4 min read

Short answer

Compare old and new on the same untouched test set, count which examples each one alone gets right, put a confidence interval on the difference and repeat with other seeds. In one measured test on pure random numbers, a common leakage mistake reported 84% accuracy where the honest answer was 51%.

A higher number on one run is weak evidence. Models are trained with random starting points, tests hold a limited number of examples, and people tend to stop looking once the score goes up. Each of those can hand you an "improvement" that vanishes the next time you try.

This project measures four traps directly, using scikit-learn's digits and a set of pure random numbers. The companion script, 04_honest_testing.py, ran in about five seconds here on an Apple M5 Max with two processor threads. For testing language models specifically, including bits per byte and benchmarks, see how to tell whether a language model you trained is any good. This article covers what applies to any model.

Trap 1: luck looks like progress

Same data, same recipe, 20 different random seeds. Training on 300 digit images and testing on another 300:

MeasureValue
Lowest accuracy94.33%
Highest accuracy97.33%
Standard deviation0.85 points

So a three-point "improvement" can appear with no change at all except the seed. Before you compare two ideas, find out how much the score moves when nothing else changes. A change smaller than that band is not yet evidence.

Trap 2: comparing totals instead of examples

Two models tested on the same 300 images give you more information than two percentages. Count where they disagree.

import numpy as np

def compare(a_right, b_right, rounds=5000, seed=0):
    """a_right and b_right: arrays of True/False, one per test example."""
    rng, n = np.random.default_rng(seed), len(a_right)
    diffs = []
    for _ in range(rounds):
        i = rng.integers(0, n, n)                # resample the test set
        diffs.append(b_right[i].mean() - a_right[i].mean())
    return {"only_a": int((a_right & ~b_right).sum()),
            "only_b": int((b_right & ~a_right).sum()),
            "ci95": np.percentile(diffs, [2.5, 97.5]).round(4)}

This is a paired bootstrap: it reuses the test set many times, drawn at random with repeats, to see how much the difference would wobble with a different sample. Measured here:

ComparisonABOnly A rightOnly B right95% interval for B − A
Small tweak to the regularization setting96.67%96.00%20−1.67 to 0.00 points
Different model family (support vector machine)96.67%98.33%160.00 to +3.33 points

The second result looks clearly better: 6 examples to 1. Yet the interval touches zero. An honest report says "probably better, and a larger test would settle it". Only seven of 300 test images even separate the two models. Everything rests on those seven, which is a good reason to read them.

Trap 3: the test set wears out

Every time you peek at a test score and react, some of the test set leaks into your decisions. To measure this, the script tried 60 settings on a small training set, kept whichever scored best on a test set of 60 images, and then checked that winner on 900 fresh images. It repeated this 20 times.

Average accuracy
Reported: best of 60 on the small test92.67%
Same winners on fresh data91.50%

The gap of 1.2 points is modest here, because these settings do not differ wildly and the digits are easy. It grows with more tries, smaller tests and harder problems. Keep three piles: training data, a development set you may peek at freely, and a final test you open rarely. If you change anything because of the final test, it has become a development set.

Trap 4: leakage turns noise into skill

Data leakage happens when information unavailable at prediction time gets into model building. This is the most dramatic result in the project. The data is 100 rows of 2,000 random features with random labels, so nothing can be learned and the true skill is 50%.

from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline

# WRONG: choose the features using every row, test rows included
chosen = SelectKBest(f_classif, k=20).fit(N, Y).transform(N)
leaky = cross_val_score(LogisticRegression(max_iter=500), chosen, Y, cv=5).mean()

# RIGHT: choosing features is part of training, redone inside each split
honest_model = make_pipeline(SelectKBest(f_classif, k=20), LogisticRegression(max_iter=500))
honest = cross_val_score(honest_model, N, Y, cv=5).mean()
Method, averaged over 20 drawsAccuracy on pure noise
Features chosen before splitting84.3%
Features chosen inside each split50.9%

Same data, one line in a different place, and a useless model reports 84%. Common real-world forms: scaling on all the data, near-duplicates on both sides of the split, and a feature that exists only after the outcome is known, such as "refund issued" used to predict refund requests.

Two cheap sanity checks catch many of these. Train with shuffled labels: an honest pipeline must fall to chance, and here it scored 8.67% against a chance level of 10%. And compare with a baseline: always answering the most common digit got 10.33% on this test.

Try it: Add one test: run your current best model with shuffled training labels. If the score does not drop to the level of guessing, stop and look for a leak before doing anything else.

What should a results note contain, so that someone else can trust it?

Six lines are enough, and they take a few minutes to write:

LineExample
ChangeReplaced logistic regression with a support vector machine
Data and splitDigits, 300 train, 300 test, stratified, seed 1, untouched
BaselinesMost common class 10.33%; previous model 96.67%
Result98.33%; only-new-right 6, only-old-right 1; 95% interval 0.00 to +3.33 points
Noise checkSame recipe across 20 seeds moved 3 points; shuffled labels fell to chance
DecisionProbably better; confirm on a larger test before adopting

The note's real value is the discipline of writing the decision after the numbers. For tools you use instead of train, the same idea applies with a small set of realistic tasks. A low score is useful too, since it tells you where to look. What wastes months is a high score you cannot explain.

Download the companion code for this series (zip), with every script and the recorded results.

Sources

  1. Common pitfalls and recommended practices — scikit-learn
  2. Cross-validation: evaluating estimator performance — scikit-learn
  3. A Recipe for Training Neural Networks — Andrej Karpathy, April 25, 2019

From the Guide to Artificial Intelligence, by Robert Scales, published by Holistic Hybrid Inc. Share and adapt it under CC BY-NC-SA 4.0: credit the guide, link to https://guidetoartificialintelligence.com/en/learn/did-your-model-improve-honest-testing/, no commercial use without permission, and keep the same licence.

Get the free Guide to Artificial Intelligence

Sign up for the newsletter and receive the free guide: what changed in AI, one lesson, one prompt to try. Unsubscribe in one click.

How we handle your address

Back to top

Display

Your choices are saved in this browser only. No data is sent.

Text size
100%
Colours
Reading
More settings
Accessibility statement

Get the app

Add the guide to your home screen. It opens like an app, and the pages you have read stay available offline.

Read without a connection: download the articles

iPhone and iPad (Safari, Chrome, Edge)

  1. Tap the Share button (the square with an arrow). In Chrome and Edge, it is in the address bar or the menu.
  2. Choose “Add to Home Screen”.
  3. Tap “Add”.

Android (Chrome, Edge, Samsung Internet, Firefox)

  1. Open the browser menu (⋮ or ☰).
  2. Choose “Install app” or “Add to Home screen”.
  3. Confirm with “Install”.

Computer (Chrome, Edge)

  1. Click the install icon at the right end of the address bar, or open the browser menu (⋮ or …).
  2. In Chrome, choose “Cast, save and share”, then “Install page as app”. In Edge, choose “Apps”, then “Install this site as an app”.
  3. Click “Install”.

Mac (Safari)

  1. Open the File menu.
  2. Choose “Add to Dock”.
  3. Click “Add”.

Firefox on a computer can’t install a site as an app. Use Chrome, Edge or Safari instead.