Skip to content

Overview

Hands-on handout β€” Regularization in practice: making a network stop memorizing

A two-hour lab that takes one network deliberately trained to memorize its training set and turns it, one technique at a time, into one that generalizes β€” measuring every step. Real runs, figures from the runs, and checkpoints with solutions.

open the handout

A model that fits the training data perfectly is not the goal β€” it is a warning sign. What we actually want is a model that does well on data it has never seen, and those two things pull in opposite directions. Regularization is the name for every technique that deliberately makes the training fit worse in exchange for a better fit on everything else.

Formally, training minimizes the loss we can measure, over the \(N\) examples we happen to have:

\[ \hat{R}(\theta) = \frac{1}{N}\sum_{i=1}^{N} L\big(f_\theta(x^{(i)}), y^{(i)}\big) \]

but what we care about is the loss over the whole distribution the data came from, which we cannot measure:

\[ R(\theta) = \mathbb{E}_{(x,y)\sim \mathcal{D}}\big[L(f_\theta(x), y)\big] \]

The difference \(R(\theta) - \hat{R}(\theta)\) is the generalization gap, and every technique in this chapter is an attempt to keep it small.

What this chapter is about

Every section below answers one question, and they are meant to be read in this order:

flowchart TD
    A["Train and validation error<br/>read together"] --> B{Which failure?}
    B -->|"both high"| C["Underfitting<br/>more capacity, train longer,<br/>regularize less"]
    B -->|"train low,<br/>validation high"| D["Overfitting"]
    D --> E["1. More data<br/>or augmentation"]
    D --> F["2. Constrain the weights<br/>L2 / L1"]
    D --> G["3. Inject noise<br/>dropout, BatchNorm"]
    D --> H["4. Limit the search<br/>early stopping"]
    D --> I["5. Soften the target<br/>label smoothing, mixup"]

Nine live simulators run through the page. None of them needs installing anything β€” the whole point is that you can move a slider and watch the failure appear and disappear before we write down a single equation about it.

Seeing it happen

Nothing explains overfitting like watching it. Below, 12 noisy points are drawn from a smooth function, and a polynomial of the degree you choose is fitted to them by least squares. The dashed line is the truth we are trying to recover; the crosses are held-out points the fit never saw.

Three experiments worth running, in order:

  1. Degree 1, then 3, then 14, with \(\lambda = 0\). Degree 1 cannot bend at all. Degree 3 lands close to the truth. Degree 14 threads every single training point β€” train error collapses to essentially zero β€” and lurches violently between them. That is overfitting: not a model that is wrong, but a model that is right about the noise.
  2. Stay at degree 14 and raise \(\lambda\). Nothing about the model shrinks; it still has 15 coefficients. But the wild curve calms down and the held-out error drops. Capacity was never the problem β€” unconstrained capacity was.
  3. Stay at degree 14, \(\lambda = 0\), and drag the number of points up. The same model that was useless with 12 points becomes reasonable with 60. More data is regularization too, and the most effective kind.
Check yourself β€” why does raising Ξ» fix degree 14, when the model still has 15 coefficients?

Because capacity is not the same thing as used capacity. The penalty does not remove coefficients, it makes them expensive: the fit can still bend as violently as it likes, but only if the loss reduction pays for the weight growth. Fitting noise buys almost nothing in loss and costs a great deal in norm, so it stops being worth it β€” while the broad shape of the curve, which buys a lot of loss for little norm, survives. Capacity was never the problem; unconstrained capacity was.

Watch β€–wβ€–Β² in the readout

Sweep the degree at 12 points and follow the coefficients: \(\lVert w \rVert^2\) goes from about \(5\) at degree 3 to \(5\times10^{5}\) at degree 14. Enormous coefficients of alternating sign are exactly how a curve produces those violent swings between points, and that observation is what every penalty method in this chapter is built on: at a fixed amount of data, huge weights are the signature of a model that is memorizing.

The qualifier matters. Raise the data to 60 points and degree 14 generalizes well while still having a huge norm β€” so the norm is not a verdict on its own, which is why bounds based only on weight size explain much less about deep networks than we would like1.

Bias and variance

The two failures in that simulator have names, and they come from splitting the expected error of a model into pieces. For squared loss, at a point \(x\), averaging over all training sets we might have drawn:

\[ \mathbb{E}\big[(y - \hat{f}(x))^2\big] \;=\; \underbrace{\big(\mathbb{E}[\hat{f}(x)] - f(x)\big)^2}_{\text{bias}^2} \;+\; \underbrace{\mathbb{E}\big[(\hat{f}(x) - \mathbb{E}[\hat{f}(x)])^2\big]}_{\text{variance}} \;+\; \underbrace{\sigma^2}_{\text{noise}} \]

The classic way in is a shooting target. Fix a point \(x\), then imagine training the same model many times, each time on a different draw of the training data, and marking where each of those models lands. The bullseye is the truth \(f(x)\); each dot is one model's prediction.

each dot is one model, trained on its own draw of the data

The two failures are independent β€” a model can have either, both, or neither β€” and that is the whole reason the decomposition is worth writing down. Read its three terms as three different complaints:

  • Bias β€” the model is systematically wrong, in the same way, no matter which training set it gets. On the target: the dashed line, from the bullseye to where the dots land on average. The degree-1 fit is biased: it cannot bend, so it misses the curve every time.
  • Variance β€” the model changes wildly depending on which training set it happened to see. On the target: the scatter of the dots around their own average. The degree-14 fit has enormous variance: resample the 12 points and you get a completely different curve.
  • Noise β€” the irreducible part, \(\sigma^2 = 0.032\) in the simulator. On the target: a gust of wind on every shot β€” no aim removes it. No model beats it, and a model that scores below it on training data is fitting noise by definition.

Model capacity trades the first against the second, which is the classic bias-variance tradeoff:

Underfitting and overfitting. Source: GeeksforGeeks2

That is the drawn version. Here is the measured one. For each polynomial degree, the same fit is repeated on 140 different training sets drawn from the same truth, and the error at 41 held-out points is split into the two terms of the equation above β€” \(\big(\text{average} - \text{truth}\big)^2\) and the scatter around that average:

The U is the white curve, and it is a U for a reason you can now see rather than assert: the two curves underneath it run in opposite directions, so their sum has to have a bottom somewhere. Four things to do with it:

  1. Read the two ends. At degree 1 the error is almost entirely biasΒ² β€” every training set produces roughly the same wrong curve. At degree 11 it is almost entirely variance β€” every training set produces a different curve. Same total error, opposite diseases, opposite cures.
  2. Turn the L2 penalty up. A ghost line appears: the total error without it. The right-hand arm of the U flattens out while the left-hand arm barely moves β€” the penalty buys a large reduction in variance for a small increase in bias. That trade is the whole business of this chapter.
  3. Drag the training points from 12 to 60. The valley widens and the right arm sinks: with more data, high capacity stops being dangerous. Compare with experiment 3 in the first simulator.
  4. Drop the noise Οƒ. The optimum moves right β€” the less noise there is to mistake for signal, the more capacity you can afford.

Why degree 2 is no better than degree 1

The biasΒ² curve has a flat step there, and it is not a bug: the truth in this simulator, \(0.75\sin(2.6x) + 0.25x\), is an odd function, so its \(x^2\) coefficient is zero. Adding that term to the basis buys no bias reduction at all β€” and still costs variance. It is the cleanest small example of a general fact: capacity only helps when it is capacity in the direction the truth actually needs.

  • Too much bias (underfitting)


    Train and validation error are both high and close to each other.

    • Increase capacity: more layers, more units.
    • Train longer β€” you may simply not have converged.
    • Add or engineer features; check the data pipeline.
    • Reduce the regularization you already have.
  • Too much variance (overfitting)


    Train error is low, validation error is much higher. The gap is the symptom.

    • More data, or augmentation to simulate more data.
    • Penalize the weights (L1/L2), add dropout, stop early.
    • Reduce capacity β€” the last resort, not the first.
    • Check for leakage before believing any of this.

The U-curve is not the whole story any more

The classic picture says test error falls, bottoms out, then rises forever as capacity grows. Modern networks break it: push capacity past the point where the model interpolates the training set perfectly and test error often falls again, sometimes below the first minimum. This is double descent34, and it is why "just make the model smaller" is bad advice for deep networks. The curve you were taught still describes the underparameterized regime β€” most deep networks simply do not live there.

Penalizing the weights: L1 and L2

The simulator's lesson was that memorizing needs enormous coefficients. So add their size to the loss and let the optimizer weigh the two objectives:

\[ \tilde{J}(w) = J(w) + \frac{\lambda}{2}\sum_i w_i^2 \]

The gradient of the penalty is \(\lambda w\), so a gradient step becomes

\[ w \leftarrow w - \eta\nabla J(w) - \eta\lambda w = \underbrace{(1 - \eta\lambda)}_{\text{shrink}} w - \eta \nabla J(w) \]

Every step multiplies the weights by a factor slightly below 1 before the gradient is applied β€” which is exactly why the same idea is called weight decay5. Weights survive only if the gradient keeps pushing them back up, so the penalty acts as constant pressure toward zero that useful weights must earn their way out of.

\[ \tilde{J}(w) = J(w) + \lambda\sum_i |w_i| \]

The gradient of the penalty is \(\lambda\,\mathrm{sign}(w)\) β€” the same size no matter how small \(w\) is. Where L2 pushes proportionally (and so pushes ever more gently as the weight shrinks), L1 pushes with constant force all the way to zero, and holds it there.

The result is exact zeros, not just small values: L1 performs feature selection as a side effect of the optimization6.

Why L1 gives you zeros and L2 does not

This is easier to see than to prove. Below, the ellipses are the contours of the unregularized loss and the shaded region is what the penalty allows; the penalized solution sits where the smallest ellipse touches that region. Drag the black dot β€” the unregularized optimum β€” and watch what each penalty does with it.

drag the black dot to move the unregularized optimum

The shapes are the whole argument. The L2 region is a circle, which is smooth everywhere, so the point where it first touches an ellipse is almost never on an axis β€” both coordinates end up small but alive. The L1 region is a diamond, and a diamond is all corners; the corners lie on the axes, and a shrinking ellipse hits a corner for a wide range of directions it could have come from. That is sparsity: not a numerical accident, but a consequence of the shape.

Try dragging \(\hat{w}\) close to one axis and raising \(\lambda\) β€” the L1 solution snaps onto the axis and stays there, while the L2 solution slides toward the origin without ever arriving.

Check yourself β€” you want to drop 90 of 100 input features. L1 or L2?

L1. L2 will shrink the useless coefficients toward zero without ever arriving, so you still have 100 features and no way to say which ten matter; you would have to pick a threshold by hand. L1 puts exact zeros in the vector and the selection is done for you β€” that corner of the diamond is the whole reason to prefer it. If you want both the selection and stable coefficients among correlated features, that combination is elastic net: \(\lambda_1\lVert w\rVert_1 + \lambda_2\lVert w\rVert_2^2\).

Weight decay is not L2 penalty once you use Adam

Adding \(\frac{\lambda}{2}\lVert w \rVert^2\) to the loss and decaying the weights by \((1-\eta\lambda)\) each step are the same operation for plain SGD β€” as the derivation above shows. They stop being the same the moment the optimizer rescales the gradient per parameter, because the penalty gets rescaled along with it. That is precisely the bug AdamW fixes, and it is covered in the optimization chapter. In PyTorch: torch.optim.AdamW(..., weight_decay=0.01) decays; Adam(..., weight_decay=0.01) penalizes, and they do not mean the same thing.

Do not penalize everything

Penalize weight matrices. Leave out the biases (they shift, they do not scale, and shrinking them just biases the output) and the gain/offset of every normalization layer (shrinking \(\gamma\) toward 0 fights the very thing the layer is there to do). Typical \(\lambda\): \(10^{-5}\) to \(10^{-2}\) for SGD-trained CNNs, \(0.01\)–\(0.1\) for AdamW-trained Transformers.

Dropout: break the co-adaptations

Dropout7 attacks a different failure. A network can memorize by building co-adapted groups of neurons: detectors that only work because some other specific neuron is there to correct them. Such a committee is brittle, and it is exactly what fails on new data.

The fix is brutal. At every training step, each unit is deleted independently with probability \(p\):

\[ m_j \sim \text{Bernoulli}(1-p), \qquad \tilde{h}_j = \frac{m_j}{1-p}\, h_j \]

No unit can rely on any other being present, because any of them may be gone on the next step. Each neuron is forced to be useful on its own, and the network stops putting all its confidence in a few fragile paths.

There is a second reading of the same equation. Every mask is a different thinned subnetwork, and with \(n\) units there are \(2^n\) of them sharing weights. Training with dropout trains that whole ensemble a little at a time, and testing without it approximates averaging their predictions β€” an ensemble at the price of a single model.

Left: a standard net with two hidden layers. Right: one thinned net produced by dropout; the crossed units were dropped for that step. Source: Srivastava et al.7

The \(1/(1-p)\) everyone gets wrong

Deleting units at training time lowers the expected value of what the next layer receives. That has to be compensated, and there are two ways to do it:

when what happens at test time
Original dropout (the 2014 paper7) multiply weights by \(1-p\) at test time inference must know \(p\) and rescale
Inverted dropout (what everyone implements) divide by \(1-p\) during training nothing β€” inference is a plain forward pass

Every framework in use today does the inverted version, which is why model.eval() simply turns dropout off rather than rescaling anything. Watch the expectation being preserved:

Let it run: the average of the training output converges to the inference output, whatever \(p\) is. That is the entire point of dividing by \(1-p\) β€” the next layer sees the same expected input in both modes, so the network you train is the network you deploy. Push \(p\) to 0.9 and watch the average take much longer to settle: heavy dropout preserves the mean but adds enormous variance, which is why it slows convergence.

Forgetting model.eval()

Dropout active at inference makes predictions random, and batch normalization keeps updating its running statistics. It is the most common bug in student code β€” and it does not crash, it just quietly makes results worse and irreproducible. Always model.eval() for validation and inference, model.train() to go back.

Where dropout is used today

Fully connected layers Its home ground. \(p = 0.2\)–\(0.5\)
Convolutional layers Rarely, and with small \(p\); the weight sharing already regularizes, and BatchNorm competes
Transformers Yes, but modestly (\(p = 0.1\) typical) on attention and MLP blocks, plus stochastic depth
Output layer Never
Input layer Occasionally, with small \(p\) (10–20%) β€” closer to data augmentation than to dropout

Dropout and BatchNorm disagree

Placed together, they fight: dropout changes the variance of the activations between training and inference, while BatchNorm memorized statistics from the training regime. The mismatch is documented8, and the practical rules that came out of it are to put dropout after the normalization layer, never between convolution and BN, and to use less of both than you would use of either alone. When modern architectures pick one, they usually pick normalization.

Normalization layers

Batch normalization9 standardizes the input of a layer using the statistics of the current mini-batch:

\[ \hat{x} = \frac{x - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}}, \qquad y = \gamma \hat{x} + \beta \]

where \(\mu_B\) and \(\sigma_B^2\) are the mean and variance over the batch, and \(\epsilon \approx 10^{-5}\) keeps the root away from zero. The learned \(\gamma\) and \(\beta\) then let the layer undo the normalization if that is what helps β€” including recovering the identity, so the layer can never cost the network representational power.

The \(\epsilon\) goes inside the square root

\(\dfrac{x-\mu}{\sqrt{\sigma^2+\epsilon}}\), not \(\dfrac{x-\mu}{\sigma+\epsilon}\). The second form is a common typo and it is not equivalent: it changes the scale of every activation by a quantity that depends on \(\sigma\), instead of merely guarding a division.

Training and inference are different computations

This is the part that trips people up, and it is worth being exact about, because it is also where the regularizing effect comes from.

statistics used \(\gamma,\beta\) running stats
Training (model.train()) mean/variance of the current batch learned updated
Inference (model.eval()) fixed running averages collected during training learned frozen

At training time a sample's normalized value depends on which other samples landed in its batch β€” so the same image gets a slightly different representation depending on its company. That is noise injected into the forward pass, and it is why batch normalization regularizes at all. At inference the batch is gone and the network must be deterministic, so the stored averages take over.

Note what this does not say: batch normalization is not "turned off" at inference. It always runs β€” it just switches which statistics it uses. Turning it off would change the scale of every activation and destroy the model.

That last paragraph is easier to believe once you have seen it. Below, one fixed activation \(x^\star = 2.3\) is normalized over and over, each time inside a freshly drawn batch. Nothing about the sample changes β€” only its company does.

The orange histogram is the same number computed hundreds of times, and it is wide. The green line is what inference computes β€” one value, always. Two things to try:

  1. Drag B from 4 to 128. The histogram tightens around the inference value: bigger batches mean less noise, and so less regularization. This is the reason batch normalization stops helping as a regularizer at very large batch sizes, and why people then add back the noise elsewhere.
  2. Go back to B = 4 and look at the mean of the histogram, not just its width. With very small batches the training-time value is not only noisy, it is biased away from what inference will compute β€” which is why BatchNorm degrades badly at tiny batch sizes and GroupNorm or LayerNorm are used instead.
Check yourself β€” why does BatchNorm regularize but LayerNorm does not?

Because the noise comes from the batch composition, not from the normalization. LayerNorm normalizes each sample by its own features: draw a different batch and nothing about that sample's normalized value changes, so there is no histogram to widen β€” nothing random is injected. It buys determinism (training and inference compute the same thing) and pays for it by giving up the regularizing effect.

Why it works β€” not the reason originally given

The original paper explained the benefit as reducing internal covariate shift: the drift in each layer's input distribution as the layers below it learn. That story was tested directly and did not hold β€” a network with noise deliberately injected after normalization, so the shift is restored, trains just as well10. The current explanation is that normalization makes the loss surface smoother and its gradients more predictable, which is what allows the larger learning rates. The technique works; the mechanism in the 2015 abstract does not.

The backward pass: one sample moves them all

On the way forward, \(\mu\) and \(\sigma^2\) come from the batch. That has a consequence almost everyone gets wrong when implementing it by hand: a sample's gradient does not depend on that sample alone. Nudging \(x_i\) moves the mean and the variance, and with them the \(\hat{x}_j\) of every other sample in the batch.

Writing \(g_i = \gamma\,\partial L/\partial y_i\), with \(m\) for the batch size:

\[ \frac{\partial L}{\partial x_i} \;=\; \frac{1}{m\,\sigma} \left( m\,g_i \;-\; \sum_j g_j \;-\; \hat{x}_i \sum_j g_j\,\hat{x}_j \right) \]

The layer's own two parameters are simpler: each one adds up, over the batch, the gradient that reaches it.

\[ \frac{\partial L}{\partial \gamma} = \sum_i \frac{\partial L}{\partial y_i}\,\hat{x}_i \qquad\qquad \frac{\partial L}{\partial \beta} = \sum_i \frac{\partial L}{\partial y_i} \]

The two sums inside \(\partial L/\partial x_i\) are the two extra paths: the first is the one through the mean, the second the one through the variance. Drop them and the expression collapses to \(g_i/\sigma\), the classic mistake of anyone writing the layer from scratch β€” and it does not blow up, it just trains worse.

There is a cheap check on the formula: \(\sum_i \partial L/\partial x_i = 0\), always. Shifting the whole batch changes nothing after normalization, so the gradient cannot have a component along that direction. The script below prints that sum and compares the formula with finite differences.

"""A passagem reversa da batch normalization, conferida por diferenΓ§as finitas.

O ponto da conta: como ΞΌ e σ² dependem do batch inteiro, o gradiente de UMA amostra
carrega termos de TODAS as outras. Γ‰ por isso que a forma compacta tem duas somas.
"""
import numpy as np

EPS = 1e-4

def forward(x, gamma, beta):
    mu, var = x.mean(), x.var()
    xhat = (x - mu) / np.sqrt(var + EPS)
    return gamma * xhat + beta, xhat, np.sqrt(var + EPS)

def backward(dy, xhat, sigma, gamma):
    m = dy.size
    g = dy * gamma                                    # βˆ‚L/βˆ‚xΜ‚
    dx = (m * g - g.sum() - xhat * (g * xhat).sum()) / (m * sigma)
    return dx, (dy * xhat).sum(), dy.sum()            # dx, dgamma, dbeta

x = np.array([1.0, 2.0, 3.0, 4.0])
dy = np.array([0.1, 0.2, -0.1, 0.3])
gamma, beta = 1.0, 0.0

y, xhat, sigma = forward(x, gamma, beta)
dx, dgamma, dbeta = backward(dy, xhat, sigma, gamma)

h, num = 1e-6, np.zeros_like(x)
for i in range(x.size):
    xp, xm = x.copy(), x.copy()
    xp[i] += h; xm[i] -= h
    num[i] = ((dy * forward(xp, gamma, beta)[0]).sum() - (dy * forward(xm, gamma, beta)[0]).sum()) / (2 * h)

np.set_printoptions(precision=4, suppress=True)
print("x       =", x)
print("xΜ‚       =", xhat, " (mΓ©dia 0, desvio 1)")
print("dy      =", dy)
print("dx      =", dx)
print("dgamma  =", round(dgamma, 4), " dbeta =", round(dbeta, 4))
print("soma de dx =", round(dx.sum(), 12), " β€” sempre zero: deslocar o batch inteiro nΓ£o muda xΜ‚")
print("erro mΓ‘ximo contra diferenΓ§as finitas:", f"{np.abs(num - dx).max():.1e}")
x       = [1. 2. 3. 4.]
xΜ‚       = [-1.3416 -0.4472  0.4472  1.3416]  (mΓ©dia 0, desvio 1)
dy      = [ 0.1  0.2 -0.1  0.3]
dx      = [ 0.0179  0.0805 -0.2147  0.1163]
dgamma  = 0.1342  dbeta = 0.5
soma de dx = 0.0  β€” sempre zero: deslocar o batch inteiro nΓ£o muda xΜ‚
erro mΓ‘ximo contra diferenΓ§as finitas: 1.7e-10

LayerNorm is the same formula with the sums running over one sample's features instead of over the batch. It is literally the arithmetic the depth panel of chapter 9 runs at every layer.

LayerNorm, and why Transformers use it instead

Batch normalization has a structural weakness: it needs a batch. That breaks with batch size 1, with variable-length sequences, and it makes training and inference behave differently by construction.

LayerNorm11 normalizes over the features of a single sample instead of over the batch:

\[ \mu = \frac{1}{d}\sum_{j=1}^{d} x_j, \qquad \sigma^2 = \frac{1}{d}\sum_{j=1}^{d}(x_j - \mu)^2 \]

Each sample is now normalized by its own statistics. There is nothing to average across the batch, so there are no running statistics, training and inference are the same computation, and batch size is irrelevant. That is why every Transformer uses it β€” and also why LayerNorm does not regularize: no batch, no noise from the batch composition.

The two are easiest to tell apart on a concrete matrix. Below, rows are the samples of a batch and columns are features. Click a cell and the purple outline marks the values its \(\mu\) and \(\sigma\) come from; the box underneath writes out the arithmetic, with \(\gamma = 1\) and \(\beta = 0\). Every button changes one thing and reports what it did to the output of sample 1, the orange row.

Four things to try, each isolating one property:

  1. Click a cell and switch between the two. BatchNorm's group is the column β€” one feature, across the batch. LayerNorm's is the row β€” one sample, across its features. Same formula, other axis.
  2. Redraw the other samples. Under BatchNorm the output of sample 1 moves, though sample 1 did not change: that is the noise the histogram above showed, made of its company. Under LayerNorm it does not move at all.
  3. Drag B down to 1. BatchNorm is left with one value per column, which is its own mean: every output becomes 0 and the input is erased. PyTorch refuses to train like that and raises an error. LayerNorm does not notice.
  4. Multiply sample 1 by 10, then put feature 3 in thousands. Each normalization is blind to exactly one of these. LayerNorm erases the scale of a sample, so Γ—10 leaves its output identical. BatchNorm erases the scale of a feature, so the thousands leave every output identical. The other one pays: under BatchNorm the Γ—10 sample drags the mean and deviation of every column and the rest of the batch moves with it; under LayerNorm feature 3 takes over each sample's \(\sigma\) and the other four features come out almost equal, at about \(-0.5\).

That last pair is the practical rule. When features live on different scales β€” tabular inputs, the channels of a CNN β€” per-feature statistics are what you want: BatchNorm. When every coordinate of a vector is the same kind of quantity and samples differ in overall size β€” the embedding of a token β€” per-sample statistics are: LayerNorm, which also brings no dependence on the batch.

Stopping early

Train long enough on a model with spare capacity and it will begin to fit the noise. Validation loss records the moment: it falls with the training loss for a while, bottoms out, and then rises while the training loss keeps going down. Early stopping is the decision to keep the model from the bottom of that curve.

The recipe has three parts, and the third is the one people forget:

  1. Evaluate on a validation split every epoch (or every \(k\) steps).
  2. Patience12 β€” how many evaluations without improvement you tolerate before stopping. Too small and you quit on noise; too large and you waste compute past the point of no return.
  3. Restore the best checkpoint. Stopping is not enough β€” the weights in memory when you stop are the worse ones, from patience epochs past the minimum. Keep a copy of the best.

Below, a degree-15 polynomial is fitted to 10 noisy points by plain gradient descent. Nothing is penalized and nothing is dropped; the only thing that varies is when you stop.

The right-hand panel is the point: one training run, two very different models. The green curve β€” the one you keep β€” follows the truth. The red one cost 160 times more compute (20 000 epochs against 125), threads every training point, and is worse everywhere in between. Its training loss is genuinely lower. That is the whole trap.

Check yourself β€” your validation loss is still falling at the last epoch. Was early stopping useless?

No β€” it told you something: you undertrained. The curve never turned around, so the best checkpoint is the last one and stopping changed nothing, which is exactly the information you needed to justify training longer. Early stopping costs one validation split and it is never a mistake to have it on; it just does not always fire.

Early stopping is regularization, in a precise sense

It is not a heuristic bolted on the side. For a linear model trained by gradient descent from zero, stopping after \(t\) steps yields almost exactly the ridge solution with \(\lambda \approx 1/(\eta t)\): gradient descent picks up the well-determined directions of the data first and the noisy, poorly-determined ones last, so stopping early leaves the latter near zero β€” the same thing the L2 penalty does explicitly13. Training longer and regularizing less are two names for one dial.

More data, and how to manufacture it

Go back to the first simulator and drag the number of points from 12 to 60 with degree 14 and no penalty. The model that was useless becomes fine. Nothing about the model changed β€” the constraint came from the data. This is worth stating plainly, because it reorders every priority in this chapter: when overfitting is the problem, more data is the best available answer, and everything else is what you do when you cannot get any.

You often can manufacture some, though, by exploiting invariances you already know about:

A rotated cat is still a cat, so a rotated training image is a free extra example. Each transformation you apply asserts an invariance the model should have β€” which is why augmentation is domain knowledge in disguise, and why the wrong transformation hurts: flipping digits horizontally teaches the model that 2 and 5 are the same thing.

train_tf = transforms.Compose([
    transforms.RandomResizedCrop(224, scale=(0.6, 1.0)),
    transforms.RandomHorizontalFlip(),          # a cat, mirrored, is a cat
    transforms.ColorJitter(0.3, 0.3, 0.3),      # so is a cat under warmer light
    transforms.ToTensor(),
])
val_tf = transforms.Compose([                   # never augment validation
    transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(),
])

Automated policies (RandAugment14 and its relatives) search the transformation set for you and are now standard in vision.

Instead of asking for probability 1 on the correct class, ask for \(1-\alpha\):

\[ y^{\text{LS}}_k = (1-\alpha)\, y_k + \frac{\alpha}{K} \]

With one-hot targets the cross-entropy loss is minimized only as the correct logit runs to infinity, which is a direct invitation to overconfidence. Smoothing (\(\alpha = 0.1\) typically) gives the optimum a finite location15. It reliably improves accuracy and calibration β€” though it also collapses the geometry of the penultimate layer, which hurts if you plan to use those embeddings for retrieval or distillation16.

Train on convex combinations of pairs of examples and their labels17:

\[ \tilde{x} = \lambda x_i + (1-\lambda) x_j, \qquad \tilde{y} = \lambda y_i + (1-\lambda) y_j \]

with \(\lambda \sim \text{Beta}(\alpha, \alpha)\). It sounds absurd β€” half-cat, half-dog images with half-and-half labels β€” and it works, because it forces the model to behave linearly between training examples instead of doing something arbitrary there. That "something arbitrary between the points" is exactly what the overfitted polynomial was doing.

Label smoothing and mixup are both easier to see than to describe: one changes where the target is, the other changes which examples exist.

On the left, drag \(\alpha\). At \(\alpha = 0.05\) almost every \(\lambda\) lands near 0 or 1, so the mixed points sit right next to real examples β€” a mild edit. At \(\alpha = 2\) the middle is sampled just as often, and the training set fills up with points that are genuinely half of each class. That is why \(\alpha\) between 0.1 and 0.4 is the usual choice for images: enough to smooth the space between examples, not enough to spend the model on chimeras.

On the right, drag \(\varepsilon\). At \(\varepsilon = 0\) the target is a hard 1 and the optimum is an infinite logit gap β€” the loss keeps pushing forever, which is exactly what overconfidence is. Any \(\varepsilon > 0\) puts the optimum at a finite, reachable place.

Check yourself β€” mixup on a medical dataset?

Ask what a half-example means. Half a cat and half a dog is a picture nobody will ever see, and that is fine: the point is only to force the model to interpolate smoothly between them. But half a "benign" and half a "malignant" tumour asserts that a 50/50 label is a meaningful thing to be, and it can also blend two patients' scans into an anatomically impossible image. Mixup is a claim about the geometry of your label space β€” make sure the claim is true before you use it.

Putting it together

No single technique is the regularizer; real training stacks several, and they are not interchangeable. Sorted by what they actually do:

Mechanism Technique Where it earns its place
Constrain the weights L2 / weight decay Almost always on. 1e-4 with SGD, 0.01–0.1 with AdamW
L1 When you want sparsity or feature selection, rarely in deep nets
Inject noise Dropout Fully connected layers, Transformer blocks. \(p = 0.1\)–\(0.5\)
Batch normalization CNNs β€” as a side effect; its main job is optimization
Augmentation, mixup Vision and audio, where invariances are known. The strongest lever after raw data
Limit the search Early stopping Free, always. It costs one validation split
Soften the target Label smoothing Classification with many classes, \(\alpha = 0.1\)
Add data More labeled data Beats every row above. Try this first

And now you can stack them yourself. The simulator below trains a real network β€” 2 β†’ h β†’ h β†’ 1, tanh, full-batch gradient descent β€” on 40 points from two interleaving moons, 8% of which have the wrong label. Every control is one of the techniques above, and the left panel shows the decision boundary the model actually learned while the right panel shows both losses.

2 β†’ h β†’ h β†’ 1, tanh, full-batch gradient descent, 3000 epochs

Five experiments, in order. Each one takes about fifteen seconds:

  1. Everything off. Train with \(\lambda = 0\), \(p = 0\), \(\sigma = 0\), 24 units. Uncheck show the early-stopped model to see the final weights: the training loss keeps falling while the validation loss turns around and climbs, and the boundary grows a little island around every mislabelled point. Around 95% train against 78% validation.
  2. Check the box again. Same run, same compute β€” but reading the weights from the best validation epoch instead of the last one recovers validation accuracy, from 78% to 81% on this draw of the data. That is early stopping, and it cost nothing.
  3. Raise the weight decay. Around \(\lambda \approx 10^{-2}\) the islands dissolve and the boundary becomes one smooth curve; validation catches up with training. Keep going to the top of the slider and watch it collapse into underfitting: the penalty eventually drowns the signal too.
  4. Turn weight decay off and raise dropout. \(p = 0.2\) helps. \(p = 0.4\) on a network this small destroys it β€” there is not enough redundancy left for half the units to disappear. Dropout is a technique for networks with capacity to spare.
  5. Turn everything off and drag the training points from 40 to 200. The gap closes on its own, with no regularizer at all. That is the ordering of this chapter in one experiment.
Check yourself β€” the boundary is smooth, but validation is worse than training by 15 points. What now?

Nothing on this page, yet. A large, stable gap with a sensible-looking boundary usually means the two sets are not measuring the same thing: a validation split that is too small to be reliable, a distribution shift between the splits, or a leak. Check the split before you reach for a regularizer β€” every technique here assumes train and validation come from the same distribution, and none of them fixes a broken split.

A sane order to try things

  1. Get the model to overfit first. A model that cannot overfit a small subset has a bug, not a regularization problem β€” and no amount of dropout will fix a broken pipeline.
  2. Turn on the free ones: weight decay and early stopping.
  3. Add augmentation if the domain has invariances you can name.
  4. Only then reach for dropout, label smoothing, mixup β€” one at a time, measuring each.
  5. Shrink the model last. Capacity is rarely the real problem.

Reading the two curves

What you see What it means What to do
Train ↓, validation ↓, close together Healthy, still learning Keep training
Train ↓, validation flat then ↑ Overfitting from here on Stop early; add regularization; get data
Train and validation both high, close Underfitting More capacity, longer training, less regularization
Train ≫ validation error (validation better) Dropout/BN still on at eval, or a leak between splits Check model.eval(), check the split
Validation jumping around wildly Validation set too small, or learning rate too high Larger split, lower Ξ·
Perfect validation score A leak. Always a leak Audit how the split was built

Key takeaways

  1. The goal is the generalization gap, not the training loss. A training loss of zero is a symptom to investigate, not an achievement.
  2. Underfitting and overfitting are diagnosed by two numbers, never one: train error and validation error, read together.
  3. Overfitting shows up in the weights β€” huge coefficients of alternating sign β€” which is what makes penalizing them work at all.
  4. L2 shrinks weights proportionally and never reaches zero; L1 pushes with constant force and produces exact zeros. The difference is the shape of the region each one allows.
  5. Dropout breaks co-adaptation and approximates an ensemble of \(2^n\) subnetworks. Modern implementations scale by \(1/(1-p)\) during training, so inference is a plain forward pass.
  6. Normalization helps optimization first and regularizes second; batch norm's regularizing noise comes from the batch composition, which is exactly what LayerNorm gives up.
  7. Early stopping is not a heuristic β€” for linear models it is provably close to ridge, with \(\lambda \approx 1/(\eta t)\).
  8. More data beats all of it. Augmentation is the second-best thing, because it is more data that you built out of what you already knew.

Additional Resources

Three places to go next, in the order that makes sense:

  1. Machine Learning Fundamentals: Bias and Variance β€” Starmer, J., StatQuest. Ten minutes on the decomposition this chapter opens with, at a gentler pace and with a different set of pictures. Watch it first if the bias-variance section felt abstract:

  2. A Recipe for Training Neural Networks β€” Karpathy, A. The practical counterpart to this page: the order in which to actually do things, why you should overfit a single batch before anything else, and a catalogue of the mistakes that look like overfitting but are bugs.

  3. Deep Learning, chapter 7 β€” Regularization for Deep Learning β€” Goodfellow, I., Bengio, Y., & Courville, A. The reference treatment, with the derivations this page only gestures at β€” including the proof that early stopping and L2 coincide for linear models.

References

The works cited through the text, in order of appearance:


  1. Zhang, C., Bengio, S., Hardt, M., Recht, B., & Vinyals, O. (2017). Understanding deep learning requires rethinking generalization β€” ICLR. Networks fit perfectly random labels, which no bound based on weight size anticipates. β†©

  2. Underfitting and Overfitting in Machine Learning β€” GeeksforGeeks. Source of the figure above. β†©

  3. Belkin, M., Hsu, D., Ma, S., & Mandal, S. (2019). Reconciling modern machine-learning practice and the classical bias–variance trade-off β€” PNAS 116(32), 15849–15854. Where the double-descent curve was named. β†©

  4. Nakkiran, P., Kaplun, G., Bansal, Y., et al. (2021). Deep Double Descent: Where Bigger Models and More Data Hurt β€” ICLR. Shows the effect in real networks, along model size, dataset size and training time. β†©

  5. Hoerl, A. E., & Kennard, R. W. (1970). Ridge Regression: Biased Estimation for Nonorthogonal Problems β€” Technometrics 12(1), 55–67. The L2 penalty, from statistics, decades before deep learning. β†©

  6. Tibshirani, R. (1996). Regression Shrinkage and Selection via the Lasso β€” Journal of the Royal Statistical Society B 58(1), 267–288. The L1 penalty, and the sparsity argument drawn above. β†©

  7. Srivastava, N., Hinton, G., Krizhevsky, A., Sutskever, I., & Salakhutdinov, R. (2014). Dropout: A Simple Way to Prevent Neural Networks from Overfitting β€” Journal of Machine Learning Research 15(56), 1929–1958. Mind the notation: in the paper \(p\) is the probability of keeping a unit, so it says the weights are multiplied by \(p\) at test time. This page uses \(p\) for the probability of dropping, which is what every framework's Dropout(p) means β€” hence \(1-p\) here. β†©β†©β†©

  8. Li, X., Chen, S., Hu, X., & Yang, J. (2019). Understanding the Disharmony between Dropout and Batch Normalization by Variance Shift β€” CVPR. Measures the mismatch and derives the placement rules. β†©

  9. Ioffe, S., & Szegedy, C. (2015). Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift β€” ICML. β†©

  10. Santurkar, S., Tsipras, D., Ilyas, A., & Madry, A. (2018). How Does Batch Normalization Help Optimization? β€” NeurIPS. The experiment that dismantles the internal-covariate-shift explanation. β†©

  11. Ba, J. L., Kiros, J. R., & Hinton, G. E. (2016). Layer Normalization. β†©

  12. Prechelt, L. (1998). Early Stopping β€” But When? β€” in Neural Networks: Tricks of the Trade, Springer LNCS 1524. Compares stopping criteria on real runs; the patience heuristics still in use come from here. β†©

  13. Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning, chapter 7 β€” MIT Press. Section 7.8 proves the early-stopping / L2 equivalence for linear models. β†©

  14. Cubuk, E. D., Zoph, B., Shlens, J., & Le, Q. V. (2020). RandAugment: Practical Automated Data Augmentation with a Reduced Search Space β€” NeurIPS. β†©

  15. Szegedy, C., Vanhoucke, V., Ioffe, S., Shlens, J., & Wojna, Z. (2016). Rethinking the Inception Architecture for Computer Vision β€” CVPR. Section 7 introduces label smoothing. β†©

  16. MΓΌller, R., Kornblith, S., & Hinton, G. (2019). When Does Label Smoothing Help? β€” NeurIPS. Confirms the gains, and shows what it does to the penultimate layer. β†©

  17. Zhang, H., CissΓ©, M., Dauphin, Y. N., & Lopez-Paz, D. (2018). mixup: Beyond Empirical Risk Minimization β€” ICLR. β†©