← All Posts
Deep Learning · Architectures · Transformers· Foundations

Token Embeddings: Just a Table Lookup

An embedding layer takes a token ID and returns a list of learned numbers. That is the operation: vector = table[token_id]. Let’s make it concrete.

Before the first block → token embeddings. View the model diagram →

First, give each token an ID

Suppose our sentence is “cat sat cat”. For this small example, the tokenizer treats each word as one token and assigns cat → 4 and sat → 1. The sentence becomes [4, 1, 4].

The IDs are just labels. ID 4 is not “more cat” than ID 1, and nearby IDs need not have related meanings. Real tokenizers can split words into smaller pieces; we use whole words here to keep the lookup visible.

The same three tokens, in three forms
Tokens
cat
sat
cat
Token IDs
4
1
4
Embedding vectors
[1, 0, 1]
[0, 2, −1]
[1, 0, 1]

The tokenizer supplies the IDs. The embedding table supplies the vectors. These IDs and numbers are made up for this example.

An ID selects one row

Imagine a table with one row per token. Each row stores a vector: simply a fixed-length list of numbers. To embed cat, take row 4. To embed sat, take row 1. Put those rows in sentence order.

Try the two occurrences of cat below. They select the same table row, but fill different positions in the sentence.

Try the lookupClick a token to find its vector

For “cat sat cat”, select rows 4, 1, 4 in that order. The tables show the complete example; JavaScript adds clickable tokens.

The embedding table
IDTokenStored vector
0the[−1, 0, 1]
1sat[0, 2, −1]
2on[1, 1, 0]
3mat[−1, 1, 1]
4cat[1, 0, 1]
Vectors for our sentence
PositionTokenLooked-up vector
1cat[1, 0, 1]
2sat[0, 2, −1]
3cat[1, 0, 1]

Nothing multiplies the vector by 4. Nothing searches for the closest row. The ID is an address: table[4] directly selects the stored vector.

Count rows and columns separately

Our vocabulary has 5 tokens, and each vector has 3 numbers. The table therefore has shape [5, 3]: five rows, three columns. Its embedding dimension is 3.

The sentence has 3 tokens, so its output has shape [3, 3]. A ten-token sentence would produce [10, 3]. Sentence length changes the number of output rows; it does not change the width of each vector.

Who chooses the numbers?

When training a model from scratch, the table usually starts with random values. The model makes predictions, a loss measures its errors, and the optimizer adjusts the table along with the other model weights. Useful vectors are learned through that process. With a pretrained model, you load its already learned table.

The columns do not come with labels such as “animal”, “size”, or “happiness”. They are numbers the rest of the network learns to use. An ID identifies a token; its vector provides adjustable features.

There is one shared row for cat. Training examples that use it can help update that row. Reading a new sentence during ordinary inference does not rewrite the table.

The same token can acquire different context

At lookup time, both occurrences of cat get [1, 0, 1]. They can acquire different representations later, when the model uses position information and surrounding context. In a causal decoder, the second cat can see the earlier “cat sat”; the first cannot see those future tokens.

Same starting vector; different available context
First tokencat · position 1
Embedding lookup[1, 0, 1]
Later model layersNo earlier sentence tokens
Third tokencat · position 3
Embedding lookup[1, 0, 1]
Later model layersCan use “cat sat”

This diagram shows what context is available, not measured hidden vectors. The table supplies token identity; the later layers combine it with context.

The whole lookup in PyTorch

This code uses exactly the table above. freeze=False makes the table trainable; running the lookup itself does not update its values.

import torch
from torch import nn

table = torch.tensor([
    [-1., 0.,  1.],  # 0: the
    [ 0., 2., -1.],  # 1: sat
    [ 1., 1.,  0.],  # 2: on
    [-1., 1.,  1.],  # 3: mat
    [ 1., 0.,  1.],  # 4: cat
])
embedding = nn.Embedding.from_pretrained(table, freeze=False)
ids = torch.tensor([4, 1, 4])
vectors = embedding(ids)  # selects table[4], table[1], table[4]
print(vectors)
# tensor([[1., 0., 1.], [0., 2., -1.], [1., 0., 1.]], ...)

For a fresh random table, use nn.Embedding(5, 3). The two arguments mean five possible IDs and three numbers per vector. PyTorch’s Embedding documentation.

Optional: notation, one-hot vectors, and the output head

If the vocabulary size is $V$ and the vector width is $d$, the table has shape $V\times d$. With a batch of $B$ sentences of length $n$, IDs have shape $[B,n]$ and their vectors have shape $[B,n,d]$.

A one-hot vector with its 1 at index 4, multiplied by the table, selects row 4. That is a mathematical way to express the same lookup. Implementations can gather the row directly without building the one-hot vector.

The output vocabulary head has a different job: after the transformer layers, it turns a hidden vector into scores for possible next tokens. Some models reuse the input table’s weights for that calculation, a technique called weight tying. It saves a second set of weights; it is not needed to understand the input lookup.

Position embeddings and optional embedding scaling are architecture choices applied around the lookup. The position chapter explains them separately.

Check yourself: how many table rows are used by “cat sat cat”?

Two distinct rows: row 4 for cat and row 1 for sat. There are three output vectors because the sentence has three token positions.

Next: let the tokens exchange information

You now have one vector per token. Attention explains how those vectors use context. If the ID step is still unclear, read tokenization first.