目标检测的应用场景很广泛,我们也用得比较多,从检测公司logo,到合同文档的页眉页脚,到楼道里的消防设备等,趁着周末就总结一下。
在开始之前,我们先区分一下“检测”与“识别”的区别,这是很容易混淆的概念。检测通常是指检测出图像上某些区域有什么东西,例如目标检测,人脸检测,行人检测,文字检测等,但是并不会输出这个人脸是谁,这个行人是谁,这个文字是什么字,当然目标检测是会输出这个目标是什么类别的。而对应的人脸识别,行人重识别,文字识别等,就是要识别出具体的内容了。
看完这个文章,你应该能基本理解yolo v1的模型原理。
0x01 目标检测
目标检测做的是检测出图像上哪些区域有我们关注的什么类别的目标,从这个理解上,任务就是分成两个步骤的:
- 检测出图像的哪些区域有目标;
- 对这些目标做分类。
例如下面一个图像:
图像来自作者论文:https://arxiv.org/pdf/1506.02640.pdf
图中框框框住的区域可能就是我们感兴趣的目标了,对于不同的任务场景,大家关注的目标往往是不同的。
应该说目标检测是我们人类视觉无时无刻不在执行的功能,我们没看一眼这个世界,都在做目标检测。从我们的经验来说,目标检测应该也是分成两个步骤的,就像我们在地上找一个纽扣一样:
- 先粗略看地面上有哪些区域可能有类似纽扣的;
- 再凑近点看看,这个区域是否真的有纽扣。
这是很自然的思路(二阶段,two-stage),先找到可能的候选框,再对候选框进行判断,这就是RCNN家族干的事(这里有点大言不惭,RCNN相关的算法并没有深入了解过):先是有人搞出了一个RCNN,有人觉得这个太慢了,于是升级成了Fast RCNN,可还是有人觉得太慢,于是又升级成了Faster RCNN,再下去单词就不好用了,改一下吧,变成了Mask RCNN。
0x02 Yolo v1
对于我们来说,我们是要给企业解决问题的,不是要发论文,我们上来通常就是yolo,它指标可能不一定时候最好的,可它够快。当然我们用的时候yolov3或者v4,不是我们今天要讲的古董v1,不过v1是他们的祖宗,我们就从他们的祖宗开始讲起。
前面我们说了,我们人类视觉在进行目标检测时可能也是two stage的,那我们能否一步(one stage)到位呢?对于目标检测,对于每个目标我期待的输出是:
代码语言:javascript复制(x, y, w, h, p1, p2, ..., pk)
其中(x, y)是目标中心点,(w, h)是目标box的宽高,而(p1, p2, ..., pk)是该box属于k个类别目标的概率。对应到我们的标注数据,一个box只会属于一个类别,例如该box属于类别m,那么:
代码语言:javascript复制pi=1, 如果i等于m
pi=0, 如果i不等于m
对于这个多维的向量,显然我们是可以计算损失(loss)的。关键问题还是前面的box怎么生成,如果整个图像去生成,那计算量可大了。不然怎么办呢?
yolo v1的做法是,先将图像分成7*7的网格,共有49个格子,如:
狗狗这个目标的中心点就落在上图的蓝色网格内,就用这个格子来复杂对狗狗的预测,而格子内的红点正是狗狗实际的中心点。
每个格子的参数对应两个2边界框(Bounding Box,简称bbox),每个bbox有5个参数:
代码语言:javascript复制x, y, w, h, c
其中(x, y)是中心点,(w, h)是宽高,c是置信度,可以理解为该bbox有目标的概率,显然该值越大,则表示该bbox越可能有我们关注的目标。
此外每个网格还会有一组类别概率值:
代码语言:javascript复制p1, p2, ..., p20
表示该格子里属于各个类别的概率大小,因为作者的目标有20个类别,所有有20个概率值。正因为这里只有一组概率值,所以一个网络最多只能预测一个目标,如果多个目标出现在一个网格内,就会出现漏检。
组合起来,这样我们每个网格的参数量就会有:
代码语言:javascript复制# 2个bbox,每个bbox有5个参数
2*5 20 = 30
当我们输入一个图像,其需要预测输出的值就有:
代码语言:javascript复制7 * 7 * 30 = 1470
0x03 标注数据是怎么跟输出关联起来的
我们知道了预测结果是输出1470个值,可我们标注的数据可不是这样的,需要先将我们标注的数据处理成和输出一样的格式,否则模型训练时就没法计算损失。上面说过,我们标注的数据可以处理成这种格式:
代码语言:javascript复制(x, y, w, h, p1, p2, ..., p20)
其中目标类别概率p1到p20,有一个值为1,其余为0。如果我们在图像上标注了两个目标,那就会有两组这样的数据。
处理过程大概这样:
- 初始化一个7*7*30的张量,所有值为0;
- 循环处理每个标注的目标,当前目标得到一组向量:(x, y, w, h, p1, p2, ..., p20);
- 先计算该目标的中心点(x, y)落在在哪个网格上,就用该网格来负责预测该目标。假设该网格的左上角坐标为(x0, y0),宽高为(w0, h0),那么该目标在该网格上的中心点坐标就是:( (x-x0)/w0, (y-y0)/h0 ),其实就是目标中心点坐标相对于网格左上角的相对偏移值,显然坐标的x和y都是0到1之间的值。
- 而bbox的宽高计算则简单一些(w/img_w, h/img_h),对应除以图像的宽高即可。
- bbox是否有对象的置信度自然都是1了,后面的类别概率就是(p1, p2, ..., p20)。注意,bbox是由两组的,只是值都是一样的。
- 回到步骤2。
这样我们就将每个标注好的图像的目标值,都转化成了7*7*30的张量,输入和输出也就关联了起来,而且每个值的取值范围都在0到1之间,也就可以计算损失了。
正式有了这样的设计,所有也就能one stage了,一次就将这些值回归出来。
0x04 为什么是2个bbox
这个2一直让我挺纠结,为什么是2个,而不是3个,4个,或者1个?和同事讨论这个问题,得到的结论就是这是作者的实验结果,发现设置为2的时候,效果最好。
不过对此,我一直有疑惑,因为2个bbox,每个bbox会有一个概率值来判断该bbox是否包含了目标,但是后面只有一组类别概率值,也就是说,这两个bbox不论哪个大哪个小,最终都只能指向一组类别概率,也就是说一个网络最多只能预测一个目标。那这两个bbox的意义何在呢?为了使得损失最小,那训练的最后是不是这两个bbox都会趋向标注值?因为只有趋向标注值,损失才会越来越小。
显然,只要每个bbox对应一组类别概率,就可能可以解决重叠目标的漏检问题。
0x05 损失函数
有了上面的分析,其实我们自己都能构造一个损失函数,差别只是效果够不够好而已。我们就直接看作者论文定义的损失函数:
在作者的论文中,S=7,B=2,就是7*7的网格,每个网格2个bbox。损失函数共分为几个部分:
- 第一部分:中心点的损失;
- 第二部分:宽高的损失;
- 第三部分:置信度的损失;
- 第四部分:包含目标的网格的bbox的置信度的损失;
- 第五部分:不包含目标的网格的bbox的置信度的损失;
- 第六部分:类别概率的损失,这部分损失只跟网格有关,跟bbox无关。
这个损失函数的定义很好理解,比较技巧性的地方我看有两个:
- 置信度的损失拆成了两个部分,对于没有目标的部分前面乘了一个超参数,在作者的论文里,该值是一个小于0的值,显然是要降低这部分的影响。毕竟一个图像上的目标通常是比较少的,也就是说大多数网格其实都是没有对应的目标的,通过超参数来降低影响也在情理之中。
- 宽高的损失计算是,先对宽和高做了一次开根号,这是一个细节。因为目标的宽高往往是比图像的宽高小很多的,这时目标的宽高除以图像的宽高就会变得比较小,这里开一个根号实际是增大了宽高的差异。(当然如果你开三次方应该也是一样可以的,效果估计也差不多吧)注意:宽高的取值与中心点的取值的细微差异。
损失函数是为针对任务设计的,只要能达到目的其实都是可以的,但是这个设计的好与不好,对训练往往也是影响很大的。
问题来了,如果你来设计这个损失函数,你是否能想到这些点呢?
0x06 后记
这是yolo系列的第一篇,也是理解Yolo v1的第一步,理解的关键我觉得就是模型预测目标的设计,正是有了这个设计,所以才能实现one stage。
待续。