MoCo论文详解


前言

 MoCo是Facebook团队在2020年提出的一篇论文,论文全称是《Momentum Contrast for Unsupervised Visual Representation Learning》通过动量对比进行无监督的视觉表征学习,MoCo就是Momentum Contrast的缩写。MoCo通过使用对比学习的无监督方法在大规模数据集上进行预训练,然后在下游任务上进行迁移,其在下游任务上的表现在分割/检测等7个任务上优于现有的有监督的预训练方法,在视觉领域有监督方法和无监督方法之间的差距被大大减小。接下来将对MoCo这篇论文进行介绍。

一、对比学习简介

 对比学习(Contrastive Learning)是一种无监督或自监督学习方法,它通过比较样本对来学习数据的表示。这种方法的核心思想是,相似的样本应该在表示空间中彼此靠近,而不相似的样本应该彼此远离。对比学习的典型范式就是:代理任务+目标函数。如下图所示:
在这里插入图片描述
 在监督学习中,模型的输出与标签通过目标函数计算出Loss然后进行反向传播更新参数,而在对比学习中,模型的输出通过代理任务计算出Loss然后进行反向传播,可以理解为代理任务起到了标签的作用,监督模型完成我们指定的任务。举一个具体的例子:

假如我们现在有一些图片,其中图片A和图片B是比较相似的,然后我们将这些图片经过编码器,我们希望相似的图片A和B经过编码器后的输出距离较近,而与其他不相似的距离较远。所以我们要做的就是训练这样一个编码器。这里,图片A和图片B(相似的图片)是正样本对,其他图片是负样本。

 那么如何知道哪些图片是相似的,哪些图片是不相似的呢?这个可以通过人为规定代理任务而进行定义,因此对比学习可以看作是一种无监督或者自监督学习。

二、MoCo理论

 MoCo认为之前的这些基于对比学习的工作都可以归纳为构造一个动态的字典。字典中的key是通过在数据集采样然后通过编码器进行编码得到的。然后通过训练这个编码器,让编码后的查询query与字典中对应的key尽可能相似,而与字典中其他的key相似度尽可能低。构造的这个动态字典应该有两个特性:1.大。一个大的字典可以包含更多的样本,在算相似性的时候更具有一般性。2.一致性。这个字典中的key应该是由同一个或相似的编码器编码得到,这样在与query进行比较时才公平。之前的这些方法都很难同时满足这两个特性。
因为如果想让一个很大的字典中的所有元素都是由相同的编码器得到,那么你的batchsize需要很大才行,但是这样的话你的显卡肯定是装不下的,就算能装下也不好更新参数。可能有聪明的小伙伴会说,那我加个循环分好几个batchsize放进去就可以了,比如字典中需要有1280个元素,每次batchsize为128,循环10次放进去就可以了。但是这样在更新编码器参数的时候,是只根据最后一个batchsize更新的,也就是说1280个元素,只有最后的128个元素参与了更新,这是不符合常理的。
 MoCo的思想是使用队列来构建这个很大的字典,首先将一个batch的数据通过编码器,然后更新队列,让最早的那一批batch出去,然后在队尾放入新的batch,这样字典的大小就与batchsize的大小分离开来,接着在根据当前字典中的key与query算loss,更新编码器的参数。这里有个问题,既然每算一个batch就会更新一次参数,那么这个字典中的每一个batch都是由不同的编码器得到的,那就不符合一致性的特点了。为了解决这个问题,MoCo使用动量来进行编码器的缓慢更新,来近似保持一致。并且由于队列先进先出的特性,最早的编码器产生的batch早就被移出去了,所以字典中的元素可以近似看作是同一个编码器产生的。
其具体框架如下图所示:
在这里插入图片描述
 MoCo使用了两个编码器,分别是左边的编码器q和右边的动量编码器。在论文里这两个编码器的结构是一样的,但是更新后的参数是不一样的。首先会进来一个查询 x q u e r y x^{query} xquery和一个batch的数据 [ x 0 k e y , x 1 k e y , x 2 k e y . . . ] [x_0^{key},x_1^{key},x_2^{key}...] [x0key,x1key,x2key...]分别经过编码器得到 q q q [ k 0 , k 1 , k 2 . . . ] [k_0,k_1,k_2...] [k0,k1,k2...],接着使用Contrastive loss计算损失,然后使用梯度下降法更新编码器q,使用动量下降法缓慢更新动量编码器,然后将 [ k 0 , k 1 , k 2 . . . ] [k_0,k_1,k_2...] [k0,k1,k2...]放进字典更新队列。

在大规模数据集上训练完成后,可以认为编码器q已经学到了数据之间的差异,可以把不同的输入映射到不同的空间,然后使用编码器q在下游任务上进行迁移学习。

接下来介绍MoCo中的几个关键点。
代理任务
 MoCo使用的是对比学习,其代理任务使用个体判别任务(instance discrimination task)。如下图所示:有一批数据 [ x 1 , x 2 , x 3 , x 4 , x 5 ] [x_1,x_2,x_3,x_4,x_5] [x1,x2,x3,x4,x5],数据 x 1 x_1 x1经过变换后得到两张图片 x 1 1 , x 1 2 x_1^1,x_1^2 x11,x12,这两张图片就是正样本对,对于 x 1 1 x_1^1 x11来说, x 1 2 x_1^2 x12是与它相似的,而其他所有图片 [ x 2 , x 3 , x 4 , x 5 ] [x_2,x_3,x_4,x_5] [x2,x3,x4,x5]是不相似的,是负样本。然后 x 1 1 x_1^1 x11可以充当查询Query,其他图片就是key。
在这里插入图片描述

损失函数
 MoCo的损失函数为InfoNCE,公式如下:

L q = − log ⁡ exp ⁡ ( q ∗ k + / τ ) ∑ i = 0 K exp ⁡ ( q ∗ k i / τ ) L_q = -\log \frac{\exp(q*k_+/\tau)}{\sum_{i=0}^K\exp(q*k_i/\tau)} Lq=logi=0Kexp(qki/τ)exp(qk+/τ)

这个公式可以看作是K分类的交叉熵损失,K为字典的大小,也就是字典中有多少个元素就是多少类, τ \tau τ为温度系数, τ \tau τ越大,那么softmax后的数值分布越集中, τ \tau τ越小,softmax后的数值分布越分散。 q q q为编码器q的输出, k + k_+ k+为正样本。

动量更新
 使用队列的方法使得字典变大,但是这也使得动量编码器难以进行反向传播,因为进行反向传播的时候梯度应该回传到所有样本中,而显卡很难装下这么多样本。一个简单的方法是每次更新完编码器q后直接把q的参数赋值给动量编码器,但是如此频繁的参数更新影响力字典中的一致性。因此,MoCo使用动量来更新参数。其公式为:

θ k = m θ k + ( 1 − m ) θ q \theta_k = m\theta_{k} + (1-m)\theta_q θk=mθk+(1m)θq

其中, θ k \theta_k θk为动量编码器的参数,动量编码器的参数加上编码器q的参数得到更新后的参数。通过 m m m的大小来控制动量编码器的更新程度。在MoCo中 m m m取0.999。

三、MoCo伪代码实现

#f_q,f_k:编码器q和动量编码器
for epoch in range(epochs):
	f_k.params = f_q.params # 初始化,将q的参数赋值给动量编码器k
	for x in loader: # 假设Batchsize为N
		x_q = aug(x) # 进行图像变换  
		x_k = aug(x) # 进行图像变换  
		q = f_q.forward(x_q) # NxC C为编码后的维度
		k = f_k.forward(x_k) # NxC C为编码后的维度
		k = k.detach() # 去掉梯度,因为也不用梯度进行更新
		# 相当于是损失函数的分子 Nx1
		l_pos = bmm(q.view(N,1,C), k.view(N,C,1))
		# 相当于是损失函数的分母 NxK  K为字典中的元素个数
		l_neg = mm(q.view(N,C), queue.view(C,K))
		# logits: Nx(1+K)
		logits = cat([l_pos, l_neg], dim=1)
		# 对于batch中的每个元素正确答案都是在第一个位置上,也就是0
		labels = zeros(N) 
		loss = CrossEntropyLoss(logits/t, labels)
		# 梯度下降法更新编码器q的参数
		loss.backward()
		update(f_q.params)
		# 动量更新编码器k的参数
		f_k.params = m*f_k.params+(1-m)*f_q.params
		# 更新字典队列
		enqueue(queue, k) # 将最新batch的编码后特征送进队列
		dequeue(queue) # 弹出队列最前面的batch

四、疑问

 其实,对于这篇论文我还是有些疑问:

  1. 既然使用动量的目的就是缓慢更新编码器参数,那我直接不更新不好吗。

  2. 在代码中标签都是0,这样在训练的时候不会过拟合吗?

有知道的小伙伴麻烦在评论区告诉我一下。

总结

 MoCo是一种对比学习方法,使用队列构建了一个动态的字典,然后使用动量学习的方法缓慢更新动量编码器的参数,实现了一个又大又一致性的字典,并且在迁移到下游任务的时候取得了很不错的结果。

MoCo( Momentum Contrast)是一种对比学习框架,在计算机视觉领域具有重要影响。虽然当前引用未直接提及 MoCo 的原始论文,但可以提供其核心信息如下: MoCo 原始论文标题为《Momentum Contrast for Unsupervised Visual Representation Learning》,由 He et al. 提出,并发表于 CVPR 2020 [^3]。该论文的核心贡献在于设计了一种动态字典的方法来解决对比学习中的内存瓶颈问题。通过引入动量更新机制,MoCo 能够维持一个一致的队列以存储负样本特征,从而提高无监督表示学习的效果。 以下是 MoCo 的主要特点: - 动量更新编码器:利用两个编码器(query encoder 和 key encoder),其中 key encoder 使用动量更新策略逐步调整参数。 - 队列机制:为了避免显存占用过高,MoCo 设计了一个循环队列来保存历史负样本特征。 ```python import torch.nn as nn class MoCo(nn.Module): def __init__(self, base_encoder, dim=128, K=65536, m=0.999, T=0.07): super(MoCo, self).__init__() self.K = K self.m = m self.T = T # 创建 query 和 key 编码器 self.encoder_q = base_encoder(num_classes=dim) self.encoder_k = base_encoder(num_classes=dim) # 初始化 key 编码器权重与 query 编码器相同 for param_q, param_k in zip(self.encoder_q.parameters(), self.encoder_k.parameters()): param_k.data.copy_(param_q.data) param_k.requires_grad = False # 注册队列 self.register_buffer("queue", torch.randn(dim, K)) self.queue = nn.functional.normalize(self.queue, dim=0) @torch.no_grad() def _momentum_update_key_encoder(self): """ 更新 key 编码器参数 """ for param_q, param_k in zip(self.encoder_q.parameters(), self.encoder_k.parameters()): param_k.data = param_k.data * self.m + param_q.data * (1. - self.m) ``` [^3]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值