前言
OctaveNet网络paper是《Drop an Octave: Reducing Spatial Redundancy in Convolutional Neural Networks with Octave Convolution》,是CVPR2019中的一篇论文。
OctaveNet是一个用于ImageNet Classfication任务的backbone结构。这篇论文提出了一种新型的卷积结构,或者叫做卷积模块,叫做 Octave Convolution。
Octave Convolution号称是一种可以无缝嵌入到任何已有backbone中的模块,简单好用,能有效降低已有模型的计算量并带来小幅的性能提升,听起来还是让人非常兴奋的。
从频域的角度理解图像
我们都知道,一副图像从空间域的角度看,它一般情况下是一个
3
×
W
×
H
3 \times W \times H
3×W×H的矩阵,矩阵中每一个位置都有一个[0,255]的值,而从频域的角度出发的话,一副图像都可以被分解为描述平稳变化结构的低空间频率分量(低频域、low-frequency)和描述快速变化的精细细节的高空间频率分量(高频域、high-frequency),就像下面这幅图:
最左侧为原始图像,中间为低频的部分,它比较多的反应的是图像的整体信息,最右侧为高频部分,它更多的反应图像的细节信息,比如边缘。这就好比空间域下的梯度,图像中存在边缘的地方,往往就是梯度大的地方。
特征图的高频与低频表示
既然对于图像来说可以区分高频与低频,那么对于特征图也是这样,特征图无非就是一个channel更多的矩阵而已,但是对于一个端对端的CNN模型,总不能在网络中引入一种频域计算,所以Octave Convolution显示的定义了“下采样”操作后的特征图叫做“低频域”,而不做下采样的原始尺寸叫做“高频域”。这样一来由于下采样带来的特征图尺寸减小,从而使得Octave Convolution计算量降低,此外网络有了不同尺度的信息(两个频域),并且两个频域的信息会在卷积完成后聚合,这个特性使得Octave Convolution具有比之前更好的性能。
“下采样”的scale,采用的是2的幂次,而目前文章只讨论了
2
1
2^{1}
21次幂的情况,说白了就是特征图的长宽都缩小了2,就像下面这张图:
图(b)是一个原始的特征图,并人为的切分特征图为Low Frequency和High Frequency,切分的标准是0.25,0.5,0.75三个系数,比如一个channel=64的特征图,系数为0.5的情况下,那么32个通道为低频,另外32个为高频。图©是用下采样操作实现低频域,就是上面说到缩小2倍。图(d)想要说明这个低频和高频要通过卷积做update,然后还有聚合交换的部分,反正只看(d)是看不出来,后面再具体介绍。
在这里不得不吐槽一点,论文由图像引出了高频和低频,但是到了卷积的地方直接过渡到了“下采样”,此后low-frequency和high-frequency还一直贯穿全文,这给人一种写论文写的过劲的感觉,毕竟Low Frequency、High Frequency和Octave 要比upsample和subsample好听,但是其实就是下采样完了上采样,尤其是我们要去实现它的时候。 ≡(▔﹏▔)≡
Octave Convolution
Octave Convolution原理
既然我们知道了Octave Convolution是一种下采样和上采样的组合,那么它的实现也就好理解了:
一个特征图的通道数
c
i
n
c_{in}
cin根据预设系数
a
i
n
a_{in}
ain切分为高频
(
1
−
a
i
n
)
c
i
n
\left ( 1- a_{in}\right )c_{in}
(1−ain)cin与低频
a
i
n
c
i
n
a_{in}c_{in}
aincin的部分,低频部分的宽高都缩小为原来的一半。然后Octave Convolution会做下面四个部分:
(1)高频部分直接卷积:
f
(
X
H
)
f\left ( X^{H}\right )
f(XH),即高频到高频的卷积,输出通道数
(
1
−
a
o
u
t
)
c
o
u
t
\left ( 1- a_{out}\right )c_{out}
(1−aout)cout;
(2)高频部分先做下采样再卷积,这里的下采样是
p
o
o
l
(
X
H
,
2
)
pool\left ( X^{H},2\right )
pool(XH,2),然后
f
(
p
o
o
l
(
X
H
,
2
)
)
f\left ( pool\left ( X^{H},2\right )\right)
f(pool(XH,2)),即高频到低频的卷积,输出通道数
a
o
u
t
c
o
u
t
a_{out}c_{out}
aoutcout;
(3)低频部分直接卷积后做上采样:
f
(
X
L
)
f\left ( X^{L}\right )
f(XL),这里的
u
p
s
a
m
p
l
e
(
f
(
X
L
)
)
upsample\left ( f\left ( X^{L}\right )\right )
upsample(f(XL))所用的上采样方法我们后面再说,即低频到高频的卷积,输出通道数
(
1
−
a
o
u
t
)
c
o
u
t
\left ( 1- a_{out}\right )c_{out}
(1−aout)cout;
(4)低频部分直接卷积:
f
(
X
L
)
f\left ( X^{L}\right )
f(XL),即低频到低频的卷积,输出通道数
a
o
u
t
c
o
u
t
a_{out}c_{out}
aoutcout。
这四个部分完成之后,接下来就要做信息的聚合,也就是(1)和(3)的结果做一个对应位置的按位加操作,(2)和(4)的结果做一个对应位置的按位加操作。
这样Octave Convolution就完成了,它其实在做的就是把原来的一个卷积操作,拆成了4个,而这4个中有三个处理的输入都是原来特征图w,h的一半,所以计算量就下来了。
所以一个one-stream的网络,在使用Octave Convolution之后,其实会变成two-stream结果,也就是高频流和低频流,在每一次的卷积结束之后,两个stream的信息会聚合一次。既然是中间是two-stream结构,那原有的网络怎么开始和收尾呢?
假如最开始时的输入只有一个,通道数还用
c
i
n
c_{in}
cin表示,那么要分出高频和低频两个流,就是只做(1)和(2),但是区别在于,输入特征图的通道数就是
c
i
n
c_{in}
cin。而最后就是只做(1)和(3),区别是输出特征图的通道数就是
c
o
u
t
c_{out}
cout。
上采样和下采样
下采样:
Octave Convolution的低频域输出可以用下面的公式表示:
Y
p
,
q
L
=
Y
p
,
q
L
→
L
+
Y
p
,
q
H
→
L
Y_{p,q}^{L}=Y_{p,q}^{L\rightarrow L}+Y_{p,q}^{H\rightarrow L}
Yp,qL=Yp,qL→L+Yp,qH→L
Y
p
,
q
L
→
L
=
∑
i
,
j
∈
N
k
W
i
+
k
−
1
2
,
j
+
k
−
1
2
L
→
L
X
p
+
i
,
q
+
j
L
Y_{p,q}^{L\rightarrow L}=\sum_{i,j\in N{_{k}}}W_{i+\frac{k-1}{2},j+\frac{k-1}{2}}^{L\rightarrow L}X_{p+i,q+j}^{L}
Yp,qL→L=i,j∈Nk∑Wi+2k−1,j+2k−1L→LXp+i,q+jL
Y
p
,
q
H
→
L
=
∑
i
,
j
∈
N
k
W
i
+
k
−
1
2
,
j
+
k
−
1
2
H
→
L
X
2
∗
p
+
i
,
2
∗
q
+
j
H
Y_{p,q}^{H\rightarrow L}=\sum_{i,j\in N{_{k}}}W_{i+\frac{k-1}{2},j+\frac{k-1}{2}}^{H\rightarrow L}X_{2*p+i, 2*q+j}^{H}
Yp,qH→L=i,j∈Nk∑Wi+2k−1,j+2k−1H→LX2∗p+i,2∗q+jH
其中
Y
p
,
q
L
→
L
Y_{p,q}^{L\rightarrow L}
Yp,qL→L就是(4)的结果,
Y
p
,
q
H
→
L
Y_{p,q}^{H\rightarrow L}
Yp,qH→L就是(2)的结果,
Y
p
,
q
H
→
L
Y_{p,q}^{H\rightarrow L}
Yp,qH→L的公式就解释了下采样的过程,
W
i
+
k
−
1
2
,
j
+
k
−
1
2
H
→
L
W_{i+\frac{k-1}{2},j+\frac{k-1}{2}}^{H\rightarrow L}
Wi+2k−1,j+2k−1H→L是一个对卷积核的遍历操作,当遍历到某一个点
(
i
,
j
)
\left ( i,j \right )
(i,j)之后,去对应特征图上
X
2
∗
p
+
i
,
2
∗
q
+
j
H
X_{2*p+i, 2*q+j}^{H}
X2∗p+i,2∗q+jH的点,由于特征图上
(
p
,
q
)
\left ( p,q \right )
(p,q)的遍历是以2倍的系数走的,所以这可以理解为,每一次都选择一个四方格的左上角的点,从而跨过其余三个点,开始卷积。这样一来,就下采样了,这也其实相当于一个跨步的卷积。
此外,这个公式还可以改写成
X
2
∗
p
+
0.5
+
i
,
2
∗
q
+
0.5
+
j
H
X_{2*p+0.5+i, 2*q+0.5+j}^{H}
X2∗p+0.5+i,2∗q+0.5+jH,但是0.5在特征图上是没有值的,所以它想表达的意思就是要聚合那个四方格,那就是平均池化喽。
于是,Octave Convolution的下采样策略就清楚了,要么做跨步卷积,要么先平均池化然后做步长为1的卷积。
当然论文中推荐了后者,因为如果原来网络中就做步长为2的卷积,为了下采样,难道要做步长为4吗?显然这不合理。
上采样:
Octave Convolution的高频域输出可以用下面的公式表示:
Y
p
,
q
H
=
Y
p
,
q
H
→
H
+
Y
p
,
q
L
→
H
Y_{p,q}^{H}=Y_{p,q}^{H\rightarrow H}+Y_{p,q}^{L\rightarrow H}
Yp,qH=Yp,qH→H+Yp,qL→H
Y
p
,
q
H
→
H
=
∑
i
,
j
∈
N
k
W
i
+
k
−
1
2
,
j
+
k
−
1
2
H
→
H
X
p
+
i
,
q
+
j
H
Y_{p,q}^{H\rightarrow H}=\sum_{i,j\in N{_{k}}}W_{i+\frac{k-1}{2},j+\frac{k-1}{2}}^{H\rightarrow H}X_{p+i,q+j}^{H}
Yp,qH→H=i,j∈Nk∑Wi+2k−1,j+2k−1H→HXp+i,q+jH
Y
p
,
q
L
→
H
=
∑
i
,
j
∈
N
k
W
i
+
k
−
1
2
,
j
+
k
−
1
2
H
→
L
X
p
2
+
i
,
q
2
+
j
L
Y_{p,q}^{L\rightarrow H}=\sum_{i,j\in N{_{k}}}W_{i+\frac{k-1}{2},j+\frac{k-1}{2}}^{H\rightarrow L}X_{\frac{p}{2}+i, \frac{q}{2}+j}^{L}
Yp,qL→H=i,j∈Nk∑Wi+2k−1,j+2k−1H→LX2p+i,2q+jL
有了上面的介绍,这个就可以简单点说了,应特征图上而
X
p
2
+
i
,
q
2
+
j
L
X_{\frac{p}{2}+i, \frac{q}{2}+j}^{L}
X2p+i,2q+jL的点其实是一个点复制成了一个四方格,或者说,这分明是一个最邻近插值。
OctaveConv如何减低计算量
假如我们有一个这样的卷积操作,那么它的计算量应该是:
其中一次卷积的计算量为:
C
o
m
p
o
n
c
e
=
(
3
×
3
+
8
)
×
c
+
c
−
1
Comp_{once} = \left ( 3\times3+8\right )\times c + c-1
Componce=(3×3+8)×c+c−1
那么完成所有的运算的计算量就应该是:
C
o
m
p
b
=
C
o
m
p
o
×
c
×
w
×
h
Comp_{b} =Comp_{o}\times c \times w \times h
Compb=Compo×c×w×h
为了让后续的约分方便,我们把这个计算近似一下,忽略单次卷积里面的逐通道相加操作:
C
o
m
p
b
=
(
3
×
3
+
8
)
×
c
×
c
×
w
×
h
Comp_{b} = \left ( 3\times3+8\right )\times c \times c \times w \times h
Compb=(3×3+8)×c×c×w×h
那么这样的一个操作按照上面提到的Octave Convolution实现,应该怎么计算呢?
假设系数
a
i
n
=
a
o
u
t
=
0.5
a_{in}=a_{out}=0.5
ain=aout=0.5
步骤(1):
C
o
m
p
o
1
=
(
3
×
3
+
8
)
×
1
2
c
×
1
2
c
×
w
×
h
=
1
4
C
o
m
p
a
Comp_{o1} = \left ( 3\times3+8\right )\times \frac{1}{2}c \times \frac{1}{2}c \times w \times h = \frac{1}{4}Comp_{a}
Compo1=(3×3+8)×21c×21c×w×h=41Compa
步骤(2)忽略下采样:
C
o
m
p
o
2
=
(
3
×
3
+
8
)
×
1
4
c
×
1
4
c
×
1
4
w
×
1
4
h
=
1
16
C
o
m
p
a
Comp_{o2} = \left ( 3\times3+8\right )\times \frac{1}{4}c \times \frac{1}{4}c \times \frac{1}{4}w \times \frac{1}{4}h = \frac{1}{16}Comp_{a}
Compo2=(3×3+8)×41c×41c×41w×41h=161Compa
步骤(3):
C
o
m
p
o
3
=
(
3
×
3
+
8
)
×
1
4
c
×
1
4
c
×
1
4
w
×
1
4
h
=
1
16
C
o
m
p
a
Comp_{o3} = \left ( 3\times3+8\right )\times \frac{1}{4}c \times \frac{1}{4}c \times \frac{1}{4}w \times \frac{1}{4}h = \frac{1}{16}Comp_{a}
Compo3=(3×3+8)×41c×41c×41w×41h=161Compa
步骤(4)忽略上采样:
C
o
m
p
o
4
=
(
3
×
3
+
8
)
×
1
4
c
×
1
4
c
×
1
4
w
×
1
4
h
=
1
16
C
o
m
p
a
Comp_{o4} = \left ( 3\times3+8\right )\times \frac{1}{4}c \times \frac{1}{4}c \times \frac{1}{4}w \times \frac{1}{4}h = \frac{1}{16}Comp_{a}
Compo4=(3×3+8)×41c×41c×41w×41h=161Compa
最后加起来:
C
o
m
p
o
=
=
7
16
C
o
m
p
a
Comp_{o} = = \frac{7}{16}Comp_{a}
Compo==167Compa
OctaveConv的Caffe实现
octave_upsample_layer
在此之前,OctaveConv已经有了MXNet和Pytorch版本的实现,分别是OctaveConv和OctaveConv_pytorch,但是还没有Caffe版本的实现,按照OctaveConv的原理,这个网络用Caffe是可以搭的,Caffe的已有上采样方式都不适用。
所以我添加了一个自定义层:octave_upsample_layer,以支持OctaveConv的上采样操作。
在这个层中,forward的部分就是上面提到的,而backward的部分,实现方式是这样:
自定义层注册
要把这个层添加到Caffe中需要
message LayerParameter {
optional OctaveUpsampleParameter octaveupsample_param = 最后一个ID;
}
并且它有下面几个参数:
message OctaveUpsampleParameter {
// DEPRECATED. No need to specify upsampling scale factors when
// exact output shape is given by upsample_h, upsample_w parameters.
optional uint32 scale = 1 [default = 2];
// DEPRECATED. No need to specify upsampling scale factors when
// exact output shape is given by upsample_h, upsample_w parameters.
optional uint32 scale_h = 2;
// DEPRECATED. No need to specify upsampling scale factors when
// exact output shape is given by upsample_h, upsample_w parameters.
optional uint32 scale_w = 3;
// DEPRECATED. Specify exact output height using upsample_h. This
// parameter only works when scale is 2
optional bool pad_out_h = 4 [default = false];
// DEPRECATED. Specify exact output width using upsample_w. This
// parameter only works when scale is 2
optional bool pad_out_w = 5 [default = false];
optional uint32 upsample_h = 6;
optional uint32 upsample_w = 7;
}
重新Build就好了。
Example
已有的主干网络添加OctaveConv的例子已经上传到了OctaveConv_Caffe项目。
OctaveConv一定能让网络变快吗?
这个答案是否定的,因为对于一个模型,在输入图像尺寸固定的情况下,它的计算量就是确定的,但是对于一个模型forward的速度,却和平台有关。所以OctaveConv一定能降低已有模型的计算量,这个是公式可证的。
但是它不一定能让已有模型的速度变得更快,这是因为,OctaveConv把原来一次就能完成的卷积分开了四次完成,这里就会增加额外的数据传输时的消耗,此外还有额外的上采样、下采样、按位加操作。
所以,比如我们在一个高性能的处理器上forward一个模型,它处理卷积操作的速度很快,那么如果Octaveconv节省下来的卷积操作耗时无法弥补这些额外的开销的话,网络就不会变得更快。
相反的,如果卷积操作的耗时很大,利用Octaveconv可以节省下的时间比其余的开销要大,那么网络就会变快。
下面是一个resnet18和resnet18_octave_0.5的网络耗时对比,平台是i7cpu:
resnet18的conv19耗时
conv19 forward: 6.9 ms.
resnet18_octave_0.5conv19耗时6.9ms
conv19_hf forward: 3 ms.
conv19_hf_add forward: 1.14 ms.
conv19_lf forward: 1.24 ms.
conv19_lf_add forward: 1.26 ms.
最后他们相差并不多,下面四个卷积操作并没有按照
1
4
\frac{1}{4}
41、
1
16
\frac{1}{16}
161、
1
16
\frac{1}{16}
161、
1
16
\frac{1}{16}
161的比例。一方面是因为相比于很多移动平台,i7-7700的性能还是比较强的,此外,resnet18的conv19卷积,本身计算量也不太大。
当然,这并不能说方法不work,paper本身提供的是一个思路,我们需要在合适的结构和平台下做更多的验证。