资讯动态

MMDetection 3.x多类检测每类mAP提取与排查

发布时间:2026/10/26 20:46:18 来源:尧图企业网站定制
多类目标检测训到一半最让人心里没底的不是 loss 不降而是 total mAP 从 0.342 爬到 0.361 之后你完全不知道这 19 个千分点里是哪几个类别贡献的又是哪几个类别在拖后腿。我见过太多人拿着一个总 mAP 反复调学习率结果训了三天才发现某个类别从头到尾 AP 都是 0模型根本没学会它。这篇东西就讲透一件事在 MMDetection 3.x 里用标准 COCO 格式数据集训练多类目标检测时怎么把每个类别的 mAP 一条一条扒出来包括配置怎么改、结果从哪拿、脚本怎么写、口径怎么对齐以及我自己踩过的几个坑。不管你是刚把官方 demo 跑通的新手还是已经在调长尾类别分布的老手这里面的实操路径都能直接拿去用。核心关键词就三个MMDetection 3.x、COCO 格式数据集、每类 mAP。1. 别只盯一个 total mAP多类检测为什么必须看每类结果1.1 总 mAP 掩盖了什么COCO 的 mAP 是所有类别 AP 的算术平均这个平均动作本身就是一次信息压缩。假设你的数据集有 20 个类其中 3 个大类样本量大、特征明显AP 能到 0.6剩下 17 个小类 AP 只有 0.1最后的 mAP 依然是 0.175看起来还在涨。这种数字在训练曲线上是平滑上升的但它掩盖的事实是模型已经放弃了一大半类别只是被头部类别的分数撑着。更糟的情况是某些类别的 AP 直接被算成 0 或者不参与统计你在总数上看到的波动可能只是那几个大类在抖动跟小类一点关系都没有。我自己的习惯是从第一个 epoch 验证开始就打开每类指标不是为了看绝对值而是看分布。一个健康的训练过程头部类和尾部类的 AP 应该都在往上走哪怕尾部类慢一些如果某个类的 AP 从第 3 个 epoch 开始就不动了或者掉下去再也没回来那基本可以判定这一类出了问题——可能是标注噪声可能是样本太少也可能是它跟另一个类在特征空间上完全重合。这些东西看总 mAP 是永远看不出来的。还有一种更隐蔽的情况类别索引和标注文件里的 category_id 对不上。COCO 的 category_id 不要求连续你在自定义数据集里把类别写成 1 到 20模型训练时按 0 到 19 处理评估时又按 json 里的 id 匹配中间任何一环错位最后表现出来的都是某几个类 AP 特别低但你会误以为是模型能力问题。打开每类指标这类问题一眼就能看出来。1.2 COCO 口径下每类 AP 到底怎么算出来的要读懂每类 AP得先知道它是怎么来的。COCO 评估的核心是pycocotools里的COCOeval它对每个类别、每个 IoU 阈值、每个面积区间、每个最大检测数都算出一条 precision-recall 曲线然后对 precision 做插值积分得到 AP。最终的stats[0]是在 IoU 从 0.5 到 0.95、步长 0.05 共 10 个阈值上取平均同时对所有类别取平均的结果。单类的 AP 就是把对所有类别取平均这一步去掉。具体做法是在 precision 张量里固定类别维度取所有面积区间all、所有最大检测数通常是 100下的 precision再对 10 个 IoU 阈值求平均。所以你在 MMDetection 里看到的 per-class AP默认口径是AP[.5:.95]不是 AP50也不是 AP75。这一点很多人第一次看会懵明明某个类目测效果挺好显示的 AP 才 0.4 出头那是因为定位精度在 0.75 以上的 IoU 上被打折了。另外要清楚某一类如果 GT 数量为 0或者你的预测里完全没有这一类precision 张量对应的位置会是 -1被过滤掉后 AP 就是 nan。这不是 bug是 COCO 的既定行为。理解这一点后面看到某个类显示-1或者nan时你就不会慌也不会浪费时间去找不存在的代码问题。2. 先摸清 MMDetection 3.x 的评估链路2.1 val_evaluator 与 test_evaluator 的分工3.x 相比 2.x 最大的结构变化是把 Runner、Loop、Hook 全部交给了 MMEngine配置文件的写法也跟着变了。评估这块2.x 时代你在data.val里塞一个evaluation dict(metricbbox)3.x 直接把它拆成了两个独立字段val_evaluator和test_evaluator。前者在训练过程中每个验证周期被ValLoop调用后者在tools/test.py里被TestLoop调用。两者可以写成一模一样的内容也可以是两套不同配置。这个拆分看起来很琐碎但实际用起来很方便。比如训练时你只想看总 mAP 省点日志验证时用classwiseFalse等训练完了单独跑测试再开classwiseTrue详细看每类。完全不冲突。反过来也有坑有人改了val_evaluator的ann_file然后跑tools/test.py发现指标不对其实是test_evaluator里那份没改。我排查过好几次这种问题现在养成习惯看到指标异常先 grep 一下配置文件里出现了几次ann_file。在 3.x 的早期小版本里test_evaluator这个字段还不存在只能靠val_evaluator兜底跑tools/test.py的时候会复用验证集的 evaluator。如果你手上的代码库版本比较老配置文件里搜不到test_evaluator那就直接改val_evaluator效果一样。2.2 CocoMetric 里跟每类相关的几个参数CocoMetric是mmdet/evaluation/metrics/coco_metric.py里定义的类继承自 MMEngine 的BaseMetric。跟每类指标直接相关的参数其实就一个classwise但周边的几个参数会直接影响你拿到什么口径的结果得一起说清楚。参数默认值作用与注意事项ann_file无必填GT 标注路径与验证集 dataloader 的ann_file必须指向同一份文件写相对路径时会和配置里的data_root拼接metricbbox可选bbox、segm、proposal也可以是列表列表时每类 AP 会分别按两种任务输出classwiseFalse打开后额外计算并输出每个类别的 AP这是本文的核心开关iou_thrsnp.linspace(0.5, 0.95, 10)改动它会同时改变总 mAP 和每类 AP 的口径慎改proposal_nums[100, 300, 1000]影响 AR 系列指标每类 AP 用的是第一个值100format_onlyFalse置为True时只导出预测结果不算指标通常配合outfile_prefix做离线评估outfile_prefixNone导出文件的路径前缀实际会生成.bbox.json或.segm.jsonprefixcoco指标 key 的前缀日志里看到的coco/bbox_mAP就是这个拼出来的关于iou_thrs我踩过一次印象很深的坑。当时为了快速看 AP50随手在 evaluator 里写了iou_thrs[0.5]结果每类 AP 确实变成 AP50 了但日志里的coco/bbox_mAP也跟着变成了 AP50 的数值和之前跑的实验完全没法对比。更麻烦的是实验记录里没标这一条过了两周回头翻日志怎么也对不上账。后来我的做法是日常训练坚持默认口径不动需要 AP50、AP75 的时候走独立脚本第 4 节会给两边互不干扰。proposal_nums也值得提一句。COCO 默认对每张图最多保留 100 个检测参与 AP 计算如果你的场景小目标极其密集一张图里真实目标就超过 100 个那 AP 会被这个上限压制。这个时候去改proposal_nums是有意义的但它同样会影响全局指标口径改之前记得在实验记录里写一笔。2.3 一行配置把 classwise 打开最直接的改法就是打开配置文件找到 evaluator 那一段加一个键值。假设你的配置文件里原本是这样val_evaluator dict( typeCocoMetric, ann_filedata_root annotations/instances_val.json, metricbbox) test_evaluator val_evaluator改成val_evaluator dict( typeCocoMetric, ann_filedata_root annotations/instances_val.json, metricbbox, classwiseTrue, format_onlyFalse) test_evaluator val_evaluator注意test_evaluator val_evaluator这种写法在 3.x 里是允许的配置系统会做一次深拷贝改一个不会污染另一个。但如果你写的是test_evaluator dict(...)重新定义一遍那两边的classwise就要分别确认别只改了一处。如果你不想动配置文件比如这个 config 是上游维护的每次同步都会被覆盖tools/train.py和tools/test.py都支持--cfg-options临时覆盖python tools/train.py configs/_custom_/det_multi_cls.py \ --cfg-options val_evaluator.classwiseTruepython tools/test.py work_dirs/det_multi_cls/det_multi_cls.py \ work_dirs/det_multi_cls/epoch_24.pth \ --cfg-options test_evaluator.classwiseTrue--cfg-options的点号语法对单层 dict 是好用的但如果你的val_evaluator是个列表比如同时挂了CocoMetric和某个自定义 metric就得用下标val_evaluator.0.classwiseTrue这种写法在部分 MMEngine 版本上不一定能正确合并遇到报错就直接去改配置文件别在这上面耗时间。3. 三种拿到每类 mAP 的做法按省事程度排3.1 做法一训练/测试时直接开 classwise这是成本最低的一条路配置改完就不用管了。每个验证周期结束终端和日志里会多打印一坨内容形如一个列表里面是每个类别的名字和对应的 AP 值。日志文件在work_dirs/实验名/时间戳/vis_data/时间戳.log和同目录下的.json里都能翻到。这个做法的好处是完全零成本不需要额外跑推理预测结果在评估时已经算好了多算一个每类 AP 的开销可以忽略不计。缺点也很明显第一输出格式是给人看的不是给程序读的想批量对比多个实验就得一坨一坨复制粘贴第二训练过程中每个 epoch 都打印一遍 20 个类别日志会迅速膨胀翻起来很烦第三它只给你默认口径的 AP想要 AP50 每类还得另外想办法。我的实际用法是训练阶段一直开着但只作为监控用看趋势、看有没有某个类突然归零。真正做实验分析、写报告、横向对比的时候走做法三。另外提醒一句多卡分布式训练时评估结果只在 rank 0 上打印和写日志其他卡的进程不会重复输出。这是BaseMetric的设计不需要额外配置。如果你发现日志里一次出现了好几份重复的评估结果那多半是DistributedSampler或者collect_device配错了检查一下有没有手动改过dist相关参数。3.2 做法二从 pkl 结果里把 per-category 薅出来tools/test.py支持--out参数把 evaluator 返回的完整结果 dump 成 picklepython tools/test.py work_dirs/det_multi_cls/det_multi_cls.py \ work_dirs/det_multi_cls/epoch_24.pth \ --out work_dirs/det_multi_cls/test_result.pkl这个 pkl 里是一个字典包含data_list、meta以及一堆指标 key。打开classwiseTrue之后里面会有一个跟每类相关的 key不同小版本的名字会有点飘常见的是coco/bbox_per_category_AP或者直接叫per_category_AP值通常是一个列表列表元素是字典包含类别名和对应的 AP。因为 key 名不稳定我从来不硬编码都是先打出来看一眼import pickle with open(work_dirs/det_multi_cls/test_result.pkl, rb) as f: obj pickle.load(f) for k, v in obj.items(): if isinstance(v, (int, float)): print(f{k:40s} {v:.4f}) elif category in k.lower() or class in k.lower(): print(f{k:40s} {type(v)} len{len(v)})看到那个列表之后解析逻辑就很直白了。但要注意列表元素的结构也可能不同有的是{category: xxx, AP[.5:.95]: 0.42}有的是{xxx: 0.42}写解析的时候加个兜底判断def to_ap_map(raw): 把 per-category 的原始结构统一成 {类名: AP} out {} for item in raw: if not isinstance(item, dict): continue if category in item: name item[category] ap item.get(AP[.5:.95], item.get(AP, float(nan))) out[name] float(ap) elif len(item) 1: name, ap next(iter(item.items())) out[name] float(ap) return out这条路的优势是不需要重新跑一遍推理pkl 已经在手上了随便怎么解析。劣势是它只给你 evaluator 当时算的口径AP50、AP75、分面积区间的 AP 一个都拿不到。如果只要一个总览够用了。3.3 做法三format_only 导出 json自己跑 COCOeval这是我最推荐的方式灵活性最高。核心思路是让 MMDetection 只负责推理和导出把预测结果写成标准 COCO 结果 json剩下所有指标计算都交给pycocotools想怎么算就怎么算。这样做有几个实打实的好处口径完全可控AP、AP50、AP75、APs/APm/APl 都能拿可以反复用不同参数重算不用重跑模型导出出来的 json 可以拿去跟别的框架对比也可以交给同事复核。配置只需要改一个地方test_evaluator dict( typeCocoMetric, ann_filedata_root annotations/instances_val.json, metricbbox, format_onlyTrue, outfile_prefix./work_dirs/per_class_eval/val)然后正常跑测试不需要额外参数python tools/test.py work_dirs/det_multi_cls/det_multi_cls.py \ work_dirs/det_multi_cls/epoch_24.pth跑完会在./work_dirs/per_class_eval/下生成val.bbox.json如果你同时配了metric[bbox, segm]还会有val.segm.json。这个 json 的格式就是标准的 COCO detection results一个列表每个元素包含image_id、category_id、bbox、score分割任务再加一个segmentation字段。有一点必须提醒format_onlyTrue之后终端上不会打印任何指标coco/bbox_mAP之类的 key 也不会出现。第一次用的人经常会以为出错了其实是正常的——它只负责导出不负责算分。跑完去看输出目录有没有 json 文件就行。4. 完整实操从导出预测到生成每类指标表4.1 先把预测导出成 COCO 结果 json上面已经给了配置这里补充几个实操细节。第一outfile_prefix用相对路径时是相对于你执行命令的当前工作目录不是相对于配置文件。我一般统一写成./work_dirs/实验名/per_class/val这种形式跟实验目录绑在一起方便追溯。第二验证集的ann_file和 evaluator 里的ann_file必须是同一份。如果验证 dataloader 用的是instances_val.jsonevaluator 里却指向了instances_val_remap.json导出的检测结果本身没问题它只是坐标和类别 id但后面算分的时候 GT 对不上指标会莫名其妙地偏低。这类问题在我这儿出现过两次每次都是改配置时复制粘贴留下的后遗症。第三如果你的数据集是自定义的类别名不是 COCO 那 80 类metainfodict(classesCLASSES)里的顺序要和 json 里categories的顺序保持一致。注意是顺序还是id 对应这两件事不一样COCO 评估靠category_id匹配理论上顺序无所谓但如果你在别的脚本里按索引去查类别名顺序错了就会张冠李戴。稳妥起见我一般让两者完全一致省得后面出幺蛾子。4.2 逐类评估脚本拿到val.bbox.json之后写个独立脚本跑 COCOeval。核心技巧是全局评估跑一次拿总指标然后对每个category_id单独设置params.catIds再跑一次拿单类指标。# tools/per_class_eval.py import argparse import csv import json import os import numpy as np from pycocotools.coco import COCO from pycocotools.cocoeval import COCOeval def eval_one_cat(coco_gt, coco_dt, cat_id, iou_typebbox): coco_eval COCOeval(coco_gt, coco_dt, iou_type) coco_eval.params.catIds [cat_id] coco_eval.evaluate() coco_eval.accumulate() # 屏蔽 summarize 的 stdout 输出避免刷屏 import contextlib, io with contextlib.redirect_stdout(io.StringIO()): coco_eval.summarize() return coco_eval.stats def main(): parser argparse.ArgumentParser() parser.add_argument(--ann, requiredTrue, helpGT 的 coco json) parser.add_argument(--det, requiredTrue, help导出的预测 json) parser.add_argument(--iou-type, defaultbbox, choices[bbox, segm]) parser.add_argument(--csv, defaultNone, help结果输出路径) args parser.parse_args() coco_gt COCO(args.ann) with open(args.det, r) as f: dets json.load(f) assert len(dets) 0, 预测结果为空检查 outfile_prefix 和 evaluator 配置 coco_dt coco_gt.loadRes(dets) # 全局指标先打一份作为基准 full COCOeval(coco_gt, coco_dt, args.iou_type) full.evaluate() full.accumulate() full.summarize() rows [] for cat in coco_gt.loadCats(coco_gt.getCatIds()): cat_id cat[id] num_gt len(coco_gt.getAnnIds(catIds[cat_id])) s eval_one_cat(coco_gt, coco_dt, cat_id, args.iou_type) rows.append({ cat_id: cat_id, name: cat[name], num_gt: num_gt, AP: s[0], AP50: s[1], AP75: s[2], APs: s[3], APm: s[4], APl: s[5], AR100: s[8], }) rows.sort(keylambda r: (np.isnan(r[AP]), r[AP])) header [cat_id, name, num_gt, AP, AP50, AP75, APs, APm, APl, AR100] print(\n \t.join(header)) for r in rows: print(\t.join( f{r[h]:.4f} if isinstance(r[h], float) else str(r[h]) for h in header)) if args.csv: os.makedirs(os.path.dirname(args.csv) or ., exist_okTrue) with open(args.csv, w, newline) as f: writer csv.DictWriter(f, fieldnamesheader) writer.writeheader() writer.writerows(rows) print(f\n已写出 {args.csv}) if __name__ __main__: main()几个关键点解释一下。coco_dt coco_gt.loadRes(dets)这一步会做一次合法性校验如果你的 json 里出现了 GT 中不存在的category_id它会直接报 Results do not correspond to current coco set这是最有效的错位检查手段比后面肉眼看指标靠谱得多。coco_eval.params.catIds [cat_id]必须在evaluate()之前设置因为evaluate()内部会遍历这个列表。设置完之后再accumulate()和summarize()stats里就只剩这一类的指标了。循环里每次都新建一个COCOeval对象是为了避免上一次的eval字典残留污染虽然稍微慢一点但结果干净。contextlib.redirect_stdout那段是为了压掉summarize()每次都要打印 12 行表格的噪音。20 个类别就是 240 行眼睛都要瞎了。压掉之后全局那一次summarize()保留输出作为参照。跑法python tools/per_class_eval.py \ --ann data/coco/annotations/instances_val.json \ --det work_dirs/per_class_eval/val.bbox.json \ --csv work_dirs/per_class_eval/per_class.csv输出大概长这样数值是示意cat_id name num_gt AP AP50 AP75 APs APm APl AR100 3 small_part 1182 0.4120 0.6613 0.4402 0.1822 0.4011 0.4920 0.5531 1 main_obj 9640 0.3866 0.6210 0.4108 0.2011 0.3822 0.4510 0.5231 7 rare_obj 96 -1.0000 -1.0000 -1.0000 -1.0000 -1.0000 -1.0000 -1.0000看到最后那种全是-1的行就说明这一类在你的预测结果里一个检测框都没有或者 GT 样本数为 0。这时候别急着改模型先看num_gt再看导出的 json 里到底有没有这个category_id。4.3 结果整理成 CSV 和 Markdown 表CSV 已经有了往实验记录里贴的时候我一般再转一版 Markdown只保留最关键的几列按 AP 升序排一眼就能看出短板在哪def to_markdown(rows, top_nNone): rows sorted(rows, keylambda r: (np.isnan(r[AP]), r[AP])) if top_n: rows rows[:top_n] lines [| 类别 | GT 数 | AP | AP50 | AR100 |, | --- | --- | --- | --- | --- |] for r in rows: lines.append( f| {r[name]} | {r[num_gt]} | {r[AP]:.4f} | f{r[AP50]:.4f} | {r[AR100]:.4f} |) return \n.join(lines)把每个实验的这版表存成独立文件命名带上实验名和 epoch比如exp_det_multi_cls_epoch24.md。做 A/B 对比的时候两张表并排一看哪个类涨了、哪个类掉了、掉多少比看总 mAP 直观十倍。我现在的实验记录文件夹里每份记录都附了这么一张表回溯的时候非常省事。5. 踩坑记录与排查速查表5.1 类别顺序错位与 ann_file 路径重复类别错位是每类指标里最容易骗人的问题。表现是某个类 AP 特别低但你看图上的检测结果其实还行。原因往往是训练时metainfo里的classes顺序和标注 json 里categories的顺序不一致模型输出的label索引在导出时被映射成了错误的category_id评估时自然对不上。判断方法很简单拿一张图的可视化结果看上面对应的类别名对不对。或者更直接一点检查导出的 json 里的category_id集合和 GT json 里的categories集合是否完全一致。如果预测里出现了 GT 中根本不存在的 idloadRes会直接报错这是最省事的自检。路径重复是另一个高频坑。3.x 的配置文件里data_root会在 dataset 和 evaluator 里各拼一次。如果你在ann_file里已经写了完整的相对路径data/coco/annotations/instances_val.json同时data_root data/coco/实际拼出来就是data/coco/data/coco/annotations/...直接找不到文件。3.x 在启动时会做一次路径检查并给出相对友好的报错但如果你把ann_file写成了一个看似存在、实际是别的数据集的路径比如不小心指到了 train 的标注那就不会报错只会得到一堆莫名其妙的低分。5.2 出现 -1、nan 或某类直接消失现象最常见原因排查动作某类 AP 全是-1该类的num_gt为 0或预测里没有这个category_id先数 GT 标注条数再 grep 导出 json某类 AP 是nan该类有 GT 但 precision 全被过滤检查score_thr、nms是否把该类框全压掉了loadRes报结果不匹配预测里有 GT 不存在的category_id对比两边的 categories 集合每类 AP 都低但总 mAP 正常多半是看错了 evaluatorval 改成了 test 没改grep 配置文件里所有ann_file导出 json 为空列表format_only没生效或outfile_prefix目录不存在看终端有没有写文件日志手动建目录score_thr这个点在 3.x 里特别值得说。很多配置文件会在test_cfg里设score_thr0.05CocoMetric导出的时候会按这个阈值过滤低于阈值的框不会写进 json。对头部类没影响但小类本来就出不了几个高分框被一过滤就彻底消失了表现出来就是全是-1。遇到这种情况把score_thr降到0.001再导一次大概率就出来了。5.3 日志刷屏、多卡只打一次、口径被改乱classwiseTrue打开之后日志体积会明显变大。如果你坚持按 epoch 保存日志几十个 epoch 下来几百兆是常事。我的处理办法是训练阶段只在val_evaluator里开classwiseFalse训练完用tools/test.py单独跑一次test_evaluator拿详细指标。既省日志又不影响分析。多卡那边CocoMetric的collect_device默认是cpu所有进程的结果会 gather 到 rank 0 上计算只有 rank 0 输出。如果你看到多个进程各打一份检查一下是不是自己改过collect_device或者手动绕过了BaseMetric的收集逻辑。口径被改乱这件事前面提过iou_thrs还有一个隐蔽的是proposal_nums。有人为了提速把它改成[100]只有一个值结果AR1000这类指标直接不输出stats数组长度都变了后面按索引取值就全错位。如果你写脚本读stats一定要按索引名而不是位置记忆或者直接打印coco_eval.params确认一下。6. 数字出来之后怎么用6.1 长尾类与小类的针对性处理拿到每类 AP 之后第一件事是按num_gt排序看看分布。通常你会发现一个很明显的规律GT 数量前 20% 的类别贡献了大部分 mAP尾部 30% 的类别 AP 长期在 0.1 以下。这时候盲调学习率是没用的得针对性地动手。我常用的几招按性价比排一是重采样让尾部类别在每个 batch 里出现的概率提高MMDetection 里可以通过自定义Sampler或者在数据集层面做重复采样代价是头部类别的收敛会慢一点二是增强针对小类做 copy-paste把稀有类别的实例贴到别的图上这个对样本数在几十到几百量级的类别效果最明显三是调整 loss 的类别权重但这一招我不太推荐因为它会同时影响所有类别的梯度尺度调不好整体指标会掉。还有一个容易被忽略的点如果某个类的 AP 一直上不去但AR100挺高说明模型其实能找到它只是定位不准这时候该动的是回归分支或者 anchor 匹配策略如果AP和AR100都很低那才是真的学不会得回头看数据。把AP和AR100两列并排看能省掉很多瞎试的时间。6.2 配合混淆矩阵与日志曲线定位问题单看每类 AP 只能知道哪个类差不知道为什么差。这时候配合两个工具会高效很多。第一个是混淆矩阵MMDetection 3.x 里对应tools/analysis_tools/confusion_matrix.pypython tools/analysis_tools/confusion_matrix.py \ configs/_custom_/det_multi_cls.py \ work_dirs/det_multi_cls/epoch_24.pth \ work_dirs/confusion/ \ --show它会输出一个矩阵横轴是预测类别纵轴是 GT 类别。如果某两个类在矩阵上互相串得很厉害那基本可以确定是特征区分度不够而不是样本量的问题。这种情况加数据增强或者换更强的 backbone 比调阈值有用。第二个是日志曲线用tools/analysis_tools/analyze_logs.py把每轮的指标画出来python tools/analysis_tools/analyze_logs.py plot_curve \ work_dirs/det_multi_cls/20240101_120000/vis_data/20240101_120000.json \ --keys coco/bbox_mAP coco/bbox_mAP_50 \ --legend mAP AP50 \ --out work_dirs/map_curve.png如果classwiseTrue的每类指标也写进了 json 日志取决于版本有些版本会写那就可以把某几个重点类别的曲线一起画出来看它们是不是同步上升。不同步的话说明这几个类的学习节奏不一样可能需要分阶段解冻或者调整采样策略。我个人在实际操作中的体会是每类指标最大的价值不是看分数而是排除法。当总 mAP 停滞时先拉每类表如果所有类都在缓慢上升那说明只是还没训够继续跑如果只有一两个类在拖那就针对性地查这两个类的数据和标注如果所有类都突然同时掉那八成是配置或者数据管道出了问题跟模型本身没关系。这个判断顺序帮我省下的时间比任何调参技巧都多。

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

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

免费获取报价 →
↑