简介
首先题外话,You Only Look Once,这名字我太喜欢了。
现在最新的是YOLO v3,不过要讲YOLO v3,需要YOLO v1和YOLO v2做铺垫,所以一并介绍。
注:本文参考了很多优秀的文章,本文只做个人笔记备忘使用,所有链接已在最后给出,大佬们的文章真的是超级优秀!超级感谢!
近几年来,目标检测算法取得了很大的突破。比较流行的算法可以分为两类,一类是基于Region Proposal的R-CNN系算法(R-CNN,Fast R-CNN, Faster R-CNN),它们是two-stage的,需要先使用启发式方法(selective search)或者CNN网络(RPN)产生Region Proposal,然后再在Region Proposal上做分类与回归。而另一类是Yolo,SSD这类one-stage算法,其仅仅使用一个CNN网络直接预测不同目标的类别与位置。第一类方法是准确度高一些,但是速度慢,但是第二类算法是速度快,但是准确性要低一些。本文介绍的是Yolo算法,其全称是You Only Look Once: Unified, Real-Time Object Detection,其实个人觉得这个题目取得非常好,基本上把Yolo算法的特点概括全了:You Only Look Once说的是只需要一次CNN运算,Unified指的是这是一个统一的框架,提供end-to-end的预测,而Real-Time体现是Yolo算法速度快。这里我们谈的是Yolo-v1版本算法,其性能是差于后来的SSD算法的,但是Yolo后来也继续进行改进,产生了Yolo9000和YOLO v3算法。本文将依次讲解这三个算法及其相应的改进。
补充知识:利用全卷积网络代替全连接层,实现共享卷积。
简单来说就是网络中用卷积层代替了全连接层,如下图所示。输入图片大小是16x16,经过一系列卷积操作,提取了2x2的特征图,但是这个2x2的图上每个元素都是和原图是一一对应的,如图上蓝色的格子对应蓝色的区域,这不就是相当于在原图上做大小为14x14的窗口滑动,且步长为2,共产生4个子区域。最终输出的通道数为4,可以看成4个类别的预测概率值,这样一次CNN计算就可以实现窗口滑动的所有子区域的分类预测。这其实是overfeat算法的思路。之所可以CNN可以实现这样的效果是因为卷积操作的特性,就是图片的空间位置信息的不变性,尽管卷积过程中图片大小减少,但是位置对应关系还是保存的。说点题外话,这个思路也被R-CNN借鉴,从而诞生了Fast R-cNN算法。
YOLO v1
创新
YOLO将物体检测作为回归问题求解。基于一个单独的end-to-end网络,完成从原始图像的输入到物体位置和类别的输出(一步到位)。从网络设计上,YOLO与rcnn、fast rcnn及faster rcnn的区别如下:
- YOLO训练和检测均是在一个单独网络中进行。YOLO没有显示地求取region proposal的过程。而rcnn/fast rcnn 采用分离的模块(独立于网络之外的selective search方法)求取候选框(可能会包含物体的矩形区域),训练过程因此也是分成多个模块进行。Faster rcnn使用RPN(region proposal network)卷积网络替代rcnn/fast rcnn的selective search模块,将RPN集成到fast rcnn检测网络中,得到一个统一的检测网络。尽管RPN与fast rcnn共享卷积层,但是在模型训练过程中,需要反复训练RPN网络和fast rcnn网络(注意这两个网络核心卷积层是参数共享的)。
- YOLO将物体检测作为一个回归问题进行求解,输入图像经过一次inference,便能得到图像中所有物体的位置和其所属类别及相应的置信概率。而rcnn/fast rcnn/faster rcnn将检测结果分为两部分求解:物体类别(分类问题),物体位置即bounding box(回归问题)。
相比R-CNN系列算法,YOLO是一个统一的、可端到端训练的算法,其速度更快,可以达到实时检测的效果。
网络结构
YOLO的检测思想不同于R-CNN系列的思想,它将目标检测作为回归任务来解决。
下面来看看YOLO的整体结构:
由上两图所示,网络是根据GoogLeNet改进的,输入图片为448*448大小,输出为7×7×(2×5+20),现在看来这样写输出维度很奇怪,下面来看一下输出是怎么定义的。
将图片分为S×S个格子个单元格(原文中S=7),之后的输出是以单元格为单位进行的:
- 如果一个object的中心落在某个单元格上,那么这个单元格负责预测这个物体。
- 每个单元格需要预测B个bbox值(bbox值包括坐标和宽高,原文中B=2),同时为每个bbox值预测一个置信度(confidence scores)。也就是每个单元格需要预测B×(4+1)个值。
- 每个单元格需要预测C(物体种类个数,原文C=20,这个与使用的数据库有关)个条件概率值。
所以,最后网络的输出维度为S×S×(B×5+C)。
还是不太懂? 没关系,借用知乎“小小将”文章中的解释(炒鸡棒呀有木有~),如下:
Yolo的CNN网络将输入的图片分割成 S × S S \times S S×S网格,然后每个单元格负责去检测那些中心点落在该格子内的目标,如下图所示,可以看到这只狗子的中心落在左下角一个单元格内,那么该单元格负责预测这只狗子。每个单元格会预测 B B B个边界框(bounding box)以及边界框的置信度(confidence score)。所谓边界框的置信度其实包含两个方面,一是这个边界框含有目标的可能性大小,二是这个边界框的准确度。前者记为 P r ( o b j e c t ) Pr(object) Pr(object),当该边界框是背景时(即不包含目标),此时 P r ( o b j e c t ) = 0 Pr(object) = 0 Pr(object)=0 。而当该边界框包含目标时, P r ( o b j e c t ) = 1 Pr(object) = 1 Pr(object)=1 。边界框的准确度可以用预测框与实际框(ground truth)的IOU(intersection over union,交并比)来表征,记为 I O U p r e d t r u t h IOU_{pred}^{truth} IOUpredtruth 。因此置信度可以定义为 P r ( o b j e c t ) × I O U p r e d t r u t h Pr(object) \times IOU_{pred}^{truth} Pr(object)×IOUpredtruth 。很多人可能将Yolo的边界框置信度看成边界框是否含有目标的概率,但是其实它是两个因子的乘积,预测框的准确度也反映在里面。边界框的大小与位置可以用4个值来表征: { x , y , w , h } \{x, y, w, h\} {x,y,w,h} ,其中 { x , y } \{x, y\} {x,y}是边界框的中心坐标而 w w w和 h h h是边界框的宽与高。还有一点要注意,中心坐标的预测值 { x , y } \{x, y\} {x,y} 是相对于每个单元格左上角坐标点的偏移值,并且单位是相对于单元格大小的,单元格的坐标定义下图所示。而边界框的 w w w和 h h h预测值是相对于整个图片的宽与高的比例,这样理论上4个元素的大小应该在 [ 0 , 1 ] [0, 1] [0,1]范围。这样,每个边界框的预测值实际上包含5个元素: { x , y , w , h , c } \{x, y, w, h, c\} {x,y,w,h,c},其中前4个表征边界框的大小与位置,而最后一个值是边界框的置信度。
![](https://i-blog.csdnimg.cn/blog_migrate/d09b7e8d855f1cb7b10af1fb2da65412.png)
还有分类问题,对于每一个单元格其还要给出预测出 C C C个类别概率值,其表征的是由该单元格负责预测的边界框其目标属于各个类别的概率。但是这些概率值其实是在各个边界框置信度下的条件概率,即 Pr ( Class i ∣ Object ) \operatorname{Pr}\left(\text { Class }_{i} | \text { Object }\right) Pr( Class i∣ Object )。值得注意的是,不管一个单元格预测多少个边界框,其只预测一组类别概率值,这是Yolo算法的一个缺点,在后来的改进版本中,Yolo9000是把类别概率预测值与边界框是绑定在一起的。同时,我们可以计算出各个边界框类别置信度(class-specific confidence scores):
Pr ( Class i ∣ Object ) ∗ Pr ( Object ) ∗ I O U pred truth = Pr ( Class i ) ∗ I O U pred truth \operatorname{Pr}\left(\text { Class }_{i} | \text { Object }\right) * \operatorname{Pr}(\text { Object }) * \mathrm{IOU}_{\text { pred }}^{\text { truth }}=\operatorname{Pr}\left(\text { Class }_{i}\right) * \mathrm{IOU}_{\text { pred }}^{\text { truth }} Pr( Class i∣ Object )∗Pr( Object )∗IOU pred truth =Pr( Class i)∗IOU pred truth
注:注意区分上述边界框置信度和类别置信度。
边界框类别置信度表征的是该边界框中目标属于各个类别的可能性大小以及边界框匹配目标的好坏。后面会说,一般会根据类别置信度来过滤网络的预测框。
总结一下,每个单元格需要预测 ( B ∗ 5 + C ) (B*5 + C) (B∗5+C)个值。如果将输入图片划分为 S × S S \times S S×S个网格,那么最终预测值为 S × S × ( B ∗ 5 + C ) S \times S \times (B*5+C) S×S×(B∗5+C)大小的张量。整个模型的预测值结构如下图所示。对于PASCAL VOC数据,其共有20个类别,如果使用 S = 7 B = 2 S=7 \ B=2 S=7 B=2,那么最终的预测结果就是 7 × 7 × 30 7 \times 7 \times 30 7×7×30大小的张量。在下面的网络结构中我们会详细讲述每个单元格的预测值的分布位置。
Yolo采用卷积网络来提取特征,然后使用全连接层来得到预测值。网络结构参考GooLeNet模型,包含24个卷积层和2个全连接层,如下图所示。对于卷积层,主要使用1x1卷积来做channle reduction,然后紧跟3x3卷积。对于卷积层和全连接层,采用Leaky ReLU激活函数:
m
a
x
(
x
,
0.1
x
)
max(x, 0.1x)
max(x,0.1x)。但是最后一层却采用线性激活函数。
可以看到网络的最后输出为
7
×
7
×
30
7 \times 7 \times 30
7×7×30大小的张量。这和前面的讨论是一致的。这个张量所代表的具体含义下图所示。对于每一个单元格,前20个元素是类别概率值,然后2个元素是边界框置信度,两者相乘可以得到类别置信度,最后8个元素是边界框坐标
{
x
,
y
,
w
,
h
}
\{x, y, w, h\}
{x,y,w,h} 。
损失函数
Yolo算法将目标检测看成回归问题,所以采用的是均方差损失函数。但是对不同的部分采用了不同的权重值。首先区分定位误差和分类误差。对于定位误差,即边界框坐标预测误差,采用较大的权重 λ c o o r d = 5 \lambda_{coord} = 5 λcoord=5。然后其区分不包含目标的边界框与含有目标的边界框的置信度,对于前者,采用较小的权重值 λ n o o b j = 0.5 \lambda_{noobj} = 0.5 λnoobj=0.5。其它权重值均设为1。然后采用均方误差,其同等对待大小不同的边界框,但是实际上较小的边界框的坐标误差应该要比较大的边界框要更敏感。为了保证这一点,将网络的边界框的宽与高预测改为对其平方根的预测,即预测值变为 { x , y , w , h } \{x, y, \sqrt{w}, \sqrt{h}\} {x,y,w,h}。
另外一点时,由于每个单元格预测多个边界框。但是其对应类别只有一个。那么在训练时,如果该单元格内确实存在目标,那么只选择与ground truth的IOU最大的那个边界框来负责预测该目标,而其它边界框认为不存在目标。这样设置的一个结果将会使一个单元格对应的边界框更加专业化,其可以分别适用不同大小,不同高宽比的目标,从而提升模型性能。大家可能会想如果一个单元格内存在多个目标怎么办,其实这时候Yolo算法就只能选择其中一个来训练,这也是Yolo算法的缺点之一。要注意的一点时,对于不存在对应目标的边界框,其误差项就是只有置信度,坐标项误差是没法计算的。而只有当一个单元格内确实存在目标时,才计算分类误差项,否则该项也是无法计算的。
综上,损失函数为:
其中第一项是边界框中心坐标的误差项, I i j o b j I_{ij}^{obj} Iijobj指的是第 i i i个单元格存在目标,且该单元格中的第 j j j个边界框负责预测该目标。第二项是边界框的高与宽的误差项。第三项是包含目标的边界框的置信度误差项。第四项是不包含目标的边界框的置信度误差项。而最后一项是包含目标的单元格的分类误差项, I i o b j I_{i}^{obj} Iiobj指的是第 i i i个单元格存在目标。
Inference
下面就来分析Yolo的预测过程,这里我们不考虑batch,认为只是预测一张输入图片。根据前面的分析,最终的网络输出是 7 × 7 × 30 7 \times 7 \times 30 7×7×30,但是我们可以将其分割成三个部分:类别概率部分为 [ 7 , 7 , 20 ] [7, 7, 20] [7,7,20] ,边框置信度部分为 [ 7 , 7 , 2 ] [7, 7, 2] [7,7,2],而边界框坐标部分为 [ 7 , 7 , 2 , 4 ] [7, 7, 2, 4] [7,7,2,4](对于这部分不要忘记根据原始图片计算出其真实值)。然后将前两项相乘(矩阵 [ 7 , 7 , 20 ] [7, 7, 20] [7,7,20]乘以 [ 7 , 7 , 2 ] [7, 7, 2] [7,7,2]可以各补一个维度来完成 [ 7 , 7 , 1 , 20 ] × [ 7 , 7 , 2 , 1 ] [7, 7, 1, 20] \times [7, 7, 2, 1] [7,7,1,20]×[7,7,2,1])可以得到类别置信度值为 [ 7 , 7 , 2 , 20 ] [7, 7, 2, 20] [7,7,2,20],这里总共预测了 7 × 7 × 2 = 98 7 \times 7 \times 2 = 98 7×7×2=98个边界框。
在得到边界框和对应的类别置信度之后,我们要进行NMS来得到最终的检测结果。
所有的准备数据已经得到了,那么我们先说第一种策略来得到检测框的结果,我认为这是最正常与自然的处理。首先,对于每个预测框根据类别置信度选取置信度最大的那个类别作为其预测标签,经过这层处理我们得到各个预测框的预测类别及对应的置信度值,其大小都是 [ 7 , 7 , 2 , 20 ] [7, 7, 2, 20] [7,7,2,20]。一般情况下,会设置置信度阈值,就是将置信度小于该阈值的box过滤掉,所以经过这层处理,剩余的是置信度比较高的预测框。最后再对这些预测框使用NMS算法,最后留下来的就是检测结果。一个值得注意的点是NMS是对所有预测框一视同仁,还是区分每个类别,分别使用NMS。Ng在deeplearning.ai中讲应该区分每个类别分别使用NMS,但是看了很多实现,其实还是同等对待所有的框,我觉得可能是不同类别的目标出现在相同位置这种概率很低吧。
上面的预测方法应该非常简单明了,但是对于Yolo算法,其却采用了另外一个不同的处理思路(至少从C源码看是这样的),其区别就是先使用NMS,然后再确定各个box的类别。其基本过程如图12所示。对于98个boxes,首先将小于置信度阈值的值归0,然后分类别地对置信度值采用NMS,这里NMS处理结果不是剔除,而是将其置信度值归为0。最后才是确定各个box的类别,当其置信度值不为0时才做出检测结果输出。这个策略不是很直接,但是貌似Yolo源码就是这样做的。Yolo论文里面说NMS算法对Yolo的性能是影响很大的,所以可能这种策略对Yolo更好。但是我测试了普通的图片检测,两种策略结果是一样的。
存在问题
- YOLO对相互靠的很近的物体(挨在一起且中点都落在同一个格子上的情况),还有很小的群体检测效果不好,这是因为一个网格中只预测了两个框,并且只属于一类。
- 测试图像中,当同一类物体出现的不常见的长宽比和其他情况时泛化能力偏弱。
- 由于损失函数的问题,定位误差是影响检测效果的主要原因,尤其是大小物体的处理上,还有待加强。
有问题欢迎留言交流。
done~
References
- https://zhuanlan.zhihu.com/p/32525231
- https://zhuanlan.zhihu.com/p/25236464
- https://blog.csdn.net/u014380165/article/details/72616238
- https://www.jianshu.com/p/d147b94df939
- https://www.cnblogs.com/MY0213/p/9816104.html
- https://www.cnblogs.com/zyly/p/9274472.html
- http://lanbing510.info/2017/08/28/YOLO-SSD.html
- https://wanglichun.tech/algorithm/YOLO.html
- http://www.fanyeong.com/2018/01/31/cnn-object-detection-yolo-part2/