简介
- 意义:conv层和bn层的融合能够给网络提速。
- 背景:先学习了这个博客的内容。学习时发现代码的实现和理论的介绍不太一样,遂深入研究了一番,修正了部分内容。
- 文章结构:本文先介绍基本理论知识,然后解释代码。
理论知识
卷积操作
在介绍conv与bn的融合之前,需要基本了解卷积核的卷积操作。
卷积层的卷积操作
对于卷积层而言,若卷积层输入为
F
i
F_i
Fi,输出为
F
o
F_o
Fo,输入通道数为
i
i
i,输出通道数为
j
j
j,一个卷积核在特征图上卷积的次数为
n
n
n(下文会解释n的含义),则有:
F
o
=
W
F
i
+
b
F_o = WF_i + b
Fo=WFi+b
[ F o 1 F o 2 . . . F o j ] = [ w 1 w 2 . . . w j ] [ F i 1 F i 2 . . . F i n ] + [ b 1 b 2 . . . b j ] \begin{bmatrix} F_{o1} \\ F_{o2} \\ ... \\ F_{oj} \end{bmatrix} = \begin{bmatrix} w_1 \\ w_2 \\ ... \\ w_j \end{bmatrix} \begin{bmatrix} F_{i1} & F_{i2} & ... & F_{in} \end{bmatrix} + \begin{bmatrix} b_1 \\ b_2 \\ ... \\ b_j \end{bmatrix} ⎣⎢⎢⎡Fo1Fo2...Foj⎦⎥⎥⎤=⎣⎢⎢⎡w1w2...wj⎦⎥⎥⎤[Fi1Fi2...Fin]+⎣⎢⎢⎡b1b2...bj⎦⎥⎥⎤
需要注意的是, F i F_i Fi的大小并非是 ( n , c , w , h ) (n, c, w, h) (n,c,w,h),而是对其进行了特别处理。先回忆每个 w k ( k = 1 , 2 , . . . , j ) w_k(k=1, 2, ..., j) wk(k=1,2,...,j)在特征图上卷积时的过程:从特征图的第一个区域开始卷积,每次卷积完毕,需要“滑动”到下一个区域再次进行卷积,直到最后一个区域,一共需要卷积 n n n次。因此, F k ( k = 1 , 2 , . . . , n ) F_k(k = 1, 2, ..., n) Fk(k=1,2,...,n)应该为特征图中被卷积核第 k k k次卷积的区域。
举个简单的例子,假设特征图为一个二维矩阵:
[
1
2
3
2
3
4
3
4
5
]
\begin{bmatrix} 1 & 2 & 3 \\ 2 & 3 & 4 \\ 3 & 4 & 5 \end{bmatrix}
⎣⎡123234345⎦⎤
若卷积核大小为
2
∗
2
2*2
2∗2,则
n
=
4
n = 4
n=4,且有:
F
i
1
=
[
1
2
2
3
]
F
i
2
=
[
2
3
3
4
]
F
i
3
=
[
2
3
3
4
]
F
i
4
=
[
3
4
4
5
]
F_{i1} = \begin{bmatrix} 1 & 2\\ 2 & 3 \end{bmatrix}\ F_{i2} = \begin{bmatrix} 2 & 3\\ 3 & 4 \end{bmatrix}\ F_{i3} = \begin{bmatrix} 2 & 3\\ 3 & 4 \end{bmatrix}\ F_{i4} = \begin{bmatrix} 3 & 4\\ 4 & 5 \end{bmatrix}
Fi1=[1223] Fi2=[2334] Fi3=[2334] Fi4=[3445]
但是,这里的 F i 1 , F i 2 , . . . , F i n F_{i1}, F_{i2}, ..., F_{in} Fi1,Fi2,...,Fin与 w 1 , w 2 , . . . , w j w_1, w_2, ..., w_j w1,w2,...,wj均为二维矩阵;在实际应用中,它们均为三维矩阵。又注意到它们之间的运算是对应位置相乘,因此可以将其通通reshape成1维矩阵进行点乘运算,运算后再reshape回去,且不影响计算结果。此时,矩阵 W W W和 F i F_i Fi变成了二维矩阵,卷积层操作便可以由一个二维矩阵乘法的表达式来表示!
这一节实际上只为说明一件事:虽然卷积层的卷积操作是基于“滑动窗口”的机制、在每一个窗口区域的运算是对应位置相乘的较为繁琐的运算,但该操作是能够通过二维矩阵乘法来表示的。因此,后续我们可以利用矩阵乘法的性质来对公式进行变形。
将归一化层看成卷积核为1*1的卷积操作
对于归一化层而言,若某通道均值为
μ
\mu
μ,方差为
σ
\sigma
σ,归一化层的权重为
γ
\gamma
γ,偏移为
β
\beta
β,该通道输入为
X
X
X,输出为
Y
Y
Y,则有:
Y
=
γ
X
−
μ
σ
2
+
ϵ
2
+
β
=
γ
σ
2
+
ϵ
2
X
+
(
β
−
γ
μ
σ
2
+
ϵ
2
)
Y = \gamma\frac{X - \mu}{\sqrt[2]{\sigma^2 + \epsilon}} + \beta = \frac{\gamma}{\sqrt[2]{\sigma^2 + \epsilon}} X + (\beta - \frac{\gamma\mu}{\sqrt[2]{\sigma^2 + \epsilon}})
Y=γ2σ2+ϵX−μ+β=2σ2+ϵγX+(β−2σ2+ϵγμ)
因此,归一化层可以看成输入和输出通道数相同的、卷积核大小为1*1的卷积层。其中,归一化层输出的第
i
i
i个通道对应输入的第
i
i
i个通道,与其它通道没有关系。因此,若将归一化层的操作用卷积层的矩阵相乘来表示,则
W
W
W应该是一个对角矩阵。事实上,令归一化层的权重为
W
b
n
W_{bn}
Wbn,偏移为
b
b
n
,
b_{bn},
bbn,则有:
W
b
n
=
[
γ
1
σ
1
2
+
ϵ
2
γ
2
σ
2
2
+
ϵ
2
.
.
.
γ
j
σ
j
2
+
ϵ
2
]
b
b
n
=
[
β
1
−
γ
1
μ
1
σ
1
2
+
ϵ
2
β
2
−
γ
2
μ
2
σ
2
2
+
ϵ
2
.
.
.
β
j
−
γ
j
μ
j
σ
j
2
+
ϵ
2
]
W_{bn} = \begin{bmatrix} \frac{\gamma_1}{\sqrt[2]{\sigma_1^2 + \epsilon}}\\ & \frac{\gamma_2}{\sqrt[2]{\sigma_2^2 + \epsilon}}\\ & & ...\\ & & & \frac{\gamma_j}{\sqrt[2]{\sigma_j^2 + \epsilon}} \end{bmatrix}\ b_{bn} = \begin{bmatrix} \beta_1 - \frac{\gamma_1 \mu_1}{\sqrt[2]{\sigma_1^2 + \epsilon}}\\ \beta_2 - \frac{\gamma_2 \mu_2}{\sqrt[2]{\sigma_2^2 + \epsilon}}\\ ...\\ \beta_j - \frac{\gamma_j \mu_j}{\sqrt[2]{\sigma_j^2 + \epsilon}} \end{bmatrix}
Wbn=⎣⎢⎢⎢⎢⎡2σ12+ϵγ12σ22+ϵγ2...2σj2+ϵγj⎦⎥⎥⎥⎥⎤ bbn=⎣⎢⎢⎢⎢⎡β1−2σ12+ϵγ1μ1β2−2σ22+ϵγ2μ2...βj−2σj2+ϵγjμj⎦⎥⎥⎥⎥⎤
将卷积层与归一化层融合
令卷积层的输入为
F
0
F_0
F0,卷积层的输出或归一化层的输入为
F
1
F_1
F1,归一化层的输出为
F
2
F_2
F2;卷积层的权重为
W
c
o
n
v
W_{conv}
Wconv,偏移为
b
c
o
n
v
b_{conv}
bconv,归一化层的权重为
W
b
n
W_{bn}
Wbn,偏移为
b
b
n
{b_{bn}}
bbn,则有:
F
1
=
W
c
o
n
v
F
0
+
b
c
o
n
v
F_1 = W_{conv}F_0 + b_{conv}
F1=WconvF0+bconv
F 2 = W b n F 1 + b b n F_2 = W_{bn}F_1 + b_{bn} F2=WbnF1+bbn
利用矩阵乘法的交换律,有:
F
2
=
W
b
n
(
W
c
o
n
v
F
0
+
b
c
o
n
v
)
+
b
b
n
=
(
W
b
n
W
c
o
n
v
)
F
0
+
(
W
b
n
b
c
o
n
v
+
b
b
n
)
\begin{aligned} F_2 &= W_{bn} (W_{conv}F_0 + b_{conv}) + b_{bn}\\ &= (W_{bn} W_{conv}) F_0 + (W_{bn} b_{conv} + b_{bn}) \end{aligned}
F2=Wbn(WconvF0+bconv)+bbn=(WbnWconv)F0+(Wbnbconv+bbn)
其中 W c o n v , b c o n v , W b n , b b n W_{conv}, b_{conv}, W_{bn}, b_{bn} Wconv,bconv,Wbn,bbn均需要reshape成 ( n u m _ o u t _ c h a n n e l s , − 1 ) (num\_out\_channels, -1) (num_out_channels,−1)
代码实现
参考博客代码
参考博客代码如下:
def fuse_conv_and_bn(conv, bn):
#
# init
fusedconv = torch.nn.Conv2d(
conv.in_channels,
conv.out_channels,
kernel_size=conv.kernel_size,
stride=conv.stride,
padding=conv.padding,
bias=True
)
#
# prepare filters
w_conv = conv.weight.clone().view(conv.out_channels, -1)
w_bn = torch.diag(bn.weight.div(torch.sqrt(bn.eps+bn.running_var)))
fusedconv.weight.copy_( torch.mm(w_bn, w_conv).view(fusedconv.weight.size()) )
#
# prepare spatial bias
if conv.bias is not None:
b_conv = conv.bias
else:
b_conv = torch.zeros( conv.weight.size(0) )
b_bn = bn.bias - bn.weight.mul(bn.running_mean).div(torch.sqrt(bn.running_var + bn.eps))
fusedconv.bias.copy_( b_conv + b_bn )
#
# we're done
return fusedconv
注意到变量b_conv
直接与b_bn
相加,并没有乘上w_bn
。然而,该博客的测试代码所测得的结果并没有反应这个问题。下面是测试代码:
import torch
import torchvision
torch.set_grad_enabled(False)
x = torch.randn(16, 3, 256, 256)
rn18 = torchvision.models.resnet18(pretrained=True)
rn18.eval()
net = torch.nn.Sequential(
rn18.conv1,
rn18.bn1
)
y1 = net.forward(x)
fusedconv = fuse_conv_and_bn(net[0], net[1])
y2 = fusedconv.forward(x)
d = (y1 - y2).norm().div(y1.norm()).item()
print("error: %.8f" % d)
代码测试均值的偏差占原均值的比例,运行结果为:
error: 0.00000022
确实是很小的,说明融合后的新卷积层和原来的双层结构等效。但前面提到,b_conv
并没有乘上w_bn
。事实上,通过打印发现,原始的卷积层没有bias,因此b_conv
全为0,乘与不成都没有区别。为了验证这一说法:
- 重写融合函数,将
b_conv
乘上w_bn
后再与b_bn
相加。 - 重写测试代码,为初始卷积层添加bias(打印可以看到其初始化值不全为0)。
修正代码
重写代码如下:
- 对于融合函数,在
b_conv
初始化后增加一行:
b_conv = torch.mm(w_bn, b_conv.view(conv.out_channels, -1)).view(fusedconv.bias.size())
- 对于测试代码,在
rn18.eval()
后添加如下几行:
print(rn18.conv1.bias)
# # rn18.conv1.bias = torch.nn.parameter.Parameter(torch.ones(rn18.conv1.weight.size(0)))
rn18.conv1 = torch.nn.Conv2d(
rn18.conv1.in_channels,
rn18.conv1.out_channels,
kernel_size=rn18.conv1.kernel_size,
stride=rn18.conv1.stride,
padding=rn18.conv1.padding,
bias=True
)
print(rn18.conv1.bias)
将重写前后的代码分别测试,结果如下:
error: 0.10046744
error2: 0.00000011
这说明,原代码确实存在问题,并且新代码修正了该问题。
经同学提醒,一般情况下,卷积层不使用bias。因此,从实际应用而言,这个错误的修正并没有多大意义,倒可以更多地被用于学习卷积层的操作以及卷积层与归一化层的融合过程。