Baselines and Choosing a Model
Deciding a model is good enough
Past a certain point every extra try improves the validation score and not the model, so a stopping rule written before the work starts is worth more than another week of tuning.
- 13 min read
- 3 reading levels
- Published
On this page 5
One lesson, three depths. Pick the one that fits you today — you can switch any time.
Beginner — No maths. Plain English.
A model is good enough when the decision it feeds stops changing — and past that point, more tuning improves your score without improving your model.
A cook sharpens the knife until it slices the onion cleanly. That is the finish line. Sharpening for another twenty minutes does not make the onion more sliced; it wears the blade and the dinner is late.
Nobody argues about this in a kitchen, because "cuts the onion" is a plain test that either passes or fails.
Model work has no such test, so people sharpen forever. There is always one more idea, and the score always has one more decimal place.
Why it exists
"Make it as accurate as possible" sounds responsible and is a trap. It has no end, so the project cannot finish, and the team cannot tell a good week from a wasted one.
Two things go wrong once you pass the useful point.
The first is boring: time. Weeks spent moving a score from 0.86 to 0.87 are weeks not spent on the real blockers. The data, the interface, the people who have to trust it.
The second is worse. Every time you try something and keep the version that scored best, you learn a little bit about your test data by accident. After enough tries, the score is going up and the model is not. You are choosing the luckiest model, not the best one.
So the rule has to be written down before the work begins.
How it works
score
| .-------------- you are here, still "improving"
| .----'
| .---' <- past this point the gains are luck
| .---'
| .--' <- real improvement lives here
| .'
+-------------------------------------
weeks of work
Three questions that end the argument:
1. Has it reached the number we agreed before starting?
2. Is the last gain bigger than the wobble in the measurement?
3. Is the model still the thing holding the product back?
Any "no" means stop.Question 3 is the one people forget. Suppose a model at 0.86 already beats the humans doing the job, and nobody trusts its answers. Another point of accuracy fixes nothing.
A real example you have seen
Photo editing on a phone. You brighten, you adjust, you compare. After a while every change makes it different rather than better, and if you keep going you ruin it.
Experienced editors set a limit before they start. Not because they cannot see improvements — because they know that after a point, what they are seeing is not improvement.
Remember this
- Agree the finish line before the work starts. "As high as possible" is not a finish line.
- If the last gain is smaller than the measurement wobble, it is not a gain.
- Stop when the model is no longer the thing holding the product back.
What to learn next
- Test-set overfitting — the mechanism that inflates the number you are chasing.
- Defining the success metric — writing the finish line before the work starts.
- Monitoring and drift — why "good enough" has an expiry date.
Developer — Code and libraries.
Setup
pip install scikit-learnVerified with scikit-learn 1.7.2, numpy 1.26.4, Python 3.10, CPU. Takes about a minute — it trains eighty models. Data is generated in memory.
Eighty tries, and where the real improvement stopped
An honest tuning campaign: random configurations, keep whichever scores best on validation, and — the part nobody normally gets to see — record what that model actually scores on a large untouched test set.
import numpy as np
from sklearn.datasets import make_classification
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
X, y = make_classification(n_samples=5000, n_features=20, n_informative=6,
flip_y=0.15, random_state=0) # 15% of labels are noise
Xtr, Xrest, ytr, yrest = train_test_split(X, y, test_size=0.6, random_state=0)
Xva, Xte, yva, yte = train_test_split(Xrest, yrest, train_size=250, random_state=0)
print(f"train {len(ytr)} validation {len(yva)} test {len(yte)}")
rng = np.random.default_rng(1)
best_val, best_test, hist = -1.0, 0.0, []
for i in range(1, 81):
m = HistGradientBoostingClassifier(
learning_rate=float(10**rng.uniform(-2.2, -0.3)),
max_leaf_nodes=int(rng.integers(4, 64)),
min_samples_leaf=int(rng.integers(5, 80)),
l2_regularization=float(10**rng.uniform(-3, 1)),
random_state=0).fit(Xtr, ytr)
v = accuracy_score(yva, m.predict(Xva))
if v > best_val: # the usual keep-the-winner rule
best_val, best_test = v, accuracy_score(yte, m.predict(Xte))
hist.append((i, best_val, best_test))
print("\ntries best on validation that model on test gap")
for i, v, t in hist:
if i in (1, 3, 6, 12, 25, 50, 80):
print(f"{i:5d} {v:18.4f} {t:18.4f} {v-t:+.4f}")train 2000 validation 250 test 2750
tries best on validation that model on test gap
1 0.8400 0.8367 +0.0033
3 0.8480 0.8404 +0.0076
6 0.8560 0.8535 +0.0025
12 0.8560 0.8535 +0.0025
25 0.8600 0.8480 +0.0120
50 0.8640 0.8520 +0.0120
80 0.8640 0.8520 +0.0120The walkthrough
The validation column never goes down. It cannot — it is a running maximum. That is what makes it such a persuasive and such a dishonest progress report. Watching it climb 0.8400 → 0.8640 over eighty tries feels like eighty tries of progress.
The test column says the campaign finished at try 6. The genuinely best model on unseen data appeared at try 6 with 0.8535. Try 25 scored higher on validation and lower on test (0.8480). Seventy-four further attempts produced nothing, and finished slightly behind where they started.
The gap widens from +0.0033 to +0.0120. That widening is the accumulated luck, and it is why the validation score stops being a usable estimate of quality. You would have reported 0.864 and delivered 0.852. See test-set overfitting for the full mechanism.
The 250-row validation set is the accelerant, and it is realistic. At 250 rows one example is 0.4 accuracy points, so a difference of 0.004 is a single row changing its mind. With 2,500 validation rows the same campaign would drift far more slowly — the arithmetic is in noisy validation curves.
flip_y=0.15 sets a ceiling nobody can pass. Fifteen percent of labels are random, so no model — however large, however tuned — beats roughly 0.925 on this data. Every campaign has such a ceiling. Estimating it early converts "we are at 0.86, keep pushing" into something usable: "we are at 0.86 of a possible 0.92".
Estimating the ceiling before you chase it
You cannot know the true ceiling, and you can bracket it cheaply.
- Ask two people to label the same 200 examples independently. Their disagreement rate is a practical upper bound on achievable accuracy. If two experts disagree on 12% of cases, a model reaching 88% has met the humans it imitates.
- Relabel a sample carefully and measure how many original labels were wrong. That noise rate is a floor on irreducible error, and it is often the largest single number in the whole project.
- Fit a deliberately over-large model on the training set and see where it saturates. If a model that can memorise the training data still plateaus on validation, the remaining error is in the data. The reasoning is in too small a model, or trained too little?.
Two hours of this changes the conversation more than two weeks of tuning, and it is the input a learning curve needs to answer whether more data would help.
A stopping rule you can write down
Write these into the project brief before the first model is trained.
- The target. One number, on one metric, on one population, agreed with whoever asked for the model — see defining the success metric.
- The worst-slice floor. The overall target plus a minimum for every named slice, so the finish line cannot be reached by failing a segment (evaluating by slice).
- The noise band. Measured, not guessed: run the current recipe with three to five seeds and take the spread (seed variance and error bars). Any gain smaller than this is not a gain.
- A patience budget. "Stop after three consecutive ideas that fail to beat the noise band." Ideas, not hours — this survives a good week and a bad one.
- A cost per point. What does one accuracy point buy, in money or in decisions changed? When the answer is "we do not know", that is the finding, and it outranks any tuning.
- A hold-out you have never touched. Scored exactly once, at the end. It is the only number in the project that is not inflated by selection.
Common mistakes
Reporting the validation number as the result. After eighty tries it is a maximum over eighty noisy draws, not an estimate of quality. Report the untouched test number, and say how many configurations were tried.
Stopping because the team is tired. Fatigue and diminishing returns arrive at similar times and are not the same thing. A written rule protects you from stopping too early on a good project and from continuing too long on a finished one.
Chasing the metric after the bottleneck moved. If predictions arrive too late to be used, or nobody trusts them, or the data pipeline breaks weekly, accuracy is no longer the constraint. Say so out loud, in the review, with evidence.
Ignoring what improvement costs to keep. A gain that arrives with a model ten times larger costs latency, money and operational risk forever — the trade-off in buying accuracy with size. Small gains rarely survive that arithmetic.
Treating "good enough" as permanent. It is a decision for today's data and today's users. Both change, which is why the model needs monitoring rather than a farewell.
Try it yourself
Change train_size=250 to train_size=2000 for the validation split and rerun. Watch the gap between the validation and test columns shrink, and note how much later the campaign's true peak arrives. Then decide, for your own project, which is worth more: those rows in the training set, or those rows making your stopping decision trustworthy.
What to learn next
- Test-set overfitting — the mechanism that inflates the number you are chasing.
- Defining the success metric — writing the finish line before the work starts.
- Monitoring and drift — why "good enough" has an expiry date.
Researcher — Mathematics and papers.
The running maximum is a biased estimator
The validation column above is $\max_{j \le i} \hat{R}_V(h_j)$ over $i$ candidates, and the quantity of interest is $R(h_{\hat{j}})$ for the selected $\hat{j}$. These differ by an optimism term. For $i$ candidates with roughly independent evaluation noise of scale $\sigma$, the expected maximum of the noise exceeds zero by approximately $\sigma\sqrt{2\ln i}$, so the reported score inflates with the logarithm of the number of things tried, at fixed model quality. The bound is loose under the strong correlation between similar configurations, but the direction is guaranteed and monotone: more search always inflates the reported number, and never deflates it.
This is the multiple-comparisons problem in a form that has no natural correction, because the hypotheses are chosen adaptively as the campaign proceeds. Dwork et al. (2015), The Reusable Holdout: Preserving Validity in Adaptive Data Analysis (Science), formalise adaptive data analysis and show that after $k$ adaptive queries a holdout of size $n$ supports accurate answers only for $k$ growing polynomially in $n$ under naive reuse — and give differential-privacy-based mechanisms (Thresholdout) that restore validity to exponentially many queries by adding calibrated noise and answering only when the holdout materially disagrees with the training estimate. The practical descendant is the rule above: a final holdout consulted exactly once.
The reassuring empirical counterweight is Recht et al. (2019), Do ImageNet Classifiers Generalize to ImageNet? (ICML), who built fresh test sets for CIFAR-10 and ImageNet after a decade of community-wide adaptive selection. Absolute accuracy dropped substantially on the new sets, but the ranking of models was almost perfectly preserved. The lesson is precise: adaptive selection corrupts the reported level far more than the ordering, so a leaderboard remains useful for choosing between models and useless for predicting deployed performance.
The ceiling: irreducible error and its estimation
Excess risk decomposes into approximation, estimation and optimisation terms over an irreducible floor — the Bayes error $R^\star = \mathbb{E}_x[\min_k(1 - P(y=k \mid x))]$, determined by the overlap of the class-conditional distributions and by label noise. Under a uniform label-flip rate $\rho$ applied to an otherwise separable problem, the floor is $\rho$ directly. Note the arithmetic for this dataset: scikit-learn's flip_y re-assigns that fraction of labels uniformly over the $k$ classes rather than flipping them, so the effective flip rate is $\rho = \text{flip_y} \cdot (k-1)/k = 0.075$ for two classes — which is why flip_y=0.15 caps this dataset near 0.925 rather than near 0.85.
Estimating $R^\star$ on real data is genuinely hard, and the practical proxies each answer a slightly different question. Inter-annotator agreement (Cohen's $\kappa$, Krippendorff's $\alpha$) bounds the achievable accuracy of a model trained to imitate those annotators, and is the standard instrument in NLP and medical imaging. Re-annotation studies of benchmark test sets — Northcutt, Athalye and Mueller (2021), Pervasive Label Errors in Test Sets Destabilize Machine Learning Benchmarks (NeurIPS Datasets and Benchmarks) — found label errors averaging 3.4% across ten widely used benchmarks and, more consequentially, showed that correcting them can reverse the ranking of models whose measured gap is smaller than the error rate. Any reported improvement smaller than the test set's label-noise rate is uninterpretable, which is a sharper stopping criterion than most projects apply.
Direct Bayes-error estimation from data is possible under assumptions — nearest-neighbour error rates bracket $R^\star$ asymptotically via the classical Cover–Hart result $R^\star \le R_{\text{1NN}} \le 2R^\star(1-R^\star)$ — but the constants are weak enough at finite sample that the annotation-based estimates are the ones that change decisions.
Stopping as a decision problem
The engineering question "when do we stop" is a sequential decision under uncertainty, and the framings that transfer are worth naming. Under optimal stopping, one continues while the expected value of the next experiment exceeds its cost; since gains decay roughly as a power law in effort while cost is linear, a finite optimal horizon always exists and is usually much shorter than teams assume. Bandit and Bayesian-optimisation formulations make this operational: expected improvement acquisition functions supply an explicit termination criterion when expected improvement falls below a threshold (Bayesian optimisation for hyperparameters), and successive halving allocates a fixed budget rather than an open-ended one — a structural defence against unbounded campaigns.
The larger point is that the metric is a proxy whose relationship to value is rarely linear. Utility is usually a step function of a thresholded decision, so accuracy gains that do not move examples across the operating threshold have literally zero value (choosing a threshold from costs). Converting a metric delta into a decision delta — how many cases change hands, at what expected cost — is the calculation that ends most tuning arguments, and it belongs in the project brief beside the target rather than being improvised at the end.
What to learn next
- Test-set overfitting — the mechanism that inflates the number you are chasing.
- Defining the success metric — writing the finish line before the work starts.
- Monitoring and drift — why "good enough" has an expiry date.