你的 ResNet 是时候更新了 —— ResNeSt 来也!

作者简介

CW,广东深圳人,毕业于中山大学(SYSU)数据科学与计算机学院,毕业后就业于腾讯计算机系统有限公司技术工程与事业群(TEG)从事Devops工作,期间在AI LAB实习过,实操过道路交通元素与医疗病例图像分割、视频实时人脸检测与表情识别、OCR等项目。

目前也有在一些自媒体平台上参与外包项目的研发工作,项目专注于CV领域(传统图像处理与深度学习方向均有)。

前言

赶在4月末,终于有时间写文了,最近工作上需求比较急,抽不出时间来更文,但我心早已狂热!在我敲上这行字的过程中,真的很开心,因为真心很享受这种静静地码字向别人分享知识的时光(虽然不知道有没有人看..)。如今就为大家奉上这个新鲜出炉的新品 —— ResNeSt

文末可获取相关Paper&源码链接

你没看错,是 ResNeSt 而不是 ResNet 哟!这是张航、李沐等大佬创造的 ResNet 改进版,在参数量没有显著增加的情况下显著提升了性能,并且可以很方便地如 ResNet 般集成到现有算法框架中。通过本文,我们就一起来看看它有多香吧!

Outline

I. 主要思想

II. 分组的通道注意力机制:Split-Attention

III. 从代码出发,知行合一

1
基本思想
ResNeSt 很好懂,不复杂,简单来说就是结合了 ResNeXt 的分组卷积和 SE-Net 的通道注意力机制,将通道进行分组,对每组运用注意力机制,同时保留了 ResNet 的残差结构。
2
分组的通道注意力机制:Split-Attention

SplAtConv2d

这部分我们来详谈分组的通道注意力是怎样一种操作,作者论述到可能的实现方式有多种,这里我先谈谈其中一种。

了解 ResNeXt 的朋友们都知道,其引入了 Cardinality 的概念,代表分组的组数,为方便叙述,这里记为 K;ResNeSt 则在此基础上进一步分组,称为 split 操作,同时引入一个超参 Radix,代表将 K 个组中的每一个进一步划分的组数,这里记为 R这里的分组都是在通道这个维度上进行,由此看来,就是将输入在通道这个维度划分为 KxR 个组。

分组完毕后,对每个组实施不同的特征变换(Conv+Bn+Relu 等),然后将它们分成 R 份,这样每份就包含原来的 K 个组,对每一份应用投票机制形成注意力(Softmax or Sigmoid),接着将这 R 份注意力与特征图对应相乘(element-wise multiply),最后将这 R 份结果加起来(element-wise sum)形成输出,输出相当于对应了原来的 K 个组。

梳理下,可以知道注意力在是分了 K 个组后再分R个组上执行的,记 R 中的每一份为 r,K 中的每一份为k,那么每个 r 上得到的注意力是不同的,即每个 k split 下的每个 r 上的注意力不同,而同一个 r 下对应的不同 k 的注意力是一致的。

很奇妙,对于分得的K个组,每个组内切分R份分配不同的注意力,但不同组依次对应的这R份注意力却分别是一致的,是谓同又不尽全同!
3
从代码出发,知行合一

看过 paper 和源码的朋友们可能会一头雾水,paper 中展示的结构图和代码实现的有出入,一开始 CW 也是如此,看了几篇文但总感觉自己理解得依旧不那么清晰,于是乎亲自把代码手撸一遍,并结合画图理解,最终眼前的迷雾也就散开了。

我国古代优秀大佬王阳明推崇知行合一,虽然凡事不一定硬要知行结合,但是吾以为有了认知才有“行”的方向,“行”了才能加深认知或者说真正认知,这是一个循环,最终达到合二为一的高手境界。

接下来进入正题~

作者在源码中对于 split attention 的实现有两种方式,分别对应两个类,其中一个类为 SplAtConv2d,对应于上一部分展示的图中结构;另一个类为 RadixMajorNaiveImp,对应下图中的结构。

RadixMajorNaiveImp

RadixMajorNaiveImp

结合上图和代码,先来看看 RadixMajorNaiveImp 具体如何实现。

首先将输入分为 KxR 个组,然后依次对K中的每个 k 执行注意力机制,具体做法是取出同一个 k 下的所有 r,然后把它们加起来,输入全局平均池化层和两层全连接层。

RadixMajorNaiveImp (i)

接着令通道这个维度等于 R,在这个维度上生成注意力权重,同时,将同一 k 下的所有 r 在通道这个维度上拼接起来,与注意力权重相乘,相乘后的结果分为 R 份,将这 R 份结果加起来形成这一个 k 的输出,最终将K组中所有 k 的结果在通道数这个维度上拼接起来。

RadixMajorNaiveImp (ii)

总的来说,这种方式就是依次对 K 组中的每份 k 进行处理,每份 k 进一步 split 成 R 份,其中每份 r 生成不同的注意力,K 组中的每份 k 都结合完注意力后,再将它们的结果在通道上拼接起来。

SplAtConv2d

接下来看看 SplAtConv2d 的实现方式。

SplAtConv2d

仔细观察上图,我们可以发现,这种实现方式是将输入分为 R 份,其中的每份 r 包含了 K 个组,每份 r 生成的注意力不同(对应上图中的虚线框),上一节便说到了,同一 k  不同的 split r 上形成的注意力不一致,但不同的 k 对应相同的 r 上形成的注意力却是一致的

再回顾下 RadixMajorNaiveImp 的实现方式,同一 k  不同的 split r 上形成的注意力也是不一致,但不同 k 的注意力是独立生成的,它们之间并没有联系,这就是两种实现方式的最大差别了。

一起来瞄瞄代码~

SplAtConv2d (i)

这里提醒大家注意下,训练过程中在测试这个模块时,记住把 batch size 设置大于1,由于使用了 global average pooling,输出特征的大小变为1x1,因此其后接 bn 的话(上图中 self.bn1)就要求每个通道上多于一个元素,而如果 batch size 1的话就会报错了:

ValueError: Expected more than 1 value per channel when training

bn 是在每个通道上(channel-wise)做归一化的,如果通道上只有1个元素,那么归一化就无意义了,所以在训练过程中, bn 要求每个通道上必须多于1个元素。

SplAtConv2d (ii)

另外,SplAtConv2d 这种实现方式不需要依次对 K 组中的每份进行处理,而是直接对 K 个组同时进行处理,相比于 RadixMajorNaiveImp 的方式更加简洁些。

作者在 paper 和 github 源码中也给出了两者等价性的证明,源码可以查看《SplAtConv2d 和 RadixMajorNaiveImp 的等价性证明》,链接:https://github.com/zhanghang1989/ResNeSt/blob/master/tests/test_radix_major.py#L100

4
最后

对于 ResNeSt, 初次接触时往往会感觉其代码实现和paper描述得有出入,因此要把它讲述明白,自己本身一定要理解得透彻。如果没有亲自敲过一遍代码,就很难做到。对于其它算法模型也一样,能真正掌握的办法就是亲自上阵实践一番,所谓知而不行,乃是未知。

【参考】

源码链接:

https://github.com/zhanghang1989/ResNeSt

https://github.com/zhanghang1989/ResNeSt/blob/master/tests/test_radix_major.py#L100

Paper:

ResNeSt: Split-Attention Networks

(0)

相关推荐