R-CNN 系列详解(转)
RCNN算法流程可分为4个步骤
产生候选区域:对于输入的图片,使用Selective Search方法(了解即可),选择出2000个候选区域(每个区域大小不一)。
特征提取:使用AlexNet对每一个候选区域提取特征,得到2000*4096维的特征向量。
注1:在进行特征提取之前,需要先对上一步生成的候选区域进行调整,使其变为227*227,这是因为AlexNet输入是固定的,当然,其实卷积操作的输入可以不固定,全连接层的输入大小才是固定的,这也是后面几个模型会改进的。
注2:调整的方法比较暴力,不管输入是多大的图片,直接缩放到227*227
候选区域类别判断:将2000*4096维特征向量送入到21个(20个类别+1个背景)SVM分类器中,每一个SVM分类器包含4096个参数,所以可以看成两个矩阵相乘,即 $W1(2000×4096)×W2(4096×21)=W3(2000×21) $,这样就得到了每一个候选区域是某个类别的概率值。因为一张图片出现有上千个物品的可能性微乎其微,所以必定有大量的候选区域是重叠的,因此需要去除冗余的候选框。作者在此处使用非极大值抑制(NMS)方法来去除冗余候选框,对于每一个类别中IoU大于给定阈值的候选区域。这样就得到了每一个类别得分最高的一些候选区域。
使用回归器对候选框位置调整。
Fast R-CNN
Fast R-CNN的流程主要分为三步:
- 使用 Selective Search 方法生成2K个图片候选区域。
- 对整张图片进行特征提取得到相应的特征图(这是对R-CNN的一大改进,参考了SPPNet),并将上一步生成的候选区域映射到特征图中。
- 使用ROI Pooling将所有的候选区域特征统一缩放到7*7大小,然后将这2K个特征向量展平,并连接到全连接层上,得到两个输出结果,一个是K+1类(类别数+背景类)的概率,还有一个是每个类的预测边框。
Fast R-CNN与R-CNN相比主要有以下几点不同:
不再是对每一个候选区域单独提取特征,而是在提取整个图像的特征后,将每一个候选区域映射到特征图上,具体的映射方法如下:
这一映射主要是基于图像经过多层卷积与池化之后,图像的相对位置不变这一特性来实现的。
因为Fast R-CNN的Backbone是VGG,而在VGG中,卷积操作是不改变图像尺寸的,主要是池化操作会改变尺寸大小。
如上图所示,映射操作是在conv5之后,ROI Pooling之前进行的,在这之间进行了4次最大池化操作,每一次都将特征图的尺寸缩小1/2,所以最后特征图就变为了原始图像的 124=116 ,那么相应的每一个候选区域的坐标也应该按比例缩放,也就是说映射到特征图上的坐标是原始坐标的 116 。
例如:假设某个候选区域的坐标为 $(x_{min},y_{min},x_{max}, y_{max})$ ,则映射到特征图上就是$(x_{min}^{‘},y_{min}^{‘}, x_{max}^{‘}, y_{max}^{‘})$ 。
$$
x_{min}^{‘} = round(x_{min} \times \frac{1}{16}) \
y_{min}^{‘} = round(y_{min} \times \frac{1}{16}) \
x_{max}^{‘} = round(x_{max} \times \frac{1}{16}) \
y_{max}^{‘} = round(y_{max} \times \frac{1}{16})
$$
在R-CNN中为了统一输入使用了比较暴力的方法,但在Fast R-CNN中,使用了RoI Pooling,这一方法参考了SPPNet的空间金字塔池化(如下图所示)(将输入的任意大小的图片通过池化获得统一大小形成向量输入到全连接层中),可以将RoI Pooling看做空间金字塔池化的一个简化版。简单来说,RoI Pooling 就是将每一个候选区域的特征图分割成$7*7=49$等分,对于每一个区域使用最大池化操作,这样就能将所有输入统一到7*7的大小。
使用了多任务的损失函数来简化R-CNN中的多阶段训练。
Faster RCNN
Faster R-CNN算法流程可分为3个步骤
- 输入图像到特征提取器中,得到整张图片的feature map。
- 使用RPN生成候选框,并投影到feature map上,得到每一个候选区域的特征矩阵。
- 将每一个特征矩阵经过ROI Pooling缩放到7*7大小,然后经过展平处理后通过全连接层获得预测的分类以及候选区域位置偏移信息。
RPN
RPN网络是Faster RCNN的精髓所在,将传统的候选区域生成方法使用卷积运算来代替,而且根据原文作者所说,使用卷积来生成候选区域使得RPN具有一定注意力机制,效果与前几代的RCNN相比,提升明显。
首先,RPN网络接受任意尺寸大小的feat map作为输入,然后会生成9K个(为什么是9K,而不是K个,下面会有说明)anchor,并且RPN有两个输出,一个是anchor的类别信息,也就是该anchor是背景还是前景(只要有要识别的物品就属于前景),还有一个输出是该anchor的位置偏移信息(如果该anchor是背景,则该输出不重要)。
如上图所示,就是文章作者给出的RPN网络结构,但这张图其实略去了许多细节,配合下面这张图进行讲解会好很多,如下图所示
如图所示,对于通过上一步特征提取器得到的feature map,根据最开始的输入假设,此时我们的feature map为(1, 512, 50, 37),接着这个特征图会经过一个3*3的卷积(用于对feature map进一步提取信息),该卷积不改变特征图的尺寸大小,然后会分别经过两个1*1的卷积,1*1卷积的主要作用就是降维,并且1*1的卷积也能看做一种全连接,所以此处图中上面的输出是anchor的类别信息(背景or前景),下面的输出是每一个anchor的位置偏移信息。
什么是Anchor
我们在这里多次提到了anchor,那什么是anchor,这其实是理解RPN的关键所在,我这里尽力进行解释一下。
事实上,每一个anchor都是一个矩形框,而要表示一个矩形框就需要四个参数,作者使用了两种表示方式:
- 中心坐标+长宽:$(x_{center}, y_{center}, width, height)$
- 左上角坐标+右下角坐标:$(x_{min}, y_{min}, x_{max}, y_{max})$
在原图中生成anchor主要分为三步:
有一个base anchor,这个base anchor的尺寸可以自定义,默认尺寸为16*16。
从这个base acnhor生成9个不同尺寸的anchor,可以把这9个anchor视为后续anchor的模板。
这9个anchor的生成主要依靠两组参数,一组是scales,用于缩放anchor的宽度和高度,默认值是[8, 16, 32];另一组是ratios,即anchor的宽高之比,默认值是[0.5, 1, 2]。
首先对base anchor的宽高进行缩放操作,这一步比较简单,因为base anchor是16*16,那么使用scales的默认值,在经过一系列的缩放操作后,就得到了三个不同大小的anchor,即[128*128, 256*256, 512*512]。
然后用ratios的默认值对宽高进行变化,需要注意的是这一步的变化不可以改变anchor的面积,换句话说原来面积多大,变化后还是多大(可以有一定的上下浮动)。
我们假定以下几个变量:S代表面积,R代表宽高的比值,W代表宽,H代表高度,W’和H’分别代表经过变化后的宽和高,下图是进行这一转换的公式以及相关推导:
$$
S = W \times H \
R = \frac{W}{H} \
W^{‘} = R \times H^{‘} \
S = R \times H^{‘2} \
H^{‘} = \sqrt{\frac{S}{R}}
$$
3.(关键步骤)使用上一步生成的9个anchor模板在原始图像上生成具体的anchor。
- 理解原理的角度:简单来说,就是对图像的feature map每个点应用这生成的9个anchor模板(理解算法原理的话,到这里足矣)。
- 从源码的角度:首先对原始图像从(0, 0)点开始,从横轴和纵轴,每隔16步打一个格点,如下图所示:
- 可以把上述的每一个格点看做是feature map上的一个点,然后把这些格点作为anchor模板的中心,因为上一步生成的anchor模板的中心是(0, 0),所以可以直接将这些格点的坐标加上anchor的坐标,就得到了在图像上的anchor坐标。每一个格点生成9个不同尺寸大小的anchor,如图所示,一共有50*379=16650个anchor,这些就是在原图上生成的候选区域。
好了,现在我想大家对于什么是anchor,以及如何生成的应该有了一个大致的理解,那么我接着往下讲。
或许此时大家有一个疑问,就是anchor的生成似乎与rpn没什么关系,其实现在生成的anchor还并不能进行使用,可以认为是比较粗糙的初成品,还需要进行进一步的精加工,这一步在R-CNN以及Fast RCNN也都有用到,也就是bounding box regression。
Bounding Box Regression原理
在上一小节中生成的anchors需要经过一系列的微调才能进行使用,微调的方法就是本节要介绍的bounding box regression(名字太长,我就简写成bbox reg了)。
在上文提到,作者在代码中表示一个anchor主要用了两种方式,其中第二种方式用的比较多,但在bbox reg中主要用第一种,也就是中心坐标+宽高的表示方式。
下面这张图我在前文也有,让我们重点关注图片的第二个输出,这一部分就是进行bbox reg的关键部分。需要注意的是,我在前文有提到,rpn有两个输出,一个是anchor的类别信息,另一个是该anchor的位置偏移信息,而此处就需要用到这个位置偏移信息来对之前生成的anchor进行修正。
在经过下面这个1*1的卷积之后,根据前文对输入的假设,此时feature map变成了(1, 36, 50, 37),其中通道数为36,这36个通道每4个代表一个anchor的位置偏移信息,一共有9组,而上文也提到,每一个feature map上的点会生成9个尺度不一的anchor。
每一个anchor的位置偏移信息格式是:$(d_x, d_y,d_w,d_h)$ ,每一个分量代表的含义如下:
- $d_x$ 与 $d_y$ 分别代表anchor在x轴和y轴上的偏移量。
- $d_w$ 与 $d_h$ 分别代表anchor在宽和高上的缩放量。
特别需要注意,此处作者把anchor的宽高分别作为x轴和y轴的单位长度,换句话说 $d_x$ 与 $d_y$ 并不是相对于anchor中心的偏移量,它需要做一个转换,具体转换如下:
$$
\Delta x = w \times d_x \
\Delta y = h \times d_y
$$
此处的$\Delta x$ 与$\Delta y$才是真正的相对于anchor中心的偏移量;同理$d_w$与$d_h$其实是一个以 e 为底的宽高缩放量,也需要进行转换,具体转换如下所示:
$$
\Delta w = e^{d_w} \
\Delta h = e^{d_h}
$$
这样一来,我想对于每一个anchor具体的位置偏移修正公式很容易就可以想到,如下所示:
$$
X^{‘} = \Delta x + X = w \times d_x + X \
Y^{‘} = \Delta y = Y = h \times d_y + Y \
w^{‘} = w \times \Delta w = w \times e ^{d_w} \
h^{‘} = h \times \Delta h = h \times e^{d_h}
$$
判断anchor的类别
上一节中讲解了rpn如何对anchor的位置进行修正,这一节讲一下rpn如何识别每一个anchor的类别,注意这边进行的是二分类,即判断anchor的内容是背景还是前景,而不是具体的类别,具体的类别判断还在这之后。
下图就是rpn中对anchor类别进行判断的部分。
在feature map经过另一个1*1的卷积之后,维度变为了(1,18,50,37),其中通道数为18,又因为有9个anchor,所以每两个通道为一个anchor的类别,这两个通道分别代表了anchor是背景和前景的概率,这一部分需要特别注意特征维度的变化。
因为我们需要对类别进行softmax,但由于类别信息是在第1维(此处从0开始计数),所以需要进行reshape操作。
- 首先将类别信息放到第3维中,也就是变为(1,50,37,18)。
- 然后增加一个维度,变为(1,50,37,9,2),这样就可以对每一个anchor进行softmax分类。
- 完成softmax分类之后再去掉背景的概率,只保留anchor是前景的概率,此时维度变为(1,50,37,9,1)。
- 最后再进行一次reshape操作,去掉所有多余的维度,只保留两个维度,一个是batch,另一个是前景概率,即(1, 16650),这样就得到了每一anchor属于前景的概率。
对anchor的一些筛选工作
现在我们已经有一堆经过修正后的anchor,并且也知道了每一个anchor属于前景的概率,但我们细想一下,现在anchor的数量是不是太多了,我们只用了一张800*600的图像作为输入就生成了16650个anchor,如果全部作为RoI(Region of Intererst,也就是感兴趣区域或者说候选区域)输入到后续网络中,这计算量属实有点大,所以就需要进行一些筛选工作,这其实也就是RPN网络中Proposal层所做的工作。
首先,现在我们的anchor有许多因为是在边缘生成的,所以它们的坐标可能是负值,或者简单来说就是超出了图片的范围,那么就需要对这些anchor进行裁剪,把它们统一裁剪到图片范围内,也就是将anchor左上角坐标小于0的值用0代替,右下角坐标的X轴数值大于W就用W代替,Y轴数值大于H的用H代替。
经过上一步的裁剪工作,就会有许多anchor会变得很小,这里我们设定一个阈值,凡是小于16*16的anchor,我们都把它丢弃掉。
接着,因为我们已经有了每一个anchor属于前景的概率,那么很明显如果一个anchor属于前景的概率太小,那么也没有留着的必要性,所以对这些anchor的前景概率从大到小进行argsort,得到每一个anchor的排序索引,只取前6000个,到这一步anchor还是很多,但此时不能再鲁莽的去除anchor,因为有可能会有误判(毕竟这个前景概率只是rpn的预测,并不是真实的),此时需要用NMS方法把IoU大于0.7的进行合并,对于合并完的anchor再取前300个,这样就把输入到RoI网络的anchor的数量大大减少了。
注:需要注意的是,此处的6000以及300是测试阶段的配置,训练与此处不一样,后文会有介绍。
RPN小结
RPN部分内容比较多,第一次了解可能会被绕晕,此处我做一个简单的小结吧。
上图是我参照RPN部分的实现代码以及论文所绘制的,基本上把所有的细节点都囊括在了里面,大家配合上文的讲解以及此处的图应该能够理解了。
在作者代码中,主要把RPN主要分成了两部分,一个是RPN Head,另一个是Proposal,RPN Head主要负责anchor的生成、anchor位置偏移量预测以及anchor的类别判断;Proposal负责对生成的anchor进行进一步的筛选,将筛选后的anchor作为RoI输入到后续的网络中。
需要注意的是,因为不管是anchor的类别预测还是位置偏移量预测,这些都是在channel里面的,所以对这两个输出都需要使用pytorch的permute函数来将维度进行换位。
RoIHead
上文有提到,Faster RCNN就是RPN+Fast RCNN,上一小节已经对RPN讲解的比较详细了,只剩下一个RoIHead部分,这一部分与Fast RCNN很相像,所以此处只是简单介绍一下。
首先我们可以看到有两个输入,一个是黄色线的输入,这个是VGG16的输出,也就是feature map,另一个是紫色线的输入,也就是RPN的输出(每一个RoI的坐标信息)。
我们将上述两组数据输入到RoI Pooling中,得到每一个RoI对应位置的feature map,且每一个feature map的尺寸均为7*7(至于为什么都是7*7,在Fast RCNN中已有介绍),或许有人会有疑惑,输入的feature map与RoI是不匹配的,因为RoI的坐标都是从原始图像上生成的,很明显不能用于feature map上,但其实不用担心,在网络初始化的时候,已经把feature map与原始图像的缩放比值作为参数传入了,所以pytorch可以自动调整RoI的尺寸以适应feature map。
在经过RoI Pooling后,我们会得到300个7*7大小的feature map,每一个feature map的通道数是512(因为原始的feature map就是512维的),所以输出的shape就是(300, 512, 7, 7),然后经过一次reshape操作转为(300,512*7*7),这样就可以输入到剩余的两层全连接层,这两个全连接层其实就是VGG16中最后去掉输出的分类器部分。
在经过两层全连接层后,会得到一个shape等于300的输出,可以认为是这300个RoI经过进一步特征提取后的深层特征,接着把这300个RoI分别进行类别预测以及边界框回归。上图中左边的FC 21(20个类别+1个背景)就是对RoI的类别进行预测,右边的FC 84是对每一个类别的边界框进行回归,特别要注意,是会对每一个RoI的每一个可能的类别都做边界框回归,这也是为什么输出维度是84的原因,即21*4(21个类别。每个类别4个坐标值)
Faster RCNN 训练
Faster RCNN有三个部分需要训练,分别是特征提取器VGG16,RPN以及RoIHead。其中特征提取器一般是采用预训练模型进行微调,所以此处重点介绍RPN的训练以及RoI的训练,虽然原论文中Faster RCNN是将这两部分分开训练的,但现在大多数实现都是进行联合训练的方式。
注:此处我们依然使用前文对Faster RCNN输入所做的假设,但阶段改为训练阶段。
RPN训练
首先来回想一下RPN的网络结构,在上文我把它分成了两部分,一部分是RPN Head Layer,另一部分是Proposal Layer,但只有RPN Head真正有参数需要训练,Proporsal只是用来进行RoI筛选的,并不需要训练,所以我们重点关注RPN Head部分,如下图所示:
上文有提到,RPN Head部分主要用于anchor的位置偏移预测以及anchor类别的预测,对于前文假定的图像输入,RPN Head会生成50*37*9=16650个anchor,很显然把这些全部用于训练并不合理,因为这里面有大量的负样本,所以需要先进行一波筛选,选出256个作为训练样本(这个数目是作者提出的),其中正样本128个,负样本128个,其中负样本个数肯定可以满足,但正样本基本很难会有128个,所以作者在文中说,如果正样本不足128个,则空缺部分用负样本填充,具体的训练样本筛选步骤如下:
- 去掉所有不在图片范围的anchor,并将剩余的所有anchor的标签标记为-1;
- 将与ground truth(gt)的IoU小于0.3的anchor作为负样本,标签记为0;
- 将与每个gt的IoU最大的anchor作为正样本,标签记为1;
- 将与gt的IoU不小于0.7的anchor作为正样本,标签记为1;
- 如果某一类样本超过128个,则随机从中选择多出的样本将其标签记为-1;
- 如果正样本小于128个,则使用负样本填充,保证总体样本数为256。
- 仅将标签为0和1的样本用于训练,忽略标签为-1的anchor
在筛选出了训练样本之后,就需要计算每一个anchor的Loss。
如上图所示,RPN的损失函数由两部分组成,一个是分类损失,另一个是边界框回归损失,其中公式中一些变量的含义已在图中标明了。
首先是分类损失,此处的类别仅仅是指anchor属于物品还是背景,所以这是一个二分类问题,因此在论文中作者是使用了**二值交叉熵损失**来计算RPN的分类损失。
然后是边界框回归损失,具体如下图所示:
如上图所示, $L_reg(t^i, t^ * )$ 是Smooth L1函数(这里可以考虑下为什么用Smooth L1而不是L2,可以参考Single Bounding Box Regression,$t_i$是anchor的四个回归预测值,它代表了预测的偏移量,即预测anchor的中心坐标以及宽高相对于真实anchor的偏移量$(t_x, t_y, t_w, t_h)$ ,$t_i^ * $ 代表了真实的偏移量$(t_x^ * ,t_y^ * ,t_w^ * ,t_h^ * )$ 。
此处特别需要注意: ti 其实就是RPN网络的一个输出,即下图中框出的部分,我看论文的时候被作者的那些公式迷惑住了,之后看代码才会明白,即 ti 是神经网络的输出,而不是公式计算所得,公式仅仅只是用来解释 ti 所代表的含义。
而真正需要用公式进行计算的是$t_i^*$,* 也就是预测的anchor与真实bbox的偏移量,计算公式就如上图所示,其中$(x^* , y^* , w^*, h^*)$都代表真实的bbox的中心坐标与宽高,$(x_a, y_a, w_a, h_a)$代表预测的anchor的中心坐标以及宽和高。
最后,在知道了预测的偏移量以及真实的偏移量后,就可以使用Smooth L1计算回归损失了。
RoIHead训练
我在前文3.2.4节中有提到RPN网络中的Proposal Layer会对生成的anchor进行一些筛选工作,筛选出的anchor就是RoI,而且在测试阶段筛选出的RoI数量是300,但在训练阶段RPN会筛选出2000个RoI,然后再在这2000个RoI中挑选出128个高质量样本用于RoIHead的训练,其中正负样本的比例为1:3,具体的样本筛选步骤如下所示:
- 计算每个RoI与每个gt bbox的IoU。
- 获得每一个RoI的最大IoU值。
- 若某个RoI的最大IoU不小于0.5,则为正样本,并将其类别标签记为最大IoU对应gt bbox的类别标签,也就是说,如果这个RoI与第3个gt bbox的IoU最大,且大于0.5,那么就把这个RoI的标签记为第3个gt bbox所对应的类别。
- 若某个RoI的最大IoU小于0.5,则标记为负样本,类别标签为0(背景)。
- 限制正负样本的数量,正样本数量不超过32个,负样本数量不超过96个。
- 如果正样本数量少于32个,空缺的使用负样本填充。
在有了训练样本后,就需要计算该部分的损失,RoIHead的损失计算和RPN几乎一模一样,也是分为分类损失与回归损失,分类损失使用交叉熵损失函数(注意,这是与RPN训练的一个不同点,RoI的分类是多分类问题),回归也是用Smooth L1损失,具体的计算过程也和RPN差不多,此处就不再赘述了。