# 为什么不用汉字

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

- 作者：David（道雾轩）
- 专栏：从 Bit 到 AGI（https://daiw.org/manual/bit-to-agi.md）
- 最后更新：2026-07-17
- 原文：https://daiw.org/manual/bit-to-agi/06-tokenizer/why-not-chinese-char
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

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

## 直觉

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

## 定义

**字级 Tokenization**：以 Unicode 码点（或 GBK 双字节单元）为原子符号，词表元素 ≈ 汉字集合 + 标点 + 少量特殊符。

**子词 Tokenization**：词表元素是更短的片段（字节、BPE 合并单元等），任意 UTF-8 文本都可由有限词表组合表示。

设文本含 $C$ 个汉字、词表覆盖率为 $p$（随机字在词表内的概率），则 OOV 期望次数约为 $C(1-p)$。当 $p = 0.99$、$C = 1000$ 时，仍有约 **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 参数量 $= V \times d$。假设 $d = 4096$：

| 词表 $V$ | 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 汉字。训练数据里大量字节被浪费在极少出现的码位上（参数效率低）。

信息论视角：汉字内部有偏旁部首、形声结构的统计规律。**子词切分**相当于学习这些规律，用更少参数表达更多组合——与 [第三章](https://daiw.org/manual/bit-to-agi/03-information/compression-removes-redundancy) 的压缩思想一致。

## 常见误区

- **「中文模型应该用字级 Tokenizer 才地道」**：早期部分中文 BERT 确实用字级，但 GPT 类自回归模型更依赖子词以控制词表大小并与英文、代码共用同一套机制。
- **「字级一定比子词省 Token」**：高频词组（「人工智能」「中华人民共和国」）在 BPE 里可能是单个 Token；字级反而无法利用词组统计。
- **「OOV 可以映射到 `` 就行」**：`` 丢失信息，梯度学不到该位置的语义；生产系统应尽量避免。

## 动手试一试

统计一段文本里有多少 Unicode 码点不在常用字集：

```python
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 类 **自回归生成** 更强调：

- 与英文、数字、代码 **共用一套子词机制**；
- 控制 $V$ 使 Embedding 不把参数预算吃光；
- 在 100K+ 上下文下，字级长文会使 $N$ 膨胀，注意力 $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 文本，并把参数集中在高频片段上。

下一篇：[为什么不用单词](https://daiw.org/manual/bit-to-agi/06-tokenizer/why-not-english-word)
