负采样与对比学习——从 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 ops

MNIST 是一个 10 类分类问题;word2vec 则是百万类的——词表里每个词对应一个输出神经元。输出层里的一切开销——权重、矩阵乘法、softmax——都随这个类别数线性增长。

word2vec 其实从来没有真的跑过那个 V 路 softmax。论文一开始就引入了负采样——丢掉对归一化项的需求,转而只关注单个得分。每一对 (center, w) 不再是一次 V 类分类(衡量 w 相对词表中其他所有词有多可能),而变成它自己的独立得分,用来表示这一对具体样本有多可能是真实的。 对每个被选中的对,模型产出一个 sigmoid 得分 σ(v_c · v'_w)——真实对被推,随机对被推。 因为每次判断都是独立的,损失就是各对之间的简单求和;而这种可分性正是你只需给 E'k+1 行打分、其余不管的全部理由:其他词根本不进入这一步的损失。

从机制上看,这把打分步骤大幅缩小了。softmax 要对整个输出矩阵做一次矩阵乘法以一次性拿到所有得分,负采样则改为做若干次单独的点积——k 个随机负样本各一次,加上真实对一次,共 k+1 次。E' 其余 V − (k+1) 行不会被读取、不参与乘法,这一步也不会更新。这 k+1 个点积各自独立地过一遍自己的 sigmoid——没有共同分母把它们耦合起来。每对样本的损失分解成 k+1 个独立项之和——与 softmax 是同一套最大似然机制,只不过对象换成了 k+1 个独立的伯努利分布,而非一个覆盖全词表的多项分布

这一机制叫做对比学习——把配对的东西拉近,把随机的东西推开——而且它可以用在任何可嵌入的东西上。 你可能遇到过的两种形态:像 DPR 这样的稠密检索器(RAG 的发动机),训练时把每个查询与其相关段落配成正样本、随机段落作负样本;以及 RLHF 奖励模型,其中被偏好的回复扮演正样本、被拒绝的扮演负样本。

从样本对到负样本

word2vec 的训练数据是滑动窗口扫过语料得到的 (center, context) 对:the cat sat on the mat 产生 (sat, cat)(sat, on)(sat, the) 等等——数十亿条真实的共现。

负采样保持这些对完全不变,只是不再单纯地最大化真实 (center, context) 匹配的似然,而是每步再加 k 个随机对作为配重——调整权重,让它们的似然同时被最小化。

两类对共享同一个中心词,但挑选陪伴词(搭档)的方式不同:

  • 真实对——扫过语料,取每个中心词的上下文窗口里实际出现的词。搭档由语料中真正共同出现的东西决定(联合分布)——(sat, cat) 出现是因为它真的发生过。
  • 随机对——保持同一个中心词,但按每个词在语料中出现的频率抽取搭档(一元语法分布),完全忽略共现。于是得到 (sat, banana) 这样的对——两个都是真词,但它们从未彼此靠近出现过。

负采样保留了完整 softmax 的骨架——三个前向阶段、一个损失、一个梯度——只改变每一步内部发生的事。唯一真正新增的操作是采样:为每一对从一元语法分布中抽 k 个负样本词(即上面说的随机对抽取)。其余一切都是范围受限的完整 softmax:查表仍然从 E 读一行,打分步骤仍然把中心词与 E' 的若干行做点积——只不过是 k+1 行(目标加上 k 个负样本),而不是全部 V 行。

k+1 次点积代替 V 次——与完整 softmax 执行的 v_c · v'_w 是同一种运算,只是只对进入损失的那些词求值,而不是整个词表。softmax 所需的完整 v_c @ E' 矩阵乘法压根不会发生。

原始点积 v_c · v'_w 可以是任意实数——正的、负的、大的、小的——但损失需要一个 0 到 1 之间的概率:「这一对有多可能是真的?」把我们带到那里的是 sigmoid 函数 σ

有了得分,我们就可以用 sigmoid 把每个得分变成概率σ 把任意实值得分压进 (0, 1)

点积 v_c · v'_wσ模型说
很大的正数~1「真实对」
~00.5不确定
很大的负数~0「随机对」

对每个真实对,我们希望 σ 爬向 1;对每个随机对,希望它落向 0——训练把点积往这些方向推。

在与 word2vec 那篇文章相同的五词玩具例子上,下面的小组件跑的正是这个打分步骤。挑一个 (center, target) 训练对,切换哪些词被采样为负样本,然后逐步走过这 k+1 个点积——score[w] = v_c · v'_w,逐项展开,再过一遍 sigmoid。只有目标和负样本会被打分;E' 的其他行保持灰色,从不被读取。

scoring one pair · k+1 dot products, not V
training pair
negatives — sampled
all 3 dot products done — loss is defined
E (V=5 × d=3)
row sat = v_c
cat
0.21
-0.43
0.15
mat
0.07
0.62
-0.31
on
-0.55
0.18
0.40
sat
0.33
-0.27
0.84
the
-0.12
0.49
-0.06
E' (V=5 × d=3)
target + k negatives — the rest unread
cat
0.45
0.62
-0.20
mat
-0.31
0.15
0.48
on
0.18
-0.40
0.27
sat
-0.22
0.33
0.11
the
0.07
0.55
-0.39
scores → σ target + k negatives
Stage 2 — score · k+1 dot products, not V
cat
mat
on
sat
the
-0.19
0.26
0.39
·
skip
Stage 3 — sigmoid · independent σ, no softmax
cat
mat
on
sat
the
0.45
0.56
0.60
·
each score through its own σ — no shared denominator; target → 1, negatives → 0.
loss −Σ log σ
k+1 terms
1.95
wordrolescoreσ(score)direction
onpositive+0.3940.597→ pushed toward 1
catnegative-0.1870.453→ pushed toward 0
matnegative+0.2600.565→ pushed toward 0

完整 softmax 把 V 个得分推入一个归一化分布,负采样则让 k+1 个得分各自独立地过自己的 σ——σ(v_c · v'_w) 独立成立,就是模型对「(center, w) 是真实对」的估计,没有共享分母。是 k+1 个独立概率,而不是一个覆盖全词表的分布。

负样本是怎么采的?

word2vec 最初提出从一元词频分布中采负样本——按词出现的频率加权,从词表里随机挑词。 然而现代对比系统——CLIPDPRSentence-BERT——完全跳过这一步,直接把同一训练批次里的其他样本当作负样本。 这种做法叫批内负样本(in-batch negatives)。 我们快速看看两者。

一元词频采样这条路上,我们偏向高频词:与模型真正会遇到的词做对比,比与罕见词做对比更重要。如果 zebra 在语料里只出现寥寥几次,模型很少需要学会「其他词不该与 zebra 有相似关联」——那是被浪费掉的信号。

不过,纯按频率加权会导致约 98% 的负样本是与什么都共现、不带区分信息的词——也就是 theand 这类停用词。折中办法是对频率做平滑:仍然偏向高频词(使多数负样本贴近现实),但压得足够平,让 catmatking 这类有实义的词被采到的次数足以教会模型真正的语义区分。

每个词被采样的概率正比于该词在语料中单独计数的出现频率(它的一元语法频率)。原论文从经验上发现,在归一化之前把这些计数取 3/4 次幂效果最好——0.5 到 1.0 之间的取值都能给出不错的结果,而 0.75 恰好在甜点上:

P(w)=count(w)0.75wcount(w)0.75P(w) = \frac{\text{count}(w)^{0.75}}{\sum_{w'} \text{count}(w')^{0.75}}

这里的 P(w) 是词 w 被抽为负样本的概率。这个形状——计数除以计数之和——与 softmax 用的配方一样;归一化前先取 0.75 次幂则是一种温度缩放手法,把分布压平。分子是每个词的 count(w)^0.75;分母把它们在整个词表上求和,使概率之和为 1。

有了这个分布,采样就很直接:用上面的公式为词表中每个词算出 P(w),然后对每个正样本对 (center, context),从这个分布中抽 k 个随机词——就是在词表上做一次加权掷骰、重复 k 次,P(w) 更高的词被挑中得更频繁。

相比之下,现代系统根本不从词表分布里采样。它们复用手头正在处理的批次——这种做法叫批内负样本

训练按批进行,每批 N 个真实的 (query, positive) 对——(Q1, P1), …, (QN, PN)。每个 Qi 真正匹配的是它自己的 Pi(查询与其相关段落、图像与其标题等等)。对 Qi 的负样本,只需看批中其他的正样本——j ≠ iPj——它们相对 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}

巧妙之处在于:为了给每个 Qi · Pi 打分,你本来就要对 P1PN 做一次前向传播。所有 P 的嵌入已经在内存里了。给 Qij ≠ iPj 打分只是多一次点积——不需要再过一遍编码器。批大小 256 就为每个查询「免费」提供 255 个负样本,相当于 k = 255,而 word2vec 通常只有 k = 5–20

这些负样本通常质量也更高:是模型必须真正区分的真实文本或图像,而不是随机停用词。梯度更锐利,收敛更快。代价是:它们只覆盖当前批次,而非整个语料——这正是 MoCo 之类的系统在其上叠一个记忆队列、另一些系统采用难负样本挖掘以获得更紧对比的原因。

损失

损失。 每对样本的损失是 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 相加。

随着 σ 变化,−log σ(z) 长什么样?当 σ → 1,该项趋于 0——预测正确,没有损失。当 σ → 0,该项爆向无穷——预测错误,重罚。负样本是镜像:σ → 0−log(1 − σ) → 0(正确),σ → 1→ ∞(模型错误地认为一个随机对是真的)。正是这条不对称曲线让损失自带节流——几乎正确的预测几乎不动损失,非常错误的预测则主导它。梯度下降于是自动把注意力集中到最差的预测上。

每个被打分的词贡献一项——正样本是 −log σ,每个负样本是 −log(1 − σ)

角色σ
on正样本0.60−log(0.60)0.51
cat负样本0.45−log(1 − 0.45)0.60
mat负样本0.56−log(1 − 0.56)0.83

合计:L ≈ 1.95。贡献最大的是 mat——它的 σ(0.56)离负样本该在的位置(0)最远。

而这个求和不只是图方便——它是 k+1 个独立二元决策的联合对数损失。因为每个 (center, w) 对是独立判断的,所有 k+1 个决策都正确的概率是各自概率的乘积

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)

这正是上面那一行项——把每对的 −log 加起来并不是额外的建模选择;独立决策的联合对数似然本来就长这样。

退一步看:softmax 和负采样都在做最大似然——它们只是在建模的概率族上不同。

softmax负采样
建模对象全词表上的 P(target | center)对每个 wP(真实对 | center, w)
分布类型一个 V 类多项分布k+1独立伯努利分布
输出V 个数,和为 1k+1 个 (0,1) 内的数,无求和约束
损失一次 V 路交叉熵k+1 个二元交叉熵之和

上表那一行中的每个 σ 都是合法的概率——模型对「这个特定的 (c, w) 对是真的」的估计——而总损失是货真价实的联合对数似然。负采样是完全概率化的——只不过是局部地(每对一个伯努利),而不是全局地(词表上一个归一化分布)。

你放弃的是那个在 V 上求和为 1 的、经过校准的 P(target | center)——全局的、归一化的视角。你保留的是最大似然那套机制:每个 σ 都是真实概率,梯度也是真实的极大似然梯度。Mikolov 等人在前面那段更深层的讨论里对这个取舍讲得很明白——他们只想要好的向量,而不是一个校准过的概率模型。当正样本 σ → 1每个负样本 σ → 0总和 → 0 时,损失就满意了——而每一步梯度都朝那个方向推。

梯度

反向传播只触碰前向传播读过的东西。E' 中被打分的 k+1 行会更新——目标那一行被拉向 v_c,每个负样本那一行被推开——而其余 V − (k+1) 行从未被读,也就完全收不到梯度。在输入侧,单独一行 E[center] 会更新,和完整 softmax 中一模一样。

要最小化 L,我们需要它对前向传播中涉及的每个参数的梯度:v_c(中心词在 E 中的行)、v'_t(目标在 E' 中的行)以及每个 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 方向,缩放系数为 σ_t − 1(一个负数——σ_t 小于 1)。当我们减去梯度时,v'_t 就被拉向 v_c 的方向——朝向 v_c
  • 每个负样本的梯度同样沿 v_c 方向,缩放系数为 σ_n(正数)。减去它会把每个 v'_n 推向 −v_c 方向——远离 v_c
  • 中心词的梯度把它们全部合起来——来自目标的一次拉,每个负样本的一次推——各自按其 sigmoid 当前错得多离谱来加权。

代入上面的玩具数值(σ_on = 0.60σ_cat = 0.45σ_mat = 0.56,嵌入表取自打分小组件):

∂L/∂v_c = (0.60 − 1) · v'_on  +  0.45 · v'_cat  +  0.56 · v'_mat
        ≈ [ −0.04,  0.53,  0.07 ]

正样本带来的负系数(σ_on − 1 = −0.40)与负样本带来的正系数合成了我们接下来要让中心词迈步的方向。

还有一个优雅的性质值得注意:同一个标量 (1 − σ_t) 既出现在目标的更新里(v'_t(1 − σ_t)·v_c 移向 v_c),也出现在中心词的拉动里(v_c(1 − σ_t)·v'_t 移向 v'_t)。两个向量彼此靠近的幅度完全相同——这就是点积损失的共适应对称性。对每个负样本同样成立,只是把系数换成 σ_n。下一节的更新会把这一点形式化。

更新

学习率为 η 的梯度下降:

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'_n

注意这些步长

  • v'_t 移动 η(1 − σ_t)——当 σ_t 很小(正样本没学好)时最大。
  • v'_n 移动 η · σ_n——当 σ_n 很大(负样本被误当成真的)时最大。

所以在我们当前的状态下,mat(σ = 0.56)被推得比 cat(σ = 0.45)更狠;而任何一对错得越厉害,得到的修正就越大。这种自带节流——模型已经对了就小修,没对就大改——正是驱动损失在训练中单调下降的东西。

把上面的梯度代入,取 η = 0.1,中心词被轻轻推动:

v_c     ≈ [ 0.33, −0.27,  0.84 ]
v_c_new = v_c − η · ∂L/∂v_c  ≈ [ 0.33, −0.32,  0.83 ]

一小步,但方向正是损失所要求的。v'_onv'_catv'_mat 会在同一时刻按上面的公式各自更新;这里我们只盯着 v_c,好让推演简短。

验证这一步

这次更新真的有用吗?用新的 v_c 重算那三个点积(为清楚起见沿用同样的 v'_w——实际中它们会同时更新,效果更明显):

之前之后方向
v_c · v'_on0.390.41上升——正样本更对齐 ✓
v_c · v'_cat−0.19−0.22下降——负样本被推开 ✓
v_c · v'_mat0.260.25下降——负样本被推开 ✓

每个 σ 都朝正确方向移动,每对样本的损失从 1.95 降到约 1.92。一对样本、一小步——乘上数十亿对样本和许多轮次,正是它雕刻出那种嵌入几何:匹配的东西落得近,随机的东西落得远。

对照上面那段六行的 NumPy 循环——那三行更新正是这三个更新:Ep[t] -= lr * (s_t - 1) * v_cv'_t 的步,Ep[negs] -= lr * s_n[:, None] * v_c 是负样本的步,E[c] -= lr * ((s_t - 1) * v_t + s_n @ v_n)v_c 的步。

为什么丢掉 softmax 不损失质量

从「V 类分类」转向「带随机负样本的二分类」,看起来应该丢失信息——我们不再直接最大化 P(真实目标 | 中心词) 了。那它为什么还能产出同样好的嵌入?

它之所以有效:梯度信号本质上是一样的。完整 softmax 的梯度把 v_c 拉向 v'_t,同时把它从其他每个词的输出嵌入推开,权重是该词的预测概率。负采样做的是同一件事,只是在一个采样子集上——拉向 v'_t,推离 k 个随机选中词的嵌入。在大量样本上,期望梯度与完整 softmax 的梯度在常数倍意义下相同。模型收敛到相似的几何结构;它只是以每步一个小常数量级的计算量抵达那里,而不是随词表规模增长的计算量。

one SGD step on a (center, context) pair · simplified to 2D
v_sat (center)v_cat (true)v_bananav_zebrav_rocket
σ(v_sat · v_w)
cat0.576→ 1
banana0.582→ 0
zebra0.511→ 0
rocket0.516→ 0
loss = 2.866
steps taken: 0

一对样本,一步。中心词的输入向量 v_sat(绿色)被拉真实目标 v_cat(蓝色)——抬高它们的点积,于是 σ(v_sat · v_cat) 爬向 1——同时被推每个负样本 v_bananav_zebrav_rocket(红色),于是每个 σ(v_sat · v_neg) 落向 0。反复按 step,看几何自己理顺。真实的 word2vec 在几百维空间里跑同样的更新,带 5–20 个负样本,对数十亿对样本逐一执行。

把同一个循环跑在整个词表上——很多对,很多次更新——嵌入表就会自己按语义分出簇来:

step-by-step skip-gram training · 21-word vocab, 2-D embeddings, k=5 negatives
thekingsatinpalacequeenruledworkedplayedgardenamanchairwomanboyrangirlonmatcatdog
step
0
epoch ≈ 0 · 532 pairs/epoch
current pair
legend
royalty
adult
youth
animal
verb
place
function

真正的带负采样 skip-gram,在浏览器里对一个极小的合成语料实时运行。每一步挑一个 (center, context) 对,采 5 个随机负样本,并施加一次 SGD 更新:v_center 被拉向 v_context,并被推离每个负样本。第 0 步时向量是随机的——同簇的词并不比随机对更靠近。按 play+200,看它们自己归位:王室类的词飘到一起,成年人、少年、动物、动词也各自成团。没人告诉模型这些分组存在;它们之所以浮现,是因为在语料中共享上下文的词,最终拿到的行必须共享同一套预测几何。为了能画出来,向量被强行压到二维——真正的算法在 300 维下完全相同。

没人告诉这两个模型这些分组存在;它们之所以浮现,是因为在语料中共享上下文的词,最终拿到的行必须共享同一套预测几何。为了能画出来,向量被强行压到二维——在 d=300 时无法可视化,但算法完全相同。

用 NumPy 写,内层循环只有六行:

for c, t in pairs:
    negs = rng.choice(V, size=k, p=neg_dist)
    v_c, v_t, v_n = E[c], Ep[t], Ep[negs]
    s_t = sigmoid(v_c @ v_t)        # want → 1
    s_n = sigmoid(v_c @ v_n.T)       # want → 0 (each)
    Ep[t]    -= lr * (s_t - 1) * v_c           # pull v_t toward v_c
    Ep[negs] -= lr * s_n[:, None]  * v_c       # push v_n's away from v_c
    E[c]     -= lr * ((s_t - 1) * v_t + s_n @ v_n)  # both, into v_c

这就是整个训练步。每次迭代都是上面小组件的一帧;跑遍数十亿对样本,就产出了一张真正的 word2vec 嵌入表。

这种重新表述还解释了模型为何能学到词频之外的东西:真实对携带共现信号,随机对只携带边缘频率。这两个分布之比的对数就是逐点互信息——所以一个把两者分开的分类器,隐含地就在学习中心词与上下文之间的 PMI。

Mikolov 等人(2013)表明,得到的向量在标准类比基准上与完整 softmax 版本相差不过几个百分点。Levy 与 Goldberg(2014)后来证明,带负采样的 skip-gram 隐式地在分解一个平移过的 PMI(逐点互信息)矩阵——正是 SVD 这类「先计数再分解」的老方法试图显式分解的那个矩阵。这个技巧不是权宜之计;它是通往同一个数学目的地的另一条路。

什么时候用哪个

负采样和完整 softmax 是面向不同任务的工具。softmax 保有两个真实优势,它们对 word2vec 的目标无关紧要,但在别处很重要:

  • 校准过的概率——softmax 给出真正的条件概率 P(target | center),在词表上求和为 1。负采样的 k+1 个独立伯努利并不构成一个自洽的分布;你可以把每个读作「这一对是真的吗?」,却不能读作「这个词在全部 V 个候选中排第几?」。
  • 每步更稠密的梯度——softmax 的梯度会同时把目标顶到每一个候选之上。负采样每步只与 k 个随机样本对比,所以单步更新更噪——在大量样本的期望意义下结果相同,但每一步单独看都是更弱的信号。

所以如果你的词表小到 V 路 softmax 负担得起,softmax 在准确度上占优,而你为此付出的代价也不大。这就是多数现代分类器仍坚持用它的原因:

  • BERT 的掩码语言建模在其约 30,000 个 WordPiece 词表上用完整 softmax。
  • GPT 式的下一个 token 预测在其约 50,000–128,000 的字节对编码(BPE)词表上用完整 softmax。
  • 标准图像分类(10 到约 21,000 类)用完整 softmax。

当类别是诸如词表里的每个词、目录里的每件商品、语料里的每个段落、平台上的每个用户这类东西时——集合大到完整 softmax 变得不可行——负采样才成为正确选择。

把词级词表这一层剥掉,负采样就推广成现代 ML 中最重要的训练范式之一:对比学习。给定成对相配的东西和成对不相配的东西,学出这样的嵌入:相配的对落得近(点积/余弦相似度高),不相配的落得远。「东西」可以是任何可嵌入的对象:词、图像、句子、段落、音频片段、图节点、用户-物品对。损失的形状不变——把正样本聚拢,把负样本推开——变的是数据和编码器。

这个方法支撑了现代训练流程中很大一部分。DPR(Dense Passage Retrieval)RAG 的发动机,在 (查询, 相关段落) 正样本加上采样得到的 (查询, 无关段落) 负样本上训练——与 word2vec 的 (c, t)(c, 随机) 是同一个形状。RLHF 奖励模型(偏好回复, 拒绝回复) 对上训练——(正样本, 负样本) 的结构类比,同一种对比味道。CLIP 把匹配的 (图像, 标题) 嵌入拉到一起,把批内不匹配的组合推开——图文对齐就是一个巨大的批内对比损失。

CLIP——word2vec 的想法,被极大地放大

对比学习适用于一切可嵌入之物,而不只是词——CLIP 把这一点展示得最清楚。word2vec 从句子共现中训练 300 维词向量;CLIP 则从互联网上抓取的 4 亿对 (图像, 标题) 中训练图文联合嵌入——用的是同一个「把配对的拉近、把随机的推开」的损失,只不过用两个编码器代替一张嵌入表,用批内负样本代替按一元词频抽样。

在本文语境下,CLIP 值得细看的原因:

  • 它验证了核心论点——方法可以推广。 如果负采样只对语言建模有效,CLIP 就不可能成立。它确实成立,而且得到的嵌入空间丰富到足以支撑零样本图像分类(下面细说),这就直接证明了拉近/推开的几何在做真正的语义工作——而不只是记住词频。
  • 它是多数现代多模态系统遵循的架构模板。 DALL-E 用 CLIP。Stable Diffusion 用 CLIP 的文本编码器。几乎每个现代图文对齐系统内部某处都有一根 CLIP 式的对比脊梁。所以理解负采样加批内负样本,就是理解这些系统是怎么训练出来的。

两个编码器——图像用 vision transformer,文本用 transformer——把输入映射到共享的 512 维空间。训练数据:从互联网抓取的 4 亿对 (图像, 标题)

对每个含 N 对样本的训练批 (I_1, T_1), ..., (I_N, T_N)

  • 把每张图像编码成嵌入;把每条标题编码成嵌入。
  • 计算 N × N 的相似度矩阵 S[i][j] = I_embed[i] · T_embed[j]——每张图像的嵌入与每条标题的嵌入做点积。
  • 对角元 S[i][i]匹配对(正样本);对角线之外的一切都是不匹配的(批内负样本,由批中其余部分免费提供)。
  • 损失:按行做 softmax 交叉熵(图像从 N 个候选中挑自己的标题)加上按列做 softmax 交叉熵(标题从 N 个候选中挑自己的图像),两者相加。

以 CLIP 的 32K 批大小计,每张图像免费获得 32K − 1 个负样本——批中其他所有标题。对比 word2vec 的 k = 5–20。本文前面提到的批内捷径,在这个尺度上就是全部的玩法。

你得到的是:一个语义相关的图像与文本落得近、不相关的落得远的嵌入空间。这就是 CLIP 能做零样本图像分类的原因——为类别名称计算文本嵌入(「一张狗的照片」「一张猫的照片」等等),然后按图像最接近哪个类别嵌入来分类。对比损失雕出的几何早已把含义编码进了两种模态;不需要任何带标签的分类器。

与 word2vec 引入的是同一个方法。不同的数据、不同的编码器、互联网规模的批次——但骨架完全一样:把配对的拉近,把随机的推开。

本文略过的一个微妙之处:负样本到底该被推开多远?这里每个负样本项都把 σ(v_c · v'_n) 逼向 0,也就是不断把点积往下拽——拽向余弦 −1,完全对立。但一旦类别多于两个,这个目标在几何上就不可能:k 个单位向量没法两两互指相反方向。它们能做到的最好情形是摊成一个正则单纯形,其中每一对的余弦都是 −1/(k−1)——随着类别数增长,这个值趋向正交cos ≈ 0)。Taha Bouhsine 的 Untangling the Moons 沿着整条对比学习谱系追踪了这一张力——pair-contrastive、triplet、InfoNCE/NT-Xent、SupCon、CLIP、SigLIP——并主张那些朝 −1 推的损失瞄过了几何允许的位置,白白浪费优化力气,也部分解释了为何 CLIP 需要如此巨大的批次才能逼近正确的排布。它是本文这套概率叙事的几何对偶,配有每种损失的交互式「双月」可视化;很值得一读。

读懂当下格局的干净方式是:基于 softmax 的训练(LLM、BERT、图像分类器)用于从一个固定的小集合中预测正确的 token;负采样式的对比训练用于学习两类东西的嵌入,使匹配的对落得近、随机的对落得远。现代 ML 两者都用,各司其职。word2vec 教会了这个领域如何高效地做后者,而这一课经受住了时间的考验。