本文介绍 Self-Supervised Learning 的原理和合理性,并记录
MoCo
系列对SeIf-Supervised Learning
领域产生的影响。
Self-Supervised Learning
,又称为自监督学习,我们知道一般机器学习分为有监督学习,无监督学习和强化学习。而 Self-Supervised Learning
是无监督学习里面的一种,主要是希望能够学习到一种通用的特征表达用于下游任务 (Downstream Tasks)。
Self-Supervised Learning
是未来的大势所趋。
MoCo
至今已出现了三个版本,第一版 MoCo v1
是在 SimCLR
诞生之前的一种比较流行的无监督学习方法,这个系列的前2个工作 MoCo v1
和 v2
是针对 CNN 设计的,而 MoCo v3
是针对 Transformer
模型设计的,本文主要介绍 Moco v1 的思想喝来源。
Unsupervised Pre-train
, Supervised Fine-tune
Visual Representation
。预训练模型的时候,就是模型参数从 一张白纸 到 初步成型 的这个过程,还是用无标签数据集。等我把模型参数训练个八九不离十,这时候再根据你 下游任务 (Downstream Tasks) 的不同去用带标签的数据集把参数训练到 完全成型,那这时用的数据集量就不用太多了,因为参数经过了第1阶段就已经训练得差不多了。
MoCo 系列也遵循这个思想,预训练的 MoCo 模型也会得到 Visual Representation,它们可以通过 Fine-tune 以适应各种各样的下游任务,比如检测和分割等等。MoCo在 7 个检测/语义分割任务(PASCAL VOC, COCO, 其他的数据集)上可以超过他的有监督训练版本。有时会超出很多。这表明在有监督与无监督表示学习上的差距在许多视觉任务中已经变得非常近了。
还有一些常见的 Pretext Task 诸如 denoising auto-encoders,context autoencoders,cross-channel auto-encoders等等。
kaiming大神利用 momentum (移动平均更新模型权重) 与queue (字典) 轻松的解决这个问题。
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.detach()
# 这里bmm是分批矩阵乘法
1_pos = bmm(q.view(N, 1, C), k.view(N, C, 1)) # 输出Nx1,也就是自己与自己的增强图的特征的匹配度
这里得到的I_pos的维度是**(N,1,1), N个数代表N**张图片的自己与自己的增强图的特征的匹配度。
1_neg = mm(q.view(N, C), queue.view(C, K) # 输出Nxk,自己与上一批次所有图的匹配度(全不匹配)
logits = cat([1_pos, 1_neg], dim=1) # 输出 N x (1+k)
每一行的第1个元素是某张图片自己与自己的匹配度,每一行的后面K 个元素是某张图片与其他 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) # 为了生成反例,所以引入了队列 dequeue(queue)
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) # 为了生成反例,所以引入了队列 dequeue(queue)
Encoder f_{\mathrm{q}} 的输入是一个Batch的样本 \mathbf{x} 的增强版本 x__q。 Momentum Encoder f_{\mathrm{Mk}} 的输入是一个 Batch 的样本 \mathbf{x} 的另一个增强版本 和 队列x_k Momentum Encoder 得到代码中的变量 queue。
Encoder f_{\mathrm{q}} 在每个 step 都会通过反向传播更新参数,假设 1 个 epoch 里面有500 个 step,Encoder f_{\mathrm{q}} 就更新 500次。Momentum Encoder f_{\mathrm{Mk}} 在每个 step 都会通过动量的方式更新参数,假设 1 个 epoch 里面有500 个 step,Momentum Encoder f_{\mathrm{Mk}} 就更新 500次,只是更新的方式是:
式中, m 是动量参数,默认取 m=0.999 ,这意味着 Momentum Encoder的更新是极 其缓慢的,而且并不是通过反向传播来更新参数,而是通过动量的方式来更新。
在 [1 原始的端到端自监督学习方法] 里面,Encoder f_{\mathrm{q}} 和 Encoder f_{\mathrm{k}} 的参数每个s tep 都更新,这个问题在前面也有提到,因为Encoder f_{\mathrm{k}} 输入的是一个 Batch 的 negativ e samples,所以输入的数量不能太大,即dictionary不能太大,即 Batch size不能太大。 现在的 Momentum Encoder f_{\mathrm{Mk}} 的更新是以动量的方法更新的,不涉及反向传播,所以 f_{\mathrm{Mk}} 输入的负样本 (negative samples) 的数量可以很多,具体就是 Queue 的大 小 可 以 比 较 大 , 那当然是负样本的数量越多越好了。这就是 Dictionary as a queue 的含义,即通过动量更新的形式,使得可以包含更多的负样本。而且 Momentum Encoder f_{\mathrm{Mk}} 的更新极其缓慢 (因为 m=0.999 很接近于1),所以Momentum Encoder f_{\mathrm{Mk}} 的更新相当于是看了很多的 Batch,也就是很多负样本。 在 [2 采用一个较大的memory bank存储较大的字典] 方法里面,所有样本的 representa tion 都存在 memory bank 里面,根据上文的描述会带来最新的 query 采样得到的 key 可能是好多个step之前的编码器编码得到的 key,因此丧失了一致性的问题。但是MoCo的 每个step都会更新Momentum Encoder,虽然更新缓慢,但是每个step都会通过动量更新 一下Momentum Encoder,这样 Encoder f_{\mathrm{q}} 和 Momentum Encoder f_{\mathrm{Mk}} 每个step 都有更新,就解决了一致性的问题。
file:///C:/Users/issuser/Downloads/202205281415755.pdf