Layer Normalization & Residual Connections in Transformers
1886 words
9 min read
Visual companion
Python
Type and operator map
Python Week 1: the first filter for runtime behavior
View
Revision summary
What this note is really saying
Short form
# Layer Normalization & Residual Connections in Transformers ## 🎯 Learning Objectives - Explain why Transformers need normalization and residual connections - Compute layer normalization step by step - Distinguish layer norm from batch norm - Understand how residual connections enable deep networks - Analyze gradie...

Layer Normalization & Residual Connections in Transformers
🎯 Learning Objectives
- Explain why Transformers need normalization and residual connections
- Compute layer normalization step by step
- Distinguish layer norm from batch norm
- Understand how residual connections enable deep networks
- Analyze gradient flow through residual paths
📋 Prerequisites
- Basic neural network training concepts
- Gradient descent and backpropagation
- Transformer architecture
1. 📖 Core Content
1.1 Why Normalization?
Deep neural networks suffer from internal covariate shift: the distribution of layer inputs changes during training as previous layer parameters update. This makes training unstable and requires careful learning rate tuning.
Normalization addresses this by ensuring each layer receives data with consistent mean and variance.
Batch Normalization vs Layer Normalization
| Aspect | Batch Normalization | Layer Normalization |
|---|---|---|
| Normalizes across | Batch dimension | Feature dimension |
| Computation | σBx−μB per channel | σLx−μL per token |
| Dependencies | Batch (needs multiple samples) | Single sample |
| RNN friendly | No (batch stats vary) | Yes (per-token) |
| Transformer friendly | No | Yes |
Why LayerNorm and not BatchNorm for Transformers?
- Transformers process variable-length sequences — batch statistics are unreliable
- LayerNorm is independent of batch size
- LayerNorm works the same at training and inference
- LayerNorm preserves the per-token representation structure
1.2 Layer Normalization — Formal Definition
For an input vector x∈Rd:
Where:
- μ=d1∑i=1dxi (mean across features)
- σ2=d1∑i=1d(xi−μ)2 (variance across features)
- ϵ: Small constant for numerical stability (e.g., 10−5)
- γ∈Rd: Learnable scale parameter
- β∈Rd: Learnable shift parameter
Worked Example
Input vector: x=[2.0,4.0,1.0,3.0]
Step 1: Compute mean μ=(2.0+4.0+1.0+3.0)/4=10.0/4=2.5
Step 2: Compute variance σ2=((2.0−2.5)2+(4.0−2.5)2+(1.0−2.5)2+(3.0−2.5)2)/4 σ2=(0.25+2.25+2.25+0.25)/4=5.0/4=1.25 σ=1.25=1.118
Step 3: Normalize (ϵ=10−5, negligible) xnorm=[(2.0−2.5)/1.118,(4.0−2.5)/1.118,(1.0−2.5)/1.118,(3.0−2.5)/1.118] xnorm=[−0.447,1.342,−1.342,0.447]
Step 4: Scale and shift (with learned γ=1, β=0 initially) y=xnorm (identical if γ=1, β=0)
python# runnable import numpy as np def layer_norm(x, gamma=None, beta=None, eps=1e-5): """ Layer Normalization Args: x: Input array (..., d_model) gamma: Learnable scale (d_model,) beta: Learnable shift (d_model,) eps: Numerical stability constant Returns: normalized: Layer-normalized output """ mean = np.mean(x, axis=-1, keepdims=True) variance = np.var(x, axis=-1, keepdims=True) x_norm = (x - mean) / np.sqrt(variance + eps) if gamma is not None: x_norm = x_norm * gamma if beta is not None: x_norm = x_norm + beta return x_norm # Test with our example x = np.array([2.0, 4.0, 1.0, 3.0]) gamma = np.ones(4) beta = np.zeros(4) result = layer_norm(x, gamma, beta) print(f"Input: {x}") print(f"Normalized: {np.round(result, 4)}") print(f"Mean after norm: {np.mean(result):.6f}") print(f"Std after norm: {np.std(result):.6f}")
1.3 Pre-LN vs Post-LN
The original Transformer ("Attention Is All You Need") used Post-LN:
But most modern Transformers (GPT, BERT, LLaMA) use Pre-LN:
| Aspect | Post-LN (Original) | Pre-LN (Modern) |
|---|---|---|
| Order | Add → Norm | Norm → Add |
| Gradient flow | Through LayerNorm | Direct path through residual |
| Training stability | Less stable | More stable |
| Warmup needed | Yes | Less critical |
| Used in | Original Transformer | GPT, BERT, LLaMA, ViT |
(Diagram)
1.4 Residual Connections
Residual connections (skip connections) were introduced to train very deep networks. The idea: instead of learning a complete transformation F(x), learn a residual Δx:
If the optimal transformation is identity, F(x) simply has to learn 0 (easy) instead of learning identity mapping from scratch (hard).
Gradient Flow
During backpropagation:
The "1" term provides a direct gradient highway — gradients can flow from the output to the input without passing through any learned layers. This prevents vanishing gradients even in 100+ layer networks.
1.5 Why Both Are Necessary
Without residual connections, gradients vanish as network depth increases. The Transformer typically has 6-12 layers on each side, and without residuals, training would be very difficult.
Without layer normalization, activations would grow or shrink unpredictably, causing training instability.
Together, they enable the stable training of deep Transformer networks.
1.6 Implementation Example
python# runnable import numpy as np class TransformerBlock: """A complete Transformer block with Pre-LN""" def __init__(self, d_model, d_ff, num_heads): self.d_model = d_model self.d_ff = d_ff self.num_heads = num_heads # Learnable LayerNorm parameters (simplified) self.gamma1 = np.ones(d_model) self.beta1 = np.zeros(d_model) self.gamma2 = np.ones(d_model) self.beta2 = np.zeros(d_model) # Attention projections (simplified - random init) np.random.seed(42) self.W_Q = np.random.randn(d_model, d_model) * 0.1 self.W_K = np.random.randn(d_model, d_model) * 0.1 self.W_V = np.random.randn(d_model, d_model) * 0.1 self.W_O = np.random.randn(d_model, d_model) * 0.1 # FFN weights self.W1 = np.random.randn(d_model, d_ff) * 0.1 self.W2 = np.random.randn(d_ff, d_model) * 0.1 def layer_norm(self, x, gamma, beta): mean = np.mean(x, axis=-1, keepdims=True) var = np.var(x, axis=-1, keepdims=True) x_norm = (x - mean) / np.sqrt(var + 1e-5) return x_norm * gamma + beta def forward(self, x): # Pre-LN: Norm -> Sublayer -> Residual # Sublayer 1: Multi-head Attention x_norm1 = self.layer_norm(x, self.gamma1, self.beta1) # Simplified attention Q = x_norm1 @ self.W_Q K = x_norm1 @ self.W_K V = x_norm1 @ self.W_V scores = Q @ K.T / np.sqrt(self.d_model) attn = np.exp(scores) / np.sum(np.exp(scores), axis=-1, keepdims=True) attn_out = attn @ V @ self.W_O x = x + attn_out # Residual connection # Sublayer 2: FFN x_norm2 = self.layer_norm(x, self.gamma2, self.beta2) ffn_out = np.maximum(x_norm2 @ self.W1, 0) @ self.W2 x = x + ffn_out # Residual connection return x # Test block = TransformerBlock(d_model=64, d_ff=256, num_heads=8) x = np.random.randn(10, 64) # 10 tokens, d_model=64 output = block.forward(x) print(f"Input shape: {x.shape}") print(f"Output shape: {output.shape}") print(f"Input norm: {np.linalg.norm(x, axis=-1).mean():.4f}") print(f"Output norm: {np.linalg.norm(output, axis=-1).mean():.4f}")
4. 📐 Key Formulas / Concepts
| Concept | Formula | Purpose |
|---|---|---|
| Layer Normalization | σ2+ϵx−μ⋅γ+β | Stabilize activations |
| Residual Connection | x+Sublayer(x) | Enable deep networks |
| Pre-LN | x+Sublayer(LN(x)) | Modern, more stable |
| Post-LN | LN(x+Sublayer(x)) | Original Transformer |
| Gradient flow | ∂x∂L=∂y∂L(1+∂x∂F) | Direct gradient highway |
5. ⚠️ Common Pitfalls
Pitfall 1: Confusing LayerNorm with BatchNorm
The mistake: Using BatchNorm thinking it will work the same as LayerNorm.
Correction: BatchNorm normalizes across the batch dimension (different samples). LayerNorm normalizes across the feature dimension (different features in one sample). For Transformers, LayerNorm is correct because:
- Sequence lengths vary across samples
- Batch size may be 1 during inference
- LayerNorm preserves per-token statistics
Pitfall 2: Placing LayerNorm after the residual addition (Post-LN)
The mistake: Using Post-LN with very deep models and experiencing instability.
Why: In Post-LN, gradients must flow through the LayerNorm which can amplify or suppress gradient signals. Pre-LN provides a cleaner gradient path through the residual connection.
Correction: Use Pre-LN (LayerNorm before sublayer) for improved stability, especially with deep (>12 layer) Transformers.
Pitfall 3: Forgetting that LayerNorm has learnable parameters
The mistake: Thinking LayerNorm is just standardization (mean=0, std=1).
Correction: LayerNorm has learnable γ (scale) and β (shift) parameters per dimension. These allow the model to "undo" normalization if needed and are typically initialized to γ=1, β=0.
6. 📝 Practice Questions
Q1: Compute LayerNorm for x=[3, 5, 2, 6] with γ=1, β=0μ = (3+5+2+6)/4 = 4 σ² = ((3-4)² + (5-4)² + (2-4)² + (6-4)²)/4 = (1+1+4+4)/4 = 2.5 σ = 1.581x_norm = [(3-4)/1.581, (5-4)/1.581, (2-4)/1.581, (6-4)/1.581] = [-0.632, 0.632, -1.265, 1.265] Q2: If γ=[0.5, 1.0, 1.5, 2.0] and β=[0, 0, 0, 0], what is the output of LayerNorm for x=[3, 5, 2, 6]?From Q1, x_norm = [-0.632, 0.632, -1.265, 1.265]y = x_norm * γ + β y = [-0.632×0.5, 0.632×1.0, -1.265×1.5, 1.265×2.0] y = [-0.316, 0.632, -1.898, 2.530] Q3: Why does Pre-LN make training more stable than Post-LN?In Post-LN: output = LN(x + Sublayer(x)). Gradients must pass through LN's normalization computation, which can amplify/attenuate gradients.In Pre-LN: output = x + Sublayer(LN(x)). The residual path provides a direct gradient highway (the "1" term in ∂x∂L=∂y∂L(1+∂x∂F)). Gradients bypass the sublayer entirely.This direct path ensures that even 100+ layer networks receive gradient signals. Q4: What happens if ε (epsilon) is too large in LayerNorm?If ε is too large (e.g., 1.0 instead of 1e-5), the normalized values become: x_norm = (x - μ) / √(σ² + 1.0)For small σ² (uniform activations), the denominator is dominated by ε, not σ. This means:
- Normalized values are much smaller than they should be
- The output becomes less sensitive to input variations
- The model loses representational capacity
Typical ε values: 1e-5 to 1e-8. Q5: In a Transformer with d_model=512, how many learnable parameters does each LayerNorm have?LayerNorm has γ and β, each of shape (d_model,):
- γ: 512 parameters
- β: 512 parameters
- Total: 1,024 parameters per LayerNorm
The Transformer has 2 LayerNorms per encoder layer + 3 per decoder layer, so for 6 encoder + 6 decoder: Total LN params = (6 × 2 + 6 × 3) × 1024 = 30 × 1024 = 30,720 learnable normalization parameters. Q6: What is RMS Norm and how does it differ from LayerNorm?RMS Norm (Root Mean Square Layer Normalization) simplifies LayerNorm by removing the mean subtraction:RMSNorm(x)=d1∑i=1dxi2+ϵx⋅γRMS Norm only scales by the RMS, no shift or mean subtraction. It's used in some modern LLMs (like LLaMA) because it's computationally simpler and empirically works as well as full LayerNorm. Q7: If a residual connection is removed from a 12-layer Transformer, what happens during training?Without residual connections in a 12-layer network:
- Gradients vanish: The gradient at layer 1 is the product of 12 Jacobians. Each Jacobian has eigenvalues typically < 1, so the product becomes exponentially small.
- Layer 1 almost doesn't learn: The first layer receives negligible gradient updates.
- Effective depth is much less than 12: Only the last few layers learn meaningful representations.
This is exactly why residual connections were introduced — they make very deep networks trainable. Q8: How would you implement Pre-LN in a Transformer encoder layer using PyTorch-like pseudocode?pythonclass TransformerEncoderLayer: def forward(self, x): # Pre-LN pattern x_norm = layer_norm(x) attn_out = self_attention(x_norm) x = x + attn_out # First residual x_norm = layer_norm(x) ffn_out = ffn(x_norm) # FFN: ReLU(xW₁+b₁)W₂+b₂ x = x + ffn_out # Second residual return xNote the order: LN → Sublayer → Add (residual).
7. 🔗 Cross-References
- Next: Pre-training Objectives (Week 3)
- Related: Normalization techniques in deep learning
- Video: BSDA5004 Week 2 transcripts Join Discord PreviousDecoder & Cross-AttentionNextPre-training Objectives