030、多尺度特征融合 PAN-FPN:自顶向下 Upsample加Concat 与自底向上 Downsample加Concat

发布时间:2026/6/5 22:02:37

030、多尺度特征融合 PAN-FPN:自顶向下 Upsample加Concat 与自底向上 Downsample加Concat 030、多尺度特征融合 PAN-FPN自顶向下 Upsample加Concat 与自底向上 Downsample加Concat记得去年在调试一个无人机小目标检测项目用的YOLOv5s跑VisDrone数据集。训练了200个epochmAP卡在0.32上不去。我盯着TensorBoard里的特征图可视化发现高层语义特征确实丰富但小目标的细节信息在深层几乎被稀释没了。当时第一反应是“加个FPN不就完了”结果加上之后mAP只涨了0.03。后来翻YOLOv4论文看到PANet那部分才意识到问题出在哪——FPN只有单向的信息流动底层的位置信息传到高层就衰减了高层语义想回传到底层没门。这个坑我踩了整整一周。后来把PAN-FPN接上mAP直接跳到0.41。今天就把这个结构掰开揉碎了讲清楚代码逐行注释包括那些容易翻车的地方。从FPN到PAN-FPN为什么需要双向融合FPNFeature Pyramid Network的核心思路很简单自顶向下把高层的语义信息通过上采样Upsample传递到低层然后和低层的特征做Concat。这样低层特征图既有高分辨率的位置细节又包含了高层语义。但这里有个隐藏问题——信息流是单向的。低层特征对高层没有任何贡献。在目标检测里小目标依赖低层的高分辨率特征大目标依赖高层的语义特征。FPN只解决了“高层帮低层”没解决“低层帮高层”。PAN-FPNPath Aggregation Network - Feature Pyramid Network在FPN基础上加了一条自底向上的路径。简单说就是先自顶向下传语义再自底向上传位置。两条路径通过横向连接Lateral Connection融合形成一个“沙漏”结构。YOLOv4和YOLOv5都用了这个设计区别在于YOLOv5把PAN-FPN做得更轻量去掉了原版PANet里的一些冗余卷积。自顶向下路径Upsample Concat先看自顶向下这部分。假设我们从Backbone拿到了三个尺度的特征图C3大尺度高分辨率、C4中尺度、C5小尺度高语义。C5经过1x1卷积降维后得到P5然后P5上采样到和C4一样大小和C4经过1x1卷积后的结果Concat再经过3x3卷积融合得到P4。同理P4上采样后和C3融合得到P3。代码实现里最容易翻车的地方是上采样的尺寸对齐。PyTorch的nn.Upsample默认用最近邻插值但如果你用scale_factor2当特征图尺寸是奇数时上采样后的尺寸会变成偶数导致Concat时维度不匹配。我踩过这个坑后来改成size参数手动指定目标尺寸或者用F.interpolate配合align_cornersFalse。# 自顶向下路径以YOLOv5的PAN-FPN实现为例classFPN(nn.Module):def__init__(self,in_channels_list,out_channels):super().__init__()# 横向连接1x1卷积降维注意这里in_channels_list是[C3, C4, C5]的通道数self.lateral_convsnn.ModuleList([nn.Conv2d(in_ch,out_channels,1)forin_chinin_channels_list])# 融合后的3x3卷积用于消除上采样的混叠效应self.fpn_convsnn.ModuleList([nn.Conv2d(out_channels,out_channels,3,padding1)for_inrange(len(in_channels_list)-1)])# 这里踩过坑如果直接用nn.Upsample(scale_factor2)当特征图尺寸为奇数时上采样后尺寸会变成偶数# 比如7x7上采样后变成14x14但目标可能是13x13Concat直接报错# 所以这里用nn.Upsample(sizetarget_size)或者F.interpolatedefforward(self,inputs):# inputs是[C3, C4, C5]注意顺序是从大到小c3,c4,c5inputs# 先对每个层做1x1卷积降维p5self.lateral_convs[2](c5)# 小尺度高语义p4self.lateral_convs[1](c4)p3self.lateral_convs[0](c3)# 自顶向下从p5开始上采样后和p4融合# 注意上采样时用F.interpolatemodenearestalign_cornersFalse# 别这样写nn.Upsample(scale_factor2, modebilinear)bilinear会引入额外平滑破坏特征p4p4F.interpolate(p5,sizep4.shape[2:],modenearest)p4self.fpn_convs[1](p4)# 3x3卷积融合p3p3F.interpolate(p4,sizep3.shape[2:],modenearest)p3self.fpn_convs[0](p3)return[p3,p4,p5]# 注意返回顺序还是从大到小这里有个细节YOLOv5的FPN实现里横向连接后的特征直接和上采样后的特征相加Add而不是Concat。原版FPN用的是Concat但YOLOv5为了减少参数量改成了Add。效果上差别不大但Add对梯度流动更友好。如果你自己改代码建议先试Add不行再换Concat。自底向上路径Downsample Concat自底向上路径是PAN-FPN的精髓。从P3开始经过步长为2的3x3卷积下采样或者用MaxPool得到和P4一样大小的特征图然后和P4融合。注意这里融合方式也是Add但YOLOv5在融合后还会再接一个3x3卷积。下采样时最容易犯的错误是直接用nn.MaxPool2d(2)。MaxPool会丢失空间信息尤其对小目标不友好。YOLOv5用的是步长为2的3x3卷积既能下采样又能提取特征。如果你用MaxPool试试把下采样后的特征图可视化会发现边缘信息模糊了。# 自底向上路径接在FPN后面classPAN(nn.Module):def__init__(self,in_channels_list,out_channels):super().__init__()# 下采样卷积步长为2的3x3卷积注意输入输出通道数相同self.downsample_convsnn.ModuleList([nn.Conv2d(out_channels,out_channels,3,stride2,padding1)for_inrange(len(in_channels_list)-1)])# 融合后的3x3卷积self.pan_convsnn.ModuleList([nn.Conv2d(out_channels,out_channels,3,padding1)for_inrange(len(in_channels_list)-1)])# 这里踩过坑下采样卷积的padding要算准stride2时padding1才能保持尺寸整除# 如果输入是偶数尺寸下采样后正好减半如果是奇数会向下取整导致后续Concat尺寸不匹配# 所以建议在数据预处理时保证特征图尺寸是2的幂次defforward(self,inputs):# inputs是FPN的输出[p3, p4, p5]注意顺序是从大到小p3,p4,p5inputs# 自底向上从p3开始下采样后和p4融合# 别这样写nn.MaxPool2d(2)会丢失细节p4_downself.downsample_convs[0](p3)# 下采样到和p4一样大p4p4p4_down# 融合p4self.pan_convs[0](p4)# 3x3卷积p5_downself.downsample_convs[1](p4)# 继续下采样p5p5p5_down p5self.pan_convs[1](p5)return[p3,p4,p5]# 最终输出三个尺度的特征图注意这里下采样卷积的输入输出通道数相同都是out_channels。YOLOv5的PAN-FPN里所有特征图的通道数统一为256或根据模型大小调整。这样做的好处是后续的Head可以共享权重减少参数量。完整PAN-FPN结构从Backbone到Head把FPN和PAN串起来就是完整的PAN-FPN。Backbone输出C3、C4、C5经过FPN得到P3、P4、P5再经过PAN得到N3、N4、N5。注意命名YOLOv5里把PAN的输出叫P3、P4、P5但为了区分我这里用N3、N4、N5。classPANFPN(nn.Module):def__init__(self,in_channels_list,out_channels):super().__init__()self.fpnFPN(in_channels_list,out_channels)self.panPAN(in_channels_list,out_channels)# 注意这里in_channels_list其实没用只是为了接口统一defforward(self,inputs):# inputs是[C3, C4, C5]fpn_outself.fpn(inputs)# [P3, P4, P5]pan_outself.pan(fpn_out)# [N3, N4, N5]returnpan_out实际使用时Backbone输出的特征图顺序要注意。YOLOv5的BackboneCSPDarknet输出顺序是[P3, P4, P5]对应下采样倍数分别是8、16、32。如果你的Backbone输出顺序反了记得在传入PAN-FPN前调整。个人经验性建议通道数选择YOLOv5s的PAN-FPN输出通道是128YOLOv5m是192YOLOv5l是256。如果你在改进自己的模型建议从128开始试不够再加。通道数翻倍参数量翻四倍训练时间也翻倍。上采样方式别用双线性插值用最近邻。双线性插值会引入平滑破坏特征的锐利度尤其对小目标检测不利。我试过把YOLOv5的最近邻改成双线性mAP掉了0.02。下采样方式步长为2的3x3卷积比MaxPool好。如果你用MaxPool试试把下采样后的特征图可视化会发现边缘信息模糊了。卷积下采样还能学习到更好的下采样核。融合方式Add比Concat好。Concat会增加通道数导致后续卷积参数量爆炸。Add保持通道数不变而且梯度可以直接回传训练更稳定。如果你非要用Concat记得在Concat后加一个1x1卷积降维。多尺度训练PAN-FPN对输入尺寸敏感。如果你的输入尺寸是640x640特征图尺寸分别是80x80、40x40、20x20。如果输入尺寸是1280x1280特征图尺寸翻倍。建议在训练时使用多尺度训练Mosaic 随机缩放让PAN-FPN适应不同尺度的特征。调试技巧如果发现小目标检测效果不好先检查PAN-FPN的输出特征图。把P3大尺度的特征图可视化看看小目标区域是否有响应。如果P3上小目标区域是黑的说明信息没传过来检查下采样卷积的padding和stride。别盲目堆叠PAN-FPN不是层数越多越好。YOLOv5只用了三层P3、P4、P5再加一层P2下采样4倍虽然能提升小目标检测但计算量翻倍。如果你要检测极小目标比如人脸检测可以考虑加P2但记得调整Anchor尺寸。代码实现细节注意特征图尺寸的奇偶性。如果你的Backbone输出尺寸是奇数比如75x75下采样后变成37x37再上采样回来变成74x74Concat时尺寸不匹配。解决办法是在数据预处理时保证输入尺寸是2的幂次或者用F.interpolate的size参数手动指定。最后说一句PAN-FPN不是万能的。如果你的数据集里目标尺度变化不大比如都是中等大小的行人FPN就够用了。PAN-FPN的优势在于处理极端尺度变化比如同时有无人机和行人。别为了炫技而加结构先分析你的数据分布。

相关新闻