首页 / 资讯中心 / 文章详情

MMDetection 3.x按类mAP输出与多类目标检测诊断实践

MMDetection 3.x按类mAP输出与多类目标检测诊断实践 ★ FEATURED ARTICLE
多类目标检测项目跑到验收阶段时最常被问到的一句话是哪个类别拖后腿了。可当你打开 MMDetection 3.x 的训练日志看到的是bbox_mAP、bbox_mAP_50、bbox_mAP_75、bbox_mAP_s/m/l加一串bbox_mAR_*十几行数字整整齐齐唯独没有单个类别的成绩。这些数字是 COCO 格式数据集下所有类别平摊之后的结果一个稀烂的小类和十个表现优秀的大类混在一起求平均均值看起来还行实际上线后某些类根本没法用。问题不在于框架不给而在于大多数人没有把classwise这个开关打开或者打开了却不知道怎么把结果从控制台里捞出来做分析。这篇内容围绕 MMDetection 3.x COCO 格式标注这套组合讲清楚每类 mAP 是怎么算出来的、在哪几个位置可以打开、开完之后常见的数据对齐坑以及拿到数字后怎么判断该补数据还是改模型。适合已经跑通tools/train.py、但还停留在只看总 mAP 阶段的读者也适合需要向非技术同事汇报分类型效果的场景。1. 默认日志只给聚合值背后的计算链路决定了你看到什么这个问题得从 COCO 评估的数学结构说起不然你会误以为框架藏了数据其实数据一直都在只是没有被打印出来。1.1 那十几行数字是把类别维度平均掉的产物COCO 的评估核心在pycocotools里COCOeval.accumulate()算完之后会得到一个名为precision的四维张量形状是[T, R, K, A, M]。T是 IoU 阈值数量从 0.50 到 0.95 每隔 0.05 取一个共 10 个R是召回率采样点101 个K是类别数A是面积范围依次为 all、small、medium、largeM是每张图最多保留的检测框数量通常是 1、10、100。总 mAP 的计算方式是把这个张量里所有-1的有效值全部取出来直接求算术平均。关键就在全部这两个字上。类别维度K被一视同仁地摊平了不管这个类有五千个标注框还是只有二十个在求平均时权重完全一样。所以当你的数据集里有person这种上万实例的大类也有stapler这种只有三十来个实例的长尾类总 mAP 完全无法反映后者的真实水平。更麻烦的是如果某个类在验证集里压根没有真值标注它在precision里对应的整片区域都是-1会被自动剔除出平均于是总 mAP 反而可能比所有类都参与计算时更高这是一种相当隐蔽的失真。理解了这个张量结构后面的操作就顺理成章了想拿到单类成绩本质上就是固定K这一维的索引再去求剩下的平均。classwise参数做的事情就是从precision[:, :, idx, 0, -1]这个切片里取值求均值注意第三个维度是类别下标第四个维度固定为 0 表示 areaall第五个维度固定为 -1 表示 maxDets100。也就是说控制台里打出来的单类 AP和总 mAP 用的是同一套 IoU 阈值区间只是不再跨类平均。1.2 从 2.x 到 3.x评估入口整体换了一套机制如果你之前用过旧版本最直观的困惑是--eval bbox这个命令行参数不见了。旧版本的评估依赖EvalHook和DistEvalHook挂在 runner 上训练结束后再靠tools/test.py --eval触发。3.x 把这套机制整体交给了mmengine的BaseMetric体系评估器变成配置里的普通字段挂在val_evaluator和test_evaluator上训练过程中的验证由ValLoop驱动间隔由val_interval控制起始轮次由val_begin控制。这个改动带来的直接影响是评估行为完全由配置决定命令行不再能随口指定bbox还是segm。CocoMetric的metric字段接受字符串或列表写metric[bbox, segm]就会同时算检测框和分割掩码两套指标。每个CocoMetric实例内部维护一个COCO对象初始化时读取ann_file并用metainfo[classes]去反查cat_ids这个反查过程很关键后面讲坑的时候会重点展开。CocoMetric的执行流程分三步process()逐批收集预测结果并按图像聚合collect_results()把各进程的结果汇总单卡走 CPU 版本多卡走分布式版本超大验证集会临时落盘compute_metrics()把结果喂给COCOeval跑完evaluate、accumulate、summarize三步之后整理成字典。classwise的开关就是在最后这一步起作用的。1.3 打开 classwise 之后metrics 字典里多了什么把classwiseTrue打开之后compute_metrics()会在原有十二项指标之外额外遍历cat_ids对每个类别单独算一次平均精度并把结果塞进返回的metrics字典。键名形如coco/类别名_bbox_mAP具体前缀取决于prefix字段的取值不同小版本的拼装细节可能有一点点出入但规律是统一的前缀 类别名 任务类型 指标名。这里有个容易忽略的细节这个单类 AP 和总 mAP 在数值上不是简单的组成部分关系。假设三个类别的 AP 分别是 0.80、0.60、0.10总 mAP 是 0.50但如果你拿某个类的 AP 去乘以类别数得到的结果和总 mAP 对不上原因就是前面说的总 mAP 在求平均时已经把无效值剔除了参与平均的类别数可能小于你的类别总数。另外metrics字典被mmengine的日志系统接管后会以表格形式打印。类别数一多表格会横向铺满整个终端一行里塞进几十个键值对肉眼基本没法读。所以在实际项目中我不建议在训练过程中一直开着classwise更常见的做法是训练期只看聚合指标训练结束后单独跑一次测试把详细结果导出来。这个取舍在下一节会具体讲。2. 三个位置可以打开开关选哪个取决于你现在处于哪个阶段classwise不是只有一个入口配置文件、命令行覆盖、独立测试脚本这三条路都能到但代价和适用场景完全不同。2.1 改配置文件最稳但需要动源文件标准做法是在数据集的配置文件里找到val_evaluator和test_evaluator两处都加上classwiseTrue。这两处必须同时改因为训练中验证走的是val_evaluator而tools/test.py走的是test_evaluator只改一处的话你以为改好了实际跑测试时还是老样子。# 以 COCO 格式自定义数据集为例 metainfo dict( classes(class_a, class_b, class_c, class_d), palette[(220, 20, 60), (119, 11, 32), (0, 0, 142), (0, 0, 230)] ) val_evaluator dict( typeCocoMetric, ann_filedata/mydata/annotations/instances_val.json, metric[bbox], classwiseTrue, format_onlyFalse, outfile_prefixwork_dirs/mydata/val_result ) test_evaluator dict( typeCocoMetric, ann_filedata/mydata/annotations/instances_val.json, metric[bbox], classwiseTrue, format_onlyFalse, outfile_prefixwork_dirs/mydata/test_result )metric字段要按你的任务类型写。纯检测写[bbox]实例分割写[bbox, segm]这时候classwise会对两种任务都输出分类型结果日志长度直接翻倍。如果你的标注文件里没有segmentation字段却写了segm评估阶段会直接崩报错信息通常指向缺失的键排查时优先看这一条。outfile_prefix的作用是给格式化输出指定文件前缀配合format_only使用。很多人误以为设了这个前缀就能拿到指标文件实际上它只控制预测结果转成 COCO json 时的落盘路径指标本身还是只打在日志里。这一点下一节会详细说。2.2 训练期间就开着日志变长但能看收敛趋势有时候你不想等训练完中途就想知道某个难类有没有在进步。那就让val_evaluator带着classwiseTrue配合val_interval控制频率。我一般会把val_interval设成 3 到 5太频繁的话每轮验证都要跑一遍完整的COCOeval大验证集上单次耗时可能比一个训练 epoch 还长。日志膨胀的程度取决于类别数。四五个类还好表格两行就排完了八十类的数据集每次验证会打出几十行混在训练日志里非常难读。有个实操上的小技巧训练期间把classwise关掉只在最后几轮打开或者干脆训练完单独跑测试这样日志干净排查收敛问题时不会被无关信息干扰。还要注意验证集和训练集的标注文件通常是分开的两个 json。如果你的val_evaluator里ann_file指到了训练集上指标会虚高得离谱而且classwise输出的每类 AP 也会跟着虚高这种错误在只看总 mAP 时反而不容易发现因为数值好看不会引起警觉。2.3 训完单独跑 tools/test.py最推荐的路径绝大多数场景下最省事的路径是训练完之后单独跑一次测试。好处是配置可以临时覆盖不用动源文件也不会污染训练日志。python tools/test.py \ configs/mydata/faster-rcnn_r50_fpn_1x.py \ work_dirs/faster-rcnn_r50_fpn_1x/epoch_12.pth \ --work-dir work_dirs/faster-rcnn_r50_fpn_1x_test \ --cfg-options test_evaluator.classwiseTrue--cfg-options后面接的是点号分隔的嵌套键test_evaluator.classwiseTrue会在配置加载之后覆盖对应字段。这个机制对临时试验特别友好你想对比开和不开classwise的结果跑两条命令就行配置文件一个字都不用改。如果你还想同时把预测结果落盘方便后续用pycocotools自己重算加一个--outpython tools/test.py \ configs/mydata/faster-rcnn_r50_fpn_1x.py \ work_dirs/faster-rcnn_r50_fpn_1x/epoch_12.pth \ --work-dir work_dirs/mydata_test \ --out work_dirs/mydata_test/result.pkl \ --cfg-options test_evaluator.classwiseTrue这里的result.pkl装的是逐图的预测结果框坐标、类别、分数不是指标。很多人打开这个文件找 mAP找了半天没找到就是因为混淆了预测结果和评估指标这两样东西。指标只在日志和runner.test()的返回值里。多卡环境下把python换成bash tools/dist_test.sh后面跟配置文件、权重、卡数其余参数一致。多卡测试除了快还有个附带好处collect_results走的是分布式版本中间结果分片存放遇到超大验证集不容易把单进程内存吃满。2.4 用几行脚本直接把 metrics 拿进内存不想解析日志的话可以直接写一个测试脚本用Runner跑完之后拿返回值。这种方式最适合需要把结果写进报告、存成 CSV、或者接后续分析流程的场景。from mmengine.config import Config from mmengine.runner import Runner cfg Config.fromfile(configs/mydata/faster-rcnn_r50_fpn_1x.py) cfg.load_from work_dirs/faster-rcnn_r50_fpn_1x/epoch_12.pth cfg.test_evaluator.classwise True runner Runner.from_cfg(cfg) metrics runner.test() for k, v in metrics.items(): if bbox_mAP in k and k ! coco/bbox_mAP: print(k, round(float(v), 4))Runner.from_cfg会自动根据配置里的test_dataloader和test_evaluator组装TestLooprunner.test()跑完后返回的字典就是完整指标。这段代码的好处是可以直接在 notebook 里跑一边看数字一边改配置做对比试验比反复改文件、反复执行命令行要顺手得多。需要注意的是如果配置里缺test_dataloader或者test_evaluatorRunner.from_cfg会直接报错。有些项目的配置文件只写了val_dataloader和val_evaluator这时候要么把这两项复制一份改名要么在脚本里手动补上cfg.test_dataloader cfg.val_dataloader cfg.test_evaluator cfg.val_evaluator cfg.test_evaluator.classwise True3. 名字对不上、数值打出 nan多类评估最容易翻车的几个点classwise本身不复杂真正花时间的是前面那些数据对齐的准备工作。这一节列的四个问题每一个我都实际遇到过排查过程也都不算短。3.1 metainfo 的 classes 和 json 里的 categories 是两套独立的东西这是最高频的问题。CocoMetric初始化时会调用self.coco.get_cat_ids(cat_namesself.metainfo[classes])用类别名去annotations.json的categories数组里逐个匹配匹配不到会直接抛异常。也就是说你配置里写的类别名必须和 json 里categories[i][name]一字不差包括大小写、空格、连字符。实际踩过的几个具体情形配置里写traffic_lightjson 里是traffic light中间是空格不是下划线直接报错配置里写Dressjson 里是dress大小写不一致同样报错还有一种更隐蔽的json 里某个类别的name字段带有首尾空格肉眼完全看不出来print(repr(name))才能发现。遇到检查不出问题的报错先干这件事import json with open(data/mydata/annotations/instances_val.json) as f: ann json.load(f) for c in ann[categories]: print(repr(c[id]), repr(c[name]))把输出和配置文件里的classes元组并排对照一眼就能看出差异。cat_id本身不要求连续也不要求从 1 开始。数据集里出现id: 3, 7, 11这样跳号是正常的CocoDataset在加载标注时会建立cat2label映射把原始cat_id映射到 0 到 N-1 的连续训练标签。真正要求对齐的是数量和名称categories的数量必须等于metainfo[classes]的长度名称必须逐一匹配。还有一个容易忽略的顺序问题。在较新的版本里CocoMetric提供了一个sort_categories参数默认值为False此时cat_ids按metainfo[classes]的顺序排列classwise的输出顺序自然也就跟着配置走。如果你的配置是从别处拷来的classes顺序和 json 里categories的顺序不一致虽然不会报错但打印出来的类别名和你的直觉顺序会对不上分析时容易看错行。把sort_categoriesTrue打开可以让顺序跟随标注文件或者在写配置时就手动对齐两边的顺序二选一。3.2 某个类打出 nan 或者 -1先分清是哪一种classwise输出里出现nan是常见现象成因有三种处理方式完全不同现象成因判断方法处理该行全是 nan验证集里这个类没有真值标注统计 json 中该category_id的annotation数量该类别无法评估需补标注或从评估中排除该类 AP 为 nan其他类正常模型在这个类上没有产生任何超过分数阈值的预测检查result.pkl里该label的预测数量检查训练是否收敛、是否有该类的训练样本全部类别都异常低area或iscrowd字段异常抽查几条标注重新生成标注文件第一种情况最容易被误判成模型完全学不会这个类。实际上如果你的验证集里恰恰没有这个类的样本COCOeval在evaluateImg阶段就把它标成忽略precision里对应区域全部是-1取均值时取到空数组返回nan。这时候无论模型训多久结果都是nan跟模型能力无关。第二种情况反过来真值有但模型一个框都没输出。常见于分数阈值过高、该类别在训练集里样本极少、或者类别名虽然对上了但训练标签生成过程中出现了偏移。定位方法很简单把result.pkl读出来统计一下每个label的预测框数量和验证集里每个类的真值数量放一起对比import pickle from collections import Counter with open(work_dirs/mydata_test/result.pkl, rb) as f: results pickle.load(f) pred_cnt Counter() for item in results: for lab in item[pred_instances][labels]: pred_cnt[int(lab)] 1 print(sorted(pred_cnt.items()))result.pkl的结构在不同版本里略有差异有些版本存的是DataSample序列化后的字典有些存的是纯列表先打印一条看结构再写统计逻辑比照着文档猜要快。3.3 area 和 iscrowd手写转换脚本最容易漏掉的两列自己写脚本把其他格式转成 COCO 时绝大多数人只关心image_id、category_id、bbox这三样area和iscrowd往往被随手填成 0 或者干脆不写。后果是classwise的单类 AP 看起来正常但bbox_mAP_s、bbox_mAP_m、bbox_mAP_l全线异常或者干脆全为 -1。原因是 COCO 在按面积划分评估范围时读的是标注里的area字段不是bbox算出来的宽高乘积。bbox模式下标准做法是area bbox[2] * bbox[3]分割模式下是掩码的像素面积。如果所有标注的area都是 0那所有真值都会被归到 small 区间medium 和 large 两个区间没有样本对应指标自然算不出来。iscrowd同理漏写这个字段会让评估器在匹配时把本该忽略的区域当成普通目标person这类密集场景下的分数会明显偏低。生成标注时老老实实把这两个字段补上ann dict( idann_id, image_idimg_id, category_idcat_id, bbox[x, y, w, h], areafloat(w * h), iscrowd0, segmentation[] # 纯检测任务可以留空 )segmentation留空数组在纯检测任务里没问题但如果评估时metric里带了segm就会在这一步报错。3.4 验证集太大的时候结果收集本身会变成瓶颈CocoMetric的collect_results在单卡下走的是 CPU 版本把每个 batch 的结果累积在内存里。验证集有几千张图时还好上万张图、每张图几百个检测框的情况下内存占用会相当可观。多卡跑的时候走的是分布式版本结果会临时写到tmpdir指定的目录再由主进程读回来合并这时候tmpdir所在磁盘的剩余空间也得留意。如果跑到一半出现内存不足或者磁盘写满的报错优先检查这两个地方。tmpdir可以在配置里指定成容量大的分区test_evaluator dict( typeCocoMetric, ann_file..., metric[bbox], classwiseTrue, collect_devicecpu, prefixcoco )collect_device设成cpu是默认值一般不用改。真正需要调的是多卡测试时的进程数卡数越多单卡分到的图越少峰值内存越低。4. 把控制台里的散装数字整理成可排序的表格拿到每类 AP 只是第一步接下来要做的是把这些数字变成能排序、能对比、能贴进报告的形态。4.1 解析日志最省事但对格式敏感的办法训练或测试结束后work-dir下会有一个.log文件里面记录了完整的指标输出。用正则把类别和数值抓出来写进 CSV是成本最低的方案import re import pandas as pd pattern re.compile(rcoco/(\w)_bbox_mAP:\s*([\d.]|-?nan)) rows [] with open(work_dirs/mydata_test/20240101_120000.log) as f: for line in f: for name, val in pattern.findall(line): if name in (bbox,): continue try: rows.append({class: name, ap: float(val)}) except ValueError: rows.append({class: name, ap: None}) df pd.DataFrame(rows).drop_duplicates(subsetclass, keeplast) df df.sort_values(ap, ascendingTrue) df.to_csv(per_class_ap.csv, indexFalse)drop_duplicates(keeplast)是为了处理日志里出现多次验证记录的情况保留最后一次的结果。正则里的\w要求类别名只包含字母数字下划线如果你的类别名里有中文或者连字符需要把字符类放宽成[^_]之类的写法否则会抓不到。这种办法的缺点是对日志格式敏感。框架版本升级、日志格式化配置变化都可能导致正则失效。所以我在正式项目里更倾向于直接从runner.test()的返回值取数稳定性高得多。4.2 用 pycocotools 自己重算顺便拿到 AP50 和 AP75classwise只给单类的AP[.5:.95]。如果你还想知道每个类的AP50和AP75光靠classwise是拿不到的得自己调COCOeval。前提是先用format_only模式把预测结果导成 COCO jsonpython tools/test.py \ configs/mydata/faster-rcnn_r50_fpn_1x.py \ work_dirs/faster-rcnn_r50_fpn_1x/epoch_12.pth \ --format-only \ --cfg-options test_evaluator.outfile_prefixwork_dirs/mydata_test/pred执行完会在work_dirs/mydata_test/下生成pred.bbox.json。然后自己跑一遍评估import numpy as np from pycocotools.coco import COCO from pycocotools.cocoeval import COCOeval ann COCO(data/mydata/annotations/instances_val.json) dt ann.loadRes(work_dirs/mydata_test/pred.bbox.json) ev COCOeval(ann, dt, bbox) ev.evaluate() ev.accumulate() ev.summarize() prec ev.eval[precision] # [T, R, K, A, M] table [] for i, cid in enumerate(ev.params.catIds): name ann.loadCats(cid)[0][name] p_all prec[:, :, i, 0, -1] def mean_valid(arr): v arr[arr -1] return float(np.mean(v)) if v.size else float(nan) table.append({ class: name, AP: mean_valid(p_all), AP50: mean_valid(p_all[0]), AP75: mean_valid(p_all[5]), APs: mean_valid(prec[:, :, i, 1, -1]), APm: mean_valid(prec[:, :, i, 2, -1]), APl: mean_valid(prec[:, :, i, 3, -1]), }) import pandas as pd df pd.DataFrame(table).sort_values(AP) print(df.to_string(indexFalse)) df.to_csv(per_class_full.csv, indexFalse)这段代码的价值在于一次性把每个类在不同 IoU 阈值、不同尺度下的表现全拿出来了。某个类AP50很高但AP75很低说明框的位置不够准边界回归有问题某个类APl正常但APs很差说明小目标检测能力不足可以考虑调整anchor尺度或者用更高分辨率的输入。ev.params.catIds的顺序和ann.loadCats的返回顺序是一致的这个索引关系必须严格对齐因为prec张量的第三个维度就是按catIds顺序排列的。如果你在构建COCOeval之后手动改过params.catIds张量就错位了得到的类别名会张冠李戴且不会报错这是个很危险的静默错误。4.3 两套结果的交叉校验classwise打出来的单类 AP 和你自己用COCOeval重算的AP列理论上应该完全一致。如果对不上说明中间某个环节出了问题可能是format_only导出时用了不同的分数阈值可能是maxDets设置不一致也可能是classwise的cat_ids顺序和params.catIds顺序不同导致标签错位。我习惯把两边结果做个差值的绝对值超过 0.001 就逐行核对。这个小校验动作救过我好几次尤其是改动过test_evaluator配置之后肉眼看不出的偏差全靠它兜底。5. 数字到手之后怎么判断该补数据还是调模型每类 AP 是一个结果不是一个答案。真正难的判断在后面某个类的分数低到底是标注质量的问题、样本量的问题还是模型结构的问题。5.1 AP 的分布形状比平均分本身更有信息量把所有类按 AP 从低到高排一遍看分布形态。如果大部分类集中在 0.6 到 0.8 之间只有一两个类掉到 0.2 以下大概率是这两类的数据问题标注框不完整、类别定义有歧义、样本量太少、或者和其他类的外观过于相似导致混淆。如果整体呈现平滑的阶梯状下降从 0.8 一路递减到 0.3那更可能是长尾分布的固有特性需要从采样策略、损失函数权重这些角度入手。还有一种典型形态是两极分化一半类很高一半类很低中间几乎没有过渡。这种情况我遇到过两次一次是类别标注标准在两批数据之间发生了变化另一次是不同类别来自不同的采集设备图像质量差异明显。这两种问题都不是靠改模型能解决的必须回到数据上去。排名本身也要结合样本量看。一个只有 50 个实例的类AP 波动范围可能从 0.1 到 0.6 都正常因为评估结果的置信区间非常宽。反过来一个上万实例的类 AP 只有 0.35那就是实打实的问题样本量足够说明模型确实没学好或者标注确实有问题。5.2 AP 低和 AR 低指向的是不同的问题单看 AP 容易误判把同一类的召回率拉出来对比会清楚很多。COCO 的 AR 张量形状是[T, K, A, M]没有 recall 维度取值方式和 AP 类似。判断逻辑大致是这样AP 低、AR 也低说明模型根本没检出这个类的目标属于检出能力问题。可能原因包括训练样本太少、anchor尺度和目标的实际尺寸不匹配、该类的特征被其他类压制。处理方向是补充样本、调整anchor配置、或者检查num_classes和标签映射有没有错位。AP 低、AR 不低说明框检出来了但排序或者定位有问题。这种情况下模型有能力找到目标只是置信度排序不好或者框的位置偏差大。处理方向是调整正负样本分配策略、检查IoU阈值设置、看看是不是 NMS 参数过于激进把正确框压掉了。还有个细节值得单独看小目标 AP。如果某个类的整体 AP 还行但APs特别差说明大框检得准小目标全靠蒙。这在远景监控、航拍这类场景里非常普遍单纯增加数据量收益有限更有效的是提高输入分辨率或者改用更适合小目标的检测头。5.3 把低分类别的图捞出来看比看指标有效得多数字只能告诉你哪里有问题不能告诉你是什么问题。我通常会针对 AP 最低的两三个类从验证集里随机抽二三十张包含该类标注的图跑一遍推理把预测框和真值框画在同一张图上对比。真值是实线框预测按分数从高到低用不同颜色一眼就能看出是漏检、误检、还是框位置偏移。from mmdet.apis import init_detector, inference_detector import mmcv config configs/mydata/faster-rcnn_r50_fpn_1x.py checkpoint work_dirs/faster-rcnn_r50_fpn_1x/epoch_12.pth model init_detector(config, checkpoint, devicecuda:0) result inference_detector(model, data/mydata/images/val/xxx.jpg) # result 为 DetDataSample取 pred_instances 自行可视化inference_detector返回的是DetDataSample里面pred_instances包含bboxes、scores、labels三个字段配合mmdet.visualization.DetLocalVisualizer可以直接出图。自己写可视化逻辑的时候注意metainfo里的palette要和类别数对齐颜色数不够会报索引越界。看图的过程中几个高频发现标注框只框住了目标的一部分比如只框了人的上半身同一个目标被标成了两个不同的类某些类别的目标几乎全是遮挡状态标注时被标成了iscrowd从而不计入评估。这些问题在指标上表现为这个类怎么训都上不去看图五分钟就能定位。5.4 类别混淆可以用预测结果的标签分布侧面验证有一种情况是类别之间本身难以区分比如两种外观极其相似的零件。这时候单类 AP 会同时偏低但两者加起来的总量是对的。验证方法很直接统计模型在所有验证图上的预测标签分布再和真值标签分布对比。如果 A 类的预测数量远少于真值B 类的预测数量远多于真值而 AB 的总量基本吻合基本可以确认是混淆。确认之后的选择有两个一是合并这两个类如果业务上不需要区分的话这是最省事的做法二是针对性补充能区分两者的样本特别是处于决策边界的困难样本。前者成本低见效快后者效果上限更高具体选哪个取决于业务对细分类别的实际需求。6. 几个我在实际项目里攒下来的小习惯配置层面我现在会固定维护两份评估配置一份是训练期用的轻量版只算聚合指标一份是验收用的完整版开着classwise并且导出预测结果。两份配置通过_base_继承同一个主干只在评估器那几行有差异切换成本很低也不会出现忘了改回去的情况。跑测试之前我会先花一分钟做三件事确认ann_file指向的是验证集而不是训练集确认 json 里categories的名称和metainfo[classes]完全一致确认每条标注都有合法的area和iscrowd。这三件事挡掉了我在评估环节遇到过的绝大部分问题比事后对着nan排查要省太多时间。还有一个关于类别数量的经验类别数超过二三十之后控制台输出基本失去可读性与其盯着终端看不如直接把runner.test()的返回值 dump 成 json再用脚本生成图表。柱状图按 AP 排序横轴类别纵轴分数低于某个阈值的柱子标红拿去和团队同步进度时比贴一堆日志截图清楚得多。最后提一句关于复现的每次改动评估配置或者数据集标注之后一定要把上一次的per_class_ap.csv保留下来。单类 AP 的变化趋势能反映出很多单次结果看不出来的信息比如某次优化之后总 mAP 涨了 0.5但拆开看是三个大类各涨了 2 个点、五个小类各跌了 1 个点这种此消彼长的情况只看聚合值是完全发现不了的。
阅读完成 · 觉得有帮助?
咨询建站