本文翻译自One-shot object detection,原作者保留版权。

作为计算机视觉领域的一项重要任务,目标检测是要找到一张图片里的感兴趣物体:

这比图像分类任务更高级,因为分类只需要告诉图像中主要物体是什么,然而目标检测要找到多个物体,不仅要分类,而且要定位出它们在图像中的位置。目标检测模型不仅要预测出各个物体的边界框(bounding boxes),还要给出每个物体的分类概率。通常情况下目标检测要预测许多边界框。每个边界框还需要一个置信度(confidence score),代表其包含物体的可能性大小。在后处理中,通过设定置信度阈值来过滤那些置信度较低的边界框。目标检测相比分类任务更复杂,其面临的一个主要问题在于一张图片中可能存在许多位置不定的物体,而且模型会输出很多预测结果,要计算损失函数就需要匹配真实框(ground-truth bounding box)与预测框。

这里我们主要关注one-stage目标检测算法(也称one-shot object detectors),其特点是一步到位,速度相对较快。另外一类目标检测算法是two-stage的,如Faster R-CNN算法先生成候选框(region proposals,可能包含物体的区域),然后再对每个候选框进行分类(也会修正位置)。这类算法相对就慢,因为它需要多次运行检测和分类流程。而one-stage检测方法,仅仅需要送入网络一次就可以预测出所有的边界框,因而速度较快,非常适合移动端。最典型的one-stage检测算法包括YOLOSSDSqueezeDet以及DetectNet

尽管这些方法都已经公布paper和源码,但是大部分paper对技术细节并没有完全给出来。因而,这篇博文将详细讲述one-shot检测算法(以YOLO和SSD作为典例)的原理,以及它是如何训练和预测的。

为什么目标检测问题更难

图像分类是生成单个输出,即类别概率分布。但是这只能给出图像整体内容的摘要,当图像有多个感兴趣的物体时,它就不行了。在下面的图像中,分类器可能会识别出图像即包含猫,也包含狗,这是它最擅长的。

而目标检测模型将通过预测每个物体的边界框来给出各个物体的位置:

因为可以专注于对边界框内的物体进行分类并忽略外部背景,因此模型能够为各个物体提供更加准确的预测。如果数据集带有边界框标注,则可以非常轻松地在模型添加一个定位预测:只需预测额外4个数字,分别用于边界框的每个角落。

现在该模型有两部分输出:类别概率分布和边界框回归。模型的损失函数只是将边界框的回归损失与分类的交叉熵损失相加,通常使用均方误差(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方法对模型优化训练,这是一个预测实例:

模型已正确对图像中物体(狗)分类,并给出它在图像中的位置。红色框是真实框,而青色框是预测框,虽然有偏差,但非常接近。为了评估预测框与真实框的匹配程度,我们可以计算两个边界框之间的IOU(intersection-over-union,也称为Jaccard index)。IOU在0到1之间,越大越好。理想情况下,预测框和真框的IOU为100%,但实际上任何超过50%的预测通常都被认为是正确的。对于上面的示例,IOU为74.9%,因而预测框比较精确。

使用回归方法预测单个边界框可以获得较好的结果。然而,当图像中存在多个感兴趣的物体时,就会出现问题:

由于模型只能预测一个边界框,因而它必须要选择一个物体,这会最终落在中间位置。实际上这很容易理解:图像里有两个物体,但是模型只能给出一个边界框,因而选择了折中,预测框位于两者中间,也许大小也是介于两个物体大小之间。

注意:也许你可能认为模型应该给出一个包含两个物体的边界框,但是这不太会发生,因为训练不是这样的,真实框都是各个物体分开标注的,而不是一组物体进行标注。

你也许认为,上述问题很好解决,对于模型的回归部分增加更多的边界框预测不就好了。毕竟,如果模型可以预测N个边界框,那么就应该可以正确定位N个物体。听起来不错,但是并没有效。就算模型有多个检测器(这里一个边界框回归称为一个检测器),我们得到的边界框依然会落在图像中间:

为什么会这样?问题是模型不知道应该将哪个边界框分配给哪个物体,为了安全起见,它将它们放在中间的某个位置。该模型无法决定:“我可以在左边的马周围放置边界框1,并在右边的马周围放置框2。”相反,每检测器仍然试图预测所有物体,而不是一个检测器预测一个物体。尽管该模型具有N个检测器,但它们无法协同工作。具有多个边界框检测器的模型的效果与仅预测一个边界框的模型完全相同。

我们需要的是使边界框检测器更专一化的一些方法,以便每个检测器将尝试仅预测单个物体,并且不同的探测器将找到不同的物体。在不专一的模型中,每个检测器应该能够处理图像中任何可能位置的各类物体。这太简单了,模型学会的是预测位于图像中心的方框,因为这样整个训练集实际上会最小化损失函数。从SGD的角度来看,这样做平均得到了相当不错的结果,但在实践中它却不是真正有用的结果,所以我们需要更加有效地训练模型。

通过将每个边界框检测器分配到图像中的特定位置,one-stage目标检测算法(例如YOLO,SSD和DetectNet)都是这样来解决这个问题。因为,检测器学会专注于某些位置的物体。为了获得更好的效果,我们还可以让检测器专注于物体的形状和大小。

使用网格

使用固定网格上的检测器是one-stage目标检测算法的主要思想,也是它们与基于候选框的目标检测方法(如R-CNN)的区别所在(实际上Faster R-CNN中RPN网络也采用网格检测)。

让我们考虑这类模型最简单的架构。它首先有一个充当特征提取器的基础网络。像大多数特征提取器一样,它通常在ImageNet上训练。对于YOLO,特征提取器以416×416图像作为输入。SSD通常使用300×300大小的图像。它们比用于分类的图像(如224×224)更大,因为我们不希望丢失细节。

作为特征提取器的基础网络可以是任何CNN网络,如Inception,ResNet以及YOLO中DarkNet,对于移动端可以采用轻量级的网路,如SqueezeNet以及MobileNet等。在特征提取器的后面是几个额外的卷积层。这是模型的目标检测部分,这些都经过训练后学习如何预测这些边界框以及框内物体的分类概率。

有许多用于训练目标检测算法的通用数据集。这里,我们将使用Pascal VOC数据集,该数据集有20个类。因此神经网络的第一部分在ImageNet上进行训练,而检测部分是在VOC数据集上训练。

YOLO或SSD的实际架构要比这个简单的示例网络稍微复杂一点,比如有残差结构,但我们稍后会介绍。目前,上述模型已经可用于构建快速且相当精确的目标检测模型。

最后一层的输出是一个特征图(上图中的绿色图)。对于我们的示例模型,这是一个包含125个通道的13×13大小的特征图。

注意:此处的13x13是因为输入图片大小为416x416,而此处使用的特定基础网络具有五个池化层(或具有stride=2的卷积层),这样按比例缩小32倍:416/32 = 13。如果你想要一个更精细的网格,例如19×19,那么输入图像应该是19×32 =608像素宽和高(或者你可以使用一个较小步幅的网络)。

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

这里的关键是检测器的位置是固定的:它只能检测位于该单元附近的物体(实际上,物体的中心必须位于网格单元内)。这可以避免上一部分所说的问题,当时的检测器有太多的自由度。使用此网格,图像左侧的检测器将永远不会预测位于右侧的物体。

每个物体检测器产生25个值:

  • 表征类别概率的20个值
  • 4个边界框坐标(中心x,中心y,宽度w,高度h)
  • 1个值表示置信度

由于每个单元有5个检测器,5×25 = 125,这就是我们有125个输出通道的原因。

与常规分类器一样,类别概率可以采用softmax获得。我们可以通过查看最高概率值确定类别。 (虽然通常情况下将其视为多标签分类,但是这里20个类是独立的,我们使用sigmoid而不是softmax)

置信度介于0和1(或100%)之间,用于表征模型认为此预测边界框包含真实物体的可能性。请注意,这个分数只说明了这是否是一个物体,但没有说明这是什么类型的物体,后面需要分类概率才能确定。

该模型总是预测固定数量的边界框:13×13个单元乘以5个检测器给出845个预测。显然,绝大多数这些预测都不会有用,毕竟大多数图像最多只包含少量物体,而不是超过800。置信度告诉我们哪些预测框可以忽略。通常情况下,我们最终会得到模型认为很好的十几个预测。其中一些将重叠,这是因为相邻的单元可能都对同一个物体进行预测,有时单个单元会进行多次预测(尽管在训练中这是被抑制的)。

具有许多大部分重叠的预测框在目标检测中比较常见。标准后处理方法是应用非最大抑制(NMS)来去除重叠框。简而言之,NMS保留拥有最高的置信度的预测框,并删除任何与之重叠超过一定阈值的预测框(例如60%)。通常我们只保留10个左右的最佳预测并丢弃其他预测。理想情况下,我们希望图像中的每个物体只有一个边界框。

好吧,上面描述了使用网格进行物体检测的基本思路。但为什么它有效呢?

约束让模型更容易学习

前面已经提到将每个边界框检测器分配到图像中的固定位置是one-stage目标检测算法的技巧。我们使用13×13网格作为空间约束,使模型更容易学习如何预测对象。使用这种(架构)约束是神经网络特别有用的技巧。事实上,卷积本身也是一个约束:卷积层实际上只是一个全连接(FC)层的更受限制的版本。(这就是为什么你可以使用FC层实现卷积,反之亦然,它们基本上是相同的)

如果我们只使用普通的FC层,那么神经网络模型要学习图像要困难得多。对卷积层施加的约束:它一次只看几个像素,并且连接共享相同的权重。我们使用这些约束来消除自由度并引导模型学习我们想要学习的内容。

同样,网格结构强制让模型学习专门针对特定位置的物体检测器。左上角单元格中的检测器仅预测位于左上角单元格附近的物体,而不会预测距离较远的物体(对模型进行训练,使得给定网格单元中的检测器仅负责检测其中心落入该网格单元内的物体)。

前面所说的模型没有这样的约束,因此它的回归层永远不会限制在特定位置。

先验框(Anchors,锚)

网格是一种有用的约束,它限制了检测器可以在图像中找到物体的位置。我们还可以添加另一个约束来帮助模型做出更好的预测,就是物体形状的约束。

我们的示例模型具有13×13个网格单元,每个单元具有5个检测器,因此总共有845个检测器。但为什么每个网格单元有5个检测器而不是一个?好吧,就像检测器很难学会如何预测可以位于任何地方的物体一样,检测器也很难学会预测任何形状或大小的物体。

我们使用网格来专门限制检测器仅查看某个特定空间位置。相应地,为每个网格单元设置几个不同的检测器,我们可以使这些物体检测器中的每一个都专注于某种物体形状。

我们在5种特定形状上训练检测器:

红色框是训练集中五个最典型的物体形状。青色框分别是训练集中最小和最大的物体。请注意,这些边界框显示在输入为416×416中。 (该图还以浅灰色显示网格,因此可以看到这五个形状网格单元相关。每个网格单元在输入图像中覆盖32×32像素)

这五种形状称为先验框或者锚。先验框只是一个宽度和高度列表:

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个先验框并非偶然。网格单元中的每个检测器都有一个先验框。就像网格对检测器施加位置约束一样,先验框迫使检测器专门处理特定的物体形状。

单元中的第一个检测器负责检测与第一个先验框相似的物体,第二个检测器负责与第二个先验框相似的物体,依此类推。因为我们每个单元有5个检测器,所以我们也有5个先验框。因此,检测器1将拾取小物体,检测器2拾取稍大的物体,检测器3拾取长而扁平的物体,检测器4拾取高而薄的物体,而检测器5拾取大物体。

注意:上面代码片段中先验框的宽度和高度用网格的13×13坐标系表示,因此第一个先验框略宽于1个网格单元格,高度接近2个网格单元格。最后一个先验框覆盖了几乎整个网格,超过10×10个单元格。这就是YOLO设置先验框的方式。然而,SSD具有几个不同尺寸的不同网格,因此使用先验框的标准化坐标(在0和1之间),它们与网格的大小无关。任何一种方法都可以。

重要的是要了解这些先验框是事先选择的。它们是恒定的,在训练期间不会改变。

由于先验框只是一个宽度和高度集合,而且它们是事先选择的,所以YOLO纸也称它们为“dimension priors”(Darknet,官方的YOLO源代码,称它们为“biases”,也是比较合理 - 检测器偏向于预测某个形状的物体,但是这个术语令人困惑)。

YOLO通过在所有训练图像的所有边界框上进行k-means聚类来选择先验框(k=5,因此它找到五个最常见的物体形状)。因此,YOLO的先验框适合当前训练(和测试)的数据集。k-means算法中数据点是数据集中所有真实边界框的宽度和高度值。如果我们在Pascal VOC数据集的框中运行k-means,将得到5个簇:

这些簇表示此数据集中存在的不同物体形状的五个“平均值”。你可以看到k-means在蓝色簇中将非常小的物体组合在一起,在红色簇中将较大的物体组合在一起,将非常大的物体分组为绿色。它决定将中间物体分成两组:一组边界框(黄色)宽大于高,另一组高于宽(紫色)。

但5个簇是最佳选择吗?我们可以设定不同的k进行k-means,并计算真实框与它们最接近的先验框之间的平均IOU。当然,使用更多的质心(更大的k值)会产生更高的平均IOU,但这也意味着我们需要在每个网格单元中使用更多的检测器,这会使模型运行得更慢。对于YOLO v2,他们选择5个聚类中心,这是在准确度和模型复杂性之间的折衷。

SSD不使用k-means来确定先验框。相反,它使用数学公式来计算先验框尺寸,因此SSD的先验框与数据集无关(SSD称它们为“default boxes”)。SSD和YOLO的先验框设置,到底哪种方式比较好,并没有定论。另一个小差异是:YOLO的先验框只是宽度和高度,但SSD的先验框也有x,y位置。其实,YOLO也包含位置,只是默认先验框位置始终位于网格单元格的中心(对于SSD,先验框也是在网格中心)。

由于先验框,检测器仅预测相比边界框的偏移值,这使得训练更容易,因为预测值全为零时等价于输出先验框,平均上更接近真实物体。如果没有先验框,每个检测器都必须从头开始学习不同的边界框形状,这相当困难。

注意:本文所说的YOLO是指YOLOv2或v3,这与YOLOv1是非常不同的。YOLOv1具有较小的网格(7×7,每个单元仅有2个检测器),使用全连接层而不是卷积层来预测网格,并且不使用先验框。那个版本的YOLO现在已经过时了。相比之下,v2和v3之间的差异要小得多。而YOLOv3在很多方面与SSD非常相似。

模型到底预测了什么

让我们更加细致地看一下模型的输出是什么,由于模型仅仅是一个卷积神经网络,所以前向过程是这样的:

你输入一个416x416的RGB图像,经过卷积层之后是一个13x13x125的特征图。由于是回归预测,最后的层没有激活函数。最后的输出包含21,125个数,我们要将它们转化为边界框。

我们采用4个数来表示一个边界框的坐标。通常有两种方式:或者用xmin, ymin, xmax, ymax表示边界框左上和右下点坐标,或者使用center x, center y, width, height。两个方式都可以(两者可以互相转换),但是这里采用后者,因为知道框中心更加容易将边界框与网格单元进行匹配。

模型预测的不是边界框在图像中的绝对坐标,而是4个偏移值(delta or offset):

  • 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

这里的ij是网格(0-12)的行与列,而b是检测器(0-4)的索引。预测框比原始图片宽或者高都是正常的,但是宽和高不应该会是负值,因此这里取预测值的指数。如果delta_w<0,那么exp(delta_w)介于0~1,此时预测框比先验框小。若delta_w>0,那么exp(delta_w)大于1,此时预测框比先验框大。特别地,delta_w=0,那么exp(delta_w)为1,此时预测框与先验框一样大小。顺便说一下,这里之所以要乘以32是因为先验框坐标是13x13网格上的,网格上各个像素点等价于原始416x416图像上的32个像素点。

注意:在损失函数中,我们使用上述公式的逆运算:不是将预测的偏移值通过exp转换到实际坐标,而是将真实框通过log运算转换为偏移值。后面会详细介绍。

利用下面公式可以得到预测框中心的像素坐标:

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_xdelta_y必须限制在0~1之间,代表预测框在网格单元的相对位置,这里用sigmod函数限制值的范围。然后我们加上单元格的坐标i和j(0~12之间),乘以每个单元格所包含的像素数(32)。这样box_xbox_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和YOLO采用的坐标系不一样。SSD的先验框坐标是归一化到[0,1],这样它们独立于网格大小(SSD之所以这样是采用了不同大小的网格)。

可以看出,即使YOLO和SSD大致原理是相同的,但是具体的实现细节是有差异的。

除了坐标之外,模型每个边界框预测了一个置信度,一般采用sigmoid函数得到0~1的概率值:

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

实际上我们的模型预测了845个边界框,但是一张图片中的物体一般是很少的。在训练过程中,由于我们仅让一个检测器预测一个真实框,因此只有很少的预测框的置信度较高,那些不应该找到物体的检测器应该得到较小的置信度。

最后,我们预测分类概率,对于Pasval VOC数据每个边界框有20个值,采用softmax得到概率分布:

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

除了采用softmax,还可以采用sigmoid,这将变成一个多标签分类器,每个预测框实际上可以同时有多个类别,SSD和YOLOv3是这样。

但是SSD没有预测置信度,它给分类器增加了一个特殊的类:背景。如果分类结果是背景,那么意味着检测器没有找到物体,这实际上等价于YOLO给出一个较低的置信度。

由于很多预测是不需要的,所以对于置信度较低的预测我们将舍弃。在YOLO中,结合置信度和最大类别概率来过滤预测值(条件概率):

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

置信度低表示模型很不确定预测框是否含有物体,而低分类概率表示模型不确定哪种物体在预测框内,只有两者都高时,我们才采纳这个预测值。

由于大部分的框不含物体,我们可以忽略那些置信度低于某个阈值(比如0.3)的预测框,然后对余下的框执行NMS(non-maximum suppression),去除重叠框,这样处理后一般得到1~10个预测框。

这是卷积预测

卷积神经网络天然适合这样的网格检测器,13×13的网格是卷积层得到的. 卷积实际上是在整个输入图像上以较小的窗口(卷积核)进行滑动,卷积核在每个位置是共享的。我们的样例模型最后的卷积层包含125个卷积核。

为什么是125?因为有5个检测器,每个检测器有25个卷积核。这25个卷积核中的每一个都预测边界框的属性值:坐标x和y,宽度,高度,置信度,20类概率。

注意:一般来说,如果数据集有k类,而模型有b个检测器,那么网格需要有b×(4+1+k)个输出通道。

这125个卷积核在13×13特征图中的每个位置上滑动,并在每个位置上进行预测。然后,我们将这125个数字解释为5个预测边界框。最初,在每个网格位置预测到的125个数字将是完全随机和无意义的,但是随着训练,损失函数将引导模型学习做出更有意义的预测。

尽管每个网格单元中有5个检测器,但对于845个检测器来说,这个模型实际上只学习了总共5个检测器,而不是每个网格单元都学习5个唯一的检测器。这是因为卷积层的权重在每个位置都相同,因此在网格单元之间共享。但模型为每个先验框学习一个检测器,它们在图像上滑动,以获得845个预测值,网格上每个位置有5个。因此,尽管我们总共只有5个独特的检测器,但由于卷积的缘故,这些检测器与它们在图像中的位置无关,因此无论它们位于何处,都可以检测到物体。每个位置的输入像素与检测器所学习到的权重,决定了该位置的最终边界框预测。

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

YOLO vs SSD

上面关于one-stage的目标检测模型的原理的描述几乎适用于所有的检测器。但是在解释输出的方式上可能存在细微的差异(例如,分类概率采用sigmoid而不是softmax),但一般是相同的。

然而,不同版本的YOLO和SSD之间存在一些结构上的差异。以下是YOLOv2&v3和SSD模型的架构示意图:

可以看到,YOLOv3和SSD非常相似,尽管它们是通过不同的方法得到网格(YOLO使用升序采样,SSD使用降序采样)。而YOLOv2(我们的示例模型)只有一个13×13的输出网格,而SSD有几个不同大小的网格。Mobilenet+SSD版本有6个网格,大小分别为19×19、10×10、5×5、3×3、2×2和1×1。因此,SSD网格的范围从非常细到非常粗。这样做是为了在更广泛的物体尺度上获得更精确的预测。比较精细的19×19网格,其网格单元非常靠近,负责预测最小的物体。而最后一层生成的1×1网格负责预测基本上占据整个图像的较大物体,其他层的网格适应比较宽范围的物体。YOLOv3更像SSD,因为它使用3个不同大小的网格预测边界框。

和YOLO一样,每个SSD网格单元都会进行多个预测。每个网格单元的检测数量各不相同:在更大、更细粒度的网格上,SSD每个网格单元有3或4个检测器,在较小的网格上,每个网格单元有6个检测器。而YOLOv3的每个网格单元均使用3个检测器。

坐标预测是相对于先验框的,在SSD文件中被称为“default boxes”,但有一个区别是,SSD的预测中心坐标可以超出其网格单元。先验框位于单元的中心,但SSD不会对预测的X,Y偏移应用sigmoid。所以理论上,模型右下角的一个检测器可以预测一个中心在图像左上角的边界框(但这在实践中可能不会发生)。

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

SSD在先验框上与YOLO稍有不同。因为YOLO必须从单个网格进行所有预测,所以它使用的先验框的范围从小(大约单个网格单元的大小)到大(大约整个图像的大小)。SSD更为保守。19×19网格上的先验框比10×10网格上的要小,比5×5网格上的更小,以此类推。与YOLO不同的是,SSD不使用先验框来使检测器专注于物体大小,而是使用不同的网格来实现这一点。SSD的先验框主要用于使检测器处理物体形状(长宽比)的变化,而不是它们的大小。如前所述,SSD的先验框是使用公式计算的,而YOLO的先验框是通过对训练数据运行k-means聚类来找到的。

因为SSD使用3到6个先验框,它有6个网格而不是1个,所以实际上它总共使用了32个独特的检测器(这与具体网络结构有关)。因为SSD有更多的网格和检测器,它将输出更多的预测。YOLO进行了845次预测,而MobileNet-SSD则包含1917次预测。而SSD512甚至输出24564个预测!其优点是可以更容易找到图像中的所有物体。缺点是,你最终不得不做更多的后处理来找出你想要保留的预测。

由于这些差异,将真实框与检测器的匹配方式在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”(总共16551张图像),然后随机选取10%左右的图像用于验证。可以在2007测试集上测试模型,因为label已经给出。还有一个2012年的测试集,但label是不公开的(也有习惯于将2007年测试集包括在训练数据中,数据越多越好)。

2007+2012组合训练集有8218个带物体框标注的图像,验证集有8333个图像,2007测试集有4952个图像。这比ImageNet的130万张图片要少得多,所以最好使用迁移学习,而不是从头开始训练模型。这就是为什么我们从一个已经在ImageNet上预训练过的特征提取器开始。

标注

标注描述了图像中的内容。简而言之,标注提供了我们训练所需的目标。标注采用XML格式,每个训练图像一个。标注文件包含一个或多个带有类名称的部分:用xmin、xmax、ymin、ymax描述的边界框以及每个object的一些其他属性。如果一个物体被标为difficult,我们将忽略它,这些通常是非常小的物体,它们也被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,包含两个物体:人和摩托车。没有被标注为difficult或者truncated (部分在图像外).

注意:Pascal VOC数据集坐标从1开始,而不是0,也许是采用MATLAB的格式。 我们可以画出这个图像的各个物体的边界框:


VOC2007和2012共包含如下的图像:

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


约有一半的图像仅有一个物体,其它的包含1个以上,下面是训练集统计的直方图:

一张图片中最大物体数为39,验证集和测试集的直方图大致类似。同样地,我们给出训练集中所有物体区域大小的直方图(长和宽归一化到[0,1]):


可以看到许多物体相对较小。峰值为1.0是因为有相当多的物体大于图像(例如,只有部分可见的人),因此边界框填充整个图像。这里还有另一种方法来查看这些数据:边界框宽度与高度的关系图。图中的“坡度”显示框的高宽比。


数据扩增

由于数据集相当小,在训练时经常使用大量的数据扩增,如随机翻转、随机裁剪、颜色抖动等。值得注意的是,对图像所做的任何操作都必须对边界框同样执行!比如,如果翻转图像,还必须对应地翻转真值框的坐标。
YOLO的数据扩增流程如下:

  • 加载原始图像;
  • 通过随机增加/减去原始大小的20%来选择新的宽度和高度;
  • 按照新大小裁剪图像,如果新图像在一个或多个边上大于原始图像,则用零填充
  • 将图像resize到416×416,使其成为正方形
  • 随机水平翻转图像(50%的概率)
  • 随机改变图像的色调、饱和度和曝光(亮度)
  • 对应地,要通过移动和缩放边界框坐标来调整边界框,以适应前面所做的裁剪和调整大小,以及水平翻转等操作

旋转也是一种常见的数据扩增技术,但这会比较麻烦,因为我们还需要同时旋转边界框,所以通常不会这样做。而SSD采用的数据扩增方式包括:

  • 随机裁剪一个图像区域,使该区域中物体的最小IOU(与原始图像中物体)为0.1、0.3、0.5、0.7或0.9,IOU越小,模型就越难检测到物体
  • 使用“缩小”增强,将图像变小,从而构建包含小物体的训练数据,这对于模型更好地处理小物体很有用。

随机裁剪可能会导致物体的部分(或全部)落在裁剪图像之外。因此,我们只希望保留中心位于该裁剪区域某个位置的边界框,而不希望保留中心位于裁剪区域之外的框。

注意高宽比

我们的预测是在13x13的正方形网格上,输入图像也是正方形的(416x416)。但是训练数据集中图像通常不是正方形的,而且测试图像一般也不是。而且,所有的图像大小可能并不相同。下图是VOC数据集中所有图像的高宽比的可视化:

红色框是宽大于高,而青色框恰恰相反。 虽然存在一些奇怪的高宽比,但是大部分是1.333 (4:3), 1.5 (3:2), 0.75 (3:4)。有些图片甚至很宽,这里是一个极端例子:

由于网络的输入是416×416大小的正方形图像,因此我们必须将训练图像放在该正方形中。下面是几种方法:

  • 直接将图像resize到416×416,这可能会挤压图像;
  • 将最小边调整为416,然后从图像中裁剪出416×416区域;
  • 将最大边调整为416,用零填充另外的短边;

上述方法都是有效的,但每个方法都有其副作用。我们直接将其高宽比更改为1:1,可能会挤压图像。如果原始图像宽大于高,则所有物体都比平常窄。如果原来的物体高大于宽,那么所有的物体都会变平。通过裁剪,虽然高宽比保持不变,但我们可能切掉图像的重要部分,使模型更难看到真实的物体,这样模型可能需要预测部分位于图像外部的边界框。而对于方法3,它可能会使物体太小而无法检测,尤其是在高宽比极端的情况下。

为什么这很重要?训练前,我们将边界框的xmin和xmax除以图像宽度,ymin和ymax除以图像高度,以归一化坐标,使它们介于0和1之间。这样做是为了使训练独立于每个图像的实际像素大小。但是输入的图像通常不是正方形的,所以x坐标除以一个与y坐标不同的数字。根据图像的尺寸和高宽比,每个图像的除数可能不同,这会影响我们如何处理边界框坐标和先验框。
方法1是最简单粗暴,尽管它会暂时破坏图像的高宽比。如果所有的图像都有相似的高宽比(在VOC中没有),或者高宽比不太极端,那么神经网络仍然可以正常工作。CNN网络似乎对于物体的“厚度”变化相当健壮(意思是物体挤压时CNN依然有效)。

对于方法2和3,在归一化边界框坐标时,我们应该记住高宽比。现在有可能边界框比输入图像大,因为我们只是对裁剪部分进行预测。而由于物体可能部分落在图像之外,边界框也可能部分落在图像之外。裁剪的缺点是我们可能会丢失图像的重要部分,这可能比稍微挤压物体更糟糕。挤压还是裁剪也会影响如何从数据集中计算先验框。使用先验框的重要因素,这些先验框的形状类似于数据集中最常见的物体形状。这在裁剪时仍然是正确的。一些先验框现在可能部分落在图像之外,但至少它们的高宽比真正代表了训练数据中的物体。对于挤压,计算出的先验框并不能真正代表真正的框,不同的高宽比会被忽略,因为每个训练图像的挤压方式略有不同。现在,先验框更像是在不同的扭曲图像求平均结果。

数据扩增也会有副作用。通过随机截取图像,然后将大小调整为416×416,这也会扰乱高宽比(更像故意的)。

总结来看,直接对原始图像进行resize,而忽略边界框的高宽比,这是最简单有效的。这也是Yolo和SSD所采用的方式,这种方式可以看成让模型学会自适应高宽比。如果我们在处理固定大小的输入图像,例如1280×720,那么使用裁剪可能更合适。


模型是如何训练的


前面都是预备项,接下来我们将来介绍这类目标检测模型是如何训练的。该模型使用卷积神经网络直接进行预测,然后把这些预测数字转换成边界框。数据集包含真实框,表示训练图像中实际存在哪些物体,因此要训练这种模型,我们需要设计一个损失函数,将预测框与真实框进行比较。

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

如果我们直接简单地进行匹配,例如总是将第一个真实框分配给第一个检测器,将第二个物体分配给第二个检测器,以此类推,或者通过将物体随机分配给检测器,那么每个检测器都将被训练来预测各种各样的物体:一些较大的物体,一些是极小的物体,有的会在图像的一角,有的会在相反的一角,有的会在中间,等等。这就出现前面所提到的问题:为什么仅仅在模型中添加一组回归输出就难有效。解决方案是使用带有固定大小网格的检测器,其中每个检测器只负责检测位于图像该部分的物体,并且只负责特定大小的物体。
现在,损失函数需要知道哪个物体归属于哪个检测器,或者说在哪个网格单元中,相反地,哪些检测器没有与它们相关联的真实框。这就是我们所说的“匹配”。


将真实框与检测器匹配

匹配的方法是各种各样的,在YOLO中,图像中的每个物体仅由一个检测器负责来预测。由于我们要找到边界框中心落在哪个网格单元中,那个单元与整个物体关联,而其它的网格单元如果预测了这个物体将被损失函数所惩罚。
VOC数据集给出的边界框标注为xmin, ymin, xmax, ymax。由于我们需要知道边界框中心,所以需要将边界框坐标转为center x, center y, width, and height。我们一般会先将边界框坐标归一化到[0, 1],这样它们独立于输入图像的大小(因为训练图像的大小并不一致)。

由于需要匹配,我们在采用一些数据扩增如随机翻转,要同时应用在图像和边界框上。

注意:对于一些数据扩增如随机裁剪和翻转,我们在每个epoch需要重新对真实框与检测器进行匹配。这个过程无法提前完成,并缓存下来,因为数据扩增是随机的,一般会改变匹配结果。

仅仅为每个物体选择网格单元是不够的。每个网格单元都有多个检测器,我们只需要其中一个检测器来查找物体,我们需要选择其先验框与物体的真实框最匹配的检测器。这通常采用IOU来衡量匹配度。这样,最小的物体被分配给检测器1(有最小的先验框),非常大的物体使用检测器5(有最大的先验框),以此类推。所以,只有那个单元中的特定检测器才可以预测这个物体。此规则使得不同的检测器更专注于处理形状和大小与先验框相似的物体(记住,物体的大小不必与先验框的大小完全相同,因为模型预测会预测相对于先验框的位置和大小偏移,先验框只是一个参考)。

因此,对于一个给定的训练图像,一些检测器将有一个与之相关的物体,而其他检测器将不会。如果训练图像中有3个物体,即有3个真实框,那么845个检测器中只有3个应该进行预测,而其他842个检测器则应该预测“无物体”(就我们的模型输出而言,得到的是置信度很低的边界框,理想情况下为0)。

从现在开始,我们用正例指代一个匹配到物体的检测器,而对于一个没有关联物体的检测器来说,则是负例,也可以说是“无对象”或背景。

由于模型的输出是13×13×125张量,因此损失函数所使用的目标张量也将是13×13×125。这个数字125来自:5个检测器,每个检测器预测类别的20个概率值+4个边界框坐标+1个置信度得分。在目标张量中,对于正例,我们会给出物体的边界框坐标和onehot编码的类别向量,而置信度为1.0(因为我们100%确定这是一个真实的物体)。对于负例,目标张量的所有值为0,边界框坐标和类向量在这里并不重要,因为它们将被损失函数所忽略,并且置信度得分为0,因为我们100%确定这里没有物体。

因此,训练的每个迭代过程,需要的是一个batch×416×416×3的图像张量和一个batch×13×13×125的目标张量,这个目标张量中元素的大多都是0,因为大多数检测器不负责预测一个物体。

匹配时还需要考虑一些其他细节。例如,当有多个物体的中心恰好落在同一个单元中时,该怎么处理?虽然实际上这可能不是一个大问题,特别是如果网格足够大时,但是我们仍然需要一种方法来处理这种情况。理论上,物体基于最佳IOU来匹配检测器,例如,物体A的边界框与检测器2的IOU最大,物体B的边界框与检测器4的IOU最大,那么我们可以将这物体与该单元中的不同检测器匹配。然而,这并不能避免有两个物体需要相同检测器这个问题。

YOLO的解决方案比较粗暴:每次随机打乱真实框,每个单元只选择第一个进入它中心的物体。因此,如果一个新的真实框与一个已经负责另一个物体的单元相匹配,那么我们就只能忽略它了。这意味着在YOLO中,每个单元至多有一个检测器被匹配到物体,而其他检测器不应该检测到任何东西(如果检测到了,就会受到惩罚)。

这只是YOLO的策略,SSD的匹配策略却不相同。SSD可以将同一个真实框与多个检测器匹配:首先选择具有最佳IOU值的检测器,然后选择那些与之IOU超过0.5的但是未被匹配过的检测器(注意检测器和先验框是绑定的,一一对应,所以说IOU指的是检测器的先验框与物体的边界框之间的重叠)。这应该使模型更容易学习,因为它不必在哪个检测器应该预测这个对象之间进行唯一选择,毕竟多个检测器可以预测这个对象。

注意:两者设计似乎是矛盾的。YOLO将一个物体只分配给一个检测器(而该单元的其他检测器则是无物体),以帮助检测器更专注。但是SSD说多个检测器可以预测同一个物体。两者实际上都可以。对于SSD,检测器专注于形状而不是大小。


损失函数

损失函数实际上是告诉模型它应该学习什么。对于目标检测,我们需要损失函数它能够使模型预测出正确的边界框,并对这些框正确分类,另一方面,模型不应该预测不存在的物体。这实际上是多任务学习。因此,损失函数由几个不同的部分组成,其中一部分是回归以预测边界框位置,另一部分用于分类。

对于任何一个检测器,有两种可能的情况:

  • 这个检测器没有与之相关的真实框,这是负例,它不应该检测到任何物体(即它应该预测一个置信度为0的边界框)。
  • 这个检测器匹配到了一个真实框,即正例,它负责检测到物体。

对于不应该检测到物体的检测器,当它们预测出置信度大于0的边界框时要惩罚它们。因为它们给出的检测是假阳性,图像中的这个位置上并没有真实物体。过多的误检会降低模型的效果。相反,如果检测器是正例,当出现下面的情况时,我们希望惩罚它:

  • 当坐标错误
  • 当置信度太低时
  • 分类错误

理想情况下,检测器应该预测一个与真实框完全重叠的框,类别也应该一致,并且具有较高的置信度。当置信度得分过低时,预测结果将被视为假阴性(false negative),这也意味着模型没有找到真正的物体。但是,如果置信度得分高,但坐标不准确或分类错误,则预测将被视为假阳性(false positive)。尽管模型检测出一个物体,但它是错误的。

这意味着相同的预测可以被判定为假阴性(会减低模型的召回),也可能是假阳性(降低模型的准确度)。只有当所有三个方面——坐标、置信度、类别都正确时,预测才算真阳性(true positive)。因为任何一个方面都可能出错,损失函数由几个部分组成,分别来衡量模型给出的预测的不同类型“错误性”,将这些部分相加,得到整体损失函数。

SSD、YOLO、Squezedet、Detectnet和其他one-stage目标检测模型的损失函数可能有差异,但是它们往往由相同的部分组成。

(1)没有被匹配的检测器(负例)

对于负例,损失函数仅包含置信度部分,因为没有真实框,所以没有任何坐标或类别标签来计算损失。如果这样的检测器确实找到了一个物体,它应该受到惩罚。置信度分数表示检测器是否认为有一个物体的中心在这个网格单元中。对于这样的检测器,目标张量中的真实置信度得分被设置为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]。可以看到,上面的loss仅仅是计算预测值与目标值之差的平方。而no_object_scale是一个超参数,一般取0.5, 这样这部分loss占整体比重不大. 由于图像中只有少量物体,所以845个检测器中的大部分仅计算这类“no object”损失。由于我们不想让模型仅仅学习到“no objects”,这部分loss不应该比那些匹配到物体的检测器的loss重要。

上述公式仅是计算一个网格单元中一个检测器的loss,实际上要将所有网格中的负例检测器的loss求和才是最终的loss。对于那些正例检测器,这项loss总是0。SqueezeDet求的是各个检测器loss的平均值(总loss除以负例检测器数量),而在YOLO中直接取loss和。

实际上,YOLO还有一个特别处理之处。如果一个检测器的预测框与所有真实框的IOU最大值大于一个阈值(比如0.6),那么忽略这个检测器的no_object_loss。换句话说,如果一个检测器被认为不应该预测一个物体,但是实际上却预测了一个不错的结果,那么最好是忽略它(或者鼓励它预测物体,也许我们应该让这个检测器与这个物体匹配)。这个trick到底会起多大作用,并无法评估(深度学习很多这样无法讲明白的trick)。

SSD没有这项loss,因为它将背景类看成一个特殊类进行处理。如果预测的是背景类,那个检测器被认为没有检测到物体。

注意:YOLO采用平方和误差(sum-squared error,SSE),而不是常见的用于回归的均方差(mean-squared error,MSE),或是用于分类的交叉熵。一个可能原因是每张图片物体数量并不同,如果取平均,那么包含10个物体的图片与包含1个物体的图片的loss的重要性一样,而采用求和,前者的重要性约是后者的10倍,这可能更公平。

(2)被匹配的检测器(正例)

前面所说的是不负责检测物体的负例检测器,接下来讲另一类检测器:它们应该检测到物体。当这类检测器没有检测到物体,或者给物体错误分类时,它们就被判定出错,有三部分loss来评估错误。

(a)置信度

首先是置信度loss:

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。YOLO在计算loss时不采用理想值,而是使用两个框的实际IOU值。这也讲得通:当IOU值低时,置信度会低,反之IOU值高,置信度也会高。对于no-object loss,我们一直希望预测的置信度为0,而这里我们并不是想要模型的置信度一直是100%。相反,模型应该能够学习评估预测的边界框的实际好坏,而IOU恰好可以反映这一点。前面已经提到, SSD不预测置信度,所以这项loss也不计算。

(b)类别概率

每个检测器都会预测物体的类别,这与边界框坐标是分开的。本质上,我们为不同大小的物体训练了5个独立的分类器(同一个网格中的每个检测器的分类器是不同的)。YOLOv1和v2按如下方式计算分类的loss:

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

这里true_class是onehot编码的目标向量(对于VOC数据集,大小为20) ,而pred_class是预测的logits向量。注意这里我们虽然使用了softmax,但是并没有计算交叉熵,反而是误差平方和loss,或者这是为了与其它loss保持一致。实际上,甚至可以不应用softmax也是可以的。

而YOLOv3和SSD采用不同的方式,它们将这个问题看成多标签分类问题。所以不采用softmax(它导致各个类别互斥),反而使用sigmoid,这样允许预测多个标签。进一步,它们采用标准的二元交叉熵计算loss。

由于SSD不预测置信度,所以它增加了一个背景类。如果检测器预测是背景,那么此检测器没有检测到物体,即忽略这个预测。实际上SSD的no-object loss就是背景类的分类loss。

(c)边界框坐标

最后一项loss是边界框坐标,也称为定位损失,其实就是简单地计算边界框的4个坐标的回归损失:

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

我们需要进行对模型的预测做一定的后处理才能得到有效的坐标值。由于模型实际上不是直接预测有效的边界框坐标,所以损失函数中的真实框也要与之对应,即我们要先将真实框的实际坐标进行逆向转换:

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

对于其它项坐标也是如此,这项loss对边界值更不敏感(曲线更平稳)。

开始训练

接下来,我们可以给出一个完整的模型训练过程,首先我们需要:

  • 一个包含图片以及边界框标注的数据集(如Pascal VOC);
  • 一个可以拥有网格检测器的模型,并采用一个匹配策略将真实框转化为目标张量;
  • 一个计算预测值与目标值的损失函数。

然后就可以采用SGD对模型进行训练,由于检测器对正例和负例的loss计算方式不同,需要一定的循环才可以计算出整个loss,简单的伪代码如下:

for i in 0 to 12:
  for j in 0 to 12:
    for b in 0 to 4:
      gt = target[i, j, b]   # ground-truth
      pred = grid[i, j, b]   # prediction from model
      # is this detector responsible for an object?
      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是各项loss的加权和:

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

但是实际上可以将上述循环过程向量化以可以在GPU上加速运算,主要思路是采用一个mask屏蔽那些不需要计算的部分:

# the mask is 1 for detectors that have an object, 0 otherwise
mask = (target.conf == 1)
# compute IOUs between each detector's predicted box and
# the corresponding ground-truth box from the target tensor
ious = IOU(target.coords, grid.coords)
# compute the loss terms for the entire grid at once:
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以及其它的one-stage目标检测模型在计算loss时有稍微的不同,因而你有很多可选择的余地进行设计。

另外有一些值得注意的小技巧来训练模型:

  • 多尺度训练。一般情况下,目标检测模型用于不同大小的图片,因而也包含不同尺度的物体。一个可以让模型可以对不同大小的输入泛化的方法是每一定的迭代过程中随机选择不同的输入尺寸。比如随机从320×320到608×608之间选择的输入,而不是恒定在416x416。
  • 热身训练(Warm-up training)。 YOLO在早期训练阶段为每个单元中心增加一个假的真实框(先验框),采用这个额外的坐标损失来鼓励模型的预测可以匹配到检测器的先验框。
  • 难例挖掘(Hard negative mining)。前面已经说过大部分检测器是不负责检测任何物体的。这意味着正例数量要远少于负例。YOLO采用一个超参数no_object_scale 来处理这种情况,但是SSD采用难例挖掘:它不是计算所有负例的损失,而是只计算那些预测结果最错的部分损失(即置信度较高的负例)。

即使一旦训练后模型就能很好地工作,但是你有时候需要这些技巧让模型快速学习。

如何评价模型

为了评估一个分类模型,你可以简单的计算在测试集上预测正确的数量,并除以测试图片的总数,从而得到分类准确度。然而对于目标检测模型,你需要评估以下几个部分:

  • 每个检测物体的分类准确度;
  • 预测框与真实框的重合度(IOU)
  • 模型是否找到图片中的所有物体(召回,recall)。

仅采用任何一个指标是不够的。比如,如果设定IOU的阈值为50%,当一个预测框与一个真实框的IOU值大于该阈值时,被判定为真阳(TP),反之被判定为假阳(FP)。但是这并不足以评估模型的好坏,因为我们无法知道模型是否漏检了一些物体,比如存在某些模型没有预测出的真实框(假阴,FN)。

注意:在目标检测中没有真阴(true negatives),一个真阴是本来物体也不存在,正好模型也没有给出预测。大部分情况下我们更关心包含物体的地方,而不关心背景部分,实际上真阴也无法计算。

为了将以上几种不同因素转化为一个单一指标,通常我们计算mAP(mean average precision)。mAP值越高,模型越好。计算mAP的方法随数据集略有差异。

计算mAP

对于Pascal VOC数据集,首先我们要单独计算各个类别的AP(average precision),然后取平均值得到最终的mAP,所以mAP是平均的平均。对于precision,它是真阳数除以检测的总数:

precision = TP / (TP + FP)

在这个场景中,假阳值是检测器预测了一个在图像中并不存在的物体。这一般发生在预测框与图像中的真实框差异很大(IOU值低于阈值),或者预测的类别是错误的。

注意:这里我们并关心到底是哪个检测器给出的预测。在评估模型时我们并不会像训练过程那样将特定的检测器分配给某个物体,而是仅仅将预测框与真实框进行比较,以确定到底检测出了多少物体。

另外一个与 precision一起计算的指标是recall(true positive rate or the sensitivity):

recall = TP / (TP + FN)

recall和precision的唯一区别是分母不同,前者是真阳数加假阴数,即所有真实物体的总数。对于假阴,就是检测器没有找到一个真实的物体,或者给出的置信度较低。

举例来说,precision衡量的在预测为猫的物体中,到底有多少是真的猫,这里FP就是那些预测为猫但实际上却不是猫的数量。而recall衡量的是模型找到了图像中所有真实猫的多少个,FN是指的遗漏检测的猫的数量。比如,模型预测出了3只猫,但是实际上其中一个是狗,另外一个不存在物体,那么precision就等于1/3=0.33(三个预测中仅有一个是对的)。如果图像中存在4只猫,那么recall就是1/4=0.25,因为仅检测出了一只猫。如果图像中存在一只狗,那么对于狗这类,precision和recall都是0,因为狗的TP为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"
    find IOUs between true_boxes and prediction
    choose ground-truth box with biggest IOU overlap
    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

如果某个预测框的分类正确,且与真实框的IOU值大于50%,那么就认为是TP,反之则是FP。如果存在两个及以上的预测与某个真实框的IOU大于50%,那么我们必须选择其中的一个认为是正确的预测,其它的将被当做FP。我们希望模型仅对每个物体预测一个框,这里我们通常会选择那个置信度最高的预测框。

由于对同个物体进行多次预测是受到惩罚的,所以最好先进行NMS以尽可能地去除重复的预测。最好也要扔掉那些置信度较低的预测(如低于0.3),否则它们会被当成FP。YOLO模型给出845个预测,而SSD给出1917个预测,这远远多于真实物体,因为大部分图像只含有1到3个物体。

目前为止,我们并没有计算FN,实际上并不需要。因为计算recall公式的分母是TP+FN,这实际上等于图像中真实物体的数量(我们所关注的特定类)。

现在我们计算出了precision和recall,但是单个precision和recall无法说明模型的效果。所以我们将计算一系列的precision和recall对,然后画出precision-recall曲线。对每个类,我们都会做出这样的曲线。而某个类的AP值就是曲线下的面积。

precision-recall曲线

如下是狗这个类的precision-recall曲线:

其中x坐标是recall,从0(没有检测到物体)到1(发现了所有物体),而y坐标是precision。这里precision看成是recall的函数,所以曲线的面积实际上就是这类物体的平均precision,因此叫做 “mean average precision”:我们想知道不同recall下的precision的平均值。

如何解释这条曲线?precision-recall曲线通常是通过设定不同的阈值来计算precision和recall对。对于一个二分类器来说,高于阈值就被判定为正例。在目标检测领域,我们会不断改变阈值(对应预测框的置信度)来得到不同的precision和recall。首先,我们计算第一个预测值(最大阈值)的precision和recall,然后计算第一个和第二个预测值(稍微降低阈值)的precision和recall,接着是前三个预测值的precision和recall(阈值更低),直到我们计算所有预测值下(阈值最低)的precision和recall。每对precision和recall对应就是曲线的一个点,x值为recall而y值是precision。在较大阈值处,recall是较低的,因为只包含很少预测结果,所以会有非常多的FN。你可以看到在曲线的最左侧,precision为100%,因为我们只包含了非常肯定的预测框。但是recall是极低的,因为漏掉了很多物体。随着阈值降低,将包含更多物体,recall增加。但precision上下波动,但由于FP会越来越多,它往往会变得更低。在最低阈值处,recall是最大的,因为现在包含了模型的所有预测。

可以看到,模型预测值的FP和FN之间始终存在折中。使用precision-recall曲线可以衡量这种折中,并找到一个较好的置信度阈值。选择高阈值意味着我们保留较少的预测,因此将减少FP(减少了错误),但我们也会有更多的FN(错过了更多的物体)。阈值越低,包含的预测越多,但它们通常质量较低。

理想情况下,各个recall下的precision都很高。计算出所有recall下的precision的平均值,可以给出模型在检测此特定类物体的总体效果。一旦我们获得了所有不同阈值下的precision和recall,就可以通过计算该曲线下的面积来得到AP。对于Pascal VOC数据集,实际上有两种不同的方法:2007版本使用近似方法; 2012版本更精确(使用积分)但分数一般更低。最终的mAP仅仅是20个类的AP平均值。当然,mAP越高越好。但这并不意味着mAP就是最重要的, YOLO的mAP一般低于其它模型,但速度却更快,特别是在移动设备上使用时,我们希望使用在速度和准确度之间具有较好折衷的模型。


码字不易,欢迎给个赞!

欢迎交流与转载,文章会同步发布在公众号:机器学习算法全栈工程师(Jeemy110)