In the last lesson, GPT-4o's tokenizer knew 200,019 pieces. It cut "Tokenization" into "Token" and "ization", and kept " unbelievably", with its space in front, whole.
Who decided that? Nobody sat down and typed 200,019 pieces. The list was learned from text, by one very small rule, repeated many times.

Think of a table covered with small tiles, one letter on each. You look for the two tiles that sit next to each other most often, like "t" then "h". You glue every such pair into one longer tile. Then you look again, and glue again.
After a few thousand rounds, common words are single tiles and rare words are two or three. That is the whole idea. It is called byte pair encoding, or BPE. (A byte is the small code a computer stores one letter in.)
In this lesson I build it in plain Python. I train it on this site's own lessons, and measure how much each round saves.
If a word below is new, read its line. The first four come from the last lesson.

Token. One small piece of text that a model reads as a single unit.
Tokenizer. The tool that cuts text into tokens.
Vocabulary. The fixed list of every piece a tokenizer knows.
Tokens per word. How many pieces an average word is cut into. Lower means shorter text for the model.
Pair. Two pieces that sit right next to each other, like "t" and "h" in "the".
Merge. Gluing one pair into one new, longer piece, and adding it to the vocabulary.
Training. Running the merge rule over a large amount of text to learn which merges to keep.
Held-out text. Text kept aside during training and used only for testing, so the score is honest.
Byte pair encoding repeats three steps.

Then go back to step 1. You stop after a chosen number of merges. That number sets the size of the vocabulary.

One detail matters. The text is first split into words. A merge never joins the end of one word to the start of the next. So "the" can become one piece, but "the cat" never becomes one piece.
Here is the rule on seven short words: low, lower, lowest, newer, newest, wider, widest. This is the real file, open in my VS Code.

And this is what it printed when I ran it.

Follow the first merges:

After 8 merges, "lowest" is two pieces, "lowe" and "st". Nobody told the program about "low", "new" or "wid". It found the common pieces by counting.
To run it yourself, save the file below as tiny_bpe.py and run python3 tiny_bpe.py (on Windows: ). It needs no library and no setup; any Python 3 works. The "(venv)" in my screenshot is just my own Python setup.
A toy is not a measurement. So I trained the same rule on real text: this site's own lessons.


One honest simplification: my trainer starts from characters. Real BPE tokenizers, like tiktoken's, start from bytes, the tiny codes a computer stores text in. For English letters, one character is one byte, so the two are almost the same here. But not quite: the test lessons held 4 characters training never saw (<, ×, Σ and ). A character-level tokenizer has no piece for them. A byte-level one never has that gap.
The first merges are the most common pairs in English lesson text.

Merge 1 joined a space and "t". Merge 2 joined a space and "a". Merge 3 was "h" and "e". By merge 10, it had built " the" with its leading space, the most common word in English.
Look at how many early merges begin with a space. A space before a letter marks the start of a word, and word starts are very common. That is why, in the last lesson, the space belonged to the word after it.
This is the main result.

With no merges, every character is its own token: 6.12 tokens per word on the held-out lessons. That includes the space before each word.
GPT-4o's tokenizer took 1.28 on the same text.

The first 100 merges saved 2.43 tokens a word. The last 2,000 merges, from 2,000 to 4,000, saved only 0.26. The common pairs are glued early. After that, every new piece covers rarer and rarer text.
That is why real vocabularies are so big. Getting from 1.58 to 1.28 takes far more pieces. It also takes far more training text than 200,000 words.
Here is how five real words were cut at 100 merges, and again at 4,000.

At 100 merges, "database" was five pieces. At 4,000 it was one. "" went from eight pieces to one, because this course mentions it often.
"unbelievably" was still six pieces at 4,000 merges. It is rare in these lessons. GPT-4o's tokenizer, trained on far more text, keeps it whole.

So a tokenizer reflects the text it was trained on. Words common in that text become cheap. Words rare in it stay expensive.
The rule you built is the real rule. OpenAI's own description of BPE, in the tiktoken README, lists what it gives. Inside the model, each piece is stored as a number, which is why it says tokens are numbers:

Real tokenizers add a few things on top:


Training happens once. After that, the tokenizer only uses its list of merges.

To cut a new word, it starts from single characters (or bytes). Then it applies the merges it learned, in the order it learned them. The earliest merges are the most common pairs, so they are applied first.
This is why the same word is always cut the same way. The tokenizer does not guess. It follows its list.
This box runs the same tiny trainer in your browser. Press Run.
Then change MERGES = 8 to MERGES = 2, and run it again. You will see the words stay in small pieces. Try 20 next.
Then change the words in text to your own, in any language, and watch which pieces it learns.

You almost never train your own tokenizer. But knowing how the pieces were chosen tells you where the costs will be.

Measured: my own BPE trainer, starting from characters. It trained on 200,000 words of this site's lessons, and was tested on 175 lessons it never saw, at seven merge counts. And GPT-4o's tokenizer on the same test text.
Not measured: training on bytes, other languages, or more than 4,000 merges. My tokenizer is a teaching version. Its numbers show the shape of the curve, not the quality of a real tokenizer.


4 questions - Score 80% to pass
How does byte pair encoding decide which new piece to add next?
On the held-out lessons, the first 100 merges saved 2.43 tokens per word, and the merges from 2,000 to 4,000 saved 0.26. Why?
After 4,000 merges, Kubernetes was one piece but unbelievably was six. Why?
My trainer started from characters. What do real tokenizers like tiktoken's start from, and why?
python tiny_bpe.py# Train a tiny BPE tokenizer from scratch. Plain Python, no library.
# The same idea built the tokenizers behind GPT-2, GPT-4 and GPT-4o.
text = "low lower lowest newer newest wider widest"
MERGES = 8 # try 2, or 20
# every word starts as single characters
words = [list(w) for w in text.split()]
for step in range(1, MERGES + 1):
# 1. count every pair of neighbouring pieces
pairs = {}
for w in words:
for a, b in zip(w, w[1:]):
pairs[a, b] = pairs.get((a, b), 0) + 1
if not pairs:
break
# 2. the most common pair becomes one new piece
best = max(pairs, key=pairs.get)
print(f"merge {step}: {best[0]!r} + {best[1]!r} -> {best[0] + best[1]!r}"
f" (seen {pairs[best]} times)")
# 3. glue that pair together everywhere
new_words = []
for w in words:
out, i = [], 0
while i < len(w):
if i + 1 < len(w) and (w[i], w[i + 1]) == best:
out.append(w[i] + w[i + 1])
i += 2
else:
out.append(w[i])
i += 1
new_words.append(out)
words = new_words
print()
for w in words:
print("|".join(w))
→Here are the stored results, replayed in the terminal. A "|" at the start of a word is the space before it.
