两阶经典检测器:Faster RCNN(二)算法简述


1. 准备工作

1) 首先从GitHub上下载本章所用的代码, 地址如下:

    git clone git@github.com:dongdonghy/Detection-PyTorch-Notebook.git

2)  然后创建data文件夹

    cd Detection-PyTorch-Notebook/chapter4/faster-rcnn-pytorch && mkdir data

3) 下载PASCAL VOC 2012数据集, 在data文件夹下创建软连接
    cd data && ln -s "your data directory" VOC2012

4) 从下面网址下载预训练模型并放到data/pretrained_model/文件夹下

   https://www.dropbox.com/s/s3brpk0bdq60nyb/vgg16_caffe.pth?dl=0

5) 安装所需依赖包

  pip install -r requirements.txt
6) 根据自己的GPU型号, 修改lib/make.sh文件中的 CUDA_ARCH
7) 由于在NMSRoI Pooling中使用了CUDA进行加速, 因此还需要对此进行编译

  cd lib && sh make.sh

2. Faster RCNN总览
   如图4.3所示为Faster RCNN算法的基本流程, 从功能模块来讲, 主要包括4部分: 特征提取网络、 RPN模块、 RoI PoolingRegion of Interest) 模块与RCNN模块, 虚线表示仅仅在训练时有的步骤。 Faster RCNN延续了RCNN系列的思想, 即先进行感兴趣区域RoI的生成, 然后再把生成的区域分类, 最后完成物体的检测, 这里的RoI使用的即是RPN模块, 区域分类则是RCNN网络。

   特征提取网络Backbone: 输入图像首先经过Backbone得到特征图,在此以VGGNet为例, 假设输入图像的维度为3×600×800, 由于VGGNet包含4Pooling层(物体检测使用VGGNet时, 通常不使用第5Pooling层) , 下采样率为16, 因此输出的feature map的维度为512×37×50

   RPN模块: 区域生成模块, 如图4.3的中间部分, 其作用是生成较好的建议框, 即Proposal, 这里用到了强先验的AnchorRPN包含5个子模块:
     ·Anchor生成: RPNfeature map上的每一个点都对应了9Anchors, 这9Anchors大小宽高不同, 对应到原图基本可以覆盖所有可能出现的物体。 因此, 有了数量庞大的AnchorsRPN接下来的工作就是从中筛选, 并调整出更好的位置, 得到Proposal
     ·RPN卷积网络: 与上面的Anchor对应, 由于feature map上每个点对应了9Anchors, 因此可以利用1×1的卷积在feature map上得到每一个Anchor的预测得分与预测偏移值。
     ·计算RPN loss: 这一步只在训练中, 将所有的Anchors与标签进行匹配, 匹配程度较好的Anchors赋予正样本, 较差的赋予负样本, 得到
分类与偏移的真值, 与第二步中的预测得分与预测偏移值进行loss的计算。
     ·生成Proposal: 利用第二步中每一个Anchor预测的得分与偏移量,可以进一步得到一组较好的Proposal, 送到后续网络中。
    ·筛选Proposal得到RoI: 在训练时, 由于Proposal数量还是太多(默认是2000) , 需要进一步筛选Proposal得到RoI(默认数量是256) 。 在测试阶段, 则不需要此模块, Proposal可以直接作为RoI, 默认数量为300

RoI Pooling模块: 这部分承上启下, 接受卷积网络提取的feature mapRPNRoI, 输出送到RCNN网络中。 由于RCNN模块使用了全连接网络, 要求特征的维度固定, 而每一个RoI对应的特征大小各不相同, 无法送入到全连接网络, 因此RoI PoolingRoI的特征池化到固定的维度, 方便送到全连接网络中。
RCNN模块: 将RoI Pooling得到的特征送入全连接网络, 预测每一个RoI的分类, 并预测偏移量以精修边框位置, 并计算损失, 完成整个
Faster RCNN过程。 主要包含3部分:
   ·RCNN全连接网络: 将得到的固定维度的RoI特征接到全连接网络中, 输出为RCNN部分的预测得分与预测回归偏移量。
   ·计算RCNN的真值: 对于筛选出的RoI, 需要确定是正样本还是负样本, 同时计算与对应真实物体的偏移量。 在实际实现时, 为实现方便, 这一步往往与RPN最后筛选RoI那一步放到一起。
    ·RCNN loss: 通过RCNN的预测值与RoI部分的真值, 计算分类与回归loss
   从整个过程可以看出, Faster RCNN是一个两阶的算法, 即RPNRCNN, 这两步都需要计算损失, 只不过前者还要为后者提供较好的感兴趣区域。 具体每一个模块的细节实现, 将在后面章节中详细讲解。

3. 详解RPN
   RPN部分的输入、 输出如下
      ·输入: feature map、 物体标签, 即训练集中所有物体的类别与边框位置。
       ·输出: Proposal、 分类Loss、 回归Loss, 其中, Proposal作为生成的区域, 供后续模块分类与回归。 两部分损失用作优化网络。
   RPN模块的总体代码逻辑如下, 源代码文件见lib/model/faster_rcnn/faster_rcnn.py
3.1 理解Anchor
     理解Anchor是理解RPN乃至Faster RCNN的关键。 Faster RCNN先提供一些先验的边框, 然后再去筛选与修正, 这样在Anchor的基础上做物体检测要比从无到有的直接拟合物体的边框容易一些。
     Anchor的本质是在原图大小上的一系列的矩形框, 但Faster RCNN将这一系列的矩形框和feature map进行了关联。 具体做法是, 首先对feature map进行3×3的卷积操作, 得到的每一个点的维度是512维, 这512维的数据对应着原始图片上的很多不同的大小与宽高区域的特征,这些区域的中心点都相同。 如果下采样率为默认的16, 则每一个点的坐标乘以16即可得到对应的原图坐标。

     为适应不同物体的大小与宽高, 在作者的论文中, 默认在每一个点上抽取了9Anchors, 具体Scale{8,16,32}Ratio{0.5,1,2}, 将这9Anchors的大小反算到原图上, 即得到不同的原始Proposal, 如图4.4所示。 由于feature map大小为37×50, 因此一共有37×50×9=16650Anchors。 而后通过分类网络与回归网络得到每一个Anchor的前景背景概率和偏移量, 前景背景概率用来判断Anchor是前景的概率, 回归网络则是将预测偏移量作用到Anchor上使得Anchor更接近于真实物体坐标。

     在具体的代码实现时, lib/model/rpn下的anchor_target_layer.pyproposal_layer.py在类的初始化中均生成了所需的Anchor, 下面从代码角度简单讲解一下生成过程, 源代码文件见lib/model/rpn/generate_anchors.py

3.2 RPN的真值与预测量
   理解RPN的预测量与真值分别是什么, 也是理解RPN原理的关键。对于物体检测任务来讲, 模型需要预测每一个物体的类别及其出现的位置, 即类别、 中心点坐标xy、 宽w与高h5个量。 由于有了Anchor这个先验框, RPN可以预测Anchor的类别作为预测边框的类别, 并且可以预测真实的边框相对于Anchor的偏移量, 而不是直接预测边框的中心点坐标xy、 宽高wh
   举个例子, 如图4.5所示, 输入图像中有3Anchors与两个标签, 从位置来看, Anchor AC分别和标签MN有一定的重叠, 而Anchor B位置更像是背景。

    首先介绍模型的真值。 对于类别的真值, 由于RPN只负责区域生成, 保证recall, 而没必要细分每一个区域属于哪一个类别, 因此只需要前景与背景两个类别, 前景即有物体, 背景则没有物体。
    RPN通过计算Anchor与标签的IoU来判断一个Anchor是属于前景还是背景。 IoU的含义是两个框的公共部分占所有部分的比例, 即重合比例。 在图4.5中, Anchor A与标签MIoU计算公式如式(4-1) 所示。

    IoU大于一定值时, 该Anchor的真值为前景, 低于一定值时, 该Anchor的真值为背景。
   然后是偏移量的真值。 仍以图4.5中的Anchor A与标签M为例, 假设Anchor A的中心坐标为xaya, 宽高分别为waha, 标签M的中心坐标为xy, 宽高分别为wh, 则对应的偏移真值计算公式如式(4-2) 所示。

   从式(4-2) 中可以看到, 位置偏移txty利用宽与高进行了归一化, 而宽高偏移twth进行了对数处理, 这样的好处是进一步限制了偏 移量的范围, 便于预测。

  有了上述的真值, 为了求取损失, RPN通过卷积网络分别得到了类别与偏移量的预测值。 具体来讲, RPN需要预测每一个Anchor属于前景与背景的概率, 同时也需要预测真实物体相对于Anchor的偏移量, 记为t*xt*yt*wt*h。 具体的网络下一节细讲。另外, 在得到预测偏移量后, 可以使用式(4-3) 的公式将预测偏移量作用到对应的Anchor上, 得到预测框的实际位置x*y*w*h*

    如果没有Anchor, 做物体检测需要直接预测每个框的坐标, 由于框的坐标变化幅度大, 使网络很难收敛与准确预测, 而Anchor相当于提供了一个先验的阶梯, 使得模型去预测Anchor的偏移量, 即可更好地接近真实物体。
   实际上, Anchor是我们想要预测属性的先验参考值, 并不局限于矩形框。 如果需要, 我们也可以增加其他类型的先验, 如多边形框、 角度和速度等。

3.3 RPN卷积网络
  为了实现上述的预测, RPN搭建了如图4.6所示的网络结构。 具体实现时, 在feature map上首先用3×3的卷积进行更深的特征提取, 然后利用1×1的卷积分别实现分类网络和回归网络。

                  

     在物体检测中, 通常我们将有物体的位置称为前景, 没有物体的位置称为背景。 在分类网络分支中, 首先使用1×1卷积输出18×37×50的特征, 由于每个点默认有9Anchors, 并且每个Anchor只预测其属于前景还是背景, 因此通道数为18。 随后利用torch.view()函数将特征映射到2×333×75, 这样第一维仅仅是一个Anchor的前景背景得分, 并送到Softmax函数中进行概率计算, 得到的特征再变换到18×37×50的维度,最终输出的是每个Anchor属于前景与背景的概率。

     在回归分支中, 利用1×1卷积输出36×37×50的特征, 第一维的36包含9Anchors的预测, 每一个Anchor4个数据, 分别代表了每一个Anchor的中心点横纵坐标及宽高这4个量相对于真值的偏移量。 RPN的网络部分代码如下, 源代码文件见lib/model/rpn/rpn.py

 3.4 RPN真值的求取
   上一节的RPN分类与回归网络得到的是模型的预测值, 而为了计算预测的损失, 还需要得到分类与偏移预测的真值, 具体指的是每一个Anchor是否对应着真实物体, 以及每一个Anchor对应物体的真实偏移值。 求真值的具体实现过程如图4.7所示, 主要包含4步, 下面具体介绍。

1) Anchor生成
      这部分与前面Anchor的生成过程一样, 可以得到37×50×9=16650Anchors。 由于按照这种方式生成的Anchor会有一些边界在图像边框外, 因此还需要把这部分超过图像边框的Anchors过滤掉, 具体生成过程如下, 源代码文件见lib/model/rpn/anchor_target_layer.py
2) Anchor与标签的匹配
    为了计算Anchor的损失, 在生成Anchor之后, 我们还需要得到每个Anchor的类别, 由于RPN的作用是建议框生成, 而非详细的分类, 因此只需要区分正样本与负样本, 即每个Anchor是属于正样本还是负样本。

    前面已经介绍了通过计算Anchor与标签的IoU来判断是正样本还是负样本。 在具体实现时, 需要计算每一个Anchor与每一个标签的IoU,因此会得到一个IoU矩阵, 具体的判断标准如下:
      ·对于任何一个Anchor, 与所有标签的最大IoU小于0.3, 则视为负样本。
      ·对于任何一个标签, 与其有最大IoUAnchor视为正样本。
      ·对于任何一个Anchor, 与所有标签的最大IoU大于0.7, 则视为正样本。
   匹配与筛选的代码示例如下, 源代码文件见lib/model/rpn/anchor_target_layer.py
   需要注意的是, 上述三者的顺序不能随意变动, 要保证一个Anchor既符合正样本, 也符合负样本时, 赋予正样本。 并且为了保证这一阶段的召回率, 允许多个Anchors对应一个标签, 而不允许一个标签对应多个Anchors

3) Anchor的筛选
     由于Anchor的总数量接近于2万, 并且大部分Anchor的标签都是背景, 如果都计算损失的话则正、 负样本失去了均衡, 不利于网络的收敛。 在此, RPN默认选择256Anchors进行损失的计算, 其中最多不超过128个的正样本。 如果数量超过了限定值, 则进行随机选取。 当然,这里的256128都可以根据实际情况进行调整, 而不是固定死的。

     Anchor筛选的代码示例如下, 源代码文件见lib/model/rpn/anchor_target_layer.py

4) 求解回归偏移真值

   上一步将每个Anchor赋予正样本或者负样本代表了预测类别的真值, 而回归部分的偏移量真值还需要利用Anchor与对应的标签求解得到, 具体公式见式4-2
    得到偏移量的真值后, 将其保存在bbox_targets中。 与此同时, 还需要求解两个权值矩阵bbox_inside_weightsbbox_outside_weights, 前者是用来设置正样本回归的权重, 正样本设置为1, 负样本设置为0, 因为负样本对应的是背景, 不需要进行回归; 后者的作用则是平衡RPN分类损失与回归损失的权重, 在此设置为1/256
    求解回归的偏移真值示例如下, 源代码文件见lib/model/rpn/anchor_target_layer.py

    真值的求取部分最后的输出包含了分类的标签label、 回归偏移的真值bbox_targets, 以及两个权重向量bbox_inside_weightsbbox_outside_weights

5)损失函数设计
   有了网络预测值与真值, 接下来就可以计算损失了。 RPN的损失函数包含分类与回归两部分, 具体公式如式(4-4) 所示。

      代表了256个筛选出的Anchors的分类损失, Pi为每一个Anchor的类别真值, p*i为每一个Anchor的预测类别。 由于RPN的作用是选择出Proposal, 并不要求细分出是哪一类前景, 因此在这一阶段是二分类, 使用的是交叉熵损失。 值得注意的是, 在F.cross_entropy()函数中集成了Softmax的操作, 因此应该传入得分, 而非经过Softmax之后的预测值

     代表了回归损失, 其中bbox_inside_weights实际上起到了p*i进行筛选的作用, bbox_outside_weights起到了 

来平衡两部分损失的作用。 回归损失使用了smoothL1函数, 具体公式如式(4-5) 与式(4-6) 所示。

       从式(4-6) 中可以看到, smoothL1函数结合了1阶与2阶损失函数,原因在于, 当预测偏移量与真值差距较大时, 使用2阶函数时导数太 大, 模型容易发散而不容易收敛, 因此在大于1时采用了导数较小的1阶损失函数。
     损失函数的代码接口如下, 源代码文件见ib/model/rpn/rpn.py

6) NMS与生成Proposal
   完成了损失的计算, RPN的另一个功能就是区域生成, 即生成较好的Proposal, 以供下一个阶段进行细分类与回归。
   NMS生成Proposal的主要过程如图4.8所示, 首先生成大小固定的全部Anchors, 然后将网络中得到的回归偏移作用到Anchor上使Anchor更加贴近于真值, 并修剪超出图像尺寸的Proposal, 得到最初的建议区域。 在这之后, 按照分类网络输出的得分对Anchor排序, 保留前12000个得分高的Anchors。 由于一个物体可能会有多个Anchors重叠对应, 因此再应用非极大值抑制(NMS) 将重叠的框去掉, 最后在剩余的Proposal中再次根据RPN的预测得分选择前2000个, 作为最终的Proposal, 输出到下一个阶段。

 NMSProposal的筛选过程如下, 源代码文件见lib/model/rpn/proposal_layer.py

 7) 筛选Proposal得到RoI

   在训练时, 上一步生成的Proposal数量为2000个, 其中仍然有很多背景框, 真正包含物体的仍占少数, 因此完全可以针对Proposal进行再一步筛选, 过程与RPN中筛选Anchor的过程类似, 利用标签与Proposal构建IoU矩阵, 通过与标签的重合程度选出256个正负样本。 这一步有3个作用:

  ·筛选出了更贴近真实物体的RoI, 使送入到后续网络的物体正、 负样本更均衡, 避免了负样本过多, 正样本过少的情况。
  ·减少了送入后续全连接网络的数量, 有效减少了计算量。
  ·筛选Proposal得到RoI的过程中, 由于使用了标签来筛选, 因此也为每一个RoI赋予了正、 负样本的标签, 同时可以在此求得RoI变换到对应标签的偏移量, 这样就求得了RCNN部分的真值。
   具体实现时, 首先计算Proposal与所有的物体标签的IoU矩阵, 然后根据IoU矩阵的值来筛选出符合条件的正负样本。 筛选标准如下:
      ·对于任何一个Proposal, 其与所有标签的最大IoU如果大于等于0.5, 则视为正样本。
      ·对于任何一个Proposal, 其与所有标签的最大IoU如果大于等于0且小于0.5, 则视为负样本。
  经过上述标准的筛选, 选出的正、 负样本数量不一, 在此设定正、负样本的总数为256个, 其中正样本的数量为p个。 为了控制正、 负样本的比例基本满足1:3, 在此正样本数量p不超过64, 如果超过了64则从正样本中随机选取64个。 剩余的数量256-p为负样本的数量, 如果超过了 256-p则从负样本中随机选取256-p个。经过上述操作后, 选出了最终的256RoI, 并且每一个RoI都赋予了正样本或者负样本的标签。 在此也可以进一步求得每一个RoI的真值, 即属于哪一个类别及对应真值物体的偏移量。
  筛选Proposal过程的代码示例如下, 源代码文件见lib/model/rpn/proposal_target_layer_cascade.py

 最终返回分类的真值label, 回归的偏移真值bbox_targets, 以及每一个Proposal对应的权重bbox_inside_weightsbbox_outside_weights

5. RoI Pooling

   上述步骤得到了256RoI, 以及每一个RoI对应的类别与偏移量真值, 为了计算损失, 还需要计算每一个RoI的预测量。
   前面的VGGNet网络已经提供了整张图像的feature map, 因此自然联想到可以利用此feature map, 将每一个RoI区域对应的特征提取出来, 然后接入一个全连接网络, 分别预测其RoI的分类与偏移量。
   然而, 由于RoI是由各种大小宽高不同的Anchors经过偏移修正、 筛选等过程生成的, 因此其大小不一且带有浮点数, 然而后续相连的全接网络要求输入特征大小维度固定, 这就需要有一个模块, 能够把各种维度不同的RoI变换到维度相同的特征, 以满足后续全连接网络的要求, 于是RoI Pooling就产生了。

  对RoI进行池化的思想在SPPNet中就已经出现了, 只不过在FastRCNN中提出的RoI Pooling算法利用最近邻差值算法将池化过程进行了简化, 而在随后的Mask RCNN中进一步提出了RoI Align的算法, 利用双线性插值, 进一步提升了算法精度。
  在此我们举一个例子来讲解这几种算法的思想, 假设当前RoI大小为332×332, 使用VGGNet的全连接层, 其所需的特征向量维度为512×7×7, 由于目前的特征图通道数为512Pooling的过程就是如何获得7×7大小区域的特征。

5.1)RoI Pooling简介
     RoI Pooling的实现过程如图4.9所示, 假设当前的RoI为图4.9中左侧图像的边框, 大小为332×332, 为了得到这个RoI的特征图, 首先需要将 该区域映射到全图的特征图上, 由于下采样率为16, 因此该区域在特征图上的坐标直接除以16并取整, 而对应的大小为332/16=20.75。 在此,RoI Pooling的做法是直接将浮点数量化为整数, 取整为20×20, 也就得到了该RoI的特征, 即图4.9中第3步的边框。

   下一步还要将该20×20区域处理为7×7的特征, 然而20/7≈2.857, 次出现浮点数, RoI Pooling的做法是再次量化取整, 将2.857取整为2,然后以2为步长从左上角开始选取出7×7的区域, 这样每个小方格在特征图上都对应2×2的大小, 如图4.9中第4步所示。
   最后, 取每个小方格内的最大特征值, 作为这个小方格的输出, 最终实现了7×7的输出, 也完成了池化的过程, 如图4.9中第5步所示。

   从实现过程中可以看到, RoI本来对应于20.75×20.75的特征图区域, 最后只取了14×14的区域, 因此RoI Pooling算法虽然简单, 但量化取整带来的偏差势必会影响网络, 尤其是回归物体位置的准确率.

5.2) RoI Align简介   

  RoI Align的思想是使用双线性插值获得坐标为浮点数的点的值, 主要过程如图4.10所示, 依然将RoI对应到特征图上, 但坐标与大小都保留着浮点数, 大小为20.75×20.75, 不做量化。

  接下来, 将特征图上的20.75×20.75大小均匀分成7×7方格的大小中间的点依然保留浮点数。 在此选择其中2×2方格为例, 如图4.11所示, 在每一个小方格内的特定位置选取4个采样点进行特征采样, 如图4.11中每个小方格选择了4个小黑点, 然后对这4个黑点的值选择最大值, 作为这个方格最终的特征。 这4个小黑点的位置与值该如何计算呢?

  对于黑点的位置, 可以将小方格平均分成2×24份, 然后这4份更小单元的中心点可以作为小黑点的位置。

  至于如何计算这4个小黑点的值, RoI Align使用了双线性插值的方法。 小黑点周围会有特征图上的4个特征点, 利用这4个特征点双线性插值出该黑点的值。

  由于Align算法最大可能地保留了原始区域的特征, 因此Align算法对检测性能有显著的提升, 尤其是对于受RoI Pooling影响大的情形, 如本身特征区域较小的小物体, 改善更为明显。

 6. 全连接RCNN模块
  在经过RoI Pooling层之后, 特征被池化到了固定的维度, 因此接下来可以利用全连接网络进行分类与回归预测量的计算。 在训练阶段, 最后需要计算预测量与真值的损失并反传优化, 而在前向测试阶段, 可以直接将预测量加到RoI上, 并输出预测量。

6.1) RCNN全连接网络
  RCNN全连接网络如图4.12所示, 4.5节中256RoI经过池化之后得到固定维度为512×7×7的特征, 在此首先将这三个维度延展为一维, 因为全连接网络需要将一个RoI的特征全部连接起来。

   接下来利用VGGNet的两个全连接层, 得到长度为4096256RoI特征。 为了输出类别与回归的预测, 将上述特征分别接入分类与回归的全连接网络。 在此默认为21类物体, 因此分类网络输出维度为21, 回归网络则输出每一个类别下的4个位置偏移量, 因此输出维度为84

  值得注意的是, 虽然是256RoI放到了一起计算, 但相互之间是独立的, 并没有使用到共享特征, 因此造成了重复计算, 这也是FasterRCNN的一个缺点。 RCNN全连接部分的代码接口如下, 源代码文件见lib/model/faster_rcnn/faster_rcnn.py

 6.2 损失函数设计
  RCNN部分的损失函数计算方法与RPN部分相同, 不再重复。 只不过在此为21个类别的分类, 而RPN部分则是二分类, 需要注意回归时至多有64个正样本参与回归计算, 负样本不参与回归计算。
  计算RCNN损失的代码接口如下, 源代码文件见lib/model/faster_rcnn/faster_rcnn.py