3.C — Batch Normalization (rigorous) [appendix]
Goal: Derive the BN forward pass, the learnable affine, the EMA at inference, and sketch the backward.
Optional appendix. BatchNorm is the dominant normalisation in CNNs (ResNet, EfficientNet) but is not used in Transformers — LayerNorm and RMSNorm (3.8) win for the reasons listed there. Read this appendix if you want a rigorous BN derivation as background, or to understand the historical contrast between BN and LN. Skip if you only care about the LLM stack.
1. Why this session exists
Batch Normalization (Ioffe & Szegedy, 2015)[1] was, with ReLU + He init + Adam, one of the four pillars that made deep networks routinely trainable to 100+ layers around 2015. The idea: normalise each layer's pre-activations to zero mean and unit variance over the current mini-batch, then learn an affine transformation to undo the normalisation if it helps.
This session does the math: the forward pass, the affine, the test-time EMA, and a brief look at the backward pass (which is where many bugs live).
2. The normalisation step
Within a mini-batch of size , focus on a single hidden coordinate (one entry of the pre-activation vector ). Collect the values across the batch: . Compute the batch mean and batch variance:
Normalise:
After this step, the batch has zero mean and unit variance along this coordinate. avoids division by zero (and stabilises gradients when the variance is small).
So far the operation is parameter-free — there is nothing to learn. But there's a problem: by forcibly normalising to zero mean and unit variance, we've removed the ability of the previous layer to choose its own scale. The next step restores it.
3. The learnable affine
Apply a learnable per-coordinate affine:
with trainable parameters (one and one per hidden coordinate; total extra parameters per BN layer). The output is what the next layer sees.
Why this affine matters: setting and recovers . The BN layer can represent the identity. If a layer would have been better off without normalisation, it can learn to undo it. The point is that the optimiser has the choice, and gradient descent on tends to leave most BN layers near the identity plus a small per-coordinate shift — the behaviour that helps optimisation.
4. Why it helps: the original "internal covariate shift" claim, and the modern view
Ioffe & Szegedy originally argued BN helps by reducing internal covariate shift — the change in distribution of each layer's inputs as the previous layer's weights update. The intuitive story: stabilise the input distribution, and the layer doesn't have to constantly readapt.
Santurkar et al. (2018, "How Does Batch Normalization Help Optimization?") later showed this story is essentially wrong. Adding deliberate covariate shift after BN didn't break training. The real effect is on the loss landscape: BN makes the loss surface smoother (smaller Lipschitz constant of the gradient), which lets the optimiser take larger steps without diverging. Empirically, the practical effects are:
- Larger learnable LR. Pre-BN, training a 50-layer net at on ImageNet would diverge. With BN, trains stably.
- Less sensitivity to init. He init (2.10) is good but not magic; BN absorbs much of the variance-explosion problem.
- Regularisation effect. Each mini-batch's are noisy estimates of the population statistics. The noise acts a bit like dropout — small, beneficial.
In practice "BN works because it smooths the loss landscape and acts as a mild regulariser" is the modern reading. The original "covariate shift" framing is mostly historical.
5. Test-time: EMAs of
At test time, we don't have a batch — we want deterministic predictions, one example at a time. The fix is to keep running estimates of the population mean and variance, updated during training:
with to — the EMA from 3.2. At inference time, BN switches from batch statistics to these running estimates:
Two practical consequences:
- Mode toggle.
model.train()uses batch stats;model.eval()uses running stats. Forgetting to call.eval()at inference is a classic bug; predictions on a single example use the single-example , so identically, and the model outputs garbage. - Batch-size dependence in the running stats. Very small training batches give very noisy , which biases the EMAs. For , BN is undefined. This is why distributed training with tiny per-GPU batches uses synchronised BN (compute stats across GPUs) or, more often, abandons BN for LayerNorm / GroupNorm — see 3.11.
6. The backward pass (sketch)
BN's backward is notably not a one-liner: the normalisation couples all examples in the batch (through ). So for .
The full derivation (Ioffe & Szegedy, Appendix; reproduced in every "batchnorm backward from scratch" blog post) goes through three terms. Sketch:
After simplification (write it out once, factor common terms), the final formula reads
Three terms: the upstream gradient at , minus the mean of upstream gradients, minus a correlation term. The whole formula involves a sum over the batch, which is what makes BN's backward expensive enough that some implementations (e.g., FlashAttention-style fused kernels) hand-optimise it.
For backprop through :
These are simple outer-product accumulations over the batch.
7. Where to put BN
In an MLP/conv block, the standard order is
i.e. before the nonlinearity. The bias is redundant once BN runs ( already shifts), so frameworks often set bias to zero in BN'd layers.
Alternative: post-activation BN () has been tried and works less well empirically. The default is pre-activation BN, occasionally combined with the "pre-activation" residual block (He et al., 2016) where the normalisation precedes the convolution itself.
BN is not used in modern Transformers — LayerNorm is. See 3.11 for why.
8. Takeaways
- BN per coordinate: , .
- The affine lets BN represent the identity if needed.
- At inference, swap batch stats for EMA stats accumulated during training.
- Backward couples all examples in the batch via .
- Effect: smoother loss surface, larger usable LR, faster convergence, mild regularisation.
- Fails for and for variable-length sequences; Transformers use LayerNorm instead.
(a) Show that setting and makes BN exactly the identity: .
(b) Derive the backward formula in §6. Compute explicitly, distinguishing the cases and .
(c) A practitioner trains a BN'd ResNet for 100 epochs and at inference runs it with model.train() (not .eval()) by accident. They report "the model works on batches but produces nonsense on single images." Explain.
(d) For a 1D-CNN classifier on spectroscopic data ( samples, channels, spectral positions), would BN be applied per spectral coordinate, per channel, or globally? Sketch the operation and the shapes of .
References
- [1]
Ioffe, S., & Szegedy, C. (2015). "Batch normalization: Accelerating deep network training by reducing internal covariate shift". ICML. arXiv:1502.03167 — BatchNorm (Appendix 3.C).
↩