对比学习损失函数实战:从InfoNCE到HCL的代码实现与调参技巧
最近在几个视觉项目里折腾对比学习,发现一个挺有意思的现象:很多朋友能把模型结构讲得头头是道,但一到损失函数这块,代码跑起来效果不对,调参时却总有种“盲人摸象”的感觉。要么是照着论文把公式敲进去,结果训练loss震荡得厉害;要么是看到别人代码里那些tau_plus、beta参数,完全不知道从何调起。
其实对比学习的核心魔力,很大程度上就封装在那个小小的损失函数里。它决定了模型如何看待样本之间的关系,如何从海量无标签数据中“挤”出有用的特征。今天我们不聊那些宏大的理论框架,就扎进代码里,把InfoNCE和它的“硬核”变体HCL掰开揉碎了看,重点聊聊怎么实现、为什么这么实现,以及调参时那些真正管用的技巧。如果你正在自己的项目里尝试对比学习,或者感觉损失函数这块总是雾里看花,那这篇文章或许能给你带来些不一样的视角。
1. 理解对比学习损失的核心:从相似度到概率分布
在动手写代码之前,我们得先搞清楚对比学习损失函数到底在优化什么。简单来说,它的目标非常直观:让相似的样本在特征空间里靠得更近,让不相似的样本离得更远。但这个“相似”和“不相似”的定义,以及如何量化地推动这个目标,就是损失函数要解决的核心问题。
想象一下,你有一堆未标注的图片。对比学习通常通过数据增强来创造“相似”的样本对——比如对同一张图片进行随机裁剪、颜色抖动,得到两个略有不同的版本,它们就被视为正样本对。而批次中其他所有图片,自然就成了负样本。损失函数的工作,就是为每个“锚点”样本计算一个分数:它和它的正样本伴侣应该得到高分,和所有负样本应该得到低分。
这里最关键的转变,是把相似度分数转换成了一个概率分布。我们并不是直接最小化锚点与负样本的相似度,而是让模型学会一种“分类”能力:在所有候选样本(一个正样本 + 多个负样本)中,正确识别出那个唯一的正样本。这种视角的转换,让优化过程变得更加稳定和有效。
提示:很多初学者会混淆“相似度”和“距离”。在对比学习中,我们通常使用余弦相似度或点积作为相似度度量,值越大表示越相似。这与欧氏距离(值越小越相似)正好相反,在实现时务必注意。
1.1 InfoNCE:对比学习的标准“配方”
InfoNCE(Information Noise-Contrastive Estimation)损失,可以说是对比学习领域最广为使用的“标准配方”。它的形式优雅,而且有扎实的信息论基础——旨在最大化正样本对之间的互信息。
它的数学表达式看起来是这样的:
[ \mathcal{L} = -\log \frac{\exp(\text{sim}(q, k_+) / \tau)}{\sum_{i=0}^{K} \exp(\text{sim}(q, k_i) / \tau)} ]
别被公式吓到,我们拆开看:
sim(q, k)是查询样本q和关键样本k的相似度,常用点积或余弦相似度。k_+是那个唯一的正样本关键向量。- 分母是对所有样本(1个正样本 + K个负样本)的相似度进行指数求和。
τ(tau) 是一个超参数,通常称为温度系数,它控制着概率分布的“尖锐”程度。
这个公式在代码里通常有两种实现路径,它们看似不同,实则等价,理解这种等价性对灵活运用至关重要。
第一种,也是更“深度学习框架原生”的方式,是利用交叉熵损失。我们来看看这种视角下的实现逻辑:
import torch import torch.nn as nn def info_nce_loss_crossentropy(query, positive_key, negative_keys, temperature=0.07): """ 使用交叉熵损失实现InfoNCE。 query: [batch_size, feature_dim] positive_key: [batch_size, feature_dim] - 与query对应的正样本 negative_keys: [batch_size, num_negatives, feature_dim] - 负样本池 """ batch_size, feat_dim = query.shape num_negatives = negative_keys.shape[1] # 计算相似度:query与所有key(正+负)的点积 # 正样本相似度: [batch_size, 1] pos_sim = torch.sum(query * positive_key, dim=-1, keepdim=True) # 点积 # 负样本相似度: [batch_size, num_negatives] # 这里将query扩展以匹配负样本的维度 query_expanded = query.unsqueeze(1) # [batch_size, 1, feat_dim] neg_sim = torch.sum(query_expanded * negative_keys, dim=-1) # [batch_size, num_negatives] # 拼接成正负样本的logits: [batch_size, 1 + num_negatives] logits = torch.cat([pos_sim, neg_sim], dim=1) # 正样本放在第0列 # 应用温度系数 logits = logits / temperature # 标签:对于每个query,正样本在logits中的索引是0 labels = torch.zeros(batch_size, dtype=torch.long, device=query.device) # 使用交叉熵损失 # PyTorch的CrossEntropyLoss内部会做softmax和负对数似然 loss_fn = nn.CrossEntropyLoss() loss = loss_fn(logits, labels) return loss这种实现巧妙的地方在于,它把对比学习问题重构成了一个多类分类问题。对于每个锚点查询query,我们把所有候选样本(一个正样本和多个负样本)的相似度分数排列起来,形成一个“logits”向量。然后,我们告诉模型:“正确答案是这个向量里的第0个元素(正样本)”。交叉熵损失会自然地计算softmax概率,并惩罚模型没有给正样本分配最高概率的情况。
为什么标签是全零?这是理解这种实现的关键。因为我们在构造logits时,刻意把正样本的相似度放在了所有向量的第0个位置(通过torch.cat([pos_sim, neg_sim], dim=1))。所以标签为0,意味着对于每个样本,我们希望模型预测的“正确类别”索引是0,即正样本所在的位置。
1.2 温度系数τ:不起眼却至关重要的“调节阀”
在继续深入之前,我们必须单独谈谈温度系数τ。这个看起来只是个分母上的缩放因子,实际上对对比学习的性能有着极其显著的影响。
τ控制着softmax分布的熵。具体来说:
- τ值较小(如0.05):softmax输出会更“尖锐”,概率质量更集中在相似度最高的样本上。这会让模型更关注那些最难区分的负样本(即与锚点相似度较高的负样本),从而学习到更精细的判别特征。
- τ值较大(如0.5):分布更平缓,所有样本得到的概率更均匀。这会降低对困难负样本的惩罚力度,可能导致模型学到的特征区分度不够。
在实际项目中,τ往往是需要仔细调整的超参数之一。我个人的经验是:
| 任务类型 | 推荐τ范围 | 调整策略 |
|---|---|---|
| 图像分类(ImageNet级别) | 0.07 - 0.1 | 从0.07开始,如果训练不稳定可适当增大 |
| 细粒度图像检索 | 0.05 - 0.07 | 更小的τ有助于学习细微差别 |
| 文本语义匹配 | 0.05 - 0.1 | 对负样本质量敏感,需配合负采样策略 |
| 多模态学习 | 0.05 - 0.2 | 跨模态差异大,可能需要更大的τ |
一个实用的调试技巧:在训练初期,监控正样本对的平均相似度。如果这个值非常低(比如小于0.1),可能需要增大τ来避免梯度消失;如果相似度已经很高(大于0.8),可以尝试减小τ来挖掘困难负样本。
2. HCL:当负样本“硬”起来
标准的InfoNCE假设所有负样本都是“平等”的——在分母中,每个负样本的贡献只是简单的指数相似度求和。但现实中,负样本的“硬度”是不同的:有些负样本与锚点非常相似,容易让模型混淆;有些则完全不同,对模型来说区分起来轻而易举。
HCL(Hard Contrastive Learning)的核心思想就是:不是所有负样本都同等重要,我们应该特别关注那些“硬”的负样本。这里的“硬”指的是与锚点相似度较高的负样本,它们对模型来说更难区分,因此也蕴含了更多的学习信号。
2.1 HCL的数学直觉
HCL在InfoNCE的基础上,引入了一个重新加权机制。原始的InfoNCE损失可以看作:
[ \mathcal{L} = -\log \frac{\exp(s_+ / \tau)}{\exp(s_+ / \tau) + \sum_{i=1}^{N} \exp(s_i^- / \tau)} ]
其中 ( s_+ ) 是正样本相似度,( s_i^- ) 是第i个负样本的相似度。
HCL的修改在于分母中的负样本项。它不再是简单的求和,而是变成了:
[ \text{负样本项} = \frac{\sum_{i=1}^{N} w_i \cdot \exp(s_i^- / \tau)}{\frac{1}{N}\sum_{i=1}^{N} w_i} ]
这里的权重 ( w_i ) 与负样本的“硬度”相关——相似度越高的负样本,权重越大。这种重新加权带来了两个关键效果:
- 困难负样本被放大:与锚点更相似的负样本会对损失产生更大影响,迫使模型学习更精细的区分能力。
- 梯度重新分配:在反向传播时,模型会从困难负样本获得更强的梯度信号。
2.2 HCL的代码实现:拆解与优化
让我们直接看一个完整的HCL实现,我会逐段解释关键部分:
import torch import numpy as np def hard_contrastive_loss(features, temperature=0.5, tau_plus=0.1, beta=1.0, estimator='hard'): """ HCL损失函数的完整实现。 features: 经过L2归一化的特征向量,形状为[2*batch_size, feature_dim] 前batch_size个是原始样本,后batch_size个是增强样本 """ device = features.device batch_size = features.shape[0] // 2 # 1. 计算所有样本对之间的相似度矩阵 # 特征已经归一化,所以点积就是余弦相似度 sim_matrix = torch.mm(features, features.t()) # [2*batch_size, 2*batch_size] # 2. 创建掩码,排除不相关的样本对 # 我们需要一个掩码来选出有效的负样本对 mask = get_negative_mask(batch_size).to(device) # 3. 提取负样本相似度并应用指数 neg = torch.exp(sim_matrix / temperature) # 使用掩码选择有效的负样本对 neg = neg.masked_select(mask).view(2 * batch_size, -1) # [2*batch_size, num_negatives] # 4. 计算正样本相似度 # 正样本对:每个样本与其增强版本 pos = torch.exp(torch.sum(features[:batch_size] * features[batch_size:], dim=-1) / temperature) pos = torch.cat([pos, pos], dim=0) # 扩展到所有样本 # 5. 负样本重新加权(HCL的核心) if estimator == 'hard': N = batch_size * 2 - 2 # 每个样本的负样本数量 # 计算重要性权重:基于负样本相似度的β次方 imp = (beta * torch.log(neg)).exp() # 重新加权的负样本项 reweight_neg = (imp * neg).sum(dim=-1) / imp.mean(dim=-1) # 计算Ng:调整后的负样本贡献 Ng = (-tau_plus * N * pos + reweight_neg) / (1 - tau_plus) # 可选:对Ng进行下界约束,避免数值问题 Ng = torch.clamp(Ng, min=N * np.exp(-1 / temperature)) elif estimator == 'easy': # 回退到标准InfoNCE:简单求和 Ng = neg.sum(dim=-1) else: raise ValueError("estimator必须是'hard'或'easy'") # 6. 计算最终损失 loss = (-torch.log(pos / (pos + Ng))).mean() return loss def get_negative_mask(batch_size): """ 生成负样本掩码。 对于大小为2*batch_size的批次,我们需要排除: 1. 对角线(自己与自己) 2. 正样本对(样本i与样本i+batch_size) """ mask = torch.ones((2*batch_size, 2*batch_size), dtype=bool) # 排除对角线 mask.fill_diagonal_(0) # 排除正样本对 for i in range(batch_size): mask[i, i + batch_size] = 0 mask[i + batch_size, i] = 0 return mask这段代码有几个关键点值得深入讨论:
负样本掩码的设计:在对比学习中,我们通常使用一个批次内的所有其他样本作为负样本。但需要小心排除:
- 样本自身(对角线)
- 样本的正样本对(原始样本与其增强版本)
重新加权机制:imp = (beta * torch.log(neg)).exp()这行代码是HCL的灵魂。让我们拆解一下:
- 首先取
neg的对数(neg已经是exp(相似度/τ)) - 乘以β系数控制重新加权的强度
- 再取指数,得到最终的重要性权重
β控制着对困难负样本的强调程度:β越大,高相似度的负样本获得的权重就越大。
tau_plus的作用:这个参数引入了对正样本的“补偿”。在原始论文中,作者发现直接重新加权负样本可能会导致模型过于关注负样本而忽略正样本。tau_plus实际上在负样本项中减去了一部分正样本的贡献,从而保持正负样本学习的平衡。
3. 实战调参:从理论到效果的跨越
理解了实现原理后,我们进入最实用的部分:如何调参才能让对比学习真正work起来?我结合自己在多个项目中的经验,总结了一些可操作的调参策略。
3.1 温度系数τ的精细调整
τ可能是对比学习中最敏感的参数。我的调参流程通常是这样的:
初始探测:先在一个小数据集上运行几个epoch,尝试不同的τ值(如0.05, 0.07, 0.1, 0.2),观察:
- 训练损失的收敛速度
- 正样本对平均相似度的变化
- 验证集上的下游任务性能(如果有的话)
基于相似度分布调整:训练几个epoch后,查看正负样本相似度的分布:
# 监控相似度分布的代码片段 with torch.no_grad(): # 计算正样本相似度 pos_sim = torch.sum(features[:batch_size] * features[batch_size:], dim=-1) # 计算负样本相似度(随机采样一部分) neg_sim = torch.mm(features[:10], features[10:].t()) # 示例:只计算前10个 print(f"正样本相似度 - 均值: {pos_sim.mean():.4f}, 标准差: {pos_sim.std():.4f}") print(f"负样本相似度 - 均值: {neg_sim.mean():.4f}, 范围: [{neg_sim.min():.4f}, {neg_sim.max():.4f}]")如果正样本相似度均值低于0.2,可能需要增大τ;如果高于0.8,可能需要减小τ。
动态调整策略:在一些最新研究中,有人尝试在训练过程中动态调整τ。一个简单但有效的方法是:
# 随着训练进行,逐渐降低τ,让模型越来越关注困难样本 def dynamic_temperature(epoch, total_epochs, base_temp=0.1, min_temp=0.05): # 线性衰减 return base_temp - (base_temp - min_temp) * (epoch / total_epochs) # 或者在每个epoch内根据相似度自适应调整 def adaptive_temperature(pos_sim_mean, base_temp=0.07): # 如果正样本相似度太低,增大τ帮助稳定训练 if pos_sim_mean < 0.1: return base_temp * 1.5 elif pos_sim_mean > 0.7: return base_temp * 0.7 else: return base_temp
3.2 HCL参数的协同优化
当使用HCL时,我们有了更多的控制参数,但也需要更小心的调参。β和tau_plus之间存在相互作用:
β(重新加权强度):
- β = 1.0:论文中的默认值,通常是个不错的起点
- β < 1.0:降低对困难负样本的强调,更接近标准InfoNCE
- β > 1.0:更加强调困难负样本,适合负样本质量较高的场景
tau_plus(正样本补偿):
- tau_plus = 0:没有补偿,完全依赖重新加权
- tau_plus = 0.1:论文默认值,通常效果较好
- tau_plus > 0.1:更多补偿,当发现模型过于关注负样本时使用
我的调参经验是采用网格搜索+早停的策略:
# 一个简单的参数网格搜索框架 param_grid = { 'temperature': [0.05, 0.07, 0.1], 'beta': [0.5, 1.0, 1.5], 'tau_plus': [0.05, 0.1, 0.2] } best_params = None best_loss = float('inf') for temp in param_grid['temperature']: for beta in param_grid['beta']: for tau_plus in param_grid['tau_plus']: # 在小验证集上快速评估 val_loss = evaluate_params(temp, beta, tau_plus, num_epochs=5) if val_loss < best_loss: best_loss = val_loss best_params = {'temperature': temp, 'beta': beta, 'tau_plus': tau_plus} print(f"最佳参数组合: {best_params}, 验证损失: {best_loss:.4f}")3.3 批次大小与负样本数量的权衡
对比学习的效果很大程度上依赖于负样本的数量和质量。更大的批次意味着更多的负样本,但也会带来内存压力。这里有几个实用策略:
梯度累积:当GPU内存有限时,可以通过梯度累积来模拟大批次训练:
accumulation_steps = 4 # 累积4个小批次的梯度 optimizer.zero_grad() for i, (images, _) in enumerate(train_loader): features = model(images) loss = contrastive_loss(features) # 缩放损失,因为梯度会累积 loss = loss / accumulation_steps loss.backward() # 每accumulation_steps步更新一次 if (i + 1) % accumulation_steps == 0: optimizer.step() optimizer.zero_grad()负样本队列:MoCo提出的方法,维护一个动态的负样本队列:
class NegativeQueue: def __init__(self, feature_dim, queue_size=65536): self.queue_size = queue_size self.queue = torch.randn(queue_size, feature_dim) self.ptr = 0 def enqueue_dequeue(self, new_features): """入队新特征,出队旧特征""" batch_size = new_features.shape[0] # 替换队列中的一部分 self.queue[self.ptr:self.ptr+batch_size] = new_features # 更新指针 self.ptr = (self.ptr + batch_size) % self.queue_size def get_negatives(self, batch_size): """随机采样负样本""" indices = torch.randint(0, self.queue_size, (batch_size,)) return self.queue[indices]困难负样本挖掘:主动寻找与锚点相似的负样本:
def mine_hard_negatives(query_features, candidate_features, top_k=10): """ 挖掘最困难的负样本(与查询最相似的候选样本) """ # 计算相似度矩阵 sim_matrix = torch.mm(query_features, candidate_features.t()) # 对于每个查询,找到top_k个最相似的候选(排除自己) # 这里需要小心处理,确保不把正样本当作负样本 # 获取相似度最高的k个负样本索引 values, indices = torch.topk(sim_matrix, k=top_k+1, dim=1) # +1因为可能包含自己 # 过滤掉自己(如果存在) hard_negatives = [] for i in range(query_features.shape[0]): # 移除可能是正样本或自己的索引 valid_indices = [idx for idx in indices[i] if not is_positive_pair(i, idx)] hard_negatives.append(candidate_features[valid_indices[:top_k]]) return torch.stack(hard_negatives)
4. 高级技巧与避坑指南
在实战中,有些细节问题如果不注意,可能会让你浪费大量时间调试。这里分享一些我踩过的坑和对应的解决方案。
4.1 特征归一化:为什么、何时、如何做
几乎所有对比学习实现都会对特征进行L2归一化,这不是随意的选择,而是有重要原因的:
- 稳定训练:归一化后,相似度(点积)被限制在[-1, 1]范围内,避免了数值爆炸。
- 几何解释:在超球面上,余弦相似度有更直观的几何意义。
- 温度系数敏感性:归一化后,温度系数的效果更加一致和可预测。
但归一化也有时机问题:
# 错误做法:在损失函数内部归一化 def contrastive_loss_wrong(features): # 临时归一化 features = F.normalize(features, dim=1) # 计算损失... return loss # 正确做法:在模型输出时归一化,或确保输入已归一化 class YourModel(nn.Module): def forward(self, x): features = self.backbone(x) features = F.normalize(features, dim=1) # 在这里归一化 return features一个常见问题:归一化后,所有特征向量的模长都是1,那么相似度的理论范围是[-1, 1]。但实际上,由于随机初始化和训练动态,相似度往往集中在某个区间。如果发现相似度始终接近1或-1,可能需要检查模型容量或温度系数是否合适。
4.2 对称损失与非对称实现
在对比学习中,我们通常希望损失是对称的:对于一对正样本(A, B),既考虑A作为锚点B作为正样本,也考虑B作为锚点A作为正样本。这可以通过两种方式实现:
# 方法1:显式计算两个方向的损失 def symmetric_loss(features, batch_size): # 分割特征 features1, features2 = features[:batch_size], features[batch_size:] # 计算两个方向的损失 loss_12 = contrastive_loss_one_way(features1, features2) loss_21 = contrastive_loss_one_way(features2, features1) return (loss_12 + loss_21) / 2 # 方法2:在批次构造时确保对称性 def prepare_symmetric_batch(images): """ 为对称损失准备批次 返回形状为[2*batch_size, ...]的张量 其中前batch_size是原始图像,后batch_size是增强图像 但顺序是交错的:img1, aug1, img2, aug2, ... """ batch_size = images.shape[0] # 创建增强版本 augmented = augment_batch(images) # 交错排列:img1, aug1, img2, aug2, ... interleaved = torch.stack([images, augmented], dim=1).flatten(0, 1) return interleaved对称损失的优势:
- 更稳定的梯度:两个方向的梯度可以互相补充
- 更好的特征一致性:确保特征提取器对原始样本和增强样本的处理方式一致
- 通常能带来1-3%的性能提升
4.3 数值稳定性:避免exp溢出和下溢
对比学习损失涉及大量指数运算,数值稳定性是个实际问题。当相似度/τ很大时,exp可能溢出;当很小时,可能下溢为0。
解决方案:
def stable_info_nce_loss(logits, temperature): """ 数值稳定的InfoNCE损失计算 logits: 相似度分数,形状为[batch_size, num_classes] """ # 减去最大值进行数值稳定(softmax的标准技巧) logits = logits / temperature logits_max, _ = torch.max(logits, dim=1, keepdim=True) logits_stable = logits - logits_max # 计算log_softmax exp_logits = torch.exp(logits_stable) log_prob = logits_stable[:, 0] - torch.log(exp_logits.sum(dim=1)) # 平均负对数似然 loss = -log_prob.mean() return loss # 对于HCL,也需要类似的稳定性处理 def stable_hcl_loss(pos_sim, neg_sim, temperature, beta, tau_plus): # 对pos_sim和neg_sim分别进行数值稳定处理 max_val = torch.max(pos_sim.max(), neg_sim.max()) pos_stable = (pos_sim - max_val) / temperature neg_stable = (neg_sim - max_val) / temperature # 使用稳定后的值继续计算...监控数值稳定性:在训练初期添加检查点:
def check_numerical_stability(logits, temperature, threshold=50): """检查是否有数值问题""" scaled = logits / temperature if torch.any(torch.isnan(scaled)) or torch.any(torch.isinf(scaled)): print(f"警告: 发现NaN或Inf值") return False if torch.max(torch.abs(scaled)) > threshold: print(f"警告: 缩放后的logits绝对值过大: {torch.max(torch.abs(scaled)):.2f}") return False return True4.4 学习率与优化器选择
对比学习对优化器设置比较敏感。我的经验是:
学习率预热:对比学习通常需要学习率预热,因为初期特征变化剧烈。
def get_cosine_schedule_with_warmup(optimizer, num_warmup_steps, num_training_steps): """带预热的余弦退火调度器""" def lr_lambda(current_step): if current_step < num_warmup_steps: return float(current_step) / float(max(1, num_warmup_steps)) progress = float(current_step - num_warmup_steps) / float(max(1, num_training_steps - num_warmup_steps)) return max(0.0, 0.5 * (1.0 + math.cos(math.pi * progress))) return torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)优化器选择:
- AdamW:通常是最安全的选择,特别是对于Transformer backbone
- LARS:对于大批次训练(>4096)效果很好,但需要仔细调参
- SGD:在某些视觉任务上仍然有效,但需要配合动量
权重衰减:对比学习通常需要较小的权重衰减(1e-4到1e-6),因为模型需要学习更丰富的特征表示,而不是过度正则化。
4.5 监控与调试:看懂训练曲线
对比学习的训练曲线有其特点,正确解读这些信号能帮你快速定位问题:
健康的训练曲线特征:
- 损失稳步下降,但不是直线下降(有波动是正常的)
- 正样本相似度逐渐上升,最终稳定在0.6-0.9之间
- 负样本相似度逐渐下降,与正样本的差距拉大
常见问题与解决方案:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 损失震荡剧烈 | 学习率太大或τ太小 | 减小学习率,增大τ |
| 损失下降后反弹 | 批次大小太小或负样本不足 | 增大批次,使用负样本队列 |
| 正样本相似度始终很低 | 模型容量不足或特征归一化问题 | 检查模型结构,确保正确归一化 |
| 正负样本相似度差异小 | τ太大或负样本质量差 | 减小τ,改进数据增强或负采样 |
实用的监控代码:
class ContrastiveTrainingMonitor: def __init__(self): self.history = { 'loss': [], 'pos_sim_mean': [], 'neg_sim_mean': [], 'grad_norm': [] } def update(self, loss, features, batch_size): with torch.no_grad(): # 计算正负样本相似度统计 pos_sim = torch.sum(features[:batch_size] * features[batch_size:], dim=-1) neg_sim = torch.mm(features[:batch_size], features[batch_size:].t()) # 排除对角线(正样本对) mask = ~torch.eye(batch_size, dtype=bool, device=features.device) neg_sim_masked = neg_sim[mask] self.history['loss'].append(loss.item()) self.history['pos_sim_mean'].append(pos_sim.mean().item()) self.history['neg_sim_mean'].append(neg_sim_masked.mean().item()) def plot(self): # 绘制训练曲线... pass在实际项目中,我通常会在训练脚本中加入这样的监控,特别是当尝试新的损失函数或参数设置时。有了这些实时反馈,你就能更快地判断当前设置是否有效,而不是等到训练完才发现效果不好。
对比学习的损失函数虽然数学形式简洁,但其中的实现细节和调参技巧却非常丰富。从InfoNCE的标准实现到HCL的困难负样本挖掘,每个选择都会影响最终的学习效果。关键是要理解每个参数背后的动机,建立直观的感受,然后通过系统的实验找到适合你特定任务的最佳配置。记住,没有一套参数能适用于所有场景,最好的调参策略是:从小实验开始,建立基线,然后有方向地迭代优化。