查看 Markdown

验证与指标

验证通过 val() 把一个模型跑在一个数据集划分(split)上,返回一个由指标键和 float 值组成的扁平字典。这些键都是字面字符串,你拿到哪些取决于任务,而不是家族。

运行一次验证

val() 接收数据集,返回指标。

Python
from libreyolo import LibreYOLO model = LibreYOLO("LibreYOLO9s.pt")metrics = model.val(data="coco8.yaml") print(metrics["metrics/mAP50-95"])print(metrics["metrics/mAP50"])print(metrics["speed/total_ms"])
CLI
libreyolo val model=LibreYOLO9s.pt data=coco8.yaml
在其他划分上
from libreyolo import LibreYOLO model = LibreYOLO("LibreYOLO9s.pt")metrics = model.val(data="coco8.yaml", split="train", batch=4) print(metrics)

返回值是一个由指标键到数值组成的扁平 dict。每个键都是字面量,所以按名字读,而不是按 位置读。对于检测、分割和分类,它是一个 dict 子类,还带有作为属性的逐类别结果,见下文。

主要参数是 data、split、batch、imgsz、conf、iou、workers、device、 augment、save_json 和 verbose。conf 默认是 0.001,iou 默认是 0.6, 两个都比预测的默认值宽松得多,因为一次 mAP 扫描需要低置信度的那条长尾。imgsz 默认取模型自身的输入尺寸,而不是一个固定数字。split 只接受 val、test 或 train,别的都不行。不传 data 时,训练过的检查点会在它训练所用的数据集上验证; 已发布的权重不携带数据集,需要传 data=。

验证配置里的其他字段都会作为关键字参数透传,包括 save_dir、max_det、 eval_max_det、half、amp_dtype、cache、save_plots、single_cls 和 agnostic_nms。在 CUDA 上设 half=True 时,验证在 amp_dtype 的 autocast 下运行,bfloat16 的输出会在后处理之前转成 float32;用 amp_dtype="bfloat16" 训练时的验证也是这样做的。

agnostic_nms=True 会在重叠超过 iou 的检测框之间,无论类别如何,只保留得分最高的那一个。它作用在家族已经完成的检测结果上,在 classes= 之后,所以 mAP、混淆矩阵、曲线和逐图像计数打分的都是同一批检测框。它覆盖检测和分割,任何其他任务都会抛出 ValueError。预测一侧见阈值与过滤。

single_cls=True 把检测当作单一类别来打分。真值被合并为类别 0,模型的检测按模型自身标签空间里的 classes= 保留,然后重新标为 0,所以一个在多个类别上训练的检查点会被当作单类别检测器来打分。

每种任务的指标键

目标检测返回 COCO 那一族数字:

metrics/mAP50-95   metrics/mAP50    metrics/mAP75metrics/mAP_small  metrics/mAP_medium  metrics/mAP_largemetrics/AR1  metrics/AR10  metrics/AR100  metrics/AR_max_detmetrics/AR_small  metrics/AR_medium  metrics/AR_largemetrics/precision  metrics/recallmetrics/precision(B)  metrics/recall(B)  metrics/mAP50(B)  metrics/mAP50-95(B)

其中有两个是坑。metrics/precision 和 metrics/recall 是为向后兼容保留的别名: 它们装的是 mAP 50-95 和 AR@100 的值,而不是一对查准率和查全率。用带名字的键。

实例分割把上面那些 mAP 和 AR 数字当作掩码(mask)数字放在不带后缀的键下,检测框 版本放在 (B) 后缀下,掩码版本在 (M) 下再重复一遍。这个任务里的查准率和查全率 只有带后缀的形式,也就是 metrics/precision(B)/metrics/recall(B) 和 metrics/precision(M)/metrics/recall(M),两对装的都是和 detect 一样的别名值: (B) 那对是检测框 mAP50-95 和检测框 AR@100,(M) 那对是掩码 mAP50-95 和掩码 AR@100。

任务键
detectmetrics/mAP50-95、metrics/mAP50、metrics/mAP75,以及上面按尺寸和查全率的细分
segment上面 detect 那些键的掩码版本(不带后缀的键就是掩码);precision/recall 只以 (B)/(M) 的形式存在,两者的别名方式相同
posemetrics/keypoints_mAP50-95、metrics/keypoints_mAP50、metrics/keypoints_mAP75、metrics/keypoints_mAP_M、metrics/keypoints_mAP_L,以及对应的 keypoints_AR 各键
obbmetrics/mAP50-95、metrics/mAP50、metrics/mAP75、metrics/precision、metrics/recall,外加带 (OBB) 后缀的副本
classifymetrics/accuracy_top1、metrics/accuracy_top5
semanticmetrics/mIoU、metrics/pixel_accuracy
panopticmetrics/PQ、metrics/SQ、metrics/RQ、metrics/PQ_things、metrics/PQ_stuff、metrics/categories
depthmetrics/abs_rel、metrics/rmse、metrics/delta1、metrics/delta2、metrics/delta3
normalmetrics/mean_angular_error、metrics/median_angular_error、metrics/within_11_25、metrics/within_22_5、metrics/within_30
edgemetrics/ODS、metrics/OIS、metrics/best_threshold
restoremetrics/PSNR、metrics/SSIM
mattemetrics/MAE、metrics/Smeasure
ocrmetrics/det_precision、metrics/det_recall、metrics/det_hmean、metrics/e2e_precision、metrics/e2e_recall、metrics/e2e_f1、metrics/rec_1-NED
pointmetrics/precision、metrics/recall、metrics/f1、metrics/MLE、metrics/MAE、metrics/RMSE,外加一个 mAP 扫描键

OBB 的 metrics/precision 和 metrics/recall 不是别名:它们是 IoU 0.50 下真实的 查准率和查全率,取在最宽松的工作点上(每一条挺过 conf 的预测,默认 0.001)。 带 (OBB) 后缀的副本以任务专属的名字把同样这四个值重复一遍,和上面 (B) 与 (M) 的约定一样。

accuracy_top5 其实是 top-min(5, num_classes),所以在一个三类的数据集上它就是 top-3,每个样本都满足,因此读数是 1.0。分类结果还把这两个值暴露为属性 top1 和 top5,与 confusion_matrix 放在一起。

point 任务的扫描键是按距离阈值拼出来的,所以用默认值时它是 metrics/mAP@[0.01:0.10],单阈值的那个键是 metrics/mAP@0.01。传入 dist_thresholds 会把这两个字符串都改掉。

大多数任务还会返回一个 fitness 键,也就是最佳检查点(checkpoint)选择默认用的 那个单一数字。目标检测、分割和 OBB 不带这个键;它们的家族按 metrics/mAP50-95 来选,而这个键它们的字典确实会返回。姿态既不返回 fitness 也不返回 metrics/mAP50-95;它的训练器改把 best_metric_key 设成 metrics/keypoints_mAP50-95。

ImageFolder 分类新增宏平均 metrics/precision、metrics/recall 和 metrics/f1,对验证目标中出现的类别取平均。top-1 仍是默认适应度。检测还返回 metrics/best_conf 和 metrics/best_conf_f1,在 IoU 0.50 下选择 micro-F1 最优阈值,并把以类别名称为键的阈值放在 metrics.box.best_conf_per_class 上。同分检测保持分组;并列时选择更高的阈值。没有正 F1 时返回 0.0。分割不提供这些阈值键。

混淆矩阵

检测、分割和分类的结果带有 results.confusion_matrix。每次验证都会计算它,不管有没有写出图表,它的 matrix 是一个整数数组,索引为 matrix[predicted, true]。

混淆矩阵
from libreyolo import LibreYOLO model = LibreYOLO("LibreYOLO9s.pt")metrics = model.val(data="coco8.yaml") cm = metrics.confusion_matrixprint(cm.matrix.shape)      # (81, 81):80 个类别加背景print(cm.class_accuracy())  # 类别名:找到的目标中被判为该类别的占比tp, fp = cm.tp_fp()

对于检测和分割,置信度达到 0.25(或更高的 conf)的预测,会与任意类别的真值检测框在 IoU 0.5 下一对一配对,IoU 高的优先。一个配对按它预测的类别和真实的类别计数,所以位置准确但类别错误的框会落在对角线之外。矩阵多出一个 background 行和列:没有配上的真值计入 background 行,没有配上的预测计入 background 列。分割配对的是检测框,不是掩码。设了 classes= 时,其他类别的真值会被排除。

COCO 的 crowd 区域(iscrowd=1)遵循 mAP 所用的规则。一个没有被其类别的任何真值匹配到的预测,如果至少有一半落在同类别的 crowd 区域内(交集除以预测自身的面积),就会被忽略而不是被计数。box.image_metrics 和 visualize=True 的图像使用同样的阈值和同样的 crowd 规则。YOLO 格式的标注没有 crowd 区域。

对于分类,每张图像只计一次,按它的 top-1 类别对照它的标签,没有 background。

方法返回
summary(normalize=False)每个预测类别一个字典,{"Predicted": name, <true class>: value};设 normalize 时是占每个真实类别的比例
to_csv()、to_json()以 CSV 或 JSON 文本形式给出的汇总
to_df()以 Polars DataFrame 形式给出的汇总;需要 pip install polars
tp_fp()逐类别的真正例(对角线)和假正例(预测行的其余部分)
nonzero()已填充的单元格,以对齐的 (predicted, true, count) 数组给出
class_accuracy()逐类别,其实例被判为该类别的占比;对于检测,只统计找到的目标
plot(normalize=True, save_dir="")写出 confusion_matrix_normalized.png,用 normalize=False 则写出 confusion_matrix.png,并返回路径

超过 2,000 个类别时,不会构建稠密的分类矩阵:matrix、summary()、to_*() 导出和 plot() 会抛错,而 tp_fp()、nonzero() 和 class_accuracy() 仍然可用。

这个类是 libreyolo.validation.ConfusionMatrix,构造方式为 ConfusionMatrix(nc, iou_thres=0.5, conf_thres=0.25, *, names=None, task="detect"),其中 task 是 "detect" 或 "classify"。

逐类别的查准率、查全率和 F1

检测和分割的结果带有每个类别在各个置信度阈值上的查准率、查全率和 F1,在 IoU 0.5 下匹配。

逐类别曲线
from libreyolo import LibreYOLO model = LibreYOLO("LibreYOLO9s.pt")metrics = model.val(data="coco8.yaml") box = metrics.boxprint(box.px.shape, box.f1_curve.shape)  # (1000,) 和 (classes, 1000)for index, f1 in zip(box.ap_class_index, box.f1):    print(model.names[int(index)], round(float(f1), 3))

box.px 保存 1000 个置信度,在 [0, 1] 上均匀分布。box.p_curve、box.r_curve 和 box.f1_curve 是 (classes, 1000) 的数组,每一行对应 box.ap_class_index 的一个条目,即在这个划分里有真值的类别索引,按升序排列。box.p、box.r 和 box.f1 是每个类别在使这些类别平均 F1 最大的置信度上的值。分割时它们全部由检测框算出。运行没有产生曲线时,它们是空数组。

逐图像检测框指标

检测和分割的结果保持字典兼容,同时提供 results.box.image_metrics。每个文件名对应 precision、recall、f1、tp、fp 和 fn,即使关闭可视化,也使用可视化的匹配规则。这里分割统计的是检测框。文件基名重复时,第一次出现后改用完整路径。分母为零时返回 0.0。这些记录不会在分布式 rank 之间汇总。

速度键

每个验证器都会加上计时:

speed/preprocess_ms   speed/inference_ms   speed/postprocess_msspeed/total_ms        speed/total_s        speed/images_seen

这些是整轮运行平均下来的每张图像毫秒数。它们描述的是你运行时的机器和设置,所以从 里面取出来的数字,只有连同硬件、批大小和数值精度一起报告才有意义。

评估后端

目标检测和分割的指标是通过一个 COCO 评估器算出来的,默认的 faster_coco_eval=True 会在装了 faster-coco-eval 包时选用 C++ 后端。没装的 时候,这次运行会回退到 pycocotools,每个进程警告一次:

faster_coco_eval requested but not installed; falling back to pycocotools.Install with: pip install faster-coco-eval

实际跑的是哪个后端会以 last_eval_backend 记录在模型上,CLI 对检测类任务会在输出 里报出来。设置 LIBREYOLO_FASTER_COCO_EVAL 可以从环境覆盖配置里的值。

iou_thresholds 只在 OBB 这条路径上生效。COCO 那条路径按它自己固定的 0.50 到 0.95 扫描来评估,忽略这个值。

验证损失

默认情况下验证只报告精度。val_loss=True 会在验证批次上同时计算这个家族的训练 目标。

Python
from libreyolo import LibreYOLO model = LibreYOLO("LibreYOLO9s.pt")model.train(data="coco8.yaml", epochs=10, val_loss=True)

它会输出 metrics/loss,外加每一项对应的一个 metrics/loss/<component>,加权 方式和训练时完全一致,所以各分量加起来正好是总和。经过 logger 时它们显示为 val/loss 和 val/loss/<component>,而 libreyolo monitor 会把 metrics/loss 和 train/loss 叠在一起画。

各分量是每个家族自己的:

任务家族分量
detectyolo9、yolo9_p2、yolo9_e2ebox、cls、dfl
detectyolonascls、iou、dfl
detectrfdetrce、bbox、giou
detectrtdetr、rtdetrv2vfl、bbox、giou
detectdfinevfl、bbox、giou、fgl、ddf
detectdomedetrvfl、bbox、giou、fgl、ddf、defe_density、defe_reg
detectdeim、deimv2、rtdetrv4、ecmal、bbox、giou、fgl、ddf
detectrtmdetcls、bbox
detectpicodetcls、bbox、dfl
detectyoloxiou、obj、cls、l1
detectyolo7iou、obj、cls
pointfomoce
classifyresnet、convnext、mobilenetv4、efficientnetv2ce
semanticsegformer、lingbotvision、dinov2sem
restorenafnetrestore

它默认关闭,因为标签分配(target assignment)会给验证增加时间和内存开销。验证器 复用已经为精度指标产生的模型输出,而不是再跑一次前向,它在评估模型或 EMA 模型上以 no_grad 运行,多卡训练下则在 rank 0 上本地计算,不做任何集合通信。最佳检查点的 选择仍然按精度指标来。

有三件事它是故意不做的。它从不包含对比去噪(contrastive denoising)项,因为那些项 在前向时需要真值(ground truth),而验证的前向拿不到真值。它报告的是评估模式下的 模型,所以当一个家族的训练前向和评估前向确实不同时,比如在 BatchNorm 统计量或者 随机深度(stochastic depth)上,数字反映的是评估模式;这正是想要的那种对比。还有, 家族没有为某个任务实现它时,会在准备阶段抛出一个配置错误,而不是悄悄跳过:

val_loss=True currently supports RF-DETR detection only; segment, pose, OBB,classify, and semantic tasks are not supported

FOMO 是那个什么都不改变的例外:它的验证器一直都在算这个损失,val_loss=True 只 影响它发布在哪些键下。

带数据增强的验证和验证损失不能一起用,两个都要就会抛错。

一次验证会写出的文件

val() 总会把 config.yaml 写进它的保存目录,没给 save_dir 时这个目录默认是 runs/val/<model>_<size>_<timestamp>。project、name 和 exist_ok 按训练的方式选择这个 目录:project/name,除非 exist_ok=True,否则加上递增后缀。

写出 COCO 格式的预测
from libreyolo import LibreYOLO model = LibreYOLO("LibreYOLO9s.pt")model.val(data="coco8.yaml", save_json=True, save_dir="runs/val/exp")

save_json=True 对目标检测写出 predictions.json,对分割写出 predictions_bbox.json 和 predictions_masks.json。OBB 不支持它,而且会明说。

save_plots=True,或者它的别名 plots=True,会写进一个 plots/ 子目录,并且需要 matplotlib,pip install "libreyolo[plots]" 会装上它。目标检测会拿到 box_metrics.png、 各类别的 AP 和查全率图表、查准率-查全率曲线和置信度曲线、f1_conf_box.png 里的逐类别 F1 曲线、confusion_matrix.png 里以计数表示的混淆矩阵、confusion_matrix_normalized.png 里以各真实类别占比表示的混淆矩阵,装了 OpenCV 时还有标注过的样本图像。分割会加上指标图表、 逐类别图表以及查准率-查全率曲线和置信度曲线在掩码那侧的副本;F1 曲线和混淆矩阵仍然是检测框层面的。 姿态有自己的一套指标和曲线。分类会写出这两个混淆矩阵文件,超过 2,000 个类别时除外。其他验证器没有实现图表;语义分割、全景分割、深度估计、法向估计、 边缘检测、图像复原、抠图、OCR、OBB 和点检测在那里什么都不写。画图失败只会警告, 绝不会中止这次运行。

visualize=True 为检测和分割写出考虑类别的检测框 TP/FP/FN 图像,为 ImageFolder 分类写出标签与 top-1 的对照图像,分别放入 visualize/errors/ 和 visualize/correct/。匹配使用 IoU 0.5 和置信度 max(0.25, conf)。默认值为 visualize=False、show_labels=True 和 show_conf=True。不支持的任务以及 V-JEPA 2 视频片段验证会拒绝可视化。

plot_samples=8 限制单独的样例图数量;0 禁用,-1 保留全部图像。这不会改变指标或可视化输出。

训练过程中的验证

训练每隔 eval_interval 轮,并且总在最后一轮之后,在数据集的 val 划分上验证一次, 它产生的指标就是驱动 best.pt 选择、patience 提前停止以及每个 logger 里 val/ 各键的东西。EMA 打开时,验证跑在 EMA 权重上。它的文件写入运行目录里的 val/ 目录。 val=False 会关闭训练中的验证,最后一轮也包括在内。

eval_interval、patience 和 save_plots 见 超参数, 数字最后去了哪里见 实验 logger。

  • 数据集,验证器读取的划分键和格式都在那里。

已针对 LibreYOLO v1.6.0 验证。