Tokens And Embeddings

How a Tokenizer Learns Its Pieces: Byte Pair Encoding, Built From Scratch

0 of 15 complete

0%

Contents

Back|Tokens And EmbeddingsHow a Tokenizer Learns Its Pieces: Byte Pair Encoding, Built From Scratch
1/15
35 min left
Prerequisites
What a Token Is: How a Language Model Reads Textrequired
1 of 15

Nobody Writes the List by Hand

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.

An illustration of an engineer at a table covered with small blank tiles, gluing two tiles together into one longer tile, with a stack of joined tiles beside her. A line says small tiles become longer tiles one glued pair at a time.

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.

The Words You Need First

If a word below is new, read its line. The first four come from the last lesson.

A hand-drawn word list. Tokens per word: pieces an average word is cut into. Pair: two pieces side by side. Merge: glue one pair into one new piece. Training: running the merge rule over lots of text. Held-out text: text kept aside, used only for testing. BPE: byte pair encoding, this whole rule.

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.

The Whole Rule in Three Steps

Byte pair encoding repeats three steps.

A flowchart: 1, count every neighbouring pair; 2, pick the pair seen most often; 3, glue it into one new piece; then a question, enough merges? If no, back to step 1. If yes, the result is the vocabulary.

  1. Count every pair of neighbouring pieces in the training text.
  2. Pick the pair seen most often.
  3. Glue that pair into one new piece, everywhere it appears. The new piece joins the vocabulary.

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

A sequence diagram with three columns: training text, pair counter and vocabulary. Step 1, every neighbouring pair goes to the counter. Step 2, the most common one goes to the vocabulary. Step 3, it is glued everywhere in the text. Step 4, the words are now shorter. A note says a merge never joins two words.

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.

A Tiny Example You Can Follow by Hand

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.

A real screenshot of tiny_bpe.py open in VS Code: 38 lines of plain Python that split seven words into characters, then repeat 8 times: count every pair of neighbouring pieces, pick the most common, and glue it together everywhere.

And this is what it printed when I ran it.

A real screenshot of VS Code's terminal after running python tiny_bpe.py. It prints 8 merges, from w plus e into we, seen 4 times, to wi plus d into wid, then the seven words cut into pieces, such as lowe and st, and newe and r.

Follow the first merges:

  • Merge 1: "w" then "e" appears 4 times (lower, lowest, newer, newest). It becomes "we".
  • Merge 2: "l" then "o" appears 3 times. It becomes "lo".
  • Merge 3: "s" then "t" appears 3 times. It becomes "st".
  • Merge 4: "lo" then "we" is now a pair too. Two merges combine into "lowe".

The 8 merges of the tiny example, one row each, with the two pieces and the new piece: w and e make we, 4 times; l and o make lo; s and t make st; lo and we make lowe; n and e make ne; ne and we make newe; w and i make wi; wi and d make wid. A note says merge 4 glues two earlier 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.

Training on Real Text

A toy is not a measurement. So I trained the same rule on real text: this site's own lessons.

Two panels. Training: 200,000 words from 4 of every 5 lessons, and 94 characters as the starting vocabulary. Testing: 242,692 words in 175 held-out lessons, measured as tokens per word at 7 merge counts. A note says the same test text was also cut by GPT-4o's tokenizer.

  • The text. All the other lessons on the site, with quizzes, code and formatting removed. Every fifth lesson was held out: 175 lessons, 242,692 words, never seen in training.
  • Training. The first 200,000 words of the other lessons.
  • The start. 94 different characters appear in that text. That is the starting vocabulary.
  • The test. At seven points, from 0 to 4,000 merges, cut the held-out lessons and count tokens per word.
  • The comparison. GPT-4o's tokenizer on the same held-out lessons. Its name is o200k_base; tiktoken is the Python library that runs it.

An isometric row of four blocks joined by arrows: lessons, 200,000 words; the trainer, count and glue; a cylinder for the merge list, 4,000 learned; and the test, 175 lessons. A note says the trainer starts from characters, while real ones start from bytes.

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.

What It Learned First

The first merges are the most common pairs in English lesson text.

The first 12 merges learned from the lessons, in two columns: space and t, space and a, h and e, i and n, e and r, r and e, o and n, a and t, space and s, space-t and he making space-the, space and c, and e and s. A note says 5 of the first 12 start with a space, the start of a word.

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.

Each Merge Saves Less Than the One Before

This is the main result.

A line chart of tokens per word on the held-out lessons against merges learned, with unequal steps along the bottom, falling steeply then flattening: 6.12 at 0, 3.68 at 100, 3.03 at 250, 2.57 at 500, 2.18 at 1,000, 1.84 at 2,000 and 1.58 at 4,000. A dashed line marks GPT-4o's tokenizer at 1.28.

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.

  • After 100 merges: 3.68.
  • After 1,000 merges: 2.18.
  • After 4,000 merges: 1.58.

GPT-4o's tokenizer took 1.28 on the same text.

A bar chart of tokens per word saved by each stretch of merges: 2.43 for the first 100, then 0.66, 0.46, 0.39, 0.34, and 0.26 for merges 2,000 to 4,000.

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.

Watching Words Become Whole

Here is how five real words were cut at 100 merges, and again at 4,000.

Five words shown as rows of token boxes at 100 merges and at 4,000 merges. tokenization: 6 pieces, then 2, token and ization. unbelievably: 10, then 6. database: 5, then 1. Kubernetes: 8, then 1. latency: 5, then 1.

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.

Two panels for the word unbelievably: 6 pieces with my BPE after 4,000 merges, and 1 piece with GPT-4o's tokenizer, trained on far more text. A line says a tokenizer reflects the text it trained on.

So a tokenizer reflects the text it was trained on. Words common in that text become cheap. Words rare in it stay expensive.

How the Real Ones Differ

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:

A real screenshot of the tiktoken README on GitHub, section on byte pair encoding. It lists four properties: reversible and lossless, works on any text, compresses text to about 4 bytes per token on average, and lets the model see common subwords like ing.

Real tokenizers add a few things on top:

A hand-drawn comparison. My trainer starts from characters, uses 200,000 words of lessons and has 4,094 pieces after 4,000 merges. tiktoken's starts from bytes, uses far more text in many languages and code, and has 200,019 pieces in o200k_base. A note says the rule is the same, only the scale differs.

  • They start from bytes, not characters. So any text, in any language or with any emoji, can always be cut. Nothing is ever unknown.
  • They train on huge amounts of text, from many languages and a lot of code.
  • They run far more merges. GPT-4o's tokenizer has 200,019 pieces; mine stopped at 4,094.
  • They split text into words with a more careful rule than "split on spaces". For example, the GPT-4 and GPT-4o tokenizers keep digits in groups of up to three.

Three cards with real logos: Python ran my trainer with no extra library, tiktoken was used only to compare, and VS Code is where the tiny trainer ran. A note says training 4,000 merges took 93 seconds on a laptop.

How New Text Is Cut

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

The word database, with a space in front, cut three ways by the learned merge list: as 9 single characters at the start, as 5 pieces after 100 merges (space-d, at, ab, as, e), and as 1 piece after 4,000 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.

Train It Yourself

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.

Why This Matters in Your Work

Three cards on where tokenizer pieces cost you: text unlike the training text needs more pieces and more money; when choosing a model, a tokenizer trained on text like yours reads it cheaper; with odd spellings or rare names, the model sees pieces, not letters.

  • Your text is not like the training text. A tokenizer trained mostly on English cuts other languages, rare jargon and product names into more pieces. That costs you money, and room in the amount of text a model can read at once.
  • You are choosing between models. A model whose tokenizer was trained on text like yours will read it in fewer tokens.
  • You see odd behaviour on spelling or rare names. The model sees the pieces its training chose, not the letters.

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

What This Lab Measured, and What It Did Not

Two columns. Measured: a teaching BPE on characters, 175 held-out lessons, seven merge counts up to 4,000. Not measured: training on bytes, other languages, more than 4,000 merges.

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.

What to Do Next

A hand-drawn list of four things to do: run the tiny trainer on your own words, remember the rule of counting pairs and gluing the most common, expect rare words to cost more pieces, and count your text with your model's tokenizer.

  1. Run the tiny trainer with your own words, and watch the pieces form.
  2. Remember the rule: count pairs, glue the most common, repeat.
  3. Expect rare words to cost more. They were rare in the training text too.
  4. Check your own text with your model's tokenizer, as in the last lesson.

Two numbers to keep: tokens per word fell from 6.12 to 1.58 between 0 and 4,000 merges, and GPT-4o's tokenizer took 1.28 on the same lessons.

Knowledge Check

Knowledge Check

4 questions - Score 80% to pass

Q1

How does byte pair encoding decide which new piece to add next?

Q2

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?

Q3

After 4,000 merges, Kubernetes was one piece but unbelievably was six. Why?

Q4

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.

A real terminal recording of bpe_train.py replaying its stored results. It prints the training and test sizes, the 94 starting characters, the first 12 merges, tokens per word on the held-out lessons from 6.12 at 0 merges to 1.58 at 4,000, GPT-4o's tokenizer at 1.28, and five words cut at 100 and at 4,000 merges.