Debugging Training Runs
Training failure is a symptom, not a diagnosis. Begin with the smallest deterministic batch that should overfit, verify shapes and labels, then add data and complexity while logging enough evidence to distinguish optimization, generalization, and evaluation failures.
Prerequisites
Complete Batch and Layer Normalization and the earlier lessons on losses, optimizers, and regularization.
Learning goals
- separate data, forward-pass, gradient, optimizer, and evaluation checks.
- recognize overfitting, exploding gradients, and leakage from observable evidence.
- use anomaly detection and clipping as focused diagnostic tools, not blanket cures.
Worked example
A compact run log can expose multiple hypotheses. Falling training loss with rising validation loss is an overfitting signal, not proof of its cause. A rapidly growing gradient norm suggests an unstable update path. Fitting preprocessing on all splits is direct leakage.
import torch
history = [
{"train_loss": 0.72, "validation_loss": 0.70, "gradient_norm": 1.2},
{"train_loss": 0.31, "validation_loss": 0.44, "gradient_norm": 3.8},
{"train_loss": 0.08, "validation_loss": 0.91, "gradient_norm": 180.0},
]
overfit_signal = (
history[-1]["train_loss"] < history[-2]["train_loss"]
and history[-1]["validation_loss"] > history[-2]["validation_loss"]
)
exploding_gradient = history[-1]["gradient_norm"] > 100
train = torch.tensor([0.0, 2.0])
test = torch.tensor([10.0])
train_mean = train.mean()
leaked_mean = torch.cat([train, test]).mean()
print(f"overfit_signal={overfit_signal} exploding_gradient={exploding_gradient}")
print(f"train_mean={train_mean.item():.1f} leaked_mean={leaked_mean.item():.1f}")
Output:
overfit_signal=True exploding_gradient=True
train_mean=1.0 leaked_mean=4.0
Use a layered checklist:
- Data and split: inspect raw rows, target prevalence, duplicates, entity/time boundaries, missing values, and train-only preprocessing. Never use test behavior to tune the run.
- Shapes and values: assert batch/output/target shapes, dtypes, devices, finite inputs, finite logits, and the loss contract.
- Tiny-batch test: with stochastic regularizers disabled, verify that a sufficiently expressive model can drive loss down on a few examples. Failure points toward wiring or optimization; success does not prove generalization.
- Gradient path: log norms by layer,
Nonegradients, dead/saturated activation fractions, and finite values before clipping. - Update: compare parameter values before and after
step(), confirm gradient clearing, and lower the learning rate as a controlled experiment. - Evaluation: pair
model.eval()withtorch.inference_mode(), preserve the untouched test set, and compare like-for-like reductions and metrics.
torch.autograd.detect_anomaly() can identify a backward operation that produces a non-finite value, but it adds overhead and should be scoped to diagnosis. clip_grad_norm_ can keep one run finite; log the unclipped norm and investigate initialization, scale, architecture, data, and learning rate rather than hiding persistent instability.
Common mistakes
- Changing data, architecture, optimizer, and learning rate simultaneously.
- Looking only at the final scalar loss and not the first failing layer or batch.
- Using validation/test rows to compute normalization statistics.
- Confusing
eval()with disabled gradient recording; the two controls are orthogonal. - Treating one successful seed or one tiny-batch overfit as evidence of robust learning.
Practice
Add a finite check for every numeric field in the sample history. Then write the first three experiments you would run if training loss stayed flat, and a different first three if training loss fell while validation loss rose.
Next steps
Apply the complete workflow in Project: MLP from Data to Evaluation.
Primary sources
Checked 2026-10-16: PyTorch autograd anomaly detection, PyTorch gradient clipping, PyTorch autograd mechanics, and PyTorch reproducibility.