Dropout, BatchNorm, LayerNorm
The three regularization and stabilization tricks every modern network uses — what each does, why it works, and when to reach for which.
What you'll learn
- Why dropout works (and what it actually does)
- The difference between batch norm and layer norm — and why transformers picked layer norm
- When you can skip these entirely
Before you start
Three techniques show up in almost every modern deep network: dropout, batch normalization, and layer normalization. They solve different problems but are easy to confuse. Here’s what each one actually does and when to use it.
Dropout — averaging over subnetworks
At training time, dropout randomly zeros out a fraction p of
activations (say 10–50%). At test time (inference), it does nothing —
all neurons are active. A common mistake is forgetting model.eval(),
which leaves dropout on and makes predictions non-deterministic.
import numpy as np
rng = np.random.default_rng(42)
def dropout(x, p, training=True):
if not training or p == 0:
return x
# Inverted dropout: scale by 1/(1-p) so expected magnitude is unchanged
mask = (rng.random(x.shape) > p).astype(x.dtype)
return x * mask / (1 - p)
x = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0])
print("input: ", x)
print("dropout p=0.5:", dropout(x, 0.5, training=True))
print("dropout p=0.5:", dropout(x, 0.5, training=True)) # different mask each call
print("eval mode: ", dropout(x, 0.5, training=False))
input: [1. 2. 3. 4. 5. 6. 7. 8.]
dropout p=0.5: [ 2. 0. 6. 8. 0. 12. 14. 16.]
dropout p=0.5: [ 0. 0. 0. 8. 10. 12. 0. 0.]
eval mode: [1. 2. 3. 4. 5. 6. 7. 8.]
Two things to notice:
- The mask changes every call. Each training step sees a different “thinned” network. You’re effectively training an ensemble of exponentially many subnetworks, all sharing weights.
- Survivors get scaled up by
1/(1-p). This keeps the expected activation magnitude the same as without dropout — so you don’t need to change anything at inference time.
Why it regularizes: a feature can’t rely on any single co-adapted upstream feature (it might be dropped). The network spreads useful work across many features — more robust representations.
Batch normalization — normalize per minibatch
BatchNorm normalizes each feature channel across the batch dimension: subtract the batch mean, divide by the batch std, then apply a learnable scale + shift.
y = γ · (x − μ_batch) / sqrt(σ²_batch + ε) + β
It was the breakthrough that made deep CNN training stable in 2015. Two big advantages:
- Activations stay roughly mean-0, variance-1 throughout the network.
- You can use much higher learning rates without divergence.
Two big downsides:
- Different behavior in train vs eval mode. During training, you normalize with batch statistics. At eval, you use running averages accumulated during training. This means batch size affects your results.
- Awful for small batches. With batch size 1 or 2, batch statistics are noisy. With batch size 8, they’re still iffy.
BatchNorm is still dominant in vision CNNs (ResNet, ConvNeXt). It’s mostly out of fashion in transformers and small-batch training.
Layer normalization — normalize per sample
LayerNorm normalizes each sample across the feature dimension:
y = γ · (x − μ_features) / sqrt(σ²_features + ε) + β
The key difference: no dependence on other samples in the batch. Same behavior at train and eval. Works with batch size 1. Doesn’t care if your minibatch is small.
import numpy as np
# (batch, features) = (3, 4)
x = np.array([
[1.0, 2.0, 3.0, 4.0],
[10.0, 0.0, 5.0, 5.0],
[-1.0, -2.0, -3.0, -4.0],
])
# LayerNorm: normalize each ROW (across feature axis)
mean = x.mean(axis=-1, keepdims=True)
std = x.std(axis=-1, keepdims=True) + 1e-5
layer_norm = (x - mean) / std
# BatchNorm: normalize each COLUMN (across batch axis)
mean_b = x.mean(axis=0, keepdims=True)
std_b = x.std(axis=0, keepdims=True) + 1e-5
batch_norm = (x - mean_b) / std_b
print("LayerNorm (each row mean≈0, std≈1):")
print(layer_norm.round(2))
print("BatchNorm (each col mean≈0, std≈1):")
print(batch_norm.round(2))
LayerNorm (each row mean≈0, std≈1):
[[-1.34 -0.45 0.45 1.34]
[ 1.41 -1.41 0. 0. ]
[ 1.34 0.45 -0.45 -1.34]]
BatchNorm (each col mean≈0, std≈1):
[[-0.49 1.22 0.39 0.58]
[ 1.39 0. 0.98 0.83]
[-0.91 -1.22 -1.37 -1.41]]
Same operation, different axis. The axis choice changes everything:
| BatchNorm | LayerNorm | |
|---|---|---|
| Normalizes across | batch axis | feature axis |
| Train ≠ Eval? | yes (uses running stats) | no |
| Works with batch=1? | no | yes |
| Sensitive to batch size? | yes | no |
| Used in | CNNs | Transformers, RNNs |
RMSNorm — the modern variant
Modern LLMs increasingly use RMSNorm — LayerNorm without the mean subtraction:
y = γ · x / sqrt(mean(x²) + ε)
Slightly cheaper (no mean, no bias), empirically just as good — and now the default in essentially every open LLM (Llama, Mistral, Qwen, Gemma, DeepSeek). Where it sits relative to the residual connection (pre-norm) is covered in inside the transformer block.
Putting it in PyTorch
# CNN: BatchNorm after conv, before activation
nn.Sequential(
nn.Conv2d(64, 128, 3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.Dropout(0.1),
)
# Transformer block: LayerNorm in pre-norm position (modern standard)
nn.Sequential(
nn.LayerNorm(d_model),
nn.Linear(d_model, d_ff),
nn.GELU(),
nn.Linear(d_ff, d_model),
)
model.train() and model.eval() flip dropout and batch-norm’s
behavior. Forgetting to call model.eval() before inference is a
classic bug — predictions become non-deterministic and BatchNorm uses
the wrong statistics.
In one breath
- Dropout randomly zeros a fraction p of activations in training (off at inference), scaling survivors by 1/(1−p) — it trains an ensemble of subnetworks so no feature can lean on a single co-adapted neighbor.
- It’s standard for fine-tuning and smaller models but often near-zero in large LLM pretraining, which is data-bottlenecked, not overfitting.
- BatchNorm normalizes each feature across the batch — great for vision CNNs and high learning rates — but differs train vs eval and breaks on tiny batches.
- LayerNorm normalizes each sample across its features — no batch dependence, same in train and eval, works at batch size 1 — which is why transformers use it.
- Modern LLMs use RMSNorm (LayerNorm minus the mean-subtraction): cheaper, just as good. Always call model.eval() before inference, or dropout and BatchNorm misbehave.
Quick check
Quick check
Practice this in an interview
All questionsBatch normalisation normalises each feature across the mini-batch to zero mean and unit variance, then applies learnable scale and shift parameters. It stabilises internal activation distributions — reducing internal covariate shift — which allows higher learning rates, reduces dependence on careful weight initialisation, and provides mild regularisation through the noise in batch statistics.
Data augmentation artificially expands the training set by applying label-preserving transformations to existing images, improving generalisation and regularisation without collecting more data. Geometric transforms (flip, crop, rotation) and colour jitter are universally effective; stronger methods like CutMix, MixUp, and RandAugment consistently improve accuracy on top of basic augmentation.
Dropout randomly zeroes each neuron's output with probability p during training, forcing the network to learn redundant representations and preventing co-adaptation of neurons. At inference, dropout is disabled and all neurons are active — but to keep expected activations the same as during training, outputs are scaled by 1/(1−p). Forgetting to switch modes produces incorrect, noisy predictions.
Early stopping monitors validation loss after each epoch and halts training when it has not improved for a set number of epochs (the patience). It prevents the model from memorising training data past the point of best generalisation, acting as a free regulariser that requires no change to the model or loss function.
L2 regularisation adds a penalty equal to the sum of squared weights multiplied by a coefficient λ to the loss function, which encourages the optimiser to keep weights small. This penalises large, specialised weights and pushes the model toward simpler solutions that generalise better. In SGD it is equivalent to shrinking weights by a constant factor each step (hence weight decay), though in Adam the two diverge — requiring AdamW for correct decoupled decay.
Deep nets overfit when they memorize training examples rather than learning generalizable patterns — usually because the model has far more parameters than the signal in the data can constrain. The fix is a layered defence: regularization, data augmentation, early stopping, and architecture choices.
This divergence is the signature of overfitting: the model has enough capacity to memorise training-set specifics — noise, label errors, dataset-specific patterns — that do not generalise. Training loss measures fit to what has already been seen; validation loss measures generalisation to held-out data. As the model memorises rather than learns structure, it scores better on training data and worse on everything else.
Common techniques include L1 and L2 weight penalties, dropout (randomly zeroing activations), early stopping, data augmentation, and label smoothing. They reduce overfitting by constraining model capacity or adding noise so the network cannot memorize the training set.