← All tutorials

How Subword Tokenizers Are Built: BPE, WordPiece and Unigram

On this page

1 Byte Pair Encoding merges the most frequent pair 2 Byte-level BPE starts from 256 bytes 3 WordPiece divides the pair count by the counts of its parts 4 Unigram starts big and prunes 5 SentencePiece runs BPE or Unigram on raw text 6 Training your own vocabulary 7 References
grow by mergingshrink by pruningBPESection 1merges the top pairbyte-level BPESection 2BPE on 256 bytesGPT-2, GPT-4o, Llama 3WordPieceSection 3merges by a scoreBERTUnigramSection 4prunes a big listT5SentencePieceSection 5, a toolkitruns BPE or Unigramon raw textLlama 2, Gemma, T5
Figure 1. Merging algorithms on the left, pruning on the right, the SentencePiece toolkit running either.

How does a tokenizer get its vocabulary? Byte Pair Encoding (BPE), byte-level BPE, WordPiece and Unigram each build a fixed list of pieces once, from a large training text called a corpus, and then split every new text with it. SentencePiece is the toolkit that runs BPE or Unigram on raw text. They differ in how the pieces are chosen: BPE and WordPiece grow the list by merging (byte-level BPE is BPE run on bytes), and Unigram shrinks a big list by pruning.

A token is one piece of text, its token id is its position in the vocabulary, V is the number of vocabulary entries and T the number of tokens in one input, as in Tokenizers. Turning text into ids is encoding; turning ids back into text is decoding. A bigger V holds longer pieces, so the same text needs a smaller T (how big the vocabulary should be). A word-level vocabulary maps every word it lacks to one unknown token, <unk>. UTF-8 writes every character as 1 to 4 bytes, and a byte has only 256 values.

Each algorithm runs once, on the corpus, before the model is trained, and saves a fixed file (when a tokenizer runs). Encoding replays that file on every text and keeps no state between calls; only optional sampling (Section 4.3), which exists for both Unigram and BPE, changes the split from call to call, and only while the model trains.

1 Byte Pair Encoding merges the most frequent pair

How do you build a vocabulary from the bottom up? Start from single characters, merge the adjacent pair that appears most often into one new token, and repeat a fixed number of times. Sennrich, Haddow and Birch (2016) brought BPE to tokenization, taking the idea from a 1994 compression method by Gage that replaces the most frequent pair of bytes with an unused byte.

1.1 Counting pairs and merging

BPE first counts how often each word appears, so a corpus becomes a short list of words with their frequencies. The BPE paper also ends each word with a special end-of-word symbol, which this page’s toy leaves out.

Count every adjacent pair, merge the most frequent one, repeat.

c(a, b) = Σw f(w) · nw(a, b)
nw(a, b) counts how often symbol a sits right before symbol b inside word w. Multiplying by the word’s frequency f(w) and adding over all words, Σw, gives the pair count c(a, b) in the whole corpus without scanning repeated words again.
(a*, b*) = argmax(a, b) c(a, b)
argmax returns the pair itself, not its count. The winner becomes one new symbol a*b*, every adjacent a* b* in the corpus is replaced by it, the merge is added to the end of the merge list, and the counts are taken again. In this page’s toy, a tie goes to the pair found first.
V = V0 + M
Each merge adds one entry, so the final size is the starting alphabet V0 plus the number of merges M, plus any special tokens, reserved entries such as an end-of-text marker that never come from text. M is the only hyperparameter in the BPE paper. More merges give longer tokens and a shorter T; M = 0 leaves single characters. The original GPT (2018) has 478 base tokens plus 40,000 merges, 40,478 entries (Reference 10).
toy corpuspair countshug×10pug×5pun×12bun×4hugs×5u g20p u17u n16h u15g s5b u41after merge 1hug×10pug×5pun×12bun×4hugs×52ug is one token now
Figure 2. Toy corpus. u g is the most frequent pair (1), so ug becomes one token everywhere (2). Data from Reference 10.
start

The line of the card in Section 1.1 that this step runs.

c(a, b) = Σw f(w) · nw(a, b)
(a*, b*) = argmax(a, b) c(a, b)
Every word starts as single characters. Each blue bar is one pair’s count c(a, b) over the whole corpus.
The orange bar is the pair this line picks. Each merge adds one entry to the vocabulary.
The toy’s merge count M is 6, so it stops here even though b un could still merge. Each merge added one entry, so the vocabulary has 13.

Figure 3. Toy corpus. Each step merges the pair with the tallest bar, marked next, into one token.

Code 1 is the equation card in Section 1.1 as code. It learns the first three merges of the toy corpus.

from collections import Counter

def merge(symbols, pair):
    out, i = [], 0
    while i < len(symbols):
        if symbols[i:i + 2] == pair:
            out.append(symbols[i] + symbols[i + 1])
            i += 2
        else:
            out.append(symbols[i])
            i += 1
    return tuple(out)

def train_bpe(word_freqs, num_merges):
    words = {tuple(w): f for w, f in word_freqs.items()}
    merges = []
    for _ in range(num_merges):
        pairs = Counter()
        for symbols, f in words.items():
            for pair in zip(symbols, symbols[1:]):
                pairs[pair] += f
        best = max(pairs, key=pairs.get)
        merges.append(best)
        words = {merge(symbols, best): f for symbols, f in words.items()}
    return merges, words

corpus = {"hug": 10, "pug": 5, "pun": 12, "bun": 4, "hugs": 5}
merges, words = train_bpe(corpus, 3)
print(merges)  # [('u', 'g'), ('u', 'n'), ('h', 'ug')]
print(words)   # {('hug',): 10, ('p', 'ug'): 5, ('p', 'un'): 12, ('b', 'un'): 4, ('hug', 's'): 5}
  1. 1merge replaces every adjacent copy of the pair with one symbol.
  2. 2Line 1 of the equation card counts each word’s pairs once, weighted by its frequency f.
  3. 3Line 2, where max takes the most frequent pair, the first found on a tie.
  4. 4Line 3, one merge per pass, with the corpus rewritten by it.
  5. 5The three merges are steps 1 to 3 of Figure 3.

1.2 Splitting new text with the merge list

To split a word, start from its characters and replay the merges in the order they were learned. A character never seen in training, such as a rare script or an emoji, has no starting symbol, so character BPE still needs <unk>.

Code 2 splits two words that are not in the toy corpus, using the merges from Code 1. The second word starts with m, a letter the corpus never had.

def encode(word, merges, alphabet):
    symbols = tuple(c if c in alphabet else "<unk>" for c in word)
    for pair in merges:
        symbols = merge(symbols, pair)
    return symbols

alphabet = set("".join(corpus))  # h, u, g, p, n, b, s
print(encode("bugs", merges, alphabet), encode("mug", merges, alphabet))  # ('b', 'ug', 's') ('<unk>', 'ug')
  1. 1A new word starts as single characters; a character outside the training alphabet becomes <unk>.
  2. 2The merges are replayed in learned order, never counted again.
  3. 3bugs splits into known pieces; m was never seen, so mug keeps only ug.

2 Byte-level BPE starts from 256 bytes

GPT-2 (Radford et al., 2019) runs BPE on UTF-8 bytes instead of characters. The starting alphabet is exactly 256 symbols, against more than 130,000 Unicode characters, so every string can be encoded and no <unk> exists. GPT-2 has 256 bytes plus 50,000 merges plus one end-of-text token, a special token that marks where a document ends, 50,257 entries (Reference 10). The cost is that a rare character can be cut into byte pieces that are not text on their own, the hatched cells of the one string, many vocabulary sizes tool, and scripts other than English need more tokens for the same text (other languages cost more tokens).

OpenAI’s tiktoken encodings, such as cl100k_base and o200k_base, are byte-level BPE vocabularies; the list of what current models use shows which model uses which.

Before merging, a regular expression cuts the text into chunks of one character category, such as letters, digits or punctuation, and keeps a single space on the front of the next chunk. Merges never cross a chunk border. Without this, the GPT-2 paper reports, BPE learned many copies of one word with punctuation attached, such as dog., dog! and dog?.

dog. dog! dog?dog.␣dog!␣dog?1I'll pay 8302080 nowI'll␣pay␣8302080␣now␣ is a space
Figure 4. GPT-2’s real pattern (Reference 11) cuts text into chunks before any merge, and a space stays on the word after it (1).

3 WordPiece divides the pair count by the counts of its parts

WordPiece merges bottom up like BPE but picks a different pair. Schuster and Nakajima (2012) introduced it; Google’s translation system (Wu et al., 2016) and BERT (Devlin et al., 2019, 30,000 tokens) used it. The idea is to merge pieces that belong together, not just pieces that are common. Pieces inside a word carry the prefix ##. Google never published its training code, so the training here is Hugging Face’s reconstruction (Reference 12).

The pair count divided by the counts of both parts.

s(a, b) = c(a, b)c(a) · c(b)
c(a) is how often the single piece a appears. Dividing asks whether the pair shows up together more than its parts’ own counts would predict. A pair of very common pieces scores low even with a high count, and a pair whose second piece almost never appears elsewhere scores high. WordPiece merges the pair with the highest score s.
BPE picks the most frequent pairu g20p u17u n16h u15g s5b u41WordPiece picks the highest score##g ##s0.050##u ##g0.028##u ##n0.028h ##u0.028p ##u0.028b ##u0.0282
Figure 5. Same toy corpus. BPE merges u g (1); WordPiece merges ##g ##s, because s only ever appears after g (2). Data from References 10, 12.

To split new text, WordPiece does not replay merges. It takes the longest vocabulary entry that matches the start of the word, then the longest ## entry for the rest, and so on. If nothing matches, the whole word becomes BERT’s unknown token, [UNK]. bert-base-uncased splits antidisestablishmentarianism into anti ##dis ##est ##ab ##lish ##ment ##arian ##ism.

4 Unigram starts big and prunes

Unigram (Kudo, 2018) works the other way round. It starts from a large list of candidate pieces, every character plus the corpus’s most frequent substrings, gives each a probability, and removes the pieces that help least until the target size is left.

4.1 Splitting with piece probabilities

One word can be split many ways, and each piece has a probability.

toypiece probabilitieshug0.10ug0.08h0.05u0.05s0.05g0.04no other piece fits inside hugshugs0.10 × 0.05 = 0.005hugs0.05 × 0.08 × 0.05 = 0.0002hugs0.05 × 0.05 × 0.04 × 0.05 = 0.0000051highest, kept
Figure 6. Toy. A split’s probability is the product of its pieces’ probabilities, and the tokenizer keeps the highest (1).

Score a split by multiplying its piece probabilities, then keep the best split.

P(x) = ∏i=1n p(xi)
A split x of a word cuts it into n pieces x1 to xn. ∏ multiplies their probabilities p(xi). Every extra piece multiplies in a number below 1, so splits into fewer, likelier pieces win.
x* = argmaxx ∈ S(X) P(x)
S(X) is the set of all splits of the word X. The tokenizer outputs the split x* with the highest probability. The Viterbi algorithm finds it without listing every split: it walks the word left to right and keeps only the best way to reach each position.

4.2 Training by pruning

Training repeats one loop until V pieces are left: fit the piece probabilities to the corpus, then drop the pieces the corpus misses least. The seed list is found with a suffix array, a sorted index of every substring; SentencePiece keeps 1,000,000 of them by default, seed_sentencepiece_size (Reference 13).

seed listevery character plus themost frequent substrings1E stepweigh every split by P(x)and add up each piece’s uses2M stepeach piece’s probability isits share of those uses3prunedrop the pieces whose removalcosts least likelihood; keepthe top 80% and everysingle character4repeatstop when V pieces are left
Figure 7. The seed list (1) is fitted by the E step (2) and M step (3), then pruned (4); steps 2 to 4 repeat.

Fit the probabilities to the corpus, then drop the pieces the corpus misses least.

L = Σs log ( Σx ∈ S(X(s)) P(x) )
X(s) is sentence s of the corpus. For each sentence, add up the probability of every way to split it, take the log, and sum over sentences. Training picks the piece probabilities that make L largest, by expectation maximization (EM): the E step weighs every split of each sentence by its probability and adds up how often each piece is expected to be used (2 in Figure 7), and the M step sets each piece’s probability to its share of those expected uses (3).
Δi = L − L−i
L−i is L without piece i, so Δi is the likelihood the corpus loses without it. Each round keeps the share η of pieces with the largest Δi and always keeps single characters (4 in Figure 7).
The Unigram paper’s example keeps η = 80% each round, and SentencePiece’s shrinking_factor defaults to 0.75 (References 4, 13). A higher η prunes gently over more rounds; a lower one prunes hard in few rounds.

The weakness is training cost. Each round runs EM over the whole corpus and then recomputes the likelihood L without each piece to get its loss Δi, where BPE only counts adjacent pairs.

4.3 Sampling a different split each time

While the model trains, the Unigram paper samples a different split of each word every time instead of always the best. The model sees varied splits and becomes more robust; the paper calls this subword regularization. Sampling is for training only: when the trained model is used, the tokenizer returns the single best split x*.

Each split’s chance of being drawn, sharpened by α.

P(x | X) = P(x)αΣx′ ∈ S(X) P(x′)α
Raise each split’s probability to the power α and divide by the total over all splits, so the chances add to 1. At α = 0 every power is 1 and all splits are equally likely. As α grows the best split takes over, and a large α almost always returns the Viterbi split. The paper sets no default and its experiments use 0.1 to 0.5; the SentencePiece README uses 0.1.
evenbest split

Figure 8. Toy. The three splits of hugs from Figure 6; bar length is the chance of being drawn.

BPE can sample too. BPE-dropout (Provilkov, Emelianenko and Voita, 2020) keeps the merge list but skips each possible merge with a probability p while splitting a word, and uses plain BPE once training is done. Raising p gives more and shorter pieces: p = 0 is plain BPE and p = 1 leaves single characters; the paper uses p = 0.1 for most languages, and SentencePiece’s alpha sets this p for a BPE model. SentencePiece runs both through the same enable_sampling option (References 15, 16).

5 SentencePiece runs BPE or Unigram on raw text

SentencePiece (Kudo and Richardson, 2018) is a toolkit, not a new algorithm: it trains and runs BPE or Unigram, Unigram by default. It does not cut text on spaces first. The space is an ordinary symbol, ▁, so decoding the tokens gives back the text exactly, spaces included, after one cleanup step described next. This matters for languages written without spaces.

In training and every time it encodes, it rewrites look-alike characters into one standard form. The default rule, nmt_nfkc, builds on Unicode NFKC, the standard rule that turns a full-width A into A and the ligature fi into fi (Reference 13). This is SentencePiece’s weakness: decoding gives back the rewritten text, so A comes back as A, and without byte fallback a character outside the vocabulary becomes <unk>.

Two switches change how text is split. Byte fallback writes a character missing from the vocabulary as its UTF-8 bytes instead of <unk>, and digit splitting makes every digit its own token. Llama 2 and Gemma turn both on, and Gemma also keeps runs of spaces instead of collapsing them (References 8, 9).

character_coverage is the share of the corpus’s characters the vocabulary must cover; rarer characters fall to <unk> or to bytes. The default 0.9995 suits large character sets such as Chinese and Japanese, and 1.0 suits small alphabets such as English (Reference 13).

best split▁This▁is▁a▁test2844711415400ids1sampled split, α = 0.1▁This▁is▁a▁test2
Figure 9. SentencePiece keeps each space as ▁ (1), so the spaces can be rebuilt exactly; the numbers under the best split are its ids, and sampling varies the split (2). Data from Reference 14.

6 Training your own vocabulary

You train a vocabulary once, on text that looks like what the model will read, and every later step depends on it. The official code lives in Hugging Face tokenizers and SentencePiece.

Training reads the whole corpus into memory by default; for a large corpus, input_sentence_size trains on a random sample of that many sentences instead (Reference 13). Unigram is the pick when you want each sampled split drawn by its probability (Section 4.3); byte-level BPE is the pick when no text may ever be unknown.

Code 3 trains a small Unigram vocabulary on corpus.txt, any plain text file with one sentence per line, then splits a string twice.

import sentencepiece as spm

spm.SentencePieceTrainer.train(
    input="corpus.txt",
    model_prefix="tok",
    vocab_size=8000,
    model_type="unigram",
    character_coverage=0.9995,
    byte_fallback=True,
    split_digits=True,
)
sp = spm.SentencePieceProcessor(model_file="tok.model")
print(sp.encode("This is a test", return_type=str))
print(sp.encode("This is a test", return_type=str,
                enable_sampling=True, alpha=0.1, nbest_size=-1))
  1. 1The final size and the algorithm of Section 4.
  2. 2The Llama 2 and Gemma choices of Section 5.
  3. 3The best split, line 2 of the equation card in Section 4.1.
  4. 4A sampled split with α = 0.1; set Figure 8 to 0.1 to see its draw shares.
SettingRaise or onLower or offDefault
vocab_size (V), vocabulary sizeshorter T, bigger table; very large keeps whole wordslonger T, smaller table; just above the character count leaves single characters8000 [13]; models 30K to about 262K
character_coverage, Section 5keeps rarer characters; 1.0 keeps allmore characters fall to <unk> or bytes0.9995 [13]
byte_fallback, Section 5a missing character becomes UTF-8 bytesit becomes <unk>off [13]; on in Llama 2, Gemma [8, 9]
split_digits, how numbers splitone token per digitlearned digit runs stayoff [13]; on in Llama 2, Gemma [8, 9]
shrinking_factor (η), Section 4.2prunes gently over more rounds; near 1 barely prunesprunes hard in few rounds0.75 [13]; paper 0.8 [4]
alpha (α), Section 4.3Unigram: closer to the best split; BPE: alpha is the dropout probability, so more piecesUnigram: closer to even, 0 makes all splits equal; BPE: fewer skipped mergesnone; 0.1 in the README [14]
Figure 10. The SentencePiece settings this page uses, what raising or lowering each one does, and its default.

The model’s last layer gives one score for each of the V entries at every step. How does it turn those scores into the next id? Softmax, Temperature and Decoding answers that; there, decoding means choosing the next token, and the tokenizer then turns the chosen ids back into text.

7 References

  1. Sennrich, R., Haddow, B., Birch, A. (2016). Neural Machine Translation of Rare Words with Subword Units. ACL 2016. arXiv:1508.07909.
  2. Gage, P. (1994). A New Algorithm for Data Compression. The C Users Journal. Cited via Reference 1.
  3. Schuster, M., Nakajima, K. (2012). Japanese and Korean Voice Search. ICASSP 2012. Wu, Y., et al. (2016). Google’s Neural Machine Translation System. arXiv:1609.08144. Schuster and Nakajima cited via Reference 12.
  4. Kudo, T. (2018). Subword Regularization. ACL 2018. arXiv:1804.10959.
  5. Kudo, T., Richardson, J. (2018). SentencePiece. EMNLP 2018 demo. arXiv:1808.06226.
  6. Radford, A., et al. (2019). Language Models are Unsupervised Multitask Learners. OpenAI.
  7. Devlin, J., et al. (2019). BERT. NAACL 2019. arXiv:1810.04805.
  8. Touvron, H., et al. (2023). Llama 2. arXiv:2307.09288.
  9. Gemma Team (2024). Gemma. arXiv:2403.08295.
  10. Hugging Face. Summary of the tokenizers.
  11. OpenAI. tiktoken, openai_public.py and model.py.
  12. Hugging Face LLM Course 6.6. WordPiece tokenization.
  13. SentencePiece. Training options.
  14. SentencePiece. Python README.
  15. Provilkov, I., Emelianenko, D., Voita, E. (2020). BPE-Dropout: Simple and Effective Subword Regularization. ACL 2020. arXiv:1910.13267.
  16. SentencePiece. README, Subword Regularization and BPE-Dropout; sentencepiece_processor.h, SampleEncode, where alpha is the dropout probability for BPE.