1. 项目概述:为什么词元化是大模型的第一道“工序”?

如果你最近在玩大语言模型,不管是自己部署一个开源模型,还是调用API,你可能会好奇,你输入的一段话,模型到底是怎么“看懂”的?它看到的不是“你好,世界”这几个汉字,而是一串数字。这个将人类可读的文本,转换成模型可理解的数字序列的过程,其核心的第一步,就是 词元化 。你可以把它想象成给模型准备“食材”的过程:我们吃苹果是整个啃,但模型“消化”文本,需要先把它切成大小合适的“块”,这些“块”就是词元。今天要聊的BPE、WordPiece和Unigram,就是三种最主流的“切菜刀法”。

为什么这个词元化如此关键?因为它直接决定了模型的“词汇量”和“理解粒度”。用字符当词元?那“apple”一个单词就得拆成5个字母,序列太长,效率低下,模型也很难学到有意义的组合。用整个单词当词元?那词汇表会爆炸(想想所有可能的单词和它们的变形),还会遇到没见过的新词。所以,我们需要一种折中的、智能的切分方法,这就是子词词元化技术登场的背景。它让模型既能处理常见词(如“playing”),也能通过组合处理罕见词或新词(如“ChatGPT”)。理解了BPE、WordPiece和Unigram,你才算真正摸到了大模型文本处理的门道。

2. 核心算法原理与设计思路拆解

2.1 目标与挑战:寻找文本的“最优分割”

在深入具体算法前,我们必须明确词元化要解决的核心问题:给定一个庞大的文本语料库,如何学习一个固定大小(比如5万)的词表,使得用这个词表来表示任何新文本时,所需的词元总数尽可能少?这本质上是一个数据压缩和表示学习问题。

这里有几个关键约束和考量:

  1. 词表大小可控 :不能无限大,否则模型嵌入层参数爆炸,计算和存储都无法承受。
  2. 覆盖度与粒度平衡 :词表需要能高效表示常见词(粒度粗一些),同时又能通过组合表示罕见词(粒度细一些),避免出现“未登录词”。
  3. 算法效率 :训练阶段从海量文本中学习词表,以及推理阶段对新文本进行切分,都需要高效的算法。
  4. 语言无关性 :理想的方法应该对英语、中文、代码、公式等各种文本都能较好地处理。

BPE、WordPiece和Unigram给出了三种不同的解题思路,它们共享一个核心理念: 从基础单元(如字符)开始,通过迭代合并,形成更大的、更有意义的子词单元。

2.2 Byte Pair Encoding:从数据压缩到自然语言处理

BPE最早是一种简单的数据压缩算法,核心思想非常直观: 迭代地合并语料中最频繁共现的相邻符号对 。

算法步骤详解:

  1. 初始化 :将训练语料中所有单词按字符拆分,并在每个单词末尾添加一个特殊的结束符(如 </w> ),用以区分单词边界。此时,词表就是所有字符加上结束符。
  2. 统计频次 :统计所有相邻符号对的共现频率。例如,“l o w”中,“l o”是一个对,“o w”是一个对,“w ”是一个对。
  3. 合并最高频对 :找到频率最高的符号对,将它们合并成一个新的符号,并加入词表。例如,如果“e”和“s”经常连在一起出现(如“desk”,拆成“d e s k”),那么就将“es”作为一个新词元。
  4. 迭代 :重复步骤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看 合并带来的似然值提升 。

算法核心:似然最大化

  1. 初始化与BPE相同。
  2. 对于每一种可能的合并(将两个相邻词元A和B合并为AB),我们计算合并后对整个训练语料库的 语言模型似然值 的影响。具体来说,通常使用一个非常简单的基于词元频率的一元语言模型。
  3. 选择能最大程度增加似然值的合并对 。公式化的选择标准是: score = freq(A, B) / (freq(A) * freq(B)) 。这个公式可以理解为,A和B的共现频率,相对于它们各自独立出现的频率的“惊喜”程度。分数越高,说明A和B的绑定关系越强,合并后对模型似然的提升越大。
  4. 迭代执行,直到词表达到目标大小。

WordPiece的优势 : 通过引入概率标准,它倾向于合并那些 结合紧密、语义上更可能是一个整体 的符号对。例如,相比于泛泛的“er”,“##ing”(WordPiece中常用 ## 表示子词前缀)这种具有明确语法功能的后缀,其合并得分可能会更高,因为它与动词词干的结合更具特异性。这使得生成的词元往往在语言学上更有意义。

实操心得 :在实现WordPiece时,那个似然计算公式的简化版本 freq(A,B)/(freq(A)*freq(B)) 计算量很大,因为每次合并后都要重新计算所有相邻对的分数。工程上会有一些优化,比如只更新受影响的局部统计信息。但理解其“概率驱动”的核心思想,比记住公式更重要。

2.4 Unigram Language Model:一种“自上而下”的逆向思维

如果说BPE和WordPiece是“自底向上”的合并策略,那么Unigram Language Model(ULM)则是一种“自顶向下”的拆分策略。它的思路非常不同: 我们先假设一个很大的种子词表(比如所有常见子词和字符),然后评估每个词元的重要性,逐步淘汰最不重要的那个。

算法步骤详解:

  1. 初始化一个大词表 :通常可以用其他算法(如BPE)先产生一个明显大于目标值的词表,或者简单地用所有频繁出现的子串初始化。
  2. 定义概率模型 :为词表中的每一个词元分配一个出现概率,所有词元概率之和为1。对于一个给定的句子,它的出现概率可以用所有可能的分割方式中,各词元概率乘积的最大值(或和)来估算。
  3. 期望最大化训练 :
    • E步(分割) :固定当前词元和它们的概率,使用维特比(Viterbi)算法为语料库中的每个句子找到 最可能 的词元分割序列。
    • M步(更新) :根据E步得到的所有句子的分割结果,重新统计每个词元出现的次数,并更新它们的概率(次数/总词元数)。
  4. 词表剪枝 :在每一轮EM迭代后,计算每个词元的“损失”——如果从词表中移除该词元,对语料库总似然值的影响有多大。然后移除那些损失最小的词元(即最不重要的词元)。
  5. 迭代 :重复步骤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 中文词元化的特殊处理

对于中文、日文等没有显式空格分隔的语言,词元化面临更大挑战。直接应用上述算法会将每个字符当作独立单元,这可能不是最优的,因为中文词语通常由多个字符组成。

常见策略:

  1. 直接字符级 :简单将每个汉字作为一个词元。优点是简单,词表小(几千),但模型需要自己学习词语组合,对序列长度和模型能力要求高。
  2. 先分词,再应用子词算法 :使用中文分词工具(如jieba、pkuseg、HanLP)先将句子分成词语,然后在词语序列上应用BPE/WordPiece。这样,子词算法在词语内部进行,能更好地捕获词语结构。 这是很多中文预训练模型(如ERNIE, Chinese-BERT)的做法。
  3. 纯子词算法 :直接对原始中文文本(不加空格)运行SentencePiece。SentencePiece会将整个文本视为字符流,通过算法自动学习出常见的字符组合(可能是词或词的一部分)。 这是当前很多多语言大模型(如mT5, XLM-R)处理中文的方式。

如何选择?

  • 如果你的任务严重依赖词汇信息(如实体识别), 策略2(先分词) 可能更有优势。
  • 如果你追求端到端的简洁性和多语言统一处理, 策略3(纯子词) 是主流趋势。大模型强大的上下文学习能力可以部分弥补未显式分词的不足。
  • 策略1(字符级) 现在较少使用,除非是针对古汉语等特殊场景。

5.2 词表大小的影响与调优

词表大小是一个超参数,需要仔细调整。

  • 词表太小(如8k) :压缩率高,每个样本的词元序列长,模型需要处理更长的依赖关系,训练和推理速度慢。同时,很多概念被迫拆分成过于细碎的片段,可能损害模型表现。
  • 词表太大(如200k) :词元序列短,但模型嵌入层参数巨大,增加内存消耗和过拟合风险。也可能导致常见词被过度拆分,引入噪声。
  • 经验范围 :对于单语言模型, 32k 是一个广泛使用的甜点值。对于多语言模型,由于要覆盖多种语言的词汇,词表通常更大,如 250k (mT5)甚至更高。

调优建议 :

  1. 绘制学习曲线 :在开发集上,尝试不同的词表大小(如16k, 32k, 64k, 128k),观察下游任务(如分类准确率)的变化。通常存在一个“平台区”,选择平台区内较小的值以节省资源。
  2. 观察OOV率 :在验证集上计算未登录词(Out-Of-Vocabulary)的比例。一个好的词表应该将OOV率控制在极低水平(如<0.1%)。
  3. 检查词元分布 :查看词表中高频词元和低频词元都是什么。一个健康的词表应该有合理的齐夫(Zipf)分布,头部是一些常见功能词和子词,尾部是一些有意义的稀有词或专业术语。

5.3 常见问题与排查技巧

问题1:训练时损失震荡或不收敛,下游任务效果差。

  • 排查 :首先检查训练语料的质量。语料是否干净?是否有大量重复、乱码、无关文本? 脏数据是词表训练的头号杀手 。
  • 解决 :彻底清洗语料,去重,过滤低质量文本。可以尝试减少 --input_sentence_size ,先用一个干净的子集训练一个小词表看效果。

问题2:分词结果出现大量无意义的字符片段或数字碎片。

  • 排查 :这通常是算法将高频但不语义紧密的字符组合合并了(BPE更常见)。例如,数字和标点可能粘在一起。
  • 解决 :
    • 考虑在预处理阶段,将数字用特殊标记(如 <NUM> )替换,或者将标点与文字用空格分开。
    • 尝试使用WordPiece或Unigram算法,它们对这类问题的鲁棒性稍好。
    • 在SentencePiece中,可以调整 --split_digits=True 参数将数字单独拆分。

问题3:对于领域特定术语(如“Transformer”、“BERT”),分词器将其拆散了。

  • 排查 :通用词表缺乏领域知识。
  • 解决 :
    • 领域自适应 :在你的领域语料上,从头开始训练一个词表,或者用通用词表作为初始词表,在你的语料上继续训练(增量训练)。SentencePiece支持 --input_sentence_size 和增量训练。
    • 添加自定义词元 :在词表文件中手动添加这些术语,并赋予它们一个很高的概率(如果使用Unigram),或者在分词后做后处理合并。

问题4:序列长度超限,尾部信息被截断。

  • 排查 :输入文本经过词元化后,产生的词元数量超过了模型的最大位置编码长度(如512、1024、2048)。
  • 解决 :
    • 预处理截断 :使用分词器的 truncation=True 参数。
    • 滑动窗口 :对于长文档,可以将其分割成重叠的块,分别处理后再聚合结果。
    • 模型层面 :考虑使用支持更长序列的模型架构,如Longformer、BigBird,或使用ALiBi等外推位置编码。

问题5:部署时,分词速度成为瓶颈。

  • 排查 :分词是在线预测服务中CPU上的操作,如果文本很长或QPS很高,可能成为瓶颈。
  • 解决 :
    • 批量分词 :尽可能将多个文本组成batch一起送入分词器,利用向量化优化。
    • 缓存 :对频繁出现的相同或相似查询文本的分词结果进行缓存。
    • 使用C++库 :像SentencePiece本身是用C++编写的,Python只是其封装。确保你安装的是编译优化版本。对于极致性能场景,可以考虑直接调用C++接口。

词元化虽然是大模型流水线中相对“低调”的一环,但它奠定了模型理解文本的基础。选择和理解不同的词元化算法,不仅能帮助你在使用预训练模型时更得心应手,也是当你需要为自己的特定领域或任务定制模型时,必须掌握的核心技能。从BPE的频率统计,到WordPiece的似然驱动,再到Unigram的概率化剪枝,每一种方法都体现了对“如何用有限符号高效表示无限文本”这一问题的深刻思考。在实际操作中,多实验、多分析分词结果,结合任务目标选择最合适的“刀法”,你的模型效果很可能因此获得意想不到的提升。

更多推荐