Tokenization: BPE, WordPiece, SentencePiece
1978 words
10 min read
Visual companion
Python
Type and operator map
Python Week 1: the first filter for runtime behavior
View
Revision summary
What this note is really saying
Short form
# Tokenization: BPE, WordPiece, SentencePiece ## 🎯 Learning Objectives - Explain why subword tokenization is necessary for LLMs - Implement Byte Pair Encoding (BPE) - Understand WordPiece and SentencePiece algorithms - Compare different tokenization approaches and their trade-offs ## 📋 Prerequisites - Basic NLP pr...

Tokenization: BPE, WordPiece, SentencePiece
🎯 Learning Objectives
- Explain why subword tokenization is necessary for LLMs
- Implement Byte Pair Encoding (BPE)
- Understand WordPiece and SentencePiece algorithms
- Compare different tokenization approaches and their trade-offs
📋 Prerequisites
- Basic NLP preprocessing concepts
- Character encoding (UTF-8)
1. 📖 Core Content
1.1 The Tokenization Problem
The problem: Language models need a fixed vocabulary size, but natural language has an open vocabulary (new words, typos, rare words, morphology).
Three approaches:
| Approach | Example | Pros | Cons |
|---|---|---|---|
| Word-level | ["the", "cat", "sat"] | Intuitive, preserves meaning | Huge vocab, can't handle OOV |
| Character-level | ["t", "h", "e", " "] | Tiny vocab, no OOV | Long sequences, loses morphology |
| Subword | ["the", "cat", "sat"] | Balanced vocab, handles OOV | Complex algorithm |
Subword tokenization is the sweet spot: it splits rare words into smaller pieces while keeping common words as units.
1.2 Byte Pair Encoding (BPE)
Intuition
BPE is a data compression algorithm adapted for tokenization. It iteratively merges the most frequent pair of adjacent tokens:
- Start with characters as tokens
- Count all adjacent pairs
- Merge the most frequent pair
- Repeat until desired vocabulary size
Worked Example
Corpus: "low lower lowest low low lower"
Initialization: Each character is a token Vocabulary: {l, o, w, e, r, s, t}
Corpus with spaces: l o w _ l o w e r _ l o w e s t _ l o w _ l o w _ l o w e r
Step 1: Count pairs
- "lo": 8 occurrences
- "ow": 8 occurrences
- "we": 3 occurrences
- "er": 2 occurrences
- "es": 1 occurrence
- "st": 1 occurrence Merge "lo" → "lo" Corpus: lo w _ lo w e r _ lo w e s t _ lo w _ lo w _ lo w e r Vocabulary: {l, o, w, e, r, s, t, lo} Step 2: Count pairs
- "ow": 8 occurrences
- "lo" + "w" → "low": 8 occurrences
- "we": 3 occurrences
- "er": 2 occurrences Merge "ow" → "ow" Corpus: lo ow _ lo ow e r _ lo ow e s t _ lo ow _ lo ow _ lo ow e r Better: Merge "lo"+"w" → "low" 🠖 actually let's merge "low": After merging "lo", the most frequent pair is "ow" (8). But "low" appears 8 times too. Let's say we merge "low": Corpus: low _ low e r _ low e s t _ low _ low _ low e r Vocabulary: {l, o, w, e, r, s, t, low} Step 3: Count pairs
- "l"+"o" doesn't exist anymore (it's "low")
- "we": 3 occurrences ("low"+"e", but "e" is a separate token)
- Actually "low" + " " + "low" → "low" is now a single token Let me simplify with a clearer example.
python# runnable from collections import Counter import re def bpe_train(corpus, vocab_size): """ Train BPE tokenizer Args: corpus: List of strings vocab_size: Target vocabulary size Returns: vocab: Set of tokens merges: List of merges performed """ # Initialize vocabulary with characters vocab = set() for text in corpus: vocab.update(list(text)) # Add special tokens special_tokens = ['<unk>', '<s>', '</s>'] vocab.update(special_tokens) # Split corpus into characters with word boundaries words = [list(word) + ['</w>'] for word in corpus[0].split()] merges = [] while len(vocab) < vocab_size: # Count adjacent pairs pairs = Counter() for word in words: for i in range(len(word) - 1): pairs[(word[i], word[i+1])] += 1 if not pairs: break # Find most frequent pair best_pair = pairs.most_common(1)[0][0] merges.append(best_pair) # Merge the pair new_words = [] for word in words: new_word = [] i = 0 while i < len(word): if i < len(word) - 1 and (word[i], word[i+1]) == best_pair: new_word.append(word[i] + word[i+1]) i += 2 else: new_word.append(word[i]) i += 1 new_words.append(new_word) words = new_words # Add new token to vocab vocab.add(best_pair[0] + best_pair[1]) return vocab, merges corpus = ["low lowest lower low low lower"] vocab, merges = bpe_train(corpus, 20) print(f"Vocabulary: {sorted(vocab)}") print(f"Merges: {merges}")
1.3 BPE in GPT Models
GPT-2 uses BPE at the byte level (Byte-Level BPE). This means:
- Vocabulary size: 50,257
- Base tokens are bytes (256 values), not characters
- Can encode any Unicode string
- No [UNK] token needed (everything is representable)
1.4 WordPiece
Used in: BERT, DistilBERT
WordPiece is similar to BPE but differs in the merge criterion. Instead of frequency, it merges pairs that maximize the likelihood of the training data.
Merge criterion: Choose the pair that reduces perplexity the most:
This measures how much more likely the pair is together than by chance.
| Aspect | BPE | WordPiece |
|---|---|---|
| Merge criterion | Frequency | Likelihood improvement |
| Training speed | Faster | Slower (needs model evaluation) |
| Used in | GPT, LLaMA | BERT |
| Special tokens | Added separately | [CLS], [SEP], [MASK] |
1.5 SentencePiece
Used in: T5, LLaMA, ALBERT
SentencePiece is a language-independent tokenizer that treats the input as a raw byte stream (no pre-tokenization needed).
Key features:
- No language-specific preprocessing: Doesn't rely on spaces (works for Chinese, Japanese)
- Two training algorithms: Can use BPE or Unigram LM
- Directly models spaces: Space is a regular token
Unigram LM Tokenization
The Unigram approach:
- Start with a large vocabulary (all possible substrings up to some length)
- Compute the likelihood of the corpus under a unigram model
- Iteratively remove tokens that least reduce likelihood
- Stop at target vocabulary size Loss function:
Where S(Xi) is the set of all possible segmentations of Xi.
1.6 Tokenization Comparison
| Feature | BPE | WordPiece | SentencePiece (Unigram) |
|---|---|---|---|
| Base unit | Characters | Characters | Raw bytes |
| Merge criterion | Frequency | Likelihood gain | Likelihood loss |
| Pre-tokenization | Required | Required | Not required |
| Language support | Space-separated | Space-separated | All languages |
| Deterministic | Yes | Yes | No (uses Viterbi) |
| Vocabulary size | 50K-100K | 30K-50K | 32K-100K |
| Special tokens | Manually added | Manually added | Can be learned |
1.7 Why This Matters
Tokenization is often overlooked but critically affects model behavior:
- Vocabulary coverage: Larger vocab = fewer tokens per word = efficient processing
- OOV handling: Subword tokenization eliminates unknown words
- Language bias: Space-based tokenizers underperform on Chinese/Japanese
- Token efficiency: Some languages need 2× more tokens for the same meaning
- Adversarial robustness: Token boundaries affect which inputs are "adversarial"
6. 📝 Practice Questions
Q1: For the corpus "aa ab aa ab ba", show the first two BPE merges.Initial tokens: [a, a, _, a, b, _, a, a, _, a, b, _, b, a]Step 1: Count pairs "aa": 2 (positions 0-1, 6-7) "a_": 2 (positions 1-2, 7-8) "a": 2 (positions 2-3, 8-9) "ab": 2 (positions 3-4, 9-10) "b": 2 (positions 4-5, 10-11) "_b": 1 "ba": 1Ties broken arbitrarily. Let's merge "aa" first: Corpus: aa _ ab _ aa _ ab _ baStep 2: Count pairs again "aa_": 2 "a": 2 "ab": 2 "b": 2 "_b": 1 "ba": 1Merge "ab" (or any other pair with count 2): Corpus: aa _ ab _ aa _ ab _ ba Q2<strong>Q2</strong>: Why does SentencePiece not need pre-tokenization?SentencePiece treats the raw input as a sequence of bytes or Unicode characters. It doesn't assume word boundaries (spaces). Instead, it includes the space character as a regular token (often denoted as "_" or "▁"). This means the tokenizer can learn subword units across word boundaries, and it works for languages like Chinese, Japanese, or Thai that don't use spaces.During decoding, spaces are simply concatenated like any other token. Q3: GPT-2 has 50,257 vocabulary. If each token embedding is d_model=768, what is the size of the embedding matrix?Embedding matrix: 50,257 × 768 = 38,597,376 parameters ≈ 38.6MThis is the embedding layer (token embeddings). GPT-2 also has positional embeddings (1024 × 768 = 0.79M). Combined, the embedding layers have ~39.4M parameters out of GPT-2 Small's total 124M (~32% of all parameters).Large vocabularies significantly increase model size through the embedding layer. Q4: A word "internationalization" takes 20 characters. With BPE tokens ["inter", "national", "ization"], how many tokens does it become?It becomes 3 tokens: ["inter", "national", "ization"] instead of 20 characters or a single [UNK] token.This is ~7× more efficient than character-level (3 vs 20) and avoids the OOV problem of word-level tokenization. Q5: What happens if BPE vocabulary is too small (e.g., 1000 tokens)?With 1000 tokens:
- Most words are split into many subword pieces ("unbelievable" → ["un", "bel", "iev", "ab", "le"])
- Sequence lengths increase → computational cost increases
- Each subword carries less semantic meaning
- The model must learn compositionality of subword pieces
- Potential for "tokenization artifacts" where unfortunate splits create confusing pieces
Typical BERT-style vocabularies are 30K-50K, balancing coverage and efficiency. Q6<strong>Q6</strong>: How does BPE handle the word "don't" when apostrophes are rare in the training corpus?"don't" might be tokenized as:
- ["don", "'", "t"] if apostrophe is infrequent
- ["do", "n't"] if "n't" is a common merge
- ["don't"] if the word appears frequently enough
The exact tokenization depends on the frequency of "don't" and its components in the training corpus. Common contractions like "n't" often become their own tokens because they appear frequently. Q7<strong>Q7</strong>: Why might a model tokenize "hello" and "Hello" differently?Case-sensitive tokenizers treat "h" and "H" as different characters. If the corpus has more lowercase "hello" than capitalized "Hello", the tokenizer might:
- Merge "he" + "llo" → "hello" as a single token (frequent)
- Keep "H" + "ello" as separate tokens (less frequent)
This means "hello" is 1 token and "Hello" is 2 tokens, which affects the model's understanding and generation. Some models use lowercasing or case-preserving tokenization to handle this. Q8<strong>Q8</strong>: In SentencePiece Unigram, how is the optimal segmentation of a sequence determined?SentencePiece uses the Viterbi algorithm to find the most likely segmentation. Given the unigram probabilities of each subword token, Viterbi finds:segmentation(X)=argmaxx1,...,xk∏i=1kP(xi)This is a classic dynamic programming problem — the probability of segmenting up to position i is:P(i)=maxj<iP(j)⋅P(token(j,i))Viterbi efficiently finds the optimal segmentation in O(n × max_token_length). Q9<strong>Q9</strong>: A BPE tokenizer trained primarily on English text is used for German. What issues might arise?Potential issues:
- Compound words: German has long compounds ("Donaudampfschifffahrtsgesellschaftskapitän") that might be split into many pieces
- Different character distributions: German has "ä", "ö", "ü", "ß" which may be rare in English training data, causing splits
- Morphology differences: German has more inflectional morphology, requiring more subwords per word
- Efficiency drop: German text might need 30-50% more tokens than English for the same meaning
- Suboptimal merges: Merges that are useful for English may not be optimal for German
Solutions: Joint training on multilingual data, or language-specific tokenizers. Q10<strong>Q10</strong>: BPE vocabulary is typically 50K-100K tokens. If we double it to 200K, what trade-offs occur?Pros:
- Fewer tokens per sequence (common words remain whole)
- Lower effective sequence length → faster processing
- Less subword composition needed
- Better coverage of rare words
Cons:
- Larger embedding matrix → more parameters (200K × 768 = 154M params)
- Less parameter sharing across related words ("run" and "running" might be separate)
- Potential overfitting to training corpus vocabulary
- Slower final linear layer (vocabulary × d_model)
- Memory: logits computation becomes more expensive
The optimal vocabulary size balances token efficiency against model capacity.
7. 🔗 Cross-References
- Previous: BERT Architecture
- Next: Fine-tuning Methods (Week 7)
- Video: BSDA5004 Week 6 transcripts Join Discord PreviousBERT ArchitectureNextFine-tuning & PEFT