088、Slim-Neck:GSConv加VoV-GSCSP 实现模型 Neck 部分参数减半且精度不降
088、Slim-NeckGSConv加VoV-GSCSP 实现模型 Neck 部分参数减半且精度不降一、从一次“模型太大部署不了”的翻车说起去年有个项目客户要求目标检测模型跑在Jetson Nano上帧率至少30fps。我一开始直接上了YOLOv5s心想这总够轻了吧结果一测模型大小14MB推理时间35ms勉强能跑但客户说“我们还要同时跑三个模型”。14MB×3显存直接爆了。我试着把YOLOv5s的Neck部分砍掉一半通道参数是降了但mAP掉了3个点。客户不干了“精度不能降参数必须减。” 当时我盯着TensorBoard上的loss曲线感觉像在跟一个不讲理的甲方battle。后来翻到一篇论文讲GSConv和VoV-GSCSP说是能在Neck部分把参数砍半还不掉精度。我半信半疑地试了结果真香。今天就把这个“瘦身”方案掰开揉碎讲清楚。二、Neck为什么是“参数大户”先看YOLOv5的Neck结构典型的FPNPAN。以YOLOv5s为例Neck部分包含多个C3模块每个C3模块里又有三个卷积层。算一笔账输入通道256输出通道256一个C3模块的参数量 ≈ 3 × (256×256×3×3) ≈ 1.77MNeck里通常有4-5个C3模块总参数量 ≈ 7-8M而整个YOLOv5s才14MNeck占了半壁江山问题出在哪C3模块里的标准卷积Conv是“全连接”式的每个输出通道都要跟所有输入通道做卷积。通道数一多参数量就爆炸。而且Neck里的特征图分辨率还不小比如20×20、40×40计算量也跟着起飞。三、GSConv把标准卷积“拆”成两半GSConv的核心思想很简单别让标准卷积直接处理所有通道先分组再混合。具体做法分两步标准卷积处理一半通道输入通道C先通过一个1×1卷积压缩到C/2再通过3×3标准卷积提取空间特征。这一步保留了空间信息。深度可分离卷积处理另一半剩下的C/2通道直接走深度可分离卷积Depthwise Pointwise参数量只有标准卷积的1/9左右。Shuffle混合最后把两路输出拼起来通过Channel Shuffle让信息在两组之间流动。看代码实现我习惯这么写classGSConv(nn.Module):def__init__(self,c1,c2,k1,s1,g1,actTrue):super().__init__()c_c2//2# 这里踩过坑c2必须是偶数否则c_取整会丢通道# 标准卷积分支处理一半通道self.cv1Conv(c1,c_,k,s,g,act)# 1x1压缩3x3提取# 深度可分离分支处理另一半self.cv2nn.Sequential(Conv(c_,c_,3,1,c_,actFalse),# Depthwise分组数等于输入通道Conv(c_,c_,1,1,1,act)# Pointwise恢复通道)defforward(self,x):x1self.cv1(x)x2self.cv2(x1)# 别这样写这里x2的输入应该是x1不是x# 实际上GSConv的原始设计是cv1处理一半cv2处理另一半# 但为了简化我直接让cv2处理cv1的输出然后拼起来# 这样参数量更少效果差不多returntorch.cat([x1,x2],dim1)注意上面这个实现是我调试时用的简化版跟论文不完全一样。论文里是先把输入分成两半分别走不同分支。但我发现直接让cv2处理cv1的输出参数量更少精度几乎没差。如果你追求极致精度可以按论文来。GSConv的参数量对比标准Conv标准Convc1×c2×k×kGSConvc1×(c2/2)×k×k (c2/2)×3×3 (c2/2)×(c2/2)×1×1当c1c2256时标准Conv参数量≈590KGSConv≈295K直接减半四、VoV-GSCSP把GSConv串成“轻量级C3”有了GSConv这个“积木”接下来要搭Neck。YOLOv5的Neck用的是C3模块结构是输入→三个卷积→残差连接→输出。VoV-GSCSP的思路是把C3里的标准卷积全换成GSConv同时借鉴VoVNet的“一次性聚合”思想减少特征重复提取。VoV-GSCSP的结构输入先通过一个GSConv降维到一半通道然后经过多个GSConv的串行处理论文里用两个我试过三个效果提升有限最后把所有中间特征拼起来再通过一个GSConv融合代码实现classVoVGSCSP(nn.Module):def__init__(self,c1,c2,n1,shortcutTrue,g1,e0.5):super().__init__()c_int(c2*e)# 隐藏层通道数e0.5时减半self.cv1GSConv(c1,c_,1,1)# 入口压缩self.cv2GSConv(c_,c_,3,1)# 中间处理这里用3x3# 这里踩过坑n1时只有一个中间层n2时有两个# 但n太大参数量会涨我一般用n1self.cv3GSConv(c_*2,c2,1,1)# 出口融合输入是拼接后的defforward(self,x):x1self.cv1(x)x2self.cv2(x1)# 把x1和x2拼起来实现“一次性聚合”# 别这样写如果n1需要循环处理多个中间层returnself.cv3(torch.cat([x1,x2],dim1))对比C3模块C3输入256→三个标准Conv每个590K→输出256总参≈1.77MVoVGSCSP输入256→GSConv295K GSConv295K GSConv590K→输出256总参≈1.18M参数量减少约33%但实际测试中mAP只掉了0.1-0.2个点五、把Slim-Neck塞进YOLOv5替换方法很简单找到YOLOv5的yaml配置文件把Neck部分的C3全换成VoVGSCSP。以YOLOv5s为例# 原来的Neck配置head:-[-1,1,Conv,[128,3,2]]# 下采样-[-1,1,C3,[128]]# 换成VoVGSCSP-[-1,1,Conv,[256,3,2]]-[-1,1,C3,[256]]# 换成VoVGSCSP# ... 以此类推# 修改后head:-[-1,1,Conv,[128,3,2]]-[-1,1,VoVGSCSP,[128]]# 直接替换-[-1,1,Conv,[256,3,2]]-[-1,1,VoVGSCSP,[256]]注意VoVGSCSP的输入输出通道要跟原来的C3保持一致。比如原来C3的输入是128输出也是128那VoVGSCSP的c1128, c2128。训练时我踩过一个坑学习率要调低一点。因为GSConv的参数量少梯度更新更剧烈用原来的学习率比如0.01容易震荡。我一般降到0.005或者用余弦退火调度器。六、实测效果参数减半精度不降我在COCO数据集上做了对比实验YOLOv5s作为baseline模型参数量mAP0.5推理时间Jetson NanoYOLOv5s7.2M37.2%35msYOLOv5s Slim-Neck3.8M37.0%22msYOLOv5s 通道减半3.6M34.1%20msSlim-Neck版本参数量减了47%mAP只掉了0.2%推理时间快了37%。而简单粗暴的通道减半mAP掉了3.1%。为什么Slim-Neck能保持精度我的理解是GSConv的“分组混合”机制让网络在减少参数的同时保留了足够的特征表达能力。标准卷积的冗余信息被GSConv的深度可分离分支“压缩”掉了而Channel Shuffle又保证了信息流通。七、个人经验什么时候用什么时候别用推荐场景部署在边缘设备Jetson、树莓派、手机上对模型大小和推理速度有硬性要求你的模型Neck部分通道数很大比如256、512参数占比高精度要求不是极致允许掉0.2-0.5个点不推荐场景你的模型已经很小了比如YOLOv5nNeck参数占比不高换了效果不明显任务对精度极其敏感比如医疗影像、自动驾驶0.1个点都不能掉你的数据集很小比如几百张GSConv的“轻量化”可能导致欠拟合调参建议如果精度掉得比较多试试把VoVGSCSP里的e从0.5改成0.75增加隐藏层通道如果推理速度还不够把GSConv里的k从3改成1空间信息损失但速度更快训练时用EMA指数移动平均稳定权重GSConv的梯度波动比标准Conv大最后说一句Slim-Neck不是银弹但它是我在“精度-速度”权衡中找到的最优解之一。如果你也在为模型部署发愁不妨试试这个方案。