前言:Hello大家好,我是小哥谈。YOLOv5项目代码中,val.py 是一个代表验证(validation)的 Python 脚本文件名。通常在机器学习或深度学习的任务中,我们会将数据集分为训练集和验证集,使用训练集来训练模型,然后使用验证集来评估模型的性能。val.py 包含了验证集上的代码实现,用于评估模型的准确性、精度或其他指标。具体的内容和功能需要查看该文件的代码实现才能确定。🌈
前期回顾:
YOLOv5源码中的参数超详细解析(1)— 项目目录结构解析
YOLOv5源码中的参数超详细解析(2)— 配置文件yolov5s.yaml
YOLOv5源码中的参数超详细解析(3)— 训练部分train.py
YOLOv5源码中的参数超详细解析(4)— 推理部分detect.py
目录
🚀1.val.py介绍
🚀2.val.py代码详解
💥💥2.1 导包和基本配置
💞💞💞2.1.1 导入安装好的Python库
💞💞💞2.1.2 获取当前文件的绝对路径
💞💞💞2.1.3 加载自定义模块
💥💥2.2 保存信息
💞💞💞2.2.1 保存预测信息到txt文件
💞💞💞2.2.2 保存预测信息到coco格式的json字典
💥💥2.3 计算指标
💥💥2.4 执行run()函数
💞💞💞2.4.1 设置参数
💞💞💞2.4.2 初始化/加载模型以及设置设备
💞💞💞2.4.3 加载配置
💞💞💞2.4.4 加载val数据集
💞💞💞2.4.5 初始化
💞💞💞2.4.6 验证过程
💥💥2.5 设置opt参数
🚀3.总结
🚀1.val.py介绍
YOLOv5是一个目标检测算法,而val.py是YOLOv5项目目录结构中的一个脚本文件,用于进行模型验证(validation)操作。这个文件主要是在每一轮训练结束后,验证当前模型的mAP、混淆矩阵等指标。这个文件本来是叫 test.py 的,后来改成了val.py,该脚本主要用于评估模型在验证集上的性能表现,并生成相应的评估结果。🌴
具体来说主要分为如下几个步骤:
(1)加载模型和数据:val.py 会加载训练好的模型权重文件和用于验证的数据集。它会根据配置文件中的设置,加载模型架构和权重,并准备验证数据。
(2)图像预处理:在验证过程中,输入图像会被预处理以适应模型的要求。这包括调整图像大小、归一化像素值等操作,以确保输入图像符合模型的要求。
(3)模型推理:val.py 在验证集上运行模型进行推理。它会将预处理后的图像输入模型,得到模型对图像中目标的预测结果。
(3)后处理和评估:在模型推理完成后,val.py 会对模型的输出结果进行后处理。它会将模型输出的边界框进行解码和筛选,并计算预测结果的精度指标,如精确率、召回率、平均精确率等。
(4)输出结果和指标:val.py 会将评估结果和指标打印输出到控制台,以提供模型在验证集上的性能信息。这些信息可以用于评估模型的准确性和优化模型的参数和超参数。
通过运行val.py脚本,可以对已经训练好的YOLOv5模型在验证集上进行性能评估,以便更好地了解模型的表现和调整相关参数或网络结构。🍃
说明:♨️♨️♨️
train.py、detect.py 和 val.py 的区别❓❓❓
train.py:训练部分,读取数据集,加载模型并训练。
detect.py:推理部分,获取实际中最佳推理结果。
val.py:验证部分,获取当前数据集上的最佳验证结果。
🚀2.val.py代码详解
💥💥2.1 导包和基本配置
💞💞💞2.1.1 导入安装好的Python库
'''============导入安装好的python库=========='''
import argparse # 解析命令行参数的库
import json # 实现字典列表和JSON字符串之间的相互解析
import os # 与操作系统进行交互的文件库 包含文件路径操作与解析
import sys # sys系统模块 包含了与Python解释器和它的环境有关的函数
from pathlib import Path # Path将str转换为Path对象 使字符串路径易于操作的模块
from threading import Thread # python中处理多线程的库
import numpy as np # 矩阵计算基础库
import torch # pytorch 深度学习库
from tqdm import tqdm # 用于直观显示进度条的一个库
首先需要导入的是常用python库:
argparse:它是一个用于命令项选项与参数解析的模块,通过在程序中定义好我们需要的参数,argparse 将会从 sys.argv 中解析出这些参数,并自动生成帮助和使用信息。
json:实现字典列表和JSON字符串之间的相互解析。
os:它提供了多种操作系统的接口。通过os模块提供的操作系统接口,我们可以对操作系统里文件、终端、进程等进行操作。
sys:它是与python解释器交互的一个接口,该模块提供对解释器使用或维护的一些变量的访问和获取,它提供了许多函数和变量来处理 python 运行时环境的不同部分。
pathlib:这个库提供了一种面向对象的方式来与文件系统交互,可以让代码更简洁、更易读。
threading:python中处理多线程的库。
numpy:科学计算库,提供了矩阵、线性代数、傅里叶变换等的解决方案,最常用的是它的N维数组对象。
torch:这是主要的Pytorch库。它提供了构建、训练和评估神经网络的工具。
tqdm:训练时的进度条显示。
💞💞💞2.1.2 获取当前文件的绝对路径
'''===================获取当前文件的绝对路径========================'''
FILE = Path(__file__).resolve()# __file__指的是当前文件(即val.py),FILE最终保存着当前文件的绝对路径,比如D://yolov5/val.py
ROOT = FILE.parents[0] # YOLOv5 root directory ROOT保存着当前项目的父目录,比如 D://yolov5
if str(ROOT) not in sys.path: # sys.path即当前python环境可以运行的路径,假如当前项目不在该路径中,就无法运行其中的模块,所以就需要加载路径
sys.path.append(str(ROOT)) # add ROOT to PATH 把ROOT添加到运行路径上
ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative ROOT设置为相对路径
这段代码会获取当前文件的绝对路径,并使用Path库将其转换为Path对象。📚
这一部分的主要作用有两个:
- 将当前项目添加到系统路径上,以使得项目中的模块可以调用。
- 将当前项目的相对路径保存在ROOT中,便于寻找项目中的文件。
💞💞💞2.1.3 加载自定义模块
'''===================加载自定义模块============================'''
from models.common import DetectMultiBackend # yolov5的网络结构(yolov5)
from utils.callbacks import Callbacks # 和日志相关的回调函数
from utils.datasets import create_dataloader # 加载数据集的函数
from utils.general import (LOGGER, NCOLS, box_iou, check_dataset, check_img_size, check_requirements, check_yaml,
coco80_to_coco91_class, colorstr, increment_path, non_max_suppression, print_args,
scale_coords, xywh2xyxy, xyxy2xywh) # 定义了一些常用的工具函数
from utils.metrics import ConfusionMatrix, ap_per_class # 在YOLOv5中,fitness函数实现对 [P, R, mAP@.5, mAP@.5-.95] 指标进行加权
from utils.plots import output_to_target, plot_images, plot_val_study # 定义了Annotator类,可以在图像上绘制矩形框和标注信息
from utils.torch_utils import select_device, time_sync # 定义了一些与PyTorch有关的工具函数
这个是自定义的库,具体内容为:
models.common:网络结构类模块
utils.callbacks:定义回调函数,为logger服务。
utils.datasets:dataset和dataloader,数据集和数据加载。
utils.general:常用的工具函数,比如检查文件存在、检查图片大小、打印命令行参数等。
utils.metrics:模型验证指标,包括ap、混淆矩阵等。
utils.plots:定义了Annotator类,绘制图像的信息。
utils.torch_utils:Pytorch有关的工具函数。
💥💥2.2 保存信息
💞💞💞2.2.1 保存预测信息到txt文件
'''======================保存预测信息到txt文件====================='''
def save_one_txt(predn, save_conf, shape, file):
# Save one txt result
# gn = [w, h, w, h] 对应图片的宽高 用于后面归一化
gn = torch.tensor(shape)[[1, 0, 1, 0]] # normalization gain whwh
# 将每个图片的预测信息分别存入save_dir/labels下的xxx.txt中 每行: class_id + score + xywh
for *xyxy, conf, cls in predn.tolist():
# 将xyxy(左上角+右下角)格式转为xywh(中心点+宽高)格式,并归一化,转化为列表再保存
xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh
# line的形式是: "类别 xywh",若save_conf为true,则line的形式是:"类别 xywh 置信度"
line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format
# 将上述test得到的信息输出保存 输出为xywh格式 coco数据格式也为xywh格式
with open(file, 'a') as f:
# 写入对应的文件夹里,路径默认为“runs\detect\exp*\labels”
f.write(('%g ' * len(line)).rstrip() % line + '\n')
这段代码主要是保存预测信息到txt文件。📚
保存的信息为:
- cls:图片类别
- xywh:图片的中心点+宽高
- conf:置信度
💞💞💞2.2.2 保存预测信息到coco格式的json字典
'''======================保存预测信息到coco格式的json字典====================='''
def save_one_json(predn, jdict, path, class_map):
# 储存格式 {"image_id": 42, "category_id": 18, "bbox": [258.15, 41.29, 348.26, 243.78], "score": 0.236}
# 获取图片id
image_id = int(path.stem) if path.stem.isnumeric() else path.stem
# 获取预测框 并将xyxy转为xywh格式
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])], # 类别 coco91class()从索引0~79映射到索引0~90
'bbox': [round(x, 3) for x in b], # 预测框坐标
'score': round(p[4], 5)}) # 预测得分
这段代码主要是保存预测信息到coco格式的json字典。📚
保存的信息为:
- image_id:图片id,即属于哪张图片。
- category:类别,coco91class()从索引0~79映射到索引0~90。
- bbox:预测框坐标
- score:预测得分
💥💥2.3 计算指标
'''========================计算指标==========================='''
def process_batch(detections, labels, iouv):
"""
Return correct predictions matrix.
返回每个预测框在10个IoU阈值上是TP还是FP
Both sets of boxes are in (x1, y1, x2, y2) format.
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
"""
# 构建一个[pred_nums, 10]全为False的矩阵
correct = torch.zeros(detections.shape[0], iouv.shape[0], dtype=torch.bool, device=iouv.device)
# 计算每个gt与每个pred的iou,shape为: [gt_nums, pred_nums]
iou = box_iou(labels[:, 1:], detections[:, :4])
# iou超过阈值而且类别正确,则为True,返回索引
x = torch.where((iou >= iouv[0]) & (labels[:, 0:1] == detections[:, 5])) # IoU above threshold and classes match
# 如果存在符合条件的预测框
if x[0].shape[0]: # 至少有一个TP
# 将符合条件的位置构建成一个新的矩阵,第一列是行索引(表示gt索引),第二列是列索引(表示预测框索引),第三列是iou值
matches = torch.cat((torch.stack(x, 1), iou[x[0], x[1]][:, None]), 1).cpu().numpy() # [label, detection, iou]
if x[0].shape[0] > 1:
# argsort获得有小到大排序的索引, [::-1]相当于取反reserve操作,变成由大到小排序的索引,对matches矩阵进行排序
matches = matches[matches[:, 2].argsort()[::-1]]
matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
'''
参数return_index=True:表示会返回唯一值的索引,[0]返回的是唯一值,[1]返回的是索引
matches[:, 1]:这里的是获取iou矩阵每个预测框的唯一值,返回的是最大唯一值的索引,因为前面已由大到小排序
这个操作的含义:每个预测框最多只能出现一次,如果有一个预测框同时和多个gt匹配,只取其最大iou的一个
'''
# matches = matches[matches[:, 2].argsort()[::-1]]
matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
'''
matches[:, 0]:这里的是获取iou矩阵gt的唯一值,返回的是最大唯一值的索引,因为前面已由大到小排序
这个操作的含义: 每个gt也最多只能出现一次,如果一个gt同时匹配多个预测框,只取其匹配最大的那一个预测框
'''
# 以上操作实现了为每一个gt分配一个iou最高的类别的预测框,实现一一对应
matches = torch.Tensor(matches).to(iouv.device)
correct[matches[:, 1].long()] = matches[:, 2:3] >= iouv
'''
当前获得了gt与预测框的一一对应,其对于的iou可以作为评价指标,构建一个评价矩阵
需要注意,这里的matches[:, 1]表示的是为对应的预测框来赋予其iou所能达到的程度,也就是iouv的评价指标
'''
# 在correct中,只有与gt匹配的预测框才有对应的iou评价指标,其他大多数没有匹配的预测框都是全部为False
return correct
这段代码主要是计算correct,来获取匹配预测框的iou信息。📚
这个函数主要有两个作用:
- 作用1:对预测框与gt进行匹配。
- 作用2:对匹配上的预测框进行iou数值判断,用True来填充,其余没有匹配上的预测框的所有行数全部设置为False。
💥💥2.4 执行run()函数
💞💞💞2.4.1 设置参数
'''======================设置参数====================='''
@torch.no_grad()
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文件的形式保存模型预测框的坐标 默认False
save_hybrid=False, # 是否保存预测每个目标的置信度到预测txt文件中 默认False
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)
):
这段代码定义了run()函数,并设置了一系列参数,用于指定物体检测和识别的相关参数。📚
这些参数包括:
data:数据集文件的路径,默认为COCO128数据集的配置文件路径。
weights:模型权重文件的路径,默认为yolov5s.pt的权重文件路径。
batch_size:前向传播的批次大小,运行val.py传入默认为32。
imgsz:输入图像的大小,默认为640×640。
conf_thres:置信度阈值,默认为0.001.
iou_thres:非极大值抑制的iou阈值,默认为0.6。
task:设置测试的类型,有train、val、test、speed or study几种,默认为val。
device:使用的设备类型,默认为空,表示自动选择最合适的设备。
single_cls:数据集是否只用一个类别,运行val.py传入默认False,运行train.py则传入single_cls。
augment:是否使用数据增强的方式进行检测,默认为False。
verbose:是否打印出每个类别的mAP。运行test.py传入默认Fasle 运行train.py则传入nc < 50 and final_epoch。
save_txt:是否以txt文件的形式保存模型预测框的坐标 默认False。
save_hybrid:是否保存预测每个目标的置信度到预测txt文件中,默认False。
save_conf:保存置信度,默认False
save_json:是否按照coco的json格式保存预测框,并且使用cocoapi做评估(需要同样coco的json格式的标签),运行test.py传入默认Fasle 运行train.py则传入is_coco and final_epoch(一般也是False)。
project:验证结果保存的根目录 默认是 runs/val。
name:验证结果保存的目录,默认是exp。
exist_ok:如果文件存在就increment name,不存在就新建 ,默认False(默认文件都是不存在的)。
half:是否使用 FP16 的半精度推理,默认False。
dnn:是否使用OpenCV DNN作为ONNX推理的后端,默认为False。
model:模型,如果执行val.py就为None。
dataloader:数据加载器。
save_dir:文件保存路径。
plots:是否可视化
callback:回调函数
compute_loss:损失函数
💞💞💞2.4.2 初始化/加载模型以及设置设备
'''======================初始化/加载模型以及设置设备====================='''
# Initialize/load model and set device
training = model is not None
if training: # 通过 train.py 调用的run函数
# 获得记录在模型中的设备 next为迭代器
device, pt = next(model.parameters()).device, True
# 精度减半
# 如果设备类型不是cpu 则将模型由32位浮点数转换为16位浮点数
half &= device.type != 'cpu' # half precision only supported on CUDA
model.half() if half else model.float()
else: # 直接通过 val.py 调用 run 函数
# 调用torch_utils中select_device来选择执行程序时的设备
device = select_device(device, batch_size=batch_size)
# 路径
# 调用genera.py中的increment_path函数来生成save_dir文件路径 run\test\expn
save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run
# mkdir创建路径最后一级目录
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir
model = DetectMultiBackend(weights, device=device, dnn=dnn)
stride, pt = model.stride, model.pt
# 调用general.py中的check_img_size函数来检查图像分辨率能否被32整除
imgsz = check_img_size(imgsz, s=stride) # check image size
# 如果不是CPU,使用半进度(图片半精度/模型半精度)
half &= pt and device.type != 'cpu' # half precision only supported by PyTorch on CUDA
if pt:
model.model.half() if half else model.model.float()
else:
half = False
batch_size = 1 # export.py models default to batch-size 1
device = torch.device('cpu')
# 打印耗时
LOGGER.info(f'Forcing --batch-size 1 square inference shape(1,3,{imgsz},{imgsz}) for non-PyTorch backends')
# Data
# 调用general.py中的check_dataset函数来检查数据文件是否正常
data = check_dataset(data) # check
这段代码主要是初始化/加载模型以及设置设备。📚
首先会判断模型是否存在。
接着判断是否是训练时调用run函数,即执行train.py,如果不是,就调用select_device选择设备,并生成save_dir + make dir + 加载模型model + check imgsz + 加载data配置信息。
- 训练时(train.py)调用:初始化模型参数、训练设备
- 验证时(val.py)调用:初始化设备、save_dir文件路径、make dir、加载模型、check imgsz、 加载+check data配置信息
最后判断设备类型并仅仅单GPU支持一半的精度。Half model 只能在单GPU设备上才能使用, 一旦使用half,不但模型需要设为half,输入模型的图片也需要设为half。如果设备类型不是CPU 则将模型由32位浮点数转换为16位浮点数。
💞💞💞2.4.3 加载配置
'''======================加载配置====================='''
# Configure
# 将模型转换为测试模式 固定住dropout层和Batch Normalization层
model.eval()
# 通过 COCO 数据集的文件夹组织结构判断当前数据集是否为 COCO 数据集
is_coco = isinstance(data.get('val'), str) and data['val'].endswith('coco/val2017.txt') # COCO dataset
# 确定检测的类别数目
nc = 1 if single_cls else int(data['nc']) # number of classes
# 计算mAP相关参数
iouv = torch.linspace(0.5, 0.95, 10).to(device) # mAP@0.5:0.95 的iou向量
# numel为pytorch预置函数 用来获取张量中的元素个数
niou = iouv.numel()
这一段是加载数据的yaml配置文件信息。📚
首先进入模型验证模式,然后确定检测的类别个数nc ,以及计算mAP相关参数,设置iou阈值从0.5-0.95取10个(0.05间隔) 所以iou: [0.50000, 0.55000, 0.60000, 0.65000, 0.70000, 0.75000, 0.80000, 0.85000, 0.90000, 0.95000]
💞💞💞2.4.4 加载val数据集
'''======================加载val数据集====================='''
# Dataloader
if not training:
if pt and device.type != 'cpu':
# 创建一张全为0的图片(四维张量)
model(torch.zeros(1, 3, imgsz, imgsz).to(device).type_as(next(model.model.parameters()))) # warmup
pad = 0.0 if task == 'speed' else 0.5
task = task if task in ('train', 'val', 'test') else 'val' # path to train/val/test images
# 调用datasets.py文件中的create_dataloader函数创建dataloader
dataloader = create_dataloader(data[task], imgsz, batch_size, stride, single_cls, pad=pad, rect=pt,
prefix=colorstr(f'{task}: '))[0]
这段是加载val数据集。📚
如果是训练的时候调用 ,则不需要这段代码。如果是val调用,则需要用create_dataloader创建数据集。
💞💞💞2.4.5 初始化
'''======================初始化====================='''
# 初始化已完成测试的图片数量
seen = 0
# 调用matrics中函数 存储混淆矩阵
confusion_matrix = ConfusionMatrix(nc=nc)
# 获取数据集所有类别的类名
names = {k: v for k, v in enumerate(model.names if hasattr(model, 'names') else model.module.names)}
# 调用general.py中的函数 获取coco数据集的类别索引
class_map = coco80_to_coco91_class() if is_coco else list(range(1000))
# 设置tqdm进度条的显示信息
s = ('%20s' + '%11s' * 6) % ('Class', 'Images', 'Labels', 'P', 'R', 'mAP@.5', 'mAP@.5:.95')
# 初始化detection中各个指标的值
dt, p, r, f1, mp, mr, map50, map = [0.0, 0.0, 0.0], 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
# 初始化网络训练的loss
loss = torch.zeros(3, device=device)
# 初始化json文件涉及到的字典、统计信息、AP、每一个类别的AP、图片汇总
jdict, stats, ap, ap_class = [], [], [], []
pbar = tqdm(dataloader, desc=s, ncols=NCOLS, bar_format='{l_bar}{bar:10}{r_bar}{bar:-10b}') # progress bar
这段代码主要是获取数据集的相应参数。📚
(1)初始化已完成测试图片数量,设置seen=0
(2)初始化混淆矩阵
(3)获取数据集类名 和coco数据集的类别索引
(4)设置tqdm进度条的显示信息
(5)初始化p、 r、 f1、 MP、MR、mAP50、mAP指标和初始化测试集的损失以及初始化json文件中的字典统计信息、AP等
💞💞💞2.4.6 验证过程
(1)开始验证前的预处理
'''===开始验证前的预处理==='''
for batch_i, (im, targets, paths, shapes) in enumerate(pbar):
# 获取当前时间
t1 = time_sync()
if pt:
# 将图片数据拷贝到device(GPU)上面
im = im.to(device, non_blocking=True)
#对targets也做同样拷贝的操作
targets = targets.to(device)
# 将图片从64位精度转换为32位精度
im = im.half() if half else im.float() # uint8 to fp16/32
# 将图像像素值0-255的范围归一化到0-1的范围
im /= 255 # 0 - 255 to 0.0 - 1.0
# 四个变量分别代表batchsize、通道数目、图像高度、图像宽度
nb, _, height, width = im.shape # batch size, channels, height, width
# 获取当前时间
t2 = time_sync()
# dt[0]: 累计处理数据时间
dt[0] += t2 - t1
这段代码主要是预处理图片和target。📚
(2)前项推理
'''===前向推理==='''
# Inference
out, train_out = model(im) if training else model(im, augment=augment, val=True) # inference, loss outputs
# 累计前向推理时间 dt[1]
dt[1] += time_sync() - t2
这段代码主要是模型前向推理、记录前向推理时间。📚
- out: 推理结果。1个 ,[bs, anchor_numgrid_wgrid_h, xywh+c+20classes] = [1, 19200+4800+1200, 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]
(3)计算损失
'''===计算损失==='''
# Loss
# compute_loss不为空 说明正在执行train.py 根据传入的compute_loss计算损失值
if compute_loss:
# loss 包含bounding box 回归的GIoU、object和class 三者的损失
loss += compute_loss([x.float() for x in train_out], targets)[1] # box, obj, cls
这段代码主要是计算验证集损失。📚
loss 包含bounding box 回归的GIoU、object和class 三者的损失
- 分类损失(cls_loss):该损失用于判断模型是否能够准确地识别出图像中的对象,并将其分类到正确的类别中。
- 置信度损失(obj_loss):该损失用于衡量模型预测的框(即包含对象的矩形)与真实框之间的差异。
- 边界框损失(box_loss):该损失用于衡量模型预测的边界框与真实边界框之间的差异,这有助于确保模型能够准确地定位对象。
说明:♨️♨️♨️
置信度损失指的是模型预测的物体是否存在的概率和实际存在的概率之间的差距。
边界框损失指的是模型预测的物体边界框位置和实际位置之间的差距。
两种损失函数的重点不同,置信度损失的重点在判断物体是否存在,而边界框损失的重点在于精确地定位物体的位置。
(4)NMS获得预测框
'''===NMS获得预测框==='''
# NMS
# targets: [num_target, img_index+class_index+xywh] = [31, 6]
targets[:, 2:] *= torch.Tensor([width, height, width, height]).to(device) # to pixels
# 提取bach中每一张图片的目标的label
# lb: {list: bs} 第一张图片的target[17, 5] 第二张[1, 5] 第三张[7, 5] 第四张[6, 5]
lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else [] # for autolabelling
# 计算NMS过程所需要的时间
t3 = time_sync()
# 调用general.py中的函数 进行非极大值抑制操作
out = non_max_suppression(out, conf_thres, iou_thres, labels=lb, multi_label=True, agnostic=single_cls)
# 累计NMS时间
dt[2] += time_sync() - t3
这段代码主要是运行NMS目标检测的后处理模块,用于删除冗余的bounding box。📚
(5)统计真实框、预测框信息
'''===统计真实框、预测框信息==='''
# Metrics
# si代表第si张图片,pred是对应图片预测的label信息
for si, pred in enumerate(out):
# 获取第si张图片的gt标签信息 包括class, x, y, w, h target[:, 0]为标签属于哪张图片的编号
labels = targets[targets[:, 0] == si, 1:]
# nl为图片检测到的目标个数
nl = len(labels)
# tcls为检测到的目标的类别 label矩阵的第一列
tcls = labels[:, 0].tolist() if nl else [] # target class
# 第si张图片对应的文件路径
path, shape = Path(paths[si]), shapes[si][0]
# 统计测试图片数量 +1
seen += 1
# 如果预测为空,则添加空的信息到stats里
if len(pred) == 0:
if nl: # 预测为空但同时有label信息
# stats初始化为一个空列表[] 此处添加一个空信息
# 添加的每一个元素均为tuple 其中第二第三个变量为一个空的tensor
stats.append((torch.zeros(0, niou, dtype=torch.bool), torch.Tensor(), torch.Tensor(), tcls))
continue
# Predictions
# 预测
if single_cls:
pred[:, 5] = 0
# 对pred进行深复制
predn = pred.clone()
# 调用general.py中的函数 将图片调整为原图大小
scale_coords(im[si].shape[1:], predn[:, :4], shape, shapes[si][1]) # native-space pred
# Evaluate
# 预测框评估
if nl:
# 获得xyxy格式的框
tbox = xywh2xyxy(labels[:, 1:5]) # target boxes
# 调用general.py中的函数 将图片调整为原图大小
scale_coords(im[si].shape[1:], tbox, shape, shapes[si][1]) # native-space labels
# 处理完gt的尺寸信息,重新构建成 (cls, xyxy)的格式
labelsn = torch.cat((labels[:, 0:1], tbox), 1) # native-space label
# 对当前的预测框与gt进行一一匹配,并且在预测框的对应位置上获取iou的评分信息,其余没有匹配上的预测框设置为False
correct = process_batch(predn, labelsn, iouv)
if plots:
# 计算混淆矩阵 confusion_matrix
confusion_matrix.process_batch(predn, labelsn)
else:
# 返回一个形状为为pred.shape[0, 类型为torch.dtype,里面的每一个值都是0的tensor
correct = torch.zeros(pred.shape[0], niou, dtype=torch.bool)
# 每张图片的结果统计到stats里
stats.append((correct.cpu(), pred[:, 4].cpu(), pred[:, 5].cpu(), tcls)) # (correct, conf, pcls, tcls)
# Save/log
# 保存预测信息到txt文件
if save_txt:
save_one_txt(predn, save_conf, shape, file=save_dir / 'labels' / (path.stem + '.txt'))
# 保存预测信息到json字典
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])
这段代码主要是统计每张图片真实框和预测框的相关信息。📚
首先统计每张图片的相关信息,如预测label信息、标签gt信息等。
然后统计检测到的目标个数和类别以及相对应的文件路径。
接着利用得到的上述信息进行目标的预测,并将结果保存同时输出日志,分别保存预测信息到image_name.txt文件和coco格式的json字典。
(6)画出前3个batch图片的gt和pred框
'''===画出前三个batch图片的gt和pred框==='''
# Plot images
# 画出前三个batch的图片的ground truth和预测框predictions(两个图)一起保存
if plots and batch_i < 3:
f = save_dir / f'val_batch{batch_i}_labels.jpg' # labels
Thread(target=plot_images, args=(im, targets, paths, f, names), daemon=True).start()
'''
Thread()函数为创建一个新的线程来执行这个函数 函数为plots.py中的plot_images函数
target: 执行的函数 args: 传入的函数参数 daemon: 当主线程结束后, 由他创建的子线程Thread也已经自动结束了
.start(): 启动线程 当thread一启动的时候, 就会运行我们自己定义的这个函数plot_images
如果在plot_images里面打开断点调试, 可以发现子线程暂停, 但是主线程还是在正常的训练(还是正常的跑)
'''
# 传入plot_images函数之前需要改变pred的格式 target则不需要改
f = save_dir / f'val_batch{batch_i}_pred.jpg' # predictions
Thread(target=plot_images, args=(im, output_to_target(out), paths, f, names), daemon=True).start()
这段代码主要是创建子进程进行绘图,画出前3个batch图片的gt和pred框。📚
- gt:真实框,
Ground truth box
, 是人工标注的位置,存放在标注文件中 pred:
预测框,Prediction box
, 是由目标检测模型计算输出的框
(7)计算指标
'''===计算指标==='''
# Compute metrics
# 将stats列表的信息拼接到一起
stats = [np.concatenate(x, 0) for x in zip(*stats)] # 转换为对应格式numpy
# stats[0].any(): stats[0]是否全部为False, 是则返回 False, 如果有一个为 True, 则返回 True
if len(stats) and stats[0].any():
# 计算上述测试过程中的各种性能指标
p, r, ap, f1, ap_class = ap_per_class(*stats, plot=plots, save_dir=save_dir, names=names)
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()
nt = np.bincount(stats[3].astype(np.int64), minlength=nc) # number of targets per class
else:
nt = torch.zeros(1)
这段代码主要是计算评分类效果的各种指标。📚
(8)打印日志
'''===打印日志==='''
# Print results
# 按照以下格式来打印测试过程的指标
pf = '%20s' + '%11i' * 2 + '%11.3g' * 4 # print format
LOGGER.info(pf % ('all', seen, nt.sum(), mp, mr, map50, map))
# 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
# 打印 推断/NMS过程/总过程 的在每一个batch上面的时间消耗
t = tuple(x / 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)
这段代码主要是打印各项指标。📚
(9)保存验证结果
'''===保存验证结果==='''
# Plots
# 绘图
if plots:
# confusion_matrix.plot()函数绘制混淆矩阵
confusion_matrix.plot(save_dir=save_dir, names=list(names.values()))
# 调用Loggers中的on_val_end方法,将日志记录并生成一些记录的图片
callbacks.run('on_val_end')
# 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
# 注释的json格式
anno_json = str(Path(data.get('path', '../coco')) / 'annotations/instances_val2017.json') # annotations json
# 预测的json格式
pred_json = str(save_dir / f"{w}_predictions.json") # predictions json
# 在控制台打印coco的api评估各个指标,保存到json文件
LOGGER.info(f'\nEvaluating pycocotools mAP... saving {pred_json}...')
# 打开pred_json文件只用于写入
with open(pred_json, 'w') as f: # w:打开一个文件只用于写入。如果该文件已存在则将其覆盖。如果该文件不存在,创建新文件。
# 测试集的标签也需要转成coco的json格式。将 dict==>json 序列化,用json.dumps()
json.dump(jdict, f)
try: # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb
check_requirements(['pycocotools'])
# 以下过程为利用官方coco工具进行结果的评测
from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval
# 获取并初始化测试集标签的json文件
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.img_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}')
这段代码主要是绘制混淆矩阵和利用cocoapi进行相关性能指标的评估。📚
(10)返回结果
'''===返回结果==='''
# Return results
# 返回测试指标结果
model.float() # 将模型转换为适用于训练的状态
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
这段代码主要是用于返回对应的测试结果。📚
💥💥2.5 设置opt参数
'''==============设置opt参数=================='''
def parse_opt():
parser = argparse.ArgumentParser()
# 数据集配置文件地址 包含数据集的路径、类别个数、类名、下载地址等信息
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path')
# 模型的权重文件地址yolov5s.pt
parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s.pt', help='model.pt path(s)')
# 前向传播的批次大小 默认32
parser.add_argument('--batch-size', type=int, default=32, help='batch size')
# 输入网络的图片分辨率 默认640
parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='inference size (pixels)')
# object置信度阈值 默认0.001
parser.add_argument('--conf-thres', type=float, default=0.001, help='confidence threshold')
# 进行NMS时IOU的阈值 默认0.6
parser.add_argument('--iou-thres', type=float, default=0.6, help='NMS IoU threshold')
# 设置测试的类型 有train, val, test, speed or study几种 默认val
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')
# 数据集是否只用一个类别 默认False
parser.add_argument('--single-cls', action='store_true', help='treat as single-class dataset')
# 测试是否使用TTA Test Time Augment 默认False
parser.add_argument('--augment', action='store_true', help='augmented inference')
# 是否打印出每个类别的mAP 默认False
parser.add_argument('--verbose', action='store_true', help='report mAP by class')
# 是否以txt文件的形式保存模型预测的框坐标, 默认False
parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
# 保存label+prediction杂交结果到对应.txt,默认False
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')
# 是否按照coco的json格式保存预测框,并且使用cocoapi做评估(需要同样coco的json格式的标签) 默认False
parser.add_argument('--save-json', action='store_true', help='save a COCO-JSON results file')
# 测试保存的源文件 默认runs/val
parser.add_argument('--project', default=ROOT / 'runs/val', help='save to project/name')
# 测试保存的文件地址 默认exp 保存在runs/val/exp下
parser.add_argument('--name', default='exp', help='save to project/name')
# 是否存在当前文件 默认False 一般是 no exist-ok 连用 所以一般都要重新创建文件夹
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
# 是否使用半精度推理 默认False
parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference')
# 是否使用 OpenCV DNN对ONNX 模型推理
parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference')
# 解析上述参数
opt = parser.parse_args()
opt.data = check_yaml(opt.data)
# |或 左右两个变量有一个为True 左边变量就为True
opt.save_json |= opt.data.endswith('coco.yaml')
opt.save_txt |= opt.save_hybrid
print_args(FILE.stem, opt)
return opt
参数解析:
data:数据集文件的路径,默认为COCO128数据集的配置文件路径。
weights:模型权重文件的路径,默认为yolov5s.pt的权重文件路径。
batch_size:前向传播的批次大小,运行val.py传入默认为32。
imgsz:输入图像的大小,默认为640×640。
conf_thres:置信度阈值,默认为0.001.
iou_thres:非极大值抑制的iou阈值,默认为0.6。
task:设置测试的类型,有train、val、test、speed or study几种,默认为val。
device:使用的设备类型,默认为空,表示自动选择最合适的设备。
single_cls:数据集是否只用一个类别,运行val.py传入默认False,运行train.py则传入single_cls。
augment:是否使用数据增强的方式进行检测,默认为False。
verbose:是否打印出每个类别的mAP。运行test.py传入默认Fasle 运行train.py则传入nc < 50 and final_epoch。
save_txt:是否以txt文件的形式保存模型预测框的坐标 默认False。
save_hybrid:是否保存预测每个目标的置信度到预测txt文件中,默认False。
save_conf:保存置信度,默认False
save_json:是否按照coco的json格式保存预测框,并且使用cocoapi做评估(需要同样coco的json格式的标签),运行test.py传入默认Fasle 运行train.py则传入is_coco and final_epoch(一般也是False)。
project:验证结果保存的根目录 默认是 runs/val。
name:验证结果保存的目录,默认是exp。
exist_ok:如果文件存在就increment name,不存在就新建 ,默认False(默认文件都是不存在的)。
half:是否使用 FP16 的半精度推理,默认False。
dnn:是否使用OpenCV DNN作为ONNX推理的后端,默认为False。
💥💥2.6 执行main()函数
'''=============执行main()函数================'''
def main(opt):
# 检测requirements文件中需要的包是否安装好了
check_requirements(requirements=ROOT / 'requirements.txt', exclude=('tensorboard', 'thop'))
# 如果task in ['train', 'val', 'test']就正常测试 训练集/验证集/测试集
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 will produce invalid mAP values.')
run(**vars(opt))
else:
weights = opt.weights if isinstance(opt.weights, list) else [opt.weights]
opt.half = True # FP16 for fastest results
# 如果opt.task == 'speed' 就测试yolov5系列和yolov3-spp各个模型的速度评估
if opt.task == 'speed': # speed benchmarks
# 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)
# 如果opt.task = ['study']就评估yolov5系列和yolov3-spp各个模型在各个尺度下的指标并可视化
elif opt.task == 'study': # speed vs mAP benchmarks
# 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坐标
x, y = list(range(256, 1536 + 128, 128)), [] # x axis (image sizes), y axis
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
# 将y输出保存
np.savetxt(f, y, fmt='%10.4g') # save
# 命令行执行命令将study文件进行压缩
os.system('zip -r study.zip study_*.txt')
# 调用plots.py中的函数 可视化各个指标
plot_val_study(x=x) # plot
# python val.py --data data/mask_data.yaml --weights runs/train/exp_yolov5s/weights/best.pt --img 640
if __name__ == "__main__":
opt = parse_opt()
main(opt)
这段代码主要是根据解析的opt参数,执行main()函数。📚
🚀3.总结
总结:♨️♨️♨️
这部分代码主要是对
train.py
训练后的模型进行评估和验证,其难点在于统计真实框、预测框信息和计算mAP,需结合metrics.py
脚本一起看。