概述
该文件类似于质量检查与评估团队,主要就是负责在建筑项目中的各个阶段进行质量检测和性能评估,从而确保最终的项目是符合要求的
注意这个文件会记录模型的性能数据,在YOLOv3的源码中train.py已经对其进行了调用,也是可以在模型训练完成后通过调用该文件从而判断整体性能如何
主要组成
- 参数解析与初始化
- 功能:解析命令行参数,设置验证配置
- 质量检查团队制定详细的检测计划和资源分配
- 模型与数据加载
- 功能:加载模型和数据集,配置设备,初始化评估工具
- 质量检查团队准备检测工具和资料,确保检测过程顺利进行
- 推理与结果处理
- 功能:对图像进行推理,处理预测结果,匹配真实标签,计算评估指标
- 质量检查团队对每个建筑部分进行实际检测和记录,确保每个部分符合设计标准
- 指标计算与评估
-
- 功能:计算和评估模型的各项指标,生成报告和可视化图表
- 质量检查团队总结和报告建筑质量评估结果,提供给项目经理和相关方参考
- 结果保存与报告
- 功能:将检测结果保存为文本文件和JSON文件,生成报告
- 质量检查团队记录和存档检测结果,便于后续分析和追踪
主要模块
参数解析与初始化
与train.py文件中的参数设置类似
def parse_opt():
"""
解析命令行参数,配置模型推理或评估的相关设置。
主要功能:
- 通过命令行输入配置文件路径、模型权重路径、推理相关的参数等。
- 返回解析后的参数对象 opt。
返回值:
opt: 包含所有命令行参数的命名空间对象。
"""
# 创建 ArgumentParser 对象,用于管理命令行参数
parser = argparse.ArgumentParser()
# 添加参数:数据集路径
parser.add_argument(
'--data',
type=str,
default=ROOT / 'data/you.yaml',
help='dataset.yaml path' # 数据集配置文件路径
)
# 添加参数:模型权重路径
parser.add_argument(
'--weights',
nargs='+',
type=str,
default=ROOT / 'runs/train/exp/weights/best.pt',
help='model.pt path(s)' # 支持多个权重文件路径(使用空格分隔)
)
# 添加参数:批量大小
parser.add_argument(
'--batch-size',
type=int,
default=2,
help='batch size' # 推理或验证的批量大小
)
# 添加参数:推理图片尺寸
parser.add_argument(
'--imgsz', '--img', '--img-size',
type=int,
default=416,
help='inference size (pixels)' # 输入图像的分辨率大小
)
# 添加参数:置信度阈值
parser.add_argument(
'--conf-thres',
type=float,
default=0.5,
help='confidence threshold' # 置信度分数阈值,低于此值的检测框将被丢弃
)
# 添加参数:IoU阈值
parser.add_argument(
'--iou-thres',
type=float,
default=0.6,
help='NMS IoU threshold' # 非极大值抑制(NMS)的IoU阈值
)
# 添加参数:任务类型
parser.add_argument(
'--task',
default='test',
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' # 指定设备(如GPU或CPU)
)
# 添加参数:单类别数据集
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' # 是否按类别报告mAP
)
# 添加参数:保存文本结果
parser.add_argument(
'--save-txt',
action='store_false',
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_false',
help='save confidences in --save-txt labels' # 是否在保存的标签中包含置信度
)
# 添加参数:保存JSON文件
parser.add_argument(
'--save-json',
action='store_false',
help='save a COCO-JSON results file' # 是否将结果保存为COCO格式的JSON文件
)
# 添加参数:项目路径
parser.add_argument(
'--project',
default=ROOT / 'runs/val',
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' # 是否使用FP16半精度进行推理
)
# 添加参数:OpenCV DNN支持
parser.add_argument(
'--dnn',
action='store_true',
help='use OpenCV DNN for ONNX inference' # 是否使用OpenCV DNN进行ONNX推理
)
# 解析命令行参数
opt = parser.parse_args()
# 检查数据集配置文件路径是否为 YAML 格式
opt.data = check_yaml(opt.data)
# 如果数据集配置文件以 "coco.yaml" 结尾,则强制保存为 JSON
opt.save_json |= opt.data.endswith('coco.yaml')
# 如果启用了 save_hybrid,则自动启用 save_txt
opt.save_txt |= opt.save_hybrid
# 打印解析的参数
print_args(FILE.stem, opt)
# 返回解析后的参数对象
return opt
模型与数据加载
主要作用是负责加载模型和数据集,配备训练设备,初始化混淆矩阵和评价指标。类似于质量检查团队准备检测工具和资料,确保一切准备就绪以进行有效的质量评估
@torch.no_grad()
def run(data,
weights=None, # 模型权重文件路径
batch_size=32, # 批量大小
imgsz=640, # 输入图片大小(像素)
conf_thres=0.001, # 置信度阈值
iou_thres=0.6, # 非极大值抑制(NMS)的IoU阈值
task='val', # 任务类型:'train', 'val', 'test', 'speed' 或 'study'
device='', # 设备选择,例如 'cuda:0' 或 'cpu'
single_cls=False, # 是否将数据集视为单类别
augment=False, # 是否在推理过程中应用数据增强
verbose=False, # 是否输出详细信息
save_txt=False, # 是否保存预测结果到 *.txt 文件
save_hybrid=False, # 是否保存标签和预测的混合结果到 *.txt 文件
save_conf=False, # 是否保存置信度到结果文件中
save_json=False, # 是否保存预测结果为 COCO 格式的 JSON 文件
project=ROOT / 'runs/val', # 保存结果的目录
name='exp', # 结果保存的实验名称
exist_ok=False, # 是否允许覆盖已存在的目录
half=True, # 是否使用 FP16 半精度推理
dnn=False, # 是否使用 OpenCV 的 DNN 模式进行 ONNX 推理
model=None, # 预加载的模型,如果为 None 则加载新的模型
dataloader=None, # 预加载的数据加载器,如果为 None 则重新创建
save_dir=Path(''), # 保存结果的路径
plots=True, # 是否绘制结果图
callbacks=Callbacks(), # 回调函数,用于自定义过程
compute_loss=None, # 自定义损失函数
):
"""
YOLOv3 验证函数,执行模型推理和性能评估。
参数:
data: 数据集配置文件路径或解析后的字典
weights: 模型权重路径
batch_size: 批量大小
imgsz: 输入图像大小(像素)
conf_thres: 置信度阈值
iou_thres: IoU 阈值
task: 任务类型('val', 'train', 'test'等)
device: 设备选择
single_cls: 是否单类别数据集
augment: 是否启用数据增强
verbose: 是否输出详细日志
save_txt: 是否保存预测结果到文本文件
save_hybrid: 是否保存混合结果
save_conf: 是否保存置信度
save_json: 是否保存结果为 COCO 格式的 JSON 文件
project: 保存结果的主目录
name: 保存结果的子目录
exist_ok: 是否允许覆盖已存在的目录
half: 是否使用 FP16 半精度推理
dnn: 是否启用 OpenCV DNN 推理
model: 预加载模型(若无则重新加载)
dataloader: 数据加载器(若无则重新创建)
save_dir: 保存结果路径
plots: 是否绘制结果图
callbacks: 回调函数
compute_loss: 自定义损失函数
"""
# 判断是否为训练阶段
training = model is not None # 如果已加载模型,则为训练阶段,否则为推理阶段
if training:
# 设置设备为模型参数所在的设备
device, pt = next(model.parameters()).device, True
half &= device.type != 'cpu' # 如果不是 CPU,则启用半精度推理
model.half() if half else model.float() # 设置模型为 FP16 或 FP32
else:
# 如果未提供模型,则选择设备并加载模型权重
device = select_device(device, batch_size=batch_size) # 自动选择 GPU 或 CPU
save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # 创建保存目录
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # 创建保存结果的目录
model = DetectMultiBackend(weights, device=device, dnn=dnn) # 加载模型
stride, pt = model.stride, model.pt # 获取模型步长和类型(是否为 PyTorch)
imgsz = check_img_size(imgsz, s=stride) # 检查并调整输入图片大小以适应模型的步长
half &= pt and device.type != 'cpu' # 如果是 PyTorch 且不是 CPU,则启用半精度
if pt:
model.model.half() if half else model.model.float() # 设置模型精度
else:
half = False
batch_size = 1 # 非 PyTorch 模型强制设置 batch_size 为 1
device = torch.device('cpu') # 强制使用 CPU
LOGGER.info(f'强制使用 --batch-size 1 和输入形状 (1,3,{imgsz},{imgsz}) 对于非 PyTorch 后端')
data = check_dataset(data) # 加载和检查数据集配置
# 设置模型为评估模式
model.eval()
is_coco = isinstance(data.get('val'), str) and data['val'].endswith('coco/val2017.txt') # 检查是否为 COCO 数据集
nc = 1 if single_cls else int(data['nc']) # 获取类别数,如果是单类别则设置为 1
iouv = torch.linspace(0.5, 0.95, 10).to(device) # IoU 阈值从 0.5 到 0.95,分成 10 段
niou = iouv.numel() # IoU 阈值的数量
# 创建数据加载器
if not training: # 如果不是训练阶段
if pt and device.type != 'cpu': # 如果是 PyTorch 且不是 CPU
model(torch.zeros(1, 3, imgsz, imgsz).to(device).type_as(next(model.model.parameters()))) # 运行一次前向传播以初始化模型
pad = 0.0 if task == 'speed' else 0.5 # 填充策略
task = task if task in ('train', 'val', 'test') else 'val' # 确保任务类型有效
dataloader = create_dataloader(data[task], imgsz, batch_size, stride, single_cls,
pad=pad, rect=pt, prefix=colorstr(f'{task}: '))[0] # 创建数据加载器
# 初始化变量
seen = 0 # 已处理的图像数量
confusion_matrix = ConfusionMatrix(nc=nc) # 初始化混淆矩阵
names = {k: v for k, v in enumerate(model.names if hasattr(model, 'names') else model.module.names)} # 获取类别名称
class_map = coco80_to_coco91_class() if is_coco else list(range(1000)) # 如果是 COCO 数据集,则获取类别映射
s = ('%20s' + '%11s' * 6) % ('Class', 'Images', 'Labels', 'P', 'R', 'mAP@.5', 'mAP@.5:.95') # 打印表头格式
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 = torch.zeros(3, device=device) # 初始化损失
jdict, stats, ap, ap_class = [], [], [], [] # 初始化 JSON 结果字典和统计变量
pbar = tqdm(dataloader, desc=s, ncols=NCOLS, bar_format='{l_bar}{bar:10}{r_bar}{bar:-10b}') # 进度条
推理与结果处理
在每个批次中,对图像进行推理(预测过程),然后应用非极大值抑制(NMS,主要是用于过滤重复的检测结果,确保每个问题区域只可以被记录一次),处理检测结果与真实标签的匹配,计算评估指标,最后根据需要保存结果。
类似于质量检查团队对每个建筑部分进行实际检测和记录,确保每个部分符合设计标准
代码主要逻辑
- 预处理
- 将输入图像转换为设备可用的格式,并归一化到 [0,1] 范围。
- 如果是训练阶段,还需将标签调整为像素坐标。
- 推理阶段
- forword方法,生成预测结果
- 损失计算
- 在训练阶段计算损失,用于后续优化(验证阶段通常不进行)
- 非极大值抑制(NMS)
- 对模型输出进行 NMS,去除冗余预测框
- 评估阶段
- 统计预测框与标签的匹配情况,计算各项指标(例如 P、R、mAP 等)
- 如果启用绘图,则绘制预测和标签结果
- 保存结果
- 根据配置保存预测结果到文本文件或 JSON 文件
- 调用回调函数以便执行自定义逻辑
for batch_i, (im, targets, paths, shapes) in enumerate(pbar):
"""
遍历数据加载器中的每个批次,进行推理、损失计算、NMS(非极大值抑制)以及评估指标的计算。
参数:
batch_i: 当前批次索引
im: 当前批次的图像张量
targets: 当前批次的标签张量
paths: 当前批次的图像路径
shapes: 当前批次的原始图像尺寸和缩放信息
"""
t1 = time_sync() # 记录当前时间,用于计时
# 如果是 PyTorch 模型
if pt:
im = im.to(device, non_blocking=True) # 将图像张量加载到设备上(GPU 或 CPU)
targets = targets.to(device) # 将标签加载到设备上
# 将图像转换为 FP16 或 FP32 浮点类型
im = im.half() if half else im.float()
im /= 255 # 将像素值归一化到 [0, 1] 范围
nb, _, height, width = im.shape # 获取批次大小和图像的高度、宽度
t2 = time_sync() # 记录时间
dt[0] += t2 - t1 # 记录数据加载和预处理的时间
# 推理
out, train_out = model(im) if training else model(im, augment=augment, val=True)
"""
如果在训练阶段,调用模型进行标准推理;否则调用模型进行验证推理。
augment 参数用于是否启用数据增强。
train_out 为训练阶段的输出,out 为推理结果。
"""
dt[1] += time_sync() - t2 # 记录推理时间
# 损失计算(仅在训练阶段进行)
if compute_loss:
loss += compute_loss([x.float() for x in train_out], targets)[1] # 计算损失并累积
# NMS(非极大值抑制)处理
targets[:, 2:] *= torch.Tensor([width, height, width, height]).to(device)
"""
将目标标签的坐标从相对坐标(归一化)转换为像素坐标。
width 和 height 分别是图像的宽度和高度。
"""
lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else []
"""
如果 save_hybrid 为 True,则根据目标标签生成与 NMS 配合的辅助标签。
"""
t3 = time_sync() # 记录时间
out = non_max_suppression(out, conf_thres, iou_thres, labels=lb, multi_label=True, agnostic=single_cls)
"""
对模型的输出进行 NMS 操作,去除冗余预测框。
参数:
conf_thres: 置信度阈值
iou_thres: IoU 阈值
labels: 传入的辅助标签(如果有)
multi_label: 是否支持多标签
agnostic: 是否类别无关
"""
dt[2] += time_sync() - t3 # 记录 NMS 时间
# 评估指标计算
for si, pred in enumerate(out):
"""
遍历当前批次的每张图像的预测结果。
si: 当前图像在批次中的索引
pred: 当前图像的预测结果
"""
labels = targets[targets[:, 0] == si, 1:] # 获取当前图像的标签
nl = len(labels) # 获取标签数量
tcls = labels[:, 0].tolist() if nl else [] # 提取标签类别
path, shape = Path(paths[si]), shapes[si][0] # 获取图像路径和原始形状
seen += 1 # 统计已处理的图像数量
# 如果没有预测结果
if len(pred) == 0:
if nl: # 如果标签数量不为 0,则统计错误样本
stats.append((torch.zeros(0, niou, dtype=torch.bool), torch.Tensor(), torch.Tensor(), tcls))
continue
# 如果是单类别数据集,则将所有预测的类别设为 0
if single_cls:
pred[:, 5] = 0
# 将预测框的坐标转换为原始图像的坐标
predn = pred.clone() # 克隆预测结果
scale_coords(im[si].shape[1:], predn[:, :4], shape, shapes[si][1]) # 坐标缩放到原始图像尺寸
# 如果有标签,则计算精确匹配
if nl:
tbox = xywh2xyxy(labels[:, 1:5]) # 将标签的坐标从 (x, y, w, h) 转换为 (x1, y1, x2, y2)
scale_coords(im[si].shape[1:], tbox, shape, shapes[si][1]) # 缩放标签坐标到原始图像尺寸
labelsn = torch.cat((labels[:, 0:1], tbox), 1) # 合并标签类别和坐标
correct = process_batch(predn, labelsn, iouv) # 计算预测框与标签框的匹配情况
if plots: # 如果启用绘图,则更新混淆矩阵
confusion_matrix.process_batch(predn, labelsn)
else:
# 如果没有标签,则所有预测框均为错误预测
correct = torch.zeros(pred.shape[0], niou, dtype=torch.bool)
# 更新评估统计信息
stats.append((correct.cpu(), pred[:, 4].cpu(), pred[:, 5].cpu(), tcls))
# 保存预测结果到文本文件(如果启用)
if save_txt:
save_one_txt(predn, save_conf, shape, file=save_dir / 'labels' / (path.stem + '.txt'))
# 保存预测结果为 COCO 格式 JSON 文件(如果启用)
if save_json:
save_one_json(predn, jdict, path, class_map)
# 调用回调函数
callbacks.run('on_val_image_end', pred, predn, path, names, im[si])
# 如果启用绘图,并且当前批次的索引小于 3,则绘制标签和预测结果
if plots and batch_i < 3:
# 绘制标签结果
f = save_dir / f'val_batch{batch_i}_labels.jpg'
Thread(target=plot_images, args=(im, targets, paths, f, names), daemon=True).start()
# 绘制预测结果
f = save_dir / f'val_batch{batch_i}_pred.jpg'
Thread(target=plot_images, args=(im, output_to_target(out), paths, f, names), daemon=True).start()
指标计算与评估
计算和评估模型的各项指标,然后保存成可视化图表
# 计算评估指标
stats = [np.concatenate(x, 0) for x in zip(*stats)] # 将统计信息中的每一项(如正确预测、置信度、类别等)按批次合并
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)
"""
调用 `ap_per_class` 计算各类的评估指标,包括精度 (p)、召回率 (r)、平均精度 (AP)、F1 值 (f1) 和 AP 对应的类别 (ap_class)。
参数:
stats: 合并后的统计信息
plot: 是否绘制指标图
save_dir: 保存结果的目录
names: 类别名称
"""
ap50, ap = ap[:, 0], ap.mean(1) # 提取 AP@0.5 和平均 AP(所有 IoU 阈值的平均值)
mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean()
"""
计算整体的评估指标:
mp: 平均精度
mr: 平均召回率
map50: 平均 AP@0.5
map: 平均 AP(所有 IoU 阈值的平均值)
"""
nt = np.bincount(stats[3].astype(np.int64), minlength=nc) # 统计每类的目标数量
else:
nt = torch.zeros(1) # 如果没有统计信息,目标数量为 0
# 打印整体结果
pf = '%20s' + '%11i' * 2 + '%11.3g' * 4 # 格式化输出字符串
print(pf % ('all', seen, nt.sum(), mp, mr, map50, map))
"""
打印评估指标:
all: 表示所有类别的整体结果
seen: 总共处理的图像数
nt.sum(): 总目标数量
mp: 平均精度
mr: 平均召回率
map50: 平均 AP@0.5
map: 平均 AP
"""
# 打印每个类别的评估结果
if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats):
"""
如果启用了 verbose 模式,或者类别数少于 50 且不在训练模式中,则逐类打印结果。
"""
for i, c in enumerate(ap_class):
"""
遍历每个类别的评估结果:
i: 当前类别的索引
c: 类别编号
"""
print(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i]))
"""
打印该类别的评估指标:
names[c]: 类别名称
nt[c]: 当前类别的目标数量
p[i]: 精度
r[i]: 召回率
ap50[i]: AP@0.5
ap[i]: 平均 AP
"""
# 打印处理速度
t = tuple(x / seen * 1E3 for x in dt) # 计算每张图像的平均处理时间(单位:毫秒)
if not training:
shape = (batch_size, 3, imgsz, imgsz) # 输入图像的形状
print(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {shape}' % t)
"""
打印速度:
pre-process: 预处理时间
inference: 推理时间
NMS: 非极大值抑制时间
shape: 输入图像的形状
"""
# 绘制混淆矩阵
if plots:
confusion_matrix.plot(save_dir=save_dir, names=list(names.values())) # 绘制混淆矩阵并保存到指定目录
callbacks.run('on_val_end') # 运行验证结束时的回调函数
# 保存 JSON 文件
if save_json and len(jdict): # 如果启用了 JSON 保存并且有预测结果
w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else ''
"""
获取权重文件的名称(不带路径和后缀)。
"""
anno_json = str(Path(data.get('path', '../coco')) / 'annotations/instances_val2017.json')
"""
定义 COCO 数据集的注释文件路径。
"""
pred_json = str(save_dir / f"{w}_predictions.json") # 定义预测结果保存路径
LOGGER.info(f'\nEvaluating pycocotools mAP... saving {pred_json}...')
"""
日志记录,提示保存预测结果到 JSON 文件。
"""
with open(pred_json, 'w') as f:
json.dump(jdict, f) # 将预测结果保存为 JSON 文件
# 使用 pycocotools 进行评估
try:
check_requirements(['pycocotools']) # 检查是否安装了 pycocotools
from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval
anno = COCO(anno_json) # 加载 COCO 注释文件
pred = anno.loadRes(pred_json) # 加载预测结果
eval = COCOeval(anno, pred, 'bbox') # 创建评估器
if is_coco: # 如果是 COCO 数据集,设置图像 ID
eval.params.imgIds = [int(Path(x).stem) for x in dataloader.dataset.img_files]
eval.evaluate() # 执行评估
eval.accumulate() # 累计结果
eval.summarize() # 打印评估摘要
map, map50 = eval.stats[:2] # 获取 mAP 和 mAP@0.5
except Exception as e:
LOGGER.info(f'pycocotools unable to run: {e}') # 如果出现异常,记录日志
# 返回结果
model.float() # 将模型切换为 FP32 精度
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 # 初始化每类的 mAP 为整体 mAP
for i, c in enumerate(ap_class): # 遍历每个类别
maps[c] = ap[i] # 更新每类的 mAP
return (mp, mr, map50, map, *(loss.cpu() / len(dataloader)).tolist()), maps, t
"""
返回最终结果:
mp: 平均精度
mr: 平均召回率
map50: 平均 AP@0.5
map: 平均 AP
loss: 损失值
maps: 每类的 mAP
t: 每阶段的时间消耗
"""