Shuffle Net 系列
- 实践部分
- 1.文件划分
- 2.Block(差异文件)
- 3.Network(差异文件)
- 3.总结
实践部分
在看代码之前先叠加一个甲,本文并未跑出来这个实验结果。实验条件有限,论文中采用的ImageNet dataset 数据集有些太大了。后续会出一个简化版本的代码进行实践。本小节主要是对Shuffle Net V2实践旷世实验室的源码进行解读,学习。公开代码就是难得的学习材料。便于读者能够在自己的模型中有所实践,提升自己的代码能力。所以真想实践的读者可以自行下载数据集开展实验,本章节主要是解读其git上的代码,对初学者有一个较简单直白的入门版本。如果可以接受那么就开始本章节的代码学习吧!!!文中机器翻译使用的是shuffle混洗实际上就是通道重排操作,有疑问的同学可以看一下Shuffle Net系列详解 (1) 论文的讲解。仅仅是是翻译的不同
代码点击这里!!!!!
1.文件划分
下图是其git中的文件目录,很规整希望各位在今后的学习中能够做的好点,便于自己的学习和理解,其实看这一部分和shufflenet V1的代码部分都是重合的,其基本变化的部分也是block文件中的代码。在拜读过其原文也确实是这样的,其论文深远意义就是其对研究的深刻和多维度实验结果,首先来看一下:
观察到README、train、以及utils文件在内容上的一致性,我们能够得出一个直观的理解:这表明在大规模训练的策略及其有效性方面,已经建立了一套成熟且有效的代码架构。这实际上凸显了两个主要优势:
-
训练模型的策略有效性已被验证:在ShuffleNet V1的设计和实现过程中,模型训练的策略和技术手段已经经过了验证,并证明是有效的。这意味着,当我们考虑如何提升模型性能时,现阶段真正可以作出改进的领域,已不再是训练策略,而是模型结构本身。
-
未变化的技术是经过验证的:所有保持不变的技术手段都是经过实践探索并证明有用的方法。这些已经证明有效的技术为现阶段的模型构建提供了一个坚实的基础。也就是说,我们所处的这个阶段,对于模型的进一步改进和评估,是在一个相对公平的基础上进行的。
其实可以多学习学习这部分没变化的代码,这些小的trick真的是很珍贵有效,属于是练功夫的内功了,你多厉害的秘籍没好的内力也发挥不出真实的水平,尤其是现阶段百分之1的提升都是创新的时代。很有可能你创新点的有效性就被垃圾的训练策略和各种没调整的参数毁灭掉。
2.Block(差异文件)
接下来直接看代码的主体部分吧:
代码中分支1构建的部分就是,延续了V1中的代码风格,先构建分支然后按需求实例化。下面是其代码对应论文图中的block,将这一组操作定义为了主分支:
主要就是11卷积然后dw卷积,再次使用11卷积
branch_main = [
# pw 常规的1*1卷积层
nn.Conv2d(inp, mid_channels, 1, 1, 0, bias=False),
nn.BatchNorm2d(mid_channels),
nn.ReLU(inplace=True),
# dw
nn.Conv2d(mid_channels, mid_channels, ksize, stride, pad, groups=mid_channels, bias=False),
nn.BatchNorm2d(mid_channels),
# pw-linear
nn.Conv2d(mid_channels, outputs, 1, 1, 0, bias=False),
nn.BatchNorm2d(outputs),
nn.ReLU(inplace=True),
]
self.branch_main = nn.Sequential(*branch_main)
现在看一下block中的全部代码
import torch
import torch.nn as nn # 老样子常规操作
# 其实这里值得注意的就是,思考下block块就是由卷积操作堆叠,其本质上就是一个神经网络所以使用nn其实很正常
class ShuffleV2Block(nn.Module): # 命名为V2Block
def __init__(self, inp, oup, mid_channels, *, ksize, stride):
# 每一个块实例化需要的参数都是一样的,输入输出通道个数,放在神经网络里这就是输入输出向量的维度。
# mid就是翻译过来就是中间通道的个数,就是隐藏层的维度。剩下的就是卷积核,步长,值得注意的是就是
# *,后面需要固定赋值,上一章节对这部分进行了解释。
super(ShuffleV2Block, self).__init__()
self.stride = stride
assert stride in [1, 2] # 还是会判断步长是不是在正常的范畴内。1就是常规卷积,2就是下采样
self.mid_channels = mid_channels
self.ksize = ksize
pad = ksize // 2 # 为了保证卷积过程中不会出现特征图变小需要进行填充,其按照卷积核的大小进行判断
self.pad = pad
self.inp = inp
outputs = oup - inp
# outputs的取值为输出通道数量减去输入通道数量,这个参数主要是为了实现什么呢?
branch_main = [
# pw 常规的1*1卷积层
nn.Conv2d(inp, mid_channels, 1, 1, 0, bias=False),
nn.BatchNorm2d(mid_channels),
nn.ReLU(inplace=True),
# dw
nn.Conv2d(mid_channels, mid_channels, ksize, stride, pad, groups=mid_channels, bias=False),
nn.BatchNorm2d(mid_channels),
# pw-linear
nn.Conv2d(mid_channels, outputs, 1, 1, 0, bias=False),
nn.BatchNorm2d(outputs),
nn.ReLU(inplace=True),
]
self.branch_main = nn.Sequential(*branch_main)
其实可以看到其多种模块需要构建各种各样的分支结构组件使用啊。所以下面的代码就是针对其余的组建进行堆叠从而顺利的构建出其他的blcok
接下里是下采样的分支构建代码,和之前V1不同的地方在于,V2下采样中则是采用了两种不同Block完成下采样操作,这是论文中的原图部分:
如上图所示,其设计了两种模块实现下采样。继续看代码部分是如何实现的呢?
# 下采样模块的分支构建
if stride == 2: # 在论文中只有非下采样block采用了直连结构,所以如果步长为2那么就可以着手设计其分支下采样的部分结构。说白了就步长为2就使用branch_proj这个非直连的特殊分支结构
branch_proj = [ # 然后就是下采样中左侧的分支一共两种形态,一种是简单的一个是复杂的。下述代码就是复杂的复制处理就是d_block中的左侧分支
# dw 先深度分离卷积 其步长为2,并且groups=inp有多少个输入通道就分多少组就是最基本的DW卷积
nn.Conv2d(inp, inp, ksize, stride, pad, groups=inp, bias=False),
nn.BatchNorm2d(inp),
# pw-linear # DW卷积后跟进一个正常的卷积1*1卷积pw-linear
nn.Conv2d(inp, inp, 1, 1, 0, bias=False),
nn.BatchNorm2d(inp),
nn.ReLU(inplace=True),
]
self.branch_proj = nn.Sequential(*branch_proj)
else:
self.branch_proj = None # 简单的就不给你整啥花里胡哨的了,直接在前向传播给你接一个均值池化。
def forward(self, old_x): # 首先判断步长
if self.stride==1: # c_block
#####################注意如果没看懂通道重排先去看通道重排的代码,下面进行讲解举例子,不要跳过这个理解部分
x_proj, x = self.channel_shuffle(old_x) # 先进行通道重排和分离这里和V1的重排就不一致了。要注意了。 # 先讲解这个通道重排擦操作吧。回过头来再看这个输出,就是输出了通道的一半和另一半,
return torch.cat((x_proj, self.branch_main(x)), 1)# 卷积完后进行拼接
elif self.stride==2: # d_block注意这里不执行通道分离操作,所以就直接进行卷积分支的映射
x_proj = old_x
x = old_x
return torch.cat((self.branch_proj(x_proj), self.branch_main(x)), 1)
def channel_shuffle(self, x):
# 获取输入数据的尺寸,包括批次大小、通道数、高度和宽度
batchsize, num_channels, height, width = x.data.size()
# 确保通道数能够被4整除,这是由于ShuffleNet的设计中采用分组卷积,通道数通常是4的倍数
assert (num_channels % 4 == 0)
# 重塑张量形状,将通道数分为两组,每组含有 num_channels // 2 个通道。
# 这实际上是将通道维度展开,便于之后的混洗操作。原始形状为(batchsize, num_channels, height, width)
# 变形后的形状为(batchsize * num_channels // 2, 2, height * width),便于接下来的混洗操作
x = x.reshape(batchsize * num_channels // 2, 2, height * width)
# 通过permute操作交换张量的维度,实现通道组之间的交换
x = x.permute(1, 0, 2) ##########这个时候已经忘成了通道的混洗操作
# 重塑张量形状,恢复到接近原始的四维形状,为(batchsize, num_channels // 2 * 2, height, width)
# 这一步骤完成了通道混洗的过程
x = x.reshape(2, -1, num_channels // 2, height, width)
# 最终返回两部分混洗后的特征图,准备进行下一步的操作
return x[0], x[1]
举个例子来解释上述的行为啊。 reshpe在进行这样的是会先将数据进行flatten然后再次reshape首先要有这样的一个理解 举例子开始
假设初始时有一个四维张量,形状为[4, 4, 3, 3]
,代表有4个样本,每个样本有4个通道,每个通道的特征图大小为3x3
。
- 第一步 -
reshape
操作:- x = x.reshape(batchsize * num_channels // 2, 2, height * width) 先考虑后面,变成一个
[4, 4, 9]
这样的一个形状。然后再考虑前面的将4x4
变成一个8x2
- 举个例子,通道是这样排列的
- x = x.reshape(batchsize * num_channels // 2, 2, height * width) 先考虑后面,变成一个
[[1, 2 , 3 , 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]]
然后被reshape下
[[1, 2],
[3, 4],
[5, 6],
[7, 8],
[9, 10],
[11, 12],
[13, 14],
[15, 16]]
其实可以看成是一个拉平然后重构的过程。按照两组一个区分,这是为啥呢,因为人家还要做一下这个通道分离的操作了,即这个代码实际上是通道分离的代码,所以分成了两组。现阶段咱们看通道还是[1,2]是一个列表里。
- 通过
reshape
操作,将这16个通道分成两组,每组8个通道,形状变为[8, 2, 9]
(这里直接把3x3
看作一个维度,实际操作中需考虑两个维度)。
-
第二步 -
permute
置换操作:- 通过
permute
操作,实现维度的置换,将形状变为[2, 8, 9]
,就是两行了,成功将相同实现了划分。原来的13579来自于多个组,现在被划分成了一组了,只有这样才能够实现交叉的分批。
- 通过
-
**第三步 - 再次
reshape
操作:**形状变为[2, -1, 2, 3, 3]
。
可以看到这个2和上面的289是一致的就是将全部通道分成两个批次应对,这个通道分离操作。通道都分离出去一半了,之前一个图4个通道,分离出去了一半当然变成2了。所以[2, -1, 2, 3, 3]
。第三位置为2。后面的3*3是一致的。-1则是不输入大小让函数自己计算。
所以人家这个通道重排和通道分离集成在一起了。之所以实现了通道重排的效果就是人家是1234个通道不是12,34一组而是在这个过程中变成了13,24一组。这样即使满足了分离又实现了重排。可以好好思考下,这个操作还是很巧妙的。这样就不会出现分组卷积中的近亲传播现象。有点类似于卷积操作前,使用扑克牌的洗牌方法对通道进行一个洗牌然后分成两组。一组不执行卷积一组执行卷积操作,每次分离操作之前都这么洗一次牌。- 最后一次
reshape
操作将置换后的张量恢复到近似原始的四维形状,形状变为[2, -1, 2, 3, 3]
。这里的-1
用来自动计算需要合并的维度,基于总元素数量的不变原则,-1
处自动计算得到的值是4
,因为我们之前通过置换操作已经将通道分成了两组,每组具有原先半数的通道。 - 最终形状
[2, 4, 2, 3, 3]
意味着,原始的4个样本现在被分成了两组,每组含有2个新的"样本",每个"样本"有2个通道,且每个通道的特征图大小为3x3
。
- 最后一次
通过上述步骤,原本紧邻的通道现在被重新排列,实现了通道间的信息交流和混合,这是ShuffleNet架构的关键特点之一。每一步的操作都是为了实现这个目标,从而提升深度网络的性能。最终,将混洗后的张量分解为两个部分,每一部分又可以参与后续层的计算,进一步处理特征信息。
让我们进一步阐述为什么要通过reshape
操作来触碰和重排通道,以及这个过程的具体意义。
3.Network(差异文件)
看完了每一个block构成,可以看看咱们这个新型高达(shufflenet V2)是怎么拼起来的。
import torch
import torch.nn as nn
from blocks import ShuffleV2Block # 从构建的block文件中将一个组件导入
class ShuffleNetV2(nn.Module):
def __init__(self, input_size=224, n_class=1000, model_size='1.5x'):# 老规矩还是一样的224图片大小,1000分类数量 1.5是默认的模型尺寸,不同尺寸对应着不同的卷积通道个数。
super(ShuffleNetV2, self).__init__()
print('model size is ', model_size)
self.stage_repeats = [4, 8, 4] # 每一个阶段对应的重复次数
self.model_size = model_size
if model_size == '0.5x':
self.stage_out_channels = [-1, 24, 48, 96, 192, 1024]
elif model_size == '1.0x':
self.stage_out_channels = [-1, 24, 116, 232, 464, 1024]
elif model_size == '1.5x':
self.stage_out_channels = [-1, 24, 176, 352, 704, 1024]
elif model_size == '2.0x':
self.stage_out_channels = [-1, 24, 244, 488, 976, 2048]
else:
raise NotImplementedError
# building first layer
input_channel = self.stage_out_channels[1]
self.first_conv = nn.Sequential( # 其第一个卷积模块
nn.Conv2d(3, input_channel, 3, 2, 1, bias=False), # 输入通道RGB图像三个通道,然后 3, 2,卷积核大小,填充为1.将这个图生成通道数符合input_channel的大小
nn.BatchNorm2d(input_channel), # BN层
nn.ReLU(inplace=True), # 激活
)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) # 构建了一个block中的最大池化层,主要是下采样所以 stride=2
self.features = [] # 用来存网络的主体
for idxstage in range(len(self.stage_repeats)): # 一共三个阶段所以 0,1,2
numrepeat = self.stage_repeats[idxstage] # 看一下这个阶段冲重复几次4,8,4
# self.stage_out_channels = [-1, 24, 244, 488, 976, 2048]
output_channel = self.stage_out_channels[idxstage+2]
# 其实上述代码是什么意思呢就是【0,1,2】 +2 = 【2,3,4】.取出来的输出通
# 道个数应该是和这几样数值对应的,其作为网络的主体结构,并不是开头也不是结尾。
# 所以这三个阶段通过block是网络的腰部结构
for i in range(numrepeat): # 假设第一个计算numrepeat=4 那么就构建4个block
if i == 0: # 第一个block永远都是下采样模块,目的就是通道数增加,特征图缩小
self.features.append(ShuffleV2Block(input_channel, output_channel,
mid_channels=output_channel // 2, ksize=3, stride=2))
else: #剩下的都是正常的非下采样模块self.features.append存起来
self.features.append(ShuffleV2Block(input_channel // 2, output_channel,
mid_channels=output_channel // 2, ksize=3, stride=1))
input_channel = output_channel
self.features = nn.Sequential(*self.features) # 将构建的这一多个模块组装起来
self.conv_last = nn.Sequential(
nn.Conv2d(input_channel, self.stage_out_channels[-1], 1, 1, 0, bias=False),
nn.BatchNorm2d(self.stage_out_channels[-1]),
nn.ReLU(inplace=True)
) # 最后一个模块
self.globalpool = nn.AvgPool2d(7) # 全剧池化层
if self.model_size == '2.0x':
self.dropout = nn.Dropout(0.2)
self.classifier = nn.Sequential(nn.Linear(self.stage_out_channels[-1], n_class, bias=False))# 分类器具
self._initialize_weights()# 初始化参数 完结撒花
def forward(self, x):
x = self.first_conv(x)
x = self.maxpool(x)
x = self.features(x)
x = self.conv_last(x)
x = self.globalpool(x)
if self.model_size == '2.0x':
x = self.dropout(x)
x = x.contiguous().view(-1, self.stage_out_channels[-1])
x = self.classifier(x)
return x
def _initialize_weights(self):
for name, m in self.named_modules():
if isinstance(m, nn.Conv2d):
if 'first' in name:
nn.init.normal_(m.weight, 0, 0.01)
else:
nn.init.normal_(m.weight, 0, 1.0 / m.weight.shape[1])
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
if m.bias is not None:
nn.init.constant_(m.bias, 0.0001)
nn.init.constant_(m.running_mean, 0)
elif isinstance(m, nn.BatchNorm1d):
nn.init.constant_(m.weight, 1)
if m.bias is not None:
nn.init.constant_(m.bias, 0.0001)
nn.init.constant_(m.running_mean, 0)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
if m.bias is not None:
nn.init.constant_(m.bias, 0)
if __name__ == "__main__": #测试整个网络的的流通性
model = ShuffleNetV2()
# print(model)
test_data = torch.rand(5, 3, 224, 224)
test_outputs = model(test_data)
print(test_outputs.size())
3.总结
在之前的论文精讲环节,确实还有许多内容需要进一步深入探讨。如果您对某些部分感兴趣,欢迎随时联系我,我会持续更新以提供更多信息。我真心希望读者能够从中获得收益,这里分享的都是基于个人理解的见解。接下来,我计划跟进一些在小型模型上可以执行的实践代码,旨在帮助读者更深入地理解这些概念。其实我看下来这两个代码模型的主体构建都是很容易看懂的。真正的难点是其整体的训练逻辑的堆积。以及各种各样自己写的小的trick。值得注意的是现阶段新技术的引入有些是可以替换的,各位酌情学习哦。