BPE training: start with the text as a sequence of bytes (256 base tokens, so any string is representable). Count every adjacent pair, merge the most frequent pair into a new token, replace it everywhere, and repeat until the vocabulary reaches the target size (GPT-2: about 50k; modern models: 100k–200k+).
Encoding new text applies the learned merges in order. Decoding concatenates the bytes of each token. Frequent words become single tokens; rare words split into pieces; nothing is ever out-of-vocabulary. Step through merges in the simulation.
Production tokenizers first split text with a regex (so merges don't cross word, number or punctuation boundaries) and add special tokens like <|endoftext|>.
def get_stats(ids):
counts = {}
for pair in zip(ids, ids[1:]):
counts[pair] = counts.get(pair, 0) + 1
return counts
def merge(ids, pair, new_id):
out, i = [], 0
while i < len(ids):
if i < len(ids) - 1 and (ids[i], ids[i + 1]) == pair:
out.append(new_id); i += 2
else:
out.append(ids[i]); i += 1
return out
ids = list("low lower lowest".encode("utf-8"))
for k in range(10):
pair = max(get_stats(ids).items(), key=lambda kv: kv[1])[0]
ids = merge(ids, pair, 256 + k)Going deeper
Byte-level BPE (GPT-2 onward) starts from 256 bytes, so any UTF-8 text is encodable; a pre-tokenization regex stops merges from crossing word, number and punctuation boundaries.
SentencePiece treats the input as a raw stream (spaces become '▁'), making it language-agnostic. Its Unigram algorithm prunes a large vocabulary down instead of growing a small one.
Best resources for this lesson
Where this comes back
- Week 26Token counts set cost and context limits.