经典目标检测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,先给出实现的网络结构图。