目标检测中的one-stage
共 8077字,需浏览 17分钟
·
2021-03-15 09:33
关于目标检测
目标检测不同于分类,它其实包含了两个问题:
⒈ 目标在哪(位置)
⒉ 这个目标是什么(分类)
按照这两个问题可以把目标检测分为两大类,two-stage和one-stage目标检测方法。
Two-stage目标检测
比如Faster R-CNN,会先生成一些候选区域(region proposals),这些区域有可能会包含一个待检测目标(定位),紧接着再采取一些后续措施来区分每个候选区域里具体包含了那些目标(分类)。
One-stage 目标检测
比如YOLO, SSD, SqueezeDet, 和DetectNet,通过一遍网络得出目标的位置和类别信息。这样和two-stage相比,可以预见到检测速度会快很多。
本文将对one-stage目标检测做一个系统的介绍。
为什么目标检测问题这么复杂
通常来说,图像分类只能产生一个输出结果,这个结果会告诉你这张图像归属于哪种类别,比如下面这张图:
图像分类可以告诉你这是一张关于猫的图像或者这是一张关于狗的图像,但是无法区分到底是猫还是狗,只能告诉你是哪个类别的可能性更高(其实这张图里面既有猫又含有狗)。而对于一个目标检测器来说,它能准确的把这两种类别区分开,并能告诉你每个类别的物体具体位置在哪里:
因为目标检测的分类器只要专注于对目标框内部的东西做分类,而不需要关心目标框的外部干扰信息,通常可以给出每个目标非常准确的分类信息。
因此,模型现在能够产生两个输出结果:
⒈ bounding box的回归结果
⒉ 可能的类别分布结果
正因为目标检测永远离不开位置和分类两个信息,通常损失函数中也会包含这两部分的损失计算:位置损失和分类损失。假定位置损失我们用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)
有了损失之后,就可以用随机梯度下降来训练优化你的网络模型了。你的网络模型训练好了之后,可能得出了蓝色的检测结果,虽然没有和红色的ground truth完全吻合,但是非常接近了。
但是还是有点问题,如果我们的网络只输出一个bounding box,并且图像中只有一个待检测目标,那可能这个检测结果会蛮好的,但是如果图片里面有很多的待检测目标,比如像下面这张图中有两匹马,那问题可能就暴露出来了:
这张图里面有两匹马,如果你的网络只能输出一个bounding box结果,那很有可能会是中间的那个黄色框,为什么会这样呢?因为你的网络只能输出一个目标框,但是图像中含有两个目标,这样网络就会非常为难,因为他不知道应该检测左边的马还是右边的马,这样网络经过训练后很有可能会采取一个折中的办法,得到中间黄色框的检测结果。可能你会说,那为什么不会得出一个大的检测框包含这两匹马,因为事实上我们的ground truth在标注的时候都是针对于单个物体的,那么网络经过训练后,输出的结果自然就会倾向于单个物体的bounding box而不是N多物体的。
有一种解决办法就是让网络多输出几个bounding boxes,每个bounding box对应着一个目标,那也许问题就解决了。然而,现实并不是这样的...比如下面这张图:
得到的检测结果又像黄色框那样,他们全在中间。这样也很合理,因为网络并不知道应该将哪个框对应到哪个物体上,没有我们想象的那么智能,于是就得到了上面的这个结果。所以说如果不采取一定的策略,即使输出再多的bounding boxes也没有什么用,他们最终的位置可能都会像黄色框那样,而不是想要的结果。
one-stage目标检测中有一种常用的方法,利用网格(grid)把图像分区域,每个区域内的bounding box只检测这个区域内的物体,这样巧妙的解决了我们的问题。
关于grid
Grid是one-stage目标检测中非常重要的部分之一了,他的功能有点类似于R-CNN里的region proposal,目的都是提供一定的候选区域,使得bounding box不会乱跑。
和大多数网络一样,我们会选取一个骨干网络提取特征,因为目标检测的特殊性,骨干网络会在某个图像分类的数据集上做训练,用于特征提取,比如ImageNet。像YOLO用416*416的图像作为网络输入(SSD可能是300*300),其实这些输入比传统的图像分类网络输入要大(图像分类的网络输入通常为224*224),这样做是因为目标检测中有可能会含有比较小的物体,提高网络输入也是为了提取到更多小目标的特征。那可以剔除一个这样的网络:
前Layer1~Layer6都是你的骨干网络,骨干网络可以是很多种类:ResNet、Inception或者是YOLO的Darknet都可以,这些网络用于特征的提取。有时候为了追寻网络小型化和速度的提升,比如SqueezeNet和MobileNet。
题外话:据博客作者说,他试过提高MobileNet V1的输入从224到448然后在ILSVRC 2012上训练了7个epoch,然后用MobileNet做骨干网络用于大图像做目标检测,效果比YOLO这种直接采取骨干网络416*416的输入要好。
LayerA~LayerC是骨干网络外的一些卷积层,这些层是用来训练网络得到目标bounding box以及做目标分类用的。这样我们就构建了一个目标检测网络的框架。
对于目标检测来说,有很多可用的数据集,比如 Pascal VOC有20个类别。本文假定骨干网络在ImageNet上做训练,目标检测网络在VOC上做训练。
如果你有看过YOLO或者SSD的论文的话,你会发现整个网络框架像上面那样,但是又没有这么简单,其中有很多地方可能比这个框架要复杂一些,这些会随后讲到。下面主要讲一讲grid是怎么使用的。
假定我们就用上面的这个网络来做目标检测,经过网络的卷积等操作后,最终的输出结果可能是一个13*13*25的feature map,就像下面这样:
我们称这13*13的特征图的每一个点都是一个grid cell(也就是先前提到的grid)。而每一个grid有125个通道,这125个通道被分成了5个detector。那每个grid的作用就是:限定这每个detector只负责检测目标中心点坐标落在grid上的目标。这样就解决了之前我们提到的每个bounding box不知道应该负责检测哪个物体的问题。
最终输出的特征图之所以大小为13*13和网络输入大小416有关,假设基础网络有5个pooling并且步长为2,这样相当于对输入进行了32倍的缩放,即416/32=13。如果想要最终的特征图更大一些,比如19*19,就需要更大的网络输入比如19*32=608。或者你也可以通过调整步长来达到这个结果也OK。
并且每个detector是这样构成的:
20个分类类别的置信度
4个bounding box的坐标值(中心坐标x,y,宽高width,height)
1个是否包含目标的置信度(confidence score)
以上(20+4+1)*5=125,正好每个grid有125个通道。
通常分类器可能用softmax,但是值得注意的是像YOLOV3和SSD,将每个类别看做一个单独的任务,变成了多标签分类,用sigmoid取代了softmax,这个问题后面也会提到。
所谓的confidence score是detector是否包含目标的一个衡量值,这个值通常在0~1之间,在yolo中这个值为0表明当前detector不含目标(或者说为背景),而在SSD中则是否包含目标归为了了classification中的一个类,即背景类。
至此,一共13*13个grid,每个grid含有5个detector,共13*13*5=845个detector,也就是说对于一个输入图像,将会有845个输出结果,这些结果中可能大部分都是无用的,毕竟一张图片包含的目标是有限的。可以通过confidence score去除那些无用的输出结果,最终你的检测结果可能是这样的:
有一些检测框可能产生了重叠,这也是正常的,因为相邻的grid cell可能检测到了同一个物体而产生了两个结果,只需要通过设置一个NMS阈值,就能去除重叠的检测框。
这就是grid机制来限制检测结果不要“乱跑”,但是为什么这样做会有效呢?
约束确实有效
其实上面有提到过,grid起到了约束的作用使得你的检测框不会“乱跑”,每个检测器各司其职。这样的策略在机器学习(尤其指深度学习)里是非常有效的技巧。
Grid的这种机制迫使网络的detector专注于检测图像中某些空间位置上的某些目标。在坐标回归的机制上(后面会讲到),detector只会检测中心坐标位于对应grid cell内的目标而永远不会原理当前的grid cell(当然这是通过训练得到的结果)。就像先前我们没用grid的机制的时候,检测框的结果就不会那么的准确(黄色框)。
Anchors
除了grid之外,另一个能有效提升detector检测效率的就是Anchor。Grid相当于在空间位置上对detector增加了约束,而anchor则在每个detector所检测目标的形状大小上增加了一定的约束。
前面的例子中我们的网络输出了13*13的grid,每个grid含有5个detector,但是为什么要输出5个detector而不是一个detector呢?事实上待检测目标可能位于图像的任意位置,同样的目标也会存在各种不同的大小,而对于每个detecotor来讲,每个grid中的目标大小是随机变化的,这种变化是很难通过训练来学习到。所以我们用anchor做一个类似于proposal的功能,尽量用anchor涵盖不同的物体大小,然后最终的检测结果针对于anchor做一个回归就可以了(具体如果通过anchor得到最终的目标框,后面会再讲)。
假定我们选取了5种不同形状的框,如下图:
红色的框代表着5种常见的目标框的形状和大小(以416*416的输入图像为例)。这5个框就是我们所说的anchor,以下面这种形式来记录:
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]
每个anchor含有两个值,分别代表anchor的宽和高。上面的5个anchor,最小的负责检测小而细长型的目标,最大的负责检测大而方正的目标。当然这不是绝对,实际上每个anchor检测的目标都是尺寸和形状上与之相似就行了。
这个地方要再说明一下,这个宽和高是相对于grid来讲的,或者说相对于你的最终feature map的大小,比如1.19对于13*13的feature map,在原输入图像中其实对应的是1.19*32=38这么大的一个框。
另外,在YOLOv2中的anchor还是采用这种相对于grid的大小形式,在YOLOv3中,anchor的大小已经转换成了相对于实际输入图像的大小。
在SSD中,则是将anchor的大小归一化到(0,1)的区间上,这样anchor的大小和grid的大小就无关了。
因为anchor中只包含宽和高而没有指定具体的位置,所以其实他们是我们可以事先指定的先验信息(如果包含坐标那就和图像相关了,没有办法通过先验信息来指定)。比如在YOLO中,通过对训练集中目标框的大小做kmeans聚类的方法来得到anchor,比如训练集是VOC的话,anchor聚类的结果可能就是下面这样的:
小目标被聚类到蓝色,稍大一点的聚类到红色,最大的为绿色,然后介于红色和绿色之间的大小还被聚类成了两种不同的形状。在SSD中作者没有用到k-means的方法来聚类anchor,SSD中的anchor其实和数据集是完全不相关的。
5个anchor是一个折中的较好的结果,这个结果YOLO的作者经过测试,毕竟如果更多的anchor使得你的网络每个grid会有更多的detector,这会拖慢网络的速度。
另外在YOLOv3中,作者聚类的anchor改成了9个,并且对应着三个不同的feature map,即每层输出用到了3个anchor,分别对应着小目标~大目标检测。
另外SSD的anchor不止包含宽、高还包含了x,y的坐标位置,因为YOLO在做法上有限制anchor的中心点坐标必须位于grid cell的中心位置。
模型到底是如何工作的
其实这部分主要讲的是,我们的网络是如何跟gird、anchor关联上以及如何通过网络的最终输出得到目标的位置及分类结果。
还记得上面我们的提出了一个网络结构是这样子的:
输入的416*416的RGB图像经过一系列处理变成了13*13*125的输出feature map,这个feature map里包含了我们需要的一些信息。还是要把下面这个图再贴一下:
下面这部分我按照自己的理解来阐述,不会严格按照英文原博客这部分的顺序:
13*13对应着每一个grid,每个grid含有125个通道,并且被分成了5个detector,这5个detector,每个detector针对不同的anchor大小去做回归。每个detector含有25个通道即25个数值,这25个数值对应着4个位置坐标、1个confidence score置信度以及20个分类类别(对应VOC)。
我们从4个坐标开始说,对于目标的位置,通常可以采用两个坐标点比如:xmin,ymin,xmax,ymax的这种形式;也可以采用坐标点+宽高的表示形式比如:center x,center y,width,height。这两种形式都可以,但是本文会用到坐标点+宽高的这种表示形式。
有个要注意的地方,我们说每个detector的25个数值含有4个坐标位置,其实“坐标位置”这个词是不准确的,每个detector中的值其实是“delta” value,(我觉得叫做偏移值可能更加准确)。
delta_x,delta_y:目标框的中心坐标值偏移值
delta_w,delta_h:目标框相对于anchor的偏移值
为什么是偏移值而不直接得出坐标呢?这其实和anchor有关系,前面说到anchor限定了每个detector负责检测目标的大小和形状,但是anchor并不是最终输出的目标框结果,真实的结果是在anchor的基础上加了一些“偏移”得到的(博客原文写的是:Each detector makes a prediction relative to its anchor box.)。这也就是为什么每个detector得到的是“偏移”而不是实质的坐标位置。
都是卷积的功劳
其实上文我们所做的这些工作都是卷积神经网络计算的结果,13*13*125的feature map也是网络卷积层各种操作后的杰作。
记得我们之前说过,每个grid包含125个通道,这125个通道又被分成了5个detector,即每个detector包含4个“坐标”,1个置信度,20个类别。在网络训练的过程中,我们通过定义相应的loss,“指导”网络模型能够得到想要的结果:即每个detector负责检测特定大小的目标。可能在最初的时候,网络检测的结果是完全一团乱麻,但是随着损失函数的惩罚措施和反向传播,我们的模型参数会变得越来越好。
虽然我们一直强调说每个grid含有5个detector,13*13*5将会有845个detector!但实际上因为卷积神经网络的卷积核实参数共享的,换言之对于每个grid,生成这5个detector的125个卷积核是完全一样的,只是输入的结果不一样。所以其实我们只学习了5个detector。这是一个抽象的概念,因为卷积核参数的权值共享,实际上是通过125个相同的卷积核得到了5个detector,但是因为卷积核不断的滑窗,在不同的网络输入位置做卷积,每个位置的输入结果不同,导致这5个detector会得到不一样的结果:检测到不同位置不同大小的目标。
同时这也从另一个方面解释了为什么我们强调每个grid只检测中心坐标落在grid内的目标,因为卷积神经网络的特性,卷积核会以滑窗的形式对输入image做卷积,所以得到的预测结果,总是和滑窗当前的位置是有关系的,可以理解为每个grid的感受野不相同。
YOLO SSD 大PK!
先看一下网络结构:
列举了YOLOv2、YOLOv3、SSD的网络框架,可以看到他们网络结构是有些相似的,但是也有一些区别,这一部分原博客写的有点乱,我总结一下:
YOLOv2只有一个输出的feature map,13*13;SSD有多种不同大小的输出:19*19, 10*10, 5*5, 3*3 ,2*2甚至1*1;对于YOLOv3有三个不同尺度的输出:52*52, 26*26, 13*13。
YOLOv3是通过上采样得到不同尺度输出,SSD则采用的是下采样。
对于YOLOv3,不同尺度的输出,每个grid有相同个数的detector(这个和anchor的个数有关,默认是3);对于SSD,对于不同尺度的输出,含有不同个数的detector,比如19*19可能是每个grid3~4个detector,2*3可能是每个grid含有6个detector。
之前也提到过,SSD中没有confidence score这一项,而是在类别中增加了一类“background”
YOLO的anchor是通过在数据集上做kmeans聚类得来的,而SSD的anchor则和rcnn有点像,覆盖了不同形状和尺寸的物体。
SSD有更多数量的anchor,更多尺度的输出,则对应着更多的detector和很多的输出结果,即使是MobileNet-SSD也有1917个prediction结果。更多的detector可能更有利于去检测图像中的所有目标,但是弊端是,要花费更多的处理工作去处理哪些prediction是有用的,哪些prediction是无用的,换言之也更容易误检,这就是recall和precision之间的平衡。
因为从网络输出到实际目标框的计算过程不同,二者的loss计算方式会有些许不同。这个后面会说。