原作者的代码实现py-faster-rcnn,用的框架是caffe,由于对caffe不熟悉,所以在github上找了一个tensorflow版本的代码实现,地址是tf-faster-rcnn
在github上阅读代码之前,肯定是要先读一遍readme,根据作者写的说明将代码运行起来,这样也便于后面在代码中添加log来分析代码。
下载代码
git clone https://github.com/endernewton/tf-faster-rcnn.git
保证除了tensorflow外还需要cython
, opencv-python
, easydict这三个包。
sudo pip install Cython
sudo pip install opencv-python
sudo pip install easydict
根据自己的电脑配置来修改setup.py
cd tf-faster-rcnn/lib
vim setup.py
比如根据我的电脑是GTX 1080 (Ti),所以修改了-arch为sm_61,具体的型号可以在README中查看。
extra_compile_args={'gcc': ["-Wno-unused-function"],
'nvcc': ['-arch=sm_61',
'--ptxas-options=-v',
'-c',
'--compiler-options',
"'-fPIC'"]},
include_dirs = [numpy_include, CUDA['include']]
如果我们的训练电脑只有CPU,那么可以把./lib/model/config.py中的__C.USE_GPU_NMS = True改为False。
然后运行make编译出gpu_nms.so和cpu_nms.so,这部分是原作者为nms做GPU加速而设计出来的代码。
下载VOCdevkit
wget http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCtrainval_06-Nov-2007.tar
wget http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCtest_06-Nov-2007.tar
wget http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCdevkit_08-Jun-2007.tar
tar xvf VOCtrainval_06-Nov-2007.tar
tar xvf VOCtest_06-Nov-2007.tar
tar xvf VOCdevkit_08-Jun-2007.tar
创建软链接
cd $FRCN_ROOT/data
ln -s $VOCdevkit VOCdevkit2007
./data/scripts/fetch_faster_rcnn_models.sh
下载使用Resnet101网络对VOC07+12数据集训练出来的weights。如果无法下载可以试试作者提供的google drive下载。
下载的文件解压后放在data目录下,然后创建软链接
NET=res101
TRAIN_IMDB=voc_2007_trainval+voc_2012_trainval
mkdir -p output/${NET}/${TRAIN_IMDB}
cd output/${NET}/${TRAIN_IMDB}
ln -s ../../../data/voc_2007_trainval+voc_2012_trainval ./default
cd ../../..
我们准备的数据是VOC2007,而下载的weights是根据2007和2012进行训练的,不过我们只是需要将流程跑通,不下载VOC2012关系不大。
运行demo
GPU_ID=0
CUDA_VISIBLE_DEVICES=${GPU_ID} ./tools/demo.py
可以看到对data/demo中的图片都进行了预测。
运行test
GPU_ID=0
./experiments/scripts/test_faster_rcnn.sh $GPU_ID pascal_voc_0712 res101
可以得到对每个类别预测的准确率
Saving cached annotations to /local/share/DeepLearning/stesha/tf-faster-rcnn-master/data/VOCdevkit2007/VOC2007/ImageSets/Main/test.txt_annots.pkl
AP for aeroplane = 0.8300
AP for bicycle = 0.8684
AP for bird = 0.8129
AP for boat = 0.7411
AP for bottle = 0.6853
AP for bus = 0.8764
AP for car = 0.8805
AP for cat = 0.8830
AP for chair = 0.6231
AP for cow = 0.8683
AP for diningtable = 0.7080
AP for dog = 0.8852
AP for horse = 0.8727
AP for motorbike = 0.8297
AP for person = 0.8272
AP for pottedplant = 0.5319
AP for sheep = 0.8115
AP for sofa = 0.7767
AP for train = 0.8461
AP for tvmonitor = 0.7938
Mean AP = 0.7976
~~~~~~~~
Results:
0.830
0.868
0.813
0.741
0.685
0.876
0.880
0.883
0.623
0.868
0.708
0.885
0.873
0.830
0.827
0.532
0.811
0.777
0.846
0.794
0.798
~~~~~~~~
基于ImageNet的分类训练的权重来训练Faster RCNN,所以我们需要先下载ImageNet训练权重
mkdir -p data/imagenet_weights
cd data/imagenet_weights
wget -v http://download.tensorflow.org/models/vgg_16_2016_08_28.tar.gz
tar -xzvf vgg_16_2016_08_28.tar.gz
mv vgg_16.ckpt vgg16.ckpt
cd ../..
然后就可以训练了,也可以将数据集替换成自己的数据进行训练。
./experiments/scripts/train_faster_rcnn.sh 0 pascal_voc vgg16
当我们运行train_faster_rcnn.sh进行训练时实际上是运行python ./tools/trainval_net.py并且传入了一些参数。在trainval_net.py一开始会打印出所有参数
if __name__ == '__main__':
args = parse_args()
print('Called with args:')
print(args)
output:
Called with args:
Namespace(cfg_file='experiments/cfgs/vgg16.yml', imdb_name='voc_2007_trainval', imdbval_name='voc_2007_test', max_iters=70000, net='vgg16', set_cfgs=['ANCHOR_SCALES', '[8,16,32]', 'ANCHOR_RATIOS', '[0.5,1,2]', 'TRAIN.STEPSIZE', '[50000]'], tag=None, weight='data/imagenet_weights/vgg16.ckpt')
然后读取cfg_file中的配置信息放入config.py,整理set_cfgs中的信息后也放入config.py。常用的配置信息一开始已经放入config.py中了,这步操作相当于是增加了一些网络特有的配置信息。代码如下
if args.cfg_file is not None:
cfg_from_file(args.cfg_file)
if args.set_cfgs is not None:
cfg_from_list(args.set_cfgs)
接着准备imdb和roidb
imdb, roidb = combined_roidb(args.imdb_name)
print('{:d} roidb entries'.format(len(roidb)))
imdb是imdb.py的类对象,便于后面使用imdb提供的方法。
roidb是通过_load_pascal_annotation解析xml文件,获取其中ground truth的boxes,gt_classes,gt_overlaps,flipped,seg_areas信息。
boxes的shape是(len(objs), 4),表示图片中每个元素有一组box信息:xmin,xmax,ymin,ymax
gt_classes的shape是len(objs),图片中每个元素有一个数字表示的类别信息cls
gt_overlaps的shape是(len(objs), num_classes),图片中每个类别有一个对应cls位置为1.0,其他位置都为0的矩阵。
flipped表示是原图还是翻转图True或者False。
seg_areas的shape是len(objs),每个类别有一个矩阵面积。
然后在prepare_roidb函数的调用过程中还会增加一些信息到roidb中:
image表示对应的image的路径
width表示图片的宽
height表示图片的长
max_overlaps表示gt_overlaps每一行最大的值。因为是ground truth的最大值,所以都是1.0,比如一张图片4个物体那么max_overlaps的值为[1. 1. 1. 1.]
max_classes表示gt_overlaps每一行最大值的位置。其实就是一张图片上每个物体的类别,比如一张图片上4个物体,max_classes的值为[15 15 18 9]
下面设置output_dir
output_dir = get_output_dir(imdb, args.tag)
print('Output will be saved to `{:s}`'.format(output_dir))
#./tensorboard/vgg16/voc_2007_trainval/default
再设置tensorboard路径
#tensorboard/vgg16/voc_2007_trainval/default
tb_dir = get_output_tb_dir(imdb, args.tag)
print('TensorFlow summaries will be saved to `{:s}`'.format(tb_dir))
用同样的方式取出validation set的valroidb,但是不进行图片的翻转
orgflip = cfg.TRAIN.USE_FLIPPED
cfg.TRAIN.USE_FLIPPED = False
_, valroidb = combined_roidb(args.imdbval_name)
print('{:d} validation roidb entries'.format(len(valroidb)))
cfg.TRAIN.USE_FLIPPED = orgflip
准备网络实例,以vgg16网络结构为例
net = vgg16()
最后一步就是训练网络,将前面准备的所有数据都传入了train_net函数。
train_net(net, imdb, roidb, valroidb, output_dir, tb_dir,
pretrained_model=args.weight,
max_iters=args.max_iters)
对roidb和valroidb进行过滤,就是只保留max_overlaps大于等于0.5(前景),或者小于0.5大于等于0.1(背景)的值。我想对于ground truth的roidb而言,应该只能去掉一些图片中没有任何标记的例子。
roidb = filter_roidb(roidb)
valroidb = filter_roidb(valroidb)
创建SolverWrapper对象,将函数的参数都保存在SolverWrapper内部,便于后面调用train_model的时候直接使用。
sw = SolverWrapper(sess, network, imdb, roidb, valroidb, output_dir, tb_dir,
pretrained_model=pretrained_model)
然后进行训练,max_iters是传入的值为70000
sw.train_model(sess, max_iters)
首先对roidb和valroidb初始化RoIDataLayer对象,并且调用_shuffle_roidb_inds打乱db index顺序,比如roidb的长度是10022,所以RoIDataLayer内部存储的self._perm是打乱[0, 1, 2, ......, 10019, 10020, 10021]这个数组的顺序的向量。另外因为cfg.TRAIN.ASPECT_GROUPING为False,所以调用的是np.random.permutation,这个方法和shuffle的区别是会返回一个打乱顺序的数组,但是不会改变原来的数组。
self.data_layer = RoIDataLayer(self.roidb, self.imdb.num_classes)
self.data_layer_val = RoIDataLayer(self.valroidb, self.imdb.num_classes, random=True)
接着调用construct_graph函数
lr, train_op = self.construct_graph(sess)
在这个函数中调用了一个很重要的函数create_architecture,这个函数是搭建网络的核心,现在先跳过,后面重点分析这个函数。另外在construct_graph中取出了'total_loss',设定了learning_rate,指定了optimizer。
再回到train_model函数,接着判断是否有可以restore的snapshot,如果有就恢复最后一个snapshot
# Find previous snapshots if there is any to restore from
lsf, nfiles, sfiles = self.find_previous()
# Initialize the variables or restore them from the last snapshot
if lsf == 0:
rate, last_snapshot_iter, stepsizes, np_paths, ss_paths = self.initialize(sess)
else:
rate, last_snapshot_iter, stepsizes, np_paths, ss_paths = self.restore(sess,
str(sfiles[-1]),
str(nfiles[-1]))
如果调用initialize,其实会将ImageNet中训练的vgg16的checkpoint恢复。恢复参数的代码在vgg16.py中的fix_variables函数中实现。
接着进入训练的循环中,blobs是每次取出来的一个minibatch,因为data_layer中的self._perm是已经打乱过顺序了,所以第一次取minibatch顺序就是乱的。另外因为TRAIN.IMS_PER_BATCH为1,所以我们每次只取一个roidb的数据,对应一张图片。
blobs = self.data_layer.forward()
def forward(self):
"""Get blobs and copy them into this layer's top blob vector."""
blobs = self._get_next_minibatch()
return blobs
具体准备blobs的代码在get_minibatch函数中实现,blobs的内容有三部分:
blobs['data']:是预处理后的图片数据。1.图片减去均值,2.短边压缩到600,如果压缩后长边大于1000,那么长边压缩到1000
blobs['im_info']:三个数字,前两个是压缩后的图片宽高,第三个是图片的压缩比例
blobs['gt_boxes']:shape是(len(objs), 5),5个数字中前4个是boxes信息,第5个是对应obj的cls信息,过滤掉了background这个类别的数据。
然后调用train_step进行训练
# Compute the graph without summary
rpn_loss_cls, rpn_loss_box, loss_cls, loss_box, total_loss = \
self.net.train_step(sess, blobs, train_op)
def train_step(self, sess, blobs, train_op):
feed_dict = {self._image: blobs['data'], self._im_info: blobs['im_info'],
self._gt_boxes: blobs['gt_boxes']}
rpn_loss_cls, rpn_loss_box, loss_cls, loss_box, loss, _ = sess.run([self._losses["rpn_cross_entropy"],
self._losses['rpn_loss_box'],
self._losses['cross_entropy'],
self._losses['loss_box'],
self._losses['total_loss'],
train_op],
feed_dict=feed_dict)
return rpn_loss_cls, rpn_loss_box, loss_cls, loss_box, loss
训练是将rpn的loss和fast rcnn的loss相加作为total loss来进行优化的,跟论文中提到的交替训练方式不太一样。不过这样会让训练更加的方便。
前面跳过了create_architecture,现在再来分析这个函数。
首先创建了三个placeholder,对应我们从minibatch中取出来的blobs的三个信息。因为传入图片的尺寸并不固定,所以self._image的长宽shape设置为None。
self._image = tf.placeholder(tf.float32, shape=[1, None, None, 3])
self._im_info = tf.placeholder(tf.float32, shape=[3])
self._gt_boxes = tf.placeholder(tf.float32, shape=[None, 5])
然后初始化了一些参数,self._num_anchors表示每个中心点anchor的个数,是9个。
self._num_anchors = self._num_scales * self._num_ratios
然后两个关键的函数就是self._build_network和self._add_losses,将搭建网络的中间参数和创建的loss最后都放入layers_to_output中做为create_architecture函数的返回值。
先看_build_network,在这个函数的实现中也有两个主要的部分,
1.调用_image_to_head,搭建了vgg网络,如果传入的图片尺寸是(1, 600, 800, 3),经过计算后成为(1, 38, 50, 512)
def _image_to_head(self, is_training, reuse=None):
with tf.variable_scope(self._scope, self._scope, reuse=reuse):
net = slim.repeat(self._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],
trainable=is_training, scope='conv3')
net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool3')
net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3],
trainable=is_training, scope='conv4')
net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool4')
net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3],
trainable=is_training, scope='conv5')
self._act_summaries.append(net)
self._layers['head'] = net
return net
2.调用self._anchor_component()
def _anchor_component(self):
with tf.variable_scope('ANCHOR_' + self._tag) as scope:
# just to get the shape right
# 用图片原本的长宽除以16,得到的就是经过vgg运算后的feature map的尺寸
height = tf.to_int32(tf.ceil(self._im_info[0] / np.float32(self._feat_stride[0])))
width = tf.to_int32(tf.ceil(self._im_info[1] / np.float32(self._feat_stride[0])))
if cfg.USE_E2E_TF:
# 生成了相对原图的所有anchors
anchors, anchor_length = generate_anchors_pre_tf(
height,
width,
self._feat_stride,
self._anchor_scales,
self._anchor_ratios
)
......
关键函数是generate_anchors_pre_tf,这个函数的目的是生成相对原图坐标而言的所有anchors。以feature map的坐标还原到原图上的位置,然后以这些位置为中心点与相对尺寸的9个anchors相加,为每个中心点生成9个anchors。所以anchors的个数为38*50*9=17100,就是论文中说的,feature map的每个像素点对应9个anchors。
def generate_anchors_pre_tf(height, width, feat_stride=16, anchor_scales=(8, 16, 32), anchor_ratios=(0.5, 1, 2)):
# 将feature map的横向还原到原图,并且间隔16设置一个点
shift_x = tf.range(width) * feat_stride # width
# 将feature map的纵向还原到原图,并且间隔16设置一个点
shift_y = tf.range(height) * feat_stride # height
# 将x与y的坐标对应起来,生成原图上横纵都间隔16的网格点坐标sw和sy。
shift_x, shift_y = tf.meshgrid(shift_x, shift_y)
sx = tf.reshape(shift_x, shape=(-1,))
sy = tf.reshape(shift_y, shape=(-1,))
# sx和sy作为原图框的起始坐标和结束坐标,因为起始坐标和结束坐标相同,所以其实[sx,sy,sx,sy]框是一个面积为0的点
shifts = tf.transpose(tf.stack([sx, sy, sx, sy]))
K = tf.multiply(width, height)
# shifts尺寸为(width*height, 1, 4)
shifts = tf.transpose(tf.reshape(shifts, shape=[1, K, 4]), perm=(1, 0, 2))
# 利用anchor_ratios和anchor_scales生成固定比例的框
# array([[ -83., -39., 100., 56.],
# [-175., -87., 192., 104.],
# [-359., -183., 376., 200.],
# [ -55., -55., 72., 72.],
# [-119., -119., 136., 136.],
# [-247., -247., 264., 264.],
# [ -35., -79., 52., 96.],
# [ -79., -167., 96., 184.],
# [-167., -343., 184., 360.]])
anchors = generate_anchors(ratios=np.array(anchor_ratios), scales=np.array(anchor_scales))
A = anchors.shape[0]
# anchor_constant尺寸为(1, 9, 4)
anchor_constant = tf.constant(anchors.reshape((1, A, 4)), dtype=tf.int32)
length = K * A
# (1, 9, 4) + (width*height, 1, 4),按照广播原则相加得到的尺寸为(width*height, 9, 4),然后reshape成(width*height*9, 4),这就是相对原图上的所有anchors
anchors_tf = tf.reshape(tf.add(anchor_constant, shifts), shape=(length, 4))
return tf.cast(anchors_tf, dtype=tf.float32), length
3.调用self._region_proposal搭建rpn网络
def _region_proposal(self, net_conv, is_training, initializer):
rpn = slim.conv2d(net_conv, cfg.RPN_CHANNELS, [3, 3], trainable=is_training, weights_initializer=initializer,
scope="rpn_conv/3x3")
self._act_summaries.append(rpn)
rpn_cls_score = slim.conv2d(rpn, self._num_anchors * 2, [1, 1], trainable=is_training,
weights_initializer=initializer,
padding='VALID', activation_fn=None, scope='rpn_cls_score')
# change it so that the score has 2 as its channel size
rpn_cls_score_reshape = self._reshape_layer(rpn_cls_score, 2, 'rpn_cls_score_reshape')
rpn_cls_prob_reshape = self._softmax_layer(rpn_cls_score_reshape, "rpn_cls_prob_reshape")
rpn_cls_pred = tf.argmax(tf.reshape(rpn_cls_score_reshape, [-1, 2]), axis=1, name="rpn_cls_pred")
rpn_cls_prob = self._reshape_layer(rpn_cls_prob_reshape, self._num_anchors * 2, "rpn_cls_prob")
rpn_bbox_pred = slim.conv2d(rpn, self._num_anchors * 4, [1, 1], trainable=is_training,
weights_initializer=initializer,
padding='VALID', activation_fn=None, scope='rpn_bbox_pred')
if is_training:
rois, roi_scores = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
rpn_labels = self._anchor_target_layer(rpn_cls_score, "anchor")
# Try to have a deterministic order for the computing graph, for reproducibility
with tf.control_dependencies([rpn_labels]):
rois, _ = self._proposal_target_layer(rois, roi_scores, "rpn_rois")
......
self._predictions["rpn_cls_score"] = rpn_cls_score
self._predictions["rpn_cls_score_reshape"] = rpn_cls_score_reshape
self._predictions["rpn_cls_prob"] = rpn_cls_prob
self._predictions["rpn_cls_pred"] = rpn_cls_pred
self._predictions["rpn_bbox_pred"] = rpn_bbox_pred
self._predictions["rois"] = rois
return rois
以feature map尺寸为(38, 50, 512)为例,下图是这些变量之间的生成关系与尺寸。
里面调用的三个比较重要的函数需要解说一下
# 由于计算出来的rpn_bbox_pred中的dx,dy,dw,dh都是相对anchor的偏移
# 所以这个函数就是相对anchors计算出来pred_boxes的坐标,将超出范围的pred_box进行裁剪
# 然后先取出分数排行前12000的scores和boxes,再用nms取出2000个boxes和对应的scroes
# 剩下proposals的shape是(2000, 4),返回的rois是对proposals第一列增加了一个数字全为0的列
# 所以rois的shape是(2000, 5),roi_scores的shape是(2000,)
rois, roi_scores = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
# 这个函数实际上是调用了anchor_target_layer函数
# anchor_target_layer函数的实现比较复杂,是为了生成四个返回值
# rpn_labels, rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights
# 1.所有anchors中超出图片范围的剔除,生成在图片内部的anchor列表,假设是5944个
# 2.创建labels向量,长度为5944,里面的值默认全部设置为-1
# 3.计算每一个anchor与每一个gt_box之间的IOU,假设有4个gt_box,那么生成的overlaps尺寸为(5944, 4)
# 4.每个anchor与4个gt_box之间的IOU取出最大值,如果最大值小于0.3,那么这个anchor对应的label标记为0(负样本)
# 5.每个anchor与4个gt_box之间的IOU取出最大值,如果最大值小于0.7,那么这个anchor对应的label标记为1(正样本)
# 6.为了防止最大值没有大于0.7的情况,取出overlaps中列最大值,如果有相同的最大值也一起取出,对应的label标记为1
# 7. 筛选正负样本,使它们的个数都不超过128个,没有被筛选上的anchor对应的label标记为-1(不关心的值)
# 8. gt_boxes[argmax_overlaps, :]表示取gt_boxes的哪一组值来进行计算,是根据anchor跟哪一个box的IOU最大决定的
# 9. bbox_targets的数据是anchor相对gt_box的dx,dy,dw,dh,是偏移压缩比例。(5944, 4)
# 10. bbox_inside_weights是(5944, 4)的全0矩阵,将label为1的位置的数据改为[1.0,1.0,1.0,1.0]
# 11. bbox_outside_weights是(5944, 4)的全0矩阵,将label为1和为0的位置的数据改为[0.00390625 0.00390625 0.00390625 0.00390625](用1除以正负样本总和算出来的)
# 12. 把长度5944的label还原长度为17100的label,用-1来补新增位置的值
# 13. 把尺寸为(5944, 4)的bbox_targets,还原到尺寸为(17100, 4),用0来补新增位置的值
# 14. bbox_inside_weights和bbox_outside_weights同样操作,尺寸都成为(17100, 4)
# 15. rpn_labels是对labels进行reshape,尺寸为(1, 1, 9*38, 50)
# 16. rpn_bbox_targets是对bbox_targets进行reshape,尺寸为(1, 38, 50, 36)
# 17. rpn_bbox_inside_weights和rpn_bbox_outside_weights同上一样,尺寸为(1, 38, 50, 36)
rpn_labels = self._anchor_target_layer(rpn_cls_score, "anchor")
# 这个函数实际上是调用了proposal_target_layer,然后调用_sample_rois
# 因为anchors已经准备了256个正负样本参与计算,而proposal还是2000个,所以要进一步过滤proposal的个数
# 1.overlaps是计算出来的大约2000个proposal与gt_boxes之间的IOU,(2000, 4)
# 2.gt_assignment表示这2000个proposal跟哪个obj的overlap最大,(2000,)
# 3.labels = gt_boxes[gt_assignment, 4],gt_boxes的尺寸是(len(objs), 5),所以labels是每一个proposal对应的object的cls,(2000,)
# 4.fg_inds是最大IOU大于等于0.5的index,bg_inds是最大IOU大于等于0.1小于0.5的index
# 5.调整fg_inds和bg_inds,使他们的个数相加为rois_per_image(256)
# 6.保留labels中的fg_inds和bg_inds,(256,)
# 7.从fg_rois_per_image到结束是背景位置,设置label的值为0,而此时前景的label仍然是cls id
# 8.计算出proposal与gt_box的相对位移dx,dy,dw,dh,放入bbox_target_data并将label列放入第一列的位置,尺寸是(256, 5)
# 9.创建尺寸为(256, 84)的bbox_targets和bbox_inside_weights,84是因为有21个类,每个类留4个位置
# 经过计算后返回的rois尺寸为(256, 5)
rois, _ = self._proposal_target_layer(rois, roi_scores, "rpn_rois")
4.调用_crop_pool_layer实现RoI Pooling层,因为proposal的尺寸各不相同,如果要送入region calssification会计算出问题,所以这里将他们统一尺寸。原理上是将每个proposal对应到feature map上的位置,然后划分成同样的尺寸比如7*7,这样在49个区域里面进行max pooling,就可以将所有proposal转成7x7的尺寸。最后的返回值是(256, 7, 7, 512)
5.调用_head_to_tail继续构建网络,最后的fc7尺寸为(256, 4096)
def _head_to_tail(self, pool5, is_training, reuse=None):
with tf.variable_scope(self._scope, self._scope, reuse=reuse):
pool5_flat = slim.flatten(pool5, scope='flatten')
fc6 = slim.fully_connected(pool5_flat, 4096, scope='fc6')
if is_training:
fc6 = slim.dropout(fc6, keep_prob=0.5, is_training=True,
scope='dropout6')
fc7 = slim.fully_connected(fc6, 4096, scope='fc7')
if is_training:
fc7 = slim.dropout(fc7, keep_prob=0.5, is_training=True,
scope='dropout7')
return fc7
6.调用_region_classification搭建proposal的分类网络,cls_score的尺寸为(256, 21),cls_prob的尺寸为(256, 21),cls_pred的尺寸为(256,),bbox_pred的尺寸为(256, 84)
def _region_classification(self, fc7, is_training, initializer, initializer_bbox):
cls_score = slim.fully_connected(fc7, self._num_classes,
weights_initializer=initializer,
trainable=is_training,
activation_fn=None, scope='cls_score')
cls_prob = self._softmax_layer(cls_score, "cls_prob")
cls_pred = tf.argmax(cls_score, axis=1, name="cls_pred")
bbox_pred = slim.fully_connected(fc7, self._num_classes * 4,
weights_initializer=initializer_bbox,
trainable=is_training,
activation_fn=None, scope='bbox_pred')
self._predictions["cls_score"] = cls_score
self._predictions["cls_pred"] = cls_pred
self._predictions["cls_prob"] = cls_prob
self._predictions["bbox_pred"] = bbox_pred
return cls_prob, bbox_pred
至此,_build_network的所有工作就完成了,返回值是rois, cls_prob, bbox_pred,但是还保存了很多中间变量放在了self._predictions中。
再看_add_losses,
首先是RPN class loss,RPN的loss都是相对anchors来计算的。
# 是RPN网络中由feature map通过卷积计算生成的(1, 38, 50, 18)reshape得到的,尺寸是(9*38*50, 2)
rpn_cls_score = tf.reshape(self._predictions['rpn_cls_score_reshape'], [-1, 2])
# (9*38*50,)
rpn_label = tf.reshape(self._anchor_targets['rpn_labels'], [-1])
# 找出rpn_label中不为-1的部分,-1表示not care的数据
rpn_select = tf.where(tf.not_equal(rpn_label, -1))
# 根据rpn_select找出rpn_cls_score的对应位置
rpn_cls_score = tf.reshape(tf.gather(rpn_cls_score, rpn_select), [-1, 2])
# 根据rpn_select找出rpn_label的对应位置
rpn_label = tf.reshape(tf.gather(rpn_label, rpn_select), [-1])
# 计算cross entropy loss
rpn_cross_entropy = tf.reduce_mean(
tf.nn.sparse_softmax_cross_entropy_with_logits(logits=rpn_cls_score, labels=rpn_label))
其次是RPN bbox loss
# (1, 38, 50, 36)
rpn_bbox_pred = self._predictions['rpn_bbox_pred']
# (1, 38, 50, 36)
rpn_bbox_targets = self._anchor_targets['rpn_bbox_targets']
# (1, 38, 50, 36)
rpn_bbox_inside_weights = self._anchor_targets['rpn_bbox_inside_weights']
# (1, 38, 50, 36)
rpn_bbox_outside_weights = self._anchor_targets['rpn_bbox_outside_weights']
# l1 loss
rpn_loss_box = self._smooth_l1_loss(rpn_bbox_pred, rpn_bbox_targets, rpn_bbox_inside_weights,
rpn_bbox_outside_weights, sigma=sigma_rpn, dim=[1, 2, 3])
接着是RCNN class loss,RCNN的loss都是相对proposal
# (256, 21)
cls_score = self._predictions["cls_score"]
# (256,)
label = tf.reshape(self._proposal_targets["labels"], [-1])
# cross entropy loss
cross_entropy = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits=cls_score, labels=label))
最后是RCNN bbox loss
# (256, 84)
bbox_pred = self._predictions['bbox_pred']
# (256, 84)
bbox_targets = self._proposal_targets['bbox_targets']
# (256, 84)
bbox_inside_weights = self._proposal_targets['bbox_inside_weights']
# (256, 84)
bbox_outside_weights = self._proposal_targets['bbox_outside_weights']
# l1 loss
loss_box = self._smooth_l1_loss(bbox_pred, bbox_targets, bbox_inside_weights, bbox_outside_weights)
total_loss就是将以上4个loss相加,我们要优化的就是total_loss的值。
总结一下RPN和RCNN的loss:
从demo.py入手看如何预测一张图片。
当运行demo.py脚本的时候,会运行main下面的代码。
主要是准备session,调用create_architecture构建网络,restore网络,调用demo预测
函数create_architecture的实现主体和training时一样,有以下几个区别。
1.在构建网络的时候_region_proposal中只需要调用_proposal_layer,因为我们不需要构建loss,所以training中后面的步骤不需要。
if is_training:
rois, roi_scores = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
rpn_labels = self._anchor_target_layer(rpn_cls_score, "anchor")
# Try to have a deterministic order for the computing graph, for reproducibility
with tf.control_dependencies([rpn_labels]):
rois, _ = self._proposal_target_layer(rois, roi_scores, "rpn_rois")
else:
if cfg.TEST.MODE == 'nms':
rois, _ = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
elif cfg.TEST.MODE == 'top':
rois, _ = self._proposal_top_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
else:
raise NotImplementedError
另外在_proposal_layer函数中,因为cfg_key为test,所以pre_nms_topN为6000,pos_nms_topN为300,因此我们的proposal有300个。
def proposal_layer(rpn_cls_prob, rpn_bbox_pred, im_info, cfg_key, _feat_stride, anchors, num_anchors):
......
pre_nms_topN = cfg[cfg_key].RPN_PRE_NMS_TOP_N
post_nms_topN = cfg[cfg_key].RPN_POST_NMS_TOP_N
nms_thresh = cfg[cfg_key].RPN_NMS_THRESH
2.在create_architecture中需要对预测数据做处理
if testing:
stds = np.tile(np.array(cfg.TRAIN.BBOX_NORMALIZE_STDS), (self._num_classes))
means = np.tile(np.array(cfg.TRAIN.BBOX_NORMALIZE_MEANS), (self._num_classes))
self._predictions["bbox_pred"] *= stds
self._predictions["bbox_pred"] += means
做这些处理是因为我们对target做过相反的处理
if cfg.TRAIN.BBOX_NORMALIZE_TARGETS_PRECOMPUTED:
# Optionally normalize targets by a precomputed mean and stdev
targets = ((targets - np.array(cfg.TRAIN.BBOX_NORMALIZE_MEANS))
/ np.array(cfg.TRAIN.BBOX_NORMALIZE_STDS))
self._predictions["bbox_pred"]的尺寸是(300, 84),就是我们前面选出来的300个proposal。
im_detect就是进行预测的主要代码,生成的scores shape是(300, 21),boxes的shape是(300, 84)
scores, boxes = im_detect(sess, net, im)
然后对除了__background__以外的每个类别单独分析
for cls_ind, cls in enumerate(CLASSES[1:]):
cls_ind += 1 # because we skipped background
#将对应class的box挑选出来,(300, 4)
cls_boxes = boxes[:, 4*cls_ind:4*(cls_ind + 1)]
#将对应class的分数挑选出来,(300, 1)
cls_scores = scores[:, cls_ind]
#合并成(300, 5)的数据score放在最后
dets = np.hstack((cls_boxes,
cls_scores[:, np.newaxis])).astype(np.float32)
#keep表示通过nms挑选出来的index,比如挑选出来30个
keep = nms(dets, NMS_THRESH)
#取出挑选出来的dets,(30, 5)
dets = dets[keep, :]
#将dets中score大于0.8的框保留下来画在图片上,这样就拿到了bbox和score和class id
im = vis_detections(im, cls, dets, thresh=CONF_THRESH)
调用net.test_image进行预测
def test_image(self, sess, image, im_info):
feed_dict = {self._image: image,
self._im_info: im_info}
cls_score, cls_prob, bbox_pred, rois = sess.run([self._predictions["cls_score"],
self._predictions['cls_prob'],
self._predictions['bbox_pred'],
self._predictions['rois']],
feed_dict=feed_dict)
return cls_score, cls_prob, bbox_pred, rois
然后再根据相对坐标计算出真实坐标
if cfg.TEST.BBOX_REG:
# Apply bounding-box regression deltas
box_deltas = bbox_pred
pred_boxes = bbox_transform_inv(boxes, box_deltas)
pred_boxes = _clip_boxes(pred_boxes, im.shape)
预测完成。
以上为本文所有内容,感谢阅读,欢迎留言。