词元化(Tokenization)是自然语言处理的第一步,指将连续的输入文本分割为若干词元(tokens)的过程。
我们已经讨论了三种可能的词元单位:词(words)、语素(morphemes)和字符(characters)。 但每种作为基本单位都存在问题。 词和语素在语义上似乎处于 NLP 处理的理想粒度,因为它们通常具有相对稳定的含义,但难以形式化地精确定义。 字符虽然定义清晰,但作为词元单位又过小,难以承载足够的语义信息。
本节将介绍当前 NLP 实践中真正采用的方法:通过数据驱动的方式定义词元,这些词元通常大小接近词或语素,但在必要时也可小至单个字符。
为什么需要对输入进行词元化? 一个原因是,将输入转换为一组确定且固定的单位后,不同算法和系统才能就基本问题达成一致。 例如,这段文本有多长?(包含多少个单位?) 或者,“don’t” 或 “New York” 算一个词元还是两个? 因此,标准化的词元化对 NLP 实验的可复现性至关重要。本书后续介绍的许多算法(如语言模型的困惑度(perplexity)指标)都默认所有文本已采用统一的词元化方案。
此外,包含更小单位(如语素或字母)的词元化方法还能有效解决未登录词(unknown words)问题。 什么是未登录词?正如下一章将看到的,NLP 算法通常从一个训练语料库(training corpus)中学习语言规律,再将这些规律用于处理另一个独立的测试语料库(test corpus)。 例如,若训练语料中包含 low、new 和 newer,但不包含 lower,那么当 lower 出现在测试集中时,系统将无法识别它。
为应对这一问题,现代词元器会自动归纳出一组子词(subwords),即比完整单词更小的词元单位。 这些子词可以是任意子字符串,也可以是带有语义的单位(如语素 -est 或 -er)。 在现代词元化方案中,许多词元仍是完整单词,但也包含大量高频出现的语素或其他子词(如 -er)。 这样一来,任何未见过的词都可以被表示为若干已知子词的组合。 例如,即使我们从未见过 lower,当它出现时,仍可成功将其切分为 low 和 er,这两个子词已在训练中出现过。 在最极端的情况下,一个极其罕见的词(比如缩写词 GRPO)甚至可以被切分为单个字母序列。
当前大语言模型广泛采用两种子词切分算法:字节对编码(Byte-Pair Encoding, BPE)(Sennrich et al., 2016)和 unigram 语言模型(Unigram Language Modeling, ULM)(Kudo, 2018)1。 本节重点介绍 BPE 算法(Sennrich et al., 2016;Gage, 1994),见图 2.6。
与大多数词元化方案类似,BPE 算法包含两个部分:训练器(trainer)和编码器(encoder)。 在训练阶段,我们输入一个原始训练语料(通常已通过空格等简单方式粗略分词),从中归纳出一个词表(vocabulary),即一组学习得到的词元。 在编码阶段,编码器接收一条新的测试句子,并将其切分为训练阶段所学词表中的词元序列。
2.4.1 BPE 训练
字节对编码(BPE)的训练算法通过迭代合并高频相邻词元,逐步生成越来越长的词元。
算法从一个仅包含所有单个字符的初始词表开始,然后扫描训练语料,找出出现频率最高的两个相邻字符(或词元)。
假设我们的原始语料由 10 个字符组成,使用 5 个字符的词汇表 {A, B, C, D, E}:
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”,于是我们再次合并,得到新词元 CAB,词表扩展为 7 项 {A, B, C, D, E, AB, CAB},语料进一步缩短为:
AB D CAB E CAB
算法持续统计并合并,不断生成更长的字符串,直到完成 k 次合并操作,从而新增 k 个词元。因此,k 是 BPE 算法的一个关键超参数。 最终的词表由原始字符集加上这 k 个新符号构成。 这就是 BPE 的核心机制。
除此之外,实践中仅有一点复杂之处,BPE 算法通常不会在整个字符序列上无差别地运行,而是限制在单词内部进行合并,即不允许跨词边界合并。 为此,输入语料通常会先通过空格和标点进行初步分割(例如使用本章后面介绍的正则表达式)。 这样得到一组“词级”字符串(每个字符串对应一个词的字符序列,通常在词首保留一个空格标记),并附带各词的出现频次。 后续的计数针对语料库,但是合并操作仅限于每个词内部的字符序列,不能跨越不同词。
我们在一个小型合成语料上完整演示 BPE 的工作流程,我们在词间显式标注了空格(用 ␣ 表示)2:
(2.11) set␣new␣new␣renew␣reset␣renew
首先将语料按词切分(保留词首空格),得到以下 4 种词及其出现次数。注意:合并操作不能跨越这些词边界。 结果如下,包含 4 个词构成的列表,和 7 构成的初始词表。
| corpus | vocabulary |
|---|---|
2 ␣ n e w | ␣, e, n, r, s, t, w |
2 ␣ r e n e w | |
1 s e t | |
1 ␣ r e s e t |
BPE 训练算法首先统计所有相邻字符对,发现 n e 出现次数最多(在 new 中出现 2 次,在 renew 中出现 2 次,共 4 次)。
合并这两个符号,把 ne 当作一个符号加入此表,然后再次统计:
| corpus | vocabulary |
|---|---|
2 ␣ ne w | ␣, e, n, r, s, t, w, ne |
2 ␣ r e ne w | |
1 s e t | |
1 ␣ r e s e t |
最频繁对变为 ne w(共 4 次),合并为 new:
| corpus | vocabulary |
|---|---|
2 ␣ new | ␣, e, n, r, s, t, w, ne, new |
2 ␣ r e new | |
1 s e t | |
1 ␣ r e s e t |
␣ r 出现 3 次,合并为 ␣r,接着 ␣r e 也出现 3 次,合并为 ␣re。 这实际上让系统“学到了”一个常见的词首前缀 re-:
| corpus | vocabulary |
|---|---|
2 ␣ new | ␣, e, n, r, s, t, w, ne, new, r, re |
2 ␣re new | |
1 s e t | |
1 ␣re s e t |
持续进行下去,接下来的合并是:
| merge | current vocabulary |
|---|---|
(␣, 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 |
function BYTE-PAIR-ENCODING(字符串集合 C, 合并次数 k) 返回词表 V
V ← C 中所有唯一字符 # 初始词元为字符
for i = 1 到 k do # 执行 k 次合并
tL, tR ← C 中最频繁的相邻词元对
tNEW ← tL + tR # 拼接形成新词元
V ← V + tNEW # 将新词元加入词表
将 C 中所有 tL, tR 替换为 tNEW # 更新语料表示
return V
图 2.6 BPE 算法的训练部分,接收一个分割为单独字符或字节的语料库,通过迭代合并相邻词元来学习词表。图改编自 Bostrom and Durrett (2020)。
2.4.2 BPE 编码器
一旦我们通过训练得到了词表,就可以使用 BPE 编码器(encoder)对测试句子进行词元化。
编码器的工作方式很简单,按照训练阶段学到的合并规则顺序,在测试数据上依次应用这些规则。
这一过程是贪心的(greedy),且完全依赖训练语料中的频率信息,测试数据本身的频率不会影响切分结果。
首先将测试句子中的每个词拆分为单个字符。
然后依次应用训练中学到的合并规则。先应用第一条规则:将所有 n e 替换为 ne;再应用第二条规则:将所有 ne w 替换为 new;依此类推,直到所有 k 条规则都应用完毕。
最终,许多合并操作会还原出训练集中已有的完整单词。
但更重要的是,这些规则也隐式地学到了语素结构。例如前缀 re- 可能出现在训练中未见过的组合中,如 revisit 或 rearrange;或不带前导空格的子词 new(即词内部形式)可用于句首单词(如句首的 “New”)或训练中未出现的新词,比如 anew。
当然,在实际应用中,BPE 通常会在非常大的语料库上执行数万次合并操作,从而生成规模为 50,000、100,000 甚至 200,000 的词表。 这样的设计使得大多数常见词可被表示为单个词元,仅少数低频词或未登录词需要被拆分为多个子词词元。 不过,上述优势主要适用于英语等资源丰富的语言。 在多语言系统中,由于英语数据通常占主导地位,导致其他语言可用的词元数量相对不足,这一点我们将在下文进一步讨论。
2.4.3 BPE 在实践中的应用
前面的例子仅展示了基于 ASCII 字节序列的简单 BPE 学习过程。 那么,BPE 如何处理 Unicode 输入呢?
在实际系统中,我们通常将 BPE 应用于 UTF-8 编码后的字节序列。 具体来说,首先将文本的 Unicode 表示(即一系列码位)用 UTF-8 编码为字节,然后将每个字节作为 BPE 的输入单元。 因此,BPE 在训练初期往往会“重新发现” UTF-8 中常见的 2 字节或 3 字节编码模式。 此外,如前所述,仅在预切分的词内部运行 BPE,有助于避免跨字符边界产生非法合并。 由于一个字节只有 256 种可能取值,因此不会出现“未知字节”。不过,在极少数情况下,BPE 可能在字符边界处学习到非法的 UTF-8 字节序列。这类情况非常罕见,可通过后处理过滤器轻松剔除。
我们看看像 OpenAI GPT-4o 这类大型系统中使用的工业级 BPE 分词器的例子。 该词元化器拥有 20 万个词元(200K tokens),属于规模较大的设计。 我们可以使用 Tat Dat Duong 开发的 Tiktokenizer 可视化工具(https://tiktokenizer.vercel.app/)来查看任意句子的词元数量。 例如,下面是我们构造的一句无意义英文句子的分词结果(可视化工具用居中圆点表示空格):

图中用不同颜色区分单词,但请注意:分词器的真实输出只是一串唯一的词元 ID。 (如果你感兴趣,这句共包含以下 13 个词元 ID:11865, 8923, 11, 31211, 6177, 23919, 885, 220, 19427, 7633, 18887, 147065, 0)
我们注意到,大多数单词(通常包含前导空格)被整体保留为单个词元。
附着词(clitics)如 ’s 在专有名词(如 Jane)后会被单独切分,但在高频词(如 she’s)中则与主词合并。
数字通常被按三位一组切分。
某些词在句首大写时会被拆分(如 Anyhow → Any + how),而在小写、非句首位置则作为一个整体(anyhow)。
这些步骤中,有些属于阶段。
正如前文简要提到的,语言模型通常会在 BPE 之前先进行预分词,使用正则表达式分割输入,例如在空格和标点处分割文本、剥离附着词(如 's, n't)、将长数字按三位分组等。
我们将在 2.7 节详细介绍正则表达式的使用。
也可以修改预分词策略,允许 BPE 词元跨越多个单词。 例如,SuperBPE 算法分两阶段工作:第一阶段采用预分词,学习常规 BPE 词元。 第二阶段允许跨越空格和标点进行合并。 结果是生成一组更大的复合词元,从而提升编码效率。见图 2.7。

图 2.7 SuperBPE 算法通过第二阶段的跨空格合并,生成更大的词元。引自 Liu et al. (2025)。
当前大多数大语言模型使用的分词器都是多语言的,在多种语言数据上联合训练。 但由于大语言模型的训练数据在很大程度上以英文文本为主,这些多语言 BPE 分词器往往会将大部分词元用于英语,留给其他语言的词元就相对较少。 其后果是英语分词效果较好,其他语言的词常被切分为更短的词元。 例如,下面是一句来自大蕉食谱的西班牙语句子及其英文翻译:
英文句子共 18 个词元,14 个单词全部未被切分(每个词对应一个词元):

而对应的西班牙语原句虽只有 16 个单词,却被编码为 33 个词元,数量大增。
注意许多基础词汇被拆开。例如 hondo(“深”)拆分为 h + ondo。
同样还有 jugo(juice)、nuez(nut)、jenjibre(ginger):

西班牙语并非低资源语言;在真正低资源的语言中,这种过度切分问题会更加严重,甚至退化到逐字符切分。 这种过度分割会带来一系列下游问题。 正如我们在第 8 章介绍 Transformer 模型时将更清楚看到的那样,这种碎片化导致语义表示质量下降、需要更长的上下文窗口、模型训练成本显著增加 (Rust et al., 2021;Ahia et al., 2023)。