MoCo v1(Momentum Contrast,v1版),是一种在CV任务上,通过对比学习(contrast learning)做无监督(unsupervised)训练的方法。利用对比学习在CV任务上做无监督训练

在这个系列之前的文章中,我们介绍过MAE和CLIP。MAE将Bert那套做完形填空进行自监督(self-supervised)预训练的方式搬到了CV任务上,通过训练模型做像素级别的图像重建,来得到一个强有力的预训练模型,以便运用到下游任务上。而CLIP则是运用了对比学习(contrast learning),将图像和文字的语义信息进行比对,来完成多模态训练的任务。

今天我们要讲的MoCo v1(Momentum Contrast,v1版),就是一种在CV任务上,通过对比学习(contrast learning)做无监督(unsupervised)训练的方法。它比MAE和CLIP诞生的时间都要早,甚至早于Vision Transformer(MoCo v3中才引入了Transformer的骨架,在之前的版本中仍是用例如ResNet这样的CNN骨架),严格来说,它甚至都不算是大模型,但是我们为什么要讲它呢,因为:

  • MoCo v1提出的在CV上做无监督预训练的方法,是非常具有前瞻性的。它不仅在CNN的架构上证明了无监督的训练效果可以强于有监督,甚至在VIT诞生之前,就预言了可以在CV任务上做类比Bert的像素级重建的可能。
  • MoCo v1采用的对比学习的思想,也延伸至了大模型时代的多模态任务中。我们可以通过MoCo了解对比学习的基本概念,这对之后研读多模态的工作也会有所帮助。

需要说明的是,自监督学习(self-supervised learning)是无监督学习(unsupervised learning)的一种,本质上两者都做的是无标签训练。MoCo v1的论文中采用了更宽泛的定义,称其为无监督学习。

一、MoCo诞生的背景

在MoCo诞生前,NLP领域的自监督预训练模型代表作Bert和GPT已经获得了很大的成功和热度(TODO:插入链接。对Bert和GPT原理不了的朋友,可以参考这篇和这篇文章)。如果在CV上,也能和NLP任务一样,用不带标签的数据做预训练,不就能节省下一大笔标注费用了吗?可是,无监督的训练在CV上很难做起来,原因是:

  • 对NLP任务来说,无监督训练可以看作是从一个词典(dictionary)中预测下一词。这要怎么理解呢?我们知道文字是可以枚举的,有时甚至能通过词根词缀的拆分,来进一步减少枚举值。想象一下,若我们把这些枚举值装进一个字典中,然后把每个值看作一个类别(class),那么NLP任务里的“完形填空”或者“下一词预测”就相当于是在一个离散空间(discrete signal spaces)中做分类预测,从原理上来说是可行的。
  • 对CV任务来说,无监督训练的难点在于像素值本身是在一个连续、高维的空间中。就算是人类,也很难像对待NLP任务那样,从中总结出结构化的、可理解的语义信息,因此在CV上做无监督任务,相对来说更加困难。

如果CV任务也能像NLP任务那样,构建出一个字典就好了。但是在CV无监督预训练中,这个字典应该长什么样,又要怎么用呢?

二、对比学习与代理任务

2.1 预训练的作用:培养强大的特征提取器

在我们解释CV无监督预训练中的字典前,我们先回退一步:假设现在需要由你来设计一个无监督的CV预训练任务,你会怎么做。

我们知道预训练任务的本质目的是,训练出一个能够有效提取数据特征的预训练模型。有了预训练模型之后,我们就可以借助它强大的特征提取能力,在其之上做微调等操作,将它迁移到更多的下游任务上,使它在更多的垂域场景下也能取得好效果。

那怎么培养模型提取特征的能力呢?一个最直观的想法就是,让它理解数据。理解数据的方法就有很多种了,举例来说:

  • 在VIT中,我们使用ImageNet,通过让模型去学习一张图具体属于什么类别,来教会模型理解图片内容
  • 在MAE中,我们对原始图片做随机mask,通过让模型去重建mask部分的像素,来教会模型理解图片内容

2.2  对比学习(Contrast Learning)

类比于以上的例子,我们大胆发挥想象,提出一种新方法:

假设有一份用作输入的图片数据,和一个存储海量图片的仓库。我们喂给模型一张图片,让模型去仓库中寻找哪些图片与输入数据相似,哪些图片与输入数据不相似。只有当模型真正理解数据内容时,它才能给出正确的答案。因此,这也是一种培养模型特征提取能力的好方法。

我们可以将这个idea以下图表示。图中分别是两只漂亮的猫酱与我家的大冤种。我们通过预训练模型,提取出它们三个的特征f1,f2,f3。而我们的训练目标是使得在特征空间中,f1和f2尽量相似,同时和我家大冤种的f3尽量远离。

MoCov1_sed

或者如下图,我对大冤种做一些数据增广,之后的训练目标依然是f1和f2相似,并与f3远离:

MoCov1_损失函数_02

以上这种通过样本间对比,使得模型产生的特征中,同类特征尽量接近,异类特征尽量远离的方法,就被称为对比学习(Contrast Learning)

2.3 代理任务(Pretext Task)

我们想通过对比学习,来对模型做预训练,从而培养其强大的特征提取能力的这种想法,听起来很靠谱也很美好。但是,这里有个致命的问题:“图片间是否相似”这个东西,其实也是一种label。而我们的目标,是要不花一分标注钱,实现无监督的预训练。

那该怎么办呢?一种简洁有效的解决办法是,我们制定一种能用代码写出来的规则,根据这种规则,让代码自动地判断两张图片是否相似。

而这种规则,其实我们在2.2的最后一张图已经给出了示例。我们对一张输入图片做数据增广,例如把图片反转,或者扭曲,或者截取图片的某一部分,然后我们定义,只有来自同一数据增广的图片才是相似的,而和其余图片都是不相似的。通过这种办法,我们就能实现无监督的训练了,这也是MoCo V1最终选用的训练方法。

上述这种能真正实现无监督训练的训练任务,我们称之为代理任务(pretext task),而MoCo V1中通过数据增广来实现用相似性做预训练的方法,我们称之为实例判别法(instance discrimination method,一张图片是一个实例)。不难理解,代理任务是一个统称,其下涵盖许多具体的实现方法,而实例判别法就是其中一种。

2.3.1 代理任务的更多例子

如果讲到这里,你对代理任务依然有些迷惑的话,那我们在这里再列举一些例子。

(1)NLP中的代理任务举例

  • 在预训练Bert时,我们通过随机遮盖预料中的单词,迫使Bert依据上下文对遮盖的词进行预测,从而完成预训练。这种“完形填空”式的训练任务,就是一种代理任务。
  • 在预训练GPT时,我们给出上文,让模型预测下文,这种“预测下一词”式的训练任务,也是一种代理任务。

(2)CV中的代理任务

  • 重建整张图
  • 根据上下文patch,重建被遮盖的patch(比如之前谈过的MAE)
  • 生成伪标签,即MoCo采用的方法
  • 把图片分成若干个patch,并对patch标好位置序号,让模型去预测patch的真实位置序号

2.3.2 实例判别法(Instance Discrimination Method)

前文我们已经介绍了实例判别法的定义。接下来我们通过一张架构图,更深入地理解MoCo是如何运用这种方法的。

MoCov1_sed_03

(1)首先,我们有一张输入图片x

MoCov1_人工智能_04

(5)有了正负样本,我们自然就可以定义对比学习的损失函数(contrastive loss) ,来完成让特征q和尽量靠近,而和其余尽量远离的目的了。我们会在下文进一步探讨损失函数要如何构建。

(6)按照(1)~(5)的步骤训练完后,encoder就能成为一个强有力的特征提取器了。我们可以将它取出,在它上面做微调等操作,更好迁移到下游任务上去。

三、动态字典

读完第二部分,我们对MoCo整体无监督预训练流程,以及要如何运用MoCo预训练好的权重,就有大致的了解了。但我们仍遗留了很多细节需要讨论。首先,我们来看2.3.2中提及到的图片库(动态字典)

MoCo的作者认为,为了使得instance discrimination method的效果更好,对应的动态字典应该满足两个条件。

3.1 特性1:  足够大

这一点不难理解,当我们字典中的数据(即图片量)足够大时,模型比较的范围更广,因此可以达到更好的效果。

但是不做特殊处理的情况下,字典的大小其实就是batch size(即一个batch内的图片互相比较)。如果想扩大batch size,就又要面临显存和收敛的双重考验。

同时,当字典过大时,还可能会产生以下问题:

  • 字典太大,GPU塞不进去
  • 就算把字典塞进去了,我们对momentum enocder做backward时,是要对字典里所有的sample回传梯度的,这样产生的中间结果也是十分庞大的,很可能打爆显存

3.2 特性2:一致性

我们举一个例子来直观理解一致性。

MoCov1_sed_05

这就产生了一个严重的问题,和做比较的字典中的样本,可能来自不同的特征空间,这样再去比较,就很有问题了。所以,一个理想的字典,应该尽量保证字典中的图片特征都在尽可能一致的特征空间中。

3.3 MoCo如何构建理想的动态字典

现在,我们知道了字典需要满足“足够大”和“一致性”这两个重要的特征。我们来看看,MoCo是如何实现这两点的。

3.3.1 解决一致性问题

我们首先来看字典一致性问题的解决,因为它关乎MoCo(Momentum Contrast,动量对比)中的词眼:动量。

MoCov1_sed_06

3.3.2 解决大小问题

如果MoCo不去关心字典大小问题,那会发生什么呢?不难理解,在不关心大小的前提下,我们每次给模型传送一个batch的数据,那么字典的大小其实也就是这一整个batch的大小,我们其实是在batch内做数据间的对比学习。

而如果为了增大字典大小,而盲目扩大batch size,不仅会对显存产生压力,也会影响模型的收敛效果。

所以MoCo增大字典大小的核心思想就是:使得字典大小脱离batch的束缚。所以MoCo采用了一个很巧妙的方法,用queue来存储这个字典。

MoCov1_sed_07

如上图所示,绿色长列即为MoCo中的动态字典,在MoCo里设它的长度为65537,即可以存储65537个图像特征。

  • 当b4这个batch进来的时候,它和b0~b3做对比学习,然后更新Momentum Encoder。
  • 接下来,我们把b4(灰色)装入更新后的Momentum Encoder中,得到新的特征b4(绿色)。
  • 对于动态字典,由于它是一个queue,我们就可以将最早装入字典的b0移开,然后将b4插入。

通过queue实现动态字典的好处是:

  • 字典的大小摆脱了batch size的限制。
  • 及时更新字典,用老的Momentum Encoder生成的图像特征将被渐渐移走,从一定程度上也保证了一致性。

同时,由于在MoCo设计里,Momentum Encoder不做梯度回传了,所以也能帮助我们省去对字典中每一个sample回传梯度时产生的中间存储。

3.4 MoCo之前的方法是如何构建字典的

我们再来看两个早于MoCo的构建动态字典的方法,以此来更好理解MoCo的改进之处。

(1)end-to-end

MoCov1_sed_08

从图中不难看出,字典的大小和batch size密切相关(同时由于encoder k更新频繁,因此也只能同个batch内做比较)

(2) Memory Bank

MoCov1_数据_09

Memory Bank已衍生出了字典的思想(灰色的memory bank就是),但此时它未及时对bank中的特征做去旧更新,随着训练step的增加,bank中的特征分布差异会越来越大。

四、对比损失函数

最后,我们来看下MoCo的对比学习损失函数要怎么定义吧。

总体来看,对比学习损失函数应该要满足以下两点:

  • 当两个特征相似时,损失函数尽量小
  • 当两个特征不相似时,损失函数也不能过大

第一点好理解,但第二点怎么理解呢?假设我们每次喂给模型的数据里,只有负样本,而没有正样本,同时识别这个负样本对模型来说难度也不大。由于我们的目的是让输入数据和它的负样本间距离变大,那么随着训练step的增加,用于衡量距离的loss是越来越大了,但是模型也越来越不可能收敛了。

所以,当两个特征不相似时,损失函数可以大,但必须是有界的。

在这个思想下,MoCo对比学习的损失函数定义为:

MoCov1_损失函数_10

这个损失函数理解起来并不难,其实和交叉熵基本一致。这里的temperature的参数,是用于改变logit的分布。当temperature变大时,logit的分布变得更平滑了,这意味着对比损失函数对所有的负样本都一视同仁,导致模型的学习没有侧重点。当temperature变小时,logit的分布变得更陡峭了,这意味着对比损失函数会去关心那些难学的负样本(即可能长得和正样本很相似的负样本),导致模型很难收敛,也很难泛化。

好!目前为止,我们就把MoCo相关的细节说完了。受篇幅限制,本文略去了对实验部分的讲解,感兴趣的朋友,可以自行研读论文相关部分。

参考

1、https://arxiv.org/abs/1911.05722
2、https://github.com/facebookresearch/moco
3、https://www.bilibili.com/video/BV1C3411s7t9/?spm_id_from=333.999.0.0