012-YOLO11-COCO指标与TIDE误差分析-拆解检测效果不好
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 更适合指导改进。例如:
- Localization 错误高:优先关注标注质量、框回归、检测头和输入尺寸;
- Missed 错误高:关注小目标、特征融合、召回率和数据增强;
- Background 错误高:关注负样本、置信度、类别边界和背景纹理;
- 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
如果没有生成,先检查:
- 验证集是否有标签;
- 是否真的传入
save_json=True; - 结果目录是否被写到新的
val2、val3; - 数据集路径是否正确。
六、运行 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、检测头或损失函数改进才更有方向。
更多推荐




所有评论(0)