自然语言处理使用的WordPiece分词算法详解

WordPiece 是一种广泛应用于自然语言处理(NLP)的子词分词算法,由Google 于 2016 在 BERT 模型中首次引入,旨在解决长尾词汇(如罕见词、复合词)的语义表示问题,同时平衡词表大小与语义覆盖率。现已成为 BERT、MPNet 等主流 Transformer 模型的核心分词技术。

WordPiece 广泛应用于现代基于 Transformer 的预训练语言模型,如 BERT、DistilBERT、Electra、XLNet 等。它的核心思想是将单词拆分成更小的、有意义的子单元(subword units)。

作为 Sentence Transformers 等现代模型的基础,WordPiece 在处理复杂语言结构平衡计算效率方面展现出不可替代的价值,是当前 NLP 工程实践的基石技术。

术语解释

似然:统计学的专业述语,英文是Likelihood,指可能性顺眼程度合理性。在已知数据的情况下,衡量某个原因或参数的合理性(有多靠谱)。是统计学中用于从数据反推模型参数的核心概念

核心思想

WordPiece 基于一种贪心的高频符号合并策略,其本质是一种数据压缩词表学习算法。
它的底层逻辑是:两个子词如果经常一起出现,那么将它们合并成一个新的子词,可以帮助减少文本序列的长度,同时更好地捕捉语言的规律。

WordPiece 的核心目标:平衡词汇表大小与未登记词处理能力

  1. 解决词汇表外问题: 传统基于单词的分词方法会遇到训练集中未出现过的单词(OOV),导致模型无法处理。WordPiece 通过将未知单词拆分成已知的子词来解决这个问题。
  2. 平衡词表大小与粒度: 纯字符分词词表极小但丢失了单词内部的结构信息;纯单词分词词表巨大且稀疏。WordPiece 找到了一种折中,词表大小可控(通常在 10k 到 100k 之间),同时能有效表示常见单词和罕见/未知单词。
  3. 捕捉单词内部结构: 许多单词由词根、前缀、后缀组成(如 “unhappiness” -> “un”, “happi”, “ness”)。WordPiece 能学习到这些有语言学意义的片段。

与传统方法的对比

分词方法 示例输入 输出 问题
单词级 "unhappiness" ["unhappiness"] 词汇表爆炸(百万级)
字符级 "unhappiness" ["u","n","h","a",...] 语义丢失严重
WordPiece "unhappiness" ["un", "##happiness"] 理想平衡点

算法工作原理

WordPiece 基于一个简单的策略:贪婪地合并最频繁共现的字符对。它从一个基础字符集开始,逐步构建词表。

算法步骤

从训练阶段到构建词表:初始化 > 计算频率 > 合并高频 > 子词单元 > 最终词汇表

flowchart TB
    subgraph 训练流程
        A[原始语料] --> B[初始化词汇表
(所有字符+常见词)] B --> C[统计所有相邻子词对频率] C --> D{合并最高频子词对?} D -->|是| E[创建新子词单元] E --> C D -->|达到目标大小| F[最终词汇表] end

第一步:初始化词表

算法的起点是一个极其微小的词表,通常包含:

  • 将所有文本拆分成最小单元(通常是字符,包括字母、中文汉字、数字、标点符号等)。
  • 定义目标词表大小 V(一个预设的超参数)。
  • 初始词表就是所有基础字符 + 一些特殊的标记(如 [CLS], [SEP], [UNK], [PAD], [MASK])。
  • 必要的标点符号。

此时,任何一个单词都可以被拆分成单个字符来表示(例如, “playing” 被表示为 p l a y i n g)。

第二步:预分词

在进行子词合并之前,通常会用简单的空格将文本分割成单词(对于英文等语言)。这样做的目的是将合并的范围限制在单词内部,防止算法跨单词边界学习到无意义的连接(例如将两个单词连在一起)。最终的分词结果会由这些单词内部的子词拼接而成。

第三步:选择与合并

这是一个循环过程,直到词表大小达到预设的目标值为止:

  1. 统计相邻对:遍历整个语料库,统计当前词表状态下,所有相邻的两个子词共同出现的次数。
  2. 计算合并得分:这是 WordPiece 与 BPE 最大的不同点。
    • BPE 单纯统计相邻对出现的频率,频率最高的直接合并。
    • WordPiece 不只看频率,它计算的是这两个子词组合在一起后,对整体语言模型似然度的提升程度。通常,它会计算一个 互信息似然增益 的分数。公式大致思想是:比较两个子词作为整体出现的概率,除以它们各自独立出现的概率。
      • 如果 p(ab) 远大于 p(a) * p(b)(即 ab 在一起比它们随机相遇的概率大得多),说明这两个字符具有强烈的绑定关系,应该合并。
      • 这个机制能避免合并那些虽然常见但独立性很强的高频符号(比如 es 在英语中可能只是因为单词复数而经常相遇,但 WordPiece 通过概率增益评估,可能会更倾向于合并 i n g 这种语义块)。
  3. 合并:选择得分最高的相邻对,将其合并成一个新的子词单元,并加入词表。
  4. 重新切分:语料库中的所有单词都会根据更新后的词表重新进行最长匹配切分,然后回到第一步,进行下一轮统计。

第四步:停止条件

当词表大小达到预设的阈值(例如 BERT 使用 3 万左右),或者合并带来的增益低于某个阈值时,训练停止。

最终输出的是一个包含基础字符、常用单词、常用词缀(前缀、后缀、词根)和常用字符组合的子词集合。

应用阶段

一旦训练好 WordPiece 词表,就可以用它来对新的文本进行分词。在对新文本进行分词时,它采用最长匹配优先(Maximum Matching)或前向最大匹配的策略:

  1. 从左到右扫描一个单词。

  2. 从该单词的开头开始,在词表中查找尽可能长的子词。

  3. 如果找到了,就在此处切分,然后从切分点的下一个字符开始,继续重复这个过程。

  4. 如果遇到词表中没有的单个字符,则使用特殊的 [UNK] 标记代替。

  5. 初始化: 将待分词的句子拆分成单词(空格通常是基本分隔符)。

  6. 处理每个单词:

    • 从左到右扫描一个单词。
    • 从单词的开头开始,尝试匹配词表中最长的可能子词
    • 如果找到了匹配的子词,将其作为一个 token 切分出来。
    • 对于单词剩余的部分,继续重复第二步。
    • 如果剩余部分不能匹配词表中的任何子词,但不是空字符串
      • 如果该部分包含未知字符(不在基础字符集中),通常用 [UNK] 表示。
      • 否则,将其拆分成尽可能长的已知子词序列(通常是字符级),并给除第一个子词外的其他子词添加特殊前缀(如 ##)以表明它是单词的一部分而不是开头。
  7. 添加特殊标记: 根据模型要求,在句子开头添加 [CLS],在句子结尾添加 [SEP],在需要的地方添加 [PAD]

多语言处理示例

语言 输入文本 WordPiece 输出 优势体现
英语 "Transformer" ["Transform", "##er"] 保留词根语义
中文 "自然语言处理" ["自","然","语","言","处","理"] 无需空格,按字拆分
德语 "Hauptbahnhof" ["Haupt", "##bahn", "##hof"] 分解复合词(主火车站)
日语 "ディープラーニング" ["ディープ","##ラーニング"] 处理片假名复合词

分词过程举例

假设词表中已有 play, ing,以及单个字母:

  • 输入词:playing
  • 分词过程:
    • playing 整个词是否在词表?不在。
    • playin 是否在词表?不在。
    • play 是否在词表?。 -> 切出 play
    • 剩下 ing 是否在词表?。 -> 切出 ing
  • 结果:['play', 'ing']

Python实现示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
"""
此模块实现了 WordPiece 分词算法,用于将输入文本进行分词。
"""

def wordpiece_tokenize(text: str, vocab: set) -> list:
"""
使用 WordPiece 算法将输入文本进行分词。
WordPiece 是一种基于子词的分词算法,采用最长匹配优先策略,
将文本拆分为词汇表中存在的最长子词。对于词汇表中不存在的子词,将其标记为 [UNK]。

Args:
text (str): 需要进行分词的输入文本。
vocab (set): 包含所有有效子词的词汇表。

Returns:
list: 分词后的子词列表。
"""
tokens = []
start = 0
while start < len(text):
end = len(text)
found = False
while start < end:
substr = text[start:end]
if start > 0:
substr = "##" + substr
if substr in vocab:
tokens.append(substr)
start = end
found = True
break
end -= 1
if not found:
tokens.append("[UNK]")
start += 1
return tokens

if __name__ == "__main__":
text = "jumping"
vocab = {
"un",
"##able",
"##ing",
"happ",
"happy",
"ness",
"jump",
"!"
}
result = wordpiece_tokenize(text, vocab)
print(result)

Java实现示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package com.example.demo.split;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
* @author gxing
* @desc 使用 WordPiece 算法将输入文本进行分词
* @date 2025/6/15
*/
public class WordSplit {

/**
* 示例词汇表,包含所有有效子词
*/
static Set<String> vocab = new HashSet<>();
static {
vocab.add("un");
vocab.add("##able");
vocab.add("##ing");
vocab.add("happ");
vocab.add("happy");
vocab.add("##ness");
vocab.add("jump");
vocab.add("!");
}

/**
* 使用 WordPiece 算法将输入文本进行分词。
* WordPiece 是一种基于子词的分词算法,采用最长匹配优先策略,
* 将文本拆分为词汇表中存在的最长子词。对于词汇表中不存在的子词,将其标记为 [UNK]。
*
* @param text 需要进行分词的输入文本
* @param vocab 包含所有有效子词的词汇表
* @return 分词后的子词列表
*/
public static List<String> wordpieceTokenize(String text, Set<String> vocab) {
// 初始化一个空列表,用于存储分词后的子词
List<String> tokens = new ArrayList<>();
// 初始化起始索引,从文本的第一个字符开始
int start = 0;
// 当起始索引小于文本长度时,继续进行分词操作
while (start < text.length()) {
// 初始化结束索引,从文本的最后一个字符开始
int end = text.length();
// 标记是否找到匹配的子词
boolean found = false;
// 最长匹配优先策略,从最长的可能子词开始尝试匹配
while (start < end) {
// 截取从起始索引到结束索引的子字符串
String substr = text.substring(start, end);

// 检查截取的子词是否在词汇表中
if (vocab.contains(substr)) {
// 如果在词汇表中,将该子词添加到分词结果列表中
tokens.add(substr);
// 更新起始索引,从当前匹配子词的下一个字符开始继续匹配
start = end;
// 标记已找到匹配的子词
found = true;
// 跳出内层循环,开始下一轮匹配
break;
}

if (start > 0) {
// 处理非起始子词,添加 '##' 前缀
substr = "##" + substr;
if (vocab.contains(substr)) {
// 如果在词汇表中,将该子词添加到分词结果列表中
tokens.add(substr);
// 更新起始索引,从当前匹配子词的下一个字符开始继续匹配
start = end;
// 标记已找到匹配的子词
found = true;
// 跳出内层循环,开始下一轮匹配
break;
}
}
// 若未匹配到,缩短子词长度,继续尝试
end--;
}
// 如果遍历完所有可能的子词都未找到匹配项,处理未知字符
if (!found) {
// 将未知字符标记为 [UNK] 并添加到分词结果列表中
tokens.add("[UNK]");
// 起始索引后移一位,继续处理下一个字符
start++;
}
}
// 返回分词后的子词列表
return tokens;
}
// 示例词汇表,使用集合存储


public static void main(String[] args) {
// 示例文本
String text = "unhappiness";
// 调用 wordpieceTokenize 方法
List<String> result = wordpieceTokenize(text, vocab);
// 输出分词结果
System.out.println(result);
}
}

输出结果:[un, happ, [UNK], ##ness]

WordPiece与BPE的区别

特性 Byte Pair Encoding (BPE) WordPiece
合并依据 最高频次。哪个相邻对出现的次数最多,就合并哪个。 似然增益/互信息。计算合并后能最大化训练数据似然度的相邻对。
数学直觉 统计频率。 概率比。衡量两个子词的绑定强度是否超过偶然。
代表模型 GPT 系列,RoBERTa BERT,DistilBERT

优点与缺点

优点

  • 处理未登录词:任何新词都可以被拆分成已知的子词,分解未见过的单词(如 "tokenization"["token","##ization"]),避免了 [UNK] 泛滥。
  • 平衡词表大小:比纯字符效率高,比纯单词词表小且覆盖广。词汇表高效压缩,规模仅为全词模型的 1/10~1/100,30k 词汇表可覆盖 95%+ 的常见文本(英语),单词级需 100k+ 才能达到同等覆盖率。
  • 学习词根词缀:子词往往能对应有意义的语言单元(如 estingpre)。
  • 训练稳定性:通过语言模型优化,避免 BPE 的纯频率偏差。
  • 跨语言一致性:同一套算法处理所有语言,特别适合多语言模型(如 mBERT、XLM-R)。

缺点

  • 训练复杂度较高:计算概率比相对单纯数数更复杂,合并阶段需遍历所有字符对,时间复杂度为 O(N2)。

  • 依赖语言模型假设:可能过度依赖训练语料的统计特性,忽略语法结构。空格分词在中文、日文等语言中不适用,需要额外的预处理。

  • 前缀标记问题## 符号增加额外负载(约 5-10% token 增长)

  • 子词歧义"pain" 可能被拆为 ["pa","##in"] 失去 “疼痛” 语义,对于这种情况模型(如 BERT)具备一定的纠错能力,但仍需要额外的学习成本来纠错这个语义,这在某些细粒度的情感分析或语义理解任务中,可能会对性能造成一定影响。

    这是WordPiece 的一个核心困境:为了追求覆盖率和词表效率,可能会牺牲语义的完整性。

分词方法对比

方法 合并依据 典型应用 代表模型
WordPiece 最大化语言模型似然 BERT、ALBERT 降低词表至 3 万级
BPE 最高频字符对 GPT、T5 平衡词表与覆盖率
Unigram 最小化损失函数 T5、mBART 动态优化子词选择

分词过程示例

假设词表包含:["un", "##able", "##ing", "happ", "happy", "ness", "!"](以及基础字符和特殊标记)

  • 单词 “unhappiness”:
    • 最长匹配:"un" (匹配) -> tokens: ["un"]
    • 剩余部分:"happiness"
    • 最长匹配:"happ" (匹配) -> tokens: ["un", "happ"]
    • 剩余部分:"iness"
    • 最长匹配:"##iness" 不在词表。尝试更短:"##ine" 不在,"##in" 不在… 最终匹配单个字符失败(假设 ##i 不在词表)。那么退而求其次:
      • 最长匹配剩余部分:"ness" (匹配,但需加 ## 表示非开头) -> tokens: ["un", "happ", "##ness"]
      • 剩余部分 "i" -> 作为单独字符 "##i" (如果 ##i 在基础词表中) 或 [UNK]
    • 更常见的实际情况是词表包含"happiness""##iness" 会被合并成更合理的片段。
    • 理想情况下,可能会被分成 ["un", "happi", "##ness"](如果 "happi" 在词表中)。
  • 单词 “jumping”:
    • 最长匹配:"jump" (假设在词表中) -> tokens: ["jump"]
    • 剩余部分:"ing"
    • 最长匹配:"##ing" (匹配) -> tokens: ["jump", "##ing"]
  • 单词 “apple”:
    • 如果 "apple" 在词表中,直接分成 ["apple"]
    • 否则,可能分成 ["app", "##le"]["a", "##pp", "##le"] 等,取决于词表内容。
  • 单词 “😊” (表情符号):
    • 如果该字符不在基础字符集和词表中,会被分成 [UNK]

实际应用场景

现代NLP模型依赖

模型家族 使用分词器 典型词汇表大小
BERT WordPiece 30,522
MPNet WordPiece 30,000
ELECTRA WordPiece 35,000

部署优化技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")

# 高级功能使用示例
encoding = tokenizer(
text,
truncation=True, # 自动截断超长文本
padding='max_length', # 填充到统一长度
max_length=512, # 设置最大长度
return_tensors='pt', # 返回PyTorch张量
return_offsets_mapping=True # 获取token-原文映射
)

自定义词汇表训练

1
2
3
4
5
# 使用Hugging Face工具训练自定义WordPiece
python tokenizers_train.py \
--files /path/to/corpus.txt \
--vocab_size 32000 \
--output my_custom_tokenizer.json

历史演进与变体

  1. 原始 WordPiece (2016)
    • 基于频率的贪婪合并
  2. BPE (Byte Pair Encoding)
    • WordPiece 前身,未使用 ## 标记
  3. Unigram LM (2018)
    • 基于概率模型的分词,被 T5 采用
  4. SentencePiece (2018)
    • Google 改进版:直接处理原始字节,无需预分词

WordPiece 主导 NLP

WordPiece技术优势分布

自然语言处理使用的WordPiece分词算法详解

http://blog.gxitsky.com/2026/03/04/AI-LangChain-023-TextSpliter-WordPiece/

作者

光星

发布于

2026-03-04

更新于

2026-03-05

许可协议

评论