2.4 子词词元化:字节对编码
词元化(tokenization)是自然语言处理的第一阶段,指把连续的输入文本切分成词元的过程。
前文已经考察了三种可能的词元单位:词、语素和字符。然而,它们各自都存在问题。词和语素通常具有相对稳定的意义,似乎大致处于适合 NLP 处理的粒度,却很难作出形式化定义。字符的定义更加明确,但作为词元单位又显得过小。
本节介绍 NLP 实践中采用的方法:以数据驱动的方式定义词元,使得到的单位通常接近语素或词的大小,但偶尔也会小到单个字符。
为什么要对输入进行词元化?一个原因是,把输入确定性地转换成一组固定单位,可以让不同算法和系统对一些简单问题达成一致。例如:这段文本有多长(包含多少个单位)?或者:don’t 和 New York 各算一个词元还是两个?因此,标准化对 NLP 实验的可复现性至关重要;本书介绍的许多算法(例如语言模型的困惑度指标)都假定所有文本具有固定的词元划分。
如果词元化算法还包含表示语素和字母的较小词元,就可以消除未知词问题。什么是未知词?下一章将会看到,NLP 算法常常先从一个语料库(训练语料库)中学习有关语言的事实,再利用这些事实对另一个独立测试语料库及其语言作出判断。因此,假设训练语料库包含 low、new 和 newer,却不包含 lower;当 lower 出现在测试语料库中时,系统就不知道该如何处理。
为解决这种未知词问题(unknown word problem),现代词元化器会自动归纳一组包含小于词之单位的词元,这些单位称为子词(subword)。子词可以是任意子串,也可以是 -est 或 -er 等承载意义的语素。在现代词元化方案中,许多词元是完整的词,另一些则是频繁出现的语素或 -er 等其他子词。这样,每个未见词都能表示成若干已知子词单位组成的序列。例如,即使我们从未见过 lower,在它出现时仍可以成功地把它切分成已经见过的 low 和 er。最坏情况下,一个非常罕见的词(例如 GRPO 这样的缩写)也可以在必要时切分成单个字母的序列。
现代语言模型广泛使用两种词元化算法:字节对编码(byte-pair encoding,BPE)(Sennrich et al., 2016)和一元语言建模(unigram language modeling,ULM)(Kudo, 2018)。[2] 本节介绍字节对编码或 BPE 算法(Sennrich et al., 2016; Gage, 1994),参见图 2.6。
与大多数词元化方案相同,BPE 算法包含两部分:训练阶段和编码器阶段。一般来说,在词元训练阶段,词元化器接收原始训练语料库(通常已经按词进行粗略切分,例如按空白符切分),再归纳出一个词元集合,即词表(vocabulary)。随后在编码阶段,词元化器接收一个原始测试句子,使用训练时学到的词表把它编码成相应词元。
2.4.1 BPE 训练¶
BPE 训练算法反复合并频繁相邻的词元,从而创建越来越长的词元。算法最初的词表只包含所有单个字符。接着,它检查训练语料库,找出最频繁相邻的两个字符。假设原始语料库长 10 个字符,使用由 A、B、C、D、E 这 5 个字符组成的词表:
A B D C A B E C A B
最常见的相邻字符对是“A B”,因此将二者合并,把新的合并词元 AB 加入词表,并把语料库中每一对相邻的 A 和 B 替换成新的 AB:
AB D C AB E C AB
现在,可能的词元共有 6 个:A、B、C、D、E、AB,而语料库的长度变为 7。此时最常见的词元对是“C AB”,所以再将它们合并。这样,词表包含 A、B、C、D、E、AB、CAB 这 7 个词元,语料库长度变为 5。
AB D CAB E CAB
算法继续计数并合并,创建越来越长的字符串,直到完成 次合并并产生 个新词元;因此, 是算法的一个参数。最终词表由原始字符集和 个新符号构成。这就是算法的核心。
唯一需要补充的复杂之处在于:实践中的算法通常不会直接在原始字符序列上运行,而是只在词的内部运行。也就是说,它不会跨越词边界进行合并。为实现这一点,输入语料库通常先按空白符和标点切分(使用本章稍后定义的正则表达式)。这样得到一组初始字符串,每个字符串对应一个词的字符序列(空白符通常附在词的开头),同时还带有每个词的计数。随后,计数来自整个语料库,但只允许在各字符串内部合并。
下面用一个很小的合成语料库说明完整算法,其中显式标出了词间空格:[3]
(2.11) set new new renew reset renew
首先,把语料库拆分成带前导空格的词,并记录它们的计数;任何合并都不能越过这些词边界。结果是下面由 4 个词组成的列表,初始词表包含 7 个字符:
语料库 词表
2 □ new □, e, n, r, s, t, w
2 □ renew
1 set
1 □ resetBPE 训练算法首先统计所有相邻符号对。最常见的是 n e,因为它在 new 中出现 2 次、在 renew 中出现 2 次,总计 4 次。然后将这两个符号合并,把 ne 视为一个符号,再重新计数:
语料库 词表
2 □ ne w □, e, n, r, s, t, w, ne
2 □ r e ne w
1 set
1 □ reset现在最常见的符号对是 ne w(总计 4 次),因此将其合并。
语料库 词表
2 □ new □, e, n, r, s, t, w, ne, new
2 □ re new
1 set
1 □ reset接下来,□ r(总计 3 次)合并为 □r,随后 □r e(总计 3 次)合并为 □re。系统实际上归纳出了位于词首的前缀 re-:
语料库 词表
2 □ new □, e, n, r, s, t, w, ne, new, □r, □re
2 □re new
1 set
1 □reset如果继续运行,接下来的合并是:
当前合并 当前词表
(_, new) _, e, n, r, s, t, w, ne, new, _r, _re, _new
(_re, new) _, e, n, r, s, t, w, ne, new, _r, _re, _new, _renew
(s, e) _, e, n, r, s, t, w, ne, new, _r, _re, _new, _renew, se
(se, t) _, e, n, r, s, t, w, ne, new, _r, _re, _new, _renew, se, set图 2.6 BPE 算法的训练部分:输入拆分成单个字符或字节的语料库,通过反复合并词元学习词表。改编自 Bostrom and Durrett(2020)。
2.4.2 BPE 编码器¶
学到词表之后,就可以使用 BPE 编码器对测试句子进行词元化。编码器只是把从训练数据中学到的合并依次应用于测试数据,顺序与学习时相同(即采用贪心方式,从训练数据中最频繁的合并开始)。测试数据中的频率不起作用,只有训练数据中的频率会决定顺序。因此,首先把每个测试句子中的词切分成字符;然后应用第一条规则,把测试语料库中的每个 n e 替换成 ne;再应用第二条规则,把每个 ne w 替换成 new;依此类推。最终,许多合并当然只是重新构造出训练集中的词。不过,这些合并也学到了 re- 前缀等语素(它可能出现在训练时未见的 revisit 或 rearrange 等组合中),以及不带前导空格、因而可位于词内的语素 new;后者可能出现在句首,也可能出现在训练时未见的 anew 等词中。
在实际场景中,BPE 会在非常大的输入语料库上执行数万次合并,产生包含 5 万、10 万甚至 20 万个词元的词表。最终,大多数词可以表示成单个词元,只有较罕见的词(以及未知词)需要表示成多个词元——至少对英语而言如此。对于多语言系统,词元可能被英语占据大半,留给其他语言的词元较少;下文会讨论这一问题。
2.4.3 实践中的 BPE¶
上面的例子只展示了如何从 ASCII 字节序列中学习简单的 BPE。面对 Unicode 输入时,BPE 如何工作?通常,我们在 UTF-8 编码文本的单个字节上运行 BPE。也就是说,先把由一系列码点组成的 Unicode 文本表示用 UTF-8 编码成字节,再把每个字节分别作为 BPE 的输入。因此,BPE 一开始很可能会重新发现 UTF-8 用于编码不同码点的双字节序列和常见三字节序列。同样,只在预先切分的词内部运行 BPE 有助于避免问题。由于一个字节只有 256 种可能取值,不会出现未知词元;不过,BPE 有可能跨字符边界学到某些非法 UTF-8 序列。这类序列非常少见,可以用过滤器消除。
下面考察大型系统中的 BPE 词元化器如何实际应用,例如 OpenAI GPT-4o 使用的词元化器。它含有 20 万个词元,数量相对较大。我们可以使用 Tat Dat Duong 的 Tiktokenizer 可视化工具(https://
Anyhow,·she’s·seen·Jane’s·224123·flowers·anyhow!
可视化使用颜色区分各个词,不过词元化器的真实输出当然只是一系列唯一词元 ID。(如果你感兴趣,对应的 13 个词元是:11865、8923、11、31211、6177、23919、885、220、19427、7633、18887、147065、0。)
请注意,大多数词各自构成一个词元,通常还包含前导空格。’s 等附着词出现在 Jane 这样的专有名词后时会被切分出来,但在 she’s 等高频词中则作为词的一部分计数。数字往往按每 3 位一组切分。有些词(例如 anyhow)在句首大写时与前面带空格且小写时的切分方式不同:句首的 Anyhow 被切分成 Any 和 how 两个词元,而带前导空格的小写 anyhow 只构成一个词元。
其中一些现象与预处理步骤有关。如前文简要提到的,语言模型通常先在预词元化(pretokenization)阶段使用正则表达式切分输入,例如在空格和标点处断开、剥离附着词,并把数字拆分成若干位数字组成的集合;随后才创建词元。第 2.6 节将介绍正则表达式的用法。
我们也可以改变这种预词元化方式,让 BPE 词元跨越多个词。例如,SuperBPE(Liu et al., 2025)和 BoundlessBPE(Schmidt et al., 2025)算法首先通过强制预词元化来归纳常规 BPE 子词词元,然后运行 BPE 的第二阶段,允许跨空格和标点进行合并。由此得到的一大组词元可以提高效率(图 2.7)。
| BPE: | By the way, I am a fan of the Milky Way. |
| SuperBPE: | By the way, I am a fan of the Milky Way. |
图 2.7 SuperBPE 算法通过第二阶段跨空格合并,创建更大的词元。图片来自 Liu et al.(2025)。
实践中用于大语言模型的许多词元化器都是在多种语言上训练的多语言词元化器。然而,由于大语言模型的训练数据绝大部分是英语文本,这些多语言 BPE 词元化器往往把大多数词元用于英语,留给其他语言的词元较少。结果是,它们对英语的词元化效果更好,而其他语言的词往往会被拆成更短的词元。例如,下面给出一道大蕉食谱中的西班牙语句子及其英语译文。
英语版本有 19 个词元;14 个词中有 13 个各自构成词元,只有 nutmeg 被拆成 2 个词元,另外还有 3 个逗号和句末句号。
In·a·deep·bowl,·mix·the·orange·juice·with·the·sugar,·g
inger,·and·nutmeg.
相比之下,原始西班牙语句子的 16 个词被编码成 33 个词元,数量多得多。许多基本词都被拆成片段。例如,表示“深”的 hondo 被切分成 h 和 ondo;表示“果汁”的 jugo、表示“坚果”的 nuez 和表示“姜”的 jengibre 也有类似情况:
En·un·recipiente·hondo,·mezclar·el·jugo·de·naranja·con ·el·azúcar,·jengibre,·y·nuez·moscada.
西班牙语并不是低资源语言;对资源更少的语言而言,这种过度切分(oversegmentation)可能更加严重,甚至经常一直拆到单个字符。这些极小词元会给后续语言处理造成各种问题。第 7 章介绍 Transformer 模型后,这一点会更加清楚:这种碎片化可能导致意义表示变差、所需上下文变长,并提高模型训练成本(Rust et al., 2021; Ahia et al., 2023)。