Type at least 2 letters. Enter opens the first result, Esc closes.

    Deep Learning · Lesson 2 · Networks and how they learn

    Training a network: losses, mini-batches and the training loop

    Beginner 65 min 4 interactive figures 3 videos Colab notebook

    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:

    1. take a small random mini-batch of training examples;
    2. run the network on it (the forward pass) and compute the average loss;
    3. compute the gradient of that loss with respect to every weight (lesson 03 shows how);
    4. move every weight a small step against its gradient: θ←θ−η g\theta \leftarrow \theta - \eta\, g.

    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.

    Interactive figure: loading…

    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 log⁡2≈0.69\log 2 \approx 0.69, 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, η=0.01\eta = 0.01, 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 nn training examples. A mini-batch of BB examples gives an average over only BB of them. Different mini-batches give different directions, scattered around the full gradient. Larger batches scatter less, but each step costs more.

    Interactive figure: loading…

    Mini-batch gradients on a linear regression. The violet point is the parameter w=(w1,w2)w = (w_1, w_2); the grey ellipses are level sets of the training loss, with its minimum w∗w^* in aqua. The thick orange arrow is the full gradient step; the thin arrows are 50 mini-batch steps. Predict first: if you make BB 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 tr⁡Σ/B\sqrt{\operatorname{tr}\Sigma / B}. Drag ww close to w∗w^*: the full gradient almost vanishes, but the mini-batch steps still scatter.

    The step size has a limit #

    The simplest loss is a parabola, L(w)=λ2w2L(w) = \frac{\lambda}{2}w^2, with curvature λ\lambda. Its gradient is λw\lambda w, so one gradient descent step gives w←w−ηλw=(1−ηλ)ww \leftarrow w - \eta\lambda w = (1 - \eta\lambda)w. Every step multiplies ww by the same number 1−ηλ1 - \eta\lambda. If this number is between −1-1 and 11, ww shrinks to 0. If it is below −1-1, ww changes sign and grows at every step: the loss explodes.

    Interactive figure: loading…

    Find the stability limit. Move the learning rate η\eta. The blue parabola is the loss; the violet points are the iterates w0,w1,…w_0, w_1, \dots (magenta when they diverge). The chart shows ∣wt∣|w_t| on a log scale: a straight line with slope log⁡∣1−ηλ∣\log|1 - \eta\lambda|. Use the presets: a small step is slow, η=1/λ\eta = 1/\lambda jumps to the minimum in one step, between 1/λ1/\lambda and 2/λ2/\lambda it zig-zags but converges, and above 2/λ2/\lambda it diverges.

    Quick check

    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?

    Quick check

    The batch size goes from 32 to 128. What happens to the standard deviation of the mini-batch gradient (examples drawn independently)?

    3. Definitions and main results #

    3.1 The learning problem #

    A network fθf_\theta has parameters θ\theta (all its weights and biases). The data are pairs (xi,yi)(x_i, y_i), i=1,…,ni = 1, \dots, n: an input and its target. A loss function ℓ(y^,y)\ell(\hat y, y) measures how wrong a prediction y^\hat y is for the target yy. Training means minimizing the empirical risk, the average loss over the training set:

    R^(θ)=1n∑i=1nℓ(fθ(xi), yi).\hat R(\theta) = \frac{1}{n} \sum_{i=1}^{n} \ell\big(f_\theta(x_i),\, y_i\big).

    What we really care about is the population risk R(θ)=E(x,y) ℓ(fθ(x),y)R(\theta) = \mathbb{E}_{(x, y)}\,\ell(f_\theta(x), y): the expected loss on new examples from the same distribution. We cannot compute RR, 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 θ\theta 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 RR used once, at the end; choices based on it make it optimistic

    Three different "losses". The mini-batch loss is the average over the BB examples of one step; it is noisy. The training loss R^(θ)\hat R(\theta) 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 pθ(y∣x)p_\theta(y \mid x) for the target. Maximum likelihood chooses θ\theta to make the observed targets as probable as possible. For independent examples this means maximizing ∏ipθ(yi∣xi)\prod_i p_\theta(y_i \mid x_i), which is the same as minimizing the negative log-likelihood −1n∑ilog⁡pθ(yi∣xi)-\frac1n \sum_i \log p_\theta(y_i \mid x_i). Each loss below is this recipe for one choice of distribution.

    Regression: Gaussian noise gives the squared error. Assume y=fθ(x)+noisey = f_\theta(x) + \text{noise}, with Gaussian noise of variance σ2\sigma^2. Then

    −log⁡pθ(y∣x)=(y−fθ(x))22σ2+12log⁡(2πσ2).-\log p_\theta(y \mid x) = \frac{(y - f_\theta(x))^2}{2\sigma^2} + \tfrac12 \log(2\pi\sigma^2).

    The second term does not depend on θ\theta. 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 z∈Rz \in \mathbb{R}, and s(z)=1/(1+e−z)s(z) = 1/(1 + e^{-z}) (the logistic sigmoid) is the probability of class 1. For y∈{0,1}y \in \{0, 1\}, p(y)=sy(1−s)1−yp(y) = s^y (1 - s)^{1 - y}, so

    ℓ(z,y)=−[ylog⁡s(z)+(1−y)log⁡(1−s(z))],\ell(z, y) = -\big[y \log s(z) + (1 - y) \log(1 - s(z))\big],

    the binary cross-entropy (BCE). Its gradient is short. Use ddzlog⁡s=1−s\frac{d}{dz}\log s = 1 - s and ddzlog⁡(1−s)=−s\frac{d}{dz}\log(1 - s) = -s:

    ∂ℓ∂z=−[y(1−s)−(1−y)s]=s−y.\frac{\partial \ell}{\partial z} = -\big[y(1 - s) - (1 - y)s\big] = s - y .

    KK classes: softmax and cross-entropy. The network outputs KK logits z∈RKz \in \mathbb{R}^K. The softmax turns them into probabilities:

    pc=softmax⁡(z)c=ezc∑j=1Kezj,pc>0,∑cpc=1.p_c = \operatorname{softmax}(z)_c = \frac{e^{z_c}}{\sum_{j=1}^{K} e^{z_j}}, \qquad p_c > 0, \quad \sum_c p_c = 1 .

    With a one-hot target yy (1 at the true class, 0 elsewhere) the negative log-likelihood is the cross-entropy

    ℓ(z,y)=−∑c=1Kyclog⁡pc=−zc∗+log⁡∑jezj,\ell(z, y) = -\sum_{c=1}^{K} y_c \log p_c = -z_{c^*} + \log \sum_{j} e^{z_j},

    where c∗c^* is the true class. It is the cross-entropy between the target distribution yy and the predicted distribution pp (information-theory track).

    Theorem (gradient of softmax cross-entropy). For any target yy with ∑cyc=1\sum_c y_c = 1,

    ∂ℓ∂z=p−y.\frac{\partial \ell}{\partial z} = p - y .

    Proof.

    1. log⁡pc=zc−log⁡∑jezj\log p_c = z_c - \log \sum_j e^{z_j}, and ∂∂zklog⁡∑jezj=ezk∑jezj=pk\frac{\partial}{\partial z_k} \log \sum_j e^{z_j} = \frac{e^{z_k}}{\sum_j e^{z_j}} = p_k.
    2. So ∂ℓ∂zk=−∑cyc (δck−pk)=−yk+pk∑cyc=pk−yk\frac{\partial \ell}{\partial z_k} = -\sum_c y_c\,(\delta_{ck} - p_k) = -y_k + p_k \sum_c y_c = p_k - y_k. ■\blacksquare

    The gradient is "predicted minus target". Its entries sum to ∑kpk−∑kyk=0\sum_k p_k - \sum_k y_k = 0. For a mini-batch the loss is the mean over the BB rows, so the gradient with respect to the logit matrix is (P−Y)/B(P - Y)/B; lesson 03 starts the backward pass from there. The binary case is the same result with K=2K = 2.

    Quick check

    A 3-class model outputs p=(0.2,0.7,0.1)p = (0.2, 0.7, 0.1), and the true class is class 3. What is ∂ℓ/∂z\partial \ell / \partial z for the cross-entropy loss?

    Why not a sigmoid followed by the squared error? Take one logit, target y=1y = 1, and the loss ℓ=12(s(z)−y)2\ell = \frac12 (s(z) - y)^2. The chain rule with s′(z)=s(1−s)s'(z) = s(1 - s) gives

    ∂ℓ∂z=(s−y) s (1−s).\frac{\partial \ell}{\partial z} = (s - y)\, s\,(1 - s) .

    When the model is confidently wrong (z≪0z \ll 0, so s≈0s \approx 0), the factor s(1−s)s(1 - s) is close to 0, and the gradient almost vanishes. At z=−5z = -5: s=0.0067s = 0.0067, the BCE gradient is s−1=−0.993s - 1 = -0.993, and the MSE gradient is −0.0066-0.0066, about 150 times smaller. The ratio is always 1/(s(1−s))≥41/(s(1 - s)) \ge 4. Cross-entropy learns fastest from its worst mistakes; sigmoid + MSE learns slowest from them.

    Interactive figure: loading…

    Two losses for one logit. Top: the losses, cross-entropy (solid) and squared error after a sigmoid (dashed), in blue. Bottom: their gradients dℓ/dzd\ell/dz in orange. Move zz into the magenta band, where the model is confidently wrong: the BCE loss grows like ∣z∣|z| and its gradient stays near −1-1, while the MSE loss is flat near 0.50.5 and its gradient is close to 0. Switch the target to y=0y = 0 to see the mirror image.

    3.3 Numerically stable losses #

    Computing the softmax as written fails for large logits. In float64, eze^{z} overflows to ∞\infty for z>709.8z > 709.8; in float32 already for z>88.7z > 88.7. For very negative logits, eze^{z} underflows to 0, and log⁡0=−∞\log 0 = -\infty.

    Softmax ignores a common shift. For any number aa, softmax⁡(z+a1)=softmax⁡(z)\operatorname{softmax}(z + a\mathbf{1}) = \operatorname{softmax}(z), because ezc+a/∑jezj+a=eaezc/(ea∑jezj)e^{z_c + a} / \sum_j e^{z_j + a} = e^a e^{z_c} / (e^a \sum_j e^{z_j}). So we may subtract the largest logit m=max⁡jzjm = \max_j z_j before exponentiating. This is the log-sum-exp trick:

    log⁡∑jezj=m+log⁡∑jezj−m,log⁡pc=zc−m−log⁡∑jezj−m.\log \sum_j e^{z_j} = m + \log \sum_j e^{z_j - m}, \qquad \log p_c = z_c - m - \log \sum_j e^{z_j - m}.

    Now every exponent is ≤0\le 0, so nothing overflows, and the largest term is e0=1e^0 = 1, so the sum is at least 1 and its log is finite. The loss is computed as log⁡p\log p 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 zz and uses the stable form max⁡(z,0)−zy+log⁡(1+e−∣z∣)\max(z, 0) - zy + \log(1 + e^{-|z|}). Example in float32 with logits (0,−110)(0, -110): the softmax gives p2=0p_2 = 0 (underflow) and log⁡p2=−∞\log p_2 = -\infty, while log-softmax gives exactly −110-110.

    3.4 Gradient descent and mini-batch SGD #

    Gradient descent (GD) updates θ←θ−η∇R^(θ)\theta \leftarrow \theta - \eta \nabla \hat R(\theta) with a learning rate η>0\eta > 0. One step needs the gradient of all nn examples: too expensive when nn is in the millions.

    Mini-batch stochastic gradient descent (SGD) picks a random set B\mathcal{B} of BB indices and uses

    gB=1B∑i∈B∇θ ℓi(θ),ℓi(θ)=ℓ(fθ(xi),yi).g_{\mathcal{B}} = \frac{1}{B} \sum_{i \in \mathcal{B}} \nabla_\theta\, \ell_i(\theta), \qquad \ell_i(\theta) = \ell(f_\theta(x_i), y_i).

    Theorem (the mini-batch gradient is unbiased, with variance Σ/B\Sigma/B). Let the BB indices be drawn independently and uniformly from {1,…,n}\{1, \dots, n\}, and let Σ=1n∑i(∇ℓi−∇R^)(∇ℓi−∇R^)⊤\Sigma = \frac1n \sum_i (\nabla \ell_i - \nabla \hat R)(\nabla \ell_i - \nabla \hat R)^\top be the covariance of the per-example gradients. Then

    E[gB]=∇R^(θ),Cov⁡(gB)=ΣB.\mathbb{E}[g_{\mathcal{B}}] = \nabla \hat R(\theta), \qquad \operatorname{Cov}(g_{\mathcal{B}}) = \frac{\Sigma}{B}.

    Proof.

    1. One uniformly drawn index ii gives E[∇ℓi]=1n∑j∇ℓj=∇R^\mathbb{E}[\nabla \ell_i] = \frac1n \sum_{j} \nabla \ell_j = \nabla \hat R, and by the definition of Σ\Sigma, Cov⁡(∇ℓi)=Σ\operatorname{Cov}(\nabla \ell_i) = \Sigma.
    2. The mean of the BB terms has the same expectation: E[gB]=∇R^\mathbb{E}[g_{\mathcal{B}}] = \nabla \hat R.
    3. The BB terms are independent, so the covariance of their sum is the sum of the covariances, BΣB\Sigma. Dividing the sum by BB divides the covariance by B2B^2: Cov⁡(gB)=BΣ/B2=Σ/B\operatorname{Cov}(g_{\mathcal{B}}) = B\Sigma/B^2 = \Sigma/B. ■\blacksquare

    So the standard deviation of each gradient entry falls like 1/B1/\sqrt{B}. 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: ΣB⋅n−Bn−1\frac{\Sigma}{B} \cdot \frac{n - B}{n - 1}.

    Vocabulary. One iteration (or step) uses one mini-batch. One epoch is one pass over the whole training set: n/Bn/B iterations. Shuffling the order every epoch keeps the batches random.

    Why noise is acceptable. Each step is n/Bn/B 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: m←β1m+(1−β1)gm \leftarrow \beta_1 m + (1 - \beta_1) g (a smoothed gradient, momentum) and v←β2v+(1−β2)g2v \leftarrow \beta_2 v + (1 - \beta_2) g^2 (a smoothed squared gradient). The update is θ←θ−η m^/(v^+ϵ)\theta \leftarrow \theta - \eta\, \hat m / (\sqrt{\hat v} + \epsilon), where m^,v^\hat m, \hat v are the averages divided by 1−β1t1 - \beta_1^t and 1−β2t1 - \beta_2^t (bias correction at step tt). Dividing by v^\sqrt{\hat v} gives every parameter a step of similar size, whatever the scale of its gradient. The defaults are β1=0.9\beta_1 = 0.9, β2=0.999\beta_2 = 0.999, ϵ=10−8\epsilon = 10^{-8}; a common learning rate is 10−310^{-3}. The optimization track derives SGD with momentum, Adam and AdamW in detail.

    3.5 The learning rate on a quadratic #

    Theorem. Gradient descent on L(w)=λ2w2L(w) = \frac{\lambda}{2} w^2 with λ>0\lambda > 0 converges to 0 from every w0≠0w_0 \ne 0 if and only if 0<η<2/λ0 < \eta < 2/\lambda.

    Proof.

    1. L′(w)=λwL'(w) = \lambda w, so wt+1=wt−ηλwt=(1−ηλ) wtw_{t+1} = w_t - \eta \lambda w_t = (1 - \eta\lambda)\, w_t, and wt=(1−ηλ)tw0w_t = (1 - \eta\lambda)^t w_0.
    2. (1−ηλ)t→0(1 - \eta\lambda)^t \to 0 if and only if ∣1−ηλ∣<1|1 - \eta\lambda| < 1, that is, −1<1−ηλ<1-1 < 1 - \eta\lambda < 1.
    3. The right inequality gives η>0\eta > 0; the left one gives η<2/λ\eta < 2/\lambda. ■\blacksquare

    The fastest choice is η=1/λ\eta = 1/\lambda (one step). For 1/λ<η<2/λ1/\lambda < \eta < 2/\lambda the iterates change sign every step (a zig-zag) but still converge. For a quadratic in many dimensions with Hessian HH, the same argument runs along each eigenvector of HH (linear algebra, lesson 05), so the limit is η<2/λmax⁡(H)\eta < 2/\lambda_{\max}(H): the most curved direction sets the limit. For a network, λmax⁡\lambda_{\max} of the Hessian (the sharpness) changes during training; lesson 12 shows how warmup and learning-rate schedules deal with this.

    Quick check

    Gradient descent on L(w)=2w2L(w) = 2w^2 (so λ=4\lambda = 4) uses η=0.4\eta = 0.4. What happens?

    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 to p.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 tt 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, using p.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.
    Quick check

    What does model.eval() do?

    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 η\eta
    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 η\eta (a factor 3 to 10), warmup, gradient clipping (lesson 12)
    loss stays at log⁡K\log K 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 log⁡(1+(K−1)/e)\log(1 + (K - 1)/e) softmax applied before CrossEntropyLoss pass logits

    Sanity checks before a long run.

    1. Initial loss. With near-zero logits, p=(1/K,…,1/K)p = (1/K, \dots, 1/K) and the loss is log⁡K\log K: 0.6930.693 for 2 classes, 2.3032.303 for 10, 6.916.91 for 1000. A very different first loss points to a bug or a bad initialization.
    2. 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.
    3. Watch both curves. A training loss alone cannot show overfitting.

    Answers to section 1. (1) About log⁡10≈2.30\log 10 \approx 2.30: logits near 0 give p≈1/10p \approx 1/10 for every class, and the loss is −log⁡(1/10)-\log(1/10). (2) It halves: the variance falls by 128/32=4128/32 = 4, the standard deviation by 4=2\sqrt 4 = 2. (3) L(w)=w2L(w) = w^2 has λ=2\lambda = 2 and limit 2/λ=12/\lambda = 1. With η=1.1\eta = 1.1, wt+1=(1−2.2) wt=−1.2 wtw_{t+1} = (1 - 2.2)\,w_t = -1.2\,w_t: ww 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 z=(2,1,−1)z = (2, 1, -1), true class 1, so y=(1,0,0)y = (1, 0, 0).

    1. Exponentials: e2=7.389e^2 = 7.389, e1=2.718e^1 = 2.718, e−1=0.368e^{-1} = 0.368. Their sum is 10.47510.475.
    2. Probabilities: p=(7.389,2.718,0.368)/10.475=(0.7054,0.2595,0.0351)p = (7.389, 2.718, 0.368)/10.475 = (0.7054, 0.2595, 0.0351).
    3. Loss: ℓ=−log⁡0.7054=0.3490\ell = -\log 0.7054 = 0.3490.
    4. Gradient: p−y=(0.7054−1, 0.2595, 0.0351)=(−0.2946,0.2595,0.0351)p - y = (0.7054 - 1,\ 0.2595,\ 0.0351) = (-0.2946, 0.2595, 0.0351). The entries sum to 0.
    5. Suppose we could move the logits directly, with η=1\eta = 1: z←z−(p−y)=(2.2946,0.7405,−1.0351)z \leftarrow z - (p - y) = (2.2946, 0.7405, -1.0351). The new probabilities are (0.8018,0.1695,0.0287)(0.8018, 0.1695, 0.0287) and the new loss is −log⁡0.8018=0.2209-\log 0.8018 = 0.2209. The step raised the true logit, lowered the others, and the loss fell from 0.3490.349 to 0.2210.221.

    (b) The same loss, stably. Add 998 to every logit: z=(1000,999,997)z = (1000, 999, 997). A naive softmax computes e1000=∞e^{1000} = \infty and fails. With m=1000m = 1000:

    log⁡∑jezj=1000+log⁡(e0+e−1+e−3)=1000+log⁡1.4177=1000.3490,\log \sum_j e^{z_j} = 1000 + \log\big(e^{0} + e^{-1} + e^{-3}\big) = 1000 + \log 1.4177 = 1000.3490,

    and ℓ=1000.3490−z1=1000.3490−1000=0.3490\ell = 1000.3490 - z_1 = 1000.3490 - 1000 = 0.3490: 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 gi=1,3,−1,5g_i = 1, 3, -1, 5.

    1. Full gradient: (1+3−1+5)/4=2(1 + 3 - 1 + 5)/4 = 2.
    2. Per-example variance: Σ=14[(1−2)2+(3−2)2+(−1−2)2+(5−2)2]=1+1+9+94=5\Sigma = \frac14\big[(1-2)^2 + (3-2)^2 + (-1-2)^2 + (5-2)^2\big] = \frac{1 + 1 + 9 + 9}{4} = 5.
    3. Batches of B=2B = 2 drawn with replacement: there are 4×4=164 \times 4 = 16 equally likely ordered pairs. The average of their means is 2 (unbiased), and the variance of their means is 5/2=2.5=Σ/B5/2 = 2.5 = \Sigma/B.
    4. Without replacement there are 6 pairs, with variance 52⋅4−24−1=1.667\frac{5}{2} \cdot \frac{4 - 2}{4 - 1} = 1.667.

    (d) The learning-rate limit. L(w)=w2L(w) = w^2 (λ=2\lambda = 2), w0=2.4w_0 = 2.4. With η=0.3\eta = 0.3: 1−ηλ=0.41 - \eta\lambda = 0.4, so w=2.4,0.96,0.384,0.154,…w = 2.4, 0.96, 0.384, 0.154, \dots (converges). With η=1.1\eta = 1.1: 1−ηλ=−1.21 - \eta\lambda = -1.2, so w=2.4,−2.88,3.456,−4.147,…w = 2.4, -2.88, 3.456, -4.147, \dots (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 1.4611.461 for K=10K = 10, 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 log⁡V\log V: log⁡50,257≈10.8\log 50{,}257 \approx 10.8 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 log⁡1000≈6.9\log 1000 \approx 6.9), 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 σ(x)\sigma(x).
    • Choosing hyperparameters. Learning rate, batch size and the number of steps are chosen on the validation set; the stability limit 2/λ2/\lambda explains why the best learning rate is often just below the value where training diverges.

    7. Common mistakes #

    • Softmax (or sigmoid) before the loss. CrossEntropyLoss and BCEWithLogitsLoss take 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. Use torch.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 1/B1/\sqrt{B} while its cost grows like BB, and the learning rate usually has to change with it (lesson 12).

    8. Check yourself #

    1. A 1000-class model starts with logits near 0. What loss do you expect at step 0?
      Answerlog⁡1000≈6.91\log 1000 \approx 6.91. Near-zero logits give pc≈1/1000p_c \approx 1/1000 for every class.
    2. Show that the softmax does not change when you add the same number to every logit.
      Answerezc+a/∑jezj+a=eaezc/(ea∑jezj)=ezc/∑jezje^{z_c + a}/\sum_j e^{z_j + a} = e^a e^{z_c}/(e^a \sum_j e^{z_j}) = e^{z_c}/\sum_j e^{z_j}. This is why we may subtract max⁡jzj\max_j z_j before exponentiating.
    3. A binary classifier outputs the logit z=3z = 3 for an example with y=0y = 0. Compute the BCE gradient ∂ℓ/∂z\partial \ell / \partial z, and the gradient of 12(s(z)−y)2\frac12(s(z) - y)^2.
      Answers(3)=0.9526s(3) = 0.9526. BCE: s−y=0.9526s - y = 0.9526. MSE after a sigmoid: (s−y)s(1−s)=0.9526⋅0.9526⋅0.0474=0.0430(s - y)s(1 - s) = 0.9526 \cdot 0.9526 \cdot 0.0474 = 0.0430, about 22 times smaller.
    4. Per-example gradients have total variance tr⁡Σ=8\operatorname{tr}\Sigma = 8. What is the expected squared distance between a mini-batch gradient with B=32B = 32 (independent draws) and the full gradient?
      AnswerE∥gB−∇R^∥2=tr⁡Cov⁡(gB)=tr⁡Σ/B=8/32=0.25\mathbb{E}\|g_{\mathcal{B}} - \nabla \hat R\|^2 = \operatorname{tr}\operatorname{Cov}(g_{\mathcal{B}}) = \operatorname{tr}\Sigma / B = 8/32 = 0.25, so the typical distance is about 0.50.5.
    5. A quadratic loss has Hessian eigenvalues 0.50.5 and 1010. Which learning rates make gradient descent converge?
      Answer0<η<2/λmax⁡=2/10=0.20 < \eta < 2/\lambda_{\max} = 2/10 = 0.2. The flat direction (λ=0.5\lambda = 0.5) would allow η<4\eta < 4, but the steep one sets the limit.
    6. 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.
      AnswerOverfitting: 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.
    Recall cardsAnswer in your head first, then reveal. Add the cards to your review deck to see them again just before you would forget.
    1. What is the gradient of softmax cross-entropy with respect to the logits zz?

    2. Which loss does maximum likelihood give for regression with Gaussian noise of fixed variance?

    3. A KK-class classifier outputs logits near 0. What is its cross-entropy loss?

    4. Why does a sigmoid followed by squared error learn slowly from confident mistakes?

    5. Why do PyTorch's CrossEntropyLoss and BCEWithLogitsLoss take logits instead of probabilities?

    6. Write the log-sum-exp trick.

    7. How does the variance of a mini-batch gradient depend on the batch size BB (independent draws)?

    8. For which learning rates does gradient descent on λ2w2\frac{\lambda}{2}w^2 converge?

    9. Name the five lines inside the PyTorch training loop, in order.

    10. What does opt.zero_grad() prevent?

    Open the review →

    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 p−yp - y, which stays large when the model is confidently wrong. With a sigmoid, MSE has gradient (s−y)s(1−s)(s - y)s(1 - s), which vanishes there.
    • "Derive the gradient of softmax cross-entropy." — ℓ=−zc∗+log⁡∑jezj\ell = -z_{c^*} + \log\sum_j e^{z_j}; the derivative of log-sum-exp is pp, so ∇zℓ=p−y\nabla_z \ell = p - y.
    • "What is the expected initial loss of your classifier?" — log⁡K\log K 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 1/B1/B, the cost grows like BB. 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() and torch.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:

    1. log_softmax and cross_entropy in NumPy with the log-sum-exp trick, checked against PyTorch on logits as large as 1000.
    2. The gradient p−yp - y, checked by central finite differences.
    3. Measure the variance of mini-batch gradients for several batch sizes and fit the slope −1-1 on a log-log plot.
    4. Write the PyTorch training loop for a two-layer MLP on make_moons and reach more than 95% validation accuracy.
    5. Find the bug: a loop without zero_grad(). Predict the effect, run it, and fix it.

    11. Watch and read #

    The cost function as an average over training examples, and gradient descent as small downhill steps.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    A dataset, a loss, and the training loop written by hand, including the forgotten zero_grad bug.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    Softmax cross-entropy, its expected initial value, and mini-batch gradient descent.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play