terminal

//lorem_ipsum_terminal

$cat From Equations to Code: Reimplementing the Transformer
ROOT_DIRECTORY: /home/ankan/blog
// DISPLAY_OPTIONS:
font=
size=

Introduction

Since its introduction in 2017, the Transformer has become the foundation of modern deep learning, powering large language models, vision transformers, and many multimodal architectures. Although libraries such as PyTorch and Hugging Face provide highly optimized implementations, they often abstract away the mathematical ideas that make the architecture work.

In this article, we will reimplement the original Transformer proposed in Attention Is All You Need from scratch using PyTorch. Rather than relying on high-level modules like nn.Transformer, we will build every component ourselves, translating the equations from the paper into clean, modular code.

Our goal is not to reproduce the latest Transformer variants or incorporate modern improvements such as Rotary Position Embeddings (RoPE), FlashAttention, or SwiGLU. Instead, we will faithfully follow the original 2017 architecture, implementing each building block exactly as described by the authors.

Throughout the article, we will:

  • derive the mathematical formulation of each component,
  • develop the intuition behind its design,
  • implement it from scratch in PyTorch, and
  • verify that the implementation behaves as expected before assembling the complete model.

By the end, we will have reconstructed the entire Transformer—from token embeddings and positional encodings to multi-head attention, encoder and decoder layers, and the final sequence-to-sequence architecture—providing a clear bridge between the original paper and a working implementation.

1. Token Embeddings

Background

The first component of the Transformer architecture is the token embedding layer. Neural Networks operate better on continuous vectors compared to discrete token IDs, so the first step would be to map every token to a learned dense representation.

Now, given a input sequence,

X=[x1,x2,,xn]X = \left[x_1, x_2, \ldots, x_n \right]

each token is looked up in a learnable embedding matrix ERV×dmodelE \in \mathbb{R}^{ |V| \times d_{model}}, where V|V| is the size of the vocabulary and dmodeld_{model} is the dimension of the embedding vectors.

Each row in EE represents the embedding corresponding to a token in the vocabulary. Therefore for a token xix_i, its corresponding embedding

ei=E[xi]e_i = E[x_i]

which is basically the xithx_i^{th} row in the embedding matrix.

Scaling

However, there is all small detail mentioned in the paper which we need to account for.

In the embedding layers, we multiply those weights by dmodel\sqrt{d_{model}}

Thus the output of the embedding layer is

zi=eidmodelz_i = e_i \cdot \sqrt{d_{model}}

where ziRdmodelz_i \in \mathbb{R}^{d_{model}}

This is due to the fact that immediately after the token embedding we need to add positional embeddings. Scaling the embeddings helps keep their magnitude comparable to that of the positional embeddings, preventing either component from dominating the combined representation during the forward pass.

PyTorch Implementation

class TokenEmbedding(nn.Module):
    def __init__(self, vocab_size:int, d_model: int):
        super().__init__()
 
        self.d_model = d_model
        self.embedding = nn.Embedding(vocab_size, d_model)
 
    def forward(self, x):
        """
        Args:
            x: (batch, seq_len)
        Returns:
            (batch, seq_len, d_model)
        """
        return self.embedding(x) * math.sqrt(self.d_model)
 

2. Positional Encodings

Background

Since our model contains no recurrence and no convolution, in order for the model to make use of the order of the sequence, we must inject some information about the relative or absolute position of the tokens in the sequence. To this end, we add "positional encodings" to the input embeddings ...

As the authors have mentioned in the paper, we need to add the sense of positions in the architecture since further calculations are permutation invariant in nature. In other words, the order of the tokens i.e. swapping the position of any two tokens does not change the output of the model. So we add positional encodings to the input embeddings to give the model a sense of position.

Formulae

The authors decided to use sine and cosine functions to generate sinusoidal positional encodings. The idea is to use different frequencies of sine and cosine functions to generate different positional encodings.

PE(pos,2i)=sin(pos100002i/dmodel)PE(pos, 2i) = \sin\left(\frac{pos }{10000^{2i/d_{model}}}\right) PE(pos,2i+1)=cos(pos100002i/dmodel)PE(pos, 2i+1) = \cos\left(\frac{pos }{10000^{2i/d_{model}}}\right)

where pospos is the position of the token in the sequence and ii is the dimension of the embedding vector.

PyTorch Implementation

class SinusoidalPositionalEncoding(nn.Module):
    def __init__(self, d_model: int, max_len:int=5000, dropout=0.1):
        super().__init__()
 
        pe = torch.zeros(max_len, d_model)
        positions = torch.arange(max_len).unsqueeze(1)
 
        div_term = torch.exp(torch.arange(0, d_model, 2)* (-math.log(10000.0) / d_model))
        
        pe[:, 0::2] = torch.sin(positions * div_term)
        pe[:, 1::2] = torch.cos(positions * div_term)
 
        pe = pe.unsqueeze(0)
 
        self.dropout = nn.Dropout(dropout)
        self.register_buffer("pe", pe)
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        Adds positional encodings to the input embeddings and applies dropout.
 
        Args:
            x: Input tensor of shape (batch_size, seq_len, d_model).
 
        Returns:
            Tensor of the same shape with positional encodings added and dropout applied.
        """
        x = x + self.pe[:, :x.size(1)]
        return self.dropout(x)

Visualization

import matplotlib.pyplot as plt
import seaborn as sns
 
d_model, max_len = 128, 100
spe = SinusoidalPositionalEncoding(d_model, max_len)
 
def plot_spe(spe):
    plt.figure(figsize=(12, 6))
    sns.heatmap(spe.pe.squeeze(0),cmap="coolwarm_r",
        xticklabels=8,yticklabels=8,
        cbar_kws={"label": "Encoding Value"},
    )
    plt.title("Sinusoidal Positional Encoding", fontsize=18, weight="bold", pad=15)
    plt.xlabel("Embedding Dimension", fontsize=13)
    plt.ylabel("Position", fontsize=13)
    plt.tight_layout()
    plt.show()
    
plot_spe(spe)
Sinusoidal Positional Encoding
Sinusoidal Positional Encoding