论文:
Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks
博客:
RCNN 系列详解
一文读懂Faster RCNN
睿智的目标检测27——Pytorch搭建Faster R-CNN目标检测平台
代码:
bubbliiiing/faster-rcnn-pytorch
WZMIAOMIAO/deep-learning-for-image-processing
捋一捋pytorch官方FasterRCNN代码
从编程实现角度学习Faster R-CNN(附极简实现)
视频:
Pytorch 搭建自己的Faster-RCNN目标检测平台(Bubbliiiing 深度学习 教程)
Faster RCNN源码解析(pytorch)
Faster R-CNN算是RCNN系列算法的最杰出产物,也是 two-stage
中最为经典的物体检测算法。
Faster RCNN可以看作 RPN+Fast RCNN,其中RPN使用CNN来生成候选区域,并且RPN网络可以认为是一个使用了注意力机制的候选区域选择器。
整个Faster RCNN网络可以分为四个部分:
Faster-RCNN可以采用多种的主干特征提取网络,常用的有 VGG
,Resnet
,Xception
等等,本文以Resnet50
网络为例子。
【注意】:
Faster-Rcnn对输入进来的图片尺寸没有固定,但是一般会把输入进来的图片短边固定成600,如输入一张1200x1800的图片,会把图片不失真的resize到600x900上。
ResNet50有两个基本的块,分别名为Conv Block和Identity Block,其中Conv Block输入和输出的维度是不一样的,所以不能连续串联,它的作用是改变网络的维度;Identity Block输入维度和输出维度相同,可以串联,用于加深网络的。
Conv Block
和 Identity Block
的结构如下:
Faster-RCNN的主干特征提取网络部分只包含了长宽压缩了 4 次的内容,第五次压缩后的内容在ROI中使用。以输入的图片为 600 × 600 600\times 600 600×600 为例,shape变化如下:
最后一层的输出就是公用特征层,即Feature Map的大小为 38 × 38 × 1024 38\times 38\times1024 38×38×1024 。
代码路径:/nets/resnet50.py
在代码里里面,我们使用resnet50()函数来获得resnet50的公用特征层。
其中features部分为公用特征层,classifier部分为第二阶段用到的分类器。
import math
import torch.nn as nn
from torch.hub import load_state_dict_from_url
class Bottleneck(nn.Module):
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(Bottleneck, self).__init__()
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, stride=stride, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.conv3 = nn.Conv2d(planes, planes * self.expansion, kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000):
# -----------------------------------#
# 假设输入进来的图片是600,600,3
# -----------------------------------#
self.inplanes = 64
super(ResNet, self).__init__()
# 600,600,3 -> 300,300,64
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
# 300,300,64 -> 150,150,64
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=0, ceil_mode=True)
# 150,150,64 -> 150,150,256
self.layer1 = self._make_layer(block, 64, layers[0])
# 150,150,256 -> 75,75,512
self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
# 75,75,512 -> 38,38,1024 到这里可以获得一个38,38,1024的共享特征层
self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
# self.layer4被用在classifier模型中
self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
self.avgpool = nn.AvgPool2d(7)
self.fc = nn.Linear(512 * block.expansion, num_classes)
# 初始化权重
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
def _make_layer(self, block, planes, blocks, stride=1):
downsample = None
# -------------------------------------------------------------------#
# 当模型需要进行高和宽的压缩的时候,就需要用到残差边的downsample
# -------------------------------------------------------------------#
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.inplanes, planes * block.expansion, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(planes * block.expansion),
)
layers = [block(self.inplanes, planes, stride, downsample)] # conv_block
self.inplanes = planes * block.expansion
for i in range(1, blocks):
layers.append(block(self.inplanes, planes))
return nn.Sequential(*layers)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
def resnet50(pretrained=False):
model = ResNet(Bottleneck, [3, 4, 6, 3])
# 是否加载预训练模型
if pretrained:
state_dict = load_state_dict_from_url("https://download.pytorch.org/models/resnet50-19c8e357.pth",
model_dir="./model_data")
model.load_state_dict(state_dict)
# ----------------------------------------------------------------------------#
# 获取特征提取部分,从conv1到model.layer3,最终获得一个38,38,1024的特征层
# ----------------------------------------------------------------------------#
features = list([model.conv1, model.bn1, model.relu, model.maxpool, model.layer1, model.layer2, model.layer3])
# ----------------------------------------------------------------------------#
# 获取分类部分,从model.layer4到model.avgpool,去掉了最后一个全连接层
# ----------------------------------------------------------------------------#
classifier = list([model.layer4, model.avgpool])
features = nn.Sequential(*features)
classifier = nn.Sequential(*classifier)
return features, classifier
经典的检测方法生成检测框都非常耗时,如OpenCV adaboost使用滑动窗口+图像金字塔生成检测框;或如R-CNN使用SS(Selective Search)方法生成检测框。而Faster RCNN则抛弃了传统的滑动窗口和SS方法,直接使用RPN生成检测框,这也是Faster R-CNN的巨大优势,能极大提升检测框的生成速度。
下图展示了RPN网络的具体结构,分为以下几个步骤:
(1)首先使用 3 × 3 3\times3 3×3 的filter对Feature Map进行卷积,目的是使提取出来的Feature更鲁棒。
(2)然后分为两个平行的分支:
anchors
是 positive
还是 negative
,获得anchor的类别信息,也就是该anchor是背景还是前景(只要有要识别的物品就属于前景);Ground Truth
(训练集图片上真实的框)的偏移信息,这一步也称为 bounding box regression
; (3)最后的 Proposal Layer
则负责综合(2)中的两个分支获取精确的proposals,并利用 NMS
非极大值抑制进行筛选,同时剔除太小和超出边界的proposals。
所谓anchors,实际上就是一组由rpn/generate_anchors.py生成的矩形框。直接运行作者demo中的generate_anchors.py可以得到以下输出:
[[ -84. -40. 99. 55.]
[-176. -88. 191. 103.]
[-360. -184. 375. 199.]
[ -56. -56. 71. 71.]
[-120. -120. 135. 135.]
[-248. -248. 263. 263.]
[ -36. -80. 51. 95.]
[ -80. -168. 95. 183.]
[-168. -344. 183. 359.]]
表示一个矩形框就需要四个参数,可以有两种表示方式:
- 中心坐标+长宽: ( x c e n t e r , y c e n t e r , w i d t h , h e i g h t ) (x_{center}, y_{center}, width, height) (xcenter,ycenter,width,height)
- 左上角坐标+右下角坐标: ( x m i n , y m i n , x m a x , y m a x ) (x_{min}, y_{min}, x_{max}, y_{max}) (xmin,ymin,xmax,ymax)
从上面的输出可以发现,采用的是左上角坐标+右下角坐标: ( x m i n , y m i n , x m a x , y m a x ) (x_{min}, y_{min}, x_{max}, y_{max}) (xmin,ymin,xmax,ymax)这种方式,在原图中生成anchor主要分为三步:
(1) 有一个base anchor,这个base anchor的尺寸可以自定义,默认尺寸为 16 × 16 16\times16 16×16。
(2) 从这个base acnhor生成 9 个不同尺寸的anchor,可以把这9个anchor视为后续anchor的模板。
注:关于上面的anchors size,其实是根据检测图像设置的。在python demo中,会把任意大小的输入图像reshape成800x600(即图2中的W=800,H=600)。再回头来看anchors的大小,anchors中长宽1:2中最大为352x704,长宽2:1中最大736x384,基本是cover了800x600的各个尺度和形状。
(3)使用上一步生成的9个anchor模板在原始图像上生成具体的anchor。(关键步骤)
c e i l ( 800 / 16 ) × c e i l ( 600 / 16 ) × 9 = 50 × 38 × 9 = 17100 ceil(800/16)×ceil(600/16)×9=50×38×9=17100 ceil(800/16)×ceil(600/16)×9=50×38×9=17100
VGG输出的feature map size= 50x38,ceil()表示向上取整。
这里讲一下rpn如何识别每一个anchor的类别,注意这边进行的是二分类,即判断anchor的内容是背景还是前景,而不是具体的类别,具体的类别判断还在这之后。
一副MxN大小的矩阵送入Faster RCNN网络后,到RPN网络变为(M/16)x(N/16),不妨设 W=M/16,H=N/16。在进入reshape与softmax之前,先做了1x1卷积,如下图所示:
9 x 2 =18的通道 用于预测 公用特征层上 每一个网格点上 每一个预测框内部是否包含了物体,序号为1的内容为包含物体的概率。
假设输入图像的维度为 ( 3 , 800 , 600 ) (3,800,600) (3,800,600),经过这里 1x1的卷积后,维度变为 ( 1 , 18 , 50 , 38 ) (1,18,50,38) (1,18,50,38),其中通道数为18,又因为有 9 个anchor,所以每 2 个通道为一个anchor的类别,这2个通道分别代表了anchor是背景和前景的概率,这一部分需要特别注意特征维度的变化。
因为我们需要对类别进行softmax,但由于维度为 ( 1 , 18 , 50 , 38 ) (1,18,50,38) (1,18,50,38),类别信息18是在第1维(此处从0开始计数),所以需要进行reshape操作。
如图所示绿色框为飞机的Ground Truth(GT),红色为提取的positive anchors,即便红色的框被分类器识别为飞机,但是由于红色的框定位不准,这张图相当于没有正确的检测出飞机。所以我们希望采用一种方法(Bounding Box Regression)对红色的框进行微调,使得positive anchors和GT更加接近。
Bounding Box Regression
主要是为了对生成的anchors进行位置上的微调,这里简写成bbox reg。在上文提到,作者在代码中表示一个anchor主要用了两种方式,其中第二种方式用的比较多,但在bbox reg中主要用第一种,也就是中心坐标+宽高的表示方式。
在经过下面这个1x1的卷积之后,根据前文对输入的假设,此时feature map变成了 ( 1 , 36 , 50 , 38 ) (1, 36, 50, 38) (1,36,50,38),其中通道数为36,这36个通道每4个代表一个anchor的位置偏移信息,一共有9组,而上文也提到,每一个feature map上的点会生成9个尺度不一的anchor。
9 x 4的卷积 用于预测 公用特征层上 每一个网格点上 每一个先验框的位置偏移情况。
每一个anchor的位置偏移信息格式是: ( d x , d y , d w , d h ) (d_x,d_y,d_w,d_h) (dx,dy,dw,dh) ,每一个分量代表的含义如下:
假设 ( x , y , w , h ) (x,y,w,h) (x,y,w,h)为变换前的坐标, ( x ′ , y ′ , w ′ , h ′ ) (x',y',w',h') (x′,y′,w′,h′)为变换后的坐标,则变换关系如下:
x ′ = w × d x + x y ′ = h × d y + y w ′ = w × e d w h ′ = h × e d h x' = w\times d_x+x\\ y' = h\times d_y+y\\ w'=w\times e^{d_w}\\ h'=h\times e^{d_h} x′=w×dx+xy′=h×dy+yw′=w×edwh′=h×edh
下面我们用严谨的数学公式推导重写一下上述过程:
对于窗口一般使用四维向量 ( x , y , w , h ) (x,y,w,h) (x,y,w,h) 表示,分别表示窗口的中心点坐标和宽高。对于下图,红色的框A代表原始的positive Anchors,绿色的框 G G G 代表目标的 G T G_T GT(Ground Truth),我们的目标是寻找一种关系,使得输入原始的anchor A经过映射得到一个跟真实窗口G更接近的回归窗口G’,即:
那么经过何种变换 F F F 才能从图10中的anchor A变为 G ′ G' G′ 呢? 比较简单的思路就是:
G x ′ = A w ⋅ d x ( A ) + A x G y ′ = A h ⋅ d y ( A ) + A y G_x′=A_w⋅d_x(A)+A_x \\ G_y′=A_h⋅d_y(A)+A_y Gx′=Aw⋅dx(A)+AxGy′=Ah⋅dy(A)+Ay
G w ′ = A w ⋅ exp ( d w ( A ) ) G h ′ = A h ⋅ exp ( d h ( A ) ) G_w′=A_w⋅\exp (d_w(A)) \\ G_h′=A_h⋅\exp (d_h(A)) Gw′=Aw⋅exp(dw(A))Gh′=Ah⋅exp(dh(A))
观察上面4个公式发现,需要学习的是 d x ( A ) , d y ( A ) , d w ( A ) , d h ( A ) d_x(A),d_y(A),d_w(A),d_h(A) dx(A),dy(A),dw(A),dh(A) 这四个变换。观察上面4个公式发现,当输入的anchor A与GT相差较小时,可以认为这种变换是一种线性变换, 那么就可以用线性回归来建模对窗口进行微调(注意,只有当anchors A和GT比较接近时,才能使用线性回归模型,否则就是复杂的非线性问题了)。
接下来的问题就是如何通过线性回归获得 d x ( A ) , d y ( A ) , d w ( A ) , d h ( A ) d_x(A),d_y(A),d_w(A),d_h(A) dx(A),dy(A),dw(A),dh(A) 了。线性回归就是给定输入的特征向量X, 学习一组参数 W W W, 使得经过线性回归后的值跟真实值 Y Y Y 非常接近,即 Y = W X Y=WX Y=WX。对于该问题,输入 X X X 是Feature Map
,定义为 Φ Φ Φ ;同时还有训练传入 A A A 与 G T G_T GT 之间的变换量,即 ( t x , t y , t w , t h ) (t_x,t_y,t_w,t_h) (tx,ty,tw,th)。输出是 d x ( A ) , d y ( A ) , d w ( A ) , d h ( A ) d_x(A),d_y(A),d_w(A),d_h(A) dx(A),dy(A),dw(A),dh(A) 四个变换。那么目标函数可以表示为:
d ∗ ( A ) = W ∗ T ⋅ ϕ ( A ) d_∗(A)=W_∗^T⋅ϕ(A) d∗(A)=W∗T⋅ϕ(A)
其中 ϕ ( A ) ϕ(A) ϕ(A) 是对应anchor的feature map组成的特征向量, W ∗ W_∗ W∗ 是需要学习的参数, d ∗ ( A ) d_∗(A) d∗(A) 是得到的预测值( ∗ * ∗ 表示 x , y , w , h x,y,w,h x,y,w,h,也就是每一个变换对应一个上述目标函数)。为了让预测值 d ∗ ( A ) d_∗(A) d∗(A) 与真实值 t ∗ t_∗ t∗ 差距最小,设计 L 1 L1 L1 损失函数:
L o s s = ∑ N i ∣ t ∗ i − W ∗ T ⋅ ϕ ( A i ) ∣ Loss=∑\limits_{N}\limits^i|t_∗^i−W_∗^T⋅ϕ(A_i)| Loss=N∑i∣t∗i−W∗T⋅ϕ(Ai)∣
函数优化目标为:
W ∗ ^ = a r g m i n W ∗ ∑ N i ∣ t ∗ i − W ∗ T ⋅ ϕ ( A i ) ∣ + λ ∣ ∣ W ∗ ∣ ∣ \hat{W_∗}=argmin_{W_∗}∑\limits_{N}\limits^i|t_∗^i−W_∗^T⋅ϕ(A_i)|+λ||W_∗|| W∗^=argminW∗N∑i∣t∗i−W∗T⋅ϕ(Ai)∣+λ∣∣W∗∣∣
为了方便描述,这里以L1损失为例介绍,而真实情况中一般使用soomth-L1损失。
需要说明,只有在 G T G_T GT与需要回归框位置比较接近时,才可近似认为上述线性变换成立。说完原理,对应于Faster RCNN原文,positive anchor与ground truth之间的平移量 ( t x , t y ) (t_x,t_y) (tx,ty) 与尺度因子 ( t w , t h ) (t_w,t_h) (tw,th) 如下:
t x = ( x − x a ) / w a t y = ( y − y a ) / h a t w = l o g ( w / w a ) t h = l o g ( h / h a ) t_x=(x−x_a)/w_a\\ t_y=(y−y_a)/h_a \\ t_w=log(w/w_a)\\ t_h=log(h/h_a) tx=(x−xa)/waty=(y−ya)/hatw=log(w/wa)th=log(h/ha)
对于训练bouding box regression网络回归分支,输入是cnn feature Φ Φ Φ ,监督信号是Anchor与 G T G_T GT的差距 ( t x , t y , t w , t h ) (t_x,t_y,t_w,t_h) (tx,ty,tw,th),即训练目标是:输入 Φ Φ Φ 的情况下使网络输出与监督信号尽可能接近。那么当bouding box regression工作时,再输入 Φ Φ Φ 时,回归网络分支的输出就是每个Anchor的平移量和变换尺度 ( t x , t y , t w , t h ) (t_x,t_y,t_w,t_h) (tx,ty,tw,th),显然即可用来修正Anchor位置了。
现在来总结一下:
VGG输出 50 × 38 × 256 50\times38\times256 50×38×256 的Feature Map,RPN输出:
恰好满足RPN完成positive/negative分类+bounding box regression坐标回归。
Proposal Layer
负责综合所有 [ d x ( A ) , d y ( A ) , d w ( A ) , d h ( A ) ] [d_x(A),d_y(A),d_w(A),d_h(A)] [dx(A),dy(A),dw(A),dh(A)] 变换量和positive anchors,计算出精准的proposal,并使用一些方法(NMS非极大值抑制等)剔除一些候选框,送入后续RoI Pooling Layer。
我们已经有一堆经过修正后的anchor,并且也知道了每一个anchor属于前景的概率,但我们细想一下,现在anchor的数量是不是太多了,我们只用了一张800*600的图像作为输入就生成了16650个anchor,如果全部作为RoI(Region of Intererst,也就是感兴趣区域或者说候选区域)输入到后续网络中,这计算量属实有点大,所以就需要进行一些筛选工作,这其实也就是RPN网络中Proposal层所做的工作。
Proposal Layer有4个输入:
im_info
和feat_stride
的含义为:对于一副任意大小PxQ图像,传入Faster RCNN前首先reshape到固定MxN,im_info=[M, N, scale_factor]则保存了此次缩放的所有信息。然后经过Conv Layers,经过4次pooling变为WxH=(M/16)x(N/16)大小,其中feature_stride=16则保存了该信息,用于计算anchor偏移量。
现在来梳理一下Proposal Layer的处理流程:
首先,现在我们的anchor有许多因为是在边缘生成的,所以它们的坐标可能是负值,或者简单来说就是超出了图片的范围,那么就需要对这些anchor进行裁剪,把它们统一裁剪到图片范围内,也就是将anchor左上角坐标小于0的值用0代替,右下角坐标的X轴数值大于W就用W代替,Y轴数值大于H的用H代替。
经过上一步的裁剪工作,就会有许多anchor会变得很小,这里我们设定一个阈值,凡是小于16x16的anchor,我们都把它丢弃掉。
接着,因为我们已经有了每一个anchor属于前景的概率,那么很明显如果一个anchor属于前景的概率太小,那么也没有留着的必要性,所以对这些anchor的前景概率从大到小进行argsort,得到每一个anchor的排序索引,只取前6000个,到这一步anchor还是很多,但此时不能再鲁莽的去除anchor,因为有可能会有误判(毕竟这个前景概率只是rpn的预测,并不是真实的),此时需要用NMS方法把IoU大于0.7的进行合并,对于合并完的anchor再取前300个,这样就把输入到RoI网络的anchor的数量大大减少了。
之后输出 p r o p o s a l = [ x 1 , y 1 , x 2 , y 2 ] proposal=[x_1, y_1, x_2, y_2] proposal=[x1,y1,x2,y2](左上角+右下角的坐标形式) ,注意,由于在第三步中将anchors映射回原图判断是否超出边界,所以这里输出的proposal是对应MxN输入图像尺度的,这点在后续网络中有用。
RPN网络结构就介绍到这里,总结起来就是:
(1)生成anchors -> softmax分类器提取positvie anchors;
(2)bbox reg回归positive anchors;
(3)Proposal Layer生成proposals;
在作者代码中,主要把RPN主要分成了两部分,一个是RPN Head,另一个是Proposal。
RPN Head
主要负责anchor的生成、anchor位置偏移量预测以及anchor的类别判断;Proposal
负责对生成的anchor进行进一步的筛选,将筛选后的anchor作为RoI输入到后续的网络中。先来看一个问题:对于传统的CNN(如AlexNet和VGG),当网络训练好后输入的图像尺寸必须是固定值,同时网络输出也是固定大小的vector or matrix。如果输入图像大小不定,问题就变得很麻烦,有2种解决办法:
无论采取哪种办法都不好,要么crop后破坏了图像的完整结构,要么warp破坏了图像原始形状信息。
回忆RPN网络生成的proposals的方法:对positive anchors进行bounding box regression,那么这样获得的300个proposals也是大小形状各不相同,所以Faster R-CNN中提出了RoI Pooling解决这个问题。
RoI Pooling是从Spatial Pyramid Pooling提出。
在R-CNN中为了统一输入使用了比较暴力的方法(resize),但在Fast R-CNN中,使用了RoI Pooling,这一方法参考了SPPNet的空间金字塔池化,可以将RoI Pooling看做空间金字塔池化的一个简化版。
ROI是框在conv特征图上的一个方型,用四元组定义(左上顶点r、c,高h和宽w),显然,RoI的大小是各不相同的,(无预处理的情况下)CNN无法处理大小不同的特征。这也是为什么R-CNN想不到共享特征的原因。那么,我们需要一个将特征图的特定区域改变维度(通常是降维)的工具,这个工具就是我们经常使用的池化(pooling)。
然而,Fast R-CNN中提出的兴趣域池化层 Roi Pooling 与我们熟知的各类池化层不同。
例如,指定每个区域输出的高和宽为 h 2 h_2 h2 和 w 2 w_2 w2,假设某一兴趣区域窗口的高和宽分别为 h h h 和 w w w,该窗口将被划分为形状为 h 2 × w 2 h_2 \times w_2 h2×w2 的子窗口网格,且每个子窗口的大小约为 ( h h 2 × w w 2 ) (\frac{h}{h_2}\times \frac{w}{w_2}) (h2h×w2w)。
任一子窗口的高和宽要取整,其中的最大元素作为该子窗口的输出。因此,兴趣区域池化层可从形状各异的兴趣区域中均抽取出形状相同的特征。
下图在4×4的输入上,选取了左上角的3×3区域作为Roi。对Roi做2×2的Roi Pooling 得到2×2的输出。
4个划分后的子窗口分别含有元素 (Roi pooling的每个网格大小不一定相等!) :
首先我们可以看到有两个输入,一个是黄色线的输入,这个是BackBone(ResNet50)的输出,也就是Feature Map,另一个是紫色线的输入,也就是RPN的输出(300个RoI的坐标信息)。
我们将上述两组数据输入到RoI Pooling中,得到每一个RoI对应位置的Feature Map,且每一个Feature Map的尺寸均为7x7。
从RoI Pooling获取到7x7=49大小的proposal feature maps后,送入后续Classification Layer,可以看到做了如下2件事:
cls_prob
概率向量;这里来看看全连接层InnerProduct layers:
其计算公式如下:
其中W和bias B都是预先训练好的,即大小是固定的,当然输入X和输出Y也就是固定大小。
Faster RCNN有三个部分需要训练,分别是特征提取器VGG16,RPN以及RoIHead。其中特征提取器一般是采用预训练模型进行微调,所以此处重点介绍RPN的训练以及RoI的训练。
虽然原论文中Faster RCNN是将这两部分分开训练的,但现在大多数实现都是进行联合训练的方式。分开训练的讲解可以参考:一文读懂Faster RCNN
首先来回想一下RPN的网络结构,在上文我把它分成了两部分,一部分是 RPN Head Layer
,另一部分是 Proposal Layer
,但只有RPN Head真正有参数需要训练,Proporsal只是用来进行RoI筛选的,并不需要训练,所以我们重点关注RPN Head部分,如下图所示:
上文有提到,RPN Head部分主要用于anchor的位置偏移预测以及anchor类别的预测,对于前文假定的图像输入,RPN Head会生成 50 × 38 × 9 = 17100 50\times38\times9=17100 50×38×9=17100 个anchor,很显然把这些全部用于训练并不合理,因为这里面有大量的负样本,所以需要先进行一波筛选,选出256个作为训练样本(这个数目是作者提出的),其中正样本128个,负样本128个,其中负样本个数肯定可以满足,但正样本基本很难会有128个,所以作者在文中说,如果正样本不足128个,则空缺部分用负样本填充,具体的训练样本筛选步骤如下:
在筛选出了训练样本之后,就需要计算每一个anchor的Loss。
如上图所示,RPN的损失函数由两部分组成,一个是分类损失,另一个是边界框回归损失,其中公式中一些变量的含义已在图中标明了。
首先是分类损失,此处的类别仅仅是指anchor属于物品还是背景,所以这是一个二分类问题,因此在论文中作者是使用了 二值交叉熵损失 来计算RPN的分类损失,具体如下图所示:
然后是边界框回归损失,具体如下图所示:
如上图所示, L r e g ( t i , t i ∗ ) L_{reg}(t_i,t_i^∗) Lreg(ti,ti∗) 是 S m o o t h L 1 Smooth L_1 SmoothL1 函数(这里可以考虑下为什么用 S m o o t h L 1 Smooth L_1 SmoothL1 而不是 L 2 L_2 L2,可以参考Single Bounding Box Regression), t i t_i ti 是anchor的四个回归预测值,它代表了预测的偏移量,即预测anchor的中心坐标以及宽高相对于真实anchor的偏移量 ( t x , t y , t w , t h ) (t_x,t_y,t_w,t_h) (tx,ty,tw,th) , t i ∗ t_i^∗ ti∗ 代表了真实的偏移量 ( t x ∗ , t y ∗ , t w ∗ , t h ∗ ) (t_x^∗,t_y^∗,t_w^∗,t_h^∗) (tx∗,ty∗,tw∗,th∗) 。
此处特别需要注意: t i t_i ti 其实就是RPN网络的一个输出,即下图中框出的部分,我看论文的时候被作者的那些公式迷惑住了,之后看代码才会明白,即 t i t_i ti 是神经网络的输出,而不是公式计算所得,公式仅仅只是用来解释 t i t_i ti 所代表的含义。
而真正需要用公式进行计算的是 t i ∗ t_i^∗ ti∗ ,也就是预测的anchor与真实bbox的偏移量 ,计算公式就如上图所示,其中 ( x ∗ , y ∗ , w ∗ , h ∗ ) (x^∗,y^∗,w^∗,h^∗) (x∗,y∗,w∗,h∗) 都代表真实的bbox的中心坐标与宽高, ( x a , y a , w a , h a ) (x_a,y_a,w_a,h_a) (xa,ya,wa,ha) 代表预测的anchor的中心坐标以及宽和高。
最后,在知道了预测的偏移量以及真实的偏移量后,就可以使用Smooth L1计算回归损失了。
前文中有提到RPN网络中的Proposal Layer会对生成的anchor进行一些筛选工作,筛选出的anchor就是RoI,而且在测试阶段筛选出的RoI数量是300,但在训练阶段RPN会筛选出2000个RoI,然后再在这2000个RoI中挑选出128个高质量样本用于RoIHead的训练,其中正负样本的比例为1:3,具体的样本筛选步骤如下所示:
在有了训练样本后,就需要计算该部分的损失,RoIHead的损失计算和RPN几乎一模一样,也是分为分类损失与回归损失,分类损失使用交叉熵损失函数(注意,这是与RPN训练的一个不同点,RoI的分类是多分类问题),回归也是用 S m o o t h L 1 Smooth L_1 SmoothL1损失。
1.为什么 RPN能够预测 groud truth 的位置(输入特征只有图像像素的卷积特征,完全没有位置信息)
参考:RPN网络的个人疑惑
2.为什么要生成一堆anchor,再对它们进行修正,而不是一开始直接预测候选区域的坐标?
其实YOLO v1就是没有使用anchor,直接对候选区域的坐标进行预测,但作者发现,效果并不好,主要是因为网络很难收敛,训练难度较大,所以YOLO的作者后来就将Faster RCC的RPN进行了相关的修改,加入到了YOLO v2中,效果有了显著的提高。
3.为什么Faster-rcnn、SSD中使用Smooth L1 Loss 而不用Smooth L2 Loss?
参考:为什么Faster-rcnn、SSD中使用Smooth L1 Loss
4.RPN网络相关详解
参考:
RPN网络结构及详解
RPN 解析
RPN疑点解析
Faster RCNN之RPN理解
5.什么是模型的训练、推理和部署?
【扫盲】什么是模型推理(model inference)
深度学习的宏观框架——训练(training)和推理(inference)及其应用场景