TAI BUI
Back to curriculum
CapstonePython

Token and Positional Embeddings

Ids are integers and the model wants vectors. Two lookup tables sit between them, and which positional one you pick shapes what the model can learn.

TIME: ~90 minutesPREREQUISITES: Phase 04 lessons, Phase 07 transformer lessons, Lessons 30 and 31 of this phase

Learning Objectives

  • Build a token-embedding lookup table that maps vocabulary ids to dense vectors.
  • Build a learned positional-embedding lookup table indexed by position.
  • Build a fixed sinusoidal positional embedding indexed by position with no parameters.
  • Compose token and positional embeddings into a single input for a transformer block.
  • Contrast learned and sinusoidal embeddings on length generalization and parameter count.

The frame

A token embedding is a matrix with one row per vocabulary id and one column per model dimension. The model's first contact with a token id is a row lookup in it, and the returned vector is what the rest of the model treats as the meaning of the id. Backprop updates the rows used in the forward pass. Over training, the geometry of those rows comes to encode similarity as directions.

Token ids carry no order, so the model needs a second signal that tells position one from position seventeen. There are two dominant choices. A learned positional embedding is a second lookup table with one row per position. A fixed sinusoidal positional embedding is a math formula with no parameters. The choice has consequences. A learned table is a parameter, bounded by the maximum context length the model was trained on. A sinusoidal formula extends to any position in theory, but this lesson's SinusoidalPositionalEmbedding precomputes a fixed table at max_context_length and its forward raises past that bound. Both modules therefore enforce a maximum context length here. The model may still struggle past its training length even when the table is large enough to index.

This lesson builds both and composes each with the token embedding into a single input for the next lesson's attention block.

Check: The frame0/4
1. In this build, what does max_context_length set for a positional module?Define
2. Why does a transformer input need a positional signal in addition to the token embedding?Explain
3. You built both modules with max_context_length = 128 and now feed 200 tokens. What happens?Apply
4. A teammate says switching to sinusoidal removes the 128-token limit, but feeding 200 tokens to a sinusoidal module built with max_context_length=128 raises. Which assumption was wrong?Analyze

The shape contract

The embedding stage takes a batch of token ids of shape (B, T) and returns a tensor of shape (B, T, D), where D is the model dimension. Every batch element has the same context length T, and every position gets a vector of the same dimension D.

Mermaid diagram

The composition is a sum, not a concatenation. Summing keeps D constant through the network and lets the model decide, feature by feature, whether token meaning or position dominates at each layer.

Check: The shape contract0/2
1. A batch of token ids has shape (B, T). What shape does the embedding stage return?Define
2. A developer concatenates token and positional vectors along the feature axis, and the next block, built for width D, fails on shapes. What is the root cause?Analyze

The token embedding matrix

The token embedding is a parameter tensor of shape (V, D), where V is the vocabulary size. PyTorch exposes it as nn.Embedding(V, D). At init the entries are drawn from a small Gaussian with mean zero and, traditionally for transformer-scale models, a standard deviation around 0.02. The exact init matters less than keeping it consistent across runs.

The forward pass is a single indexing operation: PyTorch maps (B, T) int64 ids to (B, T, D) floats by gathering rows. The backward pass accumulates gradients only into the rows touched in the forward pass, so a row that never appeared in the batch receives zero gradient on that step.

A subtle detail: the token embedding and the output projection at the end of the model often share weights (weight tying). When they do, every backward pass touches every row of the embedding through the output side. This lesson builds only the input side, but in a full model the same matrix could also serve as the output projection.

Check: The token embedding matrix0/5
1. What is a token embedding matrix?Define
2. What does weight tying do to the token embedding matrix?Define
3. A batch of 32 ids touches only 20 distinct rows of a 320-row token table. What gradient do the other 300 rows get on that step?Explain
4. The demo builds TokenEmbedding(vocab_size=320, d_model=64). How many parameters does it hold?Apply
5. In a tied model, a token id that never appears in the input batch still gets nonzero gradient on its embedding row. Why?Analyze

The learned positional embedding

A learned positional embedding is a second nn.Embedding of shape (max_context_length, D), keyed by position id 0, 1, 2, ..., T-1. The forward pass returns the (T, D) position vectors for the first T positions, and EmbeddingComposer broadcasts them across the batch dimension.

The cost of the learned table is that it cannot be queried at position T if the model was only trained up to position T-1. The row does not exist. Production decoder-only models that use this scheme bake the maximum context length into the architecture and refuse longer inputs.

Check: The learned positional embedding0/2
1. What is a learned positional embedding?Define
2. A learned positional table was trained only up to position T-1. Why can it not be queried at position T?Explain

The sinusoidal positional embedding

A sinusoidal positional embedding is a function from position to vector, with no parameters. Position p and feature i produce

python
angle = p / (10000 ** (2 * (i // 2) / D))
emb[p, 2k]     = sin(angle)
emb[p, 2k + 1] = cos(angle)

Every position gets a unique vector. The wavelength varies geometrically across feature dimensions, so the lower dimensions oscillate fast and encode fine position, while the higher dimensions oscillate slowly and encode coarse position.

Using sin and cos together has a useful consequence: the vector at position p + k is a linear function of the vector at position p. That gives the attention layer an easy path to learning relative-position offsets, and the model needs no separate parameter to express "look five tokens back."

The table is computed once at construction and indexed at forward time.

Check: The sinusoidal positional embedding0/7
1. How many learnable parameters does a SinusoidalPositionalEmbedding contribute?Define
2. Why does the sinusoidal scheme use both sin and cos on adjacent feature pairs?Explain
3. In the sinusoidal formula, which feature pairs oscillate fastest as position increases?Explain
4. What does SinusoidalPositionalEmbedding(max_context_length=8, d_model=5) do?Apply
5. For d_model = 8, what is row 0 of the sinusoidal table?Apply
6. With d_model = 8 and base 10000, what are pe[1, 0] and pe[1, 1]?Apply
7. You need positions with no extra trainable parameters and an easy route to relative offsets, for inputs up to 512 tokens. Which fits this build?Design

The composition

The input pipeline does three things in order: read the token ids, look up the token vectors, add the positional vectors. The sum is the output.

Mermaid diagram

The sum step replicates the (T, D) positional tensor along the batch dimension. PyTorch handles that automatically because the positional tensor has shape (1, T, D) after unsqueeze.

Check: The composition0/4
1. How are token and positional embeddings composed in EmbeddingComposer?Define
2. Why does the composer add positional vectors to token vectors instead of concatenating them?Explain
3. The composer receives ids of shape (2, 5) with D = 8, and the positional module returns shape (5, 8). What happens before the add?Apply
4. You swap the sinusoidal module for a learned one inside EmbeddingComposer, reusing the same token embedding. What must still match?Analyze

Contrastive analysis

I run both variants on the same inputs and print two diagnostics among the demo's checks.

The first is parameter count. The learned variant adds max_context_length * D parameters on top of the token embedding. The sinusoidal variant adds zero.

The second is cosine similarity between embeddings at neighbouring positions. The sinusoidal variant decays smoothly and predictably because the function is continuous. The learned variant at initialization shows near-random similarity because its rows are drawn independently. After training it typically develops a similar smooth structure, but it has to discover that structure from data.

Check: Contrastive analysis0/5
1. Why do neighbouring rows of a freshly initialized learned positional table show near-random cosine similarity?Explain
2. You halve d_model from 64 to 32 and keep max_context_length at 128. How many parameters does the learned positional embedding now add?Apply
3. You print neighbour cosine similarity for both positional tables at initialization. What do you expect?Apply
4. At initialization the learned table's neighbour similarity is near random while the sinusoidal curve decays smoothly. What is the correct reading?Analyze
5. You trained the learned variant and want to know whether it discovered smooth position structure. Which check fits?Design

What this lesson does not do

I don't build rotary positional encoding (RoPE) or AliBi here. Those are the modern choices in production transformers. They follow the same shape contract as the embeddings above (a position-dependent transformation applied to vectors of shape (B, T, D)), but they act at the attention-projection step instead of at the input. The next lesson builds the attention block, and one optional extension is to fold rotary into the query-key projections there.

I also don't train the embedding. Training needs a loss, which needs a model output, which needs attention and an LM head. That is the next lesson and the one after.

Check: What this lesson does not do0/2
1. How do rotary (RoPE) and AliBi differ from the learned and sinusoidal tables in where position enters the model?Explain
2. You want to try rotary encoding while keeping this build's (B, T, D) shape contract. Where does the change go?Design
Author's Note

I'd reach for the sum over a concatenation, because summing keeps D constant through the network. I'd also cap the sinusoidal table at max_context_length the way this build does: the formula extends in theory, but the table does not, and the model may struggle past its training length anyway.

My bet is that the neighbour cosine diagnostic is the first thing to read when comparing the two positional schemes. Sinusoidal decays smoothly by construction, while the learned table starts near random and has to discover that structure from data.

How to read the code

main.py defines three modules and a composer. TokenEmbedding wraps nn.Embedding(V, D). LearnedPositionalEmbedding wraps nn.Embedding(L, D). SinusoidalPositionalEmbedding precomputes the table and exposes it as a buffer. EmbeddingComposer ties a token embedding and a positional embedding together. The demo at the bottom prints the shapes, the parameter counts, and the neighbour-position similarity diagnostic. The tests in code/tests/test_embeddings.py pin shape, broadcast behaviour, parameter count, and the sinusoidal formula.

Run the demo. Then change the model dimension D from 64 to 32 and watch how the sinusoidal wavelength bands change.

Warm-up0/3
1. A neural network receives a token as an integer id such as 4217. What is the usual way to turn that id into something the network can compute with?Warm-up
2. Self-attention scores every token against every other token with no built-in notion of order. What follows for a model that has no extra position information?Warm-up
3. What range of values can sin(x) and cos(x) take for any real number x?Warm-up
Check: How to read the code0/1
1. TokenEmbedding(vocab_size=10, d_model=4) is called with ids = torch.zeros(2, 3, dtype=torch.float32). What happens?Apply

Code