Understanding Positional Encoding in Transformers

September 14, 2025 (1y ago)

24 min read

The Problem: Why Transformers Need Help with Order

Imagine you have a bag of word cards for the sentence "Tom chased Jerry." You shake the bag and pour out the cards. A transformer, without positional encoding, would be like someone trying to understand the sentence by looking at all the cards scattered on a table. They can see all the words and their relationships, but they have no idea what order they were originally in.

This is the fundamental challenge that transformers face. Unlike humans or RNNs that process words sequentially, transformers process all words simultaneously - which is exactly what makes them so powerful and scalable. However, this parallel processing comes with a critical limitation: transformers inherently treat these sentences identically:

  • "Tom chased Jerry"
  • "Jerry chased Tom"
  • "Tom Jerry chased"

But we know these sentences have completely different meanings! The order of words is crucial in language, and this is where positional encoding becomes essential.


What is Positional Encoding?

Positional encoding is a technique that gives each position in a sequence a unique mathematical "fingerprint" or "signature." Instead of just telling the model "this is position 1, 2, 3," we give each position a rich, multi-dimensional representation that captures its place in the sequence.

This multi-dimensional approach ensures that:

  1. Every position gets a unique identifier
  2. The model can understand relative distances between positions
  3. The system works for any sequence length
  4. The encoding values stay within reasonable bounds

Why Simple Solutions Don't Work

Before diving into the transformer solution, let's understand why obvious approaches fail.

Approach 1: Simple Counting (1, 2, 3, 4...)

The Problem with Training vs Testing Mismatch:

During Training: The model sees mostly short sentences

  • "I ran" - Position 0: "I", Position 1: "ran"
  • "Cat sits" - Position 0: "Cat", Position 1: "sits"

The model learns: "Position 1 means the sentence ends here"

During Testing: The model encounters longer sentences

  • "I quickly and carefully ran" - Positions 0, 1, 2, 3, 4

The problem: Position 1 now contains "quickly" instead of being the final word. The model's learned associations completely break down.

Approach 2: Normalization (0.0, 0.25, 0.5, 0.75, 1.0)

This seems smarter - normalize positions to always go from 0 to 1 regardless of sentence length.

Example showing the critical flaw:

Sentence A: "I ran" (2 words)

  • "I" - position 0.0, "ran" - position 1.0

Sentence B: "I quickly and carefully ran" (5 words)

  • "I" - position 0.0, "quickly" - position 0.25, "and" - position 0.5, "carefully" - position 0.75, "ran" - position 1.0

The critical issue: Position 1.0 means completely different things:

  • In sentence A: "This is word 2 of 2 - immediate conclusion"
  • In sentence B: "This is word 5 of 5 - after building context"

The model gets confused about sentence structure because the same normalized position represents fundamentally different grammatical situations.


The Elegant Solution

The transformer paper introduced a brilliant solution using sine and cosine functions:

PE(pos, 2i)=sin(pos/100002i/d_model)
PE(pos, 2i+1)=cos(pos/100002i/d_model)

Where:

  • pos = position of the word (0, 1, 2, 3...)
  • i = dimension index (0, 1, 2, 3... up to d_model/2)
  • d_model = embedding dimension (typically 512)

Why This Is Brilliant

  • Infinite Uniqueness: Mathematical functions can generate unique values for any position, no matter how long the sequence.
  • Controlled Magnitude: Sine and cosine always stay between -1 and 1, preventing explosive growth.
  • Smooth Relationships: Nearby positions get similar (but distinct) encodings, helping the model understand relationships.
  • No Training Required: Pure mathematical computation—no parameters to learn.

Breaking Down the Formula

Let's dissect the mathematical foundation step by step.

Interactive Formula Breakdown Calculator

Step-by-Step Calculation
Step 1: Calculate dimension index
i = ⌊0/2⌋ = 0
Step 2: Calculate divisor
10000^(2×0/512) = 1.0000
Step 3: Apply trigonometric function
sin(5/1.0000) = -0.958924
PE(5, 0) = -0.958924
Using sine function (dimension 0 is even)
Properties
Function Type:sine
Frequency:1.0000e+0 Hz
Wavelength:6.28 positions
Current Value:-0.958924
Both Functions at Position 5:
Sine
-0.958924
Cosine
0.283662
Wave Pattern Visualization

Sine wave Cosine wave Current position (5)

Frequency Examples (d_model = 512)
DimensionIndex (i)DivisorFrequencyPurpose
001.001.00e+0Very fast - distinguishes adjacent positions
64323.163.16e-1Medium speed
256128100.001.00e-2Slower - broader patterns
5102559.65e+31.04e-4Very slow - handles long sequences

The Frequency Calculation: 10000^(2i/d_model)

For a typical transformer with d_model=512:

// Different dimension examples
const examples = [
  { i: 0, calc: "2×0/512 = 0", result: "10000^0 = 1" },
  { i: 1, calc: "2×1/512 = 1/256", result: "10000^(1/256) ≈ 1.037" },
  { i: 128, calc: "2×128/512 = 0.5", result: "10000^0.5 = 100" },
  { i: 255, calc: "2×255/512 ≈ 0.996", result: "10000^0.996 ≈ 9550" },
];

Wave Frequency Analysis

These values become divisors in our sine/cosine functions:

  • Dimension 0: sin(pos / 1) - Very fast oscillation - distinguishes position 1 from 2
  • Dimension 2: sin(pos / 1.037) - Slightly slower - distinguishes nearby positions
  • Dimension 256: sin(pos / 100) - Much slower - distinguishes distant regions
  • Dimension 510: sin(pos / 9550) - Extremely slow - handles very long sequences

Why Different Frequencies Matter

Slow waves (later dimensions) distinguish between distant positions:

  • Position 10 vs Position 100
  • Position 100 vs Position 1000

Combined effect: Every position gets a unique 512-dimensional fingerprint that never repeats within practical sequence lengths!

Worked Example

Let's calculate positional encoding for position 2 with d_model=8:

import math

def positional_encoding(pos, d_model):
    encoding = []
    for i in range(0, d_model, 2):
        # Calculate frequency divisor
        divisor = 10000 ** (2 * i / d_model)

        # Calculate sine and cosine
        sine_val = math.sin(pos / divisor)
        cosine_val = math.cos(pos / divisor)

        encoding.extend([sine_val, cosine_val])

    return encoding

# Position 2, 8 dimensions
result = positional_encoding(2, 8)
print(f"Position 2 encoding: {[round(x, 4) for x in result]}")

Output: [0.9093, -0.4161, 0.9589, -0.2837, 0.9999, -0.0141, 1.0000, -0.0002]

Each position gets a completely different pattern!


Why Both Sine AND Cosine?

This is where the mathematical elegance really shines. Here's why we need both:

The Problem with Sine Alone

If we only used sine functions: Position 0: All sine values = 0

  • sin(0/1) = 0
  • sin(0/100) = 0
  • sin(0/1000) = 0

Result: [0, 0, 0, 0, 0, 0...] - All zeros! No information!

The Cosine Solution

Sine and cosine are 90 degrees out of phase:

  • When sin(x) = 0, cos(x) = ±1 (maximum information)
  • When sin(x) = ±1, cos(x) = 0
  • We never lose information!

Information Density Maximized

Position 0 with sine + cosine:

  • Dimensions 0,1: sin(0/1)=0, cos(0/1)=1
  • Dimensions 2,3: sin(0/100)=0, cos(0/100)=1

Result: [0, 1, 0, 1, 0, 1...] - Rich, distinctive pattern!

Mathematical Insight

Think of sine and cosine as providing two perpendicular coordinates for each frequency:

  • Sine: "How far through the wave cycle am I?"
  • Cosine: "What's my perpendicular component?"

Together, they give a complete 2D "coordinate" for each frequency band, maximizing the information density of our positional encoding.

Multi-Frequency Wave Pattern Visualizer

Selected Dimensions (4/6)
Dim 0 (sine)
Dim 2 (sine)
Dim 10 (sine)
Dim 30 (sine)
Add dimension:
Dimension Analysis
DimTypeFrequencyWavelengthValue at Pos 10Speed
0sine1.00e+06.3-0.5440Very Fast
2sine7.50e-18.40.9376Very Fast
10sine2.37e-126.50.6963Very Fast
30sine1.33e-2471.20.1330Fast

Implementation and Code

The Alternating Pattern

For 512 dimensions, we use:

  • 256 unique frequencies
  • Each frequency appears as both sine and cosine

Pattern:

  • Dimension 0: sin(pos / frequency_0)
  • Dimension 1: cos(pos / frequency_0)
  • Dimension 2: sin(pos / frequency_1)
  • Dimension 3: cos(pos / frequency_1)
  • ...and so on

Complete Implementation

import numpy as np

def get_positional_encoding(max_seq_len, d_model):
    """
    Generate positional encoding matrix

    Args:
        max_seq_len: Maximum sequence length
        d_model: Embedding dimension (must be even)

    Returns:
        pos_encoding: Matrix of shape (max_seq_len, d_model)
    """
    pos_encoding = np.zeros((max_seq_len, d_model))

    # Position indices
    position = np.arange(0, max_seq_len).reshape(-1, 1)

    # Create division term for each dimension
    div_term = np.exp(np.arange(0, d_model, 2) *
                     -(np.log(10000.0) / d_model))

    # Apply sine to even indices
    pos_encoding[:, 0::2] = np.sin(position * div_term)

    # Apply cosine to odd indices
    pos_encoding[:, 1::2] = np.cos(position * div_term)

    return pos_encoding

# Generate encodings
pe = get_positional_encoding(100, 512)
print(f"Shape: {pe.shape}")
print(f"Position 0: {pe[0, :8]}")
print(f"Position 1: {pe[1, :8]}")

Integration with Word Embeddings

The positional encoding gets added (not concatenated) to word embeddings:

# Word embeddings from embedding layer
word_embeddings = embedding_layer(input_tokens)  # Shape: (batch, seq_len, d_model)

# Positional encodings
pos_encodings = get_positional_encoding(seq_len, d_model)  # Shape: (seq_len, d_model)

# Final embeddings
final_embeddings = word_embeddings + pos_encodings

Why addition works: The model learns to separate word meaning from position information during training through the attention mechanism.


Visualizing Positional Encoding

Understanding positional encoding becomes much clearer when we can see it in action.

Interactive Positional Encoding Visualization

Positional Encoding Line Chart

Important details:

  • Each position creates a unique pattern across all dimensions
  • Even dimensions use sine, odd dimensions use cosine functions
  • Different frequencies create patterns at different scales
  • No two positions have identical encodings

What to Look For in the Visualization

  1. Wave Patterns: Notice how different dimensions create waves of different frequencies
  2. Uniqueness: Every position has a completely unique pattern across all dimensions
  3. Smooth Transitions: Nearby positions have similar but distinct encodings
  4. Scale Independence: The same mathematical formula works for any sequence length

W Takeaways

  • Early dimensions (0, 2, 4...): Fast-changing sine waves that help distinguish nearby positions
  • Later dimensions: Slower waves that help distinguish distant positions
  • Heatmap patterns: The characteristic striped pattern shows how different frequencies combine
  • Position signatures: Each row in the heatmap is a unique "fingerprint" for that position

Why It Works So Well

The combination of multiple wave frequencies creates patterns so complex that they won't repeat for lengths far exceeding any practical use case, while still being mathematically predictable enough for the model to learn meaningful relationships between positions.