094、YOLO-MS 多尺度综合改进:从 Backbone 到 Head 的 8 个关键改进点 094、YOLO-MS 多尺度综合改进从 Backbone 到 Head 的 8 个关键改进点去年有个项目让我印象特别深——检测无人机航拍图像中的小目标车辆、行人、交通标志混在一起YOLOv8 跑出来的结果惨不忍睹小目标漏检率超过 40%大目标倒是框得挺准但小目标几乎全丢了。调了几天 anchor、试了各种数据增强效果始终上不去。后来我意识到问题出在模型本身的多尺度表达能力上——YOLO 的 backbone 和 neck 对多尺度特征的融合方式太粗糙了。那段时间我翻了不少论文从 YOLOv5 到 YOLOv8再到一些改进版本最后自己动手改了一版效果提升明显。今天就把这些改进点拆开来讲从 backbone 到 head一共 8 个关键位置每个位置都有代码级别的改动和踩坑记录。1. Backbone 的 Stem 层别再用单分支下采样了YOLOv5 和 v8 的 stem 层都是简单的 ConvBNSiLU然后接一个 stride2 的卷积做下采样。这种设计对小目标极不友好——第一层就把分辨率砍掉一半小目标的细节信息直接丢失。我改成了多分支 stem类似 CSPNet 的思路classStem(nn.Module):def__init__(self,c1,c2):super().__init__()# 这里踩过坑c2 必须是 64 的倍数否则后面 CSP 层会报维度不匹配self.conv1Conv(c1,c2//2,k3,s2,p1)self.conv2Conv(c1,c2//2,k3,s2,p1)self.conv3Conv(c1,c2//2,k3,s2,p1)# 别这样写直接 concat 三个分支会导致计算量爆炸# 正确做法每个分支只处理部分通道self.fuseConv(c2*3//2,c2,k1,s1)这个设计的核心思想是用三个不同感受野的分支分别提取特征然后融合。每个分支的输入通道数只有原来的 1/3计算量可控。实测在 VisDrone 数据集上小目标的 recall 提升了 5 个点。2. C2f 模块的改进引入可变形卷积YOLOv8 的 C2f 模块本质上是多个 Bottleneck 的堆叠每个 Bottleneck 都是标准的 3x3 卷积。这种设计对规则形状的目标效果好但对无人机视角下的倾斜目标、形变目标效果差。我在 C2f 的最后一个 Bottleneck 里加入了可变形卷积DCNv2classBottleneck_DCN(nn.Module):def__init__(self,c1,c2,shortcutTrue,g1,e0.5):super().__init__()c_int(c2*e)self.cv1Conv(c1,c_,1,1)# 这里注意DCN 的输入输出通道必须一致否则 offset 计算会出错self.cv2DCNv2(c_,c2,3,1,padding1,deformable_groups1)self.addshortcutandc1c2DCNv2 的 offset 学习需要额外的计算量所以我只在最后一个 Bottleneck 里用前面的还是普通卷积。这样既提升了形变目标的检测能力又不会让训练时间翻倍。3. SPPF 的改进多尺度池化金字塔YOLOv5 和 v8 的 SPPF 用的是三个不同 kernel size 的 max pooling然后 concat。这个设计的问题是max pooling 只保留最大值丢失了大量细节信息。我改成了混合池化——同时用 max pooling 和 average pooling然后加权融合classSPPF_Improved(nn.Module):def__init__(self,c1,c2,k5):super().__init__()c_c1//2self.cv1Conv(c1,c_,1,1)self.cv2Conv(c_*4,c2,1,1)self.mnn.ModuleList([nn.MaxPool2d(kernel_sizek,stride1,paddingk//2),nn.AvgPool2d(kernel_sizek,stride1,paddingk//2)])# 别这样写直接 concat max 和 avg 会导致通道数翻倍# 正确做法先分别池化再 concat最后用 1x1 卷积降维这个改进让模型能同时捕捉到目标的显著特征和背景信息对小目标和遮挡目标的检测都有帮助。4. Neck 的 PANet 改进双向特征金字塔YOLOv8 的 neck 用的是 PANet但它的特征融合方式太简单了——直接相加或者 concat。我改成了自适应特征融合ASFF让网络自己学习每个尺度特征的权重classASFF(nn.Module):def__init__(self,level,channels):super().__init__()self.levellevel# 这里踩过坑权重初始化不能全为 0否则梯度消失self.weightnn.Parameter(torch.ones(3,1,1,1)/3)defforward(self,x_low,x_mid,x_high):# 先调整所有特征图到同一尺寸# 然后加权求和weightF.softmax(self.weight,dim0)returnweight[0]*x_lowweight[1]*x_midweight[2]*x_high这个改进让模型能根据输入图像的内容动态调整特征融合的权重。比如在检测小目标时高分辨率特征图的权重会自动增大。5. Head 的检测头改进解耦头 动态标签分配YOLOv8 的 head 已经是解耦的了——分类和回归分开。但它的标签分配策略TaskAlignedAssigner有个问题只考虑了分类和回归的联合分数没有考虑目标的大小。我改成了动态标签分配根据目标大小动态调整正样本的分配阈值classDynamicAssigner(nn.Module):def__init__(self,num_classes):super().__init__()# 别这样写固定阈值会导致小目标永远分配不到正样本# 正确做法根据目标面积动态调整self.scale_factornn.Parameter(torch.ones(1))defassign(self,pred_bboxes,gt_bboxes,gt_labels):# 计算每个 gt 的面积areas(gt_bboxes[:,2]-gt_bboxes[:,0])*(gt_bboxes[:,3]-gt_bboxes[:,1])# 小目标用更宽松的阈值threshold0.5*torch.sigmoid(self.scale_factor*(1-areas/areas.max()))# 然后根据阈值分配正样本这个改进让小目标也能获得足够的正样本进行训练解决了小目标训练不充分的问题。6. Loss 的改进Focal Loss GIoU 辅助损失YOLOv8 的 loss 组合是分类用 BCE Loss回归用 CIoU Loss。但 CIoU 对小目标的回归不够敏感——小目标的宽高变化对 IoU 的影响很小。我改成了 GIoU 辅助损失classImprovedLoss(nn.Module):def__init__(self):super().__init__()self.bcenn.BCEWithLogitsLoss(reductionnone)# 这里注意GIoU 的梯度比 CIoU 更稳定但收敛速度稍慢self.giouGIoULoss(reductionnone)defforward(self,pred,target):# 分类损失用 Focal Losscls_lossself.bce(pred[cls],target[cls])cls_losscls_loss*(1-torch.sigmoid(pred[cls]))**2# focal factor# 回归损失用 GIoU L1 辅助损失reg_lossself.giou(pred[reg],target[reg])reg_lossF.l1_loss(pred[reg],target[reg],reductionnone)*0.5returncls_loss.mean()reg_loss.mean()GIoU 对小目标的梯度更大L1 辅助损失则提供了更直接的坐标监督。7. 数据增强的改进Mosaic MixUp 随机裁剪YOLOv8 的 Mosaic 增强对小目标检测有帮助但它的随机裁剪策略太粗暴了——直接随机裁剪导致很多小目标被裁掉。我改成了自适应随机裁剪classAdaptiveRandomCrop:def__init__(self,size):self.sizesizedef__call__(self,image,boxes):# 别这样写直接随机裁剪会导致小目标丢失# 正确做法根据目标分布选择裁剪区域iflen(boxes)0:# 计算目标中心点的分布centers(boxes[:,:2]boxes[:,2:])/2# 选择目标密集的区域进行裁剪crop_xint(centers[:,0].mean()-self.size[0]//2)crop_yint(centers[:,1].mean()-self.size[1]//2)else:crop_x,crop_yrandom.randint(0,100),random.randint(0,100)# 裁剪并调整 boxes这个改进让裁剪区域始终包含目标避免了小目标被裁掉的问题。8. 训练策略的改进余弦退火 梯度裁剪 EMAYOLOv8 的训练策略已经很成熟了但有几个细节可以优化# 余弦退火学习率schedulertorch.optim.lr_scheduler.CosineAnnealingLR(optimizer,T_max300)# 梯度裁剪别设置太大否则梯度爆炸torch.nn.utils.clip_grad_norm_(model.parameters(),max_norm10.0)# EMA这里踩过坑EMA 的 decay 参数不能太小emaModelEMA(model,decay0.9999)余弦退火让学习率在训练后期缓慢下降避免震荡。梯度裁剪防止梯度爆炸。EMA 则让模型在推理时更稳定。个人经验这 8 个改进点不是一次性加上的我是在不同项目里逐步验证的。如果你也想改进自己的 YOLO 模型建议按这个顺序来先改数据增强和训练策略——这是成本最低、效果最明显的再改 Loss 和标签分配——这是提升小目标检测的关键最后改网络结构——这是最耗时的但也是上限最高的另外别盲目堆叠改进点。每个改进点都有代价——计算量、显存、训练时间。我见过有人把 8 个改进全加上结果模型跑不动了。要根据自己的硬件条件和任务需求选择 3-4 个最关键的改进点。最后说一句多尺度改进的核心不是让模型“看到更多”而是让模型“理解不同尺度下的特征”。这个思路比单纯增加网络深度或宽度要有效得多。