Course contents 03 / 37

03 / 37 · Text Foundations

Tokenization

Why word and character units both fail, how byte-pair encoding learns a subword vocabulary by merging frequent pairs and why that is greedy compression, how a trained tokenizer encodes new text, how WordPiece and SentencePiece differ, and the vocabulary-size trade-off that every later model inherits.

Prerequisites

Text as data

On this page 10

The previous note ended with two problems of word-level units: a word the training corpus never contained can only become [UNK], and most types of a long-tailed vocabulary occur too rarely to learn a vector for each. This note builds the standard answer, subword tokenization, from the two extremes it sits between, with byte-pair encoding as the main algorithm.

Notation. A token is a unit of the vocabulary VV of size ∣V∣|V|; a tokenizer maps a string to tokens and back. A corpus cut into words is summarized by the count c(w)c(w) of each word type ww. A segmentation seg(w)=(t1,…,tm)\mathrm{seg}(w) = (t_1, \dots, t_m) is a sequence of tokens whose concatenation is ww. For units a,ba, b, c(a)c(a) counts occurrences of aa and c(a,b)c(a, b) occurrences of aa immediately followed by bb, both weighted by word counts. The corpus length in tokens is L=∑wc(w) ∣seg(w)∣L = \sum_w c(w)\,|\mathrm{seg}(w)| (written LkL_k after kk merges), and L/∑wc(w)L / \sum_w c(w) is the average number of tokens per word. A model sees a sequence of TT token ids and maps each to a vector of width dd.

Reading route. The core of the note is the baseline, the BPE worked example, encoding new text, what the merge count controls and special tokens. The compression argument, the WordPiece score (a pointwise mutual information) and the unigram model (trained by EM, decoded by dynamic programming) add depth and can be skipped on a first reading.

Baseline: words or characters#

Every later model receives integer ids x1,…,xTx_1, \dots, x_T with xi∈{0,…,∣V∣−1}x_i \in \{0, \dots, |V| - 1\}, so a tokenizer fixes two numbers that pull in opposite directions: ∣V∣|V| and the length TT of a given text.

Words. Sequences are short and each token carries meaning, but the vocabulary is open: every misspelling, name, number and inflected form is a new type, so held-out text always contains words that collapse into [UNK]. Related forms such as sing, sings and singer get unrelated ids, and the stemming of the previous note patches this per language by throwing information away. Inflected languages suffer most: a Russian noun has up to twelve case and number forms.

Characters. The vocabulary is tiny and closed, so there are no unknown tokens within the alphabet. But an English word averages about five letters, so TT grows about fivefold, which every later model pays for in compute (quadratically in self-attention), and a single character carries almost no meaning.

Word-levelCharacter-level
VocabularyOpen, 10510^5–10610^6Closed, 10210^2–10310^3
Unknown tokensFrequentNever (within the alphabet)
Sequence lengthShortAbout 5× longer
Meaning per tokenHighAlmost none
Figure

One phrase, four vocabularies

The same phrase as characters, two BPE vocabularies and wordsFour rows of token boxes for the phrase the decoder learns tokenization. Characters: 28 tokens with a vocabulary of 26. BPE with 100 merges: 13 tokens, vocabulary 126. BPE with 2,000 merges: 7 tokens, vocabulary 2,026; tokenization becomes to, k, en, ization. Words: 4 tokens, vocabulary 2,790, and tokenization becomes the unknown token.Characters|V| = 26 · T = 28 · held-out 4.92 tokens per wordthedecoderlearnstokenizationBPE, 100 merges|V| = 126 · T = 13 · held-out 2.84 tokens per wordthedecoderlearnstokenizationBPE, 2,000 merges|V| = 2,026 · T = 7 · held-out 1.32 tokens per wordthedecoderlearnstokenizationWords|V| = 2,790 · T = 4 · held-out 9.4 % of words [UNK]thedecoderlearns[UNK]The same phrase as characters, two BPE vocabularies and wordsFour rows of token boxes for the phrase the decoder learns tokenization. Characters: 28 tokens with a vocabulary of 26. BPE with 100 merges: 13 tokens, vocabulary 126. BPE with 2,000 merges: 7 tokens, vocabulary 2,026; tokenization becomes to, k, en, ization. Words: 4 tokens, vocabulary 2,790, and tokenization becomes the unknown token.Characters|V| = 26 · T = 28 · held-out 4.92 per wordthedecoderlearnstokenizationBPE, 100 merges|V| = 126 · T = 13 · held-out 2.84 per wordthedecoderlearnstokenizationBPE, 2,000 merges|V| = 2,026 · T = 7 · held-out 1.32 per wordthedecoderlearnstokenizationWords|V| = 2,790 · T = 4 · held-out 9.4 % [UNK]thedecoderlearns[UNK]
The phrase “the decoder learns tokenization” cut into tokens by vocabularies of increasing size, all learned from the same English training text (26,990 running words). Each box is one token; blue boxes are multi-character tokens learned by BPE. T counts the tokens of the phrase, and the held-out figure in each label is the average number of tokens per word on other text from the same source. Characters give long sequences; the word vocabulary gives one token per word but has no entry for “tokenization”, which never occurred in training, and 9.4 % of held-out words share that fate. BPE sits in between: with 2,000 merges the frequent words are whole and the unseen one is spelled from four known pieces.

Byte-pair encoding#

Byte-pair encoding (BPE) began as a compression algorithm that replaces the most frequent pair of adjacent bytes by an unused byte (Gage, 1994); Sennrich, Haddow and Birch (2016) applied the loop to characters inside words.

Training starts from normalized text pre-tokenized into words, so that no token crosses a word boundary; only the word counts matter:

  1. Split every word into characters; this is seg0\mathrm{seg}_0. The initial vocabulary is the alphabet.

  2. At merge step kk, count every adjacent pair on the segmentations segk−1\mathrm{seg}_{k-1}, weighted by word count:

    ck(a,b)=∑wc(w)  #{i:(ti,ti+1)=(a,b) in segk−1(w)}.c_k(a, b) = \sum_w c(w)\; \#\big\{ i : (t_i, t_{i+1}) = (a, b) \text{ in } \mathrm{seg}_{k-1}(w) \big\}.
  3. Pick the most frequent pair (a∗,b∗)=arg⁡max⁡(a,b)ck(a,b)(a^*, b^*) = \arg\max_{(a,b)} c_k(a, b), with a fixed rule for ties.

  4. Add a∗b∗a^*b^* to the vocabulary, record the rule a∗+b∗→a∗b∗a^* + b^* \to a^*b^* as rule kk, and replace every occurrence of the pair from left to right, giving segk\mathrm{seg}_k.

  5. Repeat until the vocabulary reaches the target size or no pair occurs more than once.

The output is a vocabulary and an ordered list of merges; the order is part of the model. Sennrich et al. (2016) also append an end-of-word marker so that spacing can be restored.

Worked example#

Take the word counts sing 6, singer 3, sings 4, ring 5, rings 2, song 3, without the end-of-word marker for readability. That is 23 running words and L0=4⋅6+6⋅3+5⋅4+4⋅5+5⋅2+4⋅3=104L_0 = 4 \cdot 6 + 6 \cdot 3 + 5 \cdot 4 + 4 \cdot 5 + 5 \cdot 2 + 4 \cdot 3 = 104 characters over the alphabet {e,g,i,n,o,r,s}\{e, g, i, n, o, r, s\}, so ∣V∣=7|V| = 7 at the start.

Every word contains n g, so c1(n,g)=6+3+4+5+2+3=23c_1(n, g) = 6 + 3 + 4 + 5 + 2 + 3 = 23. Every word except song contains i n, so c1(i,n)=23−3=20c_1(i, n) = 23 - 3 = 20. The other pairs are rarer: s i 13, r i 7, g s 6, and e r, g e, o n, s o 3 each. The first merge is n + g → ng, shrinking the corpus from 104 to 81 tokens:

Figure

Eight BPE merges on a six-word corpus

BPE training traceA table with one row per merge step from 0 to 8. Columns give the merge rule, its count, the corpus length in tokens and the segmentations of the six word types. The merges are n plus g with count 23; i plus ng with count 20; s plus ing with count 13; r plus ing with count 7; sing plus s with count 4; e plus r with count 3; o plus ng with count 3; s plus ong with count 3. The corpus length falls from 104 to 28.kmergecLsing ×6singer ×3sings ×4ring ×5rings ×2song ×30—104s i n gs i n g e rs i n g sr i n gr i n g ss o n g1n + g → ng2381s i ngs i ng e rs i ng sr i ngr i ng ss o ng2i + ng → ing2061s ings ing e rs ing sr ingr ing ss o ng3s + ing → sing1348singsing e rsing sr ingr ing ss o ng4r + ing → ring741singsing e rsing sringring ss o ng5sing + s → sings437singsing e rsingsringring ss o ng6e + r → er334singsing ersingsringring ss o ng7o + ng → ong331singsing ersingsringring ss ong8s + ong → song328singsing ersingsringring ssongBPE training traceFor each merge step from 0 to 8: the merge rule, its count, the corpus length in tokens, and the segmentations of the six word types in two lines of three. The corpus length falls from 104 to 28.sing ×6singer ×3sings ×4ring ×5rings ×2song ×30—L = 104s i n gs i n g e rs i n g sr i n gr i n g ss o n g1n + g → ngc = 23 · L = 81s i ngs i ng e rs i ng sr i ngr i ng ss o ng2i + ng → ingc = 20 · L = 61s ings ing e rs ing sr ingr ing ss o ng3s + ing → singc = 13 · L = 48singsing e rsing sr ingr ing ss o ng4r + ing → ringc = 7 · L = 41singsing e rsing sringring ss o ng5sing + s → singsc = 4 · L = 37singsing e rsingsringring ss o ng6e + r → erc = 3 · L = 34singsing ersingsringring ss o ng7o + ng → ongc = 3 · L = 31singsing ersingsringring ss ong8s + ong → songc = 3 · L = 28singsing ersingsringring ssong
Corpus: sing ×6, singer ×3, sings ×4, ring ×5, rings ×2, song ×3 (23 running words, 104 characters, alphabet of 7 letters). Each row is one merge k: the pair with the largest frequency-weighted count c, its merged token, the corpus length L in tokens after the merge, and the current segmentation of every word type, with the new token in blue. The vocabulary grows by one per row, from 7 to 15. Amber rows had a tie for the top count (4 pairs at step 6, 3 pairs at step 7, 2 pairs at step 8); the alphabetically first pair is merged. After 8 merges the corpus is 28 tokens for 23 words.

The final vocabulary is e g i n o r s ng ing sing ring sings er ong song, and the corpus is 28 tokens for 23 words: 1.22 tokens per word against 4.52 characters per word. Frequency alone decides: sings (4 occurrences) became one token while rings (2) stayed ring s. Shared pieces appear by themselves (ing, sing er). Ties are common once counts get small; any deterministic tie-break works, and a different one at step 6 would give a different but equally valid vocabulary.

Why the most frequent pair#

Optional depth: why BPE is a greedy compressor of its training corpus. The rest of the note does not depend on it.

Each occurrence of the merged pair turns two tokens into one, so

Lk=Lk−1−ck(a∗,b∗).L_k = L_{k-1} - c_k(a^*, b^*).

The identity needs non-overlapping occurrences, which fails only when a=ba = b: in e e e the pair e e is counted twice but merged once. In the worked example every drop equals the count, 104−23=81104 - 23 = 81, 81−20=6181 - 20 = 61, down to 31−3=2831 - 3 = 28.

Figure

Each merge shortens the corpus by the count of the merged pair

Corpus length after each BPE mergeA bar chart of corpus length against merge step. The bars fall 104, 81, 61, 48, 41, 37, 34, 31, 28. A red cap on each bar shows the tokens removed by that merge, equal to its count. A dashed line at 104 marks characters and a dashed line at 23 marks one token per word. Under each bar the new token and the vocabulary size, which rises from 7 to 15.050100L0104—7−23181ng8−20261ing9−13348sing10−7441ring11−4537sings12−3634er13−3731ong14−3828song15kLnew|V|characters (104)one tokenper word (23)Corpus length after each BPE mergeA bar chart of corpus length against merge step. The bars fall 104, 81, 61, 48, 41, 37, 34, 31, 28. A red cap on each bar shows the tokens removed by that merge, equal to its count. A dashed line at 104 marks characters and a dashed line at 23 marks one token per word. Under each bar the new token and the vocabulary size, which rises from 7 to 15.050100L01047−23181ng8−20261ing9−13348sing10−7441ring11−4537sings12−3634er13−3731ong14−3828song15kLnew|V|10423
Corpus length L in tokens after k merges of the run in the previous figure. The red cap on each bar is what the merge removed: every occurrence of the pair turns two tokens into one, so L falls by exactly the pair count c (23, 20, 13, 7, 4, 3, 3, 3). The first merges remove the most; later ones remove three tokens each. Below the axis: the length L after step k, the token added and the vocabulary size |V|. Dashed lines mark the two extremes, 104 tokens as characters and 23 as one token per word.

So BPE is a greedy compressor: every step adds the token that shortens the training corpus the most. Greedy is not optimal, because an early merge can consume characters that a later, better merge would have needed.

Encoding new text#

A trained BPE tokenizer encodes a new word by replaying the merges in order on its characters. Implementations such as GPT-2’s instead repeatedly merge the adjacent pair with the lowest rule number. The two agree: a merge creates new adjacent pairs only around the new unit abab, and every rule involving abab was learned after abab itself, so the lowest rank present never decreases and the loop meets the rules in increasing order. This is how an unseen word gets meaningful pieces: singers becomes sing er s.

Figure

Encoding words the corpus never contained

Applying learned merges to new wordsLeft: the word singers passes through the 8 merge rules in order and ends as sing, er, s. Right: sneering becomes s, n, e, e, ring when merges are replayed but s, n, e, er, ing under longest match; king becomes [UNK], ing with a character base and k, ing with a byte base; rising becomes r, i, sing.Replaying the merges on “singers”starts i n g e r s1n + g → ngs i ng e r s2i + ng → ings ing e r s3s + ing → singsing e r s4r + ing → ringno match5sing + s → singsno match6e + r → ersing er s7o + ng → ongno match8s + ong → songno matchsingersTwo encoders, one vocabulary: sneeringmerges in ordersneeringlongest matchsneeringA letter outside the base: king7 letters[UNK]ing256 byteskingPieces are not morphemes: risingmerges in orderrisingApplying learned merges to new wordsLeft: the word singers passes through the 8 merge rules in order and ends as sing, er, s. Right: sneering becomes s, n, e, e, ring when merges are replayed but s, n, e, er, ing under longest match; king becomes [UNK], ing with a character base and k, ing with a byte base; rising becomes r, i, sing.Replaying the merges on “singers”starts i n g e r s1n + g → ngs i ng e r s2i + ng → ings ing e r s3s + ing → singsing e r s4r + ing → ringno match5sing + s → singsno match6e + r → ersing er s7o + ng → ongno match8s + ong → songno matchsingersTwo encoders, one vocabulary: sneeringmerges in ordersneeringlongest matchsneeringA letter outside the base: king7 letters[UNK]ing256 byteskingPieces are not morphemes: risingmerges in orderrising
All examples use the 8 merges and the 15-token vocabulary of the worked example. Replay panel: singers is split into characters and the merges are replayed in the order they were learned; rules 1, 2, 3, 6 fire (full ink), the others find no matching pair (muted), and the result is sing · er · s. Two encoders: the same vocabulary used with greedy longest match from the left gives a different segmentation of sneering, because replaying the merges lets rule 4 (r + ing) claim the r before rule 6 (e + r) is reached, while longest match works from the left and takes er first. Letter outside the base: k is not one of the 7 training letters, so a character base must emit [UNK]; a byte base contains every byte value and never does. Last panel: merges follow frequency, not meaning, so rising contains the piece sing.

Three consequences:

  • The encoder belongs to the vocabulary. Greedy longest match over the same 15 entries splits sneering as s n e er ing; replaying the merges gives s n e e ring.
  • Unknown symbols remain possible. A character outside the base alphabet still becomes [UNK]. Byte-level BPE (Radford et al., 2019) removes the problem by taking the 256 UTF-8 byte values as the base: GPT-2’s 50,257 entries are 256 bytes, 50,000 merges and one end-of-text token.
  • Pieces are frequent strings, not morphemes. They often match suffixes (ing, er, s) because suffixes are frequent, but rising becomes r i sing.

What the merge count controls#

The number of merges is the main hyperparameter: ∣V∣=∣alphabet∣+#merges+#special tokens|V| = |\text{alphabet}| + \#\text{merges} + \#\text{special tokens}, and more merges mean shorter sequences. Both sides have a price:

  • The embedding table E∈R∣V∣×dE \in \mathbb R^{|V| \times d} has ∣V∣⋅d|V| \cdot d parameters: 38.4 M for ∣V∣=50,000|V| = 50{,}000 and d=768d = 768, and 263 M for Llama 3.2 1B (128,256 tokens, d=2048d = 2048).
  • The output layer computes ∣V∣|V| logits per position, O(∣V∣⋅d)O(|V| \cdot d) work per token, with another d×∣V∣d \times |V| matrix unless it is tied to EE.
  • Late-merge entries are rare, so their embeddings see little data.
  • Longer sequences mean TT sequential steps in a recurrent model, O(T2d)O(T^2 d) per self-attention layer, and fewer words in a fixed context window.
Figure

Tokens per word fall as the vocabulary grows

Tokens per word against vocabulary sizeTwo falling curves on a log-scaled vocabulary axis from 20 to 4,000. Training text, blue: 4.995 at 26, 4.679 at 29, 4.193 at 36, 3.823 at 46, 3.584 at 56, 3.269 at 76, 2.832 at 126, 2.38 at 226, 2.119 at 326, 1.807 at 526, 1.614 at 726, 1.43 at 1026, 1.261 at 1526, 1.168 at 2026. Held-out text, red: 4.924 at 26, 4.627 at 29, 4.176 at 36, 3.801 at 46, 3.589 at 56, 3.268 at 76, 2.839 at 126, 2.43 at 226, 2.172 at 326, 1.904 at 526, 1.737 at 726, 1.567 at 1026, 1.414 at 1526, 1.318 at 2026. The red curve lies above the blue one from about 200 entries on. A single point at vocabulary 2790 and one token per word is labelled with 9.4 percent unknown words.12345301003001,0003,000vocabulary size |V| (log scale)tokens / wordwords: 9.4 % [UNK]training text: 1.17 at |V| = 2,026held-out text: 1.32characters 5.00Tokens per word against vocabulary sizeTwo falling curves on a log-scaled vocabulary axis from 20 to 4,000. Training text, blue: 4.995 at 26, 4.679 at 29, 4.193 at 36, 3.823 at 46, 3.584 at 56, 3.269 at 76, 2.832 at 126, 2.38 at 226, 2.119 at 326, 1.807 at 526, 1.614 at 726, 1.43 at 1026, 1.261 at 1526, 1.168 at 2026. Held-out text, red: 4.924 at 26, 4.627 at 29, 4.176 at 36, 3.801 at 46, 3.589 at 56, 3.268 at 76, 2.839 at 126, 2.43 at 226, 2.172 at 326, 1.904 at 526, 1.737 at 726, 1.567 at 1026, 1.414 at 1526, 1.318 at 2026. The red curve lies above the blue one from about 200 entries on. A single point at vocabulary 2790 and one token per word is labelled with 9.4 percent unknown words.12345301003001,0003,000vocabulary size |V| (log scale)tokens / wordwords: 9.4 % [UNK]training text: 1.17held-out text: 1.32characters 5.00
BPE trained on the prose of 11 Generative Models notes of this site (lowercased letters only, 26,990 running words, 2,790 word types) and evaluated on the other four notes (8,908 words). The horizontal axis is the vocabulary size |V| = 26 letters + merges, on a log scale; the vertical axis is the average number of tokens per word. On the training text (blue) the curve falls from 5.00 at the character level to 1.17 after 2,000 merges; on held-out text (red) it reaches 1.32, because the late merges build whole words that are frequent in training and absent elsewhere. Each doubling of |V| buys less. The single point at one token per word is a word vocabulary of the training types: exactly one token per word, but 9.4 % of held-out words are not in it. Our own small measurement, an illustration rather than a benchmark.

Each doubling of ∣V∣|V| buys less. Practical vocabularies are much larger than in this small experiment: 30,522 entries for BERT, 50,257 for GPT-2, 100,000–250,000 for recent multilingual models.

WordPiece and SentencePiece#

WordPiece (Schuster and Nakajima, 2012; Wu et al., 2016) is the tokenizer of BERT (Devlin et al., 2019). It runs the BPE merge loop with two differences.

The first is the merge criterion. The original adds the unit that most increases the training likelihood under a language model of units; a widely used reconstruction (Hugging Face, LLM Course, ch. 6) scores a pair by

score(a,b)=c(a,b)c(a) c(b).\text{score}(a, b) = \frac{c(a, b)}{c(a)\,c(b)} .

With NN units in total and relative-count estimates p(a,b)=c(a,b)/Np(a, b) = c(a, b)/N, p(a)=c(a)/Np(a) = c(a)/N,

score(a,b)=1N⋅p(a,b)p(a) p(b).\text{score}(a, b) = \frac{1}{N} \cdot \frac{p(a, b)}{p(a)\,p(b)} .

Up to 1/N1/N, this is the ratio whose logarithm is the pointwise mutual information of aa and bb. BPE prefers frequent pairs; WordPiece prefers cohesive ones, whose parts rarely occur apart. BERT marks every non-initial piece with ##. On the worked-example corpus the two criteria disagree from the first step:

Figure

BPE and WordPiece choose different first merges

Pair rankings by count and by WordPiece scoreTwo ranked lists of the same 9 pairs with bars. By count: ##n ##g 23, ##i ##n 20, s ##i 13, r ##i 7, ##g ##s 6, ##e ##r 3, ##g ##e 3, ##o ##n 3, s ##o 3. By score: ##e ##r 0.333, s ##o 0.063, r ##i 0.050, ##n ##g 0.043, ##i ##n 0.043, ##g ##s 0.043, ##g ##e 0.043, ##o ##n 0.043, s ##i 0.041. Lines connect each pair's two positions; the count leader drops to rank 4 by score and the score leader has the lowest count, 3.BPEcount c(a, b)WordPiecec(a, b) / (c(a) c(b))##n ##g23##i ##n20s ##i13r ##i7##g ##s6##e ##r3##g ##e3##o ##n3s ##o3##e ##r0.333s ##o0.063r ##i0.050##n ##g0.043##i ##n0.043##g ##s0.043##g ##e0.043##o ##n0.043s ##i0.041Pair rankings by count and by WordPiece scoreTwo ranked lists of the same 9 pairs with bars. By count: ##n ##g 23, ##i ##n 20, s ##i 13, r ##i 7, ##g ##s 6, ##e ##r 3, ##g ##e 3, ##o ##n 3, s ##o 3. By score: ##e ##r 0.333, s ##o 0.063, r ##i 0.050, ##n ##g 0.043, ##i ##n 0.043, ##g ##s 0.043, ##g ##e 0.043, ##o ##n 0.043, s ##i 0.041. Lines connect each pair's two positions; the count leader drops to rank 4 by score and the score leader has the lowest count, 3.BPEcount c(a, b)WordPiecec(a, b) / (c(a) c(b))##n ##g23##i ##n20s ##i13r ##i7##g ##s6##e ##r3##g ##e3##o ##n3s ##o3##e ##r0.333s ##o0.063r ##i0.050##n ##g0.043##i ##n0.043##g ##s0.043##g ##e0.043##o ##n0.043s ##i0.041
All adjacent pairs of the toy corpus before the first merge, written WordPiece style: a character that does not start a word carries ##, so s at the start and ##s inside a word are different units. Left: ranked by the frequency-weighted count c(a, b), which BPE maximizes; ##n ##g wins with 23. Right: ranked by the reconstructed WordPiece score c(a, b) / (c(a) c(b)); ##e ##r wins with 3 / (3 · 3) = 0.333, because ##e and ##r never occur apart, while ##n ##g scores 23 / (23 · 23) = 0.043. Lines join the same pair in both rankings; pairs with equal scores are listed by count.

The tokenizers library’s WordPiece trainer ignores this score and merges by frequency like BPE.

The second difference is encoding. WordPiece takes the longest vocabulary entry that matches the start of the word, then the longest ## match of the remainder, and so on; if some position has no matching piece, the whole word becomes [UNK].

from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("bert-base-uncased")   # |V| = 30,522
tok("Random text to encode").tokens()
# ['[CLS]', 'random', 'text', 'to', 'en', '##code', '[SEP]']

The cased BERT tokenizer similarly splits stationery into station + ##ery and 10,925 into 10, ,, 92, ##5.

SentencePiece (Kudo and Richardson, 2018) removes pre-tokenization. It trains on raw text in which the space is an ordinary symbol, ▁, so it needs no word splitter, works for scripts without spaces such as Chinese, and detokenizes exactly up to normalization. Besides BPE, SentencePiece supports the unigram language model (Kudo, 2018).

The unigram model works in the opposite direction: it prunes a large candidate vocabulary. Each token tt has a probability p(t)p(t), and a segmentation is scored as if its tokens were independent:

P(t1,…,tm)=∏i=1mp(ti).P(t_1, \dots, t_m) = \prod_{i=1}^{m} p(t_i).

Training alternates EM re-estimation of pp with removing the tokens whose loss lowers the corpus likelihood least, always keeping single characters. Encoding picks the most probable segmentation by dynamic programming (the Viterbi algorithm): the best score of the prefix w1:jw_{1:j} is the maximum over i<ji < j with wi+1:j∈Vw_{i+1:j} \in V of the best score of w1:iw_{1:i} times p(wi+1:j)p(w_{i+1:j}). Taking −log⁡-\log turns the product into a sum, so this is the cheapest path through a lattice whose arcs are vocabulary entries costing −log⁡p(t)-\log p(t). With toy probabilities on singer:

Figure

The most probable segmentation is a shortest path

Viterbi lattice for the word singerSeven nodes for positions 0 to 6 of singer. Arcs with costs: s from 0 to 1, 3.2; sin from 0 to 3, 5.5; sing from 0 to 4, 3.5; singe from 0 to 5, 6.2; i from 1 to 2, 3.5; in from 1 to 3, 3.9; ing from 1 to 4, 3.2; n from 2 to 3, 3.2; ng from 2 to 4, 3.9; g from 3 to 4, 3.5; ge from 3 to 5, 5.3; ger from 3 to 6, 5.8; e from 4 to 5, 3.0; er from 4 to 6, 3.0; r from 5 to 6, 3.2. Best cost at each node: 0: 0.0, 1: 3.2, 2: 6.7, 3: 5.5, 4: 3.5, 5: 6.2, 6: 6.5. The best path, highlighted, is sing · er with cost 6.5; greedy longest match gives singe · r with cost 9.4.sin5.5sing3.5singe6.2in3.9ing3.2ng3.9ge5.3ger5.8er3.0s3.2i3.5n3.2g3.5e3.0r3.20123456best cost0.03.26.75.53.56.26.5viasisinsingsingeerViterbi lattice for the word singerSeven nodes for positions 0 to 6 of singer. Arcs with costs: s from 0 to 1, 3.2; sin from 0 to 3, 5.5; sing from 0 to 4, 3.5; singe from 0 to 5, 6.2; i from 1 to 2, 3.5; in from 1 to 3, 3.9; ing from 1 to 4, 3.2; n from 2 to 3, 3.2; ng from 2 to 4, 3.9; g from 3 to 4, 3.5; ge from 3 to 5, 5.3; ger from 3 to 6, 5.8; e from 4 to 5, 3.0; er from 4 to 6, 3.0; r from 5 to 6, 3.2. Best cost at each node: 0: 0.0, 1: 3.2, 2: 6.7, 3: 5.5, 4: 3.5, 5: 6.2, 6: 6.5. The best path, highlighted, is sing · er with cost 6.5; greedy longest match gives singe · r with cost 9.4.sin5.5sing3.5singe6.2in3.9ing3.2ng3.9ge5.3ger5.8er3.0s3.2i3.5n3.2g3.5e3.0r3.20123456best cost to reach each node0.03.26.75.53.56.26.5
The word singer as a lattice: node j is the position after j characters, and every substring that is in the vocabulary is an arc from its start to its end, labelled with the token and its cost −ln p(t). The probabilities are toy values for 15 tokens. Single characters run along the baseline, longer tokens arch above it, higher for longer tokens. A segmentation is a path from node 0 to node 6, and its cost is −ln P, so the most probable segmentation is the cheapest path; there are 19 paths here. Under each node is the best cost of reaching it, computed left to right as the minimum over incoming arcs of (best cost at the arc's start + arc cost). The blue path sing · er costs 6.5. Greedy longest match takes singe · r at cost 9.4: node 5 is reached most cheaply through singe, but that prefix does not extend to the cheapest full path.

The best path, sing er, is not the greedy longest match singe r: Viterbi keeps one best cost per position and never commits to a prefix early, at O(n⋅ℓ)O(n \cdot \ell) work for nn characters and tokens of at most ℓ\ell characters.

Because the unigram model scores every segmentation, it can sample them during training, a data augmentation called subword regularization. BPE-dropout (Provilkov, Emelianenko and Voita, 2020) gets the same effect from BPE by skipping each merge with probability qq during training only.

The three algorithms side by side (Hugging Face, Tokenization algorithms):

BPEWordPieceUnigram (SentencePiece)
TrainingMerge most frequent pairMerge pair with highest c(a,b)/(c(a)c(b))c(a,b)/(c(a)c(b)) (reconstruction); frequency in HF tokenizersPrune candidates by likelihood loss
EncodingReplay merges in orderGreedy longest matchMost likely segmentation (Viterbi)
Word boundary markEnd-of-word marker, or Ġ for a leading space (byte-level)## on non-initial pieces (BERT)▁ for a leading space
Used byGPT-2 (byte-level), Llama, Qwen2BERT, DistilBERT, ELECTRAT5, BigBird, Pegasus

Special tokens, padding and masks#

A tokenizer is a pipeline in which only the model stage is learned, and its output is more than a list of ids:

Figure

From a string to a batch of ids

Tokenizer pipelineSix stages from top to bottom. Raw text Singers SING; lowercased singers sing; split into the words singers, sing; the learned merges turn them into sing er s | sing; post-processing gives [CLS] sing er s sing [SEP] with ids 2 13 16 10 13 3; a batch adds [CLS] song s [SEP] [PAD] [PAD] with ids 2 18 10 3 0 0 and attention masks 1 1 1 1 1 1 and 1 1 1 1 0 0.raw textSingers SINGnormalizelowercasesingers singpre-tokenizesplit on spacessingerssingmodel8 learned mergessingerssingpost-processspecial tokens, ids[CLS]2sing13er16s10sing13[SEP]3idsbatchpadding, mask[CLS]21sing131er161s101sing131[SEP]31[CLS]21song181s101[SEP]31[PAD]00[PAD]00idsidsmaskmaskTokenizer pipelineSix stages from top to bottom. Raw text Singers SING; lowercased singers sing; split into the words singers, sing; the learned merges turn them into sing er s | sing; post-processing gives [CLS] sing er s sing [SEP] with ids 2 13 16 10 13 3; a batch adds [CLS] song s [SEP] [PAD] [PAD] with ids 2 18 10 3 0 0 and attention masks 1 1 1 1 1 1 and 1 1 1 1 0 0.raw textSingers SINGnormalizelowercasesingers singpre-tokenizesplit on spacessingerssingmodel8 learned mergessingerssingpost-processspecial tokens, ids[CLS]2sing13er16s10sing13[SEP]3idsbatchpadding, mask[CLS]21sing131er161s101sing131[SEP]31[CLS]21song181s101[SEP]31[PAD]00[PAD]00idsidsmaskmask
The tokenizer of the worked example inside a full pipeline. Only the model stage is learned; normalization (here lowercasing) and pre-tokenization (here splitting on spaces) are fixed rules applied before it, and post-processing adds the boundary tokens [CLS] and [SEP] and maps every token to its row in the embedding table. Ids: [PAD] = 0, [UNK] = 1, [CLS] = 2, [SEP] = 3, then the 15 tokens of the vocabulary in order (e = 4, g = 5, …, song = 18). In a batch the shorter sequence (songs) is padded with [PAD] = 0 and the attention mask marks real tokens with 1. Decoding runs the stages backwards only partly: lowercasing cannot be undone, and joining the pieces needs a word-boundary mark (## in BERT's WordPiece, Ġ or ▁ for a leading space) that this toy tokenizer does not have.
  • [CLS] / <s> marks the start of a sequence and [SEP] / </s> its end or a segment boundary (BERT note). GPT-2’s single <|endoftext|> separates training documents and ends a generation (GPT note).
  • [UNK] replaces a word that the vocabulary cannot spell. The vocabularies released with Google’s BERT checkpoints keep their special tokens at fixed ids: [UNK] 100, [CLS] 101, [SEP] 102, [MASK] 103. The uncased multilingual BERT tokenizer, with 105,879 entries, turns We are very happy to show you the 🤗 Transformers library. into 14 ids that start with 101 and end with 102; the emoji has no piece in the vocabulary and becomes 100. The model sees that something stood there, not what.
  • token_type_ids give the segment of each token for the [CLS] A [SEP] B [SEP] input of the BERT note: 0 for the first segment, 1 for the second, all 0 for a single text.
  • [PAD] fills short sequences up to the longest in the batch, a tensor of shape [batch, max_len]; an attention mask of the same shape holds 1 for real tokens and 0 for padding, which later layers ignore.
  • truncation=True, max_length=T_max cuts longer sequences: Tmax⁡=32T_{\max} = 32 in the translation note, 128 for AG News in the Transformer note.
tok(["First text to encode", "Second text"], padding=True,
    max_length=3, truncation=True,
    return_token_type_ids=False, return_tensors="pt")
# input_ids:      tensor([[101, 2034, 102], [101, 2117, 102]])
# attention_mask: tensor([[1, 1, 1],        [1, 1, 1]])

Only the first word survives, because the limit counts the special tokens ([CLS] is id 101, [SEP] 102). A tokenizer also records which word each token came from; named entity recognition uses this to map one label per word to the tokens of that word.

What changed#

Against the word-level pipeline of the previous note:

Word-level pipelineSubword tokenizer
Vocabulary sizeProperty of the corpusHyperparameter
Unseen words[UNK]Split into known pieces; never [UNK] with bytes
MorphologyLost to stemming or kept as separate typesShared pieces (sing, sing er, sings)
Language dependenceStopwords, stemmers per languageTrained from data; byte-level needs no alphabet
Sequence length1 token per word1–2 per English word at common sizes; more for other languages

The n-gram note trains n-gram models on word and subword tokens of a Russian corpus and reports per-token perplexities of about 40,000 and 2,700 for the unigram models (about 30,000 and 3,900 for bigrams). These are not comparable: a subword text has more tokens, each easier to predict. Normalize the total log-likelihood by a shared unit (words, characters or bytes) instead. With NtokN_{\text{tok}} tokens and NwordN_{\text{word}} words in the test text,

PPLword=exp⁡(1Nword∑i=1Ntok−log⁡p(xi∣x<i))=PPLtok Ntok/Nword.\mathrm{PPL}_{\text{word}} = \exp\Big(\frac{1}{N_{\text{word}}}\sum_{i=1}^{N_{\text{tok}}} -\log p(x_i \mid x_{<i})\Big) = \mathrm{PPL}_{\text{tok}}^{\,N_{\text{tok}}/N_{\text{word}}}.

A toy example: a per-token perplexity of 20 at 1.5 tokens per word is a per-word perplexity of 201.5≈89.420^{1.5} \approx 89.4.

The conversion can reverse the ranking. One joke of the corpus has 11 words and 16 subword pieces besides punctuation; at that ratio a per-token 2,700 is a per-word 2,70016/11≈98,0002{,}700^{16/11} \approx 98{,}000, against about 40,000 for the word model. This is approximate (the reported perplexities are means of per-joke perplexities, and only the subword tokens keep punctuation), so these numbers do not settle which tokenizer is better.

Limitations#

  • Greedy and frequency-driven. A one-letter change can change the whole segmentation, and in byte-level schemes a word gets different tokens with and without a leading space. The unigram model matches or outperforms BPE in pretraining experiments (Bostrom and Durrett, 2020); Mielke et al. (2021) survey the alternatives.
  • Language and script bias. Languages rare in the tokenizer’s training corpus are cut into more pieces, so the same content costs more compute and context, and models do worse on it (Rust et al., 2021; Petrov et al., 2023). Byte-level BPE starts this bias before any merge: token is 5 UTF-8 bytes and токен is 10.
  • Numbers and characters. Digits are split by frequency, not place value (10 , 92 ##5), and the letters inside a multi-character token are invisible to the model, so arithmetic, spelling and counting characters are harder than they look.
  • Tokenizer and model are coupled. A model understands only its own tokenizer’s ids. Added tokens get untrained embedding rows, like the [PAD] token the fine-tuning note adds to GPT-2.
  • Normalization is baked in. bert-base-uncased cannot distinguish US from us.

What you should remember#

  • A tokenizer trades vocabulary size ∣V∣|V| (embedding and softmax cost, data per token) against sequence length TT (compute and context).
  • BPE repeatedly merges the most frequent adjacent pair and encodes by replaying the merges in order. Each merge shortens the training corpus by the pair count, so BPE is greedy compression.
  • WordPiece encodes by greedy longest match with ##; the unigram model picks the most probable segmentation, a shortest path found by Viterbi.
  • Byte-level BPE never produces [UNK]. SentencePiece drops pre-tokenization by treating the space as the symbol ▁.
  • Compare language models across tokenizers per word, character or byte, never per token.
  • Special tokens, padding and the attention mask are part of the tokenizer’s contract with the model.

Notebook#

Four notebooks of the NLP course cover this note. Cell numbers count every cell, markdown included, from 1. Quoted numbers come from the run saved with the course; cells changed since then for current library versions have no saved output.

  • week03_lm.ipynb tokenizes a corpus of short Russian jokes with the bpe package (cells 7–11). Despite its name, the package learns no merges: it keeps the 2,498 most frequent words whole, takes the 47,500 most frequent character n-grams of length 1–6 as subwords, and encodes other words by greedy longest match, as WordPiece does. Rewriting its __sow/__eow markers as ## shortens one joke from 27 to 19 tokens (cells 12–16): 16 subword pieces for its 11 words plus three punctuation marks, the ratio used for the per-word conversion in What changed. Cells 42–43 train the n-gram models compared there; the id vocabulary of the corpus has 37,580 entries (cell 49).
  • week04_lstm_attention_transformer.ipynb, cells 13–35, loads pretrained WordPiece tokenizers for English (bert-base-uncased, 30,522 entries) and German (31,102) and shows boundary tokens, masks, padding and truncation (en + ##code in cell 21, the max_length=3 batch in cell 25). Length histograms fix the maximum length at 32.
  • week05_transformer.ipynb, cells 17–27, trains a 4,000-entry WordPiece tokenizer on AG News (frequency-based merges, longest-match encoding, special tokens at ids 0–4) and saves batches of shape [256, 128]. Cell 29 takes PAD_ID from pad_token_id, the id of [PAD]: 0, because [PAD] is listed first among the special tokens. The first version of the notebook read pad_token_type_id, the segment id for padding, which is also 0 here, so the mistake had no effect.
  • week07_nlp_tasks.ipynb, cells 11–16: bert-base-cased turns a 35-word CoNLL-2003 sentence into 44 tokens, and word_ids() maps each token back to its word.

The LLM notebook of the HSE-LAMBDA Deep Generative Models course, Transformers-LangChain.ipynb, has the [UNK] example (same cell-number convention, outputs saved). Cell 26 encodes the sentence with the nlptown/bert-base-multilingual-uncased-sentiment tokenizer and prints input_ids [101, 11312, 10320, 12495, 19308, 10114, 11391, 10855, 10103, 100, 58263, 13299, 119, 102], all-zero token_type_ids and an all-one attention_mask; the tenth id, 100, is the emoji. Cell 28 encodes two sentences as one batch with padding=True, truncation=True, max_length=512 and return_tensors="pt".

Next#

Tokens are now integers from a fixed vocabulary. Before building models we need to say what “a model works” means: which data it may be trained and tuned on, which numbers summarize its errors, and which pitfalls make a good number meaningless. The next note sets those rules once, so that every later comparison is fair.

References#