模型从来没有"看见"过字符串。当你输入 Hello, world 时,进入 Transformer 第一层的并不是字母,而是一串整数 ID,比如 [15496, 11, 1917]。把人类可读的文本变成这串整数的过程,就是分词(tokenization)。它处在整个 LLM 流水线最前端,却深刻影响着上下文长度、推理成本、多语言公平性,甚至模型能不能数清楚一个单词里有几个字母。
直觉:为什么不直接用字符或单词
最朴素的两种方案各有死穴。
- 按字符切:词表极小(英文 ~100,加上 Unicode 也可控),永远不会遇到"没见过的词"。但一个句子会被切成几百个 token,序列长度爆炸;而且模型要从单个字符重新学习"组合成词"的全部规律,建模负担重。
- 按单词切:序列短、语义单元天然。但词表会膨胀到几十万甚至上百万,仍然挡不住未登录词(OOV)——人名、拼写错误、新造词、代码标识符
getUserById一律变成<unk>,信息直接丢失。
理想方案应该在两者之间:高频片段当成整体(如 the、ing),低频/没见过的内容退化成更小的片段,最坏退化到字节级,保证任何输入都能被表示。BPE(Byte Pair Encoding)正是这种思路的工程实现。
机制:BPE 的训练与编码
BPE 最早是数据压缩算法,被借用到分词上。它分**训练(学词表)和编码(切句子)**两个阶段。
训练:贪心地合并最高频相邻对
从字符(或字节)级别开始,统计语料里所有相邻符号对的出现频次,每一轮把频次最高的那一对合并成一个新符号,记入"合并规则表(merges)"。重复 N 次,N 就决定了最终词表大小。
1 | from collections import Counter |
直觉上,语料里 e 和 r 经常相邻,于是 er 先被合并;接着 er 和 </w>(词尾标记)又高频相邻,于是 er</w> 成词。高频子串就这样从字符"长"成了完整词缀乃至单词。
编码:按训练时的顺序应用合并
切一个新词时,先打散成字符,然后按 merges 表里学到的优先级顺序反复应用合并规则,直到没有可合并的对为止。注意优先级 = 训练时被学到的早晚,不是当前频次。这保证了同一个词在任何上下文里都切出确定且一致的结果。
字节级 BPE:彻底消灭 OOV
GPT-2 之后主流做法是 byte-level BPE:不在 Unicode 字符上做 BPE,而是先把文本按 UTF-8 编码成字节序列,在 0~255 这 256 个字节上跑 BPE。好处是基础词表只有 256,且任何输入都能表示——再生僻的 emoji、二进制噪声、未知语言,都是合法字节,根本不存在 <unk>。代价是非 ASCII 文字(中文、日文等)一个字符占 3~4 字节,未被合并时 token 数偏多。这也是为什么同样信息量的中文 prompt 往往比英文消耗更多 token。
值得一提,BPE 只是一族算法。WordPiece(BERT)改用最大化语言模型似然的准则挑选合并对,而非纯频次;Unigram LM(SentencePiece)则反向操作——从大词表出发,按对似然的损失逐步删词。但"子词单元 + 可回退到更小粒度"的核心思想是共通的。
工程权衡与踩坑
词表大小是一组对冲。 设词表大小为 ,隐藏维度为 ,则嵌入矩阵和输出投影各占 个参数。 翻倍,这部分显存和参数线性增长,softmax 的计算也变重。但 越大,平均每个 token 承载的字符越多,同样文本切出的序列就越短。而 Transformer 自注意力是 的,序列长度 下降带来的收益往往可观。所以 是在"嵌入/输出层开销"与"序列长度与注意力开销"之间找平衡,常见取值在数万量级。
Token 不等于字,更不等于语义。 几条反复咬人的坑:
- 数字被切得支离破碎。
12345可能是123+45,模型对算术的"手感"因此先天受损,这是很多模型算术弱的根因之一。 - 字母级任务反直觉地难。问"strawberry 里有几个 r",模型看到的是几个子词块而非逐字母,它根本没有字符视角,数错很正常。
- 空格归属。多数 BPE 把前导空格并进 token(
world和world是不同 token),手写 prompt 拼接时容易在边界处错位,触发训练分布外的切法。 - 成本与限流按 token 计。估算时一个粗略经验:英文大约 4 个字符 / token,中文常接近 1~2 字符 / token(取决于分词器),但务必用目标模型自带的分词器实测,不要凭感觉。
分词器和模型必须配对。 词表 ID 是和模型权重一起训练出来的,换一个分词器,同一个 ID 指向的嵌入向量就全乱了。微调、续训、做评测时,分词器版本错配是一类隐蔽且致命的 bug。
小结
分词把"文本 → 整数序列"这件看似琐碎的事,变成了贯穿成本、长度、能力边界的关键设计。BPE 的精髓是贪心合并高频相邻对学出一张可回退的子词表,字节级变体则用 256 个字节兜底彻底消灭 OOV。理解它,你就能解释清楚:为什么中文更费 token、为什么模型数不清字母、为什么数字算术容易翻车,以及为什么换分词器等于换了一双模型的眼睛。下次写 prompt 或估算账单时,记得先想想它会被切成什么样。