更新项目github源码:https://github.com/LongJun123456/Faster-rcnn-tensorflow
最近博主一直在准备找暑期实习,所以也是隔了很久没跟新博客。题外话,现在的计算机视觉岗竞争是真的激烈,最后才找到美团,网易,海康,Momenta的offer,打算入坑的朋友门需谨慎。最近也在Momenta实习,等实习完后会继续更新博客和继续完善github。
上一篇博文写到anchor的制作与处理了。这篇博文就主要讲一下rpn网络的搭建部分。首先是整个网络的特征提取部分,博主用的是其论文中用的是VGG16
def vgg16(self, input_image):
with tf.variable_scope('vgg_16') :
with slim.arg_scope([slim.conv2d, slim.fully_connected],
activation_fn=tf.nn.relu):
net = slim.repeat(input_image, 2, slim.conv2d, 64, [3, 3], trainable=False, scope='conv1')
net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool1')
net = slim.repeat(net, 2, slim.conv2d, 128, [3, 3], trainable=False, scope='conv2')
net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool2')
net = slim.repeat(net, 3, slim.conv2d, 256, [3, 3], scope='conv3')
net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool3')
net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3], scope='conv4')
net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool4')
net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3], scope='conv5')
return net
特征提取层默认使用的激活函数是relu,整个网络搭建是用tf的slim库完成的,搭建也比较简洁。网络的输入就是输入的图片,网络整体结构是:
2个3*3 64通道卷积+池化+2个3*3 128通道卷积+池化+3个3*3 256通道卷积+池化+3个3*3 512通道卷积+池化+3个3*3 512通道卷积。
注意其中的padding参数,卷积层用的padding参数默认都是same padding,同时池化层也指定padding参数为same。这么做的目的是为了保证图片尺寸经过将采样后是正好缩小16倍(4个池化层带来的缩小),这样就能精确计算之前anchor的位置了。
完成VGG16提取特征后,就是RPN网络的部分了:
#rpn网络 输入feature_map,返回rois的cls和bbox
def rpn_net(self, input_feature_map,num_anchor):
with tf.variable_scope('rpn') :
with slim.arg_scope([slim.conv2d, slim.fully_connected],
activation_fn=tf.nn.relu,\
weights_initializer=tf.random_normal_initializer(mean=0.0, stddev=0.01) ,\
weights_regularizer=slim.l2_regularizer(0.0005)):
rpn_feature = slim.conv2d(input_feature_map, 512, [3,3], scope='conv6')
rois_cls = slim.conv2d(rpn_feature, 2*num_anchor, [1,1], scope='conv7')
rois_reg = slim.conv2d(rpn_feature, 4*num_anchor, [1,1], scope='conv8')
return {'rois_cls':rois_cls, 'rois_bbx':rois_reg}
RPN网络部分的输入就是VGG16提取的特征,首先是3*3的滑动窗口也就是3*3卷积进一步提取特征,也就是conv6层。之后再分别接上一个分类头和一个回归头,分类头是一个1*1的卷积,输出通道数是2*num_anchor,也就是在特征图每一个点上都有2*num_anchor个输出,num_anchor就是特征图上每个点对应的anchor数,在实现过程中就是9。2代表是前景还是背景的类别,为了还要得到背景的类别呢,之前得到前景的输出概率不就完事了吗?楼主之前是这样想的,但是在实际训练过程中发现,如果只输出前景类别概率,并用这个做训练的话,损失是无法收敛的,因为输出的概率是一个0到正无穷的随机数,不符合概率分布的要求。因此要同时输出背景概率,之后再通过softmax,将背景和前景的概率映射到0,1之间,并且概率之和为1。回归头的输出是在特征图上每一个点生成4*num_anchor个输出,num_anchor和之前的定义相同,4代表输出roi的角标信息。
接下来就是rpn网络的损失函数部分了:
rpn_loss_obj = RPN_loss(net.rois_output['rois_bbx'], net.all_anchors, net.gt_boxes, \
net.rois_output['rois_cls'], net.labels, net.anchor_obj, tf.shape(net.gt_boxes)[0])
计算损失的功能由一个类来完成,类初始化传入的参数就是上一步rpn网络生成的回归框,制作好的anchors,真值的角标,rpn网络输出的前背景得分,labels是之前做的anchor的标记(0,1,-1),anchor_obj是每一个anchor对应IOU最大的那个gt的标号(一张图中可能有多个gt)。
class RPN_loss(object):
def __init__(self, prediction_bbox, anchor, ground_truth, probability, label, label_gt_order,size):
self.prediction_bbox = prediction_bbox
self.ground_truth = ground_truth
self.label_1 = label
self.useful_label = tf.reshape(tf.where(tf.not_equal(self.label_1, -1)), [-1])
self.reg_loss_nor = tf.cast(tf.shape(self.label_1)[0]/9, tf.float32)
self.label_gather = tf.gather(self.label_1, self.useful_label)
self.label_gather = tf.cast(self.label_gather, dtype=tf.int32)
self.label_gt_order = tf.gather(label_gt_order, self.useful_label) # 每个anchor对应的groundtruth编号,一维
self.anchor = tf.gather(anchor, self.useful_label) #anchor也是按顺序一维排列 和prediction_bbox_gather 的shape一样
self.size=size
self.probability = probability
self.probability = tf.squeeze(self.probability)
self.probability = tf.reshape(self.probability, [-1,9*2])
self.probability = tf.reshape(self.probability, [-1,2])
self.probability_gather = tf.gather(self.probability, self.useful_label)
self.probability_gather = tf.cast(self.probability_gather, dtype=tf.float32)
self.prediction_bbox = tf.squeeze(self.prediction_bbox)
self.prediction_bbox = tf.reshape(self.prediction_bbox, [-1,9*4])
self.prediction_bbox = tf.reshape(self.prediction_bbox, [-1,4])
self.prediction_bbox_gather = tf.gather(self.prediction_bbox, self.useful_label)
类初始化部分,去除掉label=-1的部分,找到需要参与计算的label的标号,之前说过Label=-1对应的anchor是不参与回归损失和分类损失计算的:
self.label_1 = label
self.useful_label = tf.reshape(tf.where(tf.not_equal(self.label_1, -1)), [-1])
获取useful_labe的对应label标签,预测概率,anchor,预测框,这些都是通过tf.gather函数实现的。这里大体说一下tf.gather函数的使用方法,tf.gather函数的第一个输入是一个高维矩阵,第二个输出是一个Index。功能就是选取第一维坐标是index的矩阵。相对应的代码:
self.label_gather = tf.gather(self.label_1, self.useful_label)
self.label_gather = tf.cast(self.label_gather, dtype=tf.int32)
self.label_gt_order = tf.gather(label_gt_order, self.useful_label) # 每个anchor对应的groundtruth编号,一维
self.anchor = tf.gather(anchor, self.useful_label) #anchor也是按顺序一维排列 和prediction_bbox_gather 的shape一样
self.size=size
self.probability = probability
self.probability = tf.squeeze(self.probability)
self.probability = tf.reshape(self.probability, [-1,9*2])
self.probability = tf.reshape(self.probability, [-1,2])
self.probability_gather = tf.gather(self.probability, self.useful_label)
self.probability_gather = tf.cast(self.probability_gather, dtype=tf.float32)
self.prediction_bbox = tf.squeeze(self.prediction_bbox)
self.prediction_bbox = tf.reshape(self.prediction_bbox, [-1,9*4])
self.prediction_bbox = tf.reshape(self.prediction_bbox, [-1,4])
self.prediction_bbox_gather = tf.gather(self.prediction_bbox, self.useful_label)
分类损失的计算:
def class_loss(self, p_pred, label):
l_loss_sum = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits=p_pred, labels=label))
return l_loss_sum
通过类方法class_loss来实现,p_pred就是tf.gather后的self.probability_gather,label就是tf.gather后的self.label_gather 。计算损失用的方法是tf提供的tf.nn.sparse_softmax_cross_entropy_with_logits 这个函数的作用是先对输入的预测概率进行softmax处理,然后再跟真值计算交叉熵。
回归损失的计算:
回归损失在计算之前需要先对真值的坐标进行变换,也就是变成Faster rcnn论文中说的tx*,ty*,tw*,th*。这么做的目的是为了让网络拟合的target处在0-1之间,让网络能够正确收敛。原论文中的变换公式:
其中x*是真值的中心点x坐标,相对应的y*,w*,h*分别是真值的中心y坐标和宽度高度。xa,ya,wa,ha分别是anchor的中心坐标和宽度高度。然后实际我们rpn网络的输出是tx,ty,tw,th。也就是说,我们要让rpn网络输出的self.prediction_bbox向变换后的tx*,ty*,th*,tw*的方向拟合。代码中的变换公式:
def reconsitution_coords(self):
self.re_prediction_bbox = self.prediction_bbox_gather
anchor_x1 = self.anchor[:, 0]
anchor_y1 = self.anchor[:, 1]
anchor_x2 = self.anchor[:, 2]
anchor_y2 = self.anchor[:, 3]
self.re_anchor_0 = tf.cast((anchor_x2+anchor_x1)/2.0, dtype=tf.float32)
self.re_anchor_1 = tf.cast((anchor_y2+anchor_y1)/2.0, dtype=tf.float32)
self.re_anchor_2 = tf.cast((anchor_x2-anchor_x1), dtype=tf.float32)
self.re_anchor_3 = tf.cast((anchor_y2-anchor_y1), dtype=tf.float32)
self.re_anchor = tf.squeeze(tf.stack(
[self.re_anchor_0, self.re_anchor_1, self.re_anchor_2, self.re_anchor_3], axis=1))
ground_truth_x1 = self.ground_truth[:, 0]
ground_truth_y1 = self.ground_truth[:, 1]
ground_truth_x2 = self.ground_truth[:, 2]
ground_truth_y2 = self.ground_truth[:, 3]
re_ground_truth_0 = tf.expand_dims(tf.cast((ground_truth_x1+ground_truth_x2)/2.0, dtype=tf.float32),-1)
re_ground_truth_1 = tf.expand_dims(tf.cast((ground_truth_y1+ground_truth_y2)/2.0, dtype=tf.float32),-1)
re_ground_truth_2 = tf.expand_dims(tf.cast((ground_truth_x2-ground_truth_x1+1.0), dtype=tf.float32),-1)
re_ground_truth_3 = tf.expand_dims(tf.cast((ground_truth_y2-ground_truth_y1+1.0), dtype=tf.float32),-1)
self.re_ground_truth = tf.concat([re_ground_truth_0, re_ground_truth_1, re_ground_truth_2, re_ground_truth_3], axis=1)
#self.gt_map=tf.one_hot(self.label_gt_order,self.size)
#self.re_label_gt_order=tf.matmul(self.gt_map,self.re_ground_truth)
#self.re_label_gt_order=tf.cast(self.re_label_gt_order,dtype=tf.float32)
self.re_label_gt_order = tf.gather(self.re_ground_truth, self.label_gt_order)
self.re_label_gt_order = tf.cast(self.re_label_gt_order, dtype=tf.float32)
self.label_weight_c = tf.cast((self.label_gather>0), tf.float32)
self.label_weight_c = tf.expand_dims(self.label_weight_c, axis=1)
其中这里还有最后的self.label_weight_c,是用来标记哪些anchor参与最终的回归损失计算的,也就是label=1对应的那些positive anchor。最终损失函数的计算是Smooth L1损失函数,具体的计算:
def smooth_l1_loss(self, bbox_predicted, bbox_ground_truth, weight, lmd=10.0, sigma=3.0, dim2mean=1):
# if the 4 figures of bbox have been calculated
# weight:to delete negative anchors
sigma_1 = sigma ** 2
bbox_ground_truth_0 = tf.cast((bbox_ground_truth[:, 0]-self.re_anchor_0)/self.re_anchor_2, dtype=tf.float32)
bbox_ground_truth_1 = tf.cast((bbox_ground_truth[:, 1]-self.re_anchor_1)/self.re_anchor_3, dtype=tf.float32)
bbox_ground_truth_2 = tf.cast(tf.log(bbox_ground_truth[:, 2]/self.re_anchor_2), dtype=tf.float32)
bbox_ground_truth_3 = tf.cast(tf.log(bbox_ground_truth[:, 3]/self.re_anchor_3), dtype=tf.float32)
re_bbox_ground_truth = tf.stack([bbox_ground_truth_0, bbox_ground_truth_1, bbox_ground_truth_2, bbox_ground_truth_3], axis=1)
re_bbox_predicted = bbox_predicted
bbox_diff = re_bbox_predicted - re_bbox_ground_truth
t_diff = bbox_diff*weight
t_diff_abs = tf.abs(t_diff)
compare_1 = tf.stop_gradient(tf.to_float(tf.less(t_diff_abs, 1.0/sigma_1)))
sl_loss_box = (sigma_1/2.0)*compare_1*tf.pow(t_diff_abs, 2) + (1.0-compare_1)*(t_diff_abs-0.5/sigma_1)
sum_loss_box = tf.reduce_sum(sl_loss_box)
loss_box = sum_loss_box*lmd/cfg.anchor_batch
print('reg')
print(loss_box)
return loss_box
同时再完成计算后还对损失除了个cfg.anchor_batch,也就是原论文中说的256。
def add_loss(self):
self.reconsitution_coords()
self.log_loss = self.class_loss(self.probability_gather, self.label_gather)
self.reg_loss = self.smooth_l1_loss(self.re_prediction_bbox, self.re_label_gt_order, self.label_weight_c)
self.rpn_loss = self.log_loss+self.reg_loss
tf.summary.scalar('rpn_log_loss', self.log_loss)
tf.summary.scalar('rpn_reg_loss', self.reg_loss)
return self.rpn_loss
最终将回归损失和分类损失相加后返回,在主程序中通过调用类方法add_loss就能获得rpn部分的损失了。
这就是整个rpn网络部分的搭建及损失相关的描述。接下来就是检测网络的搭建了,完成检测网络的搭建后就差不多完成整个项目可以开始训练了。最终的完成的代码楼主会尽快的建立一个github项目,也希望广大博友们多多点赞,谢谢支持啦~