The basic shape
Every PyTorch training loop has the same skeleton: for each epoch, iterate over the training loader, compute loss, backprop, step the optimizer. Periodically (usually each epoch or every N steps), evaluate on the validation set with model.eval(). Save the best checkpoint. Repeat until the validation metric stops improving.
This skeleton is so universal that PyTorch users either write it themselves once and reuse it, or use a higher-level framework (PyTorch Lightning, Hugging Face Trainer, Accelerate) that writes it for them. Both are reasonable. Writing your own is the better learning experience; using a framework is the better production choice.
What goes wrong in junior training loops
- Forgetting
opt.zero_grad()— gradients accumulate from previous steps. - Forgetting
model.eval()at validation — dropout and batchnorm misbehave, metrics are noisy. - Computing loss on the GPU but printing/logging from the CPU without
.item()— invisible CUDA syncs everywhere. - Computing accuracy with
argmaxon probabilities andargmaxon logits inconsistently — same result by luck, broken when softmax changes. - Saving the model state dict at the end of training, not at the best validation step — you ship the wrong model.
The non-negotiable additions
Beyond the skeleton, every serious training loop has: gradient clipping, learning-rate scheduling, mixed-precision (autocast + GradScaler) on GPU, periodic checkpoint saving, validation against a held-out set, and logging that you can read in your monitoring tool of choice. The next track makes each of these mechanical.