点击上方“AIWalker”,选择加“星标” 精品干货,瞬时送达
paper: httsp://arxiv.org/abs/2101.03697 code: https://github.com/DingXiaoH/RepVGG 注:公众号后台回复:RepVGG,即可下载上述论文&code&预训练模型。
该文是清华大学&旷世科技等提出的一种新颖的CNN设计范式,它将ACNet的思想与VGG架构进行了巧妙的结合,即避免了VGG类方法训练所得精度低的问题,又保持了VGG方案的高效推理优点,它首次将plain模型的精度在ImageNet上提升到了超过80%top1精度。相比ResNet、RegNet、EfficientNet等网络,RepVGG具有更好的精度-速度均衡。
本文提出一种简单而强有力的CNN架构RepVGG,在推理阶段,它具有与VGG类似的架构,而在训练阶段,它则具有多分支架构体系,这种训练-推理解耦的架构设计源自一种称之为“重参数化(re-parameterization)”的技术。
在ImageNet数据集上,RepVGG取得了超过80%的top-1精度,这是plain模型首次达到如此高的精度。在NVIDIA 1080TiGPU上,RepVGG比ResNet50快83%,比ResNet101快101%,同时具有更高的精度;相比EfficientNet与RegNet,RepVGG表现出了更好的精度-速度均衡。
该文的主要贡献包含以下三个方面:
简单的ConvNet具有这样三点优势:
Palin模型具有多种优势但存在一个重要的弱势:性能差。比如VGG16在ImageNet仅能达到72%的top-1指标。
本文所设计的RepVGG则是受ResNet启发得到,ResNet的ResBlock显示的构建了一个短连接模型信息流
,当
的维度不匹配时,上述信息流则转变为
。
尽管多分支结构对于推理不友好,但对于训练友好,作者将RepVGG设计为训练时的多分支,推理时单分支结构。作者参考ResNet的identity与
分支,设计了如下形式模块:
其中,
分别对应
卷积。在训练阶段,通过简单的堆叠上述模块构建CNN架构;而在推理阶段,上述模块可以轻易转换为
形式,且
的参数可以通过线性组合方式从已训练好的模型中转换得到。
接下来,我们将介绍如何将已训练模块转换成单一的
卷积用于推理。下图给出了参数转换示意图。
我们采用
表示输入
,输出
,卷积核为3的卷积;采用
表示输入
,输出
,卷积核为1的卷积;采用
表示
卷积后的BatchNorm的参数;采用
表示
卷积后的BatchNorm的参数;采用
表示identity分支的BatchNorm的参数。假设
分别表示输入与输出,当
时,
否则,简单的采用无identity分支的模块,也就是说只有前两项。注:bn表示推理时的BN。
首先,我们可以将每个BN与其前接Conv层合并:
注:identity分支可以视作
卷积。通过上述变换,此时上述模块仅仅具有一个
卷积核,两个
卷积核以及三个bias参数。此时,三个bias参数可以通过简单的add方式合并为一个bias;而卷积核则可以将
卷积核参数加到
卷积核的中心点得到。说起来复杂,其实看一下code就非常简单了,见文末code。
前面介绍了RepVGG的核心模块设计方式,接下来就要介绍RepVGG的网络结构如何设计了。下表给出了RepVGG的配置信息,包含深度与宽度。
RepVGG是一种类VGG的架构,在推理阶段它仅仅采用
卷积与ReLU,且未采用MaxPool。对于分类任务,采用GAP+全连接层作为输出头。
对于每个阶段的层数按照如下三种简单的规则进行设计:
基于上述考量,RepVGG-A不同阶段的层数分别为1-2-4-14-1;与此同时,作者还构建了一个更深的RepVGG-B,其层数配置为1-4-6-16-1。RepVGG-A用于与轻量型网络和中等计算量网络对标,而RepVGG-B用于与高性能网络对标。
在不同阶段的通道数方面,作者采用了经典的配置64-128-256-512。与此同时,作者采用因子
控制前四个阶段的通道,因子
控制最后一个阶段的通道,通常
(我们期望最后一层具有更丰富的特征)。为避免大尺寸特征的高计算量,对于第一阶段的输出通道做了约束
。基于此得到的不同RepVGG见下表。
为进一步降低计算量与参数量,作者还设计了可选的
组卷积替换标准卷积。具体地说,在RepVGG-A的3-5-7-...-21卷积层采用了组卷积;此外,在RepVGG-B的23-25-27卷积层同样采用了组卷积。
接下来,我们将在不同任务上验证所提方案的有效性,这里主要在ImageNet图像分类任务上进行了实验分析。
上表给出了RepVGG与不同计算量的ResNe及其变种在精度、速度、参数量等方面的对比。可以看到:RepVGG表现出了更好的精度-速度均衡,比如
另外需要注意的是:RepVGG同样是一种参数高效的方案。比如:相比VGG16,RepVGG-B2b168.com仅需58%参数量,推理快10%,精度高6.57%。
与此同时,还与EfficientNet、RegNet等进行了对比,对比如下:
此外需要注意:RepVGG仅需200epoch即可取得超过80%的top1精度,见上表对比。这应该是plain模型首次在精度上达到SOTA指标。相比RegNetX-12GF,RepVGG-B3的推理速度快31%,同时具有相当的精度。
尽管RepVGG是一种简单而强有力的ConvNet架构,它在GPU端具有更快的推理速度、更少的参数量和理论FLOPS;但是在低功耗的端侧,MobileNet、ShuffleNet会更受关注。
全文到此结束,更多消融实验分析建议各位同学查看原文。
话说在半个月多之前就听说xiangyu等人把ACNet的思想与Inception相结合设计了一种性能更好的重参数化方案RepVGG,即可取得训练时的性能提升,又可以保持推理高效,使得VGG类网络可以达到ResNet的高性能。
在初次看到RepVGG架构后,笔者就曾尝试将其用于VDSR图像超分方案中,简单一试,确实有了提升,而且不需要进行梯度裁剪等额外的一些操作,赞。
从某种程度上讲,RepVGG应该是ACNet的的一种极致精简,比如上图给出了ACNet的结构示意图,它采用了
三种卷积设计;而RepVGG则是仅仅采用了
三个分支设计。ACNet与RepVGG的另外一点区别在于:ACNet是将上述模块用于替换ResBlock或者Inception中的卷积,而RepVGG则是采用所设计的模块用于替换VGG中的卷积。
最后附上作者提供的RepVGG的核心模块实现code,如下所示。
# code from https://github.com/DingXiaoH/RepVGG
class RepVGGBlock(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size,
stride=1, padding=0, dilation=1, groups=1, padding_mode='zeros', deploy=False):
super(RepVGGBlock, self).__init__()
self.deploy = deploy
self.groups = groups
self.in_channels = in_channels
assert kernel_size == 3
assert padding == 1
padding_11 = padding - kernel_size // 2
self.nonlinearity = nn.ReLU()
if deploy:
self.rbr_reparam = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=kernel_size, stride=stride,
padding=padding, dilation=dilation, groups=groups, bias=True, padding_mode=padding_mode)
else:
self.rbr_identity = nn.BatchNorm2d(num_features=in_channels) if out_channels == in_channels and stride == 1 else None
self.rbr_dense = conv_bn(in_channels=in_channels, out_channels=out_channels, kernel_size=kernel_size, stride=stride, padding=padding, groups=groups)
self.rbr_1x1 = conv_bn(in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=stride, padding=padding_11, groups=groups)
print('RepVGG Block, identity = ', self.rbr_identity)
def forward(self, inputs):
if hasattr(self, 'rbr_reparam'):
return self.nonlinearity(self.rbr_reparam(inputs))
if self.rbr_identity is None:
id_out = 0
else:
id_out = self.rbr_identity(inputs)
return self.nonlinearity(self.rbr_dense(inputs) + self.rbr_1x1(inputs) + id_out)
def _fuse_bn(self, branch):
if branch is None:
return 0, 0
if isinstance(branch, nn.Sequential):
kernel = branch.conv.weight.detach().cpu().numpy()
running_mean = branch.bn.running_mean.cpu().numpy()
running_var = branch.bn.running_var.cpu().numpy()
gamma = branch.bn.weight.detach().cpu().numpy()
beta = branch.bn.bias.detach().cpu().numpy()
eps = branch.bn.eps
else:
assert isinstance(branch, nn.BatchNorm2d)
kernel = np.zeros((self.in_channels, self.in_channels, 3, 3))
for i in range(self.in_channels):
kernel[i, i, 1, 1] = 1
running_mean = branch.running_mean.cpu().numpy()
running_var = branch.running_var.cpu().numpy()
gamma = branch.weight.detach().cpu().numpy()
beta = branch.bias.detach().cpu().numpy()
eps = branch.eps
std = np.sqrt(running_var + eps)
t = gamma / std
t = np.reshape(t, (-1, 1, 1, 1))
t = np.tile(t, (1, kernel.shape[1], kernel.shape[2], kernel.shape[3]))
return kernel * t, beta - running_mean * gamma / std
def _pad_1x1_to_3x3(self, kernel1x1):
if kernel1x1 is None:
return 0
kernel = np.zeros((kernel1x1.shape[0], kernel1x1.shape[1], 3, 3))
kernel[:, :, 1:2, 1:2] = kernel1x1
return kernel
def repvgg_convert(self):
kernel3x3, bias3x3 = self._fuse_bn(self.rbr_dense)
kernel1x1, bias1x1 = self._fuse_bn(self.rbr_1x1)
kernelid, biasid = self._fuse_bn(self.rbr_identity)
return kernel3x3 + self._pad_1x1_to_3x3(kernel1x1) + kernelid, bias3x3 + bias1x1 + biasid