Transformer Explained: Self-Attention, Positional Encoding & Encoder-Decoder Code

This article explains the Transformer architecture from "Attention Is All You Need", covering self-attention, multi-head attention, layer normalization, positional encoding, and the encoder-decoder structure with PyTorch code, showing how Transformers enable parallel sequence processing without recurrence.

Code Archaeology
Code Archaeology
Code Archaeology
Transformer Explained: Self-Attention, Positional Encoding & Encoder-Decoder Code

Introduction: The 2017 Breakthrough

In 2017 Google published the paper Attention Is All You Need [1], introducing the Transformer architecture that rapidly became the foundation of modern NLP and AI. The title's marketing flair aside, the paper's core claim is that attention mechanisms alone can replace recurrent neural networks (RNNs) for sequence modeling.

What Transformers Do Not Need — And What They Still Require

Q: If only attention is needed, what is not needed? A: Recurrent neural networks — no step-by-step sequential processing.

Q: Is attention truly the only component? A: No. The Transformer also relies on six key building blocks:

Feed-Forward Network (FFN)

Residual Connection

Layer Normalization

Embedding Layer

Positional Encoding

Softmax Activation

Items 1, 2, 4, and 6 were already established techniques; the Transformer "stands on the shoulders of giants" by combining them with novel attention and positional encoding designs.

1. Self-Attention Mechanism

Traditional RNN attention computes alignment between decoder hidden states and encoder outputs — like a translator consulting a source document. Transformer's self-attention works differently: each token in a sequence attends to all other tokens simultaneously, akin to team members listening to each other and synthesizing a richer view. This shifts attention from "external query" to "internal introspection," building dynamic, context-aware representations within a single sequence.

How Self-Attention Computes

Each input token embedding passes through three linear projections to produce Query (Q), Key (K), and Value (V) vectors. Attention scores are computed as: Attention(Q, K, V) = softmax(QK⁻ / √d_k) V where d_k is the feature dimension. The scaling factor 1/√d_k controls the softmax input range, preventing gradient vanishing and attention saturation.

Multi-Head Attention

Just as convolutional networks use multiple kernels to capture diverse features, self-attention employs multiple parallel linear projections (heads). Each head learns different relational patterns; their outputs are concatenated and projected again. This multi-head self-attention lets the model jointly attend to information from different representation subspaces.

Self-attention computation diagram
Self-attention computation diagram

2. Layer Normalization

Normalization stabilizes deep network training and accelerates convergence. For variable-length sequences with padding tokens, Layer Normalization is preferred over batch normalization. It operates per token across all feature dimensions: first standardizing (subtract mean, divide by standard deviation) to achieve zero mean and unit variance, then applying learnable scale (γ) and shift (β) parameters to restore expressive capacity.

3. Positional Encoding

RNNs inherently capture order by sequential processing. Transformers, processing all tokens in parallel, need explicit position information. The solution is positional encoding : each dimension of the embedding vector receives a sinusoidal wave of a distinct frequency — even dimensions use sine, odd dimensions use cosine. This design ensures that attention scores depend only on relative positions, not absolute indices, which is highly beneficial for natural language where relative order matters more than absolute position.

4. Putting It Together: Encoder-Decoder Architecture

Transformer encoder-decoder structure
Transformer encoder-decoder structure

Encoder

The encoder stack consists of 6 identical blocks. Each block contains a multi-head self-attention sublayer and a feed-forward sublayer, each wrapped in a residual connection followed by layer normalization (Add & Norm). The EncoderBlock forward pass:

class EncoderBlock(nn.Module):
    def __init__(self, features: int, self_attention_block: MultiHeadAttentionBlock,
                 feed_forward_block: FeedForwardBlock, dropout: float) -> None:
        super().__init__()
        self.self_attention_block = self_attention_block
        self.feed_forward_block = feed_forward_block
        self.residual_connections = nn.ModuleList([ResidualConnection(features, dropout) for _ in range(2)])

    def forward(self, x, src_mask):
        x = self.residual_connections[0](x, lambda x: self.self_attention_block(x, x, x, src_mask))
        x = self.residual_connections[1](x, self.feed_forward_block)
        return x

class Encoder(nn.Module):
    def __init__(self, features: int, layers: nn.ModuleList) -> None:
        super().__init__()
        self.layers = layers
        self.norm = LayerNormalization(features)

    def forward(self, x, mask):
        for layer in self.layers:
            x = layer(x, mask)
        return self.norm(x)

The Encoder processes the entire sequence at once, enabling full GPU parallelism — a key advantage over sequential RNNs.

Decoder

The decoder also has 6 blocks, but each adds a third sublayer: cross-attention where queries come from the decoder, while keys and values come from the encoder output. This allows the decoder to attend to the encoded source representation. The DecoderBlock forward pass:

class DecoderBlock(nn.Module):
    def __init__(self, features: int, self_attention_block: MultiHeadAttentionBlock,
                 cross_attention_block: MultiHeadAttentionBlock, feed_forward_block: FeedForwardBlock,
                 dropout: float) -> None:
        super().__init__()
        self.self_attention_block = self_attention_block
        self.cross_attention_block = cross_attention_block
        self.feed_forward_block = feed_forward_block
        self.residual_connections = nn.ModuleList([ResidualConnection(features, dropout) for _ in range(3)])

    def forward(self, x, encoder_output, src_mask, tgt_mask):
        x = self.residual_connections[0](x, lambda x: self.self_attention_block(x, x, x, tgt_mask))
        x = self.residual_connections[1](x, lambda x: self.cross_attention_block(x, encoder_output, encoder_output, src_mask))
        x = self.residual_connections[2](x, self.feed_forward_block)
        return x

class Decoder(nn.Module):
    def __init__(self, features: int, layers: nn.ModuleList) -> None:
        super().__init__()
        self.layers = layers
        self.norm = LayerNormalization(features)

    def forward(self, x, encoder_output, src_mask, tgt_mask):
        for layer in self.layers:
            x = layer(x, encoder_output, src_mask, tgt_mask)
        return self.norm(x)

The decoder's second attention module is cross-attention, connecting decoder queries to encoder keys/values. Target masking ( tgt_mask) prevents attending to future positions during autoregressive generation.

Reference

[1] Vaswani et al., "Attention Is All You Need", 2017. arXiv:1706.03762 (https://arxiv.org/abs/1706.03762)

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

TransformerPyTorchPositional EncodingEncoder-DecoderSelf-AttentionMulti-Head AttentionLayer NormalizationAttention Is All You Need
Code Archaeology
Written by

Code Archaeology

Learn from the past to understand the present

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.