HardBERT

WordPiece Tokenization

BERT: Pre-training of Deep Bidirectional Transformers

Hard

Problem

Implement BERT WordPiece tokenization with greedy longest-match-first segmentation. Lowercase the input and split it on whitespace. For each word, choose the longest vocabulary token beginning at the current character. A token after the first piece must use the continuation prefix ##. If a word cannot be fully segmented, or its length exceeds max_word_length, emit only unk_token for that word.

For example, when play and ##ing are in the vocabulary, playing becomes play followed by ##ing. Return all pieces from all words as a Python list of strings in their original word order.

Theory

WordPiece is a subword tokenization algorithm that splits text into subword units drawn from a fixed vocabulary. Introduced by Schuster and Nakajima (2012) for Japanese and Korean segmentation, it was later adopted as the core tokenizer for BERT (Devlin et al., 2018). The algorithm uses greedy longest-match-first: given a word, it finds the longest prefix in the vocabulary, emits it, and continues from where it left off.

In BERT, WordPiece operates on a 30,000-token vocabulary. Every input sentence is first split into whitespace-delimited words, then each word is broken into WordPiece tokens. The first subword token retains its original form; all subsequent subword tokens within the same word are prefixed with ## to signal continuation. If the algorithm cannot segment a word at all, the entire word is replaced by [UNK].


What It Is / What It Does

Tokenization converts raw text into discrete units a model can process. Three broad strategies exist:

WordPiece maintains a fixed-size vocabulary of subword units covering all training text. Frequent words appear as single tokens; rare words decompose into known subwords carrying partial semantics. This is why BERT handles misspellings, neologisms, and morphological variants without [UNK].

The ## prefix disambiguates word-initial tokens from continuations. "un" starting "unhappy" is token un, while "un" mid-word would be ##un. This lets the model reconstruct word boundaries from a flat token sequence.


Key Equations

WordPiece tokenization at inference time is defined by a greedy algorithm rather than a closed-form equation. The core logic can be expressed formally as follows.

Let w = c_1 c_2 \dots c_n be a word of n characters, and let V be the vocabulary. Define the tokenization function \text{WordPiece}(w, V):

Initialize:

\text{tokens} = [\,], \quad \text{start} = 0

While \text{start} < n:

\text{end} = n, \quad \text{cur\_substr} = \text{None}

While \text{start} < \text{end}:

\text{substr} = \begin{cases} w[\text{start}:\text{end}] & \text{if } \text{start} = 0 \\ \texttt{\#\#} + w[\text{start}:\text{end}] & \text{if } \text{start} > 0 \end{cases}

\text{if } \text{substr} \in V: \quad \text{cur\_substr} = \text{substr}, \quad \text{break}

\text{end} = \text{end} - 1

If \text{cur\_substr} is None: return [\texttt{[UNK]}]

\text{tokens.append}(\text{cur\_substr}), \quad \text{start} = \text{end}

Return tokens

The key insight is greedy longest-match: at each position, try the longest possible substring first and shrink until a vocabulary match is found. This is O(n^2) worst case for a word of length n, though in practice words are short and vocabulary lookups use hash sets for O(1) membership testing.

The vocabulary construction phase (offline, before training) uses a different criterion. WordPiece merges the token pair (x, y) that maximizes:

\text{score}(x, y) = \frac{P(xy)}{P(x) \cdot P(y)}

where P denotes corpus frequency. This is pointwise mutual information -- it favors merging pairs that co-occur more than expected by chance, not merely pairs that are frequent in absolute terms.


The Algorithm Step by Step

The WordPiece tokenization algorithm for a single word proceeds as follows:

Critical details:


Paper Context

The original BERT paper (Devlin et al., 2018) states: "We use WordPiece embeddings with a 30,000 token vocabulary." Here is how WordPiece fits into BERT:

Vocabulary construction. The 30K vocabulary is built offline before training. Starting from individual characters, the algorithm iteratively merges token pairs that maximize corpus likelihood. This differs from BPE, which merges the most frequent pair. The process repeats until 30,000 tokens are reached.

Special tokens. BERT's vocabulary includes five special tokens:

Cased vs. uncased models. The uncased model lowercases text and strips accents, reducing vocabulary pressure but losing case info. The cased model preserves casing, important for NER where "Apple" (company) differs from "apple" (fruit).

Input representation. After tokenization, each token receives three embeddings summed element-wise:

Maximum sequence length. BERT supports up to 512 WordPiece tokens including [CLS] and [SEP]. Subword expansion means the effective word-level context window is shorter than 512.


WordPiece vs BPE vs Unigram

Three subword tokenization algorithms dominate modern NLP. They share the goal of decomposing text into subword units but differ in how they build vocabularies and segment at inference.

Key distinctions:


Numerical Example

Consider tokenizing "unhappiness" with vocabulary V = \{"un", "happy", "##happi", "##happiness", "##ness", "##i", "##n", "##e", "##s", "a", "h", "i", "n", "p", "s", "u", "e", ...\}

Iteration 1 (start=0):

Iteration 2 (start=2):

start=11 = word length. Result: ["un", "##happiness"]

Now with V_2 where "##happiness" is absent but "##happi" and "##ness" are present:

Iteration 1 (start=0):

Iteration 2 (start=2):

Iteration 3 (start=7):

Result: ["un", "##happi", "##ness"]

The same word produces two tokens with V and three with V_2. Vocabulary composition directly determines segmentation granularity. In both cases the algorithm greedily takes the longest available match at each position.

The [UNK] case. If we tokenize "cafe" with an accent on the final character but that accented character is absent from the vocabulary, the algorithm fails at that position and the entire word becomes [UNK]. The unaccented "cafe" tokenizes fine -- this is why BERT's uncased model strips accents.


Modern Context

WordPiece was state-of-the-art when BERT was published in 2018, but tokenization has evolved significantly.

SentencePiece (Kudo, 2018). A language-independent library that treats input as a raw byte stream, removing the need for whitespace-based pre-tokenization. It implements both BPE and Unigram, making it applicable to languages without word boundaries (Chinese, Japanese, Thai). Used by T5, ALBERT, XLNet, and mBART.

Byte-level BPE (Radford et al., 2019). GPT-2 introduced BPE on UTF-8 bytes instead of Unicode characters. The base vocabulary is 256 byte values, so any text can be encoded without [UNK]. GPT-3 and GPT-4 use the same approach via tiktoken, a fast Rust-based tokenizer with a 100K vocabulary (cl100k_base).

HuggingFace Tokenizers (2020). A Rust-based library with fast implementations of WordPiece, BPE, and Unigram behind a unified API. Makes BERT tokenization microsecond-fast.

Vocabulary size trends. BERT uses 30K. GPT-2 uses 50K. T5 uses 32K. GPT-4 uses 100K. Larger vocabularies reduce sequence length but increase embedding table size.

Multilingual challenges. Multilingual BERT uses a shared 110K vocabulary across 104 languages. Non-Latin scripts fragment heavily, increasing sequence length -- a known issue called "fertility" disparity.

Byte-level models. ByT5 and MegaByte operate directly on raw bytes, bypassing tokenization but producing much longer sequences.


Pitfalls and Common Mistakes


Examples

Example 1

Input
text = "Playing cats", vocab = {"play":1,"##ing":2,"cat":3,"##s":4,"[UNK]":0}, unk_token = "[UNK]", max_word_length = 20
Output
["play","##ing","cat","##s"]
Explanation
The tokenizer chooses play first, then matches the remaining characters with the continuation token ##ing.

Example 2

Input
text = "unaffable", vocab = {"un":1,"##aff":2,"##able":3,"##a":4,"##ff":5,"[UNK]":0}, unk_token = "[UNK]", max_word_length = 20
Output
["un","##aff","##able"]

Example 3

Input
text = "known mystery", vocab = {"known":1,"my":2,"[UNK]":0}, unk_token = "[UNK]", max_word_length = 20
Output
["known","[UNK]"]

Hints

  1. Move a start index through each word and shrink an end index until vocab contains the candidate.
  2. Add ## to a candidate whenever start is greater than zero.
  3. Discard partial pieces if any remaining character cannot be matched.

Requirements

Constraints

Starter Code

def wordpiece_tokenize(text: str, vocab: dict,
                       unk_token: str = "[UNK]", max_word_length: int = 100) -> list:
    """
    Returns the WordPiece tokens as a list of strings.
    """
    pass

Test Cases

CaseMatches
Greedy continuationspublic
Longest matchpublic
Unknown wordpublic