1. 从交叉熵到对比学习损失函数的演进之路我第一次接触交叉熵损失是在做图像分类项目时。当时觉得这个公式既简洁又强大但完全没想到它后来会成为理解对比学习损失函数的基础。交叉熵本质上衡量的是两个概率分布之间的差异这在监督学习中非常直观——我们想让模型预测的概率分布尽量接近真实标签的分布。但当我开始做自监督学习项目时发现事情变得不一样了。没有标注数据的情况下如何设计损失函数这就是InfoNCENoise Contrastive Estimation大显身手的地方。有趣的是虽然应用场景不同但InfoNCE和交叉熵在数学形式上有着惊人的相似性。这就像发现两个看似不相关的工具其实用的是同一套底层原理。2. 交叉熵损失分类任务的基石2.1 数学形式与直观理解交叉熵的公式看起来很简单def cross_entropy(p, q): return -sum(p_i * log(q_i) for p_i, q_i in zip(p, q))但这个简单的公式背后蕴含着丰富的信息论内涵。我常跟团队新人说理解交叉熵要把握三个关键点概率视角它比较的是两个完整概率分布而不是单个预测值不对称性交叉熵不是对称的交换p和q位置会得到不同的值下界特性当pq时取得最小值即真实分布与预测分布一致时在实际编码中我们通常使用PyTorch的实现import torch.nn as nn loss_fn nn.CrossEntropyLoss() logits model(inputs) # 未经softmax的输出 loss loss_fn(logits, labels)这里有个新手常踩的坑PyTorch的CrossEntropyLoss已经内置了softmax操作所以不需要也不应该在模型最后额外加softmax层。2.2 代码实现中的优化技巧在实现交叉熵时有几个优化点值得注意数值稳定性直接计算log(softmax(x))可能导致数值下溢。更好的做法是使用log_softmaxlog_probs torch.log_softmax(logits, dim-1)标签平滑防止模型对预测结果过于自信loss_fn nn.CrossEntropyLoss(label_smoothing0.1)类别权重处理不平衡数据集weights torch.tensor([1.0, 2.0, 1.5]) # 给少数类更高权重 loss_fn nn.CrossEntropyLoss(weightweights)3. InfoNCE自监督学习的核心损失3.1 从分类到对比的思想转变第一次看到InfoNCE公式时我被它的复杂性吓到了def info_nce_loss(q, k, temperature0.07): # q和k是正样本对的特征向量 logits torch.matmul(q, k.t()) / temperature labels torch.arange(len(q)).to(q.device) return F.cross_entropy(logits, labels)但当我拆解这个公式后发现它本质上还是在做分类任务——只不过是把同一样本的不同视角作为正类把其他样本作为负类。这种思想转变让我豁然开朗自监督学习其实是在创造一种特殊的分类任务。3.2 温度系数的魔法温度参数τ是InfoNCE中最关键的超参数之一。在我的实验中τ太大如1.0所有样本的相似度趋同模型难以学到有区分度的特征τ太小如0.01模型会过度关注困难负样本导致训练不稳定最佳范围通常在0.05到0.2之间具体取决于数据特性这里有个实用技巧可以动态调整τ值。我在一个项目中实现了线性warmupdef get_temperature(epoch, max_epochs): return 0.1 0.1 * (epoch / max_epochs) # 从0.1线性增加到0.24. 两者的内在联系与实现对比4.1 数学形式上的统一性仔细对比两个公式会发现惊人的相似性交叉熵-log(exp(x_true)/∑exp(x_all))InfoNCE-log(exp(sim_pos)/∑exp(sim_all))这揭示了它们共同的数学本质都是在用softmax归一化后计算负对数似然。区别仅在于交叉熵中正类由标注决定InfoNCE中正类由数据增强或时序关系决定4.2 代码实现的一脉相承在PyTorch中两者都可以用cross_entropy实现# 交叉熵标准用法 loss F.cross_entropy(logits, labels) # InfoNCE的巧妙用法 loss F.cross_entropy(similarity_matrix / tau, positive_indices)这种实现上的相似性不是巧合而是反映了它们在数学本质上的同源性。我在实现MoCo模型时就是基于这个洞察重用了大量分类任务的代码。5. 实践中的经验与技巧5.1 负样本的重要性在对比学习中负样本的数量和质量至关重要。我的经验是内存库使用memory bank存储负样本特征如MoCo的做法批量大小更大的batch size意味着更多隐式负样本困难样本挖掘关注那些相似度较高的负样本# 困难样本挖掘示例 similarity q k.t() hard_negatives similarity.topk(k10, dim1)[0][:, 1:] # 取top10但排除正样本5.2 混合精度训练的实现对比学习通常需要大批量训练混合精度可以显著节省显存scaler torch.cuda.amp.GradScaler() with torch.cuda.amp.autocast(): features model(inputs) loss info_nce_loss(features[:batch], features[batch:]) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()6. 进阶话题更高效的实现方式6.1 分布式训练技巧当使用多GPU训练时需要特别处理负样本收集# 使用all_gather收集所有GPU上的特征 def gather_tensors(tensor): gathered [torch.zeros_like(tensor) for _ in range(world_size)] torch.distributed.all_gather(gathered, tensor) return torch.cat(gathered)6.2 不对称架构设计SimCLR和MoCo v3都采用了不对称架构在线网络使用完整梯度更新目标网络使用动量更新# 动量更新示例 torch.no_grad() def update_target_network(online, target, m0.99): for param_o, param_t in zip(online.parameters(), target.parameters()): param_t.data param_t.data * m param_o.data * (1. - m)7. 从理论到实践完整案例让我们看一个完整的对比学习实现class ContrastiveLearner(nn.Module): def __init__(self, backbone): super().__init__() self.backbone backbone self.projector nn.Sequential( nn.Linear(2048, 4096), nn.ReLU(), nn.Linear(4096, 256) ) def forward(self, x1, x2): # 两个增强视图 z1 self.projector(self.backbone(x1)) z2 self.projector(self.backbone(x2)) # 归一化 z1 F.normalize(z1, dim1) z2 F.normalize(z2, dim1) # 计算损失 logits torch.matmul(z1, z2.t()) / 0.1 labels torch.arange(len(z1)).to(z1.device) loss F.cross_entropy(logits, labels) return loss这个实现包含了几个关键点投影头将特征映射到更适合对比学习的空间特征归一化确保相似度在合理范围对称的损失计算也可以只计算单边在实际项目中我发现这种基础架构加上适当的数据增强如RandomResizedCrop、ColorJitter就能在ImageNet上得到不错的线性评估结果。