共计 2849 个字符,预计需要花费 8 分钟才能阅读完成。
为什么需要对比学习?
在传统的有监督学习(Supervised Learning)中,我们需要大量标注好的数据来训练模型。比如训练一个猫狗分类器,每张图片都需要打上 ” 猫 ” 或 ” 狗 ” 的标签。但在现实世界中,获取大量标注数据成本很高,特别是对于跨模态(Cross-Modal)任务,比如同时处理图像和文本数据。

对比学习(Contrastive Learning)提供了一种自监督(Self-Supervised)的学习方式,它不需要人工标注,而是让模型自己学习数据之间的关系。核心思想是:相似的样本在特征空间中应该靠近,不相似的样本应该远离。这在 CLIP(Contrastive Language-Image Pretraining)这种需要对齐图像和文本表示的任务中特别有用。
对比学习的数学原理
对比学习的核心是 InfoNCE 损失函数(Info Noise Contrastive Estimation),其公式如下:
$$
L = -\log\frac{\exp(sim(q,k_+)/\tau)}{\sum_{i=0}^K \exp(sim(q,k_i)/\tau)}
$$
其中:
– $q$ 是查询样本(query)
– $k_+$ 是正样本(positive key)
– $k_i$ 包括正样本和负样本(negative keys)
– $sim$ 是相似度函数(通常用余弦相似度)
– $\tau$ 是温度系数(temperature),控制分布的尖锐程度
温度系数 $\tau$ 是个关键超参数:
– 当 $\tau$ 很小时,模型会更关注困难的负样本
– 当 $\tau$ 很大时,所有样本的权重会更平均
PyTorch 实现对比学习
下面我们来看一个简化的 CLIP 实现,包含三个主要部分:编码器、正负样本构建和损失计算。
1. 编码器定义
import torch
import torch.nn as nn
import torch.nn.functional as F
# 简化版图像编码器(使用 ResNet 作为 backbone)class ImageEncoder(nn.Module):
def __init__(self, embed_dim=512):
super().__init__()
self.convnet = nn.Sequential(nn.Conv2d(3, 64, kernel_size=3, stride=2, padding=1),
nn.ReLU(),
nn.MaxPool2d(2),
# 更多卷积层...
)
self.projection = nn.Linear(2048, embed_dim) # 投影到统一维度
def forward(self, x):
features = self.convnet(x)
features = features.view(features.size(0), -1)
return F.normalize(self.projection(features), dim=1)
# 简化版文本编码器(使用 Transformer)class TextEncoder(nn.Module):
def __init__(self, vocab_size, embed_dim=512):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.transformer = nn.TransformerEncoderLayer(embed_dim, nhead=8)
self.projection = nn.Linear(embed_dim, embed_dim)
def forward(self, x):
x = self.embedding(x)
x = self.transformer(x)
return F.normalize(self.projection(x.mean(dim=1)), dim=1)
2. 正负样本构建
在对比学习中,同一个 batch 内不同样本自动成为彼此的负样本。比如对于 batch 内的第 i 个图像 - 文本对:
– 正样本:对应的配对文本
– 负样本:batch 内其他所有文本
3. 损失函数计算
def contrastive_loss(image_embeds, text_embeds, temperature=0.07):
# 计算相似度矩阵
logits = torch.matmul(image_embeds, text_embeds.t()) / temperature
# 创建标签:对角线元素是正样本
batch_size = image_embeds.shape[0]
labels = torch.arange(batch_size, device=image_embeds.device)
# 计算交叉熵损失
loss_i = F.cross_entropy(logits, labels) # 图像到文本
loss_t = F.cross_entropy(logits.t(), labels) # 文本到图像
return (loss_i + loss_t) / 2
超参数调优技巧
对比学习对超参数非常敏感,这里给出一些调优建议:
温度系数与 batch size 的关系
温度系数 $\tau$ 需要与 batch size 配合调整:
– 较大的 batch size 意味着更多负样本,可以适当降低 $\tau$
– 较小的 batch size 则需要提高 $\tau$ 避免训练不稳定
典型配置表
| 超参数 | 推荐值范围 | 说明 |
|---|---|---|
| batch size | 256-1024 | 越大越好,但受限于显存 |
| temperature | 0.05-0.2 | 常从 0.07 开始调 |
| 学习率 | 3e-4 – 1e-3 | 使用学习率 warmup |
| 投影维度 | 512-1024 | 影响模型容量 |
常见问题与解决方案
1. 模态坍缩(Mode Collapse)
现象 :所有样本都映射到相同的特征,损失不再下降。
解决方法 :
– 增加 batch size
– 调整温度系数
– 使用更强大的数据增强
2. 梯度爆炸
现象 :训练早期出现 NaN 值。
解决方法 :
– 使用梯度裁剪(gradient clipping)
– 降低学习率
– 检查编码器输出是否做了归一化
3. 负样本不足
现象 :模型无法区分相似样本。
解决方法 :
– 增大 batch size
– 使用 memory bank 存储历史负样本
– 人工构造更难负样本(hard negatives)
进阶改进方向
1. 动量编码器(Momentum Encoder)
原始 CLIP 使用对称的对比损失,可以引入动量编码器来稳定训练:
– 文本编码器使用动量更新版本
– 图像编码器保持正常更新
– 两者通过动量系数(如 0.999)缓慢同步
2. 难负样本挖掘(Hard Negative Mining)
不是随机选择负样本,而是:
1. 先计算所有样本相似度
2. 选择与正样本相似度较高的负样本
3. 这些 ” 困难 ” 负样本能提供更强的学习信号
结语
对比学习为跨模态表示学习提供了一种强大的自监督方法。通过理解 InfoNCE 损失、合理调参以及避免常见陷阱,即使是初学者也能成功训练 CLIP 类模型。希望这篇指南能帮助你快速入门,后续可以尝试在具体任务上微调模型,比如图像检索、文本到图像生成等应用场景。
