⚠️⚠️⚠️本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
在本系列之前的文章中,我们介绍过来自Meta的CV自监督预训练模型MAE,以及来自OpenAI的多模态代表作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的论文中采用了更宽泛的定义,称其为无监督学习。
话不多说,进入正文吧~
CV大模型系列文章导航(持续更新中):
🌸CV大模型系列之:扩散模型基石DDPM(人人都能看懂的数学原理篇)🌸
🌸CV大模型系列之:扩散模型基石DDPM(源码解读与实操篇)🌸
🌸CV大模型系列之:全面解读VIT,它到底给植树人挖了多少坑🌸
🌸CV大模型系列之:多模态经典之作CLIP,探索图文结合的奥秘🌸
🌸CV大模型系列之:MoCo v1,利用对比学习在CV任务上做无监督训练🌸
一、MoCo诞生的背景
在MoCo诞生前,NLP领域的自监督预训练模型代表作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尽量远离。
或者如下图,我对大冤种做一些数据增广,之后的训练目标依然是f1和f2相似,并与f3远离:
以上这种通过样本间对比,使得模型产生的特征中,同类特征尽量接近,异类特征尽量远离的方法,就被称为对比学习(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中的代理任务
-
重建整张图
-
根据上下文atch,重建被遮盖的patch(比如之前谈过的MAE)
-
生成伪标签,即MoCo采用的方法
-
把图片分成若干个patch,并对patch标好位置序号,让模型去预测patch的真实位置序号
2.3.2 实例判别法(Instance Discrimination Method)
前文我们已经介绍了实例判别法的定义。接下来我们通过一张架构图,更深入地理解MoCo是如何运用这种方法的。
(1)首先,我们有一张输入图片x
(2)我们对输入图片做2次数据增广,得到两张输入图片,一张是 <math xmlns="http://www.w3.org/1998/Math/MathML"> x q u e r y x^{query} </math>xquery,另一张是 <math xmlns="http://www.w3.org/1998/Math/MathML"> x + k e y x^{key}+ </math>x+key(图中没有画出来,可以想象一下它在 <math xmlns="http://www.w3.org/1998/Math/MathML"> x k e y x^{key} </math>xkey那一排的最前头,它过momentum encoder后得到的特征为 <math xmlns="http://www.w3.org/1998/Math/MathML"> k k </math>k)。我们规定, <math xmlns="http://www.w3.org/1998/Math/MathML"> x q u e r y x^{query} </math>xquery有且只有一个正样本 <math xmlns="http://www.w3.org/1998/Math/MathML"> x + k e y x^{key}+ </math>x+key ,其余数据都是 <math xmlns="http://www.w3.org/1998/Math/MathML"> x q u e r y x^{query} </math>xquery的负样本。因此 <math xmlns="http://www.w3.org/1998/Math/MathML"> x q u e r y x^{query} </math>xquery只和 <math xmlns="http://www.w3.org/1998/Math/MathML"> x + k e y x^{key}_+ </math>x+key相似,和其余数据都不相似。
(3)那么这个其余的数据是什么呢?它是由数据集里的若干张图片组成的"图片库",更严谨一些,是这些图片的特征库,也就是图中画的 <math xmlns="http://www.w3.org/1998/Math/MathML"> k 0 , k 1 , . . . . k_{0}, k_{1}, .... </math>k0,k1,....。读到这里,你是否突然想起在本文引言中,我们曾遗留的问题:如果要像NLP那样,也给CV无监督预训练任务造一个字典,那么这个字典应该长什么样?没错,在MoCo作者的设计中,它其实就是一个图片库。我们会在下文进一步来探讨这个字典应该满足的特性。
(4)对于 <math xmlns="http://www.w3.org/1998/Math/MathML"> x q u e r y x^{query} </math>xquery,我们将训练一个encoder(在MoCo v1中,这个encoder是CNN架构,例如ResNet),由它去生成对应的特征q。而对于字典中的数据,我们将训练一个momentum encoder(动量编码器) ,从而产生一系列的特征k。我们会在下文进一步探讨"动量"的含义。
(5)有了正负样本,我们自然就可以定义对比学习的损失函数(contrastive loss) ,来完成让特征q和 <math xmlns="http://www.w3.org/1998/Math/MathML"> k + k_{+} </math>k+尽量靠近,而和其余 <math xmlns="http://www.w3.org/1998/Math/MathML"> k − k_{-} </math>k−尽量远离的目的了。我们会在下文进一步探讨损失函数要如何构建。
(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:一致性
我们举一个例子来直观理解一致性。
假设我们给MoCo放入一个batch的数据,在这个step训练过后,我们就可以将momentum encoder产生的 <math xmlns="http://www.w3.org/1998/Math/MathML"> k 0 , k 1 , . . k_{0}, k_{1}, .. </math>k0,k1,..特征扩充进图片库(字典中),以便在下一个step使用。
继续训练,我们又喂给MoCo一个batch,在这个step训练过后,我们又更新一次momentum encoder,然后把它产生的 <math xmlns="http://www.w3.org/1998/Math/MathML"> k 0 , k 1 , . . k_{0}, k_{1}, .. </math>k0,k1,..再装入字典中,准备继续使用。
以此类推,不难发现,我们每次新装进字典的一波波 <math xmlns="http://www.w3.org/1998/Math/MathML"> k 0 , k 1 , . . k_{0}, k_{1}, .. </math>k0,k1,.. ,都来自权重不同的momentum encoder,随着step的增加,它们之间的差异会越来越大。
这就产生了一个严重的问题,和 <math xmlns="http://www.w3.org/1998/Math/MathML"> x q u e r y x^{query} </math>xquery做比较的字典中的样本,可能来自不同的特征空间,这样再去比较,就很有问题了。所以,一个理想的字典,应该尽量保证字典中的图片特征都在尽可能一致的特征空间中。
3.3 MoCo如何构建理想的动态字典
现在,我们知道了字典需要满足"足够大"和"一致性"这两个重要的特征。我们来看看,MoCo是如何实现这两点的。
3.3.1 解决一致性问题
我们首先来看字典一致性问题的解决,因为它关乎MoCo(Momentum Contrast,动量对比)中的词眼:动量。
如图,MoCo的解决办法其实很简单,我们设encoder的权重为 <math xmlns="http://www.w3.org/1998/Math/MathML"> θ q \theta_{q} </math>θq,momentum encoder的权重为 <math xmlns="http://www.w3.org/1998/Math/MathML"> θ k \theta_{k} </math>θk,则有:
<math xmlns="http://www.w3.org/1998/Math/MathML"> θ k ← m θ k + ( 1 − m ) θ q \theta_{k} \gets m\theta_{k} + (1 - m)\theta_{q} </math>θk←mθk+(1−m)θq
其中,m表示动量系数。在模型最开始训练时,我们令 <math xmlns="http://www.w3.org/1998/Math/MathML"> θ k = θ q \theta_{k} = \theta_{q} </math>θk=θq。在这之后, <math xmlns="http://www.w3.org/1998/Math/MathML"> θ q \theta_{q} </math>θq正常做梯度更新。而 <math xmlns="http://www.w3.org/1998/Math/MathML"> θ k \theta_{k} </math>θk则不做任何梯度回传,反之,它会参考前一个时刻的 <math xmlns="http://www.w3.org/1998/Math/MathML"> θ k \theta_{k} </math>θk和当前更新完毕的 <math xmlns="http://www.w3.org/1998/Math/MathML"> θ q \theta_{q} </math>θq来决定它的更新方向。当m较大时, <math xmlns="http://www.w3.org/1998/Math/MathML"> θ k \theta_{k} </math>θk几乎不会有变更,因此保证了它的稳定(在MoCo实验中,作者发现m=0.999时的效果是最好的)
3.3.2 解决大小问题
如果MoCo不去关心字典大小问题,那会发生什么呢?不难理解,在不关心大小的前提下,我们每次给模型传送一个batch的数据,那么字典的大小其实也就是这一整个batch的大小,我们其实是在batch内做数据间的对比学习。
而如果为了增大字典大小,而盲目扩大batch size,不仅会对显存产生压力,也会影响模型的收敛效果。
所以MoCo增大字典大小的核心思想就是:使得字典大小脱离batch的束缚。所以MoCo采用了一个很巧妙的方法,用queue来存储这个字典。
如上图所示,绿色长列即为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
从图中不难看出,字典的大小和batch size密切相关(同时由于encoder k更新频繁,因此也只能同个batch内做比较)
(2) Memory Bank
Memory Bank已衍生出了字典的思想(灰色的memory bank就是),但此时它未及时对bank中的特征做去旧更新,随着训练step的增加,bank中的特征分布差异会越来越大。
四、对比损失函数
最后,我们来看下MoCo的对比学习损失函数要怎么定义吧。
总体来看,对比学习损失函数应该要满足以下两点:
-
当两个特征相似时,损失函数尽量小
-
当两个特征不相似时,损失函数也不能过大
第一点好理解,但第二点怎么理解呢?假设我们每次喂给模型的数据里,只有负样本,而没有正样本,同时识别这个负样本对模型来说难度也不大。由于我们的目的是让输入数据和它的负样本间距离变大,那么随着训练step的增加,用于衡量距离的loss是越来越大了,但是模型也越来越不可能收敛了。
所以,当两个特征不相似时,损失函数可以大,但必须是有界的。
在这个思想下,MoCo对比学习的损失函数定义为:
这个损失函数理解起来并不难,其实和交叉熵基本一致。这里的temperature的参数,是用于改变logit的分布。当temperature变大时,logit的分布变得更平滑了,这意味着对比损失函数对所有的负样本都一视同仁,导致模型的学习没有侧重点。当temperature变小时,logit的分布变得更陡峭了,这意味着对比损失函数会去关心那些难学的负样本(即可能长得和正样本很相似的负样本),导致模型很难收敛,也很难泛化。
好!目前为止,我们就把MoCo相关的细节说完了。受篇幅限制,本文略去了对实验部分的讲解,感兴趣的朋友,可以自行研读论文相关部分。