程序员的资源宝库

网站首页 > gitee 正文

u版pytorch的YOLOv3验证过程理解\(^o^)/

sanyeah 2024-04-01 11:45:04 gitee 4 ℃ 0 评论

注:本文中的代码基于https://github.com/ultralytics/yolov3

这里的验证过程test是用于YOLOv3在训练过程中的每一个epoch观察:训练好的模型和权重在验证集上的mAP,从而计算检测精度AP。

---------------------------------------------------------------------------------------------

1首先要加载一个epoch中训练好的model,其中包括整个model的网络结构和权重等。要把model设置成eval形式。关于model.train()model.eval()主要是针对model在训练时和评价时不同的BatchNormalizationDropout方法模式:在eval()模式下,pytorch会自动把BN层和Dropout层固定住,不会取平均值,而是用训练好的数值。不然的话,test有输入数据,即使不训练,它也会改变权值,这是model中含有BN等层所带来的性质。

2接下来分批次加载testloader中的数据:

for batch_i, (imgs, targets, paths, shapes) in enumerate(tqdm(dataloader, desc=s)):
    imgs = imgs.to(device).float() / 255.0  # uint8 to float32, 0 - 255 to 0.0 - 1.0
    targets = targets.to(device)
    nb, _, height, width = imgs.shape  # batch size, channels, height, width
    whwh = torch.Tensor([width, height, width, height]).to(device)

3然后在进行测试前要先进行梯度失能:

with torch.no_grad(): 

这是因为验证过程只是一个前向计算过程得出结果,不需要进行反向传播调整权重,因此也不需要浪费内存去跟踪计算梯度。

4接下来将这个批次的imgs内容传入model得出预测结果。

inf_out, train_out = model(imgs) # inference and training outputs 

注意:这里开启了eval()模式,在写类的时候就规定eval()模式下会返回两个预测结果,其中inf_out用于之后通过NMS非极大值抑制得到剩余目标,而train_out则用于计算此次验证的结果和验证数据集中的标签二者之间的损失函数,这个损失值包含GIOU损失和obj置信度损失。

5关于inf_outtrain_out的返回如下

if self.training:  # train模式
    return yolo_out
else:  # inference or test 验证模式
    x, p = zip(*yolo_out)  # inference output, training output
    x = torch.cat(x, 1)  # cat yolo outputs
    return x, p

其中yolo_out的类型是tuple元组类型,即train_outtuple元组类型,inf_outtorch.Tensor类型。zip函数主要是用来将所有元组的元素拼接成一个列表。其用法举例如下:

zip(*[('a', 1), ('b', 2), ('c', 3), ('d', 4)])
[('a', 'b', 'c', 'd'), (1, 2, 3, 4)]

train_out里的内容分别是三个YOLO层返回的结果,而inf_out返回的是三个YOLO层对应元素拼接在一起返回的结果。

6接下来利用train_out计算GIOU/obj/cls的损失值如下:

loss += compute_loss(train_out, targets, model)[1][:3] 

7接下对inf_out进行NMS非极大值抑制:

output = non_max_suppression(inf_out, conf_thres=conf_thres, iou_thres=iou_thres) # nms 

8非极大值抑制函数的原型如下:

output = non_max_suppression(inf_out, conf_thres=conf_thres, iou_thres=iou_thres) # nms 

其中conf_thres是置信度阈值,而iou_thresIOU阈值。

上图是NMS的基本思路,对目标框进行NMS非极大值抑制主要是为了避免多个目标框重复预测同一个目标。

 

 实际上在实现的时候NMS分为Hard-NMS(DIOU/OVERLAP/MERGE/BATCHED)Soft-NMS

1Hard-nms:一直删除相邻的同类别目标,对于密集目标的输出不友好。

2Soft-nms:改变其相邻同类别目标的置信度,后期通过置信度阈值进行过滤,适用于目标密集的场景

3Or-nmsHard-nms的非官方实现形式,只支持CPU

4Vision-nmsHard-nms的官方实现形式(C函数库),可以支持GPU,只支持单类别的输入

5Vision-batched-nmsHard-nms的官方实现形式(C函数库),可以支持GPU,可支持多类别的输入

6And-nms:在Hard-nms的逻辑基础上,增加是不是单独框的限制,删除没有重叠框的框(减少误检)

7Merge-nms:在Hard-nms的基础上,增加保留框位置平滑策略(重叠框位置信息求解平均值,使得框的位置更加精确)

8Diou-nms:在Hard-nms的基础上使用DIOU替换IOU

9本次使用的是Merge-nms,具体实现过程如下:

1首先对inf_out的所有元素进行遍历,返回预测目标所在的图片序列索引和目标结果。

for xi, x in enumerate(prediction): # image index, image inference 

2对目标结果x的置信度先进行第一轮筛选

注:目标结果x的格式为[x, y, w, h, obj, cls]

x = x[x[:, 4] > conf_thres] 

3计算obj和类别置信度得到score,用于和后面的置信度阈值进行比较。

x[..., 5:] *= x[..., 4:5] # conf = obj_conf * cls_conf 

4将目标结果的x/y/w/h转成左上角和右下角的x/y/x/y坐标

box = xywh2xyxy(x[:, :4]) 

5重新将坐标和置信度转换成新的向量形式

x = torch.cat((box, conf.unsqueeze(1), j.float().unsqueeze(1)), 1) 

6接下来实现Hard-nms

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

7然后增加保留框位置平滑策略

weights = (box_iou(boxes[i], boxes) > iou_thres) * scores[None] 
# box weights x[i, :4] = torch.mm(weights / weights.sum(1, keepdim=True), x[:, :4]).float() # merged boxes

8最后输出经过非极大值抑制处理后的目标框

output[xi] = x[i] 

type_output: <class 'list'>

返回格式为 output =8(batch_size)* n * 6 (x1, y1, x2, y2, conf, cls)。就是每张图片里面有n个目标,每个目标组成是6个元素。

10接下来所有的目标框都得到了,要进行目标框的AP计算。

# targets = [image, class, x, y, w, h]

1output进行遍历

for si, pred in enumerate(output): 

其中si表示第0-7张图片,pred是这8张图片分别的预测结果。

其实接下来要算的就是这张图片的预测和标签之间的AP

2如果目标里面的image是这张图片,那么就加载这张图片所有目标的标签的类别。

labels = targets[targets[:, 0] == si, 1:] nl = len(labels)

nl表示这张图片的标签一共有多少目标

3接下来获取所有标签目标的类别

tcls = labels[:, 0].tolist() if nl else [] # target class 

4接下来把这张图片的所有预测结果还原到416*416的图片当中

一开始得到的pred的格式是(x1, y1, x2, y2, conf, cls),但是这里的左上角和右下角的坐标是相对于1*1的图片而言的,要将其映射到真正图片上的坐标。

clip_coords(pred, (height, width))
def clip_coords(boxes, img_shape):
    # Clip bounding xyxy bounding boxes to image shape (height, width)
    boxes[:, 0].clamp_(0, img_shape[1])  # x1
    boxes[:, 1].clamp_(0, img_shape[0])  # y1
    boxes[:, 2].clamp_(0, img_shape[1])  # x2
    boxes[:, 3].clamp_(0, img_shape[0])  # y2

5接下来的correct参数是用来统计TP(真阳性:即预测的是行人,标签也是行人的个数)先将其初始化为全False0

correct = torch.zeros(pred.shape[0], niou, dtype=torch.bool, device=device) 

6接下来获取标注的类别向量

tcls_tensor = labels[:, 0]#标注类别向量 

7接下来将标注的xywh标签转成左上角和右下角,同时还要乘416,映射到原图。

tbox = xywh2xyxy(labels[:, 1:5]) * whwh 

8接下来计算每一个类别的真阳性

for cls in torch.unique(tcls_tensor):#用于去重,看一下这张图片中到底有多少类
    ti = (cls == tcls_tensor).nonzero().view(-1)  # prediction indices
    pi = (cls == pred[:, 5]).nonzero().view(-1)  # target indices

    # Search for detections
    if pi.shape[0]:
        # Prediction to target ious
        ious, i = box_iou(pred[pi, :4], tbox[ti]).max(1)
        #预测的坐标和图片目标坐标求IOU
        # best ious, indices

        # Append detections
        #iouv: tensor([0.50000], device='cuda:0')
        for j in (ious > iouv[0]).nonzero():
            d = ti[i[j]]  # detected target
            if d not in detected:
                detected.append(d)
                correct[pi[j]] = ious[j] > iouv  # iou_thres is 1xn
                #计算真阳性,目标里面有行人,实际也是有行人
                if len(detected) == nl:  # all targets already located in image
                    break

我只有行人一个类别,假如预测了3个行人,实际只有1个,那么correct可能就是[0,0,1]

(9)接下来将真阳性TP/预测的置信度/预测的cls/目标的cls组合在一起。

stats.append((correct.cpu(), pred[:, 4].cpu(), pred[:, 5].cpu(), tcls)) 

10遍历完所有的类别之后,将stats的结果全部叠加在一起变成Numpy。

stats = [np.concatenate(x, 0) for x in zip(*stats)] 

11接下来计算每一个类别的AP值。

p, r, ap, f1, ap_class = ap_per_class(*stats)
def ap_per_class(tp, conf, pred_cls, target_cls):
    # 分别是真阳性TP/预测的置信度/预测的cls/目标的cls
    #比如这张图片经过非极大值抑制之后只剩下3个,实际上有2个
    #[true true false]/[o1,o2,o3]/[0, 0, 0],[0,0]
    #tp为0表示负样本框,为1表示正样本框
    # Sort by objectness
    #按照置信度降序排列返回数据对应的索引
    i = np.argsort(-conf)
    tp, conf, pred_cls = tp[i], conf[i], pred_cls[i]
    # Find unique classes
    #对类别进行去重,因为计算AP是针对每类进行
    unique_classes = np.unique(target_cls)
    # Create Precision-Recall curve and compute AP for each class
    #为每个类创建Precision-Recall曲线并计算AP
    pr_score = 0.1  # score to evaluate P and R https://github.com/ultralytics/yolov3/issues/898
    s = [len(unique_classes), 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)
    for ci, c in enumerate(unique_classes):
        i = pred_cls == c #判断预测的类别中等于c类别的
        n_gt = (target_cls == c).sum()  # Number of ground truth objects
        #n_gt表示标签框gt中的c类别的数量
        n_p = i.sum()  # Number of predicted objects
        #n_p表示预测狂中c类别的框的数量

        if n_p == 0 or n_gt == 0:
            continue
        else:
            # Accumulate FPs and TPs
            #i列表记录着索引对应位置是否是c类别框
            #tpc列表记录着索引对应位置是否是正样本框
            #fpc记录着当预测框为ni的时候,有多上框是负样本框
            fpc = (1 - tp[i]).cumsum(0)
            tpc = tp[i].cumsum(0)
            #累加操作是便于后面计算

            # Recall
            #计算一系列的召回率,当模型预测1个box,两个box..
            #分别计算对应的召回率和精确度
            recall = tpc / (n_gt + 1e-16)  # recall curve
            r[ci] = np.interp(-pr_score, -conf[i], recall[:, 0])  # r at pr_score, negative x, xp because xp decreases

            # Precision
            precision = tpc / (tpc + fpc)  # precision curve
            p[ci] = np.interp(-pr_score, -conf[i], precision[:, 0])  # p at pr_score

            #从R-P曲线中计算出AP
            # AP from recall-precision curve
            for j in range(tp.shape[1]):
                ap[ci, j] = compute_ap(recall[:, j], precision[:, j])
    #计算F1
    # 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')

12下面是如何利用召回率和精确度的曲线计算AP。

def compute_ap(recall, precision):
    #利用召回率和精确度曲线获取AP
    # Append sentinel values to beginning and end
    mrec = np.concatenate(([0.], recall, [min(recall[-1] + 1E-3, 1.)]))
    mpre = np.concatenate(([0.], precision, [0.]))

    # Compute the precision envelope
    #将小于某元素前面的所有元素设置成该元素,如[11,3,5,8,6]
    #操作之后变成[11,8,8,8,6]
    #原因是对于每个召回率,我们要计算出对应的最大精确度
    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)
        ap = np.trapz(np.interp(x, mrec, mpre), x)  # integrate
    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

13除此之外,训练的过程中也计算了Loss,就是利用第6大步计算的。

---------------------------------------------------------------------------------------------

以上就是对u版YOLOv3验证(测试)过程代码的理解。:D

文章属于个人总结,如有错误之处,请评论指正,不胜感激。(?>ω<*?)

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表