u版pytorch的YOLOv3验证过程理解\(^o^)/
注:本文中的代码基于https://github.com/ultralytics/yolov3
这里的验证过程test是用于YOLOv3在训练过程中的每一个epoch观察:训练好的模型和权重在验证集上的mAP,从而计算检测精度AP。
---------------------------------------------------------------------------------------------
1、首先要加载一个epoch中训练好的model,其中包括整个model的网络结构和权重等。要把model设置成eval形式。关于model.train()和model.eval()主要是针对model在训练时和评价时不同的BatchNormalization和Dropout方法模式:在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_out和train_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_out是tuple元组类型,inf_out是torch.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_thres是IOU阈值。
上图是NMS的基本思路,对目标框进行NMS非极大值抑制主要是为了避免多个目标框重复预测同一个目标。
实际上在实现的时候NMS分为Hard-NMS(DIOU/OVERLAP/MERGE/BATCHED)和Soft-NMS。
(1)Hard-nms:一直删除相邻的同类别目标,对于密集目标的输出不友好。
(2)Soft-nms:改变其相邻同类别目标的置信度,后期通过置信度阈值进行过滤,适用于目标密集的场景
(3)Or-nms:Hard-nms的非官方实现形式,只支持CPU
(4)Vision-nms:Hard-nms的官方实现形式(C函数库),可以支持GPU,只支持单类别的输入
(5)Vision-batched-nms:Hard-nms的官方实现形式(C函数库),可以支持GPU,可支持多类别的输入
(6)And-nms:在Hard-nms的逻辑基础上,增加是不是单独框的限制,删除没有重叠框的框(减少误检)
(7)Merge-nms:在Hard-nms的基础上,增加保留框位置平滑策略(重叠框位置信息求解平均值,使得框的位置更加精确)
(8)Diou-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]
(1)对output进行遍历
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(真阳性:即预测的是行人,标签也是行人的个数)先将其初始化为全False,0
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
文章属于个人总结,如有错误之处,请评论指正,不胜感激。(ฅ>ω<*ฅ)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示