012-YOLO11 COCO 指标与 TIDE 分析:把“效果不好”拆成具体误差

本文基于 Ultralytics 8.3.253 的验证输出整理。COCO 指标告诉我们模型整体表现,TIDE 这类误差分析工具可以进一步拆分定位错误、分类错误、重复检测、漏检和背景误检。本文只写分析流程,不编造任何检测结果。

摘要

做 YOLO11 目标检测实验时,经常会遇到一句很笼统的话:模型效果不好。问题是,“不好”到底是漏检多,还是分类错,还是框偏,还是重复框太多?如果不拆开分析,后续改进很容易盲目加注意力、换 Neck 或改损失函数。

本文从 COCO 风格指标和 TIDE 误差分析两个角度整理排查思路。先说明 mAP50、mAP50-95、Precision、Recall 各自能回答什么问题,再给出 YOLO 标签转 COCO GT、Ultralytics 导出预测 JSON、使用 TIDE 分析错误类型的流程。本文适合后续有真实数据集和验证结果后补充实测分析。

关键词: 012、YOLO11、COCO 指标、TIDE、目标检测误差分析、mAP50、mAP50-95、Ultralytics


一、为什么要把“效果不好”拆开

如果只看一个 mAP,很多问题会被混在一起:

表面现象 可能原因
mAP50 低 目标没有被稳定检出,或分类错误较多
mAP50 还行,mAP50-95 低 框大概对,但定位不够准
Precision 低 背景误检或重复检测较多
Recall 低 漏检明显,模型没找全
某类指标很差 类别样本少、标注不一致或类别混淆

后续改 YOLO11 结构时,要先知道问题属于哪一类。否则加了模块以后,即使指标变化,也很难解释为什么。


二、COCO 指标先看什么

Ultralytics 检测任务中常见的四个指标为:

metrics/precision(B)
metrics/recall(B)
metrics/mAP50(B)
metrics/mAP50-95(B)

可以按下面方式理解:

指标 主要含义
Precision 预测框中有多少是真的,重点看误检
Recall 真实目标中有多少被检出,重点看漏检
mAP50 IoU=0.5 下的平均检测表现
mAP50-95 IoU 从 0.5 到 0.95 多阈值平均表现

如果 mAP50 和 mAP50-95 差距很大,通常说明定位质量还不够稳定。此时优先检查标注框、输入尺寸、小目标比例、回归损失和检测头,而不是只看分类能力。


三、TIDE 能拆哪些错误

TIDE 是面向目标检测和实例分割的误差分析工具,它的价值在于把总体 AP 损失拆成多个错误来源。常见错误方向可以理解为:

错误类型 说明
Classification 类别预测错误
Localization 框位置不够准
Both 类别和定位都错
Duplicate 同一个目标被重复检测
Background 背景被误检成目标
Missed 真实目标没有被检测出来

这比只看一个 mAP 更适合指导改进。例如:

  1. Localization 错误高:优先关注标注质量、框回归、检测头和输入尺寸;
  2. Missed 错误高:关注小目标、特征融合、召回率和数据增强;
  3. Background 错误高:关注负样本、置信度、类别边界和背景纹理;
  4. Duplicate 错误高:关注 NMS 和候选框筛选。

四、准备 COCO 格式 GT

TIDE 分析需要 COCO 格式的标注和预测。YOLO 数据集通常是 txt 标签,所以需要先转成 COCO GT。

新建 yolo_labels_to_coco_gt.py

import argparse
import json
from pathlib import Path

import yaml
from PIL import Image


IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".bmp", ".webp"}


def resolve_path(value, yaml_dir, dataset_root):
    path = Path(value)
    if path.is_absolute():
        return path
    root_path = dataset_root / path
    if root_path.exists():
        return root_path
    return yaml_dir / path


def collect_images(split_value, yaml_dir, dataset_root):
    split_path = resolve_path(str(split_value), yaml_dir, dataset_root)
    if split_path.is_dir():
        return sorted(p for p in split_path.rglob("*") if p.suffix.lower() in IMAGE_SUFFIXES)
    if split_path.is_file() and split_path.suffix.lower() == ".txt":
        images = []
        for line in split_path.read_text(encoding="utf-8").splitlines():
            item = Path(line.strip())
            if not item:
                continue
            images.append(item if item.is_absolute() else split_path.parent / item)
        return images
    return []


def label_path_for_image(image_path):
    parts = list(image_path.parts)
    for i in range(len(parts) - 1, -1, -1):
        if parts[i].lower() == "images":
            parts[i] = "labels"
            return Path(*parts).with_suffix(".txt")
    return image_path.with_suffix(".txt")


def normalize_names(names):
    if isinstance(names, dict):
        return {int(k): str(v) for k, v in names.items()}
    if isinstance(names, list):
        return {i: str(v) for i, v in enumerate(names)}
    return {}


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--data", required=True)
    parser.add_argument("--split", default="val")
    parser.add_argument("--out", default="coco_gt.json")
    args = parser.parse_args()

    data_yaml = Path(args.data)
    yaml_dir = data_yaml.parent
    data = yaml.safe_load(data_yaml.read_text(encoding="utf-8"))
    dataset_root = resolve_path(data.get("path", yaml_dir), yaml_dir, yaml_dir)
    names = normalize_names(data.get("names", {}))

    images = collect_images(data.get(args.split), yaml_dir, dataset_root)

    coco = {
        "images": [],
        "annotations": [],
        "categories": [{"id": i + 1, "name": names.get(i, f"class_{i}")} for i in sorted(names)],
    }

    ann_id = 1
    for image_id, image_path in enumerate(images, start=1):
        width, height = Image.open(image_path).size
        coco["images"].append({
            "id": image_id,
            "file_name": image_path.name,
            "width": width,
            "height": height,
        })

        label_path = label_path_for_image(image_path)
        if not label_path.exists():
            continue

        for line in label_path.read_text(encoding="utf-8").splitlines():
            parts = line.strip().split()
            if len(parts) < 5:
                continue
            cls, xc, yc, bw, bh = map(float, parts[:5])
            box_w = bw * width
            box_h = bh * height
            x = xc * width - box_w / 2
            y = yc * height - box_h / 2
            coco["annotations"].append({
                "id": ann_id,
                "image_id": image_id,
                "category_id": int(cls) + 1,
                "bbox": [x, y, box_w, box_h],
                "area": box_w * box_h,
                "iscrowd": 0,
            })
            ann_id += 1

    Path(args.out).write_text(json.dumps(coco, ensure_ascii=False), encoding="utf-8")
    print(f"saved: {args.out}")


if __name__ == "__main__":
    main()

运行:

python yolo_labels_to_coco_gt.py --data G:/datasets/my_dataset/data.yaml --split val --out runs/tide/coco_val_gt.json

这一步会把 YOLO 验证集标签转成 COCO GT 格式。


五、导出 YOLO11 预测 JSON

使用 Ultralytics 验证时打开 save_json

yolo detect val model=runs/yolo11_baseline/yolo11n_s0_e100_i640/weights/best.pt data=G:/datasets/my_dataset/data.yaml imgsz=640 batch=16 device=0 save_json=True project=runs/tide name=baseline_val

运行完成后,结果目录中会生成预测 JSON,例如:

runs/tide/baseline_val/predictions.json

如果没有生成,先检查:

  1. 验证集是否有标签;
  2. 是否真的传入 save_json=True
  3. 结果目录是否被写到新的 val2val3
  4. 数据集路径是否正确。

六、运行 TIDE 分析

先安装依赖:

pip install tidecv pycocotools

新建 run_tide_analysis.py

import argparse
from pathlib import Path

from tidecv import TIDE, datasets


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--gt", required=True)
    parser.add_argument("--pred", required=True)
    parser.add_argument("--out", default="runs/tide/tide_outputs")
    args = parser.parse_args()

    out_dir = Path(args.out)
    out_dir.mkdir(parents=True, exist_ok=True)

    tide = TIDE()
    tide.evaluate(datasets.COCO(args.gt), datasets.COCOResult(args.pred), mode=TIDE.BOX)
    tide.summarize()
    tide.plot(str(out_dir))


if __name__ == "__main__":
    main()

运行:

python run_tide_analysis.py --gt runs/tide/coco_val_gt.json --pred runs/tide/baseline_val/predictions.json --out runs/tide/baseline_error

输出内容会包含错误分解结果和图像文件。正式文章里可以把这些结果截图放在“误差分析”部分。


七、怎么根据错误类型决定改进方向

拿到 TIDE 结果后,可以按下面方式分析:

主要错误 优先检查
Classification 高 类别定义、类别样本量、易混类别、分类分支
Localization 高 标注框、输入尺寸、回归损失、检测头
Duplicate 高 NMS、置信度阈值、重复目标
Background 高 负样本、背景纹理、数据清洗
Missed 高 小目标、遮挡、数据增强、特征融合

比如 Missed 错误高时,后面可以更有理由尝试 P2 检测层、小目标增强或更强的浅层特征融合。Localization 高时,则不应该只写“分类能力不足”,而要重点看定位。


八、写进 YOLO11 改进文章时的表格

可以把 COCO 指标和 TIDE 错误放在两张表中:

模型 Precision Recall mAP50 mAP50-95
baseline 用真实结果填写 用真实结果填写 用真实结果填写 用真实结果填写
改进模型 用真实结果填写 用真实结果填写 用真实结果填写 用真实结果填写
模型 分类错误 定位错误 重复检测 背景误检 漏检
baseline 用 TIDE 输出填写 用 TIDE 输出填写 用 TIDE 输出填写 用 TIDE 输出填写 用 TIDE 输出填写
改进模型 用 TIDE 输出填写 用 TIDE 输出填写 用 TIDE 输出填写 用 TIDE 输出填写 用 TIDE 输出填写

这样写比单纯贴一张 mAP 表更清楚。读者能看到改进到底改善了哪类错误。


九、常见问题

1. COCO GT 和 predictions.json 类别对不上

YOLO 类别从 0 开始,COCO category_id 通常从 1 开始。本文脚本已经把 cls + 1 写入 category_id。若你使用自己的转换脚本,要确认两边类别编号一致。

2. TIDE 报找不到图片 id

说明 GT 和预测 JSON 中的 image_id 对不上。需要确保预测文件和 GT 文件来自同一个验证集,并且转换方式一致。

3. 自定义数据集一定要做 TIDE 吗

不是必须。小项目可以先看 results.csv、混淆矩阵和预测图。TIDE 更适合在准备做系统性改进时使用。

4. TIDE 结果能替代 mAP 吗

不能。TIDE 是误差拆解工具,mAP 仍然是主要评价指标。两者应该一起看。

5. 没有真实数据时能写 TIDE 结论吗

不能。没有预测 JSON 和 GT,就只能写流程,不能写哪类错误占比高。


十、总结

本文把 YOLO11 检测效果分析从单一 mAP 扩展到了 COCO 指标和 TIDE 误差拆解。COCO 指标告诉我们整体表现,TIDE 可以进一步分析分类、定位、重复检测、背景误检和漏检等问题。

后续做 YOLO11 改进时,建议先用统一 baseline 得到真实 results.csv,再在需要时导出 COCO 格式预测并运行 TIDE。只有知道错误来自哪里,后面的注意力、Neck、检测头或损失函数改进才更有方向。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐