今天分享一下李沐大神团队带来的对比学习串烧。该 talking 讲述的是对比学习在计算机视觉领域的发展历程。

第一阶段:百花齐放
方法、模型都没有统一,目标函数,代理任务也没有统一,所以说是一个百花齐放的年代。
InstDise

举个例子,将一张豹子的图片输入到一个有监督数据分类的分类器中,与其相似度较高的图片都是美洲虎或者猎豹,倒不是因为他们之间的语义相似性很高,主要是因为他们的图片之间实在太相似了。因此,作者将这种有监督的任务发挥到极致,提出了个体判别任务,作者将每一张图片都看作是一个类别,希望模型可以学习图片的表征,从而把各种图片都区分出来。

作者的方法就是使用一个卷积神经网络,将图片作为输入,获得图片的低纬向量表征,让这些向量可以表示这些图片,因为作者将每一个图片都视为是单独的类别,因此,向量的表征应该尽可能的分散开来。
训练这个神经网络的方法是对比学习,所以需要有正样本,需要有负样本。正样本就是图像本身,可能经过数据增强,负样本就是数据集中其余的所有图片,该文章使用一个 memory bank 存储这些负样本,imagenet 中有 128w 的数据,意味着 memory bank 有 128w 行,因为负样本太多了,如果使用较高的维度表示图片的话,对于负样本的存储代价过高,因此作者让向量维度为 128 维。
让我们简单的走一下这个前向传播的过程,假设模型的 batchsize 是 256,有 256 张图片进入 CNN 网络,将 256 张图片编码为 128 维的向量。因为 batchsize 是 256,因此有 256 个正样本。负样本来自 memory bank,每次从 memory bank 中随机采样出 4096 个负数样本,利用 infoNCE loss 去更新 CNN 的参数。本次更新结束后,会将 CNN 编码得到的向量替换掉 memory bank 中原有的存储。就这样循环往复的更新 CNN 和 memory bank,最后让模型收敛,就训练好一个 CNN encoder 了。
InstDise 提出了个体判别这个代理任务,而且用这个代理任务和 nce loss 去做对比学习取得了不错的无监督表征学习的结果,同时提出了用别的结构存储这些大量的负样本,以及如何进行动量的更新,为后续的对比学习的工作产生了推进的作用。
InvaSpread
可以理解为是 SimeCLR 的前身。InvaSpread 并没有用额外的数据结构存储大量的负样本,他就是用 mini batch 中的数据作为负样本,而且使用一个编码器进行端到端的学习。

该工作的思路就是对比学习。相同的图片所提取的特征应该比较类似,不同图片所提取的特征应该不类似。
该文章的代理任务也是实体判别任务,这里着重讲一下该文章中正例和负例的选择。

该文章设置的 batchsize 是 256。首先利用数据增广,将每个图片增广一次,也就是将 256 张图片变为 512 个图片了。之后将 512 张图片通过 CNN 编码为向量,并使用一个全连接层将数据的维度降低。之后将图片 xi 和其经过增广后的图片 xi’ 作为正样本,其余的 512-2 张图片都认为是负样本。所以总计有 256 个正例,有 2×(256-1) 张负例。之后让正例之间的距离拉近,让正例与负例之间的距离拉远。
该文章的思路和 SimCLR 的思路差不多,都设计用 batch 中的数据作为正例和负例,但是该文章取得的效果没有 SimCLR 的效果那般炸裂。主要是因为本文所选取的字典长度不够大,batchsize 仅为 256,本文也没有设计 SimCLR 那种投影函数和多样的数据增广方法,因此本文取得的效果不如 SimCLR 那么好。
以上两篇工作都是使用实例检测作为代理任务的,接下来介绍的这篇文章是利用其他代理任务进行对比学习的。
CPC

该模型是一个普适任务的模型,可以将音频,视频,文本等序列作为输入,利用生成的方式进行对比学习。该模型将序列的一部分输入到一个 encoder genc 中得到对应的向量表征,再将这些向量表征输入到一个自回归器 gar 中,从而得到对序列的预测序列。作者认为一个好的 encoder 是可以编码出序列的特征,并且可以通过自回归模型得到未来的预测。这个过程是如何应用对比学习的呢?
作者将真正的未来序列放入到 encoder genc 中,将得到的嵌入表示作为正例,将其他的序列作为负例,进行对比学习的训练。
CPC 利用预测的特征进行对比学习,这样的方法可以用在各种以序列为输入的任务上,可以用在音频、视频、文本、以及图片的 patch 序列中,都可以使用这种生成的任务。
CMC
在 CPC 利用预测的部分做对比学习后,CMC 使用了更为广泛的负例产生的机制,进行学习。

CMC 定义正负样本的方式:CMC 使用的数据集是 NYU RGBD 数据集,该数据集包含一张图片的四种 view 数据增强结果。该文章将多 view 作为正例,将其他图片以及其他图片的 views 作为负例子,进行训练。
CMC 的成功,让我们认识到对比学习可以如此的灵活,Open AI 团队的工作 CLIP 将图片-文本对作为输入,将互相匹配的图像-文本对作为正例,将不匹配的作为负例。同时 CMC 的原班人马利用对比学习做知识蒸馏,他们认为相同的样本在不同的编码器下得到的结果应该尽可能的相似,因此设计的 teacher 和 student 编码得到的相同图片的向量互为正例,不同图片得到的输出作为负例,利用对比学习的思路进行知识蒸馏。
但是问题在于 multi view 的工作可能需要多个编码器进行编码,训练代价可能有点高。比如 CLIP,就是用大型的语言编码器 BERT 对语言模型进行编码,用视觉模型 VIT 对视觉信息进行编码。
第一阶段介绍以上四篇工作,可以看到以上的工作代理任务不尽相同,其中有个体判别,有预测未来,还有多视角多模态。使用的目标函数也不尽相同,有 NCE,infoNCE 以及其变体。使用的模型也可以是不同的,比如 InvaSpread 使用的是相同的编码器对 key 和 query 进行编码,CMC 对 key 和 query 使用的是不同的编码,是百花齐放的。
第二阶段:MOCO和simCLR双雄
MOCO v1
主要贡献就是把之前对比学习的一些方法归纳为一个字典查询问题。提出了一个队列,一个动量编码器,从而形成一个又大又一致的字典,帮助更好的进行对比学习。

MOCO 和 InstDise 有很多类似的地方,但是 MOCO 对 InstDise 的改进可以说是简单又有效,其提出用队列替换 memory bank 以及提出了动量更新的方式,对效果有显著的提升,同时对后续工作也产生了深远的影响。
simCLR v1

假如有一个 minibatch 的图片,对整个 minibatch 的所有图片做数据增强,对图片 x 做不同的数据增强就会得到 xi 和 xj。同一个图片延申得到的两个图片就是正样本,比如 batchsize 是 n 的话,那么正样本就是 n,这个 batchsize 剩下的所有的样本以及其经过数据增强后得到的都是负样本,也就是 2(n-1)。有了正负样本之后,对其进行编码,通过一个编码器 f(·) 得到正负样本的编码结果 h。simCLR 的创新点就是在得到数据的编码之后在后面加了一个编码层 g(·) 函数,就是一个 MLP 层,得到较低维度的特征 zi 和 zj,用其进行对比学习,拉近正例之间的距离,拉远负例之间的距离。但是需要注意的一点就是投影函数仅仅在训练的时候才使用,在测试的时候是不使用的,测试的时候仅仅使用编码器 f(·)。加上投影函数的目的也仅仅是想让模型训练的更好。
SimCLR 和 InvaSpread 非常接近,不同之处在于:
- SimCLR 使用了更多的数据增强
- 加入了投影的 g(·) 函数
- 就是 SimCLR 用了更大的 batchsize,且训练的时间更久

SimCLR 使用了以上的数据增强的方式

同时对各种数据增强方式进行了详细的消融实验

作者还对这个投影层做了一些实验,可以看到使用非线性变化的效果是十分显著的,同时对于对比学习维度变化,似乎并不会对效果产生影响。
MOCO v2


MOCO v2 相当于是把 SimCLR 中值得借鉴的地方拿来借鉴。
比如其中 MLP 的投影层,更多的数据增强方式,cosine learning rate schedule,以及更多的迭代 epoch。其中 supervised 是 backbone 在有监督训练中进行训练的结果。在加入了 SimCLR 的一些改进点后,确实取得了模型性能的进步。

作者对比了 MOCO v2 和 SimCLR 在相同的 epoch 和 batch 下的效果对比,在较小的 batch 和 epoch 下,MOCO v2 取得了较好的效果,在较大的 batch 和 epoch 下,也取得了较好的效果。

作者将 MOCO v2 和 SimCLR 的算力作对比,发现 SimCLR 在 batch 较少的情况下无法发挥效果,在 batch 多的情况下才可以出效果,但是算力要求太高了。所以 MOCO 是一个对于计算资源要求不是很高,但是却很有效的模型。
SimCLR v2

SimCLR v2 文章提出了一套用自监督网络作半监督训练的流程,该流程是用大网络(SimCLR v2)作自监督的预训练,预训练部分是没有特定下游任务的,因此不具备下游任务知识;之后使用少部分有标注的数据对模型进行微调,从而让模型学习下游任务的特定知识;让微调的模型作为 teacher 模型,为更多的数据打伪标签,从而实现自学习。
SimCLR v1 是如何升级到 SimCLR v2 的呢?
- 如果使用更大的模型,则无监督训练就会训练的更好,所以 SimCLR v2 使用了 ResNet-152 并且使用了 selective kernels,从而让骨干网络更加强悍
- 原来的非线性投影层是十分有效的,那么更深的非线性层会不会更加有效呢?于是作者尝试使用 2 层,3 层,最后发现 2 层的效果是最好的
- 作者尝试了 MOCO 的动量编码器,发现效果是有提升的,但是提升的不是非常显著,大概是一个百分点,原因是 SimCLR v2 已经有很大的 batchsize 了,所以不需要太多的动量以及队列的负样本了
SwAV

以往的基于对比学习的方法都是将一个实例 x 通过两次数据增强变为 x1 和 x2,之后利用编码器对其进行编码,从而得到嵌入向量 z1 和 z2,之后使用对比学习的 loss 更新这个 encoder。
即使以往的工作是非常有效并且简洁的,但是因为负样本太多了,从而造成资源的浪费,即使是 MOCO 这样用近似的方式用 6w 个负样本,但是总共还是有 128w 个负样本的,所以 SwAV 的作者去想,能不能不做近似呢?可不可以使用先验信息,不去和大量的负样本对比,而是和一些更加简洁的东西去比呢?所以 SwAV 的作者想,可以和聚类的中心进行对比,这个聚类中心就是 C ,维度是 3000 × 向量维度,3000 表示聚类中心的数量。
前向过程依旧是一个样例 x 经过两个数据增强转化为 x1 和 x2,之后经过编码器得到 z1 和 z2,让 z1 和 z2 和聚类中心 C 进行聚类,从而得到 ground truth 的标签 Q1 和 Q2,模型让 z1 和 C 预测 Q2,让 z2 和 C 预测 Q1。通过这种交叉预测的方式对模型进行更新。
SwAV 的优势在于:
- 如果是和负例进行对比的话,需要和成千上万个负例进行对比,即使是 MOCO 中 6w 个负例,也只是一个近似的值,但是聚类的话,就仅仅需要和 3000 个聚类核心即可
- 这些聚类中心是有含义的,而如果像之前一样用负样本进行对比学习的话,有的负样本不均衡,有的还可能是正样本,不如聚类中心有效

第二个贡献就是 multi-crop 的思想,以往的对比学习方法都是在一张 256 × 256 的图片上用两个 224 × 224 的 crop 求两个正样本,但是因为 crop 过大了,所选取的 crop 都是基于全局特征的。但是可能很多局部特征才是非常有价值的,SwAV 使用了一种 multi-crop 的思路进行操作,即选择了两个 160 × 160 的 crop 去搞定全局特征,选择 4 个 96 × 96 的 crop 去搞定局部特征。这样在计算量变化不大的情况下,可以获取更多的正样本。

到了第二阶段,其实很多细节都趋于统一了,比如目标函数都是使用 infoNCE,模型都归一为用一个 encoder + projection head 了,大家都采用了一个更强的数据增强,都想用一个动量编码器,也都尝试训练更久。
第三阶段:不用负样本的对比学习
其实SwAV已经是不用负样本了,但是他还是和一个聚类的中心这样明确的对比对象进行比较,一下介绍的 BYOL 和 SimSiam 就是正样本自己在和自己玩,已经没有正样本,或者聚类中心这样明确的对比对象了。
BYOL
其实之前使用负样本的学习方法相当于给模型提供一个约束。如果模型的输入只有正样本,那么模型需要让正样本之间的距离尽量的缩小,那么模型可能会想到一个捷径从而很好的解决这个问题,就是模型直接对所有样本的数据都是一致的,这样所有正样本之间的距离无限接近,但是模型这样躺平是学习不到实例的特征的,是无效的。因此添加了负样本对模型造成一个约束,就是让正样本之间的距离接近,让负样本之间的距离拉远,这样可以对模型进行约束,不让模型躺平,所以负样本在模型中是一个必须的东西,可以防止模型躺平,学到这个捷径解。但是 BYOL 的神奇之处在于模型没有使用负样本,仅仅是模型自己和自己去学,但是也实现了很不错的效果。

让我们看看 BYOL 的前向过程,一个实例 x 经过两次数据增强得到 v 和 v’,之后经过两个编码器 fθ 和 fξ,得到啷个嵌入向量 yθ 和 y’ξ ,其中两个编码器的模型架构一样,但是参数并不相同,fξ 通过动量更新,而不是反向传播更新。得到的向量再经过两个投影层 gθ 和 gξ,同样的两个投影层也是架构一样,但是参数不一致,前者是通过梯度下降进行更新,后者是通过动量更新,得到两个嵌入向量 zθ 和 z’ξ。之后将 zθ 输入到一个预测层 qθ 中,得到 qθ(zθ),让 qθ(zθ) 和 z’ξ 无限接近,使用 mean squared error 进行参数更新,利用正样本对正样本的预测,实现模型的学习。
Siamese
Siamese 不需要用负样本,不需要大的 batchsize,不需要动量编码器,即使在这种条件下,Siamese 不仅没有模型谈谈,反而取得了很好的模型效果。


前向过程是一个实例 x 经过数据增强变为 x1 和 x2,之后经过孪生的 encoder f,得到嵌入 z1 和 z2,之后经过预测层得到 p1 和 p2,之后让 p1 预测 z2,用 p2 去预测 z1,进行模型的训练。

对比了不同的基于孪生网络的学习例子。SimCLR 使用的是端到端的训练,两个 encoder,SwAV 是和聚类中心进行对比的,BYOL 是一个预测任务,其使用的是动量编码器,SimSiam 也是预测任务,但是使用的是 stop gradiant 的方式进行预测的。
第四阶段:transformer
在 vision transformer 之后,因为其大大提升了 encoder 的效果,所以很多对比学习任务打算使用 vision transformer 作为 backbone 进行对比学习,涌现出了两篇工作,分别是 MOCO v3 和 DINO。
MOCO V3

MOCO v3 有点像 MOCO v2 和 SimSiam 的结合。使用对比学习,还有对称的 loss。

作者发现当把 backbone 从 ResNET 换为 VIT 后,虽然较小的 batch 效果还可以,但是一旦 batch 变大,模型就出现了不稳定的情况。
作者观察了一下模型梯度回传时候的梯度情况。当每次 loss 有大幅的震动,导致准确度大幅下降的时候,梯度也会有一个波峰,波峰发生在第一层,在作 patch projection 的时候,因为这一层经常出现问题,所以作者尝试将这一层 fix 住,之后再进行训练,得到了很平滑的 loss 曲线。
DINO

为了避免模型坍塌,其在 teacher 中使用了一个 centering 操作,即对 batch 求均值,之后让 batch 中的所有实例减去这个均值,对 batch 中的样本求归一化的操作。


最后就是整体的过一遍。