Faster R-CNN实现细节

本篇博客记录Fatser R-CNN的实现过程。Ref部分给的链接的代码是python+c+CUDA混合编程的,本博客不会涉及CUDA的并行,完全基于Python+Pytorch+Numpy。PS:都做人工智能了,CUDA的并行也得学习,大牛的代码都是会考虑性能的,不能还是初级阶段实现功能就行。

算法思想

包括SSD在内的这些OB算法,都是用卷积提特征,基于特征使用卷积层或者全连接计算类别和偏移量,非线性的激活函数应用到神经元里面,这样的网络结构能够拟合任意函数.相比于SSD多特征层分别应用锚框,Faster R-CNN更简单.

整体架构

在这里插入图片描述
这一部分适合在阅读完整篇文章后或者已经对Faster R-CNN了解后看.随着训练的进行,RPN的前向输出会达到一定的准确率,但是在训练前期,因为参数不是很优,RPN的前向结果不是很好,如果直接使用RPN的结果进行RoIPooling以及之后的处理的话,那么全连接层及其之后的层的学习效果就不是很好,所以在训练时,我们需要在RPN网络的输出中加入真实框,可以帮助训练全连接层及其之后的层.最后我们在测试时直接使用RPN的输出,进行RoIPooling,进而传入全连接层及其之后的层. 在实现的角度来说,我们在train模式加入真实框,在eval模式不加入真实框,直接使用RPN的输出,这一部分输出并不是最终的在图片上显示的anchors,就如应用部分所讲,得到这一部分anchors后,我们还要基于它们的分数和边界框偏移量预测值,进行进一步裁剪、移动和删减.正如所描述的,在测试阶段Faster R-CNN会进行两次anchors的移动,而在训练时因为不需要最后的输出我们只用进行RPN中的anchors移动(RPN的结果是相对于输入图片的坐标).

NMS(Non-Maximum Suppression)非极大值抑制

Faster R-CNN网络采用了anchor思想,所以anchors很多,实质就是穷举anchors,所以anchors间很多重复区域,我们希望最小化anchors数量,在那些IoU很大的anchors之间保留前景(非背景)分数较高的anchors。在Faster R-CNN中,它用于RPN中,与SSD中有点不一样,因为它将所有前景类别看做一个类别即前景,它的步骤是直接取所有anchors的前景预测分数,然后从大到小排序,首先取最大的分数的anchor,以它为基准,遍历分数比他低的所有其他anchor,如果这个anchor与它的IoU大于某个阀值,那么就把这个anchor移出,遍历完后,选择次大的且未被移出的anchor,作为基准,重复这样的过程,直到没有这样的基准,剩余的anchor就是NMS的结果。

特征层和区域提议网络(RPN)

此部分介绍整体架构中的卷积神经网络和RPN。

卷积神经网络

其中卷积神经网络部分可以使用Resnet、VGG-16,VGG-16使用conv-5_3,即最后一个特征层,最后输出的特征的尺度为原始图片尺度的1/16。在下文中,为了便于区分,我们称这一部分为特征层。

RPN

在RPN中的卷积层是一个输入输出channels均为512,核大小为3*3,填充为1的卷积层,再接上一个ReLU,它的输入是特征层的输出。在通过这一层得到了一个更加综合的特征后,对于二元类别预测部分,它是一个卷积层,输入channels为512,因为我们是二元预测,即背景和前景两个类别,所以输出channels为anchors数目*2,核大小为1,没有ReLU和填充,它的作用就是得到分别以特征图上所有点为中心,在一组anchors上的预测类别。卷积核为1的卷积层有综合特征的作用,综合各个channel的特征得到最后的预测分数。同理边界框预测部分依然是一个卷积层,它和二元类别预测部分的唯一不同是输出的channels,这里的channels的数量应该为anchors数目*4边界框预测部分的输出是计算anchors与真实框的偏移量,即一个回归问题。我们在得到了二元预测结果后需要通过softmax层得到概率(这里涉及将卷积输出的类别预测结果变换一下形状)。
然后我们需要利用类别预测信息和边界框预测信息生成anchors。首先我们使用scale=[8, 16, 32](PS:scale应根据input图片本身的大小而定,论文中进行缩放将最小边转为600,scales采用128*128、256*256、512*512,因为这是相对原图的anchor尺寸,我们需要缩放到feature map上即除以16,刚好为8*8,16*16,32*32),ratios=[0.5, 1, 2]参数产生9个anchors,首先使用ratios保持面积得对长宽进行放缩,得到三个anchors,然后对这三个anchors使用scale进行放缩,得到最终的9个anchors。anchors的数量是一个超参,这个超参决定了二元类别预测部分和边界框预测部分的网络结构。我们假定类别预测部分的输出的前anchors数目通道为对背景的预测,后anchors数目通道是对前景的预测。我们暂时只需要对前景的预测,所以我们取后anchors数目的通道的所有anchors的前景预测。
因为之前得到的9个anchors是相对坐标,我们需要将anchors的坐标放回原图的坐标中去,前面已经提到了最后的特征图的尺寸为原图的1/16,所以只需要将特征图上的坐标乘上16即可还原,然后我们加上相对坐标,就得到了anchors在原图中对应的坐标,最后的形状为(H*W*A,4),特征图上的每一个点对应了A个anchors。
我们将边界框偏移量预测的结果变换一下形状,将其应用到我们的anchors上,变换一下位置,这样就得到了提议区域
因为得到的提议区域很多,且可能提议区域坐标已超过原图大小,或者为负数,我们需要进行裁剪,将坐标变为原图范围内。我们设定了一个anchor的最小尺寸,我们需要移出那些很小的anchors,同时从上面得到的所有anchors的前景预测结果中去掉这些小的anchors。此时因为anchors还是很多,在进行NMS(Non-Maximum Suppression)前我们需要去除一部分前景得分低的anchors,这一部分的设置为取12000个anchors。然后我们进行NMS,设置Thresh为0.7。我们在实现NMS时把分高的先加入进最终的集合。这样的anchors集合同样很大,我们在进行完NMS后,取前景预测分数最高的2000个anchors。这个就是RPN的输出了。这个输出anchors是加上了RPN预测的偏移量的。因为特征层很简单,且后续的兴趣区域池化层,需要先计算RPN,我们将它合并到RPN中,所以最终RPN的前向输出就有特征层的输出和anchors信息(形状为batch_size,x1,x2,x3,x4)。anchors的坐标是相对于原图的.
在测试和训练时,RPN网络的前向输出除了前向输出的anchors数量不同外其他没什么区别.训练时输出更多的anchors,测试时输出较少的anchors.

RoI pooling

这一层就是兴趣区域池化层,这个池化层与我们平时使用的池化层有点区别.PyTorch中有torch.nn.Module和torch.autograd.Function的概念,Module一般是一层或者几层的网络,它不需要backward函数,它的求导由自动机制完成,而Function一般用于某个自定义函数的父类,它是计算图中的边,他会为每一个操作创建一个函数对象,使用计算图保存数据和计算历史,需要我们自己定义它的子类的backward函数进行反向传播.
这里我们首先创建一个继承自Module类的RoIPool类,在这个类的forward函数中定义一个继承自Function的RoIPoolFunction类,直接使用对象+()的形式就可以调用RoIPoolFunction的forward函数.下面我们关键说明RoIPoolFunction的实现.(对于RoIPool类我认为是可以省略的,它仅仅是一个打包类)
因为各个RoI的大小是不一样的,我们通过前向传播时我们希望将这些RoI的特征变成一个相同尺寸,对于一张图片,RoI Pooling的输出应该是ncwh(n表示anchors(RoIs)数量,c分别表示特征层的通道数,w h表示我们希望的特征尺度,是一个超参,我们取7),它的输入是特征层的输出和RoIs区域(这个区域在训练和测试时计算方法不一样).因为这些RoIs区域的坐标是相对于原图的,所以我们需要乘上1/16.我们将RoI在特征层输出上的区域均分为77的区域,对这49个区域我们进行最大池化操作,因为每一个区域的计算都是独立的,我们能够进行并行化,作者的代码是将输出拉成一维,然后对于输出的每一个元素进行并行化.除了得到结果,我们需要记录额外的信息,便于后面进行反向传播,我们需要记录输出的每一个元素对应于特征层的输出的位置,你也可以把特征层拉成一维时的坐标作为那个位置,只要可以还原即可.对于某一个anchor来说,它的通道与特征层的输出的c是对应的,即相同的c只能在特征层的相同c中进行RoIPooling.
接下来就是反向传播了,对于Function的backward函数的输入是对于其输出的梯度,根据链式法则,我们只需要进行乘上这一层的局部梯度得到其输入的梯度,返回即可.所以最终的梯度的维度与其输入的维度一样,即特征层的输出的维度.对于后层传来的梯度,如果特征层的输出作为最大值参加了计算,那么需要累加这些位置的梯度,因为是最大值运算,所以局部梯度为1.同样的道理,这里最终每一个梯度值的计算都是独立的,可以进行并行.算法步骤是对于每一个最终的梯度值位置我们遍历所有的RoIs,接着判断这个位置是否已经超过了当前anchor的范围,如果没有超过,说明这个位置可能参加了前向计算,接着计算这个位置如果参加前向大概在前向结果的哪些位置(实际上这个位置只可能参与某一个值的计算,但是因为我们在前向的过程中有取整的计算,所以我们最好遍历更多的位置,在估计这些位置时,尽可能最大化),接着我们就要使用之前前向传播存储的最大数位置坐标了,遍历这些位置的最大值位置,如果就等于当前位置,那么加上这些位置的梯度.为了计算这一层后向传播的梯度,除了存储最大数位置坐标,我们还需要存储RoIs 特征层输出的维度 兴趣区域池化层的输出尺寸 缩放比例(原图尺寸到特征层输出尺寸的比例).

全连接层、类别预测和边界框预测

全连接部分是由两个全连接层组成的,结构分别为(51277,4096),(4096,4096).我们在得到了RoIPooling的输出后,将每个anchor的特征(c,h,w)拉成一维的,这样RoIPooling的结果变为了(n,51277),n是anchors的数目,相当于batch_size.我们在每一个全连接层做完ReLU运算后,使用一个Dropout.
类别预测我们使用一个全连接层,它的结构是(4096,类别数),边界框预测的结构是(4096,类别数乘以4),他们均不使用ReLU,直接输出线性运算结果.他们的输入为上一层的全连接层输出.最后在测试时整个Faster R-CNN网络的输出为类别预测结果的softmax、边界框预测输出和RPN网络的前向输出的anchors.

训练

作者在论文中提到了,联合在一起训练可能难以收敛,而训练主要解决的是share feature maps,作者提出了4步交替训练法——RPN训练、基于训练好的RPN训练FAST-RCNN、固定feature maps为FAST-RCNN结果仅仅微调RPN网络、固定RPN和feature maps微调FAST-RCNN全联接部分。

RPN训练

我们需要真实的边界框、困难的边界框(是真实边界框的子集)、不关心的边界框(包含一些小目标,我们不用这个)。同样的方法,我们首先为特征图上每一点生成一组anchors,anchors的坐标都是相对于原图,然后直接去除那些坐标超过原图边界或者为负的anchors。利用剩余的anchors,然后我们根据真实的边界框计算每一个anchors和每一个真实框的IoU,然后我们得到每个anchors与哪一个真实框overlap最大,我们设置背景的overlap为0.3,那些最大overlap都小于0.3的我们标注为背景,然后我们计算每一个真实框与哪一个anchor有最大的overlap,我们将这些anchors标注为前景类别,最后我们将那些最大overlap大于等于0.7的anchors标注为前景类别。然后我们处理不关心的边界框,我们计算anchors与不关心边界框相互之间的相交面积除以anchors的面积,然后对于一个anchor我们相加这个面积,将面积和大于0.5的anchors的标签设为-1。接着我们处理困难的边界框,我们依然求anchors和困难边界框之间的IoU,对于每一个anchors来说,我们求它的最大overlap,如果overlap大于等于0.7,那么设置这个anchor的标签为-1,然后计算每一个困难边界框对应哪一个anchor有最大overlap,将这些anchors的标签设为-1。我们设置标签为1的anchors的数量最高为128,如果超过了这个数量,那么我们随机采样,把采样得到的anchors标签设为-1。然后我们计算类别为背景(0)的anchors的最大个数为256-目前标签为1的anchors数目,如果超过这个数目,那么我们依然采样,将采样的anchors设为-1。
接下来我们需要标注anchors的偏移量,依然是上述的anchors,我们不管他们的标签是什么,我们用他们和他们overlap最大值对应的真实边界框计算偏移量,偏移量的计算和移动anchors的计算刚好相反。
接着我们计算mask,这个mask是一个(H*W*A,4)形状,mask中类别为1的anchors的值为四个1,其他均为0。(PS:还有一个bbox的外部权值,它的值和mask一样,但是不知道干嘛的,应该都是用于偏移量的权值。)
因为之前我们使用的anchors都是在图片内部的anchors,不是最开始的完整的anchors,我们需要变换回去,以-1和0进行填充。为了计算损失值,我们对将label变为了(1, A * height, width,1);我们将偏移量标签、mask、bbox的外部权值变为(1, A * 4, height, width)。
接着我们就要建立损失,我们需要两种损失,一个是针对类别的,另一个是针对anchors偏移量的,对于类别损失我们只关心背景类别和前景类别,对于标签为-1的我们不关心,我们使用交叉熵损失函数,显然我们需要直接使用卷积层的输出,而不能经过softmax,这里我们是对二元类别预测部分的卷积和边界框预测部分的卷积的结果计算损失,而与RPN的前向传播结果没有太大关系.从上面的标签可见,偏移量标签维度与卷积的输出维度一样,我们只关心预测为1的anchors,所以我们先用mask按element-wise分别乘上偏移量预测和偏移量标签,然后将结果使用平滑L1范数损失,PyTorch中对这个损失函数的定义如下
在这里插入图片描述
n为总元素.我们不希望它直接除以n,因为我们的矩阵中有很多0,这样会减小损失值,我们需要设定size_average=False,然后除以前景数量,为了确保分母不为0,我们加上一个epsilon=1e-4.最终我们加上这两个损失值,其中偏移量损失我们乘上10,交叉熵损失不变.

Faster R-CNN输出部分

可以想象如果在训练的时候我们使用RPN的输出锚框,那么训练将非常困难,因为训练前期RPN锚框的输出是随机的,让最后的输出部分基于这样的锚框进行学习效果会很差.所以我们需要在RPN预测的锚框基础上加入真实的锚框.
先我们需要从真实框中去除困难框,我们基于这些简单框还要生成相对真实框抖动的框,这个抖动框和其相对应的真实框的宽度和长度一样,只是位置有所偏移(PS:这一部分为什么需要抖动,作者说为了避免将真实框加入到RoIs中将造成0损失,不太懂).接着我们将简单框和抖动框合并到RPN预测的RoIs中.
首先我们对所有anchors进行打标签,他们的标签是IoU最大的那个真实边界框的标签.接下来需要对这一部分RoIs进行筛选了,首先我们去除与困难边界框最大IoU大于等于0.5的anchors.接着去除与所有不关心边界框交互面积占当前anchor面积比例之和大于0.5的anchors.我们先找到那些最大IoU大于等于0.5的anchors,从这里面去除刚才找到的需要删除的anchors,这样我们就得到了我们认为是前景的anchors,这个前景anchors列表和前面的标签列表不一样,前面的标签列表中一些背景anchors也被设置了前景标签.接着我们设置最大的前景anchors列表的size为32,如果我们当前的列表的大小大于32,那么进行随机采样.接着我们用同样的方式找背景框,这些框的最大IoU在[0.1,0.5)之间的anchors,依然去除那些需要删除的anchors.我们用128减去当前前景anchors的个数得到背景框的最大个数,如果背景框列表的个数大于它,依然采样.我们从最开始生成的标签列表中找出这些有效的anchors,并将背景部分设为0.然后从所有的anchors中取出这些有效的anchors.然后我们生成这些anchors的边界框标签,因为我们要与Faster R-CNN最后的输出形状相匹配,所以标签为一维(n,),anchors为(n,5)(第二维的0维是图片的batch_size),边界框标签结果为(n,4类别数目),mask为(n,4类别数目).同样对于边界框标签和mask我们依然只关心标签为前景的anchors,即除了前景的anchors边界框标签其他均为0.
这个输出的anchors就是我们在训练网络最后输出部分所使用的anchors,注意在测试时直接使用RPN的输出anchors.损失的计算和RPN差不多,有一点不同在于在进行交叉熵计算时使用了权值,0类的权值为标签为前景的anchors数量除以标签为背景的anchors的数量,其他为1.根据作者代码的参数设置,训练Faster R-CNN最后一部分时使用的anchors的数量会包含RPN的结果,至少会包含背景anchors.
这里的偏移量损失一样使用平滑L1范数损失,然后它的系数依然为10.最终的损失值是这两个损失的加权和,这与RPN一样.

完整的训练过程以及参数设定

我们从特征层部分中的conv-3开始训练,因为前几层的特征层都是提取一些边缘信息,不同数据集差别不大.我们使用动量随机梯度下降更新算法,设定起始学习率为0.001,正则化系数为0.0005,动量系数为0.9,对于训练的epoch,这个还得跟数据集有关吧,也是一种超参.
我们将一张图片传入我们构造的Faster R-CNN中,根据我们的实现我们前向传播的过程中就构造了两个损失值,最终我们需要优化的损失值是RPN网络的损失加上Faster R-CNN最后部分的损失,接着将梯度置0,然后使用最终的损失值反向传播,这里防止梯度过大,我们需要进行裁剪,我们得到所有需要更新的参数的梯度,求这些梯度的平方和再开根号,我们设置这个值的最大值为10,如果超过了就让所有需要更新参数的梯度乘上10除以我们得到的值,这样新的梯度的平方和开根号就等于10.裁剪完以后我们就需要根据新的梯度更新参数.注意一个比较好的习惯是训练到一定轮数后要保存参数,不然程序崩了就又得重头开始训练,亲身经历,这种情况很难受.对于PyTorch来说需要我们自己手动进行学习率衰减,很简单就是用新的学习率重新申请一个新的优化器,其他你认为在训练时需要的log信息可以打印出来,包括一段时间内的损失值信息.到此训练部分就讲完了.

测试

测试部分与训练部分差别就是将网络改为eval模式,然后不再更新参数,测试比训练更加简单,不再多说.

应用

对于一张随意的图片我们也要能够处理,并输出边界框.其实因为我们使用了RoIPooling操作,所以我们对原图实际上不用处理为标准格式.但作者在他的demo程序中进行了缩放处理.我们首先需要对图像进行缩放处理,我们有目标范围为600,最大尺寸为1000,我们先用600除以图片宽高中的最小值,得到缩放比例,用这个系数乘上图片宽高的最大值,如果超过了最大尺寸1000,那么我们就用1000除以最大值得到缩放比例,否则我们就用已得到的缩放比例,这样都在1000以内.其实可以发现,宽高不同的图片最终结果也可能不同,验证了网络确实可以处理不同尺寸的图片.然后我们获得图片信息包括图片的尺度和相对原图的变换比例,其他的参数如真实边界框等我们就不需要了,这些只在我们计算损失即前向传播时有用.我们将图片和图片信息传入Faster R-CNN得到预测分数、边界框预测和边界框偏移量预测.者如前面所说这个anchors的数目非常大且还包括背景,我们需要裁剪.
我们从预测分数结果中找到anchors的预测类别和对应得分,我们从这个结果里面得到预测为前景的anchors和其得分大于等于0.7的anchors.然后我们得到这些anchors的边界框偏移量预测,因为我们预测的结果的第二维是4*类别数目,我们裁剪它,我们只需要它对应类别的边界框偏移量预测值,得到的第二维的大小为4.然后我们基于这个偏移量移动anchors,移动的方法和我们计算偏移量标签时的方法相反,最后因为我们对原图进行了缩放,我们需要还原回去,所以我们将最终的anchors坐标除以我们的缩放比例.在作者的代码中,先进行缩放然后移动anchors,我觉得这样不合理,我们得到的anchors的偏移量应该是基于输入图的而不是原图(PS:个人觉得原图的缩放就没有多大意义).我们最后为了确保anchors在图片内,我们需要对框进行裁剪,使得框在图片范围内.这样处理后anchors的数量还是很多,且anchors之间的重叠区域可能很大,我们需要进行NMS.我们使用前面提到的NMS方法取出最终的anchors,其中阀值设为0.3.对于这里可以直接输出最终的anchors框、分数和类别名词.最后我们可以使用OpenCV库在原图上画出边界框、分数和类别.

Ref

[1]https://github.com/longcw/faster_rcnn_pytorch
[2]https://github.com/d2l-ai/d2l-zh

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值