The backbone works but its blocks are hollow. This unit builds components 2 and 3–4 of the chapter plan: layer normalization, which keeps activations statistically tame as depth grows, and the GELU feed-forward network, the transformer block’s per-position workhorse.
4.2 Normalizing activations with layer normalization
Training deep networks is fragile: as signals and gradients pass through many layers, they can shrink toward zero or blow up — vanishing vanishing gradient Gradients shrinking toward zero as they propagate backward through many layers, starving early layers of learning signal. defined in ch. 4 — open in glossary or exploding gradients. The result is unstable training dynamics: the network struggles to find weights that minimize the loss. Layer normalization layer normalization Normalizing each token's activations to mean 0 and unit variance across the feature dimension, then applying a learnable scale and shift — stabilizes deep-network training. defined in ch. 4 — open in glossary tames this by adjusting the activations of a layer to have mean 0 and variance 1 (unit variance) — every token vector, individually, at every normalized layer.
| symbol | meaning | shape |
|---|---|---|
| one token’s activation vector | (emb_dim,) | |
mean and variance across that vector’s features (dim=-1) | scalars per token | |
| tiny constant (1e-5) so a zero-variance vector can’t cause division by zero | — | |
(scale), (shift) | learnable per-feature vectors, initialized to 1s and 0s — let the model undo or re-tune the normalization if that helps training | (emb_dim,) |
Watch it work on real numbers. A toy layer (Linear(5, 6) + ReLU) produced
the output below; step through the normalization exactly as the book’s p. 53
figure draws it:
A raw layer output — statistics all over the place
The 6 activations [0.2260, 0.3470, 0.0000, 0.2216, 0.0000, 0.0000] (ReLU zeroed the negatives) have mean 0.1324 and variance 0.0231. The NEXT layer would receive inputs whose scale depends on everything upstream — and this drift compounds across 12 blocks.
Layer normalization of one 6-value activation vector (the book's p. 53 example). The pattern of the values survives; the statistics are standardized.
class LayerNorm(nn.Module):
def __init__(self, emb_dim):
super().__init__()
self.eps = 1e-5
self.scale = nn.Parameter(torch.ones(emb_dim)) #A
self.shift = nn.Parameter(torch.zeros(emb_dim))
def forward(self, x):
mean = x.mean(dim=-1, keepdim=True) #B
var = x.var(dim=-1, keepdim=True, unbiased=False) #C
norm_x = (x - mean) / torch.sqrt(var + self.eps)
return self.scale * norm_x + self.shift - #A γ and β are trainable (emb_dim,) vectors — the only parameters LayerNorm adds: 2 × 768 = 1,536 per instance.
- #B dim=-1: statistics across the FEATURE dimension, per token. keepdim=True keeps the result broadcastable for the subtraction.
- #C unbiased=False → divide by n (biased variance), not n−1. For n = 768 the difference is negligible, and it matches GPT-2's original TensorFlow implementation — important when loading OpenAI's weights in ch5.
Biased vs. unbiased variance — and a stray comment
Plaintensor.var() defaults to the unbiased estimate (divide by ,
Bessel’s correction); the class passes unbiased=False (divide by ) for
GPT-2 compatibility. At features the two are practically identical.
Also note: the source PDF carries a stray “L2 Normalization” comment above
this class — that label is wrong. This is standard layer normalization
(standardizing to mean 0/variance 1), not L2-norm scaling of a vector to unit
length.Key idea — normalize per token, learn to re-scale
LayerNorm gives every token vector, at every normalized point in the network, mean 0 and variance 1 — so each layer receives inputs on a predictable scale regardless of depth. The learnable mean this is a soft constraint: the network can adjust the normalized distribution wherever that helps. The transformer block (§4.5) applies it before the attention and feed-forward sublayers; the GPT applies it once more before the output head.4.3 The feed-forward network with GELU activations
The second half of every transformer block is a small per-position MLP. Its activation is not ReLU but GELU gelu Gaussian Error Linear Unit — the smooth activation ≈ 0.5x(1+tanh(√(2/π)(x+0.044715x³))) used in GPT's feed-forward layers. defined in ch. 4 — open in glossary (Gaussian Error Linear Unit), used here via its tanh approximation:
| symbol | meaning |
|---|---|
| a smooth ramp: ≈ 0 for very negative , ≈ for large positive | |
| constants of the tanh approximation to the exact Gaussian-CDF formulation |
Worked example: — small but not zero (ReLU would output exactly 0), and . Explore both curves — hover to compare, and note the smooth dip below zero around :
class GELU(nn.Module):
def __init__(self):
super().__init__()
def forward(self, x):
return 0.5 * x * (1 + torch.tanh(
torch.sqrt(torch.tensor(2.0 / torch.pi)) *
(x + 0.044715 * torch.pow(x, 3))
))
class FeedForward(nn.Module):
def __init__(self, cfg):
super().__init__()
self.layers = nn.Sequential(
nn.Linear(cfg["emb_dim"], 4 * cfg["emb_dim"]), #A
GELU(),
nn.Linear(4 * cfg["emb_dim"], cfg["emb_dim"]), #B
)
def forward(self, x):
return self.layers(x) - #A Expansion: 768 → 3072 (4×). Each token position gets a wider hidden representation to compute in.
- #B Contraction: 3072 → 768, back to emb_dim — so the block stays shape-preserving and residual connections (§4.4) can add input to output.
Key idea — attention mixes, the FFN thinks
The feed-forward network feed forward network The per-position Linear → GELU → Linear module of a transformer block, expanding 4× then contracting (768→3072→768 in GPT-2 small). defined in ch. 4 — open in glossary touches each position independently — no token sees another inside it. Pairing it with attention (which does nothing but relate positions) gives the block its division of labor, spelled out in §4.5: attention gathers global context, the FFN performs a rich per-position transformation of it.Two of the four building blocks are done. Next: shortcut connections and the assembled transformer block.
📝 Check yourself: LayerNorm & GELU
0 / 51.Across which dimension does the GPT's LayerNorm compute mean and variance?
2.Predict: LayerNorm processes the layer output [0.2260, 0.3470, 0.0000, 0.2216, 0.0000, 0.0000] (mean 0.1324). What are the mean and variance AFTER normalization?
3.Why does LayerNorm add a small eps (1e-5) inside the square root?
4.What advantage does GELU have over ReLU for training GPTs?
5.The FeedForward module maps 768 → 3072 → 768. Why expand 4× and contract back?