Deep LearningTutorial

Implementing Transformers from Scratch in PyTorch

A deep dive into the inner workings of the Transformer architecture, complete with heavily annotated PyTorch code for every layer.

#Transformers#PyTorch#Python#LLMs
Author

Dr. Alan Turing

Senior AI Researcher

Implementing Transformers from Scratch in PyTorch
Ad UnitSlot: post_in_content_1 | Format: auto

Understanding how Transformers work under the hood is a rite of passage for any modern AI Engineer. In this deep dive, we'll implement a complete Transformer from scratch using PyTorch.

The Core Concept: Self-Attention

At the heart of the Transformer is the self-attention mechanism. It allows the model to weigh the importance of different words in a sequence relative to each other.

Here is the mathematical formulation: Attention(Q, K, V) = softmax(QK^T / sqrt(d_k))V

Code Implementation

Let's write the code for the MultiHeadAttention block.

python
import torch import torch.nn as nn import math class MultiHeadAttention(nn.Module): def __init__(self, d_model, num_heads): super(MultiHeadAttention, self).__init__() assert d_model % num_heads == 0, "d_model must be divisible by num_heads" self.d_model = d_model self.num_heads = num_heads self.d_k = d_model // num_heads self.W_q = nn.Linear(d_model, d_model) self.W_k = nn.Linear(d_model, d_model) self.W_v = nn.Linear(d_model, d_model) self.W_o = nn.Linear(d_model, d_model) def scaled_dot_product_attention(self, Q, K, V, mask=None): attn_scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k) if mask is not None: attn_scores = attn_scores.masked_fill(mask == 0, -1e9) attn_probs = torch.softmax(attn_scores, dim=-1) output = torch.matmul(attn_probs, V) return output def split_heads(self, x): batch_size, seq_length, d_model = x.size() return x.view(batch_size, seq_length, self.num_heads, self.d_k).transpose(1, 2) def combine_heads(self, x): batch_size, _, seq_length, d_k = x.size() return x.transpose(1, 2).contiguous().view(batch_size, seq_length, self.d_model) def forward(self, Q, K, V, mask=None): Q = self.split_heads(self.W_q(Q)) K = self.split_heads(self.W_k(K)) V = self.split_heads(self.W_v(V)) attn_output = self.scaled_dot_product_attention(Q, K, V, mask) output = self.W_o(self.combine_heads(attn_output)) return output

Position-wise Feed-Forward Networks

In addition to attention sub-layers, each of the layers in our encoder and decoder contains a fully connected feed-forward network.

python
class PositionWiseFeedForward(nn.Module): def __init__(self, d_model, d_ff): super(PositionWiseFeedForward, self).__init__() self.fc1 = nn.Linear(d_model, d_ff) self.fc2 = nn.Linear(d_ff, d_model) self.relu = nn.ReLU() def forward(self, x): return self.fc2(self.relu(self.fc1(x)))

Positional Encoding

Since the model contains no recurrence and no convolution, we inject some information about the relative or absolute position of the tokens in the sequence.

python
class PositionalEncoding(nn.Module): def __init__(self, d_model, max_seq_length): super(PositionalEncoding, self).__init__() pe = torch.zeros(max_seq_length, d_model) position = torch.arange(0, max_seq_length, dtype=torch.float).unsqueeze(1) div_term = torch.exp(torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) self.register_buffer('pe', pe.unsqueeze(0)) def forward(self, x): return x + self.pe[:, :x.size(1)]

Wrapping Up

By combining these three elements—Attention, Feed-Forward layers, and Positional Encoding—you can construct the full Transformer Encoder and Decoder. Experiment with the parameters d_model, num_heads, and d_ff to see how they impact your memory footprint and model capacity!

Ad UnitSlot: post_in_content_2 | Format: auto