HRNet 论文和代码详解

github: https://github.com/HRNet/HRNet-Semantic-Segmentation

Paper: https://arxiv.org/abs/1908.07919

High-Resoultion Net(HRNet)由微软亚洲研究院和中科大提出,发表在CVPR2019

摘要:高分辨率表示对于位置敏感的视觉问题十分重要,比如目标检测、语义分割、姿态估计。为了这些任务位置信息更加精准,很容易想到的做法就是维持高分辨率的feature map,事实上HRNet之前几乎所有的网络都是这么做的,通过下采样得到强语义信息,然后再上采样恢复高分辨率恢复位置信息(如下图所示),然而这种做法,会导致大量的有效信息在不断的上下采样过程中丢失。而HRNet通过并行多个分辨率的分支,加上不断进行不同分支之间的信息交互,同时达到强语义信息和精准位置信息的目的。

模型的主要特点是在整个过程中特征图(Feature Map)始终保持高分辨率,这与之前主流方法思路上有很大的不同。在HRNet之前,2D人体姿态估计算法是采用(Hourglass/CPN/Simple Baseline/MSPN等)将高分辨率特征图下采样至低分辨率,再从低分辨率特征图恢复至高分辨率的思路(单次或重复多次),以此过程实现了多尺度特征提取的一个过程。

HRNet在整个过程中保持特征图的高分辨率,但多尺度特征提取是姿态估计模型一定要实现的过程,那么HRNet是如何实现多尺度特征提取的呢?模型是通过在高分辨率特征图主网络逐渐并行加入低分辨率特征图子网络,不同网络实现多尺度融合与特征提取实现的。

特点与优势:

(1)作者提出的方法是并行连接高分辨率与低分辨率网络,而不是像之前方法那样串行连接。因此,其方法能够保持高分辨率,而不是通过一个低到高的过程恢复分辨率,因此预测的heatmap可能在空间上更精确。

(2)本文提出的模型融合相同深度和相似级别的低分辨率特征图来提高高分辨率的特征图的表示效果,并进行重复的多尺度融合。

缺点:因为特征图分辨率过大,而且数量多,这样肯定会导致巨额的耗时计算,对显存对硬件要求更高了

HRNet结构细节

Backbone设计

我将HRNet整个backbone部分进行了拆解,分成4个stage,每个stage分成蓝色框和橙色框两部分。其中蓝色框部分是每个stage的基本结构,由多个branch组成,HRNet中stage1蓝色框使用的是BottleNeck,stage2&3&4蓝色框使用的是BasicBlock。其中橙色框部分是每个stage的过渡结构,HRNet中stage1橙色框是一个TransitionLayer,stage2&3橙色框是一个FuseLayer和一个TransitionLayer的叠加,stage4橙色框是一个FuseLayer。

解释一下为什么这么设计,FuseLayer是用来进行不同分支的信息交互的,TransitionLayer是用来生成一个下采样两倍分支的输入feature map的,stage1橙色框显然没办法做FuseLayer,因为前一个stage只有一个分支,stage4橙色框后面接neck和head了,显然也不再需要TransitionLayer了。

整个backbone的构建流程可以总结为:make_backbone -> make_stages -> make_branches

有关backbone构建相关的看源码,主要讲一下FuseLayerTransitionLayerNeck的设计

FuseLayer设计

FuseLayer部分以绿色框为例,融合前为pre,融合后为post,静态构建一个二维矩阵,然后将pre和post对应连接的操作一一填入这个二维矩阵中。

以上图为例,图1的pre1和post1的操作为空,pre2和post1的操作为2倍上采,pre3和post1的操作为4倍上采;图2的pre1和post2的操作为3×3卷积下采,pre2和post2的操作为空,pre3和post2的操作为2倍上采;图3的pre1和post3的操作为连续两个3×3卷积下采,pre2和post3的操作为3×3卷积下采,pre3和post的操作为空。

前向计算时用一个二重循环将构建好的二维矩阵一一解开,将对应同一个post的pre转换后进行融合相加。比如post1 = f11(pre1) + f12(pre2) + f13(pre3)

FuseLayer的整体code如下:

def _make_fuse_layers(self):
  fuse_layers = []
  for post_index, out_channel in enumerate(self.out_channels[:len(self.in_channels)]):
      fuse_layer = []
      for pre_index, in_channel in enumerate(self.in_channels):
          if pre_index > post_index:
              fuse_layer.append(nn.Sequential(
                  nn.Conv2d(in_channel, out_channel, 1, 1, 0, bias=False),
                  nn.BatchNorm2d(out_channel, momentum=0.1),
                  nn.Upsample(scale_factor=2**(pre_index-post_index), mode='nearest')))
          elif pre_index < post_index:
              conv3x3s = []
              for cur_index in range(post_index - pre_index):
                  out_channels_conv3x3 = out_channel if cur_index == post_index - pre_index - 1 else in_channel
                  conv3x3 = nn.Sequential(
                      nn.Conv2d(in_channel, out_channels_conv3x3, 3, 2, 1, bias=False),
                      nn.BatchNorm2d(out_channels_conv3x3, momentum=0.1)
                  )
                  if cur_index < post_index - pre_index - 1:
                      conv3x3.add_module('relu_{}'.format(cur_index), nn.ReLU(False))
                  conv3x3s.append(conv3x3)
              fuse_layer.append(nn.Sequential(*conv3x3s))
          else:
              fuse_layer.append(None)
      fuse_layers.append(nn.ModuleList(fuse_layer))
  return nn.ModuleList(fuse_layers)

def forward(self, x):
  x_fuse = []
  for post_index in range(len(self.fuse_layers)):
      y = 0
      for pre_index in range(len(self.fuse_layers)):
          if post_index == pre_index:
              y += x[pre_index]
          else:
              y += self.fuse_layers[post_index][pre_index](x[pre_index])
      x_fuse.append(self.relu(y))

TransitionLayer设计

TransitionLayer以黄色框为例,静态构建一个一维矩阵,然后将pre和post对应连接的操作一一填入这个一维矩阵中。当pre1&post1、pre2&post2、pre3&post3的通道数对应相同时,一维矩阵填入None;通道数不相同时,对应位置填入一个转换卷积。post4比较特殊,这一部分代码和图例不太一致,图例是pre1&pre2&pre3都进行下采然后进行融合相加得到post4,而代码中post4通过pre3下采得到。

TransitionLayer整体code如下

def _make_transition_layers(self):
  num_branches_pre = len(self.in_channels)
  num_branches_post = len(self.out_channels)
  transition_layers = []
  for post_index in range(num_branches_post):
      if post_index < len(self.in_channels):
          if self.in_channels[post_index] != self.out_channels[post_index]:
              transition_layers.append(nn.Sequential(
                  nn.Conv2d(self.in_channels[post_index], self.out_channels[post_index], 3, 1, 1, bias=False),
                  nn.BatchNorm2d(self.out_channels[post_index], momentum=0.1),
                  nn.ReLU(inplace=True)
              ))
          else:
              transition_layers.append(None)
      else:
          conv3x3s = []
          for pre_index in range(post_index + 1 - num_branches_pre):
              in_channels_conv3x3 = self.in_channels[-1]
              out_channels_conv3x3 = self.out_channels[post_index] if pre_index == post_index - \
                  num_branches_pre else in_channels_conv3x3
              conv3x3s.append(nn.Sequential(
                  nn.Conv2d(in_channels_conv3x3, out_channels_conv3x3, 3, 2, 1, bias=False),
                  nn.BatchNorm2d(out_channels_conv3x3, momentum=0.1),
                  nn.ReLU(inplace=True)
              ))
          transition_layers.append(nn.Sequential(*conv3x3s))
  return nn.ModuleList(transition_layers)

def forward(self, x):
  x_trans = []
  for branch_index, transition_layer in enumerate(self.transition_layers):
      if branch_index < len(self.transition_layers) - 1:
          if transition_layer:
              x_trans.append(transition_layer(x[branch_index]))
          else:
              x_trans.append(x[branch_index])
      else:
          x_trans.append(transition_layer(x[-1]))

Neck设计

我把HRNet所描述的make_head过程理解成make_neck(因为一般意义上将最后的fc层理解成head更为清晰,这个在很多开源code中都是这样子拆解的)。下面着重讲解一下HRNet的neck设计。

HRNet的backbone输出有四个分支,paper中给出了几种方式对输出分支进行操作。

(a)图是HRNetV1的操作方式,只使用分辨率最高的feature map。

(b)图是HRNetV2的操作方式,将所有分辨率的feature map(小的特征图进行upsample)进行concate,主要用于语义分割和面部关键点检测。

(c)图是HRNetV2p的操作方式,在HRNetV2的基础上,使用了一个特征金字塔,主要用于目标检测。

而在图像分类任务上,HRNet有另一种特殊的neck设计

HRNet的neck可以分成三个部分,IncreLayer(橙色框),DownsampLayer(蓝色框)和FinalLayer(绿色框)。对每个backbone的输出分支进行升维操作,然后按照分辨率从大到小依次进行下采样同时从上到下逐级融合相加,最后用一个1x1conv升维。

def _make_neck(self, in_channels):
  head_block = Bottleneck
  self.incre_channels = [32, 64, 128, 256]
  self.neck_out_channels = 2048

  incre_modules = []
  downsamp_modules = []
  num_branches = len(self.in_channels)
  for index in range(num_branches):
      incre_module = self._make_layer(head_block, in_channels[index], incre_channels[index], 1, stride=1)
      incre_modules.append(incre_module)
      if index < num_branches - 1:
          downsamp_in_channels = self.incre_channels[index] * incre_module.expansion
          downsamp_out_channels = self.incre_channels[index+1] * incre_module.expansion
          downsamp_module = nn.Sequential(
              nn.Conv2d(in_channels=downsamp_in_channels, out_channels=downsamp_out_channels,
                        kernel_size=3, stride=2, padding=1),
              nn.BatchNorm2d(downsamp_out_channels, momentum=0.1),
              nn.ReLU(inplace=True)
          )
          downsamp_modules.append(downsamp_module)
  incre_modules = nn.ModuleList(incre_modules)
  downsamp_modules = nn.ModuleList(downsamp_modules)
  final_layer = nn.Sequential(
      nn.Conv2d(in_channels=self.out_channels[-1] * 4, out_channels=2048,
                kernel_size=1, stride=1, padding=0),
      nn.BatchNorm2d(2048, momentum=0.1),
      nn.ReLU(inplace=True)
  )
  return incre_modules, downsamp_modules, fine_layer

def forward(self, x):
  y = self.incre_modules[0](x[0])
  for index in range(len(self.downsamp_modules)):
      y = self.incre_modules[index+1](x[index+1]) + self.downsamp_modules[index](y)
  y = self.final_layer(y)
  y = F.avg_pool2d(y, kernel_size=y.size()[2:]).view(y.size(0), -1)

还有几个小细节

  1. BN层的momentom都设置为0.1
  2. stem使用的是两层stried为2的conv3x3
  3. FuseLayer的ReLU的inplace都设置为False

Swin Transformer论文解读与思考

论文: https://arxiv.org/abs/2103.14030

github:https://github.com/microsoft/Swin-Transformer

代码详解 https://zhuanlan.zhihu.com/p/384514268

Vision Transformer , Vision MLP 超详细解读 (原理分析+代码解读) (目录)

论文详解:https://space.bilibili.com/1567748478/channel/collectiondetail?sid=32744

Swin Transformer视频讲解:

https://github.com/WZMIAOMIAO/deep-learning-for-image-processing

摘要 :目前transformer应用于CV领域的挑战主要有两个,一个是图片多尺度语义信息的问题,同一个物体在不同图片中的大小尺度变化很大,另外就是难以处理高分辨的图片,如果以pix像素作为序列元素,那么计算成本太大,因此一部分方法是将CNN提取图片特征在送进transformer中 ,或者通过patch,将图片变成一个个的patch。作者提出Swin Transformer 目标是希望作为一种计算机视觉的通用主干网络(因为VIT的提出已经证明了Transformer在CV的可行性),这是一种层级的架构。通过窗口注意力以及转移窗口注意力,不仅降低了计算量,同时层级架构对于不同尺度的信息处理都十分灵活,该架构在图像分类、目标检测、语义分割等任务中表现出色。(对于图像分类、目标检测、语义分割等下游任务,尤其是密集预测任务,多尺度特征是十分必要的)

引言

首先来看作者给出的Swin Transformer 和 VIT结构对比:

VIT的patch固定16*16(可以认为是16倍下采样),多尺度特征处理不好,因为整个过程都是在同一尺度下操作的,出来的特征是单尺度的,优点是全局的特征处理比较强,因为是在全局的尺度进行操作的,但因此他的复杂度跟图像尺寸成平方倍的增长,很难处理目前图像分割检测。再来看 Swin Transformer ,通过图可以看出作者借鉴了CNN的很多设计思路,为了减少序列长度,减低计算量,仅在上面的红框中进行自注意力计算,计算复杂度会跟整张图片的大小成线性关系。另外作者使用基于窗口的注意力的也可以很好的把握物体的全局信息(因为在CV中,一个物体的绝大部分都存在单个windows窗口中,很少会横跨多个窗口),另外CNN网络的如何抓住物体的多尺度特征?是因为pool池化层的存在,每次池化能够增大卷积核看到的感受野。因此作者提出了patch merging,将相邻的四个patch合并成一个大patch(可以认为是加权池化),这样合并出来的一个 大patch就可以看到四个小patch内容感受野增大。有了多尺度特征(4*,8*,16*多尺度特征图)以后,可以接一个FPN头,由于做检测任务,也可以放在unet做分割任务,这就是作者所说的, Swin Transformer 是可以做一个通用骨干网络。

    Transformer的初衷就是更好的理解上下文,如果窗口都是不重叠的,那自注意力真的就变成孤立自注意力,就没有全局建模的能力   
    Swin Transformer 的一个关键设计因素:移动窗口操作。在第l层,通过划分不同的小窗口(实际中是一个窗口有7*7个patch(最小单位),这里示意图以4*4的patch作为一个窗口),自注意力只在窗口中计算 ,就可以有效降低序列长度,从而减少计算复杂度。shift操作可以认为是将l层的窗口整体向右下加移动两个patch所形成的新的窗口,新的特征图进行分割windws以后就有l+1层所示的这些窗口(如下图共九个)了。如果没有shift,那么所有窗口不重叠,在窗口进行自注意力时候,窗口之间无法交互,就无法达到transformer的初衷了(更好的理解上下文),shift后不同窗口的patch就可以进行交互了。再加上一个patch merging操作,不断扩大感受野,到最后几层的时候,每个patch的感受野已经很大了,实际上就可以看到大部分图片了,shift操作以后,就可以看成是全局注意力操作,这样即省内存效果也好。

引言的最后,作者坚信,一个CV和NLP大一统的框架是可以促进两个领域共同发展的,但实际上 Swin Transformer 更多的是利用了CNN的先验知识,从而在计算机视觉领域大杀四方。但是在模型大一统上,也就是 unified architecture 上来说,其实 ViT 还是做的更好的,因为它真的可以什么都不改,什么先验信息都不加,就能让Transformer在两个领域都能用的很好,这样模型不仅可以共享参数,而且甚至可以把所有模态的输入直接就拼接起来,当成一个很长的输入,直接扔给Transformer去做,而不用考虑每个模态的特性

先看结论:

这篇论文提出了 Swin Transformer,它是一个层级式的Transformer,而且它的计算复杂度是跟输入图像的大小呈线性增长的。Swin Transformerr 在 COCO 和 ADE20K上的效果都非常的好,远远超越了之前最好的方法,所以作者说基于此,希望 Swin Transformer 能够激发出更多更好的工作,尤其是在多模态方面。

因为在Swin Transformer 这篇论文里最关键的一个贡献就是基于 Shifted Window 的自注意力,它对很多视觉的任务,尤其是对下游密集预测型的任务是非常有帮助的,但是如果 Shifted Window 操作不能用到 NLP 领域里,其实在模型大一统上论据就不是那么强了,所以作者说接下来他们的未来工作就是要把 Shifted Windows用到 NLP 里面,而且如果真的能做到这一点,那 Swin Transformer真的就是一个里程碑式的工作了,而且模型大一统的故事也就讲的圆满了

方法

主要分为两大块

  • 大概把整体的流程讲了一下,主要就是过了一下前向过程,以及提出的 patch merging 操作是怎么做的
  • 基于 Shifted Window 的自注意力,Swin Transformer怎么把它变成一个transformer block 进行计算

前向过程

  • 假设说有一张224*224*3(ImageNet 标准尺寸)的输入图片
  • 第一步就是像 ViT 那样把图片打成 patch,在 Swin Transformer 这篇论文里,它的 patch size 是4*4,而不是像 ViT 一样16*16,所以说它经过 patch partition 打成 patch 之后,得到图片的尺寸是56*56*48,56就是224/4,因为 patch size 是4,向量的维度48,因为4*4*3,3 是图片的 RGB 通道
  • 打完了 patch ,接下来就要做 Linear Embedding,也就是说要把向量的维度变成一个预先设置好的值,就是 Transformer 能够接受的值,在 Swin Transformer 的论文里把这个超参数设为 c,对于 Swin tiny 网络来说,也就是上图中画的网络总览图,它的 c 是96,所以经历完 Linear Embedding 之后,输入的尺寸就变成了56*56*96,前面的56*56就会拉直变成3136,变成了序列长度,后面的96就变成了每一个token向量的维度,其实 Patch Partition 和 Linear Embedding 就相当于是 ViT 里的Patch Projection 操作,而在代码里也是用一次卷积操作就完成了,
  • 第一部分跟 ViT 其实还是没有区别的,但紧接着区别就来了
  • 首先序列长度是3136,对于 ViT 来说,用 patch size 16*16,它的序列长度就只有196,是相对短很多的,这里的3136就太长了,是目前来说Transformer不能接受的序列长度,所以 Swin Transformer 就引入了基于窗口的自注意力计算,每个窗口按照默认来说,都只有七七四十九个 patch,所以说序列长度就只有49就相当小了,这样就解决了计算复杂度的问题
  • 所以也就是说, stage1中的swin transformer block 是基于窗口计算自注意力的,现在暂时先把 transformer block当成是一个黑盒,只关注输入和输出的维度,对于 Transformer 来说,如果不对它做更多约束的话,Transformer输入的序列长度是多少,输出的序列长度也是多少,它的输入输出的尺寸是不变的,所以说在 stage1 中经过两层Swin Transformer block 之后,输出还是56*56*96
  • 到这其实 Swin Transformer的第一个阶段就走完了,也就是先过一个 Patch Projection 层,然后再过一些 Swin Transformer block,接下来如果想要有多尺寸的特征信息,就要构建一个层级式的 transformer,也就是说需要一个像卷积神经网络里一样,有一个类似于池化的操作

Patch Merging

Patch Merging 其实在之前一些工作里也有用到,它很像 Pixel Shuffle 的上采样的一个反过程,Pixel Shuffle 是 lower level 任务中很常用的一个上采样方式

  • 假如有一个张量, Patch Merging 顾名思义就是把临近的小 patch 合并成一个大 patch,这样就可以起到下采样一个特征图的效果了
  • 这里因为是想下采样两倍,所以说在选点的时候是每隔一个点选一个,也就意味着说对于这个张量来说,每次选的点是1、1、1、1
  • 其实在这里的1、2、3、4并不是矩阵里有的值,而是给它的一个序号,同样序号位置上的 patch 就会被 merge 到一起,这个序号只是为了帮助理解
  • 经过隔一个点采一个样之后,原来的这个张量就变成了四个张量,也就是说所有的1都在一起了,2在一起,3在一起,4在一起,如果原张量的维度是 h * w * c ,当然这里 c 没有画出来,经过这次采样之后就得到了4个张量,每个张量的大小是 h/2、w/2,它的尺寸都缩小了一倍
  • 现在把这四个张量在 c 的维度上拼接起来,也就变成了下图中所画出来的形式,张量的大小就变成了 h/2 * w/2 * 4c,相当于用空间上的维度换了更多的通道数
  • 通过这个操作,就把原来一个大的张量变小了,就像卷积神经网络里的池化操作一样,为了跟卷积神经网络那边保持一致(不论是 VGGNet 还是 ResNet,一般在池化操作降维之后,通道数都会翻倍,从128变成256,从256再变成512),所以这里也只想让他翻倍,而不是变成4倍,所以紧接着又再做了一次操作,就是在 c 的维度上用一个1乘1的卷积,把通道数降下来变成2c,通过这个操作就能把原来一个大小为 h*w*c 的张量变成 h/2 * w/2 *2c 的一个张量,也就是说空间大小减半,但是通道数乘2,这样就跟卷积神经网络完全对等起来了

这里其实会发现,特征图的维度真的跟卷积神经网络好像,因为如果回想残差网络的多尺寸的特征,就是经过每个残差阶段之后的特征图大小也是56*56、28*28、14*14,最后是7*7

而且为了和卷积神经网络保持一致,Swin Transformer这篇论文并没有像 ViT 一样使用 CLS token,ViT 是给刚开始的输入序列又加了一个 CLS token,所以这个长度就从196变成了197,最后拿 CLS token 的特征直接去做分类,但 Swin Transformer 没有用这个 token,它是像卷积神经网络一样,在得到最后的特征图之后用global average polling,就是全局池化的操作,直接把7*7就取平均拉直变成1了

作者这个图里并没有画,因为 Swin Transformer的本意并不是只做分类,它还会去做检测和分割,所以说它只画了骨干网络的部分,没有去画最后的分类头或者检测头,但是如果是做分类的话,最后就变成了1*768,然后又变成了1*1,000

所以看完整个前向过程之后,就会发现 Swin Transformer 有四个 stage,还有类似于池化的 patch merging 操作,自注意力还是在小窗口之内做的以及最后还用的是 global average polling,所以说 Swin Transformer 这篇论文真的是把卷积神经网络和 Transformer 这两系列的工作完美的结合到了一起,也可以说它是披着Transformer皮的卷积神经网络

主要贡献

这篇论文的主要贡献就是基于窗口或者移动窗口的自注意力,这里作者又写了一段研究动机,就是为什么要引入窗口的自注意力,其实跟之前引言里说的都是一个事情,就是说全局自注意力的计算会导致平方倍的复杂度,同样当去做视觉里的下游任务,尤其是密集预测型的任务,或者说遇到非常大尺寸的图片时候,这种全局算自注意力的计算复杂度就非常贵了,所以就用窗口的方式去做自注意力

重点:窗口注意力

原图片会被平均的分成一些没有重叠的窗口,拿第一层之前的输入来举例,它的尺寸就是56*56*96,也就说有一个维度是56*56张量,然后把它切成一些不重叠的方格(论文中使用7*7的patch作为一个window窗口)

  • 现在所有自注意力的计算都是在这些小窗口里完成的,就是说序列长度永远都是7*7=49
  • 原来大的整体特征图到底里面会有多少个窗口呢?其实也就是每条边56/7就8个窗口,也就是说一共会有8*8等于64个窗口,就是说会在这64个窗口里分别去算它们的自注意力

基于窗口的自注意力模式的计算复杂度计算:

  • 如果现在有一个输入,自注意力首先把它变成 q k v 三个向量,这个过程其实就是原来的向量分别乘了三个系数矩阵
  • 一旦得到 query 和 k 之后,它们就会相乘,最后得到 attention,也就是自注意力的矩阵
  • 有了自注意力之后,就会和 value 做一次乘法,也就相当于是做了一次加权
  • 最后因为是多头自注意力,所以最后还会有一个 projection layer,这个投射层会把向量的维度投射到我们想要的维度

如果这些向量都加上它们该有的维度,也就是说刚开始输入是 h*w*c

  • 公式(1)对应的是标准的多头自注意力的计算复杂度
  • 每一个图片大概会有 h*w 个 patch,在刚才的例子里,h 和 w 分别都是56,c 是特征的维度
  • 公式(2)对应的是基于窗口的自注意力计算的复杂度,这里的 M 就是刚才的7,也就是说一个窗口的某条边上有多少个patch

基于窗口的自注意力计算复杂度又是如何得到的呢?

  • 因为在每个窗口里算的还是多头自注意力,所以可以直接套用公式(1),只不过高度和宽度变化了,现在高度和宽度不再是 h * w,而是变成窗口有多大了,也就是 M*M,也就是说现在 h 变成了 M,w 也是 M,它的序列长度只有 M * M 这么大
  • 所以当把 M 值带入到公式(1)之后,就得到计算复杂度是4 * M^2 * c^2 + 2 * M^4 * c,这个就是在一个窗口里算多头自注意力所需要的计算复杂度
  • 那我们现在一共有 h/M * w/M 个窗口,现在用这么多个窗口乘以每个窗口所需要的计算复杂度就能得到公式(2)了

对比公式(1)和公式(2),虽然这两个公式前面这两项是一样的,只有后面从 (h*w)^2变成了 M^2 * h * w,看起来好像差别不大,但其实如果仔细带入数字进去计算就会发现,计算复杂的差距是相当巨大的,因为这里的 h*w 如果是56*56的话, M^2 其实只有49,所以是相差了几十甚至上百倍的

这种基于窗口计算自注意力的方式虽然很好地解决了内存和计算量的问题,但是窗口和窗口之间没有通信,这样就达不到全局建模了,也就文章里说的会限制模型的能力,所以最好还是要有一种方式能让窗口和窗口之间互相通信起来,这样效果应该会更好,因为具有上下文的信息,所以作者就提出移动窗口的方式

移动窗口:

移动窗口就是把原来的窗口往右下角移动一半窗口的距离,如果Transformer是上下两层连着做这种操作,先是 window再是 shifted window 的话,就能起到窗口和窗口之间互相通信的目的了

所以说在 Swin Transformer里, transformer block 的安排是有讲究的,每次都是先要做一次基于窗口的多头自注意力,然后再做一次基于移动窗口的多头自注意力,这样就达到了窗口和窗口之间的互相通信。如下图所示

  • 每次输入先进来之后先做一次 Layernorm,然后做窗口的多头自注意力,然后再过 Layernorm 过 MLP,第一个 block 就结束了
  • 这个 block 结束以后,紧接着做一次Shifted window,也就是基于移动窗口的多头自注意力,然后再过 MLP 得到输出
  • 这两个 block 加起来其实才算是 Swin Transformer 一个基本的计算单元,这也就是为什么stage1、2、3、4中的 swin transformer block 为什么是 *2、*2、*6、*2,也就是一共有多少层 Swin Transformer block 的数字总是偶数,因为它始终都需要两层 block连在一起作为一个基本单元,所以一定是2的倍数

到此,Swin Transformer整体的故事和结构就已经讲完了,主要的研究动机就是想要有一个层级式的 Transformer,为了这个层级式,所以介绍了 Patch Merging 的操作,从而能像卷积神经网络一样把 Transformer 分成几个阶段,为了减少计算复杂度,争取能做视觉里密集预测的任务,所以又提出了基于窗口和移动窗口的自注意力方式,也就是连在一起的两个Transformer block,最后把这些部分加在一起,就是 Swin Transformer 的结构

提高移动窗口的计算效率:

  • 一个是怎样提高移动窗口的计算效率,他们采取了一种非常巧妙的 masking(掩码)的方式
  • 另外一个点就是这篇论文里没有用绝对的位置编码,而是用相对的位置编码

masking(掩码)的方式计算移动窗口自注意力:为什么需要使用?

为了提高计算效率,因为如果直接计算右下图的九个窗口的自注意力,不同大小的窗口无法合并成一个batch进行计算。

  • 上图是一个基础版本的移动窗口,就是把左边的窗口模式变成了右边的窗口方式
  • 虽然这种方式已经能够达到窗口和窗口之间的互相通信了,但是会发现一个问题,就是原来计算的时候,特征图上只有四个窗口,但是做完移动窗口操作之后得到了9个窗口,窗口的数量增加了,而且每个窗口里的元素大小不一,比如说中间的窗口还是4*4,有16个 patch,但是别的窗口有的有4个 patch,有的有8个 patch,都不一样了,如果想做快速运算,就是把这些窗口全都压成一个 patch直接去算自注意力,就做不到了,因为窗口的大小不一样
  • 有一个简单粗暴的解决方式就是把这些小窗口周围再 pad 上0 ,把它照样pad成和中间窗口一样大的窗口,这样就有9个完全一样大的窗口,这样就还能把它们压成一个batch,就会快很多
  • 但是这样的话,无形之中计算复杂度就提升了,因为原来如果算基于窗口的自注意力只用算4个窗口,但是现在需要去算9个窗口,复杂度一下提升了两倍多,所以还是相当可观的
  • 那怎么能让第二次移位完的窗口数量还是保持4个,而且每个窗口里的patch数量也还保持一致呢?作者提出了一个非常巧妙的掩码方式,如下图所示

上图是说,当通过普通的移动窗口方式,得到9个窗口之后,现在不在这9个窗口上算自注意力,先再做一次循环移位( cyclic shift )

  • 经过这次循环移位之后,原来的窗口(虚线)就变成了现在窗口(实线)的样子,那如果在大的特征图上再把它分成四宫格的话,我在就又得到了四个窗口,意思就是说移位之前的窗口数也是4个,移完位之后再做一次循环移位得到窗口数还是4个,这样窗口的数量就固定了,也就说计算复杂度就固定了
  • 但是新的问题就来了,虽然对于移位后左上角的窗口(也就是移位前最中间的窗口)来说,里面的元素都是互相紧挨着的,他们之间可以互相两两做自注意力,但是对于剩下几个窗口来说,它们里面的元素是从别的很远的地方搬过来的,所以他们之间,按道理来说是不应该去做自注意力,也就是说他们之间不应该有什么太大的联系
  • 解决这个问题就需要一个很常规的操作,也就是掩码操作,这在Transformer过去的工作里是层出不穷,很多工作里都有各式各样的掩码操作
  • 在 Swin Transformer这篇论文里,作者也巧妙的设计了几种掩码的方式,从而能让一个窗口之中不同的区域之间也能用一次前向过程,就能把自注意力算出来,但是互相之间都不干扰,也就是后面的 masked Multi-head Self Attention(MSA)
  • 算完了多头自注意力之后,还有最后一步就是需要把循环位移再还原回去,也就是说需要把A、B、C再还原到原来的位置上去,原因是还需要保持原来图片的相对位置大概是不变的,整体图片的语义信息也是不变的,如果不把循环位移还原的话,那相当于在做Transformer的操作之中,一直在把图片往右下角移,不停的往右下角移,这样图片的语义信息很有可能就被破坏掉了
  • 所以说整体而言,上图介绍了一种高效的、批次的计算方式比如说本来移动窗口之后得到了9个窗口,而且窗口之间的patch数量每个都不一样,为了达到高效性,为了能够进行批次处理,先进行一次循环位移,把9个窗口变成4个窗口,然后用巧妙的掩码方式让每个窗口之间能够合理地计算自注意力,最后再把算好的自注意力还原,就完成了基于移动窗口的自注意力计算

掩码操作如何实现 :

作者通过这种巧妙的循环位移的方式和巧妙设计的掩码模板,从而实现了只需要一次前向过程,就能把所有需要的自注意力值都算出来,而且只需要计算4个窗口,也就是说窗口的数量没有增加,计算复杂度也没有增加,非常高效的完成了这个任务

作者给出了不同窗口的不同掩码矩阵:

上图示例的Cyclic Shifting方法,可以保持面向计算的window数量保持不变(还是2X2),在window内部通过attention mask来计算子window中的自注意力。

Swin Transformer的几个变体

  • Swin Tiny
  • Swin Small
  • Swin Base
  • Swin Large

Swin Tiny的计算复杂度跟 ResNet-50 差不多,Swin Small 的复杂度跟 ResNet-101 是差不多的,这样主要是想去做一个比较公平的对比

这些变体之间有哪些不一样呢?,其实主要不一样的就是两个超参数

  • 一个是向量维度的大小 c
  • 另一个是每个 stage 里到底有多少个 transform block

这里其实就跟残差网络就非常像了,残差网络也是分成了四个 stage,每个 stage 有不同数量的残差块

实验

分类

首先是分类上的实验,这里一共说了两种预训练的方式

  • 第一种就是在正规的ImageNet-1K(128万张图片、1000个类)上做预训练
  • 第二种方式是在更大的ImageNet-22K(1,400万张图片、2万多个类别)上做预训练

当然不论是用ImageNet-1K去做预训练,还是用ImageNet-22K去做预训练,最后测试的结果都是在ImageNet-1K的测试集上去做的,结果如下表所示

  • 上半部分是ImageNet-1K预训练的模型结果
  • 下半部分是先用ImageNet-22K去预训练,然后又在ImageNet-1K上做微调,最后得到的结果
  • 在表格的上半部分,作者先是跟之前最好的卷积神经网络做了一下对比,RegNet 是之前 facebook 用 NASA 搜出来的模型,EfficientNet 是 google 用NASA 搜出来的模型,这两个都算之前表现非常好的模型了,他们的性能最高会到 84.3
  • 接下来作者就写了一下之前的 Vision Transformer 会达到什么效果,对于 ViT 来说,因为它没有用很好的数据增强,而且缺少偏置归纳,所以说它的结果是比较差的,只有70多
  • 换上 DeiT 之后,因为用了更好的数据增强和模型蒸馏,所以说 DeiT Base 模型也能取得相当不错的结果,能到83.1
  • 当然 Swin Transformer 能更高一些,Swin Base 最高能到84.5,稍微比之前最好的卷积神经网络高那么一点点,就比84.3高了0.2
  • 虽然之前表现最好的 EfficientNet 的模型是在 600*600 的图片上做的,而 Swin Base 是在 384*384 的图片上做的,所以说 EfficientNet 有一些优势,但是从模型的参数和计算的 FLOPs 上来说 EfficientNet 只有66M,而且只用了 37G 的 FLOPs,但是 Swin Transformer 用了 88M 的模型参数,而且用了 47G 的 FLOPs,所以总体而言是伯仲之间
  • 表格的下半部分是用 ImageNet-22k 去做预训练,然后再在ImageNet-1k上微调最后得到的结果
  • 这里可以看到,一旦使用了更大规模的数据集,原始标准的 ViT 的性能也就已经上来了,对于 ViT large 来说它已经能得到 85.2 的准确度了,已经相当高了
  • 但是 Swin Large 更高,Swin Large 最后能到87.3,这个是在不使用JFT-300M,就是特别大规模数据集上得到的结果,所以还是相当高的

目标检测

  • 表2(a)中测试了在不同的算法框架下,Swin Transformer 到底比卷积神经网络要好多少,主要是想证明 Swin Transformer 是可以当做一个通用的骨干网络来使用的,所以用了 Mask R-CNN、ATSS、RepPointsV2 和SparseR-CNN,这些都是表现非常好的一些算法,在这些算法里,过去的骨干网络选用的都是 ResNet-50,现在替换成了 Swin Tiny
  • Swin Tiny 的参数量和 FLOPs 跟 ResNet-50 是比较一致的,从后面的对比里也可以看出来,所以他们之间的比较是相对比较公平的
  • 可以看到,Swin Tiny 对 ResNet-50 是全方位的碾压,在四个算法上都超过了它,而且超过的幅度也是比较大的
  • 接下来作者又换了一个方式做测试,现在是选定一个算法,选定了Cascade Mask R-CNN 这个算法,然后换更多的不同的骨干网络,比如 DeiT-S、ResNet-50 和 ResNet-101,也分了几组,结果如上图中表2(b)所示
  • 可以看出,在相似的模型参数和相似的 Flops 之下,Swin Transformer 都是比之前的骨干网络要表现好的
  • 接下来作者又做了第三种测试的方式,如上图中的表2(c)所示,就是系统层面的比较,这个层面的比较就比较狂野了,就是现在追求的不是公平比较,什么方法都可以上,可以使用更多的数据,可以使用更多的数据增强,甚至可以在测试的使用 test time augmentation(TTA)的方式
  • 可以看到,之前最好的方法 Copy-paste 在 COCO Validation Set上的结果是55.9,在 Test Set 上的结果是56,而这里如果跟最大的 Swin Transformer–Swin Large 比,它的结果分别能达到58和58.7,这都比之前高了两到三个点

语义分割

  • 上图表3里可以看到之前的方法,一直到 DeepLab V3、ResNet 其实都用的是卷积神经网络,之前的这些方法其实都在44、45左右徘徊
  • 但是紧接着 Vision Transformer 就来了,那首先就是 SETR 这篇论文,他们用了 ViT Large,所以就取得了50.3的这个结果
  • Swin Transformer Large也取得了53.5的结果,就刷的更高了
  • 其实作者这里也有标注,就是有两个“+”号的,意思是说这些模型是在ImageNet-22K 数据集上做预训练,所以结果才这么好

消融实验

实验结果如下图所示

  • 上图中表4主要就是想说一下移动窗口以及相对位置编码到底对 Swin Transformer 有多有用
  • 可以看到,如果光分类任务的话,其实不论是移动窗口,还是相对位置编码,它的提升相对于基线来说,也没有特别明显,当然在ImageNet的这个数据集上提升一个点也算是很显着了
  • 但是他们更大的帮助,主要是出现在下游任务里,就是 COCO 和 ADE20K 这两个数据集上,也就是目标检测和语义分割这两个任务上
  • 可以看到,用了移动窗口和相对位置编码以后,都会比之前大概高了3个点左右,提升是非常显着的,这也是合理的,因为如果现在去做这种密集型预测任务的话,就需要特征对位置信息更敏感,而且更需要周围的上下文关系,所以说通过移动窗口提供的窗口和窗口之间的互相通信,以及在每个 Transformer block都做更准确的相对位置编码,肯定是会对这类型的下游任务大有帮助的

总结

虽然前面已经说了很多 Swin Transformer 的影响力啊已经这么巨大了,但其实他的影响力远远不止于此,论文里这种对卷积神经网络,对 Transformer,还有对 MLP 这几种架构深入的理解和分析是可以给更多的研究者带来思考的,从而不仅可以在视觉领域里激发出更好的工作,而且在多模态领域里,相信它也能激发出更多更好的工作

BPR:用于实例分割的边界Patch优化(CVPR2021)

 

Look Closer to Segment Better: Boundary Patch Refinement for Instance Segmentation

代码链接:https://github.com/tinyalpha/BPR

后处理分割结果,效果是即插即用后处理模块当年的sota通过将 BPR 框架应用于 PolyTransform + SegFix 基线,我们在 Cityscapes 排行榜上排名第一。

从目前的排名来说(22.09.23),排名第五,与top1相差不到2个百分点,而 BPR后处理使得PolyTransform + SegFix的效果提升了1.5个百分点。 相比于MASK-RCNN提升了4.2个百分点。

CVPR21上一篇关于实例分割的文章。对于Mask RCNN来说,其最终得到的mask分辨率太低,因此还原到原尺寸的时候,一些boundary信息就显得非常粗糙,导致预测生成的mask效果不尽如人意。而且处于boundary的pixel本身数量相比于整张image来说很少,同时本身难以做分类。现有的一些方法试图提升boundary quality,但预测mask边界这个task本身的复杂度和segmentation很接近了,因此开销较大。

因此本文作者提出了一种crop-and-refine的策略。首先通过经典的实例分割网络(如Mask RCNN)得到coarse mask。随后在mask的boundary出提取出一系列的patch,随后将这些patch送入一个Refinement Network,这个Refinement Network负责做二分类的语义分割,进而对boundary处的patch进行优化,整个后处理的优化网络称为BPR(Boundary Patch Refinement)。该网络可以解决传统Mask RCNN预测的mask的边界粗糙的问题。

本文的核心就是在Mask RCNN一类的网络给出coarse mask后,如何设计Refine Network来对这个粗糙 mask 的边界进行优化,进而得到resolution更高,boundary quality更好的mask。

给定一个coarse mask(上图a),首先需要决定这个mask的哪些部分要做refine。这里作者提出了一种sliding-window式的方法提取到boundary处的一系列patch(上图b)。具体来说,就是在mask边界处密集assign正方形的bounding box,这些box内部囊括了boundary pixel。随后,由于这些box有的overlap太大导致redundant(冗余),这里采用NMS进行过滤(上图c),以实现速度和精度的trade-off(平衡)。

随后这些survive下来的image patch(上图d)和mask patch(上图e)都resize到同一尺寸,一起喂入Refinement Network。这里作者argue说一定要喂入mask patch,因为一旦拥有mask patch的location和semantic信息,这个refinement network就不再需要学习instance-level semantic(实例类别信息,比如这个image patch属于哪个类别)了。所以,refinement network只需要学习boundary处的hard pixel,并把它们正确分类。

关于Refinement Network,其任务是为每一个提取出来的boundary patch独立地做二分类语义分割,任何的语义分割模型都可以搬过来做这个task。输入的通道数为4(RGB+mask),输出通道数为2(BG or FG),这里作者采用了HRNetV2(CVPR 2019),这种各种level feature不断做融合的网络可以maintain高分辨率的representation。通过合理的增加input size,boundary batch就可以得到比之前方法更高的resolution。

HRNetV2 网络结构

在对每个patch独立地refine以后,需要将它们reassemble(组装)到coarse mask上面。有的相邻的patch可能存在overlap的情况,最终的结果是取平均,以0.5作为阈值判断某个pixel属于前景或是背景。

Experiment

这里的指标是AP (Average precision):指的是PR曲线的面积(AP就是平均精准度,简单来说就是对PR曲线上的Precision值求均值。)对于实例分割的评价指标:使用AP评价指标

实例分割和目标检测mAP计算时除了IOU计算方式(实例分割是mask间的IOU)不同,其他都是一样的.

对于一个二分类任务,二分类器的预测结果可分为以下4类:

二分类器的结果可分为4类

Precision的定义为:

Recall的定义为: 

Precision从预测结果角度出发,描述了二分类器预测出来的正例结果中有多少是真实正例,即该二分类器预测的正例有多少是准确的;Recall从真实结果角度出发,描述了测试集中的真实正例有多少被二分类器挑选了出来,即真实的正例有多少被该二分类器召回。

逐步降低二分类器预测正例的门槛,则每次可以计算得到当前的Precision和Recall。以Recall作为横轴,Precision作为纵轴可以得到Precision-Recall曲线图,简称为P-R图。

详细解释:目标检测/实例分割中 AP 和 mAP 的混淆指标

preview

首先通过实验证明了将mask patch一并作为输入的重要性:

patch size、不同的patch extraction策略,input size对结果的影响:

RefineNet的选取,NMS的阈值:

Cityscape上与其他方法的比较:PolyTransform + SegFix baseline,达到最高的AP。

迁移到其他model上面的结果 and coco数据集上的结果

Mask-RCNN论文

论文:http://cn.arxiv.org/pdf/1703.06870v3

代码:https://github.com/facebookresearch/maskrcnn-benchmark

B站网络详解 FPN

Introduction

我们提出了一个简单、灵活、通用的实例分割框架,称为Mask R-CNN。我们的方法能够有效检测图像中的目标,同时为每个实例生成高质量的分割掩码。Mask R-CNN通过添加一个预测对象掩码的分支,与现有的边框识别分支并行,扩展了之前的Faster R-CNN。Mask R-CNN的训练很简单,只为Faster R-CNN增加了一小部分开销,运行速度为5帧/秒。此外,Mask R-CNN很容易泛化到其他任务,如人体姿态估计。我们展示了Mask R-CNN在COCO挑战赛的实例分割、目标检测和人物关键点检测任务上的最优结果。在不使用花哨技巧的情况下,Mask R-CNN在各项任务上都优于现有的单一模型,包括COCO 2016挑战赛的冠军。我们希望Mask R-CNN能够成为一个坚实的基线,并有助于简化未来实例识别的研究。

Fast/Faster R-CNN和Fully Convolutional Network(FCN)框架极大地推动了计算机视觉领域中目标检测和语义分割等方向的发展。这些方法的概念很直观,具有良好的灵活性和鲁棒性,并且能够快速训练和推理。我们这项工作的目标是为实例分割任务开发一个相对可行的框架。

实例分割具有一定的挑战性,因为它需要正确检测图像中的所有对象,同时还要精确分割每个实例。因此,它结合了目标检测和语义分割等计算机视觉任务中的元素。目标检测旨在对单个物体进行分类,并使用边框对每个物体进行定位。语义分割旨在将每个像素归类到一组固定的类别,而不区分对象实例。鉴于此,人们可能会认为需要一套复杂的方法才能获得良好的结果。然而,我们证明了一个令人惊讶的事实:简单、灵活、快速的系统也可以超越现有的最先进的实例分割模型。

我们的方法称为Mask R-CNN,通过在每个RoI(感兴趣区域,Region of Interest)上添加一个预测分割掩码的分支来扩展Faster R-CNN,并与现有的用于分类和边框回归的分支并行。掩码分支是应用于每个RoI的一个小FCN,以像素到像素的方式预测分割掩码,并且只会增加较小的计算开销。Mask R-CNN是基于Faster R-CNN框架而来的,易于实现和训练,有助于广泛、灵活的架构设计。

原则上,Mask R-CNN是Faster R-CNN的直观扩展,但正确构建掩码分支对于获得好的结果至关重要。最重要的是,Faster R-CNN的设计没有考虑网络输入和输出之间的像素到像素的对齐。这一点在RoIPool(处理实例的核心操作)如何执行粗空间量化来提取特征上表现得最为明显。为了修正错位,我们提出了一个简单的、没有量化的层,称为RoIAlign,它忠实地保留了精确的空间位置。尽管这看起来是一个很小的变化,但是RoIAlign有很大的影响:它将掩码精度提高了10%-50%,在更严格的localization指标下显示出更大的收益。其次,我们发现有必要将掩码和类别预测解耦:我们为每个类别独立预测一个二进制掩码,类别之间没有竞争,并依靠网络的RoI分类分支来预测类别。相比之下,FCN通常执行逐像素的多分类操作,将分割和分类耦合在一起,我们的实验结果表明这种方法的实例分割效果不佳。

在不使用花哨技巧的情况下,Mask R-CNN在COCO实例分割任务上就超越了之前的所有SOTA单模型,包括COCO 2016比赛的冠军。作为副产品,我们的方法在COCO目标检测任务上也表现出色。在消融实验中,我们评估了多个基本实例,这使我们能够证明Mask R-CNN的鲁棒性,并分析其核心因素的影响。

我们的模型可以在GPU上以每帧约200ms的速度运行,在一台8-GPU的机器上进行COCO训练需要1-2天。我们相信,快速的训练和测试,以及框架的灵活性和准确性,将有利于未来实例分割的研究。

最后,我们通过COCO关键点数据集上的人体姿态估计任务展示了Mask R-CNN框架的通用性。通过将每个关键点视为一个独热二进制掩码,只需对Mask R-CNN稍加修改,即可用于检测特定实例的姿态。Mask R-CNN超越了COCO 2016关键点检测比赛的冠军,并且能够以5帧/秒的速度运行。因此,Mask R-CNN可以被更广泛地视为一个实例识别的灵活框架,并且很容易泛化到其他更复杂的任务上。

模型方法

Mask R-CNN方法很简单:Faster R-CNN对每个候选对象有两个输出,一个是类别标签,另一个是边框偏移量。在此基础上,我们添加了第三个分支,用于输出分割掩码。因此,Mask R-CNN是一个自然且直观的想法。但是掩码输出不同于类别和边框输出,需要提取更精细的对象空间布局。接下来,我们介绍了Mask R-CNN的关键元素,包括像素到像素对齐,这是Fast/Faster R-CNN所缺失的部分。

用于实例分割的Mask R-CNN框架

RoIAlign:虚线网格表示特征映射图,实线边框表示RoI(Region of Interest),点表示每个边框中的4个采样点。RoIAlign通过双线性插值从特征映射图上的相邻网格点计算每个采样点的值。

  • Network Architecture: 为了表述清晰,有两种分类方法
  1. 使用了不同的backbone:resnet-50,resnet-101,resnext-50,resnext-101;
  2. 使用了不同的head Architecture:Faster RCNN使用resnet50时,从Block 4导出特征供RPN使用,这种叫做ResNet-50-C4
  3. 作者使用除了使用上述这些结构外,还使用了一种更加高效的backbone:FPN(特征金字塔网络)
Head架构:我们扩展了两个现有的Faster R-CNN Head。
  • Mask R-CNN基本结构:与Faster RCNN采用了相同的two-state结构:首先是通过一阶段网络找出RPN,然后对RPN找到的每个RoI进行分类、定位、并找到binary mask。这与当时其他先找到mask然后在进行分类的网络是不同的。
  • Mask R-CNN的损失函数L = L{_{cls}} + L{_{box}} + L{_{mask}} (当然了,你可以在这里调权以实现更好的效果)
  • Mask的表现形式(Mask Representation):因为没有采用全连接层并且使用了RoIAlign,我们最终是在一个小feature map上做分割。
  • RoIAlign:RoIPool的目的是为了从RPN网络确定的ROI中导出较小的特征图(a small feature map,eg 7×7),ROI的大小各不相同,但是RoIPool后都变成了7×7大小。RPN网络会提出若干RoI的坐标以[x,y,w,h]表示,然后输入RoI Pooling,输出7×7大小的特征图供分类和定位使用。问题就出在RoI Pooling的输出大小是7×7上,如果RON网络输出的RoI大小是8*8的,那么无法保证输入像素和输出像素是一一对应,首先他们包含的信息量不同(有的是1对1,有的是1对2),其次他们的坐标无法和输入对应起来。这对分类没什么影响,但是对分割却影响很大。RoIAlign的输出坐标使用插值算法得到,不再是简单的量化;每个grid中的值也不再使用max,同样使用差值算法。

Implementation Details

使用Fast/Faster相同的超参数,同样适用于Mask RCNN

  • Training:

1、与之前相同,当IoU与Ground Truth的IoU大于0.5时才会被认为有效的RoI,L{_{mask}}只把有效RoI计算进去。

2、采用image-centric training,图像短边resize到800,每个GPU的mini-batch设置为2,每个图像生成N个RoI,在使用ResNet-50-C4 作为backbone时,N=64,在使用FPN作为backbone时,N=512。作者服务器中使用了8块GPU,所以总的minibatch是16, 迭代了160k次,初始lr=0.02,在迭代到120k次时,将lr设定到 lr=0.002,另外学习率的weight_decay=0.0001 momentum = 0.9。如果是resnext,初始lr=0.01,每个GPU的mini-batch是1。

3、RPN的anchors有5种scale,3种ratios。为了方便剥离、如果没有特别指出,则RPN网络是单独训练的且不与Mask R-CNN共享权重。但是在本论文中,RPN和Mask R-CNN使用一个backbone,所以他们的权重是共享的。(Ablation Experiments 为了方便研究整个网络中哪个部分其的作用到底有多大,需要把各部分剥离开)

  • Inference:在测试时,使用ResNet-50-C4作为 backbone情况下proposal number=300,使用FPN作为 backbone时proposal number=1000。然后在这些proposal上运行bbox预测,接着进行非极大值抑制。mask分支只应用在得分最高的100个proposal上。顺序和train是不同的,但这样做可以提高速度和精度。mask 分支对于每个roi可以预测k个类别,但是我们只要背景和前景两种,所以只用k-th mask,k是根据分类分支得到的类型。然后把k-th mask resize成roi大小,同时使用阈值分割(threshold=0.5)二值化

Experiments

Main Results

在下图中可以明显看出,FCIS的分割结果中都会出现一条竖着的线(systematic artifacts),这线主要出现在物体重的部分,作者认为这是FCIS架构的问题,无法解决的。但是在Mask RCNN中没有出现。

Ablation Experiments(剥离实验)

  • Architecture:
    从table 2a中看出,Mask RCNN随着增加网络的深度、采用更先进的网络,都可以提高效果。注意:并不是所有的网络都是这样。
  • Multinomial vs. Independent Masks:(mask分支是否进行类别预测)从table 2b中可以看出,使用sigmoid(二分类)和使用softmax(多类别分类)的AP相差很大,证明了分离类别和mask的预测是很有必要的
  • Class-Specific vs. Class-Agnostic Masks:目前使用的mask rcnn都使用class-specific masks,即每个类别都会预测出一个mxm的mask,然后根据类别选取对应的类别的mask。但是使用Class-Agnostic Masks,即分割网络只输出一个mxm的mask,可以取得相似的成绩29.7vs30.3
  • RoIAlign:tabel 2c证明了RoIAlign的性能
  • Mask Branch:tabel 2e,FCN比MLP性能更好

Bounding Box Detection Results    

  • Mask RCNN精度高于Faster RCNN
  • Faster RCNN使用RoI Align的精度更高
  • Mask RCNN的分割任务得分与定位任务得分相近,说明Mask RCNN已经缩小了这部分差距。

Timing 

  • Inference:195ms一张图片,显卡Nvidia Tesla M40。其实还有速度提升的空间,比如减少proposal的数量等。
  • Training:ResNet-50-FPN on COCO trainval35k takes 32 hours  in our synchronized 8-GPU implementation (0.72s per 16-image mini-batch),and 44 hours with ResNet-101-FPN。

Mask R-CNN for Human Pose Estimation

让Mask R-CNN预测k个masks,每个mask对应一个关键点的类型,比如左肩、右肘,可以理解为one-hot形式。

  • 使用cross entropy loss,可以鼓励网络只检测一个关键点;
  • ResNet-FPN结构
  • 训练了90k次,最开始lr=0.02,在迭代60k次时,lr=0.002,80k次时变为0.0002

MICCAI 2022:基于 MLP 的快速医学图像分割网络 UNeXt

论文地址: https://arxiv.org/abs/2203.04967

github:https://github.com/jeya-maria-jose/UNeXt-pytorch

UnetX 网络结构

Datasets

  1. ISIC 2018 – Link
  2. BUSI – Link

MICCAI 2022:基于 MLP 的快速医学图像分割网络 UNeXt

前言

最近 MICCAI 2022 的论文集开放下载了,地址:https://link.springer.com/book/10.1007/978-3-031-16443-9 ,每个部分的内容如下所示:

Part I: Brain development and atlases; DWI and tractography; functional brain networks; neuroimaging; heart and lung imaging; dermatology;

Part II: Computational (integrative) pathology; computational anatomy and physiology; ophthalmology; fetal imaging;

Part III: Breast imaging; colonoscopy; computer aided diagnosis;

Part IV: Microscopic image analysis; positron emission tomography; ultrasound imaging; video data analysis; image segmentation I;

Part V: Image segmentation II; integration of imaging with non-imaging biomarkers;

Part VI: Image registration; image reconstruction;

Part VII: Image-Guided interventions and surgery; outcome and disease prediction; surgical data science; surgical planning and simulation; machine learning – domain adaptation and generalization;

Part VIII: Machine learning – weakly-supervised learning; machine learning – model interpretation; machine learning – uncertainty; machine learning theory and methodologies.

其中关于分割有两个部分,Image segmentation I 在 Part IV, 而 Image segmentation II 在 Part V。

随着医学图像的解决方案变得越来越适用,我们更需要关注使深度网络轻量级、快速且高效的方法。具有高推理速度的轻量级网络可以被部署在手机等设备上,例如 POCUS(point-of-care ultrasound)被用于检测和诊断皮肤状况。这就是 UNeXt 的动机。

方法概述

之前我们解读过基于 Transformer 的 U-Net 变体,近年来一直是领先的医学图像分割方法,但是参数量往往不乐观,计算复杂,推理缓慢。这篇文章提出了基于卷积多层感知器(MLP)改进 U 型架构的方法,可以用于图像分割。设计了一个 tokenized MLP 块有效地标记和投影卷积特征,使用 MLPs 来建模表示。这个结构被应用到 U 型架构的下两层中(这里我们假设纵向一共五层)。文章中提到,为了进一步提高性能,建议在输入到 MLP 的过程中改变输入的通道,以便专注于学习局部依赖关系特征。还有额外的设计就是跳跃连接了,并不是我们主要关注的地方。最终,UNeXt 将参数数量减少了 72 倍,计算复杂度降低了 68 倍,推理速度提高了 10 倍,同时还获得了更好的分割性能,如下图所示。

UNeXt 架构

UNeXt 的设计如下图所示。纵向来看,一共有两个阶段,普通的卷积和 Tokenized MLP 阶段。其中,编码器和解码器分别设计两个 Tokenized MLP 块。每个编码器将分辨率降低两倍,解码器工作相反,还有跳跃连接结构。每个块的通道数(C1-C5)被设计成超参数为了找到不掉点情况下最小参数量的网络,对于使用 UNeXt 架构的实验,遵循 C1 = 32、C2 = 64、C3 = 128、C4 = 160 和 C5 = 256。

TokMLP 设计思路

关于 Convolutional Stage 我们不做过多介绍了,在这一部分重点专注 Tokenized MLP Stage。从上一部分的图中,可以看到 Shifted MLP 这一操作,其实思路类似于 Swin transformer,引入基于窗口的注意力机制,向全局模型中添加更多的局域性。下图的意思是,Tokenized MLP 块有 2 个 MLP,在一个 MLP 中跨越宽度移动特征,在另一个 MLP 中跨越高度移动特征,也就是说,特征在高度和宽度上依次移位。论文中是这么说的:“我们将特征分成 h 个不同的分区,并根据指定的轴线将它们移到 j=5 的位置”。其实就是创建了随机窗口,这个图可以理解为灰色是特征块的位置,白色是移动之后的 padding。

补充:MLP拥有大量参数,计算成本高且容易过度拟合,而且因为层之间的线性变换总是将前一层的输出作为一个整体,所以MLP在捕获输入特征图中的局部特征结构的能力较弱。通过轴向移动特征信息, Shifted MLP可以得到不同方向的信息流,这有助于捕获局部相关性。该操作使得我们采用纯MLP架构即可取得与CNN相同的感受野。

解释过 Shifted MLP 后,我们再看另一部分:tokenized MLP block。首先,需要把特征转换为 tokens(可以理解为 Patch Embedding 的过程,感觉这个就是个普通卷积,而且作者为了保证conv后的矩阵减半,设置步幅为2,总之,有些编故事的意思了)。为了实现 tokenized 化,使用 kernel size 为 3 的卷积(patch_size=3, stride=2),这样会使得矩阵H和W减半,并将通道的数量改为 E,E 是 embadding 嵌入维度( token 的数量),也是一个超参数。然后把这些 token 送到上面提到的第一个跨越宽度的 MLP 中。

这里会产生了一个疑问,关于 kernel size 为 3 的卷积,使用的是什么样的卷积层?答:这里还是普通的卷积,文章中提到了 DWConv(DepthWise Conv),是后面的特征通过 DW-Conv 传递。使用 DWConv 有两个原因:(1)它有助于对 MLP 特征的位置信息进行编码。MLP 块中的卷积层足以编码位置信息,它实际上比标准的位置编码表现得更好。像 ViT 中的位置编码技术,当测试和训练的分辨率不一样时,需要进行插值,往往会导致性能下降。(2)DWConv 使用的参数数量较少。

这时我们得到了 DW-Conv 传递过来的特征,然后使用 GELU 完成激活。接下来,通过另一个 MLP(跨越height)传递特征,该 MLP 把进一步改变了特征尺寸。在这里还使用一个残差连接,将原始 token 添加为残差。然后我们利用 Layer Norm(LN),将输出特征传递到下一个块。LN 比 BN 更可取,因为它是沿着 token 进行规范化,而不是在 Tokenized MLP 块的整个批处理中进行规范化。上面这些就是一个 tokenized MLP block 的设计思路。

此外,文章中给出了 tokenized MLP block 涉及的计算公式:

其中 T 表示 tokens,H 表示高度,W 表示宽度。值得注意的是,所有这些计算都是在 embedding 维度 H 上进行的,它明显小于特征图的维度 HN×HN,其中 N 取决于 block 大小。在下面的实验部分,文章将 H 设置为 768。

实验部分

实验在 ISIC 和 BUSI 数据集上进行,可以看到,在 GLOPs、性能和推理时间都上表现不错。

下面是可视化和消融实验的部分。可视化图可以发现,UNeXt 处理的更加圆滑和接近真实标签。

消融实验可以发现,从原始的 UNet 开始,然后只是减少过滤器的数量,发现性能下降,但参数并没有减少太多。接下来,仅使用 3 层深度架构,既 UNeXt 的 Conv 阶段。显着减少了参数的数量和复杂性,但性能降低了 4%。加入 tokenized MLP block 后,它显着提高了性能,同时将复杂度和参数量是一个最小值。接下来,我们将 DWConv 添加到 positional embedding,性能又提高了。接下来,在 MLP 中添加  Shifted 操作,表明在标记化之前移位特征可以提高性能,但是不会增加任何参数或复杂性。注意:Shifted MLP 不会增加 GLOPs。

一些理解和总结

在这项工作中,提出了一种新的深度网络架构 UNeXt,用于医疗图像分割,专注于参数量的减小。UNeXt 是一种基于卷积和 MLP 的架构,其中有一个初始的 Conv 阶段,然后是深层空间中的 MLP。具体来说,提出了一个带有移位 MLP 的标记化 MLP 块。在多个数据集上验证了 UNeXt,实现了更快的推理、更低的复杂性和更少的参数数量,同时还实现了最先进的性能。

另外,个人觉得 带有移位 MLP 的标记化 MLP 块这里其实有点讲故事的意思了。

我在读这篇论文的时候,直接注意到了它用的数据集。我认为 UNeXt 可能只适用于这种简单的医学图像分割任务,类似的有 Optic Disc and Cup Seg,对于更复杂的,比如血管,软骨,Liver Tumor,kidney Seg 这些,可能效果达不到这么好,因为运算量被极大的减少了,每个 convolutional 阶段只有一个卷积层。MLP 魔改 U-Net 也算是一个尝试,在 Tokenized MLP block 中加入 DWConv 也是很合理的设计。

代码实现:

class shiftmlp(nn.Module):
    def __init__(self, in_features, hidden_features=None, out_features=None, act_layer=nn.GELU, drop=0., shift_size=5):
        super().__init__()
        out_features = out_features or in_features
        hidden_features = hidden_features or in_features
        self.dim = in_features
        self.fc1 = nn.Linear(in_features, hidden_features)
        self.dwconv = DWConv(hidden_features)
        self.act = act_layer()
        self.fc2 = nn.Linear(hidden_features, out_features)
        self.drop = nn.Dropout(drop)

        self.shift_size = shift_size
        self.pad = shift_size // 2

        
        self.apply(self._init_weights)

    def _init_weights(self, m):
        if isinstance(m, nn.Linear):
            trunc_normal_(m.weight, std=.02)
            if isinstance(m, nn.Linear) and m.bias is not None:
                nn.init.constant_(m.bias, 0)
        elif isinstance(m, nn.LayerNorm):
            nn.init.constant_(m.bias, 0)
            nn.init.constant_(m.weight, 1.0)
        elif isinstance(m, nn.Conv2d):
            fan_out = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
            fan_out //= m.groups
            m.weight.data.normal_(0, math.sqrt(2.0 / fan_out))
            if m.bias is not None:
                m.bias.data.zero_()
    


    def forward(self, x, H, W):
        # pdb.set_trace()
        B, N, C = x.shape

        xn = x.transpose(1, 2).view(B, C, H, W).contiguous()
        #pad,方便后面的torch.chunk
        xn = F.pad(xn, (self.pad, self.pad, self.pad, self.pad) , "constant", 0)
        #按照dim=1维度,分成 self.shift_size(5)个块
        xs = torch.chunk(xn, self.shift_size, 1)
        #torch.roll(x,y,d)将x,沿着d维度,向上/下roll y个值
        x_shift = [torch.roll(x_c, shift, 2) for x_c, shift in zip(xs, range(-self.pad, self.pad+1))]
        x_cat = torch.cat(x_shift, 1)
        #x.narrow(*dimension*, *start*, *length*) → Tensor 表示取变量x的第dimension维,从索引start开始到(start+length-1)范围的值。
        x_cat = torch.narrow(x_cat, 2, self.pad, H)
        x_s = torch.narrow(x_cat, 3, self.pad, W)

        x_s = x_s.reshape(B,C,H*W).contiguous()
        x_shift_r = x_s.transpose(1,2)

        x = self.fc1(x_shift_r)

        x = self.dwconv(x, H, W)
        x = self.act(x) 
        x = self.drop(x)

        xn = x.transpose(1, 2).view(B, C, H, W).contiguous()
        xn = F.pad(xn, (self.pad, self.pad, self.pad, self.pad) , "constant", 0)
        xs = torch.chunk(xn, self.shift_size, 1)
        x_shift = [torch.roll(x_c, shift, 3) for x_c, shift in zip(xs, range(-self.pad, self.pad+1))]
        x_cat = torch.cat(x_shift, 1)
        x_cat = torch.narrow(x_cat, 2, self.pad, H)
        x_s = torch.narrow(x_cat, 3, self.pad, W)
        x_s = x_s.reshape(B,C,H*W).contiguous()
        x_shift_c = x_s.transpose(1,2)

        x = self.fc2(x_shift_c)
        x = self.drop(x)
        return x

class shiftedBlock(nn.Module):
    def __init__(self, dim, num_heads, mlp_ratio=4., qkv_bias=False, qk_scale=None, drop=0., attn_drop=0.,
                 drop_path=0., act_layer=nn.GELU, norm_layer=nn.LayerNorm, sr_ratio=1):
        super().__init__()


        self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
        self.norm2 = norm_layer(dim)
        mlp_hidden_dim = int(dim * mlp_ratio)
        self.mlp = shiftmlp(in_features=dim, hidden_features=mlp_hidden_dim, act_layer=act_layer, drop=drop)
        self.apply(self._init_weights)

    def _init_weights(self, m):
        if isinstance(m, nn.Linear):
            trunc_normal_(m.weight, std=.02)
            if isinstance(m, nn.Linear) and m.bias is not None:
                nn.init.constant_(m.bias, 0)
        elif isinstance(m, nn.LayerNorm):
            nn.init.constant_(m.bias, 0)
            nn.init.constant_(m.weight, 1.0)
        elif isinstance(m, nn.Conv2d):
            fan_out = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
            fan_out //= m.groups
            m.weight.data.normal_(0, math.sqrt(2.0 / fan_out))
            if m.bias is not None:
                m.bias.data.zero_()

    def forward(self, x, H, W):

        x = x + self.drop_path(self.mlp(self.norm2(x), H, W))
        return x

Vision MLP —Swin-MLP

code:https://github.com/microsoft/Swin-Transformer

Swin MLP 代码来自 Swin Transformer 的官方实现。Swin Transformer 作者们在已有模型的基础上实现了 Swin MLP 模型,证明了 Window-based attention 对于 MLP 模型的有效性。

把张量 (B, H, W, C) 分成 window (B×H/M×W/M, M, M, C),其中M是 window_size。这一步相当于得到 B×H/M×W/M 个大小为 (M, M, C) 的 window。

def window_partition(x, window_size):
    """
    Args:
        x: (B, H, W, C)
        window_size (int): window size

    Returns:
        windows: (num_windows*B, window_size, window_size, C)
    """
    B, H, W, C = x.shape
    x = x.view(B, H // window_size, window_size, W // window_size, window_size, C)
    windows = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(-1, window_size, window_size, C)
    return windows

把 window (B×H/M×W/M, M, M, C) 变回张量 (B, H, W, C)。

def window_reverse(windows, window_size, H, W):
    """
    Args:
        windows: (num_windows*B, window_size, window_size, C)
        window_size (int): Window size
        H (int): Height of image
        W (int): Width of image

    Returns:
        x: (B, H, W, C)
    """
    B = int(windows.shape[0] / (H * W / window_size / window_size))
    x = windows.view(B, H // window_size, W // window_size, window_size, window_size, -1)
    x = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(B, H, W, -1)
    return x

一个 Swin MLP Block

class SwinMLPBlock(nn.Module):
    r""" Swin MLP Block.

    Args:
        dim (int): Number of input channels.
        input_resolution (tuple[int]): Input resolution.
        num_heads (int): Number of attention heads.
        window_size (int): Window size.
        shift_size (int): Shift size for SW-MSA.
        mlp_ratio (float): Ratio of mlp hidden dim to embedding dim.
        drop (float, optional): Dropout rate. Default: 0.0
        drop_path (float, optional): Stochastic depth rate. Default: 0.0
        act_layer (nn.Module, optional): Activation layer. Default: nn.GELU
        norm_layer (nn.Module, optional): Normalization layer.  Default: nn.LayerNorm
    """

    def __init__(self, dim, input_resolution, num_heads, window_size=7, shift_size=0,
                 mlp_ratio=4., drop=0., drop_path=0.,
                 act_layer=nn.GELU, norm_layer=nn.LayerNorm):
        super().__init__()
        self.dim = dim
        self.input_resolution = input_resolution
        self.num_heads = num_heads
        self.window_size = window_size
        self.shift_size = shift_size
        self.mlp_ratio = mlp_ratio
        if min(self.input_resolution) <= self.window_size:
            # if window size is larger than input resolution, we don't partition windows
            self.shift_size = 0
            self.window_size = min(self.input_resolution)
        assert 0 <= self.shift_size < self.window_size, "shift_size must in 0-window_size"

        self.padding = [self.window_size - self.shift_size, self.shift_size,
                        self.window_size - self.shift_size, self.shift_size]  # P_l,P_r,P_t,P_b

        self.norm1 = norm_layer(dim)
        # use group convolution to implement multi-head MLP
        self.spatial_mlp = nn.Conv1d(self.num_heads * self.window_size ** 2,
                                     self.num_heads * self.window_size ** 2,
                                     kernel_size=1,
                                     groups=self.num_heads)

        self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
        self.norm2 = norm_layer(dim)
        mlp_hidden_dim = int(dim * mlp_ratio)
        self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim, act_layer=act_layer, drop=drop)

    def forward(self, x):
        H, W = self.input_resolution
        B, L, C = x.shape
        assert L == H * W, "input feature has wrong size"

        shortcut = x
        x = self.norm1(x)
        x = x.view(B, H, W, C)

        # shift
        if self.shift_size > 0:
            P_l, P_r, P_t, P_b = self.padding
            shifted_x = F.pad(x, [0, 0, P_l, P_r, P_t, P_b], "constant", 0)
        else:
            shifted_x = x
        _, _H, _W, _ = shifted_x.shape

        # partition windows
        x_windows = window_partition(shifted_x, self.window_size)  # nW*B, window_size, window_size, C
        x_windows = x_windows.view(-1, self.window_size * self.window_size, C)  # nW*B, window_size*window_size, C

        # Window/Shifted-Window Spatial MLP
        x_windows_heads = x_windows.view(-1, self.window_size * self.window_size, self.num_heads, C // self.num_heads)
        x_windows_heads = x_windows_heads.transpose(1, 2)  # nW*B, nH, window_size*window_size, C//nH
        x_windows_heads = x_windows_heads.reshape(-1, self.num_heads * self.window_size * self.window_size,
                                                  C // self.num_heads)
        spatial_mlp_windows = self.spatial_mlp(x_windows_heads)  # nW*B, nH*window_size*window_size, C//nH
        spatial_mlp_windows = spatial_mlp_windows.view(-1, self.num_heads, self.window_size * self.window_size,
                                                       C // self.num_heads).transpose(1, 2)
        spatial_mlp_windows = spatial_mlp_windows.reshape(-1, self.window_size * self.window_size, C)

        # merge windows
        spatial_mlp_windows = spatial_mlp_windows.reshape(-1, self.window_size, self.window_size, C)
        shifted_x = window_reverse(spatial_mlp_windows, self.window_size, _H, _W)  # B H' W' C

        # reverse shift
        if self.shift_size > 0:
            P_l, P_r, P_t, P_b = self.padding
            x = shifted_x[:, P_t:-P_b, P_l:-P_r, :].contiguous()
        else:
            x = shifted_x
        x = x.view(B, H * W, C)

        # FFN
        x = shortcut + self.drop_path(x)
        x = x + self.drop_path(self.mlp(self.norm2(x)))

        return x

    def extra_repr(self) -> str:
        return f"dim={self.dim}, input_resolution={self.input_resolution}, num_heads={self.num_heads}, " \
               f"window_size={self.window_size}, shift_size={self.shift_size}, mlp_ratio={self.mlp_ratio}"

注意 F.pad(x, [0, 0, P_l, P_r, P_t, P_b], “constant”, 0) 的对象是 x,维度是 (B, H, W, C)。
padding相当于是第3维 (C 这一维) 不填充,第2维 (W 这一维) 左右分别填充 P_l, P_r,第1维 (H 这一维) 左右分别填充 P_t, P_b。
x_windows = window_partition(shifted_x, self.window_size) # nW*B, window_size, window_size, C:
这句代码把 shifted_x 分成 nW*B 个 windows,其中每个 window 的维度是 (window_size, window_size, C)。

# reverse shift
if self.shift_size > 0:
P_l, P_r, P_t, P_b = self.padding
x = shifted_x[:, P_t:-P_b, P_l:-P_r, :].contiguous()
else:
x = shifted_x
这里是如果进行了 shift 操作,则最后取得结果也应该是没有 padding 的部分,正好是 shifted_x[:, P_t:-P_b, P_l:-P_r, :]。

一个 Swin MLP Block 的 FLOPs,注意 WSA 的计算量是:

FLOPs (WSA) = (window_size * window_size)^2 * dim * number_window

def flops(self):
        flops = 0
        H, W = self.input_resolution
        # norm1
        flops += self.dim * H * W

        # Window/Shifted-Window Spatial MLP
        if self.shift_size > 0:
            nW = (H / self.window_size + 1) * (W / self.window_size + 1)
        else:
            nW = H * W / self.window_size / self.window_size
        flops += nW * self.dim * (self.window_size * self.window_size) * (self.window_size * self.window_size)
        # mlp
        flops += 2 * H * W * self.dim * self.dim * self.mlp_ratio
        # norm2
        flops += self.dim * H * W
        return flops

每个 stage 之间的 PatchMerging连接,把 resolution 变为一半,dim 变为2倍。

class PatchMerging(nn.Module):
    r""" Patch Merging Layer.

    Args:
        input_resolution (tuple[int]): Resolution of input feature.
        dim (int): Number of input channels.
        norm_layer (nn.Module, optional): Normalization layer.  Default: nn.LayerNorm
    """

    def __init__(self, input_resolution, dim, norm_layer=nn.LayerNorm):
        super().__init__()
        self.input_resolution = input_resolution
        self.dim = dim
        self.reduction = nn.Linear(4 * dim, 2 * dim, bias=False)
        self.norm = norm_layer(4 * dim)

    def forward(self, x):
        """
        x: B, H*W, C
        """
        H, W = self.input_resolution
        B, L, C = x.shape
        assert L == H * W, "input feature has wrong size"
        assert H % 2 == 0 and W % 2 == 0, f"x size ({H}*{W}) are not even."

        x = x.view(B, H, W, C)

        x0 = x[:, 0::2, 0::2, :]  # B H/2 W/2 C
        x1 = x[:, 1::2, 0::2, :]  # B H/2 W/2 C
        x2 = x[:, 0::2, 1::2, :]  # B H/2 W/2 C
        x3 = x[:, 1::2, 1::2, :]  # B H/2 W/2 C
        x = torch.cat([x0, x1, x2, x3], -1)  # B H/2 W/2 4*C
        x = x.view(B, -1, 4 * C)  # B H/2*W/2 4*C

        x = self.norm(x)
        x = self.reduction(x)

        return x

    def flops(self):
        H, W = self.input_resolution
        # norm
        flops = H * W * self.dim
        # reduction
        flops += (H // 2) * (W // 2) * 4 * self.dim * 2 * self.dim
        return flops
  • Patch Merging 操作把相邻的 2×2 个 tokens 给合并到一起,得到的 token 的维度是4C。
    Patch Merging 操作再通过一次线性变换把维度降为2C。

一个 Swin MLP Layer

class BasicLayer(nn.Module):
    """ A basic Swin MLP layer for one stage.

    Args:
        dim (int): Number of input channels.
        input_resolution (tuple[int]): Input resolution.
        depth (int): Number of blocks.
        num_heads (int): Number of attention heads.
        window_size (int): Local window size.
        mlp_ratio (float): Ratio of mlp hidden dim to embedding dim.
        drop (float, optional): Dropout rate. Default: 0.0
        drop_path (float | tuple[float], optional): Stochastic depth rate. Default: 0.0
        norm_layer (nn.Module, optional): Normalization layer. Default: nn.LayerNorm
        downsample (nn.Module | None, optional): Downsample layer at the end of the layer. Default: None
        use_checkpoint (bool): Whether to use checkpointing to save memory. Default: False.
    """

    def __init__(self, dim, input_resolution, depth, num_heads, window_size,
                 mlp_ratio=4., drop=0., drop_path=0.,
                 norm_layer=nn.LayerNorm, downsample=None, use_checkpoint=False):

        super().__init__()
        self.dim = dim
        self.input_resolution = input_resolution
        self.depth = depth
        self.use_checkpoint = use_checkpoint

        # build blocks
        self.blocks = nn.ModuleList([
            SwinMLPBlock(dim=dim, input_resolution=input_resolution,
                         num_heads=num_heads, window_size=window_size,
                         shift_size=0 if (i % 2 == 0) else window_size // 2,
                         mlp_ratio=mlp_ratio,
                         drop=drop,
                         drop_path=drop_path[i] if isinstance(drop_path, list) else drop_path,
                         norm_layer=norm_layer)
            for i in range(depth)])

        # patch merging layer
        if downsample is not None:
            self.downsample = downsample(input_resolution, dim=dim, norm_layer=norm_layer)
        else:
            self.downsample = None

    def forward(self, x):
        for blk in self.blocks:
            if self.use_checkpoint:
                x = checkpoint.checkpoint(blk, x)
            else:
                x = blk(x)
        if self.downsample is not None:
            x = self.downsample(x)
        return x

    def extra_repr(self) -> str:
        return f"dim={self.dim}, input_resolution={self.input_resolution}, depth={self.depth}"

    def flops(self):
        flops = 0
        for blk in self.blocks:
            flops += blk.flops()
        if self.downsample is not None:
            flops += self.downsample.flops()
        return flops
  • 包含 depth 个 Swin MLP Block。
    注意计算 FLOPs 的方式:每个 blk 和 downsample 都自带 flops() 方法,可以直接来调用。

PatchEmbedded 操作

class PatchEmbed(nn.Module):
    r""" Image to Patch Embedding

    Args:
        img_size (int): Image size.  Default: 224.
        patch_size (int): Patch token size. Default: 4.
        in_chans (int): Number of input image channels. Default: 3.
        embed_dim (int): Number of linear projection output channels. Default: 96.
        norm_layer (nn.Module, optional): Normalization layer. Default: None
    """

    def __init__(self, img_size=224, patch_size=4, in_chans=3, embed_dim=96, norm_layer=None):
        super().__init__()
        img_size = to_2tuple(img_size)
        patch_size = to_2tuple(patch_size)
        patches_resolution = [img_size[0] // patch_size[0], img_size[1] // patch_size[1]]
        self.img_size = img_size
        self.patch_size = patch_size
        self.patches_resolution = patches_resolution
        self.num_patches = patches_resolution[0] * patches_resolution[1]

        self.in_chans = in_chans
        self.embed_dim = embed_dim

        self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size)
        if norm_layer is not None:
            self.norm = norm_layer(embed_dim)
        else:
            self.norm = None

    def forward(self, x):
        B, C, H, W = x.shape
        # FIXME look at relaxing size constraints
        assert H == self.img_size[0] and W == self.img_size[1], \
            f"Input image size ({H}*{W}) doesn't match model ({self.img_size[0]}*{self.img_size[1]})."
        x = self.proj(x).flatten(2).transpose(1, 2)  # B Ph*Pw C
        if self.norm is not None:
            x = self.norm(x)
        return x

    def flops(self):
        Ho, Wo = self.patches_resolution
        flops = Ho * Wo * self.embed_dim * self.in_chans * (self.patch_size[0] * self.patch_size[1])
        if self.norm is not None:
            flops += Ho * Wo * self.embed_dim
        return flops
  • 和 ViT 的 Patch Embedded 操作一样,本质上是一个 K=patch size,s=patch size 的 nn.Conv2d 操作,注意卷积 FLOPs 的计算公式即可。

SwinMLP 整体模型架构

class SwinMLP(nn.Module):
    r""" Swin MLP

    Args:
        img_size (int | tuple(int)): Input image size. Default 224
        patch_size (int | tuple(int)): Patch size. Default: 4
        in_chans (int): Number of input image channels. Default: 3
        num_classes (int): Number of classes for classification head. Default: 1000
        embed_dim (int): Patch embedding dimension. Default: 96
        depths (tuple(int)): Depth of each Swin MLP layer.
        num_heads (tuple(int)): Number of attention heads in different layers.
        window_size (int): Window size. Default: 7
        mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. Default: 4
        drop_rate (float): Dropout rate. Default: 0
        drop_path_rate (float): Stochastic depth rate. Default: 0.1
        norm_layer (nn.Module): Normalization layer. Default: nn.LayerNorm.
        ape (bool): If True, add absolute position embedding to the patch embedding. Default: False
        patch_norm (bool): If True, add normalization after patch embedding. Default: True
        use_checkpoint (bool): Whether to use checkpointing to save memory. Default: False
    """

    def __init__(self, img_size=224, patch_size=4, in_chans=3, num_classes=1000,
                 embed_dim=96, depths=[2, 2, 6, 2], num_heads=[3, 6, 12, 24],
                 window_size=7, mlp_ratio=4., drop_rate=0., drop_path_rate=0.1,
                 norm_layer=nn.LayerNorm, ape=False, patch_norm=True,
                 use_checkpoint=False, **kwargs):
        super().__init__()

        self.num_classes = num_classes
        self.num_layers = len(depths)
        self.embed_dim = embed_dim
        self.ape = ape
        self.patch_norm = patch_norm
        self.num_features = int(embed_dim * 2 ** (self.num_layers - 1))
        self.mlp_ratio = mlp_ratio

        # split image into non-overlapping patches
        self.patch_embed = PatchEmbed(
            img_size=img_size, patch_size=patch_size, in_chans=in_chans, embed_dim=embed_dim,
            norm_layer=norm_layer if self.patch_norm else None)
        num_patches = self.patch_embed.num_patches
        patches_resolution = self.patch_embed.patches_resolution
        self.patches_resolution = patches_resolution

        # absolute position embedding
        if self.ape:
            self.absolute_pos_embed = nn.Parameter(torch.zeros(1, num_patches, embed_dim))
            trunc_normal_(self.absolute_pos_embed, std=.02)

        self.pos_drop = nn.Dropout(p=drop_rate)

        # stochastic depth
        dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))]  # stochastic depth decay rule

        # build layers
        self.layers = nn.ModuleList()
        for i_layer in range(self.num_layers):
            layer = BasicLayer(dim=int(embed_dim * 2 ** i_layer),
                               input_resolution=(patches_resolution[0] // (2 ** i_layer),
                                                 patches_resolution[1] // (2 ** i_layer)),
                               depth=depths[i_layer],
                               num_heads=num_heads[i_layer],
                               window_size=window_size,
                               mlp_ratio=self.mlp_ratio,
                               drop=drop_rate,
                               drop_path=dpr[sum(depths[:i_layer]):sum(depths[:i_layer + 1])],
                               norm_layer=norm_layer,
                               downsample=PatchMerging if (i_layer < self.num_layers - 1) else None,
                               use_checkpoint=use_checkpoint)
            self.layers.append(layer)

        self.norm = norm_layer(self.num_features)
        self.avgpool = nn.AdaptiveAvgPool1d(1)
        self.head = nn.Linear(self.num_features, num_classes) if num_classes > 0 else nn.Identity()

        self.apply(self._init_weights)

    def _init_weights(self, m):
        if isinstance(m, (nn.Linear, nn.Conv1d)):
            trunc_normal_(m.weight, std=.02)
            if m.bias is not None:
                nn.init.constant_(m.bias, 0)
        elif isinstance(m, nn.LayerNorm):
            nn.init.constant_(m.bias, 0)
            nn.init.constant_(m.weight, 1.0)

    @torch.jit.ignore
    def no_weight_decay(self):
        return {'absolute_pos_embed'}

    @torch.jit.ignore
    def no_weight_decay_keywords(self):
        return {'relative_position_bias_table'}

    def forward_features(self, x):
        x = self.patch_embed(x)
        if self.ape:
            x = x + self.absolute_pos_embed
        x = self.pos_drop(x)

        for layer in self.layers:
            x = layer(x)

        x = self.norm(x)  # B L C
        x = self.avgpool(x.transpose(1, 2))  # B C 1
        x = torch.flatten(x, 1)
        return x

    def forward(self, x):
        x = self.forward_features(x)
        x = self.head(x)
        return x

    def flops(self):
        flops = 0
        flops += self.patch_embed.flops()
        for i, layer in enumerate(self.layers):
            flops += layer.flops()
        # adaptive average pool
        flops += self.num_features * self.patches_resolution[0] * self.patches_resolution[1] // (2 ** self.num_layers)
        # head
        flops += self.num_features * self.num_classes
        return flops
  • 由4个 Stage 组成,每个 Stage 由 BasicLayer 实现。
    传入的 depths 代表每个 Stage 的层数,比如 Swin-T 就是:[2, 2, 6, 2]。

经典CNN网络结构

回顾21世纪10年代,深度学习取得了巨大的进步,产生了巨大的影响。主要驱动力是神经网络的复兴,特别是卷积神经网络(ConvNets)。十年来,视觉识别领域成功地从工程特征转变为设计(ConvNet)架构。尽管采用反向传播训练方法的卷积神经网络自上世纪八十年代已经发明了,但直到2012年我们才看到它作为视觉特征学习的真正潜力。AlexNet的引入促成了“ImageNet的时刻”,引领了计算机视觉领域的一个新时代,这个领域因此而快速演化。具有代表性的网络有VGGNet、Inceptions、Resnet、DenseNet、MobileNet、EfficientNet、RegNet等,它们分别关注精度、效率、可扩展性等方面,并且普及了很多有用的设计原则。

1、 VGGNet

VGG16相比AlexNet的一个改进是采用连续的几个3×3的卷积核代替AlexNet中的较大卷积核(11×11,7×7,5×5)。对于给定的感受野(与输出有关的输入图片的局部大小),采用堆积的小卷积核是优于采用大的卷积核,因为多层非线性层可以增加网络深度来保证学习更复杂的模式,而且代价还比较小(参数更少)。

简单来说,在VGG中,使用了3个3×3卷积核来代替7×7卷积核,使用了2个3×3卷积核来代替5*5卷积核,这样做的主要目的是在保证具有相同感知野的条件下,提升了网络的深度,在一定程度上提升了神经网络的效果。

网络结构:

2、 Inception系列网络

Inception V1

在这之前,网络大都是这样子的:

也就是卷积层和池化层的顺序连接。这样的话,要想提高精度,增加网络深度和宽度是一个有效途径,但也面临着参数量过多、过拟合等问题。(当然,改改超参数也可以提高性能)

有没有可能在同一层就可以提取不同(稀疏或不稀疏)的特征呢(使用不同尺寸的卷积核)?于是,2014年,在其他人都还在一味的增加网络深度时(比如vgg),GoogleNet就率先提出了卷积核的并行合并(也称Bottleneck Layer),如下图。

和卷积层、池化层顺序连接的结构(如VGG网络)相比,这样的结构主要有以下改进:

  1. 一层block就包含1×1卷积,3×3卷积,5×5卷积,3×3池化(使用这样的尺寸不是必需的,可以根据需要进行调整)。这样,网络中每一层都能学习到“稀疏”(3×3、5×5)或“不稀疏”(1×1)的特征,既增加了网络的宽度,也增加了网络对尺度的适应性;
  2. 通过deep concat在每个block后合成特征,获得非线性属性。

按照这样的结构来增加网络的深度,虽然可以提升性能,但是还面临计算量大(参数多)的问题。为改善这种现象,GooLeNet借鉴Network-in-Network的思想,使用1×1的卷积核实现降维操作(也间接增加了网络的深度),以此来减小网络的参数量(这里就不对两种结构的参数量进行定量比较了),如图所示。

最后实现的inception v1网络是上图结构的顺序连接,其中不同inception模块之间使用2×2的最大池化进行下采样,如表所示。

如表所示,实现的网络仍有一层全连接层,该层的设置是为了迁移学习的实现(下同)。

在之前的网络中,最后都有全连接层,经实验证明,全连接层并不是很必要的,因为可能会带来以下三点不便:

  • 网络的输入需要固定
  • 参数量多
  • 易发生过拟合

实验证明,将其替换为平均池化层(或者1×1卷积层)不仅不影响精度,还可以减少参数量。

此外,实验室的小伙伴最近做了下实验,如果是小目标检测的话,网络的最后还是需要几层全连接层的,猜想可能是用池化的话会损失太多信息,毕竟是小目标。

———————————————————————————————–

Inception V2和Inception V3的改进,主要是基于V3论文中提到的四个原则:

  1. 避免表示瓶颈,尤其是在网络的前面。一般来说,特征图从输入到输出应该缓慢减小。
  2. 高维度特征在网络局部处理更加容易。考虑到更多的耦合特征,在卷积网络中增加非线性。可以让网络训练更快。
  3. 空间聚合可以以低维度嵌入进行,这样不会影响特征的表达能力。如,在进行大尺度卷积之前,先对输入进行降维。
  4. 平衡网络的宽度和深度。增加宽度和深度都会带来性能上的提升,两者同时增加带来了并行提升,但是要考虑计算资源的合理分配。

———————————————————————————————–

Inception v2

注意,这里实现的inception v2的结构是在inception v3论文中有介绍)

2015年Google团队又提出了inception v2的结构,基于上面提到的一些原则,在V1的基础之上主要做了以下改进:

⑴ 使用BN层,将每一层的输出都规范化到一个N(0,1)的正态分布,这将有助于训练,因为下一层不必学习输入数据中的偏移,并且可以专注与如何更好地组合特征(也因为在v2里有较好的效果,BN层几乎是成了深度网络的必备);

(在Batch-normalized论文中只增加了BN层,而之后的Inception V3的论文提及到的inception v2还做了下面的优化)

⑵ 使用2个3×3的卷积代替梯度(特征图,下同)为35×35中的5×5的卷积,这样既可以获得相同的视野(经过2个3×3卷积得到的特征图大小等于1个5×5卷积得到的特征图),还具有更少的参数,还间接增加了网络的深度,如下图。(基于原则3

figure5

⑶ 3×3的卷积核表现的不错,那更小的卷积核是不是会更好呢?比如2×2。对此,v2在17×17的梯度中使用1*n和n*1这种非对称的卷积来代替n*n的对称卷积,既降低网络的参数,又增加了网络的深度(实验证明,该结构放于网络中部,取n=7,准确率更高),如下。(基于原则3

figure6

⑷ 在梯度为8×8时使用可以增加滤波器输出的模块(如下图),以此来产生高维的稀疏特征。(基于原则2

(原则2指出,在高维特征上,采用这种结构更好,因此该模块用在了8×8的梯度上)

figure7

⑸ 输入从224×224变为229×229。

最后实现的Inception v2的结构如下表。

经过网络的改进,inception v2得到更低的识别误差率,与其他网络识别误差率对比如表所示。

如表,inception v2相比inception v1在imagenet的数据集上,识别误差率由29%降为23.4%。

Inception v3

inception模块之间特征图的缩小,主要有下面两种方式:

右图是先进行inception操作,再进行池化来下采样,但是这样参数量明显多于左图(比较方式同前文的降维后inception模块),因此v2采用的是左图的方式,即在不同的inception之间(35/17/8的梯度)采用池化来进行下采样。

但是,左图这种操作会造成表达瓶颈问题,也就是说特征图的大小不应该出现急剧的衰减(只经过一层就骤降)。如果出现急剧缩减,将会丢失大量的信息,对模型的训练造成困难。(上文提到的原则1

因此,在2015年12月提出的Inception V3结构借鉴inception的结构设计了采用一种并行的降维结构,如下图:

具体来说,就是在35/17/8之间分别采用下面这两种方式来实现特征图尺寸的缩小,如下图:

figure 5′ 35/17之间的特征图尺寸减小
figure 6′ 17/8之间的特征图尺寸缩小

这样就得到Inception v3的网络结构,如表所示。

inception v3

经过优化后的inception v3网络与其他网络识别误差率对比如表所示。

如表所示,在144×144的输入上,inception v3的识别错误率由v1的7.89%降为了4.2%。

此外,文章还提到了中间辅助层,即在网络中部再增加一个输出层。实验发现,中间辅助层在训练前期影响不大,而在训练后期却可以提高精度,相当于正则项。

Inception V4

其实,做到现在,inception模块感觉已经做的差不多了,再做下去准确率应该也不会有大的改变。但是谷歌这帮人还是不放弃,非要把一个东西做到极致,改变不了inception模块,就改变其他的。

因此,作者Christian Szegedy设计了inception v4的网络,将原来卷积、池化的顺次连接(网络的前几层)替换为stem模块,来获得更深的网络结构。stem模块结构如下

stem模块

stem之后的,同v3,是inception模块和reduction模块,如下图

inception v4 中的inception模块(分别为inception A inception B inception C)
inception v4中的reduction模块(分别为reduction A reduction B)

最终得到的inception v4结构如下图。

Inception-ResNet-v2

ResNet(该网络介绍见卷积神经网络结构简述(三)残差系列网络)的结构既可以加速训练,还可以提升性能(防止梯度弥散);Inception模块可以在同一层上获得稀疏或非稀疏的特征。有没有可能将两者进行优势互补呢?

Christian Szegedy等人将两个模块的优势进行了结合,设计出了Inception-ResNet网络。

(inception-resnet有v1和v2两个版本,v2表现更好且更复杂,这里只介绍了v2)

inception-resnet的成功,主要是它的inception-resnet模块。

inception-resnet v2中的Inception-resnet模块如下图

Inception-resnet模块(分别为inception-resnet-A inception-resnet-B inception-resnet-C)

Inception-resnet模块之间特征图尺寸的减小如下图。(类似于inception v4)

inception-resnet-v2中的reduction模块(分别为reduction A reduction B)

最终得到的Inception-ResNet-v2网络结构如图(stem模块同inception v4)。

经过这两种网络的改进,使得模型对图像识别的错误率进一步得到了降低。Inception、resnet网络结果对比如表所示。

如表,Inception V4与Inception-ResNet-v2网络较之前的网络,误差率均有所下降。

3、Resnet

ResNet网络是在2015年由微软实验室提出,斩获当年ImageNet竞赛中分类任务第一名,目标检测第一名。获得COCO数据集中目标检测第一名,图像分割第一名。下图是ResNet34层模型的结构简图。

在ResNet网络中有如下几个亮点:

(1)提出residual结构(残差结构),并搭建超深的网络结构(突破1000层)

(2)使用Batch Normalization加速训练(丢弃dropout)

在ResNet网络提出之前,传统的卷积神经网络都是通过将一系列卷积层与下采样层进行堆叠得到的。但是当堆叠到一定网络深度时,就会出现两个问题。1)梯度消失或梯度爆炸。 2)退化问题(degradation problem)。在ResNet论文中说通过数据的预处理以及在网络中使用BN(Batch Normalization)层能够解决梯度消失或者梯度爆炸问题。如果不了解BN层可参考这个链接。但是对于退化问题(随着网络层数的加深,效果还会变差,如下图所示)并没有很好的解决办法。

4、DenseNet

DenseNet模型,它的基本思路与ResNet一致,但是它建立的是前面所有层与后面层的密集连接(dense connection),它的名称也是由此而来。DenseNet的另一大特色是通过特征在channel上的连接来实现特征重用(feature reuse)。这些特点让DenseNet在参数和计算成本更少的情形下实现比ResNet更优的性能,DenseNet也因此斩获CVPR 2017的最佳论文奖.

相比ResNet,DenseNet提出了一个更激进的密集连接机制:即互相连接所有的层,具体来说就是每个层都会接受其前面所有层作为其额外的输入。图1为ResNet网络的连接机制,作为对比,图2为DenseNet的密集连接机制。可以看到,ResNet是每个层与前面的某层(一般是2~3层)短路连接在一起,连接方式是通过元素级相加。而在DenseNet中,每个层都会与前面所有层在channel维度上连接(concat)在一起(这里各个层的特征图大小是相同的),并作为下一层的输入。

图1 ResNet网络的短路连接机制(其中+代表的是元素级相加操作)
图2 DenseNet网络的密集连接机制(其中c代表的是channel级连接操作)
 DenseNet的网络结构

5、MobileNet

MobileNet的基本单元是深度级可分离卷积(depthwise separable convolution),其实这种结构之前已经被使用在Inception模型中。深度级可分离卷积其实是一种可分解卷积操作(factorized convolutions),其可以分解为两个更小的操作:depthwise convolution和pointwise convolution,如图1所示。Depthwise convolution和标准卷积不同,对于标准卷积其卷积核是用在所有的输入通道上(input channels),而depthwise convolution针对每个输入通道采用不同的卷积核,就是说一个卷积核对应一个输入通道,所以说depthwise convolution是depth级别的操作。而pointwise convolution其实就是普通的卷积,只不过其采用1×1的卷积核。图2中更清晰地展示了两种操作。对于depthwise separable convolution,其首先是采用depthwise convolution对不同输入通道分别进行卷积,然后采用pointwise convolution将上面的输出再进行结合,这样其实整体效果和一个标准卷积是差不多的,但是会大大减少计算量和模型参数量。

前面讲述了depthwise separable convolution,这是MobileNet的基本组件,但是在真正应用中会加入batchnorm,并使用ReLU激活函数,所以depthwise separable convolution的基本结构如图3所示。

加入BN和ReLU的depthwise separable convolution

MobileNet的网络结构如表1所示。首先是一个3×3的标准卷积,然后后面就是堆积depthwise separable convolution,并且可以看到其中的部分depthwise convolution会通过strides=2进行down sampling。然后采用average pooling将feature变成1×1,根据预测类别大小加上全连接层,最后是一个softmax层。如果单独计算depthwise
convolution和pointwise convolution,整个网络有28层(这里Avg Pool和Softmax不计算在内)。我们还可以分析整个网络的参数和计算量分布,如表2所示。可以看到整个计算量基本集中在1×1卷积上,如果你熟悉卷积底层实现的话,你应该知道卷积一般通过一种im2col方式实现,其需要内存重组,但是当卷积核为1×1时,其实就不需要这种操作了,底层可以有更快的实现。对于参数也主要集中在1×1卷积,除此之外还有就是全连接层占了一部分参数。

MobileNetv2

MobileNetv2相比v1的两个主要改进:linear bottleneck和inverted residual。

v2的加入了1×1升维,引入Shortcut并且去掉了最后的ReLU,改为Linear。步长为1时,先进行1×1卷积升维,再进行深度卷积提取特征,再通过Linear的逐点卷积降维。将input与output相加,形成残差结构。步长为2时,因为input与output的尺寸不符,因此不添加shortcut结构,其余均一致。

preview

首先利用3×3的深度可分离卷积提取特征,然后利用1×1的卷积来扩张通道。用这样的block堆叠起来的MobileNetV1既能较少不小的参数量、计算量,提高网络运算速度,又能的得到一个接近于标准卷积的还不错的结果,看起来是很美好的。

但是!

有人在实际使用的时候, 发现深度卷积部分的卷积核比较容易训废掉:训完之后发现深度卷积训出来的卷积核有不少是空的.

这是为什么?

作者认为这是ReLU这个浓眉大眼的激活函数的锅。(没想到你个浓眉大眼的ReLU激活函数也叛变革命了???)

针对这个问题,可以这样解决:既然是ReLU导致的信息损耗,将ReLU替换成线性激活函数。我们当然不能把所有的激活层都换成线性的啊,所以我们就悄咪咪的把最后的那个ReLU6换成Linear。作者将这个部分称之为linear bottleneck。对,就是论文名中的那个linear bottleneck。

现在还有个问题是,深度卷积本身没有改变通道的能力,来的是多少通道输出就是多少通道。如果来的通道很少的话,DW深度卷积只能在低维度上工作,这样效果并不会很好,所以我们要“扩张”通道。既然我们已经知道PW逐点卷积也就是1×1卷积可以用来升维和降维,那就可以在DW深度卷积之前使用PW卷积进行升维(升维倍数为t,t=6),再在一个更高维的空间中进行卷积操作来提取特征

也就是说,不管输入通道数是多少,经过第一个PW逐点卷积升维之后,深度卷积都是在相对的更高6倍维度上进行工作。

回顾V1的网络结构,我们发现V1很像是一个直筒型的VGG网络。我们想像Resnet一样复用我们的特征,所以我们引入了shortcut结构,这样V2的block就是如下图形式:

对比一下V1和V2:

可以发现,都采用了 1×1 -> 3 ×3 -> 1 × 1 的模式,以及都使用Shortcut结构。但是不同点呢:

  • ResNet 先降维 (0.25倍)、卷积、再升维。
  • MobileNetV2 则是 先升维 (6倍)、卷积、再降维。

刚好V2的block刚好与Resnet的block相反,作者将其命名为Inverted residuals。就是论文名中的Inverted residuals

6、EfficientNet

该论文提出了一种新的模型缩放方法,它使用一个简单而高效的复合系数来以更结构化的方式放大 CNNs。 不像传统的方法那样任意缩放网络维度,如宽度,深度和分辨率,该论文的方法用一系列固定的尺度缩放系数来统一缩放网络维度。 通过使用这种新颖的缩放方法和 AutoML[5] 技术,作者将这种模型称为 EfficientNets ,它具有最高达10倍的效率(更小、更快)。

模型扩展的有效性在很大程度上依赖于baseline网络。为了进一步提高性能,作者还开发了一个新的基线网络,通过使用 AutoML MNAS 框架执行神经结构搜索,优化了准确性和效率。 最终的架构使用移动反向bottleneck卷积(MBConv) ,类似于 mobileenetv2和 MnasNet。

移动翻转瓶颈卷积(mobile inverted bottleneck convolution,MBConv),类似于 MobileNetV2 和 MnasNet,由深度可分离卷积Depthwise Convolution和SENet构成。

每个MBConv的网络结构如下:
MBConv = 1×1升维 + Depthwise Convolution + SENet + 1×1降维 + add
在这里插入图片描述

SENet

该网络为压缩与激发网络(Squeeze-and-Excitation Network,SENet),即注意力机制。该思想由Momenta公司提出,并发于2017CVPR。SENet网络的创新点在于关注channel之间的关系,希望模型可以自动学习到不同channel特征的重要程度。

其中第一个FC层降维,降维系数为r,然后ReLU激活,最后的FC层恢复原始的维度。
在这里插入图片描述
SENet添加位置示意:
在这里插入图片描述
代码实现

7、RegNet

本文的基本贡献有三方面。

  • 提出了设计空间的设计原则。
  • 根据这些原则,一个有效的设计空间被引入(RegNet)
  • 介绍了一组SoTA网络(RegNetX和RegNetY)。

我们首先设计了一个 AnyNet,它包含三个部分

  1. Stem 一个简单的网络输入头
  2. body 网络中主要的运算量都在这里
  3. head 用于预测分类的输出头
模型网络结构设计

我们将 stem 和 head 固定下来,并专注于网络 body 设计。因为 body 部分的参数量最多,运算量也多,这部分是决定网络准确性的关键

而 Body 结构,通常包含 4 个 stage,每个 stage 都会进行降采样。而 1 个 stage 是由多个 block 进行堆叠得到的

论文中,我们的 Block 采取的是带有组卷积的残差 BottleNeck Block(即 ResNext 里的结构),我们称在这样 Block 限制条件下的搜索空间为 AnyNetX ,Block 的结构如下:

带有组卷积的残差BottleNeck Block

此时 AnyNetX 中有 16 个自由度可以设计,包含了 4 个 stage,每个 stage 有 4 个 Block 参数:

  • block 的数目 di
  • block 的宽度 wi
  • Bottleneck 的通道缩放比例 bi
  • 分组数目 gi

此时我们在这样的条件下进行采样,缩小网络设计空间:

  • di ≤ 16
  • wi ≤ 1024 (其中 wi 可被 8 整除)
  • bi ∈ {1, 2, 4}
  • gi ∈ {1, 2, . . . , 32}

因此我们在 AnyNetX 衍生出其他搜索空间

  • AnyNetXa 就是原始的 AnyNetX
  • AnyNetXb 在 AnyNetX 基础上,每个 stage 使用相同的 Bottleneck 缩放比例 bi。并且实验得出缩放比例 bi <= 2 时最佳,参考下图最右边子图
对AnyNetXb和AnyNetXc的分析
  • AnyNetXc 在 AnyNetXb 的基础上共享相同的分组数目 gi。由上图的左图和中间图可得知,从 A->C 的阶段,EDF 并没有受到影响,而我们此时已经减少了
  • AnyNetXd 在 AnyNetXc 的基础上逐步增加 Block 的宽度 wi。此时网络性能有明显提升!

AnyNetXd

  • AnyNetXe 在 AnyNetXd 的基础上在除了最后一个 stage 上,逐步增加 Block 的数量(深度)di。网络性能略微有提升
AnyNetXe

Swin Transformer v2

paper:https://arxiv.org/pdf/2111.09883.pdf

Swin Transformer V2: Scaling Up Capacity and Resolution扩展容量和分辨率

Transformer 是 Google 的团队在 2017 年提出的一种 NLP 经典模型,现在比较火热的 Bert 也是基于 Transformer。Transformer 模型使用了 Self-Attention 机制,不采用 RNN 的顺序结构,使得模型可以并行化训练,而且能够拥有全局信息。

本文介绍这篇文章是 Swin Transformer 系列的升级版 Swin Transformer v2。Swin Transformer 是屠榜各大CV任务的通用视觉Transformer模型,它在图像分类、目标检测、分割上全面超越 SOTA,在语义分割任务中在 ADE20K 上刷到 53.5 mIoU,超过之前 SOTA 大概 4.5 mIoU!可能是CNN的完美替代方案。除此之外,本文一并介绍 Swin MLP 的代码实现,Swin Transformer 作者们在已有模型的基础上实现了 Swin MLP 模型,证明了 Window-based attention 对于 MLP 模型的有效性。

Swin Transformer Block 有两种,大致结构和 Transformer Block 一致,只是内部 attention 模块分别是 Window-based MSA 和 Shifted Window-based MSA。Window-based MSA 不同于普通的 MSA,它在一个个 window 里面去计算 self-attention,计算量与序列长度 N=hw 成线性关系。Window-based MSA 虽然大幅节约了计算量,但是牺牲了 windows 之间关系的建模,不重合的 Window 之间缺乏信息交流影响了模型的表征能力。Shifted Window-based MSA 就是为了解决这个问题。将下一层 Swin Transformer Block 的 Window 位置进行移动,得到不重合的 patch。

在 Swin Transformer 的基础上,研究人员进一步开发出了用于底层复原任务的 SwinIR

Swin Transformer v2 原理分析:

Swin Transformer 提出了一种针对视觉任务的通用的 Transformer 架构,MSRA 进一步打造了一个包含3 billion 个参数,且允许输入分辨率达到1560×1560的大型 Swin Transformer,称之为 SwinV2。它在多个基准数据集 (包含 ImageNet 分类、COCO 检测、ADE20K 语义分割以及Kinetics-400 动作分类) 上取得新记录,分别是 ImageNet 图像分类84.0% Top-1 accuracy,COCO 目标检测63.1/54.4 box / mask mAP,ADE20K 语义分割59.9mIoU,Kinetics-400视频动作识别86.8% Top-1 accuracy。

Swin Transformer v2 的核心目的是把 Swin Transformer 模型做大,做成类似 BERT large 那样包含 340M 参数的预训练大模型。在 NLP 中,有的预训练的大模型,比如 Megatron-Turing-530B 或者 Switch-Transformer-1.6T,参数量分别达到了530 billion 或者1.6 trillion。

另一方面,视觉大模型的发展却滞后了。 Vision Transformer 的大模型目前也只是达到了1-2 billion 的参数量,且只支持图像识别任务。部分原因是因为在训练和部署方面存在以下困难:

  • 问题1:训练中的不稳定性问题。在大型模型中,跨层激活函数输出的幅值的差异变得更大。激活值是逐层累积的,因此深层的幅值明显大于浅层的幅值。如下图1所示是扩大模型容量时的不稳定问题。 当我们将原来的 Swin Transformer 模型从小模型放大到大模型时,深层的 activation 值急剧增加。最高和最低幅值之间的差异达到了104。当我们进一步扩展到一个巨大的规模 (658M 参数) 时,它不能完成训练,如图2所示。
图1:扩大模型容量时的不稳定问题
图2:使用 Pre-Norm,当进一步扩展到一个巨大的规模 (658M 参数) 时不能完成训练。
  • 问题2:许多下游视觉任务需要高分辨率的图像或窗口,预训练模型时是在低分辨率下进行的,而 fine-tuning 是在高分辨率下进行的。针对分辨率不同的问题传统的做法是把位置编码进行双线性插值 (bi-cubic interpolation),这种做法是次优的。如下图3所示是不同位置编码方式性能的比较,当我们直接在较大的图像分辨率和窗口大小测试预训练的 Imagenet-1k 模型 (分辨率256×256,window siez=8×8) 时,发现精度显着下降。
图3:不同位置编码方式性能的比较
  • 问题3:当图像分辨率较高时,GPU 内存消耗也是一个问题。

为了解决以上几点问题,作者提出了:

方法1:post normalization 技术:解决训练中的不稳定性问题

把 Layer Normalization 层放在 Attention 或者 MLP 的后面。这样每个残差块的输出变化不至于太大,因为主分支和残差分支都是 LN 层的输出,有 LN 归一化作用的限制。如上图1所示,这种做法使得每一层的输出值基本上相差不大。在最大的模型训练中,作者每经过6个 Transformer Block,就在主支路上增加了一层 LN,以进一步稳定训练和输出幅值。

图4:Swin v2 相对于 Swin Transformer 的改进 (红色部分)

方法2:scaled cosine attention 技术:解决训练中的不稳定性问题

原来的 self-attention 计算中,query 和 key 之间的相似性通过 dot-product 来衡量,作者发现这样学习到的 attention map 往往被少数像素对所支配。所以把 dot-product 改成了 cosine 函数,通过它来衡量 query 和 key 之间的相似性。

\[\operatorname{Sim}\left(\mathbf{q}i, \mathbf{k}_j\right)=\cos \left(\mathbf{q}_i, \mathbf{k}_j\right) / \tau+B{i j}\]
式中, \(B_{i j}\) 是下面讲得相对位置编码, \(\tau\) 是可学习参数。余弦函数是 naturally normalized,因 此可以有较温和的注意力值。

方法3:对数连续位置编码技术:解决分辨率变化导致的位置编码维度不一致问题。

  • 该方法可以 更平滑地传递在低分辨率下预先训练好的模型权值,以处理高分辨率的模型权值。
    我们首先复习下 Swin Transformer 的相对位置编码技术。
    \[\operatorname{Attention}(Q, K, V)=\operatorname{SoftMax}\left(Q K^T / \sqrt{d}+B\right) V\]
    式中, \(B \in \mathbb{R}^{M^2 \times M^2}\) 是每个 head 的相对位置偏差项 (relative position bias),\(Q, K, V \in \mathbb{R}^{M^2 \times d}\) 是 window-based attention 的 query, key 和 value。 window 的大小。

作者引入对数空间连续位置偏差 (log-spaced continuous position bias),使相对位置偏差在不同的 window 分辨率之下可以较为平滑地过渡。

方法4:节省 GPU memory 的方法:

1 Zero-Redundancy Optimizer (ZeRO) 技术:

来自论文:Zero: Memory optimizations toward training trillion parameter models

传统的数据并行训练方法 (如 DDP) 会把模型 broadcast 到每个 GPU 里面,这对于大型模型来讲非常不友好,比如参数量为 3,000M=3B 的大模型来讲,若使用 AdamW optimizer,32为的浮点数,就会占用 48G 的 GPU memory。通过使用 ZeRO optimizer, 将模型参数和相应的优化状态划分并分布到多个 GPU 中,从而大大降低了内存消耗。训练时使用 DeepSpeed framework,ZeRO stage-1 option。

2 Activation check-pointing 技术:

来自论文:Training deep nets with sublinear memory cost

Transformer 层中的特征映射也消耗了大量的 GPU 内存,在 image 和 window 分辨率较高的情况下会成为一个瓶颈。这个优化最多可以减少30%的训练速度。

3 Sequential self-attention computation 技术:

在非常大的分辨率下训练大模型时,如分辨率为1535×1536,window size=32×32时,在使用了上述两种优化策略之后,对于常规的 GPU (40GB 的内存)来说,仍然是无法承受的。作者发现在这种情况下,self-attention 模块构成了瓶颈。为了解决这个问题,作者实现了一个 sequential 的 self-attention 计算,而不是使用以前的批处理计算方法。这种优化在前两个阶段应用于各层,并且对整体的训练速度有一定的提升。

在这项工作中,作者还一方面适度放大 ImageNet-22k 数据集5倍,达到7000万张带有噪声标签的图像。 还采用了一种自监督学习的方法来更好地利用这些数据。通过结合这两种策略,作者训练了一个30亿参数的强大的 Swin Transformer 模型刷新了多个基准数据集的指标,并能够将输入分辨率提升至1536×1536 (Nvidia A100-40G GPUs)。此外,作者还分享了一些 SwinV2 的关键实现细节,这些细节导致了 GPU 内存消耗的显着节省,从而使得使用常规 GPU 来训练大型视觉模型成为可能。 作者的目标是在视觉预训练大模型这个方向上激发更多的研究,从而最终缩小视觉模型和语言模型之间的容量差距。

不同 Swin V2 的模型配置:

  • SwinV2-T: C= 96, layer numbers ={2,2,6,2}
  • SwinV2-S: C= 96, layer numbers ={2,2,18,2}
  • SwinV2-B: C= 128, layer numbers ={2,2,18,2}
  • SwinV2-L: C= 192, layer numbers ={2,2,18,2}
  • SwinV2-H: C= 352, layer numbers ={2,2,18,2}
  • SwinV2-G: C= 512, layer numbers ={2,2,42,2}

对于 SwinV2-H 和 SwinV2-G 的模型训练,作者每经过6个 Transformer Block,就在主支路上增加了一层 LN,以进一步稳定训练和输出幅值。

Experiments

模型:SwinV2-G,3B parameters

Image classification

Dataset for Evaluation:ImageNet-1k,ImageNet-1k V2

Dataset for Pre-Training:ImageNet-22K-ext (70M images, 22k classes)

训练策略:分辨率使用192×192,为了节约参数量。2-step 的预训练策略。首先以自监督学习的方式在 ImageNet-22K-ext 数据集上训练 20 epochs,再以有监督学习的方式在这个数据集上训练 30 epochs,SwinV2-G 模型在 ImageNet-1k 上面达到了惊人的90.17%的 Top-1 Accuracy,在 ImageNet-1k V2 上面也达到了惊人的84.00%的 Top-1 Accuracy,超过了历史最佳的83.33%。

图5:Image classification 实验结果

同时,使用 Swin V2 的训练策略以后,Base 模型和 Large 模型的性能也可以进一步提升。比如 SwinV2-B 和 SwinV2-L 在 SwinV1-B 和 SwinV1-L 的基础上分别涨点0.8%和0.4%,原因来自更多的 labelled data (ImageNet-22k-ext, 70M images), 更强的 Regularization,或是自监督学习策略。

Object detection,Instance Segmentation

Dataset for Evaluation:COCO

Dataset for Pre-Training:Object 365 v2

如下图6所示 SwinV2-G 模型与之前在 COCO 目标检测和实例分割任务上取得最佳性能模型进行了比较。SwinV2-G 在 COCO test-dev 上实现了 63.1/54.4 box/max AP,相比于 SoftTeacher (61.3/53.0) 提高了 + 1.8/1.4。

图6:COCO 目标检测和实例分割任务

Semantic segmentation

Dataset for Evaluation:ADE20K

如下图7所示 SwinV2-G 模型与之前在 ADE20K 语义分割基准上的 SOTA 结果进行了比较。Swin-V2-G 在 ADE20K val 集上实现了 59.9 mIoU,相比于 BEiT 的 58.4 高了 1.5。

图7:ADE20k语义分割任务

Video action classification

Dataset for Evaluation:Kinetics-400 (K400)

如下图8所示 SwinV2-G 模型与之前在 Kinetics-400 动作分类基准上的 SOTA 结果进行了比较。可以看到,Video-SwinV2-G 实现了 86.8% 的 top-1 准确率,比之前的 TokenLearner 方法的 85.4% 高出 +1.4%。

图8:K400视频动作分类任务

对比实验:post-norm 和 scaled cosine attention 的作用

如下图9所示,这两种技术均能提高 Swin-T,Swin-S 和 Swin-B 的性能,总体提高分别为 0.2%,0.4% 和 0.5%。说明该技术对大模型更有利。更重要的是,它们能让训练更稳定。对于 Swin-H 和 Swin-G 模型而言,自监督预训练使用原来的 Swin V1 无法收敛,而 Swin V2 模型训练得很好。

图9:post-norm 和 scaled cosine attention 对比实验结果

PointRend –图像细颗粒分割

title
https://arxiv.org/abs/1912.08193

论文地址: https://arxiv.org/abs/1912.08193

gitlab: https://github.com/zsef123/PointRend-PyTorch

存在的问题

在目前的语义分割网络中存在的问题主要有过采样和现采样。

1.过采样( oversample ):对于图片中低频区域( 属于同一个物体 ),没必要使用 太多的采样点,却使用太多采样点造成过采样;

2.欠采样( undersample ) :对于图片中高频区域( 靠近物体边界 ),如果这些区域的采样过于稀疏,导致分割出的边界过于平滑,不大真实

文章要解决的问题是在实例分割任务中边缘不够精细的问题。以MaskRCNN举例,由于计算量和显存的原因,对于每一个ROIAlign之后的proposal我们一般只会upsample到28*28的分辨率输出mask。这对于绝大多数物体显然是不够的。如果想得到像素级别的精度,我们不得不付出更大的计算和存储代价。那有什么办法可以在低代价下仍然得到精细的分割结果呢?其实很重要的一点是往往这些不准确的部分是在物体的边缘,这些边缘其实只占了整个物体中非常小的一部分。所以基于这样的一个想法,作者提出可以每次在预测出来的mask中只选择Top N最不确定的位置进行细分预测。每个细分点的特征可以通过Bilinear插值得到,每个位置上的classifier通过一个简单的MLP来实现。这其实是等价于用一个1*1的conv来预测,但是对于中心很确定的点并不计算。整体的示意图如下:

PointRend 解决了什么问题?

这篇论文讲了一个很好听的故事,即:把语义分割以及实例分割问题(统称图像分割问题)当做一个渲染问题来解决。故事虽然这么讲,但本质上这篇论文其实是一个新型上采样方法,针对物体边缘的图像分割进行优化,使其在难以分割的物体边缘部分有更好的表现

作为一个小白,那么问题来了:

1、什么是渲染?

2、为什么要把图像分割问题当做渲染问题呢?

要想知道什么是渲染,可以参考:

计算机中所说的「渲染」是什么意思?

简单来说,渲染就是“绘制”,把3D的物体在2D平面上绘制出来。

为什么要把图像分割问题和渲染问题扯在一起呢?因为讲故事好听啊,论文好写嘛….咳咳…不不,是因为二者有类似的问题要解决:即物体边缘难以处理。

具体来说,在图像渲染中,对于多个3D物体,在边缘要判断对于镜头而言谁先谁后,而且还得抗锯齿;而对于图像分割问题,边缘恢复也一直是个麻烦事儿,因为在典型的语义分割网络中(如FCN、DeepLab),在CNN内部一般都会相对输入图像降采样16倍,然后再想办法上采样回去。更细致地说,对于 DeepLabV3+,模型最后直接是一个4倍的双线性插值上采样,这显然对物体边缘的预测十分不利。虽然 DeepLabV3+当时在2017年就达到了秒天秒地的 89%mIoU on VOC2012 test (使用了300M JFT 数据集预训练),至今无人超越(因为JFT 数据集 Google没有公开 \手动滑稽),但显然这个上采样过程仍然存在较大的提升空间。

参考链接:Uno Whoiam:DeepLab 语义分割模型 v1、v2、v3、v3+ 概要(附 Pytorch 实现)

而在实例分割网络中,Mask R-CNN 这货生成的 Mask 才 28×28,要是把这样的 mask 拉伸到 不说多了比如 256×256,还指望它可以很好地预测边缘?我只能说这是在想Peach。

事实上,在图像分割任务上边缘预测不理想这个情况其实在许多前人的工作中都有提及,比如 Not All Pixels Are Equal: Difficulty-Aware Semantic Segmentation via Deep Layer Cascade 中就详细统计了语义分割中,模型最容易误判的 pixel基本上都在物体边缘(如下图右上红色部分标记) 。

而关于上采样其实也有一些前人的工作,如 Decoders Matter for Semantic Segmentation: Data-Dependent Decoding Enables Flexible Feature Aggregation,在实现上有点像超分辨率网络 ESPCN 里使用的 sub-pixel convolutional layer 的操作,不过多加了一个二阶范数约束:

总的来说,图像分割边缘预测是一个未被很好解决的问题,而何恺明团队的 PointRend 是对此问题的一个新的思路和解法,接下来将介绍 PointRend 是如何 work 的。

文主要贡献

1.提出可嵌入主流网络的PointRend模块,提高了图像分割精度。

2.把图像分割问题看作渲染问题,本质上是一个新型上采样方法,为图像分割提供独特视角。

3.降低了训练所需的算力。输出224×224分辨率图像,PointRend只需0.9B FLOPs。

二、总体思路

PointRend 方法要点总结来说是一个迭代上采样的过程:

while 输出的分辨率 < 图片分辨率:

  1. 对输出结果进行2倍双线性插值上采样得到 coarse prediction_i。(粗分辨率预测)
  2. 挑选出 N 个“难点”,即结果很有可能和周围点不一样的点(例如物体边缘)。
  3. 对于每个难点,获取其“表征向量”,“表征向量”由两个部分组成,其一是低层特征(fine-grained features),通过使用点的坐标,在低层的特征图上进行双线性插值获得(类似 RoI Align),其二是高层特征(coarse prediction),由步骤 1 获得。
  4. 使用 MLP 对“表征向量”计算得到新的预测,更新 coarse prediction_i 得到 coarse prediction_i+1。这个 MLP 其实可以看做一个只对“难点”的“表征向量”进行运算的由多个 conv1x1 组成的小网络。

整个过程可以这么理解:

小明同学做题,现在有已知条件(coarse prediction_0,fine-grained features),想求解答案(coarse prediction_k),发现直接求(双线性插值or其它方法)不够准确,那就一步一步来吧(从coarse prediction_1,coarse prediction_2….求到coarse prediction_k)。好的,现在求coarse prediction_1,诶,发现有好多东西不知道,不能从 coarse prediction_0 直接得到怎么办?那就找出不知道的(“难点”),在 fine-grained features 里面找出对应的线索(ROIAlign-like 双线性插值),然后在结合 coarse prediction_0 得到整体线索(“特征向量”)求解(使用MLP计算),嗯,终于得到 coarse prediction_1了。再用同样的思路反复求解,直到 coarse prediction_k。

示意图如下:

对于一个coarse prediction(4×4大小),将其上采样两倍(8×8大小,这里可以理解为检测头的输出)后,取了一些难分割的点(大多是边缘部分),取这些点的特征向量输入到MLP网络中,进行point prediction,得到每一个点的新类别,最后结果输出(8×8大小,边缘更加精确的结果)。

另外,其PointRend 训练为了节省时间,没有使用上述的迭代过程,而是使用多种组合的采样方法,不赘述,详见paper。

  1. 从PointRend的应用思路中可以看到,这里包含了两个阶段的特征处理,分别是fine-grained features和coarse prediction部分,如果主干网络是ResNet,那么fine-grained features就是ResNet的stage2输出,也就是4倍下采样时的精细分割结果,而coarse prediction就是检测头的预测结果(还未上采样还原成原图的结果)。
  2. 从coarse prediction中挑选N个“难点”,也就是结果很有可能和周围点不一样的点(比如物体边缘的点)。对于每一个难点,获取他的“特征向量”,对于点特征向量(point features),主要由两部分组成,分别是fine-grained features的对应点和coarse prediction的对应点的特征向量,将这个两个特征向量拼接成一个向量。
  3. 接着,通过一个MLP网络对这个“特征向量”进行预测,更新coarse prediction。也就相当于对这个难点进行新的预测,对他进行分类。

看完这个,我们就可以这么理解,将预测难的点(边缘点)提取出来,再提取其特征向量,经过MLP网络,将这个点的归属进行分类,然后提升这些点的分类准确率。这就是PointRend的思想。

一个PointRend模块包括三部分

1.point selection strategy:用于inference和traing的点选择

对于点采样过程,需要对模型的Train过程和Inference过程做区分

该方法的核心思想是灵活自适应地选择图像平面上的点来预测分割标签。直观地说,这些点应该更密集地位于高频区域附近,例如物体边界,类似于射线追踪中的反混叠问题。我们产生了推理训练的想法。

  • inference推理

通过仅在与其邻域有显着不同的位置进行计算,该方法可用于有效地渲染高分辨率图像(例如,通过光线跟踪)。对于所有其他位置,通过对已经计算的输出值(从粗网格开始)进行插值来获得值。

对于每个区域,我们以粗到精的方式迭代地“渲染”输出蒙版。在规则网格上的点上进行最粗糙级别的预测(例如,通过使用标准的粗糙分段预测头)。在每次迭代中,PointRend使用双线性插值对其先前预测的分段进行上采样,然后在此较密集的网格上选择N个最不确定的点(例如,对于二进制掩码,概率最接近0.5的那些)。然后,PointRend为这N个点中的每一个点计算特征,并预测它们的标签。重复该过程,直到将分段上采样到所需的分辨率为止。

  • training

对于Train过程的点采样操作,同样可以遵循Inference中的操作。但是作者发现,这样子采样对于梯度的传播不太友好,于是只能被迫选择其他的点采样策略——干脆就用随机采样的方式来进行采样。

在训练过程中,PointRend还需要选择一些点,以在这些点上构建用于训练point head的逐点(point-wise)特征。原则上,点选择策略可以类似于推理inference中使用的细分策略。但是,细分引入了一系列步骤,这些步骤对于通过反向传播训练神经网络不太友好。取而代之的是,为了训练,我们使用基于随机采样的非迭代策略

采样策略在特征图上选择N个点进行训练。它旨在使用三个原理将选择偏向不确定区域,同时还保留一定程度的均匀覆盖。对于训练和推理选择,N的值可以不同。

(i)过度生成:我们通过从均匀分布中随机采样kN个点(k> 1)来过度生成候选点。(ii)重要抽样:通过对所有kN个点的粗略预测值进行插值并计算任务特定的不确定性估计,我们将重点放在具有粗略预测的点上。从kN个候选中选择最不确定的βN个点(β∈[0,1])。(iii)覆盖范围:从均匀分布中采样剩余的(1-β)N点。我们用不同的设置来说明此过程,并将其与常规的网格选择进行比较,如下图所示。

在训练时,预测和损失函数仅在N个采样点上计算(除粗略分割外),这比通过细分步骤进行反向传播更简单,更有效。这种设计类似于在Faster R-CNN系统中对RPN + Fast R-CNN的并行训练,其推理是顺序的。

2. Point-wise Representation:逐点表示

PointRend通过组合(例如,级联)两种特征类型(细粒度和粗略预测特征)在选定点上构造逐点特征,如下所述。

  • 细粒度特征

为了允许PointRend呈现精细的分割细节,我们从CNN特征图中提取每个采样点的特征向量。 因为一个点是“实值2D坐标”,所以我们按照标准做法对特征图执行双线性插值,以计算特征向量。 可以从单个特征图中提取特征(例如,ResNet中的res2);也可以按照Hypercolumn方法,从多个特征图(例如res2到res5)中提取并连接它们。

  • 粗预测特征

细粒度的特征可以解析细节,但在两个方面也有不足:

首先,它们不包含特定区域的信息,因此,两个实例的边界框重叠的相同点将具有相同的细粒度特征。但是,该点只能位于一个实例之中。 因此,对于实例分割的任务,其中不同的区域可能针对同一点预测不同的标签,因此需要其他区域特定的信息。

其次,取决于用于细粒度特征的特征图,这些特征可能只包含相对较低级别的信息(例如,我们将对res2使用DeepLabV3)。 因此,需要有更多具有上下文和语义信息的特征。

基于这两点考虑,第二种特征类型是来自网络的粗分割预测,例如表示k类预测的区域(box)中每个点的k维向量。通过设计,粗分辨率能够提了更加全局的上下文信息,而通道则传递语义类别。这些粗略的预测与现有架构的输出相似,并且在训练过程中以与现有模型相同的方式进行监督。例如,在mask R-CNN中,粗预测可以是一个轻量级的7×7分辨率Mask头的输出。

点特征向量(point features),主要由两部分组成,分别是fine-grained features的对应点和coarse prediction的对应点的特征向量,将这个两个特征向量拼接成一个向量

3. point head

给定每个选定点的逐点特征表示,PointRend使用简单的多层感知器(MLP)进行逐点分割预测。这个MLP在所有点(和所有区域)上共享权重,类似于图卷积或PointNet。由于MLP会预测每个点的分割标签,因此可以通过特定任务的分割loss进行训练。

三、效果如何?

实验结果

  • 网络设计

实验使用ResNet-50+ FPN 的Mask-Rcnn作backbone。 Mask-RCNN中的默认head是region-wise FCN,用“ 4×conv”表示,作为用来与本文网络进行比较的基准网络。

为了计算粗略预测,我们用重量更轻的设计替换4×conv Mask头,该设计类似于Mask R-CNN的box head产生7×7Mask预测。具体来说,对于每个边界框,我们使用双线性插值从FPN的P2层提取14×14特征图。这些特征是在边界框内的规则网格上计算的(此操作可以看作是RoIAlign的简单版本)。接下来,我们使用具有256个输出通道步幅为2的 2×2卷积层,后跟ReLU, 将空间大小减小到7×7。最后,类似于Mask R-CNN的box head,用两个带1024宽的隐藏层的MLP为K类分别产生7×7的Mask预测。ReLU用于MLP的隐藏层,并且Sigmoid激活函数应用于输出。

PointRend:在每个选定点上,使用双线性插值从粗预测头的输出中提取K维特征向量,PointRend还从FPN的P2级别插值256维特征向量,步长为4。这些粗预测和细粒度特征向量是串联在一起的,我们使用具有256个通道的3个隐藏层的MLP在选定点进行K类别预测。在MLP的每个层中,我们用K个粗预测特征补充到256个输出通道中,作为下一层输入向量。在MLP中使用ReLU,并将Sigmoid激活函数应用于输出。

不得不说这个针对物体边缘进行优化的上采样方法的确在感官上和数据上都有很不错的效果:

语义分割结果:

实例分割结果(基于MaskR-CNN):

PointRend的一些代码和实现

摘自: https://chowdera.com/2022/194/202207120607167479.html

代码详解: https://www.361shipin.com/blog/1536592971120508928

  • 作者提出可以在预测出来的mask中只选择Top N最不确定的位置进行细分预测。

具体为先根据粗糙预测出来的mask,将mask按类别预测分数排序,选出分数高的前2 类别的mask,计算出在2个类别mask上均有较高得分的Top K个像素点作为K 个不确定点【1个像素点只能对应1个类别,如果它对应2个类别的分数都很高,说明它很可能是边界点,也是不确定的】

def sampling_points(mask, N, k=3, beta=0.75, training=True):
    """
    主要思想:根据粗糙的预测结果,找出不确定的像素点
    :param mask: 粗糙的预测结果(out)   eg.[2, 19, 48, 48]
    :param N: 不确定点个数(train:N = 图片的尺寸/16, test: N = 8096)    eg. N=48
    :param k: 超参
    :param beta: 超参
    :param training:
    :return: 不确定点的位置坐标  eg.[2, 48, 2]
    """
    assert mask.dim() == 4, "Dim must be N(Batch)CHW"   #this mask is out(coarse)
    device = mask.device
    B, _, H, W = mask.shape   #first: mask[1, 19, 48, 48]
    mask, _ = mask.sort(1, descending=True) #_ : [1, 19, 48, 48],按照每一类的总体得分排序
    if not training:
        H_step, W_step = 1 / H, 1 / W
        N = min(H * W, N)
        uncertainty_map = -1 * (mask[:, 0] - mask[:, 1])
        #mask[:, 0]表示每个像素最有可能的分类,mask[:, 1]表示每个像素次有可能的分类,当一个像素
        #即是最有可能的又是次有可能的,则证明它不好预测,对应的uncertainty_map就相对较大
        _, idx = uncertainty_map.view(B, -1).topk(N, dim=1) #id选出最不好预测的N个点
        points = torch.zeros(B, N, 2, dtype=torch.float, device=device)
        points[:, :, 0] = W_step / 2.0 + (idx  % W).to(torch.float) * W_step    #点的横坐标
        points[:, :, 1] = H_step / 2.0 + (idx // W).to(torch.float) * H_step    #点的纵坐标
        return idx, points  #idx:48 || points:[1, 48, 2]
  • 得到不确定点的位置以后,可以通过Bilinear插值得到对应的特征,对每个不确定点的使用一个MLP来进行单独进行细分预测【训练与预测有所区别】。

具体为:通过刚刚得到的不确定点所在图片的相对位置坐标来找到对应的特征点,将此点对应的特征向量与此点的粗糙预测结果合并,然后通过一个MLP进行细分预测。

##训练阶段
def forward(self, x, res2, out):
        """
        主要思路:
        通过 out(粗糙预测)计算出top N 个不稳定的像素点,针对每个不稳定像素点得到在res2(fine)
        和out(coarse)中对应的特征,组合N个不稳定像素点对应的fine和coarse得到rend,
        再通过mlp得到更准确的预测
        :param x: 表示输入图片的特征     eg.[2, 3, 768, 768]
        :param res2: 表示xception的第一层特征输出     eg.[2, 256, 192, 192]
        :param out: 表示经过级联空洞卷积提取的特征的粗糙预测    eg.[2, 19, 48, 48]
        :return: rend:更准确的预测,points:不确定像素点的位置
        """
        """
        1. Fine-grained features are interpolated from res2 for DeeplabV3
        2. During training we sample as many points as there are on a stride 16 feature map of the input
        3. To measure prediction uncertainty
           we use the same strategy during training and inference: the difference between the most
           confident and second most confident class probabilities.
        """
        if not self.training:
            return self.inference(x, res2, out)
		#获得不确定点的坐标
        points = sampling_points(out, x.shape[-1] // 16, self.k, self.beta) #out:[2, 19, 48, 48] || x:[2, 3, 768, 768] || points:[2, 48, 2]
		#根据不确定点的坐标,得到对应的粗糙预测
        coarse = point_sample(out, points, align_corners=False) #[2, 19, 48]
        #根据不确定点的坐标,得到对应的特征向量
        fine = point_sample(res2, points, align_corners=False)  #[2, 256, 48]
		#将粗糙预测与对应的特征向量合并
        feature_representation = torch.cat([coarse, fine], dim=1)   #[2, 275, 48]
		#使用MLP进行细分预测
        rend = self.mlp(feature_representation) #[2, 19, 48]
        return {"rend": rend, "points": points}
##推理阶段
@torch.no_grad()
    def inference(self, x, res2, out):
        """
        输入:
        x:[1, 3, 768, 768],表示输入图片的特征
        res2:[1, 256, 192, 192],表示xception的第一层特征输出
        out:[1, 19, 48, 48],表示经过级联空洞卷积提取的特征的粗糙预测
        输出:
        out:[1,19,768,768],表示最终图片的预测
        主要思路:
        通过 out计算出top N = 8096 个不稳定的像素点,针对每个不稳定像素点得到在res2(fine)
        和out(coarse)中对应的特征,组合8096个不稳定像素点对应的fine和coarse得到rend,
        再通过mlp得到更准确的预测,迭代至rend的尺寸大小等于输入图片的尺寸大小
        """
        """
        During inference, subdivision uses N=8096
        (i.e., the number of points in the stride 16 map of a 1024×2048 image)
        """
        num_points = 8096
                while out.shape[-1] != x.shape[-1]: #out:[1, 19, 48, 48], x:[1, 3, 768, 768]
        	#每一次预测均会扩大2倍像素,直至与原图像素大小一致
            out = F.interpolate(out, scale_factor=2, mode="bilinear", align_corners=True)   #out[1, 19, 48, 48]
            points_idx, points = sampling_points(out, num_points, training=self.training)   #points_idx:8096 || points:[1, 8096, 2]
            coarse = point_sample(out, points, align_corners=False) #coarse:[1, 19, 8096]   表示8096个不稳定像素点根据高级特征得出的对应的类别
            fine = point_sample(res2, points, align_corners=False)  #fine:[1, 256, 8096]    表示8096个不稳定像素点根据低级特征得出的对应类别
            feature_representation = torch.cat([coarse, fine], dim=1)   #[1, 275, 8096] 表示8096个不稳定像素点合并fine和coarse的特征
            rend = self.mlp(feature_representation) #[1, 19, 8096]
            B, C, H, W = out.shape  #first:[1, 19, 128, 256]
            points_idx = points_idx.unsqueeze(1).expand(-1, C, -1)  #[1, 19, 8096]
            out = (out.reshape(B, C, -1)