☰
MMDetection 3.x目标检测:classwise查看每类AP定位弱类
2026/10/26 5:06:29 网站建设 项目流程

多类目标检测训练到一半,最容易被整体 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,平均之后把问题盖住了。后来我们把classwise=True作为验证阶段的固定配置,每次看结果先扫一遍类别 AP,再决定要不要继续训练。

多类目标检测的评估不能只看一个总数,原因在于 COCO 评估本身是把每个类别单独算 AP,再做平均。也就是说,COCOeval 的中间结果里本来就存着每个类别的 precision,只是默认的summary()只打印整体指标。MMDetection 3.x 的CocoMetric把这个中间结果利用起来,通过classwise参数把每个类别的 AP 追加到日志里。理解这一点之后,你就知道每类 AP 不是“额外功能”,而是 COCO 评估本来就有的信息,只是平时没打开。

1.2 classwise参数与COCOeval精度矩阵的关系

CocoMetric的classwise=True背后读的是self.coco_eval[metric].eval['precision']。这个 precision 是一个五维数组,维度大致是[T, R, K, A, M]:T是 IoU 阈值数量,COCO 默认 10 个,从 0.5 到 0.95;R是 recall 采样点,默认 101 个;K是类别数量;A是面积范围,通常包含 all、small、medium、large;M是 maxDets,默认 1、10、100。计算某个类别的 AP,就是固定类别索引K、面积范围 all、maxDets 100,然后把 IoU 维度和 recall 维度上有效的 precision 取平均,再乘 100。默认整体 mAP 是对K维度再平均一次,所以每类 AP 和整体 mAP 的算法完全一致,不存在“两套标准”的问题。

这也解释了一个常见现象:某个类别在验证集里没有 GT,precision 会全是 -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.10,PyTorch 和 CUDA 按机器来,MMCV 要用 2.x 系列,MMDetection 用 3.x 系列。安装时不要混装 2.x 的 mmcv-full 和 3.x 的 mmcv,否则 import 阶段就会报一堆注册器错误。下面是一组常见的安装顺序,具体小版本按你的环境微调:

conda create -n mmdet3x python=3.9 -y conda activate mmdet3x pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install -U openmim mim install mmcv>=2.0.0 pip install mmdet>=3.0.0

装完之后做一次最小自检,确认mmdet、mmcv、mmengine都能正常导入,并且版本号符合预期:

python -c "import mmdet, mmcv, mmengine, torch; print(mmdet.__version__, mmcv.__version__, mmengine.__version__, torch.__version__)"

这里有个实际经验:如果你之前跑过 2.x 的项目,环境里可能残留旧版 mmcv,pip 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', encoding='utf-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(f'category_id={cid:<5d} name={name:<20s} gt_num={counter.get(cid, 0)}')

输出里如果某个类gt_num=0,就要提前知道:这个类在验证集上算不出 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', encoding='utf-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_head=dict(num_classes=num_classes) ) train_dataloader = dict( dataset=dict( data_root=data_root, metainfo=metainfo, ann_file='annotations/instances_train.json', data_prefix=dict(img='train/') ) ) val_dataloader = dict( dataset=dict( data_root=data_root, metainfo=metainfo, ann_file='annotations/instances_val.json', data_prefix=dict(img='val/') ) ) test_dataloader = val_dataloader val_evaluator = dict( type='CocoMetric', ann_file=data_root + 'annotations/instances_val.json', metric='bbox', classwise=True, format_only=False, outfile_prefix='work_dirs/custom_coco/val_result' ) test_evaluator = val_evaluator

这份配置里有三个点最关键:bbox_head.num_classes改成真实类别数;metainfo.classes顺序和 json 一致;val_evaluator和test_evaluator都打开classwise=True。outfile_prefix建议加上,后面离线复算每类 AP 时会用到它保存的结果文件。注意outfile_prefix的目录要存在,或者至少父目录可写,否则评估结束保存结果时可能报路径错误。

3. 在训练与测试配置里打开每类AP

3.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.classwise=True

跑完之后,终端会打印整体指标和每类 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 2>&1 | tee test_epoch12.log grep -E "coco/bbox_.*_AP|coco/bbox_mAP" test_epoch12.log

3.2 训练时验证与离线测试命令

训练命令本身不复杂,关键是--work-dir要固定,后面找 checkpoint 和日志都靠它:

python tools/train.py \ configs/custom_coco/faster-rcnn_r50_fpn.py \ --work-dir work_dirs/custom_coco

训练过程中如果配置了val_interval,每个验证周期都会跑一次评估。打开classwise=True后,验证日志里会多出每个类别的 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 输出结果怎么读:字段含义与计算逻辑

打开classwise=True后,日志里常见字段包括整体的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只取 IoU=0.5,AP75取 IoU=0.75。排查定位问题时,我通常先看AP50,因为AP50对框的位置没那么苛刻,更能反映“有没有找到”;再看AP75,如果AP50还行但AP75很低,说明框回归不准,可能是 anchor 尺度、回归损失或者标注框质量问题。这个判断比单看 AP 更有方向感。

3.4 一个多类配置片段示例

下面这个片段把训练、验证、测试三边的评估器写清楚,适合直接抄到自己的配置里改路径。注意metric='bbox'表示检测框评估,如果你做实例分割,再加一个metric='segm',每类 AP 会分别输出 bbox 和 segm 的结果:

val_evaluator = [ dict( type='CocoMetric', ann_file=data_root + 'annotations/instances_val.json', metric='bbox', classwise=True, format_only=False, outfile_prefix='work_dirs/custom_coco/val_bbox' ), ] test_evaluator = val_evaluator

如果你的配置里原本val_evaluator是一个 dict,不是 list,也可以直接写 dict;写成 list 是为了以后同时加 segm 指标。无论哪种写法,核心是type='CocoMetric'、ann_file指向验证集标注、classwise=True。还有一点:ann_file不要写成训练集标注,否则评估时会把训练集当验证集,mAP 虚高,每类 AP 也失去意义。这个错误很隐蔽,因为代码不会报错,只是结果好得不像话。

4. 用脚本从结果文件提取每类AP

4.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', encoding='utf-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_idx=0): 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_idx=None表示在 10 个 IoU 阈值上平均;iou_idx=0对应 IoU=0.5;iou_idx=5对应 IoU=0.75。area_idx=0表示 all,precisions[..., -1]取 maxDets=100。p[p > -1]是过滤无效项,避免把 -1 算进平均值。如果你发现某类全是nan,先看gt_num是不是 0。

4.2 自定义脚本计算每类AP

上面的脚本是离线复算,适合结果文件已经保存的情况。如果你希望在评估器内部直接拿到每类 AP,可以自定义CocoMetric子类。不过在 MMDetection 3.x 里,classwise=True已经能满足大多数需求,自定义子类更多是为了把每类 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

然后把配置里的type='CocoMetric'换成自定义注册名。这个写法要注意两点:self.coco_eval的 key 可能是bbox或segm,遍历时不要写死;cat2label不一定存在,先hasattr判断。自定义子类适合做二次开发,不建议新手一上来就改评估器源码,先用classwise=True和离线脚本把问题定位清楚,再考虑扩展。

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(key=lambda 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', dpi=200)

图出来后,把 AP 低于 0.2 的类标红或者单独列出来,和验证集样例一起看。我通常会抽 20 张弱类图片,看看是漏标、错标、目标太小、遮挡太严重,还是模型根本没学到。很多“模型问题”其实是标注问题,尤其是小类和长尾类,标注漏一个框,AP 就会明显掉。

5. 常见问题与排查

5.1 每类AP不输出或字段找不到

最常见的原因是classwise没打开,或者只改了val_evaluator没改test_evaluator。先确认配置文件里两处都有classwise=True,再确认你运行的配置就是改过的那份,不要出现“改了 A 配置、跑了 B 配置”的低级错误。第二种原因是版本差异,某些旧小版本的字段名可能不是coco/bbox_<name>_AP,而是别的拼接方式,直接看终端输出或者日志 JSON 里的 key 最稳。第三种原因是metric写成了segm,但你的模型没有分割分支,评估阶段可能跳过或者报错,先统一用metric='bbox'排查。

如果终端完全不打印评估结果,检查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 为 0,COCOeval 没有正样本,算不出 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开了classwise=True,测试时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 注意事项与实操心得

打开classwise=True后,类别多的时候日志会非常长,几十个类一屏刷不完。我的做法是训练时把日志重定向到文件,评估结束后用脚本解析,只在终端看整体 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 表,再画几条折线,弱类是“一直弱”还是“中途掉”一目了然。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询