内容来自CS230课程。
目录
目标定位(Object localization)
特征点检测(Landmark detection)
基于滑动窗口的目标检测算法
滑动窗口的卷积实现 (Convolutional implementation of sliding windows)
网络中的网络以及 1*1 卷积(Network in Network and 1×1 convolutions)
Bounding Box 预测(Bounding box predictions)
交并比 IOU(Intersection over union)
非极大值抑制(Non-max suppression)
Anchor Boxes
目标定位(Object localization)
在构建对象检测之前,我们先了解一下对象定位,首先我们看看它的定义。
图片分类任务我们已经熟悉了,就是算法遍历图片,判断其中的对象是不是汽车,这就是图片分类。这节课我们要学习构建神经网络的另一个问题,即定位分类问题。这意味着,我们不仅要用算法判断图片中是不是一辆汽车,还要在图片中标记出它的位置,用边框或红色方框把汽车圈起来,这就是定位分类问题。其中“定位”的意思是判断汽车在图片中的具体位置。当图片中有多个对象时,应该如何检测它们,并确定出位置。比如,你正在做一个自动驾驶程序,程序不但要检测其它车辆,还要检测其它对象,如行人、摩托车等等。
本周我们要研究的分类定位问题,通常只有一个较大的对象位于图片中间位置,我们要对它进行识别和定位。而在对象检测问题中,图片可以含有多个对象,甚至单张图片中会有多个不同分类的对象。因此,图片分类的思路可以帮助学习分类定位,而对象定位的思路又有助于学习对象检测,我们先从分类和定位开始讲起。
图片分类问题你已经并不陌生了,例如,输入一张图片到多层卷积神经网络。这就是卷积神经网络,它会输出一个特征向量,并反馈给 softmax 单元来预测图片类型。
如果你正在构建汽车自动驾驶系统,那么对象可能包括以下几类:行人、汽车、摩托车和背景,这意味着图片中不含有前三种对象,也就是说图片中没有行人、汽车和摩托车,输出结果会是背景对象,这四个分类就是 softmax 函数可能输出的结果。
这就是标准的分类过程,如果你还想定位图片中汽车的位置,该怎么做呢?我们可以让神经网络多输出几个单元,输出一个边界框。具体说就是让神经网络再多输出 4 个数字,标记为𝑏𝑥,𝑏𝑦,𝑏ℎ和𝑏𝑤,这四个数字是被检测对象的边界框的参数化表示。(这里其实是YOLO算法的思想,将检测问题当作回归问题来做)
图片左上角的坐标为(0,0),右下角标记为 (1,1)。要确定边界框的具体位置,需要指定红色方框的中心点,这个点表示为(𝑏𝑥,𝑏𝑦),边界框的高度为𝑏ℎ,宽度为𝑏𝑤。因此训练集不仅包含神经网络要预测的对象分类标签,还要包含表示边界框的这四个数字,接着采用监督学习算法,输出一个分类标签,还有四个参数值,从而给出检测对象的边框位置。此例中,𝑏𝑥的理想值是 0.5,因为它表示汽车位于图片水平方向的中间位置;𝑏𝑦大约是 0.7,表示汽车位于距离图片底部 3 /10 的位置;𝑏ℎ约为 0.3,因为红色方框的高度是图片高度的 0.3 倍;𝑏𝑤约为 0.4,红色方框的宽度是图片宽度的 0.4 倍。
这是一个四分类问题,神经网络输出的是这四个数字和一个分类标签,或分类标签出现的概率。目标标签𝑦的定义如下:
它是一个向量,第一个组件𝑝𝑐表示是否含有对象,如果对象属于前三类(行人、汽车、摩托车),则𝑝𝑐 = 1,如果是背景,则图片中没有要检测的对象,则𝑝𝑐 = 0。我们可以这样理解𝑝𝑐,它表示被检测对象属于某一分类的概率,背景分类除外。
如果检测到对象,就输出被检测对象的边界框参数𝑏𝑥、𝑏𝑦、𝑏ℎ和𝑏𝑤。最后,如果存在某个对象,那么𝑝𝑐 = 1,同时输出𝑐1、𝑐2和𝑐3,表示该对象属于 1-3 类中的哪一类,是行人,汽车还是摩托车。鉴于我们所要处理的问题,我们假设图片中只含有一个对象,所以针对这个分类定位问题,图片最多只会出现其中一个对象。 这是图片中只有一个检测对象的情况,如果图片中没有检测对象呢?如果训练样本是这样一张图片呢?
这种情况下,𝑝𝑐 = 0,𝑦的其它参数将变得毫无意义,这里我全部写成问号,表示“毫无意义”的参数,因为图片中不存在检测对象,所以不用考虑网络输出中边界框的大小,也不用考虑图片中的对象是属于𝑐1、𝑐2和𝑐3中的哪一类。针对给定的被标记的训练样本,不论图片中是否含有定位对象,构建输入图片𝑥和分类标签𝑦的具体过程都是如此。这些数据最终定义了训练集。
特征点检测(Landmark detection)
上面讲了,通过输出四个参数值𝑏𝑥、𝑏𝑦、𝑏ℎ 和𝑏𝑤给出图片中对象的边界框。更概括地说,神经网络可以通过输出图片上特征点的(𝑥, 𝑦) 坐标来实现对目标特征的识别,我们看几个例子。
假设你正在构建一个人脸识别应用,出于某种原因,你希望算法可以给出眼角的具体位置。眼角坐标为(𝑥, 𝑦),你可以让神经网络的最后一层多输出两个数字𝑙𝑥和𝑙𝑦,作为眼角的坐标值。如果你想知道两只眼睛的四个眼角的具体位置,那么从左到右,依次用四个特征点来表示这四个眼角。对神经网络稍做些修改,输出第一个特征点(𝑙1𝑥,𝑙1𝑦),第二个特征点 (𝑙2𝑥,𝑙2𝑦),依此类推,这四个脸部特征点的位置就可以通过神经网络输出了。也许除了这四个特征点,你还想得到更多的特征点输出值,这些(图中眼眶上的红色特征点)都是眼睛的特征点,你还可以根据嘴部的关键点输出值来确定嘴的形状,从而判断人物是在微笑还是皱眉,也可以提取鼻子周围的关键特征点。为了便于说明,你可以设定特征点的个数,假设脸部有 64 个特征点,有些点甚至可以帮助你定义脸部轮廓或下颌轮廓。选 定特征点个数,并生成包含这些特征点的标签训练集,然后利用神经网络输出脸部关键特征点的位置。
具体做法是,准备一个卷积网络和一些特征集,将人脸图片输入卷积网络,输出 1 或 0,1 表示有人脸,0 表示没有人脸,然后输出(𝑙1𝑥,𝑙1𝑦)……直到(𝑙64𝑥,𝑙64𝑦)。这里我用𝑙代表一个特征,这里有129 个输出单元,其中1表示图片中有人脸,因为有64个特征,64×2=128,所以最终输出 128+1=129 个单元,由此实现对图片的人脸检测和定位。这只是一个识别脸部表情的基本构造模块,如果你玩过 Snapchat 或其它娱乐类应用,你应该对 AR(增强现实)过滤器多少有些了解,Snapchat 过滤器实现了在脸上画皇冠和其他一些特殊效果。检测脸部特征也是计算机图形效果的一个关键构造模块,比如实现脸部扭曲,头戴皇冠等等。当然为了构建这样的网络,你需要准备一个标签训练集,也就是图片𝑥和标签𝑦的集合,这些点都是人为辛苦标注的。
最后一个例子,如果你对人体姿态检测感兴趣,你还可以定义一些关键特征点,如胸部的中点,左肩,左肘,腰等等。然后通过神经网络标注人物姿态的关键特征点,再输出这些标注过的特征点,就相当于输出了人物的姿态动作。当然,要实现这个功能,你需要设定这些关键特征点,从胸部中心点(𝑙1𝑥,𝑙1𝑦)一直往下,直到(𝑙32𝑥,𝑙32𝑦)。
要明确一点,特征点 1 的特性在所有图片中必须保持一致,就好比,特征点 1 始终是右眼的外眼角,特征点 2 是右眼的内眼角,特征点 3 是左眼内眼角,特征点 4 是左眼外眼角等等。所以标签在所有图片中必须保持一致,假如你雇用他人或自己标记了一个足够大的数据集,那么神经网络便可以输出上述所有特征点,你可以利用它们实现其他有趣的效果,比如判断人物的动作姿态,识别图片中的人物表情等等。
基于滑动窗口的目标检测算法
这节学习如何通过卷积网络进行对象检测,采用的是基于滑动窗口的目标检测算法。
假如你想构建一个汽车检测算法,步骤是,首先创建一个标签训练集,也就是𝑥和𝑦表示适当剪切的汽车图片样本,这张图片(编号 1)𝑥是一个正样本,因为它是一辆汽车图片,这几张图片(编号 2、3)也有汽车,但这两张(编号 4、5)没有汽车。出于我们对这个训练集的期望,你一开始可以使用适当剪切的图片,就是整张图片𝑥几乎都被汽车占据,你可以照张照片,然后剪切,剪掉汽车以外的部分,使汽车居于中间位置,并基本占据整张图片。有了这个标签训练集,你就可以开始训练卷积网络了,输入这些适当剪切过的图片(编号 6),卷积网络输出𝑦,0 或 1 表示图片中有汽车或没有汽车。训练完这个卷积网络,就可以用它来实现滑动窗口目标检测,具体步骤如下:
假设这是一张测试图片,首先选定一个特定大小的窗口,比如图片下方这个窗口,将这个红色小方块输入卷积神经网络,卷积网络开始进行预测,即判断红色方框内有没有汽车。滑动窗口目标检测算法接下来会继续处理第二个图像,即红色方框稍向右滑动之后的区域,并输入给卷积网络,因此输入给卷积网络的只有红色方框内的区域,再次运行卷积网络,然后处理第三个图像,依次重复操作,直到这个窗口滑过图像的每一个角落。
为了滑动得更快,我这里选用的步幅比较大,思路是以固定步幅移动窗口,遍历图像的每个区域,把这些剪切后的小图像输入卷积网络,对每个位置按 0 或 1 进行分类,这就是所谓的图像滑动窗口操作。
重复上述操作,不过这次我们选择一个更大的窗口,截取更大的区域,并输入给卷积神 经网络处理,你可以根据卷积网络对输入大小调整这个区域,然后输入给卷积网络,输出 0 或 1。再以某个固定步幅滑动窗口,重复以上操作,遍历整个图像,输出结果。不论汽车在图片的什么位置,总有一个窗口可以检测到它。
这种算法叫作滑动窗口目标检测,因为我们以某个步幅滑动这些方框窗口遍历整张图片,对这些方形区域进行分类,判断里面有没有汽车。
滑动窗口目标检测算法也有很明显的缺点,就是计算成本,因为你在图片中剪切出太多小方块,卷积网络要一个个地处理。如果你选用的步幅很大,显然会减少输入卷积网络的窗口个数,但是粗糙间隔尺寸可能会影响性能。反之,如果采用小粒度或小步幅,传递给卷积网络的小窗口会特别多,这意味着超高的计算成本。
所以在神经网络兴起之前,人们通常采用更简单的分类器进行对象检测,比如通过采用手工处理工程特征的简单的线性分类器来执行对象检测。至于误差,因为每个分类器的计算成本都很低,它只是一个线性函数,所以滑动窗口目标检测算法表现良好,是个不错的算法。然而,卷积网络运行单个分类人物的成本却高得多,像这样滑动窗口太慢。除非采用超细粒度或极小步幅,否则无法准确定位图片中的对象。
不过,庆幸的是,计算成本问题已经有了很好的解决方案,大大提高了卷积层上应用滑动窗口目标检测器的效率。
滑动窗口的卷积实现 (Convolutional implementation of sliding windows)
为了构建滑动窗口的卷积应用,首先要知道如何把神经网络的全连接层转化成卷积层。这里先讲解一下 1*1 卷积核。
网络中的网络以及 1*1 卷积(Network in Network and 1×1 convolutions)
过滤器为 1×1,这里是数字 2,输入一张 6×6×1 的图片,然后对它做卷积,起过滤器大小为 1×1×1,结果相当于把这个图片乘以数字 2,所以前三个单元格分别是 2、4、6 等等。用 1×1 的过滤器进行卷积,似乎用处不大,只是对输入矩阵乘以某个数字。但这仅仅是对于 6×6×1 的一个通道图片来说,1×1 卷积效果不佳。
如果是一张 6×6×32 的图片,那么使用 1×1 过滤器进行卷积效果更好。具体来说,1×1 卷积所实现的功能是遍历这 36 个单元格,计算左图中 32 个数字和过滤器中 32 个数字的元素积之和,然后应用 ReLU 非线性函数。
我们以其中一个单元为例,它是这个输入层上的某个切片,用这 36 个数字乘以这个输入层上 1×1 切片,得到一个实数,像这样把它画在输出中。
这个 1×1×32 过滤器中的 32 个数字可以这样理解,一个神经元的输入是 32 个数字(输入图片中左下角位置 32 个通道中的数字),即相同高度和宽度上某一切片上的 32 个数字,这 32 个数字具有不同通道,乘以 32 个权重(将过滤器中的 32 个数理解为权重),然后应用 ReLU 非线性函数,在这里输出相应的结果。
一般来说,如果过滤器不止一个,而是多个,就好像有多个输入单元,其输入内容为一个切片上所有数字,输出结果是 6×6 过滤器数量。所以 1×1 卷积可以从根本上理解为对这 32 个不同的位置都应用一个全连接层,全连接层的作用是输入 32 个数字(过滤器数量标记为𝑛𝐶[𝑙+1],在这 36 个单元上重复此过程),输出结果是 6×6×#filters(过滤器数量),以便在输入层上实施一个非平凡(non-trivial)计算。
这种方法通常称为 1×1 卷积,有时也被称为 Network in Network。1×1 卷积或 Network in Network 这种理念很有影响力,很多神经网络架构都受到它的影响,比如Inception网络就受到了这种理念的影响。
举个 1×1 卷积的例子,相信对大家有所帮助,这是它的一个应用。
假设这是一个 28×28×192 的输入层,你可以使用池化层压缩它的高度和宽度,这个过程我们很清楚。但如果通道数量很大,该如何把它压缩为 28×28×32 维度的层呢?你可以用 32 个大小为 1×1 的过滤器,严格来讲每个过滤器大小都是 1×1×192 维,因为过滤器中通道数量必须与输入层中通道的数量保持一致。但是你使用了 32 个过滤器,输出层为 28×28×32,这就是压缩通道数(𝑛𝑐)的方法,对于池化层我只是压缩了这些层的高度和宽度。
当然如果你想保持通道数 192 不变,这也是可行的,1×1 卷积只是添加了非线性函数,当然也可以让网络学习更复杂的函数,比如,我们再添加一层,其输入为 28×28×192,输出为 28×28×192。
1×1 卷积层就是这样实现了一些重要功能的(doing something pretty non-trivial),它给神经网络添加了一个非线性函数,从而减少或保持输入层中的通道数量不变。
回到如何把神经网络的全连接层转化成卷积层。假设对象检测算法输入一个 14×14×3 的图像,图像很小,不过演示起来方便。在这里过 滤器大小为 5×5,数量是 16,14×14×3 的图像在过滤器处理之后映射为 10×10×16。然后通过参数为 2×2 的最大池化操作,图像减小到 5×5×16。然后添加一个连接 400 个单元的全连接层,接着再添加一个全连接层,最后通过 softmax 单元输出𝑦。为了跟下图区分开,我先做一点改动,用 4 个数字来表示𝑦,它们分别对应 softmax 单元所输出的 4 个分类出现的概率。 这 4 个分类可以是行人、汽车、摩托车和背景或其它对象。
接下来就是如何把这些全连接层转化为卷积层,画一个这样的卷积网络,它的前几层和之前的一样,而对于下一层,也就是这个全连接层,我们可以用 5×5 的过滤器来实现,数量是 400 个(编号 1 所示),输入图像大小为 5×5×16,用 5×5 的过滤器对它进行卷积操作,过滤器实际上是 5×5×16,因为在卷积过程中,过滤器会遍历这 16 个通道,所以这两处的通道数量必须保持一致,输出结果为 1×1。假设应用 400 个这样的 5×5×16 过滤器,输出维度就是 1×1×400,我们不再把它看作一个含有 400 个节点的集合,而是一个 1×1×400 的输出层。从数学角度看,它和全连接层是一样的,因为这 400 个节点中每个节点都有一个 5×5×16 维度的过滤器,所以每个值都是上一层这些 5×5×16 激活值经过某个任意线性函数的输出结果。
我们再添加另外一个卷积层,这里用的是 1×1 卷积,假设有 400 个 1×1 的过滤器,在这 400 个过滤器的作用下,下一层的维度是 1×1×400,它其实就是上个网络中的这一全连接层。最后经由 1×1 过滤器的处理,得到一个 softmax 激活值,通过卷积网络,我们最终得到这个 1×1×4 的输出层,而不是这 4 个数字。
掌握了卷积知识,我们再看看如何通过卷积实现滑动窗口对象检测算法。
假设向滑动窗口卷积网络输入 14×14×3 的图片,为了简化演示和计算过程,这里我们依然用14×14的小图片。和前面一样,神经网络最后的输出层,即softmax单元的输出是1×1×4,我画得比较简单,严格来说,14×14×3 应该是一个长方体,第二个 10×10×16 也是一个长方体,但为了方便,我只画了正面。所以,对于 1×1×400 的这个输出层,我也只画了它 1×1 的那一面,所以这里显示的都是平面图,而不是 3D 图像。
假设输入给卷积网络的图片大小是 14×14×3,测试集图片是 16×16×3,现在给这个输入 图片加上黄色条块,在最初的滑动窗口算法中,你会把这片蓝色区域输入卷积网络(红色笔标记)生成 0 或 1 分类。接着滑动窗口,步幅为 2 个像素,向右滑动 2 个像素,将这个绿框区域输入给卷积网络,运行整个卷积网络,得到另外一个标签 0 或 1。继续将这个橘色区域输入给卷积网络,卷积后得到另一个标签,最后对右下方的紫色区域进行最后一次卷积操作。我们在这个 16×16×3 的小图像上滑动窗口,卷积网络运行了 4 次,于是输出了了 4 个标签。
结果发现,这 4 次卷积操作中很多计算都是重复的。所以执行滑动窗口的卷积时使得卷积网络在这 4 次前向传播过程中共享很多计算。卷积网络运行同样的参数,使得相同的 5×5×16 过滤器进行卷积操作,得到 12×12×16 的输出层。然后执行同样的最大池化,输出结果 6×6×16。照旧应用 400 个 5×5 的过滤器,得到一个 2×2×400 的输出层,现在输出层为 2×2×400,而不是 1×1×400。应用 1×1 过滤器得到另一个 2×2×400 的输出层。再做一次全连接的操作,最终得到 2×2×4 的输出层,而不是 1×1×4。最终,在输出层这 4 个子方块中,蓝色的是图像左上部分 14×14 的输出(红色箭头标识),右上角方块是图像右上部分(绿色箭头标识)的对应输出,左下角方块是输入层左下角(橘色箭头标识),也就是这个 14×14 区域经过卷积网络处理后 的结果,同样,右下角这个方块是卷积网络处理输入层右下角 14×14 区域(紫色箭头标识)的结果。
所以该卷积操作的原理是我们不需要把输入图像分割成四个子集,分别执行前向传播,而是把它们作为一张图片输入给卷积网络进行计算,其中的公共区域可以共享很多计算,就像这里我们看到的这个 4 个 14×14 的方块一样。
下面我们再看一个更大的图片样本,假如对一个 28×28×3 的图片应用滑动窗口操作,如果以同样的方式运行前向传播,最后得到 8×8×4 的结果。跟上一个范例一样,以 14×14 区域滑动窗口,首先在这个区域应用滑动窗口,其结果对应输出层的左上角部分。接着以大小为 2 的步幅不断地向右移动窗口,直到第 8 个单元格,得到输出层的第一行。然后向图片下方移动,最终输出这个 8×8×4 的结果。因为最大池化参数为 2,相当于以大小为 2 的步幅在原始图片上应用神经网络。
总结一下滑动窗口的实现过程,在图片上剪切出一块区域,假设它的大小是 14×14,把它输入到卷积网络。继续输入下一块区域,大小同样是 14×14,重复操作,直到某个区域识别到汽车。但是正如在前一页所看到的,我们不能依靠连续的卷积操作来识别图片中的汽车,比如,我们可以对大小为 28×28 的整张图片进行卷积操作,一次得到所有预测值,如果足够幸运,神经网络便可以识别出汽车的位置。
以上就是在卷积层上应用滑动窗口算法的内容,它提高了整个算法的效率。不过这种算法仍然存在一个缺点,就是边界框的位置可能不够准确。
Bounding Box 预测(Bounding box predictions)
滑动窗口法的卷积实现,这个算法效率更高,但仍然存在问题,不能输出最精准的边界框。
其中一个能得到更精准边界框的算法是 YOLO 算法,YOLO(You only look once)意思是你只看一次,这是由 Joseph Redmon,Santosh Divvala,Ross Girshick 和 Ali Farhadi 提出的算法。
是这么做的,比如你的输入图像是 100×100 的,然后在图像上放一个网格。为了介绍起来简单一些,我用 3×3 网格,实际实现时会用更精细的网格,可能是 19×19。基本思路是使用图像分类和定位算法,前几个视频介绍过的,然后将算法应用到 9 个格子上。(基本思路是,采用图像分类和定位算法,本周第一个视频中介绍过的,逐一应用在图像的 9 个格子中。)更具体一点,你需要这样定义训练标签,所以对于 9 个格子中的每一个指定一个标签𝑦,
𝑦是 8 维的,和你之前看到的一样𝑝𝑐等于 0 或 1 取决于这个绿色格子中是否 有图像。然后𝑏𝑥、𝑏𝑦、𝑏ℎ和𝑏𝑤作用就是,如果那个格子里有对象,那么就给出边界框坐标。 然后𝑐1、𝑐2和𝑐3就是你想要识别的三个类别,背景类别不算,所以你尝试在背景类别中识别 行人、汽车和摩托车,那么𝑐1、𝑐2和𝑐3可以是行人、汽车和摩托车类别。这张图里有 9 个格子,所以对于每个格子都有这么一个向量。
我们看看左上方格子,这里这个(编号 1),里面什么也没有,所以左上格子的标签向量𝑦是
。
那第四个格子呢,讲的更具体一点,这张图有两个对象,YOLO 算法做的就是,取两个对象的中点,然后将这个对象分配给包含对象中点的格子。所以左边的汽车就分配到这个格子上,然后这辆 Condor(车型:神鹰)中点在这里,分配给这个格子。所以即使中心格子同时有两辆车的一部分,我们就假装中心格子没有任何我们感兴趣的对象,所以对于中心格子,分类标签𝑦和这个向量类似,和这个没有对象的向量类似,即而对于这个格子,这个用绿色框起来的格子,目标标签就是这样的,这里有一个对象,𝑝𝑐 = 1,然后你写出𝑏𝑥、𝑏𝑦、𝑏ℎ和𝑏𝑤来指定边界框位置,然后还有类别 1 是行人,那么𝑐1 = 0,类别 2 是汽车,所以𝑐2 = 1,类别 3 是摩托车,则数值𝑐3 = 0,即𝑦 =
。右边这个格子也是类似的,因为这里确实有一个对象,它的向量应该是这个样子的,
作为目标向量对应右边的格子。对于这个例子中,左上格子是 1×1×8,对应的是 9 个格子中左上格子的输出向量。所以对于这 3×3 中每一个位置而言,对于这 9 个格子,每个都对应一个 8 维输出目标向量𝑦,其中一些值可以是 dont care(即?),如果这里没有对象的话。所以总的目标输出,这个图片的输出标签尺寸就是 3×3×8。
如果你现在要训练一个输入为 100×100×3 的神经网络,现在这是输入图像,然后你有一个普通的卷积网络,卷积层,最大池化层等等,最后你会有这个,选择卷积层和最大池化层,这样最后就映射到一个 3×3×8 输出尺寸。所以你要做的是,有一个输入𝑥,就是这样的输入图像,然后你有这些 3×3×8 的目标标签𝑦。当你用反向传播训练神经网络时,将任意输入𝑥映射到这类输出向量𝑦。
这个算法的优点在于神经网络可以输出精确的边界框,所以测试的时候,你做的是喂入输入图像𝑥,然后跑正向传播,直到你得到这个输出𝑦。然后对于这里 3×3 位置对应的 9 个输出,我们在输出中展示过的,你就可以读出 1 或 0,你就知道 9 个位置 之一有个对象。如果那里有个对象,那个对象是什么,还有格子中这个对象的边界框是什么。只要每个格子中对象数目没有超过 1 个,这个算法应该是没问题的。一个格子中存在多个对象的问题,我们稍后再讨论。但实践中,我们这里用的是比较小的 3×3 网格,实践中你可能会使用更精细的 19×19 网格,所以输出就是 19×19×8。这样的网格精细得多,那么多个对象分配到同一个格子得概率就小得多。
重申一下,把对象分配到一个格子的过程是,你观察对象的中点,然后将这个对象分配到其中点所在的格子,所以即使对象可以横跨多个格子,也只会被分配到 9 个格子其中之一,就是 3×3 网络的其中一个格子,或者 19×19 网络的其中一个格子。在 19×19 网格中,两个对象的中点(图中蓝色点所示)处于同一个格子的概率就会更低。
所以要注意,首先这和图像分类和定位算法非常像,我们在本周第一节课讲过的,就是它显式地输出边界框坐标,所以这能让神经网络输出边界框,可以具有任意宽高比,并且能输出更精确的坐标,不会受到滑动窗口分类器的步长大小限制。其次,这是一个卷积实现,你并没有在 3×3 网格上跑 9 次算法,或者,如果你用的是 19×19 的网格,19 平方是 361 次,所以你不需要让同一个算法跑 361 次。相反,这是单次卷积实现,但你使用了一个卷积网络,有很多共享计算步骤,在处理这 3×3 计算中很多计算步骤是共享的,或者你的 19×19 的网格,所以这个算法效率很高。
这也是YOLO 算法它受欢迎的原因,因为这是一个卷积实现,实际上它的运行速度非常快,可以达到实时识别。
交并比 IOU(Intersection over union)
# GRADED FUNCTION: ioudef iou(box1, box2):"""Implement the intersection over union (IoU) between box1 and box2Arguments:box1 -- first box, list object with coordinates (x1, y1, x2, y2)box2 -- second box, list object with coordinates (x1, y1, x2, y2)"""# Calculate the (y1, x1, y2, x2) coordinates of the intersection of box1 and box2. Calculate its Area.### START CODE HERE ### (≈ 5 lines)xi1 = max(box1[0], box2[0])yi1 = max(box1[1], box2[1])xi2 = min(box1[2], box2[2])yi2 = min(box1[3], box2[3])inter_area = (xi2-xi1)*(yi2-yi1) ### END CODE HERE ### # Calculate the Union area by using Formula: Union(A,B) = A + B - Inter(A,B)### START CODE HERE ### (≈ 3 lines)box1_area = (box1[2]-box1[0]) * (box1[3]-box1[1])box2_area = (box2[2]-box2[0]) * (box2[3]-box2[1])union_area = box1_area + box2_area - inter_area### END CODE HERE #### compute the IoU### START CODE HERE ### (≈ 1 line)iou = inter_area/union_area### END CODE HERE ###return iou
一般约定,在计算机检测任务中,如果𝑙𝑜𝑈 ≥ 0.5,就说检测正确,如果预测器和实际边界框完美重叠,loU 就是 1,因为交集就等于并集。但一般来说只要𝑙𝑜𝑈 ≥ 0.5,那么结果是可以接受的,看起来还可以。一般约定,0.5 是阈值,用来判断预测的边界框是否正确。一般是这么约定,但如果你希望更严格一点,你可以将 loU 定得更高,比如说大于 0.6 或者更大的数字,但 loU 越高,边界框越精确,但很少见到有人将阈值降到 0.5 以下。
人们定义 loU 这个概念是为了评价你的对象定位算法是否精准,但更一般地说,loU 衡量了两个边界框重叠地相对大小。如果你有两个边界框,你可以计算交集,计算并集,然后求两个数值的比值,所以这也可以判断两个边界框是否相似,我们将在下一个视频中再次用到这个函数,当我们讨论非最大值抑制时再次用到。
非极大值抑制(Non-max suppression)
到目前为止你们学到的对象检测中的一个问题是,你的算法可能对同一个对象做出多次检测,所以算法不是对某个对象检测出一次,而是检测出多次。非极大值抑制这个方法可以确保你的算法对每个对象只检测一次,我们讲一个例子。
假设你需要在这张图片里检测行人和汽车,你可能会在上面放个 19×19 网格,理论上这辆车只有一个中点,所以它应该只被分配到一个格子里,左边的车子也只有一个中点,所以理论上应该只有一个格子做出有车的预测。
实践中当你运行对象分类和定位算法时,对于每个格子都运行一次,所以这个格子可能会认为这辆车中点应该在格子内部,这几个格子也会这么认为。对于左边的车子也一样,所以不仅仅是这个格子,如果这是你们以前见过的图像,不仅这个格子会认为它里面有车,也许这个格子和这个格子也会,也许其他格子也会这么认为,觉得它们格子内有车。
我们分步介绍一下非极大抑制是怎么起效的,因为你要在 361 个格子上都运行一次图像检测和定位算法,那么可能很多格子都会举手说我的𝑝𝑐,我这个格子里有车的概率很高,而不是 361 个格子中仅有两个格子会报告它们检测出一个对象。所以当你运行算法的时候,最后可能会对同一个对象做出多次检测,所以非极大值抑制做的就是清理这些检测结果。这样一辆车只检测一次,而不是每辆车都触发多次检测。
所以具体上,这个算法做的是,首先看看每次报告每个检测结果相关的概率𝑝𝑐,在本周的编程练习中有更多细节,实际上是𝑝𝑐乘以𝑐1、𝑐2或𝑐3。现在我们就说,这个𝑝𝑐检测概率,首先看概率最大的那个,这个例子(右边车辆)中是 0.9,然后就说这是最可靠的检测,所以我们就用高亮标记,就说我这里找到了一辆车。这么做之后,非极大值抑制就会逐一审视剩下的矩形,所有和这个最大的边框有很高交并比,高度重叠的其他边界框,那么这些输出就会被抑制。所以这两个矩形𝑝𝑐分别是 0.6 和 0.7,这两个矩形和淡蓝色矩形重叠程度很高,所以会被抑制,变暗,表示它们被抑制了。
接下来,逐一审视剩下的矩形,找出概率最高,𝑝𝑐最高的一个,在这种情况下是 0.8,我们就认为这里检测出一辆车(左边车辆),然后非极大值抑制算法就会去掉其他 loU 值很高的矩形。所以现在每个矩形都会被高亮显示或者变暗,如果你直接抛弃变暗的矩形,那就剩下高亮显示的那些,这就是最后得到的两个预测结果。
所以这就是非极大值抑制,非最大值意味着你只输出概率最大的分类结果,但抑制很接近,但不是最大的其他预测结果,所以这方法叫做非极大值抑制。

我们来看看算法的细节,首先这个 19×19 网格上执行一下算法,你会得到 19×19×8 的输出尺寸。不过对于这个例子来说,我们简化一下,就说你只做汽车检测,我们就去掉𝑐1、𝑐2 和𝑐3,然后假设这条线对于 19×19 的每一个输出,对于 361 个格子的每个输出,你会得到这样的输出预测,就是格子中有对象的概率(𝑝𝑐),然后是边界框参数(𝑏𝑥、𝑏𝑦、𝑏ℎ和𝑏𝑤)。 如果你只检测一种对象,那么就没有𝑐1、𝑐2和𝑐3这些预测分量。
现在要实现非极大值抑制,你可以做的第一件事是,去掉所有边界框,我们就将所有的预测值,所有的边界框𝑝𝑐小于或等于某个阈值,比如𝑝𝑐 ≤ 0.6的边界框去掉。
我们就这样说,除非算法认为这里存在对象的概率至少有 0.6,否则就抛弃,所以这就抛弃了所有概率比较低的输出边界框。所以思路是对于这 361 个位置,你输出一个边界框,还有那个最好边界框所对应的概率,所以我们只是抛弃所有低概率的边界框。
接下来剩下的边界框,没有抛弃没有处理过的,你就一直选择概率𝑝𝑐最高的边界框,然后把它输出成预测结果,这个过程就是上一张幻灯片,取一个边界框,让它高亮显示,这样你就可以确定输出做出有一辆车的预测。
接下来去掉所有剩下的边界框,任何没有达到输出标准的边界框,之前没有抛弃的边界框,把这些和输出边界框有高重叠面积和上一步输出边界框有很高交并比的边界框全部抛弃。所以 while 循环的第二步是上一张幻灯片变暗的那些边界框,和高亮标记的边界重叠面积很高的那些边界框抛弃掉。在还有剩下边界框的时候,一直这么做,把没处理的都处理完,直到每个边界框都判断过了,它们有的作为输出结果,剩下的会被抛弃,它们和输出结果重叠面积太高,和输出结果交并比太高,和你刚刚输出这里存在对象结果的重叠程度过高。
Anchor Boxes
到目前为止,对象检测中存在的一个问题是每个格子只能检测出一个对象,如果你想让一个格子检测出多个对象,你可以这么做,就是使用 anchor box 这个概念。
假设你有这样一张图片,对于这个例子,我们继续使用 3×3 网格,注意行人的中点和汽车的中点几乎在同一个地方,两者都落入到同一个格子中。你可以检测这三个类别,行人、汽车和摩托车,它将无法输出检测结果,所以我必须从两个检测结果中选一个。
而 anchor box 的思路是,这样子,预先定义两个不同形状的 anchor box,或者 anchor box 形状,你要做的是把预测结果和这两个 anchor box 关联起来。一般来说,你可能会用更多的 anchor box,可能要 5 个甚至更多,但对于这个视频,我们就用两个 anchor box,这样介绍起来简单一些。
你要做的是定义类别标签,用的向量不再是上面这个:
[𝑝𝑐 𝑏𝑥 𝑏𝑦 𝑏ℎ 𝑏𝑤 𝑐1 𝑐2 𝑐3]𝑇
而是重复两次:
𝑦 = [𝑝𝑐 𝑏𝑥 𝑏𝑦 𝑏ℎ 𝑏𝑤 𝑐1 𝑐2 𝑐3 𝑝𝑐 𝑏𝑥 𝑏𝑦 𝑏ℎ 𝑏𝑤 𝑐1 𝑐2 𝑐3]𝑇
前面的𝑝𝑐, 𝑏𝑥, 𝑏𝑦, 𝑏ℎ, 𝑏𝑤, 𝑐1, 𝑐2, 𝑐3(绿色方框标记的参数)是和 anchor box 1 关联的 8 个参数,后面的 8 个参数(橙色方框标记的元素)是和 anchor box 2 相关联。因为行人的形状更类似于 anchor box 1 的形状,而不是 anchor box 2 的形状,所以你可以用这 8 个数值(前 8 个参数),这么编码𝑝𝑐 = 1,是的,代表有个行人,用𝑏𝑥, 𝑏𝑦, 𝑏ℎ和𝑏𝑤来编码包住行人的边界框,然后用𝑐1, 𝑐2, 𝑐3(𝑐1 = 1, 𝑐2 = 0, 𝑐3 = 0)来说明这个对象是个行人。
然后是车子,因为车子的边界框比起 anchor box 1 更像 anchor box 2 的形状,你就可以这么编码,这里第二个对象是汽车,然后有这样的边界框等等,这里所有参数都和检测汽车相关(𝑝𝑐 = 1, 𝑏𝑥, 𝑏𝑦, 𝑏ℎ, 𝑏𝑤, 𝑐1 = 0, 𝑐2 = 1, 𝑐3 = 0)
总结一下,用 anchor box 之前,你做的是这个,对于训练集图像中的每个对象,都根据那个对象中点位置分配到对应的格子中,所以输出𝑦就是 3×3×8,因为是 3×3 网格,对于每个网格位置,我们有输出向量,包含𝑝𝑐,然后边界框参数𝑏𝑥, 𝑏𝑦, 𝑏ℎ和𝑏𝑤,然后𝑐1, 𝑐2, 𝑐3。现在用到 anchor box 这个概念,是这么做的。现在每个对象都和之前一样分配到同一个格子中,分配到对象中点所在的格子中,以及分配到和对象形状交并比最高的 anchor box 中。所以这里有两个 anchor box,你就取这个对象,如果你的对象形状是这样的(红色框),你就看看这两个 anchor box,anchor box 1 形状是这样(紫色框),anchor box 2 形状是这样(紫色框),然后你观察哪一个 anchor box 和实际边界框(红色框)的交并比更高,不管选的是哪一个,这个对象不只分配到一个格子,而是分配到一对,即(grid cell,anchor box)对,这就是对象在目标标签中的编码方式。所以现在输出 𝑦 就是 3×3×16,上一张幻灯片中你们看到 𝑦 现在是 16 维的,或者你也可以看成是 3×3×2×8,因为现在这里有 2 个 anchor box,而 𝑦 是 8 维的。𝑦 维度是 8,因为我们有 3 个对象类别,如果你有更多对象,那么𝑦 的维度会更高。
现在还有一些额外的细节,如果你有两个 anchor box,但在同一个格子中有三个对象,这种情况算法处理不好,你希望这种情况不会发生,但如果真的发生了,这个算法并没有很好的处理办法,对于这种情况,我们就引入一些打破僵局的默认手段。还有这种情况,两个对象都分配到一个格子中,而且它们的 anchor box 形状也一样,这是算法处理不好的另一 种情况,你需要引入一些打破僵局的默认手段,专门处理这种情况,希望你的数据集里不会出现这种情况,其实出现的情况不多,所以对性能的影响应该不会很大。
这就是 anchor box 的概念,我们建立 anchor box 这个概念,是为了处理两个对象出现在同一个格子的情况,实践中这种情况很少发生,特别是如果你用的是 19×19 网格而不是 3×3 的网格,两个对象中点处于 361 个格子中同一个格子的概率很低,确实会出现,但出现频率不高。也许设立 anchor box 的好处在于 anchor box 能让你的学习算法能够更有征对性,特别是如果你的数据集有一些很高很瘦的对象,比如说行人,还有像汽车这样很宽的对象,这样你的算法就能更有针对性的处理,这样有一些输出单元可以针对检测很宽很胖的对象,比如说车子,然后输出一些单元,可以针对检测很高很瘦的对象,比如说行人。
最后,你应该怎么选择 anchor box 呢?人们一般手工指定 anchor box 形状,你可以选择 5 到 10 个 anchor box 形状,覆盖到多种不同的形状,可以涵盖你想要检测的对象的各种形状。还有一个更高级的版本,我就简单说一句,你们如果接触过一些机器学习,可能知道后期 YOLO 论文中有更好的做法,就是所谓的 k-平均算法,可以将两类对象形状聚类,如果我们用它来选择一组 anchor box,选择最具有代表性的一组 anchor box,可以代表你试图检测的十几个对象类别,但这其实是自动选择 anchor box 的高级方法。如果你就人工选择一些形状,合理的考虑到所有对象的形状,你预计会检测的很高很瘦或者很宽很胖的对象,这应该也不难做。