shufflenet网络模型由旷视科技提出,当前已经有两代,分别为v1和v2,从时间上来说shufflenet v1在mobilenet v1之后,shufflenet v2在mobilenet v2之后。从论文效果来说,shufflenet比同代的mobilenet模型更优秀,但是实际使用时要实际测试,笔者发现在很多应用下,mobilenet从速度和精度上都要优于shufflenet。shufflenet的核心思想与其名称非常符合,其核心思想就是“打乱顺序”。第一代模型主要核心为分组卷积和通道打乱,第二代模型主要核心为通道分割和通道打乱,第二代模型是在四条提速规则下对第一代模型进行调整的结果。
目前移动端CNN模型主要设计思路主要是两个方面:模型结构设计和模型压缩。ShuffleNet和MobileNet一样属于前者,都是通过设计更高效的网络结构来实现模型变小和变快,而不是对一个训练好的大模型做压缩或者迁移。
shufflenet v1采用的是分组卷积,即在卷积时将通道拆分为多个组,然后在每个组内分别独立卷积,但是如果仅仅采用分组卷积,如上图(a)那带来的问题是相当于将一个大卷积拆分为独立的几条没有任何信息交流的卷积处理路线。mobilenet v1的通道或深度可分离卷积是在分组卷积(分组数等于通道数,属于特殊案例)后加入1*1的卷积实现信息交流,shufflenet v1采用的方法是如上图(b和c)均匀打乱不同分组中的通道实现不同分组之间的信息交流,进一步解释就是,打乱不同分组的通道后,每一个分组内都包含之前不同分组内的通道层。这里的打乱并非随机打乱而是均匀的打乱。在程序上实现channel shuffle是非常容易的:假定将输入层分为g组,总通道数为g*n,首先你将通道那个维度拆分为(g, n)两个维度,然后将这两个维度转置变成(n, g),最后重新reshape成一个维度即可,如果不是很理解可以看下面代码的channel_shuffle()函数。
对于h * w * c1的特征图,经过hh * ww的卷积得到h * w * c2的特征图:
(1) 普通卷积所需的FLOPs近似是2 * h * w * hh * ww * c1 * c2;
(2) 深度可分离卷积的FLOPs近似是2 * ( h * w * hh * ww * c1 + h * w * c1 * c2),由于c1远大于hh * ww,所以普通卷积的FLOPs是深度可分离卷积的FLOPs的近似hh * ww倍;
(3) 分组卷积(不考虑后面的均匀打乱操作)的FLOPs近似是2 * h * w * hh * ww * c1/g * c2/g,其中g是分组数,所以普通卷积的FLOPs是分组卷积的FLOPs的近似g * g倍。
从以上分析来看,分组卷积的FLOPs要少很多,但是实际运行时还要考虑均匀打乱所需的时间,实际测试下来,并未发现shufflenet在时间上有显著优势,所以需要实际情况实际测试。
基于上面对shfflenet v1核心思想,建立了shfflenet v1的基本单元,其结构如上图(b和c),其是在一个残差单元的基础上改进而成的。如上图(a)所示,这是一个包含3层的残差单元:首先是1x1卷积,然后是3x3的depthwise convolution(DWConv,主要是为了降低计算量),这里的3x3卷积是瓶颈层(bottleneck),紧接着是1x1卷积,最后是一个短路连接,将输入直接加到输出上。现在,进行如下的改进:将密集的1x1卷积替换成1x1的group convolution,不过在第一个1x1卷积之后增加了一个channel shuffle操作。值得注意的是3x3卷积后面没有增加channel shuffle,按paper的意思,对于这样一个残差单元,一个channel shuffle操作是足够了。还有就是3x3的depthwise convolution之后没有使用ReLU激活函数。改进之后如上图(b)所示。对于残差单元,如果stride=1时,此时输入与输出shape一致可以直接相加,而当stride=2时,通道数增加,而特征图大小减小,此时输入与输出不匹配。一般情况下可以采用一个1x1卷积将输入映射成和输出一样的shape。但是在ShuffleNet中,却采用了不一样的策略,如上图(c)所示:对原输入采用stride=2的3x3 avg pool,这样得到和输出一样大小的特征图,然后将得到特征图与输出进行连接(concat),而不是相加。这样做的目的主要是降低计算量与参数大小。
shufflenet v2完整的网络结构如下表格,实施细节见参考【0】
ShuffleNet与MobileNet的对比实验结果如下表所示。可以看到ShuffleNet不仅计算复杂度更低,而且精度更好。
shufflenet v2的实现代码见参考【1】,由于shufflenet v2优于shufflenet v1,所以本文就不再复现shufflenet v1的代码了。
shufflenet v2是旷视提出的shufflenet的升级版本,并被ECCV2018收录。论文说在同等复杂度下,shufflenet v2比shufflenet和mobilenetv2更准确。shufflenet v2是基于四条准则对shufflenet v1进行改进而得到的,这四条准则如下:
(G1)同等通道大小最小化内存访问量 对于轻量级CNN网络,常采用深度可分割卷积(depthwise separable convolutions),其中点卷积( pointwise convolution)即1x1卷积复杂度最大。这里假定输入和输出特征的通道数分别为C1和C2,经证明仅当C1=C2时,内存使用量(MAC)取最小值,这个理论分析也通过实验得到证实。更详细的证明见参考【1】
(G2)过量使用组卷积会增加MAC 组卷积(group convolution)是常用的设计组件,因为它可以减少复杂度却不损失模型容量。但是这里发现,分组过多会增加MAC。更详细的证明见参考【1】
(G3)网络碎片化会降低并行度 一些网络如Inception,以及Auto ML自动产生的网络NASNET-A,它们倾向于采用“多路”结构,即存在一个lock中很多不同的小卷积或者pooling,这很容易造成网络碎片化,减低模型的并行度,相应速度会慢,这也可以通过实验得到证明。
(G4)不能忽略元素级操作 对于元素级(element-wise operators)比如ReLU和Add,虽然它们的FLOPs较小,但是却需要较大的MAC。这里实验发现如果将ResNet中残差单元中的ReLU和shortcut移除的话,速度有20%的提升。
根据前面的4条准则,作者分析了ShuffleNet v1设计的不足,并在此基础上改进得到了ShuffleNetv2,两者模块上的对比下图所示
在ShuffleNetv1的模块中,大量使用了1x1组卷积,这违背了G2原则,另外v1采用了类似ResNet中的瓶颈层(bottleneck layer),输入和输出通道数不同,这违背了G1原则。同时使用过多的组,也违背了G3原则。短路连接中存在大量的元素级Add运算,这违背了G4原则。
为了改善v1的缺陷,v2版本引入了一种新的运算:channel split。具体来说,在开始时先将输入特征图在通道维度分成两个分支:通道数分别为C1和 C-C1,实际实现时C1=C/2。左边分支做同等映射,右边的分支包含3个连续的卷积,并且输入和输出通道相同,这符合G1。而且两个1x1卷积不再是组卷积,这符合G2,另外两个分支相当于已经分成两组。两个分支的输出不再是Add元素,而是concat在一起,紧接着是对两个分支concat结果进行channle shuffle,以保证两个分支信息交流。其实concat和channel shuffle可以和下一个模块单元的channel split合成一个元素级运算,这符合原则G4。
ShuffleNetv2的整体结构如下表所示,基本与v1类似,其中设定每个block的channel数,如0.5x,1x,可以调整模型的复杂度。
shufflenet v2的实现代码如下,这是torch版本,关于tensorflow版本可以查看参考【1】或者直接查看tensorflow官方代码。
# 根据torch官方代码修改的shufflenetv2的网络模型 https://github.com/pytorch/vision/blob/main/torchvision/models/shufflenetv2.py
# 权重文件下载 download url: https://download.pytorch.org/models/shufflenetv2_x0.5-f707e7126e.pth
# 权重文件下载 download url: https://download.pytorch.org/models/shufflenetv2_x1-5666bf0f80.pth
from typing import Callable, Any, List
import torch
import torch.nn as nn
from torch import Tensor
# 设定整个模型的所有BN层的衰减系数,该系数用于平滑统计的均值和方差,torch与tf不太一样,两者以1为互补
momentum = 0.01 # 官方默认0.1,越小,最终的统计均值和方差越接近于整体均值和方差,前提是batchsize足够大
# 将通道均匀打乱,111222 -> 121212
def channel_shuffle(x: Tensor, groups: int) -> Tensor:
batchsize, num_channels, height, width = x.size()
channels_per_group = num_channels // groups
# reshape
x = x.view(batchsize, groups, channels_per_group, height, width)
x = torch.transpose(x, 1, 2).contiguous()
# flatten
x = x.view(batchsize, -1, height, width)
return x
# 模型基本模块,这里名为倒置残差模块,但是其实这里是先缩减通道数,然后维持通道数,不满足倒置残差设计
class InvertedResidual(nn.Module):
def __init__(self, inp: int, oup: int, stride: int) -> None:
super().__init__()
if not (1 <= stride <= 3):
raise ValueError("illegal stride value")
self.stride = stride
branch_features = oup // 2
assert (self.stride != 1) or (inp == branch_features << 1) # <<1 等效于乘以2
# 定义左分支
if self.stride > 1:
self.branch1 = nn.Sequential(
self.depthwise_conv(inp, inp, kernel_size=3, stride=self.stride, padding=1),
nn.BatchNorm2d(inp, eps=0.001, momentum=momentum),
nn.Conv2d(inp, branch_features, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(branch_features, eps=0.001, momentum=momentum),
nn.ReLU(inplace=True),
)
else:
self.branch1 = nn.Sequential()
# 定义右分支
self.branch2 = nn.Sequential(
nn.Conv2d(
inp if (self.stride > 1) else branch_features,
branch_features,
kernel_size=1,
stride=1,
padding=0,
bias=False,
),
nn.BatchNorm2d(branch_features, eps=0.001, momentum=momentum),
nn.ReLU(inplace=True),
self.depthwise_conv(branch_features, branch_features, kernel_size=3, stride=self.stride, padding=1),
nn.BatchNorm2d(branch_features, eps=0.001, momentum=momentum),
nn.Conv2d(branch_features, branch_features, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(branch_features, eps=0.001, momentum=momentum),
nn.ReLU(inplace=True),
)
@staticmethod
def depthwise_conv(i: int, o: int, kernel_size: int, stride: int = 1, padding: int = 0, bias: bool = False) -> nn.Conv2d:
return nn.Conv2d(i, o, kernel_size, stride, padding, bias=bias, groups=i)
def forward(self, x: Tensor) -> Tensor:
if self.stride == 1:
x1, x2 = x.chunk(2, dim=1)
out = torch.cat((x1, self.branch2(x2)), dim=1)
else:
out = torch.cat((self.branch1(x), self.branch2(x)), dim=1)
out = channel_shuffle(out, 2)
return out
# 定义模型模板
class ShuffleNetV2(nn.Module):
def __init__(self, stages_repeats: List[int], stages_out_channels: List[int], num_classes: int = 1000, inverted_residual: Callable[..., nn.Module] = InvertedResidual) -> None:
super().__init__()
if len(stages_repeats) != 3:
raise ValueError("expected stages_repeats as list of 3 positive ints")
if len(stages_out_channels) != 5:
raise ValueError("expected stages_out_channels as list of 5 positive ints")
self._stage_out_channels = stages_out_channels
input_channels = 3
output_channels = self._stage_out_channels[0]
self.conv1 = nn.Sequential(
nn.Conv2d(input_channels, output_channels, 3, 2, 1, bias=False),
nn.BatchNorm2d(output_channels, eps=0.001, momentum=momentum),
nn.ReLU(inplace=True),
)
input_channels = output_channels
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# Static annotations for mypy
self.stage2: nn.Sequential
self.stage3: nn.Sequential
self.stage4: nn.Sequential
stage_names = [f"stage{i}" for i in [2, 3, 4]]
for name, repeats, output_channels in zip(stage_names, stages_repeats, self._stage_out_channels[1:]):
seq = [inverted_residual(input_channels, output_channels, 2)]
for i in range(repeats - 1):
seq.append(inverted_residual(output_channels, output_channels, 1))
setattr(self, name, nn.Sequential(*seq))
input_channels = output_channels
output_channels = self._stage_out_channels[-1]
self.conv5 = nn.Sequential(
nn.Conv2d(input_channels, output_channels, 1, 1, 0, bias=False),
nn.BatchNorm2d(output_channels, eps=0.001, momentum=momentum),
nn.ReLU(inplace=True),
)
self.fc = nn.Linear(output_channels, num_classes)
def _forward_impl(self, x: Tensor) -> Tensor:
# See note [TorchScript super()]
x = self.conv1(x)
x = self.maxpool(x)
x = self.stage2(x)
x = self.stage3(x)
x = self.stage4(x)
x = self.conv5(x)
x = x.mean([2, 3]) # globalpool
x = self.fc(x)
return x
def forward(self, x: Tensor) -> Tensor:
return self._forward_impl(x)
def _shufflenetv2(*args: Any, **kwargs: Any) -> ShuffleNetV2:
model = ShuffleNetV2(*args, **kwargs)
return model
def shufflenet_v2_x0_5(**kwargs: Any) -> ShuffleNetV2:
"""
Constructs a ShuffleNetV2 with 0.5x output channels, as described in
`"ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design"
`_.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _shufflenetv2([4, 8, 4], [24, 48, 96, 192, 1024], **kwargs)
def shufflenet_v2_x1_0(**kwargs: Any) -> ShuffleNetV2:
"""
Constructs a ShuffleNetV2 with 1.0x output channels, as described in
`"ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design"
`_.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _shufflenetv2([4, 8, 4], [24, 116, 232, 464, 1024], **kwargs)
def shufflenet_v2_x1_5(**kwargs: Any) -> ShuffleNetV2:
"""
Constructs a ShuffleNetV2 with 1.5x output channels, as described in
`"ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design"
`_.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _shufflenetv2([4, 8, 4], [24, 176, 352, 704, 1024], **kwargs)
def shufflenet_v2_x2_0(**kwargs: Any) -> ShuffleNetV2:
"""
Constructs a ShuffleNetV2 with 2.0x output channels, as described in
`"ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design"
`_.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _shufflenetv2([4, 8, 4], [24, 244, 488, 976, 2048], **kwargs)
包含训练和测试的完整代码见:https://github.com/LegendBIT/torch-classification-model
0. CNN模型之ShuffleNet
1. ShuffleNetV2:轻量级CNN网络中的桂冠