WordPiece › Code Implementation
Language AI HandbookAll chapters WordPiece › Code ImplementationOne section · Sections in this chapter8 / 13 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Let us build this from the ground up. We will implement WordPiece training from scratch to understand the mechanics, then use HuggingFace's tokenizers library to train a production-quality tokenizer.
First, install the necessary packages:
import subprocess
subprocess.run(
["uv", "pip", "install", "tokenizers", "transformers"], capture_output=True
)Implementing WordPiece Training from Scratch
We will implement a minimal WordPiece trainer to show exactly how the merge loop works. This is not optimized for large corpora but is faithful to the algorithm. The implementation has four key functions: one to count word frequencies, one to convert words to their initial character-level representation, one to score all candidate pairs using the PMI formula, and one to perform a single merge step.
from collections import defaultdict
def get_word_freqs(corpus):
"""Count word frequencies in corpus."""
word_freqs = defaultdict(int)
for sentence in corpus:
for word in sentence.lower().split():
# Strip basic punctuation for simplicity
word = word.strip(".,!?;:'\"()")
if word:
word_freqs[word] += 1
return word_freqs
def word_to_chars(word):
"""Convert word to initial character-level representation with ## prefix."""
chars = list(word)
return [chars[0]] + ["##" + c for c in chars[1:]]
def compute_pair_scores(splits, word_freqs):
"""Compute PMI-based scores for all adjacent token pairs."""
token_freqs = defaultdict(int)
pair_freqs = defaultdict(int)
for word, freq in word_freqs.items():
split = splits[word]
for i, token in enumerate(split):
token_freqs[token] += freq
if i < len(split) - 1:
pair = (split[i], split[i + 1])
pair_freqs[pair] += freq
total = sum(token_freqs.values())
scores = {}
for pair, pair_count in pair_freqs.items():
a, b = pair
p_a = token_freqs[a] / total
p_b = token_freqs[b] / total
p_ab = pair_count / total
# WordPiece score = P(AB) / (P(A) * P(B))
scores[pair] = p_ab / (p_a * p_b)
return scores
def merge_pair(pair, splits, word_freqs):
"""Merge a token pair across all words in the corpus."""
a, b = pair
merged = (
a + b[2:] if b.startswith("##") else a + b
) # Remove ## from b when merging
new_splits = {}
for word, freq in word_freqs.items():
split = splits[word]
new_split = []
i = 0
while i < len(split):
if i < len(split) - 1 and split[i] == a and split[i + 1] == b:
new_split.append(merged)
i += 2
else:
new_split.append(split[i])
i += 1
new_splits[word] = new_split
return new_splits
def train_wordpiece(corpus, vocab_size=50):
"""Train a WordPiece vocabulary from scratch."""
word_freqs = get_word_freqs(corpus)
# Initialize splits: each word -> character sequence with ## prefix
splits = {word: word_to_chars(word) for word in word_freqs}
# Build initial vocabulary from all characters
vocab = set()
for word in word_freqs:
for token in splits[word]:
vocab.add(token)
while len(vocab) < vocab_size:
scores = compute_pair_scores(splits, word_freqs)
if not scores:
break
# Find the pair with the highest PMI score
best_pair = max(scores, key=lambda p: scores[p])
# Merge the pair
a, b = best_pair
new_token = a + b[2:] if b.startswith("##") else a + b
splits = merge_pair(best_pair, splits, word_freqs)
vocab.add(new_token)
return vocab, splits, word_freqsNow train on a small sample corpus:
sample_corpus = [
"playing basketball is fun",
"she is playing piano",
"players play together",
"the player played the game",
"unplayed music is rare",
"replay the game again",
"basketball players are athletic",
"play the replay again",
]
vocab, splits, word_freqs = train_wordpiece(sample_corpus, vocab_size=60)Vocabulary size: 60 Sample tokens (first 30): ##a, ##b, ##c, ##d, ##e, ##g, ##h, ##i, ##ic, ##in, ##ing, ##k, ##l, ##m, ##n, ##no, ##o, ##p, ##pl, ##r, ##rs, ##s, ##sk, ##t, ##tb, ##th, ##tic, ##u, ##y, ##ying Morphological pieces (## prefix tokens): ##a, ##b, ##c, ##d, ##e, ##g, ##h, ##i, ##ic, ##in, ##ing, ##k, ##l, ##m, ##n, ##no, ##o, ##p, ##pl, ##r, ##rs, ##s, ##sk, ##t, ##tb, ##th, ##tic, ##u, ##y, ##ying
The vocabulary reveals the PMI-based selection: continuation tokens that represent common morphological suffixes appear early because they are strongly associated with specific preceding tokens. The ## prefix tokens show how character sequences have been merged into meaningful units.
Greedy Tokenization with the Learned Vocabulary
Let us implement the greedy longest-match-first tokenizer. The key insight is that this tokenizer does not need to know the history of how the vocabulary was built: it only needs the final vocabulary set. Given the vocabulary, it simply scans each word from left to right, grabbing the longest match at each step.
def wordpiece_tokenize_word(word, vocab):
"""
Tokenize a single word using greedy longest-match-first.
Returns list of tokens, or ['[UNK]'] if tokenization fails.
"""
tokens = []
remaining = word
is_first = True
while remaining:
# Try to find the longest prefix in vocab
found = False
for end in range(len(remaining), 0, -1):
prefix = remaining[:end]
candidate = prefix if is_first else "##" + prefix
if candidate in vocab:
tokens.append(candidate)
remaining = remaining[end:]
is_first = False
found = True
break
if not found:
return ["[UNK]"]
return tokens
def wordpiece_tokenize(text, vocab):
"""Tokenize a full sentence."""
all_tokens = []
for word in text.lower().split():
word = word.strip(".,!?;:'\"()")
if word:
all_tokens.extend(wordpiece_tokenize_word(word, vocab))
return all_tokensTokenization Results: -------------------------------------------------- Input: 'playing basketball is fun' Tokens: ['pl', '##a', '##ying', 'b', '##a', '##sk', '##e', '##tb', '##a', '##l', '##l', 'is', 'fun'] Input: 'unplayed music is rare' Tokens: ['unpl', '##a', '##y', '##e', '##d', 'music', 'is', 'r', '##a', '##r', '##e'] Input: 'replay the game again' Tokens: ['r', '##e', '##pl', '##a', '##y', 'th', '##e', 'g', '##a', '##m', '##e', 'ag', '##a', '##in']
Training a Production WordPiece Tokenizer with HuggingFace
The HuggingFace tokenizers library provides a highly optimized WordPiece implementation. Let us train one on a larger English corpus to see more realistic vocabulary behavior:
import os
import tempfile
from tokenizers import Tokenizer
from tokenizers.models import WordPiece
from tokenizers.pre_tokenizers import Whitespace
from tokenizers.trainers import WordPieceTrainer
# Create a more substantial training corpus
training_corpus = [
# General English text with varied morphology
"The scientists discovered remarkable properties in the crystalline structure.",
"Remarkable discoveries often come from unexpected observations.",
"She carefully unwrapped the surprisingly beautiful gift.",
"The reconstruction project was delayed by unforeseen complications.",
"Playing music requires understanding rhythm and melody.",
"Unplayed instruments accumulate dust in forgotten corners.",
"The preprocessing step transforms raw text into clean tokens.",
"Tokenization algorithms differ in their handling of unknown words.",
"WordPiece uses pointwise mutual information to score candidate merges.",
"Byte pair encoding greedily merges the most frequent adjacent pairs.",
"BERT's vocabulary contains approximately thirty thousand tokens.",
"The model learned representations for both whole words and subwords.",
"Subword tokenization handles morphologically rich languages well.",
"Prefixes and suffixes are naturally captured by subword models.",
"The training process iteratively expands the vocabulary.",
"Each merge operation adds one new token to the growing vocabulary.",
"Greedy tokenization finds the longest matching prefix at each step.",
"Unknown characters fall back to the UNK token in BERT.",
"Special tokens mark sentence boundaries and padding positions.",
"The CLS token aggregates sequence-level information for classification.",
]
# Write corpus to a temp file (HuggingFace trainer expects file paths)
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
for line in training_corpus:
f.write(line + "\n")
corpus_path = f.name
# Initialize WordPiece tokenizer
tokenizer = Tokenizer(WordPiece(unk_token="[UNK]"))
tokenizer.pre_tokenizer = Whitespace()
# Configure trainer
trainer = WordPieceTrainer(
vocab_size=200,
special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"],
min_frequency=1,
continuing_subword_prefix="##",
)
# Train
tokenizer.train(files=[corpus_path], trainer=trainer)
os.unlink(corpus_path)Vocabulary size: 200 tokens Special tokens: ['[MASK]', '[PAD]', '[SEP]', '[UNK]', '[CLS]'] Continuation tokens (116 total): ##E, ##K, ##L, ##N, ##P, ##R, ##RT, ##S, ##T, ##a, ##ab, ##ac, ##al, ##an, ##and, ##ar, ##ark, ##ary, ##as, ##at, ##ate, ##ation, ##ations, ##ay, ##b Whole-word / initial tokens (79 total, sample): ', -, ., B, BE, C, E, G, K, L, N, P, R, S, T, The, U, Un, W, a, ad, and, b, bo, by
Tokenizing Text with the Trained Tokenizer
test_inputs = [
"playing basketball and tokenization",
"the reconstruction of wordpiece algorithms",
"unplayed music in forgotten corners",
]
# Encode each input
encodings = [(text, tokenizer.encode(text)) for text in test_inputs]WordPiece Tokenization Examples: ======================================================= Input: 'playing basketball and tokenization' Tokens: ['p', '##l', '##ay', '##ing', 'b', '##as', '##k', '##e', '##t', '##b', '##al', '##l', 'and', 'token', '##ization'] IDs: [36, 59, 140, 94, 22, 183, 68, 50, 54, 62, 106, 59, 116, 101, 153] Input: 'the reconstruction of wordpiece algorithms' Tokens: ['the', 're', '##c', '##on', '##st', '##ru', '##c', '##tion', 'of', 'w', '##ord', '##p', '##i', '##ec', '##e', 'a', '##l', '##g', '##or', '##i', '##th', '##m', '##s'] IDs: [98, 129, 65, 85, 103, 132, 65, 180, 167, 43, 109, 58, 52, 110, 50, 21, 59, 66, 82, 52, 139, 64, 51] Input: 'unplayed music in forgotten corners' Tokens: ['un', '##pl', '##ay', '##ed', 'mu', '##s', '##ic', 'in', 'for', '##g', '##o', '##t', '##ten', 'co', '##r', '##n', '##er', '##s'] IDs: [118, 186, 140, 99, 165, 51, 112, 97, 125, 66, 47, 54, 178, 160, 48, 49, 88, 51]
The tokenizer splits words like "reconstruction" into subword pieces and marks continuation tokens with ##. Words seen during training are kept whole or split at meaningful morphological boundaries. You can see how the training corpus influences which units become full tokens: words that appeared often in the training data (like "tokenization" and "algorithms") may receive their own vocabulary entries, while rare or unseen words get decomposed into smaller pieces.
Using BERT's Pre-trained WordPiece Tokenizer
In practice, you will almost always use BERT's existing tokenizer rather than training a new one from scratch. The HuggingFace transformers library makes this straightforward:
from transformers import BertTokenizer
# Load BERT's pre-trained WordPiece tokenizer
# bert-base-uncased: lowercases all text
# bert-base-cased: preserves capitalization
tokenizer_bert = BertTokenizer.from_pretrained("bert-base-uncased")example_sentences = [
"WordPiece tokenization splits words into subwords.",
"The reconstruction of the word demonstrates morphological splitting.",
"GPU parallelization dramatically accelerates transformer training.",
]
encodings_bert = [
tokenizer_bert(sentence, return_tensors=None)
for sentence in example_sentences
]BERT WordPiece Tokenization: ======================================================= Input: 'WordPiece tokenization splits words into subwords.' Tokens: ['[CLS]', 'word', '##piece', 'token', '##ization', 'splits', 'words', 'into', 'sub', '##words', '.', '[SEP]'] Input: 'The reconstruction of the word demonstrates morphological sp..' Tokens: ['[CLS]', 'the', 'reconstruction', 'of', 'the', 'word', 'demonstrates', 'morphological', 'splitting', '.', '[SEP]'] Input: 'GPU parallelization dramatically accelerates transformer tra..' Tokens: ['[CLS]', 'gp', '##u', 'parallel', '##ization', 'dramatically', 'accelerate', '##s', 'transform', '##er', 'training', '.', '[SEP]']
Notice that BERT's tokenizer automatically wraps the sequence with [CLS] at the start and [SEP] at the end. Long technical words like "parallelization" are split into recognizable morphological pieces. The word "reconstruction" becomes re, ##con, ##struct, ##ion, which corresponds to the prefix "re-", the root component "construct", and the noun suffix "-ion". This decomposition allows BERT to share learned representations across morphologically related forms.
Key Parameters
The key parameters for training a WordPiece tokenizer are:
- vocab_size: Controls the final vocabulary size. BERT uses 30,522 for English. Larger vocabularies reduce unknown token rates but require more embedding parameters. The right size depends on the language and the model capacity.
- min_frequency: The minimum number of times a token pair must appear to be eligible for merging. Higher values produce a vocabulary biased toward common patterns and may reject merges for rare but linguistically meaningful units.
- continuing_subword_prefix: The prefix appended to continuation tokens. WordPiece uses
##by convention; changing this would break compatibility with pre-trained BERT models. - special_tokens: Reserved tokens for task-specific use. BERT's five special tokens (
[UNK],[CLS],[SEP],[PAD],[MASK]) occupy fixed positions at the start of the vocabulary.