Index Ai Academy
Phase 4 · Deep learning · Training

Normalization layers

Batch and layer norm keep training stable.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

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

  1. Standardize inside the network
  2. Which numbers get averaged
  3. Worked example: the same scores, two ways
  4. A network that only trains with it
  5. Batch norm's two modes
  6. Why transformers use layer norm
  7. 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:

  • xx is one activation.
  • μ\mu (the Greek letter mu) is the mean of the group of activations.
  • σ2\sigma^2 (sigma squared) is their variance, the average squared distance from the mean.
  • ϵ\epsilon (epsilon) is a tiny number, such as 0.00001. It stops you dividing by zero.
  • x^\hat{x} (x-hat) is the standardized value.
  • γ\gamma (gamma) is the learned scale.
  • β\beta (beta) is the learned shift.
x^=x−μσ2+ϵy=γx^+β\hat{x} = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \qquad y = \gamma \hat{x} + \beta

In plain words: subtract the average. Divide by the spread (the square root of the variance). Then stretch by γ\gamma and slide by β\beta. The output is yy.

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 γ\gamma is 2 and β\beta 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])
It runs right here in your browser, with nothing to install.

The tiny ϵ\epsilon 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 γ\gamma and β\beta, the network can pick its own mean and spread for each feature.

Both start out neutral. γ\gamma starts at 1 and β\beta 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.

Quick check
This code standardizes two rows the way a norm layer does (ignoring the tiny epsilon). What does it print?
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])
  1. [-1.22, 0.0, 1.22] twice
  2. [1.0, 2.0, 3.0] then [10.0, 20.0, 30.0]
  3. [-1.0, 0.0, 1.0] then [-10.0, 0.0, 10.0]
  4. [-1.22, 0.0, 1.22] then [-12.2, 0.0, 12.2]
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?"
blankStatistics computed overBehaves differently at eval?Typical home
BatchNormeach feature, across the examples in the batchyes: uses running averagesCNNs, MLPs
LayerNormeach example, across its featuresnotransformers, 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.

Try it now

They all rescale numbers to a steady size. They differ only in which numbers go into the average.

Worked example

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))
In the lesson you can change this and run it.

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.

Quick check
A batch of activations has shape (32, 64): 32 examples, each with 64 features. How many separate means does BatchNorm compute?
  1. 1, for the whole batch
  2. 32, one per example
  3. 2,048, one per number
  4. 64, one per feature
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.
Quick check
Why does BatchNorm keep a running average during training?
  1. To speed up backprop, since the running figures have no slope to compute
  2. To save memory, since it no longer has to hold every batch's numbers
  3. To shuffle the data, so each batch has a fair mix of examples in it
  4. At test time there may be no batch to take an average from
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.

Quick check
Which normalization gives the same output for an example no matter what else is in its batch, in both training and inference?
  1. BatchNorm
  2. LayerNorm
  3. Neither
  4. Both
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 ff. Pre-norm places the norm layer before it:

x+f(norm(x))x + f(\text{norm}(x))

In plain words: standardize the numbers, run them through the sub-layer, then add the result back onto the original xx. The original xx itself is never standardized. The sub-layer's output is simply added on top.

A small example. Take xx = 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: norm(x+f(x))\text{norm}(x + f(x)). That's called post-norm. With post-norm, the main path gets re-standardized after every sub-layer. With pre-norm, the main path xx 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, γ\gamma, as before.

y=xmean(x2)+ϵ⋅γy = \frac{x}{\sqrt{\text{mean}(x^2) + \epsilon}} \cdot \gamma

In plain words: work out the typical size of the numbers, then divide every number by it. Then stretch by the learned scale γ\gamma. 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.

Quick check
This code applies RMSNorm by hand to two numbers (no epsilon, scale 1). What does it print?
import math
x = [1, 7]
rms = math.sqrt((1 * 1 + 7 * 7) / 2)
print(rms, [round(v / rms, 2) for v in x])
  1. 5.0 [0.2, 1.4]
  2. 4.0 [0.25, 1.75]
  3. 7.07 [0.14, 0.99]
  4. 5.0 [-1.0, 1.0]
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

Create a free account

Try a quiz question

For activations shaped (batch, features), over which dimension does BatchNorm compute its mean?

  1. Over the batch, separately for each feature
  2. Over the features, separately for each example
  3. Over everything
  4. Over time steps

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson