Besides attention, a transformer is built from a handful of simple layers:
- an embedding table that turns token ids into vectors;
- a normalization layer before every block, which keeps the numbers at a steady scale;
- dropout in some models, which randomly switches off parts of the network during training;
- weights that must start at the right size (initialization).
Each one is a few lines of NumPy, but each has a detail that silently breaks training when it is wrong. This page explains each layer with a small example, and points out that detail.
Embeddings: a lookup table that learns
A language model works on token ids: integers from to , where is the size of the vocabulary. But a network computes with vectors, not with ids. An embedding gives every id its own vector of numbers. All these vectors are stored in a matrix with rows and columns: the vector of token is simply row .
The numbers in are parameters, learned like any other weight. Bengio et al. (2003) introduced this idea for language models: during training, words with similar roles end up with similar rows.
Example. A vocabulary of tokens, vectors of numbers: =
[[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]]. The text with ids[2, 0, 2]becomes rows 2, 0 and 2:[[0.5, 0.6], [0.1, 0.2], [0.5, 0.6]].
The forward pass is just indexing, E[ids], for an array of ids of any shape. It gives the same result as multiplying a one-hot matrix by , without ever building that matrix (see the one-hot exercise in the NumPy section).
The backward pass is where the bug hides. Each output row came from one row of , so its gradient goes back to that row; rows that were not used get 0. And a row used twice gets both gradients added (gradients add up, see section 1). A batch of text repeats tokens all the time: "the", spaces, punctuation.
Example. Same
ids = [2, 0, 2], with upstream gradients[1, 1],[2, 2]and[3, 3]for the three output rows. The gradient of must be:
- row 0:
[2, 2](used once, by the second output);- row 1:
[0, 0](not used);- row 2:
[1, 1] + [3, 3] = [4, 4](used by the first and third outputs).In NumPy,
dE[ids] += gradreturns[3, 3]for row 2: it keeps only the last of the repeated updates.np.add.at(dE, ids, grad)returns the correct[4, 4].
LayerNorm
Deep networks train badly when the size of the numbers drifts from layer to layer: some layers end up working with values in the thousands, others with values close to 0. LayerNorm (layer normalization; Ba, Kiros and Hinton, 2016) fixes the scale of every token's vector on its own, in three steps:
- compute the mean of the vector's numbers, and their variance : the average squared distance from the mean, a measure of how spread out they are;
- subtract the mean and divide by the standard deviation : now the vector has mean 0 and variance 1;
- multiply by a learned gain and add a learned bias , so the network can still choose its own scale and shift.
As formulas, first the mean and the variance:
Then the normalized vector and the output:
where:
- is one token's vector, with entries ;
- (mu) is its mean and (sigma squared) its variance;
- ("x hat") is the normalized vector: mean 0 and variance 1;
- (gamma) and (beta) are learned vectors of numbers;
- means elementwise multiplication;
- (epsilon) is a small constant that avoids dividing by zero.
Example. .
- Mean: . Variance: the distances from the mean are , their squares , average .
- Normalized: .
- With and : .
The vector , ten times larger, gives exactly the same : the scale of the input is gone, which is the point.
Two details match PyTorch's LayerNorm: the variance divides by (not ), and (default ) goes inside the square root.
Unlike batch normalization, nothing depends on the other examples in the batch. So LayerNorm behaves identically at training and inference time, and works with a batch of one.
The backward pass is worth deriving once, because every entry of depends on every entry of , through and . With (the upstream gradient times the gain) and :
where a bar means the mean over the features. In words: the two subtracted terms remove the parts of the gradient that would change the mean and the variance of , because normalization cancels those changes anyway. For example, adding the same number to every entry of does not change , so the gradient must not push in that direction: subtracting makes its entries add up to 0.
RMSNorm
RMSNorm (root mean square layer normalization; Zhang and Sennrich, 2019) keeps only the rescaling: it does not subtract the mean, and has no bias.
where , the RMS (root mean square) of the vector, is the square root of the mean of its squared entries: a measure of its typical size, like the standard deviation but measured from 0 instead of from the mean.
Example. The same : the squares are , their mean is , and . With the output is . Compare with LayerNorm above: the mean is not removed, so all the entries stay positive.
It is cheaper (one reduction over the vector instead of two), and Zhang and Sennrich report quality comparable to LayerNorm. LLaMA (Large Language Model Meta AI) uses it, applied to the input of every sub-layer (pre-normalization).
Dropout
Dropout (Hinton et al., 2012; Srivastava et al., 2014) sets each activation to zero with probability during training, so the network cannot rely on any single unit and has to spread what it learns. To keep the average value of each activation unchanged, the survivors are multiplied by ("inverted" dropout). At inference time dropout does nothing at all.
where:
- is the probability of dropping an entry;
- is the mask: each is 1 with probability and 0 otherwise (that is what "" means);
- dividing by makes up for the dropped entries.
Example. , , and the random mask comes out as . Then .
Each entry survives half of the time, with value 2, and is 0 the other half: on average it is still 1. Without the division, the average would drop to 0.5 during training and jump back to 1 at inference, and the next layer would see different numbers in the two cases.
The backward pass reuses the same mask: . When a model sees each training token only about once, there is little to overfit: nanoGPT defaults to dropout = 0.0 for pre-training and suggests 0.1 or more for fine-tuning.
Initialization
Before training, every weight is set to a random number. The size of those random numbers matters a lot. Take a linear layer with inputs. Suppose the weights and the inputs are independent, have mean zero, and have variances and . Each output adds up products, so
where is the variance, the average squared distance from the mean. In words: one layer multiplies the variance of the signal by .
Stack 50 layers and that factor is raised to the 50th power. If it is 1.5, the activations grow by ; if it is 0.5, they shrink to almost nothing.
Example. A random input passed through 10 layers of width 512, each a matrix product followed by ReLU (rectified linear unit), measured with NumPy. The standard deviation of the output:
- weights with standard deviation 1: about , the numbers explode;
- weights with standard deviation 0.01: about , the signal vanishes;
- weights with standard deviation (He initialization, below): about 0.9, it stays steady.
So the weights must start with a variance tied to the layer's size, chosen to make the factor about 1:
- Xavier/Glorot (Glorot and Bengio, 2010): , a compromise between keeping the forward activations and the backward gradients stable. It is derived for activations that are roughly linear around 0, such as tanh.
- He/Kaiming (He et al., 2015): ReLU zeroes half its inputs, which halves the variance, so it uses : the factor 2 makes up for the lost half.
Models in the style of GPT-2 (Generative Pre-trained Transformer 2) use a simpler rule: every weight is drawn from , a normal distribution with mean 0 and standard deviation . The projections that write into the residual stream are scaled down further, by for layers, because of them are added together. This is what nanoGPT's model.py does.
References
- Y. Bengio, R. Ducharme, P. Vincent, C. Jauvin, A Neural Probabilistic Language Model, JMLR 3, 1137–1155, 2003. Learned word embeddings.
- J. L. Ba, J. R. Kiros, G. E. Hinton, Layer Normalization, arXiv, 2016.
- B. Zhang, R. Sennrich, Root Mean Square Layer Normalization, NeurIPS, 2019.
- H. Touvron et al., LLaMA: Open and Efficient Foundation Language Models, arXiv, 2023. Section 2.2: pre-normalization with RMSNorm.
- G. E. Hinton, N. Srivastava, A. Krizhevsky, I. Sutskever, R. R. Salakhutdinov, Improving neural networks by preventing co-adaptation of feature detectors, arXiv, 2012.
- N. Srivastava, G. Hinton, A. Krizhevsky, I. Sutskever, R. Salakhutdinov, Dropout: A Simple Way to Prevent Neural Networks from Overfitting, JMLR 15(56), 1929–1958, 2014.
- X. Glorot, Y. Bengio, Understanding the difficulty of training deep feedforward neural networks, AISTATS, 2010.
- K. He, X. Zhang, S. Ren, J. Sun, Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification, ICCV, 1026–1034, 2015.
- PyTorch documentation:
torch.nn.LayerNormandtorch.nn.RMSNorm.