2、常见Tokenization算法:BPE、WordPiece、SentencePiece

好,咱们直接进入正题。Tokenization(分词)是文本预处理最基础的一步,但选错算法后面全白搭。我自己踩过不少坑,比如拿BPE跑中文数据,出来的词片断全是单字,模型压根学不到语义。这一章就把三种主流算法掰开揉碎讲清楚:BPE、WordPiece、SentencePiece。你想想看,它们到底差在哪?各自用在什么场景?我先说结论:没有万能算法,只有合适的选择。

2.1 BPE — 字节对编码

BPE(Byte Pair Encoding)最早是数据压缩用的,2016年被Neural Machine Translation领域拿来搞分词。原理特别简单:反复合并频率最高的相邻字符对。举个例子:

初始词表: a, b, c, d, e, f, g, h, i, j
统计频次: ("a","b")=10, ("b","c")=8, ("c","d")=7 ...
第一次合并: "ab" 成为新符号,词表扩增
重复直到词表容量达到预设值

我最早在机器翻译项目里用BPE处理英文。跑出来效果不错,但遇到德语这种超长复合词,BPE会切得很碎。后来我发现,BPE擅长处理形态丰富的语言——因为它不关心语义,只看统计频次。说白了,BPE就是“无脑高频优先”。

核心特点: 基于字符对频次,贪心合并;不依赖语言先验知识;词表大小可人工设定(通常是32K-100K)。
我的经验: 英文场景BPE足够用;中文请慎重,除非你搭配了很好的预分词(比如Jieba),否则容易变成单字分词。

2.2 WordPiece — 词片分割

WordPiece是Google搞的,用在BERT等模型里。思路和BPE类似,但合并依据不一样:BPE看的是原始频次,WordPiece看的是互信息——也就是两个符号同时出现的概率比单独出现的概率乘积大多少。公式长这样:

score(a, b) = P(a,b) / (P(a) * P(b))

分数越高说明a和b关系越紧密。WordPiece每次只合并得分最高的符号对。嗯,这里要注意:WordPiece有个特殊标记##,用来表示这不是单词开头。比如"tokenization"会被切成["", "##ization"]。我在做BERT微调时见过不少同学忘记处理##,导致序列对齐出错。

特性BPEWordPiece
合并依据原始字符对频次互信息(似然比)
特殊标记无## 表示连续片断
典型应用, RoBERTa, T5BERT, DistilBERT
中文适用性差(需预分词)中等(配合词表)
避坑: 我曾有一次用WordPiece跑中文医疗数据,未加预分词。结果“心电图”被分成“心”、“电图”两个。模型后来把“心率”和“电图”混为一谈,准确率崩了。所以中文场景必须配预分词或改用SentencePiece。

2.3 SentencePiece — 统一处理所有语言

SentencePiece是Google开源的,2018年发布。它最大的特点:把空格也当成普通字符处理,不再依赖预分词。你喂它原始字符串,它自己决定单词边界。这一点对中文、日文这类无空格分隔的语言来说,简直是救星。

SentencePiece有两种模式:

  • Unigram语言模型: 用EM算法(期望最大化)逐步剪枝,保留概率最高的子词。
  • BPE模式: 直接套用BPE逻辑,但空格被当作普通符。

我个人习惯优先用Unigram模式,因为它能生成多个候选序列,再选最优路径,比BPE的贪心合并更灵活。我记得在给东南亚小语种做机器翻译时,当地语言没有标准分词工具,SentencePiece直接上,效果出奇好。

# 训练SentencePiece模型(Python示例)
import sentencepiece as spm

spm.SentencePieceTrainer.train(
    input='my_corpus.txt',
    model_prefix='my_tokenizer',
    vocab_size=32000,
    character_coverage=1.0,   # 覆盖所有字符
    model_type='unigram',     # 可选 bpe 或 unigram
    unk_id=0, bos_id=1, eos_id=2, pad_id=3
)
提示: character_coverage参数很关键。中文、日文等大字符集语言建议设为1.0,避免UNK(未登录词)比例过高。我以前设置过0.995,结果有0.5%的生僻字直接丢失,模型预测时反复出UNK,简直灾难。

2.4 三种算法对比与选型建议

我把三种算法放在一张表里,你可以直观对比:

维度BPEWordPieceSentencePiece
是否依赖预分词是是否(空格视为普通符)
合并策略贪心高频互信息最大Unigram剪枝 / BPE
输出可逆性是(可还原)是是(有损——空格丢失)
典型模型, T5BERT, ALBERTXLNet, T5(部分)
中文友好度★☆☆★★☆★★★
训练速度快中较慢(Unigram需多轮)

你看完表格可能会问:那我到底选哪个?我的原则很简单:

  • 纯英文或拉丁语系: BPE够用,速度快,词表稳定。
  • 多语言、中文、日语: 无脑上SentencePiece(Unigram模式)。
  • 使用预训练BERT系列: 保持原始WordPiece词表,不要自己重新训练——除非你有大量领域数据。

2.5 核心知识图谱

下面这张SVG图帮你理清三种算法的演进关系和关键区别。你可以把它当作快速记忆卡。

Tokenization算法核心图谱 Tokenization算法图谱 BPE 字节对编码 · 贪心合并 WordPiece 互信息合并 · ##标记 SentencePiece 空格视为字符 · Unigram 依赖预分词 BPE / WordPiece 必需 不需预分词 SentencePiece 原生支持 可逆性 BPE/WordPiece 完全可逆 选型总结 英文 → BPE | 多语言/BERT → WordPiece | 中文/无预分词 → SentencePiece 注意词表大小、UNK率、可逆性需求 —— 没有银弹,结合数据特点选择

2.6 落地实战中的注意事项

讲完理论,我们聊聊落地时的几个坑。我这些年踩过的雷不算少,分享出来你们少走弯路。

  1. 词表大小如何选? 太小导致UNK太多,太大导致模型参数量爆炸。我一般32K起步,看语料规模上浮。如果是通用语料,50K已经很稳定。
  2. 一定要做Byte Fallback: 细看SentencePiece或BPE的配置,开启byte_fallback选项。这样遇到生僻字符会转成UTF-8字节序列,而不是UNK。我在处理多语言语料时测试过,UNK率从5%降到0.1%以下。
  3. 一致性校验: 训练和推理必须用完全相同的tokenizer文件。我曾经手滑用了两个版本,结果线上模型编码混乱,排查了两天才发现问题。
  4. 中文的先决条件: 如果选BPE或WordPiece,务必先用预分词(如Jieba、LTP)切好。否则你会得到一堆单字。SentencePiece则不需要这一步,但要注意把“▁”(空格符)正确映射。
曾经我犯过的严重错误: 在生产环境部署BERT模型时,我直接用了HuggingFace的默认WordPiece词表。结果发现很多金融专业术语(如“可转债”)被切成“可”、“转”、“债”三个字。后来重新用领域语料微调了词表,F1提升了7个点。所以,领域微调词表真的不是可有可无。