RPN结构图
基于上一步得到的特征图[P2,P3,P4,P5,P6],介绍下MASKRCNN网络中Anchor锚框的生成,根据源码中介绍的规则,与之前的Faster-RCNN中的生成规则有一点差别。
从P2到P6层一共可以在原图上生成261888个Anchor锚框
在上一步已经生成了26188个Anchor锚框,需要借助这些Anchors建立RPN网络训练时的正类和负类,
假设每个图片中需要的正样本与负样本共计256个Anchor,即RPN_TRAIN_ANCHORS_PER_IMAGE这个参数所指定。源码中这步操作由以下几方面构成:
同时,保证正样本为128个,负样本为128个
除了对anchor box进行标记外,另一件事情就是计算anchor box与ground truth之间的偏移量
令:
所以,偏移量:
△x=(x*-x_a)/w_a △y=(y*-y_a)/h_a
△w=log(w*/w_a) △h=log(h*/h_a)
什么是IoU:
这样,经过这一步,共找到128个Anchor作为正样本和128个Anchor作为负样本, 同时,保存了这256个Anchor与真实框ground truth之间的偏移量
RPN网络在分类和回归的时候,分别将每一层的每一个Anchor分为背景和前景两类,以及回归四个位移量,比如P2层,特征图大小为256*256,即像素点有256*256个,每个像素点有三种长宽比例变换,一共有256*256*3个Anchor,如果是分类则需要分别计算每个Anchor为前景的得分(概率)或为背景的得分(概率),其数组可定义为[256*256*3,2],相应的如果是偏移量的回归则数组对应着形式为[256*256*3,4]
将从P2到P6的所有层进行同样的分类和回归操作,一共得到[261888,2]的分类信息和[261888,4]的回归信息。
在‘1.3.1 生成RPN网络数据集’这一步,在261888个Anchors中获得了256个正负样本且与真实框的偏移量。
1、分类:
从前向传播计算得到的所有Anchors得分数组中即上面所述的[261888,2]数组中找到这256个正样本和负样本所对应的得分,利用得分与正负样本的标签计算交叉熵损失值。
RPN分类使用的是基于Softmax函数的交叉熵损失函数,Softmax函数只要是将前向传播计算的得分归一化到0~1之间的概率值,同时,经过Softmax函数后,也可以保证各个分类的概率不会出现负数
Softmax函数公式:
其中,表示类别j经过网络前向传播计算出来的得分, 表示类别j经过Softmax函数后换算得到的概率
基于Softmax的交叉熵公式:
其中,表示的是真实标签,表示概率,下面用代替来表示概率,看下L的求导结果:
因此假设一个5分类任务,一张图像经过Softmax层后得到的概率向量p是[0.1,0.2,0.25,0.4,0.05],真实标签y是[0,0,1,0,0],那么损失回传时该层得到的梯度就是p-y=[0.1,0.2,-0.75,0.4,0.05]。这个梯度就指导网络在下一次forward的时候更新该层的权重参数
2、回归:
从前向传播计算得到的所有Anchors偏移量数组中即上面所述的[261888,4]数组中找到这128个正样本所在索引对应的偏移量,利用此前向传播计算得到的偏移量与正样本与真实框之间计算的偏移量计算损失值,使用的是SmoothL1Loss
SmoothL1函数:
对应的损失函数:
其中,, ,
分别表示由前向传播计算的预测框的坐标值,Anchor锚框对应的坐标值,真实框对应的坐标值
SmoothL1函数的求导:
将预测框与真实框偏移量之间的差值带入上述公式后可得到损失函数求导后的结果,用此更新权重实现反向传播
这一部分对应着总网络图中的ProposalLayer层,取出一定量的Anchors作为ROI,这个量由源码中参数POST_NMS_ROIS_TRAINING确定,假设这个参数在训练的时候设置为2000,则我们这里需要从261888个Anchors中取出2000个作为ROI
首先,按照Anchors经过RPN网络前向传播计算得出的前景(或称为正样本)的得分从高到低排序,取出前2000个得分最高的Anchors,相对应的将2000个Anchors经RPN网络前向传播计算出的偏移量累加到Anchor box上得到较为准确的box坐标。
其中,红色的A框是生成的anchor box,而蓝色的G’框就是经过RPN网络训练后得到的较精确的预测框,绿色的G是ground truth box
最后在返回前,对2000个框再进行一次非最大值抑制NMS操作
用下图一个案例来对NMS算法进行简单介绍
上图所示,一共有6个识别为人的框,每一个框有一个置信率。
现在需要消除多余的:
进行非最大值抑制的目的主要是需要剔除掉重复的框,如果经过非最大值抑制操作后得到的ROI没有事先代码中设定的2000个,则用0填充。
这一部分对应着总网络图中的DetectionTargetLayer层
在经过ProposalLayer层之后得到了2000个经过微调后的ROI(建议框box),而在DetectionTargetLayer需要对2000个ROI做以下几步:
1、首先剔除掉2000个ROI中不符合条件的ROI,主要是在ProposalLayer层最后返回的时候如果不足2000个会用0填充凑足,将这些用0填充的全部排除掉,避免参与不必要的计算
2、DetectionTargetLayer中会用到图片中的真实框信息,所以,在使用之前同样将所有真实框中那些同时框住了多个物体的框,并排除掉
3、计算每个ROI与真实框之间的IOU值
4、对于每个正样本,进一步计算与其相交最大即最接近的真实框ground truth box,将这个真实框所对应的类别即class_id赋予这个正样本,这样RCNN网络就可以区分具体哪个类别
5、同样,计算每个正样本ROI与最接近的真实框ground truth box之间的偏移量,这RPN中的计算公式一样
6、RCNN网络还需要保存与每个正样本ROI最接近的真实框ground truth box的mask掩码信息,并且知道每个mask大小为参数MASK_SHAPE所指定,一般为28*28,同时知道其所属于的类别即class_id,进行保存
7、最后DetectionTargetLayer层返回400个正、负样本,400个位移偏移量(其中300个由0填充),400个掩码mask信息(其中300个由0填充)
通过DetectionTargetLayer层,在原图上找到400个ROI,因为这些ROI可能是有各个特征层产生的Anchor,所以,现在需要将这些ROI映射回特征图上
第一步,我们需要知道每个ROI如何和特征层对应上,论文中提到的方法是利用下面的公式:
对于公式而言:w,h分别表示ROI宽度和高度;k是这个RoI应属于的特征层level; 是w,h=224,224时映射的level,一般取为4,即对应着P4,至于为什么使用224,一般解释为是因为这是ImageNet的标准图片大小,比如现在有一个ROI是112*112,则利用公式可以计算得到k=3,即P3层
第二步,开始讨论对齐的方式
当完成每个ROI能找到其对应的特征层后,就同样可以算出其对应的步长,步长只要用于解释ROI Align的原理,论文中提到的ROI Align,这个方法的思路:
①使用每个ROI的长、宽除以步长,得到ROI映射到特征图上的图片大小,比如ROI为113*113,对应P3层,步长为8,则在特征图上的感兴趣区域则为14.13*14.13
②如果要将特征图上的感兴趣区域对齐到7*7,则需要将14.13*14.13这个区域分成49份,每一份的大小为:2.02*2.02
③再将每个2.02*2.02的小区域,平分四份,每一份取其中心点位置,而中心点位置的像素,采用双线性插值法进行计算,这样,就会得到四个点的像素值,取四个像素值中最大值作为这个小区域(即:2.02*2.02大小的区域)的像素值,如此类推,同样是49个小区域得到49个像素值,组成7*7大小的feature map
以上介绍的是论文中的ROI Align方法,但是在这篇博文开头提供的代码链接中的源码并不是这样处理的。对于ROI映射到特征图上的方法是一样的,但当每个ROI找到对应的特征层厚,直接利用Crop and Resize操作,生成7*7大小的feature map
Mask掩码分支则是对齐成14*14大小的feature map
Mask-RCNN中提出了一个新的idea就是RoIAlign,其实RoIAlign就是在RoI pooling上稍微改动过来的,但是为什么在模型中不能使用RoI pooling呢?现在我们来直观的解释一下。
图13. RoIAlign与RoIpooling对比
可以看出来在RoI pooling中出现了两次的取整,虽然在feature maps上取整看起来只是小数级别的数,但是当把feature map还原到原图上时就会出现很大的偏差,比如第一次的取整是舍去了0.78,还原到原图时是0.78*32=25,第一次取整就存在了25个像素点的偏差,在第二次的取整后的偏差更加的大。对于分类和物体检测来说可能这不是一个很大的误差,但是对于实例分割而言,这是一个非常大的偏差,因为mask出现没对齐的话在视觉上是很明显的。而RoIAlign的提出就是为了解决这个问题,解决不对齐的问题。
RoIAlign的思想其实很简单,就是取消了取整的这种粗暴做法,而是通过双线性插值(听我师姐说好像有一篇论文用到了积分,而且性能得到了一定的提高)来得到固定四个点坐标的像素值,从而使得不连续的操作变得连续起来,返回到原图的时候误差也就更加的小。
1.划分7*7的bin(可以直接精确的映射到feature map上来划分bin,不用第一次ROI的量化)
图14. ROI分割7*7的bin
2.接着是对每一个bin中进行双线性插值,得到四个点(在论文中也说到过插值一个点的效果其实和四个点的效果是一样的,在代码中作者为了方便也就采用了插值一个点)
图15.插值示意图
3.通过插完值之后再进行max pooling得到最终的7*7的ROI,即完成了RoIAlign的过程。是不是觉得大佬提出来的高大上名字的方法还是挺简单的。
ROI Align (线性插值)
假设我们已知坐标 (x0, y0) 与 (x1, y1),要得到 [x0, x1] 区间内某一位置 x 在直线上的值。根据图中所示,我们得到
由于 x 值已知,所以可以从公式得到 y 的值
已知 y 求 x 的过程与以上过程相同,只是 x 与 y 要进行交换。
双线性差值
即做三次线性插值 R1,R2,P
https://zh.wikipedia.org/wiki/%E5%8F%8C%E7%BA%BF%E6%80%A7%E6%8F%92%E5%80%BC
具体流程
如下图所示,虚线部分表示feature map,实线表示ROI,这里将ROI切分成2×2的单元格。
Pooling 池化
要做2×2的最大池化,原Roi是5×7的,那么无法均分:
h方向: 5/2 = 2 (2,3)
w方向:7/2 = 3 (3,4)
output
①RCNN网络的类别分类和回归与RPN网络中的分类和回归是一样的,损失函数也都是基于Softmax交叉熵和SmoothL1Loss,只是RPN网络中只分前景(正类)、背景(负类),而RCNN网络中的分类是要具体到某个类别(多类别分类)
②mask掩码分类
在ROI 对齐操作中mask分支对齐成14*14大小的feature map,并且在‘生成RCNN网络数据集’操作中知道每个正样本mask掩码区域对应的class_id
i) 前向传播:将14*14大小feature map通过反卷积变换为[28*28*num_class],即每个类别对应一个mask区域,称之为预测mask
ii) 与‘生成RCNN网络数据集’操作中的返回的mask也是28*28,并且知道每个mask区域的真实类别class_id,称之为真实mask
iii) 通过当前得到的真实mask中的类别class_id,遍历所有的预测mask,找到class_id类别所对应的预测mask(前向传播中介绍过每个类别都有一个预测mask),比较真实mask与预测mask每个像素点信息,用的是binary_cross_entropy二分类交叉熵损失函数
iv) binary_cross_entropy是二分类的交叉熵,实际是多分类softmax_cross_entropy的一种特殊情况,当多分类中,类别只有两类时,即0或者1,因为28*28大小的mask中只有0和1,即是像素和不是像素
这个是针对概率之间的损失函数,你会发现只有(预测概率)和(真实标签)是相等时,loss才为0,否则loss就是为一个正数。而且,概率相差越大,loss就越大,根据Loss值更改权重实现反向传播
rpn = build_rpn_model(config.RPN_ANCHOR_STRIDE, # 1
len(config.RPN_ANCHOR_RATIOS), config.TOP_DOWN_PYRAMID_SIZE) # 3, 256
def build_rpn_model(anchor_stride, anchors_per_location, depth): #1,3,256
input_feature_map = KL.Input(shape=[None, None, depth],
name="input_rpn_feature_map")
outputs = rpn_graph(input_feature_map, anchors_per_location, anchor_stride) # (?,?,?,256),3,1 |||||| [(?,?,2),(?,?,2),(?,?,4)]
return KM.Model([input_feature_map], outputs, name="rpn_model")
rpn_graph
def rpn_graph(feature_map, anchors_per_location, anchor_stride): # (?,?,?,256),3,1
"""Builds the computation graph of Region Proposal Network.
feature_map: backbone features [batch, height, width, depth]
anchors_per_location: number of anchors per pixel in the feature map
anchor_stride: Controls the density of anchors. Typically 1 (anchors for
every pixel in the feature map), or 2 (every other pixel).
Returns:
rpn_class_logits: [batch, H * W * anchors_per_location, 2] Anchor classifier logits (before softmax)
rpn_probs: [batch, H * W * anchors_per_location, 2] Anchor classifier probabilities.
rpn_bbox: [batch, H * W * anchors_per_location, (dy, dx, log(dh), log(dw))] Deltas to be
applied to anchors.
"""
# TODO: check if stride of 2 causes alignment issues if the feature map
# is not even.
# Shared convolutional base of the RPN
shared = KL.Conv2D(512, (3, 3), padding='same', activation='relu',
strides=anchor_stride,
name='rpn_conv_shared')(feature_map)
# Anchor Score. [batch, height, width, anchors per location * 2].
x = KL.Conv2D(2 * anchors_per_location, (1, 1), padding='valid',
activation='linear', name='rpn_class_raw')(shared)
# Reshape to [batch, anchors, 2]
rpn_class_logits = KL.Lambda(
lambda t: tf.reshape(t, [tf.shape(t)[0], -1, 2]))(x) #[batch, H * W * anchors_per_location, 2]
# Softmax on last dimension of BG/FG.
rpn_probs = KL.Activation(
"softmax", name="rpn_class_xxx")(rpn_class_logits)
# Bounding box refinement. [batch, H, W, anchors per location * depth]
# where depth is [x, y, log(w), log(h)]
x = KL.Conv2D(anchors_per_location * 4, (1, 1), padding="valid",
activation='linear', name='rpn_bbox_pred')(shared)
# Reshape to [batch, anchors, 4]
rpn_bbox = KL.Lambda(lambda t: tf.reshape(t, [tf.shape(t)[0], -1, 4]))(x) #[batch, H * W * anchors_per_location, 4]
return [rpn_class_logits, rpn_probs, rpn_bbox]
rpn_graph 看输入参数:
feature_map:上篇博客得到的rpn_feature_maps = [P2, P3, P4, P5, P6]中的特种图,一个一个的被送进rpn_graph中
anchors_per_location:3
anchor_stride:1
这里以P2为例,所以feature_map为(1,256,256,256)。
图中有误,第二个Conv,o=6..应该o=12,是filtes=12
从上图可以看出rpn_graph共返回rpn_class_logits、rpn_probs、rpn_bbox。P2的形状已经在上图中显示,其他P3、P4、P5、P6以此类推,将得到如下表格:
rpn_class_logits | rpn_probs | rpn_bbox | |
P2 | (1,256*256*3,2) | (1,256*256*3,2) | (1,256*256*3,4) |
P3 | (1,128*128*3,2) | (1,128*128*3,2) | (1,128*128*3,4) |
P4 | (1,64*64*3,2) | (1,64*64*3,2) | (1,64*64*3,4) |
P5 | (1,32*32*3,2) | (1,32*32*3,2) | (1,32*32*3,4) |
P6 | (1,16*16*3,2) | (1,16*16*3,2) | (1,16*16*3,4) |
当返回到build_rpn_model函数中时,不难看出进行了新模型的构建,输入就是特征图,输出就是对应的[rpn_class_logits,rpn_probs,rpn_bbox]。
rpn_class_logits:形状为(1,261888,2);
rpn_class:形状为(1,261888,2),经过了softmax;
rpn_bbox:形状为(1,261888,4)。
这里的261888=256*256*3+128*128*3+64*64*3+32*32*3+16*16*3。
模拟 rpn_feature_maps数据的处理,最终得到rpn_class_logits, rpn_class, rpn_bbox
import numpy as np
‘‘‘
层与层之间主要是中间变量H与W不一致,则此处模拟2层,分别改为8与4
‘‘‘
# 模拟某层,如p3
a1=np.ones((3,8,2)) # rpn_class_logits
b1=np.ones((3,8,2)) # rpn_class
c1=np.ones((3,8,4)) # rpn_bbox
# 模拟某层,如p4
a2=np.ones((3,4,2)) # rpn_class_logits
b2=np.ones((3,4,2)) #rpn_class
c2=np.ones((3,4,4)) #rpn_bbox
layer_outputs = []
‘‘‘
以下模拟此处代码,得到layer_outputs:
for p in rpn_feature_maps:
layer_outputs.append(rpn([p]))
‘‘‘
d1=[a1,b1,c1]
d2=[a2,b2,c2]
layer_outputs.append(d1)
layer_outputs.append(d2)
‘‘‘
outputs = list(zip(*layer_outputs))
‘‘‘
output_names = ["rpn_class_logits", "rpn_class", "rpn_bbox"] # 可跳过
outputs = list(zip(*layer_outputs))
print(‘outputs‘,outputs)
‘‘‘
此处模拟以下代码,最终得到rpn_class_logits, rpn_class, rpn_bbox值
outputs = [KL.Concatenate(axis=1, name=n)(list(o)) for o, n in zip(outputs, output_names)]
‘‘‘
rpn_class_logits = np.concatenate((list(outputs[0])[0],list( outputs[0])[1]),axis=1)
print(‘rpn_class_logits=‘,rpn_class_logits)
rpn_class = np.concatenate((list(outputs[1])[0],list( outputs[1])[1]),axis=1)
print(‘rpn_class=‘,rpn_class)
rpn_bbox=np.concatenate((list(outputs[2])[0],list( outputs[2])[1]),axis=1)
print(‘rpn_bbox=‘,rpn_bbox)
看跟踪输出
test1=list(zip(outputs, output_names))
print('Test',test1)
for o,n in zip(outputs, output_names):
print('o1',o)
print('n1',n)
outputs=[[a1, a2, a3 ...], [b1, b2, b3,...], [c1, c2, c3,...]]
outputs =[t0,t1,t2] 列表有三个元素,每个元素是一个元组,
t0=[logits256,logits64,logits32,logits16]
t1=[rpn_probs256,rpn_probs64,rpn_probs32,rpn_probs16]
t2=[rpn_bbox256,rpn_bbox64,rpn_bbox32,rpn_bbox16]
list(zip(outputs, output_names))
返回三个元素,
[[t0,'rpn_class_logits'],[t1,'rpn_class'],[t02'rpn_bbox']]
o_0 [t0] list(o) = [logits256,logits64,logits32,logits16]
o_1 [t1] list(o) = [rpn_probs256,rpn_probs64,rpn_probs32,rpn_probs16]
o_2 [t2] list(o) = [rpn_bbox256,rpn_bbox64,rpn_bbox32,rpn_bbox16]
KL.Concatenate(axis=1, name=n)(list(o))
相当于
KL.Concatenate(axis=1, name=n)([logits256,logits64,logits32,logits16])