Featured image of post RepVGG:网络结构重参数化

RepVGG:网络结构重参数化

RepVGG名字就很直接,让VGG再次伟大。VGG可以说是古早的一种网络结构了,它的构造比后来花里胡哨的模型纯粹不少,就是一条直筒撸下去,这种结构访存友好,所以同参数下比ResNet要快,但是精度比不上人家。这篇文章就利用重参数这一技术让它的精度超过了ResNet。

论文:https://arxiv.org/abs/2101.03697 代码:https://github.com/DingXiaoH/RepLKNet-pytorch

一图胜千言

blog不用像论文一样长篇大论说一个简单问题了,这篇文章很简单,其实就是想把ResNet的残差以及Inception的多分支结构给引入到VGG中,但是又不想丧失掉直筒模型快速的优点,所以弄出了一种重参数化技术。看图片吧:

重参数化技术能够把训练时候的多分支结构在测试时转换成直筒结构,这样就可以同时享受精度提升和推理速度。

这张图有个重要的信息,观察残差的位置,ResNet中残差以及1x1卷积所囊括的主路里面是有个ReLU的,但是RepVGG的没有,这是刻意为之的,因为一旦包含了ReLU就没有办法在测试时转换为直筒模型,下面就来说说重参数化是怎么做的。

重参数化

定义

我们定义一些公式来说明,下面是卷积核的定义,输入维度是$C_1$,输出维度是$C_2$,卷积尺寸为$3 \times 3$:

$$ W^{(3)}\ \ \in\ {R}^{C_{2}\times C_{1}\times3\times3} $$
那尺寸为$1 \times 1$的卷积就可以写成$W^{(1)}$。

$W^{(3)}$的BN的均值和方差可以写成$\mu^{(3)}$,$\sigma^{(3)}$,缩放和偏移项就是$\gamma^{(3)}$,$\beta^{(3)}$;

对应的$1\times1$卷积的为$\mu^{(1)}$,$\sigma^{(1)}$,$\gamma^{(1)}$,$\beta^{(1)}$;

为了格式统一,残差项我们也假设有一个BN, 写为$\mu^{(0)}$,$\sigma^{(0)}$,$\gamma^{(0)}$,$\beta^{(0)}$,因为设置为均值和偏移为0,方差和缩放为1就行了,这样就是全等变换。

输入为:

$$ M^{(1)}\ \ \in\ {R}^{N\times C_{1}\times H_1\times W_1} $$

输出为:

$$ M^{(2)}\ \ \in\ {R}^{N\times C_{2}\times H_2\times W_2} $$

由于残差连接的存在,一定是$C_1=C_2, H_1=H_2,W_1=W_2$。

好好吸收一下上面的定义,都是一些尺寸定义,没什么难度,不要被公式绕晕了。

假如我们的网络结构就是上图,利用上面定义的元素,可以公式化这种结构$,*$代表卷积:

$$ M^{(2)}=bn(M^{(1)}*W^{(3)},\mu^{(3)},\sigma^{(3)},\gamma^{(3)},\beta^{(3)})+\newline bn(M^{(1)}*W^{(1)},\mu^{(1)},\sigma^{(1)},\gamma^{(1)},\beta^{(1)})+\newline bn(M^{(1)}*W^{(0)},\mu^{(0)},\sigma^{(0)},\gamma^{(0)},\beta^{(0)}) $$

吸BN

我们先来看一下BN层是怎么计算的,因为BN是逐通道应用到输出上的,$i$的取值范围为1到$C_2$:

$$ bn(M,\mu,\sigma,\gamma,\beta)_{:,i,:,:}=\gamma_i \times(M_{:,i,:,:}-\mu_i)/\sigma_i+\beta_i $$

可以看到$bn$就是线性运算,完全可以融合到卷积层中去,这称为吸BN操作,这已经是常见的工程加速手段,注意这种操作一定是卷积层挨着BN,如果中间隔了一个非线性层就不成立了。好,现在把BN融进卷积核参数,新的卷积核参数变成了:

$$ W_{i,\ :,\ :,\ :}^{'}=\gamma_i/\sigma_i\times W_{i,\ :,\ :,\ :} $$
$$ b_{i}^{'}=\beta_i -\gamma_i/\sigma_i\times \mu_{i} $$
注意一下这里的$i$为什么出现在第一个维度,这是因为卷积核的第一个维度对应的是输出的通道数,而BN是逐通道作用在输出的每一个通道上的。

所以带BN的卷积层,可以用卷积运算表达:

$$ bn(M*W,\mu,\sigma,\gamma,\beta)_{:,i,:,:}=(M*W^{'})_{:,i,:,:}+b_i^{'} $$

1x1卷积转3*3卷积

为了方便我们假设$C_1=C_2=2$,那么$W^{(3)}$就是$2\times2\times3\times3$的一个矩阵,我们可以画成下图,也就是四个绿色的方块,一行有两个方块,然后有两行,这里行数代表了输出维度$C_2$,列数代表了$C_1$这个维度。

那$1 \times 1$卷积其实就是红色的小方块,我们是有办法把它也变成绿色方块的样子的,那就是先把这四个方块也分成两行两列,然后对每个方块周围填充一圈0就行了。

这个可以很直观理解,因为卷积其实就是结合周围像素值进行加权嘛,原来的$3\times3$卷积会考虑周围8个像素以及中间像素,得出新的中间像素,而$1\times1$卷积是不管周围像素的,只是根据中间像素得到新的中间像素。那我把卷积核周围填充一圈0也是可以达到只考虑中间像素,不考虑周围像素这个目的,这两种结构完全等价。

卷积有个不错的性质,就是只用卷积核尺寸相同,卷积之后再相加等效于卷积核相加:

$$ M*W_1+M*W_2=M*(W_1+W_2) $$

这样我们就已经实现了$1\times1$卷积转$3\times3$卷积,并且不用算两次卷积了,因为直接把这两个的卷积核加在一块就行了。

残差连接转卷积

残差原理上就是一个全等变换,全等变换可不可以用$1\times1$卷积做呢?是不可以的,因为$1\times1$卷积会对空间位置x,y处所有通道上的值求加权得到新的值。但是残差连接中通道和通道之间没关系,只是当前通道的恒等变换。

其实这不就是$1\times1$的depthwise卷积嘛!depthwise卷积就是逐通道的,$1\times1$也就是不考虑周围像素,只用把卷积核值取为1,$1\times1$的depthwise卷积完全等效于恒等变换也就是残差连接。

但是我们是基于正常卷积的,更近一步的我们需要把$1\times1$的depthwise卷积变成$3\times3$的正常卷积,这分为两步,首先是周围填充一圈0,变成$3\times3$的depthwise卷积,然后再是depthwise卷积转正常卷积。

这一步得结合图理解一下,前面说过,看黄色的四个方块,行数代表了输出维度$C_2$,列数代表了$C_1$这个维度。所以第一行这两个$3\times3$的卷积核帮助我们得到$C_2$的第一个通道,我们又希望输入等于输出,那就需要屏蔽掉$C_1$输入中的第二个通道,那对应的第一行第二列必须是一个全0矩阵。

同理对于第二行这两个$3\times3$的卷积核帮助我们得到$C_2$的第二个通道,需要屏蔽掉$C_1$输入中的第一个通道,那第二行第一列必须为全0矩阵。

所以我们又搞定了残差到正常卷积的转换,后续就是把这几个卷积核加在一块得到新的卷积核,用新的卷积核计算一次卷积,等于之前多分支的结果,所以模型又可以退化成直筒结构了。

效果分析

可以看到新加的$1\times1$分支和残差连接都是有效的,如果说哪个更有效一点,那还是残差更加有效,但是这些新加入的分支在未进行重参数时都是拖慢速度的。好在本文的方法仅仅是训练速度变慢,在测试阶段是可以转换成直筒模型的,这样速度又回来了。

作者也验证了一些变体的效果,比如残差连接里带不带BN(我上面的推导里就认为残差不带BN),发现残差连接里加个BN效果更好,不然能掉将近一个点。

另外也试了不是每条支路都带BN,而是加在一起之后整体用BN,这样也会显著掉点。

还有就是每条分支在BN后加一个ReLU,然后再相加,而不是想在这样相加后再ReLU,发现前者也能提点,但是这样就没法转换成直筒了。

思考一下🤔

至于为什么这种方法有用呢?按照深度学习的基本原理来说,参数量和训练数据量这两者有一个提升了,准确率才会提升,但是本文的方法在测试时候参数量和直筒完全相同,那为啥能提升呢? 感觉是因为BN,虽然推理的时候BN是一个线性层,但是训练的时候BN并非线性层,因为它依赖于每个小批量的动态统计数据,每批样本均值和方差都不一样,所以BN在训练的时候是个非线性层。这种方法有效的原因可能还是往网络了加了更多的非线性。非线性是有用的,比如作者验证了在每条分支在BN后加一个ReLU还能继续提高精度。

comments powered by Disqus