← Back to list

LLM Loves Tokenizers! Implementing BPE from Zero

LLM is the main tool behind most of the things nowadays. For example, an agent has different loops connected via graphs, but the brain is…

Madhesh · 2026-06-05 18:19 · 0 claps · 2.6 min read
#llm #bpe #byte-pair-encoding #tokenization #token
Open on Medium ↗
Wiki topics: LLM · Large Language Models AGT · AI Agents GEN · Genomics & Sequencing 💻 · Programming 💑 · Relationships

LLM Loves Tokenizers! Implementing BPE from Zero

LLM is the main tool behind most of the things nowadays. For example, an agent has different loops connected via graphs, but the brain is LLM. When we want to do something, we always go for LLM. In that , we have a lot of stages where the tokenizer is the main and first thing.

Tokenizers are the one which converts raw strings into numbers, so that the machine can process further by converting them into encodings to understand the context and to work on it efficiently, to do the math with them.

Tiktoken and SentencePiece are the tokenizers behind modern LLMs. Google developed and uses SentencePiece. And tiktoken is used by the GPT series, byte pair encoding is the main algorithm behind tiktoken.

Byte pair encoding is an algorithm that maps unicode to characters. When a text is given to the LLM, it splits the text into chunks, and for each chunk, a tokenID is generated. It then takes the list of IDs for a word, finds the most frequently occurring pair, and assigns a new token to it. By continuously doing this, a word like “hello” can be squished into an array of with one or two token IDs. In the end, the tokenizer trains on a vocabulary of a fixed size and remembers it. The vocabulary size determines the number of tokens the tokenizer knows. For example, GPT-4 has a vocabulary size of around 100,000.

THE CODE

def __init__(self, vocab_size=32000):
        self.vocab_size = vocab_size
        self.merges = {}
        self.vocab = {}
        self.id_to_token = {}

this the intialization part where we define the vocab size and vocab id_to_token.

 def _bytes_to_unicode(self):
        bs = list(range(ord("!"), ord("~")+1)) + list(range(ord("¡"), ord("¬")+1)) + list(range(ord("®"), ord("ÿ")+1))
        cs = bs[:]
        n = 0
        for b in range(2**8):
            if b not in bs:
                bs.append(b)
                cs.append(2**8 + n)
                n += 1
        return {b: chr(c) for b, c in zip(bs, cs)}

this the basic assigning of charcters to its unicode where whe fisrt assign the basic chartes and their unocde.and for the oothers we assign to a newer unqiye character.

def _get_stats(self, tokens):
        pairs = Counter()
        for token_seq in tokens:
            for i in range(len(token_seq) - 1):
                pair = (token_seq[i], token_seq[i + 1])
                pairs[pair] += 1
        return pairs

    def _merge_pair(self, a, b, new_id, tokens):
        new_tokens = []
        for token_seq in tokens:
            new_seq = []
            i = 0
            while i < len(token_seq):
                if i < len(token_seq) - 1 and token_seq[i] == a and token_seq[i+1] == b:
                    new_seq.append(new_id)
                    i += 2
                else:
                    new_seq.append(token_seq[i])
                    i += 1
            new_tokens.append(new_seq)
        return new_tokens

the _get_stats fn() gets the pairs in a chunck and count its freq.The _merge_pair squishes the freq occured pair by giving a new ID to it.

THE TRAINING

def train(self, text_iterator, pretokenize_pattern=None):
        self.byte_to_char = self._bytes_to_unicode()
        self.char_to_byte = {v: k for k, v in self.byte_to_char.items()}

        for i in range(256):
            char = self.byte_to_char[i]
            self.vocab[char] = i
            self.id_to_token[i] = char

        if pretokenize_pattern is None:
            pretokenize_pattern = r"""'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+"""
        pretokenizer = re.compile(pretokenize_pattern)

        all_tokens = []
        for text in text_iterator:
            chunks = pretokenizer.findall(text)
            for chunk in chunks:
                token_ids = list(chunk.encode('utf-8'))
                all_tokens.append(token_ids)

        next_id = 256
        while next_id < self.vocab_size:
            pair_freqs = self._get_stats(all_tokens)
            if not pair_freqs: break
            (a, b), _ = pair_freqs.most_common(1)[0]

            new_token_str = self.id_to_token[a] + self.id_to_token[b]
            self.merges[(a, b)] = next_id
            self.vocab[new_token_str] = next_id
            self.id_to_token[next_id] = new_token_str

            all_tokens = self._merge_pair(a, b, next_id, all_tokens)
            next_id += 1
            if next_id % 1000 == 0:
                print(f"Trained {next_id}/{self.vocab_size} tokens")

The train fn() first takes the mapping of bytes to characters and the reverse of it. Then it processes the text by splitting it into chunks. For each chunk, the chuck IDs are appended to a full list all_tokens. From this list, we find all neighbouring pairs{_get_stats}and get the most frequently occurring one{freqs.most_common(1)[0]} and merge it{_merge_pair}.

The new ID is the ID we assign to every new token or new combination of word we see.

round 1:  "th" → 256    ← super common in English
round 2:  "he" → 257
round 3:  "in" → 258
...
round 500:  "the" → 756    ← "th" + "e", both already merged before
round 2000: " the" → 2256  ← space + "the"
round 8000: " world" → 8256

메타데이터
post_id
5fb5f0bbe9fa
slug
llm-loves-tokenizers-implementing-bpe-from-zero-5fb5f0bbe9fa
url
https://medium.com/@madheshsasikala81/llm-loves-tokenizers-implementing-bpe-from-zero-5fb5f0bbe9fa
canonical_url
https://medium.com/@madheshsasikala81/llm-loves-tokenizers-implementing-bpe-from-zero-5fb5f0bbe9fa
author_url
https://medium.com/@madheshsasikala81
status
ok
fetched_at
2026-06-09 15:37:30