不论是YOLOv1,还是YOLOv2,都有一个共同的致命缺陷:小目标检测的性能差
。尽管YOLOv2使用了passthrough技术将16倍降采样的特征图(即C4特征图)融合到了C5特征图中,但最终的检测仍是在C5尺度的特征图上进行的。
为了解决这一问题,YOLO作者做了第3次改进,主要改进如下:
下图是DarkNet-53的网络架构图。
相较于YOLOv2中所使用的DarkNet19,新的网络使用了53层卷积。
同时,添加了残差网络中的残差连结结构,以提升网络的性能。
DarkNet53网络中的降采样操作没有使用Maxpooling层,而是由stride=2的卷积来实现。
卷积层仍旧是线性卷积、BN层以及LeakyReLU激活函数的串联组合。
虚线框是核心模块,由一层1×1卷积和一层3×3卷积层串联构成的残差模块。
在ImageNet数据集上,DarkNet53的top1准确率和top5准确率几乎与ResNet101和ResNet152持平,但速度却显著高于后两者。因此,相较于所对比的两个残差网络,DarkNet53在速度和精度上具有更高的性价比。
不过DarkNet53没有成为学术界的主流模型,其受欢迎程度仍不及ResNet系列。
该检的检不出
,这一缺点在针对小目标检测方面表现的尤为明显。FPN工作认为网络浅层的特征图包含更多的细节信息,但语义信息较少,而深层的特征图则恰恰相反
。
随着网络深度的加深,降采样操作的增多,细节信息不断被破坏,致使小物体的检测效果逐渐变差,而大目标由于像素较多,仅靠网络的前几层还不足以使得网络能够认识到大物体(感受野不充分),但随着层数变多,网络的感受野逐渐增大,网络对大目标的认识越来越充分,检测效果自然会更好。
因此,用浅层网络负责检测较小的目标,深层网络负责检测较大的目标
。实现这一技术路线的就是SSD网络,但SSD只关注了信息数量问题,没有关注语义深浅问题。浅层特征虽然保留足够多的位置信息,但是语义信息的层次较浅,对目标的理解和认识不够充分。
考虑识别物体的类别依赖于语义信息,因此FPN利用自顶向下(top-down)
的特征融合结构,利用空间上采样
将深层网络的语义信息融合到浅层网络中(下图中的d)。
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)。
可以使用下面代码可视化,这三组先验框。
#!/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)
相较于YOLOv2的APs指标5.0,YOLOv3达到了18.3,小目标检测能力大大提高。
尽管YOLOv3的性能不及RetinaNet,但在AP50指标上,YOLOv3几乎和RetinaNet达到一个水准,但YOLOv3的速度是后者的3倍左右。
事实上,YOLOv2最大的变化就在于使用了多级检测以及FPN。
后面依然不会百分之百地复现官方的YOLOv3,先给出实现的网络结构图。