3.6 Tokenization:BPE、SentencePiece、Tiktoken
Tokenization 是把文字切成模型可處理的 token 序列的過程。看似簡單的「切字」實際上有完整算法、且 tokenizer 的選擇深刻影響模型能力、context window 利用率、跨語言表現、跟一些奇怪 bug 的成因(GPT 在某些字串上表現異常的「glitch tokens」就源於 tokenizer 設計)。
本章拆開四個主流 tokenization 算法(BPE、WordPiece、Unigram、SentencePiece)、解釋 vocabulary 怎麼學出來、為什麼中文 / 中日韓字幾乎一字一 token、tokenizer 為什麼影響 speculative decoding 的相容性。
本章目標
讀完本章後、你應該能:
- 解釋 BPE(Byte-Pair Encoding)的工作原理。
- 看到不同 model 切同段文字得到不同 token 數時、知道原因。
- 解釋為什麼 drafter 跟 target 必須共用 tokenizer。
- 看到 vocab_size = 256,000 vs 128,256 時、知道差異在哪。
Tokenization 的設計目標
理想 tokenizer 要同時滿足:
- 覆蓋率高:能 encode 任何文字、不會「碰到沒見過的字壞掉」。
- 效率高:常見字串切成少數 token、節省 context 與計算。
- 語意保留:保留有意義的 sub-word 邊界(「unhappy」切成
un+happy比unh+appy好)。 - 跨語言公平:英文跟中文 / 日文 / 阿拉伯文等都用合理數量的 token。
不同算法在這四個目標上有不同取捨。
早期方法:word-level 跟 char-level
Word-level Tokenization
最簡單的方法是「用空白跟標點切」、每個 word 一個 token。
優點:直觀。
缺點:
- Vocabulary 爆炸:英文有幾百萬個 word forms(含複數、時態、複合詞等)。
- OOV(out-of-vocabulary):新詞、typo、URL、混合語言完全壞掉。
- 中文 / 日文沒有空白:要先做 word segmentation。
現代 LLM 已淘汰 word-level、主流改用 subword 系列。
Char-level Tokenization
另一個極端是「每個 character 一個 token」。
優點:vocabulary 小、無 OOV。
缺點:序列變很長(一句話幾十到幾百 char、效率低)、模型要從很基礎學起、訓練不效率。
現代 LLM 也跳過純 char-level、改用 subword 折衷。
折衷:Subword Tokenization
主流方案是「subword tokenization」:常見字串當一個 token、罕見字串切成更小單位(甚至到 char 級別)。三個主流算法:
| 算法 | 模型例子 |
|---|---|
| BPE | GPT-2、GPT-3、GPT-4、Llama 系列 |
| WordPiece | BERT |
| SentencePiece | Gemma、PaLM、T5 |
Vocabulary size 跟 special tokens 是這幾個算法產出的 tokenizer 共同的概念維度。
BPE:Byte-Pair Encoding
BPE(Sennrich et al., 2016)的核心想法是「貪婪地合併最常出現的字元對」、迭代到 vocabulary 達到目標大小。
訓練流程
- 初始 vocabulary:所有 character。
- 統計訓練語料中、所有相鄰 character pair 的頻率。
- 把頻率最高的 pair 合併成一個新 token、加進 vocabulary。
- 用新 vocabulary 重新 tokenize 語料、重複 step 2-3。
- 直到 vocabulary 達到目標大小(如 50,000、100,000)。
例:
1初始:l o w e r → 5 個 token
2步驟 1:合併 'l' + 'o' = 'lo'、變成 lo w e r → 4 個 token
3步驟 2:合併 'lo' + 'w' = 'low'、變成 low e r → 3 個 token
4步驟 3:合併 'e' + 'r' = 'er'、變成 low er → 2 個 token訓練後、lower 就是 2 個 token。
Byte-level BPE
原始 BPE 在 character level 運作、但「character」依語言而異(Unicode 字元複雜)。Byte-level BPE 在 byte level 運作、任何文字都可以 encode 成 byte 序列、自然支援多語言。
GPT-2 / GPT-3 / GPT-4 / Llama 系列都用 byte-level BPE。
Tiktoken:OpenAI 的高效實作
Tiktoken 是 OpenAI 開源的 BPE 高效實作、Python 套件。可以拿來算「這段文字在 GPT-4 上是多少 token」:
1import tiktoken
2enc = tiktoken.encoding_for_model("gpt-4")
3tokens = enc.encode("Hello, world!")
4print(len(tokens)) # 4Tiktoken 是估算 OpenAI API 費用的標準工具。其他模型有各自的 tokenizer 套件(Llama 的 sentencepiece、Hugging Face 的 transformers.AutoTokenizer)。
WordPiece:BERT 的選擇
WordPiece(Schuster & Nakajima, 2012、後來 Google 用在 BERT)跟 BPE 類似、但合併策略不同:
- BPE:合併「最頻繁出現的 pair」。
- WordPiece:合併「合併後 likelihood 最大化的 pair」(更貴的計算、但理論上更好)。
實務差異微小。BERT 系列用 WordPiece、現代 LLM 大多回到 BPE 系列。
Unigram:機率式 subword
Unigram(Kudo, 2018)是另一條主流 subword 算法、跟 BPE 的「greedy 從下往上合併」相反、它從一個很大的 candidate vocabulary 開始、用機率模型逐步刪掉 likelihood 貢獻最小的 token:
- 起點:一個包含大量 candidate subword 的初始 vocab(可從訓練資料抓所有 substring)。
- 用 EM 算法估每個 candidate 的機率、把整段文字 tokenize 成 likelihood 最大的 segmentation。
- 評估「刪掉某個 candidate 後 total likelihood 損失多少」、刪掉損失最小的一批。
- 重複到 vocab 達目標大小。
跟 BPE 的本質差異:
- BPE:每個輸入文字只有一個切法(merge 規則決定)、結果是 deterministic。
- Unigram:每個輸入可能對應多個合法 segmentation、訓練時用機率挑、推論時取 top-1。這個性質讓 Unigram 天然支援 subword regularization(訓練時隨機取不同 segmentation、增強 robustness)。
Unigram 是 SentencePiece 預設算法、T5、Gemma 系列訓練時用。實務上 Unigram 跟 BPE 的最終 tokenization 接近、選擇看「訓練時要不要做 subword regularization」。
SentencePiece:Google 的開源實作
SentencePiece(Kudo & Richardson, 2018)是 Google 開源的 tokenization 套件、可實作 BPE 或 Unigram 算法、設計上:
- 語言無關:把輸入當 byte 流處理、不假設「word boundary 是空白」。
- 無前處理:不用先切 word、適合中文 / 日文等無空白語言。
- 可逆:tokenize → detokenize 完全還原原文。
Gemma 系列、PaLM、T5 用 SentencePiece。實務上跟 BPE 表現接近、差異主要在「對中日韓文等無空白語言更友善」。
Vocabulary 大小
各 LLM 的 vocabulary 大小:
| 模型 | vocab_size | Tokenizer |
|---|---|---|
| GPT-2 | 50,257 | byte-level BPE |
| GPT-3 / GPT-4 | ~100K | byte-level BPE (tiktoken) |
| Llama 2 | 32,000 | SentencePiece |
| Llama 3 | 128,256 | tiktoken-style BPE |
| Gemma 2 | 256,000 | SentencePiece |
| Gemma 3 | 262,144 | SentencePiece |
| Gemma 4 | 256,000 | SentencePiece |
| Qwen3 | 152,064 | byte-level BPE |
Vocabulary 大小的取捨:
| 大 vocab | 小 vocab |
|---|---|
| 同段文字切出 token 數少(context 利用率高) | 同段文字切出 token 數多(context 吃緊) |
| Embedding layer 跟 output projection 大 | Embedding 跟 output projection 小 |
| 多語言覆蓋好 | 多語言覆蓋差、可能切成 byte 級 |
| 中文 / 日文每字一 token | 中文 / 日文一字可能切 2 ~ 3 個 token |
Gemma 4 的 256K vocab 是現代 LLM 中較大的、目的之一是多語言支援。
同段文字在不同 tokenizer 上的差異
實測「The quick brown fox jumps over the lazy dog」:
| Tokenizer | Token 數 |
|---|---|
| GPT-4 | 9 |
| Llama 3 | 9 |
| Gemma 4 | 11 |
| Qwen3 | 10 |
差異不大。但中文「敏捷的棕色狐狸跳過懶狗」:
| Tokenizer | Token 數(估) |
|---|---|
| GPT-4 | 約 12 |
| Llama 2 | 約 20 (byte 級) |
| Llama 3 | 約 10 |
| Gemma 4 | 約 9 |
Llama 2 的 32K vocab 對中文支援差、Llama 3 / Gemma 4 改善很多。實務影響:中文 prompt 在 Llama 2 上吃 context 多、Gemma 4 較友善。
Tokenizer 跟模型相容性
Speculative decoding 要 target 跟 drafter 共用 tokenizer、因為兩者必須對「下個 token」的概念一致:
- Gemma 4 31B + Gemma 4 E4B:同 tokenizer、可以配對。
- Gemma 4 + Llama:不同 tokenizer、配不起來。
理解這點、能解釋為什麼 LM Studio 的 draft model UI 自動過濾相容候選、為什麼 Ollama 的 gemma4:31b-coding-mtp-bf16 model tag 內含 drafter 而不能自己組合不同家族。
Special Tokens
除了 vocabulary 中的「正常」token、還有特殊 token:
<BOS>/<bos>:Beginning of sequence、prompt 起點。<EOS>/<eos>:End of sequence、生成結束。<PAD>:Padding、batch 訓練時補齊長度。<UNK>:Unknown token(現代 BPE 少用、因為 byte-level 覆蓋所有字元)。<|im_start|>/<|im_end|>:ChatML 格式中區隔每段訊息的邊界 token。- ChatML 中的 role 名稱(
system/user/assistant)寫在<|im_start|>之後當作文字內容、不是獨立 token;模型靠「<|im_start|>+ 後接 role 字串」這個 pattern 識別說話者。
聊天 LLM 的 prompt 實際長相是用 special tokens 標記 role 跟訊息邊界,而非純文字:
1<|im_start|>system
2You are a helpful assistant.<|im_end|>
3<|im_start|>user
4Hello!<|im_end|>
5<|im_start|>assistant不同模型的 chat template 不同、Ollama / Continue.dev 等工具自動處理、但若自己呼叫 API 要注意 template 對不對。
Tokenization 引發的 bug
Tokenizer 設計的副作用:
Glitch Tokens
某些 token 在訓練資料中很少出現、模型對它們的行為怪異。Reddit 上著名的 SolidGoldMagikarp 就是 GPT-2 / GPT-3 的 glitch token、模型遇到會出現奇怪反應。原因:tokenizer 學了這個 token、但訓練資料中幾乎沒上下文、模型沒學到它的語意。
數字 tokenization
早期 BPE 對數字的處理不一致:1234 可能切成 123 + 4、1235 可能切成 12 + 35。模型對「數字加法」表現差跟這個有關。
現代 LLM 多半把每個 digit 各自當一個 token(一致 tokenization)、改善數學能力。
Code 的 indentation
寫 code 場景的 tokenizer 要妥善處理 indentation。早期 LLM 把多個空白合併成一個 token、code 結構壞掉;現代 LLM(特別是 coding-specialized)把 4 空白 / 8 空白等常見 indentation 各自當一個 token。
跟 context window 的關係
Context window 的單位是 token、不是字。1M token 的 context window 在英文約等於 750K 字、在中文約 1M 字(看 tokenizer)。
實務啟示:
- 「128K context」在不同 tokenizer 上實際容量不同。
- 計算 API 費用要用該模型的 tokenizer 算 token 數。
- 中文 prompt 用 Llama 2 比 Llama 3 / Gemma 4 吃 context 多。
下一章
下一章:3.7 想學更深:推薦公開課程。