对比学习分享

一、对比学习

1.1 自监督学习与对比学习

如果“有监督”代表数据是否存在原始标签,那么对比学习是无监督学习分类下的自监督学习方法。

有监督学习无监督学习是机器学习中两种基本的范式。监督学习是根据已有的数据集,知道输入和输出结果之间的关系。根据这种已知的关系,训练得到一个最优的模型无监督学习是不依赖任何标签值,通过对数据内在特征的挖掘,找到样本间的关系。

除此以外,还有一些其他衍生的学习范式,如半监督、远程监督、自监督等。

  • 半监督学习:大量的无标注数据与少量有标注数据;

  • 远程监督学习:利用外部知识库、外部模型对未标注数据进行标注;

  • 自监督学习:利用辅助任务从无监督的数据中挖掘自身的信息,从而学习数据的有效表征。


根据辅助任务构造的范式不同,目前自监督学习可以大概分为生成式与对比式两类。

image\.png

image\.png

其中,生成式学习以Auto-encoder作为代表,主要关注辅助任务中标签的损失。一个传统定义下的AE由Encoder-Compressed hidden-Decoder三个部分构成。Encoder会构建一个(或)多个隐藏层,包含了输入信息的低维向量,Decoder通过这个向量重建输入信息。

image\.png

image\.png

对比式学习通过将数据分别与其正例与负例,在特征空间进行对比,来学习样本的表示。对比式学习的重点在于如何构造正负样本。

image\.png

image\.png

0f5cab40-75af-11eb-ab9c-3e1a43f1851b.mp4

1.2 一般范式

对比学习可以分成以下三个步骤:

  1. 构造正负样本:本质在于从一个样本x中导出多条与之相似的正样本。在CV里,可以通过裁切、旋转、缩放、失真等方式;在NLP里,可以通过掩码、替换、甚至随机dropout的方式。

  2. 构造模型:将样本转换为向量表示,并且计算样本对之间的相似度(欧氏距离/余弦距离)

  3. 最小化损失函数

对于输入的数据x,对比学习的目标是学习一个编码器f,使得:

$\begin{equation}
\text{score}(f(x),f(x^+)) >>\text{score}(f(x),f(x^-))
\end{equation}$

其中x是原始样本,x+是构造的正样本,x-是构造的负样本。score是一个打分函数,用来度量x和正负样本之间的相似关系。公式的实际含义是:使得x+与正样本之间的分数,尽可能大于x与负样本x-之间的分数。

二、损失函数

对比学习的关键在于(1)设计正负样本的构造方式;(2)设计损失函数。由于损失函数仅由正负样本对数及其使用方式相关,与数据格式、任务类型关系不大,在这里先串一下常见的损失函数,后面再用两个例子具体介绍怎么结合样本构造与损失函数进行对比学习。

Triplet Loss

Triplet Loss 考虑了三个样本:一个锚点,一个正样本和一个负样本(一正一负)。它的目标不仅是使正样本对的距离小于负样本对的距离,而且要确保这两者之间有一个至少为$\alpha$的间距。这意味着它在确保类内相似性的同时,也强调了类间的差异。

对于一个数据三元组$(x,x^+,x^-)$,三元组的总体距离可以表示为:

$\begin{equation}
L=\max\{d(x,x^+)-d(x,x^-)+\alpha,0\}
\end{equation}$

设置margin($\alpha$)的目的:

  1. 避免模型走捷径(让x+与x-的距离很相近),导致难以区分正负例;

  2. 使得模型努力学习,拉大锚点与负例距离的同时,缩小与正例之间的距离;

image\.png

$L=0$

image\.png

$L>\alpha$

image\.png

$L\in(0,\alpha)$

由三类triplets,也可以反向导出要构造样本的原则:理论上,使用hard triplets训练模型最好,因为这样模型能够有很好的学习能力,但由于margin的存在,这类样本可能模型没法很好的拟合,训练比较困难;其次是使用semi-hard triplet,这类样本是实际使用中最优选择,因为这类样本损失不为0,而且损失不大,模型既可以学习到样本之间的差异,又较容易收敛;至于easy triplet,损失为0,不用拿来训练。

由于超参数$\alpha$的存在,需要审慎调节参数的大小,太大会导致模型损失过大,难以收敛,过小会导致模型难以区分正负例。

Code: 对比学习分享

N-Pair Loss

Triplet Loss的一种扩展,考虑了多个负样本(一正多负),通过一个锚点,一个正样本和多个负样本的相似性来学习特征表示。

image\.png

从图中可以发现,实际构造样本时,只需要针对每个原始样本构造一个正样本,随后使用其他样本的正样本,作为当前样本的负样本。

$\begin{equation}
L(\{(x_i,x_i^+)\}_{i=1}^N;f)=\frac{1}{N}\sum_{i=1}^N\log(1+\sum_{j\neq i}\exp(s(f_i,f_j^+)-s(f_i,f_i^+)))
\end{equation}$

其中s是度量相似度的打分函数。

Code: 对比学习分享

InfoNCE

相比于Triplet Loss是从向量空间的角度计算损失,InfoNCE是从概率角度来计算损失。

InfoNCE的提出基于NCE (Noise contrastive estimation),后者最初被提出来用于多分类问题中类别太多时的softmax计算时间过长的问题,做法是将多元分类转换成二元分类。

image\.png

InfoNCE loss其实就是一个cross entropy loss,做的是一个k+1类的分类任务,目的就是想把q这个图片分到k+这个类,然后概率是它和k+的相似度:

$\begin{equation}
L_q=-\log\frac{\exp(q\cdot k_+/\tau)}{\sum_{i=0}^K\exp(q\cdot k_i/\tau)}
\end{equation}$

Code: 对比学习分享

三、论文分享

ICML’20:SimCLR

Hinton组提出的工作,用自监督模型逼近了监督模型的效果。

A linear classifier trained on self-supervised representations learned by SimCLR achieves 76.5% top-1 accuracy, which is a 7% relative improvement over previous state-of-the-art, matching the performance of a supervised ResNet-50.

image\.png

SimCLR由四个部分组成:

  1. 数据增强模块,将一个图像随机转换成两个相关的图像,作为正样本对:随机截取;颜色失真;高斯平滑;

image\.png

  1. 编码器 $f(\cdot)$:ResNet;

  2. 全连接层 $g(\cdot)$:$z_i=g(h_i)=W^{(2)}\sigma(W^{(1)}h_i)$,(对比学习损失在两层全连接上效果更好);

  3. 对比学习损失:$l_{i,j}=-\log\frac{\exp(sim(z_i,z_j)/\tau)}{\sum_{k=1}^{2N}\mathbb{I}_{k\neq i}\exp(sim(z_i,z_k)/\tau)}$

实际训练时,在每一个batch内部,通过数据增强模块对每一个样本生成两个增强的样本,将batch内所有其他样本作为负样本。

image\.png

实际评估时,不使用模型上面的全连接层$g(\cdot)$,只使用编码器$f(\cdot)$,在上面增加一个线性分类器,通过监督学习训练,使用得到的精度作为评估$f(\cdot)$特征提取能力的指标。

评估结果:

  • 无监督:更宽的网络(4x),参数量更大,叠加有监督训练线性分类器(此时编码器$f(\cdot)$参数冻结),效果和有监督的ResNet50媲美;

  • 半监督:只使用1%的标签,能够达到top 5的准确度,超过AlexNet;


几个insights:

  • 数据增强的有效性:单一的数据增强方法没有很强的效果;组合的数据增强中,裁剪+颜色失真表现最好。

image\.png

  • 模型参数的影响:对于无监督任务,较大的模型更有效

image\.png

  • 非线性投影有助于提升向量的质量。但实际使用时,$z=g(h)$不是最好的表示,$h$才是最好的表示。

image\.png

CVPR’20:MoCo \& MoCo-V2

Facebook AI Research,Kaiming一作。从善如流。

整体结构:

image\.png

损失函数:

$\begin{equation}
L_q=-\log\frac{\exp(q\cdot k_+/\tau)}{\sum_{i=0}^K\exp(q\cdot k_i/\tau)}
\end{equation}$

在损失函数中,有1个正样本和K个负样本。SimCLR的损失函数和这个很像,在采用了数据增强之后,使用了增强后的样本作为候选样本集,有1个正样本和2N-1个负样本。

在训练时,损失函数的梯度只用来更新Query Encoder,而Momentum Encoder的参数用以下方式更新:

$\begin{equation}
\theta_k \leftarrow m\theta_k+(1-m)\theta_q
\end{equation}$

其中$\theta_k$表示Momentum Encoder的参数,$\theta_q$表示Query Encoder的参数,m表示更新的速度:当m越大,Momentum Encoder的更新速度越慢,

image\.png


  1. 为什么要用Momentum Encoder这样的参数更新方式?

    1. 和E2E相比:不占用显存,更节省资源;

    2. 和Memory bank相比:一致性更强,MoCo可以看作是对Query encoder进行平滑得到的,所以当前候选的向量表示在语义上是比较接近的。

image\.png

  1. 这样的更新方式效果如何?

    1. 和E2E、Memory bank等方法相比

image\.png

2. 和其他SoTA方法相比

image\.png

  1. 为什么MoCo效果没有SimCLR好?

MoCo V2 (Tech report)

  • w/ MLP

  • w/ complex data augmentation

  • w/ cosine learning rate schedule

  • w/ larger epoch size

image\.png

相同机制下,MoCo(V2)与SimCLR的对比:epoch相同,在256的batch size下,效果可以比8192 batch size的SimCLR的高0.9%;再多训练一些epoch的情况下,能够拉大差距到1.8%。

image\.png

训练时所需的显存和时间:

image\.png

EMNLP’21:SimCSE

Danqi Chen通讯作者,Novelty:

  • 在NLP预训练语言模型任务上的对比学习框架,和SimCLR类似,但采用了Dropout方式产生正负样本对;

  • 提出了度量对比学习中向量质量的指标:对齐性(Alignment/Closeness)与均匀性(Uniformity)。

对齐性:相似样本拥有相似的特征向量,应该对无关的噪声保持不变。

$\begin{equation}
M_{\text{align}}\triangleq\mathbb{E}_{(x,x^+)\stackrel{\text{i.i.d}}{\sim}p_{data}}||f(x)-f(x^+)||^2
\end{equation}$

均匀性:特征能够保持更多的信息,大致分布在超球面上。

$\begin{equation}
M_{\text{uniform}}\triangleq\mathbb{E}_{(x,x^+)\stackrel{\text{i.i.d}}{\sim}p_{data}}e^{-2||f(x)-f(y)||^2}
\end{equation}$

image\.png


训练框架:

image\.png

无监督SimCSE:利用Transformers里包含的Dropout,将同一条数据输入到样本中两次。

给定一批文本样本$\{x_i\}_{i=1}^m$,将经过模型得到的特征向量表示为$h_i^z=f_{\theta}(x_i,z)$,其中$z$表示当次Dropout的随机掩码,另外一组特征向量表示为$h_i^{z’}=f_{\theta}(x_i,z’)$。根据对比学习的思想,SimCSE的优化目标为:

$\begin{equation}
L=-\log\frac{e^{\text{sim}(h_i^{z_i},h_i^{z’_i})/\tau}}{\sum_{j=1}^N e^{\text{sim}(h_i^{z_i},h_i^{z’_j})/\tau}}
\end{equation}$

评估时,从BERT-uncased开始训练,使用[CLS]作为句子的向量表示,在1m随机抽取的英文维基百科句子。

image\.png

2024-11-21 中心化电商-ECPP 精排模型序列去噪与表征优化:自监督对比学习与最优传输

数据增强

对精排模型用到的所有序列(除搜索特征外 - 因为搜索是用户的强意图,不需要去噪),进行特征维度的Mask。为了增加对比学习的难度,对同一序列特征进行50%随机的互补Mask方式。

image\.png

特征编码

两层MLP,压缩成32维,L2 Norm。每一类序列特征(及样本)之间的参数共享,但不同类型序列特征之间的参数独立。

损失函数

$\begin{equation}
L=-\sum_{i\in N}\log\frac{\exp(s(z_i,z’_i)/\tau}{\sum){j\in B}\exp(s(z_i,z’_j)/\tau}
\end{equation}$


训练时端到端

image\.png

MISC

  • 为什么使用类似softmax的形式,而不是类似:$L_{\text{simple}}=-s_{i,i}+\lambda\sum_{j=1,j\neq i}^Ns_{i,j}$的形式?

    • softmax要给相似度更高的样本更大的惩罚。
  • 超参数$\tau$起到了什么作用?

    • $\tau$越大,对比损失越失去对困难负样本的关注能力,即实际效果越接近上面的$L_{\text{simple}}$

    • $\tau$越小,对比损失只倾向于优化最难的负样本

  • Cosine learning rate schedule是什么?

    • $lr=base\times 0.5\times (1+\cos(\frac{\pi\times epoch}{total \;epochs}))$

References

  • Contrastive Self-Supervised Learning

  • Auto-encoder认知与理解

  • SimCLR

  • MoCo-v2

  • SimCSE

  • N-Pair Loss

  • 损失函数实现

    • Triplet loss

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      # 构造形状是[BatchSize, Dim]的向量,这里BatchSize是批量大小,Dim是维度
      x = torch.randn(6, 2)
      labels = torch.tensor([0, 1, 2, 3, 4, 1], dtype=torch.int)

      # 1. 获取公式中的三元组triplets,其大小是[3,8],因采用矩阵计算产生冗余,无冗余时应该是本组最大类别样本数:4
      pos_adj_mat = (labels.unsqueeze(0) == labels.unsqueeze(1)).byte()
      neg_adj_mat = pos_adj_mat ^ 1
      pos_adj_mat.fill_diagonal_(0)
      triplets = torch.where(pos_adj_mat.unsqueeze(2) * neg_adj_mat.unsqueeze(1))

      # 2. 计算距离项,这里使用L2距离,其形状是[BatchSize,BatchSize],这里是[6,6]
      x_norm = torch.nn.functional.normalize(x, p=2, dim=1)
      distance_matrix = torch.cdist(x_norm, x_norm, p=2)

      # 3. 分别计算锚点与正样本的距离,计算锚点与负例样本的距离
      pos_loss = distance_matrix[[triplets[0], triplets[1]]]
      neg_loss = distance_matrix[[triplets[0], tripLets[2]]]

      # 4. 计算最终loss,这里假设最小距离是0.05
      loss = torch.nn.functional.relu(pos_loss - neg_loss + 0.05)
      loss = loss[loss > 0].mean()
    • N-pair loss

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      # 构造形状是[BatchSize, Dim]的向量,这里BatchSize是批量大小,D是维度
      # 构造三个样本对,位置分别在 (0,3) (1,4) (2,5)
      x = torch.randn(6, 2)
      labels = torch.tensor([0, 1, 2, 0, 1, 2], dtype=torch.int)

      #1.从label中获取锚点样本和正样本
      # 其中, anchor_idx为[0,1,2], pos_index为[3, 4, 5]
      pos_adj_mat = (labels.unsqueeze(0) == labels.unsqueeze(1)).byte()
      pos_adj_mat.fill_diagonal_(0)
      i_indexes, j_indexes = torch.where(pos_adj_mat)
      _, unique_idx = np.unique(labels[i_indexes].cpu().numpy(), rreturn_index=True)
      anchor_idx, pos_index = i_indexes[unique_idx], j_indexes[unique_idx]

      # 2. 计算锚点与正样本之间的点乘相似度
      anchor_embedding = torch.nn.functional.normalize(x[anchor_idx], p=2, dim=1)
      pos_embedding = torch.nn.functional.normalize(x[pos_index], p=2, dim=1)
      similarity_matrix = anchor_embedding @ pos_embedding.T

      # 3. 计算交叉熵
      targets = torch.arange(len(anchor_idx))
      loss = torch.nn.functional.cross_entropy(similarity_maatrix, targets)
    • InfoNCE loss

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      # 构造形状是[B,D]的向量,这里B时批量大小,D是维度
      # 构造两对标签,位置分别是(0,3)和(1,2)
      x = torch.randn(6, 2)
      labels = torch.tensor([0, 1, 1, 0, 2, 3], dtype=torch.int)
      temperature = 0.07

      # 1. 获取标签的邻接矩阵,其形状是[B,B],这里是[6,6]
      pos_adj_mat = (labels.unsqueeze(0) == labels.unsqueeze(:1).byte()
      neg_adj_mat = pos_adj_mat ^ 1
      pos_adj_mat.fill_diagonal_(0)
      pos_indices, neg_indices = torch.where(pos_adj_mat), ttorch.where(neg_adj_mat)

      # 2. 分别计算正样本对和负样本对的距离
      x_norm = torch.nn.functional.normalize(x, p=2, dim=1)
      similarity_matrix = x_norm @ x_norm.T
      pos_pairs = similarity_matrix[pos_indices] / temperature
      neg_pairs = similarity_matrix[neg_indices] / temperature

      # 3. 为每个正样本对构造负样本对,并获取其距离
      neg_per_pos = neg_indices[0].unsqueeze(0) == pos_indices[0].unsqueeze(1)
      neg_pairs = neg_pairs * neg_per_pos.float()
      neg_pairs[neg_per_pos == False] = torch.finfo(neg_pairs.dtype).min

      # 4. 计算最终损失
      pos_pairs = pos_pairs.unsqueeze(1)
      max_val = torch.max(pos_pairs, torch.max(neg_pairs, dim=1, keepdim=True)[0]).detach()
      numerator = torch.exp(pos_pairs - max_val).squeeze(1)
      denominator = torch.sum(torch.exp(neg_pairs - max_val), dim=1) + numerator
      loss = -torch.mean(torch.log((numerator / denominator) + torch.finfo(neg_pairs.dtype).tiny))