老王是小区门口的脸盲保安,手里只有一本登记册,上面写着每个住户的完整姓名。有人搬走了、有人新搬来,只要名字没登记,老王就完全认不出来。word2vec 和 GloVe 就像这本登记册:把每个单词做成一张固定“脸卡”,遇到词表里没有的新词(OOV),只能两手一摊。FastText 却换了一种思路——它不记完整姓名,而是记“姓氏”“部首”“音节”这些小积木。只要见过“李”“龙”这些常用片断,哪怕遇到“李子龙”这种从没登记过的名字,也能大概猜出他和“李小龙”沾亲带故。这些小积木,就是字符 n-gram。本文从词向量的“脸盲症”出发,讲清楚 FastText 如何用子词拼装出词向量、怎么训练、和 word2vec/GloVe 有何不同,又怎样在低资源语言中发挥作用。
对 NLP 工程师而言,掌握 FastText 子词嵌入,等于拿到处理未登录词和稀缺语言词向量的“万能积木箱”,能让模型在冷启动场景中立刻获得泛化能力。
目录
可视化演示
动态演示FastText子词嵌入的核心机制,通过概览/流程/对比/趋势四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
FastText子词嵌入:Fast FastText子词嵌入概述 FastText子词嵌入原理 核心机制可视化 FastText子词嵌入整体架构与关键组件
词向量的脸盲症
老王是小区门口的脸盲保安,手里只有一本登记册:每个住户的完整姓名对应一张“脸卡”。word2vec 的行为和他一模一样——训练时把每个词登记成一行向量,应用时就拿着这个词去登记册里查。可小区里新住户不断搬来,只要名字没登记,老王就完全不认识。这就是自然语言处理里常说的 OOV(未登录词)问题。
要理解问题,先看 word2vec 的查表表示:词表 V 里每个词都有一个整数编号,模型维护一个嵌入矩阵 E,词 w 的向量就是矩阵中对应的一行。
这种登记的坏处不止是“查不到”,还忽略了词形变化。比如“run”和“running”在登记册里被当作两个毫不相干的陌生人,可它们明明有同一个词根。老王不想再脸盲下去了,他决定改记更小的单位——姓氏和偏旁。
- 词表索引 \(i_w\)训练前先给每个出现过的词分配唯一的整数编号,相当于住户的身份证号。
- 查表取向量用这个编号从嵌入矩阵中取出一行,作为该词的稠密向量。
字符n-gram:把名字拆成小积木
老王想出了一个办法:与其死背完整姓名,不如先记住“李”“龙”“run”“ing”这种小积木。新名字“李子龙”虽然没登记过,但只要小积木见过,就能拼出个大概。FastText 就是把这个直觉变成了算法。
它先把单词用特殊符号包裹起来,比如 fast 变成 <fast>,其中 < 表示词首、> 表示词尾。然后在这个包裹后的字符串上开一个长度为 n 的滑动窗口,取出所有连续字符片段,同时把完整单词也保留在集合里。
以 fast、n=3 为例,子词集合是 {fast, <fa, fas, ast, st>}。n 通常取 3 到 6:太小会混入大量无意义片段,太大则接近整词,失去“拼积木”的泛化能力。
- 边界包裹在原始单词首尾加上特殊符号,得到
\tilde{w},让模型能区分“词首的 fa”和“词中的 fa”。 - 滑动窗口在
\tilde{w}上从左到右取长度为 n 的连续片段,得到所有字符 n-gram。 - 并入整词最后把原始单词 w 也放进集合,保留完整词信息。
拼装车间:子词向量求和
老王在脑海里建了一个“积木仓库”,每种偏旁积木都有一个对应的向量。拼一个新名字时,他把组成这个名字的所有小积木向量加起来,就得到了这个名字的向量。FastText 的模型结构就是这样一座拼装车间。
FastText 整体沿用 skip-gram 的框架,但输入不再是查表得到的整词向量。给定单词 w,先用上一节的方法取出子词集合 \(G_w\),再把集合里每个子词 g 的向量 \(\mathbf{z}_g\) 逐元素相加。
为什么这样有效?因为大量单词共享相同的 n-gram。比如“cat”“cats”“catlike”都包含 cat 这个片段,训练时梯度会同时更新它们的公共积木,数据利用率大大提高。word2vec 给每个整词单独“开模”,FastText 则让所有词共用一套积木。
- 子词向量每个字符 n-gram g 都有一个独立的 d 维向量 \(\mathbf{z}_g\),由训练过程学习得到。
- 逐元素相加把子词集合里的所有向量按位相加,得到整词表示;这就是“拼装”过程。
训练优化:负采样与纠错
拼装车间需要老师来纠错:当上下文信息和当前词相符时,就奖励这些积木;不相符时,就惩罚它们。FastText 采用负采样作为纠错信号,这也是训练的核心细节。
给定中心词 w 和上下文词 c,模型先计算它们子词向量拼装结果的内积,再用 sigmoid 映射成 0 到 1 的概率。我们希望正样本概率接近 1,随机抽取的负样本概率接近 0。
训练时,误差会通过“拼装过程”回传到该词的所有子词向量上。共享的子词被多个词反复纠错,从而学到更稳定的形态学信息。FastText 还支持层次 Softmax 和并行训练,在工业界非常高效。
- 点积得分两句话——词向量方向和大小越接近,内积越大。
- 正样本项对于真实出现在上下文里的 c,希望 sigmoid(s) 接近 1,损失取负对数。
- 负样本项对随机噪声词 n,希望 sigmoid(s) 接近 0,所以用 \(-s\) 取对数。
对比 word2vec/GloVe
三种词向量方法像三种记忆方式:word2vec 是“整名记忆法”,GloVe 是“统计户口本”,FastText 是“偏旁拆解法”。没有绝对优劣,关键是看场景。
word2vec 和 GloVe 都需要预先构建封闭词表,遇到 OOV 只能丢弃或随机初始化;FastText 却能将未见过的词拆成见过的子词再拼装。代价是参数规模更大:word2vec 每个整词一个向量,而 FastText 每个子词也要一个向量。
适用场景因此不同:在语料大、词形稳定的标准任务里,word2vec 简单高效;在形态丰富、多语言、短文本、冷启动任务中,FastText 的子词泛化能力更有价值。老王也明白了:住户流动大的社区,偏旁记忆法比整名记忆法更管用。
\displaystyle P_{\text{FastText}} = \left(\sum_{w\in V}|G_w|\right)\times d\quad\text{vs}\quadP_{\text{word2vec}} = |V|\times d- word2vec 参数词表大小为 |V|,每个词 d 维向量,所以参数总数是两者相乘。
- FastText 参数每个词 w 有 |G_w| 个子词,每个子词又有 d 维向量,对所有词累加。
低资源语言与 OOV 实战
偏远小镇的住户很少,老王记不住几张完整脸卡,但他发现小镇居民的名字里,许多偏旁都和外面大家族通用。于是靠拼积木,他依然能认出不少新面孔。低资源语言正是这样的场景。
土耳其语、芬兰语等形态丰富的语言里,一个词根能衍生几十种词形。word2vec 需要每个词形都出现足够多次才能学好,但低资源语料根本凑不齐。FastText 把稀缺的词形拆成常见的子词,用“零件复用”缓解数据稀疏。对于训练时完全没见过的 OOV 词 \(w'\),只要它的字符片段曾经在训练集中出现过,就可以拼出向量。
这种特性让跨语言迁移也受益:源语言常见的字符片段可能在目标语言中出现,模型就能实现一定程度的“零样本”词向量生成。当然,如果 OOV 词的字符组合一个都没见过,FastText 也无能为力。
- OOV 子词集合先对未登录词拆出所有字符 n-gram。
- 取交集只保留训练时见过、已经学到向量的子词。
- 累加表示把这些见过的子词向量相加,得到 OOV 词的临时向量。
评估与工程落地
拼装出来的积木到底好不好,需要用尺子量一量。FastText 的评估方式和 word2vec 类似,最常用的是余弦相似度:两个向量方向越一致,语义越接近。
日常评估包括类比推理,如“king - man + woman ≈ queen”,以及词相似度任务。工程上还可以直接用 FastText 做文本分类,因为它的输出层稍加改造就能预测句子标签。
工程落地时,建议用 gensim 或官方 fastText 库,设置 minn=3、maxn=6;若子词表过大,可以做剪枝或哈希压缩。像下面代码里的迷你实现那样,先在小语料上验证子词共享的效果,再放到大规模任务中训练。老王的故事到这里也圆满了:靠着一套可以复用的偏旁积木,他再也不是那个见到新人就犯愁的脸盲保安了。
- 点积把两个向量对应元素相乘后累加,衡量方向一致程度。
- 模长归一化除以两个向量的长度,消除向量本身长短的影响。
AI 应用场景
搜索与推荐中的 Query 纠错
阶段:召回与语义匹配
解决的问题:用户搜索时经常输入新词、拼写变体或品牌自创词,传统基于精确匹配的召回完全失效。FastText 能把未登录 query 拆成子词,映射到和已知词相近的向量空间,从而完成模糊召回。
具体案例:电商搜索中,用户输入“irobot 扫地机”而商品标题里写的是“iRobot 扫地机器人”。用 FastText 子词向量计算相似度,即使“irobot”从未出现在训练词表中,也能通过共享子词匹配到正确商品。
低资源语言的冷启动
阶段:预训练与词向量初始化
解决的问题:土耳其语、芬兰语等形态丰富语言标注数据少,word2vec 训练不出可靠的完整词向量。FastText 通过复用字符 n-gram,让稀疏词形也能获得较稳定的表示。
具体案例:为一个小语种的新闻分类系统训练词向量时,语料只有几万句。使用 FastText 后,之前 word2vec 无法覆盖的大量词形变体,都能通过共享子词获得可用向量,分类准确率提升明显。
短文本意图识别与文本分类
阶段:文本特征提取
解决的问题:日志、评论、对话等短文本充满拼写错误和网络新词,传统词向量覆盖率低。FastText 分类器把整句话的子词向量相加,再经过线性分类器,既快又稳。
具体案例:客服工单系统需要自动区分“退款”“维修”“投诉”等意图。用户常把“退款”打成“退kuan”或“退歀”,FastText 能通过子词层面的相似度正确归类,减少人工转派。
Python 演示
import mathimport random class MiniFastText: def __init__(self, dim=8, n=3, lr=0.05): self.dim = dim self.n = n self.lr = lr self.ngram_vec = {} def grams(self, word): word = '<' + word.lower() + '>' return [word[i:i + self.n] for i in range(len(word) - self.n + 1)] def _init_gram(self, gram): if gram not in self.ngram_vec: random.seed(sum(ord(ch) for ch in gram)) self.ngram_vec[gram] = [random.uniform(-0.1, 0.1) for _ in range(self.dim)] def word_vec(self, word): vec = [0.0] * self.dim for gram in self.grams(word): self._init_gram(gram) for i in range(self.dim): vec[i] += self.ngram_vec[gram][i] return vec def _update(self, grams, grad, other): for gram in grams: v = self.ngram_vec[gram] for i in range(self.dim): v[i] -= self.lr * grad * other[i] def _sigmoid(self, x): if x >= 0: return 1.0 / (1.0 + math.exp(-x)) exp_x = math.exp(x) return exp_x / (1.0 + exp_x) def train(self, sentences, epochs=20, window=1, neg_k=2): words = [w for s in sentences for w in s.lower().split()] vocab = sorted(set(words)) for w in vocab: for g in self.grams(w): self._init_gram(g) for _ in range(epochs): for sent in sentences: tokens = sent.lower().split() for i, w in enumerate(tokens): wg = self.grams(w) wv = self.word_vec(w) ctx = tokens[max(0, i - window):i] + tokens[i + 1:i + 1 + window] for c in ctx: cv = self.word_vec(c) p = self._sigmoid(sum(a * b for a, b in zip(wv, cv))) grad = p - 1.0 self._update(wg, grad, cv) self._update(self.grams(c), grad, wv) for _ in range(neg_k): neg = random.choice(vocab) nv = self.word_vec(neg) pn = self._sigmoid(sum(a * b for a, b in zip(wv, nv))) self._update(wg, pn, nv) self._update(self.grams(neg), pn, wv) def similarity(self, a, b): av = self.word_vec(a) bv = self.word_vec(b) dot = sum(x * y for x, y in zip(av, bv)) na = math.sqrt(sum(x * x for x in av)) nb = math.sqrt(sum(x * x for x in bv)) return dot / (na * nb + 1e-8) if __name__ == '__main__': corpus = [ 'the cat sat on the mat', 'the dog ran across the yard', 'cats and dogs are pets', 'catlike movements are graceful', ] model = MiniFastText(dim=8, n=3, lr=0.05) model.train(corpus, epochs=30) print('cat vs catty(OOV):', round(model.similarity('cat', 'catty'), 4)) print('cat vs dog:', round(model.similarity('cat', 'dog'), 4))
def grams(self, word)给单词加边界并拆出所有 3-gram,这是子词集合的构建入口。word_vec(self, word)将子词向量逐元素相加,得到整词向量。_update(self, grams, grad, other)把梯度回传到该词的所有子词向量上,实现共享参数更新。_sigmoid(self, x)数值稳定的 sigmoid,把内积得分映射成概率。grad = p - 1.0正样本期望输出为 1,误差是预测概率减 1。self._update(self.grams(c), grad, wv)上下文词也按子词方式更新,保证共享 n-gram 被反复纠错。for _ in range(neg_k)负采样多次,让随机词的概率尽量接近 0。similarity(self, a, b)用余弦相似度评估两个词的语义接近程度。if __name__ == '__main__'训练小语料并打印结果,验证 OOV 词也能被表示。
常见误区
误区FastText 是纯字符级模型,整词信息毫无作用,所有词都由 n-gram 拼出来。
正解FastText 在子词集合里始终保留完整单词本身,即 \(G_w = \{w\} \cup \text{n-grams}\)。整词相当于一块“大积木”,和字符片段一起参与求和。子词是为了增强形态泛化,并不是替代整词。
为何常见:因为资料总在强调字符 n-gram 和 OOV 处理,初学者容易忽略集合定义里的整词项。
误区只要使用 FastText,任何没见过的词都能得到完美向量。
正解FastText 只能利用训练时见过的子词片段。如果 OOV 词的所有 n-gram 都从未出现,它的向量依然无从谈起。它只是大幅提高了“没见过的词”的覆盖率,而不是无中生有。
为何常见:把“子词覆盖率高”误解成“无限覆盖”,忘记了 n-gram 本身也来自训练语料。
误区FastText 既然能处理 OOV,效果一定全面优于 word2vec,项目里无脑选 FastText。
正解FastText 在形态丰富、低资源、拼写变体多的场景优势明显,但在语料充足、词形稳定的标准任务中,word2vec 往往更简单高效,且内存占用更小。工具没有绝对优劣,只有是否匹配场景。
为何常见:看到一篇广告或基准测试就以为“新一代替代旧一代”,忽略了不同方法的适用边界。
误区n 越大,捕获的上下文越长,子词信息越丰富。
正解n 过大会让 n-gram 接近完整单词,失去子词共享的泛化能力;n 过小又引入大量噪音。实践中常用 minn=3、maxn=6,让模型同时拥有短片段和稍长片段的形态信息。
为何常见:直观上“信息越全越好”,但子词的价值恰恰在于小而可复用。
误区FastText 把字符 n-gram 向量平均后作为词向量,这样更平滑。
正解原版 FastText 使用的是求和,而不是平均。求和会保留向量尺度,平均会改变尺度。虽然余弦相似度不关心尺度,但训练时的内积打分关心,因此标准实现是 sum。
为何常见:很多教程在归一化时无意中把 sum 和 average 混用,读者便记住了“平均”这个错误印象。
面试题
Q1. FastText 子词嵌入最核心的改进是什么?
- A.使用字符 n-gram 拼出词向量← 正确答案
- B.使用全局共现矩阵降维
- C.使用句法树增强语义
- D.使用注意力机制加权
答案:A。FastText 的核心是用字符 n-gram 共享信息来生成词向量;全局共现矩阵是 GloVe 的思路,句法树和注意力机制与 FastText 原始设计无关。
Q2. 训练集里没出现过的 OOV 词,FastText 一般如何处理?
- A.直接忽略该词
- B.随机初始化一个向量
- C.累加其子词向量得到表示← 正确答案
- D.终止训练并报错
答案:C。FastText 把 OOV 词拆成字符 n-gram,再对见过的子词向量求和;直接忽略或随机初始化是 word2vec 常见做法,终止训练更不可能。
Q3. 给单词加 < > 边界符号的主要作用是?
- A.减少字符 n-gram 数量
- B.区分词首和词尾位置← 正确答案
- C.提高训练学习率
- D.降低向量维度
答案:B。边界符号让模型知道某个 n-gram 出现在词首还是词尾,比如 <fa 和 fa> 含义不同;它不会减少 n-gram 数量、改变学习率或降低维度。
Q4. 与 word2vec 相比,FastText 的参数规模通常?
- A.更少
- B.完全相等
- C.更多← 正确答案
- D.无法比较
答案:C。FastText 除了整词向量,还要为每个字符 n-gram 维护向量,所以参数通常比 word2vec 多;这也是它在低资源场景下换取泛化能力的代价。
Q5. 设 n=3,对 "fast" 加边界后,字符 3-gram 的个数是?(不含整词)
- A.2 个
- B.3 个
- C.4 个← 正确答案
- D.5 个
答案:C。加边界后字符串为 <fast>,长度为 6,滑动窗口能取出 6-3+1=4 个 3-gram,即 <fa、fas、ast、st>。
Q6. 负采样训练时,负样本的期望输出标签是多少?
- A.0← 正确答案
- B.1
- C.-1
- D.0.5
答案:A。正样本期望 sigmoid 输出接近 1,负样本是随机采样的噪声词,期望输出接近 0;所以负样本梯度中的目标值是 0,而不是 1 或 -1。
Q7. FastText 模型整体上属于以下哪类框架?
- A.Skip-gram 变体← 正确答案
- B.LSTM 循环网络
- C.Transformer 编码器
- D.卷积分类器
答案:A。FastText 使用上下文预测中心词或中心词预测上下文的思路,本质上是 skip-gram 的扩展,只是把输入改成了子词向量求和。
Q8. 哪些场景更适合使用 FastText?
- A.低资源语言语料← 正确答案
- B.形态丰富的语言← 正确答案
- C.严格封闭的固定词表
- D.存在大量拼写变体← 正确答案
答案:ABD。低资源、形态丰富、拼写变体多时,子词复用能显著提升覆盖率;严格封闭的固定词表恰恰是 word2vec 的舒适区,不需要 FastText 的额外开销。
Q9. 关于字符 n-gram 长度的选择,正确的说法有?
- A.常用范围是 3 到 6← 正确答案
- B.子词集合中仍保留整词← 正确答案
- C.n 越大泛化能力一定越强
- D.n 过小容易引入噪声← 正确答案
答案:ABD。minn=3、maxn=6 是常见配置;集合中保留整词是固定设计。n 过大反而接近整词,削弱泛化,因此“越大越强”是错的。
Q10. FastText 工程落地时,下列哪些做法合理?
- A.设置 minn 和 maxn 控制子词范围← 正确答案
- B.构建封闭词表并丢弃所有子词
- C.对超大子词表做剪枝或哈希压缩← 正确答案
- D.完全删除整词向量以节约内存
答案:AC。minn/maxn 控制子词粒度,剪枝或哈希能缓解内存压力;丢弃子词或整词都违背 FastText 的设计,会让模型退化为普通词向量或丧失泛化能力。