WordPiece 算法:BERT 的分词之道

一句话理解

WordPiece 是一种"把文字切成子词"的算法——切得比字大、比词小,让模型既能处理已知词,也能猜未知词。

但要注意:WordPiece 对英文及拉丁语系(法语、西班牙语、德语等)发挥核心作用,但对纯中文来说,它退化为逐字切分——子词优势主要体现在有空格分隔词边界的语言上。


为什么需要子词分词?

传统方法的问题

方法 1:按"词"切分(整词分词)
  词表:{我, 是, 一, 个, 好, 学生, ...}

  问题:
  ├── 中文词边界不清("南京市长江大桥"怎么切?)
  ├── 词表爆炸(中文几十万个词,全收录不现实)
  └── 未登录词(OOV):词表里没有的词 → [UNK] → 丢失信息

方法 2:按"字"切分(逐字分词)
  每个字都是一个 token

  优点:简单,没有未登录词
  缺点:
  ├── 丢失了"词"的信息("海洋"和"海"+"洋"语义不同)
  └── 序列太长,计算量大

WordPiece 的方案:子词分词

WordPiece 的切法:介于"字"和"词"之间

常见词 → 整词保留:  "海洋" → ["海洋"]
次常见 → 切成子词:  "海洋监测" → ["海洋", "监测"]
罕见词 → 逐字切分:  "龘" → ["龘"](词表里没有就拆成字)

好处:
├── 常见词保持完整(语义不丢失)
├── 罕见词可以拆分(不会 [UNK])
├── 词表大小可控(2-3 万个词条就够)
└── 兼顾了"字"的灵活和"词"的语义

WordPiece 的核心思想

和 BPE 的对比

WordPiece 和 BPE(Byte Pair Encoding)是最主流的两种子词算法,结构相似,但合并策略不同:

BPE(GPT 系列用的):
  合并标准 = 出现频率最高
  "play" + "##ing" 一起出现了 10000 次 → 合并!

  类比:谁加班时间最长,谁升职

WordPiece(BERT 用的):
  合并标准 = 统计关联度最高(PMI)
  "play" + "##ing" 一起出现的次数 >> 各自独立出现的预期 → 合并!

  类比:谁贡献最大,谁升职(不只看工时,还看产出)

PMI(点互信息)

PMI 衡量:两个 token "粘在一起"的程度,是否超出随机预期

score(A, B) = P(AB) / (P(A) × P(B))

P(AB)        = AB 实际一起出现的概率
P(A) × P(B)  = 如果 A 和 B 完全独立,它们碰巧在一起的预期概率

score > 1  → 超出预期,值得合并(有统计关联)
score = 1  → 刚好等于预期,合不合都行
score < 1  → 低于预期,不该合并

举例:

"的" 和 "了" 都是高频字,经常一起出现
  → 但它们各自太常见了,一起出现是"碰巧"
  → PMI 不高 → WordPiece 不会优先合并

"un" 和 "##able" 不常出现,但几乎总是一起出现
  → 关联度极高 → WordPiece 优先合并

## 前缀:词边界标记

WordPiece 用 ## 区分"词的开头"和"词的延续":

没有 ##  → 可以独立出现在词的开头
有 ##    → 只能接在前一个 token 后面

例子:
  "play"    → 独立词,可以开头
  "##ing"   → 后缀,只能接在后面:play + ##ing = playing
  "un"      → 前缀,可以开头
  "##der"   → 延续:un + ##der = understand 的一部分

为什么需要 ##?

没有 ## 的话:
  "ing" 出现在词表 → 模型不知道它是独立词还是后缀
  → 无法判断词边界

有了 ## 之后:
  "ing"   → 独立词
  "##ing" → 一定是后缀
  → 模型清楚知道词的边界在哪里

还原原文:

分词结果:["play", "##ing", "a", "game"]

还原规则:
  ## 开头 → 直接拼到前一个 token 后面
  没有 ## → 前面加空格

还原过程:play → playing → playing a → playing a game

训练算法(构建词表的过程)

三步走

第 1 步:初始化
  把所有词拆成单个字符
  "unbelievable" → ["u", "##n", "##b", "##e", "##l", "##i", "##e", "##v", "##a", "##b", "##l", "##e"]
  词表 = 所有出现过的字符

第 2 步:迭代合并
  计算所有相邻 token 对的 PMI 分数
  选分数最高的那对 → 合并成一个新 token
  更新语料中所有出现的地方
  重复,直到词表达到目标大小(如 21,128)

第 3 步:输出词表
  常见词 → 已经完整合并("海洋")
  常见子词 → 部分合并("##ing")
  罕见字 → 还是单个字符(兜底)

具体例子

假设语料中有这些词:

  Word     Count
  low      5
  lower    2
  newest   6
  wider    3

初始化后:
  low    → [l, ##o, ##w]
  lower  → [l, ##o, ##w, ##e, ##r]
  newest → [n, ##e, ##w, ##e, ##s, ##t]
  wider  → [w, ##i, ##d, ##e, ##r]

计算 PMI 分数:
  (##e, ##r) 出现 5 次(lower×2 + wider×3)
    score = 4.46  ← 高!因为 ##r 不常见,但总跟在 ##e 后面

  (##e, ##w) 出现 6 次(newest×6)
    score = 2.08  ← 低一些,因为 ##w 太常见了

WordPiece 选择合并 (##e, ##r) → ##er
  因为 PMI 更高(关联度更强)

BPE 会选择合并 (##e, ##w)
  因为出现次数更多(6 > 5)

推理时的分词过程

用户输入:"海洋监测技术"

第 1 步:贪心匹配
  从左到右,每次找词表里能匹配的最长子词

  "海" → 词表里有 → 匹配
  "洋" → 词表里有 → 匹配
  "监" → 词表里有 → 匹配
  "测" → 词表里有 → 匹配
  "技" → 词表里有 → 匹配
  "术" → 词表里有 → 匹配

第 2 步:处理未知
  如果遇到词表里没有的字 → 逐字切分(兜底)
  永远不会出现 [UNK](除非是完全没见过的字符)

第 3 步:输出
  ["海", "洋", "监", "测", "技", "术"]
  → 对应 ID → 送入模型

中文 vs 英文的 WordPiece

英文

英文有空格分隔单词 → 天然知道词边界

"unbelievable" → ["un", "##believ", "##able"]
                  ↑前缀   ↑词根延续     ↑后缀

WordPiece 在英文上效果很好:
  常见词完整保留("the", "is")
  复杂词合理拆分("un + ##believ + ##able")

中文

中文没有空格 → 需要特殊处理

bert-base-chinese 的策略:
  中文字逐字切分(每个字前后加空格)
  → 然后对每个字查词表

  "海洋监测" → ["海", "洋", "监", "测"]

  中文的 WordPiece 实际上退化为"逐字切分"
  因为中文字本身就是最小的语义单元
  ## 前缀在中文里用得少(多数中文字独立出现)

重要事实:WordPiece 对纯中文几乎"没用"

对纯中文句子,WordPiece 的子词优势完全消失:

  输入:"海洋监测技术"
  WordPiece 切法:["海", "洋", "监", "测", "技", "术"]  ← 6 个单字
  逐字切分结果:  ["海", "洋", "监", "测", "技", "术"]  ← 完全一样!

原因:
  ├── normalizer 的 handle_chinese_chars 在中文字前后加空格
  ├── 预分词阶段就把中文字切开了
  ├── WordPiece 没有机会合并两个中文字
  └── 词表中多字中文词条数 = 0(实证数据)

结论:
  对纯中文 → WordPiece ≈ 逐字切分(没有子词优势)
  对纯英文 → WordPiece 充分发挥(un + ##believ + ##able)
  对中英混合 → WordPiece 在英文部分发挥作用,中文部分仍逐字

WordPiece 在 BERT 中的真实角色

场景                         WordPiece 的作用
─────────────────────────────────────────────────────────
纯英文句子                   核心作用(子词切分,保留词根语义)
法语/西班牙语/德语等拉丁语系   核心作用(与英文类似,有空格分隔词边界)
纯中文句子                   几乎没有(退化为逐字切分)
中英混合句子                 部分作用(英文部分子词切分,中文部分逐字)
中日混合句子                 部分作用(日文部分可能子词切分)

为什么拉丁语系效果好?
  ├── 有空格分隔单词 → 预分词能识别词边界
  ├── 词根+词缀结构明显 → WordPiece 擅长切分(un+##believ+##able)
  └── 共享拉丁字母 → 词表利用率高

实际业务中(如 HXXedge 系统):
  用户输入几乎全是中文 → WordPiece 基本不发挥子词优势
  分词本质上就是"逐字切分 + 查词表转 ID"

三大子词算法对比

对比 WordPiece BPE SentencePiece
代表模型 BERT GPT、LLaMA T5、多语言模型
合并标准 PMI(统计关联度) 频率(出现次数) 似然(语言模型概率)
词边界 ## 前缀 空格/特殊标记 内置处理
输入单位 字符 字节/字符 Unicode 字符
中文处理 逐字切分 逐字/子词 直接处理(无需预分词)
优点 语义关联好 简单快速 通用性强

在我们的项目中

bert-base-chinese 使用 WordPiece 分词
  词表大小:21,128 个词条
  中文:逐字切分
  英文:子词切分(play + ##ing)

分词文件:
  tokenizer.json      ← 完整词表 + 分词规则(21,000+ 行)
  tokenizer_config.json ← 分词配置参数(23 行)

推理引擎:
  tokenizers 库(Rust 实现)加载 tokenizer.json
  → 把文字转成 input_ids
  → 送入 ONNX 模型推理

一句话总结

WordPiece = 一种"聪明地切词"的算法

核心思想:
  不是按频率切,而是按"统计关联度"切
  关联度高的子词合并(如 un + ##able)
  关联度低的保持分开

好处:
  ├── 词表小(2-3 万词条)
  ├── 没有未登录词(最差逐字切分)
  └── 保留语义(常见词完整,罕见词拆分)

重要事实:
  ├── 对英文/拉丁语系 → WordPiece 充分发挥子词优势(核心作用)
  ├── 对纯中文 → WordPiece 退化为逐字切分(子词优势消失)
  └── 对中英混合 → 仅在英文部分发挥作用

BERT 选择 WordPiece → 21,128 词条 → 中文逐字切分 → 高效理解中文

📝 创建:2026-09-11
🔄 标签:WordPiece、分词算法、子词、BERT、Tokenization、PMI

相关笔记