本文目录导读:

对比损失(Contrastive Loss)中的 InfoNCE(Info Noise-Contrastive Estimation)是自监督学习(特别是对比学习)中最核心的损失函数之一,它由 Oord 等人于 2018 年在论文《Representation Learning with Contrastive Predictive Coding》中提出。
下面从原理、数学公式、直观理解及实战技巧等方面进行详细解析。
核心思想
InfoNCE 的目标是在一个高维空间中,拉近“正样本对”的距离,同时推远“负样本对”的距离。
在自监督学习场景中(如 SimCLR、MoCo):
- 正样本对:同一张图片经过不同数据增强(如裁剪、颜色抖动)后的两个视图。
- 负样本对:不同图片的视图。
InfoNCE 本质上是一个分类任务:模型需要从一大群候选样本中,正确地找出那个唯一的正样本。
数学公式
InfoNCE 损失函数的典型形式如下:
[ L = -\log \frac{ \exp(\text{sim}(q, k+) / \tau) }{ \exp(\text{sim}(q, k+) / \tau) + \sum_{i=1}^{N} \exp(\text{sim}(q, k_i^-) / \tau) } ]
- ( q ):查询(Query)向量,例如第一张图的表征。
- ( k_+ ):正键(Positive Key),与 ( q ) 来自同一原始样本的表征。
- ( k_i^- ):负键(Negative Keys),来自不同样本的表征。
- ( \text{sim}(u, v) ):相似度函数,常用余弦相似度。
- ( \tau ):温度系数(Temperature),一个超参数,控制分布的平滑程度。
- ( N ):负样本的数量。
关键点:分母中的求和项包括了 1 个正样本 + N 个负样本,总共 (N+1) 个样本,取 log 后,实际上是一个交叉熵损失,类别数为 (N+1),正样本对应的标签为 0。
直观理解
把 InfoNCE 想象成一场考试:
- Query(问题):
q代表“请从下面 K 个选项中,选出与我对应的那张图片”。 - Positive Key(正确答案):
k+是正确答案。 - Negative Keys(干扰项/错误答案):
k-是其他所有不相干的图片。 - Softmax 分类器:模型需要计算所有这些选项与问题
q的匹配概率,并最大化正确答案的概率。
最小化 InfoNCE 损失,就是让模型:
- 最大化 ( \text{sim}(q, k_+) ) (让正确答案与问题高度相关)。
- 最小化 所有 ( \text{sim}(q, k_i^-) ) (让错误答案与问题不相关)。
为什么叫 InfoNCE?
- NCE(Noise-Contrastive Estimation):将正样本视为“信号”,负样本视为“噪声”,通过对比信号和噪声来学习特征,NCE 解决了传统 Softmax 在类别数极大(如百万级词汇表)时计算量过大的问题。
- InfoNCE 的改进:InfoNCE 将 NCE 的思想引入到互信息(Mutual Information)的最大化中。Info 代表它是在优化一个互信息的下界,优化 InfoNCE 等价于最大化 Query (X) 和 Key (Y) 之间的互信息 (I(X; Y))。
关键超参数:温度系数 (\tau)
温度系数 (\tau) 的作用非常关键:
- (\tau) 很小(如 0.07):
- 模型会更关注难分负样本(与 Query 相似度较高的负样本)。
- 梯度较大,训练更有挑战性,容易学到更细粒度的特征。
- 风险:容易导致模型崩溃或对噪声敏感。
- (\tau) 很大(如 1.0):
- 所有样本的概率分布会趋于均匀。
- 模型失去了区分正负样本的动力,难以收敛。
经验值:通常设置在 (0.07) 到 (0.2) 之间,在 SimCLR 论文中,最常用的值是 (0.5)。
与 Triplet Loss 的区别
| 特性 | Triplet Loss | InfoNCE |
|---|---|---|
| 样本组成 | (Anchor, Positive, Negative) 一个三元组 | (Query, 1个Positive, 多个Negative) |
| 负样本数量 | 1 个,或通过 Hard Mining 选取 | 大量(数千个),所有 Batch 内样本 |
| 处理策略 | 硬性给定一个 Margin:( \text{dist}(a,p) + \alpha < \text{dist}(a,n) ) | 基于 Softmax 的概率分布,无硬边界 |
| 计算效率 | 需要复杂采样策略 | 利用 In-Batch Negatives,计算高效 |
| 效果 | 对小 Batch 和特定任务有效 | 在自监督和大规模学习上显著更优 |
代码示例 (PyTorch)
以下是 SimCLR 中 InfoNCE 损失的标准实现:
import torch
import torch.nn.functional as F
def info_nce_loss(features, batch_size, temperature=0.5):
"""
Args:
features: Tensor of shape [2*batch_size, dim]
前batch_size个是view1,后batch_size个是view2
batch_size: 原始batch的大小
temperature: 温度系数
Returns:
loss: 标量损失
"""
# 1. 归一化特征向量
features = F.normalize(features, dim=1)
# 2. 计算相似度矩阵 (2N x 2N)
similarity_matrix = torch.matmul(features, features.T) / temperature
# 3. 构建标签:对于第i个样本,其正样本是i+batch_size(如果i< batch_size)
# 更简洁的方式:生成 (2N,) 的索引,正样本对是 (i, (i+N) % 2N)
labels = torch.cat([torch.arange(batch_size) for _ in range(2)], dim=0)
labels = (labels.unsqueeze(0) == labels.unsqueeze(1)).float()
# 将对角线设为0 (自己与自己的相似度不算作正样本,应该忽略)
mask = torch.eye(labels.shape[0], dtype=torch.bool)
labels = labels[~mask].reshape(labels.shape[0], -1)
similarity_matrix = similarity_matrix[~mask].reshape(similarity_matrix.shape[0], -1)
# 4. 选出一行中正样本对应的位置: 对于view1[i],其正样本在位置[i+batch_size]
# 由于移除了对角线,索引需要重新计算,更标准的方法是使用下面的方式:
positives = torch.cat([
similarity_matrix[:batch_size, batch_size:2*batch_size].diag(), # view1与view2对应
similarity_matrix[batch_size:2*batch_size, :batch_size].diag() # view2与view1对应
])
# 负样本:整行除了正样本和自身
# 对于 view1[i],负样本是: 所有除了 i 和 i+batch_size 的样本
# 构建选择 mask
negatives = []
for i in range(batch_size):
# view1的负样本: 排除 i (自身) 和 i+batch_size (正样本)
view1_neg = torch.cat([
similarity_matrix[i, :i],
similarity_matrix[i, i+1:batch_size],
similarity_matrix[i, batch_size:batch_size+i],
similarity_matrix[i, batch_size+i+1:2*batch_size]
])
# view2的负样本: 排除 batch_size+i (自身) 和 i (正样本)
view2_neg = torch.cat([
similarity_matrix[batch_size+i, :i],
similarity_matrix[batch_size+i, i+1:batch_size],
similarity_matrix[batch_size+i, batch_size:batch_size+i],
similarity_matrix[batch_size+i, batch_size+i+1:2*batch_size]
])
negatives.append(view1_neg)
negatives.append(view2_neg)
negatives = torch.stack(negatives) # shape [2N, 2N-2]
# 5. 计算损失: -log( exp(pos) / (exp(pos) + sum(exp(neg))) )
logits = torch.cat([positives.unsqueeze(1), negatives], dim=1) # shape [2N, 2N-1]
labels = torch.zeros(logits.shape[0], dtype=torch.long, device=features.device)
loss = F.cross_entropy(logits, labels)
return loss
实际应用中(如 PyTorch Lightning 的 SimCLR),有更高效的实现方式,SimCLR 论文中使用的 LARS 优化器和更大的 batch size(4096)。
- InfoNCE = 对比学习 + NCE 采样 + 互信息最大化。
- 它通过动态构造大量负样本(通常是 Batch 内的其他样本),将问题转化为一个多分类任务。
- 关键在于选择合适的温度系数 (\tau) 和负样本数量 (N)。