Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

bpe.py

15_tokenization/bpe.py  ·  124 lines  ·  view on GitHub

"""
BPE: Byte Pair Encoding
Interview question: "Implement BPE"
Simple implementation
"""
from collections import Counter, defaultdict

class SimpleBPE:
    """
    Simple BPE implementation
    """
    
    def __init__(self, vocab_size: int = 1000):
        self.vocab_size = vocab_size
        self.vocab = {}
        self.merges = []
    
    def get_word_freqs(self, corpus: list) -> dict:
        """Get word frequencies from corpus"""
        word_freqs = Counter()
        for text in corpus:
            words = text.split()
            word_freqs.update(words)
        return dict(word_freqs)
    
    def get_splits(self, word: str) -> list:
        """Split word into characters with end token"""
        return list(word) + ['</w>']
    
    def get_pairs(self, word: list) -> set:
        """Get all pairs of consecutive symbols"""
        pairs = set()
        for i in range(len(word) - 1):
            pairs.add((word[i], word[i+1]))
        return pairs
    
    def merge_vocab(self, pair: tuple, vocab: dict) -> dict:
        """Merge most frequent pair in vocabulary"""
        new_vocab = {}
        bigram = ''.join(pair)
        
        for word in vocab:
            # Replace pair with merged bigram
            new_word = word.replace(' '.join(pair), bigram)
            new_vocab[new_word] = vocab[word]
        
        return new_vocab
    
    def train(self, corpus: list):
        """Train BPE on corpus"""
        # Initialize vocabulary with character-level splits
        word_freqs = self.get_word_freqs(corpus)
        vocab = {}
        for word, freq in word_freqs.items():
            vocab[' '.join(self.get_splits(word))] = freq
        
        # Build vocabulary through merges
        num_merges = self.vocab_size - 256  # Start with 256 base chars
        
        for i in range(num_merges):
            # Count all pairs
            pairs = defaultdict(int)
            for word, freq in vocab.items():
                word_pairs = self.get_pairs(word.split())
                for pair in word_pairs:
                    pairs[pair] += freq
            
            if not pairs:
                break
            
            # Get most frequent pair
            best_pair = max(pairs, key=pairs.get)
            self.merges.append(best_pair)
            
            # Merge the pair
            vocab = self.merge_vocab(best_pair, vocab)
        
        self.vocab = vocab
    
    def tokenize(self, text: str) -> list:
        """Tokenize text using learned BPE"""
        words = text.split()
        tokens = []
        
        for word in words:
            word_split = self.get_splits(word)
            
            # Apply all merges
            for pair in self.merges:
                bigram = ' '.join(pair)
                if bigram in ' '.join(word_split):
                    word_split = ' '.join(word_split).replace(bigram, ''.join(pair)).split()
            
            tokens.extend(word_split)
        
        return tokens


# Usage Example
if __name__ == "__main__":
    print("BPE Tokenization Example")
    print("=" * 60)
    
    # Sample corpus
    corpus = [
        "hello world",
        "hello there",
        "world peace",
        "hello hello"
    ]
    
    # Train BPE
    bpe = SimpleBPE(vocab_size=50)
    bpe.train(corpus)
    
    print(f"Number of merges: {len(bpe.merges)}")
    print(f"First few merges: {bpe.merges[:5]}")
    
    # Tokenize
    text = "hello world"
    tokens = bpe.tokenize(text)
    print(f"\nTokenizing '{text}':")
    print(f"Tokens: {tokens}")