经典目标检测yolo系列(三)yolov3算法详解
-
不论是yolov1,还是yolov2,都有一个共同的致命缺陷:
小目标检测的性能差
。尽管yolov2使用了passthrough技术将16倍降采样的特征图(即c4特征图)融合到了c5特征图中,但最终的检测仍是在c5尺度的特征图上进行的。 -
为了解决这一问题,yolo作者做了第3次改进,主要改进如下:
- 使用了更好的主干网络darknet-53
- 使用了多级检测与特征金字塔fpn方法
- 修改损失函数
1 yolov3的改进之处
1.1 更好的主干网络darknet-53
-
下图是darknet-53的网络架构图。
-
相较于yolov2中所使用的darknet19,新的网络使用了53层卷积。
-
同时,添加了残差网络中的残差连结结构,以提升网络的性能。
-
darknet53网络中的降采样操作没有使用maxpooling层,而是由stride=2的卷积来实现。
-
卷积层仍旧是线性卷积、bn层以及leakyrelu激活函数的串联组合。
-
虚线框是核心模块,由一层1×1卷积和一层3×3卷积层串联构成的残差模块。
在imagenet数据集上,darknet53的top1准确率和top5准确率几乎与resnet101和resnet152持平,但速度却显著高于后两者。因此,相较于所对比的两个残差网络,darknet53在速度和精度上具有更高的性价比。
不过darknet53没有成为学术界的主流模型,其受欢迎程度仍不及resnet系列。
1.2 多级检测与特征金字塔
- yolo-v1模型精度不足的一个重要表现就是召回率低,即
该检的检不出
,这一缺点在针对小目标检测方面表现的尤为明显。 - 为了提高目标检测的召回率,yolo-v2通过使用passthrough操作将浅层特征与深层特征进行融合,使得最终用于目标预测的特征中,既包含细节信息也包含语义信息。
- 这种操作在一定程度上提高了目标检测的召回率,针对小型目标的检测能力也有明显提高。
- 然而,仅使用一层细节特征进行细节特征融合往往是不够的(ssd的目标预测在6个尺度的特征图上进行)。
- 因此,yolo-v3借助了特征金字塔网络(feature pyramid network, fpn)机制,从3个不同尺度的融合特征上进行目标预测。fpn是2017年(早于yolo-v3提出一年)提出的一种特征融合网络结构,旨在为目标检测模型提供一种有效的多尺度特征融合机制。
1.2.1 特征金字塔fpn
-
fpn工作认为网络
浅层的特征图包含更多的细节信息,但语义信息较少,而深层的特征图则恰恰相反
。 -
随着网络深度的加深,降采样操作的增多,细节信息不断被破坏,致使小物体的检测效果逐渐变差,而大目标由于像素较多,仅靠网络的前几层还不足以使得网络能够认识到大物体(感受野不充分),但随着层数变多,网络的感受野逐渐增大,网络对大目标的认识越来越充分,检测效果自然会更好。
-
因此,
用浅层网络负责检测较小的目标,深层网络负责检测较大的目标
。实现这一技术路线的就是ssd网络,但ssd只关注了信息数量问题,没有关注语义深浅问题。浅层特征虽然保留足够多的位置信息,但是语义信息的层次较浅,对目标的理解和认识不够充分。 -
考虑识别物体的类别依赖于语义信息,因此fpn利用
自顶向下(top-down)
的特征融合结构,利用空间上采样
将深层网络的语义信息融合到浅层网络中(下图中的d)。- 处于性能和算力之间的平衡考虑,我们只会使用到主干网络输出的3个尺度的特征图,即c3、c4和c5,其降采样倍数分别为8、16和32。fpn会通过1×1卷积、3×3卷积以及上采样操作得到p3、p4和p5特征图。
- 如果我们输入图像比较大,如800×1333,c5特征图感受野就不够大,无法覆盖到一些大目标,而且自身的语义信息相对较浅,因此就会在c5或者p5进一步降采样得到特征图p6,甚至p7。例如,retinanet以及fcos等。
1.3 yolov3中的fpn
-
yolov3的关键改进便是使用了fpn结构与多级检测方法。yolov3在3个尺度上去进行预测,分别是经过8倍降采样的特征图c3、经过16倍降采样的特征图c4和经过32倍降采样的特征图c5。yolov3网络结构如下图所示。
-
yolov3中的fpn,特征融合采用通道拼接,而非求和。
-
yolov3中fpn的卷积层较多。
-
yolov3最终会输出52×52×3(1+c+4)、26×26×3(1+c+4)和13×13×3(1+c+4)三个预测张量,然后将这些预测结果汇总到一起,进行后处理,得到最终的检测结果。
-
-
-
从网格角度来看,假如输入图像是416×416,那么darknet-53输出的3个特征图:c3(52×52×256)、c4(26×26×512)和c5(13×13×1024)。相当于针对输入图像做了不同疏密的网格,显然越密的网格越适合检测小物体,而越疏的网格越适合检测大物体。
-
在每个特征图上,yolov3在每个网格处放置3个先验框。
-
由于yolov3一共使用3个尺度,因此,yolov3一共设定了9个先验框,这9个先验框仍旧是使用kmeans聚类的方法获得的。
-
在coco上,这9个先验框的宽高分别是(10, 13)、(16, 30)、(33, 23)、(30, 61)、(62, 45)、(59, 119)、(116, 90)、(156, 198)、(373, 326)。
- c3特征图,每个网格处放置(10, 13)、(16, 30)、(33, 23)三个先验框,用来检测较小的物体。
- c4特征图,每个网格处放置(30, 61)、(62, 45)、(59, 119)三个先验框,用来检测中等大小的物体。
- c5特征图,每个网格处放置(116, 90)、(156, 198)、(373, 326)三个先验框,用来检测较大的物体。
-
可以使用下面代码可视化,这三组先验框。
#!/usr/bin/env python # -*- coding:utf-8 -*- import os import cv2 def show_anchor_box(picture_path, feature_map_size=13): # 输入图片尺寸 input_size = 416 # 在coco数据集上,利用kmeans聚类出来的9组不同宽高的anchor box mask52 = [0, 1, 2] mask26 = [3, 4, 5] mask13 = [6, 7, 8] anchors = [ 10, 13, 16, 30, 33, 23, # 小物体 30, 61, 62, 45, 59, 119, # 中等物体 116, 90, 156, 198, 373, 326 # 大物体 ] grid_show_flag = true img = cv2.imread(picture_path) print("原始图片的shape: ", img.shape) img = cv2.resize(img, (input_size, input_size)) # 显示网格,颜色为黑色 if grid_show_flag: height, width, channels = img.shape grid_sizex = int(input_size / feature_map_size) for x in range(0, width - 1, grid_sizex): cv2.line(img, pt1 = (x, 0), pt2 = (x, height), color = (0, 0, 0), thickness = 1, linetype = 1) # x grid grid_sizey = int(input_size / feature_map_size) for y in range(0, height - 1, grid_sizey): cv2.line(img, pt1 = (0, y), pt2 = (width, y), color = (0, 0, 0), thickness = 1, linetype = 1) # y grid if feature_map_size == 13: for ele in mask13: # 画出图像中心点聚类出来不同宽高的3组anchor box,颜色为红色 # 需要告诉函数的左上角顶点pt1和右下角顶点的坐标pt2 cv2.rectangle(img, pt1 = ((int(input_size * 0.5 - 0.5 * anchors[ele * 2]), int(input_size * 0.5 - 0.5 * anchors[ele * 2 + 1]))), pt2 = ((int(input_size * 0.5 + 0.5 * anchors[ele * 2]), int(input_size * 0.5 + 0.5 * anchors[ele * 2 + 1]))), color = (0, 0, 255), thickness = 2 ) if feature_map_size == 26: for ele in mask26: # 画出图像中心点聚类出来不同宽高的3组anchor box,颜色为红色 # 需要告诉函数的左上角顶点pt1和右下角顶点的坐标pt2 cv2.rectangle(img, pt1 = ((int(input_size * 0.5 - 0.5 * anchors[ele * 2]), int(input_size * 0.5 - 0.5 * anchors[ele * 2 + 1]))), pt2 = ((int(input_size * 0.5 + 0.5 * anchors[ele * 2]), int(input_size * 0.5 + 0.5 * anchors[ele * 2 + 1]))), color = (0, 0, 255), thickness = 2 ) if feature_map_size == 52: for ele in mask52: # 画出图像中心点聚类出来不同宽高的3组anchor box,颜色为红色 # 需要告诉函数的左上角顶点pt1和右下角顶点的坐标pt2 cv2.rectangle(img, pt1 = ((int(input_size * 0.5 - 0.5 * anchors[ele * 2]), int(input_size * 0.5 - 0.5 * anchors[ele * 2 + 1]))), pt2 = ((int(input_size * 0.5 + 0.5 * anchors[ele * 2]), int(input_size * 0.5 + 0.5 * anchors[ele * 2 + 1]))), color = (0, 0, 255), thickness = 2 ) cv2.imshow('img', img) while cv2.waitkey(1000) != 27: # loop if not get esc. if cv2.getwindowproperty('img', cv2.wnd_prop_visible) <= 0: break cv2.destroyallwindows() if __name__ == '__main__': directory = './imgs' for filename in os.listdir(directory): picture_path = os.path.join(directory, filename) show_anchor_box(picture_path, feature_map_size=13) show_anchor_box(picture_path, feature_map_size=26) show_anchor_box(picture_path, feature_map_size=52)
-
1.3 修改损失函数
- 边界框的置信度损失。
- 由yolov1及yolov2的mse损失函数,改为bce损失,即我们之前自己实现的yolov1及yolov2中的损失函数。
- 不设置正负样本的权重,尽管负样本数量远远大于正样本。
- 不使用预测框和目标框的iou值作为置信度的学习标签,而采用0/1离散值,即我们之前自己实现的yolov1做法。
- 类别损失。
- 不同于之前的mse损失函数,yolov3先使用sigmoid函数将每个类别的置信度映射到0到1之间,再使用bce去计算每个类别的损失,即我们之前自己实现的yolov1做法。
- 不使用softmax的解释。
- softmax面对的类别必须是平行互斥的,预测得到最终的类别取概率分布中的最大者。
- 当面对类别标签为非平行互斥的数据集,softmax预测将无能为力。
- 与之不同的是,sigmoid预测得到的结果仅表示属于对应类别可能性,与其他类别无关,预测类别之间不互斥在某种意义上意味着对象可以拥有多个标签。
- 边界框损失
- 使用bce函数来计算中心点偏移量的损失
- 使用mse计算宽高偏移量的损失
1.4 yolov3效果
-
相较于yolov2的aps指标5.0,yolov3达到了18.3,小目标检测能力大大提高。
-
尽管yolov3的性能不及retinanet,但在ap50指标上,yolov3几乎和retinanet达到一个水准,但yolov3的速度是后者的3倍左右。
2 yolov3的复现
-
事实上,yolov2最大的变化就在于使用了多级检测以及fpn。
-
后面依然不会百分之百地复现官方的yolov3,先给出实现的网络结构图。
发表评论