分词与 BPE —— 词表是怎么从语料里长出来的
第 1 课的地图上,流水线第一格写着「文本 → 分词 / BPE」。当时我们直接用了词——今天补上这一格:模型的输入不是词,是词元(token);把文本切成词元的那把刀,就是分词器(tokenizer)。刀法好不好,直接决定模型后面 12 层 Transformer 的难度。
一、三种刀法:按词切、按字符切、按子词切
同一个词 tokenization,三种切法完全不同(原文图 1.13):
| 刀法 | tokenization 被切成 | 词表大小 | 主要问题 |
|---|---|---|---|
| 按词 | tokenization(1 个) | 巨大(英语 20 万+) | 没见过的词直接失联 |
| 按字符 | t·o·k·e·n·i·z·a·t·i·o·n(12 个) | 极小(256 个字符) | 序列暴长,单个字符无语义 |
| 按子词 | token·ization(2 个) | 适中(GPT-2 约 5 万) | 几乎没有——这就是现代 LLM 的选择 |
按词切:三宗罪
① 词表爆炸:光英语就 20 万+ 词,其中还挤满了 this、is、a 这类信息量极低的填充词;② 亲缘词形同陌路:learn、learning、learned、learnt 被切成四个毫无关联的独立词元,模型得各学各的(原文图 1.15);③ 生词失联(OOV):拼错一个字母,running 变 runing,词表里查无此词,模型当场宕机。
按字符切:词表是小了,代价更狠
Hello, world! 从 2~6 个词元变成 13 个字符词元(原文图 1.17),序列长度暴涨,而注意力计算量随长度平方增长(这个平方我们第 11 课细算)。更要命的是:单个字母不携带词义,模型得从零重建「lowest 和 highest 共享 est」这类本可白捡的知识。
按子词切:甜点区
playground → play + ground;unhappiness → un + happiness;modernization → modern + ization(原文图 1.14/1.18)。常用片段进词表当常客,生僻内容拆回零件——词表适中、序列不爆炸、新词可以用熟悉的零件拼出来。剩下的问题只有一个:learning 到底该切几刀?按词?切 learn+ing?还是更碎?这个问题不该由人来拍脑袋,该由数据回答。这就是 BPE。
二、BPE:让语料自己投票决定怎么切
BPE(Byte Pair Encoding)1990 年代本是压缩算法,2016 年被搬进 NLP 分词(Sennrich et al.),GPT 系列沿用至今。算法只有四步,拿原文 1.4 的迷你语料走一遍——4 个单词,词频如下:
| 单词 | 词频 | 第 ② 步后(拆成字符) |
|---|---|---|
| old | ×7 | o · l · d · </w> |
| older | ×3 | o · l · d · e · r · </w> |
| finest | ×9 | f · i · n · e · s · t · </w> |
| lowest | ×4 | l · o · w · e · s · t · </w> |
四步:① 每个词末尾贴上词尾哨兵 </w>;② 拆成单字符;③ 数相邻对——统计每种相邻符号对在语料里出现多少次(乘上词频);④ 把频次冠军焊成一个新词元,回到 ③,重复。
</w>?
est 在 lowest 尾部是「最……」(后缀,该独立成词元);同样的 est 出现在 esteem 开头则毫无后缀含义。不标词尾,BPE 就分不清这两种身份。哨兵一贴,「带 </w> 的 est」和「不带的全靠位置说话」泾渭分明——词形相同、语言角色不同的片段不再混淆。
第一轮数对子的结果(数字 = 出现次数,已乘词频):
| 相邻对 | 次数 | 来源 |
|---|---|---|
e·s | 13 | finest 的 9 + lowest 的 4 |
s·t | 13 | 同上(并列) |
t·</w> | 13 | 同上(并列) |
o·l / l·d | 10 | old 的 7 + older 的 3 |
f·i / i·n / n·e | 9 | 仅 finest |
冠军是 e·s(并列时取先扫到的)。第一刀:finest → f·i·n·es·t·</w>,lowest → l·o·w·es·t·</w>。重新数一轮,冠军变成 es·t(13)——第二刀合出 est;再数,est·</w> 又是冠军——第三刀合出带词尾的 est</w>。三刀过后,词表里已经长出了层级:
- 单字符(兜底零件):
o l d e r f i n w s t </w> - 子词(高频预制件):
es、est、est</w>——继续合并还会长出ol→old、fi→fin、lo→low - 整词(超高频直接成词):继续合并,
finest到第 8 刀整个成词,old到第 9 刀(成old</w>),lowest到第 12 刀
三、亲手合:演算器
下面是真实算法在跑(不是动画脚本)。点「合并」按钮,黄块是本轮频次冠军,右侧条形图是全部相邻对的得票。连点几下,看 est 长出来、old 整个成词、词表从 15 个零件长到几十个:
o·l(10 次),而不是 finest 家的 9 次对——finest 词频虽高,但它的零件对在别处帮不上忙。BPE 完全数频次说话,不懂语言学;合并顺序由全局票数决定,不由任何单词的"重要性"决定。
四、写下来跑:迷你 BPE,然后去切没见过的新词
刚才的演算器,二十行 Python 就是全部。先训练(做 5 次合并),再用学到的词表去切语料里没有的词——重点看 slowest 和 oldest 会被切成什么:
e+s→es、es+t→est、ol+d→old)。切新词 = 从单字符开始,按训练时的顺序重放这些规则。于是:oldest 直接拼成 old + est</w>(整词+后缀,漂亮);slowest 和 lowest 都借到了尾巴 est</w>,但前几个字符只能保持单件——训练的第 4 条规则是 o+l,而它们体内是 l·o 顺序,方向对不上,规则就用不上(把 num_merges 改成 10,l+o 规则学到手,两词立刻变成 lo·w…)。新词不再失联,而是降级成零件拼装——这正是第 1 课地图里"分词"那格的全部魔法。
older 被切成 olde·r·</w>——BPE 想合 old·e,因为那一刻它是剩下频次最高的对。它不懂"olde 不是英语词",它只数频次。真实词表里这类历史遗留切片不少见,纯属正常。
五、真实世界:GPT-2 的 50257 个词元
GPT-2 把这套流程做到底:在字节层面做 50,000 次合并,得到 256 个基础字节 + 5 万个合并词元 + 1 个特殊词元 <|endoftext|> = 50,257 个词元,每个词元一个编号:Building → 25954,<|endoftext|> → 50256(原文图 1.25)。因为基础层是字节而不是字母,任何字符串都能切——生词、错拼、表情、中文,大不了退回字节零件,永远不会 OOV。
想亲眼看看真实 GPT-2 怎么切你的名字或一句中文?打开 Tiktokenizer,选 gpt2,输入随便什么文字——注意观察:常用词一整块,生僻词碎成几块。
检索练习
本课主读材料
Vizuara《The Transformers》1.3–1.4 节(本课的原始出处)
读法:今天课内已把 1.3(三种刀法)和 1.4(BPE 四步)完整推过,包括原文没有的代码实现。去原文重点看图:1.15(词级三宗罪)、1.17(字符序列爆炸)、1.20–1.25(BPE 逐步配图),与你的演算器操作互相对照。
学有余力:Karpathy 的 minbpe(约 100 行的生产级 BPE,含字节级处理)——想看"工业版和我们玩具版的差距"就读它。