Why Can't LLMs Count the R's in "Strawberry"? How Tokenizers Work
---
title: "Why Can't LLMs Count the R's in \"Strawberry\"? How Tokenizers Work"
published: true
description: "The preprocessing step behind spelling slips, shaky arithmetic, glitch tokens and your API bill"
tags: ai, llm, machinelearning, nlp
canonical_url: ragmehta.substack.com
---
The preprocessing step behind spelling slips, shaky arithmetic, glitch tokens and your API bill
Ask a language model how many r's are in "strawberry" and, for a long time, the popular answer was "two." The same models could write a sonnet about strawberries, explain their genetics and translate the word into forty languages. So why does counting letters go wrong?
The model never sees the letters. Before your prompt reaches the first transformer layer, a separate program called the tokenizer chops the text into chunks and swaps each chunk for an integer. The model only ever reads those integers. If "strawberry" becomes a single integer, the three r's are hidden inside a number the model can't take apart.
Tokenization is the least glamorous part of the LLM stack, and it causes a surprising number of problems: shaky multi-digit arithmetic, higher costs for non-English users, strange "glitch" words that make models act up, and token counts that don't match your character-based estimates. In this issue we'll open the tokenizer up: how it turns text into IDs, how Byte Pair Encoding (BPE) learns its vocabulary, and how its choices show up as spelling, arithmetic, language and glitch-token problems you can design around.
Every token split in this issue was measured with OpenAI's open-source tiktoken library on the actual GPT-2, GPT-4 (cl100k_base) and GPT-4o (o200k_base) vocabularies, so you can reproduce them yourself.
Tokens: the Model's Only Alphabet
A neural network can't do math on strings. It needs numbers. The simplest scheme would give every character a number, but then a 2,000-word document becomes more than 10,000 positions, and every transformer layer pays for each one. The other extreme gives every word a number, but no fixed list covers every word, name, typo, URL and code identifier people will ever type.
Rico Sennrich and colleagues measured this trade-off in their 2016 paper Neural Machine Translation of Rare Words with Subword Units. On a German training corpus of about 100 million words, keeping whole words needed 1.75 million vocabulary entries and still left 1,079 unknown words in the test set. Splitting into characters needed only about 3,000 symbols but stretched the corpus to 550 million positions. Their subword method, BPE, landed in between: about 63,000 symbols, 112 million positions and zero unknown words.
That middle ground is a token: a chunk of text that's usually shorter than a word for rare words and exactly one word for common ones. Modern LLMs use vocabularies of 50,000 to 200,000 tokens. tiktoken's README puts the average at about 4 bytes of text per token. On the English text of last week's issue I measured 4.4 characters per token with the GPT-4o tokenizer, or roughly 0.63 words per token.
From Text to Token IDs in Five Steps
!From raw text to token IDs: pre-split, apply BPE merges, look up IDs, then embed
From raw text to token IDs: pre-split, apply BPE merges, look up IDs, then embed
Here's what happens to the sentence "I love strawberry" on its way into GPT-4o:
1. Raw text. Your string arrives as UTF-8 bytes. Some tokenizers normalize it first (Unicode normalization, lowercasing in older models), but GPT-style tokenizers keep the bytes exactly as typed.
2. Pre-split. A regular expression cuts the text into pieces that merges are never allowed to cross: letter runs, digit runs, punctuation, whitespace. The tiktoken source shows the patterns. GPT-2's version keeps a leading space attached to the following word, so the pieces are I, love, strawberry. That's why the dot in the figure matters: " strawberry" with a space and "strawberry" without one are different tokens.
3. BPE merges. Each piece starts as a sequence of bytes. The tokenizer then applies its learned merge rules, in the order they were learned, gluing adjacent pairs together until no rule applies. Common words collapse into one token. Rare ones end up as a few subword chunks.
4. Token IDs. Each final chunk is looked up in the vocabulary. With o200k_base, "I love strawberry" becomes [40, 3047, 101830]: three integers.
5. Embeddings. Inside the model, each ID selects one row of a learned embedding matrix, a vector of a few thousand numbers. From here on, the transformer only ever works with those vectors.
Two properties fall out of this design, and the tiktoken README calls both out. Tokenization is lossless: decoding the IDs gives back the exact original bytes. And it works on any text, even strings that never appeared when the tokenizer was trained. The second property depends on a trick we'll get to in a moment.
How BPE Learns Its Vocabulary
Byte Pair Encoding started life as a data compression algorithm (Philip Gage, 1994): repeatedly find the most common pair of adjacent bytes and replace it with a new, unused symbol. Sennrich et al. adapted the idea to build a vocabulary instead of compressing a file. Training goes like this:
1. Split every word in the training corpus into characters. The starting vocabulary is just those characters.
2. Count every pair of adjacent symbols, weighted by how often each word appears.
3. Merge the most frequent pair into a new symbol, add it to the vocabulary, and record the rule.
4. Repeat until the vocabulary reaches the target size.
The number of merges is the only real setting. As the paper puts it, the final vocabulary size equals the starting symbols plus the number of merge operations.
!BPE training: count adjacent pairs, merge the most frequent, repeat until the vocabulary is full
BPE training: count adjacent pairs, merge the most frequent, repeat until the vocabulary is full
The diagram uses the toy corpus from the Hugging Face LLM course: "hug" (10 times), "pug" (5), "pun" (12), "bun" (4) and "hugs" (5).
- Start: the vocabulary is b g h n p s u.
- Merge 1: the pair u + g appears 20 times (10 in hug, 5 in pug, 5 in hugs), more than any other, so ug becomes a token.
- Merge 2: u + n now leads with 16 (12 in pun, 4 in bun), so un joins.
- Merge 3: h + ug appears 15 times, and the first three-letter token, hug, is born.
Real tokenizers run this loop tens of thousands of times over gigabytes of text. GPT-2's vocabulary has 50,257 entries. GPT-4's cl100k_base has about 100,000 and GPT-4o's o200k_base about 200,000 (100,277 and 200,019 including special tokens, according to tiktoken). Meta says Llama 3 moved to a 128K-token vocabulary that yields up to 15% fewer tokens than Llama 2 on the same text.
Encoding new text replays the merges. Split the input into characters (or bytes), then apply the learned rules in priority order. A word seen often during training collapses into one token. A word never seen still gets encoded, just in smaller pieces. With GPT-4o's vocabulary, "unbelievably" becomes un | bel | ievably.
The order of merges also explains some quirks. Merges reflect the tokenizer's training data, which isn't necessarily the model's training data. Keep that in mind; it comes back when we get to glitch tokens.
Byte-Level BPE: No More "Unknown Word"
Classic BPE starts from characters, and Unicode defines roughly 150,000 of them. Any character missing from the training corpus becomes [UNK], the "unknown" token. The Hugging Face course notes this is one reason older NLP models handled emoji badly.
GPT-2 introduced a simple fix: run BPE over bytes instead of characters. There are only 256 possible byte values, so the base vocabulary has 256 entries and every possible input, whether emoji, Tamil script or binary garbage, can be written in them. Merges then build up common multi-byte sequences. As the course puts it, GPT-2 doesn't actually have an unknown token, because an unknown character can't occur with byte-level BPE.
You can see the byte fallback at work. In GPT-2's vocabulary, the emoji 🙂 (four UTF-8 bytes) becomes 2 tokens. GPT-4o's larger vocabulary learned the whole sequence, so it's 1 token. Either way, nothing is ever "unknown." The price is that a rarely seen script can get chopped into fragments that aren't even whole characters, which leads to the language tax below.
Why the Model Can't See the R's
Back to the strawberry. Here's what GPT-4o's tokenizer does with the classic question:
!The model sees “ strawberry” as one ID with no letters inside; spelling it out or calling code exposes the letters
The model sees “ strawberry” as one ID with no letters inside; spelling it out or calling code exposes the letters
"How many r's are in strawberry?" becomes 8 tokens: How | ·many | ·r | 's | ·are | ·in | ·strawberry | ?. The word you care about is token #101830. To the model, that's one opaque symbol, mapped to one embedding vector. There's no "r" in it to count.
Whatever the model knows about how strawberry is spelled, it had to memorize from training text where the word happened to be spelled out, as in dictionaries, spelling-bee pages or kids' worksheets. That knowledge is thin and indirect, so the model guesses, and a plausible guess for a word ending in a double letter is "two."
The split also depends on context in ways people don't expect:
- strawberry (leading space, the usual case mid-sentence) is 1 token.
- strawberry (no leading space, for example at the start of a message) is 3 tokens: st | raw | berry.
- Strawberry (capitalized) is a different 3: Str | aw | berry.
So the model may have learned slightly different things about the same word.
The fixes all expose the letters. Writing the word as s-t-r-a-w-b-e-r-r-y turns it into 10 tokens, one per letter, and now counting is something the model can actually do. Better still, when exact character-level answers matter (counting, reversing a string, checking a regex), have the model call a code tool. One line of Python, "strawberry".count("r"), returns 3 every time.
The Multilingual Token Tax
The tokenizer's merges come from its training data, and that data is mostly English and code. English words get dedicated tokens. Other languages, especially ones with their own scripts, get fewer merges and are left closer to raw bytes.
!The token tax: the same sentence costs more tokens in some languages, and numbers split into left-aligned chunks
The token tax: the same sentence costs more tokens in some languages, and numbers split into left-aligned chunks
I ran one short sentence, "The weather is nice today," and simple translations of it through three of OpenAI's tokenizers. In English, it's 6 tokens in all three. In Tamil, it's 85 tokens with GPT-2's tokenizer, 42 with GPT-4's and 11 with GPT-4o's. Hindi drops from 27 to 21 to 5, and Russian from 27 to 13 to 6. That's just one sentence, not a benchmark, but the direction is clear. Larger, more multilingual vocabularies have closed much of the gap, though not all of it.
Aleksandar Petrov and colleagues studied this systematically in Language Model Tokenizers Introduce Unfairness Between Languages (2023). They found the same text can differ in token length by up to 15× across languages, and that even byte-level and character-level models show gaps of more than 4×. The consequences are concrete:
- Cost: APIs bill per token.
- Context: a 128K-token window holds far fewer pages of Tamil than of English.
- Latency: output is generated one token at a time.
If you serve a non-English market, measure token counts on your actual traffic. Don't assume the "about 4 characters per token" rule of thumb.
Numbers: Why Long Addition Goes Wrong
The same pre-split regex decides how numbers are chunked, and labs have made very different choices. Aaditya Singh and DJ Strouse surveyed them in Tokenization counts: the impact of tokenization on arithmetic in frontier LLMs (2024):
- Pure BPE (GPT-3, Claude 2.1, OLMo): whichever digit strings happened to be frequent got tokens. 710 might be one token while 711 isn't, so numbers of the same length split inconsistently.
- Single digits (PaLM, Llama 1 and 2, Mistral): every digit is its own token.
- Chunks of up to 3 digits, left to right (GPT-3.5, GPT-4): the \p{N}{1,3} rule in the tiktoken pattern. GPT-4o's o200k_base uses the same rule.
Left-to-right chunking means 1234567 becomes 123 | 456 | 7. The chunks line up with the start of the number, while place value, and the way carries move when you add, runs from the end. Insert commas and the tokenizer splits on them, giving 1 | , | 234 | , | 567. Now every chunk sits on a thousands boundary.
Singh and Strouse tested exactly this on GPT-3.5 with 7- to 9-digit addition. With plain digits, 8-shot accuracy was 75.6%. With commas, it was 97.8%. When the answer had more digits than the inputs (because of a final carry), plain-digit accuracy fell to 8.25%, and the errors weren't random: the model almost always got the first three digits right and the fourth digit wrong, right at the first token boundary. Asking the model to first rewrite the problem with commas recovered most of the lost accuracy. GPT-4 was less affected, but the effect didn't disappear.
The lesson isn't "add commas everywhere." It's that number formatting changes the problem the model has to solve. For anything that must be exact, like invoices or financial math, route the arithmetic to a calculator or code tool.
Glitch Tokens: When the Vocabulary Outlives the Data
In early 2023, Jessica Rumbelow and Matthew Watkins went looking at GPT-2 and GPT-3's token embeddings and found a cluster of strange entries: SolidGoldMagikarp, TheNitromeFan, cloneembedreportprint, petertodd. In their LessWrong write-up, they showed that asking a model to simply repeat these strings produced evasion, insults or substitutions. ChatGPT, for example, repeated " SolidGoldMagikarp" as "distribute." Some of these tokens even made GPT-3 give different answers at temperature 0, which is supposed to be deterministic.
Their partial explanation goes back to the point about merges. The tokenizer was trained on scraped web text that included Reddit usernames, e-commerce site internals and game logs. The model was trained on more heavily curated text. So strings like SolidGoldMagikarp (I confirmed it's a single token, #43453, in GPT-2's vocabulary) earned a vocabulary slot but almost never showed up during model training. Their embedding vectors stayed close to wherever they started, and the model doesn't know what to do when it meets one.
It's not just a GPT-2 curiosity. Fishing for Magikarp (Land and Bartolo, 2024) built automatic detectors that combine tokenizer analysis, weight-based signals and prompting, and found under-trained tokens across a wide range of models. In newer OpenAI vocabularies, " SolidGoldMagikarp" is 5 ordinary tokens (·Solid | Gold | Mag | ik | arp), but each new tokenizer can bring its own oddities. If you train or fine-tune models, scanning for under-trained tokens is cheap insurance.
The Other Tokenizer Families
BPE dominates today's LLMs, but you'll run into two other algorithms, often through one library:
- WordPiece (developed by Google for BERT) also learns merges bottom-up, but according to the Hugging Face course it scores a pair as its frequency divided by the product of its parts' frequencies, which favors pairs whose parts are rare on their own. It keeps only the final vocabulary and encodes each word by taking the longest piece it knows, then the next. Pieces inside a word carry a ## prefix, as in token ##ization. (Google never open-sourced its training code, so the course describes a best-guess reconstruction.)
- Unigram LM, from Taku Kudo's Subword Regularization (2018), works top-down. It starts with a large candidate vocabulary, scores how much each piece contributes to a probabilistic model of the corpus, and prunes the least useful pieces. Since the same word can be segmented several ways, Kudo trained translation models on sampled segmentations as a kind of noise, which made them more robust, especially in low-resource and out-of-domain settings.
- SentencePiece (Kudo and Richardson, 2018) is a library rather than an algorithm. It implements both BPE and Unigram and trains directly on raw sentences, with no language-specific pre-splitting. Whitespace becomes the visible symbol ▁, so I saw a girl becomes ▁I ▁saw ▁a ▁girl (example from the SentencePiece README). That makes it a natural fit for languages that don't put spaces between words, like Japanese and Chinese.
Picking a Vocabulary Size
Why not a million tokens? Every entry costs a row in the input embedding matrix and another in the output layer that scores the next token (unless the two are tied). With 4,096-dimensional vectors, a 200,000-entry vocabulary is about 820 million parameters per matrix. More entries also means fewer training examples per rare token, which is the glitch-token problem again.
Smaller vocabularies mean longer sequences: attention costs more, the context window fills faster and generation takes more steps. Recent models have moved toward larger vocabularies (50K, then 100K, 128K and 200K) as the embedding table becomes a small share of total parameters. Meta credits Llama 3's 128K tokenizer with "substantially improved model performance."
A Cheat Sheet for Engineers
!Common tokenizer gotchas, their causes and practical fixes
Common tokenizer gotchas, their causes and practical fixes
A few of these deserve a sentence more.
Count tokens with the real tokenizer. Use tiktoken.encoding_for_model("gpt-4o") for OpenAI models, or your provider's token-counting endpoint, before you enforce context limits or estimate costs. Character-based estimates are wrong for code, non-English text and JSON.
Watch the boundary at the end of your prompt. Because GPT-style tokenizers attach the space to the following word, " Paris" is a single token. A prompt that ends with a trailing space ("The capital of France is ") leaves a bare space token, and the model then has to continue with "Paris" without its usual leading space, an unusual sequence it saw less often in training. Trim trailing whitespace from completion-style prompts. The "token healing" technique cited by Singh and Strouse goes further: it backs up a few tokens and lets the model regenerate across the boundary.
Special tokens are control characters. Strings like <|endoftext|> map to reserved IDs (100257 in cl100k_base, 199999 in o200k_base) that mark document and message boundaries. By default tiktoken refuses to encode them from ordinary text: encode("hi <|endoftext|>") raises an error. Keep that protection on, so pasted text can't forge a document or message boundary.
Key Takeaways
- LLMs read integers, not letters. A tokenizer turns text into IDs before the model runs, and anything inside a token, like its spelling, is invisible unless the model memorized it.
- BPE builds the vocabulary by merging the most frequent adjacent pairs, and byte-level BPE guarantees that any input can be encoded with no "unknown" token.
- Tokenization explains classic failure modes: letter counting, multi-digit arithmetic (75.6% vs 97.8% for GPT-3.5 depending only on commas), higher costs for some languages (up to 15× longer encodings) and glitch tokens.
- The fix is usually to change the representation or hand off the work: spell words out, format numbers deliberately, and call code for anything that must be exact.
- Measure with the actual tokenizer. "4 characters per token" is a rough English-only average, not a budget.
Further Reading (sources used)
- Sennrich, Haddow and Birch, Neural Machine Translation of Rare Words with Subword Units (ACL 2016), the paper that brought BPE to NLP
- Hugging Face LLM Course, Byte-Pair Encoding tokenization, with a worked example and a from-scratch implementation
- OpenAI, tiktoken and its encoding definitions (pre-split regexes, vocabularies, special tokens)
- Singh and Strouse, Tokenization counts: the impact of tokenization on arithmetic in frontier LLMs (2024)
- Petrov et al., Language Model Tokenizers Introduce Unfairness Between Languages (2023)
- Rumbelow and Watkins, SolidGoldMagikarp (plus, prompt generation) (2023)
- Land and Bartolo, Fishing for Magikarp: Automatically Detecting Under-trained Tokens in Large Language Models (2024)
- Kudo, Subword Regularization (ACL 2018), and Kudo and Richardson, SentencePiece (2018)
- Meta AI, Introducing Meta Llama 3 (128K vocabulary, up to 15% fewer tokens)
Try it yourself: pip install tiktoken, then tiktoken.get_encoding("o200k_base").encode(" strawberry") and see what you get.
---
Originally published on AI Engineering Illustrated. Subscribe for new deep dives.
Mentions
マルチモーダルLLMの個人メモ②Tokenization はじめに この記事はAIで作成されています。multi-modal LLM関連のメモを記載しています。 目次 - Positonal Encoding - [Tokenization(今回)](#tokenization) - [①BPE(Byte Pair Encoding)](#-bpebyte-pair-encoding) - [②Byte-level BPE](#-byte-level-bpe) - [③WordPiece](#-wordpiece) Tokenization LLM は、入力されたテキストをそのまま処理するのではなく、テキストをトークンに分割し、それぞれをトークン ID に変換して処理する。この処理をトークナイズ(Tokenization)と呼ぶ。LLM では、単語をそのままトークンにするのではなく、単語より小さなサブワード単位に分割する方式が広く使われている。 代表的な方式には、次のものがある。 | 方式 | 語彙の構築方法 | 基本的な考え方 | 代表例・関連技術 | | ---------------- | ------------------ | ------------------- | --------------------------- | | ① BPE | トークンをマージして語彙を増やす | 頻出する隣接ペアを統合する | GPT 系の各種トークナイザー | | ② Byte-level BPE | バイトをマージして語彙を増やす | バイト列に BPE を適用する | GPT-2、tiktoken | | ③ WordPiece | スコアに基づいて語彙を構築する | トークンの組み合わせの有用性を評価する | BERT | | ④ Unigram | 大きな候補語彙からトークンを削除する | 確率モデルでテキストを説明する | SentencePiece の Unigram モデル | なお、BPE、WordPiece、Unigram はトークナイズのアルゴリズムであり、SentencePiece や tiktoken はトークナイザーの実装・ライブラリである。これらは同じ階層の概念ではない。 現在の主流 LLM では、BPE 系のトークナイザーが広く使われている。一方、BERT 系では WordPiece、SentencePiece を利用するモデルでは BPE や Unigram などが使われている。トークナイザーの設計は、語彙サイズ、入力系列の長さ、多言語対応、未知の文字列への対応などに影響する。 ① BPE(Byte Pair Encoding) 論文名: Neural Machine Translation of Rare Words with Subword Units URL: arxiv.org 発表日: 2015-08-31 ✅ ポイント - 隣接するトークンのペアを出現頻度に基づいてマージし、新しいトークンを作るアルゴリズム。 - 最初は文字などの小さな単位から始め、頻出ペアを統合することで語彙を増やしていく。 - マージのたびに新しいトークンが語彙に追加される。 - 目標の語彙サイズやマージ回数に達するまで、この処理を繰り返す。 - 学習時に獲得したマージ規則を使い、推論時のテキストをトークンに分割する。 🧪 学習の疑似コード ```python # 1. コーパスを読み込む corpus = load_corpus() # 2. 初期トークン列を作成する(例:文字やバイト単位) tokens = initialize_tokens(corpus) # 3. 初期語彙を作成する vocabulary = initialize_vocabulary(tokens) # 4. マージ規則を初期化する merge_rules = [] # 5. 語彙サイズが目標に達するまで学習する while len(vocabulary) < target_vocab_size: # 隣接するトークンペアの出現頻度を集計 pair_counts = count_adjacent_pairs(tokens) # 最も頻繁に出現するペアを選択 best_pair = argmax(pair_counts) # ペアをマージして新しいトークンを作成 new_token = merge_pair(best_pair) # 語彙に新しいトークンを追加 vocabulary.add(new_token) # マージ規則を追加(学習順が優先順位になる) merge_rules.append(best_pair) # コーパス内の該当するペアを置換 tokens = replace_pair(tokens, best_pair, new_token) # 6. 学習済みの語彙とマージ規則を保存する save_tokenizer(vocabulary, merge_rules) 実際の実装では、単語境界を扱う前処理や、マージの優先順位を管理する仕組みなども必要になる。 🔎 推論時の疑似コード text = input_text() # 学習済みの語彙とマージ規則を読み込む vocabulary, merge_rules = load_tokenizer() # テキストを初期単位に分割 tokens = initialize_tokens(text) # 学習済みのマージ規則を適用 tokens = apply_merge_rules(tokens, merge_rules) # トークンを ID に変換 token_ids = convert_to_ids(tokens, vocabulary) 推論時には、通常、語彙やマージ規則を再学習しない。学習済みの規則を使ってトークン化する。 🧮 BPE の処理イメージ コーパスに次の単語があるとする。 low lower lowest 最初は文字単位に分割する。 l o w l o w e r l o w e s t 例えば、l と o が頻出ペアであれば、これらをマージする。 lo w lo w e r lo w e s t 次に lo と w をマージする。 low low e r low e s t さらにマージを繰り返すことで、lower や lowest などが1つのトークンになる場合もある。 このように、頻出する短い単位を段階的にまとめて、より長いトークンを作る。 ❓ よくある質問 Q. なぜ頻出ペアをマージするの? - 頻繁に現れる文字列を1つのトークンにまとめることで、トークン数を減らしやすくするため。 - 一般的な単語やよく使われる文字列を効率的に表現できるようにするため。 Q. BPE は単語単位で処理するの? - 実装によって前処理は異なるが、一般的な BPE では、単語などの単位に分けてから、その内部をサブワードに分割することがある。 - Byte-level BPE では、バイト列を基本単位として扱う。 Q. 語彙サイズはどのように決まるの? - 初期語彙に、マージで追加されるトークンを加えていく。 - 目標の語彙サイズに達するまでマージを繰り返す。 - 実際の語彙サイズは、モデルの設計によって決まる。 Q. BPE は未知語に対応できるの? - 文字などの初期単位に分解できれば、未知の単語でも既存の単位の組み合わせとして表現できる。 - ただし、初期単位にない文字の扱いは実装に依存する。この問題に対応しやすい方式が Byte-level BPE である。 --- ② Byte-level BPE 代表例: GPT-2、tiktoken 系のトークナイザー GPT-2 論文: Language Models are Unsupervised Multitask Learners URL: cdn.openai.com 発表日: 2019-02-14 ✅ ポイント - 通常の BPE が文字などを初期単位として扱うのに対し、Byte-level BPE はバイトを初期単位として扱う。 - UTF-8 などのバイト列に変換してから BPE を適用する。 - 基本のバイト値は 256 種類であり、すべての Unicode 文字を初期語彙に登録する必要がない。 - バイト列に変換できるため、多言語のテキストや未知の文字列を扱いやすい。 - GPT-2 が採用した代表的なトークナイズ方式である。 🧪 学習の疑似コード # テキストを読み込む corpus = load_corpus() # テキストをバイト列に変換 byte_sequences = encode_utf8(corpus) # 256 種類のバイト値を初期語彙とする vocabulary = initialize_byte_vocabulary() # あとはBPEと同様 ※ 実際の実装では、正規表現などによる前処理や、特殊トークンの管理なども行う。 🔎 推論時の疑似コード text = input_text() # 学習済みのバイト語彙とマージ規則を読み込む vocabulary, merge_rules = load_tokenizer() # UTF-8 のバイト列に変換 byte_sequence = encode_utf8(text) # あとはBPEと同様 🧮 バイト列への変換例 ASCII 文字の a は UTF-8 では1バイトである。 a → 61 一方、日本語の あ は UTF-8 では3バイトである。 あ → E3 81 82 ※ 数値は16進数表記。 Byte-level BPE では、このようなバイト列を基本単位として扱う。 例えば、日本語の文字が初期語彙に登録されていなくても、バイト列に変換できれば、そのバイトの組み合わせとして表現できる。 ただし、実際のトークナイザーでは、マージ規則や前処理によって最終的なトークンの分割結果が異なる。 ❓ よくある質問 Q. なぜバイト単位にするの? - Unicode の文字をすべて初期語彙に用意する必要がないため。 - 256 種類のバイト値を基本単位にできるため。 - 初期語彙にない文字列でも、バイト列に変換して表現しやすいため。 Q. どんな文字列でも処理できるの? - UTF-8 などでバイト列に変換できる文字列に幅広く対応できる。 - ただし、入力の正規化や特殊トークンの扱いなどは別途考慮する必要がある。 - また、処理できることと、少ないトークン数で効率よく表現できることは別である。 Q. 通常の BPE との違いは? - 通常の BPE は文字などを初期単位にする場合がある。 - Byte-level BPE はバイトを初期単位にする。 - そのため、Byte-level BPE は未知の文字列に対応しやすい。 Q. GPT-2 と tiktoken は同じもの? - GPT-2 は言語モデルであり、Byte-level BPE を採用している。 - tiktoken は高速な BPE 系トークナイザーを提供するライブラリである。 - どちらもバイトレベルのトークナイズに関連するが、語彙やマージ規則が同じとは限らない。 --- ③ WordPiece 代表例: BERT 論文名: BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding URL: arxiv.org 発表日: 2018-10-11 ✅ ポイント - BPE と同様に、単語をサブワードに分割するための方式。 - BPE が基本的に隣接ペアの出現頻度を基準にマージするのに対し、WordPiece はペアの有用性を評価するスコアを使う。 - スコアの考え方の一例として、ペアの出現頻度を個々のトークンの出現頻度で正規化する方法がある。 - BERT 系のトークナイザーで使われる。 - ##ing のように、単語の途中に現れるサブワードを ## 付きで表現することがある。 🧪 学習の疑似コード # コーパスを読み込む corpus = load_corpus() # 初期語彙を作成 vocabulary = initialize_vocabulary(corpus) while len(vocabulary) < target_vocab_size: # 隣接ペアの統計を集計 pair_counts = count_adjacent_pairs(corpus) # ペアのスコアを計算 scores = calculate_wordpiece_scores( pair_counts, vocabulary ) # スコアに基づいて候補を選択 best_pair = select_best_pair(scores) # ペアをマージして新しいトークンを追加 vocabulary.add(merge_pair(best_pair)) # コーパス内の該当するペアを置換 corpus = replace_pair(corpus, best_pair) save_tokenizer(vocabulary) これは、スコアに基づいて語彙を構築する考え方を示した疑似コードである。実際の WordPiece の学習アルゴリズムやスコアの定義は、実装によって異なる。 🔎 推論時の疑似コード text = input_text() # 学習済みの語彙を読み込む vocabulary = load_vocabulary() # テキストを単語などの単位に分割 words = preprocess(text) token_ids = [] for word in words: # 語彙を使って単語をサブワードに分割 subwords = wordpiece_tokenize(word, vocabulary) # サブワードを ID に変換 token_ids.extend(convert_to_ids(subwords, vocabulary)) 一般的な WordPiece の推論処理では、学習済みの語彙を使い、単語を表現できるサブワード列に分割する。 BERT 系の実装では、未知語を [UNK] として扱う場合もある。 🧮 WordPiece のスコア ペアの有用性を説明するための近似的なスコアとして、次の式を考える。 $$ \operatorname{score}(x,y)= \frac{f(xy)}{f(x)f(y)} $$ ここで、 - (f(x)):トークン (x) の出現頻度 - (f(y)):トークン (y) の出現頻度 - (f(xy)):(x) と (y) が隣接して現れる頻度 このスコアでは、単にペアが頻繁に出現するかだけでなく、個々のトークンの出現頻度に対して、ペアがどれくらい強く結びついているかを評価できる。 例えば、次の2つのペアがあるとする。 | ペア | ペアの出現頻度 | 個々のトークンの頻度 | スコア | | --------- | ------: | ---------: | ------: | | a + b | 100 | 1000、1000 | 0.0001 | | c + d | 30 | 40、40 | 0.01875 | この例では、a + b のほうが頻出するが、正規化したスコアでは c + d のほうが高くなる。 つまり、単純な出現頻度ではなく、個々のトークンの頻度に対する相対的な結びつきを評価するという考え方である。 なお、この式は WordPiece の考え方を説明するための近似例であり、すべての実装で使用される厳密なスコア式ではない。 🧮 ##ing の意味 WordPiece では、単語の先頭以外に現れるサブワードを ## 付きで表現することがある。 例えば、 playing ↓ play + ##ing この場合、 - play:単語の先頭に現れるサブワード - ##ing:単語の途中に続くサブワード を表す。 ## はトークンの表記上の記法であり、元のテキストに含まれる文字列ではない。 ❓ よくある質問 Q. BPE との違いは? - BPE は基本的に、隣接ペアの出現頻度に基づいてマージする。 - WordPiece は、ペアの有用性を表すスコアに基づいて語彙を構築する。 - ただし、両者ともサブワードを利用し、語彙を構築する方式である。 Q. なぜ ## を付けるの? - 単語の先頭に現れるトークンと、単語の途中に現れるトークンを区別するため。 - 例えば play と ##ing を区別することで、単語の内部構造を表現できる。 Q. WordPiece は未知語に対応できるの? - 単語を既知のサブワードに分割できれば、未知の単語にも対応できる。 - ただし、分割できない場合は [UNK] などで扱う実装もある。 Q. WordPiece は BERT 専用なの? - WordPiece はトークナイズの方式であり、BERT 以外でも利用できる。 - ただし、BERT が代表的な採用例としてよく知られている。 --- ④ Unigram Language Model 代表例: SentencePiece の Unigram モデル 関連 URL: google/sentencepiece ✅ ポイント - BPE のようにペアを繰り返しマージするのではなく、大きな候補語彙から不要なトークンを削除していく方式。 - テキストの分割方法を最初から1つに固定せず、複数の分割候補に確率を割り当てる。 - 各トークンの確率を学習し、コーパス全体をうまく説明できる語彙を構築する。 - トークンを削除した場合に、コーパスの説明能力がどの程度低下するかを評価する。 - 確率推定には EM アルゴリズムなどが利用される。 🧪 学習の疑似コード # コーパスを読み込む corpus = load_corpus() # 大きめの候補語彙を作成 vocabulary = initialize_large_vocabulary(corpus) # 初期確率を設定 probabilities = initialize_probabilities(vocabulary) while len(vocabulary) > target_vocab_size: # 現在の語彙で分割候補の確率を評価 segmentation_probs = evaluate_segmentations( corpus, vocabulary, probabilities ) # EM などを使ってトークンの確率を学習 probabilities = estimate_probabilities( segmentation_probs ) # 各トークンを削除した場合の影響を評価 removal_scores = evaluate_token_removal( corpus, vocabulary, probabilities ) # 説明能力への影響が小さいトークンを削除 candidates = select_removable_tokens(removal_scores) vocabulary = remove_tokens(vocabulary, candidates) # 残った語彙で確率を再学習 probabilities = reestimate_probabilities( corpus, vocabulary ) save_tokenizer(vocabulary, probabilities) 実際のアルゴリズムでは、削除候補の選択数や確率の再推定方法などが定められている。このコードは、処理の流れを示す概念的な疑似コードである。 🔎 推論時の疑似コード text = input_text() # 学習済みの語彙と確率を読み込む vocabulary, probabilities = load_tokenizer() # 入力テキストに対する分割候補を生成 candidates = generate_segmentations(text, vocabulary) # 各分割候補の確率を計算 scores = calculate_segmentation_scores( candidates, probabilities ) # 最も確率の高い分割を選択 best_segmentation = select_best_segmentation(scores) # トークン ID に変換 token_ids = convert_to_ids( best_segmentation, vocabulary ) 推論時には、学習済みの語彙と確率を使って、入力テキストを適切なトークン列に分割する。 実際の実装では、動的計画法などを利用して最適な分割を効率的に求める。 🧮 Unigram の確率モデル テキストの分割候補を (S) とする。 各トークン (t) の確率を (p(t)) とすると、分割候補の確率は次のように表せる。 $$ P(S)=\prod_{t\in S}p(t) $$ 例えば、unhappiness に対して、次の分割候補があるとする。 候補 A: un + happiness 候補 B: un + happi + ness それぞれの分割候補の確率は、 $$ P(A)=p(\text{un})p(\text{happiness}) $$ $$ P(B)=p(\text{un})p(\text{happi})p(\text{ness}) $$ となる。 Unigram では、このような複数の分割候補を確率モデルによって評価する。 実際の学習では、コーパス中の文字列に対して可能な分割を考慮し、全体の尤度が高くなるように語彙と確率を調整する。 🧮 EM アルゴリズムとの関係 Unigram では、EM アルゴリズムなどを使ってトークンの確率を学習する。 - E ステップ:現在のモデルに基づいて、各分割候補の確率や期待出現回数を評価する。 - M ステップ:その結果を使って、各トークンの確率を更新する。 この処理を繰り返すことで、コーパスを説明する確率モデルを学習する。 ただし、EM アルゴリズム自体が、尤度の低いトークンを直接削除するわけではない。 確率モデルの学習と、語彙からトークンを削除する処理は別のステップである。 🧮 語彙削減の考え方 Unigram では、あるトークンを削除した場合に、コーパスの説明能力がどの程度低下するかを評価する。 例えば、 候補語彙: ["un", "happiness", "happi", "ness", "happy"] この中から happi を削除したとしても、別のトークンの組み合わせで同じ文字列を十分に表現できるなら、説明能力の低下は小さい可能性がある。 一方、削除によって多くの文字列をうまく分割できなくなる場合は、説明能力が大きく低下する可能性がある。 このように、トークン単体の出現頻度だけでなく、削除によるコーパス全体への影響を考慮することが重要である。 ❓ よくある質問 Q. BPE との違いは? - BPE は頻出ペアをマージして語彙を増やす。 - Unigram は大きな候補語彙からトークンを削除して語彙を小さくする。 - Unigram は、複数の分割候補を確率的に評価する。 Q. なぜ最初に大きな語彙を用意するの? - 多くの分割候補を最初から用意しておくことで、コーパスを説明するための選択肢を確保するため。 - その後、不要な候補を削除することで、効率的な語彙を構築する。 Q. EM アルゴリズムは何をしているの? - 主に、分割候補を考慮しながらトークンの確率を学習する。 - 語彙の削減は、トークンを削除した場合の説明能力の変化を評価して行う。 Q. Unigram は分割方法をランダムに決めるの? - 確率モデルでは複数の分割候補を扱う。 - 推論時には、最も確率の高い分割を選ぶ方式や、確率に基づいて分割をサンプリングする方式などがある。 - したがって、学習時に複数の候補を扱うことと、推論時に必ずランダムに分割することは別である。
Why Can't LLMs Count the R's in "Strawberry"? How Tokenizers Work --- title: "Why Can't LLMs Count the R's in \"Strawberry\"? How Tokenizers Work" published: true description: "The preprocessing step behind spelling slips, shaky arithmetic, glitch tokens and your API bill" tags: ai, llm, machinelearning, nlp canonical_url: ragmehta.substack.com --- The preprocessing step behind spelling slips, shaky arithmetic, glitch tokens and your API bill Ask a language model how many r's are in "strawberry" and, for a long time, the popular answer was "two." The same models could write a sonnet about strawberries, explain their genetics and translate the word into forty languages. So why does counting letters go wrong? The model never sees the letters. Before your prompt reaches the first transformer layer, a separate program called the tokenizer chops the text into chunks and swaps each chunk for an integer. The model only ever reads those integers. If "strawberry" becomes a single integer, the three r's are hidden inside a number the model can't take apart. Tokenization is the least glamorous part of the LLM stack, and it causes a surprising number of problems: shaky multi-digit arithmetic, higher costs for non-English users, strange "glitch" words that make models act up, and token counts that don't match your character-based estimates. In this issue we'll open the tokenizer up: how it turns text into IDs, how Byte Pair Encoding (BPE) learns its vocabulary, and how its choices show up as spelling, arithmetic, language and glitch-token problems you can design around. Every token split in this issue was measured with OpenAI's open-source tiktoken library on the actual GPT-2, GPT-4 (cl100k_base) and GPT-4o (o200k_base) vocabularies, so you can reproduce them yourself. Tokens: the Model's Only Alphabet A neural network can't do math on strings. It needs numbers. The simplest scheme would give every character a number, but then a 2,000-word document becomes more than 10,000 positions, and every transformer layer pays for each one. The other extreme gives every word a number, but no fixed list covers every word, name, typo, URL and code identifier people will ever type. Rico Sennrich and colleagues measured this trade-off in their 2016 paper Neural Machine Translation of Rare Words with Subword Units. On a German training corpus of about 100 million words, keeping whole words needed 1.75 million vocabulary entries and still left 1,079 unknown words in the test set. Splitting into characters needed only about 3,000 symbols but stretched the corpus to 550 million positions. Their subword method, BPE, landed in between: about 63,000 symbols, 112 million positions and zero unknown words. That middle ground is a token: a chunk of text that's usually shorter than a word for rare words and exactly one word for common ones. Modern LLMs use vocabularies of 50,000 to 200,000 tokens. tiktoken's README puts the average at about 4 bytes of text per token. On the English text of last week's issue I measured 4.4 characters per token with the GPT-4o tokenizer, or roughly 0.63 words per token. From Text to Token IDs in Five Steps !From raw text to token IDs: pre-split, apply BPE merges, look up IDs, then embed From raw text to token IDs: pre-split, apply BPE merges, look up IDs, then embed Here's what happens to the sentence "I love strawberry" on its way into GPT-4o: 1. Raw text. Your string arrives as UTF-8 bytes. Some tokenizers normalize it first (Unicode normalization, lowercasing in older models), but GPT-style tokenizers keep the bytes exactly as typed. 2. Pre-split. A regular expression cuts the text into pieces that merges are never allowed to cross: letter runs, digit runs, punctuation, whitespace. The tiktoken source shows the patterns. GPT-2's version keeps a leading space attached to the following word, so the pieces are I, love, strawberry. That's why the dot in the figure matters: " strawberry" with a space and "strawberry" without one are different tokens. 3. BPE merges. Each piece starts as a sequence of bytes. The tokenizer then applies its learned merge rules, in the order they were learned, gluing adjacent pairs together until no rule applies. Common words collapse into one token. Rare ones end up as a few subword chunks. 4. Token IDs. Each final chunk is looked up in the vocabulary. With o200k_base, "I love strawberry" becomes [40, 3047, 101830]: three integers. 5. Embeddings. Inside the model, each ID selects one row of a learned embedding matrix, a vector of a few thousand numbers. From here on, the transformer only ever works with those vectors. Two properties fall out of this design, and the tiktoken README calls both out. Tokenization is lossless: decoding the IDs gives back the exact original bytes. And it works on any text, even strings that never appeared when the tokenizer was trained. The second property depends on a trick we'll get to in a moment. How BPE Learns Its Vocabulary Byte Pair Encoding started life as a data compression algorithm (Philip Gage, 1994): repeatedly find the most common pair of adjacent bytes and replace it with a new, unused symbol. Sennrich et al. adapted the idea to build a vocabulary instead of compressing a file. Training goes like this: 1. Split every word in the training corpus into characters. The starting vocabulary is just those characters. 2. Count every pair of adjacent symbols, weighted by how often each word appears. 3. Merge the most frequent pair into a new symbol, add it to the vocabulary, and record the rule. 4. Repeat until the vocabulary reaches the target size. The number of merges is the only real setting. As the paper puts it, the final vocabulary size equals the starting symbols plus the number of merge operations. !BPE training: count adjacent pairs, merge the most frequent, repeat until the vocabulary is full BPE training: count adjacent pairs, merge the most frequent, repeat until the vocabulary is full The diagram uses the toy corpus from the Hugging Face LLM course: "hug" (10 times), "pug" (5), "pun" (12), "bun" (4) and "hugs" (5). - Start: the vocabulary is b g h n p s u. - Merge 1: the pair u + g appears 20 times (10 in hug, 5 in pug, 5 in hugs), more than any other, so ug becomes a token. - Merge 2: u + n now leads with 16 (12 in pun, 4 in bun), so un joins. - Merge 3: h + ug appears 15 times, and the first three-letter token, hug, is born. Real tokenizers run this loop tens of thousands of times over gigabytes of text. GPT-2's vocabulary has 50,257 entries. GPT-4's cl100k_base has about 100,000 and GPT-4o's o200k_base about 200,000 (100,277 and 200,019 including special tokens, according to tiktoken). Meta says Llama 3 moved to a 128K-token vocabulary that yields up to 15% fewer tokens than Llama 2 on the same text. Encoding new text replays the merges. Split the input into characters (or bytes), then apply the learned rules in priority order. A word seen often during training collapses into one token. A word never seen still gets encoded, just in smaller pieces. With GPT-4o's vocabulary, "unbelievably" becomes un | bel | ievably. The order of merges also explains some quirks. Merges reflect the tokenizer's training data, which isn't necessarily the model's training data. Keep that in mind; it comes back when we get to glitch tokens. Byte-Level BPE: No More "Unknown Word" Classic BPE starts from characters, and Unicode defines roughly 150,000 of them. Any character missing from the training corpus becomes [UNK], the "unknown" token. The Hugging Face course notes this is one reason older NLP models handled emoji badly. GPT-2 introduced a simple fix: run BPE over bytes instead of characters. There are only 256 possible byte values, so the base vocabulary has 256 entries and every possible input, whether emoji, Tamil script or binary garbage, can be written in them. Merges then build up common multi-byte sequences. As the course puts it, GPT-2 doesn't actually have an unknown token, because an unknown character can't occur with byte-level BPE. You can see the byte fallback at work. In GPT-2's vocabulary, the emoji 🙂 (four UTF-8 bytes) becomes 2 tokens. GPT-4o's larger vocabulary learned the whole sequence, so it's 1 token. Either way, nothing is ever "unknown." The price is that a rarely seen script can get chopped into fragments that aren't even whole characters, which leads to the language tax below. Why the Model Can't See the R's Back to the strawberry. Here's what GPT-4o's tokenizer does with the classic question: !The model sees “ strawberry” as one ID with no letters inside; spelling it out or calling code exposes the letters The model sees “ strawberry” as one ID with no letters inside; spelling it out or calling code exposes the letters "How many r's are in strawberry?" becomes 8 tokens: How | ·many | ·r | 's | ·are | ·in | ·strawberry | ?. The word you care about is token #101830. To the model, that's one opaque symbol, mapped to one embedding vector. There's no "r" in it to count. Whatever the model knows about how strawberry is spelled, it had to memorize from training text where the word happened to be spelled out, as in dictionaries, spelling-bee pages or kids' worksheets. That knowledge is thin and indirect, so the model guesses, and a plausible guess for a word ending in a double letter is "two." The split also depends on context in ways people don't expect: - strawberry (leading space, the usual case mid-sentence) is 1 token. - strawberry (no leading space, for example at the start of a message) is 3 tokens: st | raw | berry. - Strawberry (capitalized) is a different 3: Str | aw | berry. So the model may have learned slightly different things about the same word. The fixes all expose the letters. Writing the word as s-t-r-a-w-b-e-r-r-y turns it into 10 tokens, one per letter, and now counting is something the model can actually do. Better still, when exact character-level answers matter (counting, reversing a string, checking a regex), have the model call a code tool. One line of Python, "strawberry".count("r"), returns 3 every time. The Multilingual Token Tax The tokenizer's merges come from its training data, and that data is mostly English and code. English words get dedicated tokens. Other languages, especially ones with their own scripts, get fewer merges and are left closer to raw bytes. !The token tax: the same sentence costs more tokens in some languages, and numbers split into left-aligned chunks The token tax: the same sentence costs more tokens in some languages, and numbers split into left-aligned chunks I ran one short sentence, "The weather is nice today," and simple translations of it through three of OpenAI's tokenizers. In English, it's 6 tokens in all three. In Tamil, it's 85 tokens with GPT-2's tokenizer, 42 with GPT-4's and 11 with GPT-4o's. Hindi drops from 27 to 21 to 5, and Russian from 27 to 13 to 6. That's just one sentence, not a benchmark, but the direction is clear. Larger, more multilingual vocabularies have closed much of the gap, though not all of it. Aleksandar Petrov and colleagues studied this systematically in Language Model Tokenizers Introduce Unfairness Between Languages (2023). They found the same text can differ in token length by up to 15× across languages, and that even byte-level and character-level models show gaps of more than 4×. The consequences are concrete: - Cost: APIs bill per token. - Context: a 128K-token window holds far fewer pages of Tamil than of English. - Latency: output is generated one token at a time. If you serve a non-English market, measure token counts on your actual traffic. Don't assume the "about 4 characters per token" rule of thumb. Numbers: Why Long Addition Goes Wrong The same pre-split regex decides how numbers are chunked, and labs have made very different choices. Aaditya Singh and DJ Strouse surveyed them in Tokenization counts: the impact of tokenization on arithmetic in frontier LLMs (2024): - Pure BPE (GPT-3, Claude 2.1, OLMo): whichever digit strings happened to be frequent got tokens. 710 might be one token while 711 isn't, so numbers of the same length split inconsistently. - Single digits (PaLM, Llama 1 and 2, Mistral): every digit is its own token. - Chunks of up to 3 digits, left to right (GPT-3.5, GPT-4): the \p{N}{1,3} rule in the tiktoken pattern. GPT-4o's o200k_base uses the same rule. Left-to-right chunking means 1234567 becomes 123 | 456 | 7. The chunks line up with the start of the number, while place value, and the way carries move when you add, runs from the end. Insert commas and the tokenizer splits on them, giving 1 | , | 234 | , | 567. Now every chunk sits on a thousands boundary. Singh and Strouse tested exactly this on GPT-3.5 with 7- to 9-digit addition. With plain digits, 8-shot accuracy was 75.6%. With commas, it was 97.8%. When the answer had more digits than the inputs (because of a final carry), plain-digit accuracy fell to 8.25%, and the errors weren't random: the model almost always got the first three digits right and the fourth digit wrong, right at the first token boundary. Asking the model to first rewrite the problem with commas recovered most of the lost accuracy. GPT-4 was less affected, but the effect didn't disappear. The lesson isn't "add commas everywhere." It's that number formatting changes the problem the model has to solve. For anything that must be exact, like invoices or financial math, route the arithmetic to a calculator or code tool. Glitch Tokens: When the Vocabulary Outlives the Data In early 2023, Jessica Rumbelow and Matthew Watkins went looking at GPT-2 and GPT-3's token embeddings and found a cluster of strange entries: SolidGoldMagikarp, TheNitromeFan, cloneembedreportprint, petertodd. In their LessWrong write-up, they showed that asking a model to simply repeat these strings produced evasion, insults or substitutions. ChatGPT, for example, repeated " SolidGoldMagikarp" as "distribute." Some of these tokens even made GPT-3 give different answers at temperature 0, which is supposed to be deterministic. Their partial explanation goes back to the point about merges. The tokenizer was trained on scraped web text that included Reddit usernames, e-commerce site internals and game logs. The model was trained on more heavily curated text. So strings like SolidGoldMagikarp (I confirmed it's a single token, #43453, in GPT-2's vocabulary) earned a vocabulary slot but almost never showed up during model training. Their embedding vectors stayed close to wherever they started, and the model doesn't know what to do when it meets one. It's not just a GPT-2 curiosity. Fishing for Magikarp (Land and Bartolo, 2024) built automatic detectors that combine tokenizer analysis, weight-based signals and prompting, and found under-trained tokens across a wide range of models. In newer OpenAI vocabularies, " SolidGoldMagikarp" is 5 ordinary tokens (·Solid | Gold | Mag | ik | arp), but each new tokenizer can bring its own oddities. If you train or fine-tune models, scanning for under-trained tokens is cheap insurance. The Other Tokenizer Families BPE dominates today's LLMs, but you'll run into two other algorithms, often through one library: - WordPiece (developed by Google for BERT) also learns merges bottom-up, but according to the Hugging Face course it scores a pair as its frequency divided by the product of its parts' frequencies, which favors pairs whose parts are rare on their own. It keeps only the final vocabulary and encodes each word by taking the longest piece it knows, then the next. Pieces inside a word carry a ## prefix, as in token ##ization. (Google never open-sourced its training code, so the course describes a best-guess reconstruction.) - Unigram LM, from Taku Kudo's Subword Regularization (2018), works top-down. It starts with a large candidate vocabulary, scores how much each piece contributes to a probabilistic model of the corpus, and prunes the least useful pieces. Since the same word can be segmented several ways, Kudo trained translation models on sampled segmentations as a kind of noise, which made them more robust, especially in low-resource and out-of-domain settings. - SentencePiece (Kudo and Richardson, 2018) is a library rather than an algorithm. It implements both BPE and Unigram and trains directly on raw sentences, with no language-specific pre-splitting. Whitespace becomes the visible symbol ▁, so I saw a girl becomes ▁I ▁saw ▁a ▁girl (example from the SentencePiece README). That makes it a natural fit for languages that don't put spaces between words, like Japanese and Chinese. Picking a Vocabulary Size Why not a million tokens? Every entry costs a row in the input embedding matrix and another in the output layer that scores the next token (unless the two are tied). With 4,096-dimensional vectors, a 200,000-entry vocabulary is about 820 million parameters per matrix. More entries also means fewer training examples per rare token, which is the glitch-token problem again. Smaller vocabularies mean longer sequences: attention costs more, the context window fills faster and generation takes more steps. Recent models have moved toward larger vocabularies (50K, then 100K, 128K and 200K) as the embedding table becomes a small share of total parameters. Meta credits Llama 3's 128K tokenizer with "substantially improved model performance." A Cheat Sheet for Engineers !Common tokenizer gotchas, their causes and practical fixes Common tokenizer gotchas, their causes and practical fixes A few of these deserve a sentence more. Count tokens with the real tokenizer. Use tiktoken.encoding_for_model("gpt-4o") for OpenAI models, or your provider's token-counting endpoint, before you enforce context limits or estimate costs. Character-based estimates are wrong for code, non-English text and JSON. Watch the boundary at the end of your prompt. Because GPT-style tokenizers attach the space to the following word, " Paris" is a single token. A prompt that ends with a trailing space ("The capital of France is ") leaves a bare space token, and the model then has to continue with "Paris" without its usual leading space, an unusual sequence it saw less often in training. Trim trailing whitespace from completion-style prompts. The "token healing" technique cited by Singh and Strouse goes further: it backs up a few tokens and lets the model regenerate across the boundary. Special tokens are control characters. Strings like <|endoftext|> map to reserved IDs (100257 in cl100k_base, 199999 in o200k_base) that mark document and message boundaries. By default tiktoken refuses to encode them from ordinary text: encode("hi <|endoftext|>") raises an error. Keep that protection on, so pasted text can't forge a document or message boundary. Key Takeaways - LLMs read integers, not letters. A tokenizer turns text into IDs before the model runs, and anything inside a token, like its spelling, is invisible unless the model memorized it. - BPE builds the vocabulary by merging the most frequent adjacent pairs, and byte-level BPE guarantees that any input can be encoded with no "unknown" token. - Tokenization explains classic failure modes: letter counting, multi-digit arithmetic (75.6% vs 97.8% for GPT-3.5 depending only on commas), higher costs for some languages (up to 15× longer encodings) and glitch tokens. - The fix is usually to change the representation or hand off the work: spell words out, format numbers deliberately, and call code for anything that must be exact. - Measure with the actual tokenizer. "4 characters per token" is a rough English-only average, not a budget. Further Reading (sources used) - Sennrich, Haddow and Birch, Neural Machine Translation of Rare Words with Subword Units (ACL 2016), the paper that brought BPE to NLP - Hugging Face LLM Course, Byte-Pair Encoding tokenization, with a worked example and a from-scratch implementation - OpenAI, tiktoken and its encoding definitions (pre-split regexes, vocabularies, special tokens) - Singh and Strouse, Tokenization counts: the impact of tokenization on arithmetic in frontier LLMs (2024) - Petrov et al., Language Model Tokenizers Introduce Unfairness Between Languages (2023) - Rumbelow and Watkins, SolidGoldMagikarp (plus, prompt generation) (2023) - Land and Bartolo, Fishing for Magikarp: Automatically Detecting Under-trained Tokens in Large Language Models (2024) - Kudo, Subword Regularization (ACL 2018), and Kudo and Richardson, SentencePiece (2018) - Meta AI, Introducing Meta Llama 3 (128K vocabulary, up to 15% fewer tokens) Try it yourself: pip install tiktoken, then tiktoken.get_encoding("o200k_base").encode(" strawberry") and see what you get. --- Originally published on AI Engineering Illustrated. Subscribe for new deep dives.