以下文章来源于微信公众号 - GiantPandaCV ,作者陈er
以下文章来源于微信公众号-目标检测之心
class ReOrg(nn.Module):
def __init__(self):
super(ReOrg, self).__init__()
def forward(self, x): # x(b,c,w,h) -> y(b,4c,w/2,h/2)
return torch.cat([x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]], 1)
最核心的语句为torch.cat([x[…, ::2, ::2], x[…, 1::2, ::2], x[…, ::2, 1::2], x[…, 1::2, 1::2]], 1),将一张图隔列隔行取值,切分成4块再进行信息的拼接,类似的下采样操作还包括均值池化,最大值池化等,但池化操作会将原始信息进行加工(均值)或舍弃(最大值),丢失原始信息源,在一定程度上,切片拼接操作可以最大程度保留信息源,类似的操作概念示例图如下:
在YOLOv7仓库中,通过观察YOLOv7.yaml可以发现,原始的640 × 640 的图像进入ReOrg操作,先变成320 × 320的特征图,在通过拼接(Concat)后,会经过一次卷积(CBL)操作,最终变成320 × 320 × 96的特征图。
这个其实和YOLOv5几乎一致,不过从YOLOv5第六版开始后,已舍弃该操作,v5作者认为第一层采用6×6,stride=2的常规卷积产生的参数更少,效果更好。
class MP(nn.Module):
def __init__(self, k=2):
super(MP, self).__init__()
self.m = nn.MaxPool2d(kernel_size=k, stride=k)
def forward(self, x):
return self.m(x)
对于定义二,将kernel_size设为3,stride设为1,并且填充padding的MaxPool2d称为SP
class SP(nn.Module):
def __init__(self, k=3, s=1):
super(SP, self).__init__()
self.m = nn.MaxPool2d(kernel_size=k, stride=s, padding=k // 2)
def forward(self, x):
return self.m(x)
而DownC模块则使用的是最大池化的MP操作,相关代码如下:
class DownC(nn.Module):
# Spatial pyramid pooling layer used in YOLOv3-SPP
def __init__(self, c1, c2, n=1, k=2):
super(DownC, self).__init__()
c_ = int(c1) # hidden channels
self.cv1 = Conv(c1, c_, 1, 1)
self.cv2 = Conv(c_, c2//2, 3, k)
self.cv3 = Conv(c1, c2//2, 1, 1)
self.mp = nn.MaxPool2d(kernel_size=k, stride=k)
def forward(self, x):
return torch.cat((self.cv2(self.cv1(x)), self.cv3(self.mp(x))), dim=1)
通过代码可以看到,DownC模块会用到三种最基本的结构,包括1×1的point conv,3×3的standard conv,以及mp操作的MaxPool,使用这三种基础模块组装成DownC大模块,对代码进行图解,如下:
DownC Module, 详见: https://github.com/WongKinYiu/yolov7/blob/main/cfg/training/yolov7e6.yaml
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)
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))
对代码进行图解,可以看到,总的输入会被分成三段进入不同的分支,最中间的分支其实就是金字塔池化操作,左侧分支类似于depthwise conv,但是请注意,中间的3×3卷积并未进行分组,依旧是标准卷积,右侧则为一个point conv,最后将所有分支输出的信息流进行concat。
RepConv模块
这个应该是今年最火的一种模型架构策略了,以前的设计范式提出了许多复杂而又高效的网络结构,但它们在真实设备中的表现往往不如预期,特别是对于算力匮乏的设备。RepVGG是一种基于VGG网络设计的多分支模型,在训练过程中可以通过多分支提升性能,推理可以通过结构重新参数化转换为具有3×3卷积和ReLU的连续直筒型VGG类网络,实现推理速度的加快。在现代卷积神经网络架构中,推理过程使用的某些特定硬件实现推理加速显得格外复杂和繁琐,与附加的定制硬件相比,直筒型VGG类网络有着简单,推理速度快的优势,如下所示:
尽管RepConv在VGG上取得了优异的性能,但将它直接应用于ResNet和DenseNet或其他网络架构时,它的精度会显著降低。作者使用梯度传播路径来分析不同的重参化模块应该和哪些网络搭配使用。通过分析RepConv与不同架构的组合以及产生的性能,作者发现RepConv中的identity破坏了ResNet中的残差结构和DenseNet中的跨层连接,这为不同的特征图提供了梯度的多样性。基于上述原因,作者使用没有identity连接的RepConv结构。图4显示了作者在PlainNet和ResNet中使用的“计划型重参化卷积”的一个示例。
E-ELAN模块
在大多数关于设计高效网络的论文中,主要考虑的因素是参数量、计算量和计算密度。但从内存访存的角度出发出发,还可以分析输入/输出信道比、架构的分支数和元素级操作对网络推理速度的影响(shufflenet论文提出)。在执行模型缩放时还需考虑激活函数,即更多地考虑卷积层输出张量中的元素数量。因此,在本文中,作者提出了基于ELAN的扩展版本E-ELAN,其主要架构如图2(d)所示。
在大规模ELAN中,无论梯度路径长度和计算模块数量如何,都达到了稳定的状态。但如果更多计算模块被无限地堆叠,这种稳定状态可能会被破坏,参数利用率也会降低。本文提出的E-ELAN采用expand、shuffle、merge cardinality结构,实现在不破坏原始梯度路径的情况下,提高网络的学习能力。但是笔者找了整个common.py文件,似乎没有找到定义这个模块的代码,不过在yolov7-e6e.yaml中找到了拆散成单独算子的结构配置,如下:
然而,找了一圈,貌似没看到作者在论文中提到的merge cardinality和shuffle cardinality等操作,此处蹲坑。
7 . 总结
此篇续上篇博客《YOLOv7来临:论文解读附代码解析》,对YOLOv7网络结构中的一些重要模块进行学习,但在结尾处附加笔者的几处疑问,例如:
目前仓库还在频繁更新,期待作者后期给出的惊喜,另外在网络结构yaml配置文件将层拆的如此精细,笔者认为最快在一个月内,YOLOv7将提供Darknet框架的cfg模型配置文件,以适配Darknet框架的训练,和YOLOv4进行大一统。