LLMs don't see characters or words — they see tokens. A tokenizer maps raw text to integer IDs from a fixed vocabulary. The dominant algorithm is Byte Pair Encoding (BPE): start with individual bytes, iteratively merge the most frequent adjacent pair until the vocabulary reaches the target size (typically 32k–128k).
WordPiece (BERT) maximizes likelihood instead of frequency. Unigram (SentencePiece) starts with a large vocabulary and prunes. Byte-level BPE (GPT-2+) operates on UTF-8 bytes, needing no pre-tokenization — handles any language/script.
Python — BPE from scratch#
Byte-level BPE starts from UTF-8 bytes. English letters are 1 byte each, Danish text averages 1.19 (æ, ø and å take two), Greek 2, and Chinese and Hindi 3. Where the tokenizer saw little of a script, merges are few and text stays close to a token per byte — so the same sentence can cost several times more tokens, context and money in one language than in another.
- Token counts, prices and context limits are per token, not per word; compare languages on tokens actually produced (Petrov et al. 2023 measured up to 15× differences).
- Tokenizers are frozen with the model; text that is rare in the tokenizer’s training data (code in a new language, a new script) stays expensive.
- Character-level questions (“how many r’s in strawberry?”) are hard because the model never sees characters.
samples = {'English': 'pattern', 'Danish': 'mønstre på tværs', 'Greek': 'μοτίβο',
'Chinese': '模式识别', 'Hindi': 'पैटर्न'}
bytes_per_char = {lang: len(s.encode('utf-8')) / len(s) for lang, s in samples.items()}
# byte-level BPE starts from UTF-8 bytes: scripts it saw little of stay closer to one token per byteAssumes import numpy as np. Configurations named after real models use their published shapes; other numbers are made up or simulated, as the comments say.
The same idea with a library
def train_bpe(text, vocab_size):
tokens = list(text.encode('utf-8'))
merges = {}
while len(set(tokens)) < vocab_size:
pairs = {}
for i in range(len(tokens) - 1):
p = (tokens[i], tokens[i+1])
pairs[p] = pairs.get(p, 0) + 1
if not pairs: break
best = max(pairs, key=pairs.get)
new_id = max(set(tokens)) + 1
merges[best] = new_id
# Apply merge
new_tokens, i = [], 0
while i < len(tokens):
if i < len(tokens)-1 and (tokens[i], tokens[i+1]) == best:
new_tokens.append(new_id)
i += 2
else:
new_tokens.append(tokens[i])
i += 1
tokens = new_tokens
return merges- R. Sennrich, B. Haddow & A. Birch, “Neural Machine Translation of Rare Words with Subword Units”, ACL, 2016
- A. Radford, J. Wu, R. Child, D. Luan, D. Amodei & I. Sutskever, “Language Models Are Unsupervised Multitask Learners”, OpenAI, 2019
- A. Petrov, E. La Malfa, P. H. S. Torr & A. Bibi, “Language Model Tokenizers Introduce Unfairness Between Languages”, NeurIPS, 2023
Last reviewed October 2026 · How this site checks its content · Report a mistake
1. Why do language models often miscount the letters in a word?
Show answer
B. A word like “strawberry” may be two or three tokens; the letters inside a token are not directly visible to the model.
2. The same sentence in Danish and in English usually costs…
Show answer
B. BPE merges frequent character sequences. Languages that were rare in its training data split into more, shorter pieces — more cost and less context per sentence.
3. What is the trade-off of a larger vocabulary?
Show answer
B. Each extra token type adds a row to the embedding table and a column to the output layer.