资讯动态

MMDetection 3.x目标检测:classwise查看每类AP定位弱类

发布时间:2026/10/26 5:06:32 来源:尧图企业网站定制
多类目标检测训练到一半最容易被整体 mAP 骗了日志里coco/bbox_mAP看着还行上线后某一类漏得厉害。我这两年在MMDetection 3.x里做多类目标训练基本都会把COCO 格式数据集的每类 mAP打开看看到底是哪个类别在拖后腿。MMDetection 3.x 的CocoMetric自带classwise开关配置里改一行就能在验证和测试阶段输出每个类别的 AP但实际用起来还有几个坑metainfo顺序、ann_file路径、类别 id 不连续、无 GT 类别、pkl 结果离线复算。下面按我的实际操作顺序拆开讲适合刚上手 MMDetection 3.x 的同学也适合已经能跑通训练、想进一步定位弱类的朋友。1. 为什么多类目标只看整体mAP不够1.1 整体mAP会把类别短板平均掉整体 mAP 是所有类别 AP 的平均值听起来公平实际上很容易掩盖问题。一个 10 类项目里如果 7 个大类 AP 都在 0.6 以上3 个小类只有 0.1整体 mAP 可能还停在 0.45 左右看起来“还能继续调”但真正影响业务的是那 3 个小类。更麻烦的是整体 mAP 上涨 1 个点可能是大类涨了 3 个点、小类掉了 2 个点平均之后你根本看不出来。我在模拟项目 A 里就遇到过这种情况整体 mAP 从 0.512 涨到 0.521团队以为模型变稳了结果一查每类 AP发现“安全帽”这一类从 0.43 掉到 0.36而“人”这一类从 0.62 涨到 0.71平均之后把问题盖住了。后来我们把classwiseTrue作为验证阶段的固定配置每次看结果先扫一遍类别 AP再决定要不要继续训练。多类目标检测的评估不能只看一个总数原因在于 COCO 评估本身是把每个类别单独算 AP再做平均。也就是说COCOeval 的中间结果里本来就存着每个类别的 precision只是默认的summary()只打印整体指标。MMDetection 3.x 的CocoMetric把这个中间结果利用起来通过classwise参数把每个类别的 AP 追加到日志里。理解这一点之后你就知道每类 AP 不是“额外功能”而是 COCO 评估本来就有的信息只是平时没打开。1.2 classwise参数与COCOeval精度矩阵的关系CocoMetric的classwiseTrue背后读的是self.coco_eval[metric].eval[precision]。这个 precision 是一个五维数组维度大致是[T, R, K, A, M]T是 IoU 阈值数量COCO 默认 10 个从 0.5 到 0.95R是 recall 采样点默认 101 个K是类别数量A是面积范围通常包含 all、small、medium、largeM是 maxDets默认 1、10、100。计算某个类别的 AP就是固定类别索引K、面积范围 all、maxDets 100然后把 IoU 维度和 recall 维度上有效的 precision 取平均再乘 100。默认整体 mAP 是对K维度再平均一次所以每类 AP 和整体 mAP 的算法完全一致不存在“两套标准”的问题。这也解释了一个常见现象某个类别在验证集里没有 GTprecision 会全是 -1算出来就是nan或者 -1。不是代码坏了而是这个类别在验证集里没有标注样本COCOeval 没法算。还有人看到某类 AP 是 0.000先别急着调模型先确认验证集里这个类到底有没有框、框的数量是不是个位数。类别样本太少时AP 的波动会非常大今天 0.2 明天 0.05 都正常这时候更应该看 PR 曲线和误检样例而不是盯着一个小数点。1.3 适合用每类AP排查的典型场景第一类场景是类别不平衡。比如工业质检里“划痕”样本很多“凹坑”样本很少整体 mAP 会被“划痕”拉高你必须看每类 AP 才知道“凹坑”是不是已经崩了。第二类场景是类别定义容易混比如“轿车”和“SUV”、“货车”和“客车”整体指标看不出来但每类 AP 一摆出来往往能发现某两个类别互相抢框。第三类场景是上线前验收业务方通常不关心整体 mAP只关心他负责的那几类漏检率这时候每类 AP 和每类 AR 比整体数字更有说服力。第四类场景是换 backbone、换 neck、换损失函数之后的回归对比整体 mAP 可能只差 0.2但每类 AP 的结构变化能告诉你这个改动到底影响了谁。我一般会把每类 AP 和每类的 GT 数量放在同一张表里看。GT 数量很少的类别AP 低不一定是模型差可能是验证集本身不够GT 数量很多的类别AP 低就值得重点查因为样本足够还学不好通常是类别定义、标注质量、anchor 匹配或者 NMS 阈值出了问题。这个判断顺序很重要先看数据再看模型能少走很多弯路。2. 动手前的环境与数据准备2.1 版本对齐与依赖安装MMDetection 3.x 和 2.x 的配置体系差别很大3.x 基于 MMEngine评估器、数据加载器、可视化都换了写法。你要打开每类 AP第一步不是改模型而是确认版本对齐。我的习惯是先固定一组能跑通的版本再谈调参。Python 建议 3.8 到 3.10PyTorch 和 CUDA 按机器来MMCV 要用 2.x 系列MMDetection 用 3.x 系列。安装时不要混装 2.x 的 mmcv-full 和 3.x 的 mmcv否则 import 阶段就会报一堆注册器错误。下面是一组常见的安装顺序具体小版本按你的环境微调conda create -n mmdet3x python3.9 -y conda activate mmdet3x pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install -U openmim mim install mmcv2.0.0 pip install mmdet3.0.0装完之后做一次最小自检确认mmdet、mmcv、mmengine都能正常导入并且版本号符合预期python -c import mmdet, mmcv, mmengine, torch; print(mmdet.__version__, mmcv.__version__, mmengine.__version__, torch.__version__)这里有个实际经验如果你之前跑过 2.x 的项目环境里可能残留旧版 mmcvpip install mmdet不一定帮你清理干净。最稳的办法是新建 conda 环境不要在旧环境里反复升降级。另一个经验是classwise参数在 3.x 的CocoMetric里已经支持但不同小版本的输出字段名可能略有差异后面看日志时以你当前版本的源码为准不要拿别人的截图硬套。2.2 COCO格式数据集目录和标注检查COCO 格式数据集的目录结构要固定训练集和验证集分开标注文件放在annotations下图片放在对应目录下。一个常见的目录长这样data/custom_coco/ ├── annotations/ │ ├── instances_train.json │ └── instances_val.json ├── train/ │ └── *.jpg └── val/ └── *.jpg标注文件里至少要有images、annotations、categories三个字段。categories里的id可以是任意整数不要求从 0 开始连续但你要记住模型输出的类别索引是 0 到num_classes-1而 COCO 评估用的是原始category_id。MMDetection 的CocoMetric会通过ann_file解析类别 id再做映射所以metainfo里的类别顺序要和标注文件里的类别顺序保持一致。很多人在这里翻车训练能跑评估也不报错但每类 AP 的名字和类别对不上原因就是metainfo.classes的顺序和 json 里的categories顺序不一致。我一般会先跑一个小脚本把类别 id、类别名、训练集和验证集的框数量打印出来。这个脚本很短但能提前发现“验证集缺类”“类别 id 重复”“某个类只有 3 个框”这类问题import json from collections import Counter ann_file data/custom_coco/annotations/instances_val.json with open(ann_file, r, encodingutf-8) as f: data json.load(f) cats {c[id]: c[name] for c in data[categories]} counter Counter(a[category_id] for a in data[annotations]) for cid, name in cats.items(): print(fcategory_id{cid:5d} name{name:20s} gt_num{counter.get(cid, 0)})输出里如果某个类gt_num0就要提前知道这个类在验证集上算不出 AP后面日志里出现nan或-1不要慌。如果某个类gt_num只有个位数也不要指望它的 AP 稳定最好补充验证样本或者接受它的大波动。2.3 类别映射与metainfo配置MMDetection 3.x 的数据集配置里有一个metainfo字段常见写法是metainfo dict(classes(cat, dog, person))。这里的顺序会影响类别索引也会影响可视化时显示的类别名。我的建议是metainfo.classes的顺序严格按 COCO json 里categories的顺序来不要按文件夹名、不要按标注工具里的显示顺序、不要按你个人喜好排。因为CocoMetric在做 classwise 输出时类别名来自ann_file但模型训练时的类别索引来自metainfo两边不一致就会出现“AP 数值对得上、类别名对不上”的尴尬情况。如果你不想手工维护顺序可以在训练前用脚本生成metainfo片段import json ann_file data/custom_coco/annotations/instances_train.json with open(ann_file, r, encodingutf-8) as f: data json.load(f) classes tuple(c[name] for c in data[categories]) print(metainfo dict(classes%r) % (classes,)) print(num_classes , len(classes))把输出直接粘到配置里减少手抄错误。注意num_classes要等于类别数量不是最大category_id加一。比如类别 id 是 1、3、7类别数是 3不是 8。很多自定义数据集用num_classes max(cat_id) 1训练时不会立刻报错但分类分支会多出空类评估时每类 AP 的索引也可能错位。2.4 配置文件最小改动清单在 MMDetection 3.x 里一个多类自定义数据集配置通常继承基础配置然后改数据路径、类别数、评估器。下面是一个简化示例模型部分用 Faster R-CNN 示意你可以换成自己的检测器_base_ [ ../_base_/models/faster-rcnn_r50_fpn.py, ../_base_/datasets/coco_detection.py, ../_base_/schedules/schedule_1x.py, ../_base_/default_runtime.py ] data_root data/custom_coco/ metainfo dict(classes(cat, dog, person)) num_classes len(metainfo[classes]) model dict( bbox_headdict(num_classesnum_classes) ) train_dataloader dict( datasetdict( data_rootdata_root, metainfometainfo, ann_fileannotations/instances_train.json, data_prefixdict(imgtrain/) ) ) val_dataloader dict( datasetdict( data_rootdata_root, metainfometainfo, ann_fileannotations/instances_val.json, data_prefixdict(imgval/) ) ) test_dataloader val_dataloader val_evaluator dict( typeCocoMetric, ann_filedata_root annotations/instances_val.json, metricbbox, classwiseTrue, format_onlyFalse, outfile_prefixwork_dirs/custom_coco/val_result ) test_evaluator val_evaluator这份配置里有三个点最关键bbox_head.num_classes改成真实类别数metainfo.classes顺序和 json 一致val_evaluator和test_evaluator都打开classwiseTrue。outfile_prefix建议加上后面离线复算每类 AP 时会用到它保存的结果文件。注意outfile_prefix的目录要存在或者至少父目录可写否则评估结束保存结果时可能报路径错误。3. 在训练与测试配置里打开每类AP3.1 val_evaluator与test_evaluator的classwise配置训练阶段的验证由val_evaluator控制测试阶段的评估由test_evaluator控制。很多人只改了val_evaluator训练日志里能看到每类 AP但单独跑tools/test.py时又只剩整体 mAP就是因为test_evaluator没改。最省事的写法是test_evaluator val_evaluator让两边共用同一份配置这样验证和测试的评估口径一致不会出现“训练日志 mAP 0.52、测试脚本 mAP 0.49”的口径差异。如果你只想临时在测试时打开不想改配置文件可以用--cfg-options覆盖但这种方式对嵌套 dict 的覆盖容易写错我更推荐直接改配置。下面这条命令是临时覆盖的写法适合快速验证python tools/test.py \ configs/custom_coco/faster-rcnn_r50_fpn.py \ work_dirs/custom_coco/epoch_12.pth \ --cfg-options test_evaluator.classwiseTrue跑完之后终端会打印整体指标和每类 AP。如果你发现终端输出很长、类别太多不好翻可以把结果重定向到日志文件再用grep过滤python tools/test.py configs/custom_coco/faster-rcnn_r50_fpn.py \ work_dirs/custom_coco/epoch_12.pth \ --work-dir work_dirs/custom_coco/test_epoch12 21 | tee test_epoch12.log grep -E coco/bbox_.*_AP|coco/bbox_mAP test_epoch12.log3.2 训练时验证与离线测试命令训练命令本身不复杂关键是--work-dir要固定后面找 checkpoint 和日志都靠它python tools/train.py \ configs/custom_coco/faster-rcnn_r50_fpn.py \ --work-dir work_dirs/custom_coco训练过程中如果配置了val_interval每个验证周期都会跑一次评估。打开classwiseTrue后验证日志里会多出每个类别的 AP。我的习惯是把训练日志里的每类 AP 单独抽出来按 epoch 存成表格这样能看出弱类是“一直弱”还是“中途掉下去”。如果是中途掉下去通常和某个 epoch 的学习率变化、数据增强、过拟合有关如果一直弱优先查标注和样本量。训练结束后单独测试python tools/test.py \ configs/custom_coco/faster-rcnn_r50_fpn.py \ work_dirs/custom_coco/epoch_12.pth \ --work-dir work_dirs/custom_coco/test_epoch12测试时注意 checkpoint 的路径和配置要匹配。换过num_classes的配置不要去加载旧类别数的权重否则分类分支形状对不上直接报 size mismatch。如果你确实要加载旧权重做微调记得用load_from并配合--cfg-options model.bbox_head.num_classes...之前先确认分类层是否被正确重初始化不要想当然。3.3 输出结果怎么读字段含义与计算逻辑打开classwiseTrue后日志里常见字段包括整体的coco/bbox_mAP、coco/bbox_mAP_50、coco/bbox_mAP_75以及每个类别的coco/bbox_类别名_AP。不同小版本可能把类别名放在字段中间也可能用类别 id 代替具体以你终端打印为准。读的时候先看整体再看每类最后看每类的 GT 数量。整体 mAP 高但某类 AP 低说明类别不平衡或者该类难学整体 mAP 低但每类都不算太差可能是类别太多导致平均被拉低也可能是某个大类拖累。每类 AP 的计算逻辑和整体 AP 一致对该类在所有图片上的预测框和 GT 框做匹配按 IoU 阈值 0.5 到 0.95、recall 101 点插值算 precision 均值。你看到的 AP 是 0 到 1 之间的小数乘 100 就是常见的百分数。AP50只取 IoU0.5AP75取 IoU0.75。排查定位问题时我通常先看AP50因为AP50对框的位置没那么苛刻更能反映“有没有找到”再看AP75如果AP50还行但AP75很低说明框回归不准可能是 anchor 尺度、回归损失或者标注框质量问题。这个判断比单看 AP 更有方向感。3.4 一个多类配置片段示例下面这个片段把训练、验证、测试三边的评估器写清楚适合直接抄到自己的配置里改路径。注意metricbbox表示检测框评估如果你做实例分割再加一个metricsegm每类 AP 会分别输出 bbox 和 segm 的结果val_evaluator [ dict( typeCocoMetric, ann_filedata_root annotations/instances_val.json, metricbbox, classwiseTrue, format_onlyFalse, outfile_prefixwork_dirs/custom_coco/val_bbox ), ] test_evaluator val_evaluator如果你的配置里原本val_evaluator是一个 dict不是 list也可以直接写 dict写成 list 是为了以后同时加 segm 指标。无论哪种写法核心是typeCocoMetric、ann_file指向验证集标注、classwiseTrue。还有一点ann_file不要写成训练集标注否则评估时会把训练集当验证集mAP 虚高每类 AP 也失去意义。这个错误很隐蔽因为代码不会报错只是结果好得不像话。4. 用脚本从结果文件提取每类AP4.1 保存pkl结果与读取COCOeval配置里的outfile_prefix会在评估结束后保存结果文件常见有.pkl和.bbox.json两种。拿到结果文件后不用依赖训练日志可以直接用 COCO API 离线复算每类 AP。这样做的好处是你可以对同一个结果文件反复分析不用担心日志被覆盖也可以把不同 epoch 的结果文件放在一起对比。下面这个脚本兼容.pkl和.json并且会输出每类的 AP、AP50、AP75 和 GT 数量。代码不长但很实用import json import pickle import numpy as np from pycocotools.coco import COCO from pycocotools.cocoeval import COCOeval ann_file data/custom_coco/annotations/instances_val.json res_file work_dirs/custom_coco/val_bbox.pkl def load_results(path): if path.endswith(.json): with open(path, r, encodingutf-8) as f: return json.load(f) with open(path, rb) as f: obj pickle.load(f) if isinstance(obj, dict): if bbox in obj: return obj[bbox] for value in obj.values(): if isinstance(value, list): return value return obj coco_gt COCO(ann_file) results load_results(res_file) coco_dt coco_gt.loadRes(results) coco_eval COCOeval(coco_gt, coco_dt, bbox) coco_eval.evaluate() coco_eval.accumulate() coco_eval.summarize() precisions coco_eval.eval[precision] cat_ids coco_eval.params.catIds cat_names {c[id]: c[name] for c in coco_gt.dataset[categories]} gt_counter {} for ann in coco_gt.dataset[annotations]: gt_counter[ann[category_id]] gt_counter.get(ann[category_id], 0) 1 def mean_precision(iou_idx, cat_idx, area_idx0): if iou_idx is None: p precisions[:, :, cat_idx, area_idx, -1] else: p precisions[iou_idx, :, cat_idx, area_idx, -1] p p[p -1] return float(np.mean(p) * 100) if p.size else float(nan) print(f{category_id:12s} {name:20s} {AP:8s} {AP50:8s} {AP75:8s} {gt_num:8s}) for idx, cid in enumerate(cat_ids): ap mean_precision(None, idx) ap50 mean_precision(0, idx) ap75 mean_precision(5, idx) name cat_names.get(cid, str(cid)) gt_num gt_counter.get(cid, 0) print(f{cid:12d} {name:20s} {ap:8.3f} {ap50:8.3f} {ap75:8.3f} {gt_num:8d})这段脚本里iou_idxNone表示在 10 个 IoU 阈值上平均iou_idx0对应 IoU0.5iou_idx5对应 IoU0.75。area_idx0表示 allprecisions[..., -1]取 maxDets100。p[p -1]是过滤无效项避免把 -1 算进平均值。如果你发现某类全是nan先看gt_num是不是 0。4.2 自定义脚本计算每类AP上面的脚本是离线复算适合结果文件已经保存的情况。如果你希望在评估器内部直接拿到每类 AP可以自定义CocoMetric子类。不过在 MMDetection 3.x 里classwiseTrue已经能满足大多数需求自定义子类更多是为了把每类 AP 写到自定义文件、或者追加混淆矩阵之类的信息。下面给一个最小子类示例思路是在compute_metrics之后读取self.coco_eval把每类 AP 追加到返回的 metrics 字典里。实际字段名你可以按团队规范改import numpy as np from mmdet.evaluation import CocoMetric from mmdet.registry import METRICS METRICS.register_module() class ClasswiseCocoMetric(CocoMetric): def compute_metrics(self, results): metrics super().compute_metrics(results) for metric_name, coco_eval in self.coco_eval.items(): precisions coco_eval.eval[precision] cat_ids coco_eval.params.catIds for idx, cid in enumerate(cat_ids): p precisions[:, :, idx, 0, -1] p p[p -1] if p.size 0: continue name self.cat2label.get(cid, cid) if hasattr(self, cat2label) else cid metrics[f{metric_name}_{name}_AP_custom] float(np.mean(p) * 100) return metrics然后把配置里的typeCocoMetric换成自定义注册名。这个写法要注意两点self.coco_eval的 key 可能是bbox或segm遍历时不要写死cat2label不一定存在先hasattr判断。自定义子类适合做二次开发不建议新手一上来就改评估器源码先用classwiseTrue和离线脚本把问题定位清楚再考虑扩展。4.3 输出表格与弱类排序拿到每类 AP 后不要只打印一遍就完事最好按 AP 从低到高排序直接看最弱的几个类。下面这段代码在上一段基础上加一个排序输出适合复制到 notebook 里跑rows [] for idx, cid in enumerate(cat_ids): ap mean_precision(None, idx) ap50 mean_precision(0, idx) ap75 mean_precision(5, idx) rows.append((cat_names.get(cid, str(cid)), cid, ap, ap50, ap75, gt_counter.get(cid, 0))) rows.sort(keylambda x: (np.isnan(x[2]), x[2])) print(f{name:20s} {id:6s} {AP:8s} {AP50:8s} {AP75:8s} {gt:6s}) for name, cid, ap, ap50, ap75, gt_num in rows: print(f{name:20s} {cid:6d} {ap:8.3f} {ap50:8.3f} {ap75:8.3f} {gt_num:6d})下面是一个模拟输出用来说明表格怎么读不是真实项目结果类别名category_idAPAP50AP75GT数量初步判断小目标零件30.0830.2210.06142样本少且目标小先查标注和输入尺度遮挡行人70.1560.3420.102118AP50与AP75差距大框回归或遮挡问题货车20.4120.6810.398860中等看是否与客车混淆轿车10.7030.8910.7423120大类正常注意别继续过拟合这张表比一堆日志直观得多。我的经验是先看 AP 最低且 GT 数量大于 100 的类这类通常有优化空间GT 数量小于 50 的类先把验证集补到至少一两百个框再谈调参否则你调出来的提升可能只是评估波动。4.4 可视化类别AP柱状图如果类别在 10 个以内画一个横向柱状图很快能看出差距。不要用花哨的图简单的横向条形图就够。下面代码用 matplotlib 画图类别名用英文或数字避免中文字体问题import matplotlib.pyplot as plt names [row[0] for row in rows] aps [0 if np.isnan(row[2]) else row[2] for row in rows] plt.figure(figsize(8, max(4, len(names) * 0.35))) plt.barh(names, aps) plt.xlabel(AP) plt.xlim(0, 100) plt.tight_layout() plt.savefig(classwise_ap.png, dpi200)图出来后把 AP 低于 0.2 的类标红或者单独列出来和验证集样例一起看。我通常会抽 20 张弱类图片看看是漏标、错标、目标太小、遮挡太严重还是模型根本没学到。很多“模型问题”其实是标注问题尤其是小类和长尾类标注漏一个框AP 就会明显掉。5. 常见问题与排查5.1 每类AP不输出或字段找不到最常见的原因是classwise没打开或者只改了val_evaluator没改test_evaluator。先确认配置文件里两处都有classwiseTrue再确认你运行的配置就是改过的那份不要出现“改了 A 配置、跑了 B 配置”的低级错误。第二种原因是版本差异某些旧小版本的字段名可能不是coco/bbox_name_AP而是别的拼接方式直接看终端输出或者日志 JSON 里的 key 最稳。第三种原因是metric写成了segm但你的模型没有分割分支评估阶段可能跳过或者报错先统一用metricbbox排查。如果终端完全不打印评估结果检查val_dataloader是否为空、ann_file是否存在、图片路径是否正确。评估阶段如果没有加载到任何样本后面的指标自然不会出现。还有一个小坑outfile_prefix的父目录不存在时有的版本会在保存结果时抛异常导致评估中断。先手动mkdir -p work_dirs/custom_coco再跑测试。5.2 类别名错位或中文类别名导致字段难读类别名错位通常不是评估器的问题而是metainfo.classes和 json 的categories顺序不一致。表现是 AP 数值看着合理但名字和类别对不上比如“猫”的 AP 跑到了“狗”的字段上。排查方法是把metainfo.classes、json 的categories列表、训练时打印的dataset.metainfo三处放在一起对比。最稳的做法是用前面给的脚本从 json 直接生成metainfo不要手写。中文类别名不是不能用但日志字段里出现中文、空格、特殊符号时grep和表格处理都会麻烦。我的建议是训练配置里的metainfo.classes用英文短名或者拼音缩写展示层再做中文映射。这样评估字段干净后期做可视化也方便。如果必须用中文至少保证类别名里没有空格和斜杠否则日志解析容易断行。5.3 AP为-1或nan的处理AP 为 -1 或nan先看这个类在验证集里的 GT 数量。GT 为 0COCOeval 没有正样本算不出 AP这是正常现象。GT 很少比如只有 3 个框AP 可能今天是 0.3 明天是 0.0也不要当成模型崩了。处理办法是补充验证样本或者在看结果时把 GT 数量少于阈值的类单独标记不要和样本充足的类一起平均。如果 GT 数量正常但 AP 还是nan检查ann_file和图片是否匹配、category_id是否在结果里出现、结果文件是否为空。还有一种情况是结果里某类的 score 全是 0 或者被过滤完了COCOeval 也会给出异常值。先把score_thr调低确认模型确实输出了这个类的框再回头查评估流程。5.4 训练日志mAP与测试脚本不一致训练日志和测试脚本的 mAP 不一致常见原因有四个评估数据增强不同、测试分辨率不同、评估器配置不同、checkpoint 不是同一个。训练时验证用的 pipeline 通常比测试时多随机增强但 MMDetection 的val_pipeline一般只做 resize 和 normalize理论上差异不大如果你自定义了val_pipeline要确保测试时用同一套。测试分辨率不一致也会导致 mAP 差一截尤其是小目标。评估器配置不同是更隐蔽的原因训练时val_evaluator开了classwiseTrue测试时test_evaluator没开整体 mAP 理论上应该一样但如果ann_file或metric不同结果就会变。还有一种情况是测试时用了--cfg-options改了score_thr或nms参数导致结果和训练日志不可比。排查时先把两边的评估器配置打印出来对比再确认 checkpoint 路径和 epoch 号。5.5 类别不平衡下的调参方向每类 AP 摆出来之后调参要有针对性。小类 AP 低但 AP50 还行优先查框回归和输入尺度别一上来就加数据小类 AP50 也低优先查样本量和标注质量。大类 AP 很高、小类 AP 很低可以尝试类别平衡采样、focal loss、类别权重或者两阶段微调。两个类别互相抢框先看混淆矩阵和可视化样例再考虑调整 NMS 阈值或类别定义。不要为了整体 mAP 好看去牺牲业务关键类整体 mAP 只是平均值业务验收通常是按类看的。下面这张表是我常用的排查顺序按优先级从高到低现象优先排查常见处理某类 AP 为 nan验证集 GT 数量补样本或单独标记某类 AP50 高、AP75 低框回归、标注框质量查标注、调回归损失、查 anchor某类 AP 整体低且样本多类别定义、混淆看混淆矩阵、合并或拆分类别小类 AP 波动大样本量、评估稳定性增加验证样本、多 epoch 平均整体涨但业务类跌类别权重、采样类别平衡采样、按类调阈值5.6 注意事项与实操心得打开classwiseTrue后类别多的时候日志会非常长几十个类一屏刷不完。我的做法是训练时把日志重定向到文件评估结束后用脚本解析只在终端看整体 mAP 和最差的 5 个类。不要一边训练一边盯着每类 AP 手动记容易漏。另一个心得是每次换配置前先保存一份 classwise 结果命名带上时间、配置名、epoch后面做对比时直接读表不要靠回忆。还有一点不要只看 AP。AP 是精度指标业务上还关心召回。某类 AP 高但 AR 低说明模型给的框很准但漏得多某类 AP 低但 AR 高说明框多但质量差。COCO 评估里还有 AR1、AR10、AR100可以一起看。我的习惯是弱类先看 AR100如果 AR100 也低说明模型确实没找到如果 AR100 不低但 AP 低说明框的位置或分类 score 有问题。6. 实战经验定位弱类后的处理顺序6.1 先查标注和样本量弱类 AP 低第一件事不是改模型而是抽查标注。我通常会从弱类里随机抽 30 张图把 GT 框和预测框叠在一起看。常见问题包括漏标、框太松、框太紧、类别名写错、同一目标标了两次、小目标根本没标。模拟项目 B 里有一个“远处行人”类AP 一直只有 0.1后来发现验证集里一半的远处行人没标模型检出来反而算误检AP 当然低。补标之后同一套权重 AP 直接到了 0.37模型没变数据变了。样本量也要看。某类只有几十个框训练时很容易过拟合验证时波动也大。我的经验是验证集每类至少 100 个框再谈稳定 AP训练集每类至少几百个框再谈调参。低于这个量级优先补数据或者用更强的数据增强不要指望换损失函数能解决根本问题。6.2 再查类别定义和NMS类别定义模糊时模型会把两个类混在一起。比如“货车”和“客车”在远处几乎一样标注员自己都容易标错模型更学不会。这时候看每类 AP 会发现两个类都中等偏低而且误检集中在对方类别上。处理办法是明确边界规则或者把容易混的类合并成一个父类再在业务层细分。不要为了指标好看强行拆类拆得越细每类样本越少AP 越不稳定。NMS 阈值也会影响每类 AP。阈值太低密集小目标会被压掉小类 AP 下降阈值太高重叠框变多误检增加大类 AP 可能下降。我一般会按类别分别看 AP 随 NMS 阈值的变化不要全局一个值调到底。MMDetection 3.x 支持在测试配置里改nms相关参数改完跑几组对比选业务最关心的类表现最稳的那组。6.3 调整采样、损失和输入尺度确认数据没问题后再动模型。小类 AP 低、大类 AP 高可以先试类别平衡采样让每个 batch 里小类出现的频率高一些。分类损失可以试 focal loss 或者类别权重但要注意别把大类压得太狠。输入尺度也很关键小目标 AP 低时提高测试分辨率往往比换 backbone 更直接但代价是推理变慢要按业务实时性要求权衡。我的调参顺序通常是先调输入尺度和 NMS再调采样和损失最后才换 backbone 或 neck。原因是前两者改动小、可解释性强后两者训练成本高。每改一次都保存 classwise 结果和 baseline 比每类 AP不要只看整体。如果整体涨了但业务关键类跌了这次改动就不算成功。6.4 建立每类AP的对比记录最后分享一个我坚持了很久的习惯每次验证都把 classwise 结果和 checkpoint 名一起归档尤其是第 6、9、12 个 epoch 的三份结果放在同一个 csv 里。这样当整体 mAP 只涨 0.3 时我能马上看出是哪一个类别的 AP 涨了哪一个类别其实在回落。这个习惯比事后猜原因有用得多。你可以用前面给的离线脚本批量跑结果文件生成一张epoch × category的 AP 表再画几条折线弱类是“一直弱”还是“中途掉”一目了然。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑