共计 3331 个字符,预计需要花费 9 分钟才能阅读完成。
背景介绍
对比学习(Contrastive Learning)近年来在 NLP 领域取得了显著成果,其核心思想是通过拉近相似样本、推开不相似样本来学习有意义的表示。BERT 作为强大的预训练语言模型,结合对比学习能够进一步提升语义表示的质量。这种结合特别适用于语义相似度计算、文本匹配等任务,可以有效解决传统方法中语义鸿沟和效率低下的问题。
核心源码解析
1. 对比学习损失函数(ContrastiveLoss)的实现
对比学习的核心在于损失函数的设计。在 BERT 中,常用的对比损失函数是 InfoNCE(Noise Contrastive Estimation),其数学形式如下:
def contrastive_loss(anchor, positive, negatives, temperature=0.1):
"""
anchor: 锚点样本的向量表示 [batch_size, hidden_size]
positive: 正样本的向量表示 [batch_size, hidden_size]
negatives: 负样本的向量表示 [batch_size, num_negatives, hidden_size]
temperature: 温度系数,控制分布的尖锐程度
"""
# 计算锚点与正样本的相似度
pos_sim = torch.sum(anchor * positive, dim=-1) / temperature
# 计算锚点与所有负样本的相似度
neg_sim = torch.einsum('bd,bnd->bn', anchor, negatives) / temperature
# 合并所有相似度
logits = torch.cat([pos_sim.unsqueeze(-1), neg_sim], dim=-1)
# 计算交叉熵损失
labels = torch.zeros(logits.shape[0], dtype=torch.long).to(anchor.device)
loss = F.cross_entropy(logits, labels)
return loss
这个实现有几个关键点:
- 温度系数(temperature)控制着相似度分布的尖锐程度,较小的值会使模型更关注困难的负样本。
- 使用 einsum 进行高效的矩阵运算,计算锚点与所有负样本的相似度。
- 将正样本作为第一个类别,负样本作为后续类别,使用标准的交叉熵损失。
2. 负样本采样策略及代码实现
负样本的质量直接影响对比学习的效果。在 BERT 对比学习中,常见的负样本采样策略有:
- 同一 batch 内的其他样本作为负样本(in-batch negatives)
- 从外部存储库中检索的困难负样本(hard negatives)
- 随机采样的简单负样本(easy negatives)
下面是一个混合采样的实现示例:
class NegativeSampler:
def __init__(self, memory_size=65536, hard_ratio=0.5):
self.memory_bank = torch.randn(memory_size, 768) # 假设隐藏维度是 768
self.hard_ratio = hard_ratio
def sample(self, anchor, batch_negatives, num_negatives=16):
batch_size = anchor.size(0)
# 从 batch 内采样部分负样本
num_in_batch = int(num_negatives * (1 - self.hard_ratio))
in_batch_negs = batch_negatives[torch.randperm(batch_size)[:num_in_batch]]
# 从 memory bank 采样困难负样本
with torch.no_grad():
similarities = torch.einsum('bd,nd->bn', anchor, self.memory_bank)
_, hard_indices = similarities.topk(num_negatives - num_in_batch, dim=1, largest=False)
hard_negs = self.memory_bank[hard_indices]
# 合并负样本
negatives = torch.cat([in_batch_negs.unsqueeze(1), hard_negs], dim=1)
return negatives
3. 向量归一化的必要性及实现方式
在对比学习中,向量归一化(L2 normalization)是至关重要的步骤,原因有三:
- 防止向量范数影响相似度计算
- 使相似度得分落在 [-1,1] 区间
- 提高训练稳定性
实现非常简单:
normalized_embeddings = F.normalize(raw_embeddings, p=2, dim=-1)
代码示例
下面是一个完整的 BERT 对比学习训练步骤示例:
# 1. 准备输入
input_ids = tokenizer(texts, padding=True, truncation=True, return_tensors="pt")
# 2. 获取 BERT 编码(使用两个不同的 dropout mask 实现数据增强)with torch.no_grad():
anchor_emb = model(input_ids=input_ids).last_hidden_state[:, 0] # [CLS] token
# 3. 正向传播(使用不同的 dropout mask)positive_emb = model(input_ids=input_ids).last_hidden_state[:, 0]
# 4. 归一化
anchor_emb = F.normalize(anchor_emb, p=2, dim=-1)
positive_emb = F.normalize(positive_emb, p=2, dim=-1)
# 5. 采样负样本
negatives = sampler.sample(anchor_emb, batch_embeddings)
negatives = F.normalize(negatives, p=2, dim=-1)
# 6. 计算对比损失
loss = contrastive_loss(anchor_emb, positive_emb, negatives)
# 7. 反向传播
loss.backward()
optimizer.step()
性能优化
batch size 对对比学习效果有显著影响,主要体现在:
- 较大的 batch size 提供了更多 in-batch negatives,通常能提升性能
- 但过大的 batch size 可能导致内存不足和训练不稳定

图:不同 batch size 下的模型性能变化(注:此处为示例,实际应用中需要根据具体任务绘制)
建议的 batch size 范围是 64-512,可以根据 GPU 显存调整。当显存有限时,可以使用梯度累积来模拟更大的 batch size。
避坑指南
常见参数配置误区
- 温度系数:
- 过小(<0.05)可能导致训练不稳定
- 过大(>0.5)可能导致学习信号太弱
-
推荐初始值 0.1,然后根据验证集调整
-
学习率:
- 对比学习通常需要比标准 BERT 更小的学习率(1e- 5 到 5e-5)
- 配合 warmup 使用效果更好
数值不稳定问题
- 梯度爆炸:
- 解决方案:梯度裁剪(
torch.nn.utils.clip_grad_norm_) -
推荐值:1.0 或更小
-
NaN 损失:
- 可能原因:未归一化向量或温度系数过小
- 解决方案:检查归一化代码,增大温度系数
实践建议
- 数据增强策略:
- 对于文本数据,可以使用回译、随机删除 / 替换等增强方法
-
更简单有效的方法是使用不同的 dropout mask 作为数据增强
-
监控指标:
- 除了损失值,还应监控正负样本平均相似度
-
理想情况是正样本相似度 >0.8,负样本相似度 <0.2
-
混合训练:
- 可以先进行对比学习预训练,然后微调下游任务
- 也可以将对比损失与其他损失(如 MLM)联合训练
思考问题
- 如何设计更有效的负样本采样策略?除了 in-batch 和 memory bank,还有哪些创新方法?
- 对比学习与传统的监督学习方法在特征表示上有什么本质区别?这种区别如何影响下游任务的表现?
- 在低资源场景下,如何调整对比学习策略以获得更好的效果?
正文完
