想象你是一位乐高大师,面对一栋从未见过的城堡模型。图纸上没有这座城堡的编号,但你并不慌——因为你知道,任何复杂的建筑都是由标准积木块拼成的。你拆下几个有印象的模块:拱门、塔尖、窗棂,再组合起来,你就懂了这座新建筑。 这就是子词嵌入的核心理念。在自然语言处理(NLP)的世界里,模型会遇见过千上万次“没见过的词”,我们管这叫 OOV(Out-Of-Vocabulary,未登录词)。没有子词技巧之前,模型见到生词只能两手一摊。而有了子词嵌入,模型就像乐高大师一样,把生词拆成熟悉的小积木块,照样能理解它、表示它。 在这篇文章里,我们不用啃公式也能搞懂 BPE(字节对编码)、WordPiece 这些响亮的名字。你会看到:词嵌入如何“查表”,遇到生词如何“拆积木”,拆完又如何“拼回去”。读完你会明白,为什么今天几乎所有的预训练语言模型——从 BERT 到 GPT——都在用这套“积木哲学”。
对于 NLP 工程从业者,掌握子词嵌入是处理开放词汇的必修课。它能让你快速定位 OOV 事故根源,为线上模型加上“容错机制”,真正做到词表有限而词汇无限。
可视化演示
动态演示子词嵌入解决OOV难题全解的核心机制,通过概览/流程/对比/趋势四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
子词嵌入解决OOV难题全解:子词嵌入 子词嵌入与OOV概述 子词嵌入与OOV原理 核心机制可视化 子词嵌入解决OOV难题全解整体架构与关键组件
词嵌入困境
在子词出现之前,NLP 模型处理单词的方式,就像城堡里的乐高仓库:每个单词对应一个固定编号的积木箱,箱子里装着这个词的向量表示。模型把句子中的每个词换算成编号,到仓库里“查表”取出对应的向量。这套体系很直观,但它有一个致命前提——仓库必须认识这个词。
技术上讲,这个仓库就是“固定词表 + 嵌入矩阵”。词表大小为 V 时,嵌入矩阵是一个 V×d 的矩阵,第 t 个词的向量就是矩阵的第 t 行:
其中 \(E\in\mathbb{R}^{V\times d}\) 是嵌入矩阵,\(u_t\) 是词 \(word_t\) 在词表中的编号索引。
问题在于,真实世界的词汇是开放的:新品牌、网络新词、专业术语、拼写变体会源源不断地冒出来。固定词表一旦建立就被画地为牢。遇到词表外的词(OOV),模型只能用特殊的 UNK 标记顶替,而 UNK 的向量是一个“模糊的平均脸”,没有任何语义信息。这就好比乐高大师拿到一块陌生的零件,仓库里查不到编号,只能扔进“杂物箱”——这块积木在这个模型里从此失去了意义。
- 嵌入矩阵模型预先学习好的大表格,每一行是某个词的向量表示。
- 词编号每个词在词表中的位置序号,本质是查表的“门牌号”。
- 查表取值用编号检索矩阵行,得到当前词的稠密向量,供下游任务使用。
子词核心思想
乐高大师的智慧在于:从没见过整座城堡不要紧,只要认得出拱门、塔尖这些标准件就够了。子词嵌入的思路一模一样——把单词拆成更小的、反复出现的“标准积木块”,用这些积木块的组合来表示任意单词。这样词汇表就从“词级”降到了“子词级”,规模被大幅压缩,同时几乎不再有真正“未知”的输入。
这个想法有坚实的语言学根基:英语等语言有丰富的形态学结构。playing = play + ing,unhappiness = un + happi + ness。词根承载核心语义,前后缀承载语法和词性变化。子词切分天然地把这些构词单元当作最小积木,等于让模型自己学会了“词根词缀词典”。
组合爆炸在这里是朋友的化身:假设只有 3 万个子词单元,任意两三个拼起来就能覆盖绝大多数英语单词。词表的词汇覆盖面不再是“背了多少个词”,而是“积木类型有多少种”。有限的小积木,拼出无限的词汇,这正是子词方案最迷人的地方。
BPE算法
BPE(Byte Pair Encoding,字节对编码)是子词切分里最经典、最接地气的算法。它不依赖任何语言学知识,只靠一条朴素的统计直觉:如果两个字符频繁地挨在一起,它们就应该被合并成一个积木块。整个训练过程像一位学徒在反复整理积木箱——哪两块积木总被一起用,就把它们粘成一块。
算法从字符级语料出发,每一轮统计当前所有相邻符号对的频次,选最高频的一对合并,形成新的子词符号,重复直到词表达到预设规模。这一过程得到的是一串“合并操作”,也就是合并规则表:
训练完成后,模型拿到一张合并规则表。面对新词时,同样从字符开始,一遍遍套用规则表中“能合并就合并”的操作——注意,这里不需要重新统计频率,规则表是现成的。比如训练时见过 low 和 lower,学会了合并 er,那么推理时遇到生词 lowestness,也能自动把它切分成 low + est + ness。这就是 BPE 解决 OOV 的机制:不与生词硬碰硬,而是把生词拆成训练时见过的熟积木。
- 候选对当前词汇表中的两个相邻符号(可能是单字符,也可能是子词)。
- 频次统计在整个语料中数一数这对符号挨着出现的次数。
- 选出最优取频次最高的相邻对,作为本轮合并的对象。
- 迭代合并把这对符号粘合成新符号,更新词表,重复上述过程直到目标词表规模达成。
WordPiece算法
WordPiece 是 BPE 的“概率版升级”。它同样反复合并相邻符号,但选择合并对象的标准从“出现次数”换成了“语言模型似然增益”。打个比方:BPE 的学徒只看“这两块积木是不是总被一起拿”,而 WordPiece 的师傅会问:“把这两块粘起来,拼出正确模型 的概率提升多少?”
WordPiece 为每个候选子词维护一个概率,合并 \(p,q\) 得到 \(pq\) 时,要求满足:
合并后序列的语言模型概率应当比合并前更高。每轮选择能使似然提升最大的候选对:
WordPiece 与 BPE 最主要的差别就在这:一个是“谁出现得多就合并谁”,一个是“谁合起来更顺就合并谁”。实际效果上,WordPiece 倾向于生成语言上更合理的子词边界,但它需要额外的语言模型来计算概率,训练开销更高。BERT 的词表正是用 WordPiece 构建的,所以它面对生词时,能切分得更“润物细无声”。
- 基准衡量把合并前的两个符号看作独立事件,期望共现概率是两者概率的乘积。
- 合并后概率子词 \(pq\) 在语言模型中的实际出现概率。
- 增益计算合并后的对数概率与合并前基准的差值,反映合并带来的“流畅度提升”。
- 挑增益最大每一轮都选增益最大的一对合并,直到词表规模达标。
子词嵌入表示
拼好积木只是第一步,模型真正要的是“向量”。子词嵌入的完整流程是:先把生词拆成子词序列,查子词向量表拿到每个子词的向量,再把它们合成一个词向量。合并不是简单相加,而是取平均,让每个子词贡献均匀的语义分量。
设生词 \(w\) 被切分为子词 \(s_1, s_2, \ldots, s_k\),每个子词对应向量 \(g(s_i)\),则词向量为:
这种表示方式让 OOV 词不再“无家可归”。比如 unknowable 切为 un + know + able,它的向量就由“否定前缀 + 知道 + 形容词后缀”三块积木的向量平均而成——模型即便第一次见它,也能大致感知到它和 known、unknown 有千丝万缕的关系。需要提醒的是,即使是子词也可能有极低频的情况,实践上会用特殊标记兜底,保证任何输入都有向量可查。
- 子词切分序列模型把生词按合并规则切成 k 个子词单元。
- 子词查表每个子词从子词向量表中获得一个稠密向量。
- 求和把所有子词向量逐维度相加,语义分量汇总。
- 取平均除以子词数量,防止长词向量过大,保持尺度稳定。
工程实践落地
理论落地到工程,有三个关键动作:加载子词模型、控制词表规模、缓存分词结果。目前最主流的工具是 SentencePiece,它把 BPE / WordPiece / unigram 等算法统一封装成“一句代码加载、一句代码切分”的开箱工具。无论是训练自己的词表,还是直接加载 BERT 的 WordPiece 词表,流程都极为标准。
词表规模的选择需要权衡:词表太小,子词碎片化,序列变长,模型计算量上升;词表太大,又回到了“词级嵌入”的老路,OOV 问题复萌。经验上,英文模型常用 32K 或 50K 的词表,中文则在 20K~80K 之间调优。另一个工程细节是缓存:子词切分在同一轮训练中重复出现频率极高,把“字符串→token 序列”的映射结果缓存下来,能省下大量重复计算。
子词方法对中文、日文这类没有自然空格的语言同样适用——这恰恰是它最神奇的地方。中文的“积木”天然是字或词,SentencePiece 可以直接按字符建模,再学习高频相邻字的合并。一个统一的子词管线同时服务中英文,意味着算法工程师再也不用为每种语言手写一套分词器了。
AI 应用场景
搜索引擎Query纠错与召回
阶段:文本预处理 / 查询理解
解决的问题:用户搜索时输入拼写错误、中英混输或新词(如“iPhone 15 Pro Max 哈苏”),传统固定词表检索直接匹配失败,召回率为零。子词嵌入把 query 拆成子词,生词也能映射到向量空间,与文档向量的相似度计算依然可行。
具体案例:某电商搜索系统接入基于 BPE 的子词模型后,对“加绒冲锋衣女加厚”这类长尾 query,分词稳定性显著提升。即使出现新品名“防晒冲锋衣pro版”,系统也能通过 防晒 + 冲锋衣 + pro + 版 的子词组合定位到正确品类,搜索曝光量提升约 15%。
多语言机器翻译的冷启动
阶段:词嵌入 / 序列到序列建模
解决的问题:小语种语料稀缺,直接训练词级嵌入会频遭 OOV 轰炸。子词嵌入将不同语言共享的字符、音节作为积木,让模型在资源丰富的语言上学会的“拼积木”能力迁移到低资源语言上。
具体案例:某出海翻译产品要支持印尼语与马来语互译,两者拼写相似但语料极少。工程师加载多语言预训练模型(使用 SentencePiece 子词词表),零微调即可完成基础翻译;微调 1 万条平行语料后,BLEU 值达到可用水平。生词不再导致整句翻译崩溃。
情感分析中的新词鲁棒性
阶段:特征表示 / 分类建模
解决的问题:社交媒体充满“绝绝子”"yyds"“破防了”这类网络新词,传统模型只能丢 UNK。子词嵌入能把新词拆成已有子词(如 yyds 拆为 yy + ds),向量依然落入语义空间,分类器不至于失灵。
具体案例:某舆情分析系统对微博评论做情感识别,接入子词嵌入后,“这个价格我真的会谢”这类反讽新表达也能被正确分到“负面”。对比原词级模型,OOV 词的分类准确率从 52% 提升到 78%。
Python 演示
# -*- coding: utf-8 -*-from collections import defaultdict, Counter class BPE: def __init__(self, num_merges=10, end_token='_'): self.num_merges = num_merges self.end_token = end_token self.merges = {} self.word_end = {} def get_pairs(self, word): return set(zip(word, word[1:])) def learn_bpe(self, corpus): freq = Counter() for w in corpus: chars = list(w) + [self.end_token] self.word_end[tuple(chars)] = self.word_end.get(tuple(chars), 0) + 1 vocab = {tuple(list(w) + [self.end_token]): c for w, c in self.word_end.items()} for i in range(self.num_merges): pairs = Counter() for tokens, count in vocab.items(): for p in self.get_pairs(tokens): pairs[p] += count if not pairs: break best = max(pairs, key=pairs.get) self.merges[best] = '(' + best[0] + ',' + best[1] + ')' new_vocab = {} for tokens, count in vocab.items(): merged = self.merge_word(tokens, best) new_vocab[merged] = new_vocab.get(merged, 0) + count vocab = new_vocab def merge_word(self, tokens, pair): out, i = [], 0 while i < len(tokens): if i < len(tokens) - 1 and (tokens[i], tokens[i+1]) == pair: out.append(tokens[i] + tokens[i+1]) i += 2 else: out.append(tokens[i]) i += 1 return tuple(out) def tokenize_word(self, word): tokens = tuple(list(word) + [self.end_token]) while len(tokens) > 1: pairs = self.get_pairs(tokens) best_key = None for p in pairs: if p in self.merges: if best_key is None or pairs_order(p) < pairs_order(best_key): best_key = p if best_key is None: break tokens = self.merge_word(tokens, best_key) return [t for t in tokens if t != self.end_token] def pairs_order(pair): return 0 def main(): corpus = ['low', 'lower', 'lowest', 'new', 'newer', 'newest', 'newerest'] bpe = BPE(num_merges=8) bpe.learn_bpe(corpus) print('merge rules:', bpe.merges) for w in ['lowestness', 'newerest', 'unknown']: pieces = bpe.tokenize_word(w) print(w, '->', pieces) if __name__ == '__main__': main()
from collections import defaultdict, Counter引入计数器,为频次统计做准备。self.merges = {}合并规则表,训练的核心产出物,推理阶段的“积木图鉴”。self.word_end[tuple(chars)] = ...给每个单词末尾加结束标记,让词尾模式被正确建模。pairs[p] += count对每个词内相邻字符对计数,权重是该词在语料中的频次。best = max(pairs, key=pairs.get)挑出本轮最高频的字符对,这是 BPE 的决策核心。self.merges[best] = ...把合并规则存入字典,记录这个“粘积木”动作。tokens = self.merge_word(tokens, best)把当前词中所有出现该对的位置全部合并。pairs = self.get_pairs(tokens)推理时重新获取相邻对,逐对查规则表。if p in self.merges只合并训练中学过的规则,未学过的相邻对保持原样。[t for t in tokens if t != self.end_token]去掉结束标记,输出最终子词序列。
常见误区
误区BPE 是统计最优的,切出来的子词就是语言学上最合理的,比如一定能把 `unknowable` 切为 `un+know+able`。
正解BPE 的合并规则纯粹由频率驱动,不看词根词缀。它可能学出 unk + now + able 这类在语言学上“奇怪”却统计上合理的切分。子词的目标不是语言学正确,而是让模型拿到稳定、可复用的表示单元。就像乐高拼法不止一种,能稳固支撑模型的就是好拼法。
为何常见:人天然期望切分结果符合直觉,但统计算法关心的是“压缩效率”与“表示稳定性”,二者不是同一件事。
误区WordPiece 就是把 BPE 换个名字,两者没区别,学一个就够。
正解两者框架相似但准则不同:BPE 按相邻符号对的出现频次合并,WordPiece 按语言模型概率增益合并。BPE 更简单高效,WordPiece 更倾向语义完整的边界,但需要额外维护语言模型概率。类比来说,BPE 是“看谁总一起出现就粘谁”,WordPiece 是“粘完要更顺口才算数”。
为何常见:两者接口高度相似,最终产物都是子词词表,不深究内部准则时很容易混淆。
误区用了子词嵌入之后,模型就永远不会再遇到未知输入了,OOV 问题彻底消失。
正解子词把“词级 OOV”变成了“子词级 OOV”,极大缓解但无法消灭。极端情况下,单个未知字符仍可能出现在输入里。工程上会为未知字符预留专门的占位符号,保证模型不崩溃。更重要的是,OOV 的定义从“单词没见过”变成了“字符组合没见过”,概率大幅下降但不为零。
为何常见:看到子词方案对绝大多数生词都有效,就容易过度乐观地认为它是终极解法。
误区子词嵌入就是把几个子词向量简单地加在一起,加得越多语义越强。
正解直接相加会让长词的向量范数偏大,在计算相似度时产生偏差。标准做法是取平均,保证不同长度词向量的尺度一致。平均池化的隐含假设是各子词对词义的贡献均等,虽然粗糙但实验效果好,后续也有加权和 attention 等变体改进。
为何常见:“叠加”符合直觉,而平均看起来像多此一举,容易被忽视。
面试题
Q1. BPE 算法每一轮合并操作的选择标准是?
- A.出现频次最高的相邻符号对← 正确答案
- B.随机挑选一个相邻符号对
- C.长度最短的相邻符号对
- D.语言模型概率增益最大的一对
答案:A。BPE(字节对编码)的核心是贪心策略:每轮统计语料中所有相邻符号对的频次,合并出现次数最多的那一对。选项 D 是 WordPiece 的准则,B 未利用统计信息,C 与频率标准无关。
Q2. 遇到 OOV(未登录词)时,传统的词级嵌入会怎么做?
- A.自动学习一个新向量给该词
- B.用特殊标记 UNK 统一替换← 正确答案
- C.直接把该词从句子中删除
- D.用所有词的平均向量替换
答案:B。传统词级模型词表固定,遇到词表外的词只能映射为统一的 UNK 标记,这个标记的向量是共享的、没有词义信息。A 是子词方案的思路;C 会破坏句子结构;D 虽看似合理但并非标准做法。
Q3. 关于 WordPiece 与 BPE 的区别,下列说法正确的是?
- A.BPE 基于词频,WordPiece 基于字符长度
- B.两者完全等价,只是名称不同
- C.WordPiece 用语言模型概率选合并对← 正确答案
- D.BPE 比 WordPiece 多了结束标记
答案:C。WordPiece 的核心是选择使语言模型似然增益最大的相邻对进行合并,而 BPE 只看频次。两者训练框架类似但准则不同,因此不是等价关系;BPE 也可以加结束标记,这不是两者区别。
Q4. 子词嵌入解决 OOV 的核心思想是什么?
- A.无限扩大词表直到覆盖所有词
- B.把生词拆成熟知的子词单元表示← 正确答案
- C.为每个生词单独训练一个模型
- D.放弃词向量改用字向量
答案:B。子词嵌入把生词拆分为训练中学过的子词单元,再组合其向量得到生词表示,实现“以有限积木拼无限词汇”。A 不现实;C 成本过高;D 字向量只是子词的特例,核心仍是拆与拼。
Q5. BPE 算法训练过程的正确步骤包括哪些?
- A.初始化词表为所有单字符← 正确答案
- B.反复统计并合并最高频相邻对← 正确答案
- C.直接使用语言学词典切分单词
- D.记录合并规则用于推理阶段← 正确答案
答案:ABD。BPE 从字符级词汇出发统计相邻对频次并贪心合并,同时把每一步合并规则保存下来供推理时复用。它不需要预先的语言学词典,因此 C 错误。
Q6. 子词嵌入中,一个 OOV 词的向量通过什么方式生成?
- A.直接随机初始化一个向量
- B.各子词向量取平均← 正确答案
- C.选择最相似旧词的向量
- D.取第一个子词的向量
答案:B。标准做法是先将生词切分为子词序列,查子词向量表后做平均池化得到最终词向量。随机初始化浪费了子词信息;选择最相似旧词或取首子词都丢弃了部分语义。
Q7. 下列哪些属于子词切分带来的工程收益?
- A.词表规模得到有效控制← 正确答案
- B.生词也能获得平滑的向量← 正确答案
- C.模型推理速度必然提升
- D.中英文共用一套分词管线← 正确答案
答案:ABD。子词切分压缩词表、消除绝大部分 OOV,让生词通过子词组合得到向量表示,SentencePiece 等工具对中英文都适用。C 并不必然成立——子词序列可能比单词更长,推理速度取决于具体场景。
Q8. 词级嵌入矩阵的维度通常如何描述?
- A.词表大小 × 向量维度← 正确答案
- B.向量维度 × 句子长度
- C.词表大小 × 词表大小
- D.句子长度 × 向量维度
答案:A。嵌入矩阵 \(E\in\mathbb{R}^{V\times d}\),行数等于词表大小 V,列数等于向量维度 d。每个词对应一行,查表即按编号取行。其他选项描述的是序列矩阵或方阵,不符合嵌入矩阵定义。
Q9. 关于 BPE 从训练到推理的过程,说法正确的是?
- A.训练阶段统计语料中的字符对频次← 正确答案
- B.推理阶段重新统计新词频次来决定合并
- C.训练产出的合并规则表直接应用于推理← 正确答案
- D.推理切分只需要查表套用已有规则
答案:AC。BPE 训练时统计语料得到合并规则表,推理时直接套用这些规则对生词进行切分,不需要重新统计频次。B 违背了 BPE 的设计初衷——推理阶段追求高效,不可能再做全语料统计。
Q10. 子词词表规模过大时,最可能出现的问题是?
- A.子词碎片化严重,序列过长
- B.词表向词级靠拢,OOV 复萌← 正确答案
- C.模型训练速度显著加快
- D.子词向量维度自动提升
答案:B。词表过大意味着合并次数多,最终切分结果接近完整单词,等于退化为词级嵌入,OOV 问题会重新出现。词表过小才会碎片化严重;词表大通常让训练变慢;向量维度是超参数,不会自动变化。