val.py

在此声明:

  1. 咱们是按task=’test’来过这个val.py模块的。
  2. 咱们用的是自己的数据集:有3个类别,{0:HP,1:LP,2:NE};有124张测验集图片;batch_size这里设置为32;imgsz=640。

一、加载参数

def parse_opt():
    parser = argparse.ArgumentParser()  
    parser.add_argument('--data', type=str, default=ROOT / 'data/custom.yaml', help='dataset.yaml path')  
    parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'runs/2023.4.20exp/weights/best.pt', help='model path(s)')  
    parser.add_argument('--batch-size', type=int, default=1, help='batch size')  
    parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='inference size (pixels)')  
    parser.add_argument('--conf-thres', type=float, default=0.25, help='confidence threshold')  
    parser.add_argument('--iou-thres', type=float, default=0.40, help='NMS IoU threshold')  
    parser.add_argument('--max-det', type=int, default=90000, help='maximum detections per image')  
    #必定要注意这个task的参数,假如是train.py运转的,task='val',注意找对修正的地方;假如是直接运转val.py去测验,task='test'。写错了会代表着,练习数据\测验数据途径搞错,白忙活。
    parser.add_argument('--task', default='test', help='train, val, test, speed or study')  
    parser.add_argument('--device', default='1', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')  
    parser.add_argument('--workers', type=int, default=8, help='max dataloader workers (per RANK in DDP mode)')  
    parser.add_argument('--single-cls', action='store_true', help='treat as single-class dataset')  
    parser.add_argument('--augment', action='store_true', help='augmented inference')  
    parser.add_argument('--verbose', action='store_true', help='report mAP by class')  
    parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')  
    parser.add_argument('--save-hybrid', action='store_true', help='save label+prediction hybrid results to *.txt')  
    parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels')  
    parser.add_argument('--save-json', action='store_false', help='save a COCO-JSON results file')  
    parser.add_argument('--project', default=ROOT / 'runs/test/核阳三分类', help='save to project/name')  
    parser.add_argument('--name', default='exp', help='save to project/name')  
    parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')  
    parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference')
    parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference')  
    opt = parser.parse_args()  
    opt.data = check_yaml(opt.data) # check YAML  
    opt.save_json |= opt.data.endswith('coco.yaml')  
    opt.save_txt |= opt.save_hybrid  
    print_args(vars(opt))  
    return opt

二、准备测验模型和测验数据

def run():
    training = model is not None
    if training:  
        device, pt, jit, engine = next(model.parameters()).device, True, False, False 
        half &= device.type != 'cpu'  
        model.half() if half else model.float()  
    else: 
        #1. 选择设备。
        #选择设备是cpu\gpu\mps(apple的显卡)  
        device = select_device(device, batch_size=batch_size)
        #2. 创立测验存储途径。
        #每次运转模块的时候:
            #假如path=runs/test/核阳三分类/exp不存在则会创立。  
            #假如path存在,则会依次查找runs/test/核阳三分类/exp2是否存在,不存在则创立,存在则继续查找。
        save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) 
        #3. 创立测验存储文件。
        #假如save_txt为True,那么会创立测验存储文件save_dir:runs/test/核阳三分类/exp,并且在下面也会创立'labels'文件:runs/test/核阳三分类/exp/labels。
        #假如save_txt为False,那么只会创立测验存储文件save_dir:runs/test/核阳三分类/exp。
        (save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True)
        #4. 导入模型。
        model = DetectMultiBackend(weights, device=device, dnn=dnn, data=data, fp16=half)  
        #加载模型的数据:  
        # stride=32。pt文件。jit为pytorch的即时编译器,可用于优化模型的推理功能。engine为模型推理引擎。
        stride, pt, jit, engine = model.stride, model.pt, model.jit, model.engine
        #5. 查看输入图片巨细。
        #check_img_size会判别图画长宽尺度是不是32的倍数,若不是则warning提示,并改成32倍。 
        imgsz = check_img_size(imgsz, s=stride)
        ##############和推理有关,先不论################
        half = model.fp16 # FP16 supported on limited backends with CUDA  
        if engine:  
            batch_size = model.batch_size  
        else:  
            device = model.device  
            if not (pt or jit):  
                batch_size = 1 # export.py models default to batch-size 1  
                LOGGER.info(f'Forcing --batch-size 1 square inference (1,3,{imgsz},{imgsz}) for non-PyTorch models')
        ##############和推理有关,先不论################
        #6. 查看和生成数据。 
        #查看:  
        # 1) 查看data是否是zip,并解压  
        # 2) 查看data字典中的key,train、val、test是否存在  
        # 3) 查看data字典中names的key0,1,2是否为整型  
        # 4) 查看val的数据下载  
        #生成:  
        # 1) 将data.yaml读取为data字典  
        # 2) 将data[names]的value若是列表或元组则转化为字典类型  
        # 3) 将data[nc]赋值为data[names]的长度  
        # 4) 将data的train、val、test的途径变成绝对途径
        data = check_dataset(data)

三、装备测验参数

def run():
    if training:  
        ...
    else:
        ...
    #1. 装备基本参数
    #模型猜测时调用eval(),关闭BN层和dropout层。
    model.eval()  
    #这里是True/False  
    cuda = device.type != 'cpu'  
    #instance(obj,str)来查看方针obj的类型是不是str类型  
    is_coco = isinstance(data.get('val'), str) and data['val'].endswith(f'coco{os.sep}val2017.txt') # COCO dataset  
    nc = 1 if single_cls else int(data['nc']) # number of classes  
    #生成tensor序列:  
    #iouv=tensor([0.5000, 0.5500, 0.6000, 0.6500, 0.7000, 0.7500, 0.8000, 0.8500, 0.9000, 0.9500])  
    iouv = torch.linspace(0.5, 0.95, 10, device=device) # iou vector for mAP@0.5:0.95  
    #用于回来一个 Tensor 中元素的总数  
    #niou=10  
    niou = iouv.numel() 
    #2. 数据加载器。
    #假如是练习,则不需求生成Dataloader
    if not training:  
        if pt and not single_cls: # check --weights are trained on --data  
            ncm = model.model.nc  
        model.warmup(imgsz=(1 if pt else batch_size, 3, imgsz, imgsz)) # warmup  
        pad, rect = (0.0, False) if task == 'speed' else (0.5, pt) # square inference for benchmarks  
        #必定要注意这个task的参数,假如是train.py运转的,task='val',注意找对修正的地方;假如是直接运转val.py去测验,task='test'。写错了会代表着,练习\测验途径搞错,白忙活。
        task = task if task in ('train', 'val', 'test') else 'val' # path to train/val/test images  
        dataloader = create_dataloader(data[task],  
                                            imgsz,  
                                            batch_size,  
                                            stride,  
                                            single_cls,  
                                            pad=pad,  
                                            rect=rect,  
                                            workers=workers,  
                                            prefix=colorstr(f'{task}: '))[0]  
    #3. 装备测验指标参数
    #seen用来计数:输入图片的总数
    seen = 0  
    confusion_matrix = ConfusionMatrix(nc=nc)  
    #print("names",names) names {0: 'HP', 1: 'Lp', 2: 'NE'}  
    names = model.names if hasattr(model, 'names') else model.module.names # get class names  
    if isinstance(names, (list, tuple)): # old format  
        names = dict(enumerate(names))  
    class_map = coco80_to_coco91_class() if is_coco else list(range(1000))  
    s = ('%22s' + '%11s' * 6) % ('Class', 'Images', 'Instances', 'P', 'R', 'mAP50', 'mAP50-95')  
    tp, fp, p, r, f1, mp, mr, map50, ap50, map = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 
    dt = Profile(), Profile(), Profile() # profiling times  
    loss = torch.zeros(3, device=device)  
    #初始化json文件中的字典、计算信息、AP
    jdict, stats, ap, ap_class = [], [], [], []  
    callbacks.run('on_val_start')  
    #tqdm加载dataloader时有一个很明显的体验:batchsize=1时是+1%加载的。batchsize=32时是+25%加载的。  
    #desc显现s的格式:Class Images Instances P R mAP50 mAP50-95  
    #bar_format这个默认格式供给了一些基本的信息,如进展百分比、已处理和总量、用时和剩余时间、处理速度等。  
    pbar = tqdm(dataloader, desc=s, bar_format=TQDM_BAR_FORMAT) # progress bar

四、测验过程

两个循环。

外循环拿出来每个batch:

if not training:
    ...
pbar = tqdm(dataloader, desc=s, bar_format=TQDM_BAR_FORMAT)
#你就把dataloader幻想成,打包好了的一麻袋一麻袋的数据。就等着迭代他们了。
#1) batch_i: 0,1,2,3。表明迭代batch的数量,124/32=4。
#2) im: shape=(batch_size,channel,height,width)。shape=(一个batch的数量,每张图片的通道数,高,宽),表明有一个batch的图片,每张图片各个通道的像素信息。
#3) targets: shape=(obj_nums,6)。shape=(一个batch图片实在方针数之和,6),表明一个batch的图片的一切方针,每个方针对应的[图片的0-31编号,每个方针的实在类别0/1/2,x_center,y_center,w,h]
#4) paths: 表明当前批次的图画途径,长度为batch_size的列表,其间每个元素是字符串类型的图画途径。
#5) shapes: shape=(batch_size,2)。shape=(一个batch的数量,2),表明一个batch的图片,每张图片的原始[宽,高],咱们这个数据会集有[640,640]、[1000,1000]。
for batch_i, (im, targets, paths, shapes) in enumerate(pbar):  
    callbacks.run('on_val_batch_start')  
    #1. 一个batch图片像素归一化。
    with dt[0]:  
        if cuda:  
            im = im.to(device, non_blocking=True)  
            targets = targets.to(device)  
        im = im.half() if half else im.float() # uint8 to fp16/32  
        #图画归一化
        im /= 255 # 0 - 255 to 0.0 - 1.0  
        nb, _, height, width = im.shape # batch size, channels, height, width  
    #2. 获取一个batch图片猜测框的信息。
    with dt[1]:
        #6) NMS前的preds:
        #len(preds[0]) = 32是一个batchsize的巨细
        #preds[0][0].shape=(27783,8)
        #27783=848434242321213 这是每张图片得到的大、中、小一切检测框的个数。
        #∴preds[0]表明一个batch的数量,每张图片一切猜测框,每个猜测框的[3类别条件概率信息,1置信度信息,x_center,y_center,w,h]
        #len(preds[1]) = 3 是head头的数量
        #preds[1][0].shape (32,3,84,84,8)  
        #preds[1][1].shape (32,3,42,42,8)  
        #preds[1][2].shape (32,3,21,21,8)
        #∴preds[1]表明大、中、小三种检测框,每种检测框的一个batch数量,每张图片的该种检测框信息。
        #注:到NMS这里你要注意,他会只需preds[0],也就是把一个batch,每张图片三种一同的框的信息拿到。 
        #那么咱们肯定就得不到每个检测头NMS留下的框,只能得到三种一同,NMS后的框。
        preds, train_out = model(im) if compute_loss else (model(im, augment=augment), None)
    if compute_loss:  
        loss += compute_loss(train_out, targets)[1] # box, obj, cls
    #这行代码是将方针框的(x,y,w,h)=(x, y, w, h)(w, h, w, h) 将鸿沟框坐标从份额转换为像素值。
    targets[:, 2:] *= torch.tensor((width, height, width, height), device=device) 
    #是否参加主动标示,假如save_hybrid设置为True,则代表进行主动标示。
    lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else [] 
    #3. 对一个batch的图片NMS操作,得到一个batch经NMS后的猜测框信息。
    with dt[2]:
        #7) NMS后的preds:
        #preds 由32个数组组成,preds[0],preds[1]...preds[31]。每个preds[]里边shape都是[一张图片猜测方针数,6]  
        #这就阐明,preds是当每一个batch来临,对每一张图片的方针框NMS处理之后的方针框信息[x1, y1, x2, y2, conf, cls]。
        #for si, pred in enumerate(preds) 中每次迭代的pred和这里的preds[0],preds[1]...preds[31]是一模一样的。
        preds = non_max_suppression(preds,  
                conf_thres,  
                iou_thres,  
                labels=lb,  
                multi_label=False,  
                agnostic=single_cls,  
                max_det=max_det)              

内循环分析一个batch中每张图片:

for batch_i, (im, targets, paths, shapes) in enumerate(pbar):
    ...
    #1) si:0,1,2...31。表明一个batch每张图片的索引
    #2) pred:shape=(一张图片猜测方针数,6)。表明一张图片的一切猜测方针框,每个方针框的信息[x1, y1, x2, y2, conf, cls]。
    for si, pred in enumerate(preds):
        #labels: 将图片索引为si的一切实在方针的[cls, x, y, w, h] 取出来。shape=(实在方针数,5)。
        labels = targets[targets[:, 0] == si, 1:]
        #nl: 索引为si的图片实在方针数
        #npr: 索引为si的图片猜测方针数
        nl, npr = labels.shape[0], pred.shape[0]
        #path为索引为si的图片途径,由字符串类型变成了Path类型。 
        #shape为索引为si的图片宽高[640,640]
        path, shape = Path(paths[si]), shapes[si][0]  
        #初始化 shape=(每张图片的猜测方针数,10)的元素为0的tensor向量  
        correct = torch.zeros(npr, niou, dtype=torch.bool, device=device) # init   
        seen += 1
        if npr == 0:  
            if nl:  
                stats.append((correct, *torch.zeros((2, 0), device=device), labels[:, 0]))  
                if plots:  
                    confusion_matrix.process_batch(detections=None, labels=labels[:, 0])  
            continue  
        # Predictions  
        if single_cls:  
            pred[:, 5] = 0  
        predn = pred.clone()  
        #这个scale_boxes我考虑了半天是做什么的:
            #1) 缩放尺度im[si].shape[1:]=[672,672] 表明缩放调整过的图片尺度  
            #2) predn[:, :4]=[一张图片的一切方针数,x1, y1, x2, y2] 表明调整过的图片上猜测框坐标  
            #3) 原图shape=[640,640]/[1000,1000] shape表明原图的尺度  
            #4) 从缩放调整过的图片上的猜测方针坐标(xyxy)映射到原图上对应处坐标。
            #注:假如函数中传递的是数组,那么原数组上是会改变的。∴predn[:, :4]通过scale_boxes会改变的。
        scale_boxes(im[si].shape[1:], predn[:, :4], shape, shapes[si][1]) # native-space pred  
        # Evaluate  
        if nl:  
            #将图片索引为si的一切实在方针的[x, y, w, h]转化成[x1, y1, x2, y2],仍然是像素等级。
            tbox = xywh2xyxy(labels[:, 1:5]) # target boxes  
            #从缩放的图片上的实在方针坐标(xyxy)映射到原图上对应处坐标。
            scale_boxes(im[si].shape[1:], tbox, shape, shapes[si][1]) # native-space labels 
            #labelsn 为图片索引为si的一切实在方针的[cls, x1, y1, x2, y2]  
            labelsn = torch.cat((labels[:, 0:1], tbox), 1) # native-space labels  
            #predn 为猜测的[x1, y1, x2, y2, conf, cls]  
            #labelsn 为实在的[cls, x1, y1, x2, y2] 
            #process_batch的作用是:  
                #核算不同阈值下,猜测方针框和实在方针框之间(xyxy)的IoU阈值和类别匹配,若匹配则将correct中相应方位设为True。  
                #correct的shape=(每张图片的猜测方针数,10),对每个猜测方针框,10个不同阈值下的匹配True/False。
            correct = process_batch(predn, labelsn, iouv)  
            if plots:  
                confusion_matrix.process_batch(predn, labelsn)  
        #stats是一个列表[(correct、猜测置信度、猜测类别、实在类别),()...()] 里边一个元组是一张图片 
        #(correct, pred[:, 4], pred[:, 5], labels[:, 0])作为一个元组  
        stats.append((correct, pred[:, 4], pred[:, 5], labels[:, 0])) # (correct, conf, pcls, tcls)  
        # Save/log  
        if save_txt:  
            save_one_txt(predn, save_conf, shape, file=save_dir / 'labels' / f'{path.stem}.txt')  
        if save_json:  
            save_one_json(predn, jdict, path, class_map) # append to COCO-JSON dictionary  
        callbacks.run('on_val_image_end', pred, predn, path, names, im[si])
    # Plot images
    ...

xywh2xyxy(labels[:, 1:5])

def xywh2xyxy(x):
    # Convert nx4 boxes from [x, y, w, h] to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right  
    y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)  
    y[:, 0] = x[:, 0] - x[:, 2] / 2 # top left x  
    y[:, 1] = x[:, 1] - x[:, 3] / 2 # top left y  
    y[:, 2] = x[:, 0] + x[:, 2] / 2 # bottom right x  
    y[:, 3] = x[:, 1] + x[:, 3] / 2 # bottom right y  
    return y

这其实并不难理解:

20. YOLOv5源码val.py解读

correct = process_batch(predn, labelsn, iouv)

def process_batch(detections, labels, iouv):
    """  
    Return correct prediction matrix  
    Arguments:  
    detections (array[N, 6]), x1, y1, x2, y2, conf, class N是猜测方针数  
    labels (array[M, 5]), class, x1, y1, x2, y2 M是实在方针数  
    Returns:  
    correct (array[N, 10]), for 10 IoU levels N是猜测方针数  
    """  
    correct = np.zeros((detections.shape[0], iouv.shape[0])).astype(bool)  
    #核算p_box和GT_box的IOU  
    iou = box_iou(labels[:, 1:], detections[:, :4])  
    #判别类别是否共同  
    correct_class = labels[:, 0:1] == detections[:, 5]  
    #针对每个IOU阈值,遍历一切猜测框和实在框,对于满意IOU阈值要求且类别匹配的猜测框和实在框,将correct中相应方位设为True。  
    for i in range(len(iouv)):  
        x = torch.where((iou >= iouv[i]) & correct_class) # IoU > threshold and classes match  
        if x[0].shape[0]:  
            matches = torch.cat((torch.stack(x, 1), iou[x[0], x[1]][:, None]), 1).cpu().numpy() # [label, detect, iou]  
            if x[0].shape[0] > 1:  
                matches = matches[matches[:, 2].argsort()[::-1]]  
                matches = matches[np.unique(matches[:, 1], return_index=True)[1]]   
                matches = matches[np.unique(matches[:, 0], return_index=True)[1]]  
            correct[matches[:, 1].astype(int), i] = True  
    return torch.tensor(correct, dtype=torch.bool, device=iouv.device)

五、评价测验成果

for:
    ...
    for:
    ...
    # Plot images
# Compute metrics 
# 里边是一个数据会集各个图片的((correct,correct...)、(猜测置信度,猜测置信度...)、(猜测类别,猜测类别...)、(实在类别,实在类别...)) 
stats = [torch.cat(x, 0).cpu().numpy() for x in zip(*stats)] # to numpy  
#stats[0].any():stats[0]是否全为False,是则回来False,假如有一个为True,则回来True。
if len(stats) and stats[0].any(): 
    #核算每个类别的指标
    tp, fp, p, r, f1, ap, ap_class = ap_per_class(*stats, plot=plots, save_dir=save_dir, names=names)  
    #核算每个类别的 AP@0.5, AP@0.5:0.95
    ap50, ap = ap[:, 0], ap.mean(1) # AP@0.5, AP@0.5:0.95  
    #ap50, ap对类别做个均匀,就成了map50,map
    mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean() 
#计算整个数据集的一切实在方针数
nt = np.bincount(stats[3].astype(int), minlength=nc) # number of targets per class  
# Print results  
pf = '%22s' + '%11i' * 2 + '%11.3g' * 4 # print format  
#打印一切类的均匀成果  
LOGGER.info(pf % ('all', seen, nt.sum(), mp, mr, map50, map))  
if nt.sum() == 0:  
    LOGGER.warning(f'WARNING ⚠️ no labels found in {task} set, can not compute metrics without labels')  
# Print results per class  
if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats):  
    for i, c in enumerate(ap_class):  
        #打印每个类的成果  
        LOGGER.info(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i]))  
# Print speeds  
...
# Plots
...
# Save JSON
...
# Return results  
model.float() # for training  
if not training:  
    s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else ''  
    LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}")  
maps = np.zeros(nc) + map  
for i, c in enumerate(ap_class):  
    maps[c] = ap[i]  
return (mp, mr, map50, map, *(loss.cpu() / len(dataloader)).tolist()), maps, t