24. Layer Normalization From Scratch
Problem
Layer normalization normalizes each individual example's activations across the feature dimension, independent of the rest of the batch — unlike batch norm, which normalizes across the batch for each feature. That batch-independence is exactly why it became the default normalization layer for transformers and sequence models: it works identically whether you're training with batch size 512 or doing greedy decoding one token at a time.
DeepMind's research spans a wide range of normalization variants built on this same idea (RMSNorm, used in Gopher and Chinchilla, drops the mean-centering step entirely), so knowing the exact mechanics of the original LayerNorm formula is the right starting point: normalize over the last dimension using that example's own mean and (biased) variance, then apply a learned per-feature affine transform.
Source: src/24_layer_norm_from_scratch.py
def layer_norm(
x: torch.Tensor, gamma: torch.Tensor, beta: torch.Tensor, eps: float = 1e-5,
) -> torch.Tensor: ...
>>> x = torch.randn(2, 3, 8)
>>> gamma, beta = torch.ones(8), torch.zeros(8)
>>> layer_norm(x, gamma, beta).shape
torch.Size([2, 3, 8])
>>> layer_norm(x, gamma, beta).mean(dim=-1).abs().max().item() < 1e-5
True
Step-by-Step Approach
- Compute the per-example mean over the last dimension with
keepdim=True, so it broadcasts cleanly against the original shape. - Compute the per-example variance over the last dimension with
unbiased=False— LayerNorm uses the population variance (divide byd), not the sample variance (divide byd-1). - Normalize:
(x - mean) / sqrt(var + eps), withepsinside the square root for numerical stability when variance is near zero. - Apply the learned affine transform: multiply by
gammaand addbeta, both shape(d,), broadcasting against every leading dimension ofx. - Sanity-check with an identity affine (
gamma=1,beta=0): the output's per-example mean should be ~0 and variance ~1 along the last dimension.
The entire operation only ever reduces over the last dimension, regardless of how many leading batch/sequence dimensions x has — using dim=-1, keepdim=True throughout is what makes the same three lines of code correct for a 2D (batch, d) input and a 3D (batch, seq, d) input alike.
Reference solution
import torch
import torch.nn.functional as F
def layer_norm(
x: torch.Tensor, gamma: torch.Tensor, beta: torch.Tensor, eps: float = 1e-5,
) -> torch.Tensor:
# per-example statistics over the feature dim only -- keepdim so the
# subtraction/division below broadcast against x's leading dims cleanly
mean = x.mean(dim=-1, keepdim=True)
# unbiased=False: LayerNorm uses the population (1/d) variance, not the
# sample (1/(d-1)) variance -- using the wrong one is a common bug
var = x.var(dim=-1, unbiased=False, keepdim=True)
x_hat = (x - mean) / torch.sqrt(var + eps)
return x_hat * gamma + beta
Key Functions & Tricks
Tensor.mean(dim=-1, keepdim=True)— per-example mean over the feature dimension, kept as a broadcastable shapeTensor.var(dim=-1, unbiased=False, keepdim=True)— population variance over the feature dimension; the unbiased=False flag is the detail interviewers probe fortorch.sqrt(var + eps)— eps inside the square root, not just added after, keeps the denominator strictly positive even at zero varianceF.layer_norm— PyTorch's own built-in, used here only as an independent ground-truth check, not as the required implementation
How to Recognize This Pattern
Recognize this pattern whenever a problem asks you to normalize a tensor's values along a single axis using statistics computed from that same axis, then rescale with learned per-feature parameters — the axis choice (feature dim vs. batch dim vs. spatial dims) is what distinguishes LayerNorm from BatchNorm, InstanceNorm, and GroupNorm, all of which share this same normalize-then-affine skeleton. A common variation is RMSNorm, which drops the mean-subtraction step and the additive bias entirely (see the companion problem). The most common pitfall is using the sample (unbiased) variance instead of the population variance, which silently diverges from the standard LayerNorm definition on small feature dimensions.