Skip to content

Latest commit

 

History

History
328 lines (128 loc) · 19.9 KB

File metadata and controls

328 lines (128 loc) · 19.9 KB

对比学习 学习笔记:

Moco论文解读细节:

Moco 论文简单阐述

Moco是视觉领域使用对比学习一个里程碑的工作;对比学习从2019年开始到现在一直都比较火,;

Moco作为一个无监督的表征工作,不仅在分类任别务上逼近了有监督的基线模型,在其他任务,检测,分割,人体关键点检测都超越了有监督的预训练模型,也就是imagenet上的预训练模型;

Moco证明了一点,无监督学习真的可以,我们并不需要大量的标注好的数据;

什么是对比学习?

首先说对比学习想要做到一点是什么呢?我们现在有三张图,第一个图是人高兴,第二个图片是人悲伤,第三个图片是狗。

我们想得到一个一个结果,就是我们不需要知道前两个图片是人这个类别,不需要知道第三个图片是狗这个类别。但是我们能够需知道前两个图片是一个类别,第三张图片是不是一个类别。

image-20220813164714078

换句话说,我们现在把三个图片过一个模型,我们得到三个表征,我们需要让这个三个表征在特征空间中,前两个图片的表征距离比较近,第三个图片和他们的距离比较远。

image-20220813165045966

一句话说,我们希望在特征空间里,同一个类别的物体在特征空间中在相邻的区域,不同类别的物体在特征空间中不相邻的区域。

在这个过程中,我们需要知道的是,我们并没有用到标签信息,我们不需要第一个和第二个图片是人,第三个是狗。

但是我们用到了另外一种信息,就是第一个图片和第二个图片是同一个类别,第三个通篇不是同一个类别这个信息。这其实也是一种标签信息。

不过这种标签信息,我们可以使用一些代理任务,巧妙构造出来的这种信息,而不需要人为的去标注这种标签信息。这些代理任务,会去定义一些规则,这些规则可以去定义哪些图片是相似的,哪些图片是不相似的,从而可以提供一些监督信号给到模型去训练。这个过程其实也是自监督训练的一个过程。

个体判别代理任务

一个最经典的代理任务就是:instance discrimination。叫做个体判别

这个代理任务,就是如果我们有一个没有标注的数据集,里面有n个图片。

从这个数据集中,我们随机选择一个图片,xi;在这个图片上我们做随机裁剪(或者其他的数据增广操作,我们称之为traansformation);从而得到另外两张图;

一个是xi1 一个是xi2;这样我们会得到两个不太一样的照片。但是由于这两个图片是从同一个图片经过某种变化得到的,语义信息不应该发生变化。所以这两个图片就可以称之为正样本,也就是同一个类别的图片。

这个代理任务,同时认为,这个数据集中剩余的所有图片都是负样本

image-20220813170106285

为什么叫做个体判别呢?因为它认为每个图片自成一个类别,剩余的图片都不是同一个类别。

(这个粒度其实是很细,你在图片分类的时候是很多照片是同一个类别,其余的照片又分为了很多类别,所以个体判别这个代理任务经过模型训练,表征会很细)

对于imgenet这数据集来说,如果个体判别任务,就是一千个类别,而是100多万个类别。

所以个体判别这个代理任务定义了什么是正样本,什么负样本,接下来就很简单了,我们只需要经过模型,然后做一个对比学习的函数去训练模型就可以了。比如说NCEloss

image-20220813170430829

在这个过程中,其实有一个很有意思的点,就是代理任务是多样性的,是很灵活的。只要你能够得到一个判断正样本和负样本的规律,后续的损失函数之类的训练就很常规了。

比如说在视频领域,同一个视频里的任意两帧是正样本,其他视频里的帧是负样本;NLP中的simcse,你可以通过dropout判断不同句子。

精读Moco论文

Momentum Contrast

Moco这个名字就是来源于前两个单词的前两个字母,就是基于动量的对比学习。

动量是一种加权移动平均;

image-20220813170958764

y(t-1)是上一个时刻的输出,m是动量超参数,xt是当前时刻的输入。

说白了,就是不想让我当前时刻的输出只是依赖于我当前时刻的输入,我还希望和什么有关系呢?和之前时刻的输出有关系。动量这个超参数是0-1的一个参数;如果m是趋近于1的一个数,那么我的yt改变是非常缓慢的。

因为(1-m)是趋近于零的。

Moco是利用这个动量的特性,去缓慢的更新这个编码器,从而让中间学习到的字典特征尽可能保持的一致(这句话没看懂没关系,一会详细讲)

Moco摘要部分:

Moco把对比学习看成了是一个字典查询的东西,他们做了一个动态的字典,这个动态的字典分为两个部分,第一个部分是我们有一个队列,第二个部分是我们有一个移动平均的编码器。

队列里的样本呢,我们不需要做到梯度回传,所以我们可以往队列里放很多的负样本,从而让字典很大。

为什么还有一个移动平均的编码器呢,我们是想让字典里的特征尽可能的保持一致。

在训练过程中,我们发现,如果你有一个很大而且特征比较一致的字典,会让这个无监督的对比学习学的很好。

Moco从结果来说,在imagenet数据集上,如果采用linear pro去测试,Moco是可以取得和之前最好的无监督方式差不多或者更好的结果;linear pro指的是,我先预训练好一个骨干模型,然后我把这个骨干网络冻住,只取学习最后的全连接层,然后看在不同数据集上的表现结果。这样其实类似于把骨干网络当成了一个特征提取器,只从这里提取特征,这其实和我们使用resne差不多。

Moco一个很大的卖点,我们学习到的特征,在下游任务上有很好的迁移性,我们看重无监督优点就是它可以从大量无标注上的数据上学习到特征,可以迁移到没有那么多标注数据的任务上。

Moco在7个下游任务,分割,检测之类的超越之前的有监督预训练模型;举个例子,Moco使用同样的Resnet50,去做无监督,然后和有监督训练的模型去做比较。

引言部分:

GPT和BERT,已经证明无监督学习在NLP任务上是行得通的。但是CV领域,有监督预训练还是占据主导地位;

之前也有很多优秀的无监督工作,但是表现都会比无监督要差,作者认为这是因为CV领域NLP领域不同的原始信号空间。

对于自然原因来说,他们是离散的信号,也就是原始的信号空间,是有单词组成,或者更细一点,是由单词词缀组成的,所我们可以很容的去建立一个字典,然后让模型去学习特征。那么字典中的每个key就是一个类别,我们可以根据这个类别去学习模型(比如BERT就是最后一个softmax操作吗,不就是分类操作吗)

但是对于CV领域来讲,完全不一样。CV领域的信号是在一个连续而且高维的空间,它并不像单词那样有很强的的语义信息而且浓缩的非常好,没有那么简洁;所以CV领域并不适合去建立一个字典,去学习模型;如果没有这个字典,无监督就很难去建模。所以在CV领域,出现无监督还不如有监督学习。

在之前有很多优秀的对比学习工作,都可以归纳为一种字典查询的工作。

我们之前来看图:

image-20220813174006623

两个编码器,一个是E11,一个是E12;然后我们x1这个图片经过数据增强T1得到的图片X11,然后经过E11这个编码器,得到了图片表征f11;同理,我们这个图片x1,经过数据增强T2,得到的图片x12,然后经过E12这个编码器,得到了f12这个图片。

我们把X11这个图片叫做archor,瞄点,x12叫做x11的正样本。

什么是负样本呢?就是图片里剩余的所有的图片都是负样本,那么负样本走哪个编码器呢?走的是E12这个编码器,因为我们正样本和负样本我们都是相对于瞄点来说的,所以正样本和负样本要走同一个编码器,从而让特征的获取过程保持一致性。于是这样负样本x2,x3,x4等等也经过E12得到了真正的负样本表征f2,f3,fn;

那么我们把f11叫做query,把f12,f2,f3,fn叫做key;

那么对比学习的过程就是想要在特征空间里,正样本的key和我query近,其余的key离我远。

我们其实可以把key集合看成字典。那么对比学习的过程,就是想得到一个模型,让query在字典中那个和自己匹配正样本更近。

如果把对比学习的过程看成一个动态字典的过程,如果想要得到一个比较好的效果,那么字典最好需要满足两个条件,第一个就是字典足够的大,第二个就是在训练的时候尽量保持一致性。

首先第一个我们在做对比学习的时候,肯定不是一个batch一个batch的去做,所以如果key这个字典足够的大,那么我们从中抽样的可能性组合就很大,那么模型的泛化性就很大。如果字典很少,泛化性就不足,相当于数据来那个不够。

第二个是保持一致性,是因为我们需要字典中的特征尽可能使用同一个或者相近的编码器进行表征。因为如果不这样做。那么模型可能就学习到和query使用同样的编码器的那个key,导致模型泛化性不足,走了捷径。

所以Moco要做的就是一句话:在对比学习框架中,提供一个又大又一致的字典;框架图如下:

image-20220813180424913

大字典是怎么做到的:维护一个队列,把每次训练的batch-size和队列大小分离开;具体来说就是这个队列可以很大,但是我们每次更新这个队列,是一点点的更新的,也就是说当我们用一个很小的batchsize的时候,那么我们把现在batch中的特征进入队列,把最老的batch-size的特征抽离队列;那么我们的队列就可以设置的很大,比几万。这样我们用一个GPU也可以很好的训练模型;

那么一致性是如何做到的?刚才说了,每次都是使用新的编码器更新batch大小的队列特征,除了这个之外的,我们都是使用的之前的编码器得到的,这不就不一致了吗?那么就用动量更新就可以,我们最开始的右边分支的编码是由左边的初始化而来,后续更新使用对右边这个编码器参数进行动量更新,m足够大,保障右边编码器更新的非常缓慢,从公式来说,就是这个图:

image-20220813181009939

可以看到,右边编码器会被之前的k编码,和当前时刻的q编码影响,m足够大,无限接近于1,那么就是可以认为无限被k控制,更新的就会非常缓慢。

(有个疑问,直接不更新不就可以了吗,不进行梯度回传?)

Moco只是建立中间模型的一个方式,是很灵活的,可以和很多代理任务结合,这里使用个体判别,之前讲过。

无监督最大的一个卖点,就是我们的模型在大量无标注的数据集上进行训练之后,我们得到的特征,可以很好的迁移到下游任务中(比如标注数据很少的任务中);

Moco结论部分:

Moco论文在imagenet得到了很好的结果,然后在自己facebook自己数据集是上也得到了很好的结果,但是提升不大,在数据集从100万到10个亿,提升不大,作者认为大规模数据没有被利用起来,可能一个更好的代理任务会有更好的效果。所以作者谈到,除了个体判别这个任务,有没有可能把moco和mask encoded这个任务结合起来,就是类似BERT这种操作,使用mlm自监督的方式去学习。(这不就是MAE模型吗,我之前讲过);

这个其实在开头有讲CV和NLP信号空间不一致,直接把bert方式搬过来,可能不太行,具体去看MAE模型;

Moco相关工作部分:

一般来说自监督可以有两部分可以去做,一个是在损失函数部分深挖,一个是在代理任务上做文章。

(注解:自监督学习是无监督学习的一种)

NCE损失函数把一个超级大的多分类(这个时候softmax是工作不了,计算量太大)转变成一系列的二分类问题,从而让大家可以正常使用softmax,(这个是w2c很类似)

InfoNCE是NCE的一个变体,如果只

温度超参数

在看INfoNCE 的损失函数的时候,首先从softmax看起,这个是softmax的公式:

image-20220813184150145

然后我们加一个-log就是交叉熵损失函数:

image-20220813184228985

这个公式其实可以直接用在对比学习中。

什么意思呢? 交叉熵是一个多分类问题的损失函数,一个one-hot向量,和我真实输出做一个损失,目标是为了让真正标签的输出尽可能的大。

那么有一个问题,如果把这个损失函数,直接套到对比学习中去,那么是什么意义呢?

比如imagenet100万个图片,那么我当前图片的这个数据增强之后的图片经过编码器1得到了瞄点特征,经过比编码器2得到了正样本,也就是我的groud-turth;

那么除了我当前这个图片,100万个图片之外的所有图片经过编码器2这个得到的表征都是负样本,也就是会得到这样一个向量:

1 0 0 0 (1个1,100万-1个0)

在这个上面我做交叉熵,其实就是可以用在对比学习上。

但是这样做softamx计算量太大了,其实bert这种模型,也就是几万个类别,没啥问题,几百万太难了。

这个时候NCE就是一种很好的解决方式,化成一个二分类问题,就是我现在只有两个类别,一个是正常样本,除此之外的都是噪声样本。(计算量没降低下来,这个我待定在看词向量的时候再去看)

但是这样做不太清楚,所以INFONCE就出来了。

也是与其你在整个数据集去走loss,不如我抽样一部分去做loss。如果你选取的抽样的这部分很少,那么就没啥意义,不能模拟整个数据集,所以抽样的部分还是要大一点。那么这个字典的大小就很重要,也就是我字典的大小就是我们的分母下方的类别数量;那么这个过程中InfoNCE就把NCE的一系列二分类又转为了多分类。

image-20220813185327264

q就是我query表征,也就是瞄点那个图片特征,k+就是正样本,分母累加那里的K,就是我们的负样本数量,分类累加了K+1,因为K个负样本+一个正样本。

温度参数T(其实是tao),在蒸馏那里其实我讲过,如果t很大,那么softmax分布会很平滑,看不出区别,就是把所有的负样本一视同仁,导致模型学习没有轻重;如果tao很小,分布会更尖锐,会让模型只关注那个困难的负样本,其实那些负样本很有可能是潜在的正样本,如果模型过度的关注这个困难的负样本,会导致模型很难收敛,或者学号的特征不太好去泛化。

去除这个温度超参数,InfoNCE本质就是一个交叉熵损失函数,只不过类别和所有样本相比,做了个近似,做了个个随机抽样,就是字典大小。Moco伪代码InfoNCE直接就是用的交叉熵损失函数代码。

有个细节,为什么使用队列这种数据结构存储字典呢?

因为先进先出,每次一个batch进来,最老的那个部分batch数据会出去,这部分数据是过时的,从而能够保持队列中的特征尽可能的一致性。

另一个细节:

第二个分支不能随着走这一支的样本使用梯度回传进行更新,为什么呢?因为如果这样做了,第二个分支的编码器就更新的太快了,这些特征,我们是放到字典中去的,就会导致特征不一致。

为什么第二个分支直接就不更新,反而还缓慢更新(我自己理解是不太可以的,因为正样本的定义规则,经过编码器之后语义空间类似,所以是正样本。如果第二个分支一直不变,其实模型在训练的时候就很样本,因为可能到后来,第一个分支和第二个分支编码器差距越来越大,其实是本来是正样本的,损失也很大,就很难训练了。)

两个贡献:

一个是很大的字典:计算损失的时候使用多分类,能够很近似整个数据集上做多分类损失

一个是字典内特征一致性,使用动量更新:

需要注意的一点:就是infonce损失计算的是整个字典做多分类。minibatch大小和字典大小剥离开,batch可以设置为256,然后进来256个样本,每个样本都需要做一个瞄点,走一遍对比学习的流程。

动量设置为了0.99,很大了。字典大小是65536

在Moco之前的工作, 字典和字典特征一致性经常不能同时满足。

端到端的框架:

image-20220813191821956

端到端的框架就是两个编码器都可以通过梯度回传进行更新,因为xq和xk都是从同一个batch中来的,我们通过一次forward就可以拿到所有样本的特征,我们直接梯度回传就可以了。这要求,我们batc大小要足够的大,那么infonce才能起作用,做到一个近似的全部数据集的多分类。SIMCLR就是这个端到端。这样字典是高度一致的。在这种情况下,batch大小和字典大小是等价的。simclr就是用了8192作为batch大小。

另一流派,更关注字典的大,然后牺牲一些一致性,就是memory bank;在这个流派只有一个编码器,就是query的编码器,可以进行梯度回传进行更新。对于query这边,是没有一个单独的编码器。

memory bank就是把整个数据集的特征,都存到了一起。对于imagenet来说,这里就是128万个特征(作者说到,每个特征128维度,只需要600M的空间,还好。)

然后每次训练的时候,从memroy bank中随机抽样字典大小就可以了。右边的编码是在线下执行。

在执行的时候,比如字典是3,那么抽出三个来,左边做一次梯度回传之后,我们把字典中的3个用新的编码器做一个编码,放回到memroy bankl中去。

(首先,我认为为了保持正样本的定义,肯定得更新样本特征)

因为你这个更新操作,导致字典内编码特征不一致。

image-20220813193211079

Moco伪代码,讲解的非常好:

几个中点:

第一个动量是0.99,字典大小是65536

第二个是损失函数底下类别是65536+1个=65537,是把所有字典中的都是当成了负样本(这样其实很有可能存在潜在的正样本,不过影响不大 ,一定要注意,这个时候我这次更新的样本特征,还没有放入到字典中去,所以仅仅是可能存在正样本,当前这正样本是一定不在字典中的)。

参考:https://www.bilibili.com/video/BV1C3411s7t9/?spm_id_from=333.788