点击上方 【AI人工智能初学者】 ,选择 【星标】 公众号
期待您我的相遇与进步
RepVGG主体仅使用3×3卷积和ReLU!在ImageNet上,RepVGG的top-1准确性达到80%以上,这是基础模型的首次实现80%以上的精度!综合性能超越ResNet、EfficientNet等。
作者单位 :清华大学(丁贵广团队), 旷视科技(孙剑等人), 港科大, 阿伯里斯特威斯大学
1 简介
RepVGG架构是一个简单且强大的卷积神经网络架构,该架构具有类似于VGG的推理时间主体,该主体仅由3x3卷积和ReLU的堆叠组成,而训练时间模型具有多分支拓扑。训练时间和推理时间架构的这种解耦是通过结构重新参数化(re-parameterization)技术实现的,因此该模型称为RepVGG 。
在ImageNet上,RepVGG的top-1准确性达到80%以上,这是单模型首次实现超越80%的精度。在NVIDIA 1080Ti GPU上,RepVGG模型的运行速度比ResNet-50快83%,比ResNet-101快101%,具有更高的精度,并且与诸如EfficientNet和RegNet的最新模型相比,显示出良好的精度-速度折衷。
2 前人工作
2.1 Single-path到Multi-branch
自从VGG在ImageNet数据集的分类精度提高到70%以上以后,在使卷积网络变得复杂以达到高性能方面已经有了许多创新;例如GoogLeNet采用精心设计的多分支架构、ResNet提出简化两分支架构和DenseNet拓扑更复杂的连接方式。
神经结构搜索(NAS)和人工架构设计都可以生成性能更高的卷积网络,但需要耗费大量的计算资源或人力。NAS生成模型的一些大型模型甚至不能在普通GPU上训练,因此限制了应用其发展。除了实现上的不便之外,复杂的模型可能会降低并行度,从而降低推理的速度。
2.2 有效训练Single-path模型
已经有人尝试训练没有分支的卷积网络模型。然而,前人的工作主要是寻求非常深的模型的精度收敛,并没有达到比复杂模型更好的性能。因此,所建立的方法和模型既不简单也不实用。
例如,有学者提出了一种训练extremely deep plain ConvNets的初始化方法。使用mean-field-theory-based方案,10000层网络在MNIST上训练的准确率超过99%,在CIFAR-10上训练的准确率达到82%。虽然模型不实用(甚至LeNet-5在MNIST上可以达到99.3%的准确率,VGG-16在CIFAR-10上可以达到93%以上),但理论贡献是很有见地的。
最近的一项工作结合了几种技术,包括Leaky ReLU、max-norm和Careful initialization。在ImageNet上,参数为147M的普通ConvNet可以达到74.6% top-1精度。
而本文目标是建立一个简单的模型,具有合理的深度和良好的精度-速度权衡,可以简单地用最常见的组件实现(例如正则卷积和BN) 。
2.3 Model Re-parameterization
DiracNet是一种与本文相关的重新参数化的方法。它通过将卷积层的核编码为来构建深平面模型,其中是用于卷积的最终权值(一个被视为矩阵的4d张量)、a和b是学习向量、是规范化的可学习kernel。与同等参数量的ResNets相比,CIFAR-100上DiracNet的top-1准确率降低了2.29%,ImageNet上降低了0.62%。
DiracNet与本文的方法的不同之处在于:
- 本文的结构重新参数化是通过一个具体的结构来实现实际数据流,然后可以转换成另一个结构,而DiracNet仅仅使用了另一个卷积核的数学表达式,以方便优化。即,一个结构上重新参数化的普通模型是一个真正的训练时间多分支模型,但DiracNet不是。
- DiracNet模型的性能高于一般参数化的plain模型,但低于ResNet模型,而RepVGG模型的性能则大大优于ResNets模型。Asym Conv块(ACB)采用不对称Conv加强常规Conv的“Backbone”,它可以被视为另一种形式的结构性re-parameterization,它训练块转换成Conv。ACB即插即用的结构,可以插入到其他的Backbone中进行使用。
2.4 Winograd卷积
RepVGG只使用了3x3的Conv,下表显示了在1080Ti GPU上用cuDNN 7.5.0测试的理论FLOPs、实际运行时间和计算密度(TFLOPS)。
结果表明,3×3的conv的理论计算密度与其他算法相比约为4倍,这表明在不同的体系结构中,理论速度并不能代表实际速度。加速3x3的conv的一个经典算法是Winograd算法(仅当stride为1时),它已经被像cuDNN和MKL这样的库很好地支持(并默认启用)。
例如,用标准的的Winograd,一个3x3的卷积的乘法(MULs)减少到原来的4/9。由于MUL计算更耗时,文中记录了计算MULs与计算Winograd的实验(如下表)。注意,具体计算库和硬件决定是否使用Winograd每个供应商的产品。因为小规模的计算变化可能不是加速由于内存开销。
3 本文方法
3.1 操作集合
1 Simple=>Fast
最近许多多分支架构的理论FLOPs比VGG要低,但运行起来可能并不会更快。例如,VGG-16的FLOPs是EfficientNet-B3的8.4倍,但在1080Ti上运行速度要快1.8倍,这意味着前者的计算密度是后者的15倍。除了Winograd conv带来的加速外,内存访问成本(MAC)和并行度 这两个重要因素对速度有很大的影响,但内存访问成本和并行度并没有被计算在内。
例如,虽然所需要的分支加法或连接的计算是微不足道的,但MAC是重要的。此外,MAC在分组卷积中占据了很大一部分时间。另一方面,在相同的 FLOPs情况下,具有高并行度的模型可能比另一个具有低并行度的模型要快得多。由于多分支拓扑在初始化和自动生成的体系结构中被广泛采用,因此使用了多个小的运算符而不是几个大的运算符。
之前有工作说明了分割操作的数量(即在一个构建块中单个Conv或池化操作的数量)NASNET-A是13,这对GPU等拥有强大并行计算能力的设备不太友好,并引入了额外的开销,如内核启动和同步。相比之下,这个数字在ResNets中是2或3,本文所提的方法将其设为1:仅仅是单个Conv。
2 Memory-economical
多分支拓扑的内存效率是非常低下的,因为每个分支的结果都需要保留到叠加或连接,这大大提高了内存占用。如下图所示,Residual Block的输入需要保持到加法为止。假设Block保持feature map大小,则内存占用保持在2倍作为输入空间。相比之下,普通拓扑允许特定层的输入所占用的内存在操作完成后立即释放。
3 Flexible
多分支拓扑对体系结构规范施加了约束。例如,ResNet要求将conv层组织为Residual Block,这限制了灵活性,因为每个Residual Block的最后一个conv层必须产生相同形状的张量,否则Shortcut将没办法进行。
更糟糕的是,多分支拓扑限制了通道剪枝的应用。通道剪枝是一种去除一些不重要通道的实用技术,有些方法可以通过自动发现每一层合适的宽度来优化模型结构。然而,多分支模型使修剪变得棘手,并导致显著的性能下降或较低的加速比。相比之下,普通架构允许我们根据需求自由配置每个conv层,并进行修剪,以获得更好的性能-效率平衡。
3.2 多分支结构的训练时间
简单的卷积网络有很多优点,但有一个致命的缺点:性能差。例如,VGG-16使用BN等组件后在ImageNet上可以达到72%的top-1精度,但是现在看来是比较低了。本文所提结构受ResNet re-parameterization方法,构造一个shortcut分支模型,并使用一个Residual Block学习。当和的尺寸不匹配,它变成了, 是一个卷积实现的shortcut。
由于多分支拓扑在推理方面存在缺陷,但分支似乎对训练有益,因此本文使用多个分支对多个模型进行单独的训练时间集成。为了使大多数members更浅或更简单,作者使用类似了resnet的identity(仅当维度匹配时)和分支以便构建Block的训练信息流为。
本文所提架构只是简单地堆叠几个这样的块来构建训练模型。从相同的角度来看,模型是由个members和n个这样的块组成的集合。训练后,将其等价转换为,其中h由一个单独的卷积层实现,其参数由训练后的参数通过一系列代数推导而来。
3.3 重新指定推理时间模型的参数
这里描述了如何将一个训练块转换成一个单独的3×3的conv层进行推理。注意,在加法之前的每个分支中都使用了BN,如下图:
形式上,用来表示具有输入通道和输出通道的 conv层的核,用表示分支的核。使用分别表示conv后均值,标准差和学习因子以及BN层的偏差,分别表示conv后均值,标准差和学习因子以及BN层的偏差,和为 identity branch。设、分别为输入和输出,为卷积算子。如果,,有:
否则,如果不使用identity branch,因此上述方程只有前两项。这里bn是推理时间bn函数:
首先将每一个BN及其前面的卷积层转换成一个带有bias向量的卷积。让是从转换过来的kernel和bias:
上述变换也适用于identity branch,因为单位映射可以被视为以单位矩阵为核的1x1 conv。如Figure 4所示,经过这样的变换,将得到一个3x3 kernel,2个1x1 kernel,和3个bias向量。然后获得通过添加了3个bias向量的最终bias,最后3x3 kernel通过添加1x1 kernel的中心点上3x3的kernel,这可以很容易地由第1个零填充两个1x1内核实现3x3和添加3个kernel。需要注意的是,这些转换的等价性要求3x3层和1x1层具有相同的stride,而1x1层的padding配置要比3x3层少一个像素。例如,对于一个3x3层,填充一个像素的输入,这是最常见的情况,1x1层应该有padding=0。
3.4 架构说明
下表显示了RepVGG的规格,包括深度和宽度。RepVGG是VGG风格的,它采用简单的拓扑结构,大量使用3×3 conv,但它不像VGG那样使用最大池化,因为希望主体只有一种操作类型。这里安排3×3 layer分为5个阶段,第一个阶段的第1层下降样品的stride=2。对于图像分类,使用全局平均池化,然后使用全连接层作为head。对于其他任务,特定于任务的head可以用于任何层产生的特性。
根据3个简单的原则来决定每个阶段的层数:
- 第1阶段的操作分辨率较大,耗时较长,因此只使用一Layer以降低延迟。
- 最后1个阶段应该有更多的通道,所以只使用一个Layer来保存参数。
- 将大部分Layer放入最后的第2阶段(ImageNet输出分辨率为14x14),然后是ResNet及其最新版本(例如,ResNet-101在其14x14分辨率阶段使用了69层)。
这里让这5个阶段分别有1、2、4、14、1个层来构建一个名为RepVGG-A的实例。还构建了一个更深层的RepVGG-B,在阶段2、3和4中有更多的层,等等。
import torch.nn as nn
import numpy as np
import torch
def conv\_bn(in\_channels, out\_channels, kernel\_size, stride, padding, groups=1):
result = nn.Sequential()
result.add_module('conv', nn.Conv2d(in_channels=in_channels, out_channels=out_channels,
kernel_size=kernel_size, stride=stride, padding=padding, groups=groups, bias=False))
result.add_module('bn', nn.BatchNorm2d(num_features=out_channels))
return result
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
class RepVGG(nn.Module):
def \_\_init\_\_(self, num\_blocks, num\_classes=1000, width\_multiplier=None, override\_groups\_map=None, deploy=False):
super(RepVGG, self).__init__()
assert len(width_multiplier) == 4
self.deploy = deploy
self.override_groups_map = override_groups_map or dict()
assert 0 not in self.override_groups_map
self.in_planes = min(64, int(64 * width_multiplier[0]))
self.stage0 = RepVGGBlock(in_channels=3, out_channels=self.in_planes, kernel_size=3, stride=2, padding=1, deploy=self.deploy)
self.cur_layer_idx = 1
self.stage1 = self._make_stage(int(64 * width_multiplier[0]), num_blocks[0], stride=2)
self.stage2 = self._make_stage(int(128 * width_multiplier[1]), num_blocks[1], stride=2)
self.stage3 = self._make_stage(int(256 * width_multiplier[2]), num_blocks[2], stride=2)
self.stage4 = self._make_stage(int(512 * width_multiplier[3]), num_blocks[3], stride=2)
self.gap = nn.AvgPool2d(7)
self.linear = nn.Linear(int(512 * width_multiplier[3]), num_classes)
def \_make\_stage(self, planes, num\_blocks, stride):
strides = [stride] + [1]*(num_blocks-1)
blocks = []
for stride in strides:
cur_groups = self.override_groups_map.get(self.cur_layer_idx, 1)
blocks.append(RepVGGBlock(in_channels=self.in_planes, out_channels=planes, kernel_size=3,
stride=stride, padding=1, groups=cur_groups, deploy=self.deploy))
self.in_planes = planes
self.cur_layer_idx += 1
return nn.Sequential(*blocks)
def forward(self, x):
out = self.stage0(x)
out = self.stage1(out)
out = self.stage2(out)
out = self.stage3(out)
out = self.stage4(out)
out = self.gap(out)
out = out.view(out.size(0), -1)
out = self.linear(out)
return out
optional_groupwise_layers = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26]
g2_map = {l: 2 for l in optional_groupwise_layers}
g4_map = {l: 4 for l in optional_groupwise_layers}
def create\_RepVGG\_A0(deploy=False):
return RepVGG(num_blocks=[2, 4, 14, 1], num_classes=1000,
width_multiplier=[0.75, 0.75, 0.75, 2.5], override_groups_map=None, deploy=deploy)
def create\_RepVGG\_A1(deploy=False):
return RepVGG(num_blocks=[2, 4, 14, 1], num_classes=1000,
width_multiplier=[1, 1, 1, 2.5], override_groups_map=None, deploy=deploy)
def create\_RepVGG\_A2(deploy=False):
return RepVGG(num_blocks=[2, 4, 14, 1], num_classes=1000,
width_multiplier=[1.5, 1.5, 1.5, 2.75], override_groups_map=None, deploy=deploy)
def create\_RepVGG\_B0(deploy=False):
return RepVGG(num_blocks=[4, 6, 16, 1], num_classes=1000,
width_multiplier=[1, 1, 1, 2.5], override_groups_map=None, deploy=deploy)
def create\_RepVGG\_B1(deploy=False):
return RepVGG(num_blocks=[4, 6, 16, 1], num_classes=1000,
width_multiplier=[2, 2, 2, 4], override_groups_map=None, deploy=deploy)
def create\_RepVGG\_B1g2(deploy=False):
return RepVGG(num_blocks=[4, 6, 16, 1], num_classes=1000,
width_multiplier=[2, 2, 2, 4], override_groups_map=g2_map, deploy=deploy)
def create\_RepVGG\_B1g4(deploy=False):
return RepVGG(num_blocks=[4, 6, 16, 1], num_classes=1000,
width_multiplier=[2, 2, 2, 4], override_groups_map=g4_map, deploy=deploy)
def create\_RepVGG\_B2(deploy=False):
return RepVGG(num_blocks=[4, 6, 16, 1], num_classes=1000,
width_multiplier=[2.5, 2.5, 2.5, 5], override_groups_map=None, deploy=deploy)
def create\_RepVGG\_B2g2(deploy=False):
return RepVGG(num_blocks=[4, 6, 16, 1], num_classes=1000,
width_multiplier=[2.5, 2.5, 2.5, 5], override_groups_map=g2_map, deploy=deploy)
def create\_RepVGG\_B2g4(deploy=False):
return RepVGG(num_blocks=[4, 6, 16, 1], num_classes=1000,
width_multiplier=[2.5, 2.5, 2.5, 5], override_groups_map=g4_map, deploy=deploy)
def create\_RepVGG\_B3(deploy=False):
return RepVGG(num_blocks=[4, 6, 16, 1], num_classes=1000,
width_multiplier=[3, 3, 3, 5], override_groups_map=None, deploy=deploy)
def create\_RepVGG\_B3g2(deploy=False):
return RepVGG(num_blocks=[4, 6, 16, 1], num_classes=1000,
width_multiplier=[3, 3, 3, 5], override_groups_map=g2_map, deploy=deploy)
def create\_RepVGG\_B3g4(deploy=False):
return RepVGG(num_blocks=[4, 6, 16, 1], num_classes=1000,
width_multiplier=[3, 3, 3, 5], override_groups_map=g4_map, deploy=deploy)
# Use like this:
# train\_model = create\_RepVGG\_A0(deploy=False)
# train train\_model
# deploy\_model = repvgg\_convert(train\_model, create\_RepVGG\_A0, save\_path='repvgg\_deploy.pth')
def repvgg\_convert(model:torch.nn.Module, build\_func, save\_path=None):
converted_weights = {}
for name, module in model.named_modules():
if hasattr(module, 'repvgg\_convert'):
kernel, bias = module.repvgg_convert()
converted_weights[name + '.rbr\_reparam.weight'] = kernel
converted_weights[name + '.rbr\_reparam.bias'] = bias
elif isinstance(module, torch.nn.Linear):
converted_weights[name + '.weight'] = module.weight.detach().cpu().numpy()
converted_weights[name + '.bias'] = module.bias.detach().cpu().numpy()
else:
print(name, type(module))
del model
deploy_model = build_func(deploy=True)
for name, param in deploy_model.named_parameters():
print('deploy param: ', name, param.size(), np.mean(converted_weights[name]))
param.data = torch.from_numpy(converted_weights[name]).float()
if save_path is not None and save_path.endswith('pth'):
torch.save(deploy_model.state_dict(), save_path)
return deploy_model
- 实验结果
4.1 ImageNet分类
4.2 语义分割
5 参考
[1].RepVGG: Making VGG-style ConvNets Great Again.
[2].https://github.com/DingXiaoH/RepVGG.
原文获取方式,扫描下方二维码
回复【
RepVGG
】即可获取论文与源码
声明:转载请说明出处
扫描下方二维码关注【AI人工智能初学者】公众号,获取更多实践项目源码和论文解读,非常期待你我的相遇,让我们以梦为马,砥砺前行!!!
点“在看”给我一朵小黄花呗