Yolo v7网络实现细节
YOLO v7网络架构的整体介绍
不同GPU和对应模型:
- 边缘GPU:YOLOv7-tiny
- 普通GPU:YOLOv7
- 云GPU的基本模型: YOLOv7-W6
激活函数:
- YOLOv7 tiny: leaky ReLU
- 其他模型:SiLU
同时,我们还针对不同的业务需求,使用基础模型进行模型缩放,得到不同类型的模型。 对于YOLOv7,我们对颈部进行stack scaling,并使用提出的复合缩放方法对整个模型的深度和宽度进行缩放,并以此获得YOLOv7-X。
- YOLOv7缩放得到了YOLOv7-X
- 对YOLOv7-W6我们使用新提出的复合缩放方法得到 YOLOv7-E6 和 YOLOv7-D6
- EELAN 用于 YOLOv7-E6,从而完成了 YOLOv7E6E
网络模块
Yolo v7网络由多个复杂的模块拼接构建而成,在学习使用Yolo v7网络之前首先要学习各个网络模块的实现细节。
CBS模块
CBS模块是最基本的一个网络模块可以说是Yolo v7中的基础卷积模块 。CBS模块由三个部分拼接组成。在最后通过画图软件给出CBS模块的直观表示。
- CONV:卷积层
- BN:批量归一化层
- SiLU:SiLU激活函数层
CBS模块也就是三个拼接层的缩写,在common.py中的源码表示如下所示。
class Conv(nn.Module):
# Standard convolution
def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups
super(Conv, self).__init__()
self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=False)
self.bn = nn.BatchNorm2d(c2)
self.act = nn.SiLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
def forward(self, x):
return self.act(self.bn(self.conv(x)))
def fuseforward(self, x):
return self.act(self.conv(x))
注意与说明:这里的CBS模块与我之前在Yolo v5中的CBL模块相同,之所以定义为CBS模块是因为V7中使用过leakyRelu激活函数便于进行区分。
ELAN模块
在论文中也提到了YOLO v7网络在之前基础上改进网络结构的时候创新性的使用了ELAN模块,并将其应用到网络之中。
说明:在YOLO V7网络结构中用到的第一个ELAN模块输入是160x160的128通道的一个输入特征图。经过ELAN模块后变为了160x160的256通道数的输出
首先我们对论文中给出的ELAN[1]
结构进行解读:
- 将输入的2c个通道平均分为两份c,c其中的第一个通道c直接作为concat连接层的一个输入
- 另外的一个通道c输入到两个部分,第一个部分同样作为concat连接层的一个输入,另外的一个部分输入到一个3x3的卷积模块中。
- 在3x3的卷积模块结束的地方并联一个另外的3x3模块,在深度和水平方向上进行了一个拓展。
- 将上面的四个部分进行一个concat,同时通过一个1x1的卷积核调整通道数的大小。
在学习ELAN模块的源码的时候我们发现,在common.py文件中并没有单独的编写一个ELAN模块而是通过CBS模块和Concat模块多次组合而成的
YOLO V7中ELAN模块的yaml配置文件项
第一个ELAN模块在配置文件项中对应的是4层到11层(画图时便于进行编号)
[-1, 1, Conv, [64, 1, 1]], #ELAN 4
[-2, 1, Conv, [64, 1, 1]], #5
[-1, 1, Conv, [64, 3, 1]], #6
[-1, 1, Conv, [64, 3, 1]], #7
[-1, 1, Conv, [64, 3, 1]], #8
[-1, 1, Conv, [64, 3, 1]], #9
[[-1, -3, -5, -6], 1, Concat, [1]], #10
[-1, 1, Conv, [256, 1, 1]], # 11
通过concat模块拼接成一个维度torch.cat(x, self.d)
class Concat(nn.Module):
def __init__(self, dimension=1):
super(Concat, self).__init__()
self.d = dimension
def forward(self, x):
return torch.cat(x, self.d)
E-ELAN模块
E-ELAN = extend ELANA(拓展的ELAN):E-ELAN模块时YOLO v7网络中全新提出的一个模块是V7网络的一个创新点
在论文的原图中给出了E-ELAN模块的说明图如下所示。
- 拓展一份并行的ELAN模块并将其输出相加来构成。
- 使用到了分组卷积的概念,组数为2的分组卷积相当于各自的卷积进行融合操作。
- 使用分组卷积可以将两组卷积看作是两个独立的运算结构(理解为并行的来进行运算。)
将3x3 2c 2c 2 = 3x3 c c + 3x3 c c的形式可以理解为红色部分的结构extend出一份绿色的结构。这种思想也是E-ELAN模块的构造思想
论文中的结构就可以等价为这种并行的ELAN结构。
这个结构只在YOLO v7e6e的配置文件中被使用到了
[-1, 1, Conv, [64, 1, 1]],
[-2, 1, Conv, [64, 1, 1]],
[-1, 1, Conv, [64, 3, 1]],
[-1, 1, Conv, [64, 3, 1]],
[-1, 1, Conv, [64, 3, 1]],
[-1, 1, Conv, [64, 3, 1]],
[-1, 1, Conv, [64, 3, 1]],
[-1, 1, Conv, [64, 3, 1]],
[[-1, -3, -5, -7, -8], 1, Concat, [1]],
[-1, 1, Conv, [160, 1, 1]], # 12
[-11, 1, Conv, [64, 1, 1]],
[-12, 1, Conv, [64, 1, 1]],
[-1, 1, Conv, [64, 3, 1]],
[-1, 1, Conv, [64, 3, 1]],
[-1, 1, Conv, [64, 3, 1]],
[-1, 1, Conv, [64, 3, 1]],
[-1, 1, Conv, [64, 3, 1]],
[-1, 1, Conv, [64, 3, 1]],
[[-1, -3, -5, -7, -8], 1, Concat, [1]],
[-1, 1, Conv, [160, 1, 1]], # 22
[[-1, -11], 1, Shortcut, [1]], # 23
这个结构和ELAN模块的并行有一定的差异,主要体现的是另外的一个创新点——模型缩放的概念产生的ELAN+的结构,之后进行说明。
Shortcut的区别是执行的是一个按元素位置相加的一个操作步骤。
class Shortcut(nn.Module):
def __init__(self, dimension=0):
super(Shortcut, self).__init__()
self.d = dimension
def forward(self, x):
return x[0]+x[1]
MP1模块
将Yolov7.yaml中的12-16层这一经常出现的结构我们定义为MP1结构。
经过Mp1结构后通道数不变而尺寸减半了可以看作一个复杂的MaxPooling操作,出现在yolov7的前面的位置,我们定义为MP1模块
[-1, 1, MP, []], #mp1
[-1, 1, Conv, [128, 1, 1]],
[-3, 1, Conv, [128, 1, 1]],
[-1, 1, Conv, [128, 3, 2]],
[[-1, -3], 1, Concat, [1]], # 16-P3/8
SPPCSPC模块
之前的模块CBS模块,ELAN模块,和MP1模块经过组合就可以构成yolov7的主干提取网络。
在neck部分将之前在v5中使用的SPPF模块进一步进行改进,改为了SPPCSP模块
class SPPCSPC(nn.Module):
# CSP https://github.com/WongKinYiu/CrossStagePartialNetworks
def __init__(self, c1, c2, n=1, shortcut=False, g=1, e=0.5, k=(5, 9, 13)):
super(SPPCSPC, self).__init__()
c_ = int(2 * c2 * e) # hidden channels
self.cv1 = Conv(c1, c_, 1, 1)
self.cv2 = Conv(c1, c_, 1, 1)
self.cv3 = Conv(c_, c_, 3, 1)
self.cv4 = Conv(c_, c_, 1, 1)
# 存放 5x5 9x9 13x13的三个mp模块
self.m = nn.ModuleList([nn.MaxPool2d(kernel_size=x, stride=1, padding=x // 2) for x in k])
self.cv5 = Conv(4 * c_, c_, 1, 1)
self.cv6 = Conv(c_, c_, 3, 1)
self.cv7 = Conv(2 * c_, c2, 1, 1)
def forward(self, x):
x1 = self.cv4(self.cv3(self.cv1(x)))
y1 = self.cv6(self.cv5(torch.cat([x1] + [m(x1) for m in self.m], 1)))
y2 = self.cv2(x)
return self.cv7(torch.cat((y1, y2), dim=1))
我们根据源码绘制出SPPCSPC的网络结构图如下所示(输入特征图为20x20x1024)
ELAN’模块
在head部分对ELAN模块进行模型变形处理得到了,ELAN’模块对应yolov7配置文件中的56到63层的信息
其yaml配置文件如下所示
[-1, 1, Conv, [256, 1, 1]], #56 ELAN'
[-2, 1, Conv, [256, 1, 1]],
[-1, 1, Conv, [128, 3, 1]],
[-1, 1, Conv, [128, 3, 1]],
[-1, 1, Conv, [128, 3, 1]],
[-1, 1, Conv, [128, 3, 1]],
[[-1, -2, -3, -4, -5, -6], 1, Concat, [1]],
[-1, 1, Conv, [256, 1, 1]], # 63 ELAN'
之前的4个卷积组成了2个分路ELAN’可以看作是ELAN的变形,即4个卷积组成了4个分路(V7中的56-63层输入为40x40x512的输入)
MP2模块
mp2的模块结构其实可以不用单独的进行介绍,与mp1唯一的不同的地方就在于对了一个来自63层的连接输入Concat。
其实本质上和v5一样在head网络部分使用到了PAN这种整体的结构来进行构建。
[-1, 1, MP, []], #76 mp2
[-1, 1, Conv, [128, 1, 1]],
[-3, 1, Conv, [128, 1, 1]],
[-1, 1, Conv, [128, 3, 2]],
[[-1, -3, 63], 1, Concat, [1]], #80 mp2
Repconv模块
重参数卷积模块(在部署和推理的时候使用不同的结构进行)
class RepConv(nn.Module):
# Represented convolution
# https://arxiv.org/abs/2101.03697
def __init__(self, c1, c2, k=3, s=1, p=None, g=1, act=True, deploy=False):
super(RepConv, self).__init__()
self.deploy = deploy
self.groups = g
self.in_channels = c1
self.out_channels = c2
assert k == 3
assert autopad(k, p) == 1
padding_11 = autopad(k, p) - k // 2
self.act = nn.SiLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
if deploy:
self.rbr_reparam = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=True)
else:
self.rbr_identity = (nn.BatchNorm2d(num_features=c1) if c2 == c1 and s == 1 else None)
self.rbr_dense = nn.Sequential(
nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=False),
nn.BatchNorm2d(num_features=c2),
)
self.rbr_1x1 = nn.Sequential(
nn.Conv2d( c1, c2, 1, s, padding_11, groups=g, bias=False),
nn.BatchNorm2d(num_features=c2),
)
基于拼接的模型缩放处理
3.2 Model scaling for concatenation-based models
因此论文的作者提出并使用了第二个在网络模型上的创新点,一种复合的模型缩放方法。
when performing width and depth scaling, and used this to design the corresponding model scaling method.
能够同时改变模型的高度和宽度。
首先需要说明和引入的概念是yolov7x.yaml是在yolov7模型的基础上进行一个复合的模型缩放。(缩放倍数为1.25倍)即倍数因子为1.25
缩放后的模块即为ELAN+模块在网络模块中对其进行介绍。
YLOLV7X
- YOLOV7X是在yolov7的基础上进行缩放得到的缩放的倍数即为1.25
- 将ELAN模块替换为了ELAN+模块使得整个模型变得更宽
- 模型的输入和输出的通道数都变为了原来的1.25倍。
ELAN+模块
YOLOV7X中的4层到第13层。
[-1, 1, Conv, [64, 1, 1]], #ELAN+
[-2, 1, Conv, [64, 1, 1]],
[-1, 1, Conv, [64, 3, 1]],
[-1, 1, Conv, [64, 3, 1]],
[-1, 1, Conv, [64, 3, 1]],
[-1, 1, Conv, [64, 3, 1]],
[-1, 1, Conv, [64, 3, 1]],
[-1, 1, Conv, [64, 3, 1]],
[[-1, -3, -5, -7, -8], 1, Concat, [1]],
[-1, 1, Conv, [320, 1, 1]], # 13
之后涉及到的模型缩放的概念和这种思想保持一致
计划的重参数卷积
4.1. Planned re-parameterized convolution
将重参数卷积应用到残差模块中去,或者是基于拼接的模块中去。Repconv在训练时使用复杂的模型进行训练,在推理时进行重参数化使用简单的模型来进行推理。
虽然 RepConv [13] 在 VGG 上取得了优异的性能,但当我们直接将其应用于 ResNet 和 DenseNet 等架构时,其准确率会显着降低。 我们使用梯度流传播路径来分析重新参数化的卷积应该如何与不同的网络相结合。我们还相应地设计了计划的重新参数化卷积。
RepConv 实际上将 3×3 卷积、1×1 卷积和恒等连接组合在一个卷积层中。 在分析了 RepConv 和不同架构的组合和对应性能后,我们发现 RepConv 中的恒等连接破坏了 ResNet 中的残差和 DenseNet 中的连接,为不同的特征图提供了更多的梯度多样性。 基于以上原因,我们使用无身份连接的 RepConv (RepConvN) 来设计计划重参数化卷积的架构。 在我们的想法中,当一个带有残差或连接的卷积层被重新参数化的卷积代替时,应该没有恒等连接。
RepVGG是采用了VGG风格进行搭建的,采用了重参数化技术,因此叫RepVGG。
标签分配方法
深度监督
深度监督 [38] 是一种经常用于训练深度网络的技术。 它的主要概念是在网络的中间层添加额外的辅助头,以辅助损失为指导的浅层网络权重。 即使对于 ResNet [26] 和 DenseNet [32] 等通常收敛良好的架构,深度监督 [70, 98, 67, 47, 82, 65, 86, 50] 仍然可以显着提高模型在许多任务上的性能 . 图 5 (a) 和 (b) 分别显示了“没有”和“有”深度监督的目标检测器架构。 在本文中,我们将负责最终输出的head称为lead head,用于辅助训练的head称为辅助head。
总结:深度监督是指在模型训练的过程中,除了最终的检测头之外,在中间的网络层引入辅助头来参与损失函数的计算并协助更新参数的方法。
标签分配
过去,在深度网络的训练中,标签分配通常直接参考GT,并根据给定的规则生成hard label
本文方法
作者提出一个“label assigner(标签分配器)” 机制,该机制将网络预测结果与GT一起考虑,然后分配soft label
关于hard label和soft label:
hard label:有些论文中也称为hard target ,其实这也是借鉴了知识蒸馏的思想,hard字面意思上就可以看出比较强硬,是就是,不是就是不是,标签形式:(1,2,3…)或(0,1,0,0…)
soft label:soft是以概率的形式来表示。可理解为对标签的平滑也即软化,比如像[0.6,0.4]
现在比较流行的结构中,经常会将网络输出的数据分布通过一定的优化方法等与GT进行匹配生成soft label(其实我们熟悉的经过softmax的或者sigmod输出就是一种soft label
独立标签匹配
在论文提出的两种新的标签分配策略之前提出的结构
这是独立标签匹配结构,将Auxiliary head和Lead head分离,然后使用它们自己的预测结果和真实标签来进行标签分配。
先导头导向标签分配器
Lead head guided label assigner
主要基于Lead head的预测结果和GT来计算,并通过优化过程生成soft label 。这组soft label 将用作Auxiliary head和Lead head的训练。
原因:
- Lead head 的学习能力更强一些,所以从它当中得到的soft label在数据集的数据概率中更具有代表性。
- Lead head 能够只学习 Auxiliary head 没有学习到的特征。
由粗到精的 lead head 指导标签分配器
(Coarse-to-fine lead head guided label assigner)由粗至细的lead head引导标签分配器
在这个过程中生成了两组不同的soft label,即粗标签和细标签
- 细标签与Lead head 在标签分配器上生成的soft label相同
- 粗标签是通过放宽认定positive target的条件生成的,也就是允许更多的grids作为positive target
首先是细标签,网络最终输出的三个head是Lead head,会将这部分的预测结果与ground truth生成soft label,网络会觉得这个soft label得到是数据分布更接近真实的数据分布,训练得到的内容更加“细致”
再来说说粗标签,Auxiliary head由于是从中间网络部分得到的,它的预测效果肯定是没有深层网络Lead head提取到的数据或者特征更细致,所以Auxiliary head部分的内容是比较“粗糙”的,在训练过程中,会将Lead head与ground truth的soft label当成一个全新的ground truth,然后与Auxiliary head之间建立损失函数,说白了,就是让辅助head的预测结果也“近似”为Lead head