1. 简介
RepVGG 是 2021 年由清华大学提出的一种高效卷积神经网络架构,其核心思想是通过 ** 结构重参数化(Structural Re-parameterization)** 技术,将训练时的多分支结构转换为推理时的单路结构,在保持 VGG 式简单架构优势的同时,实现接近 ResNet 的性能。该架构在 ImageNet 上达到了 84.1% 的 Top-1 准确率,同时具有极低的推理延迟,特别适合硬件部署。
2. 核心创新:结构重参数化(Structural Re-parameterization)
RepVGG 的核心是训练时多分支,推理时单路的设计:
-
训练时结构:
- 每个 RepVGG 模块包含三个并行分支:
- 主分支:3×3 卷积 + BN
- 短接分支:1×1 卷积 + BN(相当于 ResNet 中的残差连接)
- 恒等分支:Identity + BN(仅在第一层之后存在)
- 多分支结构增强了模型的表达能力,类似于 ResNet
- 每个 RepVGG 模块包含三个并行分支:
-
推理时结构:
- 通过参数等价转换,将三个分支合并为一个 3×3 卷积层
- 推理时完全没有分支,成为类似 VGG 的全 3×3 卷积堆叠结构
- 单路结构在硬件上执行效率更高,减少内存访问和计算开销
3. 结构重参数化的数学原理
RepVGG 的结构重参数化基于以下数学等价性:
-
BN 层融合到卷积层:
- 对于卷积输出 \(y = W*x + b\) 和 BN 操作 \(z = \gamma\frac{y-\mu}{\sqrt{\sigma^2+\epsilon}} + \beta\)
- 可以等价转换为新的卷积参数:\(W' = \frac{\gamma}{\sqrt{\sigma^2+\epsilon}}W\) 和 \(b' = \gamma\frac{b-\mu}{\sqrt{\sigma^2+\epsilon}} + \beta\)
-
1×1 卷积转换为 3×3 卷积:
- 1×1 卷积核可以通过零填充(zero-padding)转换为 3×3 卷积核
- 例如,1×1 卷积核 \(W_{1x1}\) 转换为 3×3 卷积核 \(W_{3x3}\),其中 \(W_{3x3}\) 的中心区域为 \(W_{1x1}\),其余位置为 0
-
Identity 转换为 3×3 卷积:
- Identity 映射可以表示为中心值为 1,其余为 0 的 3×3 卷积核
- 例如,3×3 的 Identity 卷积核为:\(\begin{bmatrix} 0 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 0 \end{bmatrix}\)
-
多分支合并:
- 将三个分支的 3×3 卷积核相加,得到最终的 3×3 卷积核
- 将三个分支的偏置项相加,得到最终的偏置
4. RepVGG 的网络架构
RepVGG 采用了类似 VGG 的深度堆叠结构,主要由多个 RepVGG 模块组成:
-
基本模块:
- 每个模块包含多个 RepVGG 块,每个块内部通过结构重参数化实现多分支训练、单路推理
- 不同阶段的模块使用不同数量的通道和块
-
变体设计:
- RepVGG-A/B 系列:通过调整通道数和网络深度设计不同规模的模型
- 例如,RepVGG-A0 适合移动端,RepVGG-B3 适合高性能服务器
-
对比 VGG 和 ResNet:
- 相比 VGG:RepVGG 在训练时引入多分支结构,显著提升性能
- 相比 ResNet:RepVGG 在推理时没有分支,减少了内存访问和计算分支,速度更快
5. 性能优势
-
精度与效率的平衡:
- 在 ImageNet 上,RepVGG-B1g4 达到 83.5% 准确率,推理速度比 ResNet-50 快 83%
- RepVGG-B3 达到 84.1% 准确率,接近 Swin Transformer 等复杂模型
-
硬件友好:
- 单路 3×3 卷积结构在 GPU、FPGA 等硬件上执行效率高
- 减少内存访问成本(MACs),降低延迟
-
训练与推理的解耦:
- 训练时使用复杂结构获得高性能
- 推理时转换为简单结构实现高效率
6. 代码实现示例
以下是 RepVGG 模块的 PyTorch 实现示例,包括训练时的多分支结构和推理时的结构重参数化:
import torch
import torch.nn as nn
import torch.nn.functional as F
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 # 是否为推理模式
if deploy:
# 推理时使用单一3×3卷积
self.conv = nn.Conv2d(in_channels, out_channels,
kernel_size, stride, padding, dilation,
groups, bias=True, padding_mode=padding_mode)
else:
# 训练时使用多分支结构
self.conv3x3 = nn.Conv2d(in_channels, out_channels,
kernel_size, stride, padding, dilation,
groups, bias=False, padding_mode=padding_mode)
self.bn3x3 = nn.BatchNorm2d(out_channels)
# 1×1卷积分支
self.conv1x1 = nn.Conv2d(in_channels, out_channels,
1, stride, padding, dilation,
groups, bias=False, padding_mode=padding_mode)
self.bn1x1 = nn.BatchNorm2d(out_channels)
# Identity分支(仅当输入输出通道数和步长满足条件时存在)
if out_channels == in_channels and stride == 1:
self.bn_identity = nn.BatchNorm2d(out_channels)
else:
self.bn_identity = None
def forward(self, x):
if self.deploy:
return self.conv(x)
else:
# 计算三个分支的输出并相加
out3x3 = self.bn3x3(self.conv3x3(x))
out1x1 = self.bn1x1(self.conv1x1(x))
if self.bn_identity is not None:
out = out3x3 + out1x1 + self.bn_identity(x)
else:
out = out3x3 + out1x1
return F.relu(out)
def get_equivalent_kernel_bias(self):
# 将三个分支合并为一个3×3卷积
kernel3x3, bias3x3 = self._fuse_bn_tensor(self.conv3x3, self.bn3x3)
kernel1x1, bias1x1 = self._fuse_bn_tensor(self.conv1x1, self.bn1x1)
if self.bn_identity is not None:
input_dim = self.conv3x3.in_channels // self.conv3x3.groups
kernel_identity = torch.zeros(
(self.conv3x3.out_channels, input_dim, self.conv3x3.kernel_size[0],
self.conv3x3.kernel_size[1]), dtype=kernel3x3.dtype, device=kernel3x3.device
)
for i in range(self.conv3x3.out_channels):
kernel_identity[i, i % input_dim, self.conv3x3.kernel_size[0]//2,
self.conv3x3.kernel_size[1]//2] = 1
kernel_identity, bias_identity = self._fuse_bn_tensor(kernel_identity, self.bn_identity)
else:
kernel_identity, bias_identity = 0, 0
# 合并三个分支的参数
kernel = kernel3x3 + self._pad_1x1_to_3x3_tensor(kernel1x1) + kernel_identity
bias = bias3x3 + bias1x1 + bias_identity
return kernel, bias
def _pad_1x1_to_3x3_tensor(self, kernel1x1):
if kernel1x1 is None:
return 0
else:
return F.pad(kernel1x1, [1, 1, 1, 1])
def _fuse_bn_tensor(self, conv, bn):
# 将BN层参数融合到卷积层
if isinstance(conv, torch.Tensor):
kernel = conv
running_mean = torch.zeros_like(bn.running_mean)
running_var = torch.ones_like(bn.running_var)
gamma = bn.weight
beta = bn.bias
eps = bn.eps
else:
kernel = conv.weight
running_mean = bn.running_mean
running_var = bn.running_var
gamma = bn.weight
beta = bn.bias
eps = bn.eps
std = (running_var + eps).sqrt()
t = (gamma / std).reshape(-1, 1, 1, 1)
return kernel * t, beta - gamma * running_mean / std
def switch_to_deploy(self):
# 将模型从训练模式转换为推理模式
if self.deploy:
return
kernel, bias = self.get_equivalent_kernel_bias()
self.conv = nn.Conv2d(self.conv3x3.in_channels, self.conv3x3.out_channels,
self.conv3x3.kernel_size, self.conv3x3.stride,
self.conv3x3.padding, self.conv3x3.dilation,
self.conv3x3.groups, bias=True)
# 设置卷积层的参数
self.conv.weight.data = kernel
self.conv.bias.data = bias
# 删除不需要的模块
for para in self.parameters():
para.detach_()
self.__delattr__('conv3x3')
self.__delattr__('bn3x3')
self.__delattr__('conv1x1')
self.__delattr__('bn1x1')
if hasattr(self, 'bn_identity'):
self.__delattr__('bn_identity')
self.deploy = True
# 构建完整的RepVGG网络
class RepVGG(nn.Module):
def __init__(self, num_blocks, num_classes=1000, width_multiplier=None, deploy=False):
super(RepVGG, self).__init__()
assert len(width_multiplier) == 4
self.deploy = deploy
self.in_planes = min(64, int(64 * width_multiplier[0]))
# 第一个卷积层
self.stage0 = RepVGGBlock(3, 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.AdaptiveAvgPool2d(output_size=1)
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:
blocks.append(RepVGGBlock(self.in_planes, planes, kernel_size=3,
stride=stride, padding=1, deploy=self.deploy))
self.in_planes = planes
self.cur_layer_idx += 1
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模型
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], deploy=deploy)
def create_RepVGG_B1(deploy=False):
return RepVGG(num_blocks=[4, 6, 16, 1], num_classes=1000,
width_multiplier=[1, 1, 1, 2.5], deploy=deploy)
# 更多模型变体...
7. 面试常见问题
Q1:RepVGG 的核心创新点是什么? A1: RepVGG 的核心是结构重参数化(Structural Re-parameterization),它在训练时使用多分支结构(类似 ResNet)提高模型表达能力,在推理时将多分支合并为单一 3×3 卷积(类似 VGG),从而在保持高性能的同时显著提升推理效率。
Q2:为什么 RepVGG 的推理速度比 ResNet 快? A2: ResNet 的残差结构存在分支,需要分别计算主路径和短接路径,然后再合并,这增加了内存访问和计算开销。而 RepVGG 在推理时没有分支,完全是 3×3 卷积的线性堆叠,更适合硬件并行计算,减少了分支跳转和内存访问,因此速度更快。
Q3:如何将训练好的 RepVGG 模型转换为推理模型? A3: 转换过程基于参数等价性:
- 将每个分支的 BN 层参数融合到卷积层中
- 将 1×1 卷积核通过零填充转换为 3×3 卷积核
- 将 Identity 映射转换为 3×3 卷积核
- 将三个分支的 3×3 卷积核和偏置分别相加,得到最终的 3×3 卷积层参数
8. 总结
RepVGG 通过结构重参数化技术,巧妙地结合了 VGG 式简单架构的推理效率和 ResNet 式多分支架构的训练性能,为高效神经网络设计提供了新思路。该架构特别适合需要高性能部署的场景,如移动设备、边缘计算等。理解 RepVGG 的设计理念和实现方法,对于优化深度学习模型在实际应用中的性能具有重要意义。