目 录
1. ShuffleNet v1网络
1.1 channel shuffle
对于图(a)中简单的组卷积,输入特征矩阵通过两个串行的GConv,每次卷积都是针对每组内的channel信息进行卷积操作(图中对应着不同颜色),这样虽然能减少参数与计算量,但是每个group之间没有任何的信息交流。
针对这个问题,作者提出了channel shuffle的思想:输入特征矩阵先经过一个GConv,得到对应的输出特征矩阵,以图(b)为例,如果GConv分为3个组,得到的每个组的输出特征矩阵再进行划分,每个特征矩阵再划分为3份;再将每个group的第1份放在一起,每个group的第2份放在一起,第三份同理,融合结果如图(c)所示;将融合后的特征矩阵再进行GConv,这样就能融合不同group之间的channel信息。
在ResNeXt网络当中,残差结构中的1×1的卷积核占据了总计算量的93.4%,因此在ShuffleNet中,将1×1的卷积(图a)全部换成了1×1的GConv(图b,stride=1),图(c)是stride=2的情况,注意最后是拼接在一起而非相加。
1.2 ShuffleNet v1网络结构
其中:①对于每个stage的第一个block采用步长为2(上面图c);②对于下一个stage输出特征矩阵会进行翻倍操作;③和ResNet相似,1×1 GConv和3×3 DWConv的输出特征矩阵channel是整个block的输出特征矩阵channel的1/4;④对于stage2,不使用GConv,而是使用的普通卷积,因为输入channel很小。
2. ShuffleNet v2网络
FLOPs:指浮点运算数,理解为计算量,可以用来衡量算法或模型的复杂度。在ShuffleNet v2网络中,作者提出计算复杂度不能只看FLOPs,并提出了4条如何设计高效网络的准则,基于该准则提出了新的block设计。
2.1 设计block的4条准则
①等通道宽度使内存访问成本(MAC)最小化:当卷积层的输入特征矩阵与输出特征矩阵channel相等时MAC最小(FLOPs要保持不变),在构建网络时要使用平衡的卷积层,尽可能让输入特征矩阵的channel和输出特征矩阵的channel比值接近1:1;
②增大的组卷积的组会增加MAC:当GConv的groups增大时(保持FLOPs不变),MAC会增大,不能一昧的增大group数;
③网络碎片化会降低并行度:网络设计的碎片化程度越高(保持FLOPs不变),速度越慢,碎片化程度可以理解为分支程度,如果想要高效的网络不要设计过多的分支结构;
④Element-wise操作带来的影响不可忽视:Element-wise操作包括ReLU、AddTensor和AddBias等,这些操作的FLOPs很小但是MAC很大。
每个block单元会将输入特征矩阵划分为两个分支(对应图c的Channel Split),分别为和。为了控制网络的碎片化程度(对应准则③),左面的分支没有任何操作;右面的分支由三个卷积层组成,他们的输入channel和输出channel相同(对应准则①);两个1×1的Conv没有使用组卷积(对应准则②);两个分支计算完之后是通过concat连接(对应输入时的split),保证整个block块的输入channel和输出channel一致(对应准则①);ShuffleNet v1是对Add或concat之后的特征矩阵进行ReLU,而ShuffleNet v2只对右面分支的输出进行ReLU,这样使得后面的Concat、Channel Shuffle和下一层的Channel Split可以归并为一个Element-wise,减少了操作个数(对应准则④)。
对于图(d)stride=2的情况,没有Channel Split的操作,所以输出特征矩阵深度会翻倍(因为是两个特征矩阵的concat)。
2.2 ShuffleNet v2网络结构
3 利用Pytorch实现ShuffleNet
3.1 channel shuffle的具体实现
view函数的作用:
transpose函数的作用:
再利用view函数还原:
实现函数:
def channel_shuffle(x: Tensor, groups: int):
# 在pytorch中所得到的tensor通道排列顺序
batch_size, num_channels, height, width = x.size()
# 将channels划分为groups组
channels_per_group = num_channels // groups
# reshape
# [batch_size, num_channels, height, width] → [batch_size, groups, channels_per_group, height, width]
x = x.view(batch_size, groups, channels_per_group, height, width)
# 将维度1(groups)和维度2(channels_per_group)的信息进行交换
# transpose后,tensor在内存中的存储顺序不是连续的,contiguous可以将数据转化为连续的数据
x = torch.transpose(x, 1, 2).contiguous()
# flatten
x = x.view(batch_size, -1, height, width)
return x
3.2 ShuffleNet v2的block
class InvertedResidual(nn.Module):
# 输入特征矩阵channel,输出特征矩阵channel,步长
def __init__(self, input_c: int, output_c: int, stride: int):
super(InvertedResidual, self).__init__()
# 步长只能为1或2
if stride not in [1, 2]:
raise ValueError("illegal stride value.")
self.stride = stride
# 判断输出特征矩阵channel是否为2的整数倍,因为concat拼接两个channel相同的矩阵,输出一定是2的整数倍
assert output_c % 2 == 0
# 两个分支的channel等于输出特征矩阵channel/2
branch_features = output_c // 2
# 当stride为1时,input_channel应该是branch_features的两倍
# python中 '<<' 是位运算,可理解为计算×2的快速方法
assert (self.stride != 1) or (input_c == branch_features << 1)
# stride=2的block
if self.stride == 2:
# 左分支
self.branch1 = nn.Sequential(
self.depthwise_conv(input_c, input_c, kernel_s=3, stride=self.stride, padding=1),
nn.BatchNorm2d(input_c),
nn.Conv2d(input_c, branch_features, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(branch_features),
nn.ReLU(inplace=True)
)
else:
# stride=1的左分支不做任何处理
self.branch1 = nn.Sequential()
# 右分支
self.branch2 = nn.Sequential(
# stride=2输入特征矩阵channel就是block的输入,stride=1输入特征矩阵channel是一半
nn.Conv2d(input_c if self.stride > 1 else branch_features, branch_features, kernel_size=1,
stride=1, padding=0, bias=False),
nn.BatchNorm2d(branch_features),
nn.ReLU(inplace=True),
self.depthwise_conv(branch_features, branch_features, kernel_s=3, stride=self.stride, padding=1),
nn.BatchNorm2d(branch_features),
nn.Conv2d(branch_features, branch_features, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(branch_features),
nn.ReLU(inplace=True)
)
@staticmethod
# 静态方法DW卷积
def depthwise_conv(input_c: int,
output_c: int,
kernel_s: int,
stride: int = 1,
padding: int = 0,
bias: bool = False):
# 有BN层不需要加入偏置,博客http://t.csdn.cn/5Xxb0
return nn.Conv2d(in_channels=input_c, out_channels=output_c, kernel_size=kernel_s,
stride=stride, padding=padding, bias=bias, groups=input_c)
def forward(self, x: Tensor):
if self.stride == 1:
# chunk均分为两份,channel对应的维度(dim)为1
x1, x2 = x.chunk(2, dim=1)
# stride=1对x1不做任何处理,和x2经过右分支后的输出进行concat拼接(在channel维度dim=1)
out = torch.cat((x1, self.branch2(x2)), dim=1)
else:
# stride=2
out = torch.cat((self.branch1(x), self.branch2(x)), dim=1)
# 将输出进行channel shuffle处理
out = channel_shuffle(out, 2)
return out
3.3 ShuffleNet v2网络结构
class ShuffleNetV2(nn.Module):
def __init__(self,
# 对应每个stage的block的重复次数
stages_repeats: List[int],
# 每个stage输出特征矩阵的channel
stages_out_channels: List[int],
num_classes: int = 1000,
# 每个stage的block
inverted_residual: Callable[..., nn.Module] = InvertedResidual):
super(ShuffleNetV2, self).__init__()
# 判断stages_repeats是否是3个元素,stages_out_channels是否是5个元素
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 RGB image
input_channels = 3
# Conv1的输出是_stage_out_channels里的第一个值
output_channels = self._stage_out_channels[0]
# Conv1
self.conv1 = nn.Sequential(
nn.Conv2d(input_channels, output_channels, kernel_size=3, stride=2, padding=1, bias=False),
nn.BatchNorm2d(output_channels),
nn.ReLU(inplace=True)
)
# 将输出channel数传给下一个stage的输入channel数
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
# 得到stage2、3、4
stage_names = ["stage{}".format(i) for i in [2, 3, 4]]
# 通过zip同时遍历stage_names、stages_repeats和_stage_out_channels(从1开始遍历)
for name, repeats, output_channels in zip(stage_names, stages_repeats,
self._stage_out_channels[1:]):
# 每个stage第一个block的stride=2
seq = [inverted_residual(input_channels, output_channels, 2)]
# 每个stage的剩余block的stride=1
for i in range(repeats - 1):
seq.append(inverted_residual(output_channels, output_channels, 1))
# stage名+一系列层结构
setattr(self, name, nn.Sequential(*seq))
# 再把当前的output_channels赋值给下一个stage的input_channels
input_channels = output_channels
# 把stage_out_channels的最后一个元素(Conv5的输出特征矩阵channel)传给Conv5的output_channels
output_channels = self._stage_out_channels[-1]
# Conv5
self.conv5 = nn.Sequential(
nn.Conv2d(input_channels, output_channels, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(output_channels),
nn.ReLU(inplace=True)
)
# 全连接层
self.fc = nn.Linear(output_channels, num_classes)
def _forward_impl(self, x: 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]) # global pool,解释:http://t.csdn.cn/11L6k
x = self.fc(x)
return x
def forward(self, x: Tensor):
return self._forward_impl(x)
4. 训练结果
不使用预训练权重,训练所有参数,经过30个epoch的结果:
[epoch 26] mean loss 0.554: 100%|██████████| 184/184 [00:18<00:00, 10.00it/s]
100%|██████████| 46/46 [00:09<00:00, 5.06it/s]
[epoch 26] accuracy: 0.826
[epoch 27] mean loss 0.547: 100%|██████████| 184/184 [00:18<00:00, 10.07it/s]
100%|██████████| 46/46 [00:10<00:00, 4.45it/s]
[epoch 27] accuracy: 0.822
[epoch 28] mean loss 0.526: 100%|██████████| 184/184 [00:18<00:00, 9.70it/s]
100%|██████████| 46/46 [00:09<00:00, 4.78it/s]
[epoch 28] accuracy: 0.843
[epoch 29] mean loss 0.535: 100%|██████████| 184/184 [00:18<00:00, 9.76it/s]
100%|██████████| 46/46 [00:10<00:00, 4.59it/s]
[epoch 29] accuracy: 0.843