1. 超分处理(Focus)
设计思路:
对于一张超高分辨率图像来说,理论上可以周期性的抽出像素点重构到低分辨率图像中。虽然也可以使用插值等数学方法直接对超高分辨率图像进行压缩,但是这无疑会丢到一些图像信息。而在Focus模块中,可以对一张超高分辨率图像进行周期性抽取像素点重构为4张低分辨图像,也就是将图像相邻的四个位置进行堆叠,聚焦wh维度信息到c通道空,提高每个点感受野,并减少原始信息的丢失。这样做是为了减少计算量,加开计算速度,而不是增加网络的精度。
代码实现:
class Focus(nn.Module): def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): super(Focus, self).__init__() # concat后的卷积(最后的卷积) self.conv = Conv(c1 * 4, c2, k, s, p, g, act) def forward(self, x): # x(b,c,w,h) -> y(b,4c,w/2,h/2) image = torch.cat([x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]], dim=1) return self.conv(image)
2. Bottleneck
这是yolo系列backbone的精髓所在,yolov5的提供了两个不同的版本:
2.1 BottleneckCSP
- 第一种:BottleneckCSP
yolov5代码:
class BottleneckCSP(nn.Module): # CSP Bottleneck https://github.com/WongKinYiu/CrossStagePartialNetworks def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion super().__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = nn.Conv2d(c1, c_, 1, 1, bias=False) self.cv3 = nn.Conv2d(c_, c_, 1, 1, bias=False) self.cv4 = Conv(2 * c_, c2, 1, 1) self.bn = nn.BatchNorm2d(2 * c_) # applied to cat(cv2, cv3) self.act = nn.LeakyReLU(0.1, inplace=True) self.m = nn.Sequential(*[Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)]) def forward(self, x): y1 = self.cv3(self.m(self.cv1(x))) y2 = self.cv2(x) return self.cv4(self.act(self.bn(torch.cat((y1, y2), dim=1))))
2.2 C3
- 第二种:C3,这里由于只有3个卷积,使用命名为C3,相比与CSPBottleneck,C3更加简单,更快,且更轻量。
yolov5代码:
class C3(nn.Module): # CSP Bottleneck with 3 convolutions def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion super().__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c1, c_, 1, 1) self.cv3 = Conv(2 * c_, c2, 1) # act=FReLU(c2) self.m = nn.Sequential(*[Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)]) # self.m = nn.Sequential(*[CrossConv(c_, c_, 3, 1, g, 1.0, shortcut) for _ in range(n)]) def forward(self, x): return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), dim=1))
2.3 GhostBottleneck
- 第三种:GhostBottleneck
paper:GhostNet: More Features from Cheap Operations
这里yolov5还实现了GhostConv,这个是一个轻量级网络的结构。主要的思想就是觉得部分卷积是不需要的,可以通过一些分线性操作来代替。这是因为作者在实验过程中将ResNet50的第一个残差组的feature map进行可视化,发现里面有三对feature map,然后认为这些feature map对之间是冗余的(相关的)。
考虑到这些feature map层中的冗余信息可能是一个成功模型的重要组成部分,正是因为这些冗余信息才能保证输入数据的全面理解,所以作者在设计轻量化模型的时候并没有试图去除这些冗余feature map,而是尝试使用更低成本的计算量来获取这些冗余feature map。也就是普通的通过线性变化来获取这些类似冗余的特征图。
具体来说,将深度神经网络中的普通卷积层分为两部分。第一部分涉及普通的卷积,但它们的总数将被严格控制。给定第一部分的固有特征图,然后应用一系列简单的线性操作来生成更多的特征图。
GhostConv示意图:
yolov5代码:
class GhostConv(nn.Module): # Ghost Convolution https://github.com/huawei-noah/ghostnet def __init__(self, c1, c2, k=1, s=1, g=1, act=True): # ch_in, ch_out, kernel, stride, groups super().__init__() c_ = c2 // 2 # hidden channels self.cv1 = Conv(c1, c_, k, s, None, g, act) self.cv2 = Conv(c_, c_, 5, 1, None, c_, act) def forward(self, x): y = self.cv1(x) return torch.cat([y, self.cv2(y)], 1)
GhostBottleneck示意图:
yolov5代码:
class GhostBottleneck(nn.Module): # Ghost Bottleneck https://github.com/huawei-noah/ghostnet def __init__(self, c1, c2, k=3, s=1): # ch_in, ch_out, kernel, stride super().__init__() c_ = c2 // 2 self.conv = nn.Sequential(GhostConv(c1, c_, 1, 1), # pw DWConv(c_, c_, k, s, act=False) if s == 2 else nn.Identity(), # dw GhostConv(c_, c2, 1, 1, act=False)) # pw-linear self.shortcut = nn.Sequential(DWConv(c1, c1, k, s, act=False), Conv(c1, c2, 1, 1, act=False)) if s == 2 else nn.Identity() def forward(self, x): return self.conv(x) + self.shortcut(x)
简要分析:可以发现,其实代码里和原论文的有出入的。这里的shortcut是另外一个分支进行处理,而不是简单的残差相加。
2.4 TransformerBlock
- 第四种:TransformerBlock
关于transformer的具体介绍,可以查阅之前的两篇笔记:
1. 学习笔记——Transformer结构的完整介绍
2. Vision Transformer:笔记总结与pytorch实现
结构示意图:
在yolov5的实现代码中,同样进行了部分改动,其去除了norm的操作,MLP模块中也没有使用激活函数,而是直接两个全连接层进行操作。而且,这里的qkv全部使用Linear操作获取的,整个架构非常的简洁明了,代码如下所示:
yolov5代码:
class TransformerLayer(nn.Module): # Transformer layer https://arxiv.org/abs/2010.11929 (LayerNorm layers removed for better performance) def __init__(self, c, num_heads): super().__init__() self.q = nn.Linear(c, c, bias=False) self.k = nn.Linear(c, c, bias=False) self.v = nn.Linear(c, c, bias=False) self.ma = nn.MultiheadAttention(embed_dim=c, num_heads=num_heads) self.fc1 = nn.Linear(c, c, bias=False) self.fc2 = nn.Linear(c, c, bias=False) def forward(self, x): # 获取qkv然后进行多头自注意力机制操作 x = self.ma(self.q(x), self.k(x), self.v(x))[0] + x x = self.fc2(self.fc1(x)) + x return x class TransformerBlock(nn.Module): # Vision Transformer https://arxiv.org/abs/2010.11929 def __init__(self, c1, c2, num_heads, num_layers): super().__init__() self.conv = None if c1 != c2: self.conv = Conv(c1, c2) self.linear = nn.Linear(c2, c2) # learnable position embedding self.tr = nn.Sequential(*[TransformerLayer(c2, num_heads) for _ in range(num_layers)]) self.c2 = c2 def forward(self, x): if self.conv is not None: x = self.conv(x) b, _, w, h = x.shape # (b,c,h,w) -> (b,c,hw) -> (1,b,c,hw) -> (hw,b,c,1) -> (hw,b,c) p = x.flatten(2).unsqueeze(0).transpose(0, 3).squeeze(3) # TransformerLayer不改变特征序列的维度, 保持不变 # (hw,b,c) -> (hw,b,c) -> (hw,b,c,1) -> (1,b,c,hw) -> (b,c,h,w) return self.tr(p + self.linear(p)).unsqueeze(3).transpose(0, 3).reshape(b, self.c2, w, h) class C3TR(C3): # C3 module with TransformerBlock() def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): super().__init__(c1, c2, n, shortcut, g, e) c_ = int(c2 * e) # 这里只是简单的将Bottleneck模块替换为TransformerBlock即可 self.m = TransformerBlock(c_, c_, 4, n)
3. Neck
Neck的作用是将多个不同分辨率的特征进行融合,得到更多的信息。虽然之前已经介绍过,但由于yolov5的代码写得太好了,这里贴上来
3.1 SPP
- SPP:并行处理
yolov5代码:
class SPP(nn.Module): # Spatial Pyramid Pooling (SPP) layer https://arxiv.org/abs/1406.4729 def __init__(self, c1, c2, k=(5, 9, 13)): super().__init__() c_ = c1 // 2 # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c_ * (len(k) + 1), c2, 1, 1) self.m = nn.ModuleList([nn.MaxPool2d(kernel_size=x, stride=1, padding=x // 2) for x in k]) def forward(self, x): x = self.cv1(x) with warnings.catch_warnings(): warnings.simplefilter('ignore') # suppress torch 1.9.0 max_pool2d() warning return self.cv2(torch.cat([x] + [m(x) for m in self.m], 1))
3.2 SPPF
- SPPF:串行处理(同等性能之下的速度更快)
yolov5代码:
class SPPF(nn.Module): # Spatial Pyramid Pooling - Fast (SPPF) layer for YOLOv5 by Glenn Jocher def __init__(self, c1, c2, k=5): # equivalent to SPP(k=(5, 9, 13)) super().__init__() c_ = c1 // 2 # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c_ * 4, c2, 1, 1) self.m = nn.MaxPool2d(kernel_size=k, stride=1, padding=k // 2) def forward(self, x): x = self.cv1(x) with warnings.catch_warnings(): warnings.simplefilter('ignore') # suppress torch 1.9.0 max_pool2d() warning y1 = self.m(x) y2 = self.m(y1) return self.cv2(torch.cat([x, y1, y2, self.m(y2)], 1))
4. 维度变化
yolov5提供了维度变化的两个函数Contract、Expand。Contract函数改变输入特征的shape,将feature map的w和h维度(缩小)的数据收缩到channel维度上(放大)。如:x(1,64,80,80) to x(1,256,40,40)。Expand函数也是改变输入特征的shape,不过与Contract的相反, 是将channel维度(变小)的数据扩展到W和H维度(变大)。如:x(1,64,80,80) to x(1,16,160,160)。
这里贴上来维度变化是想需要注意,其变化时候是与普通变化是有区别的,具体可以直接看以下yolov5的代码:
class Contract(nn.Module): # Contract width-height into channels, i.e. x(1,64,80,80) to x(1,256,40,40) def __init__(self, gain=2): super().__init__() self.gain = gain def forward(self, x): b, c, h, w = x.size() # assert (h / s == 0) and (W / s == 0), 'Indivisible gain' s = self.gain x = x.view(b, c, h // s, s, w // s, s) # x(1,64,40,2,40,2) x = x.permute(0, 3, 5, 1, 2, 4).contiguous() # x(1,2,2,64,40,40) return x.view(b, c * s * s, h // s, w // s) # x(1,256,40,40) class Expand(nn.Module): # Expand channels into width-height, i.e. x(1,64,80,80) to x(1,16,160,160) def __init__(self, gain=2): super().__init__() self.gain = gain def forward(self, x): b, c, h, w = x.size() # assert C / s ** 2 == 0, 'Indivisible gain' s = self.gain x = x.view(b, s, s, c // s ** 2, h, w) # x(1,2,2,16,80,80) x = x.permute(0, 3, 4, 1, 5, 2).contiguous() # x(1,16,80,2,80,2) return x.view(b, c // s ** 2, h * s, w * s) # x(1,16,160,160)
简要分析:可以看见,在代码实现上无论是Contract还是Expand,其都是先对wh进行拆分之后,再进行维度的变化,对c进行处理。然后需要对内存进行连续化。这样才能避免对数据的破坏
5. 二级分类
yolov5中在command.py的最后提供了一个二级分类的模块,那么首先介绍一下二级分类的概念?
什么是二级分类模块?比如做车牌的识别,先识别出车牌 ,如果想对车牌上的字进行识别,就需要二级分类进一步检测。如果对模型输出的分类再进行分类,就可以用这个模块。不过这里这个类写的比较简单,若进行复杂的二级分类,可以根据自己的实际任务可以改写,这里代码不唯一。这里的功能和torch_utils.py中的load_classifier函数功能相似。
本质上的实现理念就是:(b,c1,w,h) -> (b, c2)
参考代码:
class Classify(nn.Module): # Classification head, i.e. x(b,c1,20,20) to x(b,c2) def __init__(self, c1, c2, k=1, s=1, p=None, g=1): # ch_in, ch_out, kernel, stride, padding, groups super().__init__() self.aap = nn.AdaptiveAvgPool2d(1) # to x(b,c1,1,1) self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g) # to x(b,c2,1,1) self.flat = nn.Flatten() def forward(self, x): z = torch.cat([self.aap(y) for y in (x if isinstance(x, list) else [x])], 1) # cat if list return self.flat(self.conv(z)) # flatten to x(b,c2)
分析:实现上其实也就就是一个全局池化,卷积然后进行展平的操作。
6. Other Conv
在yolov5的结构部分代码还提供了一些其他我没有见过的卷积模块,这里顺便记录下来。
6.1 MixConv2d
paper:(MixConv: Mixed Depthwise Convolutional Kernels)[https://arxiv.org/abs/1907.09595]
思想:把特征图的channel进行划分再使用不同的卷积核进行卷积拼接
其出发点是:随着卷积核的增大,其特征提取能力会随之增大,但超过某个值之后就反而会减小,并且指出以下3点发现。
- 大核卷积擅长提取大感受野特征,而小核卷积擅长提取小感受野特征
- 高感受野特征和小感受野特征是互补关系,并不是说大感受野特征一定比小感受野特征要好(可以考虑卷积核与特征图尺寸相当的极端情况)
- 如果能够混合不同感受野的特征,那么对于提高特征提取能力将会有所帮助
但是混个不同感受野的特征其实Inception上早就使用过,但是计算量有点大,所以这里提出了一个稍微轻量级的感受野融合方法,部分借鉴了可分离卷积的思想,把特征图的channel进行划分再使用不同的卷积核进行卷积拼接。
yolov5代码:
class MixConv2d(nn.Module): # Mixed Depth-wise Conv https://arxiv.org/abs/1907.09595 def __init__(self, c1, c2, k=(1, 3), s=1, equal_ch=False): super().__init__() groups = len(k) # 均匀通道 if equal_ch: # equal c_ per group # floor是去除小数点 i = torch.linspace(0, groups - 1E-6, c2).floor() # c2 indices c_ = [(i == g).sum() for g in range(groups)] # intermediate channels # 指数递减通道 else: # equal weight.numel() per group b = [c2] + [0] * groups a = np.eye(groups + 1, groups, k=-1) a -= np.roll(a, 1, axis=1) a *= np.array(k) ** 2 a[0] = 1 c_ = np.linalg.lstsq(a, b, rcond=None)[0].round() # solve for equal weight indices, ax = b self.m = nn.ModuleList([nn.Conv2d(c1, int(c_[g]), k[g], s, k[g] // 2, bias=False) for g in range(groups)]) self.bn = nn.BatchNorm2d(c2) self.act = nn.LeakyReLU(0.1, inplace=True) def forward(self, x): # 虽然思想上是想对不同的channel进行不同kernel size的卷积操作 # 但是实现上还是对全部的channel进行不同kernel size的卷积输出不同的channels再拼接在一起, 并没有分channel进行卷积实现 return x + self.act(self.bn(torch.cat([m(x) for m in self.m], 1)))
分析:使用可以看见,这样yolov5同样对原始的MixConv2d进行了改进,通过查看其核心的self.m结构就知道了:
ModuleList( (0): Conv2d(32, 16, kernel_size=(1, 1), stride=(1, 1), bias=False) (1): Conv2d(32, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) )
这里yolov5的实现上没有把特征图的channel进行划分再使用不同的卷积核进行卷积拼接,而是对全部的channel进行不同的卷积核卷积成不同的channel再拼接成一起。
6.2 CrossConv
对于这个模块,其实网上我并没有找到任何的介绍(如有有找到的朋友可以在评论上告诉我)
单单看名字来说,可以知道其基本的操作概念,就是交叉卷积,其实也可以看成是十字卷积。对于普通的3x3卷积其实是可以看成是一个局部的滑动窗口,而这里的Cross是指十字形来进行卷积操作。具体上实现,就是先使用一个(1, 3)的卷积核卷积,再使用一个(3, 1)的卷积核卷积。
曾经我曾看到一篇和transformer的文章,笔记:论文阅读笔记 | Transformer系列——CSWin Transformer。这篇paper中提出cross windons的概念是为了可以并行计算十字形窗口的竖直与水平中的自注意力,其中通过将输入特征分割成等宽的条带来得到每个条带。这提高了单层网络的感受野,同时也减少了计算参数。
再回过来看yolov5的函数代码,我个人觉得可以理解为借鉴了CSWin Transformer的一个思想:
class CrossConv(nn.Module): # Cross Convolution Downsample def __init__(self, c1, c2, k=3, s=1, g=1, e=1.0, shortcut=False): # ch_in, ch_out, kernel, stride, groups, expansion, shortcut super().__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, (1, k), (1, s)) self.cv2 = Conv(c_, c2, (k, 1), (s, 1), g=g) self.add = shortcut and c1 == c2 def forward(self, x): return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
个人理解,在视频理解领域中,由于3D卷积的参数量巨大,通常一般会先对空间进行卷积再对时间进行卷积,本质上这种操作的方法就是想在不降低精度的情况下可以减少参数量与浮点运算量的效果。
7. 加权特征融合
思想: 传统的特征融合往往只是简单的feature map叠加/相加 (sum them up), 比如使用concat或者shortcut连接, 而不对同时加进来的feature map进行区分。然而,不同的输入feature map具有不同的分辨率, 它们对融合输入feature map的贡献也是不同的, 因此简单的对他们进行相加或叠加处理并不是最佳的操作, 所以这里我们提出了一种简单而高效的加权特融合的机制。
yolov5代码:
class Sum(nn.Module): # Weighted sum of 2 or more layers https://arxiv.org/abs/1911.09070 def __init__(self, n, weight=False): # n: number of inputs super().__init__() self.weight = weight # apply weights boolean self.iter = range(n - 1) # iter object # 针对n层特征层构建n-1个可学习参数 if weight: self.w = nn.Parameter(-torch.arange(1., n) / 2, requires_grad=True) # layer weights def forward(self, x): y = x[0] # no weight if self.weight: w = torch.sigmoid(self.w) * 2 # 特征权重的加权和 # y = x[0] + x[1]*w0 + x[2]*w1 + x[3]*w2 for i in self.iter: y = y + x[i + 1] * w[i] else: # 普通的特征融合, 无权重区分 # y = x[0] + x[1] + x[2] + x[3] for i in self.iter: y = y + x[i + 1] return y # 测试代码 if __name__ == '__main__': x = torch.rand([2, 32, 8, 8]) p = [x, x, x, x] bottleneck = Sum(n=4, weight=True) print(bottleneck(p).shape)
8. Ensemble集成算法
见:YOLOv5的Tricks | 【Trick2】目标检测中进行多模型推理预测(Model Ensemble)
参考资料:
1. GhostNet_GhostModule(2020)
2. GhostNet
3. 学习笔记——Transformer结构的完整介绍
4. Vision Transformer:笔记总结与pytorch实现
5. MixConv:混合感受野的深度可分离卷积
6. 【YOLOV5-5.x 源码解读】common.py
7. 【YOLOV5-5.x 源码解读】experimental.py
8. YOLOv5的Tricks | 【Trick2】目标检测中进行多模型推理预测(Model Ensemble)