在我的理解中Loss应该是整个模型中相当重要的一部分。
一般而言深度学习模型解决问题的整体流程:
1、问题的定义,也就是说task是什么,或者说背景是什么
2、根据task设计模型
3、根据模型和task去设计Loss。一般来说看到Loss就可以知道这个模型在优化什么,解决什么问题。
CTPN模型的输出有两个,一个是检测框是不是文本(分类),一个是检测框的大小和位置(回归)。所以CTPN的Loss一个是分类Loss,一个是回归Loss。
其实这部分是整个CTPN中最为复杂的地方。包括了gt标签的制作。我个人觉得目标检测的gt挺难处理的,虽然逻辑不难,但是代码实现有点复杂。
下面是loss函数的代码。其中输入bbox_pred, cls_pred, bbox, im_info。bbox_pred, cls_pred是我们模型的输出,bbox,im_info是通过dataloader得到的gt。
def loss(bbox_pred, cls_pred, bbox, im_info):
rpn_data = anchor_target_layer(cls_pred, bbox, im_info, "anchor_target_layer")
# classification loss
# transpose: (1, H, W, A x d) -> (1, H, WxA, d)
cls_pred_shape = tf.shape(cls_pred)
cls_pred_reshape = tf.reshape(cls_pred, [cls_pred_shape[0], cls_pred_shape[1], -1, 2])
rpn_cls_score = tf.reshape(cls_pred_reshape, [-1, 2])
rpn_label = tf.reshape(rpn_data[0], [-1])
# ignore_label(-1)
fg_keep = tf.equal(rpn_label, 1)
rpn_keep = tf.where(tf.not_equal(rpn_label, -1))
rpn_cls_score = tf.gather(rpn_cls_score, rpn_keep)
rpn_label = tf.gather(rpn_label, rpn_keep)
rpn_cross_entropy_n = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=rpn_label, logits=rpn_cls_score)
# box loss
rpn_bbox_pred = bbox_pred
rpn_bbox_targets = rpn_data[1]
rpn_bbox_inside_weights = rpn_data[2]
rpn_bbox_outside_weights = rpn_data[3]
rpn_bbox_pred = tf.gather(tf.reshape(rpn_bbox_pred, [-1, 4]), rpn_keep) # shape (N, 4)
rpn_bbox_targets = tf.gather(tf.reshape(rpn_bbox_targets, [-1, 4]), rpn_keep)
rpn_bbox_inside_weights = tf.gather(tf.reshape(rpn_bbox_inside_weights, [-1, 4]), rpn_keep)
rpn_bbox_outside_weights = tf.gather(tf.reshape(rpn_bbox_outside_weights, [-1, 4]), rpn_keep)
rpn_loss_box_n = tf.reduce_sum(rpn_bbox_outside_weights * smooth_l1_dist(
rpn_bbox_inside_weights * (rpn_bbox_pred - rpn_bbox_targets)), reduction_indices=[1])
rpn_loss_box = tf.reduce_sum(rpn_loss_box_n) / (tf.reduce_sum(tf.cast(fg_keep, tf.float32)) + 1)
rpn_cross_entropy = tf.reduce_mean(rpn_cross_entropy_n)
model_loss = rpn_cross_entropy + rpn_loss_box
regularization_losses = tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES)
total_loss = tf.add_n(regularization_losses) + model_loss
tf.summary.scalar('model_loss', model_loss)
tf.summary.scalar('total_loss', total_loss)
tf.summary.scalar('rpn_cross_entropy', rpn_cross_entropy)
tf.summary.scalar('rpn_loss_box', rpn_loss_box)
return total_loss, model_loss, rpn_cross_entropy, rpn_loss_box
我们先来看第一句,调用了anchor_target_layer函数。
rpn_data = anchor_target_layer(cls_pred, bbox, im_info, "anchor_target_layer")
anchor_target_layer函数如下。其中需要注意的点是,它使用了tf.py_func()函数。这个函数的作用增加tensorflow编程的灵活性。tensorflow是静态图,可以这么理解,tensorflow数据在一个个操作之间流动,而这些操作是定死的,实现这些操作你得用tensorflow的方法,不能用普通的方法。比如说print,你直接用print是没有输出的,得用tf.Print()。然后使用 tf.py_func可以在tensorflow中操作numpy array,增加了灵活性。不过tf.py_func输出的是numpy你得把输出转成tensor才能用。
也就是说下面这个函数其实是在调用anchor_target_layer_py方法。
def anchor_target_layer(cls_pred, bbox, im_info, scope_name):
with tf.variable_scope(scope_name) as scope:
# 'rpn_cls_score', 'gt_boxes', 'im_info'
rpn_labels, rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights = \
tf.py_func(anchor_target_layer_py,
[cls_pred, bbox, im_info, [16, ], [16]],
[tf.float32, tf.float32, tf.float32, tf.float32])
rpn_labels = tf.convert_to_tensor(tf.cast(rpn_labels, tf.int32),
name='rpn_labels')
rpn_bbox_targets = tf.convert_to_tensor(rpn_bbox_targets,
name='rpn_bbox_targets')
rpn_bbox_inside_weights = tf.convert_to_tensor(rpn_bbox_inside_weights,
name='rpn_bbox_inside_weights')
rpn_bbox_outside_weights = tf.convert_to_tensor(rpn_bbox_outside_weights,
name='rpn_bbox_outside_weights')
return [rpn_labels, rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights]
然后我们接着来看anchor_target_layer_py方法。代码太长了我就不放上来了。挑选其中比较有趣的一部分讲一下。
首先第一步是生成anchor。我们先明确一下一个anchor的表达形式。一个anchor可以看成由(x_min, y_min, x_max, y_max)组成。
第一步就是生成base anchor。
_anchors = generate_anchors(scales=np.array(anchor_scales)) # 生成基本的anchor,一共10个
这里调用generate_anchors函数。这个函数比较简单我就不介绍了。一共生成了10个base anchor,如下面这列表。
这里anchor的宽度是固定死的16, 高度是[11, 16, 23, 33, 48, 68, 97, 139, 198, 283]。
[[ 0 2 15 13]
[ 0 0 15 15]
[ 0 -4 15 19]
[ 0 -9 15 24]
[ 0 -16 15 31]
[ 0 -26 15 41]
[ 0 -41 15 56]
[ 0 -62 15 77]
[ 0 -91 15 106]
[ 0 -134 15 149]]
我们要注意的是,这里生成的anchor还需要和特征图上的每一个位置配合起来。
假设我们特征图上只有4个区域,用左上顶点的坐标表示分别是
[[0 0]]
[[1 0]]
[[0 1]]
[[1 1]]
然后我们需要注意,经过VGG16之后的特征图大小是原图是1/16。那么映射到原图上就是
[[ 0 0]]
[[16 0]]
[[ 0 16]]
[[16 16]]
那么我们现在对于这样的每一个区域都配备上述的base anchor。我们只需要把每一个区域的左上顶点的坐标和anchor相加即可。就相当于做平移。base anchor相当于是0,0点的anchor。
现在开始上代码。
shift_x = np.arange(0, width) * _feat_stride
shift_y = np.arange(0, height) * _feat_stride
shift_x, shift_y = np.meshgrid(shift_x, shift_y) # in W H order
# K is H x W
shifts = np.vstack(
(shift_x.ravel(), shift_y.ravel(), shift_x.ravel(), shift_y.ravel())
).transpose()
# add A anchors (1, A, 4) to
# cell K shifts (K, 1, 4) to get
# shift anchors (K, A, 4)
# reshape to (K*A, 4) shifted anchors
A = _num_anchors
K = shifts.shape[0]
all_anchors = (_anchors.reshape((1, A, 4)) +
shifts.reshape((1, K, 4)).transpose((1, 0, 2)))
all_anchors = all_anchors.reshape((K * A, 4))
其中width,height,是特征图的宽和高。_feat_stride=16。因为特征图是原图1/16。
shift_x, shift_y 是图上点的x坐标和y坐标。
np.vstack((shift_x.ravel(), shift_y.ravel(), shift_x.ravel(), shift_y.ravel()))
上述代码的结果如下。
[[x1, x2, x3, x4...]
[y1, y2, y3, y4...]
[x1, x2, x3, x4...]
[y1, y2, y3, y4...]]
np.vstack((shift_x.ravel(), shift_y.ravel(), shift_x.ravel(), shift_y.ravel())).transpose()
加上一个transpose()之后结果如下。是不是感觉很神奇,python 的np真的好使。
[[x1, y1, x1, y1]
[x2, y2, x2, y2]
...]
这样我们就得到了原图上每一个16X16区域的左上顶点的坐标。
all_anchors = (_anchors.reshape((1, A, 4)) +
shifts.reshape((1, K, 4)).transpose((1, 0, 2)))
这一步就是为每一个区域配备base anchor。使用np的矩阵加法可以轻松实现这一步。
当然还要把超出图像范围的anchor给删除。经过这些操作anchor就已经配置完毕了。
后面的代码
下一步是为anchor上标签。策略是anchor与gt的overlap大于0.7的为正样本,每个位置上的10个anchor中与gt overlap最大的也为正样本。其余的为负样本。
并不是所有的样本都会被使用来训练,对正负样本采用,在Faster RCNN中正负样本最终的比例是1:3。
这其中需要注意的是在计算框的回归loss的时候只需要正样本的loss,代码中采用bbox_inside_weights来控制,就是正样本为1,负样本为0这样负样本的loss就为0。
分类loss和框回归loss之间的比例也需要控制,这里使用bbox_outside_weights来调节。
到这里训练数据准备完毕,也就是rpn_data准备完毕。
后面就是一些比较常规的操作。分类loss使用的是交叉熵,框回归loss使用的是smoothL1。