从 Bit 到 AGI第六章:Token

为什么不用汉字

从 Bit 到 AGI · 第六章:Token · 为什么不用汉字

作者 David更新于 第 87 篇(共 186 篇)

「一个汉字一个 Token,词表放几万个常用字不就行了?」——听起来自然,但主流大模型几乎都不这么做。本篇用数字说明:按字建词表在覆盖率、参数效率和多语言扩展上都会撞墙。

直觉

汉字像 一套固定型号的零件目录:常用字约 3500 个(一级字表),但 Unicode 里 CJK 统一汉字就有 9 万+ 码位。你不可能把每个可能的字都放进词表——生僻字、异体字、新造词、人名地名永远装不完。未登录字(OOV)必须拆成更小的块,否则模型遇到「𠮷野家」这类字就只能报错或替换成 ``。

定义

字级 Tokenization:以 Unicode 码点(或 GBK 双字节单元)为原子符号,词表元素 ≈ 汉字集合 + 标点 + 少量特殊符。

子词 Tokenization:词表元素是更短的片段(字节、BPE 合并单元等),任意 UTF-8 文本都可由有限词表组合表示。

设文本含 CC 个汉字、词表覆盖率为 pp(随机字在词表内的概率),则 OOV 期望次数约为 C(1−p)C(1-p)。当 p=0.99p = 0.99、C=1000C = 1000 时,仍有约 10 个字无法直接表示——对代码、化学式、多语言混排更糟。

例子

例子 1:覆盖率 vs 词表大小(量级估算)

策略词表条目未登录处理
3500 常用字3,500生僻字、扩展区汉字 OOV
21,003 通用规范汉字21,003Unicode 扩展 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 参数量 =V×d= V \times d。假设 d=4096d = 4096:

词表 VVEmbedding 参数量bf16 权重体积
3,500(常用字)14.3M28.6 MB
32,000(BPE)131M262 MB
90,000(全汉字)369M738 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 类 自回归生成 更强调:

  • 与英文、数字、代码 共用一套子词机制;
  • 控制 VV 使 Embedding 不把参数预算吃光;
  • 在 100K+ 上下文下,字级长文会使 NN 膨胀,注意力 O(N2)O(N^2) 不可承受。

日文、韩文 同样面临表意文字或音节文字与拉丁混排的问题;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 文本,并把参数集中在高频片段上。

下一篇:为什么不用单词

本页目录