YOLOv5改进系列(12)——更换Neck之BiFPN
🌻一、BiFPN介绍
1.1 简介
EfficientDet 是继 2019 年推出 EfficientNet 模型之后,Google 人工智能研究小组Tan Mingxing等人为进一步提高目标检测效率,以 EfficientNet 模型和双向特征加权金字塔网络 BiFPN为基础,于2020 年创新推出的新一代目标检测模型,在COCO数据集上吊打其他方法。
EfficientDet = Backbone(EfficientNet) + Neck(BiFPN) + Head(class + box)

1.2 BiFPN
(1)跨尺度连接
- 移除那些只有一条输入边的节点,这是因为如果一个节点只有一条输入边而没有特征融合,那么它对以融合不同特征为目标的特征网络的贡献就比较小。这可以简化双向网络。
- 如果原始输入节点和输出节点处于同一水平,就在它们之间增加一条额外的边,以便在不增加太多成本的情况下融合更多的特征。
- 与PANet只有一条自上而下和一条自下而上的路径不同,BiFPN将每条双向(自上而下&自下而上)路径视为一个特征网络层,并多次重复同一层,以实现更高级别的特征融合。

(2)加权特征融合
- 无限融合:

- 基于Softmax的融合:

- 快速归一化融合:

1.3 EfficientDet
(1)模型框架

- backbone:EfficientNets
- 特征网络:BiFPN
(2)复合缩放
EfficientDet使用的是EfficientNet-B0到B7作为预训练模型,所以EfficientDet的系数ϕ的选择范围也是0~7。
Backbone network—主干网络
骨干网络采用和EfficientNet B0~B6相同的缩放系数,从而可以使用它们在ImageNet上的预训练模型。
BiFPN network—BiFPN 网络
对于BiFPN的深度 Dbifpn 采用线性变换的方式因为深度需要向下取整。对于宽度 Wbifpn采用指数变换的方式,采用网格搜索确定1.35作为宽度的缩放因子。
完整的缩放公式如下:
![]()
Box/class prediction network—Box/class预测网络
宽度固定为和BiFPN的宽度相等即 Wpred=Wbifpn ,深度按下式进行线性变换:

Input image resolution—输入图像分辨率
因为BiFPN中用到了level 3-7的特征,因此输入大小需要能被 2^7=128 除尽,因此输入分辨率按下式进行线性变换:

🌻 二、添加方式1:Add操作
第①步:在common.py中添加BiFPN模块
在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函数加入类名
再来修改yolo.py,在parse_model函数中找到 elif m is Concat: 语句,在其后面加上BiFPN_Add相关语句:
- # 添加bifpn_add结构
- elif m in [BiFPN_Add2, BiFPN_Add3]:
- c2 = max([ch[x] for x in f])
如下图所示:

第③步:创建自定义的yaml文件
这里的yaml文件将所有的Concat换成了BiFPN_Add。
BiFPN_Add本质是add操作,不是concat操作,因此BiFPN_Add的各个输入层要求大小完全一致(通道数、feature map大小等)
yaml文件配置完整代码如下:
- # 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.1 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
- [-1, 1, Conv, [512, 3, 2]],
- [[-1, 13, 6], 1, BiFPN_Add3, [256, 256]], #v5s通道数是默认参数的一半
- [-1, 3, C3, [512, False]], # 20
- [-1, 1, Conv, [512, 3, 2]],
- [[-1, 10], 1, BiFPN_Add2, [256, 256]], # cat head P5
- [-1, 3, C3, [1024, False]], # 23
- [[17, 20, 23], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
- ]
第④步:验证是否加入成功
在yolo.py 文件里面配置改为我们刚才自定义的 yolov5s_BiFPN.yaml


然后运行yolo.py

我们可以看到,所有的Concat已被换成了BiFPN_Add
第⑤步:修改train.py
首先找到train.py文件里面的# Optimizer
然后将BiFPN_Add2 和 BiFPN_Add3 函数中定义的w参数,加入g1
- # 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)
如下图所示:

刚加入的时候会报错,莫慌~

这是没有导入包引起的啦~
我们可以直接导入 :

也可以加一个导包语句:
from models.common import BiFPN_Add3, BiFPN_Add2
然后就可以开始训练了

--- 注意!---
因为我使用的还是6.1版本,是可以直接在train.py进行修改的,但是在看了一些后期改进的文章,发现在yolov5-v7.0版本中,这个部分作者加入了智能优化器(smart_optimizer)。
我们ctrl+鼠标左键点击这个函数,进入之后可以发现optimizer这个函数进行了重构,之前的一重for循环被改成两重for。
另外,原来的 g[0] g[1] g[2] 被替换为g = [] [] []
- 新版将这个地方关于weight的顺序翻转了一下,这样就导致一个问题,只要不是bias或者weight no decay,那么就全都归结于weight with decay上。
- 与之前需要elif 进行判断Bi_FPN进行模型的添加相比,这里不在需要添加判断条件了,因为最后的else会把 剩余非bias 和非weight nodecay 部分全部加到weight with decay上。
- 也就是说,添加其他Neck时,不需要额外对optimizer进行添加elif判断,也就实现了一个所谓智能的优化。
所以7.0版本无需对参数g的修改,直接略过即可,智能优化器会对多余的部分进行自动增加权重。
(以上解析来自:)
🌻 三、添加方式2:Concat操作
第①步:在common.py中添加BiFPN模块
在common.py后面加入如下代码:
- # 结合BiFPN 设置可学习参数 学习不同分支的权重
- # 两个分支concat操作
- class BiFPN_Concat2(nn.Module):
- def __init__(self, dimension=1):
- super(BiFPN_Concat2, self).__init__()
- self.d = dimension
- self.w = nn.Parameter(torch.ones(2, dtype=torch.float32), requires_grad=True)
- self.epsilon = 0.0001
- def forward(self, x):
- w = self.w
- weight = w / (torch.sum(w, dim=0) + self.epsilon) # 将权重进行归一化
- # Fast normalized fusion
- x = [weight[0] * x[0], weight[1] * x[1]]
- return torch.cat(x, self.d)
- # 三个分支concat操作
- class BiFPN_Concat3(nn.Module):
- def __init__(self, dimension=1):
- super(BiFPN_Concat3, self).__init__()
- self.d = dimension
- # 设置可学习参数 nn.Parameter的作用是:将一个不可训练的类型Tensor转换成可以训练的类型parameter
- # 并且会向宿主模型注册该参数 成为其一部分 即model.parameters()会包含这个parameter
- # 从而在参数优化的时候可以自动一起优化
- self.w = nn.Parameter(torch.ones(3, dtype=torch.float32), requires_grad=True)
- self.epsilon = 0.0001
- def forward(self, x):
- w = self.w
- weight = w / (torch.sum(w, dim=0) + self.epsilon) # 将权重进行归一化
- # Fast normalized fusion
- x = [weight[0] * x[0], weight[1] * x[1], weight[2] * x[2]]
- return torch.cat(x, self.d)
如下图所示:

第②步:在yolo.py文件里的parse_model函数加入类名
再来修改yolo.py,在parse_model函数中找到 elif m is Concat: 语句,在其后面加上BiFPN_Concat相关语句:
- # 添加bifpn_concat结构
- elif m in [Concat, BiFPN_Concat2, BiFPN_Concat3]:
- c2 = sum(ch[x] for x in f)
如下图所示:

第③步:创建自定义的yaml文件
yaml文件配置完整代码如下:
- # 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_Concat2, [1]], # cat backbone P4 <--- BiFPN change
- [-1, 3, C3, [512, False]], # 13
- [-1, 1, Conv, [256, 1, 1]],
- [-1, 1, nn.Upsample, [None, 2, 'nearest']],
- [[-1, 4], 1, BiFPN_Concat2, [1]], # cat backbone P3 <--- BiFPN change
- [-1, 3, C3, [256, False]], # 17 (P3/8-small)
- [-1, 1, Conv, [256, 3, 2]],
- [[-1, 14, 6], 1, BiFPN_Concat3, [1]], # cat P4 <--- BiFPN change
- [-1, 3, C3, [512, False]], # 20 (P4/16-medium)
- [-1, 1, Conv, [512, 3, 2]],
- [[-1, 10], 1, BiFPN_Concat2, [1]], # cat head P5 <--- BiFPN change
- [-1, 3, C3, [1024, False]], # 23 (P5/32-large)
- [[17, 20, 23], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
- ]
第④步:验证是否加入成功
和上面方法一样,我们直接运行yolo.py

可以看到已经替换成功!
第⑤步:修改train.py
最后向优化器中添加BiFPN的权重参数,也和上面步骤一样。
- # BiFPN_Concat
- elif isinstance(v, BiFPN_Concat2) and hasattr(v, 'w') and isinstance(v.w, nn.Parameter):
- g1.append(v.w)
- elif isinstance(v, BiFPN_Concat3) and hasattr(v, 'w') and isinstance(v.w, nn.Parameter):
- g1.append(v.w)
这样就OK啦~~~

另外,原来的