Training a network: losses, mini-batches and the training loop
Before this lesson: lesson 01 (the MLP); logistic and softmax regression and the train/validation/test split (ml-fundamentals track); gradient descent (optimization track); maximum likelihood (probability-and-statistics track); cross-entropy (information-theory track). Eigenvalues appear once (linear algebra, lesson 05). Time: about 65 minutes, plus the notebook.
Why this matters #
Every model in this track is trained with the same loop: forward pass, loss, gradients, optimizer
step. ImageNet classifiers, GPT-style language models (next-token cross-entropy over a vocabulary of
about 50,000 tokens), diffusion models (a mean squared error) and physics-informed networks (a sum of
squared residuals) differ in the model and the loss, not in the loop. Most failed training runs come
from a few mistakes in this lesson: probabilities passed where logits are expected, a missing
zero_grad(), a learning rate above the stability limit, or model.eval() forgotten at test time.
The next lesson (backpropagation) explains how the gradients are computed; this one explains what is
done with them.
1. Predict first #
2. The picture #
Training is a loop #
A training run repeats four steps:
- take a small random mini-batch of training examples;
- run the network on it (the forward pass) and compute the average loss;
- compute the gradient of that loss with respect to every weight (lesson 03 shows how);
- move every weight a small step against its gradient: .
Every few steps we also compute the loss on validation data that the network never trains on. The training loss tells us whether the network can fit the data. The validation loss tells us whether it works on new data.
Train a network in your browser. Press Play (or +100 steps). The background shows the predicted class: blue for class 0, yellow for class 1, pale where the network is unsure. The chart shows the training loss and the validation loss on a log scale; at the start both are near , the loss of a 50/50 guess. Then try: (1) the spiral with SGD, then with Adam, width 16 and 3 layers; (2) a learning rate above 3 with SGD: the loss stops falling or jumps around; (3) moons with noise 0.6, width 16, 3 layers, Adam, , batch 32, for a few thousand steps: the training loss keeps falling while the validation loss turns up. That is overfitting.
Each step is noisy, but right on average #
The true direction of descent is the full gradient: the average of the gradients of all training examples. A mini-batch of examples gives an average over only of them. Different mini-batches give different directions, scattered around the full gradient. Larger batches scatter less, but each step costs more.
Mini-batch gradients on a linear regression. The violet point is the parameter ; the grey ellipses are level sets of the training loss, with its minimum in aqua. The thick orange arrow is the full gradient step; the thin arrows are 50 mini-batch steps. Predict first: if you make four times larger, how much smaller does the spread get? Move the slider and compare with the chart, where the measured spread (dots) follows the line . Drag close to : the full gradient almost vanishes, but the mini-batch steps still scatter.
The step size has a limit #
The simplest loss is a parabola, , with curvature . Its gradient is , so one gradient descent step gives . Every step multiplies by the same number . If this number is between and , shrinks to 0. If it is below , changes sign and grows at every step: the loss explodes.
Find the stability limit. Move the learning rate . The blue parabola is the loss; the violet points are the iterates (magenta when they diverge). The chart shows on a log scale: a straight line with slope . Use the presets: a small step is slow, jumps to the minimum in one step, between and it zig-zags but converges, and above it diverges.
In a training run, the training loss falls for a while, then starts to jump up and down, and some values are much larger than the starting loss. What is the most likely cause?
Overfitting shows as a validation loss that rises while the training loss keeps falling, and underfitting as a training loss that stays high and smooth. A training loss that jumps above its starting value means the steps overshoot: the learning rate is above the stability limit (about for curvature ). A larger batch makes the steps less noisy.
The batch size goes from 32 to 128. What happens to the standard deviation of the mini-batch gradient (examples drawn independently)?
The variance is , so it falls by a factor . The standard deviation is the square root, so it falls by . Each step costs 4 times more, but the noise only halves.
3. Definitions and main results #
3.1 The learning problem #
A network has parameters (all its weights and biases). The data are pairs , : an input and its target. A loss function measures how wrong a prediction is for the target . Training means minimizing the empirical risk, the average loss over the training set:
What we really care about is the population risk : the expected loss on new examples from the same distribution. We cannot compute , so we estimate it with held-out data. The data are split into three sets with three different jobs:
| Set | Used for | Rule |
|---|---|---|
| training set | computing gradients and updating | the only data the optimizer sees |
| validation set | choosing hyperparameters (width, learning rate, number of steps) and early stopping | never used for gradients |
| test set | one final estimate of | used once, at the end; choices based on it make it optimistic |
Three different "losses". The mini-batch loss is the average over the examples of one step; it is noisy. The training loss is the average over the whole training set. The validation (or test) loss is the same average on held-out data. Do not mix them up when you read a loss curve.
3.2 Output layers and losses from maximum likelihood #
Let the network output the parameters of a probability distribution for the target. Maximum likelihood chooses to make the observed targets as probable as possible. For independent examples this means maximizing , which is the same as minimizing the negative log-likelihood . Each loss below is this recipe for one choice of distribution.
Regression: Gaussian noise gives the squared error. Assume , with Gaussian noise of variance . Then
The second term does not depend on . So minimizing the negative log-likelihood is minimizing the mean squared error (MSE). Output layer: linear, no activation.
Binary classification: Bernoulli gives binary cross-entropy. The network outputs one logit , and (the logistic sigmoid) is the probability of class 1. For , , so
the binary cross-entropy (BCE). Its gradient is short. Use and :
classes: softmax and cross-entropy. The network outputs logits . The softmax turns them into probabilities:
With a one-hot target (1 at the true class, 0 elsewhere) the negative log-likelihood is the cross-entropy
where is the true class. It is the cross-entropy between the target distribution and the predicted distribution (information-theory track).
Theorem (gradient of softmax cross-entropy). For any target with ,
Proof.
- , and .
- So .
The gradient is "predicted minus target". Its entries sum to . For a mini-batch the loss is the mean over the rows, so the gradient with respect to the logit matrix is ; lesson 03 starts the backward pass from there. The binary case is the same result with .
A 3-class model outputs , and the true class is class 3. What is for the cross-entropy loss?
The gradient is with : . Gradient descent subtracts it, so the logit of the true class goes up and the others go down. The last option is the gradient with respect to of , not with respect to the logits.
Why not a sigmoid followed by the squared error? Take one logit, target , and the loss . The chain rule with gives
When the model is confidently wrong (, so ), the factor is close to 0, and the gradient almost vanishes. At : , the BCE gradient is , and the MSE gradient is , about 150 times smaller. The ratio is always . Cross-entropy learns fastest from its worst mistakes; sigmoid + MSE learns slowest from them.
Two losses for one logit. Top: the losses, cross-entropy (solid) and squared error after a sigmoid (dashed), in blue. Bottom: their gradients in orange. Move into the magenta band, where the model is confidently wrong: the BCE loss grows like and its gradient stays near , while the MSE loss is flat near and its gradient is close to 0. Switch the target to to see the mirror image.
3.3 Numerically stable losses #
Computing the softmax as written fails for large logits. In float64, overflows to for ; in float32 already for . For very negative logits, underflows to 0, and .
Softmax ignores a common shift. For any number , , because . So we may subtract the largest logit before exponentiating. This is the log-sum-exp trick:
Now every exponent is , so nothing overflows, and the largest term is , so the sum is at least 1 and its log is finite. The loss is computed as directly (log-softmax), never as the log of a probability that may have underflowed.
This is why PyTorch losses take logits. nn.CrossEntropyLoss expects raw logits and applies
log-softmax inside; nn.BCEWithLogitsLoss expects the logit and uses the stable form
. Example in float32 with logits : the softmax gives
(underflow) and , while log-softmax gives exactly .
3.4 Gradient descent and mini-batch SGD #
Gradient descent (GD) updates with a learning rate . One step needs the gradient of all examples: too expensive when is in the millions.
Mini-batch stochastic gradient descent (SGD) picks a random set of indices and uses
Theorem (the mini-batch gradient is unbiased, with variance ). Let the indices be drawn independently and uniformly from , and let be the covariance of the per-example gradients. Then
Proof.
- One uniformly drawn index gives , and by the definition of , .
- The mean of the terms has the same expectation: .
- The terms are independent, so the covariance of their sum is the sum of the covariances, . Dividing the sum by divides the covariance by : .
So the standard deviation of each gradient entry falls like . In practice we shuffle the data and take batches without replacement; the mini-batch gradient is still unbiased, and the variance is a little smaller: .
Vocabulary. One iteration (or step) uses one mini-batch. One epoch is one pass over the whole training set: iterations. Shuffling the order every epoch keeps the batches random.
Why noise is acceptable. Each step is times cheaper than a full-batch step, and it points in the right direction on average. Early in training the full gradient is large compared with the noise, so noisy steps make fast progress. Near a minimum the full gradient becomes small and the noise dominates (you saw this in the figure); the iterates then jump around the minimum, which is one reason to lower the learning rate late in training (lesson 12).
Adam in one paragraph. Most networks in this track are trained with Adam (Kingma and Ba 2015). It keeps two running averages per parameter: (a smoothed gradient, momentum) and (a smoothed squared gradient). The update is , where are the averages divided by and (bias correction at step ). Dividing by gives every parameter a step of similar size, whatever the scale of its gradient. The defaults are , , ; a common learning rate is . The optimization track derives SGD with momentum, Adam and AdamW in detail.
3.5 The learning rate on a quadratic #
Theorem. Gradient descent on with converges to 0 from every if and only if .
Proof.
- , so , and .
- if and only if , that is, .
- The right inequality gives ; the left one gives .
The fastest choice is (one step). For the iterates change sign every step (a zig-zag) but still converge. For a quadratic in many dimensions with Hessian , the same argument runs along each eigenvector of (linear algebra, lesson 05), so the limit is : the most curved direction sets the limit. For a network, of the Hessian (the sharpness) changes during training; lesson 12 shows how warmup and learning-rate schedules deal with this.
Gradient descent on (so ) uses . What happens?
, and , so with alternating signs. The limit is , not 1: the curvature matters, not alone. The step overshoots the minimum, but it diverges only when .
3.6 The PyTorch training loop #
Here is the complete loop for a classifier. Each line is explained below.
model = nn.Sequential(nn.Linear(2, 32), nn.ReLU(), nn.Linear(32, 2)) # outputs logits
loss_fn = nn.CrossEntropyLoss() # log-softmax + NLL, takes logits
opt = torch.optim.Adam(model.parameters(), lr=1e-2)
for epoch in range(30):
model.train() # training behaviour (dropout on, BatchNorm batch stats)
for xb, yb in train_loader: # shuffled mini-batches
logits = model(xb) # 1. forward pass
loss = loss_fn(logits, yb) # 2. mean loss over the batch
opt.zero_grad() # 3. clear the old gradients
loss.backward() # 4. backprop: fills p.grad for every parameter
opt.step() # 5. update the parameters
model.eval() # evaluation behaviour (dropout off, BatchNorm running stats)
with torch.no_grad(): # no graph, no gradients: faster, less memory
val_logits = model(X_val)
val_loss = loss_fn(val_logits, y_val).item()
val_acc = (val_logits.argmax(dim=1) == y_val).float().mean().item()
opt.zero_grad(). PyTorch adds new gradients top.grad; it does not overwrite them. This is useful for gradient accumulation (several small batches before one step), but in a normal loop the old gradient must be cleared. Without it, step uses the sum of all gradients so far, and training usually becomes unstable.loss.backward()runs backpropagation (lesson 03) from the scalar loss.opt.step()applies the update rule (SGD, Adam, …) to every parameter, usingp.grad.model.train()/model.eval()switch the behaviour of layers such as dropout (lesson 06) and BatchNorm (lesson 05). They do not turn gradients on or off.torch.no_grad()turns off the recording of the computation graph. Use it for evaluation; it does not change dropout or BatchNorm.
What does model.eval() do?
eval() only sets a flag that dropout and BatchNorm read: dropout stops dropping units, and BatchNorm uses its running statistics. Gradients are turned off by torch.no_grad(), a separate tool. In evaluation code you usually want both.
3.7 Reading loss curves #
| What you see | Likely cause | What to do |
|---|---|---|
| training and validation loss both high and flat | underfitting: model too small, too few steps, or learning rate too small | bigger model, more steps, larger |
| training loss falls, validation loss falls, then rises | overfitting: the model fits noise in the training set | stop early, more data, regularization (lesson 06) |
| loss jumps up and down, rises above its start, or becomes NaN | learning rate too large | lower (a factor 3 to 10), warmup, gradient clipping (lesson 12) |
| loss stays at | the model outputs uniform probabilities: a bug (labels shuffled, no gradient reaches the weights, learning rate 0) | check the data and that p.grad is nonzero |
| loss falls but stays above | softmax applied before CrossEntropyLoss |
pass logits |
Sanity checks before a long run.
- Initial loss. With near-zero logits, and the loss is : for 2 classes, for 10, for 1000. A very different first loss points to a bug or a bad initialization.
- Overfit one small batch. Train on a single batch of a few examples. A correct model and loop drive that loss close to 0. If it cannot, there is a bug.
- Watch both curves. A training loss alone cannot show overfitting.
Answers to section 1. (1) About : logits near 0 give for every class, and the loss is . (2) It halves: the variance falls by , the standard deviation by . (3) has and limit . With , : changes sign and grows by 20% at every step. It diverges.
4. Worked example #
(a) One softmax cross-entropy step by hand. Three classes, logits , true class 1, so .
- Exponentials: , , . Their sum is .
- Probabilities: .
- Loss: .
- Gradient: . The entries sum to 0.
- Suppose we could move the logits directly, with : . The new probabilities are and the new loss is . The step raised the true logit, lowered the others, and the loss fell from to .
(b) The same loss, stably. Add 998 to every logit: . A naive softmax computes and fails. With :
and : exactly the loss of part (a), because softmax ignores a common shift.
(c) Mini-batch variance on four examples. Take a model with one parameter and four training examples whose gradients are .
- Full gradient: .
- Per-example variance: .
- Batches of drawn with replacement: there are equally likely ordered pairs. The average of their means is 2 (unbiased), and the variance of their means is .
- Without replacement there are 6 pairs, with variance .
(d) The learning-rate limit. (), . With : , so (converges). With : , so (diverges).
5. In code #
NumPy and PyTorch check the worked example.
import itertools
import numpy as np
import torch
import torch.nn.functional as F
# (a) softmax cross-entropy and its gradient p - y
z = torch.tensor([2., 1., -1.], dtype=torch.float64, requires_grad=True)
loss = F.cross_entropy(z[None], torch.tensor([0])) # takes logits
loss.backward()
print(round(loss.item(), 4), z.grad.numpy().round(4)) # 0.349 [-0.2946 0.2595 0.0351]
# (b) shift invariance and log-sum-exp: same loss for z + 998
zb = np.array([1000., 999., 997.])
m = zb.max()
print(round(m + np.log(np.exp(zb - m).sum()) - zb[0], 4)) # 0.349
print(torch.log_softmax(torch.tensor([0., -110.]), 0)) # tensor([ 0., -110.])
print(torch.log(torch.softmax(torch.tensor([0., -110.]), 0))) # tensor([0., -inf]) in float32
# (c) mini-batch variance: Sigma / B with replacement
g = np.array([1., 3., -1., 5.])
means = [(g[i] + g[j]) / 2 for i, j in itertools.product(range(4), repeat=2)]
print(g.mean(), g.var(), np.mean(means), np.var(means)) # 2.0 5.0 2.0 2.5
# (d) gradient descent on w^2 (lambda = 2)
for eta in (0.3, 1.1):
w, ws = 2.4, []
for _ in range(4):
ws.append(round(w, 3)); w -= eta * 2 * w
print(eta, ws) # 0.3 [2.4, 0.96, 0.384, 0.154] / 1.1 [2.4, -2.88, 3.456, -4.147]
# the softmax-before-CrossEntropyLoss floor: log(1 + (K - 1)/e)
print(round(F.cross_entropy(torch.eye(10), torch.arange(10)).item(), 3)) # 1.461
The last line passes perfect one-hot "probabilities" as logits: even then the loss is for , not 0.
6. Where it shows up #
- Language models. GPT-style models are trained with softmax cross-entropy over the vocabulary to predict the next token (lesson 08). The initial loss is about : for the GPT-2 vocabulary. Large-scale runs watch exactly the loss curves of section 3.7 (lesson 12).
- Image classification. ImageNet models use cross-entropy over 1000 classes (initial loss ), mini-batches of 256 or more, and SGD with momentum or AdamW.
- Regression and generative models. Diffusion models are trained with a mean squared error on the noise; it is a Gaussian likelihood in disguise.
- ML for science. Physics-informed networks minimize a sum of squared residuals of a PDE at sampled points; a mini-batch of collocation points is a mini-batch in the sense of this lesson. Neural surrogate models of simulations are trained with MSE, or with a Gaussian negative log-likelihood when the network also predicts the noise level .
- Choosing hyperparameters. Learning rate, batch size and the number of steps are chosen on the validation set; the stability limit explains why the best learning rate is often just below the value where training diverges.
7. Common mistakes #
- Softmax (or sigmoid) before the loss.
CrossEntropyLossandBCEWithLogitsLosstake logits. Passing probabilities applies the nonlinearity twice and puts a floor on the loss. - Forgetting
opt.zero_grad(). Gradients accumulate across steps, and the updates grow until training becomes unstable. - "
model.eval()turns off gradients." It does not; it changes dropout and BatchNorm. Usetorch.no_grad()for that. - Choosing hyperparameters on the test set. Then the test loss is no longer an honest estimate. Use the validation set, and the test set once.
- Reading the mini-batch loss as the training loss. The mini-batch loss is noisy; average it over many steps, or compute the loss on the whole training set, before you draw conclusions.
- Sigmoid + MSE for classification. It works, but learns very slowly from confident mistakes. Use cross-entropy.
- "A larger batch is always better." Its noise falls only like while its cost grows like , and the learning rate usually has to change with it (lesson 12).
8. Check yourself #
- A 1000-class model starts with logits near 0. What loss do you expect at step 0?
Answer
. Near-zero logits give for every class. - Show that the softmax does not change when you add the same number to every logit.
Answer
. This is why we may subtract before exponentiating. - A binary classifier outputs the logit for an example with . Compute the BCE gradient , and the gradient of .
Answer
. BCE: . MSE after a sigmoid: , about 22 times smaller. - Per-example gradients have total variance . What is the expected squared distance between a mini-batch gradient with (independent draws) and the full gradient?
Answer
, so the typical distance is about . - A quadratic loss has Hessian eigenvalues and . Which learning rates make gradient descent converge?
Answer
. The flat direction () would allow , but the steep one sets the limit. - Your training loss falls to 0.01 while the validation loss rises from 0.3 to 0.8. What is happening, and name two remedies.
Answer
Overfitting: the model fits noise in the training set. Remedies: stop early (keep the parameters with the lowest validation loss), get more data, use regularization such as weight decay or dropout, or use a smaller model.
What is the gradient of softmax cross-entropy with respect to the logits ?
, where and is the one-hot target. For the mean over a batch: .
Which loss does maximum likelihood give for regression with Gaussian noise of fixed variance?
The squared error: .
A -class classifier outputs logits near 0. What is its cross-entropy loss?
(for example ), because every class gets probability .
Why does a sigmoid followed by squared error learn slowly from confident mistakes?
Its gradient is ; when is near 0 or 1 the factor is near 0. The BCE gradient stays near .
Why do PyTorch's
CrossEntropyLossandBCEWithLogitsLosstake logits instead of probabilities?They compute directly with the log-sum-exp trick, which never overflows and never takes . Probabilities may have underflowed to 0 already.
Write the log-sum-exp trick.
with ; it works because softmax ignores a common shift of the logits.
How does the variance of a mini-batch gradient depend on the batch size (independent draws)?
It is , where is the covariance of the per-example gradients. The standard deviation falls like .
For which learning rates does gradient descent on converge?
For , because and we need .
Name the five lines inside the PyTorch training loop, in order.
logits = model(xb),loss = loss_fn(logits, yb),opt.zero_grad(),loss.backward(),opt.step().What does
opt.zero_grad()prevent?The accumulation of gradients: PyTorch adds new gradients to
p.grad, so without clearing them each step would use the sum of all past gradients.
9. Interview angle #
- "Why cross-entropy and not MSE for classification?" — It is the maximum-likelihood loss for a categorical output; its gradient with respect to the logits is , which stays large when the model is confidently wrong. With a sigmoid, MSE has gradient , which vanishes there.
- "Derive the gradient of softmax cross-entropy." — ; the derivative of log-sum-exp is , so .
- "What is the expected initial loss of your classifier?" — with small logits. Check it before a long run.
- "Why mini-batches? What happens when you change the batch size?" — Cheaper steps with an unbiased gradient; the variance falls like , the cost grows like . The learning rate usually has to be tuned again (lesson 12).
- "Your loss is NaN after 200 steps. What do you check?" — The learning rate (stability limit), the loss computed from probabilities instead of logits, divisions or logs of zero in the data pipeline, and the gradient norm before the explosion.
- "Difference between
model.eval()andtorch.no_grad()?" — Layer behaviour (dropout, BatchNorm) against graph recording. Evaluation code uses both.
10. Practice #
The notebook for this lesson runs in Colab. It uses NumPy for the first three exercises and PyTorch (on the CPU) for the last two:
log_softmaxandcross_entropyin NumPy with the log-sum-exp trick, checked against PyTorch on logits as large as 1000.- The gradient , checked by central finite differences.
- Measure the variance of mini-batch gradients for several batch sizes and fit the slope on a log-log plot.
- Write the PyTorch training loop for a two-layer MLP on
make_moonsand reach more than 95% validation accuracy. - Find the bug: a loop without
zero_grad(). Predict the effect, run it, and fix it.
11. Watch and read #
- Simon Prince, Understanding Deep Learning (free PDF): chapter 5 (loss functions from maximum likelihood) and chapter 6 (gradient descent, SGD, Adam).
- Goodfellow, Bengio and Courville, Deep Learning, chapter 6.2 (cost functions and output units) and chapter 8 (optimization), free online.
- Andrej Karpathy, A recipe for training neural networks (2019): the sanity checks of section 3.7 in practice.
- Stanford CS231n notes, Neural networks part 3: learning and evaluation: gradient checks, loss curves, and learning rates.
- PyTorch documentation,
CrossEntropyLoss: what the input is expected to be.
Runs in your browser with nothing to install. The notebook repeats the key demos and has exercises with checks; solutions are inside, folded.