最近在看猛猿的大模型系列文章,学到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。

模型训练的目标,就是通过不断调整参数,来最小化这个总的“不满度”,让所有正确配对的“般配指数”都尽可能地成为自己那一行和那一列的最高分。

Logo

更多推荐