跳转至

NLP高频面(50)大模型(LLMs)分词(Tokenizer)详解

本文讨论了自然语言处理(NLP)任务中大型语言模型(LLM)的分词方法,详细介绍了BPE、WordPiece和Unigram三种主流子词分词算法的原理、优缺点、适用场景、训练机制以及对未登录词(OOV)的处理方式,并给出了相应的代码示例。关键要点包括:

分词的重要性:在 NLP 任务中,分词是将文本转换为模型可处理数字序列的关键步骤,其质量影响模型的词汇表大小、表示能力和对未知词汇的处理能力。

BPE 算法:通过反复合并高频符号对构建词汇表,实现简单、训练速度快,适用于大规模语料和多种语言,如 GPT-2/GPT-3 采用基于 BPE 的分词方案。

WordPiece 算法:以最大似然估计为目标选择合并对,考虑概率增益,在谷歌系的模型如 BERT 中广泛使用,能优化词表质量。

Unigram 算法:从大型候选子词集合开始,逐步淘汰不良子词,提供子词概率模型和多样性切分,适用于对分词质量要求高或需要子词正则化的场景。

OOV 处理:子词分词方法通过将新单词拆分为子词缓解 OOV 问题,字节级分词可编码任何输入,多语言模型通过混合多种语言文本训练词表减少 OOV。

训练机制:三种算法的训练机制各有不同,BPE 是逐步合并,WordPiece 基于概率选择合并对,Unigram 是迭代削减。

工程建议:根据数据规模、模型类型和任务需求选择合适的分词算法,词汇表大小是重要超参数,通常在 30k 到 50k 之间。

在自然语言处理(NLP)任务中,将文本转换为模型可处理的数字序列是必不可少的一步。这一步通常称为分词(tokenization),即把原始文本拆分成一个个词元(token)。对于大型语言模型”(LLM,Large Language Model,大型语言模型)而言,选择合适的分词方案至关重要:分词的质量直接影响模型的词汇表大小、表示能力以及对未知词汇的处理能力。

早期的做法是以“单词”为基本单位进行分词,即通过空格或规则将文本切分成独立的单词。但是纯单词级分词存在明显缺陷:词汇表(vocabulary)可能非常庞大(尤其对多语言或大量专业术语的场景),这会导致模型参数量增加(每个词元对应的嵌入向量)且无法覆盖所有可能出现的单词。当模型遇到未登录词(OOV,Out-Of-Vocabulary)时(即不在词汇表中的新词),传统处理方式只能将其标记为一个特殊的“未知”符号,完全丢失该词的信息。

另一种极端是 字符级分词 , 即将每个字符作为基本词元。字符级方法将词汇表缩小到字母表大小(例如英语26个字母再加标点、空格等), 从而彻底避免了OOV问题。然而,字符序列长度会大幅增加,模型需要跨更多的时间步才能理解完整单词的含义,训练收敛速度变慢,而且难以直接捕获跨字符的词法或语义信息。


为了解决单词和字符分词的局限,业界提出了子词分词(subword tokenization)方法。所谓子词分词,就是将单词进一步拆解成更小的常见片段(子词单位),这些片段比字符长、比单词短,能在控制词表规模的同时尽可能保留语义信息。当模型遇到未见过的新单词时,可以通过组合子词来表示它,从而缓解OOV问题。如今,大多数大模型都采用子词分词技术,典型的算法包括 BPE、WordPiece 和 Unigram 等。下面,我们将详细介绍这些分词器的原理、优缺点、适用场景,以及它们各自的训练机制和处理未登录词的策略。

一、 分词器的主要类型

子词分词算法有多种实现方式,其中最主流的三种是 Byte-Pair Encoding (BPE)、WordPiece 和 Unigram。它们都旨在自动从语料中学习一个高效的子词词汇表,但原理和策略各有差异。本节将分别介绍这三种分词器的基本原理、优缺点和典型的适用场景。

BPE 分词算法(Byte-Pair Encoding)

BPE(Byte-Pair Encoding,字节对编码)原本是一种数据压缩算法,后来在2015年被应用于机器翻译领域的子词分词【注:由Sennrich等人提出】。BPE分词的核心思想是:反复将频率最高的符号对合并为新符号,从而逐步构建出高频子词。这里的“符号”初始可以理解为单个字符(对于英文通常是字母、数字、标点等)。通过合并高频字符序列为更长的符号,BPE能学习到常见的词根或词缀等子词。

基本原理:BPE 从训练语料出发,先进行一次初步的切分(通常先按空格将文本切分成单词列表,词内再把每个单词拆成字符序列)。初始 词汇表 就是训练语料中所有出现过的字符。然后,算法会统计在所有单词中每一对相邻符号的出现频次,找到出现次数最多的一对符号,将这对符号合并为一个新的符号,并将该新符号加入词汇表。合并后,所有训练单词中的相应符号对都会被替换为新符号,从而语料的符号序列发生了变化。算法重复这一过程,不断用新的合并符号替换序列,迭代进行,直到达到预定的词汇表大小上限或者没有符号对可以合并。

通过这种贪心的频率合并策略,BPE优先保留了最常见的子串作为单个词元。例如,在英文语料中,“ing”作为连续字符出现频繁,BPE可能会将 $ \underline{\text{i}} $、 $ \underline{\text{n}} $、 $ \underline{\text{g}} $逐步合并为符号“ing”。这样,“playing”“running”等单词都可以包含子词“ing”,模型能学到这个后缀的含义。同时,像“un”这样的前缀如果常见也会成为一个子词。随着迭代进行,较长的、频繁出现的片段会变成词表中的词元,而较罕见或只出现一次的单词则可能仍被拆分为较小的单元(甚至逐字母拆分),以保证不因为罕见词占用宝贵的词表容量。

优点:

简单高效:BPE 实现相对简单,只需要统计频次和替换符号对,易于实现和并行化处理。训练速度快,适用于大规模语料。

有效的子词捕获:基于频率的合并保证了高频片段被优先保留,常见词缀、词根会成为词元,有助于模型理解单词结构。

减少 OOV:几乎所有单词都能被拆解表示出来,只要组成它的字符在词表中(通常训练语料会包含语言中的所有常用字符)。相比全词词表,BPE 大大降低了未登录词率,模型可以处理训练中未见的新词。


通用性强:BPE 对语言没有特别假设,适用于多种语言。对于形态丰富的语言(如德语、土耳其语),BPE 能将长复合词拆成可识别的子成分;对于空格分词不明显的语言(如中文),BPE 可以直接在字符级别合并常用字序列,相当于自动学习词库。

缺点:

贪心次优:BPE 每次选择最频繁的合并,缺乏全局概率模型的指导,可能不是全局最优划分。例如某些频繁对出现只是因为各自高频但组合意义未必强,BPE 仍会合并。相比之下,WordPiece 会考虑合并对概率增益。

无法生成多种划分:BPE训练得到固定的合并规则后,分词结果是确定的(对给定输入,总是按相同规则划分)。这虽然一致,但在一些任务中缺乏多样性(比如数据增强需要不同切分方式),BPE无法直接提供备选切分。

对低频组合不敏感:BPE完全依赖频次,可能忽略一些语义上有意义但频率不算最高的组合。比如两个符号组合出现次数不算顶多,但几乎总是一起出现(强依赖关系)时,BPE不一定会合并它们。WordPiece的概率比率机制在这方面更灵敏。

合并结果依赖语料:BPE词表高度取决于训练语料的统计。如果应用域与训练语料差别大,可能遇到较多拆分现象(虽然至少能拆分,不会彻底OOV,但可能生成较长的子词序列)。此外,BPE不能动态调整词表,应对新领域需要重新训练词表或接受次优的切分。

适用场景:

WordPiece 分词算法

WordPiece 分词算法最初应用于语音识别和机器翻译(据称在 2012 年由 Schuster 等人用于日语和韩语的语言模型),后在谷歌的神经机器翻译和 BERT 等预训练模型中得到广泛使用。WordPiece 与 BPE 在宏观流程上相似:也是逐步合并子词构建词汇表,但合并判断标准 不同。WordPiece 并非简单依据频次最高来合并,而是选择 可以最大程度提升语言模型概率 的合并。直观来说,WordPiece 在每次合并时会评估“将这两个符号当作一个符号,对整个语料的似然提升是多少”,选择提升最大的那对符号进行合并。

基本原理:WordPiece 以 最大似然估计 为目标来训练词表。初始词表同样包含训练语料中出现的所有字符。然后,对于每一个可能的符号对 (X, Y),如果将其合并成新符号 Z,那么语料中所有出现 XY 的地方都可以改用 Z 表示。WordPiece 会计算合并后语料的整体概率(通常假设词元序列概率是各词元概率的乘积,即简单的 “unigram” 模型),并与未合并前的概率做对比。选择使得语料训练概率增幅最大的那对符号进行合并。这种概率增益的判断,可等价转化为如下准则:选择使得

$$ \frac{P(XY)}{P(X)\times P(Y)} $$

值最大的符号对进行合并。这里 P(X) 表示符号 X 在语料中的概率,P(XY) 表示符号 X 后跟符号 Y 的联合概率。直观地说,如果两个符号独立出现的概率乘积远小于它们连在一起出现的概率,说明它们组合在一起有很强的凝聚性,更像一个整体,此时合并能带来显著的模型概率提升。


WordPiece 每次按照上述标准合并一个符号对,并更新词表和语料表示,然后重新计算各符号的概率,再继续下一次合并,直到达到预定的词表大小。由于这种策略会避免一些纯频率高但组合松散的对,而偏好那些“结合更紧密”(互信息更高)的组合,所以相比 BPE,WordPiece 的合并结果在概率意义上更优。

优点:

基于概率的全局优化:每次合并考虑的是全局的似然增益,避免了一些仅凭频率但对模型帮助不大的合并。这使得词表更符合模型的语言分布,往往能取得略好于 BPE 的性能(据一些实验,对相同词表大小下,WordPiece 有时能提高模型的预测概率或下游任务表现)。

抑制高频噪音组合: 由于分母包含 P(X)P(Y),那些虽然频繁出现但各自也很常见的组合,其评分未必最高。例如,在上面的例子中,字符 "u" 和 "g" 连在一起出现20次,看似频繁,但 "u" 本身在语料中出现很多次、"g" 也很多,而 "g" 和 "s" 只连在一起出现5次却几乎涵盖了所有 "s" 的出现(因为 "s" 很少出现且基本都跟在 "g" 后),因此 "g"+"s" 的合并对于模型来说更有价值。WordPiece 会更早选择 "g","s" 合并,使“gs”成为一个词元(对应结尾“gs”这样的组合,例如单词“hugs”就能作为整体处理),而 BPE 则先选择 "u","g"。这种差异体现了 WordPiece 对互信息的偏好,能够合并一些低频但更有粘性的片段。

与语言模型训练契合:WordPiece 的目标函数本质上与语言模型(unigram 模型)的对数似然一致,这意味着训练得到的子词集合在理论上是使语料概率最大化的子词集。这对于与语言模型的兼容性很友好,也为其后来在 BERT 等自回归/自编码模型中应用打下基础。

减少冗余词元:通过最大似然准则,一些对整体概率贡献很小的子词不会被加入词表,词表更紧凑。例如,如果一个长单词可以整体作为一个词元来极大提高概率,WordPiece 可能直接引入整个单词,而不会浪费词表容量在其零散部分上(除非这些部分也有独立意义或频率)。

缺点:

实现复杂度略高:相比 BPE 纯频率统计,WordPiece 需要计算概率增益或比例,这涉及维护词频和对每次合并计算全局似然变化。实现时通常仍可用频次近似概率(如上述比值计算),但逻辑稍复杂,训练速度可能略慢于BPE。

结果对初始概率估计依赖:WordPiece假设了一个朴素的独立概率模型。在实际计算中,一般用频率近似概率,但这种近似可能并不精确。不过实践证明这种简单估计已足够奏效。

与 BPE 结果差异有限: 尽管理念不同,但在大规模语料上,WordPiece 和 BPE 往往学到非常相似的高频子词集合。它们在大多数常见词的处理上一致,只在少数边缘情况下(如前述极端频率分布组合)有所区别。因此,相对于 BPE 来说,WordPiece 的实际收益有时不明显,很多情况下性能相近。

开源工具相对较少:BPE 有非常多开源实现(如 SentencePiece、SubwordTextEncoder 等),而原生支持 WordPiece 训练的工具相对较少(但 HuggingFace 的 tokenizers 库和 TensorFlow Text 等提供了一些支持)。不过由于两者相似,也可以通过微调 BPE 实现来达到同样效果。

适用场景:


$$ P(w)=\sum_{s\in S(w)}\prod_{t\in s}p(t) $$

其中 $ S(w) $ 表示 $ w $ 所有可能的切分集合,每个切分 $ s $ 是一些词元 $ t $ 的序列。Unigram 分词的目标是找到一组词元及其概率分布,使得整个训练语料的似然 $ P(\text{corpus}) = \prod_{w \in \text{corpus}} P(w) $ 最大。直接优化这一目标非常复杂,因为 $ S(w) $ 的组合数天文数字级别。Kudo 等人提出使用期望最大化 (EM) 算法迭代求解,同时在迭代中逐步精简词表:

  1. E 步骤(期望计算):基于当前词元概率分布,计算整个语料的对数似然 $ L = \sum_{w} \log P(w) $。同时,可以计算每个词元在语料中的“使用概率”。具体做法是对每个训练单词,用前向后算法计算其所有切分的概率分布,然后得到该词中每个候选词元出现的概率期望值。简单来说,就是算出在当前模型下,每个词元被用来解释语料的次数(是个期望值,可以小数),类似软统计。

  2. M 步骤(最大化): 根据 E 步得到的词元使用频率期望,重新估计每个词元的概率参数 $ p(t) $(通常令 $ p(t) = \frac{\text{count}(t)}{\sum_{t'} \text{count}(t')} $,即使用频次期望值重新正则化)。

Unigram 分词算法

Unigram 分词算法由 Kudo 等人在 2018 年提出,其核心思想和前两者截然不同:它不是通过逐步合并来构建词表,而是从一个大型候选子词集合开始,逐步淘汰不良子词,最终留下一个最优的子词词表。因为最终留下的子词可以视作一个一元语言模型(unigram language model)的词汇,所以称为 Unigram 分词。这个算法也是 Google 开源工具 SentencePiece 中的一种实现模式。

基本原理:Unigram 分词首先需要准备一个初始的“大词表候选集”。这个初始集合应尽可能完备,包含训练语料中可能出现的各种子串。常见做法是:把训练语料做一次初步的预切分(如按空格切成单词列表),然后收集所有单词本身以及所有可能的子词片段。例如,可以取所有出现频率大于一定阈值的子串(包括单个字符和较短的多字符串)作为初始候选词元。这个初始集合往往相当大(可能数十万甚至上百万词元)。接下来,算法基于这套候选词元对整个语料的分词概率进行建模与优化。

具体来说,Unigram 假设词元序列符合一元独立分布模型,即一个词元表征单位独立出现的概率为 $ p(t) $。那么对于训练语料中的每个单词 w,它可能有多种不同的切分方式(由候选词元组成)。模型可以计算单词 w 的总概率为:

初始化:赋予初始候选词元集合中的每个词元一个初始概率(通常根据词频或长度做一些启发式设定,或者干脆均匀分配)。确保所有基础字符都在集合中并有非零概率,以保证任何单词都有至少一种切分(逐字符切分)。

词元筛选:保留所有词元中对整体概率贡献最大的那些,淘汰贡献极低的词元。具体实现时,一般采取删除一定比例(如10%或20%)的词元:计算每个词元从词表中移除后对模型似然的影响($


\Delta L$ 增加值,如果移除该词元导致的对数似然下降很小,说明它不重要),然后根据这个影响大小从小到大排序,删除影响最小的若干词元。被删除的通常是那些使用率极低、几乎不用的词元。

迭代:移除一批词元后,剩下词元的概率重新初始化为上一轮估计值,重复执行E步和M步,更新概率分布。然后再次进行词元筛选、删除。如此循环,词表规模会逐渐缩小。

终止:当词表大小达到预设目标(例如32000个词元),或者无法再删减时,停止迭代。最终的词元集合和它们的概率分布即为训练结果。

在这个过程中,Unigram 分词不断 筛选(淘汰)子词,因此可称为“子词筛选策略”。与 BPE/WordPiece 从下往上构建词元不同,Unigram 是从上往下精简词元集合。最终的词表往往趋向于选择那些能充分解释数据且彼此配合良好的词元,而冗余或不常用的词元被移除。需要注意的是,Unigram 算法会确保 所有基础字符始终保留,以保证任意输入文本都能被切分(最差情况逐字符切分),所以不会出现完全无法切分的OOV。

优点:

全局最优词表:由于采用了概率模型和迭代优化,Unigram 理论上可以得到比贪心算法更优的全局词表配置。它同时考虑了所有切分方式,通过删除那些影响很小的词元,确保留下的组合对整体概率贡献最大。

灵活的切分选择: Unigram 最终学得每个词元的概率值,这意味着对同一个单词,可能存在多种有效切分。解码时通常选择概率最大的切分方案,但如果需要也可以根据词元概率随机采样切分(称为子词正则化,Subword Regularization)。这在数据增强或提高模型鲁棒性上很有用:训练时对同一句子每次略微不同的切分喂给模型,能让模型对词汇边界的细微变化更健壮。这一点是基于合并规则的BPE/WordPiece所不具备的。

对低频词处理友好: Unigram 会保留一些看似低频但有用的词元,只要它们组合起来能解释一定量的数据。同时,它也可能舍弃一些纯高频的冗余片段。如果某个长单词频率不高但几乎不出现别的地方,BPE 可能不会把它整个作为词元(因为频率低),但 Unigram 有可能保留它作为整体(如果它内部切分也很罕见的话)。这有助于减少过长的序列。

多语言兼容:像 SentencePiece 实现的 Unigram 可以直接在未经预先分词的原始字符序列上操作(它会用特殊符号表示空格,例如在词首加“_”表示词边界),这使它适用于任何语言,包括没有明确分词界定的语言(中文、日文等)。Unigram 能自主学出跨字符的常见组合,相当于同时完成了分词和子词划分,免去了繁琐的语言特定预处理。

缺点:

实现和计算量较大:Unigram 算法涉及到 EM 训练和多次迭代删词,计算量相比 BPE/WordPiece 要高得多。特别是在初始词表很大时,每轮需要计算概率、期望,删词,再重估参数。如果语料极大,这个过程会比较耗时。不过 Google 的开源实现(SentencePiece)在 C++ 中进行了高度优化,使其实用可行。

需要处理概率平滑:由于迭代过程中词元概率会变化,可能出现一些数值上的问题,需要注意平滑和下限控制。例如删除词元后剩余概率需要重新归一化,防止某些词元概率过高主导。此外,分词概率模型只是近似真实文本分布,可能存在过拟合语料分布的风险(不过一般影响不大)。


单词切分结果不唯一:对于已训练好的 Unigram 模型,同一个词可能有多种切分可能性(因为没有像 BPE 那样明确的合并规则限制唯一划分)。虽然通常会选最高概率的切分,但在某些应用中,不希望分词有歧义。不过实际来看,如果模型正确训练,最高概率切分往往也是直观上最合理的切分,歧义不大。

禾被厂泛米用:虽然学术上Unigram表现优秀,但在工业应用中,BPE/WordPiece仍更常用。这可能是因为BPE的简单快速足以胜任大多数任务,同时许多现有模型(如GPT系列)采用BPE成为事实标准。Unigram 主要在 Google 的部分模型中使用(如 XLNet、T5 等采用了 SentencePiece 的 Unigram 分词),在开源社区知名度相对低一些。

适用场景:

总结来看:BPE、WordPiece、Unigram 都属于子词分词的范畴,区别在于词表构建算法的不同。BPE 倾向于快速贪心地捕获高频片段,WordPiece 追求概率优化的子词选择,Unigram 则采用生成模型思路全局筛选词元。在实际应用中,需要根据资源情况和任务需求选择:若追求简单高效,BPE 足矣;如需与既有模型兼容或更好的概率解释,可用 WordPiece;需要灵活性和细粒度概率模型时,则考虑 Unigram。了解它们的异同有助于工程师更有针对性地训练分词器。

二、 分词器如何处理 OOV 问题

未登录词(OOV,Out-of-Vocabulary)问题是分词和词表设计中必须考虑的重要方面。OOV指模型在预测或生成时遇到了不在训练词汇表中的词汇。传统上,解决OOV最简单粗暴的方法是使用一个特殊的“”标记代表任何未知词。这意味着模型无法区分不同未知词,全都映射为相同的标记,损失了词级语义信息。在早期机器翻译等任务中,经常可以看到大量“”出现在翻译结果里,影响可读性和准确度。

子词分词方法在很大程度上缓解了 OOV 问题。由于子词词表包含了基本的字符或常见片段,即使一个完整单词不在词表中,我们也能将其拆分为多个子词来表示,只要组成它的子词都在词表中。理想情况下,只要一个单词可以由训练中出现的更小片段组合而成,模型就不算真正“OOV”。例如,在英文 BERT 的词表中没有“GPU”这个单词,但因为词表包含了“gp”和“##u”这两个子词(这里“##”表示该子词连接前面,不是词首),那么输入“GPU”会被切分为[“gp”,“##u”],模型仍能处理并组合出它的含义。

各类子词分词器对 OOV 的处理机制如下:

BPE/WordPiece:这两种方法都会确保所有训练出现的字符都纳入基础词表。因此,任何由已知字符组成的新单词理论上都能被逐字符地切分表示出来,不会彻底无法编码。然而,如果输入文本包含训练语料从未见过的字符,那仍然是无法处理的真·OOV,此时通常用标记替代那个字符(以及整个无法切分的部分)。不过在实践中,常见语言的字母表/字符集在训练中基本都能覆盖。只有遇到非常特殊的符号(如未训练过的表情符号、新增的罕见字形)等,才会触发。例如,上节BPE例子中,如果训练语料从未出现过字符“m”,那么词“mug”在BPE词表下无法完全切分——“m”不在词表,它就只能输出[“”,“ug”]。为避免这种情况,训练时往往会把ASCII字母表或者目标语言的基本字符全集都加入初始词表。总的来说,BPE/WordPiece通过子词组合实现开放词汇:新词被拆解为已知部分来表示,大幅降低了需要的概率。


Unigram:Unigram 同样在训练中保留所有基础字符(以及空格符号等),因此能够表示任意由这些字符组成的字符串。不同的是,Unigram 分词在推理时可能存在多种切分选择。通常解码器会选择概率最高的切分路径。如果某个新词无法用词表中的较长子词来切分(因为没有匹配的子串),模型会退而求其次使用更小的子词,直到最小单位的字符。这样即使遇到组合新奇的词,最坏情况下也能逐字符表示出来,不会出现完全不能编码的情况。因此,Unigram 天然也解决了OOV,但需要注意保证基本字符集完备。此外,Unigram 还允许随机采样切分,这对生成任务(如数据增强)有意义,但对处理未知词意义不大——遇到未知词时通常还是确定性地用最细粒度的切分。

字符级回退:有些分词器实现会在BPE/WordPiece词表之外增加一个策略:如果发现某个完整单词无法用已知子词完全覆盖,就改为按字符切分该单词(即使字符本身未在词表,也可以临时按字符处理)。这种策略需要模型也有能力逐字符处理(即embedding层能cover字符),所以实践中不常见,因为词表通常固定。相反,更常见的是确保词表涵盖字符避免这种情况。

特殊标记法:对于一些特殊情况,会有专门的标记处理。比如数字、网址、email等,如果词表不可能穷举所有变体,可能在预处理时统一替换为某种标记或拆分规则(这不是子词算法本身提供的,而是额外的规则化处理,目的是降低OOV风险)。

字节级分词:这是 OpenAI GPT-2 采用的方法,值得单独提出。GPT-2 使用了字节对编码(Byte-Level BPE),它与普通BPE区别在于:初始符号不是Unicode字符本身,而是文本的 UTF-8 字节序列(0~255 的字节值集合)。这样,模型的基本词表大小是256(所有可能字节),而词表最终大小例如 GPT-2 为50257(包含了一些控制符号和合并子词)。字节级BPE的好处是能够编码任何输入:即使输入文本包含训练中未见的新字符,也能通过其UTF-8编码字节一一表示出来,不会出现。例如,某些稀有表情符号或其他语言文字符,对于普通BPE也许是OOV,但字节级BPE可以直接把它分解成若干字节值(可能每个字节作为一个词元,或者几个字节组合成词元)。这使 GPT-2 完全摆脱了(它的词表里没有未知标记)。当然,字节级也有弊端:有些Unicode字符由多个字节组成,GPT-2 会把它拆成多个词元,稍微增加了序列长度,而且得到的子词不再是人类可读的“片段”,而是字节组合(可能出现乱码片段)。但从工程角度,这种方案保证了对任意输入的鲁棒性。后来许多多语言模型(如 GPT-3 等)也延续了字节BPE的思路,使得一个词表可以覆盖多语言文字符集而无需显式列出所有字符。

总的来说,现代LLM的分词器都尽可能减少OOV出现:要么通过子词分解覆盖新词

(BPE/WordPiece/Unigram),要么干脆通过字节覆盖所有可能符号(GPT-2方案)。这相比早期固定词汇的方案是一个巨大的进步,使模型几乎可以应对任何输入。不过,需要注意词表的语言边界:如果一个模型的词表是针对英文训练的,即使处理子词,它也难以有效表示比如中文句子(尽管能逐字符拆分为Latin字节或未知符),因为那些字符在训练中从未出现过,模型也没学过。为此,多语言模型通常在训练分词器时就混合多种语言文本,以在词表中纳入各语言的字符或子词。例如,BERT多语言版的词表包含了多种语言的字符和子词,SentencePiece可以直接在混合语料上训练得到跨语言的子词表。这样做确保模型跨语言时也极少遇到OOV。

在实现层面,常用的 HuggingFace 分词器或 SentencePiece 分词器在应用时如果遇到真的无法切分的字符,会输出特殊的 标记。我们平常可以通过 tokenizer.encode(“some text”) 来观察这一点:大部分情况下,每个输入字符都会被转为已知的 token id;只有在完全不支持的情况下才出现 unk id。在正常英文、中文等任务中,基本不会看到 unk(除非处理未训练字符,如古文字符号等)。


简而言之,子词分词的引入使得模型能够以有限大小的词表处理无限新词汇,提供了一种近似的“开放词汇”机制。不同算法在这方面的差异主要体现在是否存在极端未见字符时的策略,但对常规文字,它们都能可靠地拆分表示,极大降低了的使用频率。这也是为什么当前的LLM很少像早期系统那样输出一堆“”——得益于子词分词,模型拥有更精细的单位来应对各种语言现象。

三、 BPE分词器的训练机制

上一节介绍了 BPE 的原理和特点,本节将深入探讨 BPE 分词器是如何从训练语料中学习得到子词词汇表和合并规则的。了解训练机制有助于我们理解为什么 BPE 会选择某些子词,以及在工程实现上如何编写算法完成训练。下面我们以算法步骤和具体示例代码相结合的方式进行讲解。

BPE 算法步骤

初始准备:遍历训练语料,先进行基本的预分词。例如,可以简单地按照空格将文本切分成单词序列。收集所有唯一单词及各自的出现频次。然后将每个单词拆解成字符序列(通常在每个单词末尾还附加一个特殊结束符以表示边界,但为简化解释,这里暂不考虑结束符)。此时,初始词汇表设为训练语料中出现的所有字符集合。

统计符号对频率:扫描所有单词(按其频次计权),统计在这些字符序列中每一对相邻符号的总出现次数。例如,如果“ab”作为相邻对出现了5次(可能在不同单词中或同一单词不同位置),则频次为5。找出出现频次最高的符号对(X, Y)。

合并符号对:将上述频次最高的符号对 X Y 合并为新的符号 Z,并将 Z 加入词汇表。然后,更新所有训练单词的符号序列表示:凡是出现相邻 X Y 的地方,都替换为单个符号 Z。这样,序列长度会缩短,词表中新增加了一个元素。

重复迭代:重复步骤2和3。每次迭代都重新统计当前序列中的相邻符号对频率,选择最高者合并为新符号。不需要重新初始化频率,而是基于上次合并后的新序列继续统计。

终止条件:不断迭代直到词汇表大小达到预先设定的目标(比如设定最终子词个数为30,000,则在初始字符表基础上进行了29,000次合并),或者没有符号对可以合并(极少发生,除非语料很小)。训练结束,输出最终的词汇表和合并规则序列(记录每一次合并的符号对顺序)。

通过上述过程,我们得到了一个包含基础字符和若干合并子词的词表。实际使用BPE分词器对新文本分词时,可以按照训练得到的合并规则顺序将文本中的字符序列逐步合并,还原出最终的子词序列。这通常通过贪心匹配来实现:从左到右尽可能匹配词表中最长的符号。由于BPE的合并规则保证了词表中不存在可以进一步合并的相邻符号组合,所以这种贪心匹配是确定且等价于按规则逐步合并的结果。

需要注意的是,一些实现会在每个单词的末尾加特殊符号(如)来表示单词边界,以避免跨单词合并。这可以确保例如“low”与“lowest”的结尾不会因BPE而误合并。但在实践中,如SentencePiece等工具采取了另一种方法,即在单词开头加入特殊空格符“_”当作一个符号,以标记词边界,然后允许跨空格的合并(因为空格本身也是一个符号)。无论哪种处理方式,其目的都是保证分词后能还原空格和单词分界。在下面的示例中,我们暂不考虑特殊边界符,直接用基础字符演示算法。


BPE 训练示例代码

下面通过一个简单的Python脚本,演示BPE训练过程的几个迭代步骤。假设我们的训练语料很小,仅包含如下单词及频次: “hug” 出现 10 次 “pug” 出现 5 次 “pun” 出现 12 次 “bun” 出现 4 次 “hugs” 出现 5 次

这只是一个虚拟的小语料,旨在方便演示BPE合并过程。我们将使用该语料来一步步构建BPE词表。

代码块 from collections import Counter

初始化语料单词及频次

words = {"hug": 10, "pug": 5, "pun": 12, "bun": 4, "hugs": 5}

将每个单词初始化为字符序列(列表),并建立初始词汇表为所有字符

segmentations = {word: list(word) for word in words}

vacab = set()

for word, seg in segmentations.items(): vocab.update(seg)

print("初始词汇表:", vocab) print("初始分割表示:", segmentations)

迭代进行符号对合并

num_merges = 3 # 演示进行3次合并 for i in range(num_merges): # 统计当前所有相邻符号对的频次 pair_freq = Counter() for word, freq in words.items(): seq = segmentations[word] for j in range(len(seq) - 1): pair = (seq[j], seq[j+1]) pair_freq[pair] += freq

运行上述代码,将得到类似输出:

代码块

1 初始词汇表:{'b', 'u', 'g', 'p', 'n', 'h', 's'}


2 初始分割表示:{'hug': ['h', 'u', 'g']}, 'pug': ['p', 'u', 'g']}, 'pun': ['p', 'u', 'n']}, 'bun': ['b', 'u', 'n']}, 'hugs': ['h', 'u', 'g', 's']}

解释:初始状态下,词表是所有出现过的字符: $ {b, u, g, p, n, h, s} $,每个单词都以单字符序列表示。第1次迭代中,统计发现频率最高的相邻符号对是 "u"+"g",共出现了20次(“hug”10次和“hugs”5次里各有一次“ug”,“pug”5次里也有一次“ug”,总计20)。于是我们将“u”和“g”合并成新符号“ug”。此后,所有这些单词的表示中,“u","g”变成了一个符号“ug”(如“hug”变为[“h”,“ug”])。

第2次迭代,再次统计相邻符号对频次。合并“ug”后,新的最高频符号对是“u"+"n",出现16次(“pun”12次和“bun”4次)。我们合并得到新符号“un”,并替换这些序列中的“u","n"。此时“pun”变为[“p”,“un”],“bun”变为[“b”,“un”]。

第3次迭代,最高频符号对变为 "h"+"ug",出现15次(“hug”10次,“hugs”5次)。合并得到符号 "hug"。替换后,“hug”整个单词现在成为单个符号[“hug”],“hugs”变为[“hug”,“s”]。经过三次合并,我们停止迭代(在实际训练中会进行更多次,这里为了演示提前停止)。最终的词汇表新增了 {"ug","un","hug"} 三个子词,总大小从最初7增加到10。每个训练单词现在的切分结果如上所示。

在实际应用中,如果我们将BPE训练进行足够多次合并,直到达到设定的词表大小,比如10k、30k等,那么得到的词汇表将包含大量类似 "hug", "ug", "un", "ing", "ion", "pre", ... 这样的子词。在编码新文本时,我们会按照训练得到的词表,对输入单词尝试从左到右匹配尽可能长的词表项。例如,在上面的最终词表中,假如我们遇到新词 "bug" :我们会尝试找到以“b”开头的最长词表子词。词表里没有“bu”或“bug”(这里只是示例,小词表未必有),但有“b”和“ug”,并且输入“bug”前两字母是“bu”不在词表,而“b”在词表,所以首先匹配“b”,剩下“ug”刚好在词表,因此“bug”被切分为[“b”,“ug”]。类似地,如果遇到"mug"(假设训练中没出现过‘m’),那么‘m’不在词表,会被视为,后面“ug”可以匹配,最终得到[“”,“ug”]。这个例子说明:BPE生成的词表能应对大多数组合,但对完全没出现过的基础符号还是无能为力,只能用未知标记。

实践中的注意事项

现实中,BPE 分词训练还有一些细节优化和变体:

词边界标记:**正如前面提到的,实际实现通常会在每个单词末尾添加特殊标记(如)以避免跨词合并。例如,如果没有边界标记,“low”和“lowest”共享前缀“low”,BPE可能将“low”合并为符号,进而把“lowest”错分为[“low”,“est”]而不是[“low”,“##est”](假设##表示续接)。引入边界标记可以防止这种跨单词干扰。另一方法是在单词前加特殊符号(SentencePiece的做法),如在“hello world”变为“hello__world”,然后BPE可以在包含"”的串上操作。这使得所有词元都可以跨单词界,但“_”本身指示了新词开始,方便解码还原空格。

合并次序与词表: **BPE 训练输出一般包括合并规则列表和最终词表两种形式。实际使用中更常用词表直接进行分词(贪心匹配),而无需每次回放合并过程。词表中可以看出哪些子词包含其他子词作为前缀,比如“hug”包含“h”前缀。通常在实现中会将词表构建成前缀树或trie,加速匹配。


特殊符号: **分词器通常需要处理特殊符号或标记,比如数字、标点、控制符。BPE 本身将它们视作字符处理,但工程上可能会先将某些特殊串替换或保留。例如保留“[CLS]”,“[SEP]”之类的标记(BERT)、或者不对一些特定标记进行切分。

大小写和标准化: 训练分词前往社会对语料做标准化,比如全转小写(BERT英文就是不区分大小写分词),或者移除一些罕见符号。像 GPT-2 则保留大小写和标点,通过额外的正则清洗工具处理文本(例如去除不必要的空白,修正unicode异常)。这些预处理细节也会影响最终词表内容。

总的来说,BPE 分词器训练是一个相当直接的过程,复杂度近似线性于语料长度乘以迭代次数。在实践中训练一个30k词表的BPE在千万级单词的语料上是很快的。BPE 词表的质量依赖于语料覆盖面,工程上通常尽可能提供多样且大规模的文本给分词器训练,以获得稳健的词表。接下来,我们将对比介绍 WordPiece 和 Unigram 的训练机制,它们在流程上有所不同。

四、 WordPiece 分词器的训练机制

WordPiece 分词器的训练与 BPE 有很多相似之处:也是从字符开始不断合并词元直到达到目标词表大小。然而,由于 WordPiece 使用了最大似然准则来选择合并对,它在实现上需要计算每个候选合并对对语料概率的影响。本节我们详细说明 WordPiece 的训练步骤,并提供相应的代码示例来突出它与 BPE 的区别。

WordPiece 算法步骤

初始设置:与 BPE 相同,首先对语料进行预切分、统计词频,并将每个单词拆成字符序列。初始词汇表包含所有出现的字符。

  1. 计算初始概率:为每个词元(初始为单字符)赋予一个概率值 $ p(t) $,可以简单地按频率归一化初始化(例如 $ p(t) = \frac{\text{freq}(t)}{\text{总字符数}} $)。整篇语料的初始似然可以近似计算为各单词切分概率的乘积(或对数和)。

  2. 选择最佳合并:枚举当前所有相邻符号对 $ (X, Y) $,如果将其合并成新符号 $ Z $,计算合并后语料的对数似然增益。等价地,我们寻找能最大化 $ \frac{P(XY)}{P(X)P(Y)} $ 的那对符号(该比值大于1意味着 $ X $ 与 $ Y $ 联结后比独立出现更有利)。在实现中,我们通常利用频次近似概率,例如用 $ \ \frac{\text{freq}(XY)}{\text{freq}(X)} $ times $ \text{freq}(Y) $ 作为评分。选择评分最高的符号对 $ (X^, Y^) $ 作为本次合并。

  3. 执行合并更新:将 $ X^, Y^ $ 合并成新符号 $ Z^ $ 加入词表。扫描所有单词序列,凡是匹配 $ X^, Y^ $ 相邻的地方都替换为 $ Z^ $。更新各词元频次统计:新符号 $ Z^ $ 的频次等于原来 $ X^Y^ $ 对的频次, $ X^ $ 和 $ Y^* $ 的频次需要相应减少(因为一部分被合并掉了)。

更新概率分布:基于新的频次表,重新估计每个词元的 p(t)。可以依然使用频率占比来估计,或进行简短的EM更新以最大化似然。不过多数实现直接用频率比近似即可。更新后,重新计算可能的符号对组合及其评分。

重复迭代:重复步骤3-5,直到词汇表大小达到预定上限。


WordPiece 的上述流程看起来跟 BPE 区别不大,只是在选择合并对时用了不同的准则。然而,由于每次合并后都需要重新计算概率分布,WordPiece 训练在代码实现上比 BPE 复杂一些。好在频率和概率呈一定正相关关系,我们可以频率统计结合公式直接选对而不每次真的算对数似然。

WordPiece 选取合并对示例

为突出 WordPiece 的合并选择标准,我们继续使用上一节的小语料例子,比较 WordPiece 和 BPE 在第1次合并时可能的差异。上一节我们算得各相邻符号对频次为:

(“u”, “g”) : 20 (“p”, “u”) : 17 (“u”, “n”) : 16 (“h”, “u”) : 15 (“g”, “s”) : 5 (“b”, “u”) : 4

BPE 选择频次最高的 ("u","g") 合并。而 WordPiece 将计算每对的评分,例如使用频率近似概率:

score = freq(XY) / (freq(X) * freq(Y))。我们来计算 ("g","s") 的评分: freq("g","s")=5, freq("g")=20,

freq("s")=5 (注意freq("g")=20来自“hug”10次、“pug”5次、“hugs”5次,总共20个g;freq("s")=5来自

“hugs”5次),则

$$ \operatorname{score}(g,s)=\frac{5}{20\times5}=\frac{5}{100}=0.05. $$

再算(“u”,“g”):freq("u","g")=20,freq("u")=36,freq("g")=20(freq("u")=36来自所有单词的u出现总和),则

$$ \operatorname{score}("u","g")=\frac{20}{36\times20}\approx0.0278. $$

类似地,“h”,“u”),(“p”,“u”),(“u”,“n”)因为“u”很频繁,它们的评分都会较低(约0.028左右)。这样比较,“g”,“s”)反而成为评分最高的对。直觉解释:字符“s”非常少见,只出现伴随“g”,因此合并“gs”可以大幅提升这些位置的概率;而“u”和“g”虽常一起出现,但“u”到处都是,组合并不“惊喜”。

因此,WordPiece 首次可能会选择合并(“g”,“s”)而不是(“u”,“g”)。这样,“hugs”这个单词就能整体或部分被更好地表示。可以编写一段代码来自动选择评分最高的符号对:

代码块

假设已得到每个符号及符号对的频次字典 token_freq 和 pair_freq(如根据上例计算)

token_freq = {"h":15, "u":36, "g":20, "p":17, "n":16, "b":4, "s":5}


pair_freq = {(h",u):15, (u",g):20, (p",u):17, (u",n):16, (b",u):4, (g",s):5}

best_pair = None

best_score = 0.0

for (x, y), freq_xy in pair_freq.items(): score = freq_xy / (token_freq[x] * token_freq[y]) if score > best_score: best_score = score best_pair = (x, y) print("最佳合并符号对:", best_pair)

假如我们将上例中的频次代入运行,上述代码会输出:

代码块

1 最佳合并符号对: ('g', 's') 2 1

表明 Word Piece 选择合并“g”和“s”。后续迭代中,词频和概率会变化,Word Piece 可能进行一系列不同于 BPE 的合并。不过值得注意的是,经过足够多轮后,Word Piece 和 BPE 最终词表可能都包含{“ug”,“un”,“hug”,“gs”}等符号,只是顺序不同而已。Word Piece 的优势更多体现在每一步的决策更合理,从而在词表容量有限时优先保留对模型更有用的词元。

实际实现细节

实际训练WordPiece时,可以不严格执行每次迭代重算概率的完整EM过程,而是每轮用公式计算合并评分快速选取。这通常能得到与精确计算几乎相同的结果。很多实现为了方便,直接采用与BPE相同的实现框架,只是在选择下一个合并对时改为计算上述评分。例如,Google在翻译系统中使用WordPiece时即采用类似方法。

还有一个实现细节是:WordPiece 在生成最终词表时,往往会对词元加上特殊的格式标记,比如BERT的词表中,凡不是完整单词的词元都以“##”作为前缀。这只是输出表示上的不同,表示该词元需要连接到前一个词元才能构成原始单词。训练过程中不需要“##”,它可以在生成词表文件时根据词频信息添加:通常凡是词元内部出现过(不是词首)的子词,就标记为“##子词”,词表中也存储这种形式。这有助于分词结果的可读性和还原,但不影响模型输入使用,因为模型其实只看token id,对这些前缀并不“感知”。

模型适配:
**训练得到WordPiece词表后,就可以用相同的方式对训练数据分词,进而训练语言模型或下游模型。在使用中,WordPiece分词对OOV的表现和BPE类似:新词会被拆成已有子词或字符。由于WordPiece保证包含基础字符,未知字符仍然只可能来自完全未知的符号集,需用。

小结:WordPiece 训练相对于 BPE 多了一个 “计算评分选对” 的步骤,但本质上仍是贪心地构建词表。它通常得到与 BPE 效果相当或略优的词表,对于像 BERT 这样训练数据极其丰富的场景,


WordPiece能够细微地提升模型对词的表示能力。在工程上,如果已有成熟的BPE实现,可以尝试通过计算互信息来选择合并对,从而模拟WordPiece效果。如果使用HuggingFace的BertTokenizer或WordpieceTokenizer,背后其实已经隐含使用了这些规则,通常不需要我们手动训练;但要复现BERT词表训练过程,可参考上面的算法思路。

五、 Unigram 分词器的训练机制

最后,我们讨论 Unigram 分词算法 的训练过程。与前两种方法的渐进合并不同,Unigram 采用 迭代削减 的方法。由于它要在庞大的候选集上进行概率模型训练,理解起来相对复杂。本节将尽量以直观方式解释 Unigram 的训练,并给出部分机制的代码示例。

Unigram 训练算法步骤

前面已概述了Unigram训练的大致过程,这里再整合强调关键点:

构造初始候选集合:这一步非常重要,直接影响最后结果。通常做法是:取语料中的所有单词本身作为候选;再取所有长度大于1且频次高于某阈值的子串作为候选。为了限制规模,可以设定一个上限,比如初始候选最多包含50万项。如果语料很大,可以通过截频或其他策略控制子串数量。初始集合一定要包含所有字符。另外,通常会引入特殊的空格标记“_”作为一个符号,用来标识词或句子的开始(在SentencePiece中,每个原始单词前会加一个"",这样模型可以学习跨越空格的组合,同时知道""表示边界)。

初始化词元概率:可以将所有候选词元的初始概率设为与其频率成比例的一致值,然后归一化,使得 $ \sum t \in \text{Vp}(t) = 1 \setminus \text{sum}_{{t \in \text{V}}} p(t) = 1 \sum t \in \text{Vp}(t) = 1 $。这给了模型一个起点。

迭代过程:重复以下子步骤直到词表大小达到目标:

收敛结束:不断迭代删除和重估,直到词表大小等于目标大小(或者删除动作不再明显提升整体概率)。输出剩余词元及它们的概率。

以上过程结合了 EM 算法(在固定词表上估计概率)和启发式删减(精简词表)交替进行。由于每轮删除的都是对模型贡献最小的词元,可以认为模型的损失提升最小化地达到了词表压缩。这种方法相当于逐步逼近最优子集:初始包含所有可能子词,然后一点点逼近我们需要的小词表。

代码示例:计算单词概率与最优切分

完整实现 Unigram 训练需要较多代码,这里我们聚焦其中的关键机制:给定一个词汇表及其词元概率,如何计算单词的概率和最优切分。这既是模型计算似然的基础,也是之后分词时选取最优切分的依据。


我们可以使用动态规划来高效计算一个单词的概率和最优切分:设输入单词为 $ w $,长度为 $ N $ 个字符。定义 $ dp_prob[i] $ 为前 $ i $ 个字符(即子串 $ w[0 : i] $)的概率之和, $ dp_prob[\theta] = 1 $ 表示空串概率为1。则对于每个位置 $ \i $(1到 $ N $),我们考虑所有可能的前向切分:

$ dp_prob[i] = \sum_{j=0}^{i-1, w[j:i] \in V} dp_prob[j] \times p(w[j : i]) $

这就是计算子串 $ w[0 : i] $ 的所有切分概率和。其中 $ w[j : i] $ 表示从字符位置 $ j $ 到 $ i - 1 $ 的子串。如果这个子串是词表中的一个词元,则从状态 $ j $ 可以转移到 $ i $,概率乘以该词元的概率。最终 $ dp_prob[N] $ 即为整个单词的概率 $ P(w) $。

类似地,我们可以用 $ \text{dp_best}[i] $ 来记录前 $ ii $ 个字符的最大概率(对应最优切分),并用一个数组 $ \text{prev}[i] $ 记录实现该最大概率切分时前一个切分点的位置,以及使用的词元。这就是 Viterbi算法应用于分词的问题。具体实现如下:

代码块

1 def best_segmentation(word, vocab_probs): 2 ""利用动态规划求给定词在当前词表下的最优切分及其概率"" 3 n = len(word) 4 # dp_best[i] 表示 word[:i] 的最大概率 5 dp_best = [0.0] * (n + 1) 6 prev_index = [-1] * (n + 1) 7 best_token = [''] * (n + 1) 8 9 dp_best[0] = 1.0 # 空串概率1 10 for i in range(1, n+1): 11 dp_best[i] = 0.0 12 prev_index[i] = None 13 best_token[i] = None 14 # 遍历所有可能的切分点 j,使得 word[j:i] 在词表中 15 for j in range(0, i): 16 token = word[j:i] 17 if token in vocab_probs and dp_best[j] > 0:

上面的代码定义了一个函数 $ \underline{\text{best_segmentation}} $,它返回给定单词的最佳切分及其概率。我们还构造了一个示例的 $ \underline{\text{vocab_probs}} $ (这是训练某轮迭代后词表及概率的假想值),并对单词“hugs”进行测试。

假设我们的 $ \underline{\text{vocab_probs}} $ 包含必要的词元,其概率可能并不完全归一化(这里只是为了演示),运行这段代码,可能输出:


代码 $ _单词 $ 'hugs' 的最优切分:['hug', 's'],概率:0.00750021

(具体结果取决于设定的概率,这里只是示例。)

这个结果解释为:在给定的词表和概率下,“hugs”最可能的切分是[“hug”,“s”],对应概率0.0075。换算一下,如果 $ p(“hug”) = 0.15 $, $ p(“s”) = 0.05 $,则概率乘积为 $ 0.15 \times 0.05 = 0.0075 $,正好符合输出。而其他切分如[“h”,“ug”,“s”]概率会是 $ 0.1 \times 0.1 \times 0.05 = 0.0005 $,[“h”,“u”,“g”,“s”]更低,[“hugs”]单词本身概率可能是0.05(但这里“hugs”在词表概率0.05,比0.15*0.05小,故没被选)。通过动态规划,我们既可以得到最优切分(用于实际分词输出),也可以得到总概率(如果把上面代码改为累加而不是取最大,就能算出 $ dp_prob[n] = P(w) $,不过由于浮点下溢,实际多用对数形式存概率)。

Unigram 词表筛选策略

有了计算概率的基础,我们再看如何判断删哪些词元。直观方法是:对于词表中每个词元 tt t,我们把它从词表移除(即设其概率为0,重新归一化剩余概率),再计算新的语料对数似然。如果移除 tt t导致的对数似然下降 $ \Delta L\ \Delta L\ L $ 很小,则认为 tt t 不重要,可以删去。

完全严格地这么做计算量很大,但可以近似:

每个词元 t 对语料似然的贡献大致为 $ f(t)\log p(t) $,其中 $ f(t) $ 可以理解为词元 t 在所有单词切分中被用到的次数(概率和)。我们在 E-step 已经算出了这种期望频次 $ f(t) $。如果移除 t,那些 $ f(t) $ 次出现不得不用其他切分替代,损失大约 $ f(t)\log p(t) $(当然不是线性减,因为其他词元概率会重新分配,但此估计用于排序足够)。因此,可以根据 $ f(t)\log p(t) $ 或等价的 $ f(t)\log p(t) $ 升序来排序删除。

实际实现时,SentencePiece在每轮会计算一个loss增量数组,删除那些loss增量最小的词元以达到删除百分比。

关键的一点:永远不删除基础字符。这些字符频次虽低但一旦删了某些字就永远无法表达含该字的单词了,所以它们被锁定保留。

训练迭代示意

由于篇幅原因,这里不附上完整的 Unigram 训练代码。我们可以描述一下一个迭代的变化:

假设初始词表非常大,包括很多冗余词元。经过第一轮筛选,低频的长词元大量被移除,词表迅速减小。同时,模型重新估计概率后,会提高某些通用子词的概率,降低了一些只在特定单词中用到的片段的概率。随着迭代进行,词表逐渐收敛到那些既常见又必要的子词集合上。例如,如果“ization”作为一个整体和拆成“ize”+“ation”都能解释数据,但在概率模型下可能“ize”和“ation”各自也经常组合其它词,因而得分更高,所以“ization”可能被淘汰(除非“ization”几乎只以整体形式出现)。相反,一个非常独特的名称比如“OpenAI”如果在语料里出现几次,BPE也


许拆成 “Open” + “AI”,但 Unigram 可能保留下 “OpenAI” 整词作为一个词元(前提是它出现在候选里且几次出现没有其他替代切分比它概率更高)。

经过多轮之后,词表达到目标大小,此时每个词元都有一个概率值。对于分词,常规做法是选取最可能的切分(即用上面的Viterbi方法)。而如果需要数据增强,可以在每个位置根据词元概率抽样选择下一个词元(这实现比较复杂,需要用前向采样算法,但原理就是以 $ p(t)p(t)p(t) $为权重随机走路径)。这种随机采样切分的策略被称为子词正则化,在一些论文中证明可以提高模型的稳健性。

实际应用

在实际工程中,几乎不会手写实现 Unigram 训练,而是使用开源工具 SentencePiece。使用 SentencePiece 时,可以选择 --model_type=unigram 让其训练 Unigram 分词模型。它会自动完成上述过程,并输出一个词表文件,其中包括每个词元及一个评分(对数概率)。SentencePiece 还支持 --model_type=bpe 来训练 BPE 模型,因此成为对比不同分词算法的便利工具。

一些预训练模型使用了 Unigram 分词,例如 Google 的 XLNet(中文XLNet使用SentencePiece Unigram),以及 T5(它针对每种语言训练了一个SentencePiece模型,多语言版则在混合语料上训练统一的Unigram词表)。这些模型之所以选用 Unigram,部分原因是其开发者希望利用子词正则化提升模型泛化,或因为 SentencePiece Unigram 对多语言处理较为方便。对终端用户来说,这些区别通常是透明的:使用相应的Tokenizer即可,无需感知背后的算法。但是,当我们需要自行训练分词器、或者调整词表大小时,理解 Unigram 的机制就很有帮助。

六、 总结

综上所述,我们深入探讨了当前主流的大模型分词器算法:BPE、WordPiece 和 Unigram。它们各有思想起源:

BPE:基于频率贪心合并,高效简单,在各领域广泛应用,是子词分词的开创者之一。

WordPiece:引入了概率最大化准则,选择合并更加稳健,被BERT等模型采用。

Unigram:采用生成模型视角,全局优化词表,能提供概率和多样性,是一种更复杂但功能更强大的方法。

在优缺点方面,BPE 胜在实现容易、速度快且效果已被证明足够好;WordPiece 对细节概率分布更敏感,在极大语料上略有优势;Unigram 则提供了独特的子词概率模型,可用于正则化和多语言,代价是训练复杂度更高。

对于未登录词问题,三种方法都提供了良好的解决方案:它们确保任何由已知字符组成的新词都可以被拆成子词来表示,避免了语义完全缺失的。特别地,GPT-2的字节BPE进一步将字符集扩展到字节层面,彻底杜绝了无法表示的字符。

从工程实用性看,如果您需要训练自己的分词器,可以考虑以下建议:

数据规模较大、追求稳妥可靠:使用 BPE。现有库丰富,训练快速,效果有保障。

希望复现或对齐 BERT 等模型:使用 WordPiece(其实用 SentencePiece 的 BPE 模式也能兼容,大差不差)。


多语言或想尝试子词正则化:使用 Unigram(通过 SentencePiece 工具实现)。虽然训练慢一些,但一次生成词表即可多次使用。

无论选择哪种算法,词汇表大小都是一个重要超参数。较大的词表意味着更少的拆分、更短的序列,但也意味着模型嵌入矩阵更大、稀疏风险更高;较小的词表反之。通常30k到50k是常见范围,具体取决于语料多样性和模型规模(模型大时可适当增大词表)。

最后,分词器是 NLP 系统中经常被忽视却非常关键的一环。一个不恰当的分词方案可能会割裂有意义的词片段,或引入过多冗余符号;而一个精心训练的分词器则能提升模型的理解和生成能力。希望通过本文的详解,读者对 BPE、WordPiece、Unigram 有了全面而深入的认识,在实践中能够根据任务需要选择并调优合适的分词策略,为大型语言模型的训练和应用打下坚实的基础。