这篇文章详细介绍了单阶段目标检测器(如YOLO和SSD)的工作原理、训练过程以及评估方法。以下是文章的主要内容概括:

1. 单阶段目标检测器概述

  • 目标检测:与图像分类不同,目标检测不仅需要识别图像中的目标类别,还需要定位这些目标的位置(通过边界框)。

  • 单阶段检测器:与基于区域提议(如Faster R-CNN)的两阶段检测器不同,单阶段检测器(如YOLO、SSD)直接从图像中预测边界框和类别概率,速度更快,适合移动设备。

2. 模型架构

  • 基础网络:通常使用预训练的特征提取器(如MobileNet、ResNet),输入图像尺寸较大(如416×416),以保留细节。

  • 检测层:在特征提取器之上,添加额外的卷积层,用于预测边界框和类别概率。

  • 网格化检测:将输出特征图划分为网格(如13×13),每个网格单元格负责检测特定区域的目标。

3. 锚点(Anchors)

  • 锚点的作用:帮助检测器专注于特定形状和大小的目标,减少预测的复杂性。

  • 锚点的生成:YOLO通过k-means聚类生成锚点,而SSD使用数学公式计算锚点。

4. 损失函数

  • 多任务损失:损失函数包括边界框回归损失、类别概率的分类损失以及置信度分数的损失。

  • 正样本与负样本:正样本是与真实边界框匹配的检测器,负样本是没有目标的检测器。

  • 损失计算:通过计算预测值与目标值之间的差异,结合置信度分数、边界框坐标和类别概率,得到最终的损失值。

5. 训练数据与增强

  • 数据集:常用的数据集包括Pascal VOC、COCO等,这些数据集提供了图像及其注释(边界框和类别)。

  • 数据增强:通过随机翻转、裁剪、颜色变化等方法增加数据多样性,提高模型的泛化能力。

6. 模型评估

  • 评估指标:使用平均精度均值(mAP)来评估模型性能,mAP越高,模型越好。

  • 精度-召回率曲线:通过绘制不同阈值下的精度和召回率,评估模型在不同类别上的表现。

7. 实验与结果

  • 实验模型:作者通过实验模型展示了精度-召回率曲线,并讨论了如何通过调整阈值来平衡假阳性和假阴性。

  • 性能比较:YOLO v2的mAP为48.8%,SSD的mAP为64%,而最新的模型如refine_denseSSD达到了77.5%。

8. 实际应用

  • 移动设备优化:SSD和SSDLite在移动设备上表现良好,特别是MobileNetV2 + SSDLite,提供了速度和准确性的良好平衡。

  • 开发建议:作者建议使用MobileNetV2 + SSDLite作为快速目标检测的起点,并提供了相关资源链接。

9. 结论

  • 模型选择:选择目标检测器时,需要考虑速度、准确性和应用场景。YOLO和SSD各有优势,适用于不同的需求。

  • 未来方向:随着研究的深入,目标检测模型的性能和效率将进一步提升,为实际应用提供更好的支持。

这篇文章不仅深入探讨了单阶段目标检测器的技术细节,还提供了丰富的实验结果和实际应用建议,对从事相关领域的研究人员和开发者具有重要的参考价值。这里是自己的阅读记录,感兴趣的话可以参考一下,如果需要阅读原文的话可以看这里,如下所示:

目标检测是计算机视觉中用于在图像中查找感兴趣目标的技术:

图示说明:34 29.9FPS 延迟:0.05484秒

这比分类更为复杂,分类仅能告诉你图像是什么“主要内容”,而目标检测可以找到多个目标,对它们进行分类,并确定它们在图像中的位置。

目标检测模型会预测边界框,每个检测到的目标一个,以及每个目标的分类概率。

目标检测通常会预测过多的边界框。每个框都有一个置信度分数,表明模型认为该框真正包含目标的可能性有多大。作为一种后处理步骤,我们会过滤掉分数低于某个阈值的框(也称为非极大值抑制)。

目标检测比分类更具挑战性。其中一个问题是训练图像中可能包含从零到数十个目标,而模型可能会输出多个预测,那么在损失函数中,你该如何确定将哪个预测与哪个真实边界框进行比较呢?

像Faster R-CNN这样的高端模型首先会生成所谓的区域提议,即图像中可能包含目标的区域,然后对每个区域分别进行预测。这种方法效果很好,但速度相当慢,因为它需要多次运行模型的检测和分类部分。

另一方面,单阶段检测器只需要通过神经网络进行一次传递,并一次性预测所有边界框。这要快得多,也更适合移动设备。最常见的单阶段目标检测器的例子包括YOLO、SSD、SqueezeDet和DetectNet。

不幸的是,这些模型的研究论文省略了许多重要的技术细节,而且关于训练这些模型的深度博客文章也不多。为了弄清楚这些细节,我花费了很多时间去理解论文,研究其他人的源代码,并从头开始训练自己的目标检测器。

在这篇(很长的!)博客文章中,我将尝试解释这些单阶段检测器是如何工作的,以及它们是如何被训练和评估的。

注意:你可能也对我的其他关于在iOS上使用YOLO进行目标检测的文章感兴趣。顺便说一句,YOLO代表“你只需要看一次”,而SSD代表“单次检测多框检测器”(我觉得SSMBD不是一个很好的缩写)。

为什么目标检测很复杂

分类器将图像作为输入,产生单一输出,即类别概率分布。但这只能为你提供图像整体内容的总结,当图像中有多个感兴趣的目标时,效果并不好。

在下面的图像中,分类器可能会识别出图像中包含一定量的“猫”和一定量的“狗”,但这已经是它能做到的最好结果了。

图示说明:100% 55% 分类器 35% 10% 猫 狗 其他

另一方面,目标检测模型会告诉你各个目标的位置,通过为每个目标预测一个边界框:

图示说明:狗 90% 猫 93%

由于它现在可以专注于边界框内的物体,并忽略框外的一切,模型能够对各个目标给出更自信的预测。

如果你的数据集带有边界框注释(所谓的“真实框”),在模型中添加定位输出就相当简单了。只需额外预测4个数字,分别对应边界框的4个角。

图示说明:100% 55% 35% 10% 猫 狗 其他 (100,24,243,80)

模型的损失函数只需将边界框的回归损失添加到分类的交叉熵损失中,通常使用均方误差(MSE):

outputs = model.forward_pass(image)
class_pred = outputs[0]
bbox_pred = outputs[1]
class_loss = cross_entropy_loss(class_pred, class_true)
bbox_loss = mse_loss(bbox_pred, bbox_true)
loss = class_loss + bbox_loss
optimize(loss)

现在,你像往常一样使用SGD优化模型,使用这种组合损失,它实际上效果很好。这里是一个示例预测:

图示说明:C

模型正确地找到了这个目标的类别(狗)以及它在图像中的位置。红色框是真实框,而青色框是预测框。虽然并不完美匹配,但已经相当接近了。

注意:这里给出的52.14%的分数是类别分数(82.16%为狗)和边界框真正包含目标的可能性的置信度分数(63.47%)的组合。

为了评估预测框与真实框的匹配程度,我们可以计算它们之间的IoU(交并比,也称为Jaccard指数)。

IoU是一个介于0到1之间的数字,数值越大越好。理想情况下,预测框和真实框的IoU为100%,但在实际中,只要超过50%通常就被认为是一个正确的预测。对于上面的例子,IoU为74.9%,你可以看到这两个框匹配得很好。

使用回归输出来预测单个边界框可以获得良好的结果。然而,就像分类在图像中有多个感兴趣的目标时效果不佳一样,这种简单的定位方案也会失败:

图示说明:福

模型只能预测一个边界框,因此它必须在多个目标中选择一个,但实际上框最终出现在图像中间。实际上,这里发生的情况是完全合理的:模型知道有两个目标,但它只有一个边界框可以使用,因此它妥协地将预测框放在两个马之间。框的大小也是两个马的大小的中间值。

注意:你可能期望模型现在会围绕两个目标画一个框,但这种情况并不会发生,因为模型没有被训练成这样。数据集中的真实框注释总是围绕单个目标绘制的,而不是围绕一组目标。

你可能会想,“这听起来很容易解决,我们只需通过为模型添加更多的边界框检测器(通过增加额外的回归输出)来解决。”毕竟,如果模型可以预测N个边界框,那么它应该能够找到多达N个目标,对吧?听起来是个计划……但它不起作用。

即使是一个有多个这种检测器的模型,我们仍然会得到所有边界框都出现在图像中间的情况:

图示说明:单阶段目标检测

为什么会发生这种情况?问题是模型不知道将哪个边界框分配给哪个目标,为了安全起见,它将它们都放在图像中间。

模型没有办法决定,“我可以将边界框1放在左边的马上,将边界框2放在右边的马上。”相反,每个检测器仍然试图预测所有目标,而不仅仅是其中一个。即使模型有N个检测器,它们也不会像一个团队一样协同工作。具有多个边界框检测器的模型仍然表现得完全像只预测一个边界框的模型。

我们需要某种方法让边界框检测器专业化,以便每个检测器只尝试预测一个目标,不同的检测器将找到不同的目标。

在一个不专业化的模型中,每个检测器都被要求能够处理图像中任何可能位置的任何可能类型的对象。这实在是太过分了……模型现在倾向于学习预测总是位于图像中心的框,因为在整个训练集中,这实际上最小化了它所犯的错误数量。

从SGD的角度来看,这样做在平均情况下给出了相当好的结果,但在实践中,这也不是一个真正有用的结果……因此我们需要更聪明地训练模型。

像YOLO、SSD和DetectNet这样的单阶段检测器都通过将每个边界框检测器分配到图像中的特定位置来解决这个问题。这样一来,检测器学会了专注于特定位置的物体。为了获得更好的结果,我们还可以让检测器专注于物体的形状和大小。

引入网格

使用固定网格的检测器是单阶段检测器的主要思想,也是它们与基于区域提议的检测器(如R-CNN)的区别所在。

让我们考虑这种模型最简单的架构。它由一个基础网络组成,该网络充当特征提取器。像大多数特征提取器一样,它通常在ImageNet上进行训练。

在YOLO的情况下,特征提取器的输入是一个416×416像素的图像。SSD通常使用300×300像素的图像。这些图像比分类用的图像(通常是224×224像素)要大,因为我们不希望丢失小细节。

图示说明:特征提取网络 物体检测层(在ImageNet上训练)(在Pascal VOC上训练)

基础网络可以是任何网络,例如Inception、ResNet或YOLO的DarkNet,但在移动设备上,使用小而快的架构,如SqueezeNet或MobileNet更有意义。

(在我的实验中,我使用了一个在224×224图像上训练的MobileNet V1特征提取器。我将其放大到448×448像素,并在ILSVRC 2012数据集的130万张图像上对这个网络进行了7个周期的微调,使用了基本的数据增强,例如随机裁剪和颜色抖动。在更大的图像上重新训练特征提取器可以使模型在416×416的输入上用于目标检测时表现更好。)

在特征提取器之上是几个额外的卷积层。这些层被微调以学习如何预测边界框和边界框内物体的类别概率。这是模型的目标检测部分。

有许多常见的数据集用于训练目标检测器。为了这篇博客文章,我们将使用Pascal VOC数据集,它有20个类别。因此,神经网络的第一部分在ImageNet上进行训练,第二部分在VOC上进行训练。

当你查看YOLO或SSD的实际架构时,它们比这个简单的示例网络要复杂一些,有跳跃连接等,但稍后我们会详细介绍。目前,上面的模型已经足够,并且实际上可以用来构建一个快速且相当准确的目标检测器。(它非常类似于我之前写过的“Tiny YOLO”架构。)

最终层的输出是一个特征图(上面插图中的绿色部分)。对于我们的示例模型,这是一个13×13的特征图,有125个通道。

注意:这里的数字13来自于输入图像的416像素大小,以及这里使用的基础网络有五个池化层(或步长为2的卷积层)的事实,它们一起将输入缩小了32倍,416 / 32 = 13。如果你想有一个更细的网格,例如19×19,那么输入图像应该宽和高都是19×32 = 608像素(或者你可以使用一个步长更小的网络)。

我们将这个特征图解释为一个13×13的网格。数字是奇数,因此网格中心有一个单独的单元格。网格中的每个单元格有5个独立的目标检测器,每个检测器预测一个边界框。

图示说明:Λ25 检测器1用于网格单元格 (0,12) 检测器 (25) 检测器4 (25) 检测器3 (25) 检测器2 (25) 检测器1 (25) 13 检测器 (25) 检测器 (25) 13

关键点:位置固定的检测器

这里的关键是检测器的位置是固定的:它只能检测位于该单元格附近的物体(实际上,物体的中心必须位于网格单元格内)。这让我们避免了前面提到的问题,即检测器拥有过多的自由度。有了这个网格,位于图像左侧的检测器永远不会预测位于右侧的目标。

每个检测器会输出以下内容:

  • 20个数字,表示类别概率

  • 4个边界框坐标(中心点x、中心点y、宽度、高度)

  • 1个置信度分数

因为每个单元格有5个检测器,而 5×25=125,所以这就是为什么我们有125个输出通道。

5×25=125

与常规分类器类似,用于类别概率的20个数字已经经过了softmax处理。我们可以通过查看最高数字来确定获胜类别。(尽管通常也将其视为多标签分类,在这种情况下,20个类别是独立的,我们使用sigmoid而不是softmax。)

置信度分数是一个介于0和1(或100%)之间的数字,描述了模型认为这个预测的边界框包含真实目标的可能性有多大。它也被称为“目标性”分数。需要注意的是,这个分数只说明是否存在目标,但并不说明是什么类型的目标——这就是类别概率的作用。

这个模型总是预测相同数量的边界框:13×13个单元格乘以5个检测器等于845个预测。显然,大多数预测都不会很好——毕竟,大多数图像最多只包含几个目标,而不是超过800个。置信度分数告诉我们哪些预测的框可以忽略。

通常情况下,我们最终会得到大约十几个模型认为是好的预测。其中一些可能会重叠——这会发生,因为相邻的单元格可能会对同一个目标进行预测,有时一个单元格也会进行多次预测(尽管在训练中会尽量避免这种情况)。

非极大值抑制

在目标检测中,出现多个重叠的预测是很常见的。标准的后处理技术是应用非极大值抑制(NMS)来去除这些重复项。简而言之,NMS保留置信度分数最高的预测,并移除那些与之重叠超过一定阈值(比如60%)的其他框。

我们通常只会保留大约10个左右最好的预测,并丢弃其他预测。理想情况下,我们希望图像中的每个目标只有一个边界框。

约束是好事

我已经提到过,将每个边界框检测器分配到图像中的固定位置是单阶段目标检测器能够工作的关键。我们使用13×13的网格作为空间约束,使模型更容易学习如何预测目标。

在机器学习中,使用这种(架构上的)约束是一种有用的技巧。实际上,卷积本身也是一种约束:卷积层实际上是全连接(FC)层的一个更受限的版本。(这就是为什么你可以用FC层实现卷积,反之亦然——它们本质上是同一种东西。)

如果只使用普通的全连接层,机器学习模型学习图像会更加困难。卷积层施加的约束——它一次只查看几个像素,并且连接共享相同的权重——帮助模型从图像中提取知识。我们使用这些约束来减少自由度,并引导模型学习我们希望它学到的东西。

同样,网格迫使模型学习专注于特定位置的目标检测器。左上角单元格中的检测器只会预测位于该左上角附近的物体,而不会预测更远地方的物体。(模型是这样训练的:一个给定网格单元格中的检测器只负责检测其中心落在该网格单元格内的物体。)

原始版本的模型没有这样的约束,因此它的回归层从未得到提示,只在特定位置进行查找。

锚点(Anchors)

网格是一种有用的约束,限制了检测器在图像中可以找到目标的位置。我们还可以添加另一种约束,帮助模型更好地进行预测,那就是对目标形状的约束。

我们的示例模型有13×13个网格单元格,每个单元格有5个检测器,因此总共有845个检测器。但是,为什么每个网格单元格有5个检测器,而不是只有一个呢?正如前面提到的,让检测器学习预测任何位置的目标已经很困难了,让检测器学习预测任何形状或大小的目标也同样困难。

我们使用网格来让检测器专注于特定的空间位置,通过在每个网格单元格中设置多个检测器,我们可以让每个目标检测器专注于特定的目标形状。

我们对检测器进行训练,使其专注于5种特定的形状:

红色框是训练集中五种最常见的目标形状。青色框分别是训练集中最小和最大的目标。需要注意的是,这些边界框是在模型的416×416像素输入分辨率中显示的。(图中还以浅灰色显示了网格,这样你可以看到这五种形状与网格单元格的关系。每个网格单元格覆盖输入图像中的32×32像素。)

这五种形状被称为锚点(anchors)或锚框(anchor boxes)。锚点其实就是一个宽度和高度的列表:

anchors = [1.19, 1.99,     # width, height for anchor 1
           2.79, 4.60,     # width, height for anchor 2
           4.54, 8.93,     # etc.
           8.06, 5.29,
           10.33, 10.65]

锚点描述了数据集中五种最常见的(平均的)目标形状。这里所说的“形状”,实际上只是它们的宽度和高度,因为我们这里处理的始终是基本的矩形。

锚点的数量并非偶然,每个网格单元格中的检测器都有一个锚点。就像网格对检测器施加了位置约束一样,锚点迫使单元格内的检测器各自专注于特定的目标形状。

一个单元格中的第一个检测器负责检测与第一个锚点大小相似的目标,第二个检测器负责与第二个锚点大小相似的目标,依此类推。因为我们每个单元格有5个检测器,所以也有5个锚点。

因此,小目标将被检测器1检测到,稍大的目标由检测器2检测,长而扁的目标由检测器3检测,高而窄的目标由检测器4检测,大目标由检测器5检测。

这些聚类代表了该数据集中不同目标形状的五种“平均值”。你可以看到,k-means认为有必要将非常小的目标归为蓝色聚类,稍大的目标归为红色聚类,而非常大的目标归为绿色聚类。它决定将中等大小的目标分为两组:一组是边界框比高更宽的(黄色),另一组是比宽更高的(紫色)。

但是,5个锚点真的是最优选择吗?我们可以在不同的聚类数量下多次运行k-means,并计算真实框与它们最近的锚点框之间的平均IoU。毫不奇怪,使用更多的聚类中心(更大的k值)会得到更高的平均IoU,但这同时也意味着我们需要在每个网格单元格中放置更多的检测器,这会使模型运行得更慢。对于YOLO v2,他们选择了5个锚点,作为召回率和模型复杂度之间的一个良好折中。

SSD并没有使用k-means来寻找锚点。相反,它使用一个数学公式来计算锚点的大小。因此,SSD的锚点与数据集无关(顺便说一下,SSD论文中将它们称为“默认框”)。你可能会认为,针对特定数据集的锚点应该能够给出更好的预测,但我不确定这是否真的很重要(YOLO的作者似乎认为很重要)。

另一个小的区别是:YOLO的锚点只是一个宽度和高度,但SSD的锚点还有一个x,y位置。YOLO简单地假设锚点的位置总是位于网格单元格的中心。(对于SSD来说,这也是默认的做法。)

得益于锚点,检测器不需要费很大力气就能做出相当不错的预测,因为预测全零仅仅输出锚点框,这通常会与真实目标相当接近(平均而言)。这让训练变得容易多了!如果没有锚点,每个检测器都必须从头开始学习不同边界框的形状……这是一项艰巨得多的任务。

模型到底预测了什么?

让我们更仔细地看看示例模型的输出。因为它只是一个卷积神经网络,所以前向传播过程如下:

你输入一个416×416像素的RGB图像,卷积层会对图像的像素进行各种变换,输出是一个13×13×125的特征图。因为这是一个回归输出,所以最终层没有应用激活函数。输出仅仅是21,125个实数,而我们需要将这些数字转换成边界框。

描述一个边界框的坐标需要4个数字。有两种常见的方法:一种是使用xmin、ymin、xmax、ymax来描述框的边缘,另一种是使用中心点x、中心点y、宽度、高度。两种方法都可以,但我们这里使用后者(知道框的中心位置有助于将框与网格单元格匹配)。

模型为每个边界框预测的不是它们在图像中的绝对坐标,而是四个“差值”或偏移量:

delta_x, delta_y:框在网格单元格内的中心点
delta_w, delta_h:锚点框宽度和高度的缩放因子

每个检测器都相对于其锚点框进行预测。锚点框应该已经是实际目标大小的一个相当不错的近似值(这也是我们使用它们的原因),但不会完全精确。这就是为什么我们要预测一个缩放因子,以表明框比锚点框大多少或小多少,以及一个位置偏移量,以表明预测框与网格中心的偏差有多大。

为了得到边界框在像素坐标中的实际宽度和高度,我们这样做:

box_w[i, j, b] = anchor_w[b] * exp(delta_w[i, j, b]) * 32
box_h[i, j, b] = anchor_h[b] * exp(delta_h[i, j, b]) * 32

其中i和j是网格中的行和列(0–12),b是检测器索引(0–4)。

预测的框比原始图像更宽和/或更高是可以接受的,但框的宽度或高度为负数是没有意义的。这就是为什么我们要对预测的数字取指数。

如果预测的delta_w小于0,exp(delta_w)是一个介于0和1之间的数字,使框比锚点框小。如果delta_w大于0,那么exp(delta_w)是一个大于1的数字,使框更宽。如果delta_w恰好为0,那么exp(0)=1,预测的框与锚点框的宽度完全相同。

顺便说一下,我们乘以32是因为锚点坐标是在13×13的网格中,每个网格单元格覆盖416×416输入图像中的32个像素。

为了得到预测框的中心点x、y坐标(以像素为单位),我们这样做:

box_x[i, j, b] = (i + sigmoid(delta_x[i, j, b])) * 32
box_y[i, j, b] = (j + sigmoid(delta_y[i, j, b])) * 32

YOLO的一个关键特性是,它鼓励检测器只有在发现目标的中心位于检测器的网格单元格内时,才预测一个边界框。这有助于避免虚假检测,防止多个相邻的网格单元格都检测到同一个目标。

为了实现这一点,delta_x和delta_y必须限制在0到1之间,表示网格单元格内的相对位置。这就是sigmoid函数的作用。

然后我们将网格单元格的坐标i和j(均为0–12)相加,并乘以每个网格单元格的像素数(32)。现在,box_x和box_y就是预测边界框在原始416×416图像空间中的中心点。

SSD的做法稍有不同:

box_x[i, j, b] = (anchor_x[b] + delta_x[i, j, b]*anchor_w[b]) * image_w
box_y[i, j, b] = (anchor_y[b] + delta_y[i, j, b]*anchor_h[b]) * image_h

这里的预测delta值实际上是锚点框宽度或高度的倍数,并且没有使用sigmoid激活函数。这意味着,使用SSD时,目标的中心实际上可以位于网格单元格之外。

还需要注意的是,SSD中预测的坐标是相对于锚点框中心而不是网格单元格中心的。实际上,锚点框的中心会与网格单元格的中心完全对齐,但它们使用了不同的坐标系。SSD的锚点坐标范围是[0, 1],这样可以使它们与网格大小无关。(之所以这样做,是因为SSD使用了不同大小的多个网格。)

正如你所看到的,尽管YOLO和SSD在一般工作原理上相似,但在细节上却有所不同。

除了坐标之外,模型还会为边界框预测一个置信度分数。因为我们希望这个数字在0到1之间,所以我们使用标准技巧,将其通过一个sigmoid函数:

confidence[i, j, b] = sigmoid(predicted_confidence[i, j, b])

回顾一下,我们的示例模型总是预测845个边界框,不多也不少。但通常图像中只有少数几个真实目标。在训练过程中,我们鼓励每个真实目标只由一个检测器进行预测,因此只有少数预测的置信度分数会很高。那些没有发现目标的检测器的预测——占绝大多数——应该有一个非常低的置信度分数。

最后,我们还会预测类别概率。对于Pascal VOC数据集,每个边界框是一个包含20个数字的向量。像往常一样,我们应用softmax函数使其成为一个良好的概率分布:

classes[i, j, b] = softmax(predicted_classes[i, j, b])

除了softmax函数,你也可以使用sigmoid激活函数。这样就变成了一个多标签分类器,在这种情况下,每个预测的边界框实际上可以同时具有多个类别。(这就是SSD和YOLO v3所做的事情。)

需要注意的是,SSD并不使用这种置信度分数。相反,它在分类器中添加了一个特殊的类别——“背景”。如果预测了这个背景类别,就表示检测器没有发现目标。这与YOLO给出低置信度分数是相同的意思。

由于我们需要的预测数量远多于实际需要,而且大多数预测都不好,我们现在将过滤掉那些分数非常低的预测。在YOLO的情况下,我们通过将边界框的置信度分数(表示“这个框包含目标的可能性有多大”)与最大类别概率(表示“这个框包含这个类别的目标的可能性有多大”)结合起来实现这一点。

confidence_in_class[i, j, b] = classes[i, j, b].max() * confidence[i, j, b]

低置信度分数意味着模型不确定这个框是否真的包含目标;低类别概率意味着模型不确定这个框里是什么类型的目标。这两个分数都需要很高,预测才会被认真对待。

由于大多数框不会包含任何目标,我们现在可以忽略所有置信度_in_class低于某个阈值(例如0.3)的框,然后对剩余的框执行非极大值抑制,以去除重复项。我们通常最终会得到1到大约10个预测。

它是卷积的,宝贝!

拥有一个检测器网格实际上非常适合使用卷积神经网络。

13×13的网格是卷积层的输出。正如你所知,卷积是一个小窗口(或内核),它在输入图像上滑动。这个内核的权重在每个输入位置都是相同的。我们示例模型的最后一层有125个这样的内核。

为什么是125呢?这里有5个检测器,每个检测器有25个卷积核。这25个卷积核分别预测该检测器边界框的一个方面:x、y、宽度、高度、置信度分数以及20个类别概率。

这125个卷积核会在13×13的特征图的每个位置滑动,并在每个位置进行预测。然后我们将这125个数字解释为在该网格位置的5个预测边界框及其类别分数(这是损失函数的工作,稍后会详细介绍)。

最初,在每个网格位置预测的125个数字将是完全随机且毫无意义的,但随着训练的进行,损失函数会引导模型学习做出更有意义的预测。

尽管我一直在说每个网格单元格中有5个检测器,总共有845个检测器,但模型实际上总共只学习了5个检测器——而不是每个网格单元格中有5个独特的检测器。这是因为卷积层的权重在每个位置都是相同的,因此在网格单元格之间共享。

模型实际上为每个锚点学习一个检测器。它将这些检测器在图像上滑动,以获得845个预测,网格上的每个位置有5个。因此,尽管我们总共只有5个独特的检测器,但由于卷积的作用,这些检测器与它们在图像中的位置无关,因此可以检测到无论位于何处的目标。

在给定位置的输入像素与为该检测器/卷积核所学习的权重的组合,决定了该位置的最终边界框预测。

这也解释了为什么模型总是预测边界框相对于网格单元格中心的位置。由于该模型的卷积特性,它无法预测绝对坐标。由于卷积核在图像上滑动,它们的预测总是相对于特征图中的当前位置的。

YOLO与SSD的比较

上述关于单阶段目标检测器的工作原理的描述几乎适用于所有此类检测器。在输出的解释方式上可能有一些小的差异(例如,对类别概率使用sigmoid而不是softmax),但总体思路是相同的。

然而,YOLO和SSD的不同版本之间存在一些有趣的架构差异。

以下是YOLO v2和v3以及SSD的不同架构的草图:

正如你所看到的,在高层次上,YOLO v3和SSD非常相似,尽管它们通过不同的方法达到最终的网格大小(YOLO使用上采样,而SSD使用下采样)。

YOLO v2(以及我们的示例模型)只有一个13×13的输出网格,而SSD有多个不同大小的网格。MobileNet+SSD版本有6个网格,大小分别为19×19、10×10、5×5、3×3、2×2和1×1。

因此,SSD的网格从非常细到非常粗都有。它这样做是为了在更广泛的物体尺度范围内获得更准确的预测。

19×19的细网格,其网格单元格非常紧密,负责最小的物体。由最后一层产生的1×1网格负责响应占据几乎整个图像的大物体。其他层的网格则覆盖中间大小的物体。

YOLO v2试图通过其跳跃连接实现类似的效果,但似乎效果不佳。YOLO v3更像SSD,它使用3个不同尺度的网格来预测边界框。

与YOLO一样,SSD的每个网格单元格也会进行多次预测。每个网格单元格的检测器数量各不相同:在更大、更细致的特征图上,SSD每个网格单元格有3到4个检测器;在较小的网格上,每个单元格有6个检测器。(YOLO v3在每个尺度上使用3个检测器。)

坐标预测也是相对于锚点的——在SSD论文中称为“默认框”——但一个区别是,SSD预测的中心坐标可以超出其网格单元格。锚点框以单元格为中心,但SSD不对预测的x、y偏移量应用sigmoid函数。因此,理论上模型右下角的一个框可以预测一个边界框,其中心位于图像的左上角(尽管在实际中这可能不会发生)。

与YOLO不同,SSD没有置信度分数。每个预测只包含4个边界框坐标和类别概率。YOLO使用置信度分数来表示这个预测是实际物体的可能性。SSD通过设置一个特殊的“背景”类别来解决这个问题:如果类别预测是这个背景类别,那么就表示这个检测器没有找到物体。这与YOLO中的低置信度分数是相同的意思。

SSD的锚点与YOLO的锚点略有不同。由于YOLO必须从一个单一网格进行所有预测,因此它使用的锚点从小(大约一个网格单元格的大小)到大(大约整个图像的大小)都有。

SSD则更为保守。19×19网格上使用的锚点框比10×10网格上的小,10×10网格上的又比5×5网格上的小,依此类推。与YOLO不同,SSD不是使用锚点让检测器专注于物体的大小,而是使用不同的网格来实现这一点。

SSD的锚点主要用于让检测器专注于物体形状的不同可能纵横比,而不是它们的大小。正如前面提到的,SSD的锚点是通过一个简单的公式计算出来的,而YOLO的锚点是通过对训练数据运行k-means聚类找到的。

由于SSD使用3到6个锚点,并且有六个网格而不是一个,实际上它总共使用了大约32个独特的检测器(这个数字会根据你使用的具体模型架构略有变化)。

因为SSD有更多的网格和检测器,所以它也会输出更多的预测。YOLO产生845个预测,而MobileNet-SSD产生1917个预测。一个更大的版本,SSD512,甚至输出24,564个预测!优点是你更有可能找到图像中的所有物体。缺点是你最终需要进行更多的后处理,以确定你想保留哪些预测。

由于这些差异,SSD和YOLO在将真实边界框与检测器匹配的方式上略有不同。损失函数也略有不同。我们将在训练部分详细介绍。

好的,关于这些模型如何进行预测的理论就讲到这里。接下来,我们来看看训练目标检测模型需要什么样的数据。

数据

有几个流行的数据集用于训练目标检测模型,比如Pascal VOC、COCO、KITTI等。我们来看看Pascal VOC,因为它是一个重要的基准,也是YOLO论文中使用的数据集。

VOC数据集包含图像以及针对不同任务的注释。我们只对目标检测任务感兴趣,所以我们只看带有目标注释的图像。这里有20个目标类别:

aeroplane    bicycle  bird   boat       bottle
bus          car      cat    chair      cow
diningtable  dog      horse  motorbike  person
pottedplant  sheep    sofa   train      tvmonitor

VOC数据集自带了一个建议的训练/验证分割,大约是50/50。由于该数据集本身并不大,因此仅用50%的数据进行验证似乎有些浪费。因此,通常的做法是将训练集和验证集合并成一个大的训练集,“trainval”(总共16,551张图像),并随机挑选其中大约10%的图像用于验证。

由于2007年的测试集的答案是公开的,因此可以在该测试集上测试你的模型。还有一个2012年的测试集,但其答案是保密的。(对于2012年测试集的提交,通常也会将2007年测试集包含在训练数据中。数据越多越好。)

合并后的2007年和2012年训练集包含8218张带有目标注释的图像,验证集有8333张图像,2007年测试集有4952张图像。这比ImageNet的130万张图像要少得多,因此使用某种迁移学习而不是从头开始训练模型是个好主意。这就是为什么我们从一个在ImageNet上预训练过的特征提取器开始。

注释

注释描述了图像中包含的内容。换句话说,注释提供了我们训练所需的“目标”。

注释以XML格式提供,每个训练图像对应一个。注释文件包含一个或多个<object>

如果一个目标被标记为“困难”,我们将忽略它。这些通常是体积非常小的目标。这些目标也会被VOC挑战赛的官方评估指标所忽略。

以下是一个示例注释文件,VOC2007/Annotations/003585.xml:

<annotation>
    <folder>VOC2007</folder>
    <filename>003585.jpg</filename>
    <source>
        <database>The VOC2007 Database</database>
        <annotation>PASCAL VOC2007</annotation>
        <image>flickr</image>
        <flickrid>304100796</flickrid>
    </source>
    <owner>
        <flickrid>Huw Lambert</flickrid>
        <name>huw lambert</name>
    </owner>
    <size>
        <width>333</width>
        <height>500</height>
        <depth>3</depth>
    </size>
    <object>
        <name>person</name>
        <truncated>0</truncated>
        <difficult>0</difficult>
        <bndbox>
            <xmin>138</xmin>
            <ymin>183</ymin>
            <xmax>259</xmax>
            <ymax>411</ymax>
        </bndbox>
    </object>
    <object>
        <name>motorbike</name>
        <truncated>0</truncated>
        <difficult>0</difficult>
        <bndbox>
            <xmin>89</xmin>
            <ymin>244</ymin>
            <xmax>291</xmax>
            <ymax>425</ymax>
        </bndbox>
    </object>
</annotation>

这张图像是333×500像素,包含两个目标:一个人和一辆摩托车。这两个目标既不被认为是“困难”的,也不是被截断的(部分在图像之外)。像许多深度学习图像一样,原始照片来自Flickr。

如果我们绘制这张训练图像及其边界框,它看起来像这样:

对于2007年和2012年合并的数据集,我们有以下统计数据:

dataset   images    objects
------------------------------
train     8218      19910
val       8333      20148
test      4952      12032     (2007 only)

大约一半的图像只有一个目标,其他图像包含两个或更多目标。你可以在直方图中清楚地看到这一点(这是针对训练集的):

一个图像中目标的最大数量是39个。验证集和测试集的直方图也很相似。

为了好玩,这里是一个直方图,显示了训练集中所有边界框的面积,在将宽度和高度归一化到范围[0,1]之后:

如你所见,许多目标相对较小。在1.0处的峰值是因为有不少目标比图像还大(例如,只有部分可见的人),因此边界框填满了整个图像。

这里还有另一种查看这些数据的方法,绘制真实边界框宽度与高度的关系图。图中的“斜率”显示了边界框的纵横比。

我发现制作这类图表很有用,因为它们能让你对数据有个大致的了解。

数据增强

由于数据集相对较小,在训练时通常会使用大量的数据增强,例如随机翻转、随机裁剪、颜色变化等。

重要的是要记住,你对图像所做的任何操作也必须对边界框进行!因此,如果你翻转训练图像,你也必须翻转真实边界框的坐标。

YOLO在加载训练图像时会执行以下操作:

  • 加载图像而不进行缩放。

  • 通过随机增加或减少原始尺寸的20%来选择新的宽度和高度。

  • 裁剪图像的该部分,如果新图像在一边或多边比原始图像大,则使用零填充。

  • 将图像缩放到416×416,使其成为正方形。

  • 随机水平翻转图像(概率为50%)。

  • 随机调整图像的色调、饱和度和曝光度(亮度)。

  • 同时调整边界框坐标,通过平移和缩放它们以适应前面所做的裁剪和缩放,以及水平翻转。

旋转也是一种常见的数据增强技术,但这样做会变得复杂,因为我们还需要旋转边界框。因此,通常不会这样做。

SSD论文还建议以下增强方法:

  • 随机选择图像区域,使得与图像中目标的最小IoU为0.1、0.3、0.5、0.7或0.9。IoU越小,模型检测目标的难度越大。

  • 使用“放大”增强,实际上使图像变小,从而创建更多包含小目标的训练样本。这对于训练模型更好地检测小目标很有用。

  • 进行随机裁剪可能会导致目标部分(或完全)位于裁剪后的图像之外。因此,我们只想保留中心位于裁剪区域内的真实边界框,而不是那些中心现在位于可见图像之外的边界框。

注意纵横比!

我们在正方形网格(13×13)上进行预测,输入图像也是正方形的(416×416)。但训练图像通常不是正方形的,我们用于推理的测试图像也不是正方形的。这些图像甚至没有相同的尺寸。

以下是一个可视化图,显示了VOC训练集中所有图像的纵横比:

红色框的纵横比是宽大于高;青色框则是高大于宽。虽然有很多奇怪的纵横比,但最常见的是一些特定的值,例如1.333(4:3)、1.5(3:2)和0.75(3:4)。

正如你所看到的,有些图像非常宽。这里是一个极端的例子:

神经网络处理的是416×416像素的正方形图像,因此我们需要将训练图像适配到这个正方形中。我们有几种方法可以做到这一点:

  • 非均匀地调整到416×416像素,这会压缩图像。

  • 将最短边调整到416像素,然后从图像中裁剪出一个416×416像素的部分。

  • 将最长边调整到416像素,用零填充较短的一边,然后进行裁剪。

所有这些方法都是有效的,但每种方法都有其副作用。

当压缩图像时,我们会将其纵横比改为1:1。如果原始图像是宽高比大于1的图像,那么所有物体都会变得比平时更窄。如果原始图像是高宽比大于1的图像,那么所有物体都会变得比平时更扁。

通过裁剪来保持纵横比是完整的,但我们可能会裁掉图像的重要部分,使得模型更难识别物体的真实身份。模型现在可能需要预测一个部分在图像之外的边界框。(选项#3可能会使物体变得太小而无法被检测到,特别是如果纵横比非常极端的话。)

为什么这很重要?

在训练之前,我们会将边界框的xmin和xmax除以图像宽度,ymin和ymax除以图像高度,以归一化坐标,使它们介于0和1之间。这是为了使训练独立于每张图像的实际像素尺寸。

但输入图像通常不是正方形的,因此x坐标除以的数字与y坐标不同。这些除数可能因每张图像的尺寸和纵横比而异。这也影响了我们如何处理边界框坐标和锚点。

压缩(选项#1)是最简单的选择,尽管它会暂时破坏图像的纵横比。如果所有图像的纵横比相似(VOC数据集中并非如此),或者纵横比不是太极端,那么神经网络仍然可以正常工作。卷积网络似乎对物体的“厚度”变化相当鲁棒。

通过裁剪(选项#2和#3),我们在归一化边界框坐标时需要考虑纵横比。现在,边界框可能会变得比输入图像更大,因为我们不是在查看整个图像,而只是查看裁剪后的一部分。由于物体可能会部分位于图像之外,边界框也可能会这样。

裁剪的缺点是我们可能会丢失图像的重要部分——这可能比稍微压缩一下物体更糟糕。

你选择压缩还是裁剪,也会影响你如何从数据集中计算锚点。使用锚点的全部意义在于,这些锚点就像数据集中最常见的物体形状。

在裁剪时,这个观点仍然成立。一些锚点可能会部分位于图像之外,但至少它们的纵横比真正代表了训练数据中的物体。

在压缩时,计算出的锚点并不能真正代表真实的边界框,因为不同的纵横比被忽略了,因为每张训练图像都被以稍微不同的方式压缩。现在,锚点更像是许多不同尺寸的图像的平均值,这些图像都被以不同的方式扭曲了。

数据增强在这里也有影响。通过对图像进行随机尺寸的裁剪,然后调整到416×416像素,这些裁剪也会破坏纵横比(可能是故意的)。

长话短说:我们将坚持压缩图像并忽略边界框的纵横比,因为这是最简单的。YOLO和SSD也是这样做的。

另一种看待这个问题的方式是,与其试图让模型尊重纵横比,我们实际上是在试图让模型对纵横比不变。(如果你知道你将始终处理固定尺寸的输入图像,比如1280×720,那么使用裁剪可能更有意义。)

如何训练这个模型?

我们几乎已经掌握了训练这种目标检测模型所需的所有要素。

模型使用一个非常直接的卷积神经网络进行预测。我们将这些预测的数字转换为边界框。数据集包含了真实边界框,说明了训练图像中实际存在的目标。因此,为了训练这种模型,我们需要设计一个损失函数,将预测的边界框与真实边界框进行比较。

问题是,真实边界框的数量在不同图像之间可能会有所不同,从零到几十个不等。这些边界框可能遍布图像的各个位置,顺序也各不相同。有些边界框可能会重叠。在训练过程中,我们必须将每个检测器与这些真实边界框中的一个进行匹配,以便我们可以计算每个预测边界框的回归损失。

如果我们天真地进行这种匹配,例如,总是将第一个真实目标分配给第一个检测器,第二个目标分配给第二个检测器,依此类推,或者随机将真实目标分配给检测器,那么每个检测器都将被训练成预测各种各样的目标:有些可能是大的,有些可能是小的,有些可能在图像的一个角落,有些可能在图像的另一个角落,有些可能在中心,等等。

这就是我在这篇博客文章开头提到的问题,也是为什么仅仅在模型中增加一堆回归输出是行不通的。解决方案是使用一个具有固定数量检测器的网格,每个检测器只负责检测位于该部分图像中的目标,并且只负责一定大小的目标。

现在,损失函数需要知道哪个真实目标属于哪个网格单元格中的哪个检测器,同样地,也需要知道哪些检测器没有与真实目标相关联。这就是我们所说的“匹配”。

将真实边界框与检测器进行匹配

这种匹配是如何进行的呢?有不同的策略。YOLO的处理方式是,让图像中的一个特定目标只由一个检测器负责检测。

首先,我们找到边界框中心所在的网格单元格。这个网格单元格将负责这个目标。如果其他网格单元格也预测了这个目标,那么损失函数会对它们进行惩罚。

VOC注释将边界框坐标表示为xmin、ymin、xmax、ymax。由于模型使用了网格,我们根据真实边界框的中心来决定使用哪个网格单元格,因此将边界框坐标转换为中心点x、中心点y、宽度和高度是有意义的。

在这个阶段,我们还希望将边界框坐标归一化到范围[0, 1],使它们独立于输入图像的尺寸(因为不是所有的训练图像都有相同的尺寸)。

这也是我们应用数据增强的地方,比如随机水平翻转和颜色变化(同时应用于图像及其边界框)。

仅仅选择网格单元格是不够的。每个网格单元格有多个检测器,而我们只希望其中一个检测器找到这个目标,因此我们选择锚点框与目标的真实边界框最匹配的那个检测器。这是通过常用的IoU指标来完成的。

这样一来,最小的目标被分配给检测器1(它有最小的锚点框),非常大的目标成为检测器5的责任(它有最大的锚点框),依此类推。

只有该单元格中的那个特定检测器才应该预测这个目标。这个规则有助于不同的检测器专注于那些形状和大小与锚点框相似的目标。(记住,目标并不需要与锚点框完全一样大,因为模型会预测相对于锚点框的位置偏移和尺寸偏移。锚点框只是一个提示。)

因此,对于给定的训练图像,有些检测器会与目标相关联,而其他的检测器则不会。如果一张训练图像中有3个独特的目标,因此有3个真实边界框,那么只有845个检测器中的3个应该进行预测,其他的842个检测器应该预测“没有目标”(在我们的模型输出中,这是一个置信度分数非常低的边界框,理想情况下为0%)。

从现在开始,我会用“正样本”来指代那些有真实边界框的检测器,用“负样本”来指代那些没有目标与之关联的检测器。负样本有时也被称为“无目标”或背景。

由于模型的输出是一个13×13×125的张量,因此损失函数将使用的靶张量也将是13×13×125。再次说明,这个数字125来源于:每个检测器预测20个类别概率值 + 4个边界框坐标 + 1个置信度分数。

在靶张量中,我们只填充那些负责目标的检测器的边界框(以及独热编码的类别向量)。我们将预期的置信度分数设置为1(因为我们100%确定这是一个真实的目标)。

对于所有其他检测器——那些负样本——靶张量中包含的都是零。边界框坐标和类别向量在这里并不重要,因为它们将被损失函数忽略,而置信度分数为0,因为我们100%确定这里没有目标。

因此,当训练循环请求一批新的图像及其靶标时,它得到的是一个B×416×416×3的图像张量和一个B×13×13×125的数字张量,这些数字代表我们期望每个检测器预测的真实边界框。由于大多数检测器不会负责预测目标,因此这个靶张量中的大多数数字将是0。

在匹配过程中还有一些额外的细节需要考虑。例如,当有多个真实边界框的中心恰好落在同一个单元格中时会发生什么?在实践中,这可能不是一个大问题,特别是如果网格足够细的话,但我们仍然需要一种方法来处理这种情况。

理论上,如果每个边界框根据最佳IoU重叠度偏好不同的检测器——例如,边界框A与检测器2的IoU最大,边界框B与检测器4的重叠度最大——那么我们可以将这两个真实边界框分别与该单元格中的不同检测器匹配。然而,这并不能避免两个真实边界框都想要同一个检测器的问题。

YOLO通过首先随机打乱真实边界框,然后选择第一个与单元格匹配的边界框来解决这个问题。因此,如果一个新的真实边界框与一个已经负责另一个目标的单元格匹配,那么我们只需忽略这个新的边界框。下一轮训练时运气可能会更好!

这意味着在YOLO中,每个单元格最多只有一个检测器被赋予一个目标——该单元格中的其他检测器不被期望检测到任何东西(如果它们检测到了,就会受到惩罚)。

需要注意的是,还有其他可能的匹配策略。例如,SSD可以将同一个真实边界框与多个检测器匹配:它首先选择IoU最大的检测器,然后还会选择任何(未分配的)锚点框与该真实边界框的IoU超过0.5的检测器。(我假设这些检测器不必都在同一个单元格中,甚至不必在同一个网格中,但论文中没有明确说明。)

这被认为可以使模型更容易学习,因为它不必在哪个检测器应该预测目标之间做出选择——多个检测器现在都有机会预测这个目标。

损失函数

正如以往一样,损失函数才是真正告诉模型应该学习什么的部分。对于目标检测,我们想要一个损失函数,它鼓励模型预测正确的边界框以及这些框的正确类别。另一方面,模型不应该预测不存在的目标。

这是一个具有多个组成部分的复杂任务。因此,我们的损失由几个不同的项组成(这是一个多任务损失)。其中一些项是回归任务,因为它们预测实数值;其他一些则是分类任务。

对于任何给定的检测器,存在两种可能的情况:

  • 这个检测器没有与之关联的真实边界框。这是一个负样本;它不被期望检测到任何目标(即它应该预测一个置信度分数为0的边界框)。

  • 这个检测器有一个真实边界框。这是一个正样本。该检测器负责检测真实边界框中的目标。

对于那些不被期望检测目标的检测器,当它们预测的边界框的置信度分数大于0时,我们将对它们进行惩罚。

这样的检测被认为是假阳性,因为在图像的这个位置并没有目标存在。过多的假阳性会降低模型的精确度。

反之,如果一个检测器有一个真实边界框,我们希望在以下情况下对其进行惩罚:

  • 坐标错误

  • 置信度分数过低

  • 类别错误

理想情况下,检测器预测的边界框与真实边界框完全重叠,具有相同的类别标签,并且具有较高的置信度分数。

如果置信度分数过低,预测则被视为假阴性。模型没有找到实际存在的目标。

然而,如果置信度分数良好,但坐标错误或类别错误,预测将被计为假阳性。尽管模型声称找到了一个目标,但它可能是错误的目标,或者位置不正确。

这意味着同一个预测可能既被视为假阴性(降低模型的召回率),又视为假阳性(降低模型的精确度)。这可不太好。只有当坐标、置信度和类别这三个方面都正确时,预测才被视为真阳性。

由于可能出现如此多的问题,损失函数由几个部分组成,每个部分都衡量模型预测的不同类型的“错误”。将这些部分相加,得到整体的损失度量。

SSD、YOLO、SqueezeDet、DetectNet以及其他单阶段检测器变体都使用略有不同的损失函数。然而,它们通常由相同的元素组成。让我们来看看这些不同的部分!

没有真实边界框的检测器(负样本)

这部分损失函数只涉及置信度分数——因为这里没有真实边界框,我们没有坐标或类别标签可供比较预测结果。

这个损失项仅针对那些不负责检测目标的检测器进行计算。如果这样的检测器检测到了目标,那么它将在这里受到惩罚。

回想一下,置信度分数表示检测器是否认为在其网格单元格的中心有一个目标。对于这样的检测器,靶张量中的真实置信度分数被设置为0,因为这里没有目标。预测的分数也应该是0——或者接近0。

置信度分数的损失需要以某种方式将预测分数与真实分数进行比较。在YOLO中,它看起来像这样:

no_object_loss[i, j, b] = no_object_scale * (0 - sigmoid(pred_conf[i, j, b]))**2

其中pred_conf[i, j, b]是单元格i、j中的检测器b预测的置信度。注意,我们使用sigmoid()将预测的置信度限制在0到1之间(使其成为逻辑回归)。

为了计算这个损失项,我们只需取真实值与预测值之间的差值,然后将其平方(即平方和误差或SSE)。

no_object_scale是一个超参数。它的典型值为0.5,这样这个损失项就不会像其他部分那样重要。由于图像中通常只有少数几个真实边界框,因此845个检测器中的大多数只会受到这个“无目标”损失的惩罚,而不是我下面将要介绍的其他损失项。

因为我们不希望模型只学习“无目标”,所以这部分损失不应该比那些有目标的检测器的损失更重要。

上述公式是针对单个单元格中的单个检测器的。为了得到总的“无目标”损失,我们将所有单元格i、j和所有检测器b的no_object_loss相加。对于那些负责检测目标的检测器,no_object_loss始终为0。在SqueezeDet中,总的“无目标”损失还会除以“无目标”检测器的数量以得到平均值,但在YOLO中我们没有这样做。

实际上,YOLO还有一招。如果一个检测器与图像中任何一个真实边界框的最佳IoU大于(比如说)60%,那么no_object_loss[i, j, b]就被设置为0。

换句话说,如果一个检测器本不应该预测目标,但它实际上做出了一个非常好的预测,那么最好原谅它——甚至鼓励它继续预测目标。(事后看来,我们真的应该让这个检测器对那个目标负责。)

我不确定这个技巧是否真的对最终结果有影响,它看起来有点像黑客行为。但话说回来,如果没有这些小技巧,机器学习会是什么样子呢?

SSD没有这个“无目标”损失项。相反,它在可能的类别中增加了一个特殊的“背景”类别。如果预测了这个类别,那么该检测器的输出就被视为“无目标”。

有真实边界框的检测器(正样本)

上一部分描述了那些不负责寻找目标的检测器的情况。它们唯一可能出错的地方就是在没有目标的地方发现了一个目标。现在,让我们来看看剩下的检测器:那些应该找到目标的检测器。

当这些检测器没有找到它们的目标,或者对目标进行了错误分类时,它们就可能出错。有三个独立的损失项来处理这种情况。我们只在网格单元格i、j中的检测器b有一个真实边界框时(即靶张量中的置信度分数被设置为1时)计算这些损失项。

置信度分数

置信度分数的损失项如下:

object_loss[i, j, b] = object_scale * (1 - sigmoid(pred_conf[i, j, b]))**2

这与no_object_loss非常相似,只是这里的真实值是1,因为我们100%确定这里有一个目标。

实际上,YOLO在这里做了一些更有趣的事情:

object_loss[i, j, b] = object_scale * 
         (IOU(truth_coords, pred_coords) - sigmoid(pred_conf[i, j, b]))**2

预测的置信度分数pred_conf[i, j, b]应该表示预测边界框与真实边界框之间的IoU。理想情况下,这个值是1或100%,表示完美匹配。但YOLO并没有将预测分数与这个理想值进行比较,而是查看两个边界框之间的实际IoU。

这很有道理:如果IoU较小,那么置信度也应该较小;如果IoU较大,那么置信度也应该较大。

与“无目标”损失不同,我们并不希望预测的置信度始终为0。在这里,我们也不希望模型总是预测100%的置信度。相反,模型应该学会估计其边界框坐标的质量如何。而这正是IoU告诉你的。

正如前面提到的,SSD不预测置信度分数,因此也没有这个损失项。

类别概率

每个检测器还会预测目标的类别。这与边界框坐标无关。本质上,我们训练了5个独立的分类器,它们都学会了识别不同大小的目标。

YOLO v1和v2对预测类别概率使用了以下损失项:

class_loss[i, j, b] = class_scale * (true_class - softmax(pred_class))**2

这里,true_class是一个独热编码的20维向量(针对Pascal VOC),而pred_class是预测的logits向量。需要注意的是,尽管我们对预测结果应用了softmax,但这个损失项并没有使用交叉熵。(我认为他们可能使用平方和误差是因为这样更容易平衡这个损失项与其他损失项。实际上,softmax也是可选的。)

YOLO v3和SSD采用了不同的方法。它们不将这视为一个多类分类问题,而是视为一个多标签问题。因此,它们不使用softmax(它总是选择一个标签作为胜者),而是使用逻辑sigmoid,允许选择多个标签。它们使用标准的二元交叉熵来计算这个损失项。

由于SSD不预测置信度分数,它有一个特殊的“背景”类别来实现这个功能。如果检测器预测了背景,那就意味着检测器没有找到目标(我们只需忽略这些预测)。顺便说一下,SSD论文将这一部分的损失项称为置信度损失,而不是分类损失(有点令人困惑)。

边界框坐标

最后,边界框坐标的损失项也被称为定位损失。这是边界框的四个数字之间的简单回归损失。

coord_loss[i, j, b] = coord_scale * ((true_x[i, j, b] - pred_x[i, j, b])**2
                                   + (true_y[i, j, b] - pred_y[i, j, b])**2
                                   + (true_w[i, j, b] - pred_w[i, j, b])**2
                                   + (true_h[i, j, b] - pred_h[i, j, b])**2)

缩放因子coord_scale用于使边界框坐标预测的损失比其他损失项更重要。这个超参数的典型值是5。

这个损失项本身很简单,但重要的是要明白上面方程中的true_*pred_*值是什么。回想一下在“模型到底预测了什么?”这一节中,我给出了以下代码来计算实际的边界框坐标:

box_x[i, j, b] = (i + sigmoid(pred_x[i, j, b])) * 32
box_y[i, j, b] = (j + sigmoid(pred_y[i, j, b])) * 32
box_w[i, j, b] = anchor_w[b] * exp(pred_w[i, j, b]) * 32
box_h[i, j, b] = anchor_h[b] * exp(pred_h[i, j, b]) * 32

我们需要对模型的预测进行一些后处理,以得到有效的坐标。预测的x和y值应用了sigmoid函数。预测的宽度和高度实际上是缩放因子,需要先进行指数运算,然后乘以锚点框的宽度和高度。

由于模型并不直接预测有效的坐标,因此损失函数中使用的真实值也不应该是真实的坐标。在将真实边界框用于损失函数之前,我们需要对其进行转换:

true_x[i, j, b] = ground_truth.center_x - grid[i, j].center_x
true_y[i, j, b] = ground_truth.center_y - grid[i, j].center_y
true_w[i, j, b] = log(ground_truth.width / anchor_w[b])
true_h[i, j, b] = log(ground_truth.height / anchor_h[b])

现在,true_xtrue_y相对于网格单元格,而true_wtrue_h是锚点框尺寸的适当缩放因子。在填充靶张量时应用这种逆变换非常重要,否则损失函数将比较苹果和橘子。

SSD再次使用了一个略有不同的损失项。它的定位损失被称为“Smooth L1”损失。与简单地取平方差不同,这个损失做了一些更复杂的事情:

difference = abs(true_x[i, j, b] - pred_x[i, j, b])
if difference < 1:
    coord_loss_x[i, j, b] = 0.5 * difference**2
else:
    coord_loss_x[i, j, b] = difference - 0.5

其他坐标也是如此。这种损失被认为对异常值不太敏感。

准备训练!

至此,训练模型的所有要素都已齐备。你有:

  • 数据集(例如Pascal VOC),它提供了图像和注释(真实边界框)。

  • 一个在网格中组织了许多目标检测器的模型。

  • 一种将真实边界框放入这个网格以创建靶张量的匹配策略。

  • 以及一个将预测与靶标进行比较的损失函数。

你现在需要做的就是让SGD放手去做!

顺便说一下,根据我刚刚描述的损失函数,你可能觉得需要使用几个嵌套循环来计算损失,因为正样本的检测器使用不同的损失项,而负样本的检测器则使用其他损失项。

也许像这样(伪代码):

for i in 0 to 12:
  for j in 0 to 12:
    for b in 0 to 4:
      gt = target[i, j, b]   # 真实值
      pred = grid[i, j, b]   # 模型的预测

      # 这个检测器是否负责一个目标?
      if gt.conf == 1:
        iou = IOU(gt.coords, pred.coords)
        object_loss[i, j, b] = (iou - sigmoid(pred.conf[i, j, b]))**2
        coord_loss[i, j, b] = sum((gt.coords - pred.coords)**2)
        class_loss[i, j, b] = cross_entropy(gt.class, pred.class)
      else:
        no_object_loss[i, j, b] = (0 - sigmoid(pred.conf[i, j, b]))**2

然后总损失为:

loss = no_object_scale * sum(no_object_loss) + 
          object_scale * sum(object_loss) + 
           coord_scale * sum(coord_loss) + 
           class_scale * sum(class_loss)

但实际上,你可以将这些循环向量化,以便整个损失函数可以在GPU上运行,如下所示:

# 对于有目标的检测器,掩码为1,否则为0
mask = (target.conf == 1)

# 计算每个检测器预测框与靶张量中对应真实框之间的IoU
ious = IOU(target.coords, grid.coords)

# 一次性计算整个网格的损失项:
object_loss = sum(mask * (ious - sigmoid(grid.conf))**2)
coord_loss = sum(mask * (target.coords - grid.coords)**2)
class_loss = sum(mask * (target.class - softmax(grid.class))**2)
no_object_loss = sum((1 - mask) * (0 - sigmoid(grid.conf))**2)

现在,我们总是为所有检测器计算所有损失项,但我们使用掩码来丢弃那些我们不想计算的值。

尽管我们损失函数中的操作比图像分类复杂得多,但一旦你理解了各个部分的作用,其实也并没有那么糟糕。而且,由于YOLO、SSD以及其他单阶段模型都使用了这些损失项的略有不同的变体,因此在选择它们时似乎有很大的灵活性。

还有一些用于训练这些网络的技巧值得一提:

  • 多尺度训练:在实际应用中,目标检测器将用于处理(许多)不同尺寸的图像,这些图像中又包含不同尺寸的目标。为了让网络能够很好地处理各种输入尺寸,一种方法是每10个批次随机选择一个新的输入尺寸。与其总是用416×416的图像进行训练,不如随机选择320×320到608×608之间的图像进行训练。

  • 热身训练:YOLO在训练的早期阶段在每个单元格的中心添加一个假的真实边界框,并用它来计算额外的坐标损失。这样做的目的是鼓励预测开始与检测器的锚点匹配。

  • 难负样本挖掘:我已经多次指出,大多数检测器不会负责检测任何目标。这意味着负样本的数量远远多于正样本。YOLO通过使用超参数(no_object_scale)来处理这个问题,而SSD则使用难负样本挖掘。它不是在损失中使用所有的负样本,而是只使用那些最错误的(置信度最高的假阳性)。

尽管模型在训练完成后可能表现良好,但有时你需要这些技巧来启动模型的学习过程。

模型效果如何?

要了解分类模型的表现如何,你可以简单地统计测试集上的正确预测数量,然后除以总测试图像数量,得到分类准确率。

对于目标检测,我们可以计算几个分数:

  • 每个检测到的目标的分类准确率

  • 预测目标与真实目标的重叠程度(IoU)

  • 模型是否真的找到了图像中的所有目标(称为“召回率”)

但仅靠这些是不够的。

例如,IoU允许我们将预测视为正确(真阳性或TP),如果它与真实边界框的重叠超过50%,否则视为不正确(假阳性或FP)。

但这还不足以了解模型的表现如何,因为它没有告诉你模型何时错过了目标——如果存在模型没有做出任何预测的真实边界框(假阴性或FN)。

为了将所有这些不同的方面综合成一个数字,我们通常使用平均精度均值(mean Average Precision,mAP)。mAP越高,模型就越好。目前在Pascal VOC 2012测试集上得分最高的模型的mAP为77.5%(2018年5月14日的refine_denseSSD)。相比之下,YOLO v2的得分为48.8%,SSD的得分为64%(这可能是SSD的大版本,而不是快速移动版本)。

根据你使用的数据集,计算mAP的方法有几种。由于我一直都在谈论Pascal VOC,让我们来研究一下他们的方法。

计算mAP

对于Pascal VOC的mAP,我们首先分别计算20个类别的平均精度(Average Precision,AP),然后取这20个数字的平均值以获得最终的mAP分数。因此,mAP分数是一个平均值的平均值。

在机器学习中,“精度”(precision)是一个非常具体的术语,它是指真正例(True Positives,TP)的数量除以检测总数(即TP + 假正例(False Positives,FP)):

precision = TP / (TP + FP)

在我们的情况下,假正例是指在图像中实际不存在但被检测到的目标。当预测的边界框与图像中的任何真实边界框相差太大,或者预测的类别不同时,就会出现这种情况。

通常与精度一起使用的另一个指标是召回率(recall),也称为真正例率或敏感性:

recall = TP / (TP + FN)

这两个公式唯一的区别在于,精度在分母中使用假正例的数量,而召回率使用假负例(False Negatives,FN)的数量。当对目标没有做出预测,或者置信度分数过低时(实际上这意味着“没有预测”),就会出现假负例。

非正式地说,精度衡量的是:在你预测的所有“猫”中,有多少真的是猫?(在这里,FP是指有多少预测的猫实际上并不是猫——或者根本不是目标。)

召回率衡量的是你在图像中实际找到的猫有多少(FN是你错过了多少猫)。

例如,如果你预测图像中有三只猫,但其中一只实际上是狗,另一只根本不是目标,那么猫类别的精度是1/3 = 0.33。(三次预测中只有一次是正确的。)

如果图像中实际上有四只猫,那么猫的召回率是1/4 = 0.25,因为你只找到了其中一只。如果图像中只有一只狗,由于你错误地认为它是猫,所以狗的精度和召回率都是0。

以下是计算TP和FP数量的方法,以伪代码表示:

# 按置信度分数(从高到低)对预测进行排序
sort the predictions by confidence score (high to low)

for each prediction:
    # 获取与预测类别相同且未标记为“困难”的注释
    true_boxes = get the annotations with same class as the prediction
                     and that are not marked as "difficult"

    # 计算真实边界框与预测之间的IoU
    find IOUs between true_boxes and prediction
    # 选择IoU最大的真实边界框
    choose ground-truth box with biggest IOU overlap

    # 如果最大的IoU大于阈值(对于Pascal VOC是0.5):
    if biggest IOU > threshold (which is 0.5 for Pascal VOC):
        # 如果我们还没有为这个真实边界框做出检测:
        if we do not already have a detection for this ground-truth box:
            TP += 1
        else:
            FP += 1
    else:
        FP += 1

如果预测与真实目标的类别相同,并且它们的边界框重叠超过50%,则预测被视为真阳性。如果重叠小于50%,则预测被视为假阳性。

根据Pascal VOC的规则,如果两个或多个预测与同一个真实边界框的IoU大于50%,我们必须选择其中一个作为正确的预测。其他检测将被计为假阳性。这样做的目的是鼓励模型为每个目标只预测一个边界框。(我们选择置信度分数最高的预测作为正确的预测,而不一定是IoU最大的那个。)

由于对同一个目标的多个预测会被惩罚,因此最好先应用非极大值抑制(Non-Maximum Suppression,NMS)以尽可能减少重叠的预测。

同时,最好丢弃置信度分数非常低的预测(例如,分数低于0.3的)。否则,这些预测会被计为假阳性。像YOLO这样的模型总是产生845个预测,或者像SSD这样的模型产生1917个预测,而实际目标的数量总是比预测少得多(大多数图像只包含1到3个真实目标)。

我们还没有计算假负例,但实际上我们也不需要。召回率的分母是TP + FN,这实际上与图像中特定类别的真实边界框数量相同。

现在,我们已经具备了计算精度和召回率所需的所有条件。然而,单个精度和召回率分数并不能真正说明模型的性能,因此我们将计算一系列精度和召回率对,并将结果绘制为一条曲线,称为……等等……精度-召回率曲线(precision-recall curve)。

我们将为20个类别中的每一个绘制这样一条曲线。一个类别的平均精度(AP)就是其曲线下的面积。

精度-召回率曲线

这是我的一个实验模型的“狗”类别的精度-召回率曲线:

在x轴上是召回率,从0(未找到任何真实目标)到1(找到所有目标)。在y轴上是精度。请注意,精度是作为召回率的函数给出的,这就是为什么曲线下的面积是该类别精度的平均值。这就是“平均精度均值”(mAP)这个指标名称的由来:我们想知道在不同的召回率值下,精度是多少。

请注意,上面的精度-召回率曲线并不是很好。曲线下的面积——“狗”类别的平均精度——仅为0.42。嘿,正如我所说,这是来自一个实验模型的。(微笑)

如何解释这条曲线?

精度-召回率曲线总是通过在不同的阈值下计算精度和召回率分数来创建的。对于二元分类器来说,这个阈值是我们认为预测为正的阈值。在目标检测模型的情况下,我们改变的阈值是预测框的置信度分数。

首先,我们计算第一个预测(最高阈值)的精度和召回率,然后计算第一个和第二个预测(稍低的阈值)的精度和召回率,接着计算前三个预测(更低的阈值)的精度和召回率,依此类推,直到我们到达列表的末尾,计算所有预测的精度和召回率(最低阈值)。

这些精度/召回率值是曲线上的一个点(一个操作点),以召回率作为x轴,精度作为y轴。

在高阈值下,召回率会很低,因为只包括了少数预测,因此会有许多假阴性。你可以从曲线的左侧看到这一点:精度在100%时很高,但那是因为我们只包括了我们非常确定的预测。但这里的召回率非常低,因为我们遗漏了许多目标。

随着阈值的降低,我们在结果中包括了更多的目标,召回率增加。精度可能会上升或下降,尽管它往往会降低,因为现在发现了更多的假阳性。

在最低阈值下,召回率达到最大值,因为我们现在正在查看模型做出的所有预测。对于我的实验模型,狗的召回率最高为42%,因此即使在这个最低阈值下,它平均也只能找到每10只狗中的4只。这里还有改进的空间!

我希望这能清楚地说明,模型预测的假阳性和假阴性之间总是存在权衡。我们使用精度-召回率曲线的原因是衡量这种权衡,并为置信度分数找到一个合适的阈值。

选择高阈值意味着我们保留的预测更少,因此我们会有更少的假阳性(我们犯的错误更少),但同时我们也会有更多假阴性(我们错过更多目标)。

阈值越低,我们包含的预测就越多,但它们通常质量较低。

理想情况下,对于每一个召回率,精度都很高。取所有可能召回率值的精度平均值,可以为我们提供一个关于模型在检测这一特定类别目标方面表现如何的有用总结。

一旦我们得到了所有不同阈值下的精度和召回率,我们就可以通过计算这条曲线下的面积来得到平均精度(AP)。实际上,在Pascal VOC中,2007年测试集使用了一种近似方法,而2012年版本则更为精确(使用积分),通常会给出稍低的分数。

最终的mAP分数仅仅是20个类别的平均精度的平均值。

自然地,分数越高越好。但这并不意味着mAP是唯一重要的指标。YOLO的得分低于其一些竞争对手,但它也更快。特别是对于移动设备的使用,我们希望使用速度和准确性之间有良好权衡的模型。

在iOS上快速进行目标检测

我最近为iOS和macOS编写了一个库,其中包含了基于Metal的MobileNet V1和V2的快速实现,并支持SSD和SSDLite。

我发现SSD——特别是SSDLite——在移动设备上比YOLO v2快得多,并且具有类似的准确性。你可能会认为YOLO会更快,因为它只有一个网格,而SSD使用多个网格,但不幸的是YOLO的特征提取器并没有针对移动设备进行优化。

如果你正在寻找一个快速的目标检测器来添加到你的应用程序中,MobileNetV2 + SSDLite是一个很好的起点。点击这里了解更多信息。

更多推荐