为什么不用汉字
从 Bit 到 AGI · 第六章:Token · 为什么不用汉字
「一个汉字一个 Token,词表放几万个常用字不就行了?」——听起来自然,但主流大模型几乎都不这么做。本篇用数字说明:按字建词表在覆盖率、参数效率和多语言扩展上都会撞墙。
直觉
汉字像 一套固定型号的零件目录:常用字约 3500 个(一级字表),但 Unicode 里 CJK 统一汉字就有 9 万+ 码位。你不可能把每个可能的字都放进词表——生僻字、异体字、新造词、人名地名永远装不完。未登录字(OOV)必须拆成更小的块,否则模型遇到「𠮷野家」这类字就只能报错或替换成 ``。
定义
字级 Tokenization:以 Unicode 码点(或 GBK 双字节单元)为原子符号,词表元素 ≈ 汉字集合 + 标点 + 少量特殊符。
子词 Tokenization:词表元素是更短的片段(字节、BPE 合并单元等),任意 UTF-8 文本都可由有限词表组合表示。
设文本含 个汉字、词表覆盖率为 (随机字在词表内的概率),则 OOV 期望次数约为 。当 、 时,仍有约 10 个字无法直接表示——对代码、化学式、多语言混排更糟。
例子
例子 1:覆盖率 vs 词表大小(量级估算)
| 策略 | 词表条目 | 未登录处理 |
|---|---|---|
| 3500 常用字 | 3,500 | 生僻字、扩展区汉字 OOV |
| 21,003 通用规范汉字 | 21,003 | Unicode 扩展 B–H 区仍 OOV |
| 全部 CJK 统一汉字 | 90,000+ | Embedding 矩阵膨胀 25 倍以上 |
| BPE 子词(32K 词表) | 32,000 | 任意 UTF-8 可表示,Rare 字拆成 2–3 Token |
例子 2:同一句话的字数 vs Token 数
文本:中华人民共和国成立于1949年。
UTF-8 字节:40(11 个汉字 × 3 字节 + 数字标点)若「1 字 1 Token」,需 16 个 Token;用 cl100k_base 实际约 12–15 个 Token——因为「中华人民共和国」可能被合并成更长的子词单元,反而更省 Token。这说明子词切分并不总是比字级更费;字级的问题主要在 OOV 与词表膨胀,不在单句长度。
量化
Embedding 参数量 。假设 :
| 词表 | Embedding 参数量 | bf16 权重体积 |
|---|---|---|
| 3,500(常用字) | 14.3M | 28.6 MB |
| 32,000(BPE) | 131M | 262 MB |
| 90,000(全汉字) | 369M | 738 MB |
全汉字词表 Embedding 一项就比 32K BPE 大 2.8 倍,却仍未覆盖全部 Unicode 汉字。训练数据里大量字节被浪费在极少出现的码位上(参数效率低)。
信息论视角:汉字内部有偏旁部首、形声结构的统计规律。子词切分相当于学习这些规律,用更少参数表达更多组合——与 第三章 的压缩思想一致。
常见误区
- 「中文模型应该用字级 Tokenizer 才地道」:早期部分中文 BERT 确实用字级,但 GPT 类自回归模型更依赖子词以控制词表大小并与英文、代码共用同一套机制。
- 「字级一定比子词省 Token」:高频词组(「人工智能」「中华人民共和国」)在 BPE 里可能是单个 Token;字级反而无法利用词组统计。
- **「OOV 可以映射到
就行」**:丢失信息,梯度学不到该位置的语义;生产系统应尽量避免。
动手试一试
统计一段文本里有多少 Unicode 码点不在常用字集:
text = open("your_corpus.txt", encoding="utf-8").read()
# CJK 统一汉字基本区 U+4E00–U+9FFF,约 20992 个码位
def is_cjk(c):
return "\u4e00" <= c <= "\u9fff"
chars = [c for c in text if is_cjk(c)]
unique = set(chars)
print(f"汉字总数: {len(chars)}, 不重复: {len(unique)}")
print(f"若词表只有 3500 字,OOV 字种: {len([c for c in unique if ord(c) > 0x9fff or c not in COMMON3500])}")
# COMMON3500 需自行加载字表;此处示意思路若 不重复 远超 3500,字级词表必丢信息。
历史与特例:字级并非从未用过
中文 BERT(如 bert-base-chinese)采用 字级 Tokenizer:词表约 21,128,基本覆盖常用汉字。这在 编码器 + 分类/填空 任务上可行,因为输入长度相对短(512 Token 内),且不需要像 GPT 那样生成长文。但 GPT 类 自回归生成 更强调:
- 与英文、数字、代码 共用一套子词机制;
- 控制 使 Embedding 不把参数预算吃光;
- 在 100K+ 上下文下,字级长文会使 膨胀,注意力 不可承受。
日文、韩文 同样面临表意文字或音节文字与拉丁混排的问题;SentencePiece 的 UTF-8 路线正是为 CJK + 拉丁 + emoji 无假设 而设计。字级是特定时代的工程妥协,不是普适最优。
数字与标点:字级词表通常把 0–9、常见标点各占一条;但 2024年3月15日 这类组合不会整段入表,仍需回退或拆字。子词则可能在语料中学到 2024、-03- 等 跨语言通用片段,对日志、API 参数更友好。
繁简与异体:字级方案若为简繁各留一行,词表 再翻倍;OpenCC 转换后再 tokenize 又引入 转换错误 风险。UTF-8 子词对繁简 同一字节序列不同 自然分流,无需单独维护繁体词表——前提是训练语料覆盖两者。
Unicode 扩展区:人名、古籍、方言用字大量位于 B 区以后;字级 3500 字表 几乎必然 OOV。子词 byte fallback 保证 任意合法 UTF-8 可编码,是面向开放域文本(互联网语料)的刚需,而非奢侈。
小结
按字建词表看似简单,却面临 OOV、词表膨胀、多语言与符号无法统一 三重问题。工业界选择子词(BPE / SentencePiece),用有限词表覆盖无限 UTF-8 文本,并把参数集中在高频片段上。
下一篇:为什么不用单词