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真实目标中有多少被检出,重点看漏检
mAP50IoU=0.5 下的平均检测表现
mAP50-95IoU 从 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. 结果目录是否被写到新的 val2、val3;
  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 错误放在两张表中:

模型PrecisionRecallmAP50mAP50-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编程工具,助力开发者即刻编程。

更多推荐