注:本文中的代碼基於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
文章屬於個人總結,如有錯誤之處,請評論指正,不勝感激。(ฅ>ω<*ฅ)