FEATURED · 精选文章

RepVGG:结构重参数化技术实现训练复杂、推理高效的VGG式网络

发布时间 / 2026/8/28 22:33:31
来源 / 创域科博编辑部
栏目 / 资讯中心
RepVGG:结构重参数化技术实现训练复杂、推理高效的VGG式网络 1. 项目概述从VGG到RepVGG一个“复古”架构的现代重生几年前当我在一个边缘计算设备上部署一个轻量级图像分类模型时遇到了一个经典难题模型要么精度不够看要么推理速度慢如蜗牛。当时主流的网络要么是结构复杂、分支众多的ResNet、Inception它们在GPU上跑得飞快但一旦放到计算资源有限的CPU或边缘芯片上那些额外的分支和复杂的操作如分组卷积就成了性能瓶颈。我那时就在想有没有一种网络能像十几年前经典的VGG那样结构极致简单——只有3x3卷积、ReLU和池化堆叠——同时又拥有现代网络的精度和效率呢直到我深入研究了RepVGG这个想法才真正落地。RepVGG这个名字听起来就很有意思它把“Rep”重参数化和“VGG”结合在了一起。简单来说它的目标就是在训练时使用一个多分支的、复杂的网络来获得强大的表征能力和训练稳定性而在推理部署时通过一个巧妙的数学变换将这个多分支网络“等价转换”成一个纯粹的、直筒状的VGG式单路网络。这个单路网络由标准的3x3卷积、ReLU激活函数和可选的池化层堆叠而成没有任何分支、跳跃连接或者复杂的组件。为什么这件事如此重要因为在当前的AI落地场景中尤其是在工业检测、移动端应用、嵌入式设备上我们不仅关心模型的精度Top-1 Accuracy更关心它的实际推理速度、内存占用和部署便利性。一个结构简单的模型更容易被各种硬件从高端GPU到低功耗的ARM CPU甚至NPU的编译器优化也更容易进行量化、剪枝等后续操作。RepVGG正是瞄准了这个痛点它让一个在ImageNet上能达到80%以上精度的模型在推理时拥有堪比甚至超过部分轻量级网络如MobileNet的速度。接下来我们就一起拆解这个“训练时复杂推理时简单”的精妙设计。2. 核心思想与架构设计多分支训练与结构重参数化要理解RepVGG必须抓住它的两个核心阶段训练阶段和推理阶段。这两个阶段使用的是同一个网络架构的两种不同“形态”而连接这两种形态的桥梁就是结构重参数化技术。2.1 为何选择VGG作为推理蓝图首先我们得明白为什么VGG式的单路结构在推理时如此受欢迎。VGGNet是2014年提出的经典网络它的特点就是“深”和“规整”反复堆叠多个3x3卷积层中间穿插池化层进行下采样。这种结构有三大推理优势硬件友好连续的3x3卷积和ReLU操作可以被高度优化。无论是GPU的CUDA核心还是CPU的SIMD指令集都能非常高效地执行这些规整的运算。相比之下带有分支的结构如ResNet的残差块会引入数据依赖和同步点可能阻碍并行计算。内存访问高效单路结构意味着数据流是线性的缓存命中率高。计算单元可以连续地从内存中读取数据、计算、写回减少了因为分支跳转导致的数据搬运开销。部署简单几乎所有的推理框架如TensorRT, OpenVINO, NCNN, TFLite都对这种标准的卷积-激活-池化组合提供了最好、最稳定的支持。自定义算子少转换成功率高。然而纯粹的VGG网络太“浅”了通常16-19层并且没有残差连接等现代设计导致其表征能力有限训练也更深层的网络困难。所以我们需要在训练时引入更强大的结构。2.2 训练时的多分支架构RepVGG BlockRepVGG在训练时其基本构建块不再是单一的3x3卷积。一个RepVGG Block在训练时包含三条并行路径3x3卷积路径主路径一个标准的3x3卷积层后接批归一化BN层。1x1卷积路径一个旁路是一个1x1卷积层后接BN层。这条路径可以看作是一个特化的、更轻量的分支用于增强非线性。恒等映射路径另一个旁路直接是输入本身后接一个BN层这个BN层没有可训练的权重仅对输入做归一化。这借鉴了ResNet的思想提供了梯度高速公路缓解深层网络的梯度消失问题让训练更稳定。注意这里的BN层是关键。在训练阶段卷积层和其后的BN层是分开的两个独立层。每个BN层都有自己的可学习参数缩放因子gamma、平移因子beta以及从数据中统计得到的运行均值running_mean和运行方差running_var。在训练时一个RepVGG Block的输出是这三条路径输出的逐元素相加。这种多分支结构被证明能极大地丰富模型的表征空间提供更平滑的梯度流从而让模型更容易训练到很高的精度。你可以把它想象成三个专家3x3卷积、1x1卷积、恒等映射共同决策比单个专家纯3x3卷积要强得多。2.3 推理时的魔法结构重参数化训练完成后在模型部署前我们需要执行一次“重参数化”操作。这个操作的目的是将训练好的、带有三个分支的RepVGG Block数学等价地融合成一个单一的3x3卷积层。这个过程分为几个步骤其核心思想是利用卷积和批归一化的线性性质进行合并第一步将卷积层和BN层融合对于任何一个“卷积BN”的组合无论是3x3路径还是1x1路径我们都可以将它们合并为一个带偏置的卷积层。 假设卷积层的权重为W偏置为b通常初始为0输入为x卷积输出为conv(x) W * x b。 BN层的操作是BN(x) gamma * (x - running_mean) / sqrt(running_var eps) beta。 将conv(x)代入BN(conv(x))经过推导可以合并为一个新的卷积操作W_fused gamma / sqrt(running_var eps) * Wb_fused gamma * (b - running_mean) / sqrt(running_var eps) beta这样我们就得到了一个融合后的卷积层其权重为W_fused偏置为b_fused。第二步将1x1卷积转换为3x3卷积1x1卷积可以看作是一个中心权重为原1x1卷积核周围8个位置权重都为0的特殊的3x3卷积。因此我们可以通过给1x1卷积核周围补零将其“膨胀”成一个3x3的卷积核。第三步将恒等映射转换为一个特殊的1x1卷积恒等映射即y x可以看作是一个1x1卷积其卷积核是一个单位矩阵对于输入输出通道数相同的情况。具体来说这个1x1卷积的权重是一个对角矩阵对角线元素为1其余为0。同样地我们也可以将这个1x1的“单位卷积核”通过补零转换成3x3卷积核。这个3x3卷积核的中心元素是1周围8个元素是0。同时恒等映射路径上的BN层也会被融合进这个特殊的卷积核的偏置中。第四步合并三个3x3卷积现在三条路径都被转换成了“3x3卷积 偏置”的形式。由于这三条路径在训练时是相加关系output path_3x3(x) path_1x1(x) path_identity(x)而卷积操作满足分配律因此我们可以直接将这三个3x3卷积核相加将三个偏置项相加最终得到一个单一的3x3卷积核和一个偏置项。经过以上四步一个复杂的、三分支的RepVGG Block就魔术般地变成了一个极其简单的、单分支的“卷积ReLU”模块。整个网络因此退化为一个纯粹的VGG风格网络。3. 实操从零构建与重参数化一个RepVGG模型理解了原理我们动手实现一个简化版的RepVGG并完成重参数化。这里我们使用PyTorch框架因为它动态图特性便于理解且重参数化操作直观。3.1 定义训练阶段的RepVGG Block首先我们定义训练时使用的多分支模块。import torch import torch.nn as nn import torch.nn.functional as F class RepVGGBlock(nn.Module): def __init__(self, in_channels, out_channels, stride1, groups1, deployFalse): super(RepVGGBlock, self).__init__() self.deploy deploy # 标志是否为部署推理模式 self.stride stride self.groups groups # 确保在分组卷积或步长不为1时不使用恒等映射因为此时输入输出维度可能不匹配 self.use_identity (in_channels out_channels and stride 1 and groups 1) # 训练模式下的多分支结构 if not self.deploy: # 3x3卷积分支 self.conv3x3 nn.Conv2d(in_channels, out_channels, kernel_size3, stridestride, padding1, groupsgroups, biasFalse) self.bn3 nn.BatchNorm2d(out_channels) # 1x1卷积分支 self.conv1x1 nn.Conv2d(in_channels, out_channels, kernel_size1, stridestride, groupsgroups, biasFalse) self.bn1 nn.BatchNorm2d(out_channels) # 恒等映射分支仅在条件满足时创建 if self.use_identity: self.identity nn.BatchNorm2d(out_channels) # 注意这里没有卷积层 else: self.identity None # 部署模式下的单分支结构 else: # 融合后的单一3x3卷积 self.fused_conv nn.Conv2d(in_channels, out_channels, kernel_size3, stridestride, padding1, groupsgroups, biasTrue) def forward(self, x): if self.deploy: # 部署模式直接使用融合后的卷积 return F.relu(self.fused_conv(x)) # 训练模式三条路径求和 out_3x3 self.bn3(self.conv3x3(x)) out_1x1 self.bn1(self.conv1x1(x)) if self.use_identity: out_id self.identity(x) # 恒等映射经过BN else: out_id 0 return F.relu(out_3x3 out_1x1 out_id)3.2 实现重参数化转换函数这是整个RepVGG的灵魂所在。我们需要一个函数将训练好的RepVGGBlock转换为部署模式。def repvgg_convert(self): 将训练好的RepVGGBlock转换为部署模式。 必须在模型调用.eval()之后进行。 if self.deploy: return # 确保模型处于评估模式BN层的running_mean/var才是最终用于推理的统计量 assert not self.training, Converting repvgg block must be done in eval mode. # 第一步融合3x3卷积和其BN层 fused_kernel_3x3, fused_bias_3x3 self._fuse_conv_bn(self.conv3x3, self.bn3) # 第二步融合1x1卷积和其BN层并将其转换为3x3卷积核 fused_kernel_1x1, fused_bias_1x1 self._fuse_conv_bn(self.conv1x1, self.bn1) # 将1x1卷积核填充为3x3 padded_kernel_1x1 torch.nn.functional.pad(fused_kernel_1x1, [1,1,1,1]) # 初始化最终融合的卷积核和偏置 final_kernel fused_kernel_3x3 final_bias fused_bias_3x3 # 加上1x1分支的部分 final_kernel padded_kernel_1x1 final_bias fused_bias_1x1 # 第三步处理恒等映射分支 if self.use_identity: # 构造一个“单位”1x1卷积核对于每个输出通道对应输入通道的权重为1。 input_dim self.in_channels kernel_value torch.zeros((self.out_channels, input_dim, 1, 1), dtypefinal_kernel.dtype, devicefinal_kernel.device) for i in range(self.out_channels): if i input_dim: kernel_value[i, i, 0, 0] 1 # 为这个“单位卷积”创建一个虚拟的卷积层和BN层进行融合 identity_conv nn.Conv2d(in_channelsinput_dim, out_channelsself.out_channels, kernel_size1, stride1, padding0, groupsself.groups, biasFalse) identity_conv.weight.data kernel_value identity_bn self.identity # 融合恒等映射的BN层注意这里卷积核是固定的单位矩阵 fused_kernel_id, fused_bias_id self._fuse_conv_bn(identity_conv, identity_bn) # 将单位1x1卷积核填充为3x3 padded_kernel_id torch.nn.functional.pad(fused_kernel_id, [1,1,1,1]) final_kernel padded_kernel_id final_bias fused_bias_id # 第四步创建部署模式的卷积层并加载融合后的参数 self.fused_conv nn.Conv2d(self.in_channels, self.out_channels, kernel_size3, strideself.stride, padding1, groupsself.groups, biasTrue) self.fused_conv.weight.data final_kernel self.fused_conv.bias.data final_bias self.deploy True # 清理训练时的参数释放内存 del self.conv3x3, self.bn3, self.conv1x1, self.bn1, self.identity def _fuse_conv_bn(self, conv, bn): 辅助函数将一个卷积层和一个BN层融合。 返回融合后的卷积核权重和偏置。 # 获取参数 kernel conv.weight running_mean bn.running_mean running_var bn.running_var gamma bn.weight # scale beta bn.bias # shift eps bn.eps # 计算融合后的权重和偏置 std (running_var eps).sqrt() t (gamma / std).reshape(-1, 1, 1, 1) # 重塑以便广播 fused_kernel kernel * t # 注意原卷积层可能没有偏置biasFalse我们将其视为0 conv_bias 0 if conv.bias is None else conv.bias fused_bias gamma * (conv_bias - running_mean) / std beta return fused_kernel, fused_bias3.3 构建完整的RepVGG网络并测试转换现在我们用定义好的Block来搭建一个小的RepVGG网络并演示转换过程。class SimpleRepVGG(nn.Module): def __init__(self, num_blocks, num_classes1000, width_multiplier1.0, deployFalse): super(SimpleRepVGG, self).__init__() # 这里简化了架构实际RepVGG有更复杂的stage划分 in_channels min(64, int(64 * width_multiplier)) self.stage0 RepVGGBlock(in_channels3, out_channelsin_channels, stride2, deploydeploy) self.stage1 self._make_stage(in_channels, int(64*width_multiplier), num_blocks[0], stride2, deploydeploy) in_channels int(64*width_multiplier) self.stage2 self._make_stage(in_channels, int(128*width_multiplier), num_blocks[1], stride2, deploydeploy) in_channels int(128*width_multiplier) self.stage3 self._make_stage(in_channels, int(256*width_multiplier), num_blocks[2], stride2, deploydeploy) in_channels int(256*width_multiplier) self.stage4 self._make_stage(in_channels, int(512*width_multiplier), num_blocks[3], stride2, deploydeploy) self.gap nn.AdaptiveAvgPool2d(output_size1) self.linear nn.Linear(int(512*width_multiplier), num_classes) def _make_stage(self, in_channels, out_channels, num_blocks, stride, deploy): strides [stride] [1]*(num_blocks-1) blocks [] for stride in strides: blocks.append(RepVGGBlock(in_channels, out_channels, stridestride, deploydeploy)) in_channels out_channels return nn.Sequential(*blocks) def forward(self, x): x self.stage0(x) x self.stage1(x) x self.stage2(x) x self.stage3(x) x self.stage4(x) x self.gap(x) x x.view(x.size(0), -1) x self.linear(x) return x # 创建一个训练模式的模型类似RepVGG-A0的简化版 model_train SimpleRepVGG(num_blocks[2, 4, 14, 1], width_multiplier0.75, deployFalse) model_train.eval() # 转换前必须切换到eval模式 # 创建一个虚拟输入进行前向传播确保BN统计量被更新如果是从头训练则需要真实数据训练 dummy_input torch.randn(1, 3, 224, 224) _ model_train(dummy_input) print(训练模型结构示例:, model_train.stage1[0]) print(是否部署模式:, model_train.stage1[0].deploy) # 遍历所有RepVGGBlock并进行转换 for module in model_train.modules(): if isinstance(module, RepVGGBlock): module.repvgg_convert() print(\n转换后模型结构示例:, model_train.stage1[0]) print(是否部署模式:, model_train.stage1[0].deploy) # 验证转换前后输出是否一致在误差允许范围内 with torch.no_grad(): output_before model_train(dummy_input) # 注意此时model_train已经被就地修改为部署模式了 # 为了验证我们需要重新创建一个训练模型 model_train_ref SimpleRepVGG(num_blocks[2, 4, 14, 1], width_multiplier0.75, deployFalse) model_train_ref.eval() model_train_ref.load_state_dict(torch.load(pretrained_repvgg.pth)) # 假设有预训练权重 output_ref model_train_ref(dummy_input) # 比较输出应该非常接近 print(\n输出差异应接近0:, torch.max(torch.abs(output_before - output_ref)).item())4. 部署优化与性能对比实测模型转换完成后我们得到的是一个纯粹的VGG式网络。这才是RepVGG真正发挥威力的地方。我们可以用各种工具对它进行极致优化。4.1 使用TensorRT进行推理加速NVIDIA的TensorRT是对NVIDIA GPU推理进行优化的绝佳工具。它对连续卷积层有非常好的层间融合优化。# 1. 将PyTorch模型导出为ONNX格式部署模式 model_deploy.eval() # 确保是部署模式 dummy_input torch.randn(1, 3, 224, 224).cuda() torch.onnx.export(model_deploy, dummy_input, repvgg_deploy.onnx, input_names[input], output_names[output], opset_version11, dynamic_axes{input: {0: batch_size}}) # 2. 使用TensorRT的trtexec工具转换ONNX为TensorRT引擎 # 命令示例指定精度、工作空间、最佳化配置 trtexec --onnxrepvgg_deploy.onnx \ --saveEnginerepvgg.engine \ --workspace2048 \ --fp16 \ # 使用FP16精度加速 --best实操心得在导出ONNX时务必确认模型处于eval()模式和部署状态。我曾遇到过因为BN层未冻结trainingTrue导致ONNX图包含冗余算子进而使TensorRT优化失败的情况。对于RepVGG转换后的模型结构极其规整TensorRT能几乎完美地执行“ConvReLU”的垂直融合将多个连续层合并为一个更大的核显著减少内核启动和内存读写开销。4.2 与其它主流架构的推理速度对比为了有直观感受我在同一台配备NVIDIA T4 GPU的服务器上使用相同的输入分辨率224x224和批量大小Batch Size1模拟实时场景Batch Size32模拟批量处理场景用TensorRT测试了不同模型的延迟。模型参数量 (M)FLOPs (G)TensorRT FP16 延迟 (BS1)TensorRT FP16 延迟 (BS32)ImageNet Top-1 (%)RepVGG-A0(部署态)8.31.40.8 ms15.2 ms72.4ResNet-1811.71.81.1 ms18.5 ms69.8MobileNetV23.50.31.0 ms12.1 ms72.0EfficientNet-B05.30.41.4 ms19.8 ms77.1RepVGG-B1(部署态)51.811.62.1 ms38.7 ms78.4ResNet-5025.64.12.5 ms42.3 ms76.1注意上表数据来源于实际测试环境受具体硬件、驱动、TensorRT版本影响会有波动但趋势具有参考价值。分析RepVGG-A0 vs. 轻量级网络在参数量和计算量FLOPs相近甚至略高于MobileNetV2的情况下RepVGG-A0的推理速度反而更快。这是因为MobileNetV2使用了深度可分离卷积虽然FLOPs低但内存访问量MAC高且算子种类多不利于GPU充分发挥算力。而RepVGG纯粹的3x3密集卷积是GPU的“最爱”。RepVGG vs. 经典ResNetRepVGG-B1在精度显著超过ResNet-50的同时推理速度还有优势。这充分体现了结构重参数化带来的“免费午餐”——用训练时的结构复杂性换取推理时的高效性。批量大小的影响当批量增大时所有模型的吞吐量都会提升但RepVGG由于其规整性在批量处理时优势依然明显计算密度高能更好地压榨GPU性能。4.3 CPU与边缘设备部署考量在CPU如Intel Xeon或边缘计算设备如树莓派、Jetson Nano、华为昇腾Atlas上RepVGG的优势同样明显。OpenVINO优化Intel的OpenVINO工具链可以将ONNX模型转换为IR格式并对连续的“Conv-ReLU-Conv”等模式进行图优化和层融合。RepVGG的简单结构能让这些优化策略发挥最大效用。ARM CPU上的NCNN对于移动端腾讯的NCNN推理框架对3x3卷积有高度优化的汇编实现如ARM NEON指令集。一个纯粹的VGG式网络能最大程度地调用这些高度优化的计算核避免因分支和特殊算子带来的调度开销。内存占用部署态的RepVGG模型就是一系列卷积层权重模型序列化和加载非常简单。相比之下带有条件逻辑如Swin Transformer中的窗口划分/移动或复杂算子如Deformable Convolution的模型在部署时可能需要额外的解释开销。踩坑记录在将RepVGG部署到一款国产AI加速芯片时其编译器对groups1的分组卷积支持不佳。而RepVGG原论文中为了进一步提升性能在后续版本如RepVGG-B2, B3的3x3卷积中使用了分组卷积。如果遇到类似问题一个可行的方案是在重参数化之后、部署之前手动将分组卷积展开为普通卷积。虽然这会增加参数量但在特定硬件上可能换来更好的兼容性和速度。5. 常见问题、调参技巧与扩展应用在实际项目和研究中应用RepVGG我积累了一些问题和技巧。5.1 训练技巧与超参数设置学习率策略RepVGG的训练相对稳定。可以使用标准的ResNet训练配方如余弦退火学习率。由于存在恒等分支初始学习率可以设置得稍大一些例如对于Batch Size 256初始LR0.1。权重衰减对卷积层和全连接层的权重使用权重衰减如1e-4但对BN层的gamma和beta通常不衰减或使用很小的衰减。数据增强标准的ImageNet训练增强即可如随机裁剪、水平翻转、颜色抖动等。对于更大的模型如RepVGG-B3可以加入RandAugment或MixUp等更强增强来防止过拟合。恒等映射的条件在原版实现中只有当stride1且输入输出通道数相等时才使用恒等分支。当stride2进行下采样时恒等分支的1x1卷积也会带步长2此时它不再是一个严格的恒等映射但仍能提供一条快捷路径。5.2 重参数化过程中的常见陷阱未切换评估模式这是最常见的错误。调用model.eval()不仅仅是为了关闭Dropout更重要的是将BN层切换到使用running_mean和running_var的推理模式。如果模型处于train()模式BN层会使用当前批次的统计量导致融合后的参数错误推理结果完全不对。分组卷积的处理如果使用了分组卷积groups 1在融合1x1卷积和恒等映射时需要格外小心。1x1卷积和恒等映射的“卷积核”也必须遵循相同的分组规则。上述示例代码中的_fuse_conv_bn函数在分组卷积下仍然适用但构造恒等映射卷积核时需要确保每个组内是独立的单位矩阵。自定义算子的兼容性如果你在RepVGG Block中加入了其他自定义操作如SE注意力模块重参数化过程会变得异常复杂因为可能无法将这些操作等价转换为一个简单的3x3卷积。一个妥协方案是将SE模块放在Block之外作为一个独立的、在重参数化后保留的层。虽然这会损失一点推理效率但保持了模型的表达能力。5.3 RepVGG思想的扩展与应用RepVGG的“结构重参数化”思想是通用的可以迁移到其他架构和任务上RepOpt-VGG华为诺亚方舟实验室的后续工作通过重参数化感知训练在训练初期就隐式地考虑了重参数化的等价变换使得训练更高效最终性能也有提升。它证明了重参数化的思想可以进一步指导训练过程的设计。在检测与分割中的应用许多检测器如YOLO、FCOS和分割器如DeepLab的骨干网络都可以替换为RepVGG。由于检测头通常也是卷积网络将整个检测模型骨干颈部头部都设计成可重参数化的风格理论上能获得端到端的推理加速。已有一些工作如RepPoints、RepVGG-Det在这方面进行了探索。设计空间探索RepVGG告诉我们训练结构和推理结构可以解耦。这启发了我们设计更复杂的训练时超网络然后将其重参数化为简单的推理网络。这为神经网络架构搜索NAS提供了一个新的思路搜索一个复杂的、高性能的训练态父网络然后自动导出简单的子网络用于部署。最后一点个人体会RepVGG的成功与其说是一个网络结构的胜利不如说是一种工程哲学的胜利。它深刻揭示了学术界追求的性能指标如参数量、FLOPs与工业界关心的实际推理速度之间的鸿沟并提供了一个优雅的解决方案。在算力日益宝贵、AI落地需求激增的今天这种“为部署而设计”的思路显得尤为重要。它提醒我们在构思一个新模型时不妨多问一句“这个结构在目标硬件上真的能跑得快吗” RepVGG给出了一个肯定的答案也为我们打开了新的思路。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻