A simple, from-scratch implementation of the Byte Pair Encoding (BPE) subword tokenization algorithm, demonstrated on Herman Melville's Moby Dick (via NLTK's Gutenberg corpus).
- Overview
- Features
- Requirements
- Usage
- How It Works
- Sample Input
- Applications
- Notes / Limitations
- Contributing
- License
BPE is the algorithm behind modern subword tokenizers used in NLP models (e.g. GPT-style tokenizers). It starts with individual characters and iteratively merges the most frequent adjacent pair of symbols into a new symbol, gradually building up a vocabulary of subword units.
This script:
- Loads raw text from the NLTK Gutenberg corpus.
- Splits each word into individual characters (with an end-of-word marker
-). - Repeatedly finds and merges the most frequent adjacent character/symbol pair.
- Prints a slice of the resulting tokenized corpus.
- Implements the core Byte Pair Encoding algorithm
- Uses the NLTK Gutenberg Corpus (
Moby Dick) as input - Tokenizes text into character-level sequences
- Finds the most frequent adjacent character pair
- Iteratively merges character pairs
- Configurable number of merge operations (
k)
- Python 3
- NLTK (
pip install nltk) - NLTK data packages:
import nltk nltk.download('punkt') nltk.download('gutenberg')
python BPE.pyBy default, the script runs BPE on the full text of Moby Dick for 1000
merge iterations (k = 1000) and prints tokens 100 through 200 of the
result.
Scans every word (a space-separated sequence of symbols) and counts all
adjacent symbol pairs across the entire token list, returning the single
most common pair using collections.Counter.
Given a pair of symbols, merges every occurrence of that pair (as adjacent, space-separated symbols) into a single combined symbol, for every word in the token list.
The main driver:
- Tokenizes and lowercases the input corpus with
word_tokenize. - Converts each word into a sequence of individual characters, joined by
spaces, with a trailing
-marking the end of the word (e.g."cat"→"c a t -"). - Runs up to
kmerge iterations, each time:- Finding the most frequent adjacent pair across all words.
- Merging that pair everywhere it occurs.
- Stopping early if no pairs remain.
- Returns the final list of (partially) merged word representations.
lowest
lower
newest
widest
Character representation:
l o w e s t -
l o w e r -
n e w e s t -
w i d e s t -
After several merge operations, common sequences such as:
l o
lo w
low e
est
become merged into larger subword units.
Byte Pair Encoding is commonly used for:
- NLP preprocessing
- Machine Translation
- Large Language Models (LLMs)
- Text Compression
- Vocabulary construction
- Subword tokenization
merge_pairusesstr.replace, so a merge is applied to all occurrences of the bigram string within a word — this is efficient but assumes no unintended substring collisions between the join separators.- The end-of-word marker
-is treated as a normal symbol, so it can also be merged into subword units (e.g."the-"). - Runtime scales with
k(number of merges) and the size of the corpus, since each iteration re-scans every word to find the most frequent pair. - This is an educational/demonstration implementation, not optimized for large-scale or production tokenizer training.
Contributions are welcome! To contribute:
- Fork the repository.
- Create a new branch for your change (
git checkout -b feature/my-change). - Make your changes, following the existing code style.
- Test your changes locally to confirm the script still runs correctly.
- Commit your changes with a clear message and push to your fork.
- Open a pull request describing what you changed and why. Please open an issue first for larger changes, so we can discuss the approach before you invest time in it.
You can also reach out via email at rseyednozadi@gmail.com.
This project is licensed under the MIT License. See the LICENSE file for details.