TAI BUI
Back to curriculum
BuildPython

Regularization

Your model hits 99% on training data and 60% on test data. That's not learning, that's memorization. Regularization is the tax you pay on complexity to force the model to generalize instead.

TIME: ~75 minutesPREREQUISITES: Lesson 03.06 (Optimizers)

Learning Objectives

  • Implement dropout with inverted scaling, L2 weight decay, batch normalization, layer normalization, and RMSNorm from scratch
  • Measure the train-test accuracy gap and diagnose overfitting using regularization experiments
  • Explain why transformers use LayerNorm instead of BatchNorm and why modern LLMs prefer RMSNorm
  • Apply the correct combination of regularization techniques based on the severity of overfitting

The Problem

Overfitting is what happens when a model's training performance pulls way ahead of its test performance: the gap between the two is the tell that it memorized the training set's noise instead of learning patterns that generalize.

A neural network with enough parameters can memorize any dataset, and that's not a hypothetical. Zhang et al. (2017) trained standard networks on ImageNet with random labels and watched training loss drop to near zero. The networks memorized a million random input-output pairs with no real pattern behind them. Training loss was perfect. Test accuracy was zero.

The problem only gets worse as models scale up. GPT-3 has 175 billion parameters and trains on roughly 500 billion tokens. With that much capacity, the model can memorize large chunks of the training data verbatim. Without regularization, it would regurgitate training examples instead of learning patterns that generalize.

Every technique in this lesson closes that gap from a different angle. Dropout stops the network from leaning on any single neuron. Weight decay stops any single weight from growing too large. Batch normalization smooths the loss landscape so the optimizer lands on flatter, more generalizable minima. Layer normalization does the same job where batch normalization breaks down: small batches, variable-length sequences. RMSNorm gets there 10% faster by dropping the mean calculation entirely. Each technique on its own is simple. Put together, they're the difference between a model that memorizes and one that generalizes.

Warm-up0/3
1. If a student memorizes the exact answers to last year's practice exam instead of learning the underlying material, how would you expect them to do on this year's exam, which covers the same topics with different questions?Warm-up
Three rows for Topic A, Topic B and Topic C. For each topic, last year's practice exam has a practice question whose answer the student memorized, and this year's exam has a different question the student has never seen.
Illustrative setup.
2. Why do you evaluate a trained model on data it was never trained on?Warm-up
Ten numbered examples. Examples 1 to 8 are marked as used to train the model; examples 9 and 10 are marked as kept aside and never shown to it. The trained model is drawn with a dashed arrow towards an evaluation box labelled evaluate on the 2 kept aside.
Illustrative split.
3. What does the variance of a set of numbers measure?Warm-up
A dot plot on a number line from 0 to 10 of the eight numbers 2, 4, 4, 4, 5, 5, 7, 9, with a dashed vertical line at their mean, 5.
Illustrative numbers.
Check: The Problem0/4
1. What is overfitting in neural networks?Define
A scatter of 10 training points (blue) and 20 held-out points (green rings) with a wiggly degree-9 polynomial curve passing through every training point; the mean squared error is about 5e-28 on the training points and 0.342 on the held-out points.
Fitted with numpy on synthetic data; curve clipped at the plot edges.
2. What is the generalization gap?Define
Two bars of accuracy for the lesson's network trained 300 epochs on the circle dataset with no regularization: train 99.3% and test 97.3%, beside the constructor call and the 150/150 split.
Real run of the lesson's main.py code.
3. What is regularization?Define
A table of the four configurations from the lesson's step 5: no regularization (dropout_p 0.0, weight_decay 0.0), dropout (0.3, 0.0), weight decay (0.0, 0.01) and dropout plus weight decay (0.3, 0.01).
4. Zhang et al. trained networks on ImageNet with random labels and got near-zero training loss. What does that result show?Explain
Left: the first five of 500 training inputs of the stand-in run, each paired with a randomly drawn label from 0 to 9. Right: the training loss of a small stand-in network trained on random labels, falling from about 2.3 to 0.02 over 300 epochs.
Stand-in, not the ImageNet experiment: real torch run of an MLP 32-512-10 on 500 random vectors with random labels from 10 classes.

The Concept

The Overfitting Spectrum

Every model sits somewhere on a spectrum: underfitting (too simple to capture the pattern) on one end, overfitting (so complex it memorizes noise) on the other. The sweet spot sits between them, and regularization pushes a model toward it from the overfit side.

Mermaid diagram

Dropout

During training, randomly zero out each neuron's output with probability p. It's the simplest regularization technique in this lesson, and arguably the one with the most elegant interpretation.

output = activation(z) * mask    where mask[i] ~ Bernoulli(1 - p)

Bernoulli(1 - p) is just a weighted coin flip: it lands 1 with probability 1 - p and 0 otherwise, independently for every neuron on every forward pass. With p = 0.5, half the neurons get zeroed on every forward pass. The network can't predict which neurons will survive, so it's forced to learn redundant representations. That kills co-adaptation: neurons learning to depend on specific other neurons being present.

The ensemble interpretation is the deeper reason this works. A network with N neurons and dropout creates 2^N possible subnetworks, one for every combination of which neurons are on or off. Training with dropout approximately trains all 2^N of them at once, each on a different mini-batch. At test time you use every neuron (no dropout) and scale outputs by (1 - p) to match the expected value seen during training. That's the same as averaging the predictions of that entire 2^N-subnetwork ensemble, pulled out of a single trained model.

In practice, the scaling happens during training instead of testing. This is called inverted dropout:

During training:  output = activation(z) * mask / (1 - p)
During testing:   output = activation(z)   (no change needed)

It's cleaner this way: test code doesn't need to know dropout exists at all.

Default rates: p = 0.1 for transformers, p = 0.5 for MLPs, p = 0.2-0.3 for CNNs. Higher dropout means stronger regularization, and more risk of underfitting.

Weight Decay (L2 Regularization)

Add the squared magnitude of every weight to the loss:

total_loss = task_loss + (lambda / 2) * sum(w_i^2)

The gradient of that regularization term is lambda * w, so at every step each weight shrinks toward zero by an amount proportional to its own size. Large weights get penalized harder. The model gets pushed toward solutions where no single weight dominates.

Why does that help generalization? Overfit models tend to carry large weights that amplify noise in the training data. Weight decay keeps weights small, which caps the model's effective capacity and forces it to lean on stable, generalizable features instead of memorized quirks.

The lambda hyperparameter controls how hard that penalty bites. Typical values:

  • 0.01 for AdamW on transformers
  • 1e-4 for SGD on CNNs
  • 0.1 for heavily overfit models

Lesson 06 covers this: weight decay and L2 regularization are equivalent under SGD, not under Adam. Always use AdamW (decoupled weight decay) whenever your optimizer is Adam-based.

Batch Normalization

Normalize the output of each layer across the mini-batch, then hand it to the next layer.

For a mini-batch of activations at some layer:

mu = (1/B) * sum(x_i)           (batch mean)
sigma^2 = (1/B) * sum((x_i - mu)^2)   (batch variance)
x_hat = (x_i - mu) / sqrt(sigma^2 + eps)   (normalize)
y = gamma * x_hat + beta        (scale and shift)

Gamma and beta are learnable parameters that let the network undo the normalization if that turns out to be the better move. Without them, you'd be forcing every layer's output to zero-mean, unit-variance, whether or not the network actually wants that.

Training vs inference split: during training, mu and sigma come straight from the current mini-batch. During inference, you use running averages accumulated over training: an exponential moving average with momentum 0.1, so each update is 90% old value, 10% new.

Why BatchNorm actually works is still debated. The original paper claimed it reduces "internal covariate shift," the distribution of layer inputs shifting as earlier layers update. Santurkar et al. (2018) showed that explanation is wrong. The real reason is that BatchNorm smooths the loss landscape: gradients get more predictive, the Lipschitz constants shrink (a bound on how sharply the loss can change from a small step, now tighter), and the optimizer can safely take bigger steps. That's why BatchNorm lets you push the learning rate higher and converge faster.

It also has a real limitation: it depends on batch statistics. At batch size 1, mean and variance are meaningless. Below 32, the statistics get noisy enough to hurt performance, which is a problem for tasks like object detection, where memory caps batch size, and language modeling, where sequence lengths vary.

Layer Normalization

Normalize across features instead of across the batch. For a single sample:

mu = (1/D) * sum(x_j)           (feature mean)
sigma^2 = (1/D) * sum((x_j - mu)^2)   (feature variance)
x_hat = (x_j - mu) / sqrt(sigma^2 + eps)
y = gamma * x_hat + beta

D is the feature dimension, and each sample gets normalized independently, with no dependence on batch size at all. That's exactly why transformers use LayerNorm instead of BatchNorm: sequences have variable lengths, batch size is often small (or exactly 1 during generation), and the computation stays identical between training and inference.

Transformers apply LayerNorm after each self-attention block and each feed-forward block (Post-LN), or before them (Pre-LN, which trains more stably).

RMSNorm

LayerNorm without the mean subtraction, proposed by Zhang & Sennrich (2019).

rms = sqrt((1/D) * sum(x_j^2))
y = gamma * x / rms

That's it. No mean computation, no beta parameter. The insight: re-centering, the mean subtraction inside LayerNorm, barely moves accuracy but costs real computation. Drop it and you get the same accuracy for about 10% less overhead.

LLaMA, LLaMA 2, LLaMA 3, Mistral, and most modern LLMs use RMSNorm instead of LayerNorm. At the scale of billions of parameters and trillions of tokens, that 10% adds up fast.

Normalization Comparison

Mermaid diagram

Data Augmentation as Regularization

A change to the data, not the model: transform training inputs while keeping their labels fixed.

  • Images: random crop, flip, rotation, color jitter, cutout
  • Text: synonym replacement, back-translation, random deletion
  • Audio: time stretch, pitch shift, noise addition

The effect works out the same as any other regularizer here. It inflates the effective size of the training set, so the model has a harder time memorizing specific examples. A model that only ever sees one version of an image can memorize it outright. A model that sees 50 augmented versions of that same image has no choice but to learn the structure that stays invariant across all of them.

Early Stopping

The simplest regularizer of all: stop training the moment validation loss starts climbing, since the model hasn't overfit yet at that exact point. In practice you track validation loss every epoch, save the best model, and keep training through a "patience" window (typically 5-20 epochs). If validation loss hasn't improved by the end of that window, you stop and reload the best saved model.

When to Apply What

Mermaid diagram

Check: The Concept0/27
1. How does dropout regularize a neural network?Define
An input row of eight values 1 to 8 and three rows for training passes of Dropout(p=0.5) on it. Pass 1 outputs 2, 0, 0, 0, 10, 12, 14, 0; pass 2 outputs 0, 0, 0, 8, 0, 0, 14, 16; pass 3 outputs 0, 4, 6, 0, 10, 12, 0, 0.
Real output of the lesson's Dropout class.
2. What is the weight decay (L2 regularization) formula, and what lambda value does the lesson suggest for AdamW on transformers?Define
A bar chart of five weights, 0.5, -1.2, 3.0, 0.1 and -2.5, next to a box listing task_loss (from the data), lambda (penalty strength) and total_loss equals a question mark, labelled weight decay or L2.
Weights are the lesson's step 2 example.
3. What does batch normalization normalize over?Define
An 8 by 4 grid of numbers, one row per sample and one column per feature, drawn from a normal distribution with mean 5 and standard deviation 2, the lesson's step 3 mini-batch fed to BatchNorm(4).
Values from random.seed(42) in the lesson code.
4. What is layer normalization?Define
A vector of four values 4.71, 4.65, 4.78 and 6.40 flowing into a box LayerNorm(4).forward(x), with four question marks as the output, beside the module's gamma of four 1.0 values and beta of four 0.0 values.
x is the first row of the step 3 batch.
5. What is the key difference between RMSNorm and LayerNorm?Define
Two cards listing the attributes of the lesson's classes at num_features 4. LayerNorm(4) has gamma of four 1.0, beta of four 0.0, eps 1e-05 and num_features 4; RMSNorm(4) has gamma of four 1.0, eps 1e-06 and num_features 4.
Printed from the lesson's real objects.
6. Why does the lesson describe data augmentation as a form of regularization?Define
A 6 by 6 pixel image with a shape and four transformed copies drawn beside it: a horizontal flip, a shift by one pixel, a brighter copy and a flipped, dimmer copy.
Toy image and transforms computed with numpy.
7. How does early stopping, as described in the lesson, decide when to halt training?Define
Two loss curves from epoch 100 to 1500 of a tiny numpy network fitted to 24 noisy points: the training loss (blue) keeps falling, while the validation loss (orange) falls at first and then rises again.
Real numpy run, full-batch gradient descent; the exact epochs are not marked.
8. Why is a network trained with dropout described as approximately an ensemble?Explain
Five columns, one per training pass, each showing three hidden neurons as on (1) or zeroed (0) under Dropout(p=0.5): [[1, 1, 0], [0, 1, 0], [1, 0, 0], [1, 1, 1], [0, 1, 1]]. A sixth column for test time shows all three neurons on.
Masks from the lesson's Dropout class, random.seed(0).
9. Why does the lesson's dropout implementation scale surviving activations by 1/(1-p) during training instead of scaling at test time?Explain
Two cards. Scale during training: training output is a times mask divided by 1 minus p, testing output is a. Scale during testing: training output is a times mask, testing output is a times 1 minus p. Below, a line defining a, the Bernoulli mask and p.
10. Why does keeping weights small help a model generalize?Explain
Two bar charts on a log scale of the ten coefficients w0 to w9 of a degree-9 polynomial fitted to the same 10 points: Fit A with no penalty has a largest magnitude of 71.1, Fit B with a penalty on the squared weights has a largest magnitude of 1.6.
numpy least squares and ridge (lambda 0.01) on the same points as the earlier fit picture.
11. The gradient of the L2 term is lambda * w. What does that imply for a weight of 4.0 compared with a weight of 0.1?Explain
A number line from 0 to 4 with two dots, a weight of 0.1 very near zero and a weight of 4.0 near the right end, and a prompt about the pull toward zero from lambda times w.
12. Why does BatchNorm use running averages instead of batch statistics at inference?Explain
One card with the layer's running_mean [2.317, 2.248, 2.306, 2.351] and running_var [0.921, 0.992, 0.986, 0.910] at momentum 0.1 after 10 batches, and a second card with the single sample [5.0, 5.0, 5.0, 5.0] arriving in inference mode and a question mark for the mean and variance used.
Real values from the lesson's BatchNorm class.
13. Why does BatchNorm perform worse at small batch sizes?Explain
Three strips of dots on a number line, one for each batch size B = 2, 8 and 32, showing 2, 8 and 32 values of a single feature, each labelled that mu is the mean of those B values.
Illustrative draws from a standard normal, numpy seed 4.
14. Why can RMSNorm drop the mean subtraction that LayerNorm performs?Explain
Rows for the input 4.71, 4.65, 4.78, 6.40, the LayerNorm output -0.58, -0.66, -0.49, 1.73 with mean 0.00, and the RMSNorm output 0.91, 0.90, 0.92, 1.23 with mean 0.99.
Real output of the lesson's classes.
15. In the lesson's BatchNorm and LayerNorm formulas, what do the learnable gamma and beta parameters do after the normalization step?Explain
Two cards with the lesson's BatchNorm and LayerNorm formulas: the mean, the variance, x_hat equals x minus mu over the square root of sigma squared plus eps, and y equals gamma times x_hat plus beta (highlighted), with a note that gamma and beta are learnable parameters initialised to 1.0 and 0.0.
16. What did Santurkar et al. (2018) find about why BatchNorm actually works, contradicting the original paper's explanation?Explain
Two cards. The 2015 paper by Ioffe and Szegedy, titled Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift, with internal covariate shift defined as layer inputs shifting as earlier layers update. The 2018 paper by Santurkar et al., which studied why BatchNorm works.
Titles and definition as given in the lesson text.
17. Why does the lesson say transformers use LayerNorm instead of BatchNorm?Explain
Top: a batch of three sequences with 4, 7 and 3 tokens, padded with empty slots to the length of the longest. Bottom: text generation produces tokens t1, t2, t3 one after another for a single sequence, batch size 1.
Illustrative lengths.
18. A batch has 4 samples with 8 features each. How many means does BatchNorm compute, and how many does LayerNorm compute?Apply
An empty 4 by 8 grid: four rows labelled sample 1 to 4 and eight columns labelled feature 1 to 8, one activation value per cell.
19. A model scores 98% training accuracy and 55% test accuracy, a 43-point gap. Which weight decay value does the lesson list for a model this heavily overfit?Apply
Two accuracy bars, training 98% and test 55%, with a bracket between their tops labelled 43 points.
Bar heights are the stem's numbers; the picture is illustrative, not a measured run.
20. A detection model with BatchNorm is trained at batch size 2 because of memory limits. Validation scores are noisy and lower than expected. Which diagnosis fits?Analyze
A line chart of validation accuracy over 20 epochs for a BatchNorm network trained with batch size 2; it jumps between about 49% and 72% from epoch to epoch.
Stand-in, not a detection model: real CPU torch run of a small MLP with BatchNorm on synthetic data.
21. A teammate zeroes neurons with probability p during training but never rescales anything, then runs the full network with all neurons at test time. What goes wrong?Analyze
Two rows of eight activations. In training, only neurons 1, 5, 6 and 7 pass their values 1, 5, 6 and 7 and the rest are zero, with no rescaling; at testing all eight values 1 to 8 pass. A line asks how the sum read by the next layer compares.
Illustrative p = 0.5, mask from the lesson code.
22. A team adds an L2 penalty to the loss while training with Adam and sees weight decay behave differently than it did under SGD. What should they change?Analyze
Two line charts over 100 steps of weights starting at 4.0 and 0.4 with only an L2 penalty: under SGD both curves decay smoothly towards zero; under Adam the 4.0 weight falls in a nearly straight line and the 0.4 weight reaches zero much sooner and hovers near it.
Real numpy runs of SGD and Adam; hyperparameters illustrative.
23. Validation loss was lowest at epoch 25. Training ran with patience 15 and stopped at epoch 40, and the team deployed the epoch-40 weights, which perform worse than expected. What did they miss?Analyze
A schematic validation-loss curve over epochs 0 to 40 that falls to its lowest point at epoch 25 and rises afterwards, with markers for the lowest point at epoch 25, a patience window of 15 epochs, and training stopped at epoch 40, where the team deployed the weights.
Curve shape illustrative; epochs 25 and 40 and patience 15 are from the question.
24. A small transformer uses dropout p=0.5 and ends with low accuracy on both the training set and the test set, with a small gap between them. What is the most likely problem?Analyze
Two bars of accuracy, train 62% and test 60%, both well below 100%, next to a card: small transformer, dropout p = 0.5.
Illustrative accuracies.
25. You are training a large language model on variable-length sequences, generating one sequence at a time at inference, and want to cut normalization cost without losing accuracy. Which choice fits?Design
A transformer block drawn as self-attention, norm, feed-forward, norm, with the norm boxes highlighted, and notes that serving generates tokens one at a time with batch size 1, training uses variable-length sequences, and the goal is to cut normalization cost while keeping accuracy.
26. You have 2,000 labeled photos and cannot collect more. A network reaches near-perfect training accuracy but much lower test accuracy. Which plan follows the lesson's reasoning?Design
A field of 2,000 dots (80 by 25) for the labeled photos, and two bars: training accuracy near-perfect and test accuracy much lower.
Bar lengths illustrative.
27. You are training a small transformer with an Adam-based optimizer and want weight decay, dropout, normalization and a stopping rule. Which combination matches the lesson's guidance?Design
Four boxes for a small transformer trained with an Adam-based optimizer: optimizer with weight decay ?, regularizer with dropout p ?, normalization with norm layer ?, and training loop with stopping rule ?.

Build It

Step 1: Dropout (Train and Eval Mode)

python
import random
import math
 
 
class Dropout:
    def __init__(self, p=0.5):
        self.p = p
        self.training = True
        self.mask = None
 
    def forward(self, x):
        if not self.training:
            return list(x)
        self.mask = []
        output = []
        for val in x:
            if random.random() < self.p:
                self.mask.append(0)
                output.append(0.0)
            else:
                self.mask.append(1)
                output.append(val / (1 - self.p))
        return output
 
    def backward(self, grad_output):
        grads = []
        for g, m in zip(grad_output, self.mask):
            if m == 0:
                grads.append(0.0)
            else:
                grads.append(g / (1 - self.p))
        return grads

Step 2: L2 Weight Decay

python
def l2_regularization(weights, lambda_reg):
    penalty = 0.0
    for w in weights:
        penalty += w * w
    return lambda_reg * 0.5 * penalty
 
def l2_gradient(weights, lambda_reg):
    return [lambda_reg * w for w in weights]

Step 3: Batch Normalization

python
class BatchNorm:
    def __init__(self, num_features, momentum=0.1, eps=1e-5):
        self.gamma = [1.0] * num_features
        self.beta = [0.0] * num_features
        self.eps = eps
        self.momentum = momentum
        self.running_mean = [0.0] * num_features
        self.running_var = [1.0] * num_features
        self.training = True
        self.num_features = num_features
 
    def forward(self, batch):
        batch_size = len(batch)
        if self.training:
            mean = [0.0] * self.num_features
            for sample in batch:
                for j in range(self.num_features):
                    mean[j] += sample[j]
            mean = [m / batch_size for m in mean]
 
            var = [0.0] * self.num_features
            for sample in batch:
                for j in range(self.num_features):
                    var[j] += (sample[j] - mean[j]) ** 2
            var = [v / batch_size for v in var]
 
            for j in range(self.num_features):
                self.running_mean[j] = (1 - self.momentum) * self.running_mean[j] + self.momentum * mean[j]
                self.running_var[j] = (1 - self.momentum) * self.running_var[j] + self.momentum * var[j]
        else:
            mean = list(self.running_mean)
            var = list(self.running_var)
 
        self.x_hat = []
        output = []
        for sample in batch:
            normalized = []
            out_sample = []
            for j in range(self.num_features):
                x_h = (sample[j] - mean[j]) / math.sqrt(var[j] + self.eps)
                normalized.append(x_h)
                out_sample.append(self.gamma[j] * x_h + self.beta[j])
            self.x_hat.append(normalized)
            output.append(out_sample)
        return output

Step 4: Layer Normalization

python
class LayerNorm:
    def __init__(self, num_features, eps=1e-5):
        self.gamma = [1.0] * num_features
        self.beta = [0.0] * num_features
        self.eps = eps
        self.num_features = num_features
 
    def forward(self, x):
        mean = sum(x) / len(x)
        var = sum((xi - mean) ** 2 for xi in x) / len(x)
 
        self.x_hat = []
        output = []
        for j in range(self.num_features):
            x_h = (x[j] - mean) / math.sqrt(var + self.eps)
            self.x_hat.append(x_h)
            output.append(self.gamma[j] * x_h + self.beta[j])
        return output

Step 5: RMSNorm

python
class RMSNorm:
    def __init__(self, num_features, eps=1e-6):
        self.gamma = [1.0] * num_features
        self.eps = eps
        self.num_features = num_features
 
    def forward(self, x):
        rms = math.sqrt(sum(xi * xi for xi in x) / len(x) + self.eps)
        output = []
        for j in range(self.num_features):
            output.append(self.gamma[j] * x[j] / rms)
        return output

Step 6: Training With and Without Regularization

python
def sigmoid(x):
    x = max(-500, min(500, x))
    return 1.0 / (1.0 + math.exp(-x))
 
 
def make_circle_data(n=200, seed=42):
    random.seed(seed)
    data = []
    for _ in range(n):
        x = random.uniform(-2, 2)
        y = random.uniform(-2, 2)
        label = 1.0 if x * x + y * y < 1.5 else 0.0
        data.append(([x, y], label))
    return data
 
 
class RegularizedNetwork:
    def __init__(self, hidden_size=16, lr=0.05, dropout_p=0.0, weight_decay=0.0):
        random.seed(0)
        self.hidden_size = hidden_size
        self.lr = lr
        self.dropout_p = dropout_p
        self.weight_decay = weight_decay
        self.dropout = Dropout(p=dropout_p) if dropout_p > 0 else None
 
        self.w1 = [[random.gauss(0, 0.5) for _ in range(2)] for _ in range(hidden_size)]
        self.b1 = [0.0] * hidden_size
        self.w2 = [random.gauss(0, 0.5) for _ in range(hidden_size)]
        self.b2 = 0.0
 
    def forward(self, x, training=True):
        self.x = x
        self.z1 = []
        self.h = []
        for i in range(self.hidden_size):
            z = self.w1[i][0] * x[0] + self.w1[i][1] * x[1] + self.b1[i]
            self.z1.append(z)
            self.h.append(max(0.0, z))
 
        if self.dropout and training:
            self.dropout.training = True
            self.h = self.dropout.forward(self.h)
        elif self.dropout:
            self.dropout.training = False
            self.h = self.dropout.forward(self.h)
 
        self.z2 = sum(self.w2[i] * self.h[i] for i in range(self.hidden_size)) + self.b2
        self.out = sigmoid(self.z2)
        return self.out
 
    def backward(self, target):
        eps = 1e-15
        p = max(eps, min(1 - eps, self.out))
        d_loss = -(target / p) + (1 - target) / (1 - p)
        d_sigmoid = self.out * (1 - self.out)
        d_out = d_loss * d_sigmoid
 
        for i in range(self.hidden_size):
            d_relu = 1.0 if self.z1[i] > 0 else 0.0
            d_h = d_out * self.w2[i] * d_relu
            self.w2[i] -= self.lr * (d_out * self.h[i] + self.weight_decay * self.w2[i])
            for j in range(2):
                self.w1[i][j] -= self.lr * (d_h * self.x[j] + self.weight_decay * self.w1[i][j])
            self.b1[i] -= self.lr * d_h
        self.b2 -= self.lr * d_out
 
    def evaluate(self, data):
        correct = 0
        total_loss = 0.0
        for x, y in data:
            pred = self.forward(x, training=False)
            eps = 1e-15
            p = max(eps, min(1 - eps, pred))
            total_loss += -(y * math.log(p) + (1 - y) * math.log(1 - p))
            if (pred >= 0.5) == (y >= 0.5):
                correct += 1
        return total_loss / len(data), correct / len(data) * 100
 
    def train_model(self, train_data, test_data, epochs=300):
        history = []
        for epoch in range(epochs):
            total_loss = 0.0
            correct = 0
            for x, y in train_data:
                pred = self.forward(x, training=True)
                self.backward(y)
                eps = 1e-15
                p = max(eps, min(1 - eps, pred))
                total_loss += -(y * math.log(p) + (1 - y) * math.log(1 - p))
                if (pred >= 0.5) == (y >= 0.5):
                    correct += 1
            train_loss = total_loss / len(train_data)
            train_acc = correct / len(train_data) * 100
            test_loss, test_acc = self.evaluate(test_data)
            history.append((train_loss, train_acc, test_loss, test_acc))
            if epoch % 75 == 0 or epoch == epochs - 1:
                gap = train_acc - test_acc
                print(f"    Epoch {epoch:3d}: train_acc={train_acc:.1f}%, test_acc={test_acc:.1f}%, gap={gap:.1f}%")
        return history
Check: Build It0/7
1. A Dropout(p=0.5) in training mode keeps the first neuron and drops the second. What does forward([2.0, 4.0]) return?Apply
Input row x = [2.0, 4.0], mask row [1 (kept), 0 (dropped)] for Dropout(p=0.5) in training mode, and an output row of two question marks for forward(x).
2. A Dropout(p=0.5) forward pass produced mask [0, 1]. What does backward([1.0, 3.0]) return?Apply
A mask row [0 (dropped), 1 (kept)] from a Dropout(p=0.5) forward pass, the incoming gradient row grad_output = [1.0, 3.0], and an output row of two question marks for backward.
3. What does l2_regularization([3.0, 4.0], 0.1) return?Apply
The lesson's l2_regularization source (loop adding w times w, then returning lambda_reg times 0.5 times penalty) next to the call with weights [3.0, 4.0] and lambda_reg 0.1 returning a question mark.
Source copied from the lesson's main.py.
4. In RegularizedNetwork.backward, a w2 weight is 2.0, lr is 0.05, weight_decay is 0.1, and the task gradient for that weight is 0 (its hidden activation is 0). What is the weight after the update?Apply
The lesson's update line for w2 above a table of values: w2[i] 2.0, lr 0.05, weight_decay 0.1, hidden activation h[i] 0.0, and the new w2[i] as a question mark.
Line copied from the lesson's main.py.
5. A fresh BatchNorm(1) with momentum=0.1 is in training mode and sees the batch [[8.0], [12.0]]. What is running_mean afterward?Apply
A card with the initial state of a fresh BatchNorm(1): running_mean 0.0, running_var 1.0, momentum 0.1, training True; the batch [[8.0], [12.0]]; and running_mean afterward as a question mark.
6. With gamma = 1 and beta = 0, roughly what does LayerNorm.forward([1.0, 2.0, 3.0]) return?Apply
A bar chart of the vector [1.0, 2.0, 3.0] with an arrow into a box LayerNorm(3) whose output forward(x) is a question mark.
7. With gamma = 1, roughly what does RMSNorm.forward([3.0, 4.0]) return?Apply
A bar chart of the vector [3.0, 4.0] with an arrow into a box RMSNorm(2) whose output forward(x) is a question mark.

Use It

PyTorch ships all of this as modules:

python
import torch
import torch.nn as nn
 
model = nn.Sequential(
    nn.Linear(784, 256),
    nn.BatchNorm1d(256),
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Linear(256, 128),
    nn.BatchNorm1d(128),
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Linear(128, 10),
)
 
model.train()
out_train = model(torch.randn(32, 784))
 
model.eval()
out_test = model(torch.randn(1, 784))

The model.train() / model.eval() toggle matters more than it looks. It switches dropout on or off and tells BatchNorm whether to trust batch statistics or running statistics. Forgetting model.eval() before inference is one of the most common bugs in deep learning: your test accuracy fluctuates randomly because dropout is still firing and BatchNorm is still reading mini-batch statistics instead of the stable running averages.

Transformers follow a different pattern:

python
class TransformerBlock(nn.Module):
    def __init__(self, d_model=512, nhead=8, dropout=0.1):
        super().__init__()
        self.attention = nn.MultiheadAttention(d_model, nhead, dropout=dropout)
        self.norm1 = nn.LayerNorm(d_model)
        self.ff = nn.Sequential(
            nn.Linear(d_model, d_model * 4),
            nn.GELU(),
            nn.Linear(d_model * 4, d_model),
            nn.Dropout(dropout),
        )
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)
 
    def forward(self, x):
        attended, _ = self.attention(x, x, x)
        x = self.norm1(x + self.dropout(attended))
        x = self.norm2(x + self.ff(x))
        return x

LayerNorm, not BatchNorm. Dropout p=0.1, not p=0.5. These are the transformer defaults.

Check: Use It0/2
1. Why is it critical to call model.eval() before running inference in PyTorch?Explain
Typed PyTorch code: a model made of Linear(4, 8), BatchNorm1d(8), ReLU, Dropout(0.5) and Linear(8, 1), followed by model.eval(), a torch.no_grad() block and y = model(x).
Typed, not captured; layer sizes illustrative.
2. A PyTorch model with Dropout and BatchNorm gives a different test accuracy each time you evaluate it on the same data, with no training in between. What is the most likely cause?Analyze
A log of five evaluations of the same model on the same 400 test samples with no training in between; the test accuracy differs between evaluations: 89.8%, 88.2%, 90.5%, 90.2%, 89.8%. The model is Linear(20, 64), BatchNorm1d(64), ReLU, Dropout(0.5), Linear(64, 2).
Real CPU torch run on synthetic data; the model definition lines are typed to match it.

Ship It

This lesson produces:

  • outputs/prompt-regularization-advisor.md: a prompt that diagnoses overfitting and recommends the right regularization strategy

Exercises

  1. Implement spatial dropout for 2D data: instead of dropping individual neurons, drop entire feature channels. Simulate this by treating groups of consecutive features as channels and dropping whole groups. Compare the train-test gap to standard dropout on the circle dataset with hidden_size=32.

  2. Implement label smoothing from lesson 05 combined with dropout from this lesson. Train with four configurations: neither, dropout only, label smoothing only, both. Measure the final train-test accuracy gap for each. Which combination gives the smallest gap?

  3. Add a BatchNorm layer between the hidden layer and the activation in your circle-dataset network. Train with and without BatchNorm at learning rates 0.01, 0.05, and 0.1. BatchNorm should allow stable training at higher learning rates where the vanilla network diverges.

  4. Implement early stopping: track test loss each epoch, save the best weights, and stop if test loss hasn't improved for 20 epochs. Run the regularized network for 1000 epochs. Report which epoch had the best test accuracy and how many epochs of computation you saved.

  5. Compare LayerNorm vs RMSNorm on a 4-layer network (not just 2). Initialize both with the same weights. Train for 200 epochs and compare final accuracy, training speed (time per epoch), and gradient magnitudes at the first layer. Verify that RMSNorm is faster with the same accuracy.

Key Terms

TermWhat people sayWhat it actually means
Overfitting"Model memorized the data"When a model's training performance significantly exceeds its test performance, indicating it learned noise rather than signal
Regularization"Preventing overfitting"Any technique that constrains model complexity to improve generalization: dropout, weight decay, normalization, augmentation
Dropout"Random neuron deletion"Zeroing random neurons during training with probability p, forcing redundant representations; equivalent to training an ensemble
Weight decay"L2 penalty"Shrinking all weights toward zero by subtracting lambda * w at each step; penalizes complexity through weight magnitude
Batch normalization"Normalize per batch"Normalizing layer outputs across the batch dimension using batch statistics during training and running averages during inference
Layer normalization"Normalize per sample"Normalizing across features within each sample; batch-independent, used in transformers where batch size varies
RMSNorm"LayerNorm without the mean"Root mean square normalization; drops the mean subtraction from LayerNorm for 10% speedup with equal accuracy
Early stopping"Stop before overfit"Halting training when validation loss stops improving; the simplest regularizer, often used alongside others
Data augmentation"More data from less"Transforming training inputs (flip, crop, noise) to increase effective dataset size and force invariance learning
Generalization gap"Train-test split"The difference between training and test performance; regularization aims to minimize this gap

Further Reading

  • Srivastava et al., "Dropout: A Simple Way to Prevent Neural Networks from Overfitting" (2014) — the original dropout paper, with the ensemble interpretation and extensive experiments
  • Ioffe & Szegedy, "Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift" (2015) — introduced BatchNorm and its training procedure, one of the most cited deep learning papers
  • Zhang & Sennrich, "Root Mean Square Layer Normalization" (2019) — showed RMSNorm matches LayerNorm accuracy with reduced computation, adopted by LLaMA and Mistral
  • Zhang et al., "Understanding Deep Learning Requires Rethinking Generalization" (2017) — the landmark paper showing neural networks can memorize random labels, challenging traditional views of generalization

Code

Ships With