val.py模块参数详解

释放双眼,带上耳机,听听看~!
val.py模块参数详解,包括数据集、权重、批处理大小等参数的说明以及相关注意事项。

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. 选择设备。
        #选择设备是cpugpumps(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得到的是加载模型中的names,而不是读取的data中的names
    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:

def run():
    if training:  
        ...
    else:
        ...
    ...
    
    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的图片,每张图片各个通道的像素信息。每张图片被缩放到同一尺寸:height=width=672
    #3) targets: shape=(obj_nums,6)。
    #shape=(一个batch图片真实目标数之和,6),表示一个batch的图片的所有目标,每个目标对应的[图片的0-31编号,每个目标的真实类别0/1/2,x_center,y_center,w,h]
    #注:[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=84×84×3×42×42×3×21×21×3 这是每张图片得到的大、中、小所有检测框的个数。
            #∴preds[0]表示一个batch的数量,每张图片所有预测框,每个预测框的[3类别条件概率信息,1置信度信息,x_center,y_center,w,h]
            # 注:Anchor的[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数量,每张图片的该种检测框信息。

            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) 
        #将target从缩放图比例xywh → 缩放图像素xywh
        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]是一模一样的。
            
            # 将Anchor原图到特征图的坐标和尺寸xywh → Anchor原图到特征图的归一化值xyxy
            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我考虑了半天是做什么的:将预测框原图到特征图的归一化值xyxy → 原图像素xyxy  
            #1) 缩放尺寸im[si].shape[1:]=[672,672] 表示缩放调整过的图片尺寸  
            #2) predn[:, :4]=[一张图片的所有目标数,x1, y1, x2, y2] 表示调整过的图片上预测框坐标  
            #3) 原图shape=[640,640]/[1000,1000] 表示原图的尺寸  
        #注:如果函数中传递的是数组,那么原数组上是会改变的。∴predn[:, :4]经过scale_boxes会改变的。
        scale_boxes(im[si].shape[1:], predn[:, :4], shape, shapes[si][1]) # native-space pred  

        # Evaluate  
        if nl:  
            #将target从缩放图像素xywh → 缩放图像素xyxy
            tbox = xywh2xyxy(labels[:, 1:5]) # target boxes  
            #将target从缩放图像素xyxy → 原图像素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

这其实并不难理解:

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
本网站的内容主要来自互联网上的各种资源,仅供参考和信息分享之用,不代表本网站拥有相关版权或知识产权。如您认为内容侵犯您的权益,请联系我们,我们将尽快采取行动,包括删除或更正。
AI教程

OpenAI发布新产品Point·E,实现文本到3D点云的转换

2023-12-16 15:14:14

AI教程

FPN在one-stage检测器中的优势及YOLOF的性能评估

2023-12-16 15:27:14

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索