Tokenization & Vocabulary Design
How does byte-pair encoding build tokens from text?
The idea
Byte-pair encoding (BPE) builds tokens by repeatedly merging the most frequent adjacent pair. This character-level example starts with single characters; each merge becomes a rule used to encode new text.
Builds on Maximum Likelihood and Representation Learning & Embedding Geometry.
The picture
A model cannot take in letters or words as such. It reads tokens: entries in a fixed vocabulary, each with a number. Byte-pair encoding (BPE) builds that vocabulary from text, by counting.
Tiles. Every character starts as its own tile. BPE counts each pair of neighbours, takes the pair that occurs most often, and fuses every copy of it into one new tile. Then it counts again. Strings the text repeats grow into single tiles; strings it sees once can stay in pieces. The toy below stops when no pair occurs twice or its merge budget is reached; BPE training usually fixes a vocabulary or merge budget.
Rules. Each fusion is written down as a merge rule, and the list of rules, in order, is the tokenizer. Given new text, it splits it into characters and replays the rules. A familiar word comes out whole; an unfamiliar one comes out in fragments, provided its characters are in the starting vocabulary. Byte-level BPE avoids this character-coverage limit by starting from all 256 byte values.
Bytes. Characters are not the bottom layer. Text is stored as UTF-8 bytes: one per English letter, two to four for most other scripts and for emoji. A tokenizer that starts from bytes can spell anything with 256 symbols, but the same message can start out several times longer in one script than in another.
Fewer tokens means a shorter sequence for everything downstream: less computation, a smaller cache of past tokens, more text in the same context window. More tokens in the vocabulary means larger tables to store and learn.
The mathematics
Let the vocabulary be a set of strings (or byte sequences) . A tokenizer maps an input string into a sequence of tokens (where is the token count):
BPE and unigram tokenization search in different ways
BPE (Sennrich et al., 2016) learns vocabulary entries by merging frequent adjacent symbols. Following Kudo's unigram-language-model formulation, unigram tokenization instead scores candidate segmentations with token probabilities . For the fixed vocabulary , these form a normalized distribution: and . In the display below, the left rule is one BPE merge step (fuse the most frequent adjacent pair into a new symbol ); the right is the unigram choice, the segmentation of with the highest total log-probability:
The unigram path is found by dynamic programming over string positions (the Viterbi algorithm). The BPE merges are learned greedily, once, and then replayed in order as fixed rules on every new text.
Vocabulary size affects parameter count
If embeddings and output logits both use a matrix, then token-related parameters scale like:
If the input and output share one matrix (tied embeddings), this halves to . The tokenizer is not just preprocessing: it sets part of the model's size, the length of every sequence, and which strings the model sees as one thing.
Try it
First a worked example, then one to finish, then problems on your own. Check your answer before you look at the solution. Answers typed here are not kept when you leave.
1 · Worked example
The figure's merges, by hand
Run BPE on "banana bandana banana" until it stops. Write the space as ·. When two pairs tie, the one that appears first in the text wins.
Count. a+n 6, n+a 5, b+a 3, a+· 2, ·+b 2, n+d 1, d+a 1. Merge a+n into an: 21 tiles become 15.
Say the next step to yourself first.2 · Finish it
Finish a second text
The text is "the cat sat on the mat with the hat" (35 characters). The first three merges are done. Make the fourth: how many tokens are left after it?
Merge 1. t+h, counted 4 times: 35 tokens become 31.
Merge 2. a+t, counted 4 times: 31 become 27.
Merge 3. th+e, counted 3 times: 27 become 24. The text now reads the · c at · s at · o n · the · m at · w i th · the · h at.
- Your turn.
3 · On your own
Meeting a new word
A trained tokenizer does not relearn on new text; it replays its merge rules in order. Take the four rules learned in the first exercise (a+n, then b+an, then an+a, then ban+ana) and apply them to "bandanas". Assume "s" is also in the starting character vocabulary. How many tokens come out?
4 · On your own
What a vocabulary costs
A model has a vocabulary of 50,000 tokens and vectors of width . Its input embedding table and its output table are separate matrices. How many parameters do the two tables hold together, in millions?
The code
Both algorithms in plain Python, standard library only. learn_bpe_merges counts
pairs inside words, weighted by how often each word occurs; unigram_map finds the
most probable segmentation of one string.
from collections import Counter
import math
def learn_bpe_merges(corpus, num_merges=3):
vocab = {tuple(word): freq for word, freq in corpus.items()}
merges = []
for _ in range(num_merges):
counts = Counter()
for symbols, freq in vocab.items():
for pair in zip(symbols, symbols[1:]):
counts[pair] += freq
if not counts:
break
best = max(counts, key=counts.get)
merges.append(best)
merged_vocab = {}
for symbols, freq in vocab.items():
out, i = [], 0
while i < len(symbols):
if i + 1 < len(symbols) and (symbols[i], symbols[i + 1]) == best:
out.append(symbols[i] + symbols[i + 1])
i += 2
else:
out.append(symbols[i])
i += 1
merged_vocab[tuple(out)] = freq
vocab = merged_vocab
return merges, vocab
def unigram_map(x, tok_logp):
n, NEG = len(x), -1e30
dp, back = [NEG] * (n + 1), [None] * (n + 1)
dp[0] = 0.0
buckets = {}
for tok, lp in tok_logp.items():
buckets.setdefault(tok[0], []).append((tok, lp))
for i in range(n):
if dp[i] <= NEG / 2:
continue
for tok, lp in buckets.get(x[i], []):
if x.startswith(tok, i):
j, s = i + len(tok), dp[i] + lp
if s > dp[j]:
dp[j], back[j] = s, (i, tok)
if back[n] is None:
return None
out, j = [], n
while j > 0:
i, tok = back[j]
out.append(tok)
j = i
return out[::-1], dp[n]
merges, vocab = learn_bpe_merges({"low": 5, "lower": 2, "new": 6, "newer": 3}, num_merges=4)
print("bpe merges:", merges)
print("bpe vocab:", vocab)
raw_weights = {
"the": 0.08, "there": 0.02, "re": 0.05, "th": 0.06,
"e": 0.07, "r": 0.03, "t": 0.02, "h": 0.02,
}
normalizer = sum(raw_weights.values())
probs = {t: weight / normalizer for t, weight in raw_weights.items()}
assert all(p > 0.0 for p in probs.values())
assert abs(sum(probs.values()) - 1.0) < 1e-12
tok_logp = {t: math.log(p) for t, p in probs.items()}
x = "there"
res = unigram_map(x, tok_logp)
assert res is not None, "No valid segmentation"
seg, score = res
print("x:", x)
print("raw-weight normalizer:", round(normalizer, 3))
print("probability sum:", round(sum(probs.values()), 12))
print("seg:", seg, "tokens:", len(seg), "logp:", round(score, 3))
On its four-word corpus BPE learns ne, new, lo, low, in that order. The unigram
model keeps "there" as a single token, with log-probability −2.862, because every split
into smaller pieces scores lower.
Keep it
Why can this toy not shrink the displayed string "3.14159265358979323846", however many merges it is allowed?
Answer from memory, then check
A merge needs a pair of neighbours that occurs at least twice, and no adjacent pair of digits repeats in that string. Rare strings (IDs, hashes, unfamiliar languages) meet the same wall in real tokenizers and come out as many short tokens.
A trained tokenizer meets a word it never saw. What happens?
Answer from memory, then check
It splits the word into its smallest units and replays its merge rules in order. The word comes out as several familiar pieces, in the worst case single characters or bytes, provided those units are in its vocabulary. A character tokenizer can encounter an unknown character; one starting from all 256 bytes can encode any UTF-8 text.
What does doubling the vocabulary buy, and what does it cost?
Answer from memory, then check
More strings become single tokens, so sequences get shorter: less attention compute, a smaller cache, more text per context window. The price is larger embedding and output tables (about parameters, or if tied) and fewer training examples for each rare token.
In "aaaaaa", a+a is counted 5 times, yet merging it removes only 3 tiles. Why, and which pairs can do this?
Answer from memory, then check
Neighbouring copies share an "a", so only every other copy can merge: aa aa aa. Only a token paired with itself can overlap like this; any other pair removes exactly as many tiles as it was counted.
Why can the same message cost several times more tokens in Hindi than in English?
Answer from memory, then check
Each Devanagari character is 3 UTF-8 bytes where an English letter is 1, and the merges were mostly learned from English text, so fewer long Hindi tokens exist to pack those bytes back together.
BPE and unigram tokenizers both split text into vocabulary items. How does each decide the split?
Answer from memory, then check
BPE replays merge rules learned greedily, in the order they were learned. Unigram keeps a probability for every token and picks the segmentation with the highest total log-probability, found by dynamic programming.
Recalling an answer from memory makes it last longer than rereading it (Roediger and Karpicke, 2006). Add these 6 cards and they come back tomorrow, then after gaps of 3, 7, 16, 35 and 75 days while you still remember them. They stay in this browser only.
The edge
Nobody has settled what a model's atoms should be. Merges learned mostly from English split other languages into more pieces, so the same message can cost several times as many tokens, and so more money and more context, in some languages than in others (Petrov et al., 2023). Byte-level models drop the learned vocabulary and read raw UTF-8 (ByT5: Xue et al., 2022), at the price of much longer sequences. Newer designs group bytes into patches whose length follows how hard the next byte is to predict (Pagnoni et al., 2025). Whether a fixed tokenizer is a necessary compromise or an avoidable handicap is still open.