Normalization layers
Batch and layer norm keep training stable.
Create a free account Try part of it here
Why it matters
As a network trains, its weights change at every step. So the size of the numbers each layer receives keeps shifting. In a deep network, small shifts pile up from one layer to the next. Normalization layers fix this. They re-center and re-scale the numbers inside the network, so every layer sees values of a sensible size. Batch norm made very deep image models practical. Layer norm, and its cousin RMSNorm, sits in every block of every transformer. That includes the large language models you use, which are the models behind chat assistants.
What you'll be able to do
- Explain what batch normalization and layer normalization compute, and over which axis
- Show a deep network that won't train without normalization and does with it
- Know batch norm's train/eval difference and why transformers use layer norm instead
- Recognize RMSNorm and pre-norm placement in modern LLMs
What it covers
- Standardize inside the network
- Which numbers get averaged
- Worked example: the same scores, two ways
- A network that only trains with it
- Batch norm's two modes
- Why transformers use layer norm
- Two refinements in modern LLMs
Standardize inside the network
Picture a teacher who grades on a curve. One test was hard, so the raw scores are low. Another test was easy, so the raw scores are high. To compare them fairly, the teacher asks a simpler question for each score: how far above or below the average is it?
You did the same thing to input features in Phase 3. You standardized them: subtract the mean, then divide by the standard deviation. The result has a mean of 0 and a spread of about 1.
A normalization layer (often called a norm layer) does this inside the network. It takes the numbers a layer puts out, called its activations, and standardizes them. Then it hands them to the next layer. As the weights change during training, the next layer still sees numbers of a steady size.
There's one twist. After standardizing, the norm layer multiplies by a learned scale and adds a learned shift. So if some other size works better, the network can learn it.
Here is the full recipe. A few symbols first, one at a time:
- is one activation.
- (the Greek letter mu) is the mean of the group of activations.
- (sigma squared) is their variance, the average squared distance from the mean.
- (epsilon) is a tiny number, such as 0.00001. It stops you dividing by zero.
- (x-hat) is the standardized value.
- (gamma) is the learned scale.
- (beta) is the learned shift.
In plain words: subtract the average. Divide by the spread (the square root of the variance). Then stretch by and slide by . The output is .
Try it on one example with four activations: 2, 4, 4 and 10. The mean is 20 ÷ 4 = 5. The distances from the mean are −3, −1, −1 and 5. Square them to get 9, 1, 1 and 25. Their average is 36 ÷ 4 = 9, so the variance is 9 and the spread is √9 = 3. Divide each distance by 3:
- x-hat is −1, −0.333, −0.333 and 1.667.
Now say is 2 and is 0.5. Double each value and add 0.5:
- y is −1.5, −0.167, −0.167 and 3.833.
The same steps in Python:
import math x = [2, 4, 4, 10] # one example's four activations gamma, beta, eps = 2.0, 0.5, 1e-5 mu = sum(x) / len(x) # the mean var = sum((v - mu) ** 2 for v in x) / len(x) # the variance x_hat = [(v - mu) / math.sqrt(var + eps) for v in x] # standardized y = [gamma * h + beta for h in x_hat] # scaled and shifted print("mean", mu, " variance", var) print("x_hat", [round(h, 3) for h in x_hat]) print("y ", [round(v, 3) for v in y])
The tiny barely changes the answer here. It matters when the numbers in the group are all nearly the same, so the variance is 0 or very close to it.
Go deeper Why learn a scale and shift at all?
Standardizing forces a mean of 0 and a spread of 1. That isn't always what the next layer wants. A tanh unit, for example, might work better with a wider spread. With and , the network can pick its own mean and spread for each feature.
Both start out neutral. starts at 1 and starts at 0, so at first the layer outputs plain standardized numbers. Training then adjusts them like any other weight. The point is that the size is now held in two learned numbers, not left to drift as every earlier layer changes.
import statistics as st for row in ([1, 2, 3], [10, 20, 30]): mu, sd = st.mean(row), st.pstdev(row) print([round((v - mu) / sd, 2) for v in row])
Show the answer
[-1.22, 0.0, 1.22] twice
The second row is the first one times 10. That makes the mean and the spread 10 times bigger too, so they cancel out, and both rows print as [-1.22, 0.0, 1.22]. Standardizing removes the overall size and keeps only the pattern.
Which numbers get averaged
The recipe needs a group of numbers to take the mean and variance of. The two main norm layers differ only in which group they pick.
Inside a network, activations come in a grid. Each row is one example. Each column is one feature. The grid's shape is written (batch, features). Think of a class report card: one row per student, one column per subject.
- BatchNorm (batch normalization) works down each column. It compares each student with the rest of the class, one subject at a time. "How good is Ana's math compared with everyone's math?"
- LayerNorm (layer normalization) works across each row. It compares each student's subjects with their own other subjects. "Is math Ana's best subject?"
| blank | Statistics computed over | Behaves differently at eval? | Typical home |
|---|---|---|---|
| BatchNorm | each feature, across the examples in the batch | yes: uses running averages | CNNs, MLPs |
| LayerNorm | each example, across its features | no | transformers, RNNs |
The "eval" column is about using a trained model, which comes up in a moment. CNNs are image models. RNNs are older models for text and other sequences. You'll meet both later in this phase.
Here are both layers in PyTorch. The batch has 4 examples and 3 features, and the features have very different sizes.
import torch import torch.nn as nn torch.manual_seed(0) x = torch.randn(4, 3) * torch.tensor([1.0, 10.0, 100.0]) + 5 # 4 examples, 3 very different features print("raw feature means:", x.mean(dim=0)) bn, ln = nn.BatchNorm1d(3), nn.LayerNorm(3) b, l = bn(x), ln(x) print("after BatchNorm, each FEATURE (column): mean", b.mean(dim=0).detach().numpy().round(4) + 0.0, " std", b.std(dim=0, unbiased=False).detach().numpy().round(3)) print("after LayerNorm, each EXAMPLE (row): mean", l.mean(dim=1).detach().numpy().round(4) + 0.0, " std", l.std(dim=1, unbiased=False).detach().numpy().round(3))
(As always here, torch is the course's in-browser PyTorch subset; the same code runs on real PyTorch.)
The first line shows the raw features have very different averages. After BatchNorm, each column has a mean of 0 and a spread of 1. After LayerNorm, each row does.
They all rescale numbers to a steady size. They differ only in which numbers go into the average.
Worked example: the same scores, two ways
Three students took tests in math, art and music. Standardize the scores LayerNorm's way (each student across their own subjects) and BatchNorm's way (each subject across the class). The code leaves out the learned scale and shift and the tiny epsilon.
import statistics as st # math, art, music test scores for three students scores = {"Ana": [90, 60, 72], "Ben": [70, 80, 66], "Cy": [50, 70, 60]} def standardize(values): mu, sd = st.mean(values), st.pstdev(values) return [round((v - mu) / sd, 2) for v in values] print("LayerNorm style: each student against their own subjects") for name, row in scores.items(): print(" ", name, standardize(row)) print("BatchNorm style: each subject across the class") for i, subject in enumerate(["math", "art", "music"]): column = [row[i] for row in scores.values()] print(" ", subject, standardize(column))
Start with LayerNorm. Ana scored 90, 60 and 72. Her own average is 74. Her math score gets 1.3, the highest number in her row, so math is her best subject. Art gets −1.14, her weakest.
Now BatchNorm. The math scores were 90, 70 and 50, with a class average of 70. Ana's 90 becomes 1.22, the top of the class. Ben's 70 is exactly average, so it becomes 0.
Same scores, two different questions. A norm layer in a real network does the same sums with the statistics functions replaced by tensor operations. pstdev is the "biased" spread: it divides by the number of values, as the norm layers do.
Show the answer
64, one per feature
BatchNorm works down each column, so it computes one mean per feature: 64. LayerNorm works across each row and would compute 32, one per example.
A network that only trains with it
Here's the payoff. The code builds a deep network: eight layers, each followed by tanh. It trains on the handwritten digits for five passes over the data (five epochs). It uses mini-batch SGD, the training method from the mini-batches lesson. It does this three times: with no norm layers, with BatchNorm, and with LayerNorm.
import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split digits = load_digits() X = torch.tensor(digits.data / 16.0, dtype=torch.float32) y = torch.tensor(digits.target) X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=0) def deep_net(norm=None, depth=8, width=64): layers, n_in = [], 64 for _ in range(depth): layers += [nn.Linear(n_in, width)] + ([norm(width)] if norm else []) + [nn.Tanh()] n_in = width return nn.Sequential(*layers, nn.Linear(width, 10)) loss_fn = nn.CrossEntropyLoss() for name, norm in [("no normalization", None), ("BatchNorm1d", nn.BatchNorm1d), ("LayerNorm", nn.LayerNorm)]: torch.manual_seed(0) model = deep_net(norm) opt = torch.optim.SGD(model.parameters(), lr=0.05) for epoch in range(5): model.train() for xb, yb in DataLoader(TensorDataset(X_train, y_train), batch_size=64, shuffle=True): opt.zero_grad() loss_fn(model(xb), yb).backward() opt.step() model.eval() with torch.no_grad(): acc = (model(X_val).argmax(1) == y_val).float().mean().item() print(f"{name:<17} training loss {loss_fn(model(X_train), y_train).item():.3f} val accuracy {acc:.3f}")
Look at the training loss, which is the error on the training data, for each version. A model that spreads its bets evenly over the 10 digits scores a loss of about 2.3. That's the natural log of 10, written ln 10, and it means pure guessing. Without normalization, the deep network's loss is still about there. It hasn't started learning.
With either norm layer, it learns quickly. Norm layers also let you use larger learning rates, and they make training less sensitive to the starting weights you picked.
Batch norm's two modes
BatchNorm has a catch. Its mean and variance come from the current batch. That's fine during training, when the data arrives in batches anyway. But once the model is trained, you want each prediction to depend only on its own input, not on whatever else happens to share its batch.
So BatchNorm keeps a running average while it trains. Think of a café owner estimating a typical day's customers. They don't recount every day since opening. They keep a rough figure and nudge it a little after each new day. BatchNorm does the same with its mean and variance after every training batch.
Calling model.eval() switches the model into evaluation mode. In that mode, BatchNorm uses the stored averages instead of the batch's own numbers. Two things follow:
- Forgetting
model.eval()at test time gives different (usually worse) predictions. - Very small batches give noisy statistics. So BatchNorm struggles with batch sizes of 1 or 2.
Show the answer
At test time there may be no batch to take an average from
A single test example has no batch, and its answer shouldn't depend on what else happens to be in one.
Go deeper How the running average is updated
In PyTorch, the stored mean starts at 0 and the stored variance starts at 1. After each training batch, the stored mean becomes 0.9 × the old stored mean + 0.1 × this batch's mean. The variance is updated the same way. The 0.1 is the layer's momentum setting.
A small example: the stored mean is 5.0 and a new batch has mean 7.0. The new stored mean is 0.9 × 5.0 + 0.1 × 7.0 = 4.5 + 0.7 = 5.2. Recent batches count most, and old ones fade out gradually.
Why transformers use layer norm
Text brings two problems for BatchNorm. A batch can mix sentences of very different lengths. And when a model writes text, it often handles one sequence at a time, a batch of 1.
LayerNorm has neither problem. In a language model, which is a model that predicts the next token, each token (a word or piece of a word) has its own list of features. LayerNorm standardizes each token's features on their own, without looking at the rest of the batch. Because nothing comes from the batch, LayerNorm behaves the same in training and in use, at any batch size.
Show the answer
LayerNorm
LayerNorm's statistics come from the example's own features. BatchNorm's come from the batch during training, or from running averages in eval mode.
Two refinements in modern LLMs
Nearly every modern large language model (LLM) adds two changes to plain LayerNorm.
Pre-norm: normalize first
A transformer block has a few sub-layers. Each one is a chunk of the network, such as an MLP or the attention step you'll meet in Phase 5. Call a sub-layer . Pre-norm places the norm layer before it:
In plain words: standardize the numbers, run them through the sub-layer, then add the result back onto the original . The original itself is never standardized. The sub-layer's output is simply added on top.
A small example. Take = 2, 4, 4, 10 again. Standardized, that's −1, −0.333, −0.333 and 1.667. Pretend the sub-layer triples each number: −3, −1, −1 and 5. Add those to the original 2, 4, 4, 10 and you get −1, 3, 3 and 15.
import math def norm(v): mu = sum(v) / len(v) sd = math.sqrt(sum((a - mu) ** 2 for a in v) / len(v)) return [(a - mu) / sd for a in v] def f(v): # a stand-in for an attention or MLP sub-layer return [3 * a for a in v] x = [2, 4, 4, 10] out = [a + b for a, b in zip(x, f(norm(x)))] # pre-norm: x + f(norm(x)) print([round(v, 3) for v in out])
Pre-norm trains more stably in deep stacks. You'll see why in the lesson on residual connections, which are the shortcuts that let a signal skip a block.
Go deeper Pre-norm versus post-norm
The original 2017 transformer put the norm layer after the addition: . That's called post-norm. With post-norm, the main path gets re-standardized after every sub-layer. With pre-norm, the main path is never re-standardized. Each sub-layer only adds its output to it, and only the copy fed into each sub-layer is standardized. That clean path is easier to train when there are many blocks.
RMSNorm: skip the mean
RMSNorm drops the "subtract the mean" step. It only divides by the root mean square (RMS). Read the name from right to left: square each number, take the mean, then take the square root. Then it multiplies by a learned scale, , as before.
In plain words: work out the typical size of the numbers, then divide every number by it. Then stretch by the learned scale . Nothing gets re-centered, so the mean doesn't become 0.
With 2, 4, 4 and 10, the squares are 4, 16, 16 and 100. Their mean is 136 ÷ 4 = 34, and √34 ≈ 5.83. Dividing gives about 0.343, 0.686, 0.686 and 1.715. Compare LayerNorm's −1, −0.333, −0.333 and 1.667. The RMSNorm values are all still positive.
import math def rms_norm(v, eps=1e-6): rms = math.sqrt(sum(a * a for a in v) / len(v) + eps) return [round(a / rms, 3) for a in v] def layer_norm(v, eps=1e-5): mu = sum(v) / len(v) var = sum((a - mu) ** 2 for a in v) / len(v) return [round((a - mu) / math.sqrt(var + eps), 3) for a in v] x = [2, 4, 4, 10] print("RMSNorm: ", rms_norm(x)) print("LayerNorm:", layer_norm(x))
RMSNorm is slightly cheaper to compute and works as well in practice. Llama, Mistral and many other models use it.
import math x = [1, 7] rms = math.sqrt((1 * 1 + 7 * 7) / 2) print(rms, [round(v / rms, 2) for v in x])
Show the answer
5.0 [0.2, 1.4]
The squares are 1 and 49. Their mean is 50 ÷ 2 = 25, and √25 = 5. Dividing gives 0.2 and 1.4. There's no mean subtraction, so the answer isn't −1 and 1 as LayerNorm would give.
Practice what you just read
- 6 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 5 review cards for spaced repetition
Try a quiz question
For activations shaped (batch, features), over which dimension does BatchNorm compute its mean?