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

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

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

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

核心设计思想

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

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

原始文本

常见词保留完整

罕见词拆分子词

统一词汇表

与传统方法的对比

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

算法工作原理

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

算法步骤

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

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

    • 将所有文本拆分成最小单元(通常是字符,包括字母、数字、标点符号等)

      如:"low"["l","o","w"]

    • 定义目标词表大小 V(一个预设的超参数)。

    • 初始词表就是所有基础字符 + 一些特殊的标记(如 [CLS], [SEP], [UNK], [PAD], [MASK])。

  2. 计算频率:

    • 将训练语料库中的所有单词拆分成字符序列(单词前后通常加上特殊边界符号,如 _## 前缀)。

      例如,单词 “word” 初始表示为 ["w", "o", "r", "d"]

    • 统计所有可能的相邻字符对在训练语料库中出现的频率。

  3. 选择并合并:

    • 在所有相邻字符对中,选择出现频率最高的那一对(例如,("e", "s") 的频率非常高)。
    • 将这对字符合并成一个新的子词单元(例如,"es"),并将这个新单元加入到词表中。
    • 在语料库中,所有出现这个字符对的地方都用这个新的子词单元替换(例如,单词 “words” 原本是 ["w", "o", "r", "d", "s"],合并后变成 ["w", "o", "r", "ds"] 或更可能是 ["word", "s"] 如果 “word” 已经是一个子词了)。
  4. 重复迭代:

    • 基于更新后的分词结果(包含新的子词单元),重新计算所有相邻单元对(现在可能是字符或子词)的频率
    • 再次选择频率最高的对进行合并,并将其加入词表。
    • 重复这个过程,直到达到以下两个条件之一:
      • 达到预设的目标词表大小 V
      • 在合并任何一对都无法显著提高语言模型的似然概率(这是 WordPiece 原始论文中提到的目标函数,但实践中频率作为代理也很常用且有效)。

关键点(训练阶段)

  • 贪婪性: 每次只合并当前最优(频率最高)的一对,不考虑全局最优。虽然可能不是最优解,但计算高效,效果很好。
  • 频率驱动: 合并决策主要基于共现频率。高频组合(如英语的 ing, ed, un, tion)会被优先合并成子词。
  • 生成子词词表: 最终输出的是一个包含基础字符、常用单词、常用词缀(前缀、后缀、词根)和常用字符组合的子词集合。

分词阶段(应用阶段)

一旦训练好 WordPiece 词表,就可以用它来对新的文本进行分词:

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

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]

分词示例

分词示例

假设词表包含:["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]

多语言处理示例

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

优缺点分析

优点

  • 语义泛化:子词组合可覆盖新词(如 “blogging” → “blog” + “##ging”)。
  • 训练稳定性:通过语言模型优化,避免 BPE 的纯频率偏差。

缺点

  • 计算复杂度高:合并阶段需遍历所有字符对,时间复杂度为 O(N2)。
  • 依赖语言模型假设:可能过度依赖训练语料的统计特性,忽略语法结构。

优点

  1. 词汇表效率
    • 词汇表高效压缩,规模仅为全词模型的 1/10~1/100,30k 词汇表可覆盖 95%+ 的常见文本(英语)
    • 对比:单词级需 100k+ 才能达到同等覆盖率
  2. 未登记词处理
    • 能分解未见过的单词(如 "tokenization"["token","##ization"]
    • 对比:单词级遇到新词直接返回 [UNK]
  3. 跨语言一致性
    • 同一套算法处理所有语言
    • 特别适合多语言模型(如 mBERT、XLM-R)
  4. 语义保留能力
    • 子词单元常对应语素(如 "##ly" 表副词,"##ness" 表名词性)

缺点

  1. 前缀标记问题

    • "##" 符号增加额外负载(约 5-10% token 增长)
  2. 非确定性切分

    1
    2
    3
    # 相同词在不同位置拆分不同
    "play" → ["play"] # 作为独立词
    "playing" → ["play", "##ing"] # 作为复合词部分
  3. 子词歧义

    • "pain" 可能被拆为 ["pa","##in"] 失去 “疼痛” 语义

分词方法对比

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

实际应用场景

现代 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

1
2
3
4
5
6
pie
title 技术优势分布
"词汇表效率":35
"未登记词处理":30
"跨语言一致性":20
"语义保留能力":15

以下是其核心原理、训练流程及实际应用的详细解析:


一、核心思想与目标

  1. 子词粒度优化
    WordPiece 将单词拆分为​​语义连贯的子词单元​​(如 “unhappy” → “un” + “##happy”),而非传统的全词或字符级切分。其核心目标是:

    • 减少词表规模:避免覆盖所有罕见词,降低存储与计算成本。
    • 支持 OOV(未登记词)处理:通过子词组合覆盖未知词汇(如 “huggingface” → “hugging” + “##face”)。
    • 保留语义完整性:子词通常携带独立语义(如 “##ing” 表示进行时态)。
  2. 与 BPE 的区别

    • 合并策略

      :BPE 基于字符对的

      频率统计

      选择高频组合,而 WordPiece 基于

      语言模型似然概率

      选择合并对(最大化文本生成概率)

      6

      8

    • 分词标记:BERT 使用 ## 前缀标识非首子词(如 “##ing”),而 BPE 无此标记。


二、训练流程(以 BERT 为例)

1. 初始化阶段

  • 输入语料:大规模文本(如维基百科、书籍)。
  • 预处理:将单词拆分为字符序列,并添加特殊标记(如 <w> 表示词尾)。
  • 初始词表:包含所有单字符及特殊符号(如 h, u, g, <w>)。

2. 构建初始语言模型

  • 统计字符频率:计算每个字符及 <w> 的出现次数。
  • 训练 Unigram 模型:基于字符频率构建简单语言模型,计算每个字符的似然概率。

3. 迭代合并子词对

  • 候选对生成:遍历语料库,统计所有相邻字符对的联合频率(如 h + uhu)。
  • 计算合并分数
    使用互信息公式评估合并对的增益:
    1其中,P(merged) 为合并后的概率,P(unmerged) 为未合并时的概率。
    ​选择分数最高的合并对​​(如 ##u + ##g##ug),将其加入词表。
  • 更新语料库:将合并对替换为新子词,重新统计频率。
  • 重复迭代:直至词表达到预设大小(如 BERT 的 30,522 个词元)。

4. 输出最终词表

  • 包含内容:高频子词(如 ##ing)、低频字符组合(如 ##z)、罕见字符(如 ð)。

三、分词阶段(应用流程)

1. 贪心最长匹配算法

  • 输入单词:如 “lowest”。
  • 从左到右扫描:匹配词表中最长的子词(如 low → 匹配成功)。
  • 剩余部分递归处理:对剩余字符 est 重复匹配(##e##s##t)。
  • 最终输出[low, ##e, ##s, ##t](@ref)

2. BERT 的特殊处理

  • 前缀标记:非首子词添加 ##(如 ##happiness),避免与完整词混淆。
  • OOV 处理:若子词不在词表中,拆分为更小的已知子词(如 mugm + ##u + ##g)。

四、实际应用案例

场景 示例 优势
BERT 预训练 将 “unhappiness” 拆分为 ["un", "##happiness"],提升模型对否定语义的理解。 减少词表至 3 万级,降低显存占用。
机器翻译 德语复合词 “Donaudampfschifffahrtsgesellschaft” → ["Donau", "##dampf", "##schiff"] 处理长词时保持语义连贯性。
中文分词增强 专有名词 “BACE1” → ["BA", "##CE", "##1"],解决传统分词工具的局限性。 提升低频术语的表示能力。

五、优缺点分析


六、


七、代码实现(简化版)

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
from collections import defaultdict

class WordPieceTrainer:
def __init__(self, vocab_size=30000):
self.vocab_size = vocab_size
self.vocab = defaultdict(int)
self.pairs = defaultdict(int)

def train(self, corpus):
# 初始化字符词表
for word in corpus:
chars = list(word) + ["</w>"]
for c in chars:
self.vocab[c] += 1
# 统计字符对频率
for i in range(len(chars)-1):
pair = (chars[i], chars[i+1])
self.pairs[pair] += 1

# 迭代合并
for _ in range(100): # 假设迭代 100 次
max_score = -1
best_pair = None
for pair, freq in self.pairs.items():
score = freq / (self.vocab[pair[0]] * self.vocab[pair[1]])
if score > max_score:
max_score = score
best_pair = pair
if best_pair is None:
break
# 合并对并更新词表
self.vocab["".join(best_pair)] += self.pairs[best_pair]
del self.pairs[best_pair]
# 更新所有包含该对的字符对
# ...(省略具体实现)

# 示例用法
trainer = WordPieceTrainer()
trainer.train(["hug", "pug", "pun", "bun", "hugs"])

八、扩展与优化

  • 动态词表:根据任务需求动态调整词表大小(如 Longformer 的扩展词表)。
  • 多语言适配:为不同语言设计独立的子词规则(如 mBERT 支持 104 种语言)。
  • 混合分词:结合 BPE 与 WordPiece,平衡效率与语义(如 XLM-R)。

通过合理应用 WordPiece,开发者能在模型性能与计算资源间取得平衡,尤其适合处理大规模多语言文本场景。

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

http://blog.gxitsky.com/2026/02/26/AI-LangChain-023-Text-Splitter-WordPiece/

作者

光星

发布于

2026-02-26

更新于

2026-02-26

许可协议

评论