0%

CV 领域的自监督

还是要认真的学习一会儿,降低多巴胺的分泌。在使用 triplet loss 学表示的时候,出现了模型坍塌的情况,也就是说,模型对任何输入的输出都是一样的,损失恒定的现象。在网上搜了一些解决方案后,需要用户去花精力构造正负样本,我不喜欢这样的东西,所以开始看了自监督的论文,毕竟都是学表示。也发现自监督会在一段时间内成为未来的视觉领域的主流,正好我做的东西和自监督也算相关,做一个论文整理。包括了 MoCo,SimCLR,SimSiam 和 Barlow Twins。

模型崩塌,也就是模型为了偷懒,无论什么图片都会输出同样表示,这样结果 loss 很小,然后却没学到任何东西。

图像领域的自监督主要由两部分组成,对比损失和数据增强,而那四篇论文也是基于这两个东西去做的,无非是如何对比。

MoCo

首先是 MoCo,简单的看一下模型的结构

TsYsxS.png

对同一个样本做两次数据增强,会得到两个样本 $x_q$ 和 $x_k$,创建两个 encoder,左边那个 encoder 接入 $x_q$,右边的 encoder 接入 $x_k$,使得这两个的相似性越高越好,与此同时,期望 $x_q$ 与负样本的相似性越低越好。那么如何衡量相似性呢?使用的是 InfoNCE 这个损失函数。

\begin{aligned}
{L}_{InfoNCE} =-{E}_X\left[\log \frac{f_{k}\left(x_{t+k}, c_{t}\right)}{\sum_{x_{j} \in X} f_{k}\left(x_{j}, c_{t}\right)}\right] \\
\end{aligned}

分子是正样本的相似度,分母是负样本的相似度,这个比值越大,log 就越大,对应的损失函数就越小。MoCo 的改动如下,用点积来衡量 $x_q$ 和正负样本的距离。也就是说,使用对比损失来区分图像的高维特征。

\begin{equation}
{L}_q = -\log \frac{ \exp(q\cdot k_{+}/\tau) }{\sum_{i=0}^K \exp(q\cdot k_{i}/\tau)}
\end{equation}

那么与众不同的地方呢?换句话说,负样本从哪里来呢?模型会设计一个队列,队列负责维护将刚进入的样本视为负样本放入队列(最开始没负样本的话,用的是随机数),并弹出之前的负样本,这就需要很大的显存以及代码编写的难度,我不是很喜欢这两点。不信你来看他们官方的程序

此外需要注意的是,作者提出了动量的更新方式,来更新右侧的网络:

\begin{equation}
\theta_k \leftarrow m\theta_k + (1-m)\theta_q , m\in[0,1)
\end{equation}

也许你会有疑问,都计算好梯度了,更新左侧的网络就没事,更新右侧的网络就有事,莫非在耍流氓?这个梯度给谁不是给,为什么不用梯度更新两个网络?论文上写的是:由于样本很多,右侧网络不容易更新。很多网上的论文解析也是人云亦云,看了代码就知道存储负样本的队列和右侧的网络没半毛钱关系,队列在计算梯度的时候已经 detach 了。所以,论文写的更新困难并不是梯度回传困难,那么为什么不能用梯度更新右侧的网络呢?

先来解释为什么不把左侧网络的参数拷贝给右侧网络的参数。这么做的目的是因为不同 epoch 之间数据分布差异可能很大,encoder 的参数有可能会发生突变,不能将多个 epoch 的数据特征近似成一个静止的大 batch 数据特征,因此就使用了这么一个类似于滑动平均的方法来更新右侧网络。

再来回答为什么不用梯度更新右侧的网络。$x_k$ 经过右侧网络后,然后我们就得到了它们相对应的表示。而对于这个表示,它们不光包含了这个 mini-batch 的表示,也包含了一些之前处理过的 mini-batch 中的表示。换句极端的话说,右侧的网络掌握了全部数据的表示,来调控左侧网络该学到什么样的表示。这些表示通过右侧网络后放入队列中,相对于只在本 mini-batch 内做比较的方法而言,moco 能用较小的管理开销来获得更多的负样本。如果只用一个 mini-batch 的梯度更新右侧网络,或者说右侧网络只掌握一个 mini-batch 的表示,那右侧网络直接输出和左侧网络一样的东西不就行了,这样损失很小,但什么也没学到。因此,不能用一个 mini-batch 的梯度去更新右侧网络。

损失函数解析

此外是本文较为迷惑的损失函数,文章写的没啥迷惑,迷惑的是代码。我第一眼过去,直接理解为不管是正样本还是负样本,都和 0 去接近,正样本明明越大越好,应该和 1 去接近,这么写肯定不对呀。

1
2
3
4
5
6
l_pos = torch.einsum('nc,nc->n', [q, k]).unsqueeze(-1)
l_neg = torch.einsum('nc,ck->nk', [q, self.queue.clone().detach()])
logits = torch.cat([l_pos, l_neg], dim=1)
labels = torch.zeros(logits.shape[0], dtype=torch.long)
criterion = nn.CrossEntropyLoss()
loss = criterion(logits, target)

后来仔细抠的代码才理解,torch 的交叉熵损失函数由 logsoftmaxnllloss 组成,前者完成 logsoftmax 的计算,后者 nllloss 取出对应的标签,在 nllloss 的时候,标签为 0 的意思是取出 batch 中的第一列,而这一列正好是正样本,nlloss 期望这列的值越小越好,那么回退到 logsoftmax,就是期望这列的值越大越好,也就是,正样本的相似度很高。并不是像网上那种垃圾博客说的,无监督任务都分成 0 类即可。 这里还是建议理解一下,因为自监督的损失大多是这么设计的。

SimCLR

推荐一个比较好的实现。因为反感 MoCo 那种开显存的操作,毕竟不是所有人都有 facebook 的财力,所以继续去读了其他自监督的论文,较为相似的一篇论文是 SimCLR。还是先来看模型结构图:

TsYr28.png

注意,左右两边的网络都是相同的,也就不存在梯度该传给谁的问题。一个 batch 的样本,经过数据增强得到两个 batch 的样本,这两个 batch 的样本进入网络会得到两个 batch 的表示,期待这两组表示中,同一数据的表示很接近,放大不同数据的差异。损失同样是用的类似 InfoNCE 的损失函数。不过论文中有比较奇怪的点,放一段原文:

We randomly sample a minibatch of N examples, augmented examples derived from the minibatch, resulting in 2N data points. We treat the other 2(N-1) augmented examples within a minibatch as negative examples.The final loss is computed across all positive pairs.

问题来了,一共 $2N$ 个样本,$2(N-1)$ 都视为负样本,哪来的 positive pairs?还是直接看代码吧,其实 MoCo 的论文写的也够晕的。看了代码损失函数的设计后发现,就是自己和自己是正样本,自己和其他的样本的关系是负样本。严格来说,对于一个样本而言,有 2(N-2) 个负样本。损失函数和 MoCo 的保持一致,都是用交叉熵损失函数实现的。推荐去看这个损失函数的实现,代码写的还是比较有意思的。但是计算量会大一些。

SimSiam

MoCo 之后,提出了更简单的网络结构:

TsYD8f.png

提前声明,相似度用的是余弦函数,因为余弦函数相似性越高数值越大,因此损失函数取了负数,最后的损失值也是负的。思路很简单,对同一个图像做两次增强得到俩个正样本,$x_1$ 和 $x_2$,$x_1$ 经过 encoder 得到表示 $z1$,$z1$ 经过 predictor 得到 $p_1$,同理得到 $p_2$ 和 $z_2$,计算 $p_1$ 和 $z_2$ 以及 $p_2$ 和 $z_1$ 的相似度,然后就没了。

简单吗?简单。有坑吗?有。我当时在想,如果 predictor 的权重全部是 1,那么相似度不就会很小了,但仍然是模型坍塌的情况。然后带着疑问去看代码,结果人家直接冻结了 predictor 某一层的权重,是我大意了。后来我以抄袭的形式复现的时候,没有冻结权重,效果比 SimCLR 要好一些。

Barlow Twins

到这篇论文,思路更加简单,东西和 SimCLR 比较类似,损失函数用的是大二学过的协方差矩阵,对角线越大越好,其余位置越小越好。它虽然很简洁,但我感觉他比 SimSiam 更令人喜欢。

\begin{equation}
L=\sum_{i}(1-C_{ii})^2 + \lambda \sum_i \sum_{j\neq i} C_{ij}^2
\end{equation}

参考

  1. MoCo 参考
  2. NLLLoss 解析
  3. SimSiam 为什么 stop grad
感谢上学期间打赏我的朋友们。赛博乞讨:我,秦始皇,打钱。

欢迎订阅我的文章