· 16 min read
NLP #3: Byte-Pair Encoding
On this page
By the end of this post, you'll be able to train a tiny tokenizer by hand. You'll also predict how it splits a word it has never seen. Every natural language processing (NLP) model reads text as tokens, the units it works with. Section 2.4 of Speech and Language Processing shows where models like GPT-4o get theirs: byte-pair encoding (BPE). It starts from single characters and merges the most frequent pair of neighboring tokens, again and again.

Intuition
Tokens smaller than words
It's tempting to think of a tokenizer as a dictionary: one token per word. Our running example, a short review, shows why that fails:
the café baker bakes cakes, the café bakes soufflé
If the vocabulary holds exactly these words, a new review that says "baked" leaves the model with nothing to work with. This is the unknown word problem.
Make every character a token instead, and nothing is ever unknown. But the units
are too small: a letter like k means little by itself, and texts get long.
The answer lies between the two: subwords, pieces of any length learned from
data. Common words become single tokens. The tokenizer builds rarer words from
pieces like bake and d, falling back to single characters in the worst case.

Merge the most frequent pair, again and again
A BPE tokenizer has two parts. A trainer learns tokens from training text, and an encoder uses them to split new text.
The trainer cuts the text into words at spaces and punctuation, keeping each
space on the word after it. We write that space as a dot: ·bakes. It splits
each word into characters, which form the starting vocabulary: our review has 50
characters, 16 of them different. Then it repeats two steps:
- Count every pair of neighboring tokens inside the words.
- Merge the most frequent pair into one new token, everywhere it occurs.
a k occurs 4 times: once in baker, twice in bakes and once in cakes. No
pair beats that, so ak becomes a token. Ties go to the pair seen first. The
first five merges:
| Merge | Pair | Count | New token | Tokens left in the text |
|---|---|---|---|---|
| 1 | a + k | 4 | ak | 46 |
| 2 | ak + e | 4 | ake | 42 |
| 3 | · + c | 3 | ·c | 39 |
| 4 | · + b | 3 | ·b | 36 |
| 5 | ·b + ake | 3 | ·bake | 33 |
Notice what BPE doesn't do. A linguist would split baker into its
morphemes, bake and -er. BPE builds the
meaningless ake first and never makes er, which occurs only once. It follows
counts and nothing else: find the most frequent pair, merge it, repeat. Run it
yourself:
k = 0, |V| = 16, L = 50
Corpus (word and its count)
- the
- ·café×2
- ·baker
- ·bakes×2
- ·cakes
- ,
- ·the
- ·soufflé
Most frequent pairs
- a + k: 4
- k + e: 4
- · + c: 3
- c + a: 3
- · + b: 3
the·maker·baked·cakes

New words split into known pieces
The encoder counts nothing. It splits new text into characters, then replays the merges in the order they were learned. After five merges, "the maker baked cakes" becomes:
t h e · m ake r ·bake d ·c ake sbaked and maker never appeared in the review, yet both now use known pieces.
Real tokenizers make tens of thousands of merges. GPT-4o's tokenizer has about 200,000 tokens, and most of our review stays whole in it:
the ·café ·baker ·b akes ·cakes , ·the ·café ·b akes ·souff léThe made-up unbakeable becomes un b ake able, and ·bakes splits as
·b akes: counts decide, not meaning. Try your own text in the
Tiktokenizer.

Real tokenizers start from bytes
In practice, BPE starts from the UTF-8 bytes of the text, not characters. A byte has only 256 possible values, so no input contains an unknown starting token.
Switch on "start from UTF-8 bytes" in the widget. é now starts as two bytes,
C3 and A9. They occur together 3 times, so merge 4 joins them: BPE
rediscovers é without knowing what UTF-8 is.
Training text matters too. Most of it is English, so English gets the largest share of the tokens. In one sentence I ran through GPT-4o's tokenizer, 8 English words took 9 tokens, while my 6-word Hebrew translation took 12.

The math
The corpus
Definition 1 (Corpus and vocabulary). A corpus is the training text, cut into words: strings with counts . Each string is a sequence of tokens from a vocabulary , which starts as , the set of all characters (or bytes) in . The corpus length is
where is the number of tokens in .
Our review has 8 different strings: the (1), ·café (2), ·baker (1),
·bakes (2), ·cakes (1), , (1), ·the (1) and ·soufflé (1). So
and .

Counting pairs
Definition 2 (Pair count). For tokens and , let be the number of times directly follows in string . Then
We count each different word once and multiply by its count. For our review,
,
from ·baker, ·bakes and ·cakes.
Key equation (Next merge). Training picks the most frequent pair:
It returns the pair, not its count. Ties need a fixed rule: ours takes the pair seen first.
One merge
Definition 3 (Merge). A merge concatenates the two tokens into and adds it to . It then replaces each occurrence of followed by in with , reading left to right. Training makes merges, a number we choose.
Key equation (Sizes after a merge). Each merge adds one token to , and each replacement removes one token from :
The second equation assumes . If they're equal, occurrences can
overlap: a a a has count 2, but only one replacement fits. For our review,
and ; after five merges, and .

Encoding new text
Definition 4 (BPE encoder). Training produces an ordered list of merges . To encode new text, split it into words and each word into starting units. Then apply everywhere, then , and so on through .
Counts in the new text play no part, but order does: ake can only form after
merge 1 has made ak. The whole tokenizer in Python:
import re
from collections import Counter
def words_of(text):
return re.findall(r" ?\w+|[^\w\s]", text)
def merge(tokens, a, b): # turn each "a b" into "ab", left to right
out = []
for t in tokens:
if out and out[-1] == a and t == b:
out[-1] = a + b
else:
out.append(t)
return out
def train(text, k):
freq = Counter(words_of(text))
corpus = {w: list(w) for w in freq}
merges = []
for _ in range(k):
pairs = Counter()
for w, toks in corpus.items():
for pair in zip(toks, toks[1:]):
pairs[pair] += freq[w]
a, b = pairs.most_common(1)[0][0] # ties: the pair seen first
merges.append((a, b))
corpus = {w: merge(t, a, b) for w, t in corpus.items()}
return merges
def encode(text, merges):
out = []
for w in words_of(text):
toks = list(w)
for a, b in merges:
toks = merge(toks, a, b)
out += toks
return out
merges = train("the café baker bakes cakes, the café bakes soufflé", 5)
print(encode("the maker baked cakes", merges))
# ['t', 'h', 'e', ' ', 'm', 'ake', 'r', ' bake', 'd', ' c', 'ake', 's']
Byte-level BPE
Definition 5 (Byte-level BPE). Byte-level BPE runs the same algorithm with , on the UTF-8 bytes of the corpus. Every merged token is a sequence of bytes, so
All 256 bytes are in , so the encoder never meets an unknown token, and a text of bytes becomes at most tokens. A vocabulary of about 200,000 tokens means about merges.
The widget's byte mode counts only the bytes our review uses, so shows 17,
not 256. shows 53, since each of the three é takes 2 bytes.

What you can do now
- Train a BPE tokenizer by hand: start from characters or bytes, merge the most frequent pair of neighboring tokens, and repeat times.
- Encode a new word by replaying the merges in order:
bakedbecomes·bake d. - Predict the sizes: each merge adds one token to the vocabulary and removes tokens from the text.
- Explain why a byte-level tokenizer never meets an unknown token.
Exercises
Each question has one correct answer. Pick your answer, then read the explanation. Questions 6–10 use the definitions and equations above.
You can change your answer at any time. The answers come in a new order each time.
1. (Intuition) A tokenizer's vocabulary holds only whole words, including bake, bakes and baker. What happens when a new text contains "baked"?
2. (Intuition) Characters never cause unknown words. Why not stop there and use them as the tokens?
3. (Intuition) After training on our review, the encoder gets a text in which z is followed by q a thousand times. What does it do with those pairs?
4. (Intuition) In the real tokenizer, ·baker is one token but unbakeable takes four. Why?
5. (Intuition, widget) In the widget, switch on "start from UTF-8 bytes", leave the review as it is, and click Next merge. Which merge joins C3 and A9 into é?
6. (Math · Definition 2) A corpus has hop (count 5), hops (count 2), shop (count 3) and shops (count 1), split into letters. What is ?
7. (Math · Sizes after a merge) The same corpus has and . The first merge has count 11. What are and ?
8. (Math · Definition 3) The first two merges on that corpus are h + o and ho + p. The corpus is now hop, hop s, s hop and s hop s, with the same counts. What is the third merge?
9. (Math · Definition 4) The merges, in order, are h + o, ho + p, s + hop, hop + s. How does the encoder split shops?
10. (Math · Definition 5) A byte-level tokenizer made its 1,000 merges on English text only. It now meets "שלום", which takes 8 bytes in UTF-8. What is the most tokens it can produce?
Score: 0 of 10 (0 answered)
Next: N-gram language models, which predict the next word from the few words before it (chapter 3, section 3.1).