文章目录

  • faster rcnn 原理概括
  • 特征提取层的特点和其与feature mpa坐标映射的关系
  • RPN layer详解
  • ROI pooling详解
  • 分类层与第二次边框回归

faster rcnn 原理概括


fater rcnn就是一个几个不同功能的小卷积结合起来的一个大卷积,后面的全连接层同样可以用卷积层来代替。
我们输入的图片首先会被放缩到MxN的大小,然后经过一个特征提取网络得到我们的feature map。然后根据feature map的尺寸来得到我们的anchors。
之后feature map会进入RPN layer,这一步的最终结果会帮我们提出有物体的框,并对这些框的位置和大小进行边框回归,这些被初选出来的框会进一步进行筛选得到进入ROI pooling的最终128个框,之后128个框会变成一样的大小,之后可以看作对128张图片进行的图像识别,最终会对这些框进行识别并且进行第二次的边框回归,第二次的边框会给它增加物体类别的特征。

下面我会用代码配合原理来讲解每一步是如何实现的

特征提取层的特点和其与feature mpa坐标映射的关系

题目里面包含的信息量我个人认为就已经很大了,因为很多人不会去注意这一点。在faster rcnn的论文中就已经提到,它自身提取特征的网络是VGG16,并且这个提取特征的网络可以是我们现在用的任何一种成熟的网络。在VGG16中,有13个卷基层,13个relu层,4个maxpooling层,其结构我会去分析为什么这么做,而不是为什么不那样做。
这13个卷基层都有一个特点,那就是其卷积本身并不改变特征图的尺寸,这样有一个好处,就是虽然每次经过一个卷积以后,虽然感受野扩大了,但是你会发现感受野的中心始终没变,换句话说,如果卷积不改变大小,每一个特征点映射回上面任意特征层的中心位置就是它自己所在的这个特征层的位置,并且始终不会改变

这是我自己画的示意图,黑框表示需要卷积的特征层,红框表示为了保证卷积大小不变增加的padding,可以看到,一个3x3的卷积卷右上角的时候,其中心点在原来特征图的坐标是(0.5,0.5),其卷积以后的结果也在(0.5,0.5)这个位置,往右边移动一个步长,第二个卷积(蓝色)的中心在(0.2,1.5),其卷积以后的结果也在(0.5,1.5),这就是我刚才说的,只要你保证你卷积大小不变,感受野肯定会变大,但是中心点坐标始终不变。
所以综上所述我们便会引申出来一个问题,从我们对于产生anchors的原理我们知道,对于坐标的映射,卷积不会起到任何作用,relu就更别说了,所以问题一定出在pool上,这个pool在VGG16里面,是采用了3x3,步长为2x2,但是呢,在tf里面pool层设置了一个padding=same,设置为same时候就代表,最后的输出和池化核的大小无关,只和步长有关,除以步长就是最后的大小,这也是为什么最大池化可以将尺寸减半的原因。那么它与池化核大小无关的原理是什么呢?我们以3x3的池化核为例,只需要在需要池化的卷积层的周围补一圈0就可以了。之后我们在研究映射关系的时候就方便多了

黑框是我们需要进行池化的特征层,红框是我们padding的一圈0,池化的大小是3x3,步长是2,第一个池化的步骤的结果是下面的蓝色虚线,之后往后右移了两个单位做第二个池化,结果是绿色虚线。之后我们便会发现,下面蓝色结果中,我们的中心点坐标是(0.5,0.5),乘上2,也就是我们缩小的倍数,刚好是(1,1),也就是我们坐池化的中心格子坐标。同样的,绿色结果的中心点坐标是(1.5,0.5),其对应的池化的中心的格子坐标也是(3,1)。但是这样举例可能有一个矛盾的地方,我们在卷积的时候明明是点对点的映射,发现没有改变,为什么到池化就成点对格子了呢?
其实卷积的时候我想要强调的是格子本身在特征图与上一层的特征图中的位置是不变的,你用格子对格子,点对点得出来的结论都是一样的。而对于池化,我们必须用点对格子的方法来进行映射,你可以理解为这是一个经验,也就是说只有点对格子才会符合我们的映射公式,那我们的映射公式是什么呢?(其实就是乘缩小的倍数)我们假设feature map的大小为50x50,那么,我们feature map上面随便找到一个特征点的坐标,比如第5行,第8个格子(5,8),然后找到这个格子的中线点坐标(5.5,8.5),然后乘16,就是(88,136),所以(5,8)映射回特征图的中心点坐标是(88,136),然后就可以形成anchors了。代码奉上。

 shift_x = (np.arange(0, shape[0], dtype=keras.backend.floatx()) + 0.5) * strideshift_y = (np.arange(0, shape[1], dtype=keras.backend.floatx()) + 0.5) * stride

stride就是16,加的那个0.5就是找格子的中心点坐标

RPN layer详解

在说明之前我阐明一下我上面没提到的准备工作,就是ahchors的产生原理,其实就是feature map上面的每一个特征点映射回原图以后中心格子,以这个格子为中心,做一组三个宽高比,三种面积,一共9个anchor。我们以后的举例都用特征图为50x50来说明。如此一来,我们得到的所有anchors一共有22500个(50x50x9),我们的RPN层其实就是在对22500个anchors进行筛选

再来说明一下RNP网络的作用,这个作用不是它的功能作用,而是训练作用,我们会在RPN阶段产生两个误差,既然有误差,就一定会有标签,而两个误差对应的标签是我们根据目标框的位置自己算出来的,所以我会重点放在标签的设置上面。

首先我们会对feature map进行一个3x3的卷积,对于这个3x3的卷积的解释可谓是众说纷纭,有说可以改善鲁棒性的,我在这里给出我的一个解释,那就是这个3x3的卷积可以提高迁移性,也就是说我们允许前面的VGG16网络使用的权重是已经训练好的权重,在迁移到另一个网络以后我们就只需要训练这个3x3卷积就可以得到一个符合自己要求的特征层了。当然,这个3x3卷积的存在不影响我们解释RPN的原理。
在经过3x3卷积以后,我们会兵分两路,上路的作用是判断这个anchor内是否有物体
下路的作用是对anchor进行边框回归。我们逐一说明
首先是上路:其会首先经过1x1的卷积来改变通道数为18,准确来说是9x2,也就是50x50x(9x2),9x2表示每一个特征格产生的9个anchor内是否有物体,用一个二分类来表示。这里我要说明的是,对于判断是否有物体,我们并没有对所有的22500框进行判断,而是从中选取了8940框,这8940个框具备什么特点呢?那就是他们是完完整整都在原图尺寸范围内的,也就是说在兵分两路之前,我们会对22500个框进行判断,看看他们是否超出了原图的范围,然后把未超出范围的框全部拿出来,就是这8940个。那剩下的框我们会单独为他们这是标签。
我们会先对8940个框分别对所有的目标框所在的位置(我们假设只有两个目标框)做iou处理,并且规定iou大于0.7,被视作这个框内存在物体,给其标签定为1。小于0.3的,认为其内没有物体,标签设定为0。二者之间的,标签设为-1。这里还有一个小问题,就是万一没有0.7的怎么办,那就找到两个目标框最大的那些anchor,标签单独设置为1,其他操作不变,这种情况很少发生。
如此一来(以下数据均为假设),8940个anchors里面有200个anchor的标签被视为1。5000个标签被设为0,其余被设置为-1。可以看到正负样本之间差别太大,训练的时候会使得效果权重偏向负样本。故我们需要缩小这里面的比例,我们会从所有的正样本中随机抽取128个正样本,这128个正样本的标签保持不变,其余的正样本标签设置为-1,如果不够128个,那就不动正样本的标签。然后从5000个负样本中抽取(256-标签为1的个数)个负样本,也就是说负样本加正样本一共256个,同样的道路,我们从5000个里面随机抽要求的个数的负样本,保持其标签为0不变,其余的全部改成-1。这样以后我们的在8940里面的标签分布就是a个标签为1,b个标签为0,8940-256个标签为-1。那么,有没有一种可能,那就是正样本太多了,我们只取了128个,然后另外的128个从负样本里面去,但是负样本又太少了,凑不齐258个,那这样我们的总数就达不到256了。答,是有这种可能的,所以对于训练集的图片,目标框一定不能太多,还有就是设置合理的阈值来归类最初的正负样本。
那8940个anchors以外的anchors的标签呢?同样,全部设置为-1就可以了。
代码奉上,注释都是我自己作为一个初学者写的,有什么不对的欢迎指正。

def get_pos_neg_sample(ious,valid_anchor_len,pos_iou_threshold=0.7,neg_iou_threshold=0.3,pos_ratio=0.5,n_sample=256):#这里的流程是先判断8940个框离哪个bbox更近,然后看看对这个bbox的iou的阈值如何,来判断其正负样本argmax_ious=ious.argmax(axis=1)#(8940,)里面是[0,0,1,1,1,0..]这种形式#然后提取出这个最大iou到底是多少max_ious=ious[np.arange(valid_anchor_len),argmax_ious]#(8940,)label=np.empty((valid_anchor_len,),dtype=np.int32)label.fill(-1)label[max_ious<neg_iou_threshold]=0#首先max_ious是(8940,),其会返回小于负样本阈值的下标,然后label会取这些下标,然后全部置为0label[max_ious>pos_iou_threshold]=1#同上#但是这里有一个问题,那就是如果没有任何一个框与所有的bbox的iou都没有到0.7的阈值,那么这时候所有的8940的框的标签都会被设置为-1,相当于#这幅图里面没有正样本,也就可以说这幅图是训练里,至少在边框回归时候是无效的,但这是不能够被允许的,被白白浪费训练资源,所以必须给这些没有#到0.7阈值的框也必须从他们中间找到正样本,所以我们选择找到所有两个bbox中iou中最大的那个框或那些框,单独设置为标签为1gt_argmax_ious=ious.argmax(axis=0)#(2,)#其把axis设置为0,表示最大维度上面的求最大索引,但是最外维度一个元素有两个iou,如何比较呢,那就是比较每一个元素相同位置的iou,从里面找#一个最大的iou所在的列索引来返回,也就是说,每一个元素只有两个iou,所以最后只会返回两个数,表示与两个bbox最大的iou的anchor在8940中的#列索引get_max_ious=ious[gt_argmax_ious,np.arange(ious.shape[1])]#(2,)#gt_argmax_ious里面的第一个值是bbox中第一个bbox对应的框(假设是1000),后面对应的是0,当然从ious里面也就刚好对应的(1000,0)gt_argmax_ious_new=np.where(ious==get_max_ious)[0]#找到最大iou对应的行,[0]就是取行操作label[gt_argmax_ious_new]=1#将这部分的标签设置为0#问题来了,在gt argmax ious里面我不是已经得到iou最大值在8940中的行索引了吗,为什么不能直接label[gt_argmax_ious],而是还要去#求这个最大值是多少,然后又重新找这个最大值对应的行,然后再根据索引取设置标签呢?#那就是因为最大的iou的框很可能不止一个,如果直接用gt argmax ious去设置标签,那么最后如果没有iou>0.7的正样本,最后就只有按照这个方法得到#的两个正样本的框,太少了#经过上面的操作,我们已经给所有的8940个边框设置标签(负样本为0,不参与训练样本为-1.正样本为1),但是,框太多了,运算量太大#所以我们采用的做法是,负样本里面抽几个,正样本里面抽几个,一共抽n_sample个n_pos=pos_ratio*n_sample#需要抽取的正样本数,在这里是对半分,各自128pos_index=np.where(label==1)[0]#得到标签为1的框在8940中的索引#如果正样本数大于我们想要抽取的数量,则组要从中随机抽128个,也可以说随机抽多余的个数,然后让它们的标签为-1,不参与运算,而不是0if len(pos_index)>n_pos:disable_index_pos=np.random.choice(pos_index,size=(len(pos_index)-n_pos),replace=False)#size一定要是双括号label[disable_index_pos]=-1#负样本的抽取原理是一样的n_neg=n_sample-np.sum(label==1)#其实这里的设置挺有意思的,我原本想着,如果正样本少于128,那就应该减去len,但是如果正样本数大于128,就应该减去128,如何兼顾呢#因为这个时候如果有多余的正样本,其标签已经被置为-1了,所以最后需要提取的负样本数就是256减去标签为1的数量neg_index=np.where(label==0)[0]if len(neg_index)>n_neg:disable_index_neg=np.random.choice(neg_index,size=(len(neg_index)-n_neg),replace=False)label[disable_index_neg]=-1#大部分情况实际都是负样本太多return label,argmax_ious
#这里返回是8940个框的正负样本的label(0,1,-1),形状为(8940,),这个很好理解,另外一个呢
#argmax也是[8940,],其表示8940个框离哪一个bbox最近,为什么要返回它呢,因为我们边框回归的时候就必须知道到底和哪个框在回归,当然是iou大的那个

然后是下路:下路同样经过一个1x1的卷积来改变通道数为36,最后shape为(b,50,50,36),同样,36也可以写成(9x4),表示的是22500中的每一个anchor的四个回归系数,分别影响着xy和wh四个参数。

我们知道回归的系数其实是上面的dx,dy,dw和dh。Px是anchor的中心x,Pw是anchor的宽,Gx小尖尖是我们经过回归后的新的anchor所在的中心x的坐标。所以这里实际需要做一个转化,因为我们不知道dx是标签是多少,但是我们知道Gx小尖尖的标签是多少,就是它附近的那个目标框的中心x。所以这里设置标签,只需要用Gx小尖尖的标签,带入公式就可以推的dx的标签应该是多少,这样就可以用dx的标签和预测出来的dx进行反向传播了。

bbbox=bbox[argmax_ious]#表示每一个anchor所对应的bbox的坐标,(8940,4)
def get_coefficient(anchor,bbox):#把anchor框从左下右上模式转为中心宽高模式height=anchor[:,2]-anchor[:,0]#8940个框的高(8940,)width=anchor[:,3]-anchor[:,1]#8940个框的宽(8940,)ctr_y=anchor[:,0]+0.5*height#(8940,)ctr_x=anchor[:,1]+0.5*width#(8940,)#把bbox转为中心宽高模式#在此之前需要把bbox的形状改成与anchor相同,达到1对1的一个效果base_height=bbox[:,2]-bbox[:,0]#(8940,)base_width=bbox[:,3]-bbox[:,1]#(8940,)base_ctr_y=bbox[:,0]+0.5*base_height#(8940,)base_ctr_x=bbox[:,1]+0.5*base_width#(8940,)#准备工作做完了,需要套公式了eps=np.finfo(height.dtype).eps#np.finfo(a.dtype)表示获取与a相同的数据类型,.eps表示产生一个很小很小但是不为0的非负数#由于我们的回归系数的计算公式里面height与width位于分母的位置,所以绝对不能为0,如果为0,那就用一个很小的正数来代替height=np.maximum(height,eps)width=np.maximum(width,eps)#我们的计算公式是Gx^=Pw*dx+Px#其中Gx^在我们的标签里面就是所对的最大的bbox的中心x,Pw为anchor的宽,dx是我们需要预测的回归系数,Px是anchor的中心xdy=(base_ctr_y-ctr_y)/height#(8940,)dx=(base_ctr_x-ctr_x)/width#(8940,)#另一个对于放缩比的回归系数就简单多了,Gw^=Pw*exp(dw)#其中Gw^表示最近的bbox的宽,Pw表示anchor的宽,dw是要求的回归系数dh=np.log(base_height/height)#(8940,)dw=np.log(base_width/width)#(8940,)gt_roi_locs=np.vstack((dy,dx,dh,dw)).transpose()#np.vastack表示在竖直方向上堆叠,相当于变成了(4,8940),所以要转置一下成(8940,4)return gt_roi_locs

同理,8940个anchor以外的框的标签全部设置为0就可以了,因为你把0带进公式里面以后,前后的Gx小尖尖和Px完全一样,就代表不需要改变超出边界的框的大小和位置。

这里提出一些我个人的观点,就是为什么判断是否有物体我们的标签是3类,0,-1,1,因为我们会在计算损失的时候把标签为-1的省略。而边框回归的时候标签需要都设置为0,而好像是只有两类,为什么不设置和前者一样,超出边界的anchor就不训练了呢?那是因为超出边界的anchor的标签真的不好设置,因为你取的值很有可能和需要训练的ahchor的标签是一样的,如果单方面忽略某个标签容易对正在训练的产生影响,因为目前而言,暂时没有找到说8940个anchor的边框回归系数很少取到或者说取不到某个值,这个暂时我没看到谁研究过,如果存在,那么我认为完全可以仿照前者,超过边界的anchor的标签设置为那个取不到的数,继而不训练回归系数,这样速度可以大幅提升,因为0本身也是一种训练标签,你相当于在告诉系统,超出边界的框一定要保持不变,你都用不着这些框,为什么还要对它们有要求呢?这一点也是可以改进的地方。

还有一个有争论的点在于,我们相当于在上路的时候找到了128个正样本,128个负样本(理想情况)。我为什么不只对正样本进行边框回归,而不光回归了负样本的边框,甚至还回归了众多标签为-1的框,这不是在浪费资源吗?
我给出的解释是,我们只会计算正样本的损失并回归,并不会管其他的anchor回归,计算归计算,但是不会影响其他未参与损失计算的anchors。

我们在计算RPN阶段的损失的时候,正负样本标签采用的是交叉熵函数,边框回归采用的是smooth L1损失函数。
这里需要说明一点的就是正负样本的标签,我们之前规定了128个正样本的标签为1。128个负样本的标签为0(理想情况),可是咱们给的判断是否存在的位置明明是“2”。这个就和tf中的交叉熵函数有关了,它会自己把一个数的标签转化成one hot形式然后,也就是0转为[1,0],1转为[0,1]。
之后我们还需要把这个预测的标签的第二列拿出来,你会发现,随着训练的进行,标签为1的anchor第二列会不断趋于1,而标签为0的会趋于0,也就是说第二列可以作为是否有物体的一个得分来给我们提供信息以备后面使用。

ROI pooling详解

ROI pooling实际就是对我们选取的一众anchor进行统一大小,那我们选取的anchor是哪些呢?最直接的不就是我们在256个样本里面的128个正样本吗?有可能不够128个正样本呢?那我们就选取这256个样本,负样本的标签我们不计算不就好了。好像说得通,但是实际并不是这么操作的。这里我会先说网络里面是如何操作的,再说说我的想法。
网络里面选择优先级的顺序不是看这个anchors是否被判别是正样本,而是看这个anchor是否存在物体的得分,也就是我们上面所说的第二列,也被称作置信度。它选取的范围是所有的22500个anchors,也就说在网络并未训练成熟之前,它选取的框很有可能是负样本。我们先放下争议继续说下面的步骤。
其会选择22500里面前12000个得分最多的anchor,然后把这些anchor进行nms处理,也就是非极大值抑制,得到最多2000个,但是我自身网络只输出了1758个。也就说网络认为这1758个anchor是“相隔甚远”的,之后会把这1758个anchor与两个目标框计算iou,我设定阈值为0.5,超过0.5的视作正样本,低于0.5的视作负样本。然后故技重施,从正样本里面抽取32个,从负样本里面抽取96个,一共抽128个。如果正样本不够,就多抽一点负样本的,总数为128即可。所以,最后进入ROI pooling的框就是这128个,最后进行图像识别的也是这128个。
现在提出我的观点,那就是为什么非要放弃我们已经寻找到的基本可以确实里面有物体的框(那128个正样本),而要自己根据分数来重新选择呢?甚至可以说在训练初期,那些分高还可能是负样本,这样岂不是白白给自己找事干,直接从我们已经确定是正样本的框里面进行pooling再识别岂不是更快,白送给你的正样本你不要,你要去自己再根据分数来自己选。那么我的观点是否有可行性呢?我认为是有的,因为在RPN阶段我们选择的正样本的框,这是实实在在有物体的,也就说我们标签给它打成1,它基本就确定肯定有物体,那么唯一麻烦的地方就是,我们打上正样本的框的分布,大概率是比较密集的,因为它们会分布在目标框的周围,但是我们依然可以用nms的方法过滤掉分低的框,从而再去识别分最高的那个anchor。
现在给出我对网络如此做的看法——防止过拟合。
我为什么会给出这种解释呢?因为这是很明显地把物体种类的判别与判断是否有物体捆绑在一起了,因为二者的误差是被联系起来了。熟悉YOLOV3的人应该知道,我们最后进行边框筛选的时候设置的分数,它是是否存在物体的置信度与该框内80个物种各自概率的乘积。也就是二者必须都不低,这样的框才能被留下。换句话说,是存在confidence低,而识别物种时概率高的情况的。故其把是否存在物体与这个物体的识别联系起来是有据可循的。另一个原因,我举一个例子,如果一个anchor是负样本,但是一开始它的分很高,被选进了128个框中,然而由于它是负样本,所以只可能在计算iou的时候被放进负样本,物体识别标签被置为0(背景),pooling之后要被用来识别背景0。换句话说,在训练其分数的时候,一个是RPN阶段训练其要尽可能的低,另外它还会受到训练物体识别时候的影响,换句话说,这个负样本本身不太好收敛,这既是缺点,也是优点。一个样本越不容易收敛,其过拟合的可能性就越小(这是我自己的理解,如果有错,欢迎指正)。
如果按照我自己的想法,那么判断是否有物体与这个框中物体识别这两个步骤是分开的话,确实缺少了二者的联系,但是我觉得可以仿照YOLOV3的筛选过程,将二者结合起来一起做一遍框的过滤也可以解决这个问题。

最后对我们选取的128个anchor,映射回特征图上面(直接除以16就可以了),再进行ROI pooling就可以了,结果128anchor会变的一样的大小。
我在这里稍微解释一下为什么原图的区域除以16就是特征图的上面的区域。
在特征图的(1,1)位置上,它投影到原图上面的区域是(0,0),(0,16),(16,16),(16,0)四个点围起来。,故其从原图返回特征图的投影的区域,就是各个坐标值除以16,刚好就是(1,1)这个特征格四个顶角的坐标。

def get_propose_target(new_roi,bbox,labels,n_sample=128,pos_ratio=0.25,pos_iou_thresh=0.5,neg_iou_thresh_hi=0.5,neg_iou_thresh_lo=0.0):ious=compute_iou(new_roi,bbox)#这是我们之前定义的函数,用来计算anchor和bbox之间的iou,所以shape(1758,2)#这里为什么又要重新计算ious,之前不是计算过了吗?不能直接根据下标来提取吗,第一,根据下标提取太麻烦,第二,现在的anchor已经是经过回归后的了,iou已经不同了#找到1758个框离哪个bbox最近near_bbox_index=ious.argmax(axis=1)#shape(1758,)[0,0,0,1,0,....]#看看最近到底近多少,取出具体的各自iounear_bbox_iou=ious.max(axis=1)#shape(1758,)#给这个1785个框分别打上标签,离哪个近,就用哪个框的标签near_label=labels[near_bbox_index]#shape(1758,)[6,6,6,8,8,8....]#当然我们还可以想到的是,这个1758肯定不是我们最终识别的物体数目,肯定还会优化,而选取框的优先级就是他们与目标框的iou,一共会选取128个作为最终的预测数目#下面采用的思路有一点独特,其在128个框里面又分了正样本与负样本,自然这也是由iou决定的,然后正样本的物体识别标签不变,负样本的标签改为0,相当于自创了一个类别——背景#并且物体识别与第二次边框回归都是从这128个框里面进行的#我们规定,对最近的目标框的iou大于0.5,则视为正样本pos_index=np.where(near_bbox_iou>=pos_iou_thresh)[0]#其是1784个框中的下标#然后我们规定我们需要多少个正样本pos_aim_num=n_sample*pos_ratio#总数是128,正样本规定是32#如果不够怎么办,我们的方法是,如果超过32个,那就从所有正样本里面随机抽32个,如果不足,那就有多少用多少pos_num=int(min(pos_aim_num,pos_index.size))#在这里是不足的,只有19个,所以min选的后者#如果选的是前者呢?我们就要随机取了if pos_index.size>32:pos_index=np.random.choice(pos_index,size=pos_aim_num,replace=False)#其是1785个框的下标#不管是哪一种情况,其最后的正样本框的index都是pos index,其数目都是pos num#对于负样本框采取类似的算法neg_index=np.where((near_bbox_iou<neg_iou_thresh_hi)&(near_bbox_iou>=neg_iou_thresh_lo))[0]#如果iou小于0.5,大于等于0的,视为负样本,返回其在1784中的下标#我们同样需要计算一共需要多少个负样本,如果多了,我们需要从中随机取,如果少了,不可能少,绝对会多neg_aim_num=n_sample-pos_numneg_num=int(min(neg_aim_num,neg_index.size))#这里百分百返回的是neg aim num,因为如果返回的是后者,那就代表1784个框里面不足109个负样本,如果正样本依然去32,根本凑不够128#并且我们知道这1784个框本来就是我们筛选出来不怎么重合的框,那么它们之间的大多数就不可能产生太大的iou,即预测不是同一个物体,所以#负样本的数目绝对是大多数if neg_index.size >neg_num:#百分发百生neg_index=np.random.choice(neg_index,size=neg_num,replace=False)#将正样本与负样本在1784个框的下标放在一起keep_index=np.append(pos_index,neg_index)#这里的存放规则简单来说就是[1,2]+[3,4]=[1,2,3,4]#然后再从1784个框的物体识别标签里面,把这些框的标签提取出来,负样本的标签改为0roi_labels=near_label[keep_index]#(128,)roi_labels[pos_num:]=0#最后找到这128个框在原图的坐标就收工了,因为这些坐标还要进行第二次边框回归sample_roi=new_roi[keep_index]#(128,4)return sample_roi,keep_index,near_bbox_index,roi_labels
#返回的自然就是128个框在原图中的坐标,128个框在1784框中的下标,1784个框离哪个框最近,128个框的物体识别标签

分类层与第二次边框回归

摆在首位的就是分类层的标签是什么。
要解决这个问题,就必须弄明白,参与分类的框由什么组成,答:里面有物体的正样本的框和里面没有物体的负样本的框。由此我们如下规定,正样本的框由于包含两种正样本(两个目标框内的物体种类不一样的话),我们看看这些框与哪个目标框的iou更大,就匹配上这个目标框的种类标签。然后负样本的框标签全部置为0,(相当于我们自己创造了一个物体:背景,标签是0),这个标签0也要参与运算,并不是说它是背景就不训练了,如果不训练标签为0的anchor,相当于不只是有物体时候才使其confidence训练为,还需要没有物体,也就是背景的时候训练为confidence为0也要被考虑。(这个背景0和confidence为0是两回事,我只是想强调创建这俩的联系的必要性)。

然后是第二次边框回归的标签,这里只会对正样本里面的anchor进行第二次边框回归,因为没有必要对背景框的大小进行回归的必要,他们的标签自然也是自己iou最大的那个目标框的xyhw。但是这里要说明一下,最后全连接层输出的shape是(128,21,4),但是在正样本有32个前提下,不应该是(32,4)吗。为什么维度还一样呢?因为文章开头我们就说过,第二次边框回归是把物体特征也融入进去了,也就是这个anchor的标签是哪个号码,我就从中间那个21中拿出对应号码的一组4个回归系数,如果你有32个正样本,我就拿出来你对应的32个正样本所对应标签的那4组(32,4),刚刚好。也就说训练出来的128个anchor,其对每一个种类都设置了它们自己的4个回归系数(别忘了还有背景的那个1)。为什么背景的那个1明明不会进行回归,还要加进20里面呢?理由就是背景的标签是0,你在进行元素提取的时候为了保证标签的数字与下标一致,这个0不能省略,举个例子,如果不加标签为0的背景,那如果我需要提取标签为1的4组回归系数的时候,我使用的下标是却是0.当然你不嫌烦的话可以在调用的时候自己加一个1,毕竟最后回归的时候只会反向传播正样本的误差,背景的回归系数存不存在无所谓。

def rio_loss(pre_loc,pre_conf,target_locs,target_conf,weight=10.0):#在进行交叉熵损失计算的时候,输入必须是变量,所以需要对标签进行类型的转化target_conf=torch.autograd.Variable(target_conf.long())#计算类别损失pred_conf_loss=torch.nn.functional.cross_entropy(pre_conf,target_conf)pred_conf_loss=np.squeeze(pred_conf_loss.data.numpy())#先是变成了[xxx],然后变成了xxx#而对于回归系数的损失,则依然使用smooth L1损失#这里并不是128个框全部回归,而是只回归了正样本的框。因为128个框分为正样本和负样本,正样本的框的标签是6和8#而负样本的标签是0,也就是背景,也可以说不存在目标,并且也没有理由去回归背景的边框#其做法也是一样的,屏蔽掉负样本框的回归系数,只取true的系数进行计算pos=target_conf.data>0#其返回是是[true,true,true....false,false](128,)mask=pos.unsqueeze(1).expand_as(pre_loc)#给pos先增加一个维度,然后将其改为与pre loc一样,也就是(128,4)#取出标签与训练的数据中对应的ture的值,然后再改成4个一组重新组合起来mask_pred_loc=pre_loc[mask].view(-1,4)#shape(19,4)mask_target_loc=target_locs[mask].view(-1,4)#shape(19,4)#接下来就可以和第一阶段计算回归的误差的步骤一样了x = np.abs(mask_target_loc.numpy() - mask_pred_loc.data.numpy())#shape(19,4)pre_loc_loss = ((x < 1) * 0.5 * x ** 2) + ((x >= 1) * (x - 0.5))#这是一个数:1.44xxxnum_reg=(target_conf>0).float().sum()#[19],(1,)num_reg=np.squeeze(num_reg.numpy())#19,为数组pre_loc_loss=pre_loc_loss.sum()/num_regpre_loc_loss=np.float32(pre_loc_loss)#最后加起来就可以了total_loss=pred_conf_loss+(weight*pre_loc_loss)return  total_loss

最后我想补充一点的就是,我们一共产生了22500个anchor,但是真正用到的好像并没有这么多。在第一次判别是否存在物体的时候,我们只用了8940个框,其余的框根本没有参与训练,甚至8940个anchor里面也有不参与训练的框(当然这里不影响,因为迟早会训练到他们,8940以外的anchor就没那么幸运了)在第一次边框回归的时候,我们把8940框进行了正常的边框回归,先找他们离哪个目标框最近,然后用这个目标框的xyhw来制作正常的标签,8940以外的anchor的回归便签全部置为0.
在提取最后的128个anchos时候,我们是按照分数的,这个时候22500个anchors是平等对待的,但是我们是根据它的得分来取的,8940以外的anchor根本不会去训练分数,换句话说他们被取到的概率完全是随机的。也就说虽然候选的是一共22500个anchor,但是8940以外的anchor能够作为参考的价值就代码而言是不大的,但是也有可能它的分很高,而且iou也高,最后被训练出类别以及边框回归。故而对于第二次边框回归,其只会回归正样本,而正样本是由其iou决定的,所以它也是有可能被训练到的。综上所述,我觉得对于8940以外的anchor的分数不参与训练是及其不恰当的。
!!代码是知乎一位大神编写的,我也只是做了一个注释的工作,并且加入了我自己的想法,如果哪里理解失误了,欢迎批评指正。

faster rcnn 代码与原理结合详解相关推荐

  1. Faster RCNN代码详解(五):关于检测网络(Fast RCNN)的proposal

    在Faster RCNN代码详解(二):网络结构构建中介绍了Faster RCNN算法的网络结构,其中有一个用于生成ROI proposal target的自定义层,该自定义层的输出作为检测网络(Fa ...

  2. Mask Rcnn代码与原理相结合解析

    1:前言 文章目录 1:前言 2:图片的预处理 3:整体流程概述 4:搭建特征提取网络 4:anchors的形成 5:RPN网络的搭建 6:Proposal Layer 7:创建标签 8:ROIAli ...

  3. 【百度飞浆】RCNN系列目标检测算法详解

    RCNN系列目标检测算法详解 目录 两阶段目标检测算法发展历程 R-CNN R-CNN网络结构 R-CNN网络效果 Fast R-CNN Fast R-CNN网络效果 Faster R-CNN Fas ...

  4. 【论文阅读笔记】faster rcnn 代码阅读细节

    faster rcnn细节 bounding box regression原理 理论 faster rcnn 代码 box_coder.py 参考: https://zhuanlan.zhihu.co ...

  5. 国际C语言混乱代码大赛优胜作品详解之“A clock in one line

    国际C语言混乱代码大赛优胜作品详解之"A clock in one line" 发表于2013-04-11 17:22| 9419次阅读| 来源StackOverflow| 53  ...

  6. [Python图像处理] 三十三.图像各种特效处理及原理万字详解(毛玻璃、浮雕、素描、怀旧、流年、滤镜等)...

    此文转载自:https://blog.csdn.net/Eastmount/article/details/111568397#commentBox 该系列文章是讲解Python OpenCV图像处理 ...

  7. DL之YoloV3:Yolo V3算法的简介(论文介绍)、各种DL框架代码复现、架构详解、案例应用等配图集合之详细攻略

    DL之YoloV3:Yolo V3算法的简介(论文介绍).各种DL框架代码复现.架构详解.案例应用等配图集合之详细攻略 目录 Yolo V3算法的简介(论文介绍) 0.YoloV3实验结果 1.Yol ...

  8. [crypto]-02-非对称加解密RSA原理概念详解

    说明:本文使用的数据来自网络,重复的太多了,也不知道哪篇是原创. 算法原理介绍 step 说明 描述 备注 1 找出质数 P .Q - 2 计算公共模数 N = P * Q - 3 欧拉函数 φ(N) ...

  9. [crypto]-01-对称加解密AES原理概念详解

    1.对称加解密 术语:P是明文,C是密文,K是密钥,E是加密算法,D是解密算 (1).常用的对称加解密有哪些? (2).加解密的模式 [ecb]这种模式是将整个明文分成若干段相同的小段,然后对每一小段 ...

最新文章

  1. Quartz关闭超时订单
  2. 转贴 jQuery Datepicker by Example
  3. orm查询部分字段_ORM问题第2部分–查询
  4. 微信小程序 禁止弹框下面的内容滑动 弹窗禁止底部内容滚动
  5. python seaborn boxplot_python - 如何将文本框添加到Seaborn Boxplot? - 堆栈内存溢出
  6. 作者:周涛,男,电子科技大学教授。
  7. Cmake-cmake_minimum_required()
  8. java分布式(eclipse环境的使用)
  9. python吃香吗_python编程为何这么吃香
  10. windows 8.1无人值守安装
  11. 标准库Allocator的简易实现(二)
  12. python——爬虫豆瓣250
  13. Shawn's 杂记
  14. 亚信科技收购趋势科技中国
  15. 获取当年的法定节假日和周末_通过可配置的周末和节假日添加工作日
  16. 5G通信终端 5G通信设备 5G工业物联网
  17. c1任务如何通过进制转换修改关卡和金币
  18. 成语接龙和唐诗背诵文字游戏开发
  19. 电脑上上传安装apk到手机或者模拟器上
  20. SqlHelper类的使用

热门文章

  1. 通过nodejs搭建HTTP服务器
  2. keil 局部变量不能查看值,显示为not in scope
  3. 电阻介绍及工作原理应用
  4. WINCE 实用技巧 之 创建快捷方式详解
  5. 面向WiFi密码破解的渗透测试研究
  6. linux服务篇-Nagios监控
  7. CAD如何附着外部参照图?
  8. 《机器学习与数据挖掘》实验五 编程实现误差逆传播算法(BP算法)
  9. Partially Labeled Dirichlet Allocation(PLDA)算法的理解与编程细节(Java)
  10. 非管理员用户添加右键菜单(管理员也适用)