We have token IDs and a way to batch them. The last step of Chapter 2 turns those integers into the continuous vectors the model actually multiplies — first the token embeddings, then the positional embeddings that tell the model where each token sits.
2.7 Creating token embeddings
A token embedding token embedding The vector a learned nn.Embedding lookup assigns to a token ID. The embedding matrix has shape (vocab_size, emb_dim).
defined in ch. 2 — open in glossary
is produced by an
nn.Embedding nn.Embedding The PyTorch layer implementing a trainable lookup table; used for both token and positional embeddings.
defined in ch. 2 — open in glossary
layer, which is nothing more than
a learned lookup table: a weight matrix of shape (vocab_size, emb_dim)
where row i is the embedding of token ID i. Looking up an embedding is
just selecting a row — equivalent to multiplying a one-hot vector by the matrix,
but far cheaper.
Take a toy layer with vocab_size = 6 and
embedding dimension embedding dimension The length of each embedding vector (emb_dim / output_dim). GPT-2 small uses 768.
defined in ch. 2 — open in glossary
emb_dim = 3. Its
weight matrix starts random (here, after torch.manual_seed(123)):
| id 0 | 0.34 | -0.18 | -0.17 |
| id 1 | 0.92 | 1.58 | 1.30 |
| id 2 | 1.28 | -0.20 | -0.16 |
| id 3 | -0.40 | 0.97 | -1.15 |
| id 4 | -1.16 | 0.33 | -0.63 |
| id 5 | -2.84 | -0.78 | -1.41 |
Applying it to input_ids = [2, 3, 5, 1] stacks those four rows into a
(4 × 3) tensor:
| id 2 | 1.28 | -0.20 | -0.16 |
| id 3 | -0.40 | 0.97 | -1.15 |
| id 5 | -2.84 | -0.78 | -1.41 |
| id 1 | 0.92 | 1.58 | 1.30 |
import torch
input_ids = torch.tensor([2, 3, 5, 1])
vocab_size = 6
output_dim = 3
torch.manual_seed(123)
embedding_layer = torch.nn.Embedding(vocab_size, output_dim) #A
embedding_layer(torch.tensor([3])) #B
# tensor([[-0.4015, 0.9666, -1.1481]], grad_fn=<EmbeddingBackward0>)
embedding_layer(input_ids) #C
# tensor([[ 1.2753, -0.2010, -0.1606],
# [-0.4015, 0.9666, -1.1481],
# [-2.8400, -0.7849, -1.4096],
# [ 0.9178, 1.5810, 1.3010]], grad_fn=<EmbeddingBackward0>) - #A Creates the (vocab_size × output_dim) weight matrix, initialized randomly. requires_grad=True, so it learns during training.
- #B Embedding a single ID (3) returns exactly row 3 of the weight matrix.
- #C Embedding the whole list returns one row per ID, stacked — the lookup, vectorized.
2.8 Encoding word positions
The lookup above has a subtle gap: it is position-independent. Token ID 5
gets the vector [-2.84, -0.78, -1.41] whether it is the first or the fiftieth
token. That is fine for reproducibility, but self-attention (Chapter 3) is
itself order-agnostic — it has no built-in sense of sequence — so we must
inject position information explicitly. There are two families:
| Approach⇅ | What it encodes⇅ | Used by⇅ | |
|---|---|---|---|
| Absolute positional embedding | One vector per position index (position 0, 1, 2, …). | GPT (learned during training) | + |
| Relative positional embedding | The distance between tokens — "how far apart", not "which slot". | Various later models | + |
The recipe is delightfully simple: build a second embedding layer indexed by
position instead of by token, then add the two. A positional-embedding
layer of shape (context_length, emb_dim) produces one vector per slot; the
input embedding input embedding The element-wise sum of a token embedding and its positional embedding — the actual numeric input to the model.
defined in ch. 2 — open in glossary
is the element-wise sum.
vocab_size = 50257
output_dim = 256
max_length = 4
token_embedding_layer = torch.nn.Embedding(vocab_size, output_dim)
pos_embedding_layer = torch.nn.Embedding(max_length, output_dim) #A
dataloader = create_dataloader_v1(
raw_text, batch_size=8, max_length=max_length, stride=max_length)
inputs, targets = next(iter(dataloader)) # inputs: (8, 4)
token_embeddings = token_embedding_layer(inputs) #B
# shape: (8, 4, 256) -> (batch, tokens, emb_dim)
pos_embeddings = pos_embedding_layer(torch.arange(max_length)) #C
# shape: (4, 256)
input_embeddings = token_embeddings + pos_embeddings #D
# shape: (8, 4, 256) - #A A separate embedding layer indexed by position; context_length here equals max_length = 4.
- #B Look up the token embeddings for a batch of 8 sequences of 4 tokens → a (8, 4, 256) tensor.
- #C torch.arange(max_length) = [0,1,2,3] looks up one positional vector per slot → (4, 256).
- #D Add them. The (4, 256) positional tensor broadcasts over the batch dimension, giving the final (8, 4, 256) input the model consumes.
2.9 The full input pipeline
That completes the journey from raw string to model input. Every arrow below is a step this chapter built:
Chapter 2 in one breath: raw text → tokens → token IDs (via BPE) → token embeddings (a learned lookup) → + positional embeddings = the input tensor. LLMs can’t read text, so we turned it into continuous vectors compatible with neural-network math.
Key idea
Two embedding tables — one indexed by token, one by position — are added to form the model’s input. Their weights are random now and become meaningful only through training. Next, Chapter 3 builds the self-attention self-attention A mechanism that lets each token weigh the relevance of every other token in the sequence. It is the core component of the transformer. defined in ch. 1 — open in glossary mechanism that operates on exactly these input embeddings — and whose order-agnostic nature is the very reason we just added positions.📝 Check yourself: embeddings & positions
0 / 51.An nn.Embedding layer is often described as 'just a lookup table'. What does it look up?
2.Where do the values in the embedding matrix come from?
3.Why do GPT models add positional embeddings on top of token embeddings?
4.GPT uses which kind of positional embedding?
5.How is the final input embedding formed from the token and positional embeddings?