CLIP预训练方法——对比学习
对于一个向量 v = [v1, v2, ..., vk],它的 L2 范数(也叫欧几里得长度或模或向量的长度)是:L2 归一化就是用这个向量的每一个元素。
最近在看猛猿的大模型系列文章,学到CLIP。对于其中提到的“对比学习”还是学习的比较模糊,现写下这篇博文进行详细的注解。
阅读前,我把猛猿解析CLIP的这篇文章贴出来,大家可以详看。
(6 封私信 / 81 条消息) 关于多模态经典之作CLIP,还有哪些细节是你不知道的 - 知乎
https://zhuanlan.zhihu.com/p/660476765
博文中提到:
假设一个batch中共有N对
<图像,文字>对,那么它们过完各自的Encoder后,就会分别产生:
- N条文字向量 [T1,T2,T3,....TN]
- N条图片向量 [I1,I2,I3,...,IN]
这两组向量,将会分别过一次多模态Embedding(multimodal embedding),也就是在图中代表文字的紫色向量下,还有一层参数Wt(图中没有画出来),文字向量需要先和Wt做矩阵相乘后,才能得到最终的文字向量。对图片向量,同理也有个对应的Wi。Wt和Wi的作用可以理解成把文字、图片特征投影到多模态的特征空间中去。
经过多模态Emebdding的处理,我们得到了最终的 和 。接下来,我们就能通过“对比学习”,找到图像和文字的相似关系。做法也很简单,对于图中列出的N*N个格子,我们只需计算每个格子上对应的向量点积(余弦相似度)即可。由于对角线上的图片-文字对是真值,我们自然希望对角线上的相似度可以最大,据此我们可设置交叉熵函数,来求得每个batch下的Loss。
首先看看代码:
# image_encoder - ResNet or Vision Transformer
# text_encoder - CBOW or Text Transformer
# I[n, h, w, c] - minibatch of aligned images
# T[n, l] - minibatch of aligned texts
# W_i[d_i, d_e] - learned proj of image to embed
# W_t[d_t, d_e] - learned proj of text to embed
# t - learned temperature parameter
# extract feature representations of each modality
# -------------------------------------------------
# 1、图像/文字数据过image/text encoder,提取单模态特征
# 每张图片对应一个基本特征I_i
# 每张文字对应一个基本特征T_i
# -------------------------------------------------
I_f = image_encoder(I) #[n, d_i]
T_f = text_encoder(T) #[n, d_t]
# -------------------------------------------------
# 2. 图像/文字的基本特征过多模态Embedding,提取多模态特征
# 同时对这两个多模态特征做Layer Norm
# -------------------------------------------------
I_e = l2_normalize(np.dot(I_f, W_i), axis=1) # [n, d_i] * [d_i, d_e] = [n, d_e]
T_e = l2_normalize(np.dot(T_f, W_t), axis=1) # [n, d_t] * [d_t, d_e] = [n, d_e]
# -------------------------------------------------
# 3、计算图片-文字向量的余弦相似度
# -------------------------------------------------
logits = np.dot(I_e, T_e.T) * np.exp(t) # [n, n]
# -------------------------------------------------
# 4、计算Loss
# -------------------------------------------------
labels = np.arange(n)
loss_i = cross_entropy_loss(logits, labels, axis=0)
loss_t = cross_entropy_loss(logits, labels, axis=1)
loss = (loss_i + loss_t)/2
这是一段非常核心的伪代码,它完美地概括了CLIP(Contrastive Language-Image Pre-training)模型训练的核心思想。我们来一步一步、掰开揉碎地详细解释它。
整体目标:构建一个通用的“图文理解”模型
在深入代码之前,先记住CLIP的最终目标:创建一个共享的、多模态的嵌入空间(Embedding Space)。
在这个空间里:
-
语义上相似的图片和文本,它们的向量在空间中的位置会非常接近。
-
语义上不相关的图片和文本,它们的向量在空间中的位置会非常遥远。
这就像是建立一个通用的“万能翻译器”,可以将图片(视觉语言)和文字(人类语言)都翻译成一种中间的“概念语言”(即嵌入向量)。这段代码描述的就是如何通过“对比学习”来训练这个“翻译器”。
变量和组件解释
在看代码步骤前,我们先理解这些“零件”,也就是代码前的那一坨注释:
-
image_encoder 和 text_encoder:
-
这是两个独立的“专家”网络。image_encoder(CLIP尝试过5种不同的ResNet架构和3种VIT架构)擅长从像素中提取视觉特征,text_encoder(CLIP借鉴的是GPT2(Radford et al.2019)的架构)擅长从文字中提取语义特征。
-
它们是模型的骨干(backbone),负责最开始的特征提取。
-
-
I[n, h, w, c] 和 T[n, l]:
-
这是输入数据。n 是批次大小 (batch size),这是理解后续所有内容的关键。
-
I 是一个批次的 n 张图片,T 是一个批次的 n 段文本。
-
关键点: 它们是对齐 (aligned) 的。这意味着第0张图片 I[0] 和第0段文本 T[0] 是一对,I[1] 和 T[1] 是一对,以此类推。这 n 个对是我们的正样本 (positive pairs)。
-
-
W_i 和 W_t:
-
这是两个投影矩阵 (Projection Matrices),也叫投影头 (Projection Head)。它们是可学习的参数。
-
作用: image_encoder 和 text_encoder 输出的特征维度(d_i, d_t)可能不同,且处于各自独立的特征空间。W_i 和 W_t 的作用就像是“适配器”,将这两种不同的特征投影(变换)到同一个维度(d_e)的、共享的多模态嵌入空间中。这是实现图文对齐的关键一步。
-
-
t:
-
这是一个可学习的温度参数 (temperature parameter)。
-
作用: 它用于缩放 (scale) 相似度得分。这个参数可以控制模型对难区分的负样本的关注程度。在计算softmax时,较低的温度会使概率分布更“尖锐”,使模型更专注于区分最相似的负样本。
-
这一部分的作用听起来有点抽象,后面会详细的解释。
-
代码分步详解
假设一个批次里有n=3对样本,也就是有3张图片(猫、狗、车)和3段对应的描述。
-
I[0] = 猫的图片, T[0] = "一只猫的特写"
-
I[1] = 狗的图片, T[1] = "一只狗在公园里玩球"
-
I[2] = 车的图片, T[2] = "一辆红色的跑车"
第一步:提取单模态特征
# 1、图像/文字数据过image/text encoder,提取单模态特征
I_f = image_encoder(I) #[n, d_i]
T_f = text_encoder(T) #[n, d_t]
-
发生了什么:
-
n 张图片(I)被送入 image_encoder,产出 n 个图像特征向量 I_f。
-
n 段文本(T)被送入 text_encoder,产出 n 个文本特征向量 T_f。
-
-
结果:
-
I_f 是一个形状为 [n, d_i] 的矩阵,每一行代表一张图片的原始特征。
-
n: 这就是我们刚才讨论的批次大小 (batch size)。在例子中,n = 3。所以这个矩阵有 n=3 行。每一行都对应着批次中的一张图片。
-
第0行:代表“猫的图片”的特征。
-
第1行:代表“狗的图片”的特征。
-
第2行:代表“车的图片”的特征。
-
-
d_i: 这就是图像特征的维度 (dimension of image features)。
-
它是什么? image_encoder(比如一个ResNet-50或者Vision Transformer)在处理完一张图片后,会输出一个长长的向量(一维数组)来表示这张图片的核心内容。这个向量的长度就是 d_i。
-
它的大小是多少? 这个值取决于你使用的具体模型。
-
如果 image_encoder 是一个标准的 ResNet-50,那么在去掉最后的分类层后,它输出的特征向量维度通常是 2048。所以 d_i = 2048。
-
如果 image_encoder 是一个 Vision Transformer (ViT),比如 ViT-B/32,它的输出维度可能是 768。所以 d_i = 768。
-
-
这个向量可以被想象成一个“图片的DNA”或者“图片的指纹”,它用 d_i 个数字捕捉了图片的关键视觉信息(比如纹理、形状、颜色、物体组合等)。
-
-
-
T_f 是一个形状为 [n, d_t] 的矩阵,每一行代表一段文本的原始特征。
-
-
此时的状态: 这两种特征还处在各自的世界里,维度不同,语义空间也不同。它们还不能直接比较。
第二步:投影到多模态嵌入空间并归一化
# 2. 图像/文字的基本特征过多模态Embedding,提取多模态特征
I_e = l2_normalize(np.dot(I_f, W_i), axis=1)
T_e = l2_normalize(np.dot(T_f, W_t), axis=1)
-
发生了什么:
-
np.dot(I_f, W_i):通过矩阵乘法,将图像特征 I_f(维度 d_i)用投影矩阵 W_i 变换到目标嵌入空间,得到维度为 d_e 的新特征。文本特征同理。
-
l2_normalize(I_proj, axis=1): 对每个向量进行L2归一化。归一化后,每个向量的长度(模)都为1。
-
什么是 L2 归一化?
对于一个向量 v = [v1, v2, ..., vk],它的 L2 范数(也叫欧几里得长度或模或向量的长度)是:
||v||₂ = sqrt(v1² + v2² + ... + vk²)L2 归一化就是用这个向量的每一个元素除以它的 L2 范数(向量的长度):
normalized_v = v / ||v||₂ -
假设我们有两个原始向量(投影后的特征,但还未归一化):
向量 A: [3, 4]
向量 B: [6, 8]
我们可以在坐标系里画出它们:
向量 A:从 (0,0) 指向 (3,4)。
向量 B:从 (0,0) 指向 (6,8)。
你会发现,向量 B 只是向量 A 的一个拉长版。它们指向完全相同的方向,但是**长度(或称为“模”、“范数”)**不同。
我们来计算一下它们的长度(L2 范数):
A 的长度: sqrt(3² + 4²) = sqrt(9 + 16) = sqrt(25) = 5
B 的长度: sqrt(6² + 8²) = sqrt(36 + 64) = sqrt(100) = 10
所以,向量 A 长度为5,向量 B 长度为10。
单位球面:所有长度为1的向量的集合
在我们的二维世界里,“单位球面”其实就是一个单位圆,也就是以原点为中心、半径为1的圆。这个圆上所有的点,它们对应的向量长度都正好是 1。例如 (1,0), (0,1), (sqrt(2)/2, sqrt(2)/2) 等等。
L2 归一化:投影到单位球面上
现在,我们对向量 A 和 B 进行 L2 归一化。
归一化向量 A':
用向量 A 的每个元素除以它的长度 (5):
A' = [3/5, 4/5] = [0.6, 0.8]
我们来验证一下 A' 的新长度:
sqrt((0.6)² + (0.8)²) = sqrt(0.36 + 0.64) = sqrt(1) = 1
它的长度确实是1了!
归一化向量 B':
用向量 B 的每个元素除以它的长度 (10):
B' = [6/10, 8/10] = [0.6, 0.8]
我们发现,B' 和 A' 是完全一样的向量!
几何上发生了什么?
我们将向量 A(长度为5)“压缩”到了单位圆上,得到了向量 A'。
我们将向量 B(长度为10)也“压缩”到了单位圆上,得到了向量 B'。
因为 A 和 B 原本就指向同一个方向,所以它们被“压缩”到了单位圆上的同一个点 (0.6, 0.8)。
-
-
-
为什么归一化: 这是为了方便计算余弦相似度 (Cosine Similarity)。对于两个长度为1的向量,它们的点积 (dot product) 就等于它们的余弦相似度。这使得相似度度量只关注向量的“方向”(语义内容),而不受其“长度”(幅度)的影响,让训练更稳定。
-
这段话怎么理解呢?
-
先看公式。对于两个向量 A 和 B:
-
点积 (Dot Product): A · B = A_x * B_x + A_y * B_y + ...
-
这是一个简单的乘加运算。
-
-
余弦相似度 (Cosine Similarity): cos(θ) = (A · B) / (||A|| * ||B||)
-
||A|| 和 ||B|| 是向量 A 和 B 的长度 (magnitude)。
-
θ 是两个向量之间的夹角。
-
-
关键观察:余弦相似度就是被向量长度归一化了的点积。
-
“长度为1的向量,点积等于余弦相似度”
现在,如果向量 A 和 B 已经被L2归一化了,这意味着它们的长度都是1:
||A|| = 1
||B|| = 1
把这个代入余弦相似度的公式:
cos(θ) = (A · B) / (1 * 1) = A · B
这就是那句话的直接含义:一旦你把向量的长度都变成1,你就不再需要做那个除法了。计算一个简单的点积,得到的结果就是这两个向量的余弦相似度。这不仅在数学上更优雅,在计算机里也更快,因为乘法比除法和开方(计算长度需要)要快得多。
-
现在到了最关键的部分:为什么要关注“方向”而不是“长度”?
假设我们有一个文本向量和两个图片向量:
文本 T: [10, 0] (代表"猫",这个向量方向在x轴正向,长度为10)
图片 A: [2, 0] (是一张"猫"的图片,方向也在x轴正向,但因为图片模糊,encoder给出的向量长度只有2)
图片 B: [6, 8] (是一张"狗"的图片,方向完全不同,但因为图片非常清晰,encoder给出了一个长度很长的向量,长度为 sqrt(6²+8²) = 10)
从人的角度看:文本 T ([10, 0], "猫") 显然应该和图片 A ([2, 0], "猫") 更相似。
如果只用点积来判断相似度:
T 和 A 的点积: (10 * 2) + (0 * 0) = 20
T 和 B 的点积: (10 * 6) + (0 * 8) = 60
结论是灾难性的! 点积的结果显示,文本 T ("猫") 和图片 B ("狗") 的相似度 (60) 远远高于和图片 A ("猫") 的相似度 (20)。这是因为图片 B 的向量长度(幅度)太大了,完全主导了计算结果,即使它的方向是错的。
-
现在,我们先对所有向量进行L2归一化,把它们的长度都变成1。
归一化 T': [10, 0] 除以其长度10 -> [1, 0]
归一化 A': [2, 0] 除以其长度2 -> [1, 0]
归一化 B': [6, 8] 除以其长度10 -> [0.6, 0.8]
现在,我们用归一化后的向量计算点积(也就是余弦相似度):
T' 和 A' 的相似度: (1 * 1) + (0 * 0) = 1
T' 和 B' 的相似度: (1 * 0.6) + (0 * 0.8) = 0.6
结论是完美的! 余弦相似度正确地告诉我们,文本 T 和图片 A 的相似度是 1(方向完全相同,完美匹配),而和图片 B 的相似度只有 0.6(方向不匹配)。
-
-
结果:
-
I_e 和 T_e 都是形状为 [n, d_e] 的矩阵。现在,图像和文本的特征向量终于来到了同一个维度、同一个共享空间,可以相互比较了!
-
第三步:计算所有可能的图文对的相似度
# 3、计算图片-文字向量的余弦相似度
logits = np.dot(I_e, T_e.T) * np.exp(t) # [n, n]
-
发生了什么:
-
T_e.T: 将文本特征矩阵 T_e([n, d_e])转置为 [d_e, n]。
-
np.dot(I_e, T_e.T): 计算 [n, d_e] 矩阵和 [d_e, n] 矩阵的点积,得到一个 [n, n] 的相似度矩阵。
-
-
这个 [n, n] 矩阵是什么:
-
logits[i, j] 的值代表第 i 张图片和第 j 段文本的余弦相似度。
-
对角线元素 (i == j):logits[0,0], logits[1,1], logits[2,2]... 这些是正样本对(猫图 vs 猫描述,狗图 vs 狗描述)的相似度。我们的目标是让这些值尽可能大。
-
非对角线元素 (i != j):logits[0,1], logits[1,0], logits[2,1]... 这些是负样本对(猫图 vs 狗描述,狗图 vs 车描述)的相似度。我们的目标是让这些值尽可能小。
-
-
* np.exp(t):用温度参数 t 缩放 logits,为下一步的 softmax 计算做准备。
-
举一个例子,例如计算出了未经温度缩放的相似度矩阵 logits_raw:
logits_raw =
T_0("猫") T_1("狗") T_2("车")
+---------------------------------+
I_0("猫") | 0.90 | 0.10 | 0.00 |
+---------------------------------+
I_1("狗") | 0.10 | 0.90 | 0.00 |
+---------------------------------+
I_2("车") | 0.00 | 0.00 | 0.98 |
+---------------------------------现在,我们引入可学习的温度参数 t。记住,在CLIP的实现中,t 是一个标量(单个数值),并且模型实际学习的是 log(t),但为了计算方便,我们直接使用 t。这个 t 是通过 np.exp() 来应用的,所以我们计算的是 scaling_factor = np.exp(t)。np.exp() 是自然指数函数 e^x。
假设模型学到的温度参数 t 是 3.9。 (这是一个比较典型的log-scale值)
-
计算缩放因子:
scaling_factor = np.exp(t) = np.exp(3.9) ≈ 50 -
用缩放因子乘以 logits_raw 矩阵中的每一个元素:
logits = logits_raw * 50 -
T_0("猫") T_1("狗") T_2("车")
+---------------------------------------------+
I_0("猫") | 0.90 * 50 = 45.0 | 0.10 * 50 = 5.0 | 0.00 * 50 = 0.0 |
+---------------------------------------------+
I_1("狗") | 0.10 * 50 = 5.0 | 0.90 * 50 = 45.0| 0.00 * 50 = 0.0 |
+---------------------------------------------+
I_2("车") | 0.00 * 50 = 0.0 | 0.00 * 50 = 0.0 | 0.98 * 50 = 49.0|
+---------------------------------------------+ -
差距被放大了:
-
在第一行,"猫"图片与"猫"文本的得分(45.0)和与"狗"文本的得分(5.0)之间的差距,从原来的 0.90 - 0.10 = 0.8,变成了 45.0 - 5.0 = 40.0。
-
这个巨大的差距在下一步计算Softmax概率时,会让模型对正确答案的预测变得极其自信,概率分布会非常尖锐。
-
-
惩罚更严厉:如果模型搞错了,比如把"猫"图片和"狗"文本的相似度预测得很高,那么经过温度缩放后,这个错误也会被放大,从而在计算损失时产生一个非常大的惩罚信号(梯度),迫使模型更快地修正错误。
-
之前留了一个坑位,“在计算softmax时,较低的温度会使概率分布更“尖锐”,使模型更专注于区分最相似的负样本。”这句话怎么理解?
-
我们把这个问题拆成两部分来理解:
“较低的温度会使概率分布更‘尖锐’”是什么意思?(数学上的效果)
“使模型更专注于区分最相似的负样本”是什么意思?(训练上的意义)
-
1. “尖锐”的概率分布:一个放大镜的比喻
首先,我们来看带有温度 T 的 Softmax 函数:
Probability(i) = exp(logit_i / T) / Σ exp(logit_j / T)
这里的 logit 就是我们之前算出的相似度得分。T 就是温度参数。
让我们用一个简单的例子来看温度 T 的作用。假设我们有一张“猫”的图片,它和三个文本的相似度得分(logits)分别是:
与 "一只猫" (正样本): 5
与 "一只狗" (相似的负样本): 4.5 (分数也很高,因为它俩都是动物)
与 "一辆车" (不相似的负样本): 1
情况一:标准温度 T = 1
exp(5/1) = 148.4
exp(4.5/1) = 90.0
exp(1/1) = 2.7
分母(总和) = 148.4 + 90.0 + 2.7 = 241.1
概率分布:
P("猫") = 148.4 / 241.1 ≈ 61.5%
P("狗") = 90.0 / 241.1 ≈ 37.3%
P("车") = 2.7 / 241.1 ≈ 1.2%
解读:这是一个“平滑”或“柔软”的分布。模型认为“猫”的可能性最大,但它也给了“狗”相当大的概率。
-
情况二:低温 T = 0.1 (CLIP论文中用的就是可学习的低温)
5 / 0.1 = 50
4.5 / 0.1 = 45
1 / 0.1 = 10
exp(50), exp(45), exp(10) 的值会变得极其巨大,但它们之间的差距被指数级放大了。exp(50) 比 exp(45) 大约 exp(5) ≈ 148倍!
概率分布(近似计算):
P("猫") ≈ 99.3%
P("狗") ≈ 0.7%
P("车") ≈ 几乎为 0
解读:这是一个“尖锐”(sharp)或“尖峰”(spiky)的分布。模型几乎把100%的信心都给了得分最高的那个选项。低温就像一个放大镜,它极大地放大了最高分和其他分数之间的差距。
情况三:高温 T = 10
5 / 10 = 0.5
4.5 / 10 = 0.45
1 / 10 = 0.1
exp(0.5) = 1.65, exp(0.45) = 1.57, exp(0.1) = 1.1。它们的值非常接近。
概率分布会趋向于均匀分布,比如 P("猫")≈38%, P("狗")≈36%, P("车")≈26%。
解读:模型变得非常不确定,无法有效地区分好坏。
总结:T 控制着模型对 logits 的“信心”。
T -> 0: 赢家通吃,分布极度尖锐。
T -> ∞: 雨露均沾,分布趋于均匀。
2. 专注于“最相似的负样本”(Hard Negatives)
现在我们理解了低温的数学效果,再来看它在训练中的意义。
在我们的例子中:
正样本 (Positive Sample): "一只猫"
难区分的负样本 (Hard Negative Sample): "一只狗" (因为它和猫很像)
易区分的负样本 (Easy Negative Sample): "一辆车" (因为它和猫完全不像)
训练的目标是让模型给出正确的概率分布,即 P("猫") 尽可能接近100%,其他的都接近0。
在低温(T=0.1)的情况下:
惩罚被放大:模型的预测是 P("猫")=99.3%。虽然已经很高了,但还没到100%。计算损失(如交叉熵损失 -log(P_correct))时,模型仍然会受到惩罚,并试图让这个概率进一步接近100%。
梯度集中在“挑战者”身上:为了让P("猫")变得更高,模型必须让P("狗")和P("车")变得更低。由于"车"的原始得分已经很低了,在低温放大后,它的概率几乎为0,对损失的贡献也微乎其微。模型从中学不到什么新东西。
真正的“敌人”是“狗”:那个得分高达4.5的“狗”文本,才是阻止P("猫")达到100%的主要障碍。因为它的得分和正样本最接近,所以它成了最难区分的负样本。
模型被迫学习细微差别:低温放大了“猫”(5分)和“狗”(4.5分)之间的微小差距,使得模型在计算损失和梯度时,几乎所有的“注意力”都集中在如何进一步拉开这两者的得分上。它必须学习到更细微的特征(比如猫的胡须、耳朵的形状)来区分这两个相似的概念。它被迫去回答:“到底是什么让猫成为猫,而不是狗?”
反之,如果用高温,模型会给“猫”和“狗”差不多的概率,它受到的惩罚信号会很模糊,不知道应该重点优化哪个部分。它可能会浪费很多精力去“推开”那个本就已经很远的“车”。
结论
所以,“较低的温度会使概率分布更‘尖锐’,使模型更专注于区分最相似的负样本”这句话的意思是:
通过用一个较低的温度参数来缩放相似度得分,我们人为地加大了模型区分任务的难度。这会迫使模型忽略那些已经很容易区分的、不相关的样本(Easy Negatives),而将全部的学习精力集中在如何区分那些语义上非常接近、最容易混淆的样本(Hard Negatives)上。这最终会训练出一个具有更强辨别能力的、更稳健的 embedding space。
第四步:计算对比损失 (Contrastive Loss)
# 4、计算Loss
labels = np.arange(n) # labels = [0, 1, 2, ..., n-1]
loss_i = cross_entropy_loss(logits, labels, axis=0)
loss_t = cross_entropy_loss(logits, labels, axis=1)
loss = (loss_i + loss_t)/2
这次我们用一个非常形象的比喻来搞懂这个计算Loss的部分。
想象一下,你是一位matchmaking app(婚恋配对应用)的AI算法。
在一个速配活动上,有 n=3 位男士 和 n=3 位女士。他们是预先配好对的:
-
男1 (图片I_0 - 猫) 配 女1 (文本T_0 - "猫")
-
男2 (图片I_1 - 狗) 配 女2 (文本T_1 - "狗")
-
男3 (图片I_2 - 车) 配 女3 (文本T_2 - "车")
第一步:计算所有人的“般配指数” - logits 矩阵
你作为AI,计算了每一位男士和每一位女士之间的“般配指数”(就是我们的logits矩阵)。
女1("猫") 女2("狗") 女3("车")
+---------------------------------+
男1("猫") | 45.0 | 5.0 | 0.0 |
+---------------------------------+
男2("狗") | 5.0 | 45.0 | 0.0 |
+---------------------------------+
男3("车") | 0.0 | 0.0 | 49.0 |
+---------------------------------+
-
对角线 ([45.0, 45.0, 49.0]) 是正确配对的般配指数。
-
非对角线是错误配对的指数。
第二步:拿出“正确答案”小抄 - labels = np.arange(n)
np.arange(n) 生成了一个数组 [0, 1, 2, ...]. 在 n=3 的例子里,labels = [0, 1, 2]。
这个 labels 就是“正确答案”小抄,它告诉你:
-
男1 (索引为0) 的正确伴侣是 女1 (索引为0)。
-
男2 (索引为1) 的正确伴侣是 女2 (索引为1)。
-
男3 (索引为2) 的正确伴-侣是 女3 (索引为2)。
它完美地对应了我们“般配指数”矩阵的对角线位置。
第三步:计算Loss(计算“不满度”)
你的目标是让正确配对的指数最高。如果不是,就说明有“不满”,就要计算“不满度”(也就是Loss)。我们从两个角度来计算:
1. 从男士的角度计算不满度 (loss_t, axis=1 按行计算)
我们挨个去问每一位男士。
-
问男1 (第0行 [45.0, 5.0, 0.0]):
-
“在你看来,女1、女2、女3谁最适合你?”
-
模型给出的分数是 [45.0, 5.0, 0.0]。通过Softmax转换成概率,模型会说:“我 99.9% 的把握认为是女1!”
-
我们拿出小抄 labels[0],正确答案是0(女1)。
-
模型答对了!所以男1的“不满度”(Loss)非常低。
-
-
问男2 (第1行 [5.0, 45.0, 0.0]):
-
“你觉得谁最适合?”
-
模型会说:“我 99.9% 的把握认为是女2!”
-
我们拿出小抄 labels[1],正确答案是1(女2)。
-
模型又答对了!男2的“不满度”也非常低。
-
loss_t 就是把所有男士的“不满度”加起来求平均。因为大家都很满意,所以 loss_t 很低。
axis=1 的意思就是按行(row-wise)进行这个“提问-比较-计算不满度”的过程。
2. 从女士的角度计算不满度 (loss_i, axis=0 按列计算)
现在,我们反过来,挨个去问每一位女士。
-
问女1 (第0列 [45.0, 5.0, 0.0]):
-
“在你看来,男1、男2、男3谁最适合你?”
-
模型给出的分数是 [45.0, 5.0, 0.0]。它会说:“我 99.9% 的把握认为是男1!”
-
我们拿出小抄 labels[0],正确答案是0(男1)。
-
模型答对了!所以女1的“不满度”(Loss)非常低。
-
-
问女2 (第1列 [5.0, 45.0, 0.0]):
-
“你觉得谁最适合?”
-
模型会说:“我 99.9% 的把握认为是男2!”
-
我们拿出小抄 labels[1],正确答案是1(男2)。
-
模型又答对了!女2的“不满度”也非常低。
-
loss_i 就是把所有女士的“不满度”加起来求平均。
axis=0 的意思就是按列(column-wise)进行这个过程。
第四步:计算总不满度 - loss = (loss_i + loss_t)/2
为什么要做两次?因为一个好的配对算法,必须满足双向奔赴。
-
男1觉得女1最好。(loss_t 关注的)
-
女1也得觉得男1最好。(loss_i 关注的)
我们把从男士角度算出的总不满度 loss_t 和从女士角度算出的总不满度 loss_i 加起来求个平均,就得到了整个速配活动的总不满度 loss。
模型训练的目标,就是通过不断调整参数,来最小化这个总的“不满度”,让所有正确配对的“般配指数”都尽可能地成为自己那一行和那一列的最高分。
更多推荐
所有评论(0)