Type at least 2 letters. Enter opens the first result, Esc closes.

    Deep Learning · Lesson 5 · Making deep networks trainable

    Normalization layers and residual connections

    Intermediate 75 min 5 interactive figures 3 videos Colab notebook

    Before this lesson: lesson 03, backpropagation (adjoints, Jacobians) and lesson 04, activations and initialization (variance propagation). You also need the mean and variance of a list of numbers, and the projection onto a vector (linear algebra lesson 03). Time: about 75 minutes, plus the notebook.


    Why this matters #

    • ResNets (He et al., 2016) stack conv → BatchNorm → ReLU inside residual blocks and train with more than 100 layers. Before them, deeper plain networks had a higher training error.
    • Transformers put a LayerNorm (or RMSNorm in LLaMA) in every block, around a residual stream. The position of that norm (pre-LN or post-LN) decides whether training needs a learning-rate warmup.
    • Diffusion models and science models (weather, climate, physics) use GroupNorm or LayerNorm, because their batches per GPU are tiny and BatchNorm statistics would be noise.
    • Interviewers ask you to derive the BatchNorm backward pass and to explain why transformers do not use BatchNorm. This lesson does both.

    1. Predict first #

    2. The picture #

    Normalize, then rescale #

    Take one feature and a batch of NN numbers. BatchNorm does three things:

    1. subtract the batch mean μ\mu;
    2. divide by the batch standard deviation σ\sigma; now the numbers have mean 0 and variance 1;
    3. multiply by a learned scale γ\gamma and add a learned shift β\beta.

    Add a constant to every input, or multiply every input by a positive constant: steps 1 and 2 undo it, so the output does not change. The layer forgets the location and the scale of the batch, and γ,β\gamma, \beta put back a location and a scale that the network learns.

    Interactive figure: loading…

    Drag the blue dots, then press the shift and scale buttons. Row 2 (training mode, batch statistics) does not move when you shift or scale the whole batch. Now press Training step a few times. In eval mode (row 3, magenta) BatchNorm uses the running statistics μ^,σ^2\hat\mu, \hat\sigma^2 (the magenta band in row 1). After one step they are still far from the batch, so the same input gives a very different output. Blue = forward values computed with the batch; magenta = the train/eval mismatch.

    Which numbers share one mean and variance #

    The normalization layers differ only in the group of numbers that share one (μ,σ)(\mu, \sigma).

    Layer Input Statistics over One (μ,σ)(\mu, \sigma) per Learned
    BatchNorm (N,C)(N, C) or (N,C,H,W)(N, C, H, W) the batch (and the image positions) channel γ,β∈RC\gamma, \beta \in \mathbb{R}^C
    LayerNorm (B,T,D)(B, T, D) the DD features token γ,β∈RD\gamma, \beta \in \mathbb{R}^D
    RMSNorm (B,T,D)(B, T, D) the DD features, no mean token γ∈RD\gamma \in \mathbb{R}^D
    GroupNorm (N,C,H,W)(N, C, H, W) a group of channels and the positions example and group γ,β∈RC\gamma, \beta \in \mathbb{R}^C

    The key difference: BatchNorm mixes the examples of a batch; LayerNorm, RMSNorm and GroupNorm never do. The output for one example depends on the other examples only with BatchNorm.

    Interactive figure: loading…

    Pick a layer and click any cell. Each panel is one example of the batch. The yellow cells share one mean and one variance. BatchNorm lights up one channel in every example; LayerNorm stays inside one example (images) or one token (tokens). Switch to tokens (B, T, D) and BatchNorm: the group contains other sequences and also future tokens.

    Residual connections, simplest case #

    Take 50 scalar layers. In a plain stack xl+1=fl(xl)x_{l+1} = f_l(x_l), the chain rule multiplies 50 derivatives fl′f_l'. If each has size 0.10.1, the product has size 10−5010^{-50}: the first layer receives no signal.

    In a residual stack xl+1=xl+Fl(xl)x_{l+1} = x_l + F_l(x_l), the derivative of one block is 1+Fl′1 + F_l'. When Fl′F_l' is small, this factor is close to 1, so the product of 50 such factors does not collapse to 0. Section 4 computes 0.780.78 instead of 10−5010^{-50} for the same layer derivatives.

    Interactive figure: loading…

    Move the depth and the size ss. Both stacks use the same layer derivatives al=±sa_l = \pm s. The plain product ∏al\prod a_l has size sLs^L: a straight line on the log axis, down for s<1s < 1 and up for s>1s > 1. The residual product ∏(1+al)\prod (1 + a_l) stays near 1 for small ss. Now choose all + and a large ss: the residual product grows too. The identity path helps only while the branches are small.

    Quick check

    A batch of one channel is x=(1,3,5)x = (1, 3, 5). You change it to x′=(12,16,20)x' = (12, 16, 20). How does the BatchNorm output (training mode) change?

    Quick check

    A transformer processes a tensor of shape (B,T,D)=(8,128,512)(B, T, D) = (8, 128, 512) with LayerNorm. How many numbers share one mean and variance, and how many means are computed?

    3. Definitions and main results #

    BatchNorm, LayerNorm and RMSNorm (forward) #

    Take one group of nn numbers x1,…,xnx_1, \dots, x_n: one channel over the batch for BatchNorm, the features of one token for LayerNorm. Then

    μ=1n∑ixi,σ2=1n∑i(xi−μ)2,σ~=σ2+ε,x^i=xi−μσ~,yi=γx^i+β.\mu = \frac1n \sum_i x_i, \qquad \sigma^2 = \frac1n \sum_i (x_i - \mu)^2, \qquad \tilde\sigma = \sqrt{\sigma^2 + \varepsilon}, \qquad \hat x_i = \frac{x_i - \mu}{\tilde\sigma}, \qquad y_i = \gamma \hat x_i + \beta .
    • x^\hat x is the normalized input; γ\gamma (scale, or gain) and β\beta (shift, or bias) are learned.
    • ε\varepsilon (PyTorch default 10−510^{-5}) keeps the division safe when σ≈0\sigma \approx 0.
    • The variance used to normalize is the biased one: divide by nn, not by n−1n - 1.
    • In LayerNorm, γ\gamma and β\beta are vectors over the features and act entry by entry.

    RMSNorm (Zhang and Sennrich, 2019) drops the mean and β\beta:

    r=1D∑ixi2+ε,x^=x/r,y=γ⊙x^.r = \sqrt{\tfrac1D \textstyle\sum_i x_i^2 + \varepsilon}, \qquad \hat x = x / r, \qquad y = \gamma \odot \hat x .

    It needs one reduction (a sum over the features) instead of two, and gives similar quality. LLaMA uses it; T5 uses the same layer under the name "layer norm without bias and mean".

    Invariances. Let ε=0\varepsilon = 0, c>0c > 0, d∈Rd \in \mathbb{R}, and 1\mathbf 1 the all-ones vector. BatchNorm and LayerNorm give the same output for xx and for cx+d1cx + d\mathbf 1. RMSNorm gives the same output for xx and cxcx, but not for a shift.

    BatchNorm in training and in eval mode #

    In training, BatchNorm normalizes with the batch statistics. It also updates running estimates μ^\hat\mu, σ^2\hat\sigma^2 of the mean and variance with momentum mm (PyTorch default 0.1, the weight of the new value):

    μ^←(1−m) μ^+m μ,σ^2←(1−m) σ^2+m nn−1 σ2.\hat\mu \leftarrow (1 - m)\,\hat\mu + m\,\mu, \qquad \hat\sigma^2 \leftarrow (1 - m)\,\hat\sigma^2 + m\,\tfrac{n}{n-1}\,\sigma^2 .

    The running variance uses the unbiased estimate nn−1σ2\tfrac{n}{n-1}\sigma^2; the normalization itself uses the biased one. In eval mode BatchNorm uses μ^\hat\mu and σ^2\hat\sigma^2. Then it is a fixed affine map per channel,

    y=γ x−μ^σ^2+ε+β,y = \gamma\,\frac{x - \hat\mu}{\sqrt{\hat\sigma^2 + \varepsilon}} + \beta ,

    which can be folded into the linear or convolution layer before it. LayerNorm and RMSNorm do not use the batch, so they compute the same function in training and in eval mode.

    Scale-invariant weights

    Suppose BatchNorm or LayerNorm follows a linear layer with weight WW (for LayerNorm, the linear layer must have no bias; BatchNorm's mean subtraction removes a bias anyway). With ε=0\varepsilon = 0, the loss does not change when you scale WW: L(cW)=L(W)\mathcal{L}(cW) = \mathcal{L}(W) for every c>0c > 0.

    1. Differentiate in cc at c=1c = 1: ⟨∇L(W),W⟩=0\langle \nabla\mathcal{L}(W), W\rangle = 0. The gradient is orthogonal to WW.
    2. Differentiate in WW: c ∇L(cW)=∇L(W)c\,\nabla\mathcal{L}(cW) = \nabla\mathcal{L}(W), so ∇L(cW)=∇L(W)/c\nabla\mathcal{L}(cW) = \nabla\mathcal{L}(W)/c.
    3. One SGD step: ∥W−ηg∥2=∥W∥2−2η⟨W,g⟩+η2∥g∥2=∥W∥2+η2∥g∥2\|W - \eta g\|^2 = \|W\|^2 - 2\eta\langle W, g\rangle + \eta^2\|g\|^2 = \|W\|^2 + \eta^2\|g\|^2. So SGD never makes ∥W∥\|W\| smaller.

    The direction W/∥W∥W/\|W\| moves by about η∥g∥/∥W∥\eta\|g\|/\|W\|, and ∥g∥\|g\| itself scales like 1/∥W∥1/\|W\| by step 2. So the effective learning rate of the direction is about η/∥W∥2\eta/\|W\|^2: it falls as ∥W∥\|W\| grows. Weight decay keeps ∥W∥\|W\| small and so keeps the effective learning rate up (lesson 06).

    The backward pass removes two components #

    Theorem (backward pass of BatchNorm and LayerNorm). Take one group in training mode. Let yˉi=∂L/∂yi\bar y_i = \partial\mathcal{L}/\partial y_i be the upstream gradient and gi=γ yˉig_i = \gamma\,\bar y_i. Then

    βˉ=∑iyˉi,γˉ=∑iyˉi x^i,xˉ=1σ~(g−mean⁡(g) 1−x^ mean⁡(g⊙x^)).\bar\beta = \sum_i \bar y_i, \qquad \bar\gamma = \sum_i \bar y_i\, \hat x_i, \qquad \bar x = \frac{1}{\tilde\sigma}\Big(g - \operatorname{mean}(g)\,\mathbf 1 - \hat x\, \operatorname{mean}(g \odot \hat x)\Big).

    Here mean⁡(v)=1n∑ivi\operatorname{mean}(v) = \frac1n\sum_i v_i and ⊙\odot is the entry-wise product.

    Why this is true, in one picture. Moving xx along 1\mathbf 1 (a shift) or along x^\hat x (a scale change) does not change the output. So the gradient with respect to xx cannot have a component in these two directions. The formula takes gg and removes exactly these two components.

    Proof.

    1. From yi=γx^i+βy_i = \gamma \hat x_i + \beta we get βˉ\bar\beta and γˉ\bar\gamma directly, and ∂L/∂x^i=γ yˉi=gi\partial\mathcal{L}/\partial \hat x_i = \gamma\,\bar y_i = g_i.
    2. The mean: ∂μ/∂xj=1/n\partial \mu/\partial x_j = 1/n.
    3. The variance: ∂σ2/∂xj=2n∑i(xi−μ)(δij−1n)=2n(xj−μ)\partial \sigma^2/\partial x_j = \frac2n \sum_i (x_i - \mu)(\delta_{ij} - \frac1n) = \frac2n (x_j - \mu), because ∑i(xi−μ)=0\sum_i (x_i - \mu) = 0. So ∂σ~/∂xj=(xj−μ)/(nσ~)\partial\tilde\sigma/\partial x_j = (x_j - \mu)/(n\tilde\sigma).
    4. The quotient rule for x^i=(xi−μ)/σ~\hat x_i = (x_i - \mu)/\tilde\sigma:
    ∂x^i∂xj=δij−1/nσ~−(xi−μ)(xj−μ)nσ~3=1σ~(δij−1n−x^ix^jn).\frac{\partial \hat x_i}{\partial x_j} = \frac{\delta_{ij} - 1/n}{\tilde\sigma} - \frac{(x_i - \mu)(x_j - \mu)}{n\tilde\sigma^3} = \frac{1}{\tilde\sigma}\Big(\delta_{ij} - \frac1n - \frac{\hat x_i \hat x_j}{n}\Big).
    1. The chain rule: xˉj=∑igi ∂x^i/∂xj=1σ~(gj−mean⁡(g)−x^jmean⁡(g⊙x^))\bar x_j = \sum_i g_i\, \partial\hat x_i/\partial x_j = \frac{1}{\tilde\sigma}\big(g_j - \operatorname{mean}(g) - \hat x_j \operatorname{mean}(g\odot\hat x)\big). ■\blacksquare

    Geometry. Write xˉ=1σ~Pg\bar x = \frac1{\tilde\sigma} P g with P=I−1n11⊤−1nx^x^⊤P = I - \frac1n \mathbf 1\mathbf 1^\top - \frac1n \hat x \hat x^\top. We have 1⊥x^\mathbf 1 \perp \hat x (because ∑ix^i=0\sum_i \hat x_i = 0), ∥1∥2=n\|\mathbf 1\|^2 = n, and ∥x^∥2=nσ2/(σ2+ε)\|\hat x\|^2 = n\sigma^2/(\sigma^2 + \varepsilon), which equals nn when ε=0\varepsilon = 0. In that case PP is the orthogonal projector onto the complement of span⁡{1,x^}\operatorname{span}\{\mathbf 1, \hat x\} (projections, linear algebra lesson 03). Hence

    • ∑jxˉj=0\sum_j \bar x_j = 0 exactly (for every ε\varepsilon), and
    • ∑jxˉjx^j=0\sum_j \bar x_j \hat x_j = 0 (exactly for ε=0\varepsilon = 0).

    LayerNorm has the same formula over the feature axis of each token, with g=γ⊙yˉg = \gamma \odot \bar y; γˉ\bar\gamma and βˉ\bar\beta are then summed over all tokens.

    RMSNorm. The same steps without the mean give xˉ=1r(g−x^ mean⁡(g⊙x^))\bar x = \frac1r\big(g - \hat x\,\operatorname{mean}(g \odot \hat x)\big) with g=γ⊙yˉg = \gamma \odot \bar y. Only one component is removed, the one along x^\hat x, which matches the single invariance x↦cxx \mapsto cx.

    Interactive figure: loading…

    Step through the backward pass. Start at step 1: the faint bars are the upstream gradient gg (drag their tops). Step 2 removes the mean, so ∑ivi=0\sum_i v_i = 0. Step 3 removes the part along x^\hat x (thin blue bars), so v⋅x^=0v \cdot \hat x = 0. Step 4 divides by σ\sigma. Orange = gradients, blue = forward values. Try Random g and the other batches: both checks are always 0 after step 3.

    Quick check

    In the backward pass of BatchNorm (training mode, ε=0\varepsilon = 0), the upstream gradient is yˉ=(1,1,1,1)\bar y = (1, 1, 1, 1) and γ=2\gamma = 2. What is xˉ\bar x?

    Why transformers use LayerNorm or RMSNorm #

    BatchNorm makes the output of one example depend on the other examples of the batch. In transformers this causes concrete problems:

    • Small or variable batches. Large models fit only a few sequences on one device, so the batch statistics are noisy. Synchronizing BatchNorm across devices costs communication in every layer.
    • Train/eval mismatch. Training uses batch statistics; inference uses running ones. When a language model generates text, it processes one new token of one sequence at a time: there is no batch.
    • Padding and variable lengths. Pad tokens enter the statistics unless they are masked.
    • Causal leakage. Statistics over batch and time include future tokens, which a causal language model must not see.

    LayerNorm normalizes each token over its own features. It does not depend on other examples or positions, it computes the same function in training and inference, and it works one token at a time with a KV cache (lesson 11).

    The identity path of residual networks #

    A residual block computes xl+1=xl+Fl(xl)x_{l+1} = x_l + F_l(x_l). FlF_l is the branch (for example conv–BN–ReLU–conv, or attention, or an MLP); the term xlx_l is the skip connection. The sequence x0,x1,…x_0, x_1, \dots is often called the residual stream.

    Theorem (the identity path). Let xl+1=xl+Fl(xl)x_{l+1} = x_l + F_l(x_l) and let Jk=∂Fk/∂xkJ_k = \partial F_k/\partial x_k be the Jacobian of branch kk. Then for l<Ll < L:

    ∂xL∂xl=(I+JL−1)⋯(I+Jl)=I+∑k=lL−1Jk+∑l≤k<m≤L−1JmJk+⋯\frac{\partial x_L}{\partial x_l} = (I + J_{L-1}) \cdots (I + J_l) = I + \sum_{k=l}^{L-1} J_k + \sum_{l \le k < m \le L-1} J_m J_k + \cdots

    Proof.

    1. Differentiating the update gives ∂xk+1/∂xk=I+Jk\partial x_{k+1}/\partial x_k = I + J_k.
    2. The chain rule multiplies these factors for k=l,…,L−1k = l, \dots, L-1, later layers on the left.
    3. Expanding the product gives one term for every subset of layers; the empty subset gives II. ■\blacksquare

    Why it helps. For the gradient (a row vector), ∂L/∂xl=∂L/∂xL (I+⋯ )\partial\mathcal{L}/\partial x_l = \partial\mathcal{L}/\partial x_L\,(I + \cdots). The term ∂L/∂xL⋅I\partial\mathcal{L}/\partial x_L \cdot I reaches every layer unchanged, however small the JkJ_k are: an addition node copies its upstream gradient to both inputs (lesson 03). A plain stack gives ∂xL/∂xl=JL−1⋯Jl\partial x_L/\partial x_l = J_{L-1}\cdots J_l, which shrinks or grows exponentially unless every JkJ_k is close to an isometry. He et al. (2016) observed this as the degradation problem: deeper plain networks had a higher training error than shallower ones. A residual network with small branches starts close to the identity map, which is easy to optimize.

    The expansion has 2L−l2^{L-l} terms, one per path through the network. Veit et al. (2016) found that most of the gradient flows through the short paths, so a ResNet behaves a bit like an ensemble of shallower networks.

    The forward pass can still explode. At initialization the branch output is uncorrelated with xlx_l (the weights have mean zero), so

    Var⁡(xl+1)=Var⁡(xl)+Var⁡(Fl(xl)).\operatorname{Var}(x_{l+1}) = \operatorname{Var}(x_l) + \operatorname{Var}\big(F_l(x_l)\big).

    A He-initialized branch keeps the variance of its input, so the variance doubles in every block. Standard fixes:

    • a normalization inside the branch (pre-LN, below);
    • zero initialization of the last layer, or of the last γ\gamma, in each branch, so that each block starts as the identity;
    • scaling the branch by about 1/L1/\sqrt{L}. GPT-2 scales the initial weights of the layers that write into the residual stream by 1/N1/\sqrt{N}, where NN is the number of residual layers.
    Quick check

    A residual block with a linear branch: xl+1=xl+Axlx_{l+1} = x_l + A x_l with A=(00.1−0.10)A = \begin{pmatrix} 0 & 0.1 \\ -0.1 & 0 \end{pmatrix} (column vectors). What is ∂xl+1/∂xl\partial x_{l+1}/\partial x_l?

    Post-LN and pre-LN transformer blocks #

    post-LN:xl+1=LN⁡(xl+Fl(xl)),pre-LN:xl+1=xl+Fl(LN⁡(xl)).\text{post-LN:}\quad x_{l+1} = \operatorname{LN}\big(x_l + F_l(x_l)\big), \qquad \text{pre-LN:}\quad x_{l+1} = x_l + F_l\big(\operatorname{LN}(x_l)\big).

    The original Transformer and BERT use post-LN. GPT-2, LLaMA (with RMSNorm) and most modern models use pre-LN.

    • Identity path. In pre-LN, xL=xl+∑k=lL−1Fk(LN⁡(xk))x_L = x_l + \sum_{k = l}^{L-1} F_k(\operatorname{LN}(x_k)), so ∂xL/∂xl=I+⋯\partial x_L/\partial x_l = I + \cdots with no LayerNorm on the direct path. In post-LN, the gradient passes through L−lL - l LayerNorm Jacobians. Pre-activation ResNets (He et al., 2016, "Identity Mappings in Deep Residual Networks") made the same change for CNNs: BN and ReLU move into the branch, and the skip path is a pure identity.
    • Gradient size at initialization (Xiong et al., 2020, Theorem 1). For the weight W2,LW^{2,L} of the last FFN layer and width dd, the gradient norm is bounded by O(dln⁡d)O(d\sqrt{\ln d}) for post-LN, independent of the depth LL, and by O(dln⁡d/L)O(d\sqrt{\ln d / L}) for pre-LN. The reason: the Jacobian of a LayerNorm has norm O(d/∥x∥)O(\sqrt d/\|x\|), and in pre-LN the squared norm of the residual stream grows linearly with depth, so the final LayerNorm divides the gradient by about L\sqrt L.
    • Warmup. Large gradients near the output make a large initial learning rate unstable in post-LN. A learning-rate warmup (start with a tiny learning rate and increase it over the first steps) keeps the first updates small. Xiong et al. trained pre-LN transformers without warmup to similar results in less time.
    • Final norm. In pre-LN the residual stream itself is never normalized, so the stack needs a final LayerNorm before the output head.
    • Growth of the residual stream. In pre-LN each branch adds an O(1)O(1) amount to a stream whose squared norm grows about linearly in ll. So block ll changes the stream by a relative amount of about 1/l1/\sqrt l: late blocks change it less. Post-LN, when it trains, can reach slightly better quality; DeepNorm (Wang et al., 2022) up-weights the skip path in post-LN and trains transformers with 1000 layers.
    Interactive figure: loading…

    Change the depth, then switch to the residual stream. These are measurements at initialization (PyTorch encoder blocks, width 64, Xavier init, random data, mean of 5 seeds). The post-LN gradient of the last block stays near 0.35 for every depth. The pre-LN one shrinks like 1/L1/\sqrt{L}: the column "pre ⋅L\cdot\sqrt L" stays near 0.5. In the second view, the pre-LN residual stream grows about linearly with depth, while post-LN keeps it at 1.

    Answers to section 1. (1) Zero. BatchNorm gives the same output for xx and x+c1x + c\mathbf 1, so L(x+c1)=L(x)\mathcal{L}(x + c\mathbf 1) = \mathcal{L}(x) for every cc. Differentiating in cc gives ∑i∂L/∂xi=0\sum_i \partial\mathcal{L}/\partial x_i = 0. (2) The variance doubles in every block: 224≈1.7⋅1072^{24} \approx 1.7 \cdot 10^7. Residual connections protect the identity path of the gradient, not the forward scale; normalization or branch scaling is still needed.

    4. Worked example #

    (a) BatchNorm forward. One channel, training mode, batch x=(0,2,4,6)x = (0, 2, 4, 6), γ=1\gamma = 1, β=0\beta = 0, ε=0\varepsilon = 0, momentum 0.1, running mean 0 and running variance 1 before the step.

    • Mean: μ=(0+2+4+6)/4=3\mu = (0 + 2 + 4 + 6)/4 = 3. Deviations: (−3,−1,1,3)(-3, -1, 1, 3).
    • Biased variance: σ2=(9+1+1+9)/4=5\sigma^2 = (9 + 1 + 1 + 9)/4 = 5, so σ=5≈2.2361\sigma = \sqrt5 \approx 2.2361.
    • Output: y=x^=(−3,−1,1,3)/5=(−1.3416,−0.4472,0.4472,1.3416)y = \hat x = (-3, -1, 1, 3)/\sqrt5 = (-1.3416, -0.4472, 0.4472, 1.3416).
    • Running mean: 0.9⋅0+0.1⋅3=0.30.9 \cdot 0 + 0.1 \cdot 3 = 0.3.
    • Running variance: the unbiased estimate is 43⋅5=20/3≈6.667\tfrac43 \cdot 5 = 20/3 \approx 6.667, so 0.9⋅1+0.1⋅6.667=1.56670.9 \cdot 1 + 0.1 \cdot 6.667 = 1.5667.
    • Eval mode now maps x=3x = 3 to (3−0.3)/1.5667≈2.157(3 - 0.3)/\sqrt{1.5667} \approx 2.157. Training mode mapped it to 0. After one step the running statistics are still far from the data.

    (b) BatchNorm backward. Take L=y1\mathcal{L} = y_1, so yˉ=g=(1,0,0,0)\bar y = g = (1, 0, 0, 0) (with γ=1\gamma = 1).

    • βˉ=1\bar\beta = 1 and γˉ=x^1=−1.3416\bar\gamma = \hat x_1 = -1.3416.
    • mean⁡(g)=0.25\operatorname{mean}(g) = 0.25 and mean⁡(g⊙x^)=−1.3416/4=−0.3354\operatorname{mean}(g \odot \hat x) = -1.3416/4 = -0.3354.
    • The third term is −x^j⋅(−0.3354)=0.3354 (xj−μ)/5=0.15 (xj−μ)-\hat x_j \cdot (-0.3354) = 0.3354\,(x_j - \mu)/\sqrt5 = 0.15\,(x_j - \mu).
    • So the bracket is gj−0.25+0.15(xj−μ)g_j - 0.25 + 0.15(x_j - \mu):
      • j=1j = 1: 1−0.25+0.15⋅(−3)=0.301 - 0.25 + 0.15 \cdot (-3) = 0.30;
      • j=2j = 2: 0−0.25+0.15⋅(−1)=−0.400 - 0.25 + 0.15 \cdot (-1) = -0.40;
      • j=3j = 3: 0−0.25+0.15⋅1=−0.100 - 0.25 + 0.15 \cdot 1 = -0.10;
      • j=4j = 4: 0−0.25+0.15⋅3=0.200 - 0.25 + 0.15 \cdot 3 = 0.20.
    • xˉ=(0.30,−0.40,−0.10,0.20)/5=(0.1342,−0.1789,−0.0447,0.0894)\bar x = (0.30, -0.40, -0.10, 0.20)/\sqrt5 = (0.1342, -0.1789, -0.0447, 0.0894).
    • Checks: the entries sum to 0, and xˉ⋅x^∝0.30⋅(−3)−0.40⋅(−1)−0.10⋅1+0.20⋅3=−0.9+0.4−0.1+0.6=0\bar x \cdot \hat x \propto 0.30 \cdot (-3) - 0.40 \cdot (-1) - 0.10 \cdot 1 + 0.20 \cdot 3 = -0.9 + 0.4 - 0.1 + 0.6 = 0.
    • Meaning: raising x2x_2 lowers y1y_1. It raises the mean and shrinks the spread, and both push y1=(x1−μ)/σy_1 = (x_1 - \mu)/\sigma further below zero.

    (c) The same vector as one token. LayerNorm over its D=4D = 4 features gives the same x^\hat x. RMSNorm: r=(0+4+16+36)/4=14≈3.742r = \sqrt{(0 + 4 + 16 + 36)/4} = \sqrt{14} \approx 3.742, so y=(0,2,4,6)/3.742=(0,0.5345,1.0690,1.6036)y = (0, 2, 4, 6)/3.742 = (0, 0.5345, 1.0690, 1.6036). It is not centered.

    (d) Residual vs plain, 50 scalar layers. Let the layer derivatives (fl′f_l' for the plain stack, Fl′F_l' for the residual branches) be +0.1+0.1 for 25 layers and −0.1-0.1 for the other 25.

    • Plain: ∣∏lfl′∣=0.150=10−50\big|\prod_l f_l'\big| = 0.1^{50} = 10^{-50}.
    • Residual: ∏l(1+Fl′)=1.125⋅0.925=(1.1⋅0.9)25=0.9925≈0.778\prod_l (1 + F_l') = 1.1^{25} \cdot 0.9^{25} = (1.1 \cdot 0.9)^{25} = 0.99^{25} \approx 0.778.
    Quick check

    A batch x=(2,4,9)x = (2, 4, 9) goes through BatchNorm in training mode with γ=1\gamma = 1, β=0\beta = 0, ε=0\varepsilon = 0. What is x^3\hat x_3?

    5. In code #

    PyTorch and autograd check the worked example. PyTorch refuses ε=0\varepsilon = 0 in training, so we use ε=10−12\varepsilon = 10^{-12}.

    import torch
    import torch.nn.functional as F
    
    x = torch.tensor([[0.], [2.], [4.], [6.]], dtype=torch.float64, requires_grad=True)
    bn = torch.nn.BatchNorm1d(1, eps=1e-12, momentum=0.1, dtype=torch.float64)
    y = bn(x)                                    # training mode: batch statistics
    print(y.detach().ravel().numpy().round(4))   # [-1.3416 -0.4472  0.4472  1.3416]
    print(round(bn.running_mean.item(), 4), round(bn.running_var.item(), 4))   # 0.3 1.5667
    
    y[0, 0].backward()                           # upstream gradient (1, 0, 0, 0)
    dx = x.grad.ravel()
    print((dx * 5 ** 0.5).numpy().round(4))      # [ 0.3 -0.4 -0.1  0.2]
    xhat = y.detach().ravel()
    print(abs(dx.sum().item()) < 1e-12, abs((dx @ xhat).item()) < 1e-12)   # True True
    
    bn.eval()                                    # now uses the running statistics
    print(round(bn(torch.tensor([[3.]], dtype=torch.float64)).item(), 3))  # 2.157
    
    t = torch.tensor([0., 2., 4., 6.], dtype=torch.float64)
    print(F.rms_norm(t, (4,), eps=1e-12).numpy().round(4))   # [0.     0.5345 1.069  1.6036]
    

    The theorem of Xiong et al. with PyTorch's own blocks (norm_first=True is pre-LN):

    import torch
    import torch.nn as nn
    
    def last_ffn_grad(norm_first, L, d=64, seed=0):
        """At init: gradient norm of the last block's second FFN weight, and mean |x_L|^2 / d."""
        torch.manual_seed(seed)
        blocks = nn.ModuleList(
            nn.TransformerEncoderLayer(d, 4, 4 * d, dropout=0.0, batch_first=True, norm_first=norm_first)
            for _ in range(L))
        for p in blocks.parameters():
            if p.dim() == 2:
                nn.init.xavier_normal_(p)
        final = nn.LayerNorm(d) if norm_first else nn.Identity()   # pre-LN needs a final norm
        head = nn.Linear(d, 100)
        x, target = torch.randn(8, 16, d), torch.randint(0, 100, (8, 16))
        for blk in blocks:
            x = blk(x)
        loss = nn.functional.cross_entropy(head(final(x)).flatten(0, 1), target.flatten())
        loss.backward()
        return blocks[-1].linear2.weight.grad.norm().item(), (x.detach() ** 2).sum(-1).mean().item() / d
    
    for L in (6, 12, 24, 48):
        g_post, _ = last_ffn_grad(False, L)
        g_pre, stream = last_ffn_grad(True, L)
        print(f"L={L:2d}  post-LN {g_post:.3f}  pre-LN {g_pre:.3f}  pre-LN |x_L|^2/d {stream:.1f}")
    # L= 6  post-LN 0.363  pre-LN 0.213  pre-LN |x_L|^2/d 3.8
    # L=12  post-LN 0.371  pre-LN 0.162  pre-LN |x_L|^2/d 6.8
    # L=24  post-LN 0.373  pre-LN 0.096  pre-LN |x_L|^2/d 13.6
    # L=48  post-LN 0.398  pre-LN 0.074  pre-LN |x_L|^2/d 34.1
    

    Post-LN stays between 0.36 and 0.40 at every depth. Pre-LN falls by a factor 2.9≈82.9 \approx \sqrt 8 from L=6L = 6 to L=48L = 48, and its residual stream grows about linearly with depth. These are single random draws, so the last digit is noise (the figure in section 3 averages 5 seeds). The exact constants of Xiong et al. hold in their simplified setting, not exactly here.

    6. Where it shows up #

    • CNNs. Conv → BN → ReLU is the standard unit. For inference, BN is folded into the conv weights, also before post-training quantization. Fine-tuning with small batches often freezes BN (keeps it in eval mode).
    • Small batches. GroupNorm (Wu and He, 2018) computes statistics over groups of channels within one example. It replaces BN when the batch per device is tiny: detection, segmentation, diffusion U-Nets.
    • Transformers. Pre-LN with RMSNorm (LLaMA), a final norm, and extra norms for stability such as QK-norm (lesson 12).
    • ML for science. Global weather and climate fields are large, so the batch per GPU is often 1 or 2 and BN statistics are useless. ViT-style Earth-system models use LayerNorm; diffusion U-Nets use GroupNorm. DiT's adaptive LayerNorm (adaLN-Zero) computes γ\gamma and β\beta from the condition (the diffusion time step, a class label, or a physical input) and initializes the branch scale at zero, so each block starts as the identity.
    • Distributed training. With data parallelism, BN computes its statistics on each GPU's local batch. SyncBatchNorm gives global statistics at the cost of one all-reduce per BN layer; LayerNorm needs no communication.

    7. Common mistakes #

    • Forgetting model.eval(). BN then normalizes test data with the test batch statistics. With one value per channel, x^=0\hat x = 0 and the output is β\beta (PyTorch's BatchNorm1d raises an error in that case).
    • Wrong variance. Normalize with the biased variance (divide by nn); the running estimate uses n/(n−1)n/(n-1).
    • Momentum convention. PyTorch's momentum=0.1 is the weight of the new batch value; Keras's momentum=0.99 is the weight of the old running value.
    • Wrong LayerNorm axis. LayerNorm in a transformer normalizes over the features of each token, not over the batch and not over the sequence.
    • Pre-LN without a final norm. The output head then sees a stream whose scale grows with depth.
    • "Residual connections prevent all instability." The forward variance still grows (doubles per block for unscaled branches) unless the branches are normalized or scaled.

    8. Check yourself #

    1. BatchNorm2d on a tensor of shape (8,16,32,32)(8, 16, 32, 32): how many values enter each mean, how many learned parameters are there, and how many running values?
      AnswerEach channel's statistics use 8⋅32⋅32=81928 \cdot 32 \cdot 32 = 8192 values. There are 16+16=3216 + 16 = 32 learned parameters (γ,β\gamma, \beta) and 16+16=3216 + 16 = 32 running values (mean and variance).
    2. Show in one line, without the backward formula, that ∑ixˉi=0\sum_i \bar x_i = 0 for LayerNorm.
      AnswerLayerNorm gives the same output for xx and x+c1x + c\mathbf 1, so 0=ddcL(x+c1)∣c=0=∑i∂L/∂xi0 = \frac{d}{dc} \mathcal{L}(x + c\mathbf 1)\big|_{c=0} = \sum_i \partial \mathcal{L}/\partial x_i.
    3. Which component of gg does the RMSNorm backward pass remove, and why only one?
      AnswerThe component along x^\hat x (the radial direction): xˉ=1r(g−x^ mean⁡(g⊙x^))\bar x = \frac1r\big(g - \hat x\,\operatorname{mean}(g \odot \hat x)\big). RMSNorm is invariant only to x↦cxx \mapsto cx, not to shifts, so the mean direction stays.
    4. An eval-mode BN follows a linear layer z=xW+bz = xW + b (row vector xx). Fold the two into one affine layer xW′+b′xW' + b'.
      AnswerWith s=γ/σ^2+εs = \gamma/\sqrt{\hat\sigma^2 + \varepsilon} (one entry per output channel): W′=Wdiag⁡(s)W' = W\operatorname{diag}(s) (column jj of WW times sjs_j) and b′=s⊙(b−μ^)+βb' = s \odot (b - \hat\mu) + \beta.
    5. 24 residual blocks without normalization. Each branch output has variance Var⁡(xl)/24\operatorname{Var}(x_l)/24 and is uncorrelated with xlx_l. What is Var⁡(x24)/Var⁡(x0)\operatorname{Var}(x_{24})/\operatorname{Var}(x_0)?
      AnswerEach block multiplies the variance by 1+1/241 + 1/24, so (25/24)24≈2.66(25/24)^{24} \approx 2.66 (close to ee), instead of 2242^{24} for unscaled branches.
    6. In a pre-LN block, set the output projection of attention and the second FFN matrix (weights and biases) to zero. What does the block compute? And a post-LN block?
      AnswerPre-LN: both branches output 0, so the block is the identity and its Jacobian is II. Post-LN: LN⁡2(LN⁡1(x))\operatorname{LN}_2(\operatorname{LN}_1(x)), which is not the identity; the gradient still passes through two LayerNorm Jacobians.
    Recall cardsAnswer in your head first, then reveal. Add the cards to your review deck to see them again just before you would forget.
    1. Write the BatchNorm forward pass for one group x1,…,xnx_1, \dots, x_n.

    2. Over which numbers does BatchNorm compute one mean for an (N,C,H,W)(N, C, H, W) tensor?

    3. Over which numbers does LayerNorm compute one mean in a transformer with input (B,T,D)(B, T, D)?

    4. Write the RMSNorm formula.

    5. What are BatchNorm's running statistics, and when are they used?

    6. Which two components of g=γyˉg = \gamma\bar y does the BatchNorm/LayerNorm backward pass remove?

    7. Why is ∑i∂L/∂xi=0\sum_i \partial\mathcal{L}/\partial x_i = 0 through BatchNorm in training mode?

    8. For xl+1=xl+Fl(xl)x_{l+1} = x_l + F_l(x_l), what is ∂xL/∂xl\partial x_L/\partial x_l?

    9. Why does BatchNorm not suit transformers? Give one reason.

    10. What is the difference between post-LN and pre-LN?

    Open the review →

    9. Interview angle #

    • "BatchNorm at train and test time?" — Batch statistics in training, running averages in eval (unbiased variance in the update). In eval it is a fixed affine map that can be folded into the previous layer.
    • "Derive the BatchNorm backward pass." — The five steps of section 3. The result projects out 1\mathbf 1 and x^\hat x; check it with ∑ixˉi=0\sum_i \bar x_i = 0.
    • "Why LayerNorm in transformers?" — Batch dependence: small batches, batch-1 decoding, padding, causal leakage. LayerNorm is per token and the same in training and inference.
    • "Why do ResNets train with 1000 layers?" — The identity term in ∏(I+Jk)\prod(I + J_k), plus normalization or branch scaling to control the forward variance.
    • "Why do post-LN transformers need warmup?" — Their gradients near the output are large and do not shrink with depth at initialization (Xiong et al.); pre-LN avoids this but needs a final norm.

    10. Practice #

    The notebook for this lesson runs in Colab. Each exercise has a check cell and a folded solution.

    1. BatchNorm forward with running statistics, in training and eval mode, checked against nn.BatchNorm1d.
    2. LayerNorm and RMSNorm forward, checked against PyTorch.
    3. The closed-form BatchNorm backward pass, checked against autograd.
    4. Gradient norm per layer of a 30-layer plain MLP and of the same MLP with residual connections, at initialization.
    5. Pre-LN and post-LN stacks: gradient norm per block at initialization.

    11. Watch and read #

    From about 40 minutes: BatchNorm from scratch, its batch dependence, the running statistics, and why the bias before it is useless.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    Open on YouTube · the player loads from youtube-nocookie.com when you press play