📚 Study Notes / Home / Neural Nets / Session 6
Session 06 · Regularization

Regularization — Fighting Overfitting

Your network can train perfectly on its homework and still flunk the real exam. That gap — between "memorising the training data" and "actually learning the pattern" — is the single biggest practical problem in deep learning. This session is the toolbox for fixing it. We assume you've studied none of this before. Every topic opens with a tiny "explain like I'm 5" story, then we go deeper with real math and code. Take it slow.

⏱ 21 min read📖 5 topics

1 Overfitting vs underfitting — the bias–variance trade-off


Explain like I'm 5

Imagine studying for a maths test. One friend memorises every answer in the practice book word-for-word — but on the real test, with new numbers, they panic and fail. Another friend barely studies and only learned "maths is about numbers" — they fail too, for the opposite reason. The best student learns the actual rules, so any new question is easy. A neural network can be like any of these three friends. Our job this session is to coach it into being the good student.

When we train a network we always have (at least) two piles of data:

  • The training set — the examples the model learns from (its "practice book").
  • The validation set (and later a held-out test set) — examples it has never seen, used to check whether it truly learned the pattern.

What we actually care about is generalization: doing well on data the model has never seen. Doing well on the training data alone is worthless — that's just memorising.

The two failure modes

ProblemWhat it looks likeWhy it happens
UnderfittingBad on training data and bad on validation data.The model is too simple / under-trained to capture the real pattern. (The lazy friend.)
OverfittingGreat on training data, but much worse on validation data.The model is powerful enough to memorise the noise and quirks of the training set instead of the underlying rule. (The memoriser.)
Good fitGood on training, and almost as good on validation.The model captured the true signal and ignored the noise. (The good student.)

The bias–variance trade-off

These two failure modes have formal names that you'll hear constantly:

  • Bias — error from the model being too simple to represent the truth. High bias → underfitting. (Think: trying to fit a straight line through a curve.)
  • Variance — error from the model being too sensitive to the exact training data, so it changes wildly if you'd trained on a slightly different sample. High variance → overfitting.
The big idea

There's a tug-of-war: making a model more flexible lowers bias but raises variance, and making it simpler does the opposite. The sweet spot is the model complexity that minimises total error on unseen data. Regularization is the family of tricks that lets us keep a big, powerful network but push it back toward that sweet spot — getting the low bias of a big model without paying the full variance price.

Total expected error on new data decomposes (roughly) as:

# Expected test error
Error = Bias²  +  Variance  +  Irreducible noise
        #  ↑ too simple   ↑ too sensitive   ↑ can't fix

You can't remove the irreducible noise (the world is messy). You can trade between bias and variance, and that's the whole game.

How you actually see overfitting: the learning curves

The classic diagnostic is to plot training loss and validation loss against training time (epochs). Recall from Session 5 that loss is the number the optimizer is trying to push down. Here's the tell-tale diagram:

📉
Both fall
Early on, train & val loss both drop — model is genuinely learning.
🎯
Sweet spot
Val loss bottoms out. Best generalization is here.
↗️
Diverge
Train loss keeps falling, but val loss starts rising — overfitting begins.
🧠
Memorising
Train loss ≈ 0, val loss high. The model has memorised noise.

That growing gap between the two curves is the visual signature of overfitting. An underfitting diagram looks different: both curves plateau high and close together — the model never got good even on training data.

Worked example: fitting points with a polynomial

Say the true relationship is a gentle curve and you have 10 noisy data points.

  • Degree-1 (a line): can't bend to follow the curve → high bias → underfit.
  • Degree-3 (a smooth curve): follows the real shape, ignores the wiggles → good fit.
  • Degree-9 (very wiggly): passes exactly through all 10 points, including the noise. Training error is zero, but ask it about a point in between and it shoots off to nonsense → high variance → overfit.

A neural network with millions of parameters is like that degree-9 polynomial: easily flexible enough to memorise. Regularization is how we stop it.

Watch out

"99% training accuracy!" means nothing on its own — a model can hit that by pure memorisation. Always judge a model by its validation/test performance. If training is great but validation is poor, you're overfitting, full stop.

Recap Underfitting (high bias) = too simple, bad everywhere. Overfitting (high variance) = memorises the training noise, great on train but poor on new data. Total error trades bias against variance, and the gap between training and validation loss curves is how you spot overfitting. Regularization is our toolkit for keeping big models near the sweet spot.

2 Weight decay — L1 & L2 regularization


Explain like I'm 5

Imagine you're packing for a trip and your suitcase charges you money for every kilogram. Suddenly you only pack what truly matters and keep everything light. Weight decay does this to a network: it makes the model "pay a fee" for having big, heavy numbers (weights) inside it. To avoid the fee, the network keeps its weights small — and small, gentle weights make smoother, simpler predictions that don't overreact to noise.

Recall from Session 5 that training means minimising a loss function — say the prediction error L(w), where w is the giant pile of weights. Weight decay adds a penalty term that grows as the weights grow, so the optimizer is now told: "fit the data and keep the weights small."

L2 regularization (the most common one)

L2 regularization adds the sum of squared weights to the loss:

# L2-regularized loss
L_total(w) = L_data(w)  +  λ · Σ wᵢ²
             #   ↑ fit the data   ↑ penalty for big weights

The knob λ (lambda) is the regularization strength — a hyperparameter you choose. Bigger λ = more pressure to shrink weights = simpler model.

  • λ = 0 → no regularization at all (free to overfit).
  • λ too large → weights crushed toward zero → the model can't fit → underfit.
  • Goldilocks λ → just enough pressure to stay near the sweet spot.
Why "decay"? The connection to the update step

Recall gradient descent from Session 5: w ← w − η · ∂L/∂w. The L2 penalty's gradient is 2λw, so the update becomes:

w ← w − η·(∂L_data/∂w)  −  η·2λ·w
  = (1 − 2ηλ)·w  −  η·(∂L_data/∂w)
    #  ↑ every step, w is multiplied by a number < 1 — it DECAYS toward zero

So before each normal update, every weight is nudged a little toward zero. That's literally "weight decay." (In plain SGD, L2 penalty and weight decay are the same thing; with fancier optimizers like Adam they differ slightly, which is why the variant AdamW exists.)

L1 regularization

L1 regularization penalises the sum of absolute values instead of squares:

# L1-regularized loss
L_total(w) = L_data(w)  +  λ · Σ |wᵢ|

This small change has a big consequence: L1 tends to drive many weights to exactly zero, producing a sparse model (lots of dead connections). L2 instead makes all weights small but rarely exactly zero.

Worked example: why L1 zeroes things out and L2 doesn't

Think about the penalty's "pull" on a single tiny weight, say w = 0.01:

  • L2 penalty gradient is 2λw = 2λ(0.01) — almost nothing. As w shrinks, the pull shrinks too, so it eases off and the weight settles near (but not at) zero.
  • L1 penalty gradient is λ·sign(w) = +λ — a constant push toward zero regardless of how small w is. So it keeps pushing until the weight hits exactly zero and stays there.

Result: L1 acts like an automatic feature selector (keep the useful weights, delete the rest); L2 acts like a smoother (shrink everything gently).

AspectL1 (Lasso)L2 (Ridge / weight decay)
PenaltyΣ |wᵢ|Σ wᵢ²
Effect on weightsMany become exactly 0 → sparseAll shrink, smoothly → small
Good forFeature selection, compact modelsGeneral smoothing, the default in deep nets
GeometryDiamond-shaped constraint (corners on axes)Circular constraint (no corners)
# PyTorch: L2 weight decay is built into the optimizer
optimizer = torch.optim.SGD(model.parameters(),
                            lr=0.1,
                            weight_decay=1e-4)  # this IS λ for L2

# L1 has no built-in flag — you add it to the loss by hand:
l1 = 0.0
for p in model.parameters():
    l1 = l1 + p.abs().sum()
loss = data_loss + 1e-5 * l1
Key takeaway

Big weights let a network make sharp, jumpy decisions that can fit noise. Penalising weight size forces smoother functions that generalise better. L2 is the everyday default in deep learning ("weight decay"); reach for L1 when you specifically want sparsity.

Recap Weight decay adds λ·(penalty on weight size) to the loss. L2 (Σw²) shrinks all weights smoothly toward small values; L1 (Σ|w|) drives many to exactly zero for sparsity. λ is the dial: too small overfits, too big underfits. In code, L2 is just the optimizer's weight_decay.

3 Dropout


Explain like I'm 5

Imagine a class group project where, every single day, the teacher randomly sends some students home. Nobody knows who'll be absent tomorrow, so everyone has to learn the whole project instead of relying on "Aisha always does the slides." The team becomes robust — any subset can get the job done. Dropout does this to neurons: it randomly switches some off during each training step, so no neuron can lazily depend on a specific buddy.

Dropout is one of the most effective and beloved regularizers, introduced by Srivastava et al. in 2014. The idea is almost absurdly simple: during training, on every forward pass, randomly set each neuron's output to zero with probability p (a typical p is 0.5 for hidden layers). The surviving neurons are scaled up to keep the overall signal level steady.

Why on earth does turning off neurons help?

  • It breaks co-adaptation. Co-adaptation is when neurons form fragile little cliques that only work if their specific partners are present — a way of memorising. Random dropping forces each neuron to be useful on its own, building redundant, robust features.
  • It's like training a huge ensemble for free. Each random pattern of dropped neurons is effectively a different thinned network. Over training you implicitly train an astronomical number of these sub-networks that share weights. At test time, using all neurons approximates averaging all those networks — and ensembles (averaging many models) are a classic way to reduce variance.
The big idea

Dropout = cheaply training an exponential number of overlapping sub-networks and averaging them at test time. The forced redundancy stops any neuron from memorising, which is exactly what fights overfitting.

The crucial difference between training and test time

This trips everyone up, so go slow. Dropout behaves differently in the two modes:

Training timeTest / inference time
NeuronsRandomly drop a fraction p each step.Keep all neurons — no dropping.
WhyTo force redundancy & train the ensemble.We want the full, stable network for the real prediction.
Scaling"Inverted dropout": divide surviving outputs by (1−p) so the expected total stays the same.Nothing extra — the training-time scaling already balanced it.

Because of that scaling, at test time you do nothing special — just run the network normally. This is why frameworks make you flip a switch (model.train() vs model.eval()): it tells dropout which mode to use. Forgetting to call .eval() is a classic bug that makes test predictions randomly jittery.

Worked example: dropout in PyTorch
import torch.nn as nn

model = nn.Sequential(
    nn.Linear(784, 256),
    nn.ReLU(),
    nn.Dropout(p=0.5),   # drop 50% of these activations during training
    nn.Linear(256, 10),
)

# TRAINING: dropout is ACTIVE
model.train()
out = model(x_batch)        # random neurons zeroed each call

# EVALUATION: dropout is OFF, full network used
model.eval()
with torch.no_grad():
    preds = model(x_test)   # deterministic, all neurons on

Manually, "inverted dropout" on a layer's activations a looks like:

# Training forward pass, p = prob of DROPPING
mask = (torch.rand_like(a) > p).float()   # 1 = keep, 0 = drop
a = a * mask / (1.0 - p)               # scale up survivors
# Test time: just use `a` as-is, no mask, no scaling
Watch out

Too much dropout (e.g. p = 0.8 everywhere) starves the network and causes underfitting. Common practice: moderate dropout (0.2–0.5) on large fully-connected layers, and little-to-none on convolutional layers (they already share weights heavily). Dropout interacts with Batch Normalization (Topic 5) too — many modern architectures lean more on normalization than on heavy dropout.

Recap Dropout randomly zeroes neurons during training (prob p), scaling survivors up to compensate. It prevents co-adaptation and acts like averaging a huge ensemble of sub-networks, cutting variance. At test time all neurons are used with no dropping — so always switch to model.eval().

4 Early stopping & data augmentation


Explain like I'm 5

Early stopping is like taking a cake out of the oven the moment it's perfectly golden — leave it in longer and it just burns. Data augmentation is like practising a song in different keys, speeds, and rooms: it's the same song, but now you can play it anywhere. One stops you training too long; the other gives you more (varied) practice material — both fight overfitting.

Part A — Early stopping

From Topic 1, validation loss falls, bottoms out, then starts rising as overfitting kicks in. The obvious move: stop training at the bottom. Early stopping watches validation performance and halts training once it stops improving — keeping the model from the point where it was best at generalising.

Because validation loss can wobble up and down a little, we use a patience counter: keep going for a few more epochs after the best score, and only stop if it doesn't beat the best. We also save the best weights, not just the last ones.

🏋️
Train an epoch
Update weights on training data
📋
Check val loss
Measure on the validation set
💾
New best?
Save weights, reset patience
No improvement?
Count down patience
🛑
Patience = 0
Stop, restore best weights
Worked example: an early-stopping loop
best_val = float('inf')
patience, wait = 5, 0

for epoch in range(max_epochs):
    train_one_epoch(model, train_loader)
    val_loss = evaluate(model, val_loader)

    if val_loss < best_val:
        best_val = val_loss
        torch.save(model.state_dict(), "best.pt")  # remember the best
        wait = 0
    else:
        wait += 1
        if wait >= patience:        # no improvement for 5 epochs
            print("Early stopping!")
            break

model.load_state_dict(torch.load("best.pt"))  # roll back to the best
Why it counts as regularization

Stopping early limits how far the weights drift from their small starting values — so effectively it keeps the model simpler, much like weight decay does, but without an explicit penalty term. It's also basically free: you were going to watch validation loss anyway.

Part B — Data augmentation

The deepest cure for overfitting is simply more data — a model can't memorise what it can't keep up with. But collecting and labelling data is expensive. Data augmentation is a clever cheat: create new, slightly-altered copies of your existing examples that are still valid. The model sees more variety and learns the invariances that actually matter.

For images, common augmentations include:

  • Horizontal flip — mirror left↔right. A flipped cat is still a cat.
  • Random crop — take a random sub-region (often after padding). Teaches the model that the object can be off-centre or partly out of frame.
  • Color jitter — randomly nudge brightness, contrast, saturation, hue. A cat under warm lamp light vs cool daylight is still a cat.
  • Small rotations, scaling, translation, adding a little noise, random erasing of patches, and modern mixes like Cutout, Mixup and CutMix.
Watch out — augmentations must preserve the label

A horizontal flip is fine for a cat, but a vertical flip of a handwritten "6" turns it into something like a "9" — now the label is wrong and you've taught the model garbage. Augmentations are domain-specific: choose transforms that genuinely leave the answer unchanged. (And augment only the training set, never validation/test.)

Worked example: image augmentation in PyTorch
from torchvision import transforms

train_tf = transforms.Compose([
    transforms.RandomCrop(32, padding=4),   # shift the object around
    transforms.RandomHorizontalFlip(),         # mirror 50% of the time
    transforms.ColorJitter(brightness=0.2,
                           contrast=0.2,
                           saturation=0.2),    # lighting changes
    transforms.ToTensor(),
])

# Validation/test: NO random augmentation — just resize & tensor-ise
test_tf = transforms.Compose([transforms.ToTensor()])

From a single 32×32 cat photo, these transforms can generate millions of slightly different valid views — so the effective training set explodes, and the model learns "cat-ness" rather than the exact pixels of one photo.

Beyond images

Augmentation isn't only for vision: text can be paraphrased or have synonyms swapped, audio can be time-shifted or have background noise added, and tabular data can get small noise. The principle is universal — generate label-preserving variety.

Recap Early stopping halts training at the validation-loss minimum (with a patience buffer and saving the best weights), keeping the model from drifting into overfitting. Data augmentation manufactures more, varied, label-preserving training examples (flips, crops, color jitter…), so the model learns robust patterns instead of memorising specific samples. Both apply augmentation/checks only on the right split.

5 Batch Normalization


Explain like I'm 5

Imagine a relay race where each runner hands a baton to the next. If one runner sprints wildly fast and the next crawls, the whole team is a mess. It'd help if, between each hand-off, someone reset everyone to a sensible, steady pace. Batch Normalization does that between the layers of a network: it re-centres and re-scales the numbers flowing through, so each layer receives inputs in a calm, consistent range instead of wildly varying ones.

As signals pass through many layers, their scale can drift — getting huge or tiny — which makes training slow and unstable (we'll dig into why deeper, along with weight initialization, in Session 7). Batch Normalization (Ioffe & Szegedy, 2015) tackles this by normalising the activations of a layer across each mini-batch.

What it actually computes

Recall from Session 5 that we train on mini-batches — small groups of examples at a time. For a given activation, BatchNorm looks at its values across all examples in the current mini-batch and standardises them:

# For each feature, over the B examples in the mini-batch:
μ  = mean(x)                 # batch mean
σ² = var(x)                  # batch variance
x̂  = (x − μ) / sqrt(σ² + ε)  # normalize → mean 0, variance 1  (ε avoids /0)
y  = γ · x̂ + β               # scale & shift with LEARNED γ, β

The first three lines force the activation to have mean 0 and variance 1. The last line is the clever part: γ (gamma) and β (beta) are learnable parameters that let the network undo the normalization if that's actually better. So BatchNorm doesn't force a rigid distribution — it gives the network a well-behaved starting point and the freedom to adjust.

The big idea

By keeping each layer's inputs in a steady range, BatchNorm makes the loss landscape smoother. That lets you use higher learning rates, train much faster, and be far less fussy about how you initialize weights. It often improves final accuracy too, and its mini-batch noise adds a mild regularizing side-effect.

Why it speeds up and stabilises training

  • No more exploding/vanishing scale between layers → gradients (Session 5) stay well-behaved, so updates are reliable.
  • Smoother optimization → you can crank the learning rate up and converge in fewer epochs.
  • Mild regularization → because each example's normalization depends on the random other examples in its batch, the network sees slightly noisy activations — a bit like dropout. This is why heavy dropout is often unnecessary when BatchNorm is used.

The train vs inference difference (very important)

Just like dropout, BatchNorm behaves differently in the two modes:

TrainingInference / test
μ and σ²Computed fresh from the current mini-batch.Use a fixed running average of μ and σ² collected during training.
WhyBatch stats give the smoothing & noise benefit.At test time you may predict on a single example — there's no meaningful "batch" to average over, and predictions must be deterministic.

So during training, BatchNorm quietly keeps a moving average of the means and variances it sees; at inference it switches to those frozen statistics. Once again this is governed by model.train() vs model.eval() — forget .eval() and your test predictions will wrongly depend on whatever else happens to be in the batch.

Worked example: BatchNorm in PyTorch
import torch.nn as nn

model = nn.Sequential(
    nn.Linear(784, 256),
    nn.BatchNorm1d(256),   # normalize the 256 activations across the batch
    nn.ReLU(),                # normalize BEFORE the nonlinearity is common
    nn.Linear(256, 10),
)

model.train()   # BN uses this batch's mean/var + updates running stats
loss = criterion(model(x_batch), y_batch)

model.eval()    # BN now uses the stored running mean/var → deterministic
with torch.no_grad():
    preds = model(x_test)

For convolutional networks you'd use nn.BatchNorm2d instead, which normalises per-channel across the batch and spatial positions.

Watch out

BatchNorm relies on having a reasonable batch size — with tiny batches the per-batch mean and variance are noisy and unreliable. In those cases people switch to relatives like Layer Normalization (used heavily in Transformers — recall the GenAI notes), Group Norm, or Instance Norm. We'll meet these and the full initialization story in Session 7.

Recap BatchNorm standardises each layer's activations over the mini-batch (mean 0, variance 1) then rescales with learnable γ, β. This smooths optimization → faster, more stable training, higher learning rates, less init-sensitivity, plus a mild regularizing effect. It uses batch statistics during training but frozen running averages at inference — so, like dropout, mind model.eval().

Putting it all together


You now have a complete toolkit for the central problem of deep learning. Here's the one-paragraph story tying all five topics together:

A powerful network easily overfits — it memorises the training data's noise (high variance) instead of the real pattern, which you spot as a widening gap between the training and validation loss curves. To pull it back toward the bias–variance sweet spot, we regularize. Weight decay (L2 for smoothness, L1 for sparsity) penalises big weights so the function stays smooth. Dropout randomly silences neurons to break co-adaptation and average a huge ensemble. Early stopping halts training at the validation minimum, and data augmentation manufactures more varied, label-preserving examples so there's simply more to learn from. Batch Normalization keeps activations in a steady range, speeding and stabilising training with a regularizing bonus. Two of these — dropout and BatchNorm — behave differently at train vs test time, so always switch with model.eval(). In real projects you combine several of these at once.

Quick self-check

Your model has 99% training accuracy but 70% validation accuracy. What's happening, and name two fixes.

Overfitting (high variance) — it's memorising the training data. Fixes: add weight decay, add dropout, use early stopping, add data augmentation, or get more data. Any two are fine.

What's the practical difference between L1 and L2 regularization?

L1 (Σ|w|) drives many weights to exactly zero, producing a sparse model — good for feature selection. L2 (Σw²) shrinks all weights smoothly toward small (but nonzero) values — the usual default "weight decay" for smoothness.

Why must you call model.eval() before testing a network with dropout or BatchNorm?

Both behave differently at test time. In eval mode dropout stops dropping neurons (uses the full network), and BatchNorm switches from per-batch statistics to stored running averages — making predictions deterministic and correct. Forgetting it makes test outputs randomly depend on the batch.

Why is a vertical flip a bad augmentation for handwritten-digit images?

It can change the label — flipping a "6" can look like a "9", so the example is now mislabelled. Augmentations must preserve the correct answer; choose transforms that genuinely leave the label unchanged.

In one sentence, why does dropout reduce overfitting?

By randomly dropping neurons it prevents them from co-adapting into fragile cliques and effectively averages a huge ensemble of sub-networks, which lowers variance.

Give two concrete benefits Batch Normalization provides during training.

It lets you use higher learning rates and trains faster/more stably (smoother loss landscape, less sensitivity to initialization), and it adds a mild regularizing effect from the batch-statistic noise.

📚 References & Further Reading


Class material

Papers, docs & deep dives