Reproducibility and Running Experiments

Making sense of two hundred runs

A hyperparameter sweep is read with averages over groups, not by staring at the single best run — the top row of the leaderboard is part signal, part luck.

On this page 5
  1. Why it exists
  2. How it works
  3. A real example you have seen
  4. Remember this
  5. What to learn next

One lesson, three depths. Pick the one that fits you today — you can switch any time.

Beginner — No maths. Plain English.

Do not ask "which run won?" — ask "which settings win on average?"

A school posts its exam results: two hundred students across many sections. The topper studied six hours daily, took tuition, and slept eight hours. Should every student copy her? Not yet — the topper is one data point, and toppers are partly lucky. The useful question is different: did students who slept eight hours do better on average than those who slept five? Averages across many students wash out individual luck.

A hyperparameter sweep — running your training many times with different settings — produces exactly such a results board. Two hundred runs, each with its settings and score. The top run is the topper: interesting, but partly luck.

Why it exists

Sweeps are cheap to launch and confusing to read. The temptation is overwhelming: sort by score, copy the winner's settings, move on. Two problems follow.

First, the winner's score is inflated — among two hundred runs, the best one is best partly because luck favoured it, the best-of-many effect again. Second, copying the winner teaches you nothing about which settings mattered. Was it the learning rate, the size of each training step? The depth? You cannot tell from one row.

How it works

wrong reading:            right reading:

sort by score             group runs by ONE setting, average each group
take row 1                 
copy its settings         learning rate:  small → 0.77
                                          medium→ 0.82   ← this matters
                                          large → 0.78
                          depth:          every group lands near 0.79  ← barely matters

Averaging across a group means each average contains dozens of runs — the luck cancels, the pattern remains. You learn which knobs matter, which is knowledge that transfers to the next sweep.

A real example you have seen

"Which cricket team should bat first?" is never answered from one match. Analysts average over hundreds of matches — teams batting first win some percentage at this ground, under lights, in this season. One match is a story; the average over many is evidence. Sweep-reading is match analysis for your training runs.

Remember this

  • The single best run is part signal, part luck — respect it, do not worship it.
  • Read sweeps with group averages, one setting at a time.
  • The output of a sweep is which knobs matter, not one magic row.

What to learn next

Developer — Code and libraries.

Setup

bash
pip install pandas

Verified with pandas 2.2.3, numpy 1.26.4, CPU. Seeded, so your numbers should match.

A two-hundred-run sweep, read properly

We simulate a sweep honestly: a hidden truth (learning rate near 3e-3 helps a lot, depth 4+ helps a little, dropout does nothing) plus per-run noise — then read it back out with groupbys, as you would a real runs.jsonl from the config lesson.

read_sweep.py
import numpy as np
import pandas as pd

rng = np.random.default_rng(0)
n = 200
runs = pd.DataFrame({
    "lr": (10 ** rng.uniform(-4, -1, n)).round(6),
    "depth": rng.integers(2, 8, n),
    "dropout": rng.uniform(0, 0.5, n).round(2),
})
# a hidden truth: lr near 3e-3 helps a lot, depth >= 4 helps a little
skill = (0.75
         + 0.05 * (np.abs(np.log10(runs.lr) + 2.5) < 0.75)
         + 0.02 * (runs.depth >= 4))
runs["score"] = (skill + rng.normal(0, 0.02, n)).round(4)

print("--- top 3 runs ---")
print(runs.sort_values("score", ascending=False).head(3).to_string(index=False))

runs["lr_bucket"] = pd.cut(np.log10(runs.lr), bins=[-4, -3, -2, -1])
print("\n--- mean score by learning-rate bucket ---")
print(runs.groupby("lr_bucket", observed=True)["score"]
          .agg(["mean", "count"]).round(3).to_string())

print("\n--- mean score by depth ---")
print(runs.groupby("depth")["score"].mean().round(3).to_string())
Output
--- top 3 runs ---
      lr  depth  dropout  score
0.004595      4     0.31 0.8511
0.001887      6     0.36 0.8502
0.000924      7     0.36 0.8490

--- mean score by learning-rate bucket ---
            mean  count
lr_bucket              
(-4, -3]   0.771     57
(-3, -2]   0.815     64
(-2, -1]   0.775     79

--- mean score by depth ---
depth
2    0.782
3    0.773
4    0.794
5    0.798
6    0.787
7    0.788

The walkthrough

The top-3 table tells a thin story. Three good runs, learning rates clustered around 1e-3 to 5e-3 — suggestive. But their dropouts (0.31, 0.36, 0.36) look meaningful too, and dropout does nothing in this sweep's hidden truth. Small samples invent patterns.

The learning-rate marginal is the real finding. The middle bucket averages 0.815 over 64 runs; its neighbours sit at 0.771 and 0.775. Sixty-four runs per average — luck has been averaged down to the third decimal. This grouping is called a marginal: one knob's effect with all other knobs averaged over.

The depth marginal shows a knob that barely matters. A hint of a step at depth 4 (0.773 → 0.794), then noise. Correct conclusion: set depth to 4 and stop spending compute exploring it. Knowing what not to tune is half the value of a sweep.

Log-scale bucketing (np.log10 before pd.cut) is not optional for rates and regularisation strengths. Linear buckets would lump 1e-4 with 1e-2 and erase the pattern.

Beyond marginals: three quick upgrades

  • Top-k instead of top-1: the settings shared by the best 10 runs are far more trustworthy than the best row. Count value frequencies within the top decile.
  • Two-knob tables (pivot_table on lr-bucket × depth) catch interactions marginals hide — a rate that works at depth 2 but not at depth 7.
  • Re-run the winner with fresh seeds before believing it: its sweep score is best-of-200 inflated. Expect it to land lower — that shrinkage is normal.

Common mistakes

Copying the topper's dropout. Any setting can hitch-hike on a lucky run. Only marginals and top-k frequencies separate passengers from drivers.

Uneven bucket counts read as effects. The (-2,-1] bucket holds 79 runs and (-4,-3] holds 57 — random sampling did that. Always print count beside mean; a 5-run bucket's mean is an anecdote.

Sweeping the test set. Two hundred evaluations against one test set is test-set overfitting by industrial means. Sweep against validation data; test once at the end.

Missing runs treated as absent instead of informative. Crashed runs (divergence at high lr) vanish from the table and silently bias marginals — the surviving high-lr runs are the lucky ones. Record failures with a score of NaN and count them.

Try it yourself

Add an interaction to the hidden truth: + 0.04 * ((runs.depth >= 5) & (runs.dropout > 0.3)). Rerun. Confirm the two single-knob marginals barely move, then find the effect with pd.pivot_table(runs, values="score", index="depth", columns=pd.cut(runs.dropout, 3), observed=True).

What to learn next

Researcher — Mathematics and papers.

Why random search makes marginals readable

Bergstra and Bengio (2012), Random Search for Hyper-Parameter Optimization, JMLR: random sampling dominates grid search because performance is low-effective-dimensional — a few knobs matter, most do not — and random designs project informatively onto every axis, while grids waste trials on redundant coordinates of unimportant knobs. The same property is what makes marginal analysis valid here: under independent random sampling, grouping by one knob averages uniformly over the others, so the marginal is an unbiased estimate of that knob's average effect.

Under adaptive search (Bayesian optimisation, PBT), sampling concentrates where the optimiser already believes performance is high; naive marginals inherit that bias and need reweighting or restriction to early exploration phases.

Attribution beyond marginals

  • fANOVA (Hutter, Hoos and Leyton-Brown, 2014, An Efficient Approach for Assessing Hyperparameter Importance, ICML): fit a random-forest surrogate $\hat{S}(\theta)$ over the sweep, decompose its variance into main effects and interactions: $\operatorname{Var}(S) = \sum_j V_j + \sum_{j<k} V_{jk} + \dots$, reporting each knob's variance share. The principled generalisation of the groupby.
  • Expected best over budget (Dodge et al., 2019, Show Your Work, EMNLP): report $\mathbb{E}[\max \text{ of } b \text{ trials}]$ as a function of tuning budget $b$ — makes "our method needs less tuning" a measurable claim and exposes best-of-$n$ inflation directly.

Where:

  • $\theta$ — the hyperparameter vector; $V_j$ — variance attributable to knob $j$ alone.
  • $b$ — number of trials the practitioner can afford.

The inflation of the sweep maximum

The sweep's best observed score exceeds the best configuration's true score by a selection bias analogous to adaptive test reuse: with $n$ trials of noise scale $\sigma$, the winner's excess is $O(\sigma \sqrt{2 \ln n})$. In the demo, $\sigma = 0.02$, $n = 200$ gives $\approx 0.065$ of potential inflation at the top — the top score of 0.8511 against a true ceiling of 0.82 shows the effect live. Fresh-seed re-runs of the winner estimate the unbiased value; shrinkage-aware selection (report top-k mean, not top-1) is the cheap correction.

Early stopping of bad runs

Modern sweep infrastructure rarely runs all trials to completion: successive halving and Hyperband (Li et al., 2018, JMLR) allocate budget by promoting the best fraction at increasing horizons; ASHA (Li et al., 2020, MLSys) does it asynchronously; BOHB (Falkner et al., 2018, ICML) adds a model over $\theta$. Analysis caveat: partially-trained scores enter the record, so sweep tables must carry the budget column, and marginals should condition on comparable budgets — otherwise "low learning rates look bad" may only mean they were culled early.

What to learn next