Introduction

Welcome to "Deconstructing the Transformer Architecture"! You've just completed an incredible journey through Sequence Models & The Dawn of Attention, where you built the foundational understanding of attention mechanisms and created a standalone PyTorch module for scaled dot-product attention. Now you're ready to take the next major step in your exploration of modern NLP architectures.

In this course, you'll systematically build the complete Transformer architecture from the ground up. You'll start by enhancing your attention mechanism with Multi-Head Attention, then explore positional encodings, layer normalization, and feed-forward networks. By the end, you'll have a fully functional Transformer that can handle real sequence-to-sequence tasks. Our first lesson focuses on Multi-Head Attention, the mechanism that allows Transformers to attend to different types of information simultaneously across multiple representation subspaces.

The Power of Multiple Attention Perspectives
The Architecture: Linear Projections and Head Division
Implementing Scaled Dot-Product for Multiple Heads
Tensor Reshaping and Multi-Head Computation

The heart of Multi-Head Attention lies in how you reshape tensors to create multiple heads and then recombine their outputs. This section handles the complex tensor manipulations that enable parallel attention computation:

def forward(self, query, key, value, mask=None):
    """Forward pass for multi-head attention"""
    batch_size, seq_len_q = query.size(0), query.size(1)
    seq_len_k = key.size(1)
    
    # Linear projections and reshape for multi-head attention
    Q = self.w_q(query).view(batch_size, seq_len_q, self.num_heads, self.d_k).transpose(1, 2)
    K = self.w_k(key).view(batch_size, seq_len_k, self.num_heads, self.d_k).transpose(1, 2)
    V = self.w_v(value).view(batch_size, seq_len_k, self.num_heads, self.d_k).transpose(1, 2)

The reshaping operation is crucial: you transform tensors from (batch_size, seq_len, d_model) to (batch_size, num_heads, seq_len, d_k). The view operation splits the model dimension into separate heads, while transpose(1, 2) moves the head dimension to the second position for efficient computation. This tensor manipulation is what enables the "multi-head" aspect — you're essentially creating multiple parallel attention computations from a single input.

    # Adjust mask for multi-head attention
    if mask is not None:
        if mask.dim() == 2:  # [seq_len, seq_len]
            mask = mask.unsqueeze(0).unsqueeze(0)  # [1, 1, seq_len, seq_len]
        elif mask.dim() == 3:  # [batch_size, seq_len, seq_len]
            mask = mask.unsqueeze(1)  # [batch_size, 1, seq_len, seq_len]
    
    # Apply attention
    attention_output, attention_weights = self.scaled_dot_product_attention(Q, K, V, mask)
    
    # Concatenate heads and apply output projection
    attention_output = attention_output.transpose(1, 2).contiguous().view(
        batch_size, seq_len_q, self.d_model)
    output = self.w_o(attention_output)
    
    return output, attention_weights

The mask handling ensures compatibility with the multi-head structure by adding necessary dimensions for proper broadcasting across all heads. After computing attention, you reverse the reshaping process: transposing back and using view to concatenate all head outputs into the original d_model dimension. The contiguous() call ensures the tensor is stored in a contiguous block of memory, which is required before reshaping. Finally, the output projection w_o learns how to best combine information from all heads, implementing the concatenation and linear transformation specified in the original Transformer paper.

Network Initialization

Proper weight initialization and comprehensive testing are crucial for ensuring your Multi-Head Attention module functions correctly. Let's implement the initialization method and create a thorough testing framework:

def _init_weights(self):
    """Initialize weights using Xavier uniform initialization"""
    for linear in [self.w_q, self.w_k, self.w_v, self.w_o]:
        nn.init.xavier_uniform_(linear.weight)
        if linear.bias is not None:
            nn.init.constant_(linear.bias, 0)

Xavier uniform initialization helps ensure stable training by preventing vanishing or exploding gradients during the initial training phases. This initialization strategy considers the number of input and output connections to set appropriate initial weight magnitudes.

Testing Our Implementation

Now let's create tests to verify that your Multi-Head Attention implementation works correctly across different scenarios:

def test_multi_head_attention():
    """Test Multi-Head Attention module"""
    print("Testing Multi-Head Attention Implementation...")
    
    # Configuration
    batch_size = 2
    seq_len = 8
    d_model = 64
    num_heads = 8
    
    # Create sample input
    torch.manual_seed(42)
    x = torch.randn(batch_size, seq_len, d_model)
    
    # Initialize Multi-Head Attention
    mha = MultiHeadAttention(d_model, num_heads)
    
    print(f"Input shape: {x.shape}")
    print(f"Model config: d_model={d_model}, num_heads={num_heads}, d_k={d_model//num_heads}")
    
    # Test self-attention (Q=K=V)
    output, attention_weights = mha(x, x, x)
    
    print(f"Output shape: {output.shape}")
    print(f"Attention weights shape: {attention_weights.shape}")
    
    # Verify dimensions are preserved
    assert output.shape == x.shape, f"Output shape {output.shape} != input shape {x.shape}"
    
    # Test with causal mask
    causal_mask = torch.tril(torch.ones(seq_len, seq_len))
    masked_output, masked_attn = mha(x, x, x, causal_mask)
    
    print(f"Masked output shape: {masked_output.shape}")
    
    # Test gradient flow
    loss = output.mean()
    loss.backward()
    
    print(f"Gradient verification:")
    for name, param in mha.named_parameters():
        if param.grad is not None:
            print(f"  {name}: grad_norm={param.grad.norm():.6f}")
    
    total_params = sum(p.numel() for p in mha.parameters())
    print(f"\nTotal parameters: {total_params:,}")

This testing framework validates critical aspects of the implementation. You verify that input and output dimensions are preserved, test both unmasked and causal-masked scenarios, and examine gradient flow across all parameters. The self-attention setup (where query, key, and value are all the same input x) is fundamental to Transformer architectures and provides a clear test case for your implementation.

Verifying the Results
Computational Complexity and Optimization Considerations
Conclusion and Next Steps

Congratulations! You've successfully implemented Multi-Head Attention, a cornerstone mechanism that enables Transformers to capture multiple types of relationships simultaneously. Your implementation demonstrates how parallel attention heads can attend to different representation subspaces, providing a much richer understanding than single-head mechanisms. The careful tensor reshaping and concatenation logic you've mastered form the foundation for more complex Transformer components.

In our next lesson, you'll explore positional encodings, the mechanism that gives Transformers their understanding of sequence order. Unlike RNNs that process sequences step by step, Transformers need explicit positional information, and you'll discover the elegant mathematical solutions that make this possible. Get ready to dive deeper into the architectural innovations that make Transformers so powerful!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal