RepVGG
📣 论文下载地址:https://arxiv.org/abs/2101.03697
📣 官方源码(Pytorch实现):https://github.com/DingXiaoH/RepVGG
0 前言
🐾 R e p V G G RepVGG RepVGG最大的创新之处是:结构重参数化。结构重参数化是一个新的概念,在训练和推理阶段采用不同的策略。比如:在训练阶段,使用一个类似 R e s N e t ResNet ResNet的多分支模型,而在推理时转化成 V G G VGG VGG的单路模型。
1 结构重参数化
🐾 结构重参数化就是在训练阶段采用多分支模型,而在推理测试阶段使用单分支模型,可以理解为如下图所示,训练时模型的基本组件为
A
A
A,而在测试阶段模型的基本组件为
B
B
B。
🐾 那么关键的问题来了,训练的多分支模型是如何转化到测试的单分支模型的呢?👍 👍 👍 全文参考 强力推荐:结构重参数化主要分为两步,第一步主要是将
C
o
n
v
2
D
Conv2D
Conv2D算子和
B
N
BN
BN算子融合以及将只有
B
N
BN
BN的分支转换成一个
C
o
n
v
2
D
3
×
3
Conv2D 3\times3
Conv2D3×3分支;第二步将多个分支上的
C
o
n
v
2
D
3
×
3
Conv2D 3\times3
Conv2D3×3卷积分支融合成单路结构的一个
3
×
3
3\times3
3×3卷积。
1.1 结构重参数化第一步(将 C o n v 2 D Conv2D Conv2D算子和 B N BN BN算子融合以及将只有 B N BN BN的分支转换成一个 C o n v 2 D Conv2D Conv2D算子)
- Conv2D + BN ——> Conv2D
BN ——> Conv2D
卷积计算方式: y i = W i ∗ x i + b y_i=W_i*x_i + b yi=Wi∗xi+b,在模型训练阶段,若忽略偏置的影响,则只有公式: y i = W i ∗ x i y_i=W_i*x_i yi=Wi∗xi。标准化 B N BN BN的计算公式为: y i = x i − u i ( δ i ) 2 + ϵ × γ i + β i y_i = \frac{x_i - u_i}{\sqrt{(\delta_i)^2+\epsilon}}\times \gamma_i+\beta_i yi=(δi)2+ϵxi−ui×γi+βi,将 B N BN BN层的计算公式按类似卷积计算公式展开就为: y i = γ i δ i 2 + ϵ x i + ( β i − u i ⋅ γ i δ i 2 + ϵ ) y_i= \frac{\gamma_i}{\sqrt{\delta_i^2+\epsilon}}x_i+(\beta_i - \frac{u_i \cdot \gamma_i}{\sqrt{\delta_i^2+\epsilon}}) yi=δi2+ϵγixi+(βi−δi2+ϵui⋅γi),在所有的模型中,都是卷积层后接标准化(归一化)层,那么就有卷积层的输出是 B N BN BN层的输入,也就是说在忽略卷积偏置的条件下, y i = W i ∗ x i y_i=W_i*x_i yi=Wi∗xi就是 B N BN BN计算公式中 x i x_i xi的输入。
上步骤说到卷积在忽略偏置的条件下,计算公式为: y i = W i ∗ x i y_i=W_i*x_i yi=Wi∗xi,就输入特征图和卷积核来说,卷积计算的理论过程是对应位置相乘后相加,卷积核移位,再同理计算下一个值。如下图所示:
据上图所示,得到 y 1 y_1 y1位置的计算值为: w 1 ∙ x 1 + w 2 ∙ x 2 + w 3 ∙ x 3 + w 4 ∙ x 5 + w 5 ∙ x 6 + w 6 ∙ x 7 + w 7 ∙ x 9 + w 8 ∙ x 10 + w 9 ∙ x 11 w_1∙x_1+w_2∙x_2+w_3∙x_3+w_4∙x_5+w_5∙x_6+w_6∙x_7+w_7∙x_9+w_8∙x_{10}+w_9∙x_{11} w1∙x1+w2∙x2+w3∙x3+w4∙x5+w5∙x6+w6∙x7+w7∙x9+w8∙x10+w9∙x11,以该点位置为例,该值输入到 B N BN BN层中,计算公式为: ( w 1 ∙ x 1 + w 2 ∙ x 2 + w 3 ∙ x 3 + w 4 ∙ x 5 + w 5 ∙ x 6 + w 6 ∙ x 7 + w 7 ∙ x 9 + w 8 ∙ x 10 + w 9 ∙ x 11 ) ⋅ γ 1 δ 1 2 + ϵ + ( β 1 − u 1 ⋅ γ 1 δ 1 2 + ϵ ) (w_1∙x_1+w_2∙x_2+w_3∙x_3+w_4∙x_5+w_5∙x_6+w_6∙x_7+w_7∙x_9+w_8∙x_{10}+w_9∙x_{11}) \cdot \frac{\gamma_1}{\sqrt{\delta_1^2+\epsilon}}+(\beta_1 - \frac{u_1 \cdot \gamma_1}{\sqrt{\delta_1^2+\epsilon}}) (w1∙x1+w2∙x2+w3∙x3+w4∙x5+w5∙x6+w6∙x7+w7∙x9+w8∙x10+w9∙x11)⋅δ12+ϵγ1+(β1−δ12+ϵu1⋅γ1)。到这里 C o n v 2 D Conv2D Conv2D就和 B N BN BN层融合在一个计算公式内了。通过公式也可以发现,该公式就是加了偏置的卷积计算公式,所以就可以融合为一个卷积分支了。(理解到这里就算差不多了,但最关键的还是代码了,接着看~~)
🐾 根据学习发现,原论文中的 c o n v 2 d + B N conv2d + BN conv2d+BN或者 B N BN BN,都是转换为 3 × 3 3\times3 3×3卷积,然后再将转化的多个 3 × 3 3\times3 3×3卷积进行融合,为一个 3 × 3 3\times3 3×3卷积,才算结束。那么就有这几种转化需要理解:
- 1 × 1 卷积 1\times1 卷积 1×1卷积 ——> 3 × 3 卷积 3\times3 卷积 3×3卷积
原始 1 × 1 1\times1 1×1卷积,只需要将卷积核设置 p a d = 1 pad=1 pad=1的0填充,就可以将 1 × 1 1\times1 1×1卷积转换为 3 × 3 3\times3 3×3卷积。
如下图所示:
- B N 标准化 BN标准化 BN标准化 ——> 3 × 3 卷积 3\times3 卷积 3×3卷积
只有 B N BN BN的模型结构,需要构建一个卷积层,然后再根据 C o n v 2 D + B N Conv2D + BN Conv2D+BN转换公式进行转换,得到 3 × 3 3\times3 3×3卷积层。
1.2 结构重参数化第二步(多分支的 3 × 3 3\times3 3×3卷积融合成一个 3 × 3 3\times3 3×3卷积)
根据图例,融合后的3分支卷积的计算过程可以理解为: y 1 = w 1 ∗ x 1 + b 1 、 y 2 = w 2 ∗ x 1 + b 2 、 y 3 = w 3 ∗ x 1 + b 3 y_1=w_1*x_1+b_1、y_2=w_2*x_1+b_2、y_3=w_3*x_1+b_3 y1=w1∗x1+b1、y2=w2∗x1+b2、y3=w3∗x1+b3默认输入都是 x 1 x_1 x1,多分支再进行融合可计算为: y i = ( w 1 + w 2 + w 3 ) ∗ x 1 + ( b 1 + b 2 + b 3 ) y_i=(w_1+w_2+w_3)*x_1+(b_1+b_2+b_3) yi=(w1+w2+w3)∗x1+(b1+b2+b3)
至此,整个的融合过程就结束了。
2 代码分析
🐾 下面就根据大佬的博文的代码来详细剖析下, C o n v 2 D + B N Conv2D + BN Conv2D+BN 转换为 3 × 3 3\times3 3×3卷积。
from collections import OrderedDict
import numpy as np
import torch
import torch.nn as nn
def main():
torch.random.manual_seed(0)
f1 = torch.randn(1, 2, 3, 3)
module = nn.Sequential(OrderedDict(
conv=nn.Conv2d(in_channels=2, out_channels=2, kernel_size=3, stride=1, padding=1, bias=False),
bn=nn.BatchNorm2d(num_features=2)
))
module.eval()
with torch.no_grad():
output1 = module(f1)
print(output1)
# fuse conv + bn
kernel = module.conv.weight # Conv2D 卷积核
running_mean = module.bn.running_mean # BN 均值
running_var = module.bn.running_var # BN 方差
gamma = module.bn.weight # BN gamma 可学习参数
beta = module.bn.bias # BN beta 可学习参数
eps = module.bn.eps # BN eps 可学习参数
std = (running_var + eps).sqrt() # BN 根号下方差加eps(很小的数)
t = (gamma / std).reshape(-1, 1, 1, 1) # [ch] -> [ch, 1, 1, 1]
kernel = kernel * t
bias = beta - running_mean * gamma / std
fused_conv = nn.Conv2d(in_channels=2, out_channels=2, kernel_size=3, stride=1, padding=1, bias=True)
fused_conv.load_state_dict(OrderedDict(weight=kernel, bias=bias))
with torch.no_grad():
output2 = fused_conv(f1)
print(output2)
np.testing.assert_allclose(output1.numpy(), output2.numpy(), rtol=1e-03, atol=1e-05)
print("convert module has been tested, and the result looks good!")
if __name__ == '__main__':
main()
上述代码中可以看到是定义了一个 n n . S e q u e n t i a l nn.Sequential nn.Sequential序列容器,里面包含 3 × 3 3\times3 3×3卷积和 B N BN BN归一化,此时卷积是忽略偏置系数的,而这个卷积可以理解为是定义在模型训练阶段的,然后就是通过公式: ( x 1 1 ⋅ k 1 1 + x 2 2 ⋅ k 1 1 + x 3 3 ⋅ k 3 3 + x 4 4 ⋅ k 4 4 ) ⋅ γ 1 δ 1 2 + ϵ + ( β 1 − u 1 ⋅ γ 1 δ 1 2 + ϵ ) (x_1^1 \cdot k_1^1+x_2^2 \cdot k_1^1+x_3^3 \cdot k_3^3+x_4^4 \cdot k_4^4) \cdot \frac{\gamma_1}{\sqrt{\delta_1^2+\epsilon}}+(\beta_1 - \frac{u_1 \cdot \gamma_1}{\sqrt{\delta_1^2+\epsilon}}) (x11⋅k11+x22⋅k11+x33⋅k33+x44⋅k44)⋅δ12+ϵγ1+(β1−δ12+ϵu1⋅γ1) 来进行二者的融合。根据代码再结合此公式就很好理解了,融合后的 3 × 3 3\times3 3×3卷积的偏置系数为: β 1 − u 1 ⋅ γ 1 δ 1 2 + ϵ \beta_1 - \frac{u_1 \cdot \gamma_1}{\sqrt{\delta_1^2+\epsilon}} β1−δ12+ϵu1⋅γ1,需要注意的是融合后的卷积是有偏置系数的,而且是在模型推理阶段。然后就是卷积核的参数,其计算的公式为:待融合的卷积权重参数 × γ 1 δ 1 2 + ϵ \times \frac{\gamma_1}{\sqrt{\delta_1^2+\epsilon}} ×δ12+ϵγ1,可以发现融合过程是严格按着上述公式计算得来的。新的卷积就融合了 C o n v 2 d D + B N Conv2dD + BN Conv2dD+BN的所有参数信息。(值得注意的就是:融合前的卷积是不带有偏置的,而融合后的卷积要设置偏置为 T r u e True True。
📝 📝 📝