Interview concept

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 BB, empirical risk is L^(θ)=1∣B∣∑(x,y)∈Bℓ(fθ(x),y)\hat{L}(\theta)=\frac{1}{|B|}\sum_{(x,y)\in B}\ell(f_\theta(x),y).

  • In `PyTorch`, mark trainable tensors with requires_grad=True or use nn.Parameter; call loss.backward() to populate .grad, then update inside torch.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 and model.eval() during validation or inference. This changes behavior for Dropout and BatchNorm; torch.no_grad() or torch.inference_mode() prevents unnecessary graph construction but does not replace eval().

  • A manual `NumPy` loop must derive gradients explicitly. For linear regression, y^=XW+b\hat{y}=XW+b, residual r=y^−yr=\hat{y}-y, and mean-squared-error gradients are ∂L∂W=2nX⊤r\frac{\partial L}{\partial W}=\frac{2}{n}X^\top r and ∂L∂b=2n∑iri\frac{\partial L}{\partial b}=\frac{2}{n}\sum_i r_i.

  • 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=True can stabilize BatchNorm but discards examples; drop_last=False preserves data but creates a smaller final batch.

  • Learning-rate behavior is often the main optimization failure. SGD updates θ←θ−ηg\theta\leftarrow\theta-\eta g; 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 zmax⁡+log⁡∑jezj−zmax⁡z_{\max}+\log\sum_j e^{z_j-z_{\max}} 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. Prefer float32 for 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 L(θ+ϵ)−L(θ−ϵ)2ϵ\frac{L(\theta+\epsilon)-L(\theta-\epsilon)}{2\epsilon} for small ϵ\epsilon. 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 softmax before CrossEntropyLoss is 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

Related concepts