TAI BUI
Back to curriculum
LearnPython

Subword Tokenization — BPE, WordPiece, Unigram, SentencePiece

Word tokenizers choke on anything they've never seen. Character tokenizers dodge that by blowing your sequence length past anything usable. Subword tokenization splits the difference, keeping common words whole and breaking the rest into pieces the model has already learned. Every model shipping in 2026 runs on one.

TIME: ~60 minutesPREREQUISITES: Phase 5 · 01 (Text Processing), Phase 5 · 04 (GloVe / FastText / Subword)

Learning Objectives

  • Explain how BPE, WordPiece, and Unigram each build a vocabulary differently, and name the frontier models and libraries that use each one.
  • Implement BPE training and encoding from scratch, including the `</w>` word-boundary marker and the merge-order dependence at inference time.
  • Choose between SentencePiece, HF Tokenizers, and tiktoken for a given training-versus-inference scenario, and size the vocabulary and `character_coverage` for the target languages.
  • Diagnose the production tokenizer failure modes that still ship in 2026: tokenizer drift, whitespace ambiguity, multilingual undertraining, and emoji token bloat.

The Problem

Your vocabulary has 50,000 words, and a user types "untokenizable". Your tokenizer returns [UNK], and the model now has no signal about the word. Worse: the 90th-percentile document in your corpus has 40 rare words, which means 40 bits of dropped information per document.

Subword tokenization fixes this. Common words stay single tokens, and rare words decompose into meaningful pieces: untokenizable → un, token, izable. Because any string is ultimately a sequence of bytes, the training data ends up covering everything.

Every frontier LLM in 2026 ships on one of three algorithms (BPE, Unigram, WordPiece), wrapped in one of three libraries (tiktoken, SentencePiece, HF Tokenizers). Ship a language model and you have to pick one.

Warm-up0/3
1. A model has a fixed word-level vocabulary and meets a word it never saw in training. What does a typical word-level tokenizer do with it?Warm-up
2. How many distinct values can a single byte hold?Warm-up
3. A neural network cannot read strings directly. What does a tokenizer hand it instead?Warm-up
Check: The Problem0/1
1. What does subword tokenization buy you over a word-level vocabulary?Explain

The Concept

Every algorithm below solves the same tradeoff: keep frequent words as single tokens, and break the long tail into pieces small enough that nothing ever falls out of vocabulary. In code, whatever algorithm builds it, a trained vocabulary boils down to two objects: a token-to-ID table, and (for BPE) the ordered list of merge rules used to build it.

python
vocab = {"the": 0, "ing": 1, "un": 2, "token": 3, "izable": 4}  # token string -> id
merges = [("t", "h"), ("th", "e"), ("i", "n")]                     # BPE's ordered merge rules, applied in order

BPE vs Unigram vs WordPiece, character-by-character

Analogy: it's like spelling an unfamiliar name over the phone. Nobody spells out "Anderson"; everyone already knows that one. "Kowalczyk" gets broken into chunks the listener has heard before: K, O, WAL, CZYK. Subword tokenization does the same to text, keeping common words whole and spelling out anything rare using pieces the model already recognizes.

BPE (Byte-Pair Encoding). Start with a character-level vocabulary. Count every adjacent pair. Merge the most frequent pair into a new token. Repeat until you hit the target vocabulary size. Dominant algorithm: GPT-2/3/4, Llama, Gemma, Qwen2, Mistral.

Byte-level BPE. Same algorithm but over raw bytes (256 base tokens) instead of Unicode characters. Guarantees zero [UNK] tokens: any byte sequence encodes. GPT-2 uses 50,257 tokens (256 bytes + 50,000 merges + 1 special).

Unigram. Start with a huge vocabulary. Assign each token a unigram probability. Iteratively prune tokens whose removal least increases the corpus log-likelihood. Probabilistic at inference: can sample tokenizations (useful for data augmentation via subword regularization). Used by T5, mBART, ALBERT, XLNet, Gemma.

WordPiece. Merge pairs that maximize likelihood of the training corpus rather than raw frequency. Used by BERT, DistilBERT, ELECTRA.

SentencePiece vs tiktoken. SentencePiece is the library that trains vocabularies (BPE or Unigram) directly on raw Unicode text, encoding whitespace as ▁. tiktoken is OpenAI's fast encoder against pre-built vocabularies; it does not train.

Rule of thumb:

  • Training a new vocabulary: SentencePiece (multilingual, no pre-tokenization) or HF Tokenizers.
  • Fast inference against GPT vocab: tiktoken (cl100k_base, o200k_base).
  • Both: HF Tokenizers, one library that handles training and serving.
Check: The Concept0/8
1. What is BPE (Byte-Pair Encoding) as a way to build a vocabulary?Define
2. What is byte-level BPE, and why does it guarantee no [UNK] tokens?Define
3. How does the Unigram tokenizer build its vocabulary?Define
4. What is SentencePiece?Define
5. What is tiktoken?Define
6. Why is Unigram's tokenization useful for data augmentation that a BPE merge list cannot offer?Explain
7. How does WordPiece's merge criterion differ from BPE's?Explain
8. You load BERT's tokenizer and inspect how its vocabulary was built. Which algorithm produced it?Apply

Build It

Step 1: BPE from scratch

See code/main.py. The loop:

python
def train_bpe(corpus, num_merges):
    vocab = {tuple(word) + ("</w>",): count for word, count in corpus.items()}
    merges = []
    for _ in range(num_merges):
        pairs = Counter()
        for symbols, freq in vocab.items():
            for a, b in zip(symbols, symbols[1:]):
                pairs[(a, b)] += freq
        if not pairs:
            break
        best = pairs.most_common(1)[0][0]
        merges.append(best)
        vocab = apply_merge(vocab, best)
    return merges

Three facts the algorithm encodes. </w> marks word end so "low" (suffix) and "lower" (prefix) stay distinct. Frequency weighting makes high-frequency pairs win early. The merge list is ordered, and inference applies merges in that same training order.

Step 2: encode with the learned merges

python
def encode_bpe(word, merges):
    symbols = list(word) + ["</w>"]
    for a, b in merges:
        i = 0
        while i < len(symbols) - 1:
            if symbols[i] == a and symbols[i + 1] == b:
                symbols = symbols[:i] + [a + b] + symbols[i + 2:]
            else:
                i += 1
    return symbols

Naive O(n·|merges|). Production implementations (tiktoken, HF Tokenizers) use merge-rank lookup with priority queues and run in near-linear time.

Step 3: SentencePiece in practice

python
import sentencepiece as spm
 
spm.SentencePieceTrainer.train(
    input="corpus.txt",
    model_prefix="my_tokenizer",
    vocab_size=8000,
    model_type="bpe",          # or "unigram"
    character_coverage=0.9995, # lower for CJK (e.g. 0.9995 for English, 0.995 for Japanese)
    normalization_rule_name="nmt_nfkc",
)
 
sp = spm.SentencePieceProcessor(model_file="my_tokenizer.model")
print(sp.encode("untokenizable", out_type=str))
# ['▁un', 'token', 'izable']

Notice: no pre-tokenization required, space encoded as ▁, character_coverage controls how aggressively rare characters are preserved vs mapped to <unk>.

Step 4: tiktoken for OpenAI-compatible vocabs

python
import tiktoken
enc = tiktoken.get_encoding("o200k_base")
print(enc.encode("untokenizable"))        # [127340, 101028]
print(len(enc.encode("Hello, world!")))   # 4

Encoding-only. Fast (Rust backend). Exact match with GPT-4/5 tokenization for byte-counting, cost estimation, context-window budgeting.

Check: Build It0/10
1. What is a BPE merge list?Define
2. What does the `character_coverage` setting mean when training a SentencePiece tokenizer?Define
3. Why must inference apply the merges in the same order they were learned in training?Explain
4. Why does the BPE training loop append `</w>` to each word before counting pairs?Explain
5. Why does the lesson call `encode_bpe` naive, with cost O(n·|merges|)?Explain
6. Given merges = [("l", "o"), ("lo", "w"), ("low", "</w>")], what does encode_bpe("low", merges) return?Apply
7. With merges = [("o", "w"), ("l", "o"), ("lo", "w")], what does encode_bpe("low", merges) return?Apply
8. In train_bpe, the corpus is {"ab": 3, "cb": 2}. Which pair is merged first, and with what count?Apply
9. SentencePiece encodes "untokenizable" as ['▁un', 'token', 'izable']. What does the ▁ on the first piece show?Apply
10. A developer speeds up encode_bpe by sorting the merge list alphabetically, and tokenizations no longer match training. Which ingredient was broken?Analyze

Pitfalls that still ship in 2026

  • Tokenizer drift. Training on vocab A, deploying against vocab B. Token IDs differ; model outputs garbage. Check the tokenizer.json hash in CI.
  • Whitespace ambiguity. BPE "hello" vs " hello" produce different tokens. Always specify add_special_tokens and add_prefix_space explicitly.
  • Multilingual undertraining. English-heavy corpora produce vocabularies that split non-Latin scripts into 5-10x more tokens. Same prompt costs 5-10x more in Japanese/Arabic on GPT-3.5. o200k_base partially fixed this.
  • Emoji splits. A single emoji can take 5 tokens. Checkpoint emoji handling when budgeting context.
Check: Pitfalls that still ship in 20260/7
1. Why should production CI hash-check the deployed tokenizer.json?Explain
2. Why does emoji handling matter when you budget a model's context window?Explain
3. What is the whitespace ambiguity pitfall in BPE tokenization?Explain
4. Why does the same prompt cost several times more tokens in Japanese or Arabic than in English on an older vocabulary?Explain
5. After a redeploy the model's weights are unchanged, but its outputs are garbage. The new package shipped a different tokenizer.json. Which failure is this and what prevents it?Analyze
6. A product team finds their Japanese and Arabic users pay about 5-10x more per prompt than English users on GPT-3.5. Which diagnosis is right?Analyze
7. Two prompts differ only by a leading space before one word, yet they yield different token IDs and different model behavior. Which assumption is wrong?Analyze

Use It

The 2026 stack:

SituationPick
Training a monolingual model from scratchHF Tokenizers (BPE)
Training a multilingual modelSentencePiece (Unigram, character_coverage=0.9995)
Serving an OpenAI-compatible APItiktoken (o200k_base for GPT-4+)
Domain-specific vocab (code, math, protein)Train custom BPE on domain corpus, merge with base vocab
Edge inference, small modelUnigram (smaller vocabularies work better)

Vocabulary size is a scaling decision, not a constant. Rough heuristic: 32k for <1B params, 50-100k for 1-10B, 200k+ for multilingual/frontier.

Check: Use It0/6
1. Why does the lesson call vocabulary size a scaling decision rather than a constant?Explain
2. You are serving an OpenAI-compatible API and need exact GPT-4-class token counts for cost estimates and context budgeting. What do you use?Apply
3. You are training a multilingual model from scratch. Which pick from the 2026 stack fits?Apply
4. A team is training a 3B-parameter monolingual model. Which vocabulary size matches the lesson's heuristic?Apply
5. You are fine-tuning on protein sequences and the base tokenizer shreds domain strings into long runs of tiny tokens. Which design fits?Design
6. You must ship a small model for edge inference. Which algorithm choice does the lesson favor, and why?Design

Ship It

Save as outputs/skill-bpe-vs-wordpiece.md:

markdown
---
name: tokenizer-picker
description: Pick tokenizer algorithm, vocab size, library for a given corpus and deployment target.
version: 1.0.0
phase: 5
lesson: 19
tags: [nlp, tokenization]
---
 
Given a corpus (size, languages, domain) and deployment target (training from scratch / fine-tuning / API-compatible inference), output:
 
1. Algorithm. BPE, Unigram, or WordPiece. One-sentence reason.
2. Library. SentencePiece, HF Tokenizers, or tiktoken. Reason.
3. Vocab size. Rounded to nearest 1k. Reason tied to model size and language coverage.
4. Coverage settings. `character_coverage`, `byte_fallback`, special-token list.
5. Validation plan. Average tokens-per-word on held-out set, OOV rate, compression ratio, round-trip decode equality.
 
Refuse to train a character-coverage <0.995 tokenizer on corpora with rare-script content. Refuse to ship a vocab without a frozen `tokenizer.json` hash check in CI. Flag any monolingual tokenizer under 16k vocab as likely under-spec.
Check: Ship It0/1
1. A proposed tokenizer config for a corpus with rare-script content sets character_coverage to 0.98. What should the tokenizer-picker skill do?Design
Author's Note

Most people who read this will never train a tokenizer from scratch. You load whatever vocab ships with the base model, Llama's, GPT-4's, Gemma's, and never touch the merge list. That's the right call almost every time.

But here's the failure mode I've watched teams walk into more than once: a fine-tune underperforms on some domain the base tokenizer was never built for (legal contracts, protein sequences, a language with almost no web presence), and everyone assumes it's a data problem. It usually isn't. Run the tokenizer on a sample of your actual domain text before you touch the training loop. If an ordinary word comes back as four or five tokens instead of one, no amount of extra training data fixes that; you're feeding the model a distorted signal before it even starts learning. Five minutes with tokenizer.encode() on real examples would have told them that weeks earlier.

Exercises

  1. Easy. Train a 500-merge BPE on code/main.py's tiny corpus. Encode three held-out words. How many produced exactly 1 token vs >1 token?
  2. Medium. Compare token counts on 100 English Wikipedia sentences between cl100k_base, o200k_base, and a SentencePiece BPE you train with vocab=32k. Report the compression ratio of each.
  3. Hard. Train the same corpus with BPE, Unigram, and WordPiece. Measure downstream accuracy when using each on a small sentiment classifier. Does the choice move the needle by more than 1 point F1?

Key Terms

TermWhat people sayWhat it actually means
BPEByte-Pair EncodingGreedy merge of the most frequent character pairs until the target vocab size is hit.
Byte-level BPENo unknown tokens everBPE over raw 256 bytes; GPT-2 / Llama use this.
UnigramProbabilistic tokenizerPrunes from a large candidate set using log-likelihood; used by T5, Gemma.
SentencePieceThe whitespace oneLibrary that trains BPE/Unigram on raw text; space encoded as ▁.
tiktokenThe fast oneOpenAI's Rust-backed BPE encoder for pre-built vocabs. No training.
Merge listThe magic numbersOrdered list of (a, b) → ab merges; inference applies in order.
Character coverageHow rare is too rare?Fraction of characters in the training corpus the tokenizer must cover, typically around 0.9995.

Further Reading

Code

Ships With