这篇文章的重点是解释为什么YOLO的损失函数中的置信度损失要设计成iou的形式而不是简单的0和1,而且既然是IOU那为什么不是最好用的ciou而就是最简单的iou?
置信度损失
只输出一个网格的时候损失函数很简单,一个置信度损失一个定位损失就够了,当输出多个网格的时候情况变的复杂起来。我自己在设计损失函数的时候最自然的想法就是,真实框的中心坐标落在哪个网格内,这个网格的置信度期望就是1,剩余的网格期望就是0,且只计算这个网格的定位损失。简单这样训练的结果是,因为正负样本极不匹配,网络倾向于判断目标不存在,但一旦判断存在就是接近1的置信度。
当我正准备对交叉熵损失加权或者使用focal loss的时候,又发现一个问题,就是如果目标覆盖了多个网格的话,尤其是中心坐标恰好出现在2个网格的边界线上,这样就会出现目标稍微移动一下期望就直接从0变为1的情况,这很不合理。此时iou就巧妙地解决了使期望能从0逐渐过渡到1的问题。
所以使用iou而不是ciou的原因就是,iou的作用并不是边界框回归,而只是起一个过渡的作用而已,这里使用ciou效果可能会更好,但在定位损失里ciou只需要算一次,而在置信度损失里,输出多少个网格iou需要计算多少次,显然计算量会很大。
定位损失
定位损失除了计算ciou比较复杂以外其它的都比较简单,只需要找到中心点落在哪个网格就可以了。关于ciou请参考其他资料,本文不详述。当使用多个特征层和多个先验框的时候,基本原理还是找到最合适的那个网格。比如多个特征层+1个先验框,哪个特征层的先验框的面积(或边长)与真实框最接近就选哪个特征层。
下标索引
如果是自己从零开始写损失函数的话,下标索引绝对是最令人头大的一个地方,没什么技术含量,计算却特别复杂,稍不留神还容易出错。如果不考虑效率,for循环就是最舒服的,但要使用向量运算,下标索引就是绕不过去的坎。
data_conf = data[..., 4].flatten()
data_box = data[..., :4].reshape(-1, 4)
img_height, img_width = data.shape[1], data.shape[2]
label_box = label.repeat(img_height*img_width, 1, 1).permute(1, 0, 2).reshape(-1, 4)
iou = box_iou(data_box, label_box)
这段代码出自置信度损失函数中,作用是计算真实框与每个网格的预测框的iou。由于真实框只有一个,而预测框有img_height*img_width
个,所以要使用repeat方法。直接拿实际数据举例(输出特征图尺寸以15x20为例,批数量以32为例)。
变量 | shape |
---|---|
data | Nx15x20x5 |
data_conf | 300N |
data_box | 300Nx4 |
label | Nx4 |
label.repeat(300,1,1)① | 300xNx4 |
label.repeat(300,1,1).permute(1, 0, 2)② | Nx300x4 |
label_box | 300Nx4 |
这里有个大坑要注意,①和②的区别在哪?反正reshape之后都是300Nx4,那为什么要permute?因为两者的数据不对应。
设一个batch中的第3个label为(313,180,178,272),在15x20特征图中对应5x9,取data中data[3,5,9,:],data_box中对应1009,也就是说
计算方法是
那么label_box[1009,:]对应label中的多少呢?如果不加permute,那么
加了permute就是
这一段虽说就是简单的加减乘除,但一不小心就会出错。