Byte-Pair Encoding: Train Merges and Encode
Implement BPE from scratch. `train_bpe(corpus, num_merges)` splits each text on whitespace, represents every word as characters plus an end-of-word marker `</w>`, and repeatedly merges the most frequent adjacent pair (ties broken by the lexicographically smallest pair) until `num_merges` merges are learned or no pairs remain. `encode(word, merges)` tokenises a new word by applying the learned merges in rank order.
BPE is a greedy compression algorithm, and the rank order **is** the model: at encode time you must apply merges by their training rank (lowest rank first), not left-to-right and not by frequency in the new word, or you get token ids the model never saw in training. Count pairs weighted by word frequency over a `Counter` of unique words rather than over the raw corpus; that is the difference between minutes and hours on real data. Deterministic tie-breaking matters too: two training runs that disagree on a tie produce different vocabularies and incompatible checkpoints.
Try it first.
tests.py · 8 passing
corpus = ["low " * 5 + "lower " * 2 + "newest " * 6 + "widest " * 3]
merges = train_bpe(corpus, 5)
assert merges[:4] == [("e", "s"), ("es", "t"), ("est", EOW), ("l", "o")], merges
assert merges[4] == ("lo", "w")
assert encode("lowest", merges) == ["low", "est</w>"]
assert encode("low", merges) == ["low", EOW]
assert encode("zz", merges) == ["z", "z", EOW]
toks = encode("newest", merges)
assert "".join(toks).replace(EOW, "") == "newest"
assert train_bpe(["a"], 10) == [("a", EOW)]
assert train_bpe([""], 3) == []How does production BPE stay fast? Index pair -> words so each merge only updates affected words (O(total changes) instead of rescanning), pre-tokenise with a regex so merges never cross word or digit boundaries, and run byte-level so there is no unknown-token path. At encode time cache per-word results; most traffic is a few thousand frequent words.