前言
论文地址: https://arxiv.org/abs/1911.09070.
PyTorch实现: https://github.com/zylo117/Yet-Another-EfficientDet-Pytorch.
贡献:
提出一种全新的特征融合方法:重复加权双向特征金字塔网络 BiFPN ;
提出一种复合的缩放方法(EfficientNet方法):统一缩放 分辨率、深度、宽度、特征融合网络、box/class网络。
EfficientDet = Backbone(EfficientNet) + Neck(BiFPN) + Head(class + box)
有关于EfficientNet的内容有不了解的可以看我的另两篇博文:【论文复现】EfficientNet-V1 和【论文复现】EfficientNet-V2 。本篇我会把重心放在Neck部分。
一、研究背景
EfficientDet是谷歌大脑 Mingxing Tan、Ruoming Pang 和 Quoc V. Le 提出新架构 EfficientDet,结合 EfficientNet(同样来自该团队)和新提出的 BiFPN,实现新的 SOTA 结果。如下图:
Neck结构的发展:
1.最早的Neck部分(特征融合)最经典的就是从backbone中提取高层金字塔特征直接预测,如下图 a、b、c 三个模块,但是这种结构没有进行特征融合所有精度都比较低;
2.而后就提出了基于特征融合思想的FPN,如图 d , 在FPN中建立一条自上而下的通路,进行特征融合,用融合后的具有更高语义信息的 feature map 进行预测,可以提高一定的精度。但是经过我们研究发现这种自顶向下的FPN网络受到单向信息流的限制,精度还是不行(YOLO V3中使用);
3.而近年来使用最多的当属PANet,在YOLOV4和V5中都是以它为Neck。它是在FPN的基础上再建立一条自下而上的通路,这么做的思路是:高层的feature map具有更强的语义信息(有利于物体分类),底层的feature map具有更强的位置信息(有利于物体定位),虽然FPN结构使得预测 feature map提高了语义信息但是理论上肯定丢失了很多的位置信息,所以再新建一条从下往上的通路,将位置信息也传到预测 feature map中,使得预测 feature map同时具备较高的语义信息和位置信息(有利于目标检测)。这样做可以大大的提高目标检测任务精度。具体结构如下图 b 所示;
4.还有今年来提出的NAS-FPN结构。使用近年比较热门的neural architecture search(NAS)技术搜索最佳的网络结构。虽然这种结构的效果最好,但是搜索得到的网络不规则、难以解释和修改,而且使用NAS技术非常耗时耗力,所有并不推荐。具体结构如下图 c所示;
5.基于此,我们提出了新型的Neck(特征融合)网络结构 BiFPN,如下图 d所示,详细的设计思路我们在下一张再慢慢探讨。
二、新型Neck结构:BiFPN
BiFPN 全称 Bidirectional Feature Pyramid Network 加权双向(自顶向下 + 自低向上)特征金字塔网络。
相比较于PANet,BiFPN在设计上的改变:
总结下图:
图d 蓝色部分为自顶向下的通路,传递的是高层特征的语义信息;红色部分是自底向上的通路,传递的是低层特征的位置信息;紫色部分是上述第二点提到的同一层在输入节点和输入节点间新加的一条边。
1.我们删除那些只有一条输入边的节点。这么做的思路很简单:如果一个节点只有一条输入边而没有特征融合,那么它对旨在融合不同特征的特征网络的贡献就会很小。删除它对我们的网络影响不大,同时简化了双向网络;如上图d 的 P7右边第一个节点
2.如果原始输入节点和输出节点处于同一层,我们会在原始输入节点和输出节点之间添加一条额外的边。思路:以在不增加太多成本的情况下融合更多的特性;
3.与只有一个自顶向下和一个自底向上路径的PANet不同,我们处理每个双向路径(自顶向下和自底而上)路径作为一个特征网络层,并重复同一层多次,以实现更高层次的特征融合。如下图EfficientNet 的网络结构所示,我们对BiFPN是重复使用多次的。而这个使用次数也不是我们认为设定的,而是作为参数一起加入网络的设计当中,使用NAS技术算出来的。
Weighted Feature Fusion 带权特征融合:学习不同输入特征的重要性,对不同输入特征有区分的融合。
设计思路:传统的特征融合往往只是简单的 feature map 叠加/相加 (sum them up),比如使用concat或者shortcut连接,而不对同时加进来的 feature map 进行区分。然而,不同的输入 feature map 具有不同的分辨率,它们对融合输入 feature map 的贡献也是不同的,因此简单的对他们进行相加或叠加处理并不是最佳的操作。所以这里我们提出了一种简单而高效的加权特融合的机制。
常见的带权特征融合有三种方法,分别是:
1.Unbounded fusion:这种方法比较简单,直接加一个可学习的权重。但是由于这个权重不受限制,所有可能引起训练的不稳定,所有并不推荐。
2.Softmax-based fusion:使用这种方法可以将范围放缩到 [ 0 , 1 ] [0, 1][0,1] 之间,训练稳定,但是训练很慢,所有也不推荐。
3.Fast normalized fusion: 这种方法类似于Softmax也是将范围放缩到 [ 0 , 1 ] [0, 1][0,1] 之间,而且训练速度快效率高,所以我们使用这种带权特征融合方法。
总结BiFPN = 新型加强版的PANet(重复双向跨尺度连接) + 带权重的特征融合机制
三、EfficientDet的网络结构
Backbone: EfficientNet
Neck: BiFPN = 新型加强版的PANet(重复双向跨尺度连接)+ 带权重的特征融合机制
head: shared class and box network
到此原文【论文笔记】EfficientDet(BiFPN)(2020)_满船清梦压星河HK的博客-CSDN博客
四、添加bifpn,以yolov5s为例
修改yaml文件
本文以yolov5s.yaml为例进行修改,修改模型配置文件时要注意以下几点:
这里的yaml文件只修改了一处,也就是将19层的Concat换成了BiFPN_Add,要想修改其他层的Concat,可以类比进行修改
BiFPN_Add本质是add操作,不是concat操作,因此,BiFPN_Add的各个输入层要求大小完全一致(通道数、feature map大小等),因此,这里要修改之前的参数[-1, 13, 6],来满足这个要求:
-1层就是上一层的输出,原来上一层的输出channel数为256,这里改成512
13层就是这里[-1, 3, C3, [512, False]], # 13
这样修改后,BiFPN_Add各个输入大小都是[bs,256,40,40]
最后BiFPN_Add后面的参数层设置为[256, 256]也就是输入输出channel数都是256
# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
# Parameters
nc: 80 # number of classes
depth_multiple: 0.33 # model depth multiple
width_multiple: 0.50 # layer channel multiple
anchors:
- [10,13, 16,30, 33,23] # P3/8
- [30,61, 62,45, 59,119] # P4/16
- [116,90, 156,198, 373,326] # P5/32
# YOLOv5 v6.0 backbone
backbone:
# [from, number, module, args]
[[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2
[-1, 1, Conv, [128, 3, 2]], # 1-P2/4
[-1, 3, C3, [128]],
[-1, 1, Conv, [256, 3, 2]], # 3-P3/8
[-1, 6, C3, [256]],
[-1, 1, Conv, [512, 3, 2]], # 5-P4/16
[-1, 9, C3, [512]],
[-1, 1, Conv, [1024, 3, 2]], # 7-P5/32
[-1, 3, C3, [1024]],
[-1, 1, SPPF, [1024, 5]], # 9
]
# YOLOv5 v6.0 BiFPN head
head:
[[-1, 1, Conv, [512, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 6], 1, Concat, [1]], # cat backbone P4
[-1, 3, C3, [512, False]], # 13
[-1, 1, Conv, [256, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 4], 1, Concat, [1]], # cat backbone P3
[-1, 3, C3, [256, False]], # 17 (P3/8-small)
[-1, 1, Conv, [512, 3, 2]], # 为了BiFPN正确add,调整channel数
[[-1, 13, 6], 1, BiFPN_Add3, [256, 256]], # cat P4 <--- BiFPN change 注意v5s通道数是默认参数的一半
[-1, 3, C3, [512, False]], # 20 (P4/16-medium)
[-1, 1, Conv, [512, 3, 2]],
[[-1, 10], 1, Concat, [1]], # cat head P5
[-1, 3, C3, [1024, False]], # 23 (P5/32-large)
[[17, 20, 23], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
]
将Concat全部换成BiFPN_Add //( 复制就好,更改了通道数量 )
# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
# Parameters
nc: 80 # number of classes
depth_multiple: 0.33 # model depth multiple
width_multiple: 0.50 # layer channel multiple
anchors:
- [10,13, 16,30, 33,23] # P3/8
- [30,61, 62,45, 59,119] # P4/16
- [116,90, 156,198, 373,326] # P5/32
# YOLOv5 v6.0 backbone
backbone:
# [from, number, module, args]
[[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2
[-1, 1, Conv, [128, 3, 2]], # 1-P2/4
[-1, 3, C3, [128]],
[-1, 1, Conv, [256, 3, 2]], # 3-P3/8
[-1, 6, C3, [256]],
[-1, 1, Conv, [512, 3, 2]], # 5-P4/16
[-1, 9, C3, [512]],
[-1, 1, Conv, [1024, 3, 2]], # 7-P5/32
[-1, 3, C3, [1024]],
[-1, 1, SPPF, [1024, 5]], # 9
]
# YOLOv5 v6.0 BiFPN head
head:
[[-1, 1, Conv, [512, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 6], 1, BiFPN_Add2, [256, 256]], # cat backbone P4
[-1, 3, C3, [512, False]], # 13
[-1, 1, Conv, [256, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 4], 1, BiFPN_Add2, [128, 128]], # cat backbone P3
[-1, 3, C3, [256, False]], # 17 (P3/8-small)
[-1, 1, Conv, [512, 3, 2]], # 为了BiFPN正确add,调整channel数
[[-1, 13, 6], 1, BiFPN_Add3, [256, 256]], # cat P4 <--- BiFPN change 注意v5s通道数是默认参数的一半
[-1, 3, C3, [512, False]], # 20 (P4/16-medium)
[-1, 1, Conv, [512, 3, 2]],
[[-1, 10], 1, BiFPN_Add2, [256, 256]], # cat head P5
[-1, 3, C3, [1024, False]], # 23 (P5/32-large)
[[17, 20, 23], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
]
打印模型参数 :运行yolo.py即可在终端查看打印信息
from n params module arguments
0 -1 1 3520 models.common.Conv [3, 32, 6, 2, 2]
1 -1 1 18560 models.common.Conv [32, 64, 3, 2]
2 -1 1 18816 models.common.C3 [64, 64, 1]
3 -1 1 73984 models.common.Conv [64, 128, 3, 2]
4 -1 2 115712 models.common.C3 [128, 128, 2]
5 -1 1 295424 models.common.Conv [128, 256, 3, 2]
6 -1 3 625152 models.common.C3 [256, 256, 3]
7 -1 1 1180672 models.common.Conv [256, 512, 3, 2]
8 -1 1 1182720 models.common.C3 [512, 512, 1]
9 -1 1 656896 models.common.SPPF [512, 512, 5]
10 -1 1 131584 models.common.Conv [512, 256, 1, 1]
11 -1 1 0 torch.nn.modules.upsampling.Upsample [None, 2, 'nearest']
12 [-1, 6] 1 65794 models.common.BiFPN_Add2 [256, 256]
13 -1 1 296448 models.common.C3 [256, 256, 1, False]
14 -1 1 33024 models.common.Conv [256, 128, 1, 1]
15 -1 1 0 torch.nn.modules.upsampling.Upsample [None, 2, 'nearest']
16 [-1, 4] 1 16514 models.common.BiFPN_Add2 [128, 128]
17 -1 1 74496 models.common.C3 [128, 128, 1, False]
18 -1 1 295424 models.common.Conv [128, 256, 3, 2]
19 [-1, 13, 6] 1 65795 models.common.BiFPN_Add3 [256, 256]
20 -1 1 296448 models.common.C3 [256, 256, 1, False]
21 -1 1 590336 models.common.Conv [256, 256, 3, 2]
22 [-1, 10] 1 65794 models.common.BiFPN_Add2 [256, 256]
23 -1 1 1051648 models.common.C3 [256, 512, 1, False]
24 [17, 20, 23] 1 229245 models.yolo.Detect [80, [[10, 13, 16, 30, 33, 23], [30, 61, 62, 45, 59, 119], [116, 90, 156, 198, 373, 326]], [128, 256, 512]]
Model Summary: 278 layers, 7384006 parameters, 7384006 gradients, 17.2 GFLOPs
修改common.py
# 结合BiFPN 设置可学习参数 学习不同分支的权重
# 两个分支add操作
class BiFPN_Add2(nn.Module):
def __init__(self, c1, c2):
super(BiFPN_Add2, self).__init__()
# 设置可学习参数 nn.Parameter的作用是:将一个不可训练的类型Tensor转换成可以训练的类型parameter
# 并且会向宿主模型注册该参数 成为其一部分 即model.parameters()会包含这个parameter
# 从而在参数优化的时候可以自动一起优化
self.w = nn.Parameter(torch.ones(2, dtype=torch.float32), requires_grad=True)
self.epsilon = 0.0001
self.conv = nn.Conv2d(c1, c2, kernel_size=1, stride=1, padding=0)
self.silu = nn.SiLU()
def forward(self, x):
w = self.w
weight = w / (torch.sum(w, dim=0) + self.epsilon)
return self.conv(self.silu(weight[0] * x[0] + weight[1] * x[1]))
# 三个分支add操作
class BiFPN_Add3(nn.Module):
def __init__(self, c1, c2):
super(BiFPN_Add3, self).__init__()
self.w = nn.Parameter(torch.ones(3, dtype=torch.float32), requires_grad=True)
self.epsilon = 0.0001
self.conv = nn.Conv2d(c1, c2, kernel_size=1, stride=1, padding=0)
self.silu = nn.SiLU()
def forward(self, x):
w = self.w
weight = w / (torch.sum(w, dim=0) + self.epsilon) # 将权重进行归一化
# Fast normalized fusion
return self.conv(self.silu(weight[0] * x[0] + weight[1] * x[1] + weight[2] * x[2]))
修改yolo.py
- 在
parse_model
函数中找到elif m is Concat:
语句,在其后面加上BiFPN_Add
相关语句:
elif m is Concat:
c2 = sum(ch[x] for x in f)
# 添加bifpn_add结构
elif m in [BiFPN_Add2, BiFPN_Add3]:
c2 = max([ch[x] for x in f])
修改train.py
1. 向优化器中添加BiFPN的权重参数
- 将
BiFPN_Add2
和BiFPN_Add3
函数中定义的w
参数,加入g1
g0, g1, g2 = [], [], [] # optimizer parameter groups
for v in model.modules():
# hasattr: 测试指定的对象是否具有给定的属性,返回一个布尔值
if hasattr(v, 'bias') and isinstance(v.bias, nn.Parameter): # bias
g2.append(v.bias) # biases
if isinstance(v, nn.BatchNorm2d): # weight (no decay)
g0.append(v.weight)
elif hasattr(v, 'weight') and isinstance(v.weight, nn.Parameter): # weight (with decay)
g1.append(v.weight)
# BiFPN_Concat
elif isinstance(v, BiFPN_Add2) and hasattr(v, 'w') and isinstance(v.w, nn.Parameter):
g1.append(v.w)
elif isinstance(v, BiFPN_Add3) and hasattr(v, 'w') and isinstance(v.w, nn.Parameter):
g1.append(v.w)
2. 查看BiFPN_Add层参数更新情况
想要查看BiFPN_Add层的参数更新情况,可以参考这篇博客【Pytorch】查看模型某一层的参数数值(自用),直接定位到w
参数,随着模型训练输出对应的值。
到此为止转载完毕,原文:【YOLOv5-6.x】设置可学习权重结合BiFPN(Add操作)_yolo中如何实现add操作_嗜睡的篠龙的博客-CSDN博客