Originally published on almoghagbi.com, where the figures are interactive.
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:
▶ Interactive figure: try it on the original post.
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 s
baked 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.
1. (Intuition) A tokenizer's vocabulary holds only whole words, including bake, bakes and baker. What happens when a new text contains "baked"?
A. It splits "baked" into
bakeandd.B. It uses the closest word it knows,
bake.C. It has no token for "baked", so the word is unknown.
D. It adds "baked" to the vocabulary and carries on.
C. With whole words only, a word that's missing from the list has no token. That's the unknown word problem, and subwords exist to solve it.Answer
d, because it holds only whole words.
2. (Intuition) Characters never cause unknown words. Why not stop there and use them as the tokens?
A. They are too small: a single letter means little, and texts become much longer.
B. A computer can't store every character of every language.
C. A list of characters is longer than a list of words.
D. New words would still contain unknown letter pairs.
A. Characters solve the unknown word problem but give long sequences of units that carry little meaning. Subwords keep the safety of characters and add larger pieces.Answer
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?
A. It merges them, since they're the most frequent pair in this text.
B. It merges them after finishing the learned merges.
C. It reports an error, because
zqwas never seen.D. It leaves them as separate tokens, since no learned merge joins them.
D. The encoder only replays the merges learned in training. Counts in new text play no part, so Answer
z and q stay apart.
4. (Intuition) In the real tokenizer, ·baker is one token but unbakeable takes four. Why?
A. Short words are always kept whole.
B. Frequent strings earn merges early, and the rare ones run out of merges.
C. The tokenizer knows that "un" and "able" are English morphemes.
D. Words with a prefix are always split by a rule.
B. Each merge goes to the most frequent pair at that moment, and the number of merges is limited. Answer
·baker occurs often enough to be built up completely. unbakeable is rare, so it stays in pieces.
·bakes is short, and it's split into ·b and akes.
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 é?
A. Merge 1
B. Merge 3
C. Merge 4
D. It never happens in this text.
C. The pair occurs 3 times: twice in Answer
·café and once in ·soufflé. Merges 1 and 2 have count 4. At count 3, the pair · + c is seen first and takes merge 3, so C3 + A9 gets merge 4.
a + k, with count 4.· + c also has count 3, and it comes earlier in the text.é.
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
?
A. 3
B. 2
C. 4
D. 11
A. Answer
p is followed by s once in hops and once in shops, so the count is
2⋅1+1⋅1=3
.
shop and shops.
7. (Math · Sizes after a merge) The same corpus has and . The first merge has count 11. What are and ?
A. ,
B. ,
C. ,
D. ,
D. One merge adds one token, so
∣V1∣=4+1=5
. Each of the 11 replacements turns two tokens into one, so
L1=40−11=29
.Answer
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?
A.
hop+s, with count 3B.
s+hop, with count 4C.
s+h, with count 4D.
o+p, with count 11
B. Only two pairs are left.
count(s,hop)=3+1=4
and
count(hop,s)=2+1=3
, so Answer
s + hop wins.
h, o and p no longer appear on their own in the corpus, so these pairs have count 0.
9. (Math · Definition 4) The merges, in order, are h + o, ho + p, s + hop, hop + s. How does the encoder split shops?
A.
s hopsB.
shopsC.
shop sD.
s h o p s
C. The first two merges give Answer
s hop s. The third, s + hop, gives shop s. The fourth needs hop as a token, but it's now inside shop, so nothing changes.
hop + s before s + hop, against the learned order.shop and s.
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?
A. 8
B. 4
C. 1
D. It can't encode the word.
A. All 256 bytes are in
V0
, so the worst case is one token per byte: 8 tokens.Answer
Next: N-gram language models, which predict the next word from the few words
before it (chapter 3, section 3.1).
Co-authored with AI.








Top comments (0)