总结下 Self-Supervised Learning 的方法,用 4 个英文单词概括一下就是:
Unsupervised Pre-train, Supervised Fine-tune.
在预训练阶段我们使用无标签的数据集 (unlabeled data),因为有标签的数据集很贵,打标签得要多少人工劳力去标注,那成本是相当高的,所以这玩意太贵。相反,无标签的数据集网上随便到处爬,它便宜。在训练模型参数的时候,我们不追求把这个参数用带标签数据从初始化的一张白纸给一步训练到位,原因就是数据集太贵。于是 Self-Supervised Learning 就想先把参数从 一张白纸 训练到 初步成型,再从 初步成型 训练到 完全成型。注意这是2个阶段。这个训练到初步成型的东西,我们把它叫做 Visual Representation。预训练模型的时候,就是模型参数从 一张白纸 到 初步成型 的这个过程,还是用无标签数据集。等我把模型参数训练个八九不离十,这时候再根据你 下游任务 (Downstream Tasks) 的不同去用带标签的数据集把参数训练到 完全成型,那这时用的数据集量就不用太多了,因为参数经过了第1阶段就已经训练得差不多了。
第1个阶段不涉及任何下游任务,就是拿着一堆无标签的数据去预训练,没有特定的任务,这个话用官方语言表达叫做:in a task-agnostic way。第2个阶段涉及下游任务,就是拿着一堆带标签的数据去在下游任务上 Fine-tune,这个话用官方语言表达叫做:in a task-specific way
以上这些话就是 Self-Supervised Learning 的核心思想,如下图1所示。

MoCo v1
论文名称:Momentum Contrast for Unsupervised Visual Representation Learning
论文地址:https://arxiv.org/pdf/1911.05722.pdf
开源地址:facebookresearch/moco
MoCo 系列也遵循这个思想,预训练的 MoCo 模型也会得到 Visual Representation,它们可以通过 Fine-tune 以适应各种各样的下游任务,比如检测和分割等等。MoCo在 7 个检测/语义分割任务(PASCAL VOC, COCO, 其他的数据集)上可以超过他的有监督训练版本。有时会超出很多。这表明在有监督与无监督表示学习上的差距在许多视觉任务中已经变得非常近了。
自监督学习的关键可以概括为两点:Pretext Task,Loss Function,在下面分别介绍。
自监督学习的 Pretext Task
Pretext Task 是无监督学习领域的一个常见的术语,其中 "Pretext" 翻译过来是"幌子,托词,借口"的意思。所以 Pretext Task 专指这样一种任务:这种任务并非我们所真正关心的,但是通过完成它们,我们能够学习到一种很好的表示,这种表示对下游任务很重要。
The term "pretext" implies that the task being solved is not of genuine interest, but is solved only for the true purpose of learning a good data representation.
我这里举几个例子:
- BERT 的 Pretext Task:在训练 BERT 的时候,我们曾经在预训练时让它作填空的任务,
如下图所示,把这段输入文字里面的一部分随机盖住。就是直接用一个Mask把要盖住的token (对中文来说就是一个字)给Mask掉,具体是换成一个特殊的字符。接下来把这个盖住的token对应位置输出的向量做一个Linear Transformation,再做softmax输出一个分布,这个分布是每一个字的概率。因为这时候BERT并不知道被 Mask 住的字是 "湾" ,但是我们知道啊,所以损失就是让这个输出和被盖住的 "湾" 越接近越好。

通过这种方式训练 BERT,得到的预训练模型在下游任务只要稍微做一点 Fine-tune,效果就会比以往有很大的提升。
所以这里的 Pretext Task 就是填空的任务,这个任务和下游任务毫不相干,甚至看上去很笨,但是 BERT 就是通过这样的 Pretext Task学习到了很好的 Language Representation,很好地适应了下游任务。
- SimCLR 的 Pretext Task:在训练 SimCLR 的时候,我们曾经在预训练时试图教模型区分相似和不相似的事物,如下图3所示,假设现在有1张任意的图片 \(x\) ,叫做Original Image,先对它做数据增强,得到2张增强以后的图片 \(x_i, x_j\) 。接下来把增强后的图片\(x_i, x_j\)输入到Encoder里面,注意这2个Encoder是共享参数的,得到representation \(h_i, h_j\) ,再把 \(h_i, h_j\) 继续通过 Projection head 得到 representation \(z_i, z_j\) ,这里的2个 Projection head 依旧是共享参数的,且其具体的结构表达式是:
接下来的目标就是最大化同一张图片得到的 \(z_i^1,z_j^1\),最小化不同张图片得到的 \(z_i^1,z_j^1,z_i^2,z_j^2,z_i^3,z_j^3,z_i^4,z_j^4,z_i^5,z_j^5,...\) 。

通过上图这种方式训练 SimCLR,得到的预训练模型在下游任务只要稍微做一点 Fine-tune,效果就会比以往有很大的提升。
所以这里的 Pretext Task 就是试图教模型区分相似和不相似的事物,这个任务和下游任务毫不相干,甚至看上去很笨,但是 SimCLR 就是通过这样的 Pretext Task学习到了很好的Image Representation,很好地适应了下游任务。
还有一些常见的 Pretext Task 诸如denoising auto-encoders,context autoencoders,cross-channel auto-encoders等等,这里就不一一介绍了。
自监督学习的 Contrastive loss
Contrastive loss 来自于下面这篇 Yann LeCun 组的工作,如何理解这个对比损失呢?
http://yann.lecun.com/exdb/publis/pdf/hadsell-chopra-lecun-06.pdf
常见的损失函数是 cross entropy loss,它适合于数据的 label 是 one-hot 向量的形式。此时网络结构的最后一层是softmax,输出得到各个类的预测值。比如现在有3个类:dog, cat, horse,它们的 label 分别对应着 (1,0,0), (0,1,0), (0,0,1),cross entropy loss会让 dog 图片的输出尽量接近 (1,0,0),让 cat 图片的输出尽量接近 (0,1,0),让 horse 图片的输出尽量接近 (0,0,1)。但是这也存在一个问题,就是假设再来3个类,分别是sky,car和bus。那么按道理dog与horse的距离 应该比dog与sky的距离近,因为dog与horse都属于动物;car与bus的距离 应该也比car与cat的距离近,因为car与bus都属于车类。但是 cross entropy loss确是一视同仁地把 dog与horse的距离 和 dog与sky的距离 看作是一样的。
Contrastive loss 的初衷是想让:
- 相近的样本之间的距离越小越好。
- 相远的样本之间的距离越大越好。
如果神经网络的损失函数只满足条件1,那么网络会让任何的输入都输出相同的值,不论输入是 dog, cat, horse 还是 sky,car, bus,输出都是一样的,这确实满足了相近的样本之间的距离越小越好,但是却使得网络丧失了分类能力。
如果神经网络的损失函数只满足条件 1 和 2,是不是就完善了呢?
实际上如果想让相远的样本之间的距离越大越好,就需要一个边界,否则如果 dog 是 (1,0,0),那么 假设第1轮训练网络输出 cat 是 (0,1,0),第2轮训练网络输出 cat 是 (0,5,0)...这样下去dog 与 cat 之间的距离越来越大,网络却没法收敛。
Contrastive loss 改进的思路就是让相远的样本之间的距离越大越好,但是这个距离要有边界,即要求:
- 相近的样本之间的距离越小越好。
- 相远的样本之间的距离越大越好,这个距离最大是 \(m\) 。
如下图弹簧图所示:黑色实心球代表与蓝色球相近的样本,白色空心球代表与蓝色球相远的样本,蓝色箭头的长度代表力的大小,方向代表力的方向。
(a):Contrastive loss 使得相近的样本接近。
(b):横轴代表样本之间的距离,纵轴代表loss值。Contrastive loss 使得相近的样本距离越小,loss值越低。
(c):Contrastive loss 使得相远的样本疏远。
(d):横轴代表样本之间的距离,纵轴代表loss值。Contrastive loss 使得相远的样本距离越大,loss值越低,但距离存在上界,就是红线与x轴的交点 \(m\) ,代表距离的最大值。
(e):一个样本受到其他各个独立样本的作用,各个方向的力处于平衡状态,代表模型参数在 Contrastive loss 的作用下训练到收敛。

Contrastive loss 用公式表示为:
式中, \(\vec{X_1}, \vec{X_2}\) 是2个样本,\(D_W\) 是 \(\vec{X_1}\) 与 \(\vec{X_2}\) 在潜变量空间的欧几里德距离, \(Y=0\) 代表是相近的样本,此时要求 \(D_W\) 尽量接近0。 \(Y=1\) 代表是相远的样本,此时要求 \(D_W<m\) 时与 \(m\) 越接近越好,但是距离一旦超过 \(m\) 即失效,不会再接着更新参数使得距离越来越大。
有意思的是那些属于不同类,但两两距离天生大于m的样本对。 LeCun 的对比损失完全忽视这些样本对,大大减少了计算量。此外,Contrastive loss 提供了一个不同类别样本点之间距离的下界 \(m\) 。
MoCo v1之前的做法
了解了 Pretext Task 和 Contrastive loss,接下来就要正式介绍MoCo v1的原理了。

如上图所示,输入 \(x^q\) 通过编码器 Encoder 得到 query \(q\) ,还有几个输入 \(x^{k_0}, x^{k_1}, x^{k_2}\) 通过编码器 Encoder 得到 key \(k_0, k_1, k_2\) 。假设只有一个 key \(k_+\) 和 \(q\) 是匹配的。根据上面的 Contrastive loss 的性质,只有当 \(q\) 和相匹配的 \(k_+\) 相近,且与其他不匹配的 \(k_0, k_1,k_2\) 相远时, Contrastive loss 的值才会最小。这个工作使用点积作为相似度的度量,且使用 InfoNCE 作为Contrastive loss,则有:
式中, \(\tau\)是超参数,这个式子其实非常像把 q 分类成第 \(k_+\) 类的cross entropy loss。
这里的 \(x^q, x^k\) 可以是image,可以是image patches等, \(q=f_q(x^q), k=f_k(x^k)\), \(f_q, f_k\) 是Encoder,也可以是很多种架构。
原始的端到端自监督学习方法
对于给定的一个样本 \(x^q\) , 选择一个正样本 \(x^{k_+}\) (这里正样本的对于图像上的理解就是 \(x^q\) 的 data augmentation版本)。然后选择一批负样本 (对于图像来说,就是除了 \(x^q\) 之外的图像),然后使用 loss function \(\mathcal{L}\) 来将 \(x^q\) 与正样本之间的距离拉近,负样本之间的距离推开。样本 \(x^q\) 输入给 Encoder \(f_q\) ,正样本和负样本都输入给 Encoder \(f_k\) 。
这样其实就可以做自监督了,就可以进行端到端的训练了。实际上这就是最原始的图像领域的自监督学习方法,如下图所示,方法如上面那一段所描述的那样,通过loss function \(\mathcal{L}_q\) 来更新2个Encoder \(f_q, f_k\) 的参数。
原始的自监督学习方法里面的这一批负样本就相当于是有个字典 (Dictionary),字典的key就是负样本,字典的value就是负样本通过 Encoder \(f_k\) 之后的东西。那么现在问题来了:
问:这一批负样本,即字典的大小是多大呢?
答:负样本的规模就是 batch size,即字典的大小就是 batch size。
举个例子,假设 batch size = 256,那么对于给定的一个样本 \(x^q\) , 选择一个正样本 \(x^{k_+}\) (这里正样本的对于图像上的理解就是 \(x^q\) 的 data augmentation版本)。然后选择256个负样本 (对于图像来说,就是除了 \(x^q\) 之外的图像),然后使用 loss function \(\mathcal{L}_q\) 来将 \(x^q\) 与正样本之间的距离拉近,负样本之间的距离推开。
毫无疑问是 batch size 越大效果越好的,这一点在 SimCLR 中也得到了证明。但是,由于算力的影响 batch size 不能设置过大,因此很难应用大量的负样本。因此效率较低。


针对很难应用大量的负样本的问题,有没有其他的解决方案呢?下面给出了一种方案,如下图所示。
采用一个较大的memory bank
对于给定的一个样本 \(x^q\) , 选择一个正样本 \(x^{k_+}\) (这里正样本的对于图像上的理解就是 \(x^q\) 的 data augmentation版本)。采用一个较大的 memory bank 存储较大的字典,这个 memory bank 具体存储的是所有样本的representation。(涵盖所有的样本,比如样本一共有60000个,那么memory bank大小就是60000,字典大小也是60000),采样其中的一部分负样本 \(k^{sample}\) ,然后使用 loss function \(\mathcal{L}_q\) 来将 \(x^q\) 与正样本之间的距离拉近,负样本之间的距离推开。这次只更新 Encoder \(f_q\) 的参数,和几个采样的key值 \(k^{sample}\) 。因为这时候没有了 Encoder \(f_k\) 的反向传播,所以支持memory bank容量很大。
但是,你这一个step更新的是 Encoder \(f_q\) 的参数,和几个采样的key值 \({\color{purple}k^{sample_1}}\),下个step更新的是 Encoder \(f_q\) 的参数,和几个采样的key值 \({\color{red}k^{sample_2}}\),问题是 \({\color{purple}k^{sample_1}}\neq {\color{red}k^{sample_2}}\) ,也就是:Encoder \(f_q\) 的参数每个step都更新,但是某一个 \(k_i\) 可能很多个step才被采样到更新一次,而且一个epoch只会更新一次。这就出现了一个问题,即:每个step编码器都会进行更新,这样最新的 query 采样得到的 key 可能是好多个step之前的编码器编码得到的 key,因此丧失了一致性。

从这一点来看,原始的端到端自监督学习方法] 的一致性最好,但是受限于batchsize的影响。而 采用一个较大的memory bank存储较大的字典的字典可以设置很大,但是一致性却较差,这看起来似乎是一个不可调和的矛盾。
MoCo v1
kaiming大神利用 momentum (移动平均更新模型权重) 与queue (字典) 轻松的解决这个问题。为了便于读者理解,这里结合kaiming大神提供的伪代码一起讲解。


首先我们假设 Batch size 的大小是 \(N\) ,然后现在有个队列 Queue,这个队列的大小是 \(K(K>N)\) ,注意这里 \(K\) 一般是 \(N\) 的数倍,比如 \(K=3N,K=5N,... ,\)但是 \(K\) 总是比 \(N\) 要大的 (代码里面 \(N\)=65536 ,即队列大小实际是65536)。
下面如上图所示,有俩网络,一个是 Encoder \(f_q\) ,另一个是Momentum Encoder \(f_{MK}\) 。这两个模型的网络结构是一样的,初始参数也是一样的 (但是训练开始后两者参数将不再一样了)。 \(f_q\) 与 \(f_{MK}\) 是将输入信息映射到特征空间的网络,特征空间由一个长度为 \(C\) 的向量表示,它们在代码里面分别表示为:f_q , f_k和 C。
代码里的 k 可以看作模板,q 看作查询元素,每一个输入未知图像的特征由 f_q 提取, 现在给一系列由 f_k提取的模板特征 (比如狗的特征、猫的特征) ,就能使用 f_q与f_k的度量值来确定 f_q是属于什么。
1) 数据增强:
现在我们有一堆无标签的数据,拿出一个 Batch,代码表示为x,也就是 \(N\) 张图片,分别进行两种不同的数据增强,得到 x_q 和 x_k,则 x_q 是 \(N\) 张图片,x_k 也是 \(N\) 张图片。
for x in loader: # 输入一个图像序列x,包含N张图,没有标签
x_q = aug(x) # 用于查询的图 (数据增强得到)
x_k = aug(x) # 模板图 (数据增强得到),自监督就体现在这里,只有图x和x的数据增强才被归为一类
2) 分别通过 Encoder 和 Momentum Encoder:
x_q 通过 Encoder 得到特征 q,维度是 \(N,C\) ,这里特征空间由一个长度为 \(C=128\) 的向量表示。
x_q 通过 Momentum Encoder 得到特征 k,维度是 \(N,C\) 。
q = f_q.forward(x_q) # 提取查询特征,输出NxC
k = f_k.forward(x_k) # 提取模板特征,输出NxC
3) Momentum Encoder的参数不更新:
# 不使用梯度更新f_k的参数,这是因为文章假设用于提取模板的表示应该是稳定的,不应立即更新
k = k.detach()
4) 计算 \(N\) 张图片的自己与自己的增强图的特征的匹配度:
# 这里bmm是分批矩阵乘法
l_pos = bmm(q.view(N,1,C), k.view(N,C,1)) # 输出Nx1,也就是自己与自己的增强图的特征的匹配度
这里得到的 l_pos 的维度是 \((N, 1, 1)\),N 个数代表\(N\) 张图片的自己与自己的增强图的特征的匹配度。
5) 计算 \(N\) 张图片与队列中的 \(K\) 张图的特征的匹配度:
l_neg = mm(q.view(N,C), queue.view(C,K)) # 输出Nxk,自己与上一批次所有图的匹配度(全不匹配)
这里得到的 l_neg 的维度是 \((N, K)\),代表\(N\) 张图片与队列 Queue 中的\(K\)张图的特征的匹配度。
6) 把 4, 5 两步得到的结果concat起来:
logits = cat([l_pos, l_neg], dim=1) # 输出Nx(1+k)
这里得到的 logits 的维度是 \((N, K+1)\),把它看成是一个矩阵的话呢,有 \(N \)行,代表一个 Batch里面的 \(N\) 张图片。每一行的第1个元素是某张图片自己与自己的匹配度,每一行的后面\(K\) 个元素是某张图片与其他 \(K \)个图片的匹配度,如下图所示,展示的是某一行的信息,这里的 \(K=2\)。

7) NCE损失函数,就是为了保证自己与自己衍生的匹配度输出越大越好,否则越小越好:
labels = zeros(N)
# NCE损失函数,就是为了保证自己与自己衍生的匹配度输出越大越好,否则越小越好
loss = CrossEntropyLoss(logits/t, labels)
loss.backward()
8) 更新 Encoder 的参数:
update(f_q.params) # f_q使用梯度立即更新
9) Momentum Encoder 的参数使用动量更新:
# 由于假设模板特征的表示方法是稳定的,因此它更新得更慢,这里使用动量法更新,相当于做了个滤波。
f_k.params = m*f_k.params+(1-m)*f_q.params
10) 更新队列,删除最老的一个 Batch,加入一个新的 Batch:
enqueue(queue, k) *# 为了生成反例,所以引入了队列*
dequeue(queue)
全部的伪代码 (来自MoCo的paper):
f_k.params = f_q.params # 初始化
for x in loader: # 输入一个图像序列x,包含N张图,没有标签
x_q = aug(x) # 用于查询的图(数据增强得到)
x_k = aug(x) # 模板图(数据增强得到),自监督就体现在这里,只有图x和x的数据增强才被归为一类
q = f_q.forward(x_q) # 提取查询特征,输出NxC
k = f_k.forward(x_k) # 提取模板特征,输出NxC
# 不使用梯度更新f_k的参数,这是因为文章假设用于提取模板的表示应该是稳定的,不应立即更新
k = k.detach()
# 这里bmm是分批矩阵乘法
l_pos = bmm(q.view(N,1,C), k.view(N,C,1)) # 输出Nx1,也就是自己与自己的增强图的特征的匹配度l_neg = mm(q.view(N,C), queue.view(C,K)) # 输出Nxk,自己与上一批次所有图的匹配度(全不匹配)logits = cat([l_pos, l_neg], dim=1) # 输出Nx(1+k)labels = zeros(N)
# NCE损失函数,就是为了保证自己与自己衍生的匹配度输出越大越好,否则越小越好loss = CrossEntropyLoss(logits/t, labels)
loss.backward()
update(f_q.params) # f_q使用梯度立即更新# 由于假设模板特征的表示方法是稳定的,因此它更新得更慢,这里使用动量法更新,相当于做了个滤波。
f_k.params = m*f_k.params+(1-m)*f_q.params
enqueue(queue, k) # 为了生成反例,所以引入了队列d
equeue(queue)
FAQ
以上10步就是 MoCo 算法的流程。先把上面的流程搞清楚以后,我们思考以下几个问题:
- Encoder \(f_q\) 和 Momentum Encoder \(f_{MK}\)的输入分别是什么?
答:Encoder \(f_q\)的输入是一个Batch的样本\(x\)的增强版本 \(x_q\)。Momentum Encoder \(f_{MK}\)的输入是一个Batch的样本 \(x\)的另一个增强版本 \(x_k\) 和 队列中的所有样本 \(x_{queue}\),\(x_{queue}\)通过 Momentum Encoder 得到代码中的变量 queue。
- Encoder \(f_q\)和 Momentum Encoder \(f_{MK}\)的更新方法有什么不同?
答:Encoder \(f_q\) 在每个 step 都会通过反向传播更新参数,假设 1 个 epoch 里面有500 个 step,Encoder \(f_q\) 就更新 500次。Momentum Encoder \(f_{MK}\) 在每个 step 都会通过动量的方式更新参数,假设 1 个 epoch 里面有500 个 step,Momentum Encoder \(f_{MK}\)就更新 500次,只是更新的方式是:
式中, \(m\) 是动量参数,默认取 =0.999 ,这意味着 Momentum Encoder的更新是极其缓慢的,而且并不是通过反向传播来更新参数,而是通过动量的方式来更新。
- MoCo 相对于原来的两种方法的优势在哪里?
答:在"原始的端到端自监督学习方法"里面,Encoder \(f_q\)和 Encoder \(f_k\)的参数每个step 都更新,这个问题在前面也有提到,因为Encoder \(f_k\)输入的是一个 Batch 的 negative samples,所以输入的数量不能太大,即dictionary不能太大,即 Batch size不能太大。
现在的 Momentum Encoder \(f_{MK}\)的更新是通过(2),以动量的方法更新的,不涉及反向传播,所以 输入的负样本 (negative samples) 的数量可以很多,具体就是 Queue 的大小可以比较大,那当然是负样本的数量越多越好了。这就是 Dictionary as a queue 的含义,即通过动量更新的形式,使得可以包含更多的负样本。而且 Momentum Encoder 的更新极其缓慢 (因为 m=0.999 很接近于1),所以Momentum Encoder 的更新相当于是看了很多的 Batch,也就是很多负样本。
在"采用一个较大的memory bank存储较大的字典"方法里面,所有样本的 representation 都存在 memory bank 里面,根据上文的描述会带来最新的 query 采样得到的 key 可能是好多个step之前的编码器编码得到的 key,因此丧失了一致性的问题。但是MoCo的每个step都会更新Momentum Encoder,虽然更新缓慢,但是每个step都会通过(2)更新一下Momentum Encoder,这样 Encoder\(f_q\)和Momentum Encoder\(f_{MK}\)每个step 都有更新,就解决了一致性的问题。
实验
Encoder的具体结构是 ResNet,Contrastive loss (1)的超参数 \(\tau\)=0.07 。
数据增强的方式是 (都可以通过 Torchvision 包实现):
- Randomly resized image + random color jittering
- Random horizontal flip
- Random grayscale conversion
此外,作者还把 BN 替换成了 Shuffling BN,因为 BN 会欺骗 pretext task,轻易找到一种使得 loss 下降很快的方法。
自监督训练的数据集是:ImageNet-1M (1280000 训练集,各类别分布均衡) 和 Instagram-1B (1 billion 训练集,各类别分布不均衡)
优化器:SGD,weight decay: 0.0001,momentum: 0.9。
Batch size: N=256
初始学习率: 0.03,200 epochs,在第120和第160 epochs时分别乘以0.1,结束时是0.0003。
实验一:Linear Classification Protocol
评价一个自监督模型的性能,最关键和最重要的实验莫过于 Linear Classification Protocol 了,它也叫做 Linear Evaluation,具体做法就是先使用自监督的方法预训练 Encoder,这一过程不使用任何 label。预训练完以后 Encoder 部分的权重也就确定了,这时候把它的权重 freeze 住,同时在 Encoder 的末尾添加Global Average Pooling和一个线性分类器 (具体就是一个FC层+softmax函数),并在某个数据集上做Fine-tune,这一过程使用全部的 label。
上述方法 [1 原始的端到端自监督学习方法],[2 采用一个较大的memory bank存储较大的字典],[3 MoCo方法] 的结果对比如下图所示。

上图里面的 \(K\):
- 对于[MoCo方法] 来讲就是队列Queue的大小,也是负样本的数量。
- 对于[原始的端到端自监督学习方法] 是一个 Batch 的大小,那么这种方法的一个 Batch 有 1 个正样本和 K-1 个负样本。因为对于给定的一个样本 \(x^q\), 选择一个正样本 \(x^{k+}\)(这里正样本的对于图像上的理解就是 \(x^q\)的 data augmentation版本)。然后选择一批负样本 (对于图像来说,就是除了 $x^q$ 之外的图像),样本 $x^q$ 输入给 Encoder \(f_q\),正样本和负样本都输入给 Encoder \(f_k\),所以有 \(K-1 \)个负样本。
- 对于 [采用一个较大的memory bank存储较大的字典] 方法来讲,也是负样本的数量。
我们看到图中的3条曲线都是随着 \(K\) 的增加而上升的,证明对于每一个样本来讲,正样本的数量都是一个,随着负样本数量的上升,自监督训练的性能会相应提升。我们看图中的黑色线 \(K\) 最大取到了1024,因为这种方法同时使用反向传播更新Encoder \(f_q\)和 Encoder \(f_k\) 的参数,所以 Batch size 的大小受到了显存容量的限制。
同时橙色曲线是最优的,证明了MoCo方法的有效性。
实验二:对比不同动量参数 \(m\)
结果如下图所示, \(m\) 取0.999时性能最好,当 \(m\)=0 时,即 Momentum Encoder \(f_{Mk}\) 参数 \(\theta_k←\theta_q\) ,即直接拷贝Encoder \(f_q\) 的参数,则训练失败,说明2个网络的参数不可以完全一致。

实验三:与其他方法对比
如下图所示,设置 \(K=65536,m=0.999\) ,Encoder 架构是 ResNet-50,MoCo 可以达到60.6%的准确度,如果 ResNet-50 的宽度设为原来的 4 倍,则精度可以进一步达到 68.6%,比以往方法更占优。

实验四:下游任务 Fine-tune 结果
有了预训练好的模型,我们就相当于是已经把参数训练到了初步成型,这时候再根据你 下游任务 (Downstream Tasks) 的不同去用带标签的数据集把参数训练到 完全成型,那这时用的数据集量就不用太多了,因为参数经过了第1阶段就已经训练得差不多了。
本文的下游任务是:PASCAL VOC Object Detection 以及 COCO Object Detection and Segmentation,主要对比的对象是 ImageNet 预训练模型 (ImageNet supervised pre-training),注意这个模型是使用100%的 ImageNet 标签训练的。
PASCAL VOC Object Detection 结果
Backbone: Faster R-CNN: R50-dilated-C5 或者 R50-C4。
训练数据尺寸:训练时 [480, 800],推理时 800。
Evaluation data:即测试集是 VOC test2007 set。
如下图是在 trainval07+12 (约16.5k images) 数据集上 Fine-tune 之后的结果,当Backbone 使用 R50-dilated-C5 时,在 ImageNet-1M 上预训练的 MoCo 模型的性能与有监督学习的性能是相似的。在 Instagram-1B 上预训练的 MoCo 模型的性能超过了有监督学习的性能。当Backbone 使用 R50-dilated-C5 时,在 ImageNet-1M 或者 Instagram-1B 上预训练的 MoCo 模型的性能都超过了有监督学习的性能。

接着作者又在下游任务上对比了方法1,2 和 MoCo 的性能,如下图所示。end-to-end 的方法 (上述方法1) 和 memory bank 方法 (上述方法2) 的性能都不如MoCo。

COCO Object Detection and Segmentation 结果
Backbone: Mask R-CNN: FPN 或者 C4。
训练数据尺寸:训练时 [640, 800],推理时 800。
Evaluation data:即测试集是 val2017。
如下图是在 train2017 set (约118k images) 数据集上 Fine-tune 之后的结果,(a)(b) 展示的是 backbone 为 R50-FPN 的结果,(c)(d) 展示的是 backbone 为 R50-C4 的结果。在 2× schedule的情况下MoCo相比于有监督训练来讲更占优。




总结
MoCo v1的改进其实可以总结为2点:
- 在 [原始的端到端自监督学习方法] 里面,Encoder \(f_q\) 和 Encoder \(f_k\) 的参数每个step 都更新,这个问题在前面也有提到,因为Encoder \(f_q\) 输入的是一个 Batch 的 negative samples (N-1个),所以输入的数量不能太大,即dictionary不能太大,即 Batch size不能太大。
现在的 Momentum Encoder \(f_{Mk}\) 的更新是通过动量的方法更新的,不涉及反向传播,所以 \(f_{Mk}\)输入的负样本 (negative samples) 的数量可以很多,具体就是 Queue 的大小可以比较大,可以比mini-batch大,属于超参数。队列是逐步更新的在每次迭代时,当前mini-batch的样本入列,而队列中最老的mini-batch样本出列,那当然是负样本的数量越多越好了。这就是 Dictionary as a queue 的含义,即通过动量更新的形式,使得可以包含更多的负样本。而且 Momentum Encoder \(f_{Mk}\) 的更新极其缓慢 (因为 \(m\)=0.999 很接近于1),所以Momentum Encoder \(f_{Mk}\) 的更新相当于是看了很多的 Batch,也就是很多负样本。 - 在 [采用一个较大的memory bank存储较大的字典] 方法里面,所有样本的 representation 都存在 memory bank 里面,根据上文的描述会带来最新的 query 采样得到的 key 可能是好多个step之前的编码器编码得到的 key,因此丧失了一致性的问题。但是MoCo的每个step都会更新Momentum Encoder,虽然更新缓慢,但是每个step都会通过4式更新一下Momentum Encoder,这样 Encoder \(f_{q}\) 和 Momentum Encoder \(f_{Mk}\) 每个step 都有更新,就解决了一致性的问题。
MoCo v2
v2 将 SimCLR的两个提点的方法 (a 使用预测头 b 使用强大的数据增强策略) 移植到了 MoCo v1上面
实验
实验如下:
训练集:ImageNet 1.28 张训练数据。
评价手段:
(1) Linear Evaluation (Encoder (ResNet-50) 的参数固定不动,在Encoder后面加分类器,具体就是一个FC层+softmax激活函数,使用全部的 ImageNet label 只训练分类器的参数,而不训练 Encoder 的参数)。看最后 Encoder+分类器的性能。
(2) VOC 目标检测 使用 Faster R-CNN 检测器 (C4 backbone),在 VOC 07+12 trainval set 数据集进行 End-to-end 的 Fine-tune。在 VOC 07 test 数据集进行 Evaluation。
- a 使用预测头结果
预测头 Projection head 分类任务的性能只存在于无监督的预训练过程,在Linear Evaluation和下游任务中都是被去掉的。
Linear Evaluation 结果如下图所示:

图中的 \(\tau\) 就是温度系数超参数 。在使用预测头且 \(\tau\)=0.07 时取得了最优的性能。
VOC 目标检测如下图所示。在使用预测头且预训练的 Epoch 数为800时取得了最优的性能,AP各项指标也超越了有监督学习 supervised 的情况。

- b 使用强大的数据增强策略结果
对数据增强策略,作者在 MoCo v1 的基础上又添加了 blur augmentation,发现更强的色彩干扰作用有限。只添加 blur augmentation 就可以使得 ImageNet 分类任务的性能从60.6增长到63.4,再加上预测头 Projection head 就可以使性能进一步涨到67.3。从图 4 也可以看到:VOC 目标检测的性能和 ImageNet 分类任务的性能没有直接的联系。
与 SimCLR v1 的对比
如下图7所示为 MoCo v2 与 SimCLR v1 性能的直接对比结果。预训练的 Epochs 都取200。如果 Batch size 都取 256,MoCo v2在 ImageNet 有67.5的性能,超过了 SimCLR 的61.9的性能。即便 SimCLR 在更有利的条件下 (Batch size = 4096,Epochs=1000),其性能69.3也没有超过 MoCo v2 的71.1的性能,证明了MoCo系列方法的地位。

小结
MoCo v2 把 SimCLR 中的两个主要提升方法 (1 使用强大的数据增强策略,具体就是额外使用了 Gaussian Deblur 的策略 2 使用预测头 Projection head) 到 MoCo 中,并且验证了SimCLR算法的有效性。最后的MoCo v2的结果更优于 SimCLR v1,证明 MoCo 系列自监督预训练方法的高效性。
MoCo V3
MoCo v3 原理分析
自监督学习模型一般可以分成 Generative 类型的或者 Contrastive 类型的。在 NLP 里面的自监督学习模型 (比如BERT系列) 一般是属于 Generative 类型的,通常把模型设计成 Masked Auto-encoder,即盖住输入的一部分 (Mask),让模型预测输出是什么 (像做填空题),通过这样的自监督方式预训练模型,让模型具有一个不错的预训练参数,且模型架构一般是个 Transformer。在 CV 里面的自监督学习模型 (比如SimCLR系列) 一般是属于 Contrastive 类型的,模型架构一般是个 Siamese Network (孪生神经网络),通过数据增强的方式创造正样本,同时一个 Batch 里面的其他数据为负样本,通过使模型最大化样本与正样本之间的相似度,最小化与样本与负样本之间的相似度来对模型参数进行预训练,且孪生网络架构一般是个 CNN。
这篇论文的重点是将目前无监督学习最常用的对比学习应用在 ViT 上。作者给出的结论是:影响自监督ViT模型训练的关键是:instability,即训练的不稳定性。而这种训练的不稳定性所造成的结果并不是训练过程无法收敛 (convergence),而是性能的轻微下降 (下降1%-3%的精度)。
首先看 MoCo v3 的具体做法吧。它的损失函数和 v1 和 v2 版本是一模一样的:
那么不一样的是整个 Framework 有所差异,MoCo v3 的整体框架如下图所示,这个图比论文里的图更详细地刻画了 MoCo v3 的训练方法

MoCo v3 的训练方法和 MoCo v1/2 的训练方法的差异是:
- 取消了 Memory Queue 的机制:你会发现整套 Framework 里面没有 Memory Queue 了,那这意味着什么呢?这就意味着 MoCo v3 所观察的负样本都来自一个 Batch 的图片,也就是上图里面的\( n\)。换句话讲,只有当 Batch size 足够大时,模型才能看到足够的负样本。那么 MoCo v3 具体是取了4096这样一个巨大的 Batch size。
- Encoder \(f_q\)除了 Backbone 和预测头 Projection head 以外,还添加了个 Prediction head,是遵循了 BYOL 这篇论文的方法。
- 对于同一张图片的2个增强版本 \(x_1, x_2\),分别通过 Encoder \(f_q\)和 MomentumEncoder \(f_{Mk}\)得到 \(q_1,q_2\)和 \(k_1,k_2\)。让 \(q_1, k_2\)通过 Contrastive loss 进行优化 Encoder \(f_q\)的参数,让 \(q_2,k_1\)通过 Contrastive loss 进行优化 Encoder \(f_q\)的参数。Momentum Encoder \(f_{Mk}\)通过(4)进行动量更新。
下面是伪代码, \(f_q\) 和 \(f_{Mk}\) 在代码里面分别表示为:f_q , f_k。
1) 数据增强:
现在我们有一堆无标签的数据,拿出一个 Batch,代码表示为 x,也就是 \(N\) 张图片,分别进行两种不同的数据增强,得到 x_1 和 x_2,则 x_1 是 \(N\) 张图片,x_2 也是 \(N\) 张图片。
for x in loader: # load a minibatch x with N samples
x1, x2 = aug(x), aug(x) # augmentation
2) 分别通过 Encoder 和 Momentum Encoder:
x_1 分别通过 Encoder 和 Momentum Encoder 得到特征 q_1 和 k_1,维度是 \(N,C\) ,这里特征空间由一个长度为 \(C=128\) 的向量表示。
x_2 分别通过 Encoder 和 Momentum Encoder 得到特征 q_2 和 k_2,维度是 \(N,C\) ,这里特征空间由一个长度为 \(C=128\) 的向量表示。
q1, q2 **=** f_q(x1), f_q(x2) *# queries: [N, C] each*
k1, k2 **=** f_k(x1), f_k(x2) *# keys: [N, C] each*
3) 通过一个 Contrastive loss 优化 q_1和 k_2,通过另一个 Contrastive loss 优化 q_2 和 k_1,并反向传播更新f_q 的参数:
loss = ctr(q1, k2) + ctr(q2, k1) # symmetrized
loss.backward()
update(f_q) # optimizer update: f_q
4) Contrastive loss 的定义:
对两个维度是 \((N,C)\) 的矩阵 (比如是
q_1和k_2) 做矩阵相乘,得到维度是\( (N,N) \)的矩阵,其对角线元素代表的就是positive sample的相似度,就是让对角线元素越大越好,所以目标是整个这个 \((N,N) \)的矩阵越接近单位阵越好,如下所示。
def ctr(q, k):
logits = mm(q, k.t()) # [N, N] pairs
labels = range(N) # positives are in diagonal
loss = CrossEntropyLoss(logits/tau, labels)
return 2 * tau * loss
5) Momentum Encoder的参数使用动量更新:
f_k = m*f_k + (1-m)*f_q # momentum update: f_k
全部的伪代码 (来自MoCo v3 的paper):
# f_q: encoder: backbone + pred mlp + proj mlp
# f_k: momentum encoder: backbone + pred mlp
# m: momentum coefficient
# tau: temperature
for x in loader: # load a minibatch x with N samples
x1, x2 = aug(x), aug(x) # augmentation
q1, q2 = f_q(x1), f_q(x2) # queries: [N, C] each
k1, k2 = f_k(x1), f_k(x2) # keys: [N, C] each
loss = ctr(q1, k2) + ctr(q2, k1) # symmetrized
loss.backward()
update(f_q) # optimizer update: f_q
f_k = m*f_k + (1-m)*f_q # momentum update: f_k
# contrastive loss
def ctr(q, k):
logits = mm(q, k.t()) # [N, N] pairs
labels = range(N) # positives are in diagonal
loss = CrossEntropyLoss(logits/tau, labels)
return 2 * tau * loss
以上就是 MoCo v3 的全部方法,都可以概括在图8里面。它的性能如何呢?假设 Encoder 依然取 ResNet-50,则 MoCo v2,MoCo v2+,MoCo v3 的对比如下图20所示,主要的提点来自于大的 Batch size (4096) 和 Prediction head 的使用。

MoCo v3 自监督训练 ViT 的不稳定性
上图的实验结果证明了 MoCo v3 在 Encoder 依然取 ResNet-50 时的有效性。那么当 Encoder 变成 Transformer 时的情况又如何呢?如本节一开始所述,作者给出的结论是:影响自监督ViT模型训练的关键是:instability,即训练的不稳定性。而这种训练的不稳定性所造成的结果并不是训练过程无法收敛 (convergence),而是性能的轻微下降 (下降1%-3%的精度)。
- Batch size 过大使得训练不稳定
如下图所示是使用 MoCo v3 方法,Encoder 架构换成 ViT-B/16 ,Learning rate=1e-4,在 ImageNet 数据集上训练 100 epochs 的结果。作者使用了4种不同的 Batch size:1024, 2048, 4096, 6144 的结果。可以看到当 bs=4096 时,曲线出现了 dip 现象 (稍稍落下又急速升起)。这种不稳定现象导致了精度出现下降。当 bs=6144 时,曲线的 dip 现象更大了,可能是因为跳出了当前的 local minimum。这种不稳定现象导致了精度出现了更多的下降。

- Learning rate 过大使得训练不稳定
如下图所示是使用 MoCo v3 方法,Encoder 架构换成 ViT-B/16 ,Batch size=4096,在 ImageNet 数据集上训练 100 epochs 的结果。作者使用了4种不同的 Learning rate:0.5e-4, 1.0e-4, 1.5e-4 的结果。可以看到当Learning rate 较大时,曲线出现了 dip 现象 (稍稍落下又急速升起)。这种不稳定现象导致了精度出现下降。

- LARS optimizer 的不稳定性
如下图所示是使用 MoCo v3 方法,Encoder 架构换成 ViT-B/16 ,Batch size=4096,在 ImageNet 数据集上训练 100 epochs 的结果,不同的是使用了 LARS 优化器,分别使用了4种不同的 Learning rate:3e-4, 5e-4, 6e-4, 8e-4 的结果。结果发现当给定合适的学习率时,LARS的性能可以超过AdamW,但是当学习率稍微变大时,性能就会显著下降。而且曲线自始至终都是平滑的,没有 dip 现象。所以最终为了使得训练对学习率更鲁棒,作者还是采用 AdamW 作为优化器。因为若采用 LARS,则每换一个网络架构就要重新搜索最合适的 Learning rate。

提升训练稳定性的方法:冻结第1层 (patch embedding层) 参数
上面图的实验表明 Batch size 或者 learning rate 的细微变化都有可能导致 Self-Supervised ViT 的训练不稳定。作者发现导致训练出现不稳定的这些 dip 跟梯度暴涨 (spike) 有关,如下图24所示,第1层会先出现梯度暴涨的现象,结果几十次迭代后,会传到到最后1层。

所以说问题就出在第1层出现了梯度暴涨啊,一旦第1层梯度暴涨,这个现象就会在几十次迭代之内传遍整个网络。所以说想解决训练出现不稳定的问题就不能让第1层出现梯度暴涨!
所以作者解决的办法是冻结第1层的参数 ,也就是patch embedding那层,随机初始化后,不再更新这一层的参数,然后发现好使,如图所示。
patch embedding那层具体就是一个 k=p=16, s=p=16 的卷积操作,输入 channel 数是3,输出 channel 数是embed_dim=768/384/192。
patch embedding 代码:
self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size)
如下图所示是使用 MoCo v3 or SimCLR, BYOL 方法,Encoder 架构换成 ViT-B/16 ,Batch size=4096,在 ImageNet 数据集上训练 100 epochs 的结果,不同的是冻结了patch embedding那层的参数,使用了随机参数初始化。
不论是 MoCo v3 还是 SimCLR, BYOL 方法,冻结 patch embedding 那层的参数都能够提升自监督 ViT 的训练稳定性。除此之外, gradient-clip 也能够帮助提升训练稳定性,其极限情况就是冻结参数。


小结
MoCo v3 的改进如图8所示,取消了 Memory Queue 的机制,添加了个 Prediction head,且对于同一张图片的2个增强版本 \(x_1,x_2\) ,分别通过 Encoder \(f_q\)和 Momentum Encoder \(f_{Mk}\) 得到 \(q_1.q_2\) 和 \(k_1,k_2\) 。分别让 \(q_1,k_2\) 和\(q_2,k_1\)通过 Contrastive loss 进行优化 Encoder \(f_q\)的参数.
在 Self-supervised 训练 Transformer 的过程中发现了 instablity 的问题,通过冻住patch embedding的参数,以治标不治本的形式解决了这个问题,最终Self-supervised Transformer 可以 beat 掉 Supervised Transformer 和 Self-supervised CNN。