一,可视化理解Binary Cross-Entropy(推荐反复阅读)
介绍
如果你正在训练一个二分类器,很有可能你正在使用的损失函数是二值交叉熵/对数(binary cross-entropy / log)。
你是否想过使用此损失函数到底意味着什么?问题是,鉴于如今库和框架的易用性,很容易让人忽略所使用损失函数的真正含义。
动机
我一直在寻找一个可以向学生展示的以清晰简洁可视化的方式解释二值交叉熵/对数损失背后概念的博客文章。但由于我实在找不到,只好自己承担了编写的任务:-)
一个简单的分类问题
让我们从10个随机点开始:
x = [-2.2, -1.4, -0.8, 0.2, 0.4, 0.8, 1.2, 2.2, 2.9, 4.6]
这是唯一的特征:*x*
图0:特征
现在,让我们为点分配一些颜色:红色和绿色。这些是我们的标签。
图1:数据
因此,我们的分类问题非常简单:给定特征*x*,我们需要预测其标签:红或绿。
由于这是二分类,我们还可以提出以下问题:“该点是绿色吗? ”,或者更好的问法,“ 该点是绿色的概率是多少?” 理想情况下,绿点的概率为1.0(为绿色),而红点的概率为0.0(为绿色)。
在此设置中,绿点属于正类(是,它们是绿色),而红点属于负类(否,它们不是绿色)。
如果我们拟合模型来执行此分类,它将预测每个点是绿色的概率。假定我们了解点的颜色,我们如何评估预测概率的好坏?这就是损失函数的全部目的!对于错误的预测,它应该返回高值,对于良好的预测,它应该返回低值。
对于像我们的示例这样的二分类,典型的损失函数是binary cross-entropy / log。
损失函数:二值交叉熵/对数(Binary Cross-Entropy / Log )损失
如果您查看此损失函数,就会发现:
二值交叉熵/对数
其中y是标签(绿色点为1 , 红色点为0),p(y)是N个点为绿色的预测概率。
这个公式告诉你,对于每个绿点(y = 1),它都会将log(p(y))添加到损失中,即,它为绿色的对数概率。相反,它为每个红点(y = 0)添加log(1-p(y)),即它为红色的对数概率。看起来不难,但好像不大直观……
此外,熵与这一切有什么关系?我们为什么首先要对数概率?这些是有意义的问题,我希望在下面的“ 向我展示数学 ”部分中回答。
但是,在介绍更多公式之前,让我向你展示上述公式的直观表示 …
计算损失-可视化方式
首先,让我们根据它们的类(正或**负)**分开所有点,如下图所示:
图2:拆分数据!
现在,让我们训练一个Logistic回归来对我们的点进行分类。拟合回归是一个*sigmoid曲线,*代表对于任何给定的*x*,一个点是绿色的概率。看起来像这样:
图3:拟合Logistic回归
那么,对于属于正类(绿色)的所有点,我们的分类器给出的预测概率是多少?看sigmoid曲线下对应点x坐标上的绿色条。
图4:正确分类正类中的点的概率
OK,到目前为止还不错!那负类的点又如何呢?请记住,sigmoid曲线下方的绿色条表示给定点为绿色的概率。那么,给定点为红色的概率是多少呢?当然是sigmoid曲线以上的红色条啦 😃
图5:正确分类负类中的点的概率
放在一起,我们最终得到这样的结果:
图6:所有概率加在一起!
条形图表示与每个点的对应真实类别相关的预测概率!
好的,我们有了预测的概率…是时候通过计算二值交叉熵/对数损失来评估它们了!
这些概率就是我们要的,因此,让我们去掉*x*轴,将各个方条彼此相邻:
图7:所有点的概率
这样,吊起来的方条不再有意义,所以让我们重新定位一下:
图8:所有点的概率—看起来好多了 😃
由于我们正在尝试计算损失,因此我们需要对错误的预测进行惩罚,对吧?如果实际的类的概率是1.0,我们需要它的损失是零。相反,如果概率低,比如0.01,我们需要它的损失是巨大的!
事实证明,对于这个目的**,采用概率的(负)对数**非常适合(由于0.0和1.0之间的值的对数为负,因此我们采用负对数以获得损失的正值)。
实际上,我们为此使用对数的原因是由于交叉熵的定义,请查看下面的“ 告诉我数学 ”部分,以获取更多详细信息。
下面的图给了我们一个清晰的展示 - 实际的类的预测概率越来越接近于零,则损失指数增长:
图9:不同概率的对数丢失
很公平!让我们取概率的(负)log -这些是每个点相应的损失。
最后,我们计算所有这些损失的平均值。
图10:最后,损失!
瞧!我们已经成功地计算了这个玩具示例的二值交叉熵/对数损失。是0.3329!
代码
如果你想仔细检查我们得到的值,只需运行下面的代码 😃
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import log_loss
import numpy as np
x = np.array([-2.2, -1.4, -.8, .2, .4, .8, 1.2, 2.2, 2.9, 4.6])
y = np.array([0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0])
logr = LogisticRegression(solver='lbfgs')
logr.fit(x.reshape(-1, 1), y)
y_pred = logr.predict_proba(x.reshape(-1, 1))[:, 1].ravel()
loss = log_loss(y, y_pred)
print('x = {}'.format(x))
print('y = {}'.format(y))
print('p(y) = {}'.format(np.round(y_pred, 2)))
print('Log Loss / Cross Entropy = {:.4f}'.format(loss))
告诉我数学(真的吗?!)
这篇文章并不倾向于从数学上解释……但是对于一些的读者,希望了解熵,对数在所有方面的作用,好, 我们开始吧:-)
分布
让我们从点的分布开始。y代表我们点的类别(我们有3个红点和7个绿点),这是它的分布,我们称其为q(y),如下所示:
图11:q(y),我们的点的分布
熵(Entropy)
熵是一个与给定的分布q(y)相关的不确定性的量度。
如果我们所有的点都是绿色的,这种分布的不确定性是什么?零,对吗?毕竟,毫无疑问,点的颜色:它总是绿色!因此,熵为零!
另一方面,如果我们确切知道该点的一半是绿色和另一半是红色?那是最坏的情况,对吧?我们绝对不可能猜到一个点的颜色:它是完全随机的!在这种情况下,熵由下面的公式给出(我们有两个类(颜色)–红色或绿色-因此为2):
一半一半分布的熵
对于介于两者之间的所有其它情况,我们可以用以下公式计算分布的熵,例如q(y),其中C是类的数量:
熵
因此,如果我们知道随机变量的真实分布,则可以计算其熵。但是,如果是这样的话,为什么还要训练分类器呢?毕竟,我们知道真正的分布…
但是,如果我们不知道真实分布呢?我们可以尝试用其他一些分布(例如p(y))来近似真实分布吗?我们当然可以!😃
交叉熵(Cross-Entropy)
假设我们的点遵循这个其它分布p**(y)** 。但是,我们知道它们实际上来自真(未知)分布q(y) ,对吧?
如果我们这样计算熵,我们实际上是在计算两个分布之间的交叉熵:
交叉熵
如果我们奇迹般地将**p(y)***与***q(y)完美匹配,则交叉熵**和熵的计算值也将匹配。
由于这可能永远不会发生,因此交叉熵将比在真实分布上计算出的熵具有更大的值。
交叉熵减去熵
事实上,交叉熵和熵之间的差还有个名字……
KL散度(Kullback-Leibler Divergence)
Kullback-Leibler Divergence,简称“ *KL散度* ”,是两个分布之间差异的一种度量:
KL散度
这意味着,p(y)越接近q(y) ,差异越少,因此交叉熵也越小。
因此,我们需要找到一个合适的p(y)……但是,这不就是我们的分类器应该做的吗?确实如此!它寻找可能的最佳p(y),以最小化交叉熵的值。
损失函数
在训练过程中,分类器使用其训练集中的N个点中的每一个来计算交叉熵损失,从而有效地拟合分布p(y)!由于每个点的概率为1 / N,因此交叉熵的计算公式为:
交叉熵—点对点
还记得上面的图6至图10吗?我们需要在与每个点的实际类相关的概率上计算交叉熵。这意味着对正类(y = 1)中的点使用绿色条,对负类(y = 0)中的点使用红色的*悬挂*条,或者从数学角度看:
对应于图10的数学表达式 😃
最后一步是计算两个正负类所有点的平均
二进制交叉熵-在正负类上计算
最后,我们通过一点小处理,正类或负类中任何一点都可以用相同的公式:
二进制交叉熵-通用公式
瞧!我们回到了二进制交叉熵/对数损失的原始公式 😃
二,进入YOLOv5的LOSS(简单的流程化解释)
yolov5与yolov3
-
-
tx、ty、tw、th为logits;pw、ph为anchor的w、h;cx、cy为网格位置
v5版本
v3版本
-
v5版本中-0.5是为了当输入t=0时,边界框偏移也为0,不调整锚框位置
-
v5版本中2是对于WH回归公式,预测框WH的最大值只能为4倍的锚框WH
-
v5版本中anchor_t=4意思是在训练时,如果真实框与锚框尺寸的比值大于4,会把比值大于4的锚框删除掉
-
-
objectness loss
-
class loss
-
total loss
YOLOv5
- objectness score的损失BCEWithLogitsLoss
- class probability score的损失BCEWithLogitsLoss
- bounding box的损失1-CIOU
yolov5 loss解析
01 目标检测结果精确度的度量
目标检测任务有三个主要目的:
(1)检测出图像中目标的位置,同一张图像中可能存在多个检测目标;
(2)检测出目标的大小,通常为恰好包围目标的矩形框;
(3)对检测到的目标进行识别分类。
所以,判断检测结果精确不精确,主要基于以上三个目的来衡量:
(1)**首先我们来定义理想情况:**图像中实际存在目标的所有位置,都被检测出来**。检测结果越接近这个理想状态,也即**漏检/误检的目标越少,则认为结果越精确;
**(2)**同样定义理想情况:检测到的矩形框恰好能包围检测目标。检测结果越接近这个理想状态,那么认为结果越精确;
**(3)**对检测到的目标,进行识别与分类,分类结果与目标的实际分类越符合,说明结果越精确。
如下图所示,人、大巴为检测目标,既要检测出所有人和大巴的位置,也要检测出包围人和大巴的最小矩形框,同时还要识别出哪个矩形框内是人,哪个矩形框内是大巴。
02 yolov5网络的损失函数构成
前文我们也讲过yolov5网络的基本思想:
把640640的输入图像划分成NN(通常为8080、4040、20*20)的网格,然后对网格的每个格子都预测三个指标:矩形框、置信度、分类概率。其中:
- 矩形框表征目标的大小以及精确位置。
- 置信度表征所预测矩形框(简称预测框)的可信程度,取值范围0~1,值越大说明该矩形框中越可能存在目标。
- 分类概率表征目标的类别。
所以在实际检测时:
- 首先判断每个预测框的预测置信度是否超过设定阈值,若超过则认为该预测框内存在目标,从而得到目标的大致位置。
- 接着根据非极大值抑制算法对存在目标的预测框进行筛选,剔除对应同一目标的重复矩形框(非极大值抑制算法我们后续再详细讲)。
- 最后根据筛选后预测框的分类概率,取最大概率对应的索引,即为目标的分类索引号,从而得到目标的类别。
损失函数的作用为度量神经网络预测信息与期望信息(标签)的距离,预测信息越接近期望信息,损失函数值越小。由上述每个格子的预测信息可知,训练时主要包含三个方面的损失:矩形框损失(lossrect**)、*置信度损失(loss*obj)、分类损失(lossclc**)**。因此yolov5网络的损失函数定义为:
Loss=a*lossobj+ b*lossrect+ c*lossclc
也即总体损失为三个损失的加权和,通常置信度损失取最大权重,矩形框损失和分类损失的权重次之,比如:
a = 0.4
b = 0.3
c = 0.3
yolov5使用CIOU loss计算矩形框损失,置信度损失与分类损失都用BCE loss计算,下面我们会详细介绍各种损失函数的计算原理。
03 mask掩码矩阵
下面我们以8080网格为例来说明mask掩码的定义、用途,以及如何获取。4040网格与20*20网格也类似。
什么是mask掩码?
神经网络对一张图像分割成的8080网格预测了38080个预测框,那么每个预测框都存在检测目标吗?显然不是。所以在训练时首先需要根据标签作初步判断,哪些预测框里面很可能存在目标?mask掩码为这样的一个380*80的bool型矩阵:3*80*80个bool值与3*80*80个预测框一一对应,根据标签信息和一定规则判断每个预测框内是否存在目标,如果存在则将mask矩阵中对应位置的值设置为true,否则设置为false。
mask掩码有什么用?
神经网络对8080网格的每个格子都预测三个矩形框,因此输出了380*80个预测框,每个预测框的预测信息包括矩形框信息、置信度、分类概率。实际上,并非所有预测框都需要计算所有类别的损失函数值,而是根据mask矩阵来决定:
- 仅mask矩阵中对应位置为true的预测框,需要计算矩形框损失;
- 仅mask矩阵中对应位置为true的预测框,需要计算分类损失;
- 所有预测框都需要计算置信度损失,但是mask为true的预测框与mask为false的预测框的置信度标签值不一样。
对于80*80网格,其各类损失函数的计算表达式如下,其中a为mask为true时置信度损失的权重,通常取值0.5~1之间,使得网络在训练时更加专注于mask为true的情况。
4040网格和2020网格的损失函数计算与80*80网格类似,最后把所有网格的损失函数值作加权和,即得到一张训练图像的最后的损失函数值:
上式中α1、α2、α3为各网格损失函数值的权重系数。考虑到图像中往往小型目标比较多,中型目标次之,大型目标最少,因此通常把8080网格的权重α1设置最大,4040网格的权重α2次之,20*20网格的权重α3最小,使得训练时网络更加专注于数量多的目标,比如α1、α2、α3依次取0.5、0.3、0.2。
怎么得到mask掩码?
下面我们详细说明一下mask矩阵是怎么得到的。
首先将38080的mask矩阵**全部设置为false。*对于COCO数据集json标签文件中标注的一张图像中的*每个目标框,都按照以下步骤作判断,并根据判断结果将mask矩阵的对应位置设置为true:
(1)从json标签文件解析出单张图像中所有目标框的中心坐标和宽高,以及图像的宽高。假设解析得到一个目标框的中心坐标为(x, y),宽、高分别为w、h,图像的宽高分别为wi、hi。需要将(x, y)转换为640640图像的坐标(x’, y’),并将w、h转换为640640图像中目标框的宽高wgt、hgt。
(2)然后由(x’, y’)计算该目标框在80*80网格中的网格坐标(xg,yg),注意xg,、yg都是浮点数。
(3)接着对xg、yg向下取整,得到整型网格坐标(x0, y0)。同时为了加快训练的收敛速度,yolov5对网格(x0, y0)的左右、上下再各取一个邻近的网格:(x1, y0)和(x0, y1)。具体怎么取呢?如下图,红点为点(xg,yg):
- 如果点(xg,yg)在格子的左上角,则取左边、上方的两个格子;
- 如果点(xg,yg)在格子的右上角,则取右边、上方的两个格子;
- 如果点(xg,yg)在格子的左下角,则取左边、下方的两个格子;
- 如果点(xg,yg)在格子的右下角,则取右边、上下方的两个格子;
根据以上原则,x1和y1可按下式计算,其中round为四舍五入运算:
**(4)**经过第(3)步,得到三个互相邻近的格子(x0, y0)、(x1, y0)、(x0, y1),我们认为该目标框位于这三个格子的附近。
前文我们讲使用Kmeans聚类算法获取九个anchor框的时候,就讲过:
- 宽、高最小的anchor0、anchor1、anchor2分配给80*80网格的每个格子;
- 宽、高次小的anchor3、anchor4、anchor5分配给40*40网格的每个格子;
- 宽、高最大的anchor6、anchor7、anchor8分配给20*20网格的每个格子。
因此80*80网格中(x0, y0)、(x1, y0)、(x0, y1)这三个格子都对应anchor0、anchor1、anchor2这三个anchor框。
**(5)**假设anchor0、anchor1、anchor2这三个anchor框的宽高分别为(w0, h0)、(w1, h1)、(w2, h2),从json标签文件解析得到该目标框的宽高为(wgt, hgt),然后分别计算(wgt, hgt)与(w0, h0)、(w1, h1)、(w2, h2)的比例,再根据比例剔除不满足要求的anchor框:
将保留的anchor框标记为true,剔除的anchor框标记为false,那么anchor0、anchor1、anchor2对应的标记为(m0, m1, m2):
**(6)**根据80*80网格中(x0, y0)、(x1, y0)、(x0, y1)这三个坐标位置,我们分别对mask矩阵赋值:
![img](https://img-blog.csdnimg.cn/img_convert/1d06cdfd94d41863aa41536e185dc6cc.png
对一张图像中的所有目标框,都作以上判断并对mask矩阵赋值,即可得到该图像的mask矩阵。
04 矩形框损失计算原理
这里为什么先讲矩形框损失呢?因为后面讲的置信度损失原理会使用到矩形框损失。
我们前文讲过,yolov5对每个格子预测3个不同位置和大小的矩形框,其中每个矩形框的信息为矩形中心的x坐标、y坐标,以及矩形宽、高。假设对某个格子预测的矩形框为(xp, yp, wp, hp),该格子对应的目标矩形框为(xl, yl, wl, hl),下面依次讲解几种最常见的矩形框损失函数的计算原理。
L1、L2、smooth L1损失函数
首先是L1损失函数:
其次是L2损失函数:
接着是smooth L1损失函数:
以上式子中,记d=x1-x2,分别画出fL1(d)、fL2(d)、fsL1(d)的曲线如下图:
由以上计算公式和曲线可以看出:
**(1)**fL1(d)函数的左右两侧曲线对于d的导数(斜率)是恒定不变的,但是在d=0处该函数不可导,然而随着训练的进行,d=x1-x2会逐渐接近0,这就导致在训练后期损失函数的值在某个值附近波动,很难收敛。
**(2)**fL2(d)函数在d=0出处是可导的,不存在fL1(d)函数的问题,但是在前期训练阶段d很大的时候,fL2(d)函数对于d的导数也会很大,这很可能会导致梯度爆炸问题,从而训练没能朝着最优化的方向进行。
**(3)**fsL1(d)函数为分段函数,它将fL1(d)函数、fL2(d)函数的优点结合起来,同时完美规避了fL1(d)函数、fL2(d)函数的缺点。
IOU系列损失函数
上述计算矩形框的L1、L2、smooth L1损失时有一个共同点,都是分别计算矩形框中心点x坐标、中心点y坐标、宽、高的损失,最后再将四个损失值相加得到该矩形框的最终损失值。这种计算方法的前提假设是中心点x坐标、中心点y坐标、宽、高这四个值是相互独立的,实际上它们具有相关性,所以该计算方法存在问题。
于是,IOU系列损失函数(IOU、GIOU、DIOU、CIOU)又被陆续提了出来。
用一句话来说,就是越缺什么,就越着重弥补什么,从而达到加速优化训练的收敛速度和稳定性的目的。
由以上可得CIOU loss的计算公式为:
05 置信度损失计算原理
下面以8080网格为例,详细讲置信度损失的计算原理,4040和2020网格的置信度损失计算原理与8080网格一样,可依此类推。
神经网络预测的置信度
对于一张图像分割成的80*80的网格,神经网络对其中每个格子都预测三个位于该格子附近的矩形框(简称预测框),每个预测框的预测信息包括中心坐标、宽、高、置信度、分类概率,因此神经网络总共输出3*80*80个0~1的预测置信度,与3*80*80个预测框一 一对应。每个预测框的置信度表征这个预测框的靠谱程度,值越大表示该预测框越可信靠谱,也即越接近目标的真实最小包围框。比如下图中,红点A、B、C、D表示检测目标,那么每个红点所在格子的三个预测置信度应该比较大甚至接近1,而其它格子的预测置信度应该较小甚至接近0。
置信度的标签
标签的维度应该与神经网络的输出维度保持一致,因此置信度的标签也是维度为3*80*80的矩阵。这里就用到了上文我们讲的mask掩码矩阵:以维度同样为38080的mask矩阵为标记,对置信度标签矩阵进行赋值。yolo之前版本直接对mask矩阵为true的地方赋值1,mask矩阵为false的地方赋值0,认为只要mask为true就表示对应预测框完美包围了目标。这样做就太绝对了,因为mask为true只是表示该预测框在目标附近而已,并不一定完美包围了目标。所以yolov5改变了做法:对mask为true的位置不直接赋1,而是计算对应预测框与目标框的CIOU,使用CIOU作为该预测框的置信度标签,当然对mask为false的位置还是直接赋0。这样一来,标签值的大小与预测框、目标框的重合度有关,两框重合度越高则标签值越大。当然,上文我们讲CIOU的取值范围是-1.51,而置信度标签的取值范围是01,所以需要对CIOU做一个截断处理:当CIOU小于0时直接取0值作为标签。
BCE loss损失函数
假设置信度标签为矩阵L,预测置信度为矩阵P,那么矩阵中每个数值的BCE loss的计算公式如下:
注意BCE loss要求输入数据的取值范围必须在0~1之间。
从得到80*80网格的置信度损失值:
![img](https://img-blog.csdnimg.cn/img_convert/09eae577f7de9b7ff9c67b9760e2e2e5.webp?x-oss-process=image/format,png#pic_center
此外,我们称对应mask位true的预测框为正样本,对应mask为false的预测框为负样本,负样本肯定是远远多于正样本的,为了使训练更专注于正样本,后来Focal loss又被提了出来,我们在此暂时不细说,下篇文章再详细介绍吧。
06 分类损失计算原理
下面也以8080网格为例,详细讲分类损失的计算原理,4040和2020网格的分类损失计算原理与8080网格一样,可依此类推。神经网络对80*80网格的每个格子都预测三个预测框,每个预测框的预测信息都包含了N个分类概率。其中N为总类别数,比如COCO数据集有80个类别,那么N取80。所以对于COCO数据集,每个预测框有80个0~1的分类概率,那么神经网络总共预测3*80*80*80个分类概率,组成预测概率矩阵。
8080网格的标签概率矩阵与预测概率矩阵的维度一样,也是3808080。每个预测框的标签,由解析json标签文件得到,是一个079的数值,需要将079的数值转换成80个数的独热码:
![img](https://img-blog.csdnimg.cn/img_convert/ddc27335358de16b9d27cfa21a7172ac.webp?x-oss-process=image/format,png#pic_center
然而,为了减少过拟合,且增加训练的稳定性,通常对独热码标签做一个平滑操作。如下式,label为独热码中的所有数值,α为平滑系数,取值范围0~1,通常取0.1。
同样假设置标签概率为矩阵Lsmooth,预测概率为矩阵P,那么矩阵中每个数值的BCE loss的计算公式如下:
于是得到80*80网格的分类损失函数值的计算公式:
三,进一步解读yolov5
loss计算
yolov5的loss设计和前yolo系列差别比较大的地方就是正样本anchor区域计算,其余地方差距很小。分类分支采用的loss是BCE,conf分支也是BCE,当然可以通过h[‘fl_gamma’]参数开启focal Loss,默认配置没有采用focal los,而bbox分支采用的是Giou loss。
对于yolov3计算过程不熟悉的,可以参考目标检测正负样本区分策略和平衡策略总结(一),里面有详细分析yolov3的loss计算过程。loss的计算非常简单,核心是如何得到loss计算所需的target。yolov5的很大区别就是在于正样本区域的定义。在yolov3中,其正样本区域也就是anchor匹配策略非常粗暴:保证每个gt bbox一定有一个唯一的anchor进行对应,匹配规则就是IOU最大,并且某个gt一定不可能在三个预测层的某几层上同时进行匹配。,不考虑一个gt bbox对应多个anchor的场合,也不考虑anchor是否设置合理。不考虑一个gt bbox对应多个anchor的场合的设定会导致整体收敛比较慢,在诸多论文研究中表明,例如FCOS和ATSS:增加高质量正样本anchor可以显著加速收敛。
本文也采用了增加正样本anchor数目的做法来加速收敛,这其实也是yolov5在实践中表明收敛速度非常快的原因。其核心匹配规则为:
(1) 对于任何一个输出层,抛弃了基于max iou匹配的规则,而是直接采用shape规则匹配,也就是该bbox和当前层的anchor计算宽高比,如果宽高比例大于设定阈值,则说明该bbox和anchor匹配度不够,将该bbox过滤暂时丢掉,在该层预测中认为是背景
(2) 对于剩下的bbox,计算其落在哪个网格内,同时利用四舍五入规则,找出最近的两个网格,将这三个网格都认为是负责预测该bbox的,可以发现粗略估计正样本数相比前yolo系列,至少增加了三倍
如上图所示,绿点表示该Bbox中心,现在需要额外考虑其2个最近的邻域网格也作为该bbox的正样本anchor。从这里就可以发现bbox的xy回归分支的取值范围不再是01,而是-0.51.5(0.5是网格中心偏移,请仔细思考为啥是这个范围),因为跨网格预测了。
为了方便理解,我对预测的正样本anchor进行了可视化(dataset应用了mosaic增强):
三张图排布顺序是:0-大输出特征图(stride=8),1-中等尺度特征图(stride=16),2-小尺度特征图(stride=32),分别检测小物体、中等尺寸和大尺度问题。其中红色bbox表示该预测层中的gt bbox,黄色bbox表示该层对应位置的正样本anchor。第一幅图是大输出特征图,只检测小物体,所以人那个bbox标注被当做背景了,并且有三个anchor进行匹配了,其中包括当前网格位置anchor和2个最近邻居anchor。从这幅图中可以看出很多东西(一定要仔细看这几幅图,loss设计的思想在图中一目了然):
(1) 不同于yolov3和v4,其gt bbox可以跨层预测即有些bbox在多个预测层都算正样本
(2) 不同于yolov3和v4,其gt bbox的匹配数范围从3-9个,明显增加了很多正样本(3是因为多引入了两个邻居)
(3) 不同于yolov3和v4,有些gt bbox由于和anchor匹配度不高,而变成背景
我个人看法:作者这种特别暴力增加正样本做法还是存在很大弊端,虽然可以加速收敛,但是由于引入了很多低质量anchor,对最终结果还是有影响的。我相信这个部分作者应该还会优化的。
下面结合代码进行详细分析,具体就是compute_loss函数:
(1) build_targets
build_targets函数用于计算loss函数所需要的target。其大概流程为:
1.将targets 重复3遍(3=层anchor数目),也就是将每个gt bbox复制变成独立的3份,方便和每个位置的3个anchor单独匹配
2.对每个输出层单独匹配。首先将targets变成anchor尺度,方便计算;然后将target wh shape和anchor的wh计算比例,如果比例过大,则说明匹配度不高,将该bbox过滤,在当前层认为是bg
**3.**计算最近的2个邻居网格
**4.**对每个bbox找出对应的正样本anchor,其中包括b表示当前bbox属于batch内部的第几张图片,a表示当前bbox和当前层的第几个anchor匹配上,gi,gj是对应的负责预测该bbox的网格坐标,gxy是不考虑offset或者说yolov3里面设定的该Bbox的负责预测网格,gwh是对应的归一化bbox wh,c是该Bbox类别
由于其采用了跨网格预测,故xy预测输出不再是0-1,而是-1~1,加上offset偏移,则为-0.5-1.5;并且由于shape过滤规则,wh预测输出也不再是任意范围,而是0-4。
整个build_targets代码虽然比较乱,但是计算效率是非常高的,没有常见的复现代码中的对batch进行for循环操作。
从上述可以发现:在任何一预测层,将每个bbox复制和anchor个数一样多的数目,然后将bbox和anchor一一对应计算,去除不匹配的bbox,然后对原始中心点网格坐标扩展两个邻居像素,增加正样本anchor。有个细节需要注意,前面shape过滤时候是不考虑bbox的xy坐标的,也就是说bbox的wh是和所有anchor匹配的,会导致找到的邻居也相当于进行了shape过滤规则,故对于任何一个输出层,如果该bbox保留,那么至少有3个anchor进行匹配,并且保留的3个anchor shape是一样大的。即保留的anchor在不考虑越界情况下是3或者6或者9。
(2) loss计算
有了上述数据,计算Loss就非常容易了。
BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.Tensor([h['cls_pw']])).to(device)
BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.Tensor([h['obj_pw']])).to(device)
设置了正样本区域权重,cls和conf分支都是bce loss,xywh分支直接采用giou loss
注意:
pwh = (ps[:, 2:4].sigmoid() * 2) ** 2 * anchors[i] # wh
其没有采用exp操作,而是直接乘上anchors[i]。
类似fcos和yolov2,虽然我们引入了大量正样本anchor,但是不同anchor和gt bbox匹配度是不一样,预测框和gt bbox 的 匹配度也不一样,如果权重设置一样肯定不是最优的,故作者将预测框和bbox的giou作为权重乘到conf分支,用于表征预测质量。
核心内容就上面这些了。
总结
纵观整个yolov5代码,和前yolo系列相比,特点应该是
(1) 考虑了邻域的正样本anchor匹配策略,增加了正样本
(2) 通过灵活的配置参数,可以得到不同复杂度的模型
(3) 通过一些内置的超参优化策略,提升整体性能
(4) 和yolov4一样,都用了mosaic增强,提升小物体检测性能
其他一些操作包括:
(1) 采用了最新版本的pytorch进行混合精度以及分布式训练
(2) warmup+cos lr学习率策略,对bias不进行权重衰减
(3) 采用了yolo系列中常用的梯度累积策略,增加batch size,并对输出head部分的bias进行特殊初始化;采用了类平衡采样策略
(4) 多尺度训练,但是写的非常粗暴,直接对dataloader输出的batch图片进行双线性插值
(5) 支持onnx格式导出
(6) 采用了模型权重指数滑动平均的ema策略(比赛常用策略)
但是本仓库缺点是代码质量比较糟糕,类和类之间耦合非常严重,存在大量的直接修改类属性的方法,并且很多参数都是自己写死在代码里面,对于理解代码结构非常不利。
作者提供模型运行结果:
四,YOLOv5代码理解性注释
五,YOLOv5有关损失函数的一些小点
YOLOv5使用二元交叉熵损失函数计算类别概率和目标置信度得分的损失(分类/定位),使用CIOU Loss作为bounding box回归的损失。
分类损失/定位损失/置信度损失
类别预测 Class Prediction
两种不同的分类任务:互斥分类(mutually exclusive)和多标签分类(multi-label)。让我们分别理解这两种任务及其在模型设计和损失函数选择上的差异。
互斥分类
在互斥分类中,每个实例(如图像)只能属于一个类别。例如,在猫和狗的分类问题中,每张图像只能标记为“猫”或“狗”,不能同时是两者。
- Softmax 函数:
- 在这种任务中,通常使用 Softmax 函数将模型的输出转换为概率分布。Softmax 确保所有类别的概率总和为 1,这反映了互斥性:提高一个类别的概率会相应降低其他类别的概率。
- 损失函数:
- 对于互斥分类,常用的损失函数是交叉熵损失(Cross-Entropy Loss),它衡量的是模型预测的概率分布和真实标签的概率分布之间的差异。
多标签分类
多标签分类中,每个实例可以同时属于多个类别。例如,在目标检测任务中,一张图像可能同时包含“行人”和“儿童”两个标签。
- 独立逻辑分类器:
- 在多标签分类中,由于类别之间不是互斥的,因此不能使用 Softmax 函数。相反,每个类别通常使用一个独立的逻辑(logistic)分类器,输出该类别存在的概率。这意味着每个类别的概率是独立的,总和可以不等于 1。
- 损失函数:
- 对于多标签分类,损失函数通常是二元交叉熵损失(Binary Cross-Entropy Loss),它是交叉熵损失在二分类(每个标签独立考虑)场景的特例。这种损失函数针对每个标签独立计算,反映了每个标签的预测准确性。
YOLOv5 和多标签分类
YOLOv5 用于目标检测,通常面临的是多标签分类问题。每个检测到的物体可能有多个标签(例如,一个对象可以同时是“行人”和“儿童”)。在这种情况下,使用多个独立的逻辑分类器比使用单个 Softmax 分类器更合适,因为它允许模型为每个类别独立地输出存在的概率。
总结来说,选择 Softmax 函数或独立逻辑分类器取决于分类任务的性质(互斥还是非互斥)。在处理多标签分类问题时,独立的逻辑分类器和二元交叉熵损失提供了更灵活和准确的方法来处理类别之间的非互斥性。
softmax函数
用于多类分类问题的激活函数。
可以分成soft和max,max就是最大值嘛,soft浅理解为不是求唯一的最大值,而是为每个输出分类的结果赋予一个概率值表示可能性。确保较小的值有较小的概率,不能直接丢弃。
二元交叉熵
多用于处理二分类问题。(是一个对数运算)
计算了真实标签和预测概率之间的对数差异。需要对错误的预测进行惩罚,如果实际的类的概率是1.0,那么需要它的损失为零。如果概率比较低,那么它的损失很大。
H(p,q) = - ∑(p(x) * log(q(x)))
其中,p(x)表示真实标签的分布,q(x)表示模型预测的分布。交叉熵越小,表示两个概率分布越接近,模型的预测结果越准确。
从直观上理解,交叉熵损失函数可以看作是一种表示信息量的度量。假设一个事件发生的概率为p,那么它的信息量可以用-log§来表示。当交叉熵越小,表示模型的预测分布越接近真实分布,模型预测的结果可以更好地表示真实情况,所以损失也就越小。