Skip to content

Why model selection belongs inside validation

Tonight · ~25 min · read + a numerical sketch · energy: medium · setup: none

You tune a model’s hyperparameters and select its features using cross-validation, so your CV estimate already accounts for tuning — right? Not unless the tuning used a different CV than the one estimating performance. This lesson is nested cross-validation: the design that makes a performance estimate honest when the model itself was chosen by search.

The anchor: tuning on the same CV you report is self-grading

Suppose you have a held-out test set, and you try several alpha values for a lasso, each evaluated by 5-fold CV on the training set, then report the best one’s CV score as your performance. That CV score is optimistic because the winning alpha was chosen to do well on that very CV — you graded your own homework. With many hyperparameter combinations, some will look good by chance, and selecting the best amplifies that luck into the reported number.

The fix is to separate the loop that chooses from the loop that scores: nested cross-validation.

Nested CV, in a picture

flowchart TD
    O["Outer fold k<br/>(held-out test partition)"]
    TR["training partition<br/>(4/5 of data)"]
    O -- "outer train" --> TR
    TR --> I["inner CV loop:<br/>try alphas / features,<br/>pick best on inner-CV"]
    I --> FIT["fit final model<br/>with best config<br/>on the whole outer-train"]
    FIT --> EVAL["evaluate once<br/>on outer held-out fold"]
    EVAL --> AVG["average outer scores<br/>= honest performance"]
  • Outer loop — splits the data into K folds. Each outer fold has a held-out test partition.
  • Inner loop — on the outer-training partition only, runs its own CV to select hyperparameters/features.
  • Fit — train the model with the inner-selected config on the whole outer-training partition.
  • Evaluate — score it once on the outer held-out fold. Average the outer scores for the honest estimate.

The inner loop never sees the outer test fold, so the selection it performs cannot leak into the reported performance. The outer estimate is now honest about “how well does this whole model-selection procedure generalise”.

A small numerical sketch

Take 5 outer folds. In each, the inner loop selects the best lasso alpha. Suppose the non-nested (flat) CV reported AUROC 0.88 by picking the best alpha on that same CV. Run nested CV and you will usually see the outer average come in lower — say 0.82 — because the outer folds punish the optimism that flat CV baked in. The gap (0.88 → 0.82) is the selection optimism. It is not a bug in the model; it is the difference between grading your own homework and having someone else grade it.

When nested CV matters most

Nested CV earns its complexity when model selection is expensive in optimism terms: many hyperparameters, aggressive feature selection, small cohorts. Radiomics is the canonical case — hundreds of correlated features, tens of patients, heavy selection. For a single pre-specified model with no tuning, plain CV on a held-out test set is fine. The cost of nested CV is compute (K×L model fits instead of L), but on tabular radiomics that cost is trivial next to the credibility gained.

A practical note: the model you finally deploy is usually retrained on all data with the config the inner loops preferred; nested CV’s job is to estimate generalisation honestly, not to produce the deployed weights. Do not confuse the performance estimate with the deployed model.

TRACE-CT connection

TRACE-CT’s docs/experiment-protocol.md fixes the patient-level split and forbids cohort-fitted steps outside the CV loop — the protocol-level enforcement of “selection inside validation”. The independent-process determinism check (compare_radiomics_runs.py) ensures the numbers feeding any selection are reproducible before the statistics begin, so the inner loop operates on stable inputs.

Stop and think — then reveal

You run flat (non-nested) 5-fold CV to pick the best of 50 lasso alpha/feature-set combinations, and report the winning combination’s CV AUROC of 0.90 as your estimated performance. Why will a held-out external test probably disappoint, and what would have predicted that disappointment?

Because the 0.90 is optimistically biased: among 50 combinations, some looked good on this CV partly by chance, and you picked the best. The external test, which the selection never saw, punctures that optimism. A nested-CV estimate — where selection happens inside inner loops and performance is scored on untouched outer folds — would have come in lower (closer to the external result) and would have warned you. The lesson: flat CV after a search reports the best case; nested CV reports the honest case.

What to retain

  1. Selecting a model on the same CV you report is self-grading; the reported score is optimistically biased.
  2. Nested CV separates the inner loop (selects hyperparameters/features on outer-training only) from the outer loop (scores once on a held-out fold).
  3. The outer average is the honest estimate; the gap between flat and nested CV is the selection optimism.
  4. It matters most for radiomics (heavy selection, small cohorts, p >> n); the deployed model is retrained on all data, but its expected performance is what nested CV estimates.

Next: the regime that makes selection so dangerous in the first place — why p >> n breaks ordinary intuition.