学习资源站

13-更换Neck之BiFPN_yolov5更换bifpn

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后面加入如下代码:

  1. # BiFPN
  2. # 两个特征图add操作
  3. class BiFPN_Add2(nn.Module):
  4. def __init__(self, c1, c2):
  5. super(BiFPN_Add2, self).__init__()
  6. # 设置可学习参数 nn.Parameter的作用是:将一个不可训练的类型Tensor转换成可以训练的类型parameter
  7. # 并且会向宿主模型注册该参数 成为其一部分 即model.parameters()会包含这个parameter
  8. # 从而在参数优化的时候可以自动一起优化
  9. self.w = nn.Parameter(torch.ones(2, dtype=torch.float32), requires_grad=True)
  10. self.epsilon = 0.0001
  11. self.conv = nn.Conv2d(c1, c2, kernel_size=1, stride=1, padding=0)
  12. self.silu = nn.SiLU()
  13. def forward(self, x):
  14. w = self.w
  15. weight = w / (torch.sum(w, dim=0) + self.epsilon)
  16. return self.conv(self.silu(weight[0] * x[0] + weight[1] * x[1]))
  17. # 三个特征图add操作
  18. class BiFPN_Add3(nn.Module):
  19. def __init__(self, c1, c2):
  20. super(BiFPN_Add3, self).__init__()
  21. self.w = nn.Parameter(torch.ones(3, dtype=torch.float32), requires_grad=True)
  22. self.epsilon = 0.0001
  23. self.conv = nn.Conv2d(c1, c2, kernel_size=1, stride=1, padding=0)
  24. self.silu = nn.SiLU()
  25. def forward(self, x):
  26. w = self.w
  27. weight = w / (torch.sum(w, dim=0) + self.epsilon)
  28. # Fast normalized fusion
  29. 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相关语句:

  1. # 添加bifpn_add结构
  2. elif m in [BiFPN_Add2, BiFPN_Add3]:
  3. c2 = max([ch[x] for x in f])

如下图所示:


第③步:创建自定义的yaml文件  

这里的yaml文件将所有的Concat换成了BiFPN_Add

BiFPN_Add本质是add操作,不是concat操作,因此BiFPN_Add的各个输入层要求大小完全一致(通道数、feature map大小等)

yaml文件配置完整代码如下:

  1. # YOLOv5 🚀 by Ultralytics, GPL-3.0 license
  2. # Parameters
  3. nc: 80 # number of classes
  4. depth_multiple: 0.33 # model depth multiple
  5. width_multiple: 0.50 # layer channel multiple
  6. anchors:
  7. - [10,13, 16,30, 33,23] # P3/8
  8. - [30,61, 62,45, 59,119] # P4/16
  9. - [116,90, 156,198, 373,326] # P5/32
  10. # YOLOv5 v6.0 backbone
  11. backbone:
  12. # [from, number, module, args]
  13. [[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2
  14. [-1, 1, Conv, [128, 3, 2]], # 1-P2/4
  15. [-1, 3, C3, [128]],
  16. [-1, 1, Conv, [256, 3, 2]], # 3-P3/8
  17. [-1, 6, C3, [256]],
  18. [-1, 1, Conv, [512, 3, 2]], # 5-P4/16
  19. [-1, 9, C3, [512]],
  20. [-1, 1, Conv, [1024, 3, 2]], # 7-P5/32
  21. [-1, 3, C3, [1024]],
  22. [-1, 1, SPPF, [1024, 5]], # 9
  23. ]
  24. # YOLOv5 v6.1 BiFPN head
  25. head:
  26. [[-1, 1, Conv, [512, 1, 1]],
  27. [-1, 1, nn.Upsample, [None, 2, 'nearest']],
  28. [[-1, 6], 1, BiFPN_Add2, [256, 256]], # cat backbone P4
  29. [-1, 3, C3, [512, False]], # 13
  30. [-1, 1, Conv, [256, 1, 1]],
  31. [-1, 1, nn.Upsample, [None, 2, 'nearest']],
  32. [[-1, 4], 1, BiFPN_Add2, [128, 128]], # cat backbone P3
  33. [-1, 3, C3, [256, False]], # 17
  34. [-1, 1, Conv, [512, 3, 2]],
  35. [[-1, 13, 6], 1, BiFPN_Add3, [256, 256]], #v5s通道数是默认参数的一半
  36. [-1, 3, C3, [512, False]], # 20
  37. [-1, 1, Conv, [512, 3, 2]],
  38. [[-1, 10], 1, BiFPN_Add2, [256, 256]], # cat head P5
  39. [-1, 3, C3, [1024, False]], # 23
  40. [[17, 20, 23], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
  41. ]

第④步:验证是否加入成功

yolo.py 文件里面配置改为我们刚才自定义的 yolov5s_BiFPN.yaml

然后运行yolo.py  

 我们可以看到,所有的Concat已被换成了BiFPN_Add


第⑤步:修改train.py  

首先找到train.py文件里面的# Optimizer

然后将BiFPN_Add2 和 BiFPN_Add3 函数中定义的w参数,加入g1

  1. # BiFPN_Concat
  2. elif isinstance(v, BiFPN_Add2) and hasattr(v, 'w') and isinstance(v.w, nn.Parameter):
  3. g1.append(v.w)
  4. elif isinstance(v, BiFPN_Add3) and hasattr(v, 'w') and isinstance(v.w, nn.Parameter):
  5. 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后面加入如下代码:

  1. # 结合BiFPN 设置可学习参数 学习不同分支的权重
  2. # 两个分支concat操作
  3. class BiFPN_Concat2(nn.Module):
  4. def __init__(self, dimension=1):
  5. super(BiFPN_Concat2, self).__init__()
  6. self.d = dimension
  7. self.w = nn.Parameter(torch.ones(2, dtype=torch.float32), requires_grad=True)
  8. self.epsilon = 0.0001
  9. def forward(self, x):
  10. w = self.w
  11. weight = w / (torch.sum(w, dim=0) + self.epsilon) # 将权重进行归一化
  12. # Fast normalized fusion
  13. x = [weight[0] * x[0], weight[1] * x[1]]
  14. return torch.cat(x, self.d)
  15. # 三个分支concat操作
  16. class BiFPN_Concat3(nn.Module):
  17. def __init__(self, dimension=1):
  18. super(BiFPN_Concat3, self).__init__()
  19. self.d = dimension
  20. # 设置可学习参数 nn.Parameter的作用是:将一个不可训练的类型Tensor转换成可以训练的类型parameter
  21. # 并且会向宿主模型注册该参数 成为其一部分 即model.parameters()会包含这个parameter
  22. # 从而在参数优化的时候可以自动一起优化
  23. self.w = nn.Parameter(torch.ones(3, dtype=torch.float32), requires_grad=True)
  24. self.epsilon = 0.0001
  25. def forward(self, x):
  26. w = self.w
  27. weight = w / (torch.sum(w, dim=0) + self.epsilon) # 将权重进行归一化
  28. # Fast normalized fusion
  29. x = [weight[0] * x[0], weight[1] * x[1], weight[2] * x[2]]
  30. return torch.cat(x, self.d)

如下图所示:


第②步:在yolo.py文件里的parse_model函数加入类名

再来修改yolo.py,在parse_model函数中找到 elif m is Concat: 语句,在其后面加上BiFPN_Concat相关语句:

  1. # 添加bifpn_concat结构
  2. elif m in [Concat, BiFPN_Concat2, BiFPN_Concat3]:
  3. c2 = sum(ch[x] for x in f)

如下图所示: 


第③步:创建自定义的yaml文件  

yaml文件配置完整代码如下:

  1. # YOLOv5 🚀 by Ultralytics, GPL-3.0 license
  2. # Parameters
  3. nc: 80 # number of classes
  4. depth_multiple: 0.33 # model depth multiple
  5. width_multiple: 0.50 # layer channel multiple
  6. anchors:
  7. - [10,13, 16,30, 33,23] # P3/8
  8. - [30,61, 62,45, 59,119] # P4/16
  9. - [116,90, 156,198, 373,326] # P5/32
  10. # YOLOv5 v6.0 backbone
  11. backbone:
  12. # [from, number, module, args]
  13. [[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2
  14. [-1, 1, Conv, [128, 3, 2]], # 1-P2/4
  15. [-1, 3, C3, [128]],
  16. [-1, 1, Conv, [256, 3, 2]], # 3-P3/8
  17. [-1, 6, C3, [256]],
  18. [-1, 1, Conv, [512, 3, 2]], # 5-P4/16
  19. [-1, 9, C3, [512]],
  20. [-1, 1, Conv, [1024, 3, 2]], # 7-P5/32
  21. [-1, 3, C3, [1024]],
  22. [-1, 1, SPPF, [1024, 5]], # 9
  23. ]
  24. # YOLOv5 v6.0 BiFPN head
  25. head:
  26. [[-1, 1, Conv, [512, 1, 1]],
  27. [-1, 1, nn.Upsample, [None, 2, 'nearest']],
  28. [[-1, 6], 1, BiFPN_Concat2, [1]], # cat backbone P4 <--- BiFPN change
  29. [-1, 3, C3, [512, False]], # 13
  30. [-1, 1, Conv, [256, 1, 1]],
  31. [-1, 1, nn.Upsample, [None, 2, 'nearest']],
  32. [[-1, 4], 1, BiFPN_Concat2, [1]], # cat backbone P3 <--- BiFPN change
  33. [-1, 3, C3, [256, False]], # 17 (P3/8-small)
  34. [-1, 1, Conv, [256, 3, 2]],
  35. [[-1, 14, 6], 1, BiFPN_Concat3, [1]], # cat P4 <--- BiFPN change
  36. [-1, 3, C3, [512, False]], # 20 (P4/16-medium)
  37. [-1, 1, Conv, [512, 3, 2]],
  38. [[-1, 10], 1, BiFPN_Concat2, [1]], # cat head P5 <--- BiFPN change
  39. [-1, 3, C3, [1024, False]], # 23 (P5/32-large)
  40. [[17, 20, 23], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
  41. ]

第④步:验证是否加入成功

和上面方法一样,我们直接运行yolo.py

 可以看到已经替换成功!


第⑤步:修改train.py  

最后向优化器中添加BiFPN的权重参数,也和上面步骤一样。

  1. # BiFPN_Concat
  2. elif isinstance(v, BiFPN_Concat2) and hasattr(v, 'w') and isinstance(v.w, nn.Parameter):
  3. g1.append(v.w)
  4. elif isinstance(v, BiFPN_Concat3) and hasattr(v, 'w') and isinstance(v.w, nn.Parameter):
  5. g1.append(v.w)

这样就OK啦~~~