Tokenization and BPE
How tokenizers are built from data.
Create a free account Try part of it here
Why it matters
In Phase 1 you counted tokens to estimate cost and to see what fits in the context window. This lesson shows where tokens come from. A tokenizer is the tool that cuts text into tokens. It's trained on data before the model is, and its choices shape everything after. They set how much a prompt costs and how much text fits in context. They explain why models stumble on spelling and on counting letters. They also explain why some languages and formats cost more than others.
What you'll be able to do
- Explain why models use subword tokens rather than characters or whole words
- Run byte-pair encoding: repeatedly merge the most frequent adjacent pair
- See how a tokenizer's training data decides what's cheap and what's expensive to encode
- Connect tokenizer design to cost, context limits and quirks of LLM behavior
What it covers
- Three ways to cut up text
- Worked example: three merges by hand
- Training BPE on the catalog
- Real tokenizers
- Why tokenization explains odd LLM behavior
Three ways to cut up text
A model can't read letters. It reads a list of whole numbers, one number per token. So before anything else, text is cut into pieces, and each piece gets a number. The full list of pieces a tokenizer knows is its vocabulary.
There are three obvious ways to cut.
- Characters. Every letter, digit and symbol is a token. The vocabulary is tiny, and no word is ever unknown. But texts turn into very long lists. This paragraph alone would be hundreds of tokens. The model also has to learn spelling from scratch.
- Whole words. The lists stay short. But the vocabulary explodes, because every name, typo and product code is a new word. Any word the tokenizer never saw becomes "unknown".
- Subwords. Common words get a token of their own. Rare words are spelled from common pieces. The lists stay short, the vocabulary has a fixed size, and nothing is ever unknown.
Modern language models use subwords.
One rule, repeated
Picture writing "Thank you" by hand on a thousand cards. Sooner or later you'd order a rubber stamp for it. The phrase you write most often earns a shortcut.
Byte-pair encoding (BPE) builds a subword vocabulary the same way. It starts from single characters. Then it repeats one step, over and over:
- Count every pair of neighboring tokens in the training text.
- Find the pair that appears most often.
- Glue that pair into one new token, everywhere it appears.
Each glued pair is called a merge. After a few merges, common fragments like "er" are tokens. After many more, whole common words are. You stop when the vocabulary reaches the size you want.
In plain words: BPE keeps gluing together the two pieces that most often sit side by side. Frequent text ends up as a few big tokens. Rare text stays as many small ones.
tokens = ["l", "o", "w"] merged = ["lo", "w"] print(len(tokens) - len(merged))
Show the answer
1
3 tokens minus 2 tokens is 1. Each merge replaces a pair with one token, so a word gets one token shorter.
Common words end up as one token and rare ones as several, which explains a lot of odd LLM behavior.
Show the answer
l + o
"l + o" appears in both words: 5 + 2 = 7 times. "o + w" appears 5 times and "o + t" only 2. The most frequent pair wins.
Worked example: three merges by hand
Here's a tiny training text, written as word counts: "tea" 6 times, "team" 2 times, "steam" 3 times and "eat" 4 times. Every word starts as single letters. (Real tokenizers also keep each word's leading space. This example leaves spaces out to stay short.)
Try the first merge yourself. The pair "e" + "a" appears in all four words: 6 + 2 + 3 + 4 = 15 times. The pair "t" + "e" appears in three of them: 6 + 2 + 3 = 11 times. So "e" + "a" wins. Predict the next two merges, then run the code.
from collections import Counter word_counts = {"tea": 6, "team": 2, "steam": 3, "eat": 4} splits = {w: list(w) for w in word_counts} print("start:", sum(len(splits[w]) * n for w, n in word_counts.items()), "tokens") for step in range(3): pairs = Counter() for w, n in word_counts.items(): for a, b in zip(splits[w], splits[w][1:]): pairs[a, b] += n best = max(pairs, key=pairs.get) print(f"merge {step + 1}: {best[0]} + {best[1]} -> {best[0] + best[1]} ({pairs[best]} times)") for w in word_counts: s, out, i = splits[w], [], 0 while i < len(s): if i < len(s) - 1 and (s[i], s[i + 1]) == best: out.append(s[i] + s[i + 1]); i += 2 else: out.append(s[i]); i += 1 splits[w] = out print(splits) print("end:", sum(len(splits[w]) * n for w, n in word_counts.items()), "tokens")
Merge 1 glues "e" + "a" into "ea", seen 15 times. Now "t" + "ea" is the most common pair, at 11, so merge 2 makes "tea". Merge 3 makes "team" from "tea" + "m". That pair appears 5 times: 2 in "team" and 3 in "steam". The text started as 53 single-letter tokens and ends as 22. Look at "eat". Its "ea" got merged, but "ea" + "t" (4 times) never won a round. So "eat" stays as two tokens.
Training BPE on the catalog
This phase uses a corpus, a collection of text for training. It holds 300 short product descriptions for a made-up outdoor brand. The code below trains BPE on it, with 200 merges. It works like the worked example, with two extra steps.
- First it splits the text into words, numbers and punctuation. That way, a merge never glues two words together. This step is called pre-tokenization.
- Each word keeps its leading space, so " layer" and "layer" are different. GPT-style tokenizers do the same.
import re from collections import Counter import requests text = requests.get("https://api.gradient.test/v1/corpus").json()["text"] pattern = r" ?[A-Za-z]+| ?[0-9]+| ?[^\sA-Za-z0-9]+|\n" # split into words first word_counts = Counter(re.findall(pattern, text)) splits = {w: list(w) for w in word_counts} # every word starts as characters merges = [] for step in range(200): pairs = Counter() for w, n in word_counts.items(): for a, b in zip(splits[w], splits[w][1:]): pairs[a, b] += n # count adjacent pairs, weighted best = max(pairs, key=pairs.get) merges.append(best) for w in word_counts: # apply the merge everywhere s, out, i = splits[w], [], 0 while i < len(s): if i < len(s) - 1 and (s[i], s[i + 1]) == best: out.append(s[i] + s[i + 1]); i += 2 else: out.append(s[i]); i += 1 splits[w] = out print("first merges:", [a + b for a, b in merges[:12]]) print("merges 96–105:", [a + b for a, b in merges[95:105]]) def encode(s): tokens = [] for w in re.findall(pattern, s): piece = list(w) for a, b in merges: # replay merges in learned order out, i = [], 0 while i < len(piece): if i < len(piece) - 1 and (piece[i], piece[i + 1]) == (a, b): out.append(a + b); i += 2 else: out.append(piece[i]); i += 1 piece = out tokens += piece return tokens for s in ["It is light and fully waterproof.", "Our marketing team loves spreadsheets."]: t = encode(s) print(f"\n{len(s)} characters → {len(t)} tokens: {t}") total = sum(len(splits[w]) * n for w, n in word_counts.items()) print(f"\nwhole corpus: {len(text):,} characters → {total:,} tokens ({len(text) / total:.1f} characters per token)")
Here's what the code does.
- It downloads the catalog, splits it into words and counts how often each word appears.
- It turns every word into a list of single characters.
- It repeats 200 times: count every neighboring pair, weighted by how often its word appears. Then merge the most frequent pair everywhere, and record the merge.
encodetokenizes new text. It splits the text into words, then replays the learned merges in the order they were learned.- Last, it compares characters with tokens, for two test sentences and for the whole catalog.
The first merges are short, frequent fragments, such as " f", "ad" and "er". Later merges are whole words from the catalog, such as " layer" and " easy". Text that looks like the catalog shrinks to few tokens. The sentence about spreadsheets never appears in the catalog. So it falls apart into many small pieces.
Go deeper Why the merges replay in order
Merges build on each other. In the worked example, "tea" could only form after "ea" existed. So encode has to apply merge 1 first, then merge 2, and so on. Skip ahead, and a merge like "tea" + "m" finds no "tea" to glue. The word would stay in smaller pieces than training intended.
The pattern line does the pre-tokenization. It has four parts, tried in turn:
?[A-Za-z]+: an optional space, then a run of letters (a word).?[0-9]+: an optional space, then a run of digits (a number).?[^\sA-Za-z0-9]+: an optional space, then a run of anything that isn't a space, letter or digit (punctuation).\n: a line break, kept as its own piece.
Show the answer
Its words are rare or missing in the catalog, so few learned merges cover them
Merges are learned from the training text. Words the catalog rarely or never uses get few merges, so they're spelled from small pieces. Nothing is dropped. The sentence only needs more tokens.
Real tokenizers
Real tokenizers follow the same recipe, with a few upgrades.
They start from bytes, not characters. Computers store text as bytes: small numbers from 0 to 255. UTF-8 is the standard rule for turning text into bytes. A plain English letter takes one byte. "é" takes two, and many emoji take four. Byte-level BPE starts from all 256 possible bytes instead of from characters. GPT-2 and later GPT models use it, and so do many open models. It can encode any input at all: any language, emoji, or odd binary junk. In the worst case, it spells the text one byte at a time.
Their vocabularies are big. A real vocabulary typically holds 50,000 to 200,000 tokens.
They pre-tokenize. They split text into words, numbers and punctuation first, as the catalog code did. So merges never cross word boundaries.
They add special tokens. These are added by hand, outside the merging. Examples are a marker for "end of text" or for "the user is speaking now". Chat formats are built from them.
Other methods exist. SentencePiece is a popular tool that can train BPE or a method called Unigram. Unigram works the other way around. It starts with a large vocabulary and uses probabilities to drop the least useful pieces. The aim is the same: frequent text gets short codes.
Go deeper Bytes in Python
list("é".encode("utf-8")) gives [195, 169]: two bytes for one character. list(" cat".encode("utf-8")) gives [32, 99, 97, 116]. The space is byte 32, and each plain letter is one byte.
A byte-level tokenizer starts with token IDs 0 to 255, one per byte. Each merge adds the next ID: 256, then 257, and so on. That's exactly what you'll build in the exercise. GPT-2's vocabulary has 50,257 tokens: 256 bytes, 50,000 merges and one special end-of-text token.
Show the answer
Its base vocabulary contains all 256 bytes, so any text can be spelled from them
Any text is stored as bytes, and every byte has a token. In the worst case, text is encoded byte by byte. Merges only make common sequences shorter.
Why tokenization explains odd LLM behavior
- Cost and context. Large tokenizers trained mostly on English average roughly four characters per token on English prose. Some languages, code styles and formats were rare in the tokenizer's training data. They need more tokens for the same content. So they cost more, and they fill the context window faster.
- Spelling and counting letters. A model sees " waterproof" as one or two token IDs, not as ten letters. So "How many r's are in strawberry?" asks about something the model never sees directly.
- Numbers. Digits can be grouped unevenly. "2025" might be one token while "2026" is two. That's one reason arithmetic is harder for a model than it looks.
- Leading spaces. " Summit" and "Summit" are different tokens, with different IDs.
For marketers: brand names, product codes and campaign hashtags often split into several tokens. That's harmless for meaning. But it explains why token counts, and costs, can run higher than a word count suggests for catalog or multilingual content.
Show the answer
The rare-language version uses more tokens, so it costs more
Few merges were learned for that language, so its words split into smaller pieces, sometimes single bytes. Nothing is unknown, but the same content takes more tokens, costs more and uses more of the context window.
Practice what you just read
- 2 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 4 review cards for spaced repetition
Try a quiz question
What does each step of BPE training do?