目录
前言
YOLOV3以V1和V2为基础进行改进得。YOLO3主要的改进有:调整了网络结构;利用多尺度特征进行对象检测;对象分类用Logistic取代了softmax。如果想了解V1、V2的详细结构信息,可以参照以下链接。
YOLOv3没有太多的创新,主要是借鉴一些好的方案融合到YOLO里面。不过效果还是不错的,在保持速度优势的前提下,提升了预测精度,尤其是加强了对小物体的识别能力。但就目前而言,YOLOV3毫无疑问现在成为了工程界首选的检测算法之一了,结构清晰,实时性好。
废话不多说,再深入了解YOLOV3框架前,我们先看看其整体得网络结构吧。
正如常见的one-stage目标检测算法结构那样,YOLOV3可以大致的分为:特征提取层、多尺度检测、分类预测等三部分组成。
- 特征提取:YOLOV3提出的Darknet-53结构,该结构借鉴了残差网络residual network的做法,在相应层之间设置了短接操作。
- 多尺度检测:有点类似FPN的结构,能够获取不同尺寸感受野,实现更多细粒度的检测,这也许是YOLOV3能够检测到小物体的原因之一吧。
- 分类预测:这方面中规中矩,关于具体锚点、anchors以及loss等相关怎么计算啊,可以继续阅读,看我的更多的文章。
1、backbone
YOLO3采用Darknet-53的网络结构(含有53个卷积层)作为特征提取网络,整个网络没有池化(pooling)和全连接层(FC),在传播过程中尺寸变换是通过卷积核的步长来实现。在现实框架搭建过程中经常会用到这种方法,比如将stride=(2,2),那么就会将原来的尺寸缩小一半。
该结构主要有三种结构单元:
_ConvBlock:基本组件,由conv+BN+Leak relu组成,对于v3来说,BN和leaky relu已经是和卷积层不可分离的部分了(最后一层卷积除外),共同构成了最小组件。
_ResidualBlock:残差单元,这是yolo_v3的大组件,yolo_v3开始借鉴了ResNet的残差结构,使用这种结构可以让网络结构更深(从v2的darknet-19上升到v3的darknet-53,前者没有残差结构)。
_ConvPoolBlock:下采样单元,主要为了获取更大的感受野,对特征进行下采样。其组成与基本组件_ConvBlock结构相似。
图1、_ConvBlock结构 图2、_ResidualBlock结构
在darknet53.py中,三种结构分别定义如下:
class _ConvBlock(tf.keras.Model):
def __init__(self, filters, layer_idx, name=""):
super(_ConvBlock, self).__init__(name=name)
layer_name = "layer_{}".format(str(layer_idx))
self.conv = layers.Conv2D(filters, (3, 3), strides=(1, 1), padding='same', use_bias=False, name=layer_name)
self.bn = layers.BatchNormalization(epsilon=0.001, name=layer_name)
def call(self, input_tensor, training=False):
x = self.conv(input_tensor)
x = self.bn(x, training=training)
x = tf.nn.leaky_relu(x, alpha=0.1)
return x
class _ConvPoolBlock(tf.keras.Model):
def __init__(self, filters, layer_idx, name=""):
super(_ConvPoolBlock, self).__init__(name=name)
layer_name = "layer_{}".format(str(layer_idx))
self.pad = layers.ZeroPadding2D(((1,0),(1,0)))
self.conv = layers.Conv2D(filters, (3, 3), strides=(2, 2), padding='valid', use_bias=False, name=layer_name)
self.bn = layers.BatchNormalization(epsilon=0.001, name=layer_name)
def call(self, input_tensor, training=False):
x = self.pad(input_tensor)
x = self.conv(x)
x = self.bn(x, training=training)
x = tf.nn.leaky_relu(x, alpha=0.1)
return x
class _ResidualBlock(tf.keras.Model):
def __init__(self, filters, layer_idx, name=""):
super(_ResidualBlock, self).__init__(name=name)
filters1, filters2 = filters
layer1, layer2 = layer_idx
layer_name1 = "layer_{}".format(str(layer1))
layer_name2 = "layer_{}".format(str(layer2))
self.conv2a = layers.Conv2D(filters1, (1, 1), padding='same', use_bias=False, name=layer_name1)
self.bn2a = layers.BatchNormalization(epsilon=0.001, name=layer_name1)
self.conv2b = layers.Conv2D(filters2, (3, 3), padding='same', use_bias=False, name=layer_name2)
self.bn2b = layers.BatchNormalization(epsilon=0.001, name=layer_name2)
def call(self, input_tensor, training=False):
x = self.conv2a(input_tensor)
x = self.bn2a(x, training=training)
x = tf.nn.leaky_relu(x, alpha=0.1)
x = self.conv2b(x)
x = self.bn2b(x, training=training)
x = tf.nn.leaky_relu(x, alpha=0.1)
x += input_tensor
return x
从上边的表格可知,在backbone部分最终得到三种尺寸的特征图,分别为:52x52x256、26x26x512和13x13x1024。
2、多尺度融合检测
yolov3采用多尺度预测((13*13)(26*26)(52*52))。小尺度:13*13的feature map,这里特征图的感受野比较大,因此适合检测图像中尺寸比较大的对象;中尺度 :26*26的feature map,具有中等尺度的感受野,适合检测中等尺度的对象;大尺度:52*52的feature map,它的感受野最小,适合检测小尺寸的对象。这样做能让网络同时学习到深层和浅层的特征,通过叠加浅层特征图相邻特征到不同通道(而非空间位置),类似于Resnet中的identity mapping。这个方法把26x26x512的特征图叠加成13x13x2048的特征图,与原生的深层特征图相连接,使模型有了细粒度特征,增加对小目标的识别能力。
当我们得到多尺度特征之后,我们还需要进一步进行特征提取和抽象化,具体可以从文章开头的整体框架可以看出,在这一步的过程中我们没用进行尺度的变换,而是在维度方面进行了一系列的变化,这些卷积都设置为stride=(1,1),padding="SAME" .除了将变化后的卷积再进行预测,这里还需要对当前卷积进行上采样,变为原来的2倍与上一个特征层进行concat,然后继续进行一系列卷积操作和预测结果,以此类推。
- Convolutional Set
Convolutional Set是特征提取器的内部卷积核结构,主要由一系列1*1和3*3的卷积操作组成。其中1*1的卷积核用于降维,3*3的卷积核用于提取特征,多个卷积核交错达到目的,每个全卷积特征层是有连接的。
在yolohead.py中,Convolutional Set结构分别定义如下:
class _Conv5(tf.keras.Model):
def __init__(self, filters, layer_idx, name=""):
super(_Conv5, self).__init__(name=name)
layer_names = ["layer_{}".format(i) for i in layer_idx]
self.conv1 = layers.Conv2D(filters[0], (1, 1), strides=(1, 1), padding='same', use_bias=False, name=layer_names[0])
self.bn1 = layers.BatchNormalization(epsilon=0.001, name=layer_names[0])
self.conv2 = layers.Conv2D(filters[1], (3, 3), strides=(1, 1), padding='same', use_bias=False, name=layer_names[1])
self.bn2 = layers.BatchNormalization(epsilon=0.001, name=layer_names[1])
self.conv3 = layers.Conv2D(filters[2], (1, 1), strides=(1, 1), padding='same', use_bias=False, name=layer_names[2])
self.bn3 = layers.BatchNormalization(epsilon=0.001, name=layer_names[2])
self.conv4 = layers.Conv2D(filters[3], (3, 3), strides=(1, 1), padding='same', use_bias=False, name=layer_names[3])
self.bn4 = layers.BatchNormalization(epsilon=0.001, name=layer_names[3])
self.conv5 = layers.Conv2D(filters[4], (1, 1), strides=(1, 1), padding='same', use_bias=False, name=layer_names[4])
self.bn5 = layers.BatchNormalization(epsilon=0.001, name=layer_names[4])
def call(self, input_tensor, training=False):
x = self.conv1(input_tensor)
x = self.bn1(x, training=training)
x = tf.nn.leaky_relu(x, alpha=0.1)
x = self.conv2(x)
x = self.bn2(x, training=training)
x = tf.nn.leaky_relu(x, alpha=0.1)
x = self.conv3(x)
x = self.bn3(x, training=training)
x = tf.nn.leaky_relu(x, alpha=0.1)
x = self.conv4(x)
x = self.bn4(x, training=training)
x = tf.nn.leaky_relu(x, alpha=0.1)
x = self.conv5(x)
x = self.bn5(x, training=training)
x = tf.nn.leaky_relu(x, alpha=0.1)
return x
经过Convolutional Set系列的处理后我们最终得到的特征大小分别为:52x52x128、26x26x256和13x13x512。可以看出这一步只是维度方面的变化,特征大小起始并没有改变。
- 特征融合
(1)stage5:backbone最后一层输出的特征,经过Convolutional Set系列的处理后的大小为13x13x512。此时可以直接用于预测;
(2)stage4:backbone第二个输出的特征,其大小为26x26x512,经过Convolutional Set系列的处理后的大小为26x26x256。此时还需要concat上stage5的输出(13x13x512)。对于stage5输出,需要对其进行降维,变成256维,然后进行上采样,扩大2倍,最终变成26x26x256。然后与stage4处的输入进行concat,最终输出为26x26x512。具体上采样操作流程如下:
在yolohead.py中,上采样结构定义如下:
class _Upsamling(tf.keras.Model):
def __init__(self,filters,layer_idx,name=""):
super(_Upsamling, self).__init__(name=name)
layer_names = ["layer_{}".format(i) for i in layer_idx]
self.conv = layers.Conv2D(filters[0], (1, 1), strides=(1, 1), padding='same', use_bias=False,
name=layer_names[0])
self.bn = layers.BatchNormalization(epsilon=0.001, name=layer_names[0])
self.upsampling = layers.UpSampling2D(2)
def call(self,input_tensor,training=False):
x=self.conv(input_tensor)
x=self.bn(x,training=training)
x = tf.nn.leaky_relu(x, alpha=0.1)
z=self.upsampling(x)
return x
(3)stage3:具体计算过程同stage4,当前层的输出大小为52x52x256。
最终,整体多尺度融合网络的定义如下:
class Headnet(tf.keras.Model):
def __init__(self, n_classes=80):
super(Headnet, self).__init__(name='')
n_features = 3 * (n_classes+1+4)
self.stage5_conv5 = _Conv5([512, 1024, 512, 1024, 512],
[75, 76, 77, 78, 79])
self.stage5_conv2 = _Conv2([1024, n_features],
[80, 81],
"detection_layer_1_{}".format(n_features))
self.stage5_upsampling = _Upsamling([256], [84])
self.stage4_conv5 = _Conv5([256, 512, 256, 512, 256],
[87, 88, 89, 90, 91])
self.stage4_conv2 = _Conv2([512, n_features],
[92, 93],
"detection_layer_2_{}".format(n_features))
self.stage4_upsampling = _Upsamling([128], [96])
self.stage3_conv5 = _Conv5([128, 256, 128, 256, 128],
[99, 100, 101, 102, 103])
self.stage3_conv2 = _Conv2([256, n_features],
[104, 105],
"detection_layer_3_{}".format(n_features))
self.num_layers = 106
self._init_vars()
def call(self, stage3_in, stage4_in, stage5_in, training=False):
x = self.stage5_conv5(stage5_in, training)
stage5_output = self.stage5_conv2(x, training)
x = self.stage5_upsampling(x, training)
x = layers.concatenate([x, stage4_in])
x = self.stage4_conv5(x, training)
stage4_output = self.stage4_conv2(x, training)
x = self.stage4_upsampling(x, training)
x = layers.concatenate([x, stage3_in])
x = self.stage3_conv5(x, training)
stage3_output = self.stage3_conv2(x, training)
return stage5_output, stage4_output, stage3_output
3、模型输出预测
正如上面提到的那样,通过多尺度特征融合后,如何进行最终的计算和预测输出结果呢?这一小节中将着重讲解下YOLOV3中边界框和如何进行分类计算。
3.1、anchor box
随着输出的特征图的数量和尺度的变化,先验框的尺寸也需要相应的调整。YOLOV2已经开始采用K-means聚类得到先验框的尺寸,YOLO3延续了这种方法,为每种下采样尺度设定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),适合检测较小的对象。
感受一下9种先验框的尺寸,下图中蓝色框为聚类得到的先验框。黄色框式ground truth,红框是对象中心点所在的网格。
(解析及图片来源:https://www.jianshu.com/p/d13ae1055302)
获取到特征图之后,我们最终得到13*13、26*26、52*52三种尺寸,如13*13的特征图,可以根据该尺寸输入图像划分为13 x 13的网格,如上小狗的图图片所示。对于每一网格,可以看作一个锚点,其对应三种尺寸的方框。当然,有同学会问,如果方框与实际物体对于不上咋办,这就是网络要学习的地方。也就是说,我们把一个方框用中心点(x,y)以及宽高(w,h)表示,通过学习和微调这些参数来进行预测。
3.2、边界框和分类预测
这里先假设网络的输出tx、ty、tw、th。cx和cy是网格的左上角坐标,表示当前预测方框在网格中那个位置。anchor box 的边长为pw、ph,即上述中实际先验框的大小。bx,by,bw,bh是预测的中心坐标x,y,宽度和高度。则将网络输出转化为预测输出的结果为:
- 中心坐标(bx、by):通过公式可知,我们通过sigmoid函数来预测中心坐标,使输出的值压缩在0和1之间。也就是说他通过预测中心坐标在单元网格中的相对位置。然后与当前单元网格的左上角相加就得到当前中心点的绝对位置。
为啥要使用sigmoid函数归一化中心坐标呢?
通常情况下,YOLO不预测边界框中心的绝对坐标。它预测的偏移:
- 相对于负责预测目标的网格单元的左上角。
- 通过特征图中的单元的维度,即1,进行归一化。
例如,图1狗的图像。如果预测的中心是(0.4,0.7),那么这意味着中心位于13×13特征图上的(6.4,6.7)。 (因为红色单元的左上坐标是(6,6))。
但是等等,如果预测的x,y坐标大于1,会发生什么情况,比如(1.2,0.7)。这意味着它的中心位于(7.2,6.7)。注意现在中心位于红色单元,即第7排的第8个单元的右侧。这打破了YOLO背后的理论,因为如果我们假设红色框负责预测狗,狗的中心必须位于红色单元中,而不是位于红色单元旁。
因此,为了解决这个问题,将输出通过一个sigmoid函数,该函数把输出缩小至0到1的范围内,有效地将中心保持在预测的网格单元中。
- 边界框尺寸(bw、bh):预测的边界框尺寸通过对输出进行对数空间变换,然后与anchor box的尺寸相乘来得到,也就相当于预测的相对于anchor box尺寸的缩放比
- 目标置信度(to):使用logistic回归用于对anchor包围的部分进行一个目标性评分(objectness score),即这块位置是目标的可能性有多大。这一步是在predict之前进行的,可以去掉不必要anchor,可以减少计算量。
3.3、具体实现
- 输出处理
当我们得到最终带预测的特征图之后,我们需要对其进行预处理,需要经过两个Conv操作,最终得到3个带预测的输出矩阵,具体操作如下:
class _Conv2(tf.keras.Model):
def __init__(self,filters,layer_idx,name=""):
super(_Conv2, self).__init__(name=name)
layer_names=["layer_{}".format(i) for i in layer_idx]
self.conv1=layers.Conv2D(filters[0],(3,3),strides=(1,1),padding='same',
use_bias=False,name=layer_names[0])
self.bn = layers.BatchNormalization(epsilon=0.001, name=layer_names[0])
self.conv2 = layers.Conv2D(filters[1], (1, 1), strides=(1, 1), padding='same',
use_bias=True,name=layer_names[1])
def call(self,input_tensor,training=False):
x=self.conv1(input_tensor)
x=self.bn(x,training=training)
x=tf.nn.leaky_relu(x,alpha=0.1)
x=self.conv2(x)
return x
三种尺度的特征具体预处理位于yolohead.py中98行:
self.stage5_conv2= _Conv2([1024,n_features,[80,81],"detection_layer_1_{}".format(n_features))
104行:
self.stage4_conv2 = _Conv2([512, n_features],[92, 93],"detection_layer_2_{}".format(n_features))
110行:
self.stage3_conv2 = _Conv2([256, n_features],[104, 105],"detection_layer_3_{}".format(n_features))
函数中n_features代表输出预测的维数。一般来讲,输出预测包含:方框预测(tx、ty、tw、th),边框置信度(to)、类别概率(对于COCO数据集,有80种对象),总共有3*((4+1)+80))=255个输出特征。所以,此处n_features=225。
我们看一下YOLO3共进行了多少个预测。对于一个416*416的输入图像,在每个尺度的特征图的每个网格设置3个先验框,总共有 13*13*3 + 26*26*3 + 52*52*3 = 10647 个预测。每一个预测是一个(4+1+80)=85维向量,这个85维向量包含边框坐标(4个数值),边框置信度(1个数值),对象类别的概率(对于COCO数据集,有80种对象)。
对比一下,YOLO2采用13*13*5 = 845个预测,YOLO3的尝试预测边框数量增加了10多倍,而且是在不同分辨率上进行,所以mAP以及对小物体的检测效果有一定的提升。
经过输出处理后最终得到三个输出分别为:13x13x255、26x26x255、52x52x255。
- 计算输出结果
def detect(self, image, anchors, net_size=416):
image_h, image_w, _ = image.shape
# 归一化操作,将图片转化为416x416
new_image = preprocess_input(image, net_size)
# 输出前向传播的预测结果,最终得到三种输出矩阵:13x13x255、26x26x255、52x52x255
yolos = self.predict(new_image)
#分别对输出结果进行解析,转化为实际的方框、置信度和分类概率
boxes_ = postprocess_ouput(yolos, anchors, net_size, image_h, image_w)
if len(boxes_) > 0:
boxes, probs = boxes_to_array(boxes_)
boxes = to_minmax(boxes)
labels = np.array([b.get_label() for b in boxes_])
else:
boxes, labels, probs = [], [], []
return boxes, labels, probs
def postprocess_ouput(yolos, anchors, net_size, image_h, image_w, obj_thresh=0.5, nms_thresh=0.5):
anchors = np.array(anchors).reshape(3, 6)
boxes = []
for i in range(len(yolos)):
# 解析输出结果
boxes += decode_netout(yolos[i][0], anchors[3 - (i + 1)], obj_thresh, net_size)
correct_yolo_boxes(boxes, image_h, image_w)
nms_boxes(boxes, nms_thresh)
return boxes
def decode_netout(netout, anchors, obj_thresh, net_size, nb_box=3):
n_rows, n_cols = netout.shape[:2]# netout.shape=[13,13,255] /[26,26,255]/[52,52,255]
netout = netout.reshape((n_rows, n_cols, nb_box, -1)) #[13,13,3,85]/[26,26,3,85]/[52,52,3,85]
boxes = []
for row in range(n_rows):
for col in range(n_cols):
for b in range(nb_box):#遍历每一个预测输出
#计算实际方框尺寸
x, y, w, h = _decode_coords(netout, row, col, b, anchors)
#计算置信度和分类概率
objectness, classes = _activate_probs(netout[row, col, b, 4],
netout[row, col, b, 5:],
obj_thresh)
#对输出尺寸进行归一化
x /= n_cols
y /= n_rows
w /= net_size
h /= net_size
#筛选输出结果,去除置信度过低的方框,减少输出结果
if objectness > obj_thresh:
box = BoundBox(x, y, w, h, objectness, classes)
boxes.append(box)
return boxes
def _decode_coords(netout, row, col, b, anchors):
x, y, w, h = netout[row, col, b, :4]
#将预测输出转化为实际的输出尺寸
x = col + _sigmoid(x) #sigmoid(tx)+Cx
y = row + _sigmoid(y) #sigmoid(ty)+Cy
w = anchors[2 * b + 0] * np.exp(w) #Pw*e(w)
h = anchors[2 * b + 1] * np.exp(h) #Ph*e(h)
return x, y, w, h
def _activate_probs(objectness, classes, obj_thresh=0.3):
# 归一化置信度
objectness_prob = _sigmoid(objectness)
#归一化分类概率
classes_prob = _sigmoid(classes)
#计算实际分类概率
classes_conditional_prob = classes_prob * objectness_prob
#去除置信度过低的方框
classes_conditional_prob *= objectness_prob > obj_thresh
return objectness_prob, classes_conditional_prob
4、NMS非极大值抑制
由于预测结果存在大量的重叠方框以及置信度过低的方框,所以在上面得到的输出结果上还需要进一步进行方框的筛选工作。这里就需要介绍NMS算法了。
非极大值抑制(Non-Maximum Suppression,NMS),顾名思义就是抑制不是极大值的元素,可以理解为局部最大搜索。这个局部代表的是一个邻域,邻域有两个参数可变,一是邻域的维数,二是邻域的大小。通常用于目标检测中提取分数最高的窗口的。例如在行人检测中,滑动窗口经提取特征,经分类器分类识别后,每个窗口都会得到一个分数。但是滑动窗口会导致很多窗口与其他窗口存在包含或者大部分交叉的情况。这时就需要用到NMS来选取那些邻域里分数最高(是行人的概率最大),并且抑制那些分数低的窗口。NMS在计算机视觉领域有着非常重要的应用,如视频目标跟踪、数据挖掘、3D重建、目标识别以及纹理分析等。
NMS原理:
对于Bounding Box的列表B及其对应的置信度S,采用下面的计算方式.选择具有最大score的检测框M,将其从B集合中移除并加入到最终的检测结果D中.通常将B中剩余检测框中与M的IoU大于阈值Nt的框从B中移除.重复这个过程,直到B为空.
重叠率(重叠区域面积比例IOU)阈值:
常用的阈值是
0.3 ~ 0.5
.
其中用到排序,可以按照右下角的坐标排序或者面积排序,也可以是通过SVM等分类器得到的得分或概率,R-CNN中就是按得分进行的排序.就像上面的图片一样,定位一个车辆,最后算法就找出了一堆的方框,我们需要判别哪些矩形框是没用的。非极大值抑制的方法是:先假设有6个矩形框,根据分类器的类别分类概率做排序,假设从小到大属于车辆的概率 分别为A、B、C、D、E、F。
(1)从最大概率矩形框F开始,分别判断A~E与F的重叠度IOU是否大于某个设定的阈值;
(2)假设B、D与F的重叠度超过阈值,那么就扔掉B、D;并标记第一个矩形框F,是我们保留下来的。
(3)从剩下的矩形框A、C、E中,选择概率最大的E,然后判断E与A、C的重叠度,重叠度大于一定的阈值,那么就扔掉;并标记E是我们保留下来的第二个矩形框。
就这样一直重复,找到所有被保留下来的矩形框。
在postprocess_ouput()函数中用到NMS来进一步筛选方框。
nms_boxes(boxes, nms_thresh)
NMS算法的实现位于box.py中:
def nms_boxes(boxes, nms_threshold=0.3, obj_threshold=0.3):
if len(boxes) == 0:
return boxes
# suppress non-maximal boxes
n_classes = len(boxes[0].classes)
for c in range(n_classes):
#选择某一类的方框,并按照方框的分类概率进行排序
sorted_indices = list(reversed(np.argsort([box.classes[c] for box in boxes])))
for i in range(len(sorted_indices)):
index_i = sorted_indices[i]#选择最大的分类概率
if boxes[index_i].classes[c] == 0:
continue
else:
for j in range(i + 1, len(sorted_indices)):#遍历剩下的方框
index_j = sorted_indices[j]
#计算两个方框的iou,如果大于阈值,则将该方框的分类概率置为0
if boxes[index_i].iou(boxes[index_j]) >= nms_threshold:
boxes[index_j].classes[c] = 0
# 删除没用超过obj_threshold的方框
boxes = [box for box in boxes if box.get_score() > obj_threshold]
return boxes
通过NMS,非极大值抑制,筛选出框boxes,输出框class_boxes和置信度class_box_scores,再生成类别信息classes,最终生成检测数据框,并返回。
小结
YOLO3借鉴了残差网络结构,形成更深的网络层次,以及多尺度检测,提升了mAP及小物体检测效果。如果采用COCO mAP50做评估指标(不是太介意预测框的准确性的话),YOLO3的表现相当惊人,如下图所示,在精确度相当的情况下,YOLOv3的速度是其它模型的3、4倍。
不过如果要求更精准的预测边框,采用COCO AP做评估标准的话,YOLO3在精确率上的表现就弱了一些。如下图所示。
参考链接: