大模型词元化技术解析:BPE、WordPiece与Unigram算法对比与实践
1. 项目概述:为什么词元化是大模型的第一道“工序”?
如果你最近在玩大语言模型,不管是自己部署一个开源模型,还是调用API,你可能会好奇,你输入的一段话,模型到底是怎么“看懂”的?它看到的不是“你好,世界”这几个汉字,而是一串数字。这个将人类可读的文本,转换成模型可理解的数字序列的过程,其核心的第一步,就是 词元化 。你可以把它想象成给模型准备“食材”的过程:我们吃苹果是整个啃,但模型“消化”文本,需要先把它切成大小合适的“块”,这些“块”就是词元。今天要聊的BPE、WordPiece和Unigram,就是三种最主流的“切菜刀法”。
为什么这个词元化如此关键?因为它直接决定了模型的“词汇量”和“理解粒度”。用字符当词元?那“apple”一个单词就得拆成5个字母,序列太长,效率低下,模型也很难学到有意义的组合。用整个单词当词元?那词汇表会爆炸(想想所有可能的单词和它们的变形),还会遇到没见过的新词。所以,我们需要一种折中的、智能的切分方法,这就是子词词元化技术登场的背景。它让模型既能处理常见词(如“playing”),也能通过组合处理罕见词或新词(如“ChatGPT”)。理解了BPE、WordPiece和Unigram,你才算真正摸到了大模型文本处理的门道。
2. 核心算法原理与设计思路拆解
2.1 目标与挑战:寻找文本的“最优分割”
在深入具体算法前,我们必须明确词元化要解决的核心问题:给定一个庞大的文本语料库,如何学习一个固定大小(比如5万)的词表,使得用这个词表来表示任何新文本时,所需的词元总数尽可能少?这本质上是一个数据压缩和表示学习问题。
这里有几个关键约束和考量:
- 词表大小可控 :不能无限大,否则模型嵌入层参数爆炸,计算和存储都无法承受。
- 覆盖度与粒度平衡 :词表需要能高效表示常见词(粒度粗一些),同时又能通过组合表示罕见词(粒度细一些),避免出现“未登录词”。
- 算法效率 :训练阶段从海量文本中学习词表,以及推理阶段对新文本进行切分,都需要高效的算法。
- 语言无关性 :理想的方法应该对英语、中文、代码、公式等各种文本都能较好地处理。
BPE、WordPiece和Unigram给出了三种不同的解题思路,它们共享一个核心理念: 从基础单元(如字符)开始,通过迭代合并,形成更大的、更有意义的子词单元。
2.2 Byte Pair Encoding:从数据压缩到自然语言处理
BPE最早是一种简单的数据压缩算法,核心思想非常直观: 迭代地合并语料中最频繁共现的相邻符号对 。
算法步骤详解:
-
初始化
:将训练语料中所有单词按字符拆分,并在每个单词末尾添加一个特殊的结束符(如
</w>),用以区分单词边界。此时,词表就是所有字符加上结束符。 - 统计频次 :统计所有相邻符号对的共现频率。例如,“l o w”中,“l o”是一个对,“o w”是一个对,“w ”是一个对。
- 合并最高频对 :找到频率最高的符号对,将它们合并成一个新的符号,并加入词表。例如,如果“e”和“s”经常连在一起出现(如“desk”,拆成“d e s k”),那么就将“es”作为一个新词元。
- 迭代 :重复步骤2和3,直到词表大小达到预设目标,或者没有更多可以合并的符号对。
为什么BPE有效? 它通过频率驱动,自然地将经常一起出现的字符组合成子词。高频词如“the”、“ing”会很快被合并成独立词元,而低频词则保持字符级拆分或形成独特的子词。它的优势在于 简单、高效、无需语言先验知识 。OpenAI的GPT系列、早期的BERT都使用了BPE或其变种。
注意 :BPE在合并时只考虑相邻符号的 原始频率 ,它假设高频共现就等于“好”的合并。但这有时会产生问题,比如“e”和“r”都很高频,但“er”合并可能不如“ing”合并有意义,因为“er”可能出现在很多不相关的上下文中(如“better”、“user”、“water”)。
2.3 WordPiece:来自BERT的“概率驱动”合并策略
WordPiece是Google为BERT模型提出的算法。你可以把它看作是BPE的一个“升级版”。它们的前期步骤几乎一样:初始化字符词表,迭代合并。 关键区别在于合并的“评选标准” 。
BPE看频率,WordPiece看 合并带来的似然值提升 。
算法核心:似然最大化
- 初始化与BPE相同。
- 对于每一种可能的合并(将两个相邻词元A和B合并为AB),我们计算合并后对整个训练语料库的 语言模型似然值 的影响。具体来说,通常使用一个非常简单的基于词元频率的一元语言模型。
-
选择能最大程度增加似然值的合并对
。公式化的选择标准是:
score = freq(A, B) / (freq(A) * freq(B))。这个公式可以理解为,A和B的共现频率,相对于它们各自独立出现的频率的“惊喜”程度。分数越高,说明A和B的绑定关系越强,合并后对模型似然的提升越大。 - 迭代执行,直到词表达到目标大小。
WordPiece的优势
:
通过引入概率标准,它倾向于合并那些
结合紧密、语义上更可能是一个整体
的符号对。例如,相比于泛泛的“er”,“##ing”(WordPiece中常用
##
表示子词前缀)这种具有明确语法功能的后缀,其合并得分可能会更高,因为它与动词词干的结合更具特异性。这使得生成的词元往往在语言学上更有意义。
实操心得 :在实现WordPiece时,那个似然计算公式的简化版本
freq(A,B)/(freq(A)*freq(B))计算量很大,因为每次合并后都要重新计算所有相邻对的分数。工程上会有一些优化,比如只更新受影响的局部统计信息。但理解其“概率驱动”的核心思想,比记住公式更重要。
2.4 Unigram Language Model:一种“自上而下”的逆向思维
如果说BPE和WordPiece是“自底向上”的合并策略,那么Unigram Language Model(ULM)则是一种“自顶向下”的拆分策略。它的思路非常不同: 我们先假设一个很大的种子词表(比如所有常见子词和字符),然后评估每个词元的重要性,逐步淘汰最不重要的那个。
算法步骤详解:
- 初始化一个大词表 :通常可以用其他算法(如BPE)先产生一个明显大于目标值的词表,或者简单地用所有频繁出现的子串初始化。
- 定义概率模型 :为词表中的每一个词元分配一个出现概率,所有词元概率之和为1。对于一个给定的句子,它的出现概率可以用所有可能的分割方式中,各词元概率乘积的最大值(或和)来估算。
-
期望最大化训练
:
- E步(分割) :固定当前词元和它们的概率,使用维特比(Viterbi)算法为语料库中的每个句子找到 最可能 的词元分割序列。
- M步(更新) :根据E步得到的所有句子的分割结果,重新统计每个词元出现的次数,并更新它们的概率(次数/总词元数)。
- 词表剪枝 :在每一轮EM迭代后,计算每个词元的“损失”——如果从词表中移除该词元,对语料库总似然值的影响有多大。然后移除那些损失最小的词元(即最不重要的词元)。
- 迭代 :重复步骤3和4,直到词表大小缩减到目标值。
Unigram的核心优势与特点 :
- 灵活性 :它可以输出 多个可能的分割结果及其概率 ,而不仅仅是唯一分割。这对于处理歧义很有用。
- 概率框架清晰 :整个词表和学习过程都在一个统一的概率语言模型框架下,非常优雅。
- 子词多样性 :由于是从大词表剪枝而来,且评估标准是全局似然贡献,它可能保留一些不常出现但对特定领域很重要的子词。
SentencePiece工具包中的
--model_type=unigram
模式就是这种算法的实现。它的计算成本通常比BPE和WordPiece要高。
3. 三种算法对比与选型指南
了解了原理,我们该如何选择?下面这个表格从多个维度进行了对比,并给出了典型的应用场景。
| 特性维度 | BPE (Byte Pair Encoding) | WordPiece | Unigram Language Model |
|---|---|---|---|
| 核心思想 | 迭代合并最高频相邻对 | 迭代合并能最大提升似然的相邻对 | 从大词表开始,基于概率模型迭代剪枝最不重要词元 |
| 训练方向 | 自底向上 (合并) | 自底向上 (合并) | 自顶向下 (剪枝) |
| 驱动信号 | 原始共现频率 | 共现频率与边缘频率之比 (近似似然提升) | 词元对整体语料似然的贡献度 |
| 输出确定性 | 确定性的唯一分割 | 确定性的唯一分割 | 可输出概率化的多种分割 |
| 计算复杂度 | 低 | 中等 (需计算分数) | 高 (需要EM迭代和维特比解码) |
| 主要优势 | 简单、快速、无需预定义规则,对多语言友好 | 合并结果在语言学上可能更有意义,缓解高频但不紧密组合的问题 | 框架优雅、灵活,可输出概率,能保留重要稀有子词 |
| 潜在缺点 | 可能合并出语义不紧密的单元,对稀有词处理可能不够好 | 实现稍复杂,计算量比BPE大 | 训练速度慢,初始词表选择会影响结果 |
| 经典应用 | GPT系列、RoBERTa、XLM-R | BERT、DistilBERT、ALBERT | SentencePiece (Unigram模式)、T5、mT5 |
选型建议:
- 追求简单和速度,且语料质量高、覆盖广 : BPE 是稳妥的起点。它久经考验,在多语言任务上表现稳健,是很多开源项目的默认选择。
- 为掩码语言模型(如BERT)训练词表 : WordPiece 是经过验证的选择。它的设计初衷就是为此类任务服务,其合并策略与预测被掩码词元的任务目标有内在契合。
- 需要分割灵活性、处理高度专业领域文本(如生物医学、代码),或研究概率化分词 : Unigram 值得尝试。它能更好地适配领域特定词汇,并且概率化输出为下游任务提供了更多信息。
- 一个通用的实践 :使用 SentencePiece 工具包。它同时实现了BPE和Unigram算法,并且统一了处理流程(将空格等也视为普通字符,无需前置分词),极大方便了实验和部署。你可以先用BPE快速出一个基线,再用Unigram微调,对比效果。
4. 实操:使用SentencePiece训练与使用词表
理论说得再多,不如动手练一遍。这里我们以最流行的
SentencePiece
工具为例,展示从训练词表到实际分词的完整流程。我们假设你有一个纯文本文件
corpus.txt
作为训练语料。
4.1 环境准备与安装
首先,安装SentencePiece。最方便的方式是通过pip:
pip install sentencepiece
或者,如果你需要从源码编译以获得最新特性:
git clone https://github.com/google/sentencepiece.git
cd sentencepiece
mkdir build
cd build
cmake ..
make -j $(nproc)
sudo make install
sudo ldconfig -v
4.2 训练词表:BPE vs Unigram
SentencePiece将BPE和Unigram统一在了同一个框架下,通过
--model_type
参数指定。
使用BPE算法训练:
spm_train --input=corpus.txt \
--model_prefix=spm_bpe_model \
--vocab_size=32000 \
--model_type=bpe \
--character_coverage=1.0 \
--pad_id=0 --eos_id=1 --unk_id=2 --bos_id=-1 \
--max_sentence_length=16384 \
--input_sentence_size=1000000
使用Unigram算法训练:
spm_train --input=corpus.txt \
--model_prefix=spm_unigram_model \
--vocab_size=32000 \
--model_type=unigram \
--character_coverage=1.0 \
--pad_id=0 --eos_id=1 --unk_id=2 --bos_id=-1 \
--max_sentence_length=16384 \
--input_sentence_size=1000000
关键参数解析:
-
--input: 你的训练语料文件,一行一个句子或文档。 -
--model_prefix: 输出模型文件的前缀,会生成.model和.vocab文件。 -
--vocab_size: 目标词表大小。常见尺寸有32k, 50k, 100k等。需要根据语料规模和任务权衡。 -
--model_type: 核心参数,可选bpe,unigram,char,word。 -
--character_coverage: 对于已知字符集的覆盖率,比如对于大多数字符集设为1.0,对于像日文这样字符众多的语言可以设为0.9995。这影响初始字符集的构建。 -
--pad_id,--eos_id,--unk_id,--bos_id: 定义特殊词元的ID。-1表示禁用。通常<pad>=0,</s>=1(eos),<unk>=2是兼容Hugging Face等库的常见设置。 -
--max_sentence_length: 处理句子的最大长度,超长部分会被截断。 -
--input_sentence_size: 用于训练的子采样句子数。如果语料太大,可以用这个参数随机采样一部分,加速训练。
注意事项 :训练词表是 数据预处理中最重要的一步 ,其质量直接影响模型性能。务必使用与你的下游任务 领域相同、分布相似 的语料进行训练。用通用语料训练的词表去处理医学论文,效果会大打折扣。
4.3 加载模型与执行分词
训练完成后,我们可以在Python中使用训练好的模型。
import sentencepiece as spm
# 加载BPE模型
sp_bpe = spm.SentencePieceProcessor()
sp_bpe.load('spm_bpe_model.model')
# 加载Unigram模型
sp_uni = spm.SentencePieceProcessor()
sp_uni.load('spm_unigram_model.model')
text = "大语言模型的词元化处理是一个关键技术点。"
# 编码:文本 -> 词元ID列表
ids_bpe = sp_bpe.encode_as_ids(text)
ids_uni = sp_uni.encode_as_ids(text)
print(f"BPE IDs: {ids_bpe}")
print(f"Unigram IDs: {ids_uni}")
# 解码:词元ID列表 -> 文本
text_bpe = sp_bpe.decode_ids(ids_bpe)
text_uni = sp_uni.decode_ids(ids_uni)
print(f"BPE Decoded: {text_bpe}")
print(f"Unigram Decoded: {text_uni}")
# 查看词元化结果:文本 -> 词元列表
pieces_bpe = sp_bpe.encode_as_pieces(text)
pieces_uni = sp_uni.encode_as_pieces(text)
print(f"BPE Pieces: {pieces_bpe}")
print(f"Unigram Pieces: {pieces_uni}")
# 查看词表
vocab_size = sp_bpe.get_piece_size()
print(f"Vocab size: {vocab_size}")
for i in range(30): # 打印前30个词元
print(f"{i}: {sp_bpe.id_to_piece(i)} -> {sp_bpe.is_control(i)}")
输出分析 : 你可能会看到类似这样的结果:
BPE Pieces: ['▁大', '语言', '模型', '的', '词', '元', '化', '处理', '是', '一个', '关键', '技术', '点', '。']
Unigram Pieces: ['▁', '大', '语言', '模型', '的', '词', '元化', '处理', '是', '一个', '关键', '技术点', '。']
注意
▁
符号(一个特殊空格符),这是SentencePiece用来表示词元开头(空格替换)的标记。可以看到,BPE和Unigram对“词元化”和“技术点”的切分可能不同,这体现了算法差异。
4.4 与Hugging Face Transformers集成
在实际的大模型项目中,我们通常使用Hugging Face的
Transformers
库。它内置支持多种分词器。如果你用SentencePiece训练了自己的词表,可以轻松创建对应的
Tokenizer
。
假设你训练了一个BPE模型,并想创建一个类似GPT-2的分词器:
from transformers import GPT2TokenizerFast
# 使用自定义词表文件创建分词器
# 你需要将SentencePiece的.vocab文件转换成GPT-2格式的vocab.json和merges.txt
# 这里假设你已经转换好了,或者使用相关工具转换。
tokenizer = GPT2TokenizerFast.from_pretrained("./path_to_your_custom_tokenizer")
# 或者,更常见的是,直接使用预训练模型的分词器,其词表已经用特定算法训练好。
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("gpt2") # 使用GPT-2的BPE分词器
# tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased") # 使用BERT的WordPiece分词器
# tokenizer = AutoTokenizer.from_pretrained("t5-base") # 使用T5的SentencePiece Unigram分词器
text = "Let's tokenize this text."
encoded = tokenizer(text)
print(encoded) # {'input_ids': [...], 'attention_mask': [...]}
print(tokenizer.convert_ids_to_tokens(encoded['input_ids']))
5. 高级话题与常见问题排查
5.1 中文词元化的特殊处理
对于中文、日文等没有显式空格分隔的语言,词元化面临更大挑战。直接应用上述算法会将每个字符当作独立单元,这可能不是最优的,因为中文词语通常由多个字符组成。
常见策略:
- 直接字符级 :简单将每个汉字作为一个词元。优点是简单,词表小(几千),但模型需要自己学习词语组合,对序列长度和模型能力要求高。
- 先分词,再应用子词算法 :使用中文分词工具(如jieba、pkuseg、HanLP)先将句子分成词语,然后在词语序列上应用BPE/WordPiece。这样,子词算法在词语内部进行,能更好地捕获词语结构。 这是很多中文预训练模型(如ERNIE, Chinese-BERT)的做法。
- 纯子词算法 :直接对原始中文文本(不加空格)运行SentencePiece。SentencePiece会将整个文本视为字符流,通过算法自动学习出常见的字符组合(可能是词或词的一部分)。 这是当前很多多语言大模型(如mT5, XLM-R)处理中文的方式。
如何选择?
- 如果你的任务严重依赖词汇信息(如实体识别), 策略2(先分词) 可能更有优势。
- 如果你追求端到端的简洁性和多语言统一处理, 策略3(纯子词) 是主流趋势。大模型强大的上下文学习能力可以部分弥补未显式分词的不足。
- 策略1(字符级) 现在较少使用,除非是针对古汉语等特殊场景。
5.2 词表大小的影响与调优
词表大小是一个超参数,需要仔细调整。
- 词表太小(如8k) :压缩率高,每个样本的词元序列长,模型需要处理更长的依赖关系,训练和推理速度慢。同时,很多概念被迫拆分成过于细碎的片段,可能损害模型表现。
- 词表太大(如200k) :词元序列短,但模型嵌入层参数巨大,增加内存消耗和过拟合风险。也可能导致常见词被过度拆分,引入噪声。
- 经验范围 :对于单语言模型, 32k 是一个广泛使用的甜点值。对于多语言模型,由于要覆盖多种语言的词汇,词表通常更大,如 250k (mT5)甚至更高。
调优建议 :
- 绘制学习曲线 :在开发集上,尝试不同的词表大小(如16k, 32k, 64k, 128k),观察下游任务(如分类准确率)的变化。通常存在一个“平台区”,选择平台区内较小的值以节省资源。
- 观察OOV率 :在验证集上计算未登录词(Out-Of-Vocabulary)的比例。一个好的词表应该将OOV率控制在极低水平(如<0.1%)。
- 检查词元分布 :查看词表中高频词元和低频词元都是什么。一个健康的词表应该有合理的齐夫(Zipf)分布,头部是一些常见功能词和子词,尾部是一些有意义的稀有词或专业术语。
5.3 常见问题与排查技巧
问题1:训练时损失震荡或不收敛,下游任务效果差。
- 排查 :首先检查训练语料的质量。语料是否干净?是否有大量重复、乱码、无关文本? 脏数据是词表训练的头号杀手 。
-
解决
:彻底清洗语料,去重,过滤低质量文本。可以尝试减少
--input_sentence_size,先用一个干净的子集训练一个小词表看效果。
问题2:分词结果出现大量无意义的字符片段或数字碎片。
- 排查 :这通常是算法将高频但不语义紧密的字符组合合并了(BPE更常见)。例如,数字和标点可能粘在一起。
-
解决
:
-
考虑在预处理阶段,将数字用特殊标记(如
<NUM>)替换,或者将标点与文字用空格分开。 - 尝试使用WordPiece或Unigram算法,它们对这类问题的鲁棒性稍好。
-
在SentencePiece中,可以调整
--split_digits=True参数将数字单独拆分。
-
考虑在预处理阶段,将数字用特殊标记(如
问题3:对于领域特定术语(如“Transformer”、“BERT”),分词器将其拆散了。
- 排查 :通用词表缺乏领域知识。
-
解决
:
-
领域自适应
:在你的领域语料上,从头开始训练一个词表,或者用通用词表作为初始词表,在你的语料上继续训练(增量训练)。SentencePiece支持
--input_sentence_size和增量训练。 - 添加自定义词元 :在词表文件中手动添加这些术语,并赋予它们一个很高的概率(如果使用Unigram),或者在分词后做后处理合并。
-
领域自适应
:在你的领域语料上,从头开始训练一个词表,或者用通用词表作为初始词表,在你的语料上继续训练(增量训练)。SentencePiece支持
问题4:序列长度超限,尾部信息被截断。
- 排查 :输入文本经过词元化后,产生的词元数量超过了模型的最大位置编码长度(如512、1024、2048)。
-
解决
:
-
预处理截断
:使用分词器的
truncation=True参数。 - 滑动窗口 :对于长文档,可以将其分割成重叠的块,分别处理后再聚合结果。
- 模型层面 :考虑使用支持更长序列的模型架构,如Longformer、BigBird,或使用ALiBi等外推位置编码。
-
预处理截断
:使用分词器的
问题5:部署时,分词速度成为瓶颈。
- 排查 :分词是在线预测服务中CPU上的操作,如果文本很长或QPS很高,可能成为瓶颈。
-
解决
:
- 批量分词 :尽可能将多个文本组成batch一起送入分词器,利用向量化优化。
- 缓存 :对频繁出现的相同或相似查询文本的分词结果进行缓存。
- 使用C++库 :像SentencePiece本身是用C++编写的,Python只是其封装。确保你安装的是编译优化版本。对于极致性能场景,可以考虑直接调用C++接口。
词元化虽然是大模型流水线中相对“低调”的一环,但它奠定了模型理解文本的基础。选择和理解不同的词元化算法,不仅能帮助你在使用预训练模型时更得心应手,也是当你需要为自己的特定领域或任务定制模型时,必须掌握的核心技能。从BPE的频率统计,到WordPiece的似然驱动,再到Unigram的概率化剪枝,每一种方法都体现了对“如何用有限符号高效表示无限文本”这一问题的深刻思考。在实际操作中,多实验、多分析分词结果,结合任务目标选择最合适的“刀法”,你的模型效果很可能因此获得意想不到的提升。
更多推荐


所有评论(0)