Transformer 解剖:从 Attention 到推理系统
第 10 章 Tokenizer 工程:BPE / WordPiece / SentencePiece
第 9 章我们用字符级 tokenizer 在《全唐诗》上训了一个 mini-GPT。字符级简单粗暴,对中文小数据集够用,但真实工业里的大模型几乎全都不用字符级——它们用 BPE / WordPiece / SentencePiece / Tiktoken 等子词级(subword)tokenizer。
为什么?字符级看起来简洁,但代价是序列变长——同样一段英文句子,字符级要 100+ 个 token,BPE 大约 20-30 个。attention 的计算量和序列长度的平方成正比(O(N²)),token 数变成 5 倍,attention 计算就变成 25 倍;模型其余部分(FFN、投影)则随 token 数线性增长。词级 tokenizer 序列短,但词表会爆炸(英文词形、专名、新词数以十万计,加上各种变形拼写,OOV 率永远压不下来)。
子词 tokenizer 是这两个极端的中间方案:把常见词整体当一个 token,把不常见词拆成若干个常见子词,把任何字符都能拆到字节级别兜底。这一章我们把它的来龙去脉讲清楚。
读完这章你能:
- 解释字符级、词级、子词级三类方案各自的得失;
- 详细推导 BPE 的合并算法,并理解 WordPiece、SentencePiece、Tiktoken 各自的微妙差别;
- 看懂主流模型 tokenizer 的关键参数(词表大小、特殊 token、规范化策略);
- 给一个新模型选 tokenizer 时,知道在「英文为主」「中文为主」「多语言」「代码」等场景下分别该选什么;
- 解释为什么 GPT-4 给「strawberry 有几个 r」数错。
10.1 三类 tokenizer 的得失对照
先把三类方案的特点放一起对比:
| 维度 | 字符级 | 词级 | 子词级 |
|---|---|---|---|
| 词表大小 | 100-1000 | 数十万-数百万 | 30K-256K |
| 序列长度(同一段英文) | 长(4-5×) | 短(1×) | 中(1.3×) |
| OOV(未登录词)问题 | 无(任何字符都能编码) | 严重 | 可控(拆成已知子词) |
| 形态学信息保留 | 全部保留 | 部分丢失(变形不归一) | 部分保留 |
| 中文友好度 | 好(一字一 token) | 中(中文分词难) | 中(依赖词表) |
| 多语言友好度 | 好 | 差 | 中 |
| 训练成本 | 不需要训练(直接字符表) | 需要分词器 | 需要训练 |
| 模型表达力 | 弱(字符 → 语义路径长) | 强(一词一 token) | 强(折中) |
直观一点:
flowchart LR CHAR["字符级<br/>序列太长<br/>表达力弱"] -.-> SUBW["子词级 (BPE / SP)<br/>2016 年至今的主流方案"] WORD["词级<br/>词表爆炸<br/>OOV 严重"] -.-> SUBW SUBW --> BPE[BPE] SUBW --> WP[WordPiece] SUBW --> SP[SentencePiece] SUBW --> TT[Tiktoken]
子词 tokenizer 是 2016 年 Sennrich 等人提出 BPE 之后才成主流的,核心思想都一致:给定大量文本,统计出最频繁的「字符片段」组合,把它们合并成单独的 token。常见词整体保留("the"、"and"),不常见的词被拆开("unbelievably" → "un" + "believ" + "ably"),生僻字符仍然能通过字节级兜底。
下面我们先把 BPE 讲透——它是其他几种方案的共同基础。
10.2 BPE 算法的来龙去脉
BPE(Byte Pair Encoding,字节对编码)原本是一种通用的数据压缩算法(Gage, 1994),最早用于压缩文件。Sennrich 等人在 2016 年的论文 Neural Machine Translation of Rare Words with Subword Units 把它改造成了 NLP 的子词分词方法——这是现代 tokenizer 的起点。
BPE 训练算法
给定一个语料库(一大堆文本),目标是训练出一个大小为 V 的词表,能用最少的 token 拼出所有常见词。
步骤如下:
-
初始化:把所有词拆成字符序列(或字节序列)。每个词后面加一个特殊标记(例如
</w>)表示词尾,避免「子词跨词」的混淆。"low" → l, o, w, </w> "lower" → l, o, w, e, r, </w> "newest" → n, e, w, e, s, t, </w> "widest" → w, i, d, e, s, t, </w> -
统计每对相邻 token 的频率。比如:
("l", "o"): 2 次 ("o", "w"): 2 次 ("w", "</w>"): 1 次 ("w", "e"): 2 次 ("e", "r"): 1 次 ("r", "</w>"): 1 次 ("n", "e"): 1 次 ("e", "w"): 1 次 ("e", "s"): 2 次 ("s", "t"): 2 次 ("t", "</w>"): 2 次 ("w", "i"): 1 次 ("i", "d"): 1 次 ("d", "e"): 1 次 -
找出频率最高的对,把它合并成一个新 token。这里有六对并列 2 次,按实现的 tie-break 规则选定其一,比如
("e", "s"),合并成es。所有出现这一对的地方都被替换:"newest" → n, e, w, es, t, </w> "widest" → w, i, d, es, t, </w> -
重复 2-3 步,直到词表达到目标大小 V,或者没有可合并的对。
每次合并都会让训练语料里的 token 总数减少。最常见的字符组合先被合并("th"、"the"、"ing" 这种),最终词表里既有单字符(兜底)、又有中等长度子词(处理常见词缀)、又有完整常见词("the"、"and")。
BPE 编码(推理)
训练完得到一个有序的合并规则列表。对一个新词「unbelievably」编码时:
- 拆成字符:u, n, b, e, l, i, e, v, a, b, l, y
- 按合并规则的顺序,扫描相邻对,找到能合并的就合并。
- 一直合并到没有可合并的为止。
- 最终得到 token 序列,比如
un+believ+ably(具体取决于训练数据)。
flowchart TB WORD["unbelievably"] --> CHARS["u,n,b,e,l,i,e,v,a,b,l,y<br/>(12 个字符 token)"] CHARS --> M1["合并规则 1: u+n → un"] M1 --> S1["un, b, e, l, i, e, v, a, b, l, y"] S1 --> M2["规则 2: a+b → ab"] M2 --> S2["un, b, e, l, i, e, v, ab, l, y"] S2 --> M3["规则 3: ab+l → abl"] M3 --> S3["un, b, e, l, i, e, v, abl, y"] S3 --> DOTS[...继续合并...] DOTS --> FINAL["un, believ, ably<br/>(3 个 token)"]
这就是 BPE 的全部工作流程。简单、确定性、可控。
重要的工程细节
字节级 BPE(GPT-2 引入):原始 BPE 在字符级操作,但字符在 Unicode 下有 100K+ 个,BPE 的初始词表会很大。GPT-2 改用字节级——每个字节(0-255)作为最小单元,无论什么 Unicode 字符都能被字节序列表示。词表初始 256(所有字节),通过 BPE 合并后变成 50K 量级。
字节级 BPE 的好处:任何字符串都能被编码——绝不会出现 OOV。代价是中文等多字节字符会被拆成多个字节,输出 token 数偏多(一个汉字通常 2-3 个 token)。GPT-3.5 / GPT-4 都用字节级 BPE。
正则预分词(pre-tokenization):BPE 训练前一般先用规则把文本切成「词」级片段(按空格、标点、数字等切开),合并只在片段内部进行。GPT-2 的 BPE 在文本里把每个空格都当作下一个词的前缀(比如 "hello world" 会被处理成 "hello"、" world",注意 " world" 前面带空格),这样 BPE 自然地学到「单词在句子中间」和「单词在句子开头」的区别。
10.3 WordPiece:BERT 的变种
WordPiece 由 Google 提出(最早用在语音识别里,后来用在 BERT),思路与 BPE 几乎一致,主要差别在合并准则:
- BPE:合并频率最高的对
- WordPiece:合并能让似然增益最大的对
具体地,WordPiece 选合并对的标准是(下式是 HuggingFace 教程给出的通行写法,Google 没有公开原始训练实现):
分子是「合并后的对的频率」,分母是「两个 token 各自的频率乘积」。这相当于「相对相关性」——只有当两个 token 经常一起出现、单独出现少时,合并才有意义。
直觉对比:BPE 倾向合并最常见的对(如 "th"、"e " 这种连贯字符),WordPiece 倾向合并「相关性最强」的对(更倾向语义单元)。
WordPiece 的另一个特殊之处:子词的标记。BPE 用 </w> 标记词尾;WordPiece 在子词前加 ## 表示「这是上一个 token 的延续」:
"unbelievably" → un, ##believ, ##ably
"playing" → play, ##ing
这种标记让你看 token 序列时能立刻判断哪些是词的开头、哪些是延续——视觉上比 BPE 更清晰。BERT、DistilBERT、ELECTRA 都用 WordPiece。
实际效果上 BPE 和 WordPiece 在大多数任务上差不多——选哪个更多是历史习惯(BERT 系用 WordPiece,GPT 系用 BPE)。
10.4 SentencePiece:处理多语言的全能选手
到了 T5 / Llama / mT5 这一代多语言模型,BPE 和 WordPiece 都遇到了一个共同问题:它们假设输入已经被空格分词了——也就是说,它们用空格作为「词边界」的硬指示。
这在英文里没问题,但在中文、日文、泰文等没有空格的语言里崩盘。中文分词本身就是一门学问(jieba / pkuseg / LAC 等都有不同分词方案),强行先做中文分词再 BPE 会引入新的不一致。
SentencePiece(Kudo & Richardson, 2018)的核心思想:直接把空格当成一个普通字符,不做预分词。算法上仍然是 BPE 或 unigram,但输入是「整段原始文本」而非「已分词的词序列」。
SentencePiece 用一个特殊符号 ▁(U+2581)替代空格——比如 "hello world" 会被表示成 "▁hello▁world"——这样空格本身也成了 token 学习对象。
原始: "hello world 你好世界"
SP: ▁hello ▁world ▁你 好 世 界
这个简单的改动带来三个优势:
优势一:跨语言统一。同一个 SentencePiece tokenizer 可以同时处理英文、中文、日文、阿拉伯文——不需要任何语言特定的预处理。
优势二:可逆。从 token 序列恢复原文是无损的——「▁」对应空格、其他字符直接还原。传统的 BPE / WordPiece 依赖外部分词器先按空格切词(如 Moses tokenizer),原始空格信息在这一步就丢了,恢复时要做猜测(GPT-2 的字节级 BPE 把空格留在 token 里,同样可逆)。
优势三:和模型对齐。空格作为 token 让模型显式地建模「词边界」——比如生成时模型会自己决定下一个 token 前要不要加空格。
T5、Llama 1/2、Mistral、PaLM 等模型都用 SentencePiece;Llama 3、Qwen、DeepSeek 则转向了 GPT 式的字节级 BPE(见 §10.10)。
Unigram vs BPE 模式
SentencePiece 内部支持两种算法:BPE 和 Unigram。Unigram 模型(Kudo, 2018)的训练目标是让所有训练文本的对数似然最大化,从词表里逐步删除贡献最小的 token,直到词表大小达到目标。
| 算法 | 思路 | 优劣 |
|---|---|---|
| BPE | 自下而上:从字符开始合并 | 训练快,主流 |
| Unigram | 自上而下:从全词表开始删除 | 概率框架更纯粹,统计意义更清晰 |
实际上两种模式效果接近。Llama 默认用 BPE 模式的 SentencePiece,T5 用 Unigram 模式。
10.5 Tiktoken:GPT-4 的工程加速
Tiktoken 是 OpenAI 在 GPT-3.5 / GPT-4 时引入的 BPE 实现。它的算法和 GPT-2 的字节级 BPE 大体一致,但工程上做了几项关键优化:
- Rust 实现:核心 BPE 编码用 Rust 写。tiktoken 自己给的口径是「比同类开源 tokenizer 快 3-6 倍」,对比对象是 HuggingFace
tokenizers,不是 GPT-2 那份教学用的 Python 实现。 - Hashing-based merge:用哈希表加速合并规则查找。
- 更大词表:GPT-4 用的
cl100k_base词表 100K,比 GPT-2 的 50K 大一倍。
更大词表的好处:单 token 表达更多内容,序列变短。GPT-4 的 100K 词表里 "function"、"return"、"def" 等编程关键字都是单 token——这让代码生成的效率显著提升。
GPT-4o 之后启用的 o200k_base 词表更激进——200K 大小,对中文友好(多数常用汉字是单 token,一些常见双字词也是单 token)。GPT-4 本身用的仍是 cl100k_base。
flowchart LR G2["GPT-2: 50K BPE"] --> G3["GPT-3.5/4: 100K cl100k_base"] G3 --> G4["GPT-4o: 200K o200k_base"] G2 -.中文.-> CHN1[一字 2-3 token] G3 -.中文.-> CHN2[一字 1-2 token] G4 -.中文.-> CHN3[一字基本 1 token]
10.6 词表大小:怎么选
选词表大小是 tokenizer 工程的核心决定,影响以下维度:
词表越大:
- 序列越短(每 token 表达更多内容)→ 推理 / 训练成本下降
- embedding 参数越多(
V × d_model)→ 模型参数膨胀 - 罕见 token 训练信号稀疏 → 难训练充分
词表越小:
- 序列越长 → 推理 / 训练成本上升
- embedding 参数少 → 模型紧凑
- 每个 token 训练信号充分 → 训练更稳定
主流模型的词表大小:
| 模型 | 词表大小 | 备注 |
|---|---|---|
| GPT-2 | 50,257 | 字节级 BPE |
| GPT-3 | 50,257 | 同上 |
| GPT-3.5 / GPT-4 | 100,277 | cl100k_base |
| GPT-4o | 200,019 | o200k_base,中文友好 |
| BERT-base | 30,522 | WordPiece |
| RoBERTa | 50,265 | BPE |
| T5 | 32,128 | SentencePiece Unigram |
| Llama 1/2 | 32,000 | SentencePiece BPE |
| Llama 3 | 128,256 | tiktoken 的 100K + 28K 非英语扩充 |
| DeepSeek-V2/V3 | 102,400 / 129,280 | 自训 BPE(config 的 embedding 行数,略大于实际 token 数) |
| Qwen 2.5 | 151,936 | 自训 BPE(Qwen2Config 默认值;7B 的 checkpoint 配置为 152,064) |
| Mistral | 32,000 | SentencePiece |
可以看到一个明显趋势:2023 年以后,新模型的词表越来越多地扩到 100K+,主要驱动是多语言(特别是中文/日文/韩文)和代码——这些场景下大词表能显著缩短序列。
词表对中文的影响
英文中一个 token 对应一个完整的常用词("the", "function", "computer"),但 32K 词表收不下多少汉字:Llama 2 的词表里单个汉字的 token 只有 700 个,常用字大多在内,落到词表外的汉字会退回字节级,按 UTF-8 拆成 3 个字节 token。比如 Llama 2 的 tokenizer 处理「机器学习」:
"机器学习" → ["▁", "机", "器", "学", "<0xE4>", "<0xB9>", "<0xA0>"] # 「习」不在词表里,拆成 3 个字节
而 Llama 3 的 128K 词表里常用汉字都是单 token,一部分常见词也是单 token:
"机器学习" → ["机", "器", "学习"]
同一段中文,后者的 token 数大约只有前者的一半(我们拿一段 53 字的中文实测:Llama 2 为 70 个 token,Llama 3 为 36 个)——具体比例随文本而变,但对中文用户这是实打实的节省。
DeepSeek-V3 的 128K 词表更进一步针对中文优化:预分词时把连续的汉字单独切成一段,常见词(如「人工」「智能」,乃至「机器学习」「人工智能」)都是单 token。
10.7 Tokenizer 怪象:为什么 GPT-4 数不准 r
「strawberry 有几个 r」这个问题在 2024 年让 GPT-4 这一代模型闹出大笑话——它们常回答「2 个」(错的,正确是 3 个)。这在很大程度上要归因于 tokenizer。
让我们看 GPT-4 的 tokenizer(cl100k_base)怎么处理 "strawberry":
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
ids = enc.encode("strawberry")
print(len(ids)) # 3
print([enc.decode([i]) for i in ids]) # ['str', 'aw', 'berry']
strawberry 被拆成 3 个 token:["str", "aw", "berry"]。模型「看到」的不是 10 个字符 s,t,r,a,w,b,e,r,r,y,而是 3 个意义抽象的子词。
要让模型回答「strawberry 有几个 r」,它需要:
- 在 token 序列里识别「r」字符
- 但 "str" 里有一个 r,"berry" 里有两个 r——这些在 tokenizer 表示下是黑盒
- 模型的训练数据里也没几个例子告诉它「str 这个 token 含 1 个 r」
结果就是模型瞎猜——它在训练数据里见过类似的「数字母」问题,但答对率取决于巧合。
flowchart LR STR["人类看到的: s-t-r-a-w-b-e-r-r-y<br/>(10 个字符, 3 个 r)"] --> TOK[tokenizer] TOK --> TOK_OUT["模型看到的: [str], [aw], [berry]<br/>(3 个 token,r 字符不可见)"] TOK_OUT --> ASK["问: 几个 r?"] ASK --> GUESS["模型瞎猜:<br/>2? 3? 取决于训练样本"]
这就是 tokenizer 怪象。它揭示了一个深刻的事实:模型不是在「字符」层面理解语言,而是在「token」层面。凡是涉及「子 token 操作」(拼写、字符计数、字母组合)的任务,模型都先天受限。
类似的现象:
- 代码缩进问题:GPT-2 的词表里几乎没有「连续空格」token,8 格缩进会被拆成 7 个单空格 token 加一个
" return",代码序列被白白拉长。Codex 系列用的p50k_base和之后的cl100k_base都加入了连续空格 token,同样的缩进只占 1 个 token。 - 数字 tokenization:早期 BPE 把数字也按频率合并(「1234」可能是单 token,「1235」拆成多个 token),导致数学能力受 tokenization 偶然性的影响。新一代模型在预分词正则里就把数字切开来缓解:Llama 3 按「最多 3 位一组」切(
\p{N}{1,3},见transformers-4815a0a/src/transformers/convert_slow_tokenizer.py:1922的 tiktoken 转换器默认 pattern),Qwen 2 更彻底,每个数字单独成 token(\p{N},见transformers-4815a0a/src/transformers/models/qwen2/tokenization_qwen2.py:33的PRETOKENIZE_REGEX)。 - 多语言不平衡:英文一词一 token,泰文一字数 token——同样长度的 prompt 在不同语言下消耗的 token 量差几倍,API 成本显著差异。
10.8 用 SentencePiece 训练自己的 tokenizer
最后我们用代码训一个 SentencePiece tokenizer,体验一下完整流程。
安装:
pip install sentencepiece
训练:
import sentencepiece as spm
spm.SentencePieceTrainer.Train(
input='train_corpus.txt', # 输入文本(一行一句)
model_prefix='my_tokenizer', # 输出文件前缀
vocab_size=32000, # 词表大小
model_type='bpe', # 'bpe' 或 'unigram'
character_coverage=0.9995, # 字符覆盖率(默认值;官方建议中日文这类大字符集用它)
pad_id=0, unk_id=1, bos_id=2, eos_id=3, # 特殊 token id
)
生成的 my_tokenizer.model 是一个二进制文件,可以加载使用:
sp = spm.SentencePieceProcessor()
sp.Load('my_tokenizer.model')
# 编码
ids = sp.EncodeAsIds("Hello world! 你好世界!")
print(ids) # [..., ..., ...]
# 解码
text = sp.DecodeIds(ids)
print(text) # "Hello world! 你好世界!"——默认规范化 nmt_nfkc 把全角「!」转成了半角
# 看 token 字面值
print(sp.EncodeAsPieces("Hello world!")) # ['▁Hello', '▁world', '!']
注意 ▁ 是 SentencePiece 表示词首空格的特殊字符。
几个工程经验
经验一:词表大小要匹配数据量。如果你训练数据只有几百 MB,词表 32K 已经够大;如果有几 TB 数据(如 Common Crawl 全量),可以用 128K 或更大。词表太大、数据太少 → 罕见 token 学不动。
经验二:字符覆盖率影响多语言。character_coverage=1.0 会保证训练语料里的所有字符都进字符表;SentencePiece 官方的建议是:英文、德文这类小字符集语言用 1.0,中文、日文这类大字符集用默认的 0.9995(舍掉极罕见字符以减小词表,被舍的字符映射为 <unk>,开启 byte_fallback 时则退回字节)。
经验三:从模型特性反推 tokenizer。如果模型主要做代码生成,要确保 tokenizer 对常见编程关键字(function、return、def、class)是单 token;如果主做中文 chat,要让常见中文词是单 token。这些都需要在训练 tokenizer 时调整数据分布或显式 force 添加 token。
经验四:tokenizer 训完不宜改。词表一变,embedding 表和输出层的形状都要跟着变:transformers 的 resize_token_embeddings 能把矩阵扩出新行,但新行是未训练的,得继续训练才有用。要大规模扩词表,要么从头训模型,要么走 vocabulary expansion 这类「扩词表 + 继续训练」的路线,代价都不小。
10.9 Tokenizer 选型经验
如果你今天为一个新模型选 tokenizer,下面是经验法则:
-
绝大多数情况选 BPE(SentencePiece BPE 或字节级 BPE)——多语言友好、可逆、生态成熟。Llama 1/2、Mistral 走 SentencePiece BPE,Llama 3、Qwen、DeepSeek 走字节级 BPE。
-
纯英文场景可以用 GPT 风格 Tiktoken——cl100k_base / o200k_base 是开箱即用的成熟选项,词表已经针对代码、空格、缩进等做了优化。
-
代码 / 数学密集场景:考虑专门词表——常见编程关键字、运算符(
==、!=、++)作为单 token。也有模型只在通用词表上做少量追加,如 Code Llama 沿用 Llama 2 的词表,7B 的vocab_size从 32,000 变成 32,016,多出的位置主要给代码补全(infilling)用的特殊 token。 -
极致中文场景:用大词表(128K+)让常用中文词单 token 化。DeepSeek 2024 之后的词表是这条路线的代表。
-
embed 模型用什么词表?:通常和它派生的基础模型一致(BGE v1.5 的中文、英文版本和英文 E5 基于 BERT,用 WordPiece;bge-m3、multilingual-e5 基于 XLM-RoBERTa,用 SentencePiece)。这是因为 embed 模型常常是在基础模型上 fine-tune 出来的,一般不换词表。
-
不要自己魔改 BPE——除非你有非常具体的研究目的。SentencePiece 已经把所有踩过的坑封装好了,自己实现版本会引入新 bug。
10.10 主流模型 tokenizer 速查
把今天主流模型的 tokenizer 信息汇总:
| 模型族 | 算法 | 词表大小 | 实现库 |
|---|---|---|---|
| BERT 系 | WordPiece | 30K | HuggingFace tokenizers |
| GPT-2 / GPT-3 | 字节级 BPE | 50K | tiktoken / HF |
| GPT-3.5 / GPT-4 | 字节级 BPE | 100K (cl100k_base) | tiktoken |
| GPT-4o | 字节级 BPE | 200K (o200k_base) | tiktoken |
| T5 | SentencePiece Unigram | 32K | sentencepiece |
| Llama 1/2 | SentencePiece BPE | 32K | sentencepiece |
| Llama 3 | tiktoken-based BPE | 128K | tiktoken |
| Mistral | SentencePiece BPE | 32K | sentencepiece |
| Qwen 2.5 | 字节级 BPE | 152K | HF tokenizers |
| DeepSeek-V3 | 字节级 BPE | 129K | HF tokenizers |
| ChatGLM3 | SentencePiece BPE | 65K | sentencepiece |
| GLM-4 | 字节级 BPE | 151K | tiktoken |
可以看到一个微妙的变化:开源大模型正从 SentencePiece 转向 GPT 式的字节级 BPE——Qwen、DeepSeek 起步就是这条路,Llama 3、GLM-4 也从 SentencePiece 换了过来,实现上用 tiktoken 或 HF tokenizers。底层算法仍然是 BPE,差别在字节级兜底、预分词正则和工程实现。
本章小结
- Tokenizer 是文本和 token id 之间的桥——它的算法、词表大小直接影响模型表达力、训练成本、推理效率。
- 三类方案:字符级(简单但序列长)、词级(OOV 严重)、子词级(主流)。
- BPE 的核心是「贪心合并最频繁的相邻对」——从字符或字节出发,反复合并最常见的对,直到词表达到目标大小。
- WordPiece 用「似然增益最大」代替「频率最高」——和 BPE 几乎等价,但 BERT 系用它。
- SentencePiece 把空格当普通字符,不做预分词——多语言场景的常用选择,T5、Llama 1/2、Mistral 都用它;新一代开源模型则多转向字节级 BPE。
- Tiktoken 是 GPT-4 的工程加速版 BPE——Rust 实现 + 大词表(100K-200K),其中 o200k_base 对中文明显更友好。
- 词表大小要在「序列长度」和「embedding 参数量」之间权衡——主流趋势从 32K 扩到 128K-200K。
- Tokenizer 怪象:模型在 token 层面理解语言,「数字母」「字符级操作」类任务先天受限——「strawberry 有几个 r」是经典案例。
- 训练自己的 tokenizer:用 SentencePiece 库即可,关键参数是 vocab_size、model_type、character_coverage。
- Tokenizer 训完几乎不能改——词表大小写死了 embedding 形状。
第四部分到这里完结。我们从 50 行 Self-Attention 走到了完整的 mini-GPT,再到工业级 tokenizer——「自己从零搭一个能跑的小语言模型」的完整链路已经走通。
第五部分(11、12、13 章)我们升级视角到「规模化」——Scaling Laws 告诉我们模型、数据、算力之间该怎么配比,MoE 让万亿模型成为可能,长上下文之战推动了 4K → 1M 的演化。
延伸阅读
- Sennrich et al., Neural Machine Translation of Rare Words with Subword Units, ACL 2016——BPE 在 NLP 的经典论文。
- Kudo & Richardson, SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing, EMNLP 2018——SentencePiece 论文。
- Kudo, Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates, ACL 2018——Unigram 模型的提出。
- Radford et al., Language Models are Unsupervised Multitask Learners, 2019——GPT-2 论文,字节级 BPE 的引入。
- HuggingFace 的 tokenizers 库与教程: https://huggingface.co/docs/transformers/tokenizer_summary
- OpenAI tiktoken: https://github.com/openai/tiktoken
- 苏剑林博客「科学空间」的【中文分词系列】——非常透彻的中文 tokenization 工程视角。