释放双眼,带上耳机,听听看~!
val.py模块参数详解,包括数据集、权重、批处理大小等参数的说明以及相关注意事项。
val.py
在此声明:
- 我们是按task=’test’来过这个val.py模块的。
- 我们用的是自己的数据集:有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
这其实并不难理解:
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
本网站的内容主要来自互联网上的各种资源,仅供参考和信息分享之用,不代表本网站拥有相关版权或知识产权。如您认为内容侵犯您的权益,请联系我们,我们将尽快采取行动,包括删除或更正。