来源:计算机视觉工坊,作者:K.Fire
添加v:dddvision,备注:深度学习,拉你入群。文末附行业细分群
完整代码在github:SqueezNet-Pytorch
随着深度学习这一概念的兴起,越来越多的研究人员投身到深度神经网络搭建的浪潮中,各种各样的网络层出不穷。给定一个精度级别,通常存在多个达到该精度级别的CNN架构,但是在同等精度下,具有较少参数的CNN有三大优势:
- 更高效的分布式训练:分布式训练的数据并行方法在每个服务器上保留整个模型的副本,处理训练数据集的不同子集。因此通信开销与模型中的参数数成正比,换而言之,越小的模型,训练更快。虽然随着GPU等硬件架构的发展,很多时候我们并不需要再进行分布式训练,但如果你搭建的网络很深,训练数据集很大,分布式训练也是很有意义的。
- 在实际应用过程中,将新模型导出到客户端时,开销较小:Over-the-air update(OTA)是特斯拉等企业常用的技术,是指移动终端通过无线网络下载远程服务器上的升级包,对系统或应用进行升级的技术。训练的模型越小,需要的通信更少,因此可以实现更频繁更快速地更新。
- 更容易在嵌入式设备上部署:芯片存储空间有限,较小的模型让芯片(FPGA)嵌入式设备存储,部署神经网络更加可行,在SLAM领域,随着技术的成熟,越来越多的SLAM框架倾向于与神经网络相结合,但有很多时候苦恼于神经网络训练和运行时太耗时间,无法满足实时性需求,而小的网络往往更容易满足实时性。
因此,基于此,SqueezeNet期望探寻一种能保持相同精度,但是参数量更少的模型。
它是以AlexNet为基础进行的改进,AlexNet是当时ImageNet比赛的冠军,而且远超第二名,给了后世很多启发,但是它的不足之处(它的论文中也提到了)就是模型过大,参数过多,训练时间过长,它有5个卷积层,3个池化层,3个全连接层,模型大小为:*4/1024/1024=240M,下图为AlexNet的模型结构:
而通过我的实验,我训练完的SqueezeNet模型,只有2.97M,确实是实现了很大程度的压缩!
总的来说,Squeeze有以下几点主要贡献:
- 概述了使用很少参数进行CNN架构设计的策略,这一点不仅仅是针对SqueezeNet,而是对今后所有的网络设计都很有指导意义
- 引入了Fire Module作为网络基本构成单元,Fire模块在后来也有很多人引用借鉴,比如做点云物体语义识别的SqueezeSeg等
- 以Fire Module为基础,构建了SqueezeNet轻量级网络,就是本文的网络架构
我们在进行网络参数精简时,主要可以想到也是最直观的有三个策略:
- 用1x1卷积滤波器替换3x3卷积过滤器,1x1的参数比3x3少9倍。
- 减少输入通道的数量
- 提前下采样,当网络层数很深时,提前进行下采样,可以得到更小的特征图,后面的网络训练时就会有更少的参数
第1,2条策略一般不会过多减少得到结果的精度,而第三条策略已经有研究人员证明了会降低网络的精度,延迟下采样会得到更高的精度,Fire模块就是基于此设计的。
Fire模块如下图所示,
这是论文中给出的图,其实容易产生误导,作者的意思是:先将图像输入一个1X1的卷积网络(称为squeeze),通道数为3(图中的3个),然后将输出分别输入给4通道(图中)的1X1的卷积层和4个(图中)3X3的卷积层(称为expand),将得到的结果按通道的维度进行拼接,作为最后的输出。
注意在实际实现时,卷积后要接ReLU函数,也要注意ReLU和拼接的顺序,我的实验结果是拼接后再ReLU比较好。具体代码如下:
class Fire(nn.Module): def init(self, in_channels, squeeze_channels, expand1x1_channels, expand3x3_channels, version=1): super(Fire, self).init() self.in_channels = in_channels self.version = version self.squeeze = nn.Conv2d(in_channels, squeeze_channels, kernel_size=1) self.bn_squeeze = nn.BatchNorm2d(squeeze_channels) self.expand1x1 = nn.Conv2d(squeeze_channels, expand1x1_channels, kernel_size=1) self.activation = nn.ReLU(inplace=True) self.expand3x3 = nn.Conv2d(squeeze_channels, expand3x3_channels, kernel_size=3, padding=1) self.bn_out = nn.BatchNorm2d(expand1x1_channels + expand3x3_channels) def forward(self, x): # x1 = self.squeeze_activation(self.squeeze(x)) # e1 = self.expand1x1(x1) # e2 = self.expand3x3(x1) x1 = self.activation(self.bn_squeeze(self.squeeze(x))) e1 = self.expand1x1(x1) e2 = self.expand3x3(x1) y = torch.cat([e1, e2], 1) if self.version == 1: y = self.activation(self.bn_out(y)) elif self.version == 2: # y = self.expand_activation(y + x) y = self.activation(self.bn_out(y + x)) return y
讯享网
在这个代码中有两点需要注意(比较坑):
- 在squeeze或expand后需要进行Batch Bormalization操作,不然训练不动,结果精度极差
- 我在代码中通过version参数,实现了两种不同的Fire架构,第二种是ResNet启发的残差网络的模式,如下图:
搭建好Fire模块,就可以按照论文的架构去搭建整个Squeeze模型,我主要实现了这两种架构:
讯享网class SqueezeNet(nn.Module): def init(self, version=1, num_classes=10): super(SqueezeNet, self).init() self.num_classes = num_classes self.version = version self.net = nn.Sequential( nn.Conv2d(3, 96, kernel_size=7, stride=2, padding=2), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=3, stride=2), Fire(96, 16, 64, 64), Fire(128, 16, 64, 64), Fire(128, 32, 128, 128), nn.MaxPool2d(kernel_size=3, stride=2), Fire(256, 32, 128, 128), Fire(256, 48, 192, 192), Fire(384, 48, 192, 192), Fire(384, 64, 256, 256), nn.MaxPool2d(kernel_size=3, stride=2), Fire(512, 64, 256, 256), ) self.classifier = nn.Sequential( nn.Dropout(p=0.5), nn.Conv2d(512, self.num_classes, kernel_size=1), nn.ReLU(inplace=True), nn.AdaptiveAvgPool2d((1, 1)), # 自适应平均池化,指定输出(H,W) ) def forward(self, x): x = self.net(x) x = self.classifier(x) y = torch.flatten(x, 1) return ydef test(): test_model = SqueezeNet(version=2, num_classes=10) y = test_model(torch.randn(1, 3, 224, 224)) print(y.size()) summary(test_model, (1, 3, 224, 224))if name == ‘main’: test()
注意几点:
- 加入了Dropout层增强泛化性
- 下面定义的test函数主要是为了测试查看网络参数是否出错
在搭建模型时也可以根据作者在论文中提供的表,但是要注意,第一个卷积层是需要padding=2才能实现作者提到的尺寸,可以自己算一下:
完整代码在github:SqueezNet-Pytorch
训练时,先通过argparse包加载终端参数:
import argparseparser = argparse.ArgumentParser()parser.add_argument(‘–batch_size’, type=int, default=128, help=‘input batch size’)parser.add_argument(‘–lr’, type=float, default=0.1, help=‘learning rate’)parser.add_argument(‘–m’, type=float, default=0.09, help=‘momentum’)parser.add_argument(‘–wd’, type=float, default=0.0005, help=‘weight decay’)parser.add_argument(‘–epoch’, type=int, default=10, help=‘train epoch’)parser.add_argument(‘–version’, type=int, default=1, help=‘squeezenet version(1⁄2)’)opt = parser.parse_args()
然后加载数据集,我这里使用CIFAR10为例:
讯享网# 数据集转换参数transform = transforms.Compose([ transforms.ToTensor(), # transforms.RandomCrop(224), transforms.Resize(224), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])# 下载训练集与测试集train_data = datasets.CIFAR10( root=‘https://www.bilibili.com/read/cv31480642/CIFAR10/', train=True, # 是 train 集 download=True, # 如果该路径没有该数据集,就下载 transform=transform # 数据集转换参数)test_data = datasets.CIFAR10( root='https://www.bilibili.com/read/cv31480642/CIFAR10/', train=False, download=True, transform=transform)train_loader = DataLoader(train_data, shuffle=True, batch_size=opt.batch_size)test_loader = DataLoader(test_data, shuffle=True, batch_size=opt.batch_size)
然后我这里是定义了一个训练器类对训练流程进行了封装:
class Trainer: def init(self, opt, model, train_loader): self.opt = opt self.model = model self.train_loader = train_loader self.losses = [] def train(self): loss_fn = nn.CrossEntropyLoss() learning_rate = opt.lr optimizer = torch.optim.SGD( model.parameters(), lr=learning_rate, momentum=opt.m, weight_decay=opt.wd ) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5) for epoch in tqdm(range(opt.epoch)): scheduler.step() for i, (X, Y) in enumerate(self.train_loader): X, Y = X.to(’cuda:0‘), Y.to(’cuda:0‘) Pred = self.model(X) loss = loss_fn(Pred, Y) optimizer.zero_grad() loss.backward() optimizer.step() if i % opt.epoch == 0: self.losses.append(loss.item()) print(’训练误差为: {:.4f}‘.format(loss.item())) def printParamters(self): # 打印模型的 state_dict print(“Model’s state_dict:”) for param_tensor in self.model.state_dict(): print(param_tensor, “ ”, self.model.state_dict()[param_tensor].size()) def saveParameters(self, root=‘https://www.bilibili.com/read/cv31480642/'): try: os.makedirs(root) except OSError: pass # state_dict()表示只保存训练好的权重 torch.save(self.model.state_dict(), root + 'squeeze_model_' + 'version' + str(opt.version) + '_epoch' + str(opt.epoch) + '.pt') def plotLoss(self): Fig = plt.figure() plt.plot(range(len(self.losses)), self.losses) plt.show()
调用其中的train()方法进行训练,命令如下:
讯享网python train_SqueezeNet.py –batch_size=128 –epoch=20 –lr=0.1 –m=0.9 –version=1 –wd=0.001
然后进行测试:
correct = 0total = 0model = model.eval()with torch.no_grad(): for i, (X, Y) in enumerate(testloader): X, Y = X.to(’cuda:0‘), Y.to(’cuda:0‘) Pred = model(X) , predicted = torch.max(Pred.data, dim=1) correct += torch.sum((predicted==Y)) total += Y.size(0) print(f’第 {blue(str(i))} 批测试精准度: {100*correct/total} %‘)
启动测试的代码如下:
讯享网python test_SqueezeNet.py –batch_size=128 –epoch=20 –version=1
训练结果:
放一下version2的损失曲线如下:

迭代5次:
迭代20次:
迭代20次的精度:
与我自己用pytorch搭建的AlexNet精度基本一致,甚至有超过AlexNet的趋势!
下载
在公众号「计算机视觉工坊」后台,回复「3dcv」,即可获取工业3D视觉、SLAM、自动驾驶、三维重建、事件相机、无人机等近千余篇最新顶会论文;巴塞罗那自治大学和慕尼黑工业大学3D视觉和视觉导航精品课件;相机标定、结构光、三维重建、SLAM,深度估计、模型部署、3D目标检测等学习资料。
3D视觉方向交流群成立啦
目前工坊已经建立了3D视觉方向多个社群,包括SLAM、工业3D视觉、自动驾驶、三维重建、无人机方向,细分群包括:
[工业3D视觉]相机标定、立体匹配、三维点云、结构光、机械臂抓取、缺陷检测、6D位姿估计、相位偏折术、Halcon、摄影测量、阵列相机、光度立体视觉等。
[SLAM]视觉SLAM、激光SLAM、语义SLAM、滤波算法、多传感器融合、多传感器标定、动态SLAM、MOT SLAM、NeRF SLAM、机器人导航等。
[自动驾驶]深度估计、Transformer、毫米波|激光雷达|视觉摄像头传感器、多传感器标定、多传感器融合、自动驾驶综合群等、3D目标检测、路径规划、轨迹预测、3D点云分割、模型部署、车道线检测、Occupancy、目标跟踪等。
[三维重建]NeRF、多视图几何、OpenMVS、MVSNet、colmap、纹理贴图等
[无人机]四旋翼建模、无人机飞控等
除了这些,还有求职、硬件选型、视觉产品落地、最新论文、3D视觉最新产品、3D视觉行业新闻等交流群
大家可以添加小助理v: dddvisiona,备注:加群+方向+学校|公司, 小助理会拉你入群。
添加小助理v:dddvisiona,加群+方向+学校|公司,拉你入群

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/181908.html