nms和P,R,map原理及在Yolov5代码中的解析

您所在的位置:网站首页 yolov5输出结果在哪看 nms和P,R,map原理及在Yolov5代码中的解析

nms和P,R,map原理及在Yolov5代码中的解析

2023-10-29 00:38| 来源: 网络整理| 查看: 265

         将非极大值抑制(nms)和map放在一块进行讲解分析,因为其都是通过IOU和置信度(score)来计算,但两者方式不一样,容易产生干扰,NMS通过IOU来过滤掉候选框,而map通过IOU来筛选正负样本。

目录

nms

所有类别nms

不同类别nms

准确率,召回率

F1和map

F1:

 Ap:       

Yolov5代码中P, R和Map解析

nms

       目标检测推理过程会产生许多目标检测框,这些检测框宽高都不一致,且每个检测框都赋有一个置信度阈值,需要对这些目标框进行过滤,筛选出最优的目标框。首先,通过事先设定好的置信度阈值可以过滤掉部分检测框(即置信度小于该阈值的检测框被过滤),置信度有两种形式,一种是前景的概率(即包含有物体的概率),另一种是前景概率与类别概率的乘积。对于剩余检测框通过NMS进行过滤,最终仅保留一个与目标最匹配的检测框。

        NMS有两种思路:

所有类别nms

伪代码算法简易步骤:

all_box = all_box.sort()  ## 将所有检测出的box从大到小进行排序

for i in len(all_box):       ## 根据置信度从大到小遍历所有的box

        for  j in len(all_box) :  ## 将置信度小于某个box的其他所有box与此box对比,计算IOU

                if j > i :

                        判断all_box[i]和all_box[j]的IOU面积是否大于阈值,如果大于阈值则删除此box,否则保留此box,直到所有box被保存,即为整张图片被检测到的所有目标框。

其原理图示如下,图片源于网络 Tom Hardy博客

 对所有类别进行nms,代码以python示例:

def NMS(boxes,scores, thresholds): x1 = boxes[:,0] y1 = boxes[:,1] x2 = boxes[:,2] y2 = boxes[:,3] areas = (x2-x1)*(y2-y1) _,order = scores.sort(0,descending=True) keep = [] while order.numel() > 0: i = order[0] keep.append(i) if order.numel() == 1: break xx1 = x1[order[1:]].clamp(min=x1[i]) yy1 = y1[order[1:]].clamp(min=y1[i]) xx2 = x2[order[1:]].clamp(max=x2[i]) yy2 = y2[order[1:]].clamp(max=y2[i]) w = (xx2-xx1).clamp(min=0) h = (yy2-yy1).clamp(min=0) inter = w*h ovr = inter/(areas[i] + areas[order[1:]] - inter) ids = (ovr b.score; }); // 对检测的box根据置信度排序 std::vector vArea(input_boxes.size()); for (int i = 0; i < int(input_boxes.size()); ++i) { vArea[i] = (input_boxes.at(i).x2 - input_boxes.at(i).x1 + 1) * (input_boxes.at(i).y2 - input_boxes.at(i).y1 + 1); } // 获取所有检测出的box的面积 for (int i = 0; i < int(input_boxes.size()); ++i) { for (int j = i + 1; j < int(input_boxes.size());) { float xx1 = (std::max)(input_boxes[i].x1, input_boxes[j].x1); float yy1 = (std::max)(input_boxes[i].y1, input_boxes[j].y1); float xx2 = (std::min)(input_boxes[i].x2, input_boxes[j].x2); float yy2 = (std::min)(input_boxes[i].y2, input_boxes[j].y2); float w = (std::max)(float(0), xx2 - xx1 + 1); float h = (std::max)(float(0), yy2 - yy1 + 1); float inter = w * h; float ovr = inter / (vArea[i] + vArea[j] - inter); // IOU if (ovr >= NMS_THRESH) // 从vector begin开始,置信度最大的box与另一box对比,如果IOU大于阈值则删除此box,进而和下一个box对比,直到所有box都对比完 { input_boxes.erase(input_boxes.begin() + j); vArea.erase(vArea.begin() + j); } else { j++; } } }

        通过手动设置IOU阈值,容易产生两个主要问题,一是:当IOU阈值设置较大时,会有很多冗余的检测框不会被有效过滤;当IOU阈值设置较小,虽可有效过滤更多的检测框。但当有两个不同类别的物体相距很近时,另一个置信度较低的物体容易被过滤,从而无法被检测到;为了弥补这种缺陷往往会采用另一种方式。

不同类别nms

         通过不同类别进行NMS,伪代码算法步骤为:

for label in all_labels: 

        a. 获取此类别(label)下所有box信息   ## 坐标位置,置信度,类别概率

        b. 根据box置信度从高至低排序,保存且记录当前置信度最大box

        c. 遍历b中置信度最大box以外的其他所有box,对比其他所有box与置信度最大box的IOU,删除IOU大于阈值的其他box

        d. 对剩下box,重复循环b,c步骤

这种方法的缺陷为,当两个相同类别的物体相隔很近时,另一个被检测到置信度较低些的物体容易被过滤掉,结果仅保留此类别下的一种物体。图示如下,其中红框的犬只可以被有效检测到,但蓝框虚线犬只会被过滤,因为其IOU大于阈值。当然图示相隔还有一定距离,如果相隔更近,IOU就更大了,更加难以去除。

对于yolov5代码直接调用函数torchvision.ops.nms:

i = torchvision.ops.nms(boxes, scores, iou_thres)

对于多类别NMS的实现,通过对每个候选框坐标添加一个偏移量来实现,偏移量可以通过不同类别的索引来实现。源码如下:

max_coordinate = boxes.max() offsets = idxs.to(boxes) * (max_coordinate + torch.tensor(1).to(boxes)) boxes_for_nms = boxes + offsets[:, None] keep = nms(boxes_for_nms, scores, iou_threshold) return keep

通过torchvision.ops.boxes.batched_nms(boxes, scores, classes, nms_thresh) 调用。

在yolov5中,实现代码:

# Batched NMS c = x[:, 5:6] * (0 if agnostic else max_wh) # classes 类别 boxes, scores = x[:, :4] + c, x[:, 4] # boxes (offset by class), scores ## 采用nms将框box数量过滤,IOU设置越小,框越少; i为经过nms后剩余框的索引 i = torchvision.ops.nms(boxes, scores, iou_thres) # NMS 将所有的框box,依据置信度scores得分进行过滤

 通过设置agnostic来判定是否使用多类别NMS,当agnostic为True时,即对所有类别进行NMS,当其为False时,对每个类别分开单独进行NMS。max_wh为检测框的最大宽高(像素),yolov5中指定为4096。

这两种方式的缺陷:①IOU阈值设置过大,,单个目标物会出现多个检测框,IOU阈值设置过小,则相邻的同类物体会被过滤掉;②低于IOU阈值的,置信度设置为0,不够合理;③NMS只能在CPU上运行,影响性能。

现方法中除了nms外还有soft nms可以从原理上有效解决多个同类别物体相隔很近时的检测问题。此外对于IOU的演化,还有GIOU,CIOU,DIOU以及最新的SIOU,其将两个不同box之间的距离,重叠率,尺度,横纵比等多维度进行考量。这些方法的改进思路和方法很简单,这里就不再赘述。

准确率,召回率

对准确性和召回率,通过TP,FP,FN三者的关系对准确性和召回率进行计算,对TP,FP,FN的解析如下:

TP:  与真实框的IOU大于设定阈值的检测框,被视为模型正确识别的正样本;

FP:与真实框的IOU小于设定阈值的检测框,被视为模型错误识别的正样本;

FN:没有被模型识别为正样本的目标(即模型没有检测到)

准确率和召回率计算公式为:

准确率(Precision):

Precision=TP/(TP+FP)

表示模型预测的所有检测框中,预测正确的检测框(正样本数)所占的比例

召回率(Recall):

Recall=TP/(TP+FN)

表示模型预测的所有检测框中,预测正确的检测框与实际真实框的比例

计算过程为:首先模型对所有验证集图片进行检测,通过NMS后保留下所有验证集图片的目标检测框。再基于设定的置信度阈值,对大于此阈值的检测框进行统计分析。

以如下图示和表为例:红色框为GT框,蓝色框,黑色框和黄色虚线框为检测框,

① 假定置信度阈值为0.3,三个检测框都大于设定的置信度阈值。另假定IOU阈值为0.6,其中蓝色框,黑色框与真实框IOU大于设定阈值,黄色虚线框与真实框IOU小于设定阈值,则TP=2(即被模型识别正确的检测框——蓝色框和黑色框),FP=1(被模型识别错误的检测框——黄色虚线框),FN=2(漏检的,左图红框中的犬只与右图下面犬只未被检测到),故准确率:Precision=2/(2+1)=0.67,召回率:Recall=2/(2+2)=0.5.

②假定置信度阈值为0.7,则置信度小于0.7的检测框不纳入统计范畴,IOU依旧阈值为0.6,则蓝色框和黄色虚线框作为检测框,TP=1(蓝色框),FP=1(黄色虚线框),FN=3(四个GT框,仅蓝色框对应的GT框被检测到,其余三个为被检测到),则准确率:Precision=1/(1+1)=0.5,Recall=1/(1+3)=0.25.

③假定置信度阈值为0.7,IOU阈值为0.4,基于置信度阈值,ID为1,2的两个框被检测到并作为统计,则:TP=2(蓝色框和黄色虚线框),FP=0,FN=2,Precision=2/(2+0)=1,Recall=2/(2+2)=0.5.

基于置信度从高至低排序:

目标ID| 检测框 | 置信度 | IOU ---------------------------- 1 | 蓝色框 | 0.96 | 0.95 ---------------------------- 2 | 黄色虚线框| 0.75 | 0.42 ---------------------------- 3 | 黑色框 | 0.62 | 0.8

 总结:

        对准确率,召回率的计算,首先基于置信度阈值,选取置信度大于阈值的目标检测框,然后基于检测框和GT框的IOU,对IOU大于阈值的为预测正确的(TP),小于IOU阈值的为预测错误的(FP),GT框中没有检测到的为漏检的(FN)。但基于准确率和召回率来衡量模型的性能效果,存在一定问题:通过手动设置置信度和IOU会存在人为因素偏差,针对不同的目标有不同的效果。故需要综合权衡准确率,召回率以及IOU的设置,一方面通过F1指标来权衡准确率和召回率,另一方面通过map来衡量。

F1和map F1

F1的计算很简单,公式如下:

 Ap     

        Ap是衡量某一个类别检测效果的好坏。其根据不同的置信度和IOU阈值,对应有不同的准确率和召回率,进而通过计算准确率和召回率构成的二维曲线图面积即为Ap值。以官网图示为例:

在每个”峰值点”往左画一条直线,和上一个”峰值点”的垂直线相交,这样和坐标轴的面积就是AP值。 

 通过不同类别的Ap求取平均值即为mAP,其为衡量多个类别的检测效果:

         基于COCO的评价指标map,其IOU选择为0.5~0.95,间隔0.05,共10个IOU阈值,置信度固定为0.1或0.01,Yolov5源码中固定置信度阈值为0.1的一个线性插值,后面对Yolov5 map代码做讲解时分析。此外,其对Recall从0~1间隔0.1,分为101份小间隔,对这101个Recall对应的Precision值采用线性插值计算,最后通过计算所有这101个Recall和Precision值构成的小矩形面积,计算出Ap值。

Yolov5代码中P, R和Map解析

        以Yolov5源代码中对准确率,召回率,F1,Map的计算做展开讲解(每一行都有相对细节的文字解析):

def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='precision-recall_curve.png', names=[]): # Sort by objectness i = np.argsort(-conf) ## i 为基于所有验证集预测框的置信度的升序排序(因为添加了负号), 获取升序后置信度对应的索引(将模型检测的所有验证集图片的box汇总一起) tp, conf, pred_cls = tp[i], conf[i], pred_cls[i] ## tp为模型预测的每个框在10个IOU阈值下是否为正确的,其与GT框的IOU大于阈值则为True,否则为False,eg.tp ([ True True True True True True True True True False]);pred_cls为对应预测的类别(eg, pred_cls: [0,1,0,0],即预测的类别分别为0,1,0,0) # Find unique classes unique_classes = np.unique(target_cls) # target_cls 真实的类别(eg,[0,1,0,1]);unique_classes(唯一的类别顺序,即对所有GT框对应的类别从低到高排序且去重,例如:5个GT box对于类别[0,1,1,2,0],则unique_classes为[0,1,2]) # Create Precision-Recall curve and compute AP for each class px, py = np.linspace(0, 1, 1000), [] # for plotting pr_score = 0.1 # score to evaluate P and R 指定固定置信度阈值https://github.com/ultralytics/yolov3/issues/898 s = [unique_classes.shape[0], tp.shape[1]] # number class, number iou thresholds (i.e. 10 for mAP0.5...0.95) ap, p, r = np.zeros(s), np.zeros(s), np.zeros(s) # p.shape:[类别数,10],每一行表示每个类别,每一列代表每个IOU下的准确性(IOU:0.5~0.95) for ci, c in enumerate(unique_classes): ## unique_classes 类别序号,如:0,1,2,... 0代表:猫,1代表狗,2代表鸟,... i = pred_cls == c # i为基于预测的box列表中类别为c的索引处,当其IOU大于阈值为True,否则为False n_l = (target_cls == c).sum() # number of labels, GT box列表中为类别c的数量加和,即类别c的GT框数量 n_p = i.sum() # number of predictions;预测的box列表中为类别c的数量加和,即预测类别c的预测框数量 if n_p == 0 or n_l == 0: continue else: # Accumulate FPs and TPs ;cumsum(0),实现0轴(横轴)上的元素进行累加 fpc = (1 - tp[i]).cumsum(0) ## tp[i]的数组形状为[box数量,10],每行为一个预测box,每列对应一个IOU下此box与GT box的布尔值(若大于IOU阈值为True,否则为False),通过1 - tp[i]获取预测box与GT box小于阈值的框,进而对所有预测box的True 或者False作累加。 tpc = tp[i].cumsum(0) ## 计算所有预测box与GT box的True或者False的累加值(IOU大于阈值为True) ,每一行为上一行到此行所有预测box的准确数或错误数累加和 # Recall r[ci] recall = tpc / (n_l + 1e-16) # recall curve # recall[:, 0] 为iou为0.5在所有预测框累加的召回率 r[ci] = np.interp(-pr_score, -conf[i], recall[:, 0]) # r at pr_score, negative x, xp because xp decreases 基于各预测框置信度和召回率的对应(横轴,纵轴)关系,计算指定置信度阈值(0.1)下,采用线性插值法的召回率 # Precision precision = tpc / (tpc + fpc) # precision curve -conf[i] [。。。]每个索引对应的置信度 print('precision[:, 0]', precision[:, 0]) p[ci] = np.interp(-pr_score, -conf[i], precision[:, 0]) # p at pr_score 计算方法和召回率一致 # AP from recall-precision curve for j in range(tp.shape[1]): ap[ci, j], mpre, mrec = compute_ap(recall[:, j], precision[:, j]) ap的计算见下面分析 if plot and (j == 0): py.append(np.interp(px, mrec, mpre)) # precision at [email protected] # Compute F1 score (harmonic mean of precision and recall) f1 = 2 * p * r / (p + r + 1e-16) return p, r, ap, f1, unique_classes.astype('int32')

      对Yolov5代码整体过程简单分析:假设有5个预测box,6个gt box,置信度阈值和Yolov5一致设为0.1,IOU设置为0.5,对代码块

r[ci] = np.interp(-pr_score, -conf[i], recall[:, 0])

p[ci] = np.interp(-pr_score, -conf[i], precision[:, 0])

整体情况简析如下表:以第1,2行为例,预测box与gt box的IOU大于0.5为True,Recall=1/6=0.16, Precision=1/1=1,当rank为2,Recall=2/6=0.33, Precision=2/2=1,表格自上至下为Recall和Precision的累加形式。

Rank | box | -conf |GT(>0.5)| Recall | Precision ----------------------------------------------- 1 | Box1 | -0.95| True | 0.16 | 1 ----------------------------------------------- 2 | Box2 | -0.90| True | 0.33 | 1 ----------------------------------------------- 3 | Box3 | -0.82| False | 0.33 | 0.66 ----------------------------------------------- 4 | Box4 | -0.61| True | 0.50 | 0.75 ----------------------------------------------- 5 | Box5 | -0.05| True | 0.50 | 0.75 -----------------------------------------------

绘制-conf 和Recall,Precision曲线图,如下图所示,通过设置置信度阈值为0.1(和Yolov5源码设置的置信度一致)(则-conf为-0.1),计算其在-conf—Recall和-conf—Precision的线性插值,获取对应的准确率和召回率。

(注:上图横坐标应为-conf,保持与代码 -pr_score 一致)

 其对应的Recall-Precision曲线图如下所示,图形与横轴围成的面积即为Ap值,同理当IOU为0.5~0.95中的任意一个时,其计算方式同IOU=0.5相同。此例子中Recall(iou=0.5)=0.50,Precision(IOU=0.5)=0.75.

        Yolov5中对Map的计算代码如下,其通过对纵坐标(Recall)分为101份,采用线性插值法计算每个Recall对应的Precision值,对所有101份Recall值和Precision围成的矩形计算面积加和,即为Ap:

def compute_ap(recall, precision): """ Compute the average precision, given the recall and precision curves # Arguments recall: The recall curve (list) precision: The precision curve (list) # Returns Average precision, precision curve, recall curve """ # Append sentinel values to beginning and end mrec = np.concatenate(([0.], recall, [recall[-1] + 0.01])) mpre = np.concatenate(([1.], precision, [0.])) # Compute the precision envelope mpre = np.flip(np.maximum.accumulate(np.flip(mpre))) # Integrate area under curve method = 'interp' # methods: 'continuous', 'interp' if method == 'interp': x = np.linspace(0, 1, 101) # 101-point interp (COCO) 设置将Recall分为101份 ap = np.trapz(np.interp(x, mrec, mpre), x) # integrate # np.trapz计算x与mpre围成的面积之和; 采用np.interp(x, mrec, mpre)线性插值将基于横坐标(recall)的101个点基于线性插值,得出纵坐标(pre)的值 else: # 'continuous' i = np.where(mrec[1:] != mrec[:-1])[0] # points where x axis (recall) changes ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1]) # area under curve return ap, mpre, mrec

 Yolov5 test.py代码中(下述代码),p[:, 0], r[:, 0], ap[:, 0],为IOU=0.5时的准确率,召回率和ap,纵列为10列IOU从0.5~0.95对应的值,若p[:,1]为IOU=0.55时的准确率,p[:,10]为IOU=0.95时的准确率。因为有多个类别,需要对其采用.mean()求取平均。

p, r, ap50, ap = p[:, 0], r[:, 0], ap[:, 0], ap.mean(1) # [P, R, [email protected], [email protected]:0.95] ## 计算了IOU从0.5到0.95时准确性和召回率分别为多少,并进行了平均值的计算 mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean() nt = np.bincount(stats[3].astype(np.int64), minlength=nc) # number of targets per class



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3