大模型为什么需要 Tokenization?BPE 是如何工作的?
简化版
大模型不能直接处理字符串,需要 Tokenizer 把文本切成有限词表中的 token,再映射为整数 ID 喂给模型。BPE(字节对编码) 从字符或字节这样的最小单元出发,反复把语料里出现最频繁的相邻单元对合并成一个新单元,直到词表达到目标大小。这样高频词能用很少的 token 表示、生僻词也能拆成基础单元编码,在「词表大小、序列长度、未登录词」之间取得平衡。
详细版
Tokenizer 负责把文本拆成 token、映射为整数 ID,输出时再还原。三种极端切法各有问题:
- 整词切分:词表爆炸(每个词形都要一个条目),且遇到没见过的词(未登录词 OOV)无法编码。
- 字符/字节切分:词表极小、覆盖任意文本,但序列变得很长,计算成本高。
- 子词切分(BPE 等):折中——高频词保持完整、低频词拆成子词,是主流方案。
BPE 的训练过程:
- 用字符(或字节)作为初始单元,每个词表示成单元序列 + 词尾标记;
- 统计所有相邻单元对的出现频率;
- 合并频率最高的一对,作为新单元加入词表;
- 重复 2-3,直到词表达到目标大小。
推理时,用训练好的合并规则把输入文本切成 token。高频词最终由较少 token 表示,生僻词仍能拆成基础单元,不会因词表没有完整词就无法编码。
完整版教学
一、从文本到模型输入的完整链路
神经网络只吃数值张量,不认识字符串。简要说进入模型要走这条流水线:
文本 "我爱AI"
→ Tokenizer 切分:["我", "爱", "AI"]
→ 映射 token ID:[2769, 4263, 15837]
→ Embedding 查表:每个 ID 变成一个 d 维向量
→ 进入 Transformer
Tokenizer 处在最前端,它决定了模型眼中的”基本单元”是什么,直接影响三件大事:上下文长度(同样文本切出的 token 越多越占窗口)、训练/推理成本(token 数决定计算量)、多语言公平性(某些语言被切得更碎就更吃亏)。所以 Tokenizer 不是无关紧要的预处理,而是影响模型效率和能力的关键设计。
二、四种粒度的取舍(一张表看清)
| 粒度 | 词表大小 | 序列长度 | OOV(未登录词) | 问题 |
|---|---|---|---|---|
| 词级 | 巨大(几十万+) | 短 | 严重(新词无法编码) | 词形爆炸、OOV |
| 字符级 | 很小(几百) | 很长 | 无 | 序列太长、语义弱 |
| 子词级(BPE) | 中等(3万~15万) | 适中 | 无(可拆基础单元) | 主流折中 |
| 字节级 | 极小(256 起) | 偏长 | 无(覆盖一切字节) | 一个汉字占多字节 |
核心矛盾是:词表越大,序列越短但参数越多、低频词越难训;词表越小,参数省但序列越长。 子词级 BPE 正是这条权衡曲线上的甜点——既不让词表爆炸,又不让序列过长。
三、BPE 训练:一个可手算的例子
BPE 的精髓是「从小单元开始,不断把最常见的邻居粘起来」。用一个玩具语料手推一遍就懂了。
假设语料里有这些词(括号是词频),每个词先拆成字符 + 词尾标记 ·:
low· ×5 → l o w ·
lower· ×2 → l o w e r ·
newest· ×6 → n e w e s t ·
widest· ×3 → w i d e s t ·
第 1 步:统计相邻对频率。e s 出现在 newest(6)+widest(3)=9 次,最高 → 合并成 es。
第 2 步:现在 es t 出现 9 次,最高 → 合并成 est。
第 3 步:est · 出现 9 次 → 合并成 est·(一个表示词尾 est 的子词)。
第 4 步:l o 出现在 low(5)+lower(2)=7 次 → 合并成 lo。继续 lo w → low……
几轮之后,「est·」「low」这些高频片段都成了单个 token。于是 newest· 从 7 个字符压成 new + est· 两个 token;而一个没见过的词 slowest· 也能被拆成 s+low+est·,永远不会 OOV。这就是 BPE「高频变短、生僻可拆」的双赢。
记住这个画面:BPE 像滚雪球——从字符出发,谁和谁挨得最勤就先粘成一团,最后常用表达变成大块、生僻表达仍能拆成小块。
四、Byte-level BPE:为什么 GPT 用字节而非字符
GPT-2 之后普遍用 Byte-level BPE(字节级 BPE):初始单元不是「字符」而是「字节」(0255 共 256 个)。好处是任何文本、任何语言、任何表情符号、乱码都能编码(世界上一切都是字节),彻底消灭 OOV,且初始词表只有 256。代价是一个非 ASCII 字符会占多个字节——比如一个中文字符在 UTF-8 里是 3 字节,可能被切成 23 个 token。这解释了下一节的计费问题。
五、Token 数 ≠ 字数:直接影响成本和上限
同简要说,不同语言切出的 token 数差别很大:
"Hello world" → 约 2 个 token(英文常见词多为 1 token)
"你好世界" → 常为 4~8 个 token(中文每字常占 1~3 token)
后果很实际:
- API 计费按 token 算,中文、代码、生僻语言往往比等长英文更贵。
- 上下文上限按 token 算,128K 窗口装中文能塞的字数远少于装英文的单词数。
- 不能用字符数估算 token 数——务必用对应模型的 tokenizer 实测。
这也带来公平性问题:训练语料以英文为主的 tokenizer,对小语种切得更碎,等于让它们「用更多 token 说同样的话」,既费钱又占窗口。
六、特殊 token、聊天模板与常见工程坑
Tokenizer 还管理一批特殊 token:开始 <s>、结束 </s>、填充 <pad>、未知 <unk>,以及标记对话角色的边界符。聊天模型把 system/user/assistant 消息按聊天模板拼成训练时见过的格式,例如:
<|system|>你是助手<|end|><|user|>你好<|end|><|assistant|>
模板或结束符用错,会导致角色混乱、模型停不下来、或性能骤降。其他高频工程坑:
- 领域术语被切太碎(如把一个专有名词拆成 5 个 token)→ 序列变长、学习效率降。
- 多语言词表分配不均 → 某些语言 token 明显膨胀。
- 改了词表却没同步调整 Embedding 和输出层 → 直接崩。
- 训练与推理用了不同 tokenizer → 同一 token ID 语义完全错位,模型输出乱码。
七、常见误区与追问
- 误区:把「大模型为什么需要 Tokenization?BPE 是如何工作的?」背成名词解释就算掌握。 面试官通常会追问边界、代价和工程取舍,只会定义很容易在第二问暴露理解断层。
- 追问:这个机制主要解决什么问题? 要先说清它对应的痛点,再说明为什么大模型规模变大后这个痛点会被放大。
- 误区:参数调一调就能补齐模型能力。 解码、窗口或提示只能改变使用方式,不能凭空增加模型没有学到的知识和推理能力。
- 追问:线上系统应该怎样验证效果? 至少要看准确率、延迟、成本、稳定性和失败样例,而不是只看一次演示是否回答得漂亮。
- 误区:模型输出流畅就代表事实正确。 大模型擅长生成高概率文本,事实可靠性还需要检索、工具调用、引用证据和后处理校验共同保证。
八、加强记忆
Tokenization 记「有限词表覆盖无限文本」:模型只吃 token ID,整词切分会 OOV 又爆词表、字符切分又太长,BPE 从字符/字节出发、反复合并最高频的相邻对,让高频表达变短、生僻表达可拆、永不 OOV。三个必记要点:Byte-level BPE 用字节做初始单元、覆盖一切、消灭 OOV;token 数≠字数,中文/代码更费 token,直接影响计费和上下文上限;特殊 token 和聊天模板出错会让模型角色混乱或停不下来。简要说画面——BPE 是滚雪球式地把常在一起的字粘成词块。