相关文章
项目地址:YOLOv1 VOC 2007
笔者训练的权重地址:阿里云盘分享
10 秒文章速览
对于 YOLOv1 的损失函数,使用 Python 程序实现损失函数的计算
关于损失函数的计算,在《YOLOv1 论文简要》一文中已经进行了较详细的解释。只不过,在本文中,需要以代码的形式表达出来
平方和误差
在 YOLOv1 使用的是平方和误差,so,在这里需要由我们自定义一个损失函数。平方和误差的计算也不难,和 MSE 大差不差
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> S S E = ∑ i = 1 n ( y − y ^ ) 2 SSE = \sum_{i=1}^{n} (y-\widehat{y} )^{2} </math>SSE=i=1∑n(y−y )2
python
# 平方和误差(sum-squared error)
def SSE(y_true, y_pred):
return tf.reduce_sum(tf.square(y_pred - y_true), axis=-1)
IOU 计算
IOU
函数用于计算预测框与标签的之间的 IOU ,对于 IOU 的计算其实没什么好说的。只是这里需要注意,先依据 (x, y, w, h) 复原出与边界框的坐标
代码中 w_image
与 h_image
表示图片的尺寸,x_grid
与 y_grid
表示网格的尺寸
python
def IOU(y_true, y_pred):
bbox1_x, bbox1_y, bbox1_w, bbox1_h = y_true
bbox2_x, bbox2_y, bbox2_w, bbox2_h = y_pred
# 复原出与边界框的坐标
x1_min = bbox1_x*w_grid - bbox1_w/2*w_image
y1_min = bbox1_y*h_grid - bbox1_h/2*h_image
x1_max = bbox1_x*w_grid + bbox1_w/2*w_image
y1_max = bbox1_y*h_grid + bbox1_h/2*h_image
x2_min = bbox2_x*w_grid - bbox2_w/2*w_image
y2_min = bbox2_y*h_grid - bbox2_h/2*h_image
x2_max = bbox2_x*w_grid + bbox2_w/2*w_image
y2_max = bbox2_y*h_grid + bbox2_h/2*h_image
# 计算矩形面积
s1 = (x1_max - x1_min) * (y1_max - y1_min)
s2 = (x2_max - x2_min) * (y2_max - y2_min)
# 计算相交矩形
x_min = tf.maximum(x1_min, x2_min)
y_min = tf.maximum(y1_min, y2_min)
x_max = tf.minimum(x1_max, x2_max)
y_max = tf.minimum(y1_max, y2_max)
w = tf.maximum(0., x_max - x_min)
h = tf.maximum(0., y_max - y_min)
a1 = w * h
a2 = s1 + s2 - a1
iou = a1 / a2
return iou
输出与标签
似乎在前两篇文章中都没有对模型的输出与标签进行过具体的说明,这里是时候有个解释了🤔
对于(对于?难道和标签的shape不一样吗,这还真是的)模型的输出最后的shape都是 (b, y, x, n)
,下面解释下这个形状的含义
- b:batch size、批大小
- y、x:记得图片是要分割成S×S的吧,y与x便表示S×S的网格分布
- n:包含了网格预测的所有内容,包括类别、(x, y, w, h, confidence)
由于 YOLOv1 将图像分为7×7的网格,所以y=x=7
对于n再进一步的解释下。由于 PASCAL VOC 2007 数据集有20个类别,每个网格输出2个预测框,而预测框中包含 (x, y, w, h, confidence),所以n等于30(30是怎么算出来的再之前的文章中有过解释)。至此,可以视为每个网格都会输出一个长度为30的向量
在这个长度为30的向量中,我们指定
- 0:20:表示20个类别
- 20:25:表示第一个预测框的输出 (x, y, w, h, confidence)
- 25:30:表示第二个预测框的输出 (x, y, w, h, confidence)
所以,可知模型输出的shape是 (b, 7, 7, 30)
。好了,再回过头去,来解释解释标签的shape,标签的shape是 (b, 7, 7, 25)
,其实区别并不大,虽然每个网格生成两个预测框,但在测试阶段,网格只会选取置信度分数最大的预测框作为最终输出,所以在标签里只需要考虑一个预测框就行了
损失计算
好了,从这里开始,就要变得烧脑了🤯,跟上笔者的节奏🎶。如果在阅读过程中感到头晕😵💫,请放轻松,头晕是正常的
笔者先贴出完整的代码,然后进行进一步的解释
python
def get_loss1(y_true, y_pred):
# 定义各个值的损失
xy_loss = 0
wh_loss = 0
cond_loss = 0
classes_loss = 0
# 遍历批大小
for b in range(tf.shape(y_true)[0]):
# y 和 x 遍历网格
for y in range(7):
for x in range(7):
# one_obj 用于判断网格中是否存在目标
one_obj = y_true[b, y, x, 24]
# 存在目标时
if one_obj == 1:
# 计算两个预测框与真实框的 IOU
bbox1_iou = IOU(y_true[b, y, x, 20:24], y_pred[b, y, x, 20:24])
bbox2_iou = IOU(y_true[b, y, x, 20:24], y_pred[b, y, x, 25:29])
# 只关注 IOU 大的预测框
if bbox1_iou >= bbox2_iou:
# 计算bbox1的 (x, y) 的损失
xy_loss += 5 * SSE(y_true[b, y, x, 20:22], y_pred[b, y, x, 20:22])
# 计算bbox1的 (w, h) 的损失
wh_loss += 5 * SSE(tf.sqrt(y_true[b, y, x, 22:24]), tf.sqrt(y_pred[b, y, x, 22:24]))
# 计算bbox1的 confidence 的损失
cond_loss += SSE(bbox1_iou, y_pred[b, y, x, 24:25])
cond_loss += 0.5 * tf.square(y_pred[b, y, x, 29:30])
else:
# 计算bbox2的 (x, y) 的损失
xy_loss += 5 * SSE(y_true[b, y, x, 20:22], y_pred[b, y, x, 25:27])
# 计算bbox2的 (w, h) 的损失
wh_loss += 5 * SSE(tf.sqrt(y_true[b, y, x, 22:24]), tf.sqrt(y_pred[b, y, x, 27:29]))
# 计算bbox2的 confidence 的损失
cond_loss += SSE(bbox2_iou, y_pred[b, y, x, 29:30])
cond_loss += 0.5 * tf.square(y_pred[b, y, x, 24:25])
# 计算 classes 的损失
for i in range(20):
classes_loss += SSE(y_true[b, y, x, i:i+1], y_pred[b, y, x, i:i+1])
# 不存在目标时
else:
cond_loss += 0.5 * (tf.square(y_pred[b, y, x, 24]) + tf.square(y_pred[b, y, x, 29]))
return xy_loss + wh_loss + cond_loss + classes_loss
接下来建议结合《YOLOv1 论文简要》的损失函数解读部分进行阅读
变量 one_obj
提取的标签中的置信度,当存在目标时 one_obj=1
,否则 one_obj=0
。实现了 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 i j o b j 1^{obj}_{ij} </math>1ijobj 的效果
当网格不存在目标的时候, <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 i j o b j = 0 , 1 i j n o o b j = 1 1^{obj}{ij}=0,\ 1^{noobj}{ij}=1 </math>1ijobj=0, 1ijnoobj=1,所以只需要对不含目标的预测框进行置信度的损失计算。在 cond_loss += 0.5 * (tf.square(y_pred[b, y, x, 24]) + tf.square(y_pred[b, y, x, 29]))
中
0.5
:表示 <math xmlns="http://www.w3.org/1998/Math/MathML"> λ n o o b j λ_{noobj} </math>λnoobj 参数tf.square(y_pred[b, y, x, 24])
:计算第一个预测框的置信度损失tf.square(y_pred[b, y, x, 29])
:计算第二个预测框的置信度损失
当网格存在目标的时候,我们则需要对 (x, y, w, h, confidence) 以及类别都进行损失计算。一个网格会生成2个预测框,而在计算损失时,并非是两个预测框都直接参与计算,要有取舍。前文就提过,在多个预测框中,我们选取与真实框之间 IOU 最高的预测框来负责预测该目标
,所以我们要先计算两个预测框与真实框的 IOU
计算(x, y)与(w, h)的损失比较简单,都是先提取从 y_pred
和 y_true
中提取出(x, y)或(w, h)再带入SSE即可,5就是指 <math xmlns="http://www.w3.org/1998/Math/MathML"> λ o b j λ_{obj} </math>λobj 参数
而对于置信度的损失计算,分了两个步骤。cond_loss += SSE(bbox1_iou, y_pred[b, y, x, 24:25])
以与真实框之间的 IOU 作为标签,计算了包含目标的预测框的置信度损失。cond_loss += 0.5 * tf.square(y_pred[b, y, x, 29:30])
将与真实框之间的 IOU 较小的预测框视为不包含目标来计算损失
最后我们将所有的损失相加,来计算最终损失
再回首,损失计算
可别太天真了噢,兄嘚😏,若真要像上面所说来计算损失,这速度得多慢啊。上面只是个开头,为了好理解而已,下面将给出更加高效的计算方式
所谓的"高效",其实就是从对单个数值的计算转变为对矩阵的计算
IOU 的计算
python
def IOU(y_true, y_pred, B):
# 获取两个预测框的输出
bbox1_x, bbox1_y, bbox1_w, bbox1_h = y_true[:, :, :, 20], y_true[:, :, :, 21], y_true[:, :, :, 22], y_true[:, :, :, 23]
bbox2_x, bbox2_y, bbox2_w, bbox2_h = y_pred[:, :, :, 20+B], y_pred[:, :, :, 21+B], y_pred[:, :, :, 22+B], y_pred[:, :, :, 23+B]
# 根据模型输出,还原出边界框在原图上的坐标
x1_min = bbox1_x * w_grid - bbox1_w / 2 * w_image
y1_min = bbox1_y * h_grid - bbox1_h / 2 * h_image
x1_max = bbox1_x * w_grid + bbox1_w / 2 * w_image
y1_max = bbox1_y * h_grid + bbox1_h / 2 * h_image
x2_min = bbox2_x * w_grid - bbox2_w / 2 * w_image
y2_min = bbox2_y * h_grid - bbox2_h / 2 * h_image
x2_max = bbox2_x * w_grid + bbox2_w / 2 * w_image
y2_max = bbox2_y * h_grid + bbox2_h / 2 * h_image
# 计算矩形面积
s1 = (x1_max - x1_min) * (y1_max - y1_min)
s2 = (x2_max - x2_min) * (y2_max - y2_min)
# 计算相交矩形
x_min = tf.maximum(x1_min, x2_min)
y_min = tf.maximum(y1_min, y2_min)
x_max = tf.minimum(x1_max, x2_max)
y_max = tf.minimum(y1_max, y2_max)
w = tf.maximum(0., x_max - x_min)
h = tf.maximum(0., y_max - y_min)
a1 = w * h
a2 = s1 + s2 - a1
iou = a1 / a2
return iou
损失的计算
python
def get_loss(y_true, y_pred):
one_obj = y_true[:, :, :, 24]
# 计算两个预测框与真实框的 IOU
bbox1_iou = IOU(y_true, y_pred, 0)
bbox2_iou = IOU(y_true, y_pred, 5)
# 在两个预测框中选出哪个预测框与真实框之间的 IOU 更大
index = tf.argmax([bbox1_iou, bbox2_iou], axis=0)
bbox2_iou_idx = tf.cast(index, 'float32')
bbox1_iou_idx = 1 - bbox2_iou_idx
bbox1_iou = tf.expand_dims(bbox1_iou, axis=-1)
bbox2_iou = tf.expand_dims(bbox2_iou, axis=-1)
# 计算 (x, y) 的损失
xy_loss = 5 * one_obj * (SSE(y_true[:, :, :, 20:22], y_pred[:, :, :, 20:22]) * bbox1_iou_idx +
SSE(y_true[:, :, :, 20:22], y_pred[:, :, :, 25:27]) * bbox2_iou_idx)
# 计算 (w, h) 的损失
wh_loss = 5 * one_obj * (SSE(tf.sqrt(y_true[:, :, :, 22:24]), tf.sqrt(y_pred[:, :, :, 22:24])) * bbox1_iou_idx +
SSE(tf.sqrt(y_true[:, :, :, 22:24]), tf.sqrt(y_pred[:, :, :, 27:29])) * bbox2_iou_idx)
# 计算 conditional 的损失
# 计算包含目标的预测框的损失
cond_loss = one_obj * (SSE(bbox1_iou, y_pred[:, :, :, 24:25]) * bbox1_iou_idx +
SSE(bbox2_iou, y_pred[:, :, :, 29:30]) * bbox2_iou_idx)
# 计算包不含目标的预测框的损失
cond_loss += 0.5 * one_obj * (tf.square(y_pred[:, :, :, 24]) * bbox2_iou_idx +
tf.square(y_pred[:, :, :, 29]) * bbox1_iou_idx)
cond_loss += 0.5 * (1 - one_obj) * (tf.square(y_pred[:, :, :, 24]) +
tf.square(y_pred[:, :, :, 29]))
# 计算 classes 的损失
classes_loss = one_obj * SSE(y_true[:, :, :, 0:20], y_pred[:, :, :, 0:20])
return tf.reduce_sum(xy_loss +
wh_loss +
cond_loss +
classes_loss, axis=[1, 2])