§4.2–4.3Layer Normalization & the GELU Feed-Forward Network

Part II pp. 53–55 · ~7 min read

  • layer normalization
  • vanishing gradient
  • GELU
  • feed forward network

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 or exploding gradients. The result is unstable training dynamics: the network struggles to find weights that minimize the loss. Layer normalization 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.

LayerNorm(x)=γxμσ2+ϵ+β\mathrm{LayerNorm}(x) = \gamma \odot \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} + \beta

μ=1nixiσ2=1ni(xiμ)2\mu = \tfrac{1}{n}\textstyle\sum_i x_i \qquad \sigma^2 = \tfrac{1}{n}\textstyle\sum_i (x_i - \mu)^2

symbolmeaningshape
xxone token’s activation vector(emb_dim,)
μ, σ2\mu,\ \sigma^2mean and variance across that vector’s features (dim=-1)scalars per token
ϵ\epsilontiny constant (1e-5) so a zero-variance vector can’t cause division by zero
γ\gamma (scale), β\beta (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:

layer output 0.2260 0.3470 0.0000 0.2216 0.0000 0.0000 μ = 0.1324 subtract μ = 0.1324 · divide by √(σ² + ε), σ² = 0.0231 → σ ≈ 0.152 normalized 0.61 1.41 -0.87 0.59 -0.87 -0.87 μ = 0.00 σ² = 1.00 zero-centered mean and unit variance — whatever the layer produced
1

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.

1 / 3

Layer normalization of one 6-value activation vector (the book's p. 53 example). The pattern of the values survives; the statistics are standardized.

</> The LayerNorm module
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

Plain tensor.var() defaults to the unbiased estimate (divide by n1n-1, Bessel’s correction); the class passes unbiased=False (divide by nn) for GPT-2 compatibility. At n=768n = 768 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 γ,β\gamma, \beta 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 (Gaussian Error Linear Unit), used here via its tanh approximation:

GELU(x)0.5x(1+tanh ⁣[2π(x+0.044715x3)])\mathrm{GELU}(x) \approx 0.5 \cdot x \cdot \left(1 + \tanh\!\left[\sqrt{\tfrac{2}{\pi}}\left(x + 0.044715\,x^3\right)\right]\right)

symbolmeaning
0.5x(1+tanh[])0.5x(1 + \tanh[\cdot])a smooth ramp: ≈ 0 for very negative xx, ≈ xx for large positive xx
2/π, 0.044715\sqrt{2/\pi},\ 0.044715constants of the tanh approximation to the exact Gaussian-CDF formulation

Worked example: GELU(1)0.159\mathrm{GELU}(-1) \approx -0.159 — small but not zero (ReLU would output exactly 0), and GELU(1)0.841\mathrm{GELU}(1) \approx 0.841. Explore both curves — hover to compare, and note the smooth dip below zero around x0.75x \approx -0.75:

GELU(x)ReLU(x)
-4-2024-0.116603.9
hover the plot to read exact values
GELU vs ReLU. ReLU is a hard hinge: zero for all negatives, kinked at 0. GELU is smooth everywhere, lets small negative inputs contribute a small negative output, and approaches ReLU for large |x|.
GELU'(x) (numeric)ReLU'(x)
-4-2024-0.112801.1
hover the plot to read exact values
Their gradients. ReLU's is a step: exactly 0 for x < 0 — a 'dead' region where no learning signal flows. GELU's gradient is smooth, non-zero for small negative x, enabling nuanced weight updates during training.
</> GELU and the FeedForward module
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.
x per position768Linearhidden3072 (4×)GELUsmooth nonlinearityLinearoutput768applied to EVERY position independently — no mixing between tokens (that’s attention’s job)
The expand-then-contract FFN: Linear(768→3072) → GELU → Linear(3072→768). Roughly 4.7M parameters per block — the biggest single consumer of a GPT’s parameter budget.

Key idea — attention mixes, the FFN thinks

The feed-forward network 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 / 5
  1. 1.Across which dimension does the GPT's LayerNorm compute mean and variance?

  2. 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. 3.Why does LayerNorm add a small eps (1e-5) inside the square root?

  4. 4.What advantage does GELU have over ReLU for training GPTs?

  5. 5.The FeedForward module maps 768 → 3072 → 768. Why expand 4× and contract back?