Hyperparameter tuning & training deep models

Most failed deep-learning projects do not fail because the architecture was wrong. They fail because the training was misconfigured, the loss did not match the task, or a silent bug in the loop meant the model never saw what the engineer thought it saw. This page is about that layer: which knobs actually matter and in what order, how to choose optimizers and losses, the specific recipes that work for detection, segmentation, ViTs and CLIP, and a systematic way to diagnose a run that is not converging.

Symbols used on this page are defined in the Nomenclature → deep learning & training.

Model-specific deep dives live in Deep Learning; deployment, quantisation and throughput are in models & tooling.

The single most valuable habit. Before tuning anything, prove the pipeline can overfit a single batch to near-zero loss. If it cannot, no hyperparameter will save you — there is a bug. This one test separates "the model is not learning" from "the model is learning the wrong thing", and it takes thirty seconds. §11 builds it into a full diagnostic ladder.

1 · What to tune, and in what order

Hyperparameters are not equally important, and tuning them in the wrong order wastes most of the effort. The sensitivity ranking below is fairly stable across vision tasks:

PriorityKnobEffect if wrongSweep over
1Learning rate Diverges, or never converges. Dominates everything elselog scale, ×3 or ×10 steps
2LR schedule + warmup Early instability; leaves 1–3 points of accuracy unclaimeda few named options
3Batch size Couples to LR; breaks BatchNorm if too smallpowers of 2, as large as memory allows
4Weight decay Under- or over-regularised; interacts with the optimizer choicelog scale
5Epochs / total steps Underfit or overfit; usually resolved by early stoppingtrain long, checkpoint often
6Augmentation strength The most effective regulariser in vision, and the cheapestpreset recipes
7Optimizer + $\beta$'s Usually second-order; defaults are goodrarely worth sweeping
8Dropout Matters for transformers and heads; often 0 in modern CNNs0 to 0.3
9Architecture width/depth Capacity — only after the above are rightscale families
Grid search is the wrong tool. With a budget of $n$ trials over $k$ parameters, grid search spends that budget evaluating each parameter at only $n^{1/k}$ distinct values — and since sensitivity is wildly uneven, most of the grid is spent varying parameters that do not matter. Random search over the same budget samples every parameter at $n$ distinct values and reliably beats it (Bergstra & Bengio, 2012). Better still, use successive halving / ASHA: start many short runs, kill the worst half repeatedly, and give the survivors the compute. Bayesian optimisation helps when each trial is expensive and the budget is small.

2 · Learning rate

2.1 Why it dominates

For a quadratic loss with curvature eigenvalues in $[\mu, L]$, gradient descent converges only if

$$ \eta \lt \frac{2}{L}, $$

and the convergence rate is governed by the condition number $\kappa = L/\mu$. Two consequences that carry over to real networks: there is a hard divergence threshold set by the sharpest curvature direction, and the useful range sits just below it — which is why the best learning rate is usually within a factor of 2–3 of the one that blows up. That is an uncomfortably narrow target, and it is why LR is knob #1.

The first simulation makes the three regimes concrete. Worth doing: raise the peak LR until the loss panel turns red (that is a NaN in a real log), then add warmup and watch the same peak LR become survivable.

2.2 Finding it: the LR range test

Do not guess. Smith's LR range test finds the usable band in one short run: start at a very small LR, increase it geometrically every iteration over a few hundred steps, and plot loss against LR.

loss │╲ ╱ ← diverging │ ╲__ ╱ │ ╲___ ╱ │ ╲____ ___╱ │ ╲____________╱ ← steepest descent: pick here │ └──────────────────────────────────────── log(LR) too small GOOD BAND too large ↑ ↑ pick ~1/3 of the the minimum of this curve is divergence point ALREADY too large — the loss is about to turn up

Pick a value where the loss is still falling steeply, typically a factor of 3–10 below where it turns upward. A common mistake is to pick the minimum of this curve, which is already past the stable region.

2.3 Warmup

Warmup ramps the LR from ~0 to the peak over the first few hundred to few thousand steps. Three reasons it is needed, all about the first moments of training:

  • Adam's second-moment estimate is unreliable early. With few samples, $\hat v_t$ is noisy, so the effective step $\eta/\sqrt{\hat v_t}$ can be enormous. Warmup covers the period while the estimate settles.
  • Large-batch training needs a large LR (§3), and a large LR applied to a randomly initialised network takes a step far outside the region where the local quadratic model is valid.
  • Transformers are especially sensitive. Post-norm architectures can have very large early gradients through the residual path; skipping warmup on a ViT frequently produces an immediate divergence.

Rules of thumb: 3–5 epochs of warmup for ImageNet-scale classification, 500–2000 steps for transformer fine-tuning, ~3 epochs for YOLO-family detectors (which also warm up the momentum term). If a run dies in the first few hundred steps, warmup is the first thing to check.

2.4 Schedules

ScheduleShapeUse for
Cosine$\eta_t = \tfrac12\eta_0\left(1 + \cos(\pi t/T)\right)$ The default. No step tuning, smooth, strong final convergence. Needs $T$ known in advance
Step decay×0.1 at fixed milestones Classic detection recipes (e.g. ×0.1 at 8 and 11 epochs of a 12-epoch schedule)
Linear decayto zero, or to a floor Transformer fine-tuning; YOLO's default is linear to ~1% of peak
Constant then decayflat, then drop When the run length is not known ahead of time
Cosine with restartssawtooth Escaping poor basins; snapshot ensembling
One-cycleup then down, with inverse momentum Fast training on a fixed small budget
ReduceLROnPlateaureactive When you genuinely cannot predict the schedule. Generally worse than cosine
Decaying to near zero at the end is not cosmetic. A large final LR leaves the weights bouncing in a noise ball whose radius scales with $\eta$; the last few percent of accuracy comes from shrinking that ball. Cutting a cosine schedule short — stopping at 80% of $T$ — therefore gives a worse model than a cosine schedule properly sized to the budget you actually have.

2.5 Layer-wise and parameter-group rates

A single global LR is rarely optimal when part of the network is pretrained. Two standard patterns:

  • Backbone/head split. Fine-tuning gives the pretrained backbone a lower LR (often 10× lower) than the randomly initialised head, so good features are not destroyed while the head is still random.
  • Layer-wise LR decay. Scale the LR by $\alpha^{(\text{depth}-\ell)}$ with $\alpha \approx 0.65$–$0.9$, so early layers move least. This is standard for ViT and BERT fine-tuning and typically worth a point or two of accuracy.

A separate, important grouping: never apply weight decay to normalisation parameters or biases. Decaying a BatchNorm scale toward zero actively fights the network. In PyTorch this means constructing two parameter groups (§8.3).

3 · Batch size, and its coupling to the learning rate

3.1 The scaling rules

A minibatch gradient is an estimator of the full gradient with variance $\propto 1/B$. So the noise scale falls as $1/\sqrt B$, which permits a larger step. Two rules are in common use:

$$ \text{linear: } \eta \propto B \qquad\qquad \text{square-root: } \eta \propto \sqrt B $$
RuleKeeps constantRight forNotes
Linearthe SGD noise scale $\eta/B$SGD with momentum Goyal et al.'s "1 hour ImageNet" result. Requires warmup at large $B$
Square rootthe per-step update magnitudeAdam / AdamW Adam already normalises by the gradient scale, so linear over-steps

Both hold only up to a critical batch size $B_{\text{crit}}$. Below it, doubling the batch nearly halves the number of steps needed — near-perfect scaling. Above it, extra samples per step buy almost nothing, so you pay double the compute for a few percent fewer steps. The simulation shows this directly: the "steps to target" curve flattens while the "samples seen" curve keeps climbing.

3.2 The practical constraints

  • BatchNorm breaks at small batch. Below about 8–16 per GPU the batch statistics are too noisy, and detection/segmentation models running at batch 2 are the classic casualty. Fix by switching to GroupNorm, using SyncBatchNorm across GPUs, or freezing the BN statistics inherited from pretraining (which is what standard Faster R-CNN recipes do).
  • Gradient accumulation simulates a large batch under a memory limit: run $k$ forward/backward passes, then step once. It is equivalent for the optimizer but not for BatchNorm, whose statistics still come from the small physical batch.
  • Small batches regularise. Gradient noise is not purely a cost — it helps escape sharp minima. A very large batch trained to the same loss often generalises slightly worse, which is why extra regularisation or a longer schedule is usually needed at scale.

When changing batch size to fit a new GPU, remember that LR, weight decay's effective strength, warmup length measured in steps, and the total step count all shift. Changing batch size alone and keeping everything else fixed is a very common way to "mysteriously" lose accuracy.

4 · Epochs, early stopping and the data budget

The number of epochs is best thought of as a consequence of the dataset size and the schedule rather than an independent knob. Typical practice:

SettingTypical budgetComment
Fine-tuning a pretrained backbone, small dataset10–50 epochs Overfits quickly; early stopping matters
Detection from pretrained (COCO "1×")12 epochs The standard schedule; "3×" is 36
YOLO from scratch-ish100–300 epochs Heavy augmentation makes long schedules pay
ViT from scratch on ImageNet300 epochs Transformers are data-hungry without strong priors
Contrastive pretraining (CLIP-style)measured in samples seen Epochs are the wrong unit at this scale

Early stopping should be implemented as "train long, checkpoint the best validation score, keep training with patience" rather than "stop the first time validation rises". Validation curves are noisy and frequently rise before falling again, especially around an LR drop. Patience of 10–20 evaluations is typical, and the checkpoint you keep is the best one, not the last.

5 · Regularisation

5.1 Weight decay

Weight decay is the strongest regulariser after augmentation, and its correct implementation depends on the optimizer. With plain SGD, L2 regularisation and weight decay are the same thing. With Adam they are not, because L2 added to the gradient gets divided by $\sqrt{\hat v_t}$ along with everything else — so parameters with large gradient history get less regularisation, which is backwards. AdamW's decoupled decay applies it directly to the weights:

$$ \theta_{t+1} = \theta_t - \eta\,\frac{\hat m_t}{\sqrt{\hat v_t}+\epsilon} \;-\; \eta\,\lambda\,\theta_t . $$
SettingTypical $\lambda$
SGD, CNN classification$1\times10^{-4}$ to $5\times10^{-4}$
AdamW, ViT from scratch$0.05$ to $0.3$ — much larger than people expect
AdamW, fine-tuning$0.01$ to $0.05$
Normalisation params and biases0 — exclude them

5.2 Dropout

Dropout zeroes activations with probability $p$ at training time and rescales by $1/(1-p)$ so the expected activation is preserved. Its role has narrowed considerably:

WhereTypical $p$Notes
Modern CNN backbones0 BatchNorm plus augmentation already regularise; dropout usually hurts
ViT / transformer blocks0.0–0.1Often 0, with stochastic depth used instead
Stochastic depth (DropPath)0.1–0.4, depth-scaled The effective regulariser for deep transformers — drops whole residual branches
Classifier head / MLP0.1–0.5Still useful on small datasets
Before a BatchNorm layeravoid Dropout changes the activation variance, corrupting the BN statistics — a real and under-appreciated conflict

5.3 Augmentation is the main event in vision

Empirically, augmentation buys more generalisation per unit of effort than any other regulariser, because it injects real task knowledge (invariances) rather than generic noise.

TechniqueTaskCaution
Flip, crop, scale jittereverything Horizontal flip is wrong when handedness or text matters
Colour jitter, blur, noiseeverythingKeep within the sensor's real range
Mosaic (4 images tiled)detection YOLO's biggest single win. Disable for the last ~10 epochs — otherwise the model never sees the true image statistics
MixUp / CutMixclassificationEssential for ViT from scratch; mild for CNNs
RandAugment / TrivialAugmentclassificationStrong, and nearly free to adopt
Copy-pasteinstance segmentationVery effective for rare classes
Random erasingclassification, ReIDSimulates occlusion
Augment the training set only, and make sure geometric augmentation transforms the labels too. Flipping an image without flipping its boxes, or rotating without rotating the mask, produces a model that trains to a plausible-looking loss and localises nothing. This is one of the most common silent bugs in detection and segmentation pipelines — and it is caught immediately by rendering a few augmented samples with their labels drawn on before training (§11.1).

5.4 Normalisation layers

The choice of normaliser is effectively a hyperparameter, and it interacts directly with batch size. What separates them is only which slice of the tensor the statistics are computed over — and that single difference explains all of their practical behaviour:

LayerStatistics overBatch dependent?Use for
BatchNorm$(N, H, W)$ per channelyes CNNs at batch ≥ 32. Also speeds convergence considerably
LayerNorm$(C, H, W)$ per sampleno Transformers, always
GroupNorm$(C/G, H, W)$ per sampleno Detection/segmentation at small batch; $G$ = 32 channels per group
InstanceNorm$(H, W)$ per sample+channelno Style transfer; rarely for recognition

BatchNorm's train/eval asymmetry is a frequent source of confusion: at training time it uses the current batch's statistics, at evaluation the running averages. A model that scores well in training mode and poorly in eval mode usually has broken running statistics — too few updates, a momentum set wrong, or model.eval() never called. Conversely, forgetting model.train() after validation leaves BN frozen for the rest of training.

6 · Optimizers

6.1 The three that matter

SGD with momentum accumulates a velocity and steps along it:

$$ \mathbf v_{t+1} = \gamma\,\mathbf v_t + \nabla_\theta \mathcal L, \qquad \theta_{t+1} = \theta_t - \eta\,\mathbf v_{t+1}, \qquad \gamma \approx 0.9 . $$

Adam additionally keeps a per-parameter second moment and normalises by it, making the step size adaptive:

$$ \mathbf m_t = \beta_1 \mathbf m_{t-1} + (1-\beta_1)\,\mathbf g_t, \qquad \mathbf v_t = \beta_2 \mathbf v_{t-1} + (1-\beta_2)\,\mathbf g_t^2, $$ $$ \hat{\mathbf m}_t = \frac{\mathbf m_t}{1-\beta_1^t}, \qquad \hat{\mathbf v}_t = \frac{\mathbf v_t}{1-\beta_2^t}, \qquad \theta_{t+1} = \theta_t - \eta\,\frac{\hat{\mathbf m}_t}{\sqrt{\hat{\mathbf v}_t}+\epsilon} . $$

The bias-correction terms $1-\beta^t$ matter only for the first few hundred steps, but they matter a lot there — without them the early steps are badly scaled, which is part of why warmup helps. AdamW is Adam with the weight decay decoupled from the gradient (§5.1) and is the correct default whenever you would otherwise reach for Adam.

6.2 Choosing

OptimizerTypical LRBest forTrade
SGD + momentum$10^{-2}$ to $10^{-1}$ (batch 256) CNN classification, detection backbones Often the best final accuracy, but needs more LR tuning and more epochs
AdamW$10^{-4}$ to $10^{-3}$ Transformers, ViT, CLIP, anything with LayerNorm Converges faster and more robustly; 2× optimizer state memory
Adam$10^{-4}$ to $10^{-3}$legacy Use AdamW instead — same cost, correct decay
RMSProp$10^{-4}$ to $10^{-3}$some RL, older EfficientNet recipes Largely superseded
LAMB / LARSlayer-wisevery large batch (8k+) Only relevant at extreme scale
Lion, Sophia, Shampoovariesresearch Promising; not yet defaults. Lion needs a ~10× smaller LR than AdamW
The 100× LR gap between SGD and Adam is the most common cause of "the optimizer broke my model". Swapping SGD for AdamW while keeping $\eta = 0.01$ will diverge immediately; swapping AdamW for SGD while keeping $\eta = 3\times10^{-4}$ will appear to do nothing at all. The learning rate is a property of the pair, never of the model alone.

6.3 The parameters that occasionally need attention

ParameterDefaultWhen to change it
$\beta_1$0.9Lower (0.8) if training is unstable; it is the momentum term
$\beta_2$0.999 Lower to 0.95 for large models or noisy gradients — a long second-moment memory reacts too slowly to a genuine change in gradient scale. Standard for large transformer training
$\epsilon$$10^{-8}$ Raise to $10^{-6}$ under mixed precision, where $10^{-8}$ can underflow in fp16 and produce a division by ~0
momentum (SGD)0.90.95–0.99 for very large batch; warm it up alongside the LR
NesterovoffUsually a small free win for SGD; turn it on

6.4 Gradient clipping

Clipping bounds the update when a rare batch produces an enormous gradient:

$$ \mathbf g \leftarrow \mathbf g \cdot \min\!\left(1, \frac{c}{\|\mathbf g\|_2}\right) . $$

Clip by global norm rather than per-element value, so the update direction is preserved. Typical $c$ is 1.0 for transformers and 10 for detection heads. It is cheap insurance: a single bad batch — a corrupt label, an empty ground truth, a degenerate box — otherwise takes one catastrophic step that the run may never recover from. Log the pre-clip gradient norm; a rising trend is an early warning of instability, and frequent clipping means the LR is too high.

7 · Loss functions

7.1 Classification

Cross-entropy is the baseline, $\mathcal L = -\sum_c y_c \log \hat p_c$. Two standard modifications:

Label smoothing replaces the one-hot target with $y_c = 1-\varepsilon$ for the true class and $\varepsilon/(K-1)$ elsewhere, with $\varepsilon \approx 0.1$. This prevents the network from driving logits to infinity to chase a target it can never reach, which improves calibration and slightly improves accuracy. It is standard for ViT and for YOLO classification branches.

Focal loss addresses extreme foreground/background imbalance in dense detection:

$$ \mathcal L_{\text{focal}} = -\alpha_t\left(1 - \hat p_t\right)^{\gamma}\log \hat p_t, \qquad \gamma \approx 2, \ \alpha \approx 0.25 . $$

The factor $(1-\hat p_t)^\gamma$ down-weights already-well-classified examples. With $\gamma = 2$, an example at $\hat p = 0.9$ contributes 100× less than at $\hat p = 0.5$. This matters because a single-stage detector evaluates ~$10^5$ anchors of which a handful are positive — without focal loss the sum of many easy negatives swamps the gradient from the few positives.

7.2 Box regression

LossFormNotes
L1 / L2 on coordinates$|\Delta x| $ etc. Scale-dependent: the same pixel error is penalised equally on a large and a tiny box, which is not what the metric rewards
Smooth L1 / Huberquadratic near 0, linear beyond Classic Faster R-CNN choice; bounded influence from outlier boxes
IoU loss$1 - \text{IoU}$ Optimises the evaluation metric directly, and is scale-invariant. Zero gradient when boxes do not overlap — the defect the rest fix
GIoUIoU minus a penalty for the enclosing box Provides gradient for non-overlapping boxes
DIoU / CIoUadds centre distance, then aspect ratio Converges faster than GIoU; CIoU is the modern YOLO default
Distribution Focal Losspredicts a distribution over each edge Used with CIoU in YOLOv8+; models localisation ambiguity instead of a point estimate

7.3 Segmentation

The central issue is class imbalance: a typical mask is overwhelmingly background, so a per-pixel cross-entropy is minimised well by predicting "background everywhere".

LossBehaviourUse
Pixel cross-entropystable gradients, imbalance-sensitive The baseline; weight the classes by inverse frequency
Dice$1 - \dfrac{2|A\cap B| + \varepsilon}{|A| + |B| + \varepsilon}$ Region-overlap based, so naturally imbalance-robust. Noisy gradients for small objects
BCE + Dicesum of the two The workhorse. BCE stabilises, Dice targets the metric
Tversky / Focal Tverskyweights FP vs FN asymmetrically When missing a positive is worse than a false alarm (medical, safety)
Lovász-Softmaxa direct surrogate for IoUGood as a fine-tuning stage
Boundary / Hausdorffpenalises edge error specifically Adds thin-structure fidelity as an auxiliary term

The $\varepsilon$ in the Dice formula is not decoration — with an empty ground-truth mask the numerator and denominator are both zero, and without smoothing the loss is NaN. Empty masks occur routinely in real datasets, so this is a practical stability issue rather than a theoretical one.

7.4 Contrastive losses (CLIP)

CLIP trains image and text encoders so matched pairs align. The symmetric InfoNCE loss over a batch of $N$ pairs is

$$ \mathcal L = \tfrac12\left( \mathcal L_{\text{i}\to\text{t}} + \mathcal L_{\text{t}\to\text{i}}\right), \qquad \mathcal L_{\text{i}\to\text{t}} = -\frac1N\sum_{i} \log\frac{\exp\!\left(\mathbf z^{\text{i}}_i \cdot \mathbf z^{\text{t}}_i / \tau\right)} {\sum_{j}\exp\!\left(\mathbf z^{\text{i}}_i \cdot \mathbf z^{\text{t}}_j / \tau\right)} . $$

Two details dominate in practice. The temperature $\tau$ controls how sharply the softmax distinguishes the positive from the negatives — CLIP learns it as a parameter but clamps it, because an unconstrained $\tau$ collapses toward zero and destabilises training. And the effective number of negatives is the batch size, which is why contrastive pretraining is run at batch sizes in the tens of thousands and why gathering embeddings across all GPUs before computing the loss is essential rather than optional.

7.5 Multi-task weighting

Detection and instance segmentation losses are sums of heterogeneous terms:

$$ \mathcal L = \lambda_{\text{cls}}\mathcal L_{\text{cls}} + \lambda_{\text{box}}\mathcal L_{\text{box}} + \lambda_{\text{obj}}\mathcal L_{\text{obj}} + \lambda_{\text{mask}}\mathcal L_{\text{mask}} . $$

The $\lambda$'s matter, and their correct values depend on the numeric scale of each term — which changes if you change a loss function. Swapping smooth-L1 for CIoU, for example, changes the box term's magnitude by a large factor, so an untouched $\lambda_{\text{box}}$ silently re-weights the whole objective. Always log the individual loss terms, not just the total: if one term is 100× the others, it is the only thing being optimised. Uncertainty weighting (Kendall et al.) learns the $\lambda$'s by treating each task's noise as a parameter, and GradNorm balances by gradient magnitude; both are more principled than hand-tuning, though hand-tuned published values are usually fine as a starting point.

8 · Per-architecture recipes

These are starting points drawn from the reference implementations and papers, not universal optima. The value of writing them side by side is seeing why they differ — the pattern is that architecture dictates the normaliser, the normaliser and pretraining dictate the optimizer, and the task dictates the loss.

8.1 YOLO (detection)

SettingTypicalWhy
OptimizerSGD momentum 0.937, or AdamWSGD for long schedules; AdamW converges faster on small data
LR0.01 (SGD) / 0.001 (AdamW), linear decay to ~1%Per batch 64; scale with batch
Warmup~3 epochs, momentum warmed tooMosaic makes early batches very heterogeneous
Batch16–64 per GPULarge enough for BatchNorm
Epochs100–300Heavy augmentation needs a long schedule to pay off
Weight decay$5\times10^{-4}$Exclude BN and bias
LossCIoU + DFL (box), BCE (cls/obj)$\lambda$ ≈ 7.5 box, 0.5 cls, 1.5 DFL in v8
AugmentationMosaic, scale jitter, HSV, flip Close mosaic for the last ~10 epochs so the model adapts to real image statistics
EMA of weightsonConsistently worth ~0.5 mAP; nearly free

8.2 YOLO segmentation and Mask R-CNN

Instance segmentation adds a mask branch, and the practical differences from detection are narrow but important:

  • An extra loss term — per-instance mask BCE (Mask R-CNN) or a mask-coefficient formulation (YOLO-seg). Watch its weight relative to the box and class terms (§7.5).
  • Mask R-CNN runs at batch 2 per GPU in the standard recipe, which is why it uses frozen BatchNorm or GroupNorm. Leaving BN trainable at batch 2 is a reliable way to get a model that trains but does not generalise.
  • Mask R-CNN: SGD, LR 0.02 at batch 16, ×0.1 at epochs 8 and 11 of a 12-epoch ("1×") schedule, weight decay $10^{-4}$. These numbers are so standard that departures from them should be deliberate.
  • Mask resolution matters more than mask loss weight — the 28×28 RoI mask is the accuracy ceiling for fine boundaries, and no amount of loss tuning changes that.
  • Copy-paste augmentation is unusually effective for rare classes here.

8.3 Faster R-CNN

SettingTypical
Optimizer / LRSGD 0.9, LR 0.02 at batch 16, step ×0.1 at 8 and 11 epochs
NormalisationFrozen BN in the backbone; GroupNorm if training from scratch
LossesRPN: BCE objectness + smooth-L1. Head: cross-entropy + smooth-L1 (or GIoU)
AnchorsScales and aspect ratios must match your object statistics — check this
Sampling256 RPN anchors at 1:1 pos/neg; 512 RoIs at 1:3
ClippingGlobal norm ~10; RPN gradients can spike on crowded images

Anchor mismatch is the most under-diagnosed failure in two-stage detection. If your objects are mostly tiny and the default anchors start at 32 px, recall is capped by the anchor grid no matter how long you train. Cluster your ground-truth box dimensions and compare against the anchor set before touching anything else.

8.4 ViT

SettingFrom scratchFine-tuning
OptimizerAdamWAdamW, or SGD momentum
LR$10^{-3}$ at batch 1024, cosine$10^{-5}$ to $10^{-4}$
WarmupEssential — 5–20 epochs~500 steps
Weight decay0.05–0.30.01–0.05
AugmentationRandAugment + MixUp + CutMix + random erasinglight
Stochastic depth0.1–0.4, scaled with depth0.1
Label smoothing0.10.1
Layer-wise LR decay—0.65–0.9
Gradient clipping1.01.0
A ViT trained like a ResNet will look like a failed architecture. Transformers have no convolutional inductive bias, so they rely on data volume and strong augmentation to learn locality. The original ViT paper's central finding was exactly this: worse than ResNets on ImageNet-1k alone, better once pretrained on far more data. If a from-scratch ViT underperforms on a small dataset, the model is not at fault — either pretrain, or use a hybrid/convolutional stem, or pick a CNN.

8.5 CLIP-style contrastive training

  • Batch size is a loss hyperparameter, not just a throughput choice — it sets the number of negatives (§7.4). Gather embeddings across all ranks before computing the loss.
  • AdamW with $\beta_2 = 0.98$, weight decay 0.2, cosine schedule with warmup.
  • Learn the temperature but clamp it (CLIP caps the logit scale at 100).
  • Mixed precision is near-mandatory at this scale, but keep the loss computation and the softmax in fp32 — a large logit matrix in fp16 overflows.
  • For fine-tuning CLIP, note that naive full fine-tuning degrades its robustness; interpolating the fine-tuned and original weights (WiSE-FT) recovers much of it.

8.6 Fast / real-time segmentation

Efficient segmentation architectures (FastSeg-style, BiSeNet, DDRNet and similar) share a set of training conventions:

  • Auxiliary losses on intermediate outputs, weighted ~0.4, to get gradient into early layers of a deep encoder. Removing them typically costs a point of mIoU.
  • Online hard example mining or top-$k$ pixel cross-entropy — most pixels are easy, and averaging over all of them dilutes the signal from boundaries.
  • Poly LR schedule $\eta_t = \eta_0(1 - t/T)^{0.9}$ is the segmentation convention, and works about as well as cosine.
  • Large crops with scale jitter (0.5–2.0×); SyncBatchNorm across GPUs because batch-per-GPU is small.
  • Evaluate at the native resolution, and be explicit about whether reported mIoU uses sliding-window or whole-image inference — the two differ by enough to invalidate comparisons.

9 · Getting the PyTorch loop right

9.1 A correct baseline loop

model = Model().to(device) # two parameter groups: no weight decay on norms and biases decay, no_decay = [], [] for name, prm in model.named_parameters(): if not prm.requires_grad: continue if prm.ndim <= 1 or name.endswith(".bias"): no_decay.append(prm) else: decay.append(prm) opt = torch.optim.AdamW([{"params": decay, "weight_decay": 0.05}, {"params": no_decay, "weight_decay": 0.0}], lr=1e-3) sched = torch.optim.lr_scheduler.OneCycleLR(opt, max_lr=1e-3, total_steps=total) scaler = torch.amp.GradScaler("cuda") for epoch in range(epochs): model.train() # ← BN/dropout into train mode for x, y in train_loader: x, y = x.to(device, non_blocking=True), y.to(device, non_blocking=True) opt.zero_grad(set_to_none=True) # ← every step, before backward with torch.amp.autocast("cuda", dtype=torch.bfloat16): loss = criterion(model(x), y) # ← loss INSIDE autocast scaler.scale(loss).backward() scaler.unscale_(opt) # ← unscale BEFORE clipping torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) scaler.step(opt) scaler.update() sched.step() # ← per STEP for OneCycle/cosine model.eval() # ← BN uses running stats now with torch.no_grad(): validate(model, val_loader)

Every line flagged above is a real bug someone has shipped. The ordering of unscale_ before clipping is the subtlest: clipping a scaled gradient clips to the wrong threshold, so the clipping silently does nothing useful.

9.2 The correctness checklist

MistakeSymptomFix
Missing zero_grad()Gradients accumulate across steps; effective LR grows without bound and the loss explodes after a few iterations Call it every step, with set_to_none=True
Missing model.train() / model.eval() Train and val metrics inconsistent; BN running stats corrupted Set the mode explicitly at both boundaries
Softmax applied before CrossEntropyLoss Trains, but poorly — the loss expects raw logits Pass logits; the loss does log-softmax internally
Sigmoid before BCEWithLogitsLossSame class of error Pass logits — the fused version is also numerically stabler
Accumulating loss instead of loss.item() Memory grows every step until OOM — the whole graph is retained .item() or .detach()
scheduler.step() per epoch for a per-step schedule LR barely moves; cosine never completesMatch the granularity to the scheduler
Normalisation constants mismatched between train and inference Good training metrics, poor deployed accuracy Share one preprocessing definition between both paths
Validation transforms include random augmentation Noisy, pessimistic validation; unstable early stopping Deterministic val transforms only
Data leakage between splitsExcellent validation, poor real performance Split by group — patient, scene, video, session — not by frame
num_workers=0GPU sits idle; the run is data-bound Raise workers, use pin_memory, prefetch

9.3 Mixed precision

Mixed precision roughly doubles throughput and halves activation memory, and there is rarely a reason not to use it. The two variants behave differently:

fp16bf16
Exponent rangenarrow — overflows and underflowssame as fp32
Mantissa10 bits7 bits — less precise
Needs a GradScaleryesno
HardwareVolta onwardAmpere onward
Recommendationuse if that is what you haveprefer it — far fewer NaNs

Keep in fp32 regardless: loss reduction over a large batch, softmax over many classes or a large contrastive logit matrix, and any accumulation of a long sum. Also raise Adam's $\epsilon$ to $10^{-6}$ (§6.3). If loss becomes NaN only under AMP, the cause is almost always an overflow in one of those places rather than a genuine modelling problem.

9.4 Reproducibility

Full determinism costs some speed but makes debugging tractable — without it you cannot tell a real regression from run-to-run variance:

torch.manual_seed(s); np.random.seed(s); random.seed(s) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False # benchmark=True is faster, nondeterministic torch.use_deterministic_algorithms(True) # raises on nondeterministic ops # DataLoader: seed each worker, and set generator= for the sampler

A pragmatic middle ground for real projects: seed everything and accept cuDNN nondeterminism, but always report the mean and spread over at least 3 seeds. Single-run comparisons of two configurations differing by less than a point are not evidence of anything — seed variance on a detection task is routinely a few tenths of mAP.

9.5 Multi-GPU

  • Use DistributedDataParallel, not DataParallel. The latter is single-process, unbalanced and slow.
  • The effective batch is per-GPU batch × world size, so the LR scaling rule (§3.1) applies to the product. Forgetting this when moving from 1 to 8 GPUs is a classic accuracy regression.
  • Use SyncBatchNorm when the per-GPU batch is small, or GroupNorm to sidestep the issue.
  • Call sampler.set_epoch(epoch) or every rank sees the same order every epoch.
  • Log and checkpoint from rank 0 only; validate on all ranks and reduce, or the metric is computed on a fraction of the data.
  • For contrastive losses, gather across ranks (§8.5) — otherwise the effective negative count is the per-GPU batch, not the global one.

10 · Underfitting, overfitting and reading the curves

10.1 The diagnosis

The shape of the train and validation curves, not their absolute values, tells you which problem you have — and the two call for opposite responses, which is why misdiagnosing costs so much time.

PatternDiagnosisDo this
Train high, val high, tracking togetherUnderfitting (bias-limited) Train longer, raise LR, reduce regularisation and augmentation, increase capacity, check for a bug capping the loss
Train low, val high and risingOverfitting (variance-limited) More data, stronger augmentation, more weight decay, early stopping, smaller model
Both low and closeHealthyScale up capacity or data
Train falls, val flat from the startVal set problem or leakage-free but distribution-shifted Check val preprocessing, labels and class distribution
Val better than trainUsually not a miracle Dropout/augmentation active in train only (normal, mild), or an easier val split, or leakage
Both oscillate wildlyLR too high, or batch too small Lower LR, raise batch, add clipping
Loss flat from step 0Bug, not a hyperparameterGo to §11
Underfitting is the more common failure in practice, and the more often misdiagnosed. The instinct on seeing poor validation accuracy is to add regularisation — but if the training loss is also high, regularisation makes it strictly worse. Always read the training loss first: it tells you whether the model is capable of fitting the data at all, and only then is the gap meaningful.

10.2 Overfitting remedies, in order of effectiveness

  1. More data. Nothing else comes close. Often cheaper than it sounds via targeted collection of the failure cases rather than uniform collection.
  2. Stronger augmentation. The closest substitute for more data, because it encodes real invariances.
  3. Transfer learning. A pretrained backbone is regularisation in the form of a prior.
  4. Weight decay, then early stopping.
  5. Stochastic depth / dropout in the head.
  6. Smaller model — last, because it also lowers the ceiling.
  7. Label smoothing, EMA of weights, ensembling — each a small, reliable gain.

11 · When training will not converge

11.1 The diagnostic ladder

Work through these in order. Each rung isolates one class of cause, and the order is chosen so that the cheapest and most common causes are eliminated first.

1. LOOK AT THE DATA. Render 20 training samples with their labels drawn on, after augmentation, exactly as the model sees them. → catches: wrong label mapping, un-transformed boxes/masks, channel order (BGR vs RGB), broken normalisation, off-by-one class indices. This finds more bugs than every other step combined. 2. OVERFIT ONE BATCH. Take 8 samples, disable augmentation, shuffle off, and train until loss ≈ 0. → if it CANNOT: there is a bug (wrong loss reduction, detached graph, frozen parameters, labels not matching inputs, LR ~ 0). → if it CAN: the pipeline is wired correctly. Proceed. 3. CHECK THE INITIAL LOSS against theory. → classification with K classes should start near ln(K): K=10 → 2.30, K=80 → 4.38, K=1000 → 6.91. → far above: bad init or exploding logits. Far below: label leakage into the input, or the loss is not what you think it is. 4. SWEEP THE LEARNING RATE over 4-5 orders of magnitude (§2.2). → flat loss at every LR points back to a bug, not to tuning. 5. CHECK GRADIENT FLOW. Log per-layer gradient norms. → all zero: disconnected graph, requires_grad=False, or a dead ReLU wall. → zero only early: vanishing gradient; check init and normalisation. → huge: add clipping, lower LR, look for a bad sample. 6. TURN OFF THE EXTRAS. Disable AMP, augmentation, EMA, schedulers, DDP — then re-enable one at a time. → isolates which component introduced the instability. 7. SCALE UP. Full data, then full augmentation, then the real schedule.

11.2 Instability and NaN

SymptomLikely causeFix
Loss → NaN in the first few hundred stepsLR too high; no warmup Add warmup, lower the peak LR, clip gradients
NaN only with AMPfp16 overflow in the loss, softmax or a sum Use bf16; keep the loss in fp32; raise Adam $\epsilon$
NaN after many healthy epochsA specific bad sample, or a degenerate case Log the batch index; guard $\log(0)$, $\sqrt{0}$, division by a zero area, empty masks
Loss spikes then recoversRare hard batches Normal with clipping. If frequent, lower the LR
Loss spikes and never recoversOne catastrophic step destroyed the weights Clipping; resume from the last good checkpoint
Loss plateaus high, immediatelyDead ReLUs, bad init, or a bug §11.1 rung 2; try a smaller init or a different activation
Loss decreases then rises steadilyOverfitting, or LR too high late in the run Check the val curve to distinguish; verify the schedule is stepping
Oscillation with a period of one epochData ordering, or an unshuffled loader Shuffle; call set_epoch under DDP
Different result every runNondeterminism Seed; report over 3+ seeds (§9.4)

Two guards worth adding permanently to any long training job: assert that the loss is finite every step and dump the offending batch if not; and keep a rolling checkpoint of the last few epochs so a late divergence does not cost the whole run. Both are a few lines and have saved a great many GPU-days.

11.3 What to log

A run you cannot diagnose afterwards is a run you will have to repeat. The minimum useful set:

  • Each loss term separately, not just the total (§7.5).
  • Learning rate per step — this catches schedule bugs instantly, and they are common.
  • Gradient norm before clipping, and how often clipping fires.
  • Validation metrics on the metric you actually care about, not only the loss. Loss and mAP do not always move together.
  • Throughput (samples/s) and GPU utilisation — a data-bound run wastes most of its budget.
  • The full config and the git commit, attached to the run.
  • Sample predictions as images, periodically. Numbers hide failure modes that a picture makes obvious.

12 · Search strategy on a budget

MethodCostUse when
Start from a published recipefree Always. Reproduce a known result on your data first, then change one thing at a time
Manual, one knob at a timelowFew parameters; strong priors
Random searchparallelThe sensible default for 3+ parameters
ASHA / successive halvingvery efficient Many configurations, and bad ones are identifiable early. Best value per GPU-hour
Bayesian optimisation (TPE, GP)sequentialExpensive trials, small budget
Population-based traininghighLong runs where schedules themselves should adapt
Grid searchwastefulOnly for 1–2 parameters with genuinely discrete values

Two practical points. Tune on a proxy task first — a subset of the data, a smaller model, a shorter schedule — because most hyperparameter rankings transfer even when absolute values do not; then confirm the top few configurations at full scale. And hold out a genuine test set that is never used for selection: tuning hundreds of configurations against a validation set overfits to that set, and the reported number stops being an estimate of generalisation.

13 · Further reading

  • Google Research, Deep Learning Tuning Playbook (Godbole, Dahl, Gilmer, Shallue, Nado). The best single practical document on this subject — trial budgets, what to tune in what order, and the batch-size/LR relationship treated carefully.
  • Karpathy, "A Recipe for Training Neural Networks." The origin of the overfit-one-batch discipline and the incremental-complexity approach in §11.1.
  • PyTorch documentation — the AMP recipe, torch.optim notes, reproducibility page, and the DDP tutorial. The AMP page in particular documents the unscale_-before-clip ordering.
  • Bergstra & Bengio (2012), "Random Search for Hyper-Parameter Optimization," JMLR 13. Why grid search wastes the budget.
  • Smith (2017), "Cyclical Learning Rates for Training Neural Networks" — the LR range test; and Smith & Topin (2019) on one-cycle / super-convergence.
  • Goyal et al. (2017), "Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour." Linear LR scaling and why warmup is required.
  • Shallue et al. (2019), "Measuring the Effects of Data Parallelism on Neural Network Training," JMLR. The critical-batch-size result behind §3.1.
  • Loshchilov & Hutter (2019), "Decoupled Weight Decay Regularization" (AdamW); and (2017) "SGDR: Stochastic Gradient Descent with Warm Restarts" (cosine schedules).
  • Lin et al. (2017), "Focal Loss for Dense Object Detection."
  • Zheng et al. (2020), "Distance-IoU Loss," and Rezatofighi et al. (2019) on GIoU.
  • Wu & He (2018), "Group Normalization." The clearest analysis of why BatchNorm fails at small batch.
  • Ioffe & Szegedy (2015) (BatchNorm) and Ba, Kiros & Hinton (2016) (LayerNorm).
  • Steiner et al. (2021), "How to train your ViT? Data, Augmentation, and Regularization in Vision Transformers." A large, careful study of exactly the trade-offs in §8.4.
  • Radford et al. (2021), "Learning Transferable Visual Models From Natural Language Supervision" (CLIP) — see the appendix for the training configuration.
  • Li et al. (2020), "A System for Massively Parallel Hyperparameter Tuning" (ASHA).
  • Ultralytics and Detectron2 configuration defaults — worth reading as documentation of what a well-tuned recipe actually contains.

Related pages: Deep Learning · mAP, recall & MOTA · Model port & testing · Perception stack