Transformer Blocks: Encoder and Decoder Stacks Explained

Learning Objectives: By the end of this lesson, you will be able to:

  • Describe the structure and function of Transformer encoder and decoder blocks
  • Diagram and explain the data flow through encoder and decoder stacks
  • Identify the role of each component (Self-Attention, Feed-Forward, Add & Norm, Masked Attention, Encoder-Decoder Attention)

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Imagine you are translating a sentence from English to French. How do you think a model keeps track of both the meaning of the input and what it has already generated in the output? Why might this be challenging for traditional models?

Lesson Overview

  • Duration: Approximately 2.5 hours including all exercises (or 60–90 minutes when selected activities are assigned)
  • Prerequisites: Understanding of self-attention, multi-head attention, and positional encoding
  • Materials Needed: Python (Jupyter Notebook or IDE), PyTorch or TensorFlow, pen and paper for diagramming

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. The overall architecture of Transformer encoder and decoder stacks
  2. The flow of data through each block
  3. The function of each sub-layer (Self-Attention, Feed-Forward, Add & Norm, Masked Attention, Encoder-Decoder Attention)
  4. Hands-on practice and exercises
  5. Common pitfalls and how to avoid them

Success Criteria:

By the end of this session, you should be able to diagram and explain the full Transformer block and its data flow.

3. Foundation Knowledge (15-20 minutes)

Core Concepts

Transformer Encoder Block

Each encoder block consists of:

  • Multi-Head Self-Attention
  • Add & Layer Normalization
  • Feed-Forward Network
  • Add & Layer Normalization

Transformer Decoder Block

Each decoder block consists of:

  • Masked Multi-Head Self-Attention (prevents attending to future tokens)
  • Add & Layer Normalization
  • Encoder-Decoder Attention (attends to encoder outputs)
  • Add & Layer Normalization
  • Feed-Forward Network
  • Add & Layer Normalization

Real-World Analogy: Think of the encoder as a team of experts analyzing a document, and the decoder as a translator who consults the experts while writing the translation, making sure not to “peek” at future words.

Why This Matters: Understanding the block structure is essential for customizing, debugging, or extending Transformer models.

Key Terminology

  • Add & Norm: Residual connection followed by layer normalization
  • Masked Attention: Prevents the decoder from seeing future tokens
  • Encoder-Decoder Attention: Allows the decoder to focus on relevant encoder outputs

4. Detailed Explanation and Examples (25-30 minutes)

Understanding the Transformer Block

The encoder stack processes the input sequence in parallel, passing information up through multiple layers. The decoder stack generates the output sequence one token at a time, using both its own previous outputs and the encoder’s representations.

Practical Applications

  • Machine translation
  • Text summarization
  • Sequence-to-sequence tasks (e.g., question answering)

Common Misconceptions

  1. Misconception: The decoder can see the entire output sequence at once. Reality: Masked attention ensures the decoder only sees previous tokens.
  2. Misconception: Encoder and decoder blocks are identical. Reality: Decoder blocks have additional masked attention and encoder-decoder attention layers.

5. Live Coding Demonstration (20-25 minutes)

Example 1: Encoder Block Skeleton (PyTorch)

import torch
import torch.nn as nn

class EncoderBlock(nn.Module):
    def __init__(self, embed_dim, num_heads, ff_dim):
        super().__init__()
        self.attn = nn.MultiheadAttention(embed_dim, num_heads, batch_first=True)
        self.norm1 = nn.LayerNorm(embed_dim)
        self.ff = nn.Sequential(
            nn.Linear(embed_dim, ff_dim),
            nn.ReLU(),
            nn.Linear(ff_dim, embed_dim)
        )
        self.norm2 = nn.LayerNorm(embed_dim)
    def forward(self, x):
        attn_out, _ = self.attn(x, x, x)
        x = self.norm1(x + attn_out)
        ff_out = self.ff(x)
        x = self.norm2(x + ff_out)
        return x

Explanation:

  • Multi-head self-attention, followed by Add & Norm
  • Feed-forward network, followed by Add & Norm

Example 2: Decoder Block Skeleton (PyTorch)

class DecoderBlock(nn.Module):
    def __init__(self, embed_dim, num_heads, ff_dim):
        super().__init__()
        self.self_attn = nn.MultiheadAttention(embed_dim, num_heads, batch_first=True)
        self.norm1 = nn.LayerNorm(embed_dim)
        self.enc_dec_attn = nn.MultiheadAttention(embed_dim, num_heads, batch_first=True)
        self.norm2 = nn.LayerNorm(embed_dim)
        self.ff = nn.Sequential(
            nn.Linear(embed_dim, ff_dim),
            nn.ReLU(),
            nn.Linear(ff_dim, embed_dim)
        )
        self.norm3 = nn.LayerNorm(embed_dim)
    def forward(self, x, enc_out, tgt_mask=None):
        if tgt_mask is None:
            length = x.size(1)
            tgt_mask = torch.triu(
                torch.ones(length, length, dtype=torch.bool, device=x.device),
                diagonal=1,
            )
        self_attn_out, _ = self.self_attn(x, x, x, attn_mask=tgt_mask)
        x = self.norm1(x + self_attn_out)
        enc_dec_attn_out, _ = self.enc_dec_attn(x, enc_out, enc_out)
        x = self.norm2(x + enc_dec_attn_out)
        ff_out = self.ff(x)
        x = self.norm3(x + ff_out)
        return x

Key Points:

  • Masked self-attention prevents peeking at future tokens
  • Encoder-decoder attention allows the decoder to use encoder outputs
  • Each sub-layer is followed by Add & Norm

Example 3: Visualizing the Data Flow

# Pseudocode for data flow
input_seq -> [Encoder Stack] -> encoder_outputs
start_token -> [Decoder Stack (uses encoder_outputs)] -> output_seq

Debugging Demonstration

Show common errors and how to fix them:

# Common mistake: not using masks in decoder self-attention
try:
    self_attn_out, _ = self.self_attn(x, x, x)  # No mask
except Exception as e:
    print(e)

# Corrected version
self_attn_out, _ = self.self_attn(x, x, x, attn_mask=tgt_mask)

6. Hands-On Practice (30-40 minutes)

Exercise 1: Basic Practice (10 minutes)

Objective: Implement an encoder block and pass random data through it.

Instructions:

  1. Define an EncoderBlock class as above
  2. Pass a batch of random sequences through it
  3. Print the output shape

Starter Code:

import torch
# Your code here

Solution:

block = EncoderBlock(embed_dim=32, num_heads=4, ff_dim=64)
x = torch.randn(8, 10, 32)
out = block(x)
print(out.shape)  # (8, 10, 32)

Exercise 2: Intermediate Challenge (15 minutes)

Objective: Implement a decoder block and use it with encoder outputs.

Requirements:

  • Use masked self-attention in the decoder
  • Pass encoder outputs to encoder-decoder attention
  • Print the output shape

Hints:

  • Use nn.MultiheadAttention with attn_mask for masking
  • Input shapes should match between encoder and decoder

Exercise 3: Creative Application (15 minutes)

Objective: Diagram the full data flow through encoder and decoder stacks.

Challenge:

  • Draw or code a diagram showing how data moves from input to output
  • Label each sub-layer and connection

Extension Ideas:

  • Add comments or annotations explaining each step
  • Try implementing a mini-Transformer with multiple encoder/decoder blocks

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Forgetting to use masks in decoder self-attention Cause: Decoder can see future tokens, leading to data leakage Solution: Always use masks in decoder self-attention

  2. Problem: Mismatched input/output shapes between encoder and decoder Cause: Incorrect tensor dimensions Solution: Check and match shapes at each step

Debugging Tips

  • Print tensor shapes at each layer
  • Visualize attention masks
  • Use small test cases to verify implementation

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Use residual connections and layer normalization after each sub-layer
  • Always mask future tokens in the decoder
  • Document model architecture and data flow

Next Steps in Your Learning Journey

  • Explore full Transformer models for translation
  • Learn about training objectives and loss functions
  • Try implementing a full encoder-decoder model

Causal-Mask Requirement

During teacher-forced decoder training, construct an upper-triangular causal mask and pass it on every decoder call; a default of None permits attention to future target tokens. For example, mask = torch.triu(torch.ones(T, T, dtype=torch.bool, device=x.device), diagonal=1). Padding masks are separate and should also be supplied when batches contain padding.

9. Summary and Review (5 minutes)

Key Takeaways

  1. Transformer blocks are built from attention, feed-forward, and normalization layers
  2. Decoder blocks use masking and encoder-decoder attention
  3. Understanding the data flow is crucial for building and debugging models

Quick Review Questions

  1. What is the purpose of Add & Norm in Transformer blocks?
  2. Why is masking necessary in the decoder?
  3. How does encoder-decoder attention work?

For Instructors

Teaching Tips

  • Use diagrams to illustrate encoder and decoder stacks
  • Encourage students to step through the data flow
  • Discuss real-world applications (translation, summarization)

Assessment Ideas

  • Quiz on block structure and data flow
  • Coding assignment: implement encoder and decoder blocks
  • Peer review: students explain their diagrams

Adaptation Notes

  • For beginners: focus on single encoder/decoder block
  • For advanced students: stack multiple blocks and implement masking
  • For online delivery: use Colab or Jupyter for interactive coding and diagramming