1. 引言
受限于内存以及计算资源,将常规的CNN架构部署到移动设备是件非常困难的事。近几年来有各种移动端网络架构设计,大部分都是从减少卷积计算量的思路出发,谷歌出品的Mobilenet系列是提出了「Depthwise Pointwise卷积」来减少计算量,旷视则是提出「通道混洗」,利用转置操作,均匀的shuffle各个通道进行卷积。Mixnet是在Mobilenet基础上,关注了卷积核的大小,通过「不同大小卷积核」所生成的卷积图在不增加计算量前提下进一步提高精度。而华为的Ghostnet则是聚焦于「特征图冗余」,希望通过少量的计算(即论文里的cheap operation)得到大量特征图。而Ghostnet在相同计算量下,精度超越了Mobilenetv3,达到了75.7%分类准确率( ImageNet ILSVRC-2012 classification dataset)
2. 何为特征图冗余?
首先作者对训练好的Resnet-50模型进行了特征图可视化
Resnet-50模型进行了特征图可视化
这里作者标出了三组特征图,认为这些特征图彼此是类似的,我们可以用一些cheap operation来得到
因此我们从这里入手,以一种更高效的方式生成这些"Ghost"一样的特征图
3. GhostModules提出
在引言我们也提到了Depthwise Pointwise卷积,其中1x1卷积层也需要大量的内存和计算量
Ghost 模块
我们认为没有必要耗费大量计算资源来生成冗余的特征图,一些特征图之间是一种相似的关系,所以我们想到在原始特征图基础上,经过简单线性变换再生成新的特征图
如图中,先通过常规卷积,生成部分特征图。再在这特征图基础上得到新的特征图。最后两部分特征图在通道维度上连结,生成最终的output
我们可以看下代码实现
代码语言:javascript复制class GhostModule(nn.Module):
def __init__(self, inp, oup, kernel_size=1, ratio=2, dw_size=3, stride=1, relu=True):
super(GhostModule, self).__init__()
self.oup = oup
init_channels = math.ceil(oup / ratio)
new_channels = init_channels*(ratio-1)
self.primary_conv = nn.Sequential(
nn.Conv2d(inp, init_channels, kernel_size, stride, kernel_size//2, bias=False),
nn.BatchNorm2d(init_channels),
nn.ReLU(inplace=True) if relu else nn.Sequential(),
)
self.cheap_operation = nn.Sequential(
nn.Conv2d(init_channels, new_channels, dw_size, 1, dw_size//2, groups=init_channels, bias=False),
nn.BatchNorm2d(new_channels),
nn.ReLU(inplace=True) if relu else nn.Sequential(),
)
def forward(self, x):
x1 = self.primary_conv(x)
x2 = self.cheap_operation(x1)
out = torch.cat([x1,x2], dim=1)
return out[:,:self.oup,:,:]
代码中primary_conv就是常规的卷积,而cheap operation就是一个Depthwise卷积,通过参数ratio,来控制两部分生成特征图数量之比。最后调用torch.cat连结两部分特征图
作者也给出了计算量压缩比公式
计算量压缩比的公式
上面式子表示的是普通卷积的计算量
下面左边的式子,代表的是primaryconv的卷积量,右边式子代表的是cheap operation计算量,通过参数s(对应代码中的ratio)来控制两者卷积量之比
最后这里的近似做个说明, dxd计算量于k*k类似,而c代表的通道数远大于s,因此最终近似结果为s
3.1 其中的一个疑问
那么既然前面说了cheap operation是个线性变换,但是在代码实现中,作者在模块最后还是加入了Relu。其实我当时是很疑惑的,因为众所周知加入了Relu激活函数就打破了函数的线性关系。后面在github提了一个小issue
得到的回复是 Relu可以在最后的concat上做,之所以放到前面是为了降低latency
4. GhostBottleNeck
在GhostModule模块基础上,根据两种stride步长又提出了两种bottleneck结构
两种bottleneck结构
下面是其代码实现
代码语言:javascript复制class GhostBottleneck(nn.Module):
def __init__(self, inp, hidden_dim, oup, kernel_size, stride, use_se):
super(GhostBottleneck, self).__init__()
assert stride in [1, 2]
self.conv = nn.Sequential(
# pw
GhostModule(inp, hidden_dim, kernel_size=1, relu=True),
# dw
depthwise_conv(hidden_dim, hidden_dim, kernel_size, stride, relu=False) if stride==2 else nn.Sequential(),
# Squeeze-and-Excite
SELayer(hidden_dim) if use_se else nn.Sequential(),
# pw-linear
GhostModule(hidden_dim, oup, kernel_size=1, relu=False),
)
if stride == 1 and inp == oup:
self.shortcut = nn.Sequential()
else:
self.shortcut = nn.Sequential(
depthwise_conv(inp, inp, 3, stride, relu=True),
nn.Conv2d(inp, oup, 1, 1, 0, bias=False),
nn.BatchNorm2d(oup),
)
def forward(self, x):
return self.conv(x) self.shortcut(x)
通过两个ghostmodule与原始输入(shortcut)相加得到最终输出,若shortcut维度不一致,则需要用1x1卷积来调整通道数。并且引入了SElayer,增加了特征图注意力机制。
经过这个GhostnetBottleNeck结构堆叠,我们就得到了Ghostnet整体
下面是他的架构图
GhostNet 网络结构图
5. 总结
和主流的网络在精度和参数量上的对比结果
「GhostNet的思路,设计都很巧妙简单。着眼于特征图生成,用更cheap的Depthwise卷积来进一步生成特征图。论文中实验结果也是十分理想的,超越了大部分移动端SOTA网络。」