BPE Encode/Decode
GPT-2
Medium
BPE encoding and decoding
Independent study note. Written from the public paper and official code. This is not TensorTonic Plus and does not reproduce their exercises, starter code, or tests. For the official version, subscribe on TensorTonic.
Overview
GPT-2 must map any Unicode string to a sequence of vocabulary IDs and invert that map without loss. Section 2.2 of the paper argues that a general language model should assign probability to every string, so the tokenizer cannot drop unknown characters or depend on a huge Unicode-code-point alphabet. The chosen compromise is byte-level Byte Pair Encoding (BPE), after Sennrich et al. (2015), run on UTF-8 bytes rather than on Unicode scalars.
Encoding is not “run BPE on the raw string.” The official Encoder first cuts text with a regex, maps each UTF-8 byte to a safe Unicode code point, then applies a ranked merge table, then looks up IDs. Decoding walks the same tables backward. The paper specifies the design (byte base, no UNK, reversible); the exact regex, the byte↔︎Unicode tables, and the greedy merge loop live in src/encoder.py.
This note is the runtime path. How the merge table is grown is a separate training problem.
How it works
The released files are encoder.json (string → integer, size 50,257) and vocab.bpe (ordered merge rules). get_encoder loads both and builds bpe_ranks: merge pair → rank (0 = first learned, highest priority).
Encode (Encoder.encode):
- Pre-tokenize. A regex (
self.pat) splits text into pieces such as's, optional-space + letters, optional-space + digits, leftover punctuation, and runs of whitespace. Contractions are split off so"don't"does not become one opaque blob. - Bytes as symbols. Each piece is UTF-8 encoded.
bytes_to_unicodemaps every byte in (0\ldots 255) to a single Unicode character that is not whitespace or a control character the BPE loop would mishandle. Printable bytes keep a readable identity; the rest are remapped into a private range starting at 256. - Greedy BPE.
bpetreats the mapped string as a tuple of characters. While the current pair set is nonempty, it picks the pair with the smallest rank (unknown pairs have rank (\infty)) and splices that pair into one symbol. It stops when no remaining pair is inbpe_ranks, or the word is a single symbol. A cache memoizes the result per pre-token. - IDs. The merged symbols, split on spaces that
bpeinserts as delimiters, are looked up inencoder.
If (r(a,b)) is the rank of pair ((a,b)), each step replaces the currently cheapest adjacent pair:
(a,b)=\arg\min_{(x,y)\in\mathrm{pairs}(w)}\, r(x,y),\qquad r\notin\mathrm{ranks}\Rightarrow\infty.
Decode (Encoder.decode) is the inverse: IDs → merge-alphabet strings via decoder, concatenate, map each character through byte_decoder back to a byte, then UTF-8-decode the bytearray (errors='replace' by default).
The paper does not publish the regex or the bytes_to_unicode table. Those are official-code details. What the paper does fix is invertibility: because every byte has a symbol, every Unicode string has a token sequence, so a WebText LM can be scored on any benchmark regardless of that benchmark’s own tokenizer.
Official code
All of this is in src/encoder.py: bytes_to_unicode, get_pairs, Encoder.bpe / encode / decode, and get_encoder. Merge ranks come from vocab.bpe lines after the header; the token map is encoder.json under the model directory. The public repo does not re-train these files.
Watch-outs
- Decode is not
''.join(encoder.inv[id])on the BPE strings alone. Skippingbyte_decoderleaves you in the remapped Unicode alphabet, not UTF-8 text. - Merge order is rank, not current corpus frequency. Re-counting pairs at encode time (as training does) yields a different segmentation.
- The regex keeps a leading space on many word pieces (
" hello"vs"hello"). Those are different BPE strings and usually different IDs. Stripping spaces before encode breaks the trained vocabulary. bpejoins symbols with spaces as a reversible delimiter. Those spaces are not text spaces; splitting on them is part of the codec, not of English tokenization.
Sources
- Paper: Language Models are Unsupervised Multitask Learners (Radford et al., 2019), §2.2
- BPE: Sennrich, Haddow, and Birch, “Neural Machine Translation of Rare Words with Subword Units,” 2015
- Code: openai/gpt-2
src/encoder.py