在分享了yolo3的庖丁解牛版后,陆续有一些小伙伴发来信息说,损失函数讲的太简单了 - -,各位大哥,我错了,现在把yolo3的损失函数重新一点一点刨开,同时预先对接下来要分享的faster rcnn中的损失函数部分一起,提前跟大家一点一点解释清楚,以提早发现问题,方便我在重组代码时能把注释写得明明白白的。
先上frcnn吧。。。代码参考:https://github.com/ldhsight/keras_frcnn同事有人问我,没人star,你还去学- -
相比大众版的frcnn或者yolo3等代码,他们确实很完善,然而不便于新手上路。正是因为他们的完善和代码的优化,很多地方会导致你看不懂,比如yolo3对nms的写法,掺杂了非常不常用的tensorflow函数,数据处理大篇幅用numpy大法,空间想象力不足的话会抓狂。同时那些代码是面向工程的,很多附加内容与标准写法让你望而却步。因此,这也是为什么先把frcnn代码先进行分享,因为这篇代码,是我选了又选挑出来最平易近人的(虽然他是有问题的)
OK,开始吧
1、Faster Rcnn
要想知道损失函数的意义,必定需要详细地了解三个内容:真实数据,神经网络输出,优化目标
(1)第一阶段:
真实数据为:
1)y_rpn_cls 和 y_rpn_regr
其中y_rpn_cls.shape=(1, row, col, 18),这里以tf版为标准。18代表着:前9个值为anchor是否可用于当前区域,后9个值为该区域是前景还是背景
y_rpn_regr.shape=(1, row, col, 72),72表示:前36个值为是前景背景的X4版本,其实就用4个1表示前景,4个0表示背景,就是y_rpn_cls后9个值扩大四个长度
网络输出为:
1)x_class
x_class.shape=(n, row, col, 9)
x_regr.shape=(n, row, col, 36)
损失函数为:
def rpn_loss_cls(num_anchors):
def rpn_loss_cls_fixed_num(y_true, y_pred):
if K.image_dim_ordering() == 'tf':
# 炒鸡神奇炒鸡巧妙的损失函数
return lambda_rpn_class * K.sum(y_true[:, :, :, :num_anchors] * K.binary_crossentropy(y_pred[:, :, :, :], y_true[:, :, :, num_anchors:])) / K.sum(epsilon + y_true[:, :, :, :num_anchors])
else:
return lambda_rpn_class * K.sum(y_true[:, :num_anchors, :, :] * K.binary_crossentropy(y_pred[:, :, :, :], y_true[:, num_anchors:, :, :])) / K.sum(epsilon + y_true[:, :num_anchors, :, :])
return rpn_loss_cls_fixed_num
def rpn_loss_regr(num_anchors):
def rpn_loss_regr_fixed_num(y_true, y_pred):
if K.image_dim_ordering() == 'th':
x = y_true[:, 4 * num_anchors:, :, :] - y_pred
x_abs = K.abs(x)
x_bool = K.less_equal(x_abs, 1.0)
return lambda_rpn_regr * K.sum(
y_true[:, :4 * num_anchors, :, :] * (x_bool * (0.5 * x * x) + (1 - x_bool) * (x_abs - 0.5))) / K.sum(epsilon + y_true[:, :4 * num_anchors, :, :])
else:
x = y_true[:, :, :, 4 * num_anchors:] - y_pred
x_abs = K.abs(x)
x_bool = K.cast(K.less_equal(x_abs, 1.0), tf.float32)
return lambda_rpn_regr * K.sum(
y_true[:, :, :, :4 * num_anchors] * (x_bool * (0.5 * x * x) + (1 - x_bool) * (x_abs - 0.5))) / K.sum(epsilon + y_true[:, :, :, :4 * num_anchors])
return rpn_loss_regr_fixed_num
第一个函数:rpn_loss_cls
简化为:
其中
y_true[:9] 表示anchor选用哪个anchor
bce=binary_crossentropy表示二分类的交叉熵损失函数
y_pred是预测值
y_true[9:]表示是前景还是背景
e表示一个极小值,防止分母太小出现异常值,是常数
详细理论见:https://blog.csdn.net/wfei101/article/details/79252021
咱们看看为什么这个式子能做损失函数:
我们把sum先去掉:
(e较小,可忽略)
所以这个本质上就是一个简单交叉熵损失函数
但是呢,把这个式子提出来看一下,似乎还是非常有意思的。
如果要想这个式子越小,除开K越小,a/(e+a)也得尽可能的小,这就要求a也得越小,a越小就意味着选用的anchor越少!anchor越少!anchor越少!也就能选出相对准确的anchor索引值
所以呢,这个式子一方面能让前景选择趋于准确,同时限制了anchor的数量
第二个函数:rpn_loss_regr
其中,x为anchor回归真实值与预测值的欧氏距离。
if表达了x的绝对值小于等于1的部分
y_true前36个值表示前景背景,后36个值表示回归值
详细理论见:https://blog.csdn.net/wfei101/article/details/79809332,美曰其名为:smooth_l1_loss
理论公式为:
简化为:
(忽略e的影响)
其中bc表示前36个前景背景值判断
想要这个式子的值越小,x的值得越小越好;
而if和1-if这个因子的加入,给smooth l1的第二部分锦上添花,控制着括号里面结果的平衡
其本质就是欧氏距离的损失函数,但是经过优化、迭代、进化,变为如上的smooth l1
以上是第一阶段的损失函数,第一阶段的损失函数一下子囊括了三个方面的优化:
启用的anchor索引值、前景背景的判断值和anchor的回归值
第二阶段的损失函数是异曲同工的,只是分类器的损失函数改为softmax版的交叉熵损失函数,原理是一样的,这里就不再细说了,大家可以看看原创的、公式
def class_loss_regr(num_classes):
def class_loss_regr_fixed_num(y_true, y_pred):
x = y_true[:, :, 4 * num_classes:] - y_pred
x_abs = K.abs(x)
x_bool = K.cast(K.less_equal(x_abs, 1.0), 'float32')
return lambda_cls_regr * K.sum(y_true[:, :, :4 * num_classes] * (x_bool * (0.5 * x * x) + (1 - x_bool) * (x_abs - 0.5))) / K.sum(epsilon + y_true[:, :, :4 * num_classes])
return class_loss_regr_fixed_num
def class_loss_cls(y_true, y_pred):
return lambda_cls_class * K.mean(categorical_crossentropy(y_true[0, :, :], y_pred[0, :, :]))
OK,咱们回看一下yolo3的损失函数
详见:https://github.com/qqwweee/keras-yolo3
真实数据为:
1)y_true
y_true由三个列表组成,分别对应三层不同大小(13, 26, 52)金字塔结构,每一层shape=(batch, row,col,3,5+classnum)
以classnum=80举例:
batch表示批次大小,因为网络输入统一为416x416可以多个图一起,不想早起的frcnn只能一个一个来
row和col为(13, 26, 52)中的一个值
3表示三组anchor,每组anchor又有三个具体anchor值
5表示4个回归值+1个置信度(可以理解为frcnn中的前景概率),classnum表示类别
网络输出为:
1)grid, raw_pred, pred_xy, pred_wh = (Nx13x13x3x85),(Nx13x13x3x2), (Nx13x13x3x2])
其中grid表示(13,26,52)中的一个,以13为例
raw_pred表示整体网络输出值
pred_xy表示物体中心点网路输出值
pred_wh表示anchor长宽
损失函数为:
xy_loss = object_mask * box_loss_scale * K.binary_crossentropy(raw_true_xy, raw_pred[..., 0:2], from_logits=True)
wh_loss = object_mask * box_loss_scale * 0.5 * K.square(raw_true_wh - raw_pred[..., 2:4])
confidence_loss = object_mask * K.binary_crossentropy(object_mask, raw_pred[..., 4:5], from_logits=True) + \
(1 - object_mask) * K.binary_crossentropy(object_mask, raw_pred[..., 4:5], from_logits=True) * ignore_mask
class_loss = object_mask * K.binary_crossentropy(true_class_probs, raw_pred[..., 5:], from_logits=True)
函数一共分为四部分
(1)计算xy(物体中心坐标)的损失
object_mask就是置信度
box_loss_scale可以理解为2-w*h
raw_true_xy就是真实的xy坐标点了
raw_pred[..., :2]是xy预测坐标点
所以第一个式子想对还挺直观,简化为:
bool是置信度
其中bce是xy值的二值交叉熵损失,这个值越小整个损失值越小
bool*(2-areaPred)的值越小,则需要在确保置信度(bool)的情况下,areaPred需要越大
因此,这部分损失主要优化xy的预测值(bce)和置信度(bool)以及wh回归值(areaPred)
(2)计算wh(anchor长宽回归值)的损失
跟(1)式 的差距就在最后一项,所以我们直接简化之:
在确保置信度(bool)的情况下,areaPred需要越大,wh需要尽可能靠近真实值wh
这部分主要优化置信度(bool)wh回归值(areaPred、whTrue)
(3)计算置信度损失(前背景)损失
简化为:
其中bool为置信度,bce为预测值和实际置信度的二值交叉熵,ignore表示iou低于一定阈值的但确实存在的物体,相当于frcnn中的中性点位,既不是前景也不是背景,是忽略的,暂时不计的。
在确保置信度(bool)的情况下,预测值需要尽可能靠近真实值,同时没有物体的部分需要尽可能靠近背景真实值,同时乘以相应的需要忽略点位
这部分主要优化置信度,同时缩减了检测的目标量级
(4)计算类别损失
这个不用多说了,直接就是置信度乘上个多分类的交叉熵
这部分优化置信度损失和类别损失
(5)最后,总损失为所有损失之和相加
xy_loss = K.sum(xy_loss) / mf
wh_loss = K.sum(wh_loss) / mf
confidence_loss = K.sum(confidence_loss) / mf
class_loss = K.sum(class_loss) / mf
loss += xy_loss + wh_loss + confidence_loss + class_loss
OK,最后咱们做下小结:
(1)frcnn的损失函数分阶段,每个阶段各两个损失函数,所有的损失函数优化了一共5个方向,合计其实只有4个方向。
第一阶段主要优化前背景、一次回归值和锚框索引值;第二阶段主要优化前背景、二次回归值和分类值。
(2)yolo3的损失函数分5步,优化了xy、wh、置信度和类别
(3)frcnn分两阶段,速度略低于yolo3;但是进行两次回归,精度略高于yolo3。其损失函数相比yolo3就简单太多了,目的性强;yolo3的损失函数设计得非常妙,才促成了这么6的检测方法的形成
(4)损失函数无外乎两大形式:联合优化(yolo系)和精准优化(rcnn系);优化方向无外乎主要几种:欧氏距离、交叉熵,或者都可以用交叉熵!
对于yolo存疑的看官,可移步我写过的yolo庖丁解牛。如有错误,欢迎指正。
有一起奔跑的小伙伴欢迎主页联系!