RepVGG重参数化卷积 (Re-parameterization VGG)
!abstract 论文信息
- 标题: RepVGG: Making VGG-style ConvNets Great Again
- 作者: Xiaohan Ding, Xiangyu Zhang, Ningning Ma, Jungong Han, Guiguang Ding, Jian Sun
- 年份: 2021
- 会议: CVPR 2021
- 论文链接: https://arxiv.org/abs/2101.03697
- 核心贡献: 训练时多分支,推理时单分支,兼顾精度和速度
一、核心思想
RepVGG的核心: 训练时使用多分支提升精度,推理时融合为单分支提升速度
训练阶段 vs 推理阶段:
(如下图)
直觉理解:
- 多分支训练: 类似集成学习,提升模型表达能力
- 重参数化: 将多分支数学等价融合为单分支
- 最佳实践: 训练时复杂,推理时简单
二、模块结构
RepVGG块结构:
训练时 (多分支):
输入特征图 (C×H×W)
│
├──────────────────┐
│ │
▼ ▼
┌─────────┐ ┌─────────────┐
│ 3×3 Conv│ │ 1×1 Conv │
│ (主分支) │ │ (辅助分支) │
└─────────┘ └─────────────┘
│ │
▼ ▼
│ │
├──────────────────┤
│ │
│ ┌──────┐ │
│ │Identity│ │
│ │(恒等) │ │
│ └──────┘ │
│ │ │
▼ ▼ ▼
┌─────────────────────────────────┐
│ Add (相加) │
└─────────────────────────────────┘
│
▼
输出特征图 (C×H×W)
推理时 (单分支):
输入特征图 (C×H×W)
│
▼
┌─────────────────────────────────┐
│ 3×3 Conv (融合后的卷积) │
│ 权重 = 3×3权重 + 1×1权重填充 │
│ 偏置 = 3×3偏置 + 1×1偏置 + BN偏置│
└─────────────────────────────────┘
│
▼
输出特征图 (C×H×W)
三、数学公式
RepVGG的数学表达:
1. 训练时前向传播:
y = BN₃ₓ₃(Conv₃ₓ₃(x)) + BN₁ₓ₁(Conv₁ₓ₁(x)) + BN₀(x)
其中:
- BN₃ₓ₃: 3×3卷积后的BatchNorm
- BN₁ₓ₁: 1×1卷积后的BatchNorm
- BN₀: 恒等映射的BatchNorm
2. BatchNorm融合:
对于BN层: y = γ × (x - μ) / √(σ² + ε) + β
融合到卷积权重:
W'ᵢ = γᵢ / √(σ²ᵢ + ε) × Wᵢ
b'ᵢ = γᵢ / √(σ²ᵢ + ε) × (bᵢ - μᵢ) + βᵢ
3. 1×1卷积填充为3×3:
W₃ₓ₃₁ₓ₁[i,j] = W₁ₓ₁ if (i,j)=(1,1) else 0
即将1×1卷积核零填充到3×3大小
4. 恒等映射转换:
W_identity = 单位矩阵 (对角线为1)
转换为1×1卷积后再填充为3×3
5. 最终融合:
W_fused = W'₃ₓ₃ + Pad(W'₁ₓ₁) + Pad(W'_identity)
b_fused = b'₃ₓ₃ + b'₁ₓ₁ + b'_identity
6. 推理时:
y = Conv_fused(x) // 单次3×3卷积
四、代码实现
python
import torch
import torch.nn as nn
import torch.nn.functional as F
class RepVGGBlock(nn.Module):
"""RepVGG块: 训练时多分支,推理时单分支"""
def __init__(self, c1, c2, stride=1, deploy=False, use_se=False):
"""
Args:
c1: 输入通道数
c2: 输出通道数
stride: 步长
deploy: 是否为部署模式
use_se: 是否使用SE注意力
"""
super().__init__()
self.deploy = deploy
self.c1 = c1
self.c2 = c2
self.stride = stride
# 训练时的多分支
if deploy:
# 部署模式: 单个3×3卷积
self.fused_conv = nn.Conv2d(c1, c2, 3, stride, 1, bias=True)
else:
# 训练模式: 三个分支
self.conv3x3 = nn.Conv2d(c1, c2, 3, stride, 1, bias=False)
self.bn3x3 = nn.BatchNorm2d(c2)
self.conv1x1 = nn.Conv2d(c1, c2, 1, stride, 0, bias=False)
self.bn1x1 = nn.BatchNorm2d(c2)
# 恒等分支 (仅当c1==c2且stride==1时)
if c1 == c2 and stride == 1:
self.bn_identity = nn.BatchNorm2d(c2)
else:
self.bn_identity = None
# 激活函数
self.act = nn.ReLU()
def forward(self, x):
if self.deploy:
# 部署模式: 直接使用融合后的卷积
return self.act(self.fused_conv(x))
# 训练模式: 多分支
y3x3 = self.bn3x3(self.conv3x3(x))
y1x1 = self.bn1x1(self.conv1x1(x))
if self.bn_identity is not None:
y_identity = self.bn_identity(x)
out = y3x3 + y1x1 + y_identity
else:
out = y3x3 + y1x1
return self.act(out)
def _fuse_bn(self, conv, bn):
"""融合卷积和BN层"""
gamma = bn.weight
beta = bn.bias
mean = bn.running_mean
var = bn.running_var
eps = bn.eps
# 计算融合权重
std = torch.sqrt(var + eps)
W_fused = conv.weight * (gamma / std).view(-1, 1, 1, 1)
# 计算融合偏置
if conv.bias is not None:
b_fused = (conv.bias - mean) * (gamma / std) + beta
else:
b_fused = (-mean) * (gamma / std) + beta
return W_fused, b_fused
def _pad_1x1_to_3x3(self, W1x1):
"""将1×1卷积核填充到3×3"""
if W1x1 is None:
return 0
return F.pad(W1x1, [1, 1, 1, 1])
def _identity_to_3x3(self, identity_weight, c):
"""将恒等映射转换为3×3卷积核"""
if identity_weight is None:
return 0
# 创建单位卷积核
identity_conv = torch.zeros(c, c, 3, 3)
for i in range(c):
identity_conv[i, i, 1, 1] = 1.0
return identity_conv * identity_weight.view(-1, 1, 1, 1)
def switch_to_deploy(self):
"""将训练模式转换为部署模式"""
if self.deploy:
return
# 融合3×3分支
W3x3, b3x3 = self._fuse_bn(self.conv3x3, self.bn3x3)
# 融合1×1分支
W1x1, b1x1 = self._fuse_bn(self.conv1x1, self.bn1x1)
W1x1 = self._pad_1x1_to_3x3(W1x1)
# 融合恒等分支
if self.bn_identity is not None:
W_identity = self._identity_to_3x3(self.bn_identity.weight, self.c2)
b_identity = self.bn_identity.bias
else:
W_identity = 0
b_identity = 0
# 最终融合
W_fused = W3x3 + W1x1 + W_identity
b_fused = b3x3 + b1x1 + b_identity
# 创建融合后的卷积层
self.fused_conv = nn.Conv2d(self.c1, self.c2, 3, self.stride, 1, bias=True)
self.fused_conv.weight.data = W_fused
self.fused_conv.bias.data = b_fused
# 删除训练时的分支
del self.conv3x3
del self.bn3x3
del self.conv1x1
del self.bn1x1
if self.bn_identity is not None:
del self.bn_identity
self.deploy = True
# 使用示例
if __name__ == "__main__":
# 训练模式
block_train = RepVGGBlock(c1=64, c2=64, deploy=False)
x = torch.randn(1, 64, 32, 32)
print("训练模式:")
out_train = block_train(x)
print(f"输入形状: {x.shape}")
print(f"输出形状: {out_train.shape}")
# 转换为部署模式
block_train.switch_to_deploy()
print("\n部署模式:")
out_deploy = block_train(x)
print(f"输出形状: {out_deploy.shape}")
# 验证输出一致性
print(f"输出差异: {torch.abs(out_train - out_deploy).max().item():.6f}")
五、在YOLO中的应用
RepVGG在YOLO中的应用:
1. YOLOv6:
- 使用RepVGG块作为基本构建单元
- 训练时多分支,推理时单分支
- 推理速度提升约30%
2. YOLOv7:
- 部分层使用RepVGG块
- 结合E-ELAN结构
3. YOLOv9:
- GELAN中可选RepBlock
- 针对部署优化
RepVGG在YOLOv6中的配置:
┌─────────────────────────────────────┐
│ backbone: │
│ - [-1, 1, RepVGG, [64]] │
│ - [-1, 1, RepVGG, [128]] │
│ - [-1, 1, RepVGG, [256]] │
│ ... │
└─────────────────────────────────────┘
性能对比:
┌──────────────┬──────────┬──────────┬──────────┐
│ 模型 │ 训练模式 │ 部署模式 │ 速度提升 │
├──────────────┼──────────┼──────────┼──────────┤
│ YOLOv6-n │ 12.8ms │ 9.8ms │ +23% │
│ YOLOv6-s │ 15.2ms │ 11.6ms │ +24% │
│ YOLOv6-m │ 19.8ms │ 15.2ms │ +23% │
│ YOLOv6-l │ 25.6ms │ 19.8ms │ +23% │
└──────────────┴──────────┴──────────┴──────────┘
六、优缺点
优点
✅ 推理速度快: 单分支结构,硬件友好
✅ 精度高: 多分支训练提升表达能力
✅ 内存效率: 推理时内存占用少
✅ 部署友好: 简单的3×3卷积结构
✅ 灵活性: 可与其他技术结合使用
缺点
❌ 训练复杂: 需要维护多个分支
❌ 转换开销: 需要额外的融合步骤
❌ 结构限制: 主要适用于3×3卷积
❌ 部署代码: 需要专门的部署代码
