PyTorch And NumPy Training Loops
Asked of: Machine Learning Engineer
Last updated
What's being tested
The interviewer is testing whether you can implement, explain, and debug a complete mini-batch training loop rather than only call a high-level trainer. You should be fluent in the differences between `PyTorch` autograd and a manual `NumPy` implementation, including gradient calculation, parameter updates, batching, evaluation mode, and numerical stability. This matters for an MLE because training code must be correct, reproducible, observable, and adaptable when moving from experimentation into a production training pipeline.
Core knowledge
-
A standard supervised loop has four stages: sample a batch, compute predictions and loss, compute gradients, then update parameters. For a batch , empirical risk is .
-
In
`PyTorch`, mark trainable tensors withrequires_grad=Trueor usenn.Parameter; callloss.backward()to populate.grad, then update insidetorch.no_grad(). The optimizer should own the update rule, such as SGD, momentum, or Adam. -
Call
optimizer.zero_grad()before every backward pass unless intentionally accumulating gradients across multiple batches. Otherwise gradients add to existing values, silently changing the effective batch size and potentially causing exploding updates. -
Use
model.train()during training andmodel.eval()during validation or inference. This changes behavior for Dropout and BatchNorm;torch.no_grad()ortorch.inference_mode()prevents unnecessary graph construction but does not replaceeval(). -
A manual
`NumPy`loop must derive gradients explicitly. For linear regression, , residual , and mean-squared-error gradients are and . -
Preserve shape contracts. With features shaped
(batch, d)and weights(d, 1), predictions should be(batch, 1); use explicit reshaping rather than accidental broadcasting. A target shaped(batch,)versus(batch,1)can produce plausible but incorrect loss tensors. -
Mini-batch sampling should shuffle training indices each epoch, cover every example once when using ordinary epoch-based SGD, and avoid shuffling validation data.
drop_last=Truecan stabilize BatchNorm but discards examples;drop_last=Falsepreserves data but creates a smaller final batch. -
Learning-rate behavior is often the main optimization failure. SGD updates ; Adam maintains first and second moments. Log loss by number of examples or batches consistently, and compare training loss with validation loss to identify overfitting.
-
Use numerically stable losses. For classification, prefer
torch.nn.CrossEntropyLoss, which combines log-softmax and negative log-likelihood; do not apply softmax before it. In`NumPy`, compute log-sum-exp as to avoid overflow. -
Device and dtype must be consistent: move both model and batches with
.to(device), and avoid repeated CPU/GPU transfers inside the inner loop. Preferfloat32for ordinary training; use mixed precision only with appropriate scaling and validation of numerical behavior. -
Validate gradients before optimizing. Compare analytical gradients with finite differences, using for small . Relative error, rather than absolute error alone, is useful when gradients vary greatly in magnitude.
-
Production-quality loops checkpoint model and optimizer state, record configuration and random seeds, expose batch loss and epoch metrics, and support resume-after-failure. Save
model.state_dict()plus optimizer state, epoch, scheduler state, and any scaler state used for mixed precision.
Worked example
Representative prompt: “Implement a mini-batch linear regression training loop in `PyTorch` and `NumPy`.” First, clarify the tensor shapes, loss definition, batch-size convention, stopping criterion, and whether the implementation must support GPU execution or only demonstrate the mechanics. I would organize the answer around data batching, forward and loss computation, gradient/update semantics, and validation plus instrumentation. In `PyTorch`, I would define parameters or an nn.Module, use DataLoader, call zero_grad(), compute the loss, call backward(), and invoke optimizer.step(); evaluation would use model.eval() with inference_mode(). In `NumPy`, I would compute the forward pass and analytical gradients directly, divide gradients by the actual batch size, and update weights after each batch. I would explicitly flag the tradeoff between a simple Python loop that is easy to inspect and vectorized operations that are substantially faster for large datasets. I would add assertions for shapes, finite-loss checks, and a finite-difference gradient test before trusting the loop. If I had more time, I would add checkpointing, learning-rate scheduling, deterministic seeding, and a comparison showing that both implementations converge to similar parameters.
A second angle
Representative prompt: “Debug a training loop whose loss is not decreasing.” The framing changes from implementation to diagnosis: first verify data-label alignment, target shapes, loss scaling, finite values, and whether parameters actually have nonzero gradients. Then inspect learning rate, gradient norms, accidental graph detachment, missing zero_grad(), and incorrect train()/eval() state. A strong answer distinguishes optimization failure from a data or metric bug by overfitting a tiny batch; a correct loop should drive that batch’s loss near zero for a learnable problem. The same discipline applies in both frameworks, but `NumPy` requires checking every derivative manually while `PyTorch` adds graph and device failure modes.
Common pitfalls
Analytical mistake: Saying that loss.backward() updates weights is incorrect; it only computes gradients. The optimizer performs the update, and omitting optimizer.step() leaves parameters unchanged.
Pitfall: Applying
softmaxbeforeCrossEntropyLossis a common but incorrect pattern because that loss expects unnormalized logits and performs the stable log-softmax operation internally.
Communication mistake: Listing API calls without stating invariants makes the answer hard to trust. Explain what must be true after each stage: gradients correspond to the current batch, parameters change once per intended update, and validation does not build a training graph.
Depth mistake: Blaming a plateau immediately on the learning rate misses simpler causes such as stale gradients, labels with the wrong dtype, NaNs, or an incorrect `NumPy` derivative. Start with a tiny-batch overfit test and finite-value assertions before tuning hyperparameters.
Connections
An interviewer may pivot to distributed data-parallel training, mixed-precision training, checkpoint recovery, or data and model versioning. They may also connect the loop to offline/online metric parity, experiment tracking, and training-serving skew.
Further reading
-
`PyTorch`Autograd Notes — explains graph construction, gradient tracking, and evaluation contexts. -
Dive into Deep Learning, Optimization Algorithms — develops SGD, momentum, Adam, and training-loop mechanics mathematically.
-
CS231n: Neural Networks Part 2 — provides practical guidance on gradient checks, initialization, and debugging optimization.
Related concepts
- PyTorch Training And Model ImplementationCoding & Algorithms
- Transformer Training Pipeline DebuggingMachine Learning
- Distributed Training Parallelism And CollectivesML System Design
- Distributed Training And LLM Fine-Tuning PlatformsML System Design
- Pandas And NumPy ML Data Manipulation
- ML Fundamentals: Backprop, Attention, And RLMachine Learning