Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

By week six of a project you will have trained dozens of model variants. Without a record, “the good run” becomes a rumor: you remember that depth 8 worked, but not with which learning rate, on which data version, or whether the 0.83 you remember was validation or test. Experiment tracking is the discipline of writing four things down for every run, automatically:

  1. Parameters — every knob: hyperparameters, data options, seed.
  2. Metrics — what you measured, on which split.
  3. Artifacts — what the run produced: model file, figures, predictions.
  4. Code version — the git commit, so the run can be tied to exact code.

The discipline matters more, not less, when an agent runs the experiments for you. An agent that reports “depth 8 was best” is making a claim; the run records are how you check it. In this notebook we build a complete tracker in about 30 lines of Python, use it on a real mini-study, and then look at what the industrial tools (MLflow, Weights & Biases) add on top of the same ideas. No new dependencies.

🖥️ Lecture slides — Session 27 (Fri Dec 4)

A minimal tracker

One run = one JSON file in ./runs/. That is the whole design. JSON because it is human-readable and diff-able; one file per run because runs then never overwrite each other.

That is the entire tracker. log_run is called once at the end of each training run; load_runs turns the directory into a table for analysis. Everything else in this notebook is using it.

A real mini-study: lithology classification

The study: classify lithology from the synthetic geochemistry table of Chapter 2 (mlgeo_synth.geochem_table, with 5% label noise to keep it honest), using scikit-learn’s HistGradientBoostingClassifier. We sweep a small grid — three learning rates by two tree depths, six runs — and track every run.

train 3600, val 1200, test 1200 (untouched)
logged run fcb706c6: lr=0.03, depth=2
logged run f43a82e5: lr=0.03, depth=8
logged run cdac035e: lr=0.1, depth=2
logged run f069162f: lr=0.1, depth=8
logged run d1ef7328: lr=0.5, depth=2
logged run 30e360f5: lr=0.5, depth=8

Load, compare, decide

Six JSON files are now sitting in ./runs/. The payoff: comparison is a DataFrame operation, not archaeology.

Loading...
validation macro-F1:
Loading...
<Figure size 600x400 with 1 Axes>
winner on the VALIDATION split: run 30e360f5 (lr=0.5, depth=8, val F1=0.953)

Read the plot before moving on. The dashed training curves climb toward 1.0 as learning rate and depth grow, while the solid validation curves stay flat near 0.95 — the deep, aggressive models are memorizing the 5% label noise, and it buys them nothing on validation. With noisy labels, a training F1 of 1.000 is a warning, not an achievement. The only configuration that clearly loses is the most aggressive shallow one (learning rate 0.5, depth 2), which underfits the class structure while overreacting to noise.

Notice also how small the margins are: the top four configurations sit within a few thousandths of each other, and the “winner” leads by about 0.002. Hold that thought for the exercise.

Two rules we followed, both from the fair-evaluation thread of Chapters 3 and 4:

  • The winner is chosen on the validation split. The test split has not been touched. If we now reported the winner’s validation score as the headline result, it would be optimistically biased — we picked the maximum of six noisy numbers. The test split gets spent exactly once, at the end of the project.
  • Every number in the table traces to a run file with its parameters, code version, and timestamp. When a collaborator (or an agent, or reviewer 2) asks “how do you know which run was best?”, the answer is a file, not a memory.

What MLflow and Weights & Biases add

Our 30-line tracker records params, metrics, artifacts, and code version. That is also, exactly, the data model of the industrial tools — MLflow (open source, self-hosted or managed) and Weights & Biases (hosted service, free academic tier). What they add is engineering around the same four records:

  • A UI. Sortable run tables, parallel-coordinates plots, live-updating loss curves during training — our load_runs() and matplotlib, but instant and shareable.
  • Artifact stores. We logged artifact paths; they store the artifacts themselves (model weights, figures, datasets) with versioning and deduplication, so “the model from run 7f3a” is downloadable years later.
  • Collaboration. A team shares one tracking server; every group member’s runs land in the same searchable place, with access control. This is the difference that matters at lab scale.
  • Integrations. Autologging callbacks for scikit-learn, PyTorch, and friends record params and metrics without explicit log_run calls, plus hyperparameter-sweep orchestration.

The concepts are identical, and that is the point of having built the tracker yourself: when you adopt MLflow for your final project (encouraged), you know precisely what it is doing and what a runs/ directory of JSON would have given you for free. Use the real tools when you work with other people; never mistake them for the discipline itself.

Exercise: run-to-run variance

Your tracked study has one weakness: every run used seed=0. So “depth 8 beats depth 2 by 0.01” might be a fact, or might be luck of the initialization and subsampling.

Task. Using run_experiment, add tracked runs for the winning configuration with at least three different seeds. Load all runs, and quantify the run-to-run variance: mean, standard deviation, and range of val_f1_macro across seeds. Then answer: which differences in your six-run table are larger than the seed-to-seed spread — and therefore real?