AI / ML Theory
Chapter 3 · 22 min

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 BB, focus on a single hidden coordinate (one entry of the pre-activation vector z∈Rdz \in \mathbb{R}^d). Collect the values across the batch: z1,z2,…,zBz_1, z_2, \dots, z_B. Compute the batch mean and batch variance:

μB=1B∑i=1Bzi,σB2=1B∑i=1B(zi−μB)2.\mu_\mathcal{B} = \frac{1}{B} \sum_{i=1}^B z_i, \qquad \sigma_\mathcal{B}^2 = \frac{1}{B} \sum_{i=1}^B (z_i - \mu_\mathcal{B})^2.

Normalise:

z^i=zi−μBσB2+ε.\hat z_i = \frac{z_i - \mu_\mathcal{B}}{\sqrt{\sigma_\mathcal{B}^2 + \varepsilon}}.

After this step, the batch has zero mean and unit variance along this coordinate. ε∼10−5\varepsilon \sim 10^{-5} 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:

 yi=γ z^i+β, \boxed{\ y_i = \gamma\, \hat z_i + \beta, \ }

with γ,β∈Rd\gamma, \beta \in \mathbb{R}^d trainable parameters (one γ\gamma and one β\beta per hidden coordinate; total 2d2d extra parameters per BN layer). The output yy is what the next layer sees.

Why this affine matters: setting γ=σB2+ε\gamma = \sqrt{\sigma_\mathcal{B}^2 + \varepsilon} and β=μB\beta = \mu_\mathcal{B} recovers yi=ziy_i = z_i. 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 γ,β\gamma, \beta 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 α=0.1\alpha = 0.1 on ImageNet would diverge. With BN, α=0.1\alpha = 0.1 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 μB,σB2\mu_\mathcal{B}, \sigma_\mathcal{B}^2 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 μ,σ2\mu, \sigma^2

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:

μˉ←ρ μˉ+(1−ρ) μB,σˉ2←ρ σˉ2+(1−ρ) σB2,\bar \mu \leftarrow \rho\, \bar \mu + (1 - \rho)\, \mu_\mathcal{B}, \qquad \bar\sigma^2 \leftarrow \rho\, \bar\sigma^2 + (1 - \rho)\, \sigma_\mathcal{B}^2,

with ρ≈0.9\rho \approx 0.9 to 0.990.99 — the EMA from 3.2. At inference time, BN switches from batch statistics to these running estimates:

yi=γ⋅zi−μˉσˉ2+ε+β.y_i = \gamma \cdot \frac{z_i - \bar\mu}{\sqrt{\bar\sigma^2 + \varepsilon}} + \beta.

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 μB=z\mu_\mathcal{B} = z, so z^=0\hat z = 0 identically, and the model outputs garbage.
  • Batch-size dependence in the running stats. Very small training batches give very noisy μB,σB2\mu_\mathcal{B}, \sigma_\mathcal{B}^2, which biases the EMAs. For B=1B = 1, 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 z^i=(zi−μB)/σB2+ε\hat z_i = (z_i - \mu_\mathcal{B})/\sqrt{\sigma_\mathcal{B}^2 + \varepsilon} couples all BB examples in the batch (through μB,σB2\mu_\mathcal{B}, \sigma_\mathcal{B}^2). So ∂z^i/∂zj≠0\partial \hat z_i / \partial z_j \ne 0 for j≠ij \ne i.

The full derivation (Ioffe & Szegedy, Appendix; reproduced in every "batchnorm backward from scratch" blog post) goes through three terms. Sketch:

∂L∂zi=∂L∂z^i⋅∂z^i∂zi+∂L∂μB⋅∂μB∂zi+∂L∂σB2⋅∂σB2∂zi.\frac{\partial L}{\partial z_i} = \frac{\partial L}{\partial \hat z_i}\cdot \frac{\partial \hat z_i}{\partial z_i} + \frac{\partial L}{\partial \mu_\mathcal{B}}\cdot \frac{\partial \mu_\mathcal{B}}{\partial z_i} + \frac{\partial L}{\partial \sigma_\mathcal{B}^2} \cdot \frac{\partial \sigma_\mathcal{B}^2}{\partial z_i}.

After simplification (write it out once, factor common terms), the final formula reads

 ∂L∂zi=1B⋅σB2+ε[B⋅∂L∂z^i−∑j=1B∂L∂z^j−z^i∑j=1Bz^j⋅∂L∂z^j]. \boxed{\ \frac{\partial L}{\partial z_i} = \frac{1}{B \cdot \sqrt{\sigma_\mathcal{B}^2 + \varepsilon}}\left[B \cdot \frac{\partial L}{\partial \hat z_i} - \sum_{j=1}^B \frac{\partial L}{\partial \hat z_j} - \hat z_i \sum_{j=1}^B \hat z_j \cdot \frac{\partial L}{\partial \hat z_j}\right]. \ }

Three terms: the upstream gradient at z^i\hat z_i, 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 γ,β\gamma, \beta:

∂L∂γk=∑i=1B∂L∂yi(k)⋅z^i(k),∂L∂βk=∑i=1B∂L∂yi(k).\frac{\partial L}{\partial \gamma_k} = \sum_{i=1}^B \frac{\partial L}{\partial y_i^{(k)}} \cdot \hat z_i^{(k)}, \qquad \frac{\partial L}{\partial \beta_k} = \sum_{i=1}^B \frac{\partial L}{\partial y_i^{(k)}}.

These are simple outer-product accumulations over the batch.


7. Where to put BN

In an MLP/conv block, the standard order is

z=Wa+b,z~=BN⁡(z),y=g(z~),z = W a + b, \quad \tilde z = \operatorname{BN}(z), \quad y = g(\tilde z),

i.e. before the nonlinearity. The bias bb is redundant once BN runs (β\beta already shifts), so frameworks often set bias to zero in BN'd layers.

Alternative: post-activation BN (BN⁡(g(z))\operatorname{BN}(g(z))) 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: z^=(z−μB)/σB2+ε\hat z = (z - \mu_\mathcal{B}) / \sqrt{\sigma_\mathcal{B}^2 + \varepsilon}, y=γz^+βy = \gamma \hat z + \beta.
  • The affine (γ,β)(\gamma, \beta) lets BN represent the identity if needed.
  • At inference, swap batch stats for EMA stats (μˉ,σˉ2)(\bar\mu, \bar\sigma^2) accumulated during training.
  • Backward couples all BB examples in the batch via μB,σB2\mu_\mathcal{B}, \sigma_\mathcal{B}^2.
  • Effect: smoother loss surface, larger usable LR, faster convergence, mild regularisation.
  • Fails for B=1B = 1 and for variable-length sequences; Transformers use LayerNorm instead.

Self-test

(a) Show that setting γ=σB2+ε\gamma = \sqrt{\sigma_\mathcal{B}^2 + \varepsilon} and β=μB\beta = \mu_\mathcal{B} makes BN exactly the identity: yi=ziy_i = z_i.

(b) Derive the backward formula in §6. Compute ∂z^i/∂zj\partial \hat z_i / \partial z_j explicitly, distinguishing the cases i=ji = j and i≠ji \ne j.

(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 (BB samples, CC channels, LL spectral positions), would BN be applied per spectral coordinate, per channel, or globally? Sketch the operation and the shapes of μB,σB2,γ,β\mu_\mathcal{B}, \sigma_\mathcal{B}^2, \gamma, \beta.


References

  1. [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).

    ↩