Ultralytics YOLO27: Join the waitlist.
Get Started
Build with Ultralytics Platform·Train and Evaluate·Lesson 6/10
Lessonintermediate

Compare Runs and Pick a Winner

Compare runs in a Platform project, record why each run exists, and decide whether a candidate ships with a scripted promotion gate.

Bigger models usually win on the headline metric and then lose on one class nobody checked. Without a rule decided in advance, the run that ships is whichever one somebody liked best. This lesson records every run so its reasoning can be found later, and turns 'is it better?' into a gate script that returns PASS or FAIL.

Outcome

You can compare runs in a Platform project, document each one, and run a promotion gate that checks the overall metric, every class, and latency before a candidate ships.

Fast Track
If you already know your way around, here's the short version.
  1. Keep related runs in one project; use Table view with Diff to see what changed.

  2. Name runs <dataset-version>_<change> and save hypothesis and result as Custom Metadata.

  3. Declare the gates before looking: beat noise on mAP50-95, keep 90% of every class's AP, fit the latency budget.

  4. Confirm finalists on held-out data with the gate script; use the test split once, at the end.

Hands-on

Compare Runs in a Project#

Ultralytics Platform logistics project in Table view, with models listed in a sidebar and their training arguments and final metrics side by side

A project lists its models with checkboxes. Checked models overlay on the Charts dashboard; Table view lines up training arguments and final metrics, and Diff hides identical columns so you see at a glance that two runs differ only in imgsz. Comparison works within one project, so keep related experiments together (models can be cut and pasted between projects). When reading curves, compare the final value, how fast each run got there, and how smoothly.

Ultralytics Platform logistics project Charts view with overlaid precision and recall curves for several models and the Ask AI panel comparing completed models in a table

With two or more completed models, Compare with AI asks Ask AI to summarize differences and propose the next experiment. Treat its proposal as a hypothesis for your next run, not a verdict.

Make Every Run Explainable#

Names carry the meaning: run_5 says nothing, v3_pallets_added and v4_imgsz1024 say which dataset version and what changed. For the reasoning, open More actions > Information on the model page and save a JSON note as Custom Metadata:

{
  "hypothesis": "Pallet AP is low because val holds only 7 pallets",
  "change": "Dataset v3: +80 pallet images from cam03 and cam07; fine-tuned from v2",
  "result": "Pallet AP 0.31 -> 0.58, overall mAP50-95 0.51 -> 0.59",
  "next": "Forklift recall drops at night; collect dawn and dusk footage"
}

Platform records the rest: the Run Information card shows GPU, hourly rate, total cost, ultralytics version, host, parent model, pinned dataset version, Git commit when available, and a copyable yolo train command. Comparisons are only fair between runs on the same dataset version, which is why every run should have one pinned.

Choose the Metric and Gates Before You Look#

Ultralytics Platform model Validation view with Precision-Recall, F1-Confidence, Precision-Confidence, and Recall-Confidence curves, one line per class

MetricWhen it's the right one
mAP50-95Default for detect, segment, pose, and OBB: rewards tight boxes, stable enough to compare
mAP50Large, well-separated objects where box tightness matters little
Per-class AP for one classOne class drives the business value
Recall at fixed precisionSafety and alarm systems
Inference latency on the targetAlways, as a hard limit

The YOLO performance metrics guide explains each. A candidate ships only if it clears every gate:

Worked Example: A Promotion Gate Script#

Download the finalists' best.pt files and score them on identical data with the same code. This example uses two official checkpoints so it runs anywhere:

from ultralytics import YOLO

DATA = "coco8.yaml"  # identical held-out data for both runs; use your test split for the final call
BASELINE, CANDIDATE = "yolo26n.pt", "yolo26s.pt"  # e.g. best.pt from two Platform runs
NOISE, CLASS_FLOOR, LATENCY_BUDGET_MS = 0.005, 0.90, 100.0

def evaluate(weights):
    m = YOLO(weights).val(data=DATA, plots=False, verbose=False)
    per_class = {m.names[int(c)]: ap for c, ap in zip(m.box.ap_class_index, m.box.ap)}
    return m.box.map, per_class, m.speed["inference"]

(base_map, base_ap, _), (cand_map, cand_ap, cand_ms) = evaluate(BASELINE), evaluate(CANDIDATE)
weak = [n for n, ap in base_ap.items() if cand_ap.get(n, 0) < CLASS_FLOOR * ap]
gates = {
    f"mAP50-95 {base_map:.3f} -> {cand_map:.3f} beats noise": cand_map - base_map > NOISE,
    f"every class keeps {CLASS_FLOOR:.0%} of its AP (weak: {', '.join(weak) or 'none'})": not weak,
    f"inference {cand_ms:.1f} ms within {LATENCY_BUDGET_MS:.0f} ms": cand_ms <= LATENCY_BUDGET_MS,
}
for name, ok in gates.items():
    print("PASS" if ok else "FAIL", name)
print("verdict:", "ship the candidate" if all(gates.values()) else "keep the baseline, run more experiments")
raise SystemExit(0 if all(gates.values()) else 1)  # non-zero exit blocks a CI release step
PASS mAP50-95 0.669 -> 0.693 beats noise
FAIL every class keeps 90% of its AP (weak: elephant, potted plant)
PASS inference 44.1 ms within 100 ms
verdict: keep the baseline, run more experiments

What happened: the larger model wins overall by 2.4 points, but two classes lose more than 10% of their AP, so it fails. That's the regression a headline comparison hides. The per-class dictionary comes from ap_class_index and ap, which cover only classes present in the data; latency is measured on the machine running the script, so run the gate on hardware like your deployment target. COCO8 has just 8 validation images, far too few for a real decision. Use a held-out set of at least a few hundred images, and pass split="test" exactly once, for the final call.

Dig Into the Weak Images#

For detection runs trained with a saved dataset version, the Validation view adds Validation insights: the worst- and best-performing images by per-image F1 drawn with ground truth, a characteristic explorer correlating F1 with image size, aspect ratio, and instance count, and the classes most associated with failures. Find similar on the worst images adds look-alikes to the training set as unlabeled images, turning a failed gate into a concrete data task.

Pitfalls#

The test split became a tuning set

Symptom: the reported test mAP is great, and production is worse. Fix: tune on val. If you compared ten runs on test, it's been tuned on and its number is optimistic. Score finalists on test once, at the very end.

Comparing runs on different dataset versions

Symptom: a run 'wins' by 4 points, and nobody can reproduce why. Fix: check the pinned dataset version in each run's Run Information card. If they differ, rerun the baseline on the new version before comparing.

Treating noise as a win

Symptom: the winner changes every time you retrain with a new seed. Fix: differences under about 0.5 mAP points are run-to-run noise on most datasets. Keep the noise threshold in the gate, and use a larger holdout when results are this close.

Try It

Train two runs that differ in exactly one setting (image size, model size, or dataset version), confirm in Table view with Diff that only that argument differs, and save a Custom Metadata note on each. Then run gate.py with your two best.pt files, your held-out data, and a latency budget for your target, and record the verdict.

Done When
You've finished the lesson when all of these are true.
  • Both runs sit in one project, and Diff shows a single changed argument.

  • Each run has a Custom Metadata note with hypothesis, change, and result.

  • gate.py has printed PASS or FAIL for all three gates, and you've written down what you'll do next.

Show solution

Worked answer for the pill model: baseline v1-baseline (10 epochs) against candidate v2-finetune (5 more epochs, SGD, lr0 0.001). Set DATA = "medical-pills.yaml" and the two weights in gate.py. Output: PASS mAP50-95 0.659 -> 0.687 beats noise, PASS every class keeps 90% of its AP (weak: none), PASS inference 4.0 ms within 100 ms, verdict 'ship the candidate'. Custom Metadata on v2: hypothesis 'v1 was still improving at epoch 10', change 'fine-tune 5 epochs, SGD lr0 0.001', result 'mAP50-95 0.659 -> 0.687'. Caveat to note in the metadata: medical-pills val also guided training, so confirm on a separate holdout before production.

What's next

You have a winner. Next you'll put it behind a dedicated endpoint and call it from code that survives cold starts.