在经过漫长的训练之后,就可以对模型进行测试(验证)啦!验证集的作用不容小觑,我们不仅可以在验证集中监控模型训练是否过拟合,还可以用验证集来调整超参数(迭代次数,学习率等等),从而使模型表现越来越好。现在就来看一下这个脚本文件都执行了哪些操作吧~
本文主要借鉴了以下两篇文章:
1.YOLOv5-6.x源码分析(二)---- val.py_JehanRio的博客-CSDN博客
2.YOLOv5源码解读1.4-测试test.py_yolov5test-CSDN博客
1.获取文件路径
FILE = Path(__file__).resolve() #获取当前文件的绝对路径,D://yolov5/val.py
ROOT = FILE.parents[0] # YOLOv5 root directory,当前文件的父目录(上一级目录),D://yolov5/
if str(ROOT) not in sys.path:
sys.path.append(str(ROOT)) # add ROOT to PATH,把root添加到运行路径
ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative,将root设置为相对路径
2.存储预测信息为.txt文件
def save_one_txt(predn, save_conf, shape, file):
# Save one txt result
gn = torch.tensor(shape)[[1, 0, 1, 0]] # normalization gain whwh,gn = [w, h, w, h] 对应图片的宽高 用于后面归一化
for *xyxy, conf, cls in predn.tolist():# tolist:变为列表
xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh,将左上角和右下角的xyxy格式转为xywh(中心点位置+宽高)格式,并归一化,转化为列表再保存
line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format,若save_conf为true,则line的形式是:"类别 xywh 置信度",否则line的形式是: "类别 xywh",
with open(file, 'a') as f:
f.write(('%g ' * len(line)).rstrip() % line + '\n') # 写入对应的文件夹里,路径默认为“runs\detect\exp*\labels”
3.存储预测信息为coco格式的.json文件
def save_one_json(predn, jdict, path, class_map):
# Save one JSON result {"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236}
image_id = int(path.stem) if path.stem.isnumeric() else path.stem#获取图片ID
box = xyxy2xywh(predn[:, :4]) # xywh,转换为中心点坐标和宽、高的形式
box[:, :2] -= box[:, 2:] / 2 # xy center to top-left corner
for p, b in zip(predn.tolist(), box.tolist()):
jdict.append({
'image_id': image_id, #图片ID
'category_id': class_map[int(p[5])], #类别
'bbox': [round(x, 3) for x in b], #预测框位置
'score': round(p[4], 5)}) #预测得分
- 注意:之前的的xyxy格式是左上角右下角坐标 ,xywh是中心的坐标和宽高,而coco的json格式的框坐标是xywh(左上角坐标 + 宽高),所以 box[:, :2] -= box[:, 2:] / 2 这行代码是将中心点坐标 -> 左上角坐标
- zip():每次从predn.tolist()和box.tolist()里各拿一个组成新的元组,分别赋值给p,b
4.匹配预测框,获取IOU信息
这段函数的主要作用:
- 对预测框与gt进行匹配。一个gt,需要取置信度最高的预测框与之匹配;同样的,一个预测框要分配iou值最高的gt。
- 对匹配上的预测框进行iou数值判断,设置了iou从0.5-0.95的10个梯度,如果匹配的预测框iou大于相对于的阈值,则在对应位置设置为True,否则设置为False;而对于没有匹配上的预测框全部设置为False。
def process_batch(detections, labels, iouv):
"""
Return correct prediction matrix
Arguments:
detections (array[N, 6]), x1, y1, x2, y2, conf, class
labels (array[M, 5]), class, x1, y1, x2, y2
Returns:
correct (array[N, 10]), for 10 IoU levels
"""
correct = np.zeros((detections.shape[0], iouv.shape[0])).astype(bool)# 构建一个[pred_nums, 10]全为False的矩阵
iou = box_iou(labels[:, 1:], detections[:, :4])# 计算每个gt与每个pred的iou,shape为: [gt_nums, pred_nums]
correct_class = labels[:, 0:1] == detections[:, 5] #分类正确的
for i in range(len(iouv)):
x = torch.where((iou >= iouv[i]) & correct_class) # IoU > threshold and classes match,iou超过阈值且类别正确,返回索引
if x[0].shape[0]:#存在符合条件的anchor
matches = torch.cat((torch.stack(x, 1), iou[x[0], x[1]][:, None]), 1).cpu().numpy() ## 将符合条件的位置构建成一个新的矩阵,第一列是gt索引,第二列是预测框索引,第三列是iou值: [label, detect, iou]
if x[0].shape[0] > 1:
#当一个预测框与多个gt相对应时,取iou最大的一个
matches = matches[matches[:, 2].argsort()[::-1]]# argsort获得有小到大排序的索引, [::-1]相当于取反reserve操作,变成由大到小排序的索引,对matches矩阵进行排序
matches = matches[np.unique(matches[:, 1], return_index=True)[1]] #表示按照第二列进行去重,保留第一个出现的行(iou值最大的)
#当一个gt与多个预测框相对应时,取iou最大的一个
# matches = matches[matches[:, 2].argsort()[::-1]]
matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
correct[matches[:, 1].astype(int), i] = True #将预测框与真实框进行匹配,匹配正确的结果记为true
return torch.tensor(correct, dtype=torch.bool, device=iouv.device)
-
matches = matches[np.unique(matches[:, 1], return_index=True)[1]]:这段代码的作用是对matches数组按照第二列进行去重,保留第一个出现的行。具体来说,np.unique(matches[:, 1], return_index=True)返回一个元组,第一个元素是去重后的数组,第二个元素是去重后的数组中每个元素在原数组中第一次出现的下标。matches[:, 1]表示取matches数组的第二列,return_index=True表示返回去重后的数组和每个元素在原数组中第一次出现的下标。表示取返回元组的第二个元素,即每个元素在原数组中第一次出现的下标。最终,matches = matches[np.unique(matches[:, 1], return_index=True)]表示按照第二列进行去重,保留第一个出现的行。
5.run函数
5.1.配置参数
@smart_inference_mode()
def run(data, # 数据集配置文件地址 包含数据集的路径、类别个数、类名、下载地址等信息 train.py时传入data_dict
weights=None, # 模型的权重文件地址 运行train.py=None 运行test.py=默认weights/yolov5s
batch_size=32, # 前向传播的批次大小 运行test.py传入默认32 运行train.py则传入batch_size // WORLD_SIZE * 2
imgsz=640, # 输入网络的图片分辨率 运行test.py传入默认640 运行train.py则传入imgsz_test
conf_thres=0.001, # object置信度阈值 默认0.001
iou_thres=0.6, # 进行NMS时IOU的阈值 默认0.6
task='val', # 设置测试的类型 有train, val, test, speed or study几种 默认val
device='', # 执行 val.py 所在的设备 cuda device, i.e. 0 or 0,1,2,3 or cpu
single_cls=False, # 数据集是否只有一个类别 默认False
augment=False, # 测试时增强
verbose=False, # 是否打印出每个类别的mAP 运行test.py传入默认Fasle 运行train.py则传入nc < 50 and final_epoch
save_txt=False, # 是否以txt文件的形式保存模型预测框的坐标 默认True
save_hybrid=False, # 是否保存预测每个目标的置信度到预测txt文件中 默认True
save_conf=False, # 保存置信度
save_json=False, # 是否按照coco的json格式保存预测框,并且使用cocoapi做评估(需要同样coco的json格式的标签),
#运行test.py传入默认Fasle 运行train.py则传入is_coco and final_epoch(一般也是False)
project=ROOT / 'runs/val', # 验证结果保存的根目录 默认是 runs/val
name='exp', # 验证结果保存的目录 默认是exp 最终: runs/val/exp
exist_ok=False, # 如果文件存在就increment name,不存在就新建 默认False(默认文件都是不存在的)
half=True, # 使用 FP16 的半精度推理
dnn=False, # 在 ONNX 推理时使用 OpenCV DNN 后段端
model=None, # 如果执行val.py就为None 如果执行train.py就会传入( model=attempt_load(f, device).half() )
dataloader=None, # 数据加载器 如果执行val.py就为None 如果执行train.py就会传入testloader
save_dir=Path(''), # 文件保存路径 如果执行val.py就为‘’ , 如果执行train.py就会传入save_dir(runs/train/expn)
plots=True, # 是否可视化 运行val.py传入,默认True
callbacks=Callbacks(), # 回调函数
compute_loss=None, # 损失函数 运行val.py传入默认None 运行train.py则传入compute_loss(train)
):
5.2.初始化模型参数
# Initialize/load model and set device
# 判断是否是训练时调用run函数(执行train.py脚本), 如果是就使用训练时的设备 一般都是train
training = model is not None
if training: # called by train.py
device, pt, jit, engine = next(model.parameters()).device, True, False, False # get model device, PyTorch model
half &= device.type != 'cpu' # half precision only supported on CUDA
model.half() if half else model.float() #如果采用GPU,则将模型由32位浮点数转换为16位浮点数
else: # called directly
device = select_device(device, batch_size=batch_size)# 调用torch_utils中的select_device选择执行程序时的设备
# Directories
save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run,生成save_dir文件路径 run\test\expn
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir
# Load model
model = DetectMultiBackend(weights, device=device, dnn=dnn, data=data, fp16=half)
stride, pt, jit, engine = model.stride, model.pt, model.jit, model.engine
imgsz = check_img_size(imgsz, s=stride) # check image size检查图像分辨率能否被(32)整除
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')
# Data
data = check_dataset(data) # check
- (save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True):这行代码是在检查是否需要保存标签文件,如果需要则在指定的目录下创建一个名为"labels"的文件夹,如果不需要则在指定的目录下创建一个与项目名称相同的文件夹。其中,save_dir是指定的保存目录,name是项目名称,save_txt是一个布尔值,表示是否需要保存标签文件,exist_ok是一个布尔值,表示如果目录已经存在是否覆盖。mkdir函数是用来创建目录的,parents=True表示如果父目录不存在也会一并创建,exist_ok=True表示如果目录已经存在则不会报错。
5.3.加载数据的.yaml配置文件信息
# Configure
model.eval()# 启动模型验证模式;不启用 Batch Normalization 和 Dropout。 在eval模式下不会进行反向传播。
cuda = device.type != 'cpu'
is_coco = isinstance(data.get('val'), str) and data['val'].endswith(f'coco{os.sep}val2017.txt') # COCO dataset判断使用的是否为coco数据集
nc = 1 if single_cls else int(data['nc']) # number of classes 类别数量
iouv = torch.linspace(0.5, 0.95, 10, device=device) # iou vector for mAP@0.5:0.95,返回一个1维张量,包含在区间0.5和0.95上均匀间隔的10个点。
niou = iouv.numel()# numel为pytorch预置函数 用来获取张量中的元素个数
- iouv: [0.50000, 0.55000, 0.60000, 0.65000, 0.70000, 0.75000, 0.80000, 0.85000, 0.90000, 0.95000]
5.4.加载验证集
# Dataloader
#如果是训练的时候调用 ,则不需要这段代码。如果是val调用,则需要用create_dataloader创建数据集。
if not training:
if pt and not single_cls: # check --weights are trained on --data
ncm = model.model.nc
assert ncm == nc, f'{weights} ({ncm} classes) trained on different --data than what you passed ({nc} ' \
f'classes). Pass correct combination of --weights and --data that are trained together.'
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 = task if task in ('train', 'val', 'test') else 'val' # path to train/val/test images
#调用dataset.py中的create_dataloader创建dataloader
dataloader = create_dataloader(data[task],
imgsz,
batch_size,
stride,
single_cls,
pad=pad,
rect=rect,
workers=workers,
prefix=colorstr(f'{task}: '))[0]
5.5.初始化
seen = 0 #用来存储已完成测试图片的数量
confusion_matrix = ConfusionMatrix(nc=nc)# 初始化混淆矩阵
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))# 获取coco数据集的类别索引
s = ('%22s' + '%11s' * 6) % ('Class', 'Images', 'Instances', 'P', 'R', 'mAP50', 'mAP50-95')# 设置tqdm进度条显示信息
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) #初始化网络训练loss
jdict, stats, ap, ap_class = [], [], [], [] #初始化json文件中的字典 统计信息、ap
callbacks.run('on_val_start')
pbar = tqdm(dataloader, desc=s, bar_format=TQDM_BAR_FORMAT) # progress bar
5.6.开始验证
准备工作做好了,接下来就可以开始验证啦
验证前预处理
for batch_i, (im, targets, paths, shapes) in enumerate(pbar):
callbacks.run('on_val_batch_start')
with dt[0]: #计算运行时间
if cuda:
im = im.to(device, non_blocking=True) #将图片拷贝到GPU上
targets = targets.to(device)#将标签拷贝到GPU上
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
前向推理
# Inference
with dt[1]: #计算运行时间
preds, train_out = model(im) if compute_loss else (model(im, augment=augment), None)
- out: 推理结果。1个 ,[bs, anchor_num*grid_w*grid_h, xywh+c+20classes] = [1, 3*80*80+3*40*40+3*20*20,25] = [1,19200+4800+1200,25] = [1,25200,25]
- train_out: 训练结果。3个, [bs, anchor_num, grid_w, grid_h, xywh+c+20classes]。如: [1, 3, 80, 80, 25] [1, 3, 40, 40, 25] [1, 3, 20, 20, 25]
计算损失
# Loss
if compute_loss: #在运行train.py时为空
loss += compute_loss(train_out, targets)[1] # box, obj, cls
去除冗余预测框
# NMS
targets[:, 2:] *= torch.tensor((width, height, width, height), device=device) # to pixels 将真实框target的xywh(因为target是在labelimg中做了归一化的)映射到img(test)尺寸
lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else [] # for autolabelling,提取batch中每一张图片的label
with dt[2]: #计算代码块运行时间
preds = non_max_suppression(preds, #非极大值抑制操作
conf_thres,
iou_thres,
labels=lb,
multi_label=True,
agnostic=single_cls,
max_det=max_det)
- lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else []:这是一段Python代码,其中包含了一个列表推导式。该列表推导式的作用是将一个二维数组targets按照第一列的不同取值进行分组,将每组中第一列以外的元素作为一个子数组存入一个新的列表lb中。如果save_hybrid为False,则lb为空列表。请注意,该代码中的nb和save_hybrid变量并未给出,需要在上下文中寻找。
统计真实框、预测框信息
# Metrics
for si, pred in enumerate(preds):# si代表第i张图片 pred是对图片预测的label信息
labels = targets[targets[:, 0] == si, 1:]# 获取第si张图片的gt标签信息 包括 class x y w h
nl, npr = labels.shape[0], pred.shape[0] # number of labels, predictions,标签个数和检测到的目标个数
path, shape = Path(paths[si]), shapes[si][0] #第si张图片的文件路径
correct = torch.zeros(npr, niou, dtype=torch.bool, device=device) # init
seen += 1 #统计图片数量
if npr == 0:
if nl:# 预测为空的同时有label信息
# stats初始化为一个空列表[] 此处廷加一个空信息
# 添加的每一个元素均为tuple 第二个第三个为一个空的张量tensor
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() #对pred进行深复刻
scale_boxes(im[si].shape[1:], predn[:, :4], shape, shapes[si][1]) # native-space pred,调整图片大小为原尺寸
# Evaluate
if nl:
tbox = xywh2xyxy(labels[:, 1:5]) # target boxes,转换为xyxy格式
scale_boxes(im[si].shape[1:], tbox, shape, shapes[si][1]) # native-space labels,调整为原图大小
labelsn = torch.cat((labels[:, 0:1], tbox), 1) # native-space labels#重新构建成(cls xyxy)的格式
correct = process_batch(predn, labelsn, iouv)# 对当前预测框与gt进行匹配,并在预测框的位子上获取iou的信息评分,其余没有匹配的为False
if plots:
confusion_matrix.process_batch(predn, labelsn) #计算混淆矩阵
stats.append((correct, pred[:, 4], pred[:, 5], labels[:, 0])) # (correct, conf, pcls, tcls),将结果统计到stats里
# Save/log 输出保存日志
if save_txt: #保存预测信息到txt文件中
save_one_txt(predn, save_conf, shape, file=save_dir / 'labels' / f'{path.stem}.txt')
if save_json: #保存预测信息到.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])
- txt文件保存的预测信息:cls+xywh+conf
- jdict字典保存的预测信息:image_id + category_id + bbox + score
绘制结果
# Plot images
if plots and batch_i < 3: #只绘制前三个batch
plot_images(im, targets, paths, save_dir / f'val_batch{batch_i}_labels.jpg', names) # labels,绘制真实框
plot_images(im, output_to_target(preds), paths, save_dir / f'val_batch{batch_i}_pred.jpg', names) # pred,绘制预测框
callbacks.run('on_val_batch_end', batch_i, im, targets, paths, shapes, preds)
5.7.计算mAP
# Compute metrics
stats = [torch.cat(x, 0).cpu().numpy() for x in zip(*stats)] # to numpy
if len(stats) and stats[0].any():
# stats[0].any(): stats[0]是否全部为False, 是则返回 False, 如果有一个为 True, 则返回 True
tp, fp, p, r, f1, ap, ap_class = ap_per_class(*stats, plot=plots, save_dir=save_dir, names=names)
# 根据上面的统计预测结果计算p, r, ap, f1, ap_class(ap_per_class函数是计算每个类的mAP等指标的)等指标
# p: [nc] 最大平均f1时每个类别的precision
# r: [nc] 最大平均f1时每个类别的recall
# ap: [71, 10] 数据集每个类别在10个iou阈值下的mAP
# f1 [nc] 最大平均f1时每个类别的f1
# ap_class: [nc] 返回数据集中所有的类别index
ap50, ap = ap[:, 0], ap.mean(1) # AP@0.5, AP@0.5:0.95
mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean()
# mp: [1] 所有类别的平均precision(最大f1时)
# mr: [1] 所有类别的平均recall(最大f1时)
# map50: [1] 所有类别的平均mAP@0.5
# map: [1] 所有类别的平均mAP@0.5:0.95
nt = np.bincount(stats[3].astype(int), minlength=nc) # number of targets per class,统计整个数据集的gt框中数据集各个类别的个数
- stats(concat后): list{4} correct, conf, pcls, tcls 统计出的整个数据集的GT
- correct [img_sum, 10] 整个数据集所有图片中所有预测框在每一个iou条件下是否是TP [1905, 10]
- conf [img_sum] 整个数据集所有图片中所有预测框的conf [1905]
- pcls [img_sum] 整个数据集所有图片中所有预测框的类别 [1905]
- tcls [gt_sum] 整个数据集所有图片所有gt框的class [929]
5.8.打印结果
# Print results
pf = '%22s' + '%11i' * 2 + '%11.3g' * 4 # print format
LOGGER.info(pf % ('all', seen, nt.sum(), mp, mr, map50, map)),数据集图片数量 + 数据集gt框的数量 + 所有类别的平均precision + 所有类别的平均recall + 所有类别的平均mAP@0.5 + 所有类别的平均mAP@0.5:0.95
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]))#类别名 + 数据集图片数量 + 这个类别的gt框数量 + 这个类别的precision + 这个类别的recall + 这个类别的mAP@0.5 + 这个类别的mAP@0.5:0.95
# Print speeds,打印消耗时间
t = tuple(x.t / seen * 1E3 for x in dt) # speeds per image
if not training:
shape = (batch_size, 3, imgsz, imgsz)
LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {shape}' % t) #前向传播耗费的总时间、总时间、nms耗费总时间
5.9.保存验证结果
# Plots
if plots:
confusion_matrix.plot(save_dir=save_dir, names=list(names.values())) #混淆矩阵绘制
callbacks.run('on_val_end', nt, tp, fp, p, r, f1, ap, ap50, ap_class, confusion_matrix)#打印日志
# Save JSON,采用之前保存的json文件预测结果,通过coco的api评估指标
if save_json and len(jdict):
w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else '' # weights
anno_json = str(Path('../datasets/coco/annotations/instances_val2017.json')) # annotations,注释的json格式
if not os.path.exists(anno_json):
anno_json = os.path.join(data['path'], 'annotations', 'instances_val2017.json')
pred_json = str(save_dir / f'{w}_predictions.json') # predictions,预测的json格式
LOGGER.info(f'\nEvaluating pycocotools mAP... saving {pred_json}...') #打印coco的api评估各个指标并保存
with open(pred_json, 'w') as f:
json.dump(jdict, f) #将dict序列化为json
try: # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb
check_requirements('pycocotools>=2.0.6')
from pycocotools.coco import COCO #使用coco工具评测库
from pycocotools.cocoeval import COCOeval
anno = COCO(anno_json) # init annotations api
pred = anno.loadRes(pred_json) # init predictions api
eval = COCOeval(anno, pred, 'bbox') #创建评估器
if is_coco:
eval.params.imgIds = [int(Path(x).stem) for x in dataloader.dataset.im_files] # image IDs to evaluate
eval.evaluate()
eval.accumulate()
eval.summarize() #展示结果
map, map50 = eval.stats[:2] # update results (mAP@0.5:0.95, mAP@0.5)
except Exception as e:
LOGGER.info(f'pycocotools unable to run: {e}')
5.10.返回结果
# 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 #返回测试结果的各项指标
6.解析命令行参数
def parse_opt():
parser = argparse.ArgumentParser()
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path') #数据集配置文件地址
parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s.pt', help='model path(s)') #模型权重文件地址
parser.add_argument('--batch-size', type=int, default=32, 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.001, help='confidence threshold')
parser.add_argument('--iou-thres', type=float, default=0.6, help='NMS IoU threshold')#NMS的IOU阈值
parser.add_argument('--max-det', type=int, default=300, help='maximum detections per image') #每张图片最大检测框数量
parser.add_argument('--task', default='val', help='train, val, test, speed or study')#设置测试类型
parser.add_argument('--device', default='', 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') #是否采用TTA
parser.add_argument('--verbose', action='store_true', help='report mAP by class') #是否打印出每个类别的map
parser.add_argument('--save-txt', action='store_true', help='save results to *.txt') #是否将预测模型的框坐标保存为txt格式
parser.add_argument('--save-hybrid', action='store_true', help='save label+prediction hybrid results to *.txt')# 保存label+prediction杂交结果到对应.txt
parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels')
parser.add_argument('--save-json', action='store_true', help='save a COCO-JSON results file')
parser.add_argument('--project', default=ROOT / 'runs/val', help='save to project/name') #保存源文件
parser.add_argument('--name', default='exp', help='save to project/name') #保存的文件地址:runs/val/exp
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')# 是否存在当前文件 默认False 一般是 no exist-ok 连用 所以一般都要重新创建文件夹
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')# 是否使用 OpenCV DNN对ONNX 模型推理
opt = parser.parse_args() #解析上述参数
opt.data = check_yaml(opt.data) # check YAML
opt.save_json |= opt.data.endswith('coco.yaml') # |=:左右两个变量有一个为true,左边变量就赋值为true
opt.save_txt |= opt.save_hybrid
print_args(vars(opt))
return opt
- TTA:测试时数据增强,测试时将原始数据做不同形式的增强,然后取结果的平均值作为最终结果。可以进一步提升最终结果的精度,但是TTA时用的数据增强方法一般都比较简单,比如水平翻转,垂直翻转,90度旋转等。
- OpenCV DNN:在目标检测和图像分割应用中,对于实时视频图像进行模型推理的过程中可以获得很好的处理帧速(FPS)。通过DNN模块使用特定框架下预训练模型经常会得到更高的FPS。在OpenCV DNN 中不能够直接使用来自于Keras, Pytorch学习框架中的网络模型。通常将这些模型转换成ONNX的格式(Open Neural Network Exchange),在OpenCV DNN中我们需要后缀为 .onnx 的权重参数文件来载入 ONNX 模型。
7.main函数
def main(opt):
check_requirements(ROOT / 'requirements.txt', exclude=('tensorboard', 'thop')) #检查环境是否配置好了
if opt.task in ('train', 'val', 'test'): # run normally
if opt.conf_thres > 0.001: # https://github.com/ultralytics/yolov5/issues/1466
LOGGER.info(f'WARNING ⚠️ confidence threshold {opt.conf_thres} > 0.001 produces invalid results')
if opt.save_hybrid:
LOGGER.info('WARNING ⚠️ --save-hybrid will return high mAP from hybrid labels, not from predictions alone')
run(**vars(opt))
else:
weights = opt.weights if isinstance(opt.weights, list) else [opt.weights]
opt.half = torch.cuda.is_available() and opt.device != 'cpu' # FP16 for fastest results
if opt.task == 'speed': # speed benchmarks,测试yolov5系列和yolov3-spp各个模型的速度评估
# python val.py --task speed --data coco.yaml --batch 1 --weights yolov5n.pt yolov5s.pt...
opt.conf_thres, opt.iou_thres, opt.save_json = 0.25, 0.45, False
for opt.weights in weights:
run(**vars(opt), plots=False)
elif opt.task == 'study': # speed vs mAP benchmarks,评估yolov5系列和yolov3-spp各个模型在各个尺度下的指标并可视化
# python val.py --task study --data coco.yaml --iou 0.7 --weights yolov5n.pt yolov5s.pt...
for opt.weights in weights:
f = f'study_{Path(opt.data).stem}_{Path(opt.weights).stem}.txt' # filename to save to
x, y = list(range(256, 1536 + 128, 128)), [] # x axis (image sizes), y axis,x和y坐标
for opt.imgsz in x: # img-size
LOGGER.info(f'\nRunning {f} --imgsz {opt.imgsz}...')
r, _, t = run(**vars(opt), plots=False)
y.append(r + t) # results and times,返回相关结果和时间
np.savetxt(f, y, fmt='%10.4g') # save y
subprocess.run(['zip', '-r', 'study.zip', 'study_*.txt'])# 命令行执行命令将study文件进行压缩
plot_val_study(x=x) # plot,指标可视化
else:
raise NotImplementedError(f'--task {opt.task} not in ("train", "val", "test", "speed", "study")')
8.程序入口
if __name__ == '__main__':
opt = parse_opt()
main(opt)