对应:Stanford CS224N Winter 2026 Lecture 2(Word Vectors);Winter 2025 Lecture 2(Word Vectors and Language Models);Manning 主讲的 Winter 2021 / 2023 版第 2 讲(录像在 YouTube 上)。 配套代码:
code/lecture02/(会导入第 1 讲的玩具语料和工具,请保持code/下的目录结构不变) 预计阅读时间:4–5 小时(第一遍通读 2 小时,第二遍推导公式、对照代码 2–3 小时)
0. 本讲导航
0.1 学完本讲,你应该能够
- 解释朴素 softmax 为什么慢,以及负采样如何把” 选 1 的多分类”换成”若干个二分类”。
- 写出负采样的损失函数,独立推导它对 、、 的梯度。
- 说明噪声分布为什么取词频的 3/4 次方,高频词下采样和动态窗口各起什么作用,CBOW 和 skip-gram 有什么区别。
- 推导”SGNS 的最优解满足 “,理解计数和预测两条路线的联系。
- 从”共现概率的比值”出发推出 GloVe 的目标函数,解释加权函数 的三个设计要求。
- 区分内在评估和外在评估,会算 Spearman 相关系数,知道评估中的常见陷阱(随机波动、天花板效应、指标测的东西不同)。
- 解释一词多义的向量为什么是各义项的”线性叠加”,以及怎样通过聚类上下文把义项分开。
- 解释 fastText 如何用字符 n-gram 处理没见过的词。
0.2 先修知识
| 知识点 | 用在哪里 | 最低要求 |
|---|---|---|
| 第 1 讲 | 全讲 | skip-gram、朴素 softmax 的梯度、PPMI、SVD、梯度检验 |
| 求导与链式法则 | 第 3、5、6 节 | 第 1 讲第 10.1 节的四条规则 |
| 对数与指数 | 第 3、5、6 节 | , |
| Python / numpy | 配套代码 | 第 1 讲代码里出现过的写法;新写法(np.einsum 等)注释里会讲 |
0.3 本讲地图
第1节 回顾:第 1 讲留下的四个问题
第2节 优化回顾:梯度下降、SGD 与稀疏更新
第3节 负采样:把多分类换成二分类 ← 代码 01(本讲重点之一)
第4节 word2vec 的其他组成部分:CBOW、下采样、动态窗口 ← 代码 02
第5节 计数与预测的统一:SGNS 在隐式地分解 PMI 矩阵 ← 代码 02
第6节 GloVe:从共现概率的比值出发 ← 代码 03(本讲重点之二)
第7节 怎么评估词向量? ← 代码 04
第8节 一词多义 ← 代码 05
第9节 子词与未登录词:fastText ← 代码 05
第10–14节 小结、与作业的关系、练习、延伸阅读、下一讲预告
本讲的主线是:在第 1 讲的基础上,让词向量训练得更快(第 3–4 节)、理解得更深(第 5–6 节)、评估得更可靠(第 7 节),并正视”一个词一个向量”的局限(第 8–9 节)。
1. 回顾:第 1 讲留下的四个问题
第 1 讲用最朴素的 softmax 训练了 skip-gram:
在玩具语料上它工作得很好,但留下了四个问题:
- 太慢:分母要对整个词表求和,每个样本的代价是 ;而且 的每一行都非零,每一步都要更新整个 。第 1 讲代码 04 估算过,词表 300 万时单线程训练要好几年。
- 两条路线是什么关系?计数方法(PPMI + SVD)和预测方法(word2vec)看起来完全不同,结果却很相似。是巧合吗?
- 怎么知道词向量好不好?类比准确率可靠吗?
- 一个词一个向量:多义词怎么办?没见过的词怎么办?
本讲依次回答这四个问题。
2. 优化回顾:梯度下降、SGD 与稀疏更新
第 1 讲第 11 节已经介绍过梯度下降和随机梯度下降(SGD),这里只强调三点,它们是理解后面内容的前提。
(1) 梯度下降的更新规则
是在整个语料上的平均损失。语料有几十亿个窗口,算一次完整的梯度太贵。
(2) SGD:每次只用一个(或一小批)窗口
用一个窗口的梯度来近似整体梯度,立即更新参数。它是有噪声的估计,但便宜得多,可以做成千上万倍多的更新。
(3) 词向量的梯度是稀疏的
一个窗口里最多只有 个不同的词。对中心词向量矩阵来说,只有中心词那一行有梯度:
所以实现时只更新真正出现的那几行,不要每一步都对整个矩阵做运算。深度学习框架里的”稀疏嵌入更新”做的就是这件事。
问题在于:对朴素 softmax 来说,上下文向量矩阵 的梯度不是稀疏的——分母里每个词都出现了,每一行都有梯度。负采样正是要把它变成稀疏的。
3. 负采样:把多分类换成二分类
3.1 动机
朴素 softmax 慢,根源在分母 :为了让概率加起来等于 1,必须和词表里每个词比较。有两种常见的绕过办法:
- 层次 softmax(hierarchical softmax):把词表组织成一棵二叉树(word2vec 用的是按词频构造的哈夫曼树),每个词是一片叶子。“预测词 ” 变成”从根走到 那片叶子”,每一步是一个二选一。代价从 降到 。
- 负采样(negative sampling):干脆不要求输出一个归一化的概率分布,换一个更简单的问题来问。word2vec 论文发现它更简单、效果也更好,这是今天最常用的做法。本讲重点讲它。
3.2 思路:真的还是假的?
负采样问的问题是:
给你一对词 ,它是从语料里真实抽出来的,还是随机拼凑的?
对语料中每个真实的 (中心词 , 上下文词 ),我们再从词表中随机抽 个词 (叫负例,negative samples),构成 个”假”的词对 。模型的任务是:让真实词对的得分高,假词对的得分低。
这是一个二分类问题:每个词对是”真”(标签 1)或”假”(标签 0)。二分类的标准工具是 sigmoid 函数:
它把任意实数压到 之间,可以当作概率。我们定义”词对 是真的”的概率为:
点积越大,越可能是真的。
动手:代码
01_negative_sampling.py演示 1 打印了 sigmoid 的几个值:,,; 时也不会溢出(代码用了数值稳定的写法)。并且对每个 都有 。
最后这条性质很有用:词对是假的概率 可以写成 。
3.3 目标函数
对一个真实词对 和 个负例,我们希望:
- 真实词对被判为”真”:最大化 ;
- 每个负例被判为”假”:最大化 。
取负号变成要最小化的损失:
这就是 skip-gram 负采样(Skip-Gram with Negative Sampling,SGNS)的损失。和朴素 softmax 对比:
| 朴素 softmax | 负采样 | |||
|---|---|---|---|---|
| 问的问题 | ”$ | V | $ 个词里哪个是上下文?” | “这一对是真的还是假的?“(问 次) |
| 每个样本要算的点积 | $ | V | $ 个 | 个 |
| 中有梯度的行 | 全部 $ | V | $ 行 | 行 |
| 输出是归一化的概率吗 | 是 | 否(只是”真/假”的打分) |
通常取多少?word2vec 论文建议:小语料 5 ~ 20,大语料 2 ~ 5 就够了。
3.4 负例从哪里抽:噪声分布
负例按什么概率抽?这个分布叫噪声分布 。最自然的两种选择:
- 按词频(unigram 分布):。高频词(the、a)会被反复抽到;
- 均匀分布:。罕见词和高频词被抽到的机会一样。
word2vec 的选择介于两者之间:
3/4 次方让高频词的比例下降、罕见词的比例上升。
动手:代码 01 演示 2 在玩具语料上比较了三种噪声分布:
词 出现次数 按词频 3/4 次方 均匀 the 4810 0.1728 0.0833 0.0058 cat 252 0.0091 0.0091 0.0058 paris 78 0.0028 0.0038 0.0058 最常见的 5 个词占全部负例的比例:按词频 31.5%,3/4 次方 18.2%,均匀 2.9%。
为什么不按词频? 按词频时 the 一个词就占了 17% 的负例,模型会花大量力气学”cat 旁边不是 the”——可 the 恰恰经常出现在 cat 旁边,这些负例既浪费又有误导性。为什么不均匀? 均匀时大多数负例都是罕见词,和中心词毫不相干,太”容易”区分了,学不到多少东西。3/4 是论文的经验选择,没有严格的理论依据,但在很多任务上效果最好。
3.5 推导梯度
先准备两个关于 sigmoid 的导数(练习 1 会让你自己推):
记正例得分 、负例得分 。损失是
第 1 步:损失对每个得分的导数。
- 对 :直接用上面的公式,。它是负数:增大 会降低损失。
- 对 : 对 求导,链式法则多出一个 :。它是正数:减小 会降低损失。
第 2 步:链式法则。 因为 ,有 、。于是:
的其他行梯度都是 0。
直觉:和第 1 讲的”期望 − 观测”非常像。
- 正例:系数 ,梯度下降把 拉向 (也把 拉向 );模型越不确定( 越小),拉得越狠。
- 负例:系数 ,把 推离 ;模型越是”误以为”它是真的( 越大),推得越狠。
- 当模型完全正确(、)时,梯度趋于 0。
一个边界情况:如果同一个词被抽中两次作为负例,它的梯度要累加两次(代码里用 np.add.at)。如果负例恰好就是 本身,它既被拉近又被推开,梯度部分抵消——数学上没错,只是浪费了一个负例。原始 word2vec 会跳过这种负例。
动手:代码 01 演示 3 在一个 6 个词、3 维的例子里逐项打印了损失和梯度:中心词 cat、真实上下文词 sat、负例 car、the、mat。只有 sat、car、the、mat 这 4 行 有梯度,on 那一行完全没被碰到。演示 4 对”普通情况""负例有重复""负例恰好是正例”三种情况做了梯度检验,相对误差都在 量级。
3.6 代价
每个样本只需要 个点积,更新 行 和 1 行 ,代价是 ,与词表大小无关。
动手:代码 01 演示 6 比较了单个样本的耗时(,)。在编写讲义的电脑上, 从 1000 增加到 10 万,朴素 softmax 从约 0.04 毫秒涨到约 10 毫秒,负采样一直是约 0.015 毫秒; 万时负采样快了约 600 倍(具体数字取决于你的电脑,但”一个线性增长、一个不变”这一点不会变)。
3.7 背景:负采样从哪里来
负采样是噪声对比估计(Noise Contrastive Estimation,NCE;Gutmann & Hyvärinen,2010)的简化版。NCE 的想法是:要估计一个不想算归一化常数的概率模型,可以训练一个分类器来区分”真实数据”和”人为制造的噪声”;只要噪声足够多,分类器学到的东西就等价于原来的概率模型。Mnih 和 Teh(2012)把 NCE 用到了神经语言模型上,word2vec 又把它进一步简化成了负采样。
代价是:负采样学到的 不是一个归一化的条件概率。但我们本来要的就只是词向量,而不是概率,所以这并不重要。
NLP 联系:“让正例对得分高、负例对得分低”的思想后来演变成了对比学习(contrastive learning),是第 12 讲的稠密检索、第 18 讲的 CLIP 等方法的核心。
4. word2vec 的其他组成部分
4.1 CBOW:反过来预测
word2vec 有两个模型:
- skip-gram:用中心词预测每个上下文词(第 1 讲和本讲到目前为止讲的);
- CBOW(Continuous Bag of Words,连续词袋):用上下文词的平均向量预测中心词。
CBOW 的做法是:
损失和 SGNS 一样,只是把 换成了上下文向量的平均 、把要预测的词换成了中心词 。因为 是平均值,每个上下文词向量得到 的梯度。
“词袋”(bag of words)的意思是:上下文词的顺序被忽略了,只是放进一个”袋子”里求平均。
两者怎么选? CBOW 每个位置只产生 1 个训练样本(skip-gram 产生 个),所以训练更快;但它把上下文平均掉了,word2vec 论文的经验是 skip-gram 对罕见词学得更好、在语义类比上更强。
4.2 高频词下采样
the、a、is 这些词出现得极其频繁,但它们作为上下文提供的信息很少(“cat 旁边有 the”几乎说明不了什么)。word2vec 的做法是下采样(subsampling):在训练前,以一定概率丢掉语料中的每个词,词频越高丢得越多:
其中 是 在语料中的占比, 是一个阈值(论文对几十亿词的语料推荐 左右)。词频不超过 的词永远保留。
为什么开平方? 如果按 的比例保留,所有高频词在下采样之后的出现次数都会变得一样多,“谁更常见”的信息被完全抹掉;开平方只是”压一压”,保留了高低顺序。
下采样有两个好处:加速(语料变短了),以及让有信息量的上下文占更大比重(丢掉了 the,窗口实际上覆盖了更远的内容词)。
动手:代码
02_sgns_training.py演示 1。我们的玩具语料只有 171 个词,每个词的占比都很大,所以用 :the(占 17.28%)保留概率 0.24,a 0.44,queen 0.94,cat、paris 1.00。一次下采样后,27,836 个词还剩约 22,092 个(79%),被丢掉的几乎全是虚词。
4.3 动态窗口
原始 word2vec 的窗口大小不是固定的:对每个中心词,窗口半径在 之间随机取。效果是:距离为 1 的上下文总会被取到,距离为 的只有 的概率被取到——相当于离中心词越近的上下文权重越大。第 6 节的 GloVe 用”1/距离”给共现加权,是同一个想法。
4.4 训练结果
动手:代码 02 演示 2 用 SGNS(,,下采样 + 动态窗口,20 轮)训练玩具语料,在编写讲义的电脑上约 1.4 秒。最近邻和第 1 讲一样合理(cat → horse、sheep、dog、cow;paris → berlin、rome、tokyo、beijing)。
演示 3 把 8 种设置各跑了 3 个随机种子:
方法 区分度 类比(3 次平均) 最低 ~ 最高 第 1 讲:朴素 softmax(15 轮) 1.028 60.4% 58.3% ~ 61.5% SGNS(下采样 + 动态窗口) 1.100 56.2% 50.0% ~ 61.5% SGNS(不下采样) 1.060 53.5% 50.0% ~ 58.3% SGNS(固定窗口) 1.108 60.4% 55.2% ~ 63.5% SGNS(不下采样 + 固定窗口) 1.055 60.8% 54.2% ~ 66.7% SGNS(噪声分布 power = 1) 1.107 53.5% 53.1% ~ 54.2% SGNS(噪声分布 power = 0) 1.094 57.3% 51.0% ~ 60.4% CBOW(负采样) 1.082 41.7% 35.4% ~ 45.8%
几点观察:
- SGNS 和朴素 softmax 的效果相当,所有变体的类别纯度都是 1.000,区分度都在 1.0 以上。负采样用很小的代价换来了几乎同样好的词向量。
- 各种技巧的差别大多在随机波动范围内。注意”最低 ~ 最高”那一列:只换一个随机种子,类比准确率就能相差 10 个百分点左右。96 道题太少,单次运行的差异很大一部分是噪声。在这份玩具语料上,下采样和动态窗口没有带来可见的提升——它们在论文里的主要好处是加速和改善罕见词,这两点在只有 171 个词的语料上都体现不出来。
- CBOW 在类比上明显差一些(41.7%),和论文”skip-gram 语义类比更强”的经验一致。
- 在 171 个词的玩具词表上,朴素 softmax 并不慢;负采样的速度优势要在词表很大时才能体现(第 3.6 节)。
一个重要的方法论教训:比较两种方法时,一定要多跑几个随机种子,看差别是否超出了波动范围。很多论文里”提升 1 个点”的结论,在换个种子之后就消失了。第 7.6 节会再谈。
5. 计数与预测的统一
5.1 两条路线的对比
第 1 讲介绍了两条路线。Manning 在课上做过一个对比:
| 计数方法(LSA、PPMI + SVD……) | 预测方法(word2vec……) | |
|---|---|---|
| 训练速度 | 快(数一遍 + 一次分解) | 随语料规模增长 |
| 对统计信息的利用 | 高效:每个共现都被计入全局矩阵 | 低效:一个窗口一个窗口地看,同样的共现要反复见到 |
| 主要用途 | 早期主要用来衡量词的相似度 | 在很多下游任务上表现更好,能捕捉相似度以外的复杂规律(如类比) |
| 问题 | 高频词的计数过大,需要很多加权技巧 | 需要调的超参数较多 |
两条路线各有优缺点,但它们真的是两回事吗?
5.2 SGNS 在隐式地分解 PMI 矩阵
Levy 和 Goldberg(2014)给出了一个漂亮的结论:SGNS 的最优解满足
也就是说,SGNS 实际上是在把一个 “PMI 减去常数 ” 的矩阵分解成 ——这正是计数路线在做的事!
推导(假设向量维度足够大,每个点积都可以独立取值)。记 。在整个语料上,词对 作为正例出现了 次;中心词 一共出现了 次,每次配 个负例,其中是 的期望次数为 。所以所有和 有关的损失项加起来是:
对 求导并令其为 0(用第 3.5 节的两个导数):
注意 ,整理得
而 。两边取对数:
如果噪声分布就是词频分布 ( 是总词对数),右边第一项正是 。证毕。
(如果用 3/4 次方的噪声分布,PMI 的分母里 要换成平滑后的版本——这也正是第 1 讲代码里提到的”PPMI 的 0.75 次方平滑”的来源。)
这个结论的意义:
- 两条路线殊途同归:都是在给 PMI(或它的某种变体)矩阵找一个低秩近似。
- 它解释了 SGNS 中各个超参数的作用: 越大,目标矩阵整体下移得越多;噪声分布的指数相当于对 PMI 做平滑。
- Levy、Goldberg 和 Dagan(2015)进一步发现:把 word2vec 的各种工程技巧(下采样、动态窗口、0.75 次方平滑等)移植到 PPMI + SVD 上,两者的差距大幅缩小。很多”预测方法更好”的结论,其实来自这些技巧,而不是来自”预测”本身。
动手:代码 02 演示 4 在玩具语料上直接检验这个关系。为了贴近推导的设定,用固定窗口、不下采样、按词频的噪声分布训练 SGNS,再对出现至少 20 次的 910 个词对比较 和 :
维度 相关系数 点积的平均值 的平均值 10 0.959 +0.23 +0.39 50 0.975 +0.35 +0.39 相关性非常强;维度越大,模型越能精确地表示这个矩阵,点积也越接近理论值。(维度有限时只能做低秩近似,所以不会完全相等。)
6. GloVe:从共现概率的比值出发
6.1 出发点:比值才有意义
2014 年,斯坦福的 Pennington、Socher 和 Manning 提出了 GloVe(Global Vectors)。它的出发点是一个简单而深刻的观察:单个共现概率说明不了多少,但两个共现概率的比值很有意义。
记 ,其中 是词 出现在词 窗口内的次数,。论文用 ice(冰)和 steam(水蒸气)举例(数据来自一个 60 亿词的语料):
| 探针词 | solid(固体) | gas(气体) | water(水) | fashion(时尚) |
|---|---|---|---|---|
| 比值 | 8.9 | 0.085 | 1.36 | 0.96 |
单看 比 大得多,但这只是因为 water 本身就是常见词,并不说明 water 比 solid 更能刻画 ice。比值则把这种”常见程度”抵消掉了:
- 只和 ice 相关的词(solid):比值远大于 1;
- 只和 steam 相关的词(gas):比值远小于 1;
- 和两者都相关(water)或都无关(fashion)的词:比值接近 1。
“ice 和 steam 的意义差别”恰好体现在比值偏离 1 的那些探针词上。
动手:代码
03_glove.py演示 1 在玩具语料上复刻了这张表(窗口 2,按 1/距离加权):
探针词 比值 his 0.0309 0.0000 ∞ her 0.0000 0.0305 0 royal 0.0216 0.0186 1.16 rules 0.0302 0.0332 0.91 the 0.4360 0.4436 0.98 his、her 只和其中一个相关,比值是无穷大或 0;royal、rules(王室的上下文)和两者都相关,the 和谁都相关,比值都接近 1。区分 king 和 queen 的信息,全在性别相关的探针词上。
6.2 从比值推出模型
我们希望词向量能编码这些比值。最直接的想法是:让两个词向量的差与探针词向量的点积,等于比值的对数:
( 是作为”中心词”时的向量, 是作为”上下文/探针词”时的向量,和 word2vec 的 、 类似。)这个式子对任意 都成立的一个充分条件是:
右边的 只和 有关、和 无关,可以吸收到一个偏置 里;为了让 和 的地位对称(共现矩阵是对称的),再给 也加一个偏置 :
这就是 GloVe 的核心:用两个向量的点积(加上偏置)去拟合共现次数的对数。这种”对数 = 双线性函数”的模型叫对数双线性模型(log-bilinear model)。
为什么这样能产生类比?如果 和 与所有探针词的点积都相同(因为两对词与探针词的共现比值相同),那么这两个差向量就几乎相等——这正是 king − queen ≈ man − woman。类比结构来自共现概率比值的一致性。
6.3 加权最小二乘
把上面的等式变成一个要最小化的目标函数:
只对 的格子求和( 无定义)。 是一个加权函数,论文提出了三个要求:
- :没共现过的词对不参与;
- 单调不减:共现越多,估计越可靠,权重越大;
- 有上限:the 和 cat 共现几百次,不能让这一项把其他项都淹没。
满足这三点的一个简单选择是:
( 又出现了——和 word2vec 噪声分布的指数一样,纯属经验上的巧合。)
动手:代码 03 演示 2 打印了 的值:,,,。玩具语料的共现矩阵有 2222 个非零元素(占 7.6%),其中只有 112 个 。
训练:GloVe 只需要遍历非零元素,计算量和非零元素个数成正比(而不是 ),所以比 SVD 高效得多。论文用 AdaGrad 优化:每个参数有自己的学习率 ,经常被更新的参数(高频词)学习率很快变小,很少被更新的(罕见词)保持较大的学习率。最后用 作为词向量:因为共现矩阵对称, 和 地位完全相同,只是随机初始化不同,相加相当于两个模型取平均。
6.4 一个意外:在玩具语料上,原版 GloVe 不好用
动手:代码 03 演示 3 按论文原样训练 GloVe(,AdaGrad,100 轮,在编写讲义的电脑上约 0.1 秒)。在非零元素上,模型预测值 与 的相关系数高达 0.953——拟合得很好。可是词向量却很差:类别纯度只有 0.650,异类平均余弦高达 +0.842,cat 的最近邻里混进了 apple(0.96)和 banana(0.96)。
拟合得好,为什么词向量差? 演示 4 做了诊断:
- cat 有 26 个不同的上下文词,apple 有 28 个,两者共有 13 个:the、a、is、saw、big、small 等。
- GloVe 的损失只对 的格子求和。对 cat 来说,它只被要求”和 the、fed、sleeping……的点积要对”;至于 cat 和 sweet、drove 的点积是多少,损失函数根本不管。
- 而 cat 和 apple 共有的上下文恰恰是次数最多、权重最大的格子,于是”让 cat 和 apple 很像”完全不会受到惩罚。
换句话说,GloVe 看不到”cat 从来不和 sweet 一起出现”这条信息。而 word2vec 的负例、PPMI 矩阵里的 0,提供的正是这种”它们不该一起出现”的负信息。
修复(这是我们为玩具语料做的改动,不是论文的做法):给所有格子加一个很小的平滑计数 。原本为 0 的格子变成 0.1,目标值 很低,模型必须让这些词对的点积很小。
动手:代码 03 演示 4 加平滑后重新训练(29,241 个格子全部参与,约 1.5 秒):类别纯度 1.000,异类平均余弦 −0.087,区分度 1.051。cat 的邻居变回了 dog、horse、sheep、pig。演示 5 用 3 个随机种子做了对比:
方法 类别纯度 区分度 类比(3 次平均) PPMI + SVD(无随机性) 1.000 0.834 31.2% SGNS(代码 02 默认设置) 1.000 1.100 56.2% GloVe(论文原样,只用非零元素) 0.702 0.158 25.0% GloVe(平滑 ) 1.000 1.037 46.9% GloVe(平滑,不加权 ) 1.000 1.072 55.2% GloVe(平滑,) 1.000 1.074 47.9%
为什么论文不需要平滑? 在几十亿词的真实语料里,常见词之间几乎都共现过,“从没共现”的大多是罕见词对,这些 0 本身就很不可靠,忽略它们是合理的;而且海量的非零共现已经间接提供了足够的区分信息。我们的玩具语料由模板生成,不同类别的上下文几乎不重叠,0 反而携带了关键信息。后来也有工作(例如 2016 年的 Swivel)专门研究如何利用”未观测到的共现”。
另外,在平滑后的玩具语料上,加权函数没有带来好处(不加权的版本类比反而最高,不过波动范围也很大)。论文在大规模语料上的结论是加权很重要——这再次提醒我们,玩具实验说明不了”在真实数据上哪个设置更好”。
这一节真正想说明的是:词向量需要负信息。 word2vec 用负例提供,PPMI + SVD 用矩阵里的 0 提供,GloVe 在大语料上靠海量的非零共现间接获得。
6.5 GloVe 的意义
GloVe 论文报告它在类比、词相似度和命名实体识别上都优于当时的 word2vec,训练也很快。它把计数路线(“利用全局统计”)和预测路线(“学习低维向量、用 SGD 类方法训练”)结合了起来。斯坦福发布的 GloVe 预训练词向量(在维基百科、Common Crawl、Twitter 等语料上训练)被广泛使用,CS224N 作业 1 的第二部分用的就是它。
不过,正如第 5.2 节提到的,Levy、Goldberg 和 Dagan(2015)发现,把各种方法的超参数和技巧调到同等程度后,它们的差距很小。选哪种方法,远不如选好超参数和语料重要。
7. 怎么评估词向量?
7.1 内在评估 vs 外在评估
NLP 中评估任何表示(词向量、句子向量、语言模型的隐藏状态……)通常有两类方法:
| 内在评估(intrinsic) | 外在评估(extrinsic) | |
|---|---|---|
| 做法 | 在一个专门设计的小任务上直接测试表示本身 | 把表示用到一个真正的下游任务里,看任务效果 |
| 例子 | 类比、词相似度 | 命名实体识别、情感分类、问答 |
| 优点 | 快、便宜,帮助理解表示学到了什么 | 衡量的是真正关心的东西 |
| 缺点 | 不清楚它和真实用处有多大关系 | 慢;任务效果受很多因素影响,很难归因到表示本身 |
7.2 内在评估一:类比
第 1 讲已经用过类比。Mikolov 等人发布的 Google 类比测试集有 19,544 道题,分成语义类(国家—首都、国家—货币、家庭关系……)和句法类(形容词—副词、单复数、动词时态……)。评估方法和第 1 讲一样:计算 ,找余弦最大的词(排除三个输入词)。
GloVe 论文报告的几个趋势(在真实语料上):维度增加时类比准确率先上升、到几百维后趋于饱和;句法类比偏好小窗口、语义类比偏好大窗口;语料越大越好,而且语料来源有影响(维基百科在语义类比上优于同等规模的新闻语料)。
类比评估的问题在第 1 讲第 12.3 节已经提过:排除输入词的惯例会让结果看起来更好;而且很多类比关系是一对多的(“国家—城市”有很多正确答案),只算第一名会低估。
7.3 内在评估二:词相似度
另一种内在评估是词相似度:请很多人给一组词对打分(比如 0 ~ 10 分),然后看模型的余弦相似度和人工评分是否一致。常用的数据集有:
- WordSim-353:353 个词对,例如 tiger–cat 7.35,tiger–tiger 10,book–paper 7.46,computer–internet 7.58,stock–jaguar 0.92 等。它把”相似”和”相关”混在一起;
- SimLex-999:999 个词对,只衡量相似(similarity),不衡量相关(relatedness):car–bike 高分,car–road 低分;
- MEN:3000 个词对。
比较时不看数值本身(模型的余弦是 0.9 还是 0.6,人给的是 9 分还是 7 分,单位都不一样),只看排序是否一致。用的指标是 Spearman 等级相关系数:先把两组数各自换成名次,再算名次之间的 Pearson 相关系数。它在 之间,1 表示排序完全一致。
动手:我们没有人工评分,代码
04_evaluation.py演示 1 按语料的设计规则模拟了一份”人工评分”(同类动物 7 分;人物词 3 分起,每多共享一个特征加 2 分;国家—首都 7 分……)。在所有 946 个内容词对上:
词向量 Spearman one-hot 无法排序(所有余弦都是 0) 原始共现计数(171 维) 0.426 PPMI + SVD(10 维) 0.741 SGNS(10 维) 0.742 GloVe,平滑(10 维) 0.741
7.4 外在评估:把词向量用到任务里
外在评估的典型例子是命名实体识别(NER):给句子中的每个词打上”人名 / 地名 / 机构名 / 其他”的标签。第 3 讲会搭一个完整的 NER 模型,那里会看到:用好的词向量初始化,模型在训练时没见过的人名、地名上也能表现不错。
动手:代码 04 演示 2 做了一个简化版的外在评估——小样本词分类:7 个类别,每类只给 2 个带标签的词训练一个 softmax 分类器,测试能否认出同类的其他词(20 次随机划分取平均):
特征 测试准确率 one-hot 13.3%(随机水平约 14%) 原始共现计数 / PPMI + SVD / SGNS / GloVe 100% one-hot 的测试词对应的维度在训练时从没出现过,分类器对它们一无所知——这就是第 1 讲代码 01 结尾说的”孤立符号无法泛化”。词向量则把同类的新词放在了训练词附近,只看 2 个例子就能认出整个类别。
7.5 超参数的影响
动手:代码 04 演示 3 对 SGNS 做了超参数扫描(每个设置 3 个随机种子取平均):
设置 类比 相似度 Spearman 小样本分类 18.4% 0.689 60.6% 39.6% 0.740 100.0% 56.2% 0.740 100.0% 83.3% 0.730 98.7% 85.8% 0.727 98.5% 窗口 1 42.0% 0.736 100.0% 窗口 4 58.0% 0.740 100.0% 1000 句 25.7% 0.709 92.7% 3000 句 38.5% 0.741 100.0% 12000 句 59.7% 0.740 100.0% (窗口 2、6000 句、 的默认设置:56.2% / 0.740 / 100.0%。)
几个趋势:维度和语料规模对类比影响很大( 从 10 到 25,类比从 56% 跳到 83%);而相似度和小样本分类在 时就已经饱和了。
7.6 评估中的三个陷阱
陷阱一:不同的评估测的是不同的东西。
动手:代码 04 演示 4 在上面 12 组设置上计算指标之间的 Spearman 相关:相似度评估与小样本分类的相关是 0.846,类比与小样本分类的相关只有 0.168。
原因是:相似度和小样本分类主要都由”大类分得清不清楚”决定;而类比考的是类别内部的细微关系(男/女、国家/首都)。 从 10 增加到 50,类比大幅提高,小样本分类却没有变化。所以选评估方法时,要先问自己”我关心的是哪种性质”。
陷阱二:天花板效应。 很多设置的小样本分类都是 100%:任务太容易,就分不出好坏。第 13 讲会看到,NLP 的基准测试也常常因为被”刷满”而失去区分能力。
陷阱三:随机波动。 第 4.4 节已经看到,只换一个随机种子,类比准确率就能相差 10 个百分点。在小测试集上,“提升 2 个点”很可能只是噪声。可靠的做法是多跑几个种子,报告平均值和波动范围(第 8 讲会讲置信区间)。
8. 一词多义
8.1 问题
大多数常用词都有多个意思。以英语的 pike 为例,它可以指:长矛、一种鱼(狗鱼)、收费公路(turnpike 的简称)、“快速移动”……中文里”打”字的义项更是多达几十个(打人、打电话、打酱油、打毛衣……)。
word2vec 和 GloVe 给每个词形只分配一个向量。那么:
- 这个向量是什么样的?
- 能不能把不同义项分开?
8.2 一个向量 ≈ 各义项的加权和
Arora 等人(2018)从理论和实验两方面论证了:多义词的向量近似是各义项向量的线性叠加,权重是各义项的出现频率:
更令人惊讶的是,因为高维空间中的向量往往是”稀疏”地组合起来的,他们用稀疏编码(sparse coding)还能从这个混合向量中把各个义项分离出来。
动手:代码
05_senses_and_subwords.py演示 1 用经典的伪词技巧做实验:把语料中所有的 cat 和 train 都替换成同一个”词” cattrain,人为制造一个有两个义项(动物 / 交通工具)的词,而且我们知道它每次出现的真实义项。cattrain 一共出现 413 次,其中 252 次原本是 cat(61%)、161 次原本是 train(39%)。训练 SGNS 后,cattrain 的最近邻是 cow 0.95、pig 0.94、horse 0.93、dog 0.91、sheep 0.91,然后是 car 0.75、bus 0.72、taxi 0.69:既像动物、又有点像交通工具,偏向出现更多的动物义项。用最小二乘把它拟合成”动物类中心”和”交通工具类中心”的组合 ,3 个随机种子的 分别是 0.69、0.66、0.67,平均 0.67,接近动物义项的实际占比 0.61。
8.3 把义项分开:聚类上下文
既然一个词的不同义项出现在不同的上下文里,一个自然的想法是:把这个词每次出现的上下文聚类,每一类对应一个义项,再给每一类单独学一个向量。这就是 Huang 等人(2012)的多原型(multi-prototype)词向量。
动手:代码 05 演示 2 为 cattrain 的每次出现计算一个”上下文向量”(窗口内上下文词向量的平均),用 k-means 聚成 2 类,看和真实义项的一致率:
上下文向量的算法 一致率 基线:全部猜成 cat 61.0% 上下文词向量的等权平均 64.6% 按 1/√词频 加权平均 85.0% 加权后,一类 294 次出现(82% 原本是 cat),类中心最接近 horse、sheep、cow、dog;另一类 119 次出现(只有 8% 是 cat),类中心最接近 bus、bike、car、taxi。
等权平均为什么几乎没用?因为 the、a、is 这些虚词在每次出现的上下文里都有,把真正有区分作用的词(fed、sleeping、drove、station)冲淡了。按 1/√词频 加权(越常见的词权重越小)和第 4.2 节的下采样、信息检索中的 IDF 是同一个思想。分错的大多是两类共用模板的句子(“i saw the cattrain”、“the cattrain is fast”),连人也判断不了义项。
8.4 从静态向量到上下文相关的表示
多原型方法需要事先决定每个词有几个义项,而且义项之间的边界常常是模糊的(“paper”的”纸”和”论文”有联系)。真正解决这个问题的是 2018 年起的上下文相关表示:ELMo、BERT、GPT 等模型根据句子为每一次出现计算一个不同的向量(第 7–9 讲)。在那里,“义项”不再需要被离散地划分出来。
9. 子词与未登录词:fastText
9.1 问题
词表是有限的。训练语料里没出现过(或出现次数低于 min_count)的词,叫未登录词(out-of-vocabulary,OOV),word2vec 和 GloVe 无法给它们向量。这在以下情况特别严重:
- 形态丰富的语言:芬兰语、土耳其语的一个动词可以有成百上千种词形,绝大多数在语料里见不到几次;
- 新词、专有名词、拼写错误:bananna、covid19、某个新产品名……
- 即使是英语,复数、时态、派生词(happy → unhappiness)也让词表膨胀,而且每个词形都要单独学。
9.2 fastText:用字符 n-gram 表示词
Bojanowski 等人(2017)的 fastText 把一个词表示成它的字符 n-gram 的向量之和(实现中取平均)。先在词的两端加上边界符号 < 和 >,再取所有长度为 3 ~ 6 的片段:
where → <where> → 3-gram:<wh, whe, her, ere, re>
4-gram:<whe, wher, here, ere>
……再加上整个词 <where> 本身
边界符号让”前缀""后缀”和”词中间的片段”可以区分:3-gram her 和单词 <her> 是两个不同的东西。训练和 skip-gram 负采样完全相同,只是中心词向量换成了 n-gram 向量的平均,梯度平均分给每个 n-gram。
n-gram 的种类非常多,fastText 用哈希把它们映射到固定数量(默认 200 万个)的”桶”里,每个桶一个向量。这样任何字符串——包括训练时完全没见过的词——都能由它的 n-gram 组合出一个向量。
动手:代码 05 演示 3 在玩具语料上训练了一个 fastText 风格的模型(n-gram 长度 3 ~ 5,171 个词拆出 1554 个不同的 n-gram)。训练语料里从没出现过的词,它们的最近邻:
未登录词 最近邻(前 3) peaches peach 0.98,apple 0.97,banana 0.97 bikes bike 0.96,taxi 0.95,truck 0.94 horses horse 0.98,dog 0.98,sheep 0.97 princes prince 1.00,princess 0.99,queen 0.99 queens queen 1.00,king 1.00,princess 0.99 bananna(拼写错误) orange 1.00,banana 1.00,peach 0.99 germanic germany 0.99,spain 0.99,japan 0.99 大多数词的第一近邻就是它的原形,拼错的 bananna 的正确答案也排在前两名。
代价:拼写相近但意思无关的词也会被拉近(car 和 carpet 共享 <ca、car);很多词还共享 es>、<th 之类的常见片段,所以 fastText 向量整体更相似(代码 05 中 fastText 向量的异类平均余弦约 0.50,区分度降到 0.485);n-gram 的数量远多于词,参数也更多。fastText 在形态丰富的语言上收益最大。
9.3 从 fastText 到子词分词
fastText 只是在”词向量”这一层引入了子词信息,分词仍然是按词进行的。今天的大语言模型更进一步:直接把文本切成子词 token(BPE、WordPiece、Unigram 等,见 CS336 第 1 讲和本课第 16 讲),任何文本都能被表示,从根本上避免了”词表外”的问题。
10. 本讲小结
- 负采样:把” 选 1”的多分类换成”真词对 vs 个假词对”的二分类。损失 ,每个样本的代价从 降到 ,梯度只涉及 行 。
- 梯度:;正例被拉近、负例被推开,力度取决于模型错得有多离谱。
- 噪声分布取词频的 3/4 次方;高频词下采样丢掉大量虚词;动态窗口让近处的上下文权重更大;CBOW 用上下文平均预测中心词,更快但类比较弱。
- SGNS 在隐式地分解 矩阵:计数和预测两条路线殊途同归,很多”预测更好”的结论来自工程技巧和超参数。
- GloVe:从共现概率的比值出发,用 拟合 ,加权函数 压低罕见共现、给高频共现封顶;只用非零元素,训练高效。
- 词向量需要负信息:GloVe 只看非零元素,在上下文几乎不重叠的玩具语料上会失效;加一个很小的平滑计数就能修复。
- 评估:内在(类比、词相似度 + Spearman)vs 外在(下游任务)。三个陷阱:不同评估测不同的东西、天花板效应、随机波动。
- 一词多义:静态词向量 ≈ 各义项向量按频率的加权和;聚类上下文(并对高频词降权)可以把义项分开;根本的解决办法是上下文相关的表示(第 7–9 讲)。
- fastText:用字符 n-gram 向量的平均表示词,可以给未登录词、拼写错误、罕见词形构造向量。
11. 与作业的关系
作业 1 第二部分(Prediction-Based Word Vectors)使用斯坦福发布的预训练 GloVe 词向量,回答一系列探索性问题:
- 多义词:找一个多义词,看它的最近邻是否同时包含多个义项的近义词(第 8 节:向量是义项的线性叠加,最近邻往往只体现主要义项);
- 同义词与反义词:找出”反义词的余弦相似度比同义词还高”的例子,解释原因(反义词的上下文往往非常相似,hot 和 cold 都能填进 “the water is ___“;分布语义衡量的是”用法相似”,而不是”意义相同”);
- 类比:完成几道类比题,再找一道模型答错的,分析为什么;
- 偏见:用类比观察词向量中的性别等偏见,讨论偏见从何而来(第 1 讲第 13.2 节)。
作业 2 的书面部分(历年版本中常见)在第 1 讲朴素 softmax 梯度的基础上,要求推导负采样损失对 、、 的梯度,并说明负采样为什么比朴素 softmax 高效;还会讨论负例重复出现时梯度如何累加。本讲第 3.5 节和练习 2 就是为此准备的。
作业的具体题目以当年官方发布的版本为准。
12. 练习
练习 1(sigmoid 的导数)★:证明 (a) ;(b) ;(c) 。
参考答案
(a) ,链式法则:,因为 。 (b) 。 (c) 。
练习 2(负采样的梯度)★★:从 出发,推导 、、。如果某个词 在负例中出现了 2 次, 是什么?
参考答案
推导见第 3.5 节。结果:;;。
若 作为负例出现 2 次,损失中有两项 ,导数相加:。实现时要用 np.add.at 之类能累加重复下标的操作。
练习 3(代价): 万,,。分别估算朴素 softmax 和负采样处理一个样本需要多少次乘法(数量级即可)。
参考答案
朴素 softmax:算分数 、算 又是 、算 又是 ,约 。 负采样: 个点积和 6 行梯度,约 。相差约 50 万倍。
练习 4(噪声分布):两个词的出现次数分别是 10000 和 100。按词频采样时,前者被抽中的概率是后者的多少倍?按 3/4 次方呢?
参考答案
按词频:100 倍。按 3/4 次方: 倍。3/4 次方把高频词的优势从 100 倍压到了约 32 倍。
练习 5(下采样):,词 the 占语料的 5%。按 ,the 被保留的概率是多少?占比 的罕见词呢?
参考答案
the:,只保留约 1.4%。罕见词:,永远保留。
练习 6(SGNS 的最优解)★★:不看讲义,推导 SGNS 的最优点积 。推导中用到了什么假设?
参考答案
推导见第 5.2 节。假设:(1) 向量维度足够大,每个词对的点积可以独立取值(实际中只能做低秩近似);(2) 噪声分布是词频分布(用 3/4 次方时 PMI 中的 要换成平滑后的版本);(3) 只考虑期望意义下的目标,忽略采样的随机性。
练习 7(GloVe)★:(a) 为什么 GloVe 目标函数里要有偏置 、?(b) 如果去掉加权函数(所有 ),会发生什么?(c) 为什么 GloVe 只对 的格子求和?这在什么情况下会出问题?
参考答案
(a) 推导中 , 只和 有关,用偏置 吸收;为了让 、 对称再加上 。偏置让向量不必去编码”这个词有多常见”,可以专心编码意义。 (b) 大量只共现过一两次的词对( 的估计噪声很大)和高频词对权重一样,噪声会影响向量质量;同时高频词对(如 the 与其他词)数量虽少但不再封顶也不再被区别对待。论文报告加权在真实语料上很重要。(在我们的玩具语料上,加平滑后不加权反而略好,说明这种结论依赖数据。) (c) 因为 无定义。代价是模型看不到”从不一起出现”的负信息:如果不同类别的词的上下文几乎不重叠、而共享的上下文又都是高频虚词(我们的玩具语料就是这样),模型会把不同类别的词挤在一起(第 6.4 节)。
练习 8(Spearman):模型给 4 个词对的余弦相似度是 ,人工评分是 。计算 Spearman 相关系数。
参考答案
模型的名次:;人工的名次:。两组名次的差 ,。没有并列时可用公式 。
练习 9(线性叠加):一个伪词由词 A(出现 300 次)和词 B(出现 100 次)合并而成。按线性叠加理论,伪词向量中 A 义项的权重大约是多少?如果 A 和 B 的向量互相正交且长度相同,伪词向量与 A 的余弦相似度大约是多少?
参考答案
权重 ,。伪词向量 ,与 的余弦 。可见即使有 25% 的出现属于另一个义项,向量仍然很像主要义项——这也是为什么多义词的最近邻往往只体现主要义项。
练习 10(fastText):写出 apple 的所有字符 3-gram(含边界符号)。apple 和 apply 共享哪些 3-gram?
参考答案
<apple> 的 3-gram:<ap、app、ppl、ple、le>。<apply> 的 3-gram:<ap、app、ppl、ply、ly>。共享 <ap、app、ppl 三个。可以用代码 05 的 char_ngrams("apple", 3, 3) 验证。
练习 11(动手):
(a) 在代码 02 中把负例数 K 改成 1、15,类比准确率和训练时间如何变化?(记得多跑几个种子。)
(b) 在代码 03 中把平滑计数 0.1 改成 0.01 和 1.0,结果如何变化?
(c) 在代码 04 的 simulated_human_rating 中修改评分规则(比如让 cat–car 得 4 分),Spearman 会怎样变化?这说明了词相似度数据集的什么问题?
(d) 在代码 05 中把伪词换成两个频率更悬殊的词(比如 dog 和 taxi), 会怎样变化?
练习 12(思考)★:第 6.4 节说”GloVe 在玩具语料上需要平滑,在真实语料上不需要”。请从”负信息从哪里来”的角度解释:为什么真实大语料中的海量非零共现,也能间接起到”把不同类别的词推开”的作用?
参考答案
在真实语料中,一个词的上下文分布非常丰富:cat 会和几千个不同的词共现,apple 也是;虽然它们共享 the、a 等虚词,但各自还有大量不重叠的、次数可观的非零共现(cat 与 purr、meow、fur……;apple 与 juice、pie、tree……)。要同时拟合好这些非零格子,cat 和 apple 的向量必须指向不同的方向——非零格子之间的”差异”本身就提供了区分信息。而在玩具语料里,每个词只有二三十个上下文词,其中一半是共享的高频虚词且权重最大,不共享的部分信息太少,不足以把类别推开。
13. 延伸阅读
- Mikolov et al.,Distributed Representations of Words and Phrases and their Compositionality(2013):负采样、高频词下采样、短语向量。
- Goldberg & Levy,word2vec Explained: Deriving Mikolov et al.’s Negative-Sampling Word-Embedding Method(2014):几页纸的推导笔记,非常适合配合本讲第 3 节阅读。
- Levy & Goldberg,Neural Word Embedding as Implicit Matrix Factorization(2014);Levy, Goldberg & Dagan,Improving Distributional Similarity with Lessons Learned from Word Embeddings(2015)。
- Pennington, Socher & Manning,GloVe: Global Vectors for Word Representation(2014)。
- Gutmann & Hyvärinen,Noise-Contrastive Estimation(2010):负采样的理论源头。
- Arora, Li, Liang, Ma & Risteski,Linear Algebraic Structure of Word Senses, with Applications to Polysemy(2018)。
- Huang, Socher, Manning & Ng,Improving Word Representations via Global Context and Multiple Word Prototypes(2012)。
- Bojanowski, Grave, Joulin & Mikolov,Enriching Word Vectors with Subword Information(2017):fastText。
- Hill, Reichart & Korhonen,SimLex-999: Evaluating Semantic Models with (Genuine) Similarity Estimation(2015):相似 vs 相关。
- CS224N 课程笔记 Part II(Word Vectors II: GloVe, Evaluation and Training)。
14. 下一讲预告
到目前为止,我们只学会了”词怎么变成向量”。有了词向量之后,怎么用它们去完成真正的任务——比如判断一个词是不是人名?下一讲(神经网络基础与反向传播)会回答:
- 为什么简单的线性分类器不够用?神经网络和非线性激活函数带来了什么?
- 窗口分类:怎样用一个词和它周围的词向量,识别命名实体?
- 矩阵微积分:怎样系统地对向量、矩阵求导?
- 反向传播:深度学习框架是怎样自动算出所有梯度的?我们会从零写一个迷你的自动微分引擎。
- PyTorch 入门,以及训练神经网络的实用技巧(初始化、正则化、优化器)。
下一讲之前,建议你:跑通 code/lecture02/ 下的 5 个脚本;不看讲义推一遍负采样的梯度(练习 2)和 SGNS 的最优解(练习 6);思考练习 12。