负采样与对比学习——从 word2vec 到 CLIP
教科书版本的 word2vec 依赖一个 softmax 输出层,把模型的原始得分转成词表中每个词的概率——扩展性问题正是从这里来的。训练时,损失每次只用到每对样本的一个概率 P(target | center)。但 softmax 把它定义为整个词表总量中的一份:
P(target) = exp(score_target) / Σ_w exp(score_w)分母要对全部 V 个词求和,所以为了拿到你真正想要的那一个数,你必须算出全部 V 个得分——不是因为你需要它们,而是因为归一化项需要它们。少算任何一个得分,归一化就崩了。
当词表 V = 10⁶、嵌入维度 d = 300 时,把中心词与每个词打分是一次 V × d 的矩阵乘法——3 亿次乘加——外加为分母做的一百万次指数运算,而这只是一个训练对的开销。
为了感受这个量级,把每个样本的计算量与 MNIST 比一比:
INPUT HIDDEN OUTPUT OUTPUT-LAYER MATMUL
MNIST 784 128 10 128 × 10 = 1,280 ops
word2vec (V = 10⁶) V → 1 300 V 300 × 10⁶ = 300,000,000 opsMNIST 是一个 10 类分类问题;word2vec 则是百万类的——词表里每个词对应一个输出神经元。输出层里的一切开销——权重、矩阵乘法、softmax——都随这个类别数线性增长。
2013 年的后续论文提出负采样,作为层次 softmax 的替代方案。它将词表预测改为二分类:区分观测到的词与上下文词对,以及从噪声分布中抽取的词对。这改变了训练目标,并非仅仅近似 softmax 的分母。
一个正样本和 k 个负样本需要 k+1 次点积,成本为 O((k+1)d),而不是 O(Vd)。只有被选中的输出向量会收到梯度,重复抽到的样本会重复计入。本文将输出向量存为 (V, d) 矩阵 Ep 的行,即 word2vec 文章中 E' 布局的转置。
比较观测词对与随机词对,将负采样与对比学习联系起来。CLIP 和稠密检索模型使用相关思想,但其常见的 softmax 损失不同于 word2vec 中独立的 sigmoid 项。
从样本对到负样本
word2vec 的训练数据是滑动窗口扫过语料得到的 (center, context) 对:the cat sat on the mat 产生 (sat, cat)、(sat, on)、(sat, the) 等等——数十亿条真实的共现。
负采样保持这些对完全不变,只是不再单纯地最大化真实 (center, context) 匹配的似然,而是每步再加 k 个随机对作为配重——调整权重,让它们的似然同时被最小化。
两类对共享同一个中心词,但挑选陪伴词(搭档)的方式不同:
正样本词对来自上下文窗口。负样本独立于中心词抽取搭配词,通常按 count(w)^0.75 加权。抽到的词也可能是真实上下文词:“负”指的是抽样方式,并不表示该词对从未出现。因此可能产生假负样本。
向量查表和点积运算保持不变。变化的是对哪些词评分,以及这些分数如何进入损失函数。
原始点积 v_c · v'_w 可以是任意实数——正的、负的、大的、小的——但损失需要一个 0 到 1 之间的概率:「这一对有多可能是真的?」把我们带到那里的是 sigmoid 函数 σ。
有了得分,我们就可以用 sigmoid 把每个得分变成概率。σ 把任意实值得分压进 (0, 1):
点积 v_c · v'_w | σ | 模型说 |
|---|---|---|
| 很大的正数 | ~1 | 「真实对」 |
| ~0 | 0.5 | 不确定 |
| 很大的负数 | ~0 | 「随机对」 |
对每个真实对,我们希望 σ 爬向 1;对每个随机对,希望它落向 0——训练把点积往这些方向推。
在与 word2vec 那篇文章相同的五词玩具例子上,下面的小组件跑的正是这个打分步骤。挑一个 (center, target) 训练对,切换哪些词被采样为负样本,然后逐步走过这 k+1 个点积——score[w] = v_c · v'_w,逐项展开,再过一遍 sigmoid。只有目标和负样本会被打分;Ep 的其他行保持灰色,从不被读取。
| word | role | score | σ(score) | direction |
|---|---|---|---|---|
| on | positive | +0.394 | 0.597 | → pushed toward 1 |
| cat | negative | -0.187 | 0.453 | → pushed toward 0 |
| mat | negative | +0.260 | 0.565 | → pushed toward 0 |
每个 sigmoid 估计的是所选采样方案下的数据或噪声标签概率。它不是 P(上下文词 | 中心词),并且取决于噪声分布和负样本数量。各词对应的值无需相加为一。
负样本是怎么采的?
负样本有两种常见来源:像 word2vec 那样从噪声分布抽词,或者像 CLIP 和 DPR 那样复用同一批次中的其他样本。
将计数取小于一的幂,可以在保留频率排序的同时减弱高频词的主导作用。停用词占多少概率取决于语料,并不存在通用比例。
负采样论文报告,在其测试中,指数 0.75 的效果优于原始词频采样和均匀采样:
P(w) 是噪声采样概率。分母归一化取幂后的计数,它来自语料频率,而不是模型当前的评分。
有了这个分布,采样就很直接:用上面的公式为词表中每个词算出 P(w),然后对每个正样本对 (center, context),从这个分布中抽 k 个随机词——就是在词表上做一次加权掷骰、重复 k 次,P(w) 更高的词被挑中得更频繁。
使用批内负样本时,当前批次中的其他样本提供对照候选项。
在包含 N 个标注词对 (Qi, Pi) 的批次中,将 Pi 作为 Qi 的正样本,其余候选作为负样本。这是在分配训练标签,并不能证明所有非对角线项都没有语义关联:
batch: (Q1, P1) (Q2, P2) (Q3, P3) (Q4, P4)
for Q1: positive = P1, negatives = {P2, P3, P4}
for Q2: positive = P2, negatives = {P1, P3, P4}
for Q3: positive = P3, negatives = {P1, P2, P4}
for Q4: positive = P4, negatives = {P1, P2, P3}编码器已经计算了全部 N 个候选向量。复用它们无需额外的编码器前向传播,但 N×N 分数矩阵仍然需要计算和内存。256 个样本的批次可为每个查询提供 255 个候选负样本。
其他候选项也可能与查询相关,从而成为假负样本。批次组成、重复样本处理和难负样本选择都会影响训练;批内负样本并非天然更优。
损失
损失。 每对样本的损失是 k+1 个 log-sigmoid 项之和——正样本一项,每个负样本一项——取代完整 softmax 的 −log P(target | center):
loss = − log σ(v_c · v'_t) − Σ log σ(−v_c · v'_n)
───────────────── ───────────────────────
true (positive) pair k sampled negatives其中 v_c 是中心词的输入嵌入,v'_t 是真实目标的输出嵌入,v'_n 是被采负样本词的输出嵌入,σ 是 sigmoid 函数。第一项把真实对的点积往上推(趋向 σ(·) = 1);第二项把每个负样本的点积往下推(趋向 σ(·) = 0)。
负样本项用的是 σ(−v_c · v'_n)——点积取负——这行得通是因为恒等式 σ(−x) = 1 − σ(x)。所以 −log σ(−v_c · v'_n) 不过就是 −log(1 − σ(v_c · v'_n)):交叉熵中标准的「错误类别」那一半,用在「这不是真实对」的方向上。损失中的每一项都是作用在一个 (center, w) 对上的二元交叉熵(BCE)——正样本标签为 1,每个负样本标签为 0。总损失就是 k+1 个 BCE 相加。
模型给观测标签的概率越低,损失越大。损失对 logit 的导数是 σ(z) − y,范围在 −1 到 1 之间。因此,较大损失并不意味着 logit 梯度无界。
每个被打分的词贡献一项——正样本是 −log σ,每个负样本是 −log(1 − σ):
| 词 | 角色 | σ | 项 | 值 |
|---|---|---|---|---|
on | 正样本 | 0.5973 | −log(0.5973) | 0.5153 |
cat | 负样本 | 0.4534 | −log(1 − 0.4534) | 0.6041 |
mat | 负样本 | 0.5647 | −log(1 − 0.5647) | 0.8318 |
合计:L ≈ 1.95。贡献最大的是 mat——它的 σ(0.56)离负样本该在的位置(0)最远。
给定分数后,我们将采样标签建模为条件独立的伯努利观测。将其似然相乘,再取负对数,就得到这一求和式。这些项仍通过中心词向量共享参数。
P(all right) = P(positive right) × P(neg₁ right) × … × P(neg_k right)−log 把乘积变成求和:
−log P(all right) = −log P(positive) + −log P(neg₁) + … + −log P(neg_k)Softmax 建模上下文词的类别,负采样建模数据或噪声标签。两者优化不同的似然,回答不同的问题。无论哪一种,都不会自动在未见数据上得到校准良好的概率。
梯度
对于不带正则化的普通 SGD,只有中心词输入行和采样得到的输出行会更新。若一个词被重复抽取,需要累加它的贡献。所有梯度都应使用更新前的同一组参数计算。
要最小化 L,我们需要它对前向传播中涉及的每个参数的梯度:v_c(中心词在 E 中的行)、v'_t(目标在 Ep 中的行)以及每个 v'_n(每个负样本一行)。只需一条微积分事实——
d/dz [ −log σ(z) ] = σ(z) − 1——链式法则就给出全部三个:
∂L / ∂v'_t = (σ_t − 1) · v_c ← 目标的输出行
∂L / ∂v'_n = σ_n · v_c ← 每个负样本的输出行
∂L / ∂v_c = (σ_t − 1) · v'_t + Σ_n σ_n · v'_n ← 中心词的输入行其中 σ_t = σ(v_c · v'_t)、σ_n = σ(v_c · v'_n)——正是上面 ### 损失 一节里的那些数。
注意其中的对称性:每个输出行的梯度(∂L/∂v'_t、∂L/∂v'_n)都是一个标量乘以 v_c,而中心词的梯度是它所打分过的那些输出行的加权和。这是点积对其两个参数对称的直接后果——把任何 f(v_c · v'_w) 对 v'_w 求导,总会得到与 v_c 成比例的东西,反之亦然。
正样本输出向量的更新会加上 v_c 的某个倍数,负样本则减去。当 v_c 固定时,这会增大或减小点积。“拉近”和“推远”描述的是分数变化,并不保证欧氏距离相应减小或增大。
使用评分组件中未四舍五入的 sigmoid 值,中心向量梯度为:
∂L/∂v_c = (σ_on − 1) · v'_on + σ_cat · v'_cat + σ_mat · v'_mat
≈ [−0.0435, 0.5269, 0.0717]正样本更新共享系数 η(1 − σ_t),但步长分别是 η(1 − σ_t)‖v_c‖ 和 η(1 − σ_t)‖v'_t‖,只有向量范数相等时才相同。中心向量还会收到全部负样本的贡献。
更新
学习率为 η 的梯度下降:
v'_t ← v'_t + η · (1 − σ_t) · v_c ← 朝 v_c 迈一步
v'_n ← v'_n − η · σ_n · v_c ← 远离 v_c 迈一步
v_c ← v_c + η · (1 − σ_t) · v'_t − η · Σ_n σ_n · v'_n
← 朝 v'_t,远离每个 v'_nSigmoid 误差越大,更新系数越大,但向量范数也会影响实际步长。不同词对可能产生冲突梯度,过大的学习率也可能越过合适位置。因此损失不必在每一步都下降。
把上面的梯度代入,取 η = 0.1,中心词被轻轻推动:
v_c = [0.33, −0.27, 0.84]
v_c_new = v_c − 0.1 · ∂L/∂v_c ≈ [0.3344, −0.3227, 0.8328]一小步,但方向正是损失所要求的。v'_on、v'_cat 和 v'_mat 会在同一时刻按上面的公式各自更新;这里我们只盯着 v_c,好让推演简短。
验证这一步
先只更新 v_c,保持所有输出向量不变,以单独观察中心向量的贡献:
| 词 | 之前 | 之后 | 方向 |
|---|---|---|---|
v_c · v'_on | 0.39 | 0.41 | 上升——正样本更对齐 ✓ |
v_c · v'_cat | −0.19 | −0.22 | 下降——负样本被推开 ✓ |
v_c · v'_mat | 0.26 | 0.25 | 下降——负样本被推开 ✓ |
在这个仅更新中心向量的示例中,损失从 1.9512 降至 1.9229。若同时基于更新前的值更新输出向量,损失为 1.8626。这些结果只对应当前示例和学习率,并不保证每次训练更新都如此。
为什么负采样能学到有用的向量
负采样不是完整 softmax 梯度的无偏估计。两种目标都奖励观测到的关联,但对竞争词赋予不同权重。区分共现与噪声能够捕捉词之间的统计关系,因此可以学到有用表示,而无需复现 softmax 的解。
这个二维示例反复训练一个正词对和三个固定负样本。可以观察输入与输出向量变化时的 sigmoid 分数和损失。它用于展示训练目标,并不是这五个词的已训练语义地图。
下一个组件会在多个词对上训练,并显示输入向量的变化:
此浏览器演示在刻意构造的合成语料上训练。每一步处理一个词对,并从平滑词频分布中有放回地抽取五个负样本,也可能抽到正样本词。图中只显示输入向量,输出向量单独训练。颜色只是给读者看的分组标签,不参与训练。模型限制为二维以便显示,并不保证产生清晰的语义簇。
下面的 NumPy 步骤会在更新任何矩阵前复制所需向量。np.add.at 能正确累加重复输出索引的贡献,包括同时被抽为正、负样本的词。E 和 Ep 是两个独立的 (V, d) 浮点数组。
import numpy as np
def sgns_step(E, Ep, c, t, negatives, lr=0.1):
rows = np.r_[t, np.asarray(negatives, dtype=int)]
labels = np.zeros(len(rows))
labels[0] = 1
center = E[c].copy()
outputs = Ep[rows].copy()
logits = outputs @ center
z = np.exp(-np.abs(logits))
probabilities = np.where(logits >= 0, 1 / (1 + z), z / (1 + z))
errors = probabilities - labels
grad_center = errors @ outputs
grad_outputs = errors[:, None] * center
loss = np.sum(np.logaddexp(0, logits) - labels * logits)
np.add.at(Ep, rows, -lr * grad_outputs)
E[c] -= lr * grad_center
return float(loss) # loss before the update
# rng is a NumPy Generator; neg_dist sums to 1 over V words.
for c, t in pairs:
negatives = rng.choice(len(E), size=k, replace=True, p=neg_dist)
loss = sgns_step(E, Ep, c, t, negatives, lr)对于不受约束的分数,以及从分布 q 独立抽取的 k 个负样本,总体最优值为 s*(c,w) = log[P_data(w|c) / (k q(w))]。当 q(w) = P_data(w) 时,它变为 PMI(c,w) − log(k)。采用 q(w) ∝ count(w)^0.75 会改变修正项。有限维向量只能近似这张理想分数矩阵。
这正是 Levy 与 Goldberg 的平移 PMI 分析的基础。它解释了 SGNS 分数捕捉的内容,但不意味着 SGNS、SVD 和完整 softmax 是可互换的目标。
什么时候用哪个
当任务需要对固定候选集合给出归一化分布,而且计算成本可接受时,可以使用完整 softmax。当表示质量和训练成本支持这一选择时,可以采用采样目标。没有哪种目标能保证在所有任务上准确率更高。
BERT 的掩码词元预测和自回归语言建模通常使用词表 softmax。检索系统常将相关项与采样或批内候选项进行比较。候选集合的选择,与使用 sigmoid 还是 softmax 损失,是两个不同问题。
DPR通过候选段落上的 softmax 学习查询与段落的评分。偏好奖励模型则学习回答的相对分数,常使用 −log σ(r_preferred − r_rejected);这是排序目标,不是 word2vec 的噪声采样目标。CLIP 是另一种基于 softmax 的对比学习示例。
CLIP:对比图像与描述
CLIP使用了 4 亿个图文对进行训练。它将批内不匹配项作为负样本,但损失是对称的 softmax 交叉熵,并非 word2vec 负采样使用的独立二分类损失。
原始 CLIP 实验将文本 Transformer 与 ResNet 或视觉 Transformer 配对。投影层将两种输出映射到共享向量空间,维度取决于模型版本。
对每个含 N 对样本的训练批 (I_1, T_1), ..., (I_N, T_N):
先对两组向量进行 L2 归一化,再计算 S[i,j] = exp(t) × dot(image[i], text[j]),其中 t 是可学习的对数尺度。此时点积就是余弦相似度。以索引 i 为目标,分别对各行、各列计算交叉熵,再将两个方向的平均损失取平均。对角线被标为正样本,但非对角线仍可能包含假负样本。参见 CLIP 实现。
原始训练批次包含 32,768 个图文对,每张图像对应 32,767 个候选负描述。编码器输出可以复用,但成对评分和跨设备同步向量仍然有成本。
你得到的是:一个语义相关的图像与文本落得近、不相关的落得远的嵌入空间。这就是 CLIP 能做零样本图像分类的原因——为类别名称计算文本嵌入(「一张狗的照片」「一张猫的照片」等等),然后按图像最接近哪个类别嵌入来分类。对比损失雕出的几何早已把含义编码进了两种模态;不需要任何带标签的分类器。
原始点积不等于余弦:SGNS 可以同时改变向量范数和夹角。CLIP 则归一化向量,并学习分数尺度。这些不同的几何性质并不意味着负样本存在通用目标夹角,也不能单独解释模型需要多大的批次。
共同思想是通过比较来学习,但具体方法很重要:word2vec SGNS 独立分类采样词对,CLIP 则通过 softmax 让候选项竞争。应结合分数需要表达的含义,一并选择采样方式与损失函数。