Skip to content
AI360XpertBeta

Byte-Pair Encoding

The most frequent letter pairs earn their own tokens first. Repeat that merge and a vocabulary of useful fragments builds itself from raw text.

Byte-pair encoding merges the most frequent pair first, so lo forms before low and each merge adds one vocabulary entry
Byte-pair encoding merges the most frequent pair first, so lo forms before low and each merge adds one vocabulary entry

Why Does This Exist?

A model reads numbers, not letters, so text has to be chopped into pieces first. That chopping is tokenization: each distinct piece gets an entry in a vocabulary, and each entry gets its own ID.

Word-level chopping breaks on the first unfamiliar word. Names, typos, code, and other languages keep producing words no fixed word list contains, and every one of them lands outside the vocabulary. Character-level chopping never gets stuck, since the character set is small and closed. But it turns ten words into fifty pieces, and every extra piece costs compute downstream. Subword chopping is the middle path: frequent stretches stay whole, and rare ones split apart.

That leaves one question you can't answer by hand: which stretches deserve their own entry? Nobody can list every useful fragment of every language in advance. Byte-pair encoding refuses to guess. It counts pairs in a real corpus and lets frequency decide, most frequent first.

This page covers the counting and merging algorithm itself. The granularity trade-off lives on the tokenization page, and the quirks the merges leave behind live on tokenization artifacts.

Think of It Like This

Inventing shorthand for whatever you write most

You take lecture notes by hand, and the same letter pairs keep slowing you down. So you invent a squiggle for the pair you write most, say t and h, and use it from then on. Then you notice your new squiggle plus e shows up constantly, so that trio gets its own squiggle too. After a fixed number of inventions you stop. The pairs you write daily are now single pen strokes, while rare ones stay spelled out because they never earned a squiggle.

The corpus is your pile of notebooks. Counting is the noticing. A merge is the squiggle: two adjacent symbols fused into one entry, with earlier merges unlocking later ones.

Where it stops: your squiggles carry meanings you chose, and you still see the letters inside them. Byte-pair encoding chooses nothing and sees no letters. It fuses whatever is frequent, even across a boundary no human would join, which is why its pieces are compression artifacts rather than words.

How It Actually Works

Training starts from single characters. Byte-level variants start from the 256 raw bytes instead, which guarantees every input can be covered. Call the set of pieces the vocabulary VV. For every adjacent pair of symbols (a,b)(a, b) in the corpus, count its occurrences c(a,b)c(a, b), weighting each word by how often it appears. Find the pair with the largest count, fuse every occurrence of it into a new symbol abab, and add that symbol to VV. That is one merge. Recount with the new symbol in play and repeat, always merging the current winner, until you have performed a fixed number of merges chosen up front. Each merge adds exactly one entry, so the merge count sets the vocabulary size.

Worked example with real counts

Take a tiny corpus with frequencies attached:

  • "low" appears 3 times
  • "look" appears 2 times

Start from single characters and weight every pair by its word's frequency. From the three copies of "low" come (l,o)(l, o) three times and (o,w)(o, w) three times. From the two copies of "look" come (l,o)(l, o) twice more, plus (o,o)(o, o) and (o,k)(o, k) twice each. The totals are c(l,o)=5c(l, o) = 5, c(o,w)=3c(o, w) = 3, c(o,o)=2c(o, o) = 2, c(o,k)=2c(o, k) = 2. The winner is (l,o)(l, o), so merge 1 fuses it into a new symbol lolo, and the vocabulary gains one entry.

Recount with lolo in play. The corpus now reads [lo,w][lo, w] three times and [lo,o,k][lo, o, k] twice. The pairs are c(lo,w)=3c(lo, w) = 3, c(lo,o)=2c(lo, o) = 2, c(o,k)=2c(o, k) = 2. The winner is (lo,w)(lo, w), so merge 2 fuses it into lowlow. This is the same picture as the diagram above: lolo forms before lowlow because the pair counts say so, and each merge added exactly one entry.

Encoding new text with the learned merges

To encode text the training corpus never contained, start from its characters and apply the learned merges in the order they were learned, skipping any that do not match, until none apply. Hand the trained merges the unseen word "lows": start from [l,o,w,s][l, o, w, s], apply merge 1 where it matches to get [lo,w,s][lo, w, s], then apply merge 2 to get [low,s][low, s]. Two tokens, no retraining, no unknown-word failure. That decomposition is the whole payoff, and its output is what the next-token prediction objective consumes: IDs, not letters.

What it costs and when to skip it

Every merge shortens frequent text but grows the embedding table by one row, since each vocabulary entry needs its own vector (see embeddings). So the merge count trades sequence length against table size and data hunger: rare entries get updated rarely, which starves them of training signal. Set it with eyes open.

The method assumes your text looks like the counting corpus. Feed it something far away, another language or dense code, and familiar words shatter into tiny pieces while sequences balloon, so latency and cost follow token count instead of word count. When your data differs from the vocabulary's training mix, do not reuse the vocabulary blindly. Count tokens per word on your own sample first, then retrain or pick a vocabulary whose training mix matches yours.

Code

The point of this snippet is a check: run the two merges from the worked example and confirm the winners and counts match the hand computation.

from collections import Countercorpus: list[list[str]] = [list("low")] * 3 + [list("look")] * 2for step in range(2):    counts: Counter[tuple[str, str]] = Counter()    for w in corpus:        for a, b in zip(w, w[1:]):            counts[(a, b)] += 1    (a, b), n = counts.most_common(1)[0]    out: list[list[str]] = []    for w in corpus:        new, i = [], 0        while i < len(w):            hit = tuple(w[i:i + 2]) == (a, b)            new.append((a + b) if hit else w[i])            i += 2 if hit else 1        out.append(new)    corpus = out    print(step + 1, (a, b), n, corpus[0], corpus[3])    # -> 1 ('l', 'o') 5 ['lo', 'w'] ['lo', 'o', 'k']    # -> 2 ('lo', 'w') 3 ['low'] ['lo', 'o', 'k']

Both prints agree with the hand counts above: (l,o)(l, o) wins at 5, then (lo,w)(lo, w) wins at 3. This loop counts pairs inside words only. Real implementations add an end-of-word marker for the same reason, so merges never fuse the end of one word to the start of the next.

Watch Out For

Reading meaning into the pieces

The fragments look like morphemes, so you start treating them as words. Then a word splits somewhere no linguist would cut, your probe of that piece's embedding returns nonsense, and you blame the model for a decision the tokenizer made before the model ever ran.

The fix is procedural: always print the real tokenizer output for the exact string you are debugging, and treat pieces as compression artifacts. The gallery of ways this bites is on tokenization artifacts.

Reusing a vocabulary far from the text it counted

A vocabulary trained on English prose looks fine until you feed it another language or dense code. Then familiar words shatter into tiny pieces, sequences balloon, and latency and cost follow token count instead of word count.

The symptom is a jumped tokens-per-word rate on your own sample. Measure that rate before you commit. If it jumps, retrain the vocabulary on your mix or switch to one trained on it. Never assume a vocabulary travels.

The Quick Version

  • Byte-pair encoding learns a tokenizer vocabulary by merging the most frequent adjacent pair, one merge at a time.
  • Each merge adds exactly one vocabulary entry, so the merge count you choose up front sets the vocabulary size.
  • Frequent stretches fuse early into single tokens, while rare text stays split into small reusable pieces.
  • New words are encoded by replaying the learned merges in order, so unseen words become known pieces instead of failures.
  • The pieces carry frequency, not meaning: never read them as words, and never reuse a vocabulary far from the text it counted.