关于 Yolov5家族知识点的总结。长文。
资料来源于网上。
yolov3 的网络主干是darknet53,最基本的结构是DBL,由卷积+BN+Leaky relu组成。
class ConvolutionalLayer(nn.Module):
def __init__(self, in_channels, out_channels, kernal_size, stride, padding):
super(ConvolutionalLayer, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernal_size, stride, padding),
nn.BatchNorm2d(out_channels),
nn.LeakyReLU(0.1)
)
def forward(self, x):
return self.conv(x)
使用了resn,即残差网络,其残差部分由2个DBL堆叠而成,n代表数字,有res1,res2, … ,res8等等,表示这个res_block里含有多少个res_unit。
class ResidualLayer(nn.Module):
def __init__(self, in_channels):
super(ResidualLayer, self).__init__()
self.reseblock = nn.Sequential(
ConvolutionalLayer(in_channels, in_channels // 2, kernal_size=1, stride=1, padding=0),
ConvolutionalLayer(in_channels // 2, in_channels, kernal_size=3, stride=1, padding=1)
)
def forward(self, x):
return x + self.reseblock(x)
还有下采样层DownSample,整个yolov3结构里,没有池化层和全连接层。前向传播过程中,特征图的尺寸变换是通过下采样层来实现的。由公式 W i n = ( W o u t − K e r n e l + 2 ∗ P a d d i n g ) / S t r i d e + 1 W_{in}=(W_{out}-Kernel+2*Padding)/Stride +1 Win=(Wout−Kernel+2∗Padding)/Stride+1可知,特征图经过下采样层后,尺寸减半(向下取整)。
class DownSampleLayer(nn.Module):
def __init__(self, in_channels, out_channels):
super(DownSampleLayer, self).__init__()
self.conv = nn.Sequential(
ConvolutionalLayer(in_channels, out_channels, kernal_size=3, stride=2, padding=1)
)
def forward(self, x):
return self.conv(x)
上采样层UpSample,通过最近邻差值算法使特征图的尺寸加倍,用于特征融合。此处yolov3参考了FPN特征金字塔。
class UpSampleLayer(nn.Module):
def __init__(self):
super(UpSampleLayer, self).__init__()
def forward(self, x):
return F.interpolate(x, scale_factor=2, mode='nearest')
最后,yolov3还多次用到了5个DBL串联的网络结构。
class ConvolutionalSetLayer(nn.Module):
def __init__(self, in_channel, out_channel):
super(ConvolutionalSetLayer, self).__init__()
self.conv = nn.Sequential(
ConvolutionalLayer(in_channel, out_channel, kernal_size=1, stride=1, padding=0),
ConvolutionalLayer(out_channel, in_channel, kernal_size=3, stride=1, padding=1),
ConvolutionalLayer(in_channel, out_channel, kernal_size=1, stride=1, padding=0),
ConvolutionalLayer(out_channel, in_channel, kernal_size=3, stride=1, padding=1),
ConvolutionalLayer(in_channel, out_channel, kernal_size=1, stride=1, padding=0)
)
def forward(self, x):
return self.conv(x)
整个darknet53网络结构如下
class DarkNet53(nn.Module):
def __init__(self):
super(DarkNet53, self).__init__()
self.feature_52 = nn.Sequential(
ConvolutionalLayer(3, 32, 3, 1, 1),
DownSampleLayer(32, 64),
ResidualLayer(64),
DownSampleLayer(64, 128),
ResidualLayer(128),
ResidualLayer(128),
DownSampleLayer(128, 256),
ResidualLayer(256),
ResidualLayer(256),
ResidualLayer(256),
ResidualLayer(256),
ResidualLayer(256),
ResidualLayer(256),
ResidualLayer(256),
ResidualLayer(256)
)
self.feature_26 = nn.Sequential(
DownSampleLayer(256, 512),
ResidualLayer(512),
ResidualLayer(512),
ResidualLayer(512),
ResidualLayer(512),
ResidualLayer(512),
ResidualLayer(512),
ResidualLayer(512),
ResidualLayer(512),
)
self.feature_13 = nn.Sequential(
DownSampleLayer(512, 1024),
ResidualLayer(1024),
ResidualLayer(1024),
ResidualLayer(1024),
ResidualLayer(1024)
)
self.convolset_13 = nn.Sequential(
ConvolutionalSetLayer(1024, 512)
)
self.convolset_26 = nn.Sequential(
ConvolutionalSetLayer(768, 256)
)
self.convolset_52 = nn.Sequential(
ConvolutionalSetLayer(384, 128)
)
self.detection_13 = nn.Sequential(
ConvolutionalLayer(512, 1024, 3, 1, 1),
nn.Conv2d(1024, 15, 1, 1, 0)
)
self.detection_26 = nn.Sequential(
ConvolutionalLayer(256, 512, 3, 1, 1),
nn.Conv2d(512, 15, 1, 1, 0)
)
self.detection_52 = nn.Sequential(
ConvolutionalLayer(128, 256, 3, 1, 1),
nn.Conv2d(256, 15, 1, 1, 0)
)
self.up_26 = nn.Sequential(
ConvolutionalLayer(512, 256, 1, 1, 0),
UpSampleLayer()
)
self.up_52 = nn.Sequential(
ConvolutionalLayer(256, 128, 1, 1, 0),
UpSampleLayer()
)
def forward(self, x):
h_52 = self.feature_52(x)
h_26 = self.feature_26(h_52)
h_13 = self.feature_13(h_26)
conval_13 = self.convolset_13(h_13)
detection_13 = self.detection_13(conval_13)
up_26 = self.up_26(conval_13)
route_26 = torch.cat((up_26, h_26), dim=1)
conval_26 = self.convolset_26(route_26)
detection_26 = self.detection_26(conval_26)
up_52 = self.up_52(conval_26)
route_52 = torch.cat((up_52, h_52), dim=1)
conval_52 = self.convolset_52(route_52)
detection_52 = self.detection_52(conval_52)
return detection_13, detection_26, detection_52
在backbone部分,yolov3一共进行了5次下采样,输入为416x416,输出为13x13,所以通常都要求输入图片是32的倍数。
yolov3输出了3个不同尺度的特征图,采用多尺度来对不同尺寸的目标进行检测,越精细的网格单元就可以检测出越精细的物体。
对于COCO类别而言,有80个类别,所以每个box应该对每个类别都输出一个概率。
每个box需要有x, y, w, h, confidence(中心坐标,框的宽度和高度)五个基本参数,还要有80个类别的概率。yolov3设定的是每个网格单元预测3个box,所以得到特征图的深度为 3*(5 + 80) = 255。3个不同尺度的特征图的输出分别是 13 ∗ 13 ∗ 255 , 26 ∗ 26 ∗ 255 , 52 ∗ 52 ∗ 255 13*13*255,26*26*255,52*52*255 13∗13∗255,26∗26∗255,52∗52∗255。
VOC2012数据集有20个类别,特征图的深度为 3*(5 + 20) = 75。
随着输出的特征图的数量和尺度的变化,先验框的尺寸也需要相应的调整。YOLO v2 已经开始采用 K-means 聚类得到先验框的尺寸,YOLO v3 延续了这种方法,为每种下采样尺度设定 3 种先验框,总共聚类出 9 种尺寸的先验框。在 COCO 数据集这 9 个先验框是:(10x13),(16x30),(33x23),(30x61),(62x45),(59x119),(116x90),(156x198),(373x326)。
分配上,在最小的 13*13 特征图上(有最大的感受野)应用较大的先验框(116x90),(156x198),(373x326),适合检测较大的对象。中等的 26 * 26 特征图上(中等感受野)应用中等的先验框(30x61),(62x45),(59x119),适合检测中等大小的对象。较大的 52 * 52 特征图上(较小的感受野)应用较小的先验框(10x13),(16x30),(33x23),适合检测较小的对象。
这里注意bounding box 与anchor box的区别:
Bounding box它输出的是框的位置(中心坐标与宽高),confidence以及N个类别。
anchor box只是一个尺度即只有宽高。
下图中蓝色框为聚类得到的先验框。黄色框式ground truth,红框是对象中心点所在的网格。
神经网络的输出分为四类:(x,y),(w,h), confident,class。损失函数应该由各自特点确定。加到一起组成最终的loss_function。
lxy, lwh, lcls, lconf = ft([0]), ft([0]), ft([0]), ft([0])
txy, twh, tcls, indices = build_targets(model, targets)#在13 26 52维度中找到大于iou阈值最适合的anchor box 作为targets
#txy[维度(0:2),(x,y)] twh[维度(0:2),(w,h)] indices=[0,anchor索引,gi,gj]
# Define criteria
MSE = nn.MSELoss()
CE = nn.CrossEntropyLoss()
BCE = nn.BCEWithLogitsLoss()
# Compute losses
h = model.hyp # hyperparameters
bs = p[0].shape[0] # batch size
k = h['k'] * bs # loss gain
for i, pi0 in enumerate(p): # layer i predictions, i
b, a, gj, gi = indices[i] # image, anchor, gridx, gridy
tconf = torch.zeros_like(pi0[..., 0]) # conf
# Compute losses
if len(b): # number of targets
pi = pi0[b, a, gj, gi] # predictions closest to anchors 找到p中与targets对应的数据lxy
tconf[b, a, gj, gi] = 1 # conf
# pi[..., 2:4] = torch.sigmoid(pi[..., 2:4]) # wh power loss (uncomment)
lxy += (k * h['xy']) * MSE(torch.sigmoid(pi[..., 0:2]),txy[i]) # xy loss
lwh += (k * h['wh']) * MSE(pi[..., 2:4], twh[i]) # wh yolo loss
lcls += (k * h['cls']) * CE(pi[..., 5:], tcls[i]) # class_conf loss
# pos_weight = ft([gp[i] / min(gp) * 4.])
# BCE = nn.BCEWithLogitsLoss(pos_weight=pos_weight)
lconf += (k * h['conf']) * BCE(pi0[..., 4], tconf) # obj_conf loss
loss = lxy + lwh + lconf + lcls
yolov3使用了logistic回归来对每个anchor包围的内容进行了一个目标性评分(objectness score)。根据目标性评分来选择评分最高的预选框进行预测,而不是所有预选框都会有输出。yolov3只会对1个anchor 进行操作,也就是那个最佳anchor 。而logistic回归就是用来从9个anchor中找到objectness score(目标存在可能性得分)最高的那一个。logistic回归就是用曲线对anchor相对于 objectness score映射关系的线性建模。
yolov3要先build target,对于某个ground truth,首先要确定其中心点要落在哪个cell上,然后计算这个cell的每个anchor与ground truth的IOU值,计算IOU值时不考虑坐标,只考虑形状(因为anchor没有坐标xy信息),所以先将anchor与ground truth的中心点都移动到同一位置(原点),然后计算出对应的IOU值,IOU值最大的那个先验框anchor与ground truth匹配作为正样本参与训练,先验框anchor对应的预测框用来预测这个ground truth。
x,y,w,h是由均方差来计算loss的,其中预测的x,y进行sigmoid来与lable xy求差,label xy是grid cell中心点坐标,其值在0-1之间,所以predict出的xy要sigmoid。
class用的多类别交叉熵,置信度用的二分类交叉熵。
只有正样本才参与class,xywh的loss计算,负样本只参与confident的loss计算。
如果采用 COCOmAP-50 做评估指标,YOLO v3在精确度相当的情况下,速度是其它模型的 3、4 倍。
不过如果要求更精准的预测边框,采用 COCO AP做评估标准的话,YOLOv3 在精确率上的表现就弱了一些。
yolov5在数据预处理阶段进行了Mosaic数据增强。
Mosaic数据增强数据集中随机读取四张图片,分别对四张图片进行翻转(对原始图片进行左右的翻转)、缩放(对原始图片进行大小的缩放)、色域变化(对原始图片的明亮度、饱和度、色调进行改变)等操作。操作完成之后然后再将原始图片按照 第一张图片摆放在左上,第二张图片摆放在左下,第三张图片摆放在右下,第四张图片摆放在右上四个方向位置摆好。
完成四张图片的摆放之后,利用矩阵的方式将四张图片它固定的区域截取下来,然后将它们拼接起来,拼接成一 张新的图片,新的图片上含有框框等一系列的内容。
作者提出Mosaic,随机使用4张图片,随机缩放,再随机分布进行拼接,大大丰富了检测数据集,特别是随机缩放增加了很多小目标,让网络的鲁棒性更好。与此同时,Mosaic可以减少GPU的投入,使得小的bachsize也能训练出好的模型。
CSPDarknet53是在Yolov3主干网络Darknet53的基础上,借鉴2019年CSPNet的经验,产生的Backbone结构,其中包含了5个CSP模块。
每个CSP模块前面的卷积核的大小都是3*3,stride=2,因此可以起到下采样的作用。
CSPNet全称是Cross Stage Paritial Network,主要从网络结构设计的角度解决推理中从计算量很大的问题。
CSPNet的作者认为推理计算过高的问题是由于网络优化中的梯度信息重复导致的。
因此采用CSP模块先将基础层的特征映射划分为两部分,然后通过跨阶段层次结构将它们合并,在减少了计算量的同时可以保证准确率。
以 DenseNet 和CSPDenseNet为例
CSPDenseNet将 x 0 x_0 x0分成了 x 0 ′ x_{0'} x0′和 x 0 ′ ′ x_{0''} x0′′两个部分, x 0 ′ ′ x_{0''} x0′′部分经过Partial Dense Block后与 x 0 ′ x_{0'} x0′进行叠加,将两部分生成的特征映射连接起来。
CSP运用在残差网络上:
Yolov4在主干网络Backbone采用CSPDarknet53网络结构,主要有三个方面的优点:
增强CNN的学习能力,使得在轻量化的同时保持准确性。
降低计算瓶颈
降低内存成本
Yolov4只在Backbone中采用了Mish激活函数,网络后面仍然采用Leaky_relu激活函数。
M i s h = x ∗ t a n h ( l n ( 1 + e x ) ) Mish = x*tanh(ln(1+e^x)) Mish=x∗tanh(ln(1+ex))
从图中可以看出Mish在负值的时候并不是完全截断,而是允许比较小的负梯度流入,从而保证信息流动。
并且激活函数无边界这个特点,让他避免了饱和这一问题,比如sigmoid,tanh激活函数通常存在梯度饱和问题,在两边极限情况下,梯度趋近于1,而Mish激活函数则巧妙的避开了这一点。
另外Mish函数也保证了每一点的平滑,从而使得梯度下降效果比Relu要好。
Yolov4作者实验测试时,使用CSPDarknet53网络在ImageNet数据集上做图像分类任务,发现使用了Mish激活函数的TOP-1和TOP-5的精度比没有使用时都略高一些。
因此在设计Yolov4目标检测任务时,主干网络Backbone还是使用Mish激活函数。
Yolov4中使用的Dropblock,其实和常见网络中的Dropout功能类似,也是缓解过拟合的一种正则化方式。
(a)原始输入图像;
(b)绿色部分表示激活的特征单元,b图表示了随机dropout激活单元,但是这样dropout后,网络还会从drouout掉的激活单元附近学习到同样的信息;
©绿色部分表示激活的特征单元,c图表示本文的DropBlock,通过dropout掉一部分相邻的整片的区域(比如头和脚),网络就会去注重学习狗的别的部位的特征,来实现正确分类,从而表现出更好的泛化。
yolov4的Neck结构主要采用了SPP模块、FPN+PAN的方式。
在SPP模块中,使用 k = 1 ∗ 1 , 5 ∗ 5 , 9 ∗ 9 , 13 ∗ 13 k={1*1, 5*5, 9*9, 13*13} k=1∗1,5∗5,9∗9,13∗13的最大池化的方式,再将不同尺度的特征图进行Concat操作
class SpatialPyramidPooling(nn.Module):
def __init__(self, pool_sizes=[5, 9, 13]):
super(SpatialPyramidPooling, self).__init__()
self.maxpools = nn.ModuleList([nn.MaxPool2d(pool_size, 1, pool_size // 2) for pool_size in pool_sizes])
def forward(self, x):
features = [maxpool(x) for maxpool in self.maxpools[::-1]]
features = torch.cat(features + [x], dim=1)
return features
和Yolov3的FPN层不同,Yolov4在FPN层的后面还添加了一个自底向上的特征金字塔。
PAN通过自顶向下传达强语义特征,特征金字塔(两个PAN)则自底向上传达强定位特征。两相结合,缩短了低层与顶层特征之间的信息路径,促进了特征融合,进一步提高特征提取的能力。
两个PAN层特征的融合依旧是concat。
yolov4的损失函数由Classificition Loss(分类损失函数)和Bounding Box Regeression Loss(回归损失函数)两部分构成。
对 Regeression问题,Yolov4中采用了CIOU_Loss的回归方式,使得预测框回归的速度和精度更高一些。
CIOU_Loss函数考虑了三个重要几何因素:重叠面积、中心点距离,长宽比
在目标检测的后处理过程中,针对很多目标框的筛选,通常需要nms非极大值抑制操作。其实就是在众多候选框中选取最接近真实值的候选框,忽略掉其他的。
因为CIOU_Loss中包含影响因子v,涉及groudtruth的信息,而测试推理时,是没有groundtruth的。所以Yolov4在DIOU_Loss的基础上采用DIOU_nms的方式,而Yolov5中采用加权nms的方式。
不同的nms,会有不同的效果,采用了DIOU_nms的方式,在同样的参数情况下,将nms中IOU修改成DIOU_nms。对于一些遮挡重叠的目标,确实会有一些改进。
虽然大多数状态下效果差不多,但在不增加计算成本的情况下,有稍微的改进也是好的。
yolov5 有四种模型:yolov5s, yolov5m, yolov5l, yolov5x
个人用的比较多的是yolov5s,yolov5s网络最小,速度最快,AP精度最低。检测的以大目标为主,追求速度,是个不错的选择。其他的三种网络,在此基础上,不断加深加宽网络,AP精度也不断提升,但速度的消耗也在不断增加。
Yolov5的网络结构也分为输入端、Backbone、Neck、Prediction四个部分
在yolo算法中,针对不同的数据集,都会有初始设定长宽的锚框。在网络训练中,网络在初始锚框的基础上输出预测框,进而和真实框groundtruth进行比对,计算两者差距,再反向更新,迭代网络参数。
因此初始锚框也是比较重要的一部分。
在Yolov3、Yolov4中,训练不同的数据集时,计算初始锚框的值是通过单独的程序运行的。
但Yolov5中将此功能嵌入到代码中,每次训练时,自适应的计算不同训练集中的最佳锚框值。
比如右图的切片示意图, 4 ∗ 4 ∗ 3 4*4*3 4∗4∗3的图像切片后变成 2 ∗ 2 ∗ 12 2*2*12 2∗2∗12的特征图。
以Yolov5s的结构为例,原始 608 ∗ 608 ∗ 3 608*608*3 608∗608∗3的图像输入Focus结构,采用切片操作,先变成 304 ∗ 304 ∗ 12 304*304*12 304∗304∗12的特征图,再经过一次32个卷积核的卷积操作,最终变成 304 ∗ 304 ∗ 32 304*304*32 304∗304∗32的特征图。
class Focus(nn.Module):
def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups
super(Focus, self).__init__()
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)
return self.conv(torch.cat([x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]], 1))
需要注意的是:Yolov5s的Focus结构最后使用了32个卷积核,而其他三种结构,使用的数量有所增加,先注意下,后面会讲解到四种结构的不同点。
在常用的目标检测算法中,不同的图片长宽都不相同,因此常用的方式是将原始图片统一缩放到一个标准尺寸,再送入检测网络中。
比如Yolo算法中常用 416 ∗ 416 416*416 416∗416, 608 ∗ 608 608*608 608∗608等尺寸,比如对下面 800 ∗ 600 800*600 800∗600的图像进行缩放。
图像高度上两端的黑边变少了,在推理时,计算量也会减少,即目标检测速度会得到提升。
通过这种简单的改进,推理速度得到了37%的提升,可以说效果很明显。
Yolov5中设计了两种CSP结构,以Yolov5s网络为例,CSP1_X结构应用于Backbone主干网络,另一种CSP2_X结构则应用于Neck中。
Yolov5中采用加权nms的方式。加权非极大值抑制与传统的非极大值抑制相比,是在进行矩形框剔除的过程中,并未将那些与当前矩形框IOU大于阈值,且类别相同的框直接剔除,而是根据网络预测的置信度进行加权,得到新的矩形框,把该矩形框作为最终预测的矩形框,再将那些框剔除。
while detections.size(0):
# 得到第一个bbx与其余bbx的iou大于nms_thres的判别(0, 1), 1为大于,0为小于
large_overlap = bbox_iou(detections[0, :4].unsqueeze(0), detections[:, :4]) > nms_thres
# 判断他们的类别是否相同,只有相同时才进行nms, 相同时为1, 不同时为0
label_match = detections[0, -1] == detections[:, -1]
# Indices of boxes with lower confidence scores, large IOUs and matching labels
# 只有在两个bbx的iou大于thres,且类别相同时,invalid为True,其余为False
invalid = large_overlap & label_match
# weights为对应的权值, 其格式为:将True bbx中的confidence连成一个Tensor
weights = detections[invalid, 4:5]
# Merge overlapping bboxes by order of confidence
# 这里得到最后的bbx它是跟他满足IOU大于threshold,并且相同label的一些bbx,根据confidence重新加权得到
# 并不是原始bbx的保留。
detections[0, :4] = (weights * detections[invalid, :4]).sum(0) / weights.sum()
keep_boxes += [detections[0]]
# 去掉这些invalid
detections = detections[~invalid]
if keep_boxes:
output[image_i] = torch.stack(keep_boxes)