WordPiece: BERT's Tokenizer¶
Overview¶
WordPiece builds a subword vocabulary by greedily merging pairs that most improve the likelihood of the training data (a language-model score), and marks word continuations with ##. It was created for Google's voice search (Schuster & Nakajima, 2012) and became famous as BERT's tokenizer β also used by DistilBERT, ELECTRA, and many multilingual models.
- Paper: "Japanese and Korean Voice Search" (Schuster & Nakajima, 2012); popularized by BERT (Devlin et al., 2018)
- Adoption: BERT, DistilBERT, ELECTRA, ALBERT, many multilingual BERTs
- Trademark:
##continuation prefix β e.g. "tokenization" β["token", "##ization"] - vs BPE: same bottom-up merging idea, different merge criterion (likelihood gain vs raw frequency)
- Prerequisites: 00 Tokenization Fundamentals, 01 Bpe (Byte Pair Encoding)
How WordPiece Is Trained¶
The merge criterion: likelihood gain, not frequency¶
BPE picks the pair with the HIGHEST FREQUENCY:
pair score = count(pair)
WordPiece picks the pair that most INCREASES data likelihood:
pair score = freq(xy) / (freq(x) Β· freq(y))
Intuition:
- freq(xy): how often x and y actually appear together
- freq(x)Β·freq(y): how often they would appear together BY CHANCE
- ratio > 1 β xy is a real collocation worth merging
- ratio β€ 1 β merging adds nothing (x,y co-occur by luck)
Example:
pair "th" (freq 10M) vs pair "qk" (freq 10)
BPE: "th" merges first (10M >> 10)
WordPiece: compares against chance:
freq(th)/(freq(t)Β·freq(h)) β moderate gain
freq(qk)/(freq(q)Β·freq(k)) β HUGE gain (rare together
beyond chance β a real unit)
β WordPiece prioritizes *linguistically meaningful* units,
not merely frequent ones. This is its main quality advantage.
The training loop¶
1. Start: vocabulary = all single characters (+ special tokens)
2. Repeat until target vocab size:
a. compute pair scores: gain(xy) = freq(xy) / (freq(x)Β·freq(y))
b. merge the highest-gain pair into a new token
3. Done: vocab contains characters + merged units
Note: the exact gain formula varies slightly between papers
(Wu et al. 2016 use freq(xy)/(freq(x)Β·freq(y)); BERT's original
implementation used a likelihood-based approximation). The idea
is the same: merge the pair that buys the most likelihood.
How WordPiece Encodes Text¶
Greedy longest-match (left to right)¶
Algorithm:
word = "tokenization"
1. Find the LONGEST prefix of the word that is in the vocab
β "token" is in vocab (longer "tokeniz" is not) β take "token"
2. Mark the rest as a continuation, repeat on the remainder
β remainder "ization"
3. Find the longest prefix of the remainder in the vocab
β "##ization" is in vocab β take it
4. Output: ["token", "##ization"]
Key rules:
- the FIRST piece has no prefix; continuations are prefixed with ##
- greedy: always take the longest vocab match available
- if a character isn't in the vocab at all β [UNK]
Worked examples (BERT uncased vocab)¶
"playing" β ["playing"] (whole word in vocab)
"tokenization" β ["token", "##ization"] (## = continuation)
"unhappiness" β ["un", "##happi", "##ness"]
"ChatGPT" β ["chat", "##gp", "##t"] (cased: "Chat", "##GP", "##T")
"unaffable" β ["un", "##aff", "##able"]
Notice the pattern:
- common roots (un-, -ness, -able) are kept whole
- the first piece is the word "head", the rest carry ##
Why ## instead of Δ /β (vs BPE/SentencePiece)¶
BPE/SentencePiece: space is a PREFIX β "Δ world", "βworld"
WordPiece: continuation is a SUFFIX β "##world"
Both solve the same problem β keeping word boundaries β from
opposite sides. WordPiece's design makes the FIRST token of a word
look like a normal token and pushes the boundary signal to the end
(a design choice that suits BERT's full-sentence training).
WordPiece vs BPE vs SentencePiece (Comparison)¶
Aspect BPE (GPT) WordPiece (BERT) SentencePiece (LLaMA)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Merge criterion pair frequency likelihood gain BPE or Unigram mode
Word boundary Δ / β prefix ## suffix β prefix
Pre-tokenization required required none (raw text)
Encoding greedy merges greedy longest-match Viterbi (unigram)
Languages English-centric English-centric language-agnostic
Special tokens <|endoftext|> [CLS][SEP][MASK] <s> </s> <unk>
Typical vocab 50K 30K 32Kβ128K
Models GPT-2/3/4 BERT, ELECTRA LLaMA, Mistral
Rule of thumb:
- English-only understanding tasks (BERT-style): WordPiece is proven
- Generation / multilingual: SentencePiece or byte-level BPE
BERT's Special Token Setup¶
BERT reserves the first IDs of its vocab for structural tokens:
BERT vocab (30,522 tokens) starts with:
0: [PAD] 1: [UNK] 2: [CLS] 3: [SEP] 4: [MASK]
Usage:
[CLS] prepended to every input β its output = sentence summary
(used for classification)
[SEP] separates sentences/segments:
"[CLS] my dog is cute [SEP] he likes playing [SEP]"
[PAD] pads batches to equal length (masked in attention)
[MASK] the token BERT predicts during masked-language-model training
[UNK] unknown characters (rare, thanks to 30K vocab + coverage)
BERT also keeps "unused" placeholder tokens:
[unused1] ... [unused99] β reserved for future fine-tuning use
(you can repurpose them for custom special tokens WITHOUT
resizing the embedding layer!)
π More on special tokens: Special Tokens.
WordPiece in Practice (Hugging Face)¶
from transformers import AutoTokenizer
# BERT's tokenizer IS WordPiece
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
# Tokenize a sentence
text = "tokenization is playing with unhappiness"
tokens = tokenizer.tokenize(text)
print(tokens)
# ['token', '##ization', 'is', 'playing', 'with', 'un', '##happi', '##ness']
# Full encode β IDs (adds [CLS] and [SEP])
ids = tokenizer.encode(text)
print(ids)
# [101, 19204, 3721, 2003, 2692, 2007, 2439, 28352, 3813, 102]
# β[CLS] β[SEP]
# Decode back β identical text (tokenization is lossless)
print(tokenizer.decode(ids))
# "[CLS] tokenization is playing with unhappiness [SEP]"
Training a custom WordPiece tokenizer¶
from tokenizers import Tokenizer, models, pre_tokenizers, trainers
tokenizer = Tokenizer(models.WordPiece(unk_token="[UNK]"))
tokenizer.pre_tokenizer = pre_tokenizers.Whitespace()
trainer = trainers.WordPieceTrainer(
vocab_size=30000,
special_tokens=["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"],
)
tokenizer.train(files=["corpus.txt"], trainer=trainer)
encoded = tokenizer.encode("tokenization")
print(encoded.tokens) # ['token', '##ization']
Limitations & Gotchas¶
β Requires pre-tokenization (space-splitting)
β weak for Chinese/Japanese/Thai (no spaces)
β multilingual WordPiece models pad the vocab to 100K+ and
still lose efficiency β see [05 Multilingual Tokenization](/02-llm-modeling/00-fundamentals/00-tokenization/05-multilingual-tokenization/)
β Greedy longest-match encoding is fast but NOT optimal:
- a rare word may tokenize badly if a long-but-rare prefix
is in the vocab ("un" + "##affable" when "##unaffable" exists)
- Viterbi/Unigram decoding (SentencePiece) can do better
β Fixed special-token design is BERT-specific:
- [CLS]/[SEP]/[MASK] don't transfer to generation models
- modern LLMs (LLaMA, GPT) use different token designs
β
Still a great choice for:
- classification/NLU tasks (BERT heritage, huge ecosystem)
- English + European languages
- tasks needing [CLS]-style sentence pooling
Key Takeaways¶
π Merge by likelihood gain, not raw frequency β the WordPiece signature
Continuation marker: the first piece is the head, the rest carry ##¶
π§ Greedy longest-match encoding: fast, good enough in practice
π BERT legacy: 30K vocab + [CLS]/[SEP]/[MASK] still powers NLU stacks
π Weak for no-space languages: pre-tokenization assumption limits it
π Pick by task: BERT-style NLU β WordPiece; generation/multilingual β SentencePiece or byte-level BPE
Related Notes¶
- 00 Tokenization Fundamentals β core concepts + special tokens
- 01 Bpe (Byte Pair Encoding) β the frequency-based alternative (GPT)
- 02 Sentencepiece β the language-agnostic alternative (LLaMA)
- 05 Multilingual Tokenization β why WordPiece struggles without spaces
- 03 Token Efficiency & Compression β measuring how well a vocab fits your data
- 04 Tokenization Best Practices β choosing among BPE/WordPiece/SentencePiece