Quiz 2

Layer Normalization & Residual Connections in Transformers

1886 words
9 min read
Python Week 1: the first filter for runtime behavior
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

AspectBatch NormalizationLayer Normalization
Normalizes acrossBatch dimensionFeature dimension
ComputationxμBσB\frac{x - \mu_B}{\sigma_B} per channelxμLσL\frac{x - \mu_L}{\sigma_L} per token
DependenciesBatch (needs multiple samples)Single sample
RNN friendlyNo (batch stats vary)Yes (per-token)
Transformer friendlyNoYes
Why LayerNorm and not BatchNorm for Transformers?
  1. Transformers process variable-length sequences — batch statistics are unreliable
  2. LayerNorm is independent of batch size
  3. LayerNorm works the same at training and inference
  4. LayerNorm preserves the per-token representation structure

1.2 Layer Normalization — Formal Definition

For an input vector xRdx \in \mathbb{R}^d:
LayerNorm(x)=xμσ2+ϵγ+β\text{LayerNorm}(x) = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \cdot \gamma + \beta
Where:
  • μ=1di=1dxi\mu = \frac{1}{d}\sum_{i=1}^d x_i (mean across features)
  • σ2=1di=1d(xiμ)2\sigma^2 = \frac{1}{d}\sum_{i=1}^d (x_i - \mu)^2 (variance across features)
  • ϵ\epsilon: Small constant for numerical stability (e.g., 10510^{-5})
  • γRd\gamma \in \mathbb{R}^d: Learnable scale parameter
  • βRd\beta \in \mathbb{R}^d: Learnable shift parameter

Worked Example

Input vector: x=[2.0,4.0,1.0,3.0]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\mu = (2.0 + 4.0 + 1.0 + 3.0) / 4 = 10.0 / 4 = 2.5 Step 2: Compute variance σ2=((2.02.5)2+(4.02.5)2+(1.02.5)2+(3.02.5)2)/4\sigma^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\sigma^2 = (0.25 + 2.25 + 2.25 + 0.25) / 4 = 5.0 / 4 = 1.25 σ=1.25=1.118\sigma = \sqrt{1.25} = 1.118 Step 3: Normalize (ϵ=105\epsilon = 10^{-5}, negligible) xnorm=[(2.02.5)/1.118,(4.02.5)/1.118,(1.02.5)/1.118,(3.02.5)/1.118]x_{norm} = [(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]x_{norm} = [-0.447, 1.342, -1.342, 0.447] Step 4: Scale and shift (with learned γ=1, β=0 initially) y=xnormy = x_{norm} (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:
output=LayerNorm(x+Sublayer(x))\text{output} = \text{LayerNorm}(x + \text{Sublayer}(x))
But most modern Transformers (GPT, BERT, LLaMA) use Pre-LN:
output=x+Sublayer(LayerNorm(x))\text{output} = x + \text{Sublayer}(\text{LayerNorm}(x))
AspectPost-LN (Original)Pre-LN (Modern)
OrderAdd → NormNorm → Add
Gradient flowThrough LayerNormDirect path through residual
Training stabilityLess stableMore stable
Warmup neededYesLess critical
Used inOriginal TransformerGPT, 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)F(x), learn a residual Δx\Delta x:
y=x+F(x)y = x + F(x)
If the optimal transformation is identity, F(x)F(x) simply has to learn 0 (easy) instead of learning identity mapping from scratch (hard).

Gradient Flow

During backpropagation:
Lx=Lyyx=Ly(1+Fx)\frac{\partial L}{\partial x} = \frac{\partial L}{\partial y} \cdot \frac{\partial y}{\partial x} = \frac{\partial L}{\partial y} \cdot (1 + \frac{\partial F}{\partial x})
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

ConceptFormulaPurpose
Layer Normalizationxμσ2+ϵγ+β\frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} \cdot \gamma + \betaStabilize activations
Residual Connectionx+Sublayer(x)x + \text{Sublayer}(x)Enable deep networks
Pre-LNx+Sublayer(LN(x))x + \text{Sublayer}(\text{LN}(x))Modern, more stable
Post-LNLN(x+Sublayer(x))\text{LN}(x + \text{Sublayer}(x))Original Transformer
Gradient flowLx=Ly(1+Fx)\frac{\partial L}{\partial x} = \frac{\partial L}{\partial y}(1 + \frac{\partial F}{\partial x})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.581
x_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 Lx=Ly(1+Fx)\frac{\partial L}{\partial x} = \frac{\partial L}{\partial y}(1 + \frac{\partial F}{\partial x})). 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)=x1di=1dxi2+ϵγ\text{RMSNorm}(x) = \frac{x}{\sqrt{\frac{1}{d}\sum_{i=1}^d x_i^2 + \epsilon}} \cdot \gamma
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:
  1. 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.
  2. Layer 1 almost doesn't learn: The first layer receives negligible gradient updates.
  3. 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?
python
class 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 x
Note the order: LN → Sublayer → Add (residual).

7. 🔗 Cross-References

Document outline

Keep your place and jump directly to a heading.

Table of Contents
System Normal // Awaiting Context

Intelligence Hub

Navigate the knowledge graph to generate context. The Hub adapts dynamically to surface backlinks, related notes, and metadata insights.