在我的理解中Loss應該是整個模型中相當重要的一部分橘原。
一般而言深度學習模型解決問題的整體流程:
1利赋、問題的定義,也就是說task是什么堡距,或者說背景是什么
2甲锡、根據(jù)task設(shè)計模型
3、根據(jù)模型和task去設(shè)計Loss羽戒。一般來說看到Loss就可以知道這個模型在優(yōu)化什么缤沦,解決什么問題。
CTPN模型的輸出有兩個易稠,一個是檢測框是不是文本(分類)缸废,一個是檢測框的大小和位置(回歸)。所以CTPN的Loss一個是分類Loss驶社,一個是回歸Loss企量。
其實這部分是整個CTPN中最為復雜的地方。包括了gt標簽的制作衬吆。我個人覺得目標檢測的gt挺難處理的梁钾,雖然邏輯不難,但是代碼實現(xiàn)有點復雜逊抡。
下面是loss函數(shù)的代碼姆泻。其中輸入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
我們先來看第一句,調(diào)用了anchor_target_layer函數(shù)方咆。
rpn_data = anchor_target_layer(cls_pred, bbox, im_info, "anchor_target_layer")
anchor_target_layer函數(shù)如下月腋。其中需要注意的點是,它使用了tf.py_func()函數(shù)瓣赂。這個函數(shù)的作用增加tensorflow編程的靈活性榆骚。tensorflow是靜態(tài)圖,可以這么理解煌集,tensorflow數(shù)據(jù)在一個個操作之間流動妓肢,而這些操作是定死的,實現(xiàn)這些操作你得用tensorflow的方法苫纤,不能用普通的方法碉钠。比如說print,你直接用print是沒有輸出的卷拘,得用tf.Print()喊废。然后使用 tf.py_func可以在tensorflow中操作numpy array,增加了靈活性栗弟。不過tf.py_func輸出的是numpy你得把輸出轉(zhuǎn)成tensor才能用污筷。
也就是說下面這個函數(shù)其實是在調(diào)用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個
這里調(diào)用generate_anchors函數(shù)器腋。這個函數(shù)比較簡單我就不介紹了。一共生成了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還需要和特征圖上的每一個位置配合起來。
假設(shè)我們特征圖上只有4個區(qū)域避除,用左上頂點的坐標表示分別是
[[0 0]]
[[1 0]]
[[0 1]]
[[1 1]]
然后我們需要注意怎披,經(jīng)過VGG16之后的特征圖大小是原圖是1/16胸嘁。那么映射到原圖上就是
[[ 0 0]]
[[16 0]]
[[ 0 16]]
[[16 16]]
那么我們現(xiàn)在對于這樣的每一個區(qū)域都配備上述的base anchor。我們只需要把每一個區(qū)域的左上頂點的坐標和anchor相加即可凉逛。就相當于做平移性宏。base anchor相當于是0,0點的anchor状飞。
現(xiàn)在開始上代碼毫胜。
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()))
上述代碼的結(jié)果如下。
[[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()之后結(jié)果如下酬荞。是不是感覺很神奇,python 的np真的好使瞧哟。
[[x1, y1, x1, y1]
[x2, y2, x2, y2]
...]
這樣我們就得到了原圖上每一個16X16區(qū)域的左上頂點的坐標混巧。
all_anchors = (_anchors.reshape((1, A, 4)) +
shifts.reshape((1, K, 4)).transpose((1, 0, 2)))
這一步就是為每一個區(qū)域配備base anchor。使用np的矩陣加法可以輕松實現(xiàn)這一步勤揩。
當然還要把超出圖像范圍的anchor給刪除咧党。經(jīng)過這些操作anchor就已經(jīng)配置完畢了。
后面的代碼
下一步是為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來調(diào)節(jié)赛不。
到這里訓練數(shù)據(jù)準備完畢惩嘉,也就是rpn_data準備完畢。
后面就是一些比較常規(guī)的操作踢故。分類loss使用的是交叉熵文黎,框回歸loss使用的是smoothL1惹苗。