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. 选择设备。
#选择设备是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
这其实并不难理解:
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