查看全课大纲(第 58 / 69 节)
- 1.NLP 的研究领域、应用与学习路径
- 2.自然语言的基本术语
- 3.信息论基础:信息熵与交叉熵
- 4.语言的进化与语义变迁
- 5.主观概率、客观概率与条件概率
- 6.贝叶斯原理与推理
- 7.随机变量:二项式、期望方差与联合概率
- 8.常见概率分布
- 9.辛普森悖论与贝叶斯解题实例
- 10.语言模型:语言概率与词袋模型
- 11.n-gram 语言模型(二元 / 三元 / N-Gram)
- 12.语言模型评价:交叉熵、困惑度与插值
- 13.朴素贝叶斯文本分类:垃圾邮件
- 14.拼写检查
- 15.中文分词:构词法、最大匹配、最优路径与 Jieba
- 16.词表示问题与语义相似度
- 17.TF-IDF 权重处理
- 18.One-Hot 表达与词表示的局限
- 19.Word2Vec 核心思想
- 20.Word2Vec 训练过程
- 21.Skip-gram 模型详解
- 22.词性标注任务与标注方法(规则 + N-Gram)
- 23.EM 算法与混合模型
- 24.高斯混合模型 GMM
- 25.从混合模型到隐马尔可夫模型
- 26.隐马尔可夫模型(HMM)推导与维特比解码
- 27.线性链条件随机场 CRF
- 28.贝叶斯网络与图模型
- 29.隐马尔可夫链与 HMM 推导
- 30.PLSA 主题模型
- 31.LSI 与 SVD 向量表达
- 32.LDA 与狄利克雷分布
- 33.主题模型实验复现(困惑度 / 分类 / 相似度 / 延展)
- 34.上下文无关文法(CFG)与句法分析
- 35.概率上下文无关文法 PCFG
- 36.统计机器翻译:词对齐与翻译模型
- 37.统计机器翻译中的对齐模型与参数估计
- 38.从统计机器翻译到神经机器翻译
- 39.神经元与反向传播速览(指向深度学习课)
- 40.CNN 原理及其在 NLP 中的应用
- 41.循环神经网络的基本原理与 Elman / Jordan 网络
- 42.LSTM:长短期记忆网络的核心思想与分步实现
- 43.Encoder-Decoder 框架
- 44.Seq2Seq 与早期注意力机制
- 45.注意力机制的背景与动机
- 46.注意力模型实现原理与匹配函数
- 47.记忆网络:组成与计算
- 48.注意力机制的应用与历史延展
- 49.Transformer 结构:Encoder-Decoder 栈与 Self-Attention
- 50.Multi-Head Attention、FFNN 与位置编码
- 51.层归一化、损失函数与 Universal Transformer 延展
- 52.预训练范式:从 ELMo 到 BERT
- 53.BERT 与掩码语言模型
- 54.BERT 下游任务微调与 GLUE 口径
- 55.GPT 与自回归语言建模
- 56.GPT-1 到 GPT-4 的演进
- 57.指令微调与 RLHF
- 58.分词器:BPE、WordPiece 与中文词表
- 59.上下文窗口与长文本处理
- 60.能力边界、成本与适用场景
- 61.文本分类实战:从朴素贝叶斯到逻辑回归
- 62.命名实体识别实战
- 63.文本生成与摘要实战
- 64.语义检索与向量库实战
- 65.RAG 知识库问答
- 66.NLP 评估指标:BLEU / ROUGE / BERTScore 与人工评估
- 67.幻觉与事实性问题
- 68.文本与知识结合(知识图谱压缩)
- 69.跨媒体信息检索与未来展望
分词器:BPE、WordPiece 与中文词表
小象实战讲义 · 自然语言处理
在构建自然语言处理模型时,如何将连续的文本切分成离散的、可处理的单元,是第一步也是至关重要的一步。从早期的按词切分,到如今主流的子词(Subword)方法,分词器(Tokenizer)的设计直接影响着模型的词汇覆盖、序列长度和最终的泛化能力。本节将深入剖析现代 NLP 中三种核心的子词分词算法——BPE、WordPiece 和 Unigram LM,并探讨中文场景下的特殊考量,为你构建或选择合适的分词器提供清晰的指导。
💡 核心导读
- BPE(Byte Pair Encoding):一种自底向上、基于频次合并的子词构建算法,是众多现代分词器的基础。
- WordPiece 与 Unigram LM:与 BPE 目标相似但训练策略不同的子词算法,分别基于似然增益和语言模型概率。
- 字节级 BPE:将文本编码为字节序列后再应用 BPE,实现真正的“零未登录词”,是 GPT 系列等模型采用的关键工程实践。
- 中文分词的特殊性:解析为何不能简单照搬英文的“按词”直觉,以及现代实践中如何让模型自动学习切分单位。
- 评估与实践:了解评估分词器的关键指标,并掌握在实践中如何根据自身语料进行选择和测试。
从字符到子词:BPE 算法详解
在神经机器翻译兴起之初,一个棘手的问题是稀有词(Rare Words)或未登录词(Out-Of-Vocabulary, OOV)的处理。直接将所有稀有词映射为统一的 <UNK> 标记会丢失大量信息。2016 年,Rico Sennrich 等人在论文 Neural Machine Translation of Rare Words with Subword Units(ACL 2016,arXiv:1508.07909)中,将一种数据压缩算法——字节对编码(Byte Pair Encoding, BPE)创造性地应用于 NLP,通过子词单元有效缓解了 OOV 问题。
BPE 的核心思想是:从所有单个字符(或字节)开始,不断合并语料中出现频次最高的相邻符号对,直到达到预定的词表大小。这样,高频的字符序列(如“ing”、“ed”)会被合并成子词,而低频词则会被切分成更小的单元(如字符),从而在有限的词表内实现对任意单词的编码。
根据论文 §3.2 的描述,BPE 算法步骤如下:
- 初始化词表:将语料中每个单词拆分为字符序列,并在末尾添加一个特殊的词尾标记(如
</w>)以区分单词边界。所有不同的单字符构成初始词表。 - 统计与合并:统计所有相邻符号对在语料中的出现频次。选择频次最高的一对,将它们合并成一个新的符号,并加入到词表中。
- 重复合并:重复第 2 步,每次合并都将词表大小增加 1。持续进行 次合并,其中 是一个超参数,它决定了最终词表大小 = 初始词表大小 + 。
- 编码新文本:对新文本进行分词时,按照训练时学到的合并规则(顺序至关重要),依次尝试应用这些合并操作,直到无法再进行任何合并为止。通常采用贪心的最长匹配策略。
BPE 的关键性质在于其确定性:对于同一份语料和相同的合并次数 ,只要实现细节一致(如是否区分大小写、如何处理标点),得到的合并规则和最终词表是完全相同的。下面我们通过一个极简的例子来直观感受这个过程。
# BPE 合并过程演示(极简版)
corpus = "low lower lowest new newer newest" # 模拟语料,已小写化并用空格分词
words = corpus.split()
# 初始化:将每个词拆分为字符,并在末尾添加词尾标记 `</w>`
vocab = {}
for w in words:
chars = list(w) + ['</w>']
# 用空格连接字符,作为词表项的键
vocab[' '.join(chars)] = vocab.get(' '.join(chars), 0) + 1
print("初始状态(字符序列及频次):")
for item, freq in sorted(vocab.items()):
print(f" {item}: {freq}")
# 定义合并函数
def get_stats(vocab):
"""统计相邻符号对的频次"""
pairs = {}
for word, freq in vocab.items():
symbols = word.split()
for i in range(len(symbols)-1):
pair = (symbols[i], symbols[i+1])
pairs[pair] = pairs.get(pair, 0) + freq
return pairs
def merge_vocab(pair, vocab_in):
"""将指定的符号对合并,更新词表"""
vocab_out = {}
bigram = ' '.join(pair)
replacement = ''.join(pair)
for word in vocab_in:
# 替换所有出现的该相邻对
w_out = word.replace(bigram, replacement)
vocab_out[w_out] = vocab_in[word]
return vocab_out
# 执行3轮合并
num_merges = 3
for i in range(num_merges):
pairs = get_stats(vocab)
if not pairs:
break
best_pair = max(pairs, key=pairs.get)
best_freq = pairs[best_pair]
vocab = merge_vocab(best_pair, vocab)
print(f"\n第 {i+1} 轮合并: 合并符号对 {best_pair} (频次: {best_freq})")
print("当前词表示例:", sorted(vocab.keys())[:5]) # 只打印前5个示例初始状态(字符序列及频次):
l o w </w>: 1
l o w e r </w>: 1
l o w e s t </w>: 1
n e w </w>: 1
n e w e r </w>: 1
n e w e s t </w>: 1
第 1 轮合并: 合并符号对 ('w', 'e') (频次: 4)
当前词表示例: ['l o w </w>', 'l o we r </w>', 'l o we s t </w>', 'n e w </w>', 'n e we r </w>']
第 2 轮合并: 合并符号对 ('l', 'o') (频次: 3)
当前词表示例: ['lo w </w>', 'lo we r </w>', 'lo we s t </w>', 'n e w </w>', 'n e we r </w>']
第 3 轮合并: 合并符号对 ('n', 'e') (频次: 3)
当前词表示例: ['lo w </w>', 'lo we r </w>', 'lo we s t </w>', 'ne w </w>', 'ne we r </w>']可以看到,算法先合并了高频的相邻字符对:第 1 轮把 w 与 e 并成 we(频次 4),第 2 轮把 l 与 o 并成 lo(频次 3),第 3 轮把 n 与 e 并成 ne(频次 3)。三轮下来 low、new 的前半段已经被并成子词单元——合并顺序是按频次排出来的,不是我们指定的;er、est 这类只在两个词里出现的片段要往后排才会被合并。
WordPiece 与 Unigram LM:另辟蹊径的子词算法
BPE 基于频次进行合并,直观且有效。但研究人员很快提出了其他优化目标的分词算法,其中最具代表性的是 WordPiece 和 基于 Unigram 语言模型的分词。
WordPiece 算法最初在论文 Japanese and Korean voice search(Schuster & Nakajima, ICASSP 2012)中作为日韩语语音搜索的一种切分策略被简要提及。其训练过程与 BPE 类似,也是从字符开始自底向上合并。但关键区别在于选择合并哪一对的准则:
- BPE:选择频次最高的相邻符号对。
- WordPiece:选择合并后能最大程度提升语言模型似然的相邻符号对。这是一种通用表述,其具体实现通常是计算合并前后序列的似然值之差,选择能使似然增益最大的一对进行合并。
Unigram 语言模型分词(Unigram LM)则采用了完全不同的自顶向下思路:
- 首先,从一个非常大的种子词表开始(例如包含所有常见子词和字符)。
- 训练一个 Unigram 语言模型,为词表中的每个子词赋予一个概率。
- 迭代地移除那些对总体似然贡献最小的子词(例如通过期望最大化算法),直到词表缩小到目标大小。
- 推理时,使用维特比(Viterbi)算法找到使句子概率最大的切分方式。
为了更清晰地对比,我们将三者的核心差异总结如下:
| 特性 | BPE | WordPiece | Unigram LM |
|---|---|---|---|
| 起点 | 字符(或字节) | 字符(或字节) | 一个很大的候选子词表 |
| 训练方向 | 自底向上(合并) | 自底向上(合并) | 自顶向下(剪枝) |
| 每步决策依据 | 合并频次最高的相邻对 | 合并使语言模型似然提升最大的相邻对 | 移除概率最低、对总体似然贡献最小的子词 |
| 停止条件 | 达到目标词表规模(合并 k 次) | 达到目标词表规模 | 达到目标词表规模 |
| 推理切分 | 贪心最长匹配(按合并顺序应用) | 贪心最长匹配 | 维特比算法(寻找最大概率切分) |
需要强调的是,三者的主要差异体现在训练阶段如何构建词表。在推理(编码新文本)阶段,BPE 和 WordPiece 通常都采用相同的“最长匹配优先”的贪心策略,而 Unigram LM 则可以利用动态规划求得全局最优切分。
工程实践:字节级 BPE 与中文场景
字节级 BPE:真正的零 OOV
标准的 BPE 以字符为单位,但当遇到字符表以外的生僻字、Emoji 或多语言混合文本时,依然会出现 OOV。一个彻底的解决方案是 字节级 BPE(Byte-level BPE)。
- 将输入文本通过 UTF-8 编码转换为字节序列(每个字节取值 0-255)。
- 以这 256 个字节值作为初始词表,在此基础上运行 BPE 算法。
- 任何文本都可以被无损地转换为字节序列,因此理论上未登录词率降为 0。
代价是序列长度会增加(例如,一个 UTF-8 编码的中文字符通常占 3 个字节,可能被切分为 1 到 3 个子词)。GPT-2 及其后续模型广泛采用了这一策略。
中文分词的现代视角
中文分词长期以来是一个独立的研究课题,因为中文书写时没有空格分隔词。传统方法依赖词典和统计模型(如隐马尔可夫模型、条件随机场)进行切分,但总会面临切分歧义问题(如“乒乓球拍卖完了”)。
现代基于子词的方法为中文处理提供了新思路:
- 按字切分:将每个汉字作为独立 token。优点是完全无 OOV(前提是字表完备),词表可以很小,但序列长、且丢掉了词内字的紧密关联。
- 按词切分:使用传统分词器。优点是序列短,语义单元清晰,但严重依赖分词器质量,且不同分词器的差异会导致下游模型效果不稳定。
- 子词(BPE)切分:从单字开始,让 BPE 算法根据语料频次自动合并出高频的词语或词片段。这避免了预先定义“词”的歧义,让模型在数据驱动下学习合适的表示单位。例如,“自然语言处理”可能被切分为
["自然", "语言", "处理"]或["自然", "语言处理"],具体取决于其在训练语料中的共现频率。
现代主流实践倾向于第三种方案:不预先套用一套中文分词词典,而是让模型通过子词分词器(尤其是字节级 BPE)从语料里学切分单位。好处是中英文走同一套流程、少一层人为歧义;代价是切分结果依赖语料,换领域就得重新看压缩率与未登录率。
下面就真跑一次中文 BPE:四句小语料,按”字”起步,跑 12 次合并,再拿一句没参与训练的话对比两种切法的 token 数。
corpus = [
"自然语言处理是人工智能的重要分支",
"自然语言处理需要语言模型",
"语言模型可以处理自然语言文本",
"人工智能与自然语言处理关系密切",
]
# 中文没有空格:把每句当成一个"词",汉字当成符号
vocab = {}
for sent in corpus:
key = " ".join(list(sent) + ["</w>"])
vocab[key] = vocab.get(key, 0) + 1
def get_stats(v):
pairs = {}
for w, f in v.items():
s = w.split()
for i in range(len(s) - 1):
pairs[(s[i], s[i + 1])] = pairs.get((s[i], s[i + 1]), 0) + f
return pairs
def merge_pair(pair, v):
bigram, repl = " ".join(pair), "".join(pair)
return {w.replace(bigram, repl): f for w, f in v.items()}
merges = []
for step in range(12):
p = get_stats(vocab)
if not p:
break
best = max(p, key=p.get)
merges.append((best, p[best]))
vocab = merge_pair(best, vocab)
print("学到的合并顺序(前 12 步):")
for i, (pair, freq) in enumerate(merges, 1):
print(f" {i:>2}. {''.join(pair):<6} 频次 {freq}")
def encode(text, merges):
syms = list(text) + ["</w>"]
for pair, _ in merges:
a, b = pair
out, i = [], 0
while i < len(syms):
if i < len(syms) - 1 and syms[i] == a and syms[i + 1] == b:
out.append(a + b)
i += 2
else:
out.append(syms[i])
i += 1
syms = out
return syms
target = "自然语言处理是人工智能的分支"
chars = list(target) + ["</w>"]
subs = encode(target, merges)
print(f"\n目标句:{target}")
print(f"按字切分:{chars} → {len(chars)} 个 token")
print(f"BPE 切分:{subs} → {len(subs)} 个 token")学到的合并顺序(前 12 步):
1. 语言 频次 6
2. 自然 频次 4
3. 自然语言 频次 4
4. 处理 频次 4
5. 自然语言处理 频次 3
6. 人工 频次 2
7. 人工智 频次 2
8. 人工智能 频次 2
9. 语言模 频次 2
10. 语言模型 频次 2
11. 自然语言处理是 频次 1
12. 自然语言处理是人工智能 频次 1
目标句:自然语言处理是人工智能的分支
按字切分:['自', '然', '语', '言', '处', '理', '是', '人', '工', '智', '能', '的', '分', '支', '</w>'] → 15 个 token
BPE 切分:['自然语言处理是人工智能', '的', '分', '支', '</w>'] → 5 个 token这份输出有三处值得停下来看:
- 前 5 步合并出来的正好是”语言""自然""自然语言""处理""自然语言处理”——没有一个词是我们手工指定的,全是按频次从语料里长出来的。这就是”让数据决定切分单位”的实际含义。
- 第 11、12 步开始合并只出现 1 次的串,还越并越长(把整句”自然语言处理是人工智能”并成了一个 token)。原因是 12 次合并在四句话的语料上太多了:高频对耗尽后,算法就去吃低频对。所以 k 不是越大越好,要和语料规模匹配——这是自己跑一遍才会看到的坑。
- 序列从 15 个 token 压到 5 个,代价是第 17 行那个巨型 token 在别的语料上几乎不会命中。压缩率和泛化是此消彼长的,评估时要一起看。
如何选择与评估分词器?
今天,“子词分词(尤其是 BPE 及其变种)+ 字节级兜底” 已成为大语言模型分词器的主流设计方案。面对具体任务时,我们应如何选择和评估?
评估分词器好坏,通常关注以下三个核心指标,并且必须在自己任务的语料上进行测试:
- 压缩率:同样长度的文本,切分后产生的 token 数量。token 越少,模型需要处理的序列越短,效率通常越高。
- 未登录词率:测试集中有多少 token 不在词表内。理想情况下应为 0(字节级 BPE 可保证)。
- 下游任务效果:这是最终标准。在不同分词方案下训练或微调模型,比较其在目标任务(如分类、翻译)上的性能。
没有“放之四海而皆准”的最佳词表大小或分词算法。你需要:
- 明确任务领域和主要语言。
- 准备代表性的训练和测试语料。
- 尝试不同的分词算法(BPE, WordPiece, Unigram LM)和不同的词表大小。
- 综合比较上述三个指标,选择最适合你场景的方案。
📝 动手练一练
- 概念辨析:BPE 和 WordPiece 在训练阶段构建词表时,最核心的决策准则有什么不同?这种不同可能导致学到的词表有什么倾向性差异?
👉 点击查看参考答案
- BPE 的决策准则是:合并当前语料中频次最高的相邻符号对。这使它倾向于快速合并那些共现频率极高的字符组合(如英文中的“th”、“ing”),可能更快地形成常见的词缀和字母组合。
- WordPiece 的决策准则是:合并能使语言模型似然值提升最大的相邻符号对。这相当于不仅考虑频次,还考虑合并后对新“词”概率分布的贡献。理论上,它可能更倾向于合并那些能显著降低序列困惑度、对语言建模更有用的单元,可能对形态丰富的语言(如德语、土耳其语)中复杂的词形变化更有帮助。
两者的倾向性差异在理论上存在,但在许多实际任务和语料中,最终学到的词表效果可能相近。选择哪种往往取决于具体实现、库的支持以及历史项目惯性。
- 手算练习:假设我们有一个极小的语料,包含两个单词:
"apple"(出现 3 次)和"applet"(出现 2 次)。我们使用 BPE 算法,初始词表为单个字符{'a', 'p', 'l', 'e', 't', '</w>'}。如果只进行 2 次合并,请手动推导前两次合并的符号对分别是什么?合并后的词表(子词集合)是怎样的?(提示:注意词尾标记</w>)
👉 点击查看参考答案
首先,将单词表示为带词尾标记的字符序列并统计频次:
"apple":a p p l e </w>(频次 3)"applet":a p p l e t </w>(频次 2)
第一步:统计初始相邻对频次
('a', 'p'): 3 + 2 = 5('p', 'p'): 3 + 2 = 5('p', 'l'): 3 + 2 = 5('l', 'e'): 3 + 2 = 5('e', '</w>')(来自 apple): 3('e', 't')(来自 applet): 2('t', '</w>'): 2
频次最高的对有多组,都是 5。通常 BPE 实现会按字典序或首次出现顺序选择。假设我们选择 ('a', 'p') 进行第一次合并,新符号为 'ap'。
合并后序列变为:
ap p l e </w>(频次 3)ap p l e t </w>(频次 2)
第二步:统计新的相邻对频次
('ap', 'p'): 3 + 2 = 5('p', 'l'): 3 + 2 = 5('l', 'e'): 3 + 2 = 5('e', '</w>'): 3('e', 't'): 2('t', '</w>'): 2
频次最高的对仍有多个(5)。假设选择 ('ap', 'p') 进行第二次合并,新符号为 'app'。
合并后序列变为:
app l e </w>(频次 3)app l e t </w>(频次 2)
经过两次合并后的词表(子词集合) 包含:
- 初始字符:
{'a', 'p', 'l', 'e', 't', '</w>'} - 第一次合并产生的新符号:
'ap' - 第二次合并产生的新符号:
'app' - 总计:8 个子词单元。
延伸阅读
- Neural Machine Translation of Rare Words with Subword Units(Sennrich et al., ACL 2016,arXiv:1508.07909)—— BPE 应用于 NLP 的奠基论文。
- Japanese and Korean voice search(Schuster & Nakajima, ICASSP 2012, DOI 10.1109/ICASSP.2012.6289079)—— WordPiece 算法的原始出处。
- Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates(Kudo, 2018,arXiv:1804.10959)—— 提出基于 Unigram 语言模型的分词及子词正则化方法。
本章小结
本节我们深入探讨了现代 NLP 中核心的分词技术。我们从解决稀有词翻译问题的 BPE 算法出发,理解了其自底向上、基于频次合并的工作原理。进而,我们对比了 WordPiece(基于似然增益合并)和 Unigram LM(基于概率剪枝)这两种不同的子词构建策略。在工程实践上,字节级 BPE 通过操作字节序列实现了真正的零未登录词,成为大模型的标配。对于中文,现代最佳实践是放弃传统的有歧义的分词器,转而让模型通过子词分词器从数据中自动学习合适的表示单元。最后,我们明确了评估分词器应从压缩率、未登录词率和下游任务效果三个维度,并在自身语料上进行实测的原则。
📋 行动清单
- 使用一个小型英文/中文混合语料,亲自编写或调用一个 BPE 训练代码,观察不同合并次数下词表的变化。
- 在你的任务领域语料上,分别尝试按字、按传统分词、按 BPE 子词进行切分,并统计比较它们的序列平均长度。
- 思考:如果你要为一个处理多语言(中、英、代码、数学公式)的模型设计分词器,你会优先考虑哪种方案?为什么?
—— 小象教研组