首页 / 资讯中心 / 文章详情

EfficientNet骨干网络替换YOLOv3:Pytorch实现与优化指南

EfficientNet骨干网络替换YOLOv3:Pytorch实现与优化指南 ★ FEATURED ARTICLE
1. 项目概述与整体思路拆解最早接触YOLOv3的时候用的是原始的Darknet53作为骨干特征提取网络。后来做项目时遇到一个很现实的问题模型参数量不够精简在边缘设备上推理速度总差一口气而且对尺度过小的目标召回率不太理想。当时手头又攒了一批标注数据就想换一个更强的骨干网络试试水。对比了一圈最终决定用efficientnet系列来做特征提取主干搭一个Pytorch版的YOLOv3检测平台。这个项目的核心价值在于两点一是efficientnet的复合缩放策略能让我们按需调节模型的深度、宽度和输入分辨率二是它引入的MBConv模块在保持精度的同时大幅降低了计算量。把这两者和YOLOv3的多尺度检测机制结合本质上就是给原有的检测框架换了一个更强的心脏。适合什么人来参考已经跑通YOLOv3基本流程、对Pytorch有一定基础的读者最适合当然如果只是刚入门目标检测也可以顺着文章把整个网络结构完整过一遍收获同样不小。为什么我最终选择efficientnet而不是ResNet或者MobileNet这里有一个直接的原因ResNet的残差结构强但参数冗余多MobileNet够快但精度上限摆在那里。efficientnet在ImageNet上精度排名靠前同时参数量可控尤其是B0到B4这几个版本很适合作为检测任务的backbone。YOLOv3本身是个对backbone敏感的网络特征提取的质量直接影响三个尺度上的检测效果所以选定一个精度好、速度均衡的主干整个项目的天花板就高了一截。2. EfficientNet系列模型核心机制详解2.1 复合缩放方法要理解efficientnet系列为什么好用必须先从它的复合缩放方法说起。传统做法是单独调深度、宽度或者分辨率但efficientnet给出的思路是同时放大这三个维度用一组固定的缩放系数来协调。公式很简单用一个混合系数φ控制整体深度方向乘以α的φ次方宽度方向乘以β的φ次方分辨率方向乘以γ的φ次方其中α、β、γ的乘积近似等于2。这样做的好处是模型在不同规模下都能保持结构上的平衡不会出现某一维度成为瓶颈。我当时搭平台时选了B0作为起点输入分辨率定为416。为什么不是默认的224或者512因为YOLOv3的多尺度检测头需要下采样倍率精确匹配416刚好是32的整数倍而且也算在efficientnet缩放范围附近。实际操作中如果你要用B4甚至更大的版本输入分辨率建议同步上调否则前面的stem层和后续MBConv的感受野计算会出现别扭的错位。2.2 MBConv模块与SE注意力efficientnet的基本块是MBConv核心结构是1x1升维、3x3或5x5深度可分离卷积、再1x1降维中间用残差连接串联起来。这种结构的计算效率比标准卷积高得多但这里真正拉开差距的是SE模块对特征图先做全局平均池化压缩成通道描述向量再经过两个全连接层学习通道之间的依赖关系最后用sigmoid生成0到1之间的权重逐通道乘回去。说白了这个操作就是在告诉网络哪些通道重要哪些可以忽略。在实际替换YOLOv3骨干网络的时候SE模块的存在让特征提取对语义信息更敏感。比如检测小目标时低层的细节特征和高层的语义特征通过SE重新校准后融合效果会更干净。但要注意一个细节SE模块里的第一个全连接层会把通道数压缩到原来的四分之一然后在第二个全连接层再还原。这一缩一放都是可学习的所以如果你的预训练权重是ImageNet上来的换到检测任务时这部分参数通常能很快适应不太需要额外处理。2.3 系列版本对比与选型建议efficientnet B0到B7参数规模递增但检测任务里并不是越大越好。我自己对比过B0和B3版本的表现B3在精度上有提升但显存占用直线上升训练速度明显变慢。如果你手头只有一张消费级显卡B0甚至B1是更务实的选择。下表是我实测下来比较有参考价值的对照数据模型版本输入分辨率参数量约单卡训练显存占用推理速度参考B04165.3M5-6GB最快B14167.8M7-8GB适中B24169.2M8-9GB较慢B341612.3M10GB慢需要说明的是这只是我在固定batch size和输入尺寸下的实测参考值不同环境会有些出入。如果你的检测目标很明确是密集小目标可以在B0基础上把分辨率调到512比直接换B3性价比更高。这个思路其实就是efficientnet复合缩放思想的实践落地想要更好的检测效果先调分辨率再考虑扩大模型。3. YOLOv3检测头的构建细节3.1 特征金字塔结构YOLOv3的检测头依赖特征金字塔FPN就是把backbone不同层级的特征图拉出来做融合。原始Darknet53是提供三个尺度的特征图分别对应下采样8倍、16倍、32倍。换成efficientnet之后我们需要确定从哪里取出这三层特征。以B0为例我的做法是取倒数第3层、倒数第2层和最后一层的输出。这几个层的下采样倍数恰好和YOLOv3的需求对齐后续接上FPN就是顺理成章的事。FPN的具体操作是最深的特征图先经过1x1卷积调整通道数然后上采样到前一层的尺寸两路特征concat到一起再经过3x3卷积融合语义和空间信息。这个过程从深到浅逐级往上形成一条自上而下的路径。很多初学者在这里会犯一个错误concat之前忘记统一通道数。两个特征图的宽高一致了但通道数不一致直接concat会报维度错误或者形状对不上。所以每一路进入concat之前都必须明确记录输出通道数最好是写个辅助函数打印每层shape肉眼确认一遍再往下走。3.2 锚框与输出解码YOLOv3的检测头输出的是一个多维张量形状是batch、3个锚框、特征图高、特征图宽、5加类别数。这个5对应的是中心点x、y、宽高w、h以及置信度。注意这里网络输出的并不是真实的坐标而是经过编码的偏移量。解码的过程在推理阶段是必须写对的环节x和y要经过sigmoid约束到0到1之间表示锚框中心相对当前网格左上角的偏移比例w和h则是先取指数再乘以预设的锚框宽高。我在写解码逻辑的时候踩过一个坑直接把sigmod写成了softmax导致训练时loss下降正常但推理出来的目标中心点分布完全错乱。排查了半天才发现是解码函数里的激活函数写错了。所以建议各位在写完解码函数后随便跑一张测试图对比一下gt框和预测框的位置视觉上能发现问题就说明解码逻辑有问题。锚框的选择也要重新考虑。原版YOLOv3在COCO上聚类得到的9组锚框并不一定适合你的数据集尤其是换了efficientnet之后特征图的语义分布变了锚框和特征的对齐关系也会变化。我建议在新数据集上用k-means重新聚类一组锚框迭代次数不用太多50次左右就能收敛出合理结果。聚类时注意用平均IoU作为距离度量而不是欧氏距离这一点会直接影响聚类效果。4. 实操Pytorch网络搭建全过程4.1 环境准备与依赖安装开始搭网络之前先把环境理顺。我这里用的是Pytorch 1.8以上的版本配合torchvision里的efficientnet预训练权重接口。Pytorch官方从1.6开始就内置了efficientnet系列模型定义直接用torchvision.models.efficientnet_b0就能拿到带ImageNet权重的模型。但有一个问题torchvision自带的模型结构里分类头是1000类我们必须把最后的分类层去掉同时把前面的特征提取部分完整保留。依赖方面最基本的几个包是torch、torchvision、numpy、opencv-python、tqdm。如果要用混合精度训练还需要apex或者直接用Pytorch原生自带的torch.cuda.amp我建议优先用原生的amp省去apex的编译环节也少一些坑。数据增强部分我用的是albumentations库它对bbox的变换支持比较完善比手动写随机翻转和裁剪省心很多。4.2 骨干网络替换实现替换backbone的步骤其实不复杂关键是代码组织要干净。我习惯的做法是单独写一个backbone.py文件里面定义获取efficientnet各个stage输出的函数。torchvision的efficientnet内部是用features这个Sequential容器组织的里面包含若干个MBConv模块通过索引就能拿到需要的特征层。以下是核心的backbone替换代码结构我贴的是精简过的版本import torch import torch.nn as nn from torchvision import models class EfficientNetBackbone(nn.Module): def __init__(self, model_nameefficientnet-b0, pretrainedTrue): super().__init__() if model_name efficientnet-b0: self.backbone models.efficientnet_b0(pretrainedpretrained) elif model_name efficientnet-b1: self.backbone models.efficientnet_b1(pretrainedpretrained) # 去掉分类头 self.backbone nn.Sequential(*list(self.backbone.features.children())) # 记录要取出的特征层索引 if model_name efficientnet-b0: self.stage_idx [-3, -2, -1] def forward(self, x): feats [] for idx, layer in enumerate(self.backbone): x layer(x) if idx in [len(self.backbone) i for i in self.stage_idx]: feats.append(x) return feats注意这段代码里的stage_idx用的是相对索引便于适配不同版本。但实际运行时我建议打印一下backbone的层数和每层输出shape把实际索引定死避免后续版本升级导致索引变化。这里还有一个重要操作如果加载了预训练权重一定要把权重的strict参数设为False因为分类头已经不存在了否则加载权重时会直接报错。具体写法是load_state_dict(state_dict, strictFalse)然后在自己的训练脚本里只拷贝前面对应的键值。4.3 FPN与YOLO头的代码实现FPN部分我通常单独写一个fpn.py里面包含自顶向下的特征融合和三个独立的YOLO检测头。每个YOLO头的结构是3x3卷积加1x1卷积把通道数统一到锚框数乘以5加类别数。这里有两个细节需要说明每个尺度输出通道数理论上是一样的因为锚框数是3类别数固定时通道数自然一致但FPN每一层输入的通道数不同所以在concat前要先把深层的1x1卷积通道数调整成和浅层一致再上采样做融合。多尺度检测的标签分配逻辑也需要重新梳理。YOLOv3把gt框分配给不同尺度的特征图规则是看gt框的宽高和锚框的IoU大小。小目标倾向于分配到浅层特征图大目标分配到深层特征图。换efficientnet后浅层特征的感受野依然相对较小这种设计逻辑没有变化所以标签分配的代码基本可以沿用。网络结构搭建完成后我习惯用torchsummary打印模型的各层参数数量和输出shape核对一遍和预期是否一致。这一步虽然简单但能避免在训练到一半时才发现形状不对的尴尬。5. 训练配置与参数调整心得5.1 预训练权重加载策略换backbone之后很多人会直接沿用之前YOLOv3的预训练模型加载方式这里有个大坑需要特别提醒YOLOv3原版的完整权重文件中包含Darknet53的backbone参数直接加载到efficientnet架构里必然不匹配。正确做法是从torchvision加载efficientnet的ImageNet权重作为backbone初始值FPN和检测头部分随机初始化即可。在loss刚开始下降时会有一段比较明显的震荡期原因是检测头的权重还没有被预热但随着迭代进行会迅速收敛。如果你想冻结backbone先训练检测头可以在前面几个epoch把backbone的requires_grad设为False让检测头先拟合数据分布。等检测头有了一定预测能力后再解冻backbone用较小的学习率做微调。我实际测试下来这个策略能让训练稳定不少尤其是数据集规模比较小的时候作用特别明显。5.2 学习率与损失函数设置学习率策略上我推荐先用warmup预热再配合cosine退火调整。初始学习率设置在1e-3左右的问题是backbone微调时容易破坏预训练特征所以我在解冻backbone后会把学习率降到1e-4到5e-4之间。这里分享一个经验性的比例backbone学习率一般是检测头的十分之一如果检测头用1e-3backbone就用1e-4。虽然会增加一点调参成本但换来的是更稳定的训练曲线。损失函数方面继续沿用YOLOv3原本的三部分损失坐标损失、置信度损失和分类损失。坐标损失用MSE或者SmoothL1都可以置信度损失和分类损失用BCEWithLogits。注意预测置信度目标是IoU不是简单的0和1这一点直白说就是预测框和gt框的IoU越大置信度目标越接近1IoU小就接近0在计算损失时要用这个IoU值作为回归目标而不是硬标签。5.3 性能评估与结果分析训练完成后评估指标我通常看mAP和FPS两个维度。和原版YOLOv3对比使用efficientnet B0做backbone后在相同数据集和相同输入分辨率下mAP大约有1到2个百分点的提升模型体积缩小到原来的三分之二左右。B3版本在mAP上能再往上走一点但推理帧率下降明显。这个权衡没有标准答案完全取决于实际部署要求。有一次我把输入分辨率从416提高到512后小目标的召回率明显上升但整体FPS从45掉到32还是能接受的范围。如果你做的是边缘设备实时推理416加上B0可能是甜点组合如果离线批处理且精度优先可以考虑B3或更大版本加高分辨率。6. 常见问题与排查技巧实录6.1 特征图尺寸不匹配问题搭建过程中最频繁遇到的就是特征图尺寸对不上。efficientnet的下采样倍率和Darknet53并不是完全一致的尤其是一些版本在stem层就做了2倍下采样后面又有若干次2倍下采样导致某些中间层的输出尺寸和你预期的不一样。我的排查方法是在backbone forward里逐层打印输出shape找到实际下采样倍数符合8、16、32的位置再把这三个位置的索引固定到stage_idx里。这个方法虽然原始但比盯着论文里的结构图猜靠谱得多。还有一类尺寸问题出在FPN的上采样环节。如果浅层特征和深层特征的宽高是16比8用F.interpolate做双线性上采样时要指定scale_factor2而不是目标尺寸否则在动态输入尺寸下容易错位。我自己习惯用目标尺寸模式把浅层特征的宽高传给up_sample函数确保一倍一倍对齐。6.2 显存溢出与计算瓶颈显存溢出也是常见问题尤其当输入分辨率上调或者batch size加大后。最直接的办法是减小batch size但如果你想保住batch size可以尝试梯度累积每累积4个batch更新一次参数等效于扩大batch size而不会增加显存峰值。混合精度训练也是省显存的好办法开启amp之后大约能省30%左右的显存训练速度还有提升。不过要注意efficientnet里的某些操作对精度比较敏感例如SE模块中的平均池化在fp16下可能出现数值波动所以我把这些层单独保留在fp32中用amp的自动管理机制基本能处理。另一个计算瓶颈是深度可分离卷积在GPU上的利用率不如标准卷积高。efficientnet理论计算量很低但实际推理时如果GPU对depthwise卷积支持不够好可能不会比Darknet53快多少。我测试过在消费级显卡上B0和Darknet53的速度差距并没有想象中大但在带有深度卷积优化的设备上差距就体现出来了。这一点在你选型时要有心理准备。6.3 训练不收敛或loss异常训练过程中loss出现nan或者不下降先检查输入数据是否存在全零图像或异常标注框这类数据会让坐标损失爆炸。我的习惯是在数据加载器里加一个过滤函数gt框宽高小于等于0的直接丢弃超过图像边界的做裁剪修正。还有一个常见问题类别数设置错误。YOLOv3的输出通道计算是3乘以5加类别数这个数一旦算错网络输出和标签形状对不上训练直接报错。另外特别提醒一点如果loss一直在下降但mAP不涨大概率是置信度目标的IoU计算出了问题。很多实现里面直接用了0或1作为置信度标签但这并不符合YOLOv3原文的做法。我试过改回IoU目标之后mAP立即有了明显提升。如果你遇到类似现象从这一块入手排查大概率能解决问题。6.4 锚框聚类结果异常锚框聚类是个看似简单但容易出错的操作。我在某次实验中发现聚类出的前三组锚框宽高比极其接近导致三个尺度的检测头分化不明显。后来检查发现是聚类时用了欧氏距离结果大框的坐标值主导了距离计算小框被忽略。改成IoU距离后聚类结果立刻合理多了九个锚框分布在不同尺度上大中小三个层次都有。这个改动虽小但对多尺度检测效果的影响很直接。还有一个经验聚类时不要直接对所有gt框都聚类一次建议先按gt框面积分成大、中、小三组每组聚类出3个锚框。这样做可以让每一尺度的锚框都专注于对应尺度的目标效果比全局聚类更稳定。我在实际项目中用这个方法重新生成锚框后小目标的召回率提升了约3个百分点。7. 一些踩坑后的经验总结整个项目做下来最大的感受是框架选型固然重要但真正决定检测效果上限的是细节处理。efficientnet带来的收益在精度上确实看得见但如果没有把解码逻辑、锚框聚类、置信度目标这些细节处理到位好backbone的效果也体现不出来。我试过在同样配置下只调整锚框聚类方式mAP就从69.8升到了72.1这个提升幅度甚至超过了更换更大版本backbone的收益。训练时还有一个容易被忽略的点数据增强策略要跟着backbone一起调整。efficientnet在ImageNet上训练时使用了随机裁剪和翻转所以迁移到检测任务时mosaic增强和mixup这类强增强可以和efficientnet天然兼容。但如果你的数据集本身就很小强增强反而会影响收敛这时要优先保障训练稳定性而不是追求数据集多样性。我一般会在数据量少于5000张时关闭mixup只保留mosaic和基本翻转。部署阶段的小建议是导出模型时一定要把预处理步骤里的归一化参数和网络结构一起固化下来。efficientnet的标准化用的是ImageNet的mean和std这一点和原版YOLOv3所用的0到1归一化完全不同。如果你是拿现成项目改的直接沿用原项目的预处理就会让输入分布偏掉网络输出自然不对。我第一次导出到推理引擎时就是吃了这个亏检测出来的框全部偏移排查了大半天才发现是预处理的问题。如果你后续想继续扩展这个项目可以尝试用efficientnet作为backbone配合更轻量的检测头或者把FPN替换成BiFPN结构这些方向我都做过不同程度的验证整体来说在efficientnet的基础上做加减法弹性比Darknet53大得多。这个项目也让我对模型结构替换这件事有了更系统的认识换backbone不是简单的网络拼接而是从数据输入、特征提取、标签分配到部署推理的全链路适配。顺着这个思路走再复杂的模型组合也能一步步啃下来。
阅读完成 · 觉得有帮助?
咨询建站