← All posts

Foundations

Post 5 of 15

Normalisation: batch norm, layer norm and RMSNorm

Posts in this series · 15

In post 2 gradient descent zigzagged across a narrow valley, and in post 3 we built cleverer optimisers to cope with it. This post attacks the same problem from the other side. Instead of a smarter walker, we reshape the ground: rescale the numbers flowing through the network so that the valley is round in the first place.

That rescaling is called normalisation. It appears at the input (standardising your data), and, in almost every modern network, again and again inside it: batch normalisation in image networks, layer normalisation in Transformers and speech models, and RMSNorm in LLaMA-style language models and in Kyutai’s Moshi. All three are the same small recipe with one choice changed, and by the end you will have computed each of them by hand on the same table.

Why scale matters

Features on different scales stretch the bowl

Imagine predicting a house price from two inputs: the number of bedrooms (around 1 to 5) and the floor area in square metres (around 50 to 250). A linear model multiplies each input by a weight and adds them up. Now nudge each weight by the same small amount, say 0.01. Nudging the bedrooms weight changes the prediction by about 0.01 × 3 = 0.03. Nudging the area weight changes it by about 0.01 × 150 = 1.5, fifty times more.

So the loss is far more sensitive to one weight than the other. Seen as a landscape, the bowl is gentle in one direction and steep in the other: exactly the narrow valley from post 2. And post 2 taught us the rule: the steepest direction sets the speed limit for every direction. The learning rate has to be small enough not to bounce out of the steep direction, which makes progress along the gentle direction painfully slow.

Here is a concrete toy. Take the bowl

L=w12+25 w22L = w_1^2 + 25\, w_2^2

Start at (4,1)(4, 1), where L=16+25=41L = 16 + 25 = 41. A gradient step multiplies w1w_1 by 1−2η1 - 2\eta and w2w_2 by 1−50η1 - 50\eta (the same rule as post 2, where the slope of aw2a w^2 is 2aw2aw). For w2w_2 not to blow up we need ∣1−50η∣<1|1 - 50\eta| < 1, so η<0.04\eta < 0.04. Choose η=0.035\eta = 0.035:

After 20 steps we are at about (0.937,0.003)(0.937, 0.003) with loss 0.878. The steep direction has settled; the gentle one has barely moved.

Now standardise the second input so both are on the same scale. That is the same as writing u2=5w2u_2 = 5 w_2, and the bowl becomes round: L=w12+u22L = w_1^2 + u_2^2, starting at (4,5)(4, 5) with the same loss of 41. Both directions now shrink by 1−2η1 - 2\eta, so a learning rate of η=0.3\eta = 0.3 is safe, and each step multiplies every coordinate by 0.40.4: (4,5)→(1.6,2)→(0.64,0.8)→…(4, 5) \to (1.6, 2) \to (0.64, 0.8) \to \ldots After 5 steps the loss is 0.0043. Same problem, same starting loss, two hundred times lower loss in a quarter of the steps.

A stretched bowl with a zigzag path and a round bowl with a straight path
Figure 1. Reshape the ground, not the walker. Press Step. On the stretched bowl (learning rate 0.035) the path zigzags and the loss is still 0.878 after 20 steps; on the standardised bowl (learning rate 0.3) it heads straight in and reaches 0.0043 after 5.

Activations drift from layer to layer

The same problem reappears inside the network. The inputs to layer 3 are the outputs of layer 2, and those outputs change every time layer 2’s weights change. If layer 2’s outputs grow a little bigger each layer, layer 5 receives numbers on a very different scale from layer 1, and each layer is a stretched bowl of its own.

Ioffe and Szegedy (2015), who introduced batch normalisation, called this shifting of each layer’s input distribution internal covariate shift (“covariate” is a statistician’s word for an input). Later work by Santurkar and colleagues (2018) showed that this story is not the full explanation: batch norm helps even when the shift is deliberately put back, and its main effect seems to be making the loss landscape smoother, so gradients are more predictable and larger learning rates are safe. Both papers agree on the practical upshot: keep the numbers inside the network on a steady scale and training gets easier.

You can see the drift in a toy. Push 256 random examples through 5 layers of 64 neurons each, with random weights and ReLU (from post 1: keep positives, zero negatives). Without normalisation the spread (standard deviation, defined in the next section) of the outputs grows layer by layer: 1.13, 1.58, 2.46, 3.09, 3.39. Add layer normalisation before each ReLU and it stays put: 0.58, 0.58, 0.61, 0.57, 0.57.

Histograms of activations in five layers, widening without normalisation and steady with it
Layer norm before each ReLU
Figure 2. Drift. Toggle normalisation off and the activations widen layer by layer (standard deviation 1.13 to 3.39); toggle it on and every layer looks the same (about 0.58).

Mean, variance and standard deviation

Every normaliser in this post is built from two summaries of a list of numbers: where they sit, and how spread out they are.

Mean: where the numbers sit

In one sentence: add them up and divide by how many there are.

μ=1n∑i=1nxi\mu = \frac{1}{n} \sum_{i=1}^{n} x_i

Picture: if the numbers were weights on a ruler, the mean is the point where the ruler balances.

Example: for 3,4,53, 4, 5, the mean is (3+4+5)/3=12/3=4(3 + 4 + 5) / 3 = 12 / 3 = 4.

Variance: how spread out they are

In one sentence: measure how far each number is from the mean, square those distances, and average them.

σ2=1n∑i=1n(xi−μ)2\sigma^2 = \frac{1}{n} \sum_{i=1}^{n} (x_i - \mu)^2

Example: for 3,4,53, 4, 5 the distances are −1,0,1-1, 0, 1; squared, 1,0,11, 0, 1; average 2/3≈0.6672/3 \approx 0.667.

Standard deviation: spread in the original units

Variance is in squared units (square metres if the numbers were metres). Take its square root to get back to the original units:

σ=σ2\sigma = \sqrt{\sigma^2}

For 3,4,53, 4, 5: σ=0.667≈0.816\sigma = \sqrt{0.667} \approx 0.816. Roughly, “a typical number is about 0.8 away from the mean.”

What would change? Replace 5 with 50 and the mean jumps to 19 and the standard deviation to about 21.9: both summaries are sensitive to one big value.

A small note: some textbooks divide by n−1n - 1 instead of nn in the variance (the “sample” variance). Normalisation layers divide by nn when they normalise; we come back to the n−1n - 1 version when we meet running averages.

Standardising inputs

In one sentence: subtract the mean, then divide by the standard deviation.

x^i=xi−μσ\hat{x}_i = \frac{x_i - \mu}{\sigma}

Picture: converting exam marks from different teachers into “how many typical gaps above or below the class average” so they can be compared fairly.

Example: 3,4,53, 4, 5 with μ=4\mu = 4, σ=0.816\sigma = 0.816:

The result always has mean 0 and standard deviation 1, whatever units we started in. Multiply every original number by 1000 and the standardised numbers do not change at all; that is the whole point.

LeCun, Bottou, Orr and Müller recommended exactly this in “Efficient BackProp” (1998): shift each input so its average over the training set is near zero, and scale it so all inputs have about the same spread. One rule matters in practice: compute μ\mu and σ\sigma on the training set only, store them, and reuse them on validation, test and real data (the three piles from post 4).

Three numbers on a line before and after standardising
Figure 3. Standardising by hand. With 3, 4, 5 the steps give mean 4, variance 0.667, standard deviation 0.816 and outputs −1.22, 0, 1.22. Drag a slider and every number updates.

Batch normalisation

Standardising the inputs fixes layer 1. Batch normalisation (Ioffe and Szegedy, 2015) does the same thing inside the network, at a layer of our choosing, using the examples in the current mini-batch (the small random group of examples from post 2) to measure the mean and spread.

The recipe

For each feature (each column, if rows are examples), over the mm examples in the batch:

μB=1m∑i=1mxiσB2=1m∑i=1m(xi−μB)2\mu_B = \frac{1}{m}\sum_{i=1}^{m} x_i \qquad \sigma_B^2 = \frac{1}{m}\sum_{i=1}^{m} (x_i - \mu_B)^2 x^i=xi−μBσB2+ϵyi=γ x^i+β\hat{x}_i = \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}} \qquad y_i = \gamma\, \hat{x}_i + \beta

Why gamma and beta

Standardising forces every feature to mean 0 and spread 1. But maybe the next layer works better with a feature centred at 3 with spread 2. For example, a ReLU after a standardised feature always zeros about half of it; the network might want less or more. With γ\gamma and β\beta, the network can choose any mean and spread it likes. It can even undo the normalisation completely: if it learns γ=σB\gamma = \sigma_B and β=μB\beta = \mu_B, then yi=xiy_i = x_i again.

So batch norm does not limit what the network can represent. What it changes is how the scale is controlled: instead of the mean and spread of a feature depending on a tangle of all the earlier weights, they are set directly by two numbers, β\beta and γ\gamma. That is much easier for gradient descent to adjust.

Why epsilon

If every example in the batch has the same value for a feature, the variance is 0 and we would divide by zero. Adding ϵ\epsilon under the square root keeps the division safe. When the variance is reasonably large, ϵ\epsilon changes nothing you can see: with variance 1, 1+0.00001=1.000005\sqrt{1 + 0.00001} = 1.000005. In our hand examples we set ϵ=0\epsilon = 0 to keep the arithmetic clean.

Worked by hand on a batch of four

Here is a batch of 4 examples (rows) with 3 features (columns). The features are deliberately on different scales: column 3 is about ten times column 2.

feature 1feature 2feature 3
example 12120
example 24360
example 32320
example 44960

Feature 1 (column 2,4,2,42, 4, 2, 4):

Feature 2 (column 1,3,3,91, 3, 3, 9):

Feature 3 (column 20,60,20,6020, 60, 20, 60):

The batch-normalised table (with γ=1\gamma = 1, β=0\beta = 0):

feature 1feature 2feature 3
example 1−1−1−1
example 21−0.331
example 3−1−0.33−1
example 411.671

Every column now has mean 0 and standard deviation 1. Feature 3, which was in the tens, is now on exactly the same footing as feature 1.

Now apply a learned scale and shift. Say feature 1 has learned γ=2\gamma = 2, β=1\beta = 1. Each value becomes 2x^+12\hat{x} + 1: 2(−1)+1=−12(-1) + 1 = -1 and 2(1)+1=32(1) + 1 = 3. The column is now −1,3,−1,3-1, 3, -1, 3: mean 1, standard deviation 2, exactly as β\beta and γ\gamma asked.

What would change? Double every number in feature 3 (40, 120, 40, 120). The mean doubles to 80 and the standard deviation to 40, so the normalised column is unchanged: −1,1,−1,1-1, 1, -1, 1. Batch norm does not care about the scale a feature arrives on.

A batch of four examples by three features, normalised column by column
Figure 4. Batch norm, one column at a time. Step through feature 2: mean 4, distances −3, −1, −1, 5, variance 9, standard deviation 3, outputs −1, −0.33, −0.33, 1.67. While stepping through feature 1, set γ to 2 and β to 1 and its outputs become −1, 3, −1, 3.

Where it sits

In the original paper, batch norm is placed after a layer’s weighted sum and before the activation function: weighted sum, then batch norm, then ReLU. Because β\beta already adds a shift, the layer’s own bias becomes redundant and is usually dropped. In convolutional networks (the subject of post 7) each channel is one feature, and the mean is taken over the batch and over every position in the image.

Training versus inference: running averages

Batch norm has an awkward property: during training, the output for one example depends on the other examples in its batch, because they all share μB\mu_B and σB\sigma_B. When the trained model is used for real (inference), we often have one example at a time, and we want the same input to always give the same output.

The fix: during training, keep a slowly updated running average of the batch means and variances, and at inference use those stored numbers instead of batch statistics.

running←(1−α)⋅running+α⋅batch value\text{running} \leftarrow (1 - \alpha)\cdot\text{running} + \alpha\cdot\text{batch value}

Worked for three batches of feature 1. Start, as PyTorch does, with running mean 0 and running variance 1. Suppose the three batches have means 3, 5, 4 and variances 1, 4, 2.

after batchrunning meanrunning variance
start01
1 (mean 3, var 1)0.9×0+0.1×3=0.30.9 \times 0 + 0.1 \times 3 = 0.30.9×1+0.1×1=1.00.9 \times 1 + 0.1 \times 1 = 1.0
2 (mean 5, var 4)0.9×0.3+0.1×5=0.770.9 \times 0.3 + 0.1 \times 5 = 0.770.9×1.0+0.1×4=1.30.9 \times 1.0 + 0.1 \times 4 = 1.3
3 (mean 4, var 2)0.9×0.77+0.1×4=1.0930.9 \times 0.77 + 0.1 \times 4 = 1.0930.9×1.3+0.1×2=1.370.9 \times 1.3 + 0.1 \times 2 = 1.37

After three batches the running mean is only 1.093, far from the true value of about 4. That is the early bias from post 3: an average that starts at zero takes a while to catch up. Over thousands of training steps it does catch up, so by the end of training the stored numbers are good. But it is a classic bug to evaluate a model after only a few steps, or to forget to switch the model to evaluation mode, and see strange results.

A detail for the curious: PyTorch stores the running variance using the n−1n - 1 (“unbiased”) version of the batch variance, while it normalises the batch itself with the nn version.

A running mean creeping towards the batch means, batch by batch
Figure 5. Running averages. With momentum 0.1 and batch means 3, 5, 4, the running mean goes 0.3, 0.77, 1.093. Raise momentum to 0.5 and it goes 1.5, 3.25, 3.625.

The batch-size problem

The batch mean and variance are only estimates of the true ones, and estimates from tiny batches are noisy. Take 10,000 numbers drawn from a bell curve with mean about 5.04 and standard deviation about 2.01 (seeded, so the figure is reproducible), and split them into batches.

Over 200 batches, the batch mean wobbles with a standard deviation of 1.52 at batch size 2 and 0.38 at batch size 32, close to the textbook rule that the wobble shrinks with the square root of the batch size (2.01/2≈1.422.01/\sqrt{2} \approx 1.42, 2.01/32≈0.362.01/\sqrt{32} \approx 0.36).

So batch norm gets unreliable when batches are small, which is common when each example is large (long audio clips, high-resolution images) or when you generate one item at a time. It is also awkward for sequences of different lengths. Those are exactly the problems layer normalisation avoids.

A dataset with batches drawn from it; each batch mean drops a tick, scattering widely for small batches
Batch size
Draw
Figure 6. Small batches, noisy statistics. At batch size 2 the batch means scatter from 1.83 to 6.89; at batch size 32 they stay between 4.77 and 5.73.

Layer normalisation

Layer normalisation (Ba, Kiros and Hinton, 2016) uses the same recipe but turns it sideways: instead of normalising each feature over the batch (down a column), it normalises each example over its own features (across a row).

μ=1d∑j=1dxjσ2=1d∑j=1d(xj−μ)2yj=γjxj−μσ2+ϵ+βj\mu = \frac{1}{d}\sum_{j=1}^{d} x_j \qquad \sigma^2 = \frac{1}{d}\sum_{j=1}^{d} (x_j - \mu)^2 \qquad y_j = \gamma_j \frac{x_j - \mu}{\sqrt{\sigma^2 + \epsilon}} + \beta_j

Worked on the same matrix

Take the same four rows.

Example 1 (2,1,202, 1, 20):

Doing the same for each row (means 7.67, 22.33, 8.33, 24.33; standard deviations 8.73, 26.64, 8.26, 25.30):

feature 1feature 2feature 3
example 1−0.65−0.761.41
example 2−0.69−0.731.41
example 3−0.77−0.651.41
example 4−0.80−0.611.41

Now each row has mean 0 and standard deviation 1. Notice what happened: because feature 3 is so much bigger than the others, it dominates every row, and all four rows come out looking almost alike. That is a lesson in itself. Layer norm compares the features of one example with each other, so it only makes sense when those features are comparable, like the hidden units of a layer that all come out of the same kind of weighted sum. It is not a replacement for standardising raw inputs measured in different units.

Why Transformers and speech models use it

One batch normalised down its columns or across its rows, with the three results side by side
Normaliser
Figure 7. Down the columns or across the rows. BatchNorm averages each feature over the batch; LayerNorm and RMSNorm average each example over its features. Compare example 1: BatchNorm gives −1, −1, −1; LayerNorm −0.65, −0.76, 1.41; RMSNorm 0.17, 0.09, 1.72.

RMSNorm

RMSNorm (Zhang and Sennrich, 2019) asks: do we need to subtract the mean at all? Their answer, tested on translation and other tasks, was that the re-scaling is what matters most, and skipping the re-centring gave similar quality while running faster.

In one sentence: divide each example’s features by their root mean square, the square root of the average of the squares.

RMS(x)=1d∑j=1dxj2yj=γjxjRMS(x)+ϵ\text{RMS}(x) = \sqrt{\frac{1}{d}\sum_{j=1}^{d} x_j^2} \qquad y_j = \gamma_j \frac{x_j}{\text{RMS}(x) + \epsilon}

Picture: layer norm recentres a group photo and then zooms; RMSNorm only zooms.

Worked by hand

Take the vector (1,5,5,7)(1, 5, 5, 7):

The output has root mean square exactly 1, but its mean is 0.90.9, not 0: RMSNorm controls size, not centre.

On our matrix, example 1 (2,1,202, 1, 20): squares 4+1+400=4054 + 1 + 400 = 405, mean 135135, root mean square 135=11.62\sqrt{135} = 11.62, output 0.17,0.09,1.720.17, 0.09, 1.72. All four rows:

feature 1feature 2feature 3
example 10.170.091.72
example 20.120.091.73
example 30.170.261.70
example 40.110.261.71

What would change? If a row’s mean is already near 0, the root mean square and the standard deviation are almost the same number, and RMSNorm and layer norm give almost the same output. Hidden activations in big networks are often roughly centred, which is part of why dropping the mean costs so little.

Why it is cheaper, and who uses it

Layer norm needs two passes over the numbers (one for the mean, then one for the variance around it) and two learned vectors. RMSNorm needs one sum of squares and one learned vector. That saving is small per layer but it happens at every layer for every token. Zhang and Sennrich report running-time reductions of 7% to 64% depending on the model.

It is now the standard in large language models. LLaMA (Touvron and colleagues, 2023) normalises the input of each Transformer sub-layer with RMSNorm. Kyutai’s Moshi does the same in its Helium language model: its paper states it uses “RMS normalization (Zhang and Sennrich, 2019) at the input of the attention blocks, the feed-forward blocks and the output linear layer”.

Pre-norm and post-norm

Transformers are built from residual blocks: a sub-layer (attention or a small feed-forward network) whose output is added back to its own input, x+sublayer(x)x + \text{sublayer}(x). The full story is in post 11. There are two places to put the normalisation:

Xiong and colleagues (2020) showed why the difference matters. With post-norm, the gradients near the output are large at the start of training, so the model needs a learning-rate warm-up (from post 3) to avoid blowing up. With pre-norm the main path is a clean highway of additions, gradients are well behaved from the first step, and warm-up can be shortened or dropped. Most modern large models, LLaMA and Moshi’s Helium among them, use pre-norm. A pre-norm stack usually adds one final normalisation before the output layer, since nothing else normalises the highway.

Post-norm and pre-norm residual blocks with a dot passing through each operation
Figure 8. Where the norm sits. Post-norm normalises after the addition; pre-norm normalises only the branch, leaving the main path clean.

Where normalisation sits in a network

Putting it together, in a typical network you will find:

Counting the parameters

Normalisation adds very few parameters. The mean and variance are computed from the data, not learned; only the per-feature gain γ\gamma and shift β\beta are trained.

For our table’s 3 features: 6, 6 and 3. Batch norm also keeps its running mean and variance, but those are buffers: saved with the model and updated by averaging, never by gradient descent, so they are not counted as parameters.

import torch.nn as nn

for layer in [nn.BatchNorm1d(3), nn.LayerNorm(3), nn.RMSNorm(3)]:
    params = sum(p.numel() for p in layer.parameters())
    buffers = [name for name, _ in layer.named_buffers()]
    print(type(layer).__name__, params, buffers)

It prints:

BatchNorm1d 6 ['running_mean', 'running_var', 'num_batches_tracked']
LayerNorm 6 []
RMSNorm 3 []

Even in a large model this is tiny: a layer norm at width 768 has 2×768=1,5362 \times 768 = 1{,}536 parameters, next to millions in the layers around it.

What to remember

Try it yourself

Sources