
把自己的数据集喂给 YOLO 目标检测算法、跑通预测这一整套流程几乎是从照着教程跑通官方示例迈向做一个能交付的真实项目之间最硬的一道坎。公开数据集上的那一行命令谁都会敲换成自己拍的几百上千张图标签格式、类别索引、显存、mAP 全线出问题的概率高得吓人。我这几年带过不少刚入行的朋友也接手过若干个半途卡住的检测项目发现真正卡住人的从来不是网络结构本身而是数据准备、参数设定和推理后处理这三个看起来不像算法的环节。下面这套内容是我把从数据采集、标注规范、训练配置、模型预测到问题排查的完整链路重新梳理一遍的结果。它适合已经能跑通官方示例、但一换自己的数据集就手足无措的朋友也适合需要把检测能力接进自己业务系统的工程同学。全程只讲能直接抄作业的做法和踩过的坑不讲空洞概念。1. 先看清跑通示例和跑通自己数据之间的真实差距1.1 换数据集就翻车的三个根本原因大部分人第一次用自己的数据训练 YOLO遇到的场景几乎一模一样按教程改完配置文件训练日志正常滚动损失也在下降但一跑预测框要么一个都没有要么满屏乱框。这个现象不是模型不行而是训练链路里存在沉默的错误——它不会报错只会让结果变差。第一类原因是标签格式的隐性错位YOLO 要求的是归一化到 0~1 的class cx cy w h而很多人从别的标注工具导出的是像素坐标或者是x_min y_min x_max y_max文件能读进去训练也不报错但模型学到的全是错的东西。第二类原因是类别索引对不上data.yaml里写的names顺序和标签文件里的数字不是同一套映射模型把猫学成了狗你还以为它在认真收敛。第三类原因是数据划分时的泄漏同一个视频的相邻帧、同一次拍摄的连拍图被随机分到了训练集和验证集验证指标虚高得离谱一上真实场景立刻原形毕露。理解这三点的意义在于它们都不是调参能救的问题。我见过有人为了把 mAP 从 0.3 拉到 0.6换了三个模型规模、试了七八组学习率最后发现是标签文件里有一半的坐标没归一化。所以在动手训练之前把数据的正确性验证做到位比任何超参数搜索都值钱。后面第 2 章我会给一段自查脚本跑一遍就能把这类问题基本扫清。另外要摆正一个预期YOLO 这类单阶段检测器的强项是速度和精度的平衡它在中小目标、密集遮挡、极端长宽比这几类场景上天生不占优势。如果你的数据集里全是几十像素的小目标或者目标之间大面积重叠那么在选择模型规模、输入分辨率、增强策略时就得更保守别指望用默认参数一把过。这个判断要在动手之前做完而不是训练完发现不行再回头。1.2 版本与规模怎么选别追新追匹配YOLO 系列迭代很快从早期带 anchor 的版本到后来的 anchor-free 结构配置项和默认超参差异不小。选版本的核心判断标准只有一条你所在的团队或你参考的教程用的是哪一套就用哪一套。原因很现实不同版本的命令行参数名、配置文件字段、权重加载方式、甚至标签格式的边界处理都有差别混着用会产生大量看起来像 bug 其实只是版本不对的无效调试。我第一次做工业缺陷检测时就吃过这个亏拿着新版本的推理脚本去加载旧版本训练出来的权重报错信息指向的却是一个完全无关的层名耗了大半天。至于模型规模的取舍我给你一个朴素的判断表都是实测下来比较稳的经验区间模型规模适用场景数据量参考单卡训练可行性n最小边缘设备、实时性优先、目标形态单一500 张起消费级显卡可训s小大多数业务场景的默认起点1000 张起消费级显卡可训m中目标类别多、形态差异大3000 张起需要 8G 以上显存l / x大精度优先、离线批量处理5000 张以上建议 16G 以上显存这里有个反直觉的点数据量不足时用大模型往往比用小模型更差。参数量大的模型在小数据集上极易过拟合训练集损失降得很漂亮验证集 mAP 卡在 0.4 上不去。我的习惯是先拿 n 或 s 跑一轮完整闭环确认数据、标签、评估全链路都通再往上换规模做对比。这个顺序能帮你把数据问题和模型能力问题分离开省下大量时间。1.3 一条完整闭环到底包含哪些环节从零到能用的检测能力实际要走的是这么一条链场景定义 → 数据采集 → 标注 → 数据划分 → 配置文件 → 训练 → 验证评估 → 预测推理 → 后处理 → 效果复盘 → badcase 回流。很多人只关注中间训练那一段前后两头草草了事结果就是模型永远停在能演示的水平。我给这条链做过一个粗略的工作量统计在典型的业务项目里采集加标注大概占 60%~70% 的时间配置和训练占 15% 左右预测、后处理和部署联调占剩下的一大部分。这个比例说明了一件事模型的精度上限在数据阶段就已经被决定了。你在训练阶段再怎么调参也只是逼近那个上限不可能突破它。所以后面我把最大的篇幅放在数据准备和推理后处理上这两块才是真正拉开差距的地方。2. 数据集准备决定精度上限的七成工作量2.1 采集策略场景覆盖度比绝对数量更重要新手最常见的误区是数量崇拜觉得拍够 2000 张就万事大吉。实际上一批在同一时间、同一光照、同一角度拍出来的 2000 张图信息量可能还不如分布在不同条件下的 600 张。我通常按维度矩阵来规划采集光照顺光、逆光、弱光、强反光、角度俯视、平视、斜侧、距离远、中、近、背景干净背景、杂乱背景、目标状态完整、局部遮挡、部分出画。每个维度至少覆盖三档交叉组合后就能得到一个几十种条件的清单按这个清单去拍数据的有效性会高很多。举个具体例子之前做过一个仓储货架商品识别的模拟项目第一轮采集图全是白天正午、货架灯全开、手机正对着拍训练出来的模型 mAP 到 0.85看着挺好。结果拿到实际环境一测晚上灯光昏暗加上摄像头装在斜上方掉到 0.5 都不到。后来补采了一轮夜间和斜视角的图总共只加了三百多张mAP 就回升到了 0.78。这件事让我彻底改了采集流程先用手机或临时设备在实际部署点位上拍一批最差条件的样图看看模型扛不扛得住再决定补什么。另外提醒一句纯背景图负样本千万别全删掉。一个数据集里保留 5%~10% 的无目标图像能显著压低误检率尤其是背景里有和目标颜色纹理相近的东西时。这个技巧在工业检测里特别有用因为产线上经常有和缺陷长得很像的正常纹理。2.2 标注规范把边界情况写死在文档里标注是数据质量的核心。YOLO 的标签文件格式很朴素每张图对应一个同名.txt每行一个目标五个字段用空格分隔class_id x_center y_center width height其中后四个值都是相对于图片宽高的归一化值范围 0~1。这个格式看起来简单但实际标注时的分歧点极多必须在开工前写成明确的规范否则不同人标出来的数据一致性会很差。我一般会把这几条写进规范文档遮挡怎么处理目标被遮挡超过 60% 就不标30%~60% 按可见部分的实际外接框标30% 以内标完整框。这条必须在团队内统一。边界目标怎么处理目标有一部分出画时标注框裁剪到画面内不要标到画布外面去。一类多形态怎么办比如同一类缺陷有细长裂纹和块状污渍两种形态长宽比差异极大要么拆成两个类要么在增强时加长宽比扰动别硬塞进一类。极小目标的下限宽或高小于 8 像素的目标在 640 输入下基本学不到建议直接排除或者把输入分辨率提上去。框要紧贴目标边界留白和过度内缩都会让定位精度下降尤其是回归框位置的任务里标注松紧直接反映在 mAP 上。我强烈建议做一个标注一致性抽检让两个人独立标同一批 50 张图然后算一下两者的框 IoU 均值。如果均值低于 0.75说明规范有歧义得回去改文档而不是硬着头皮往下标。这个成本很低但能避免后面几万次标注白做。2.3 数据划分小心那些看不见的泄漏训练集、验证集、测试集的常规比例是 8:1:1 或 7:2:1这个没什么好争的。真正要小心的是泄漏问题。数据泄漏指的是验证集里出现了和训练集本质相同的样本导致评估结果虚高。几个高发的泄漏场景泄漏类型具体表现后果时序泄漏同一段视频的相邻帧被随机拆到训练集和验证集验证 mAP 虚高 10~20 个点批次泄漏同一次拍摄、同一光照的连拍图被拆开模型记住了光照而非目标主体泄漏同一个物体的多角度图被拆散对新物体泛化能力被高估增强泄漏先在原图上做增强、再划分数据集同一张图的增强版跨集合出现最稳妥的做法是先按采集批次或源视频/源物体分组再在组级别上做划分而不是在图片级别上随机打乱。写几行脚本按文件名前缀分组就行成本极低。我在一个模拟项目里就是因为这个疏忽验证集 mAP 做到了 0.9兴冲冲拿去实测只有 0.55回头一查才发现验证集里的图和训练集是同一次连拍的。这个教训花了我整整两周返工。2.4 一段能省下几小时的标签自查脚本训练前必跑的一段检查能覆盖标签缺失、空标签、字段数错误、类别越界、坐标未归一化、宽高异常这几类高频问题import os from collections import Counter IMG_EXTS {.jpg, .jpeg, .png, .bmp, .webp} def check_dataset(img_dir, lbl_dir, num_classes): problems [] cls_counter Counter() img_files [f for f in os.listdir(img_dir) if os.path.splitext(f)[1].lower() in IMG_EXTS] for name in img_files: stem os.path.splitext(name)[0] lbl_path os.path.join(lbl_dir, stem .txt) if not os.path.exists(lbl_path): problems.append(f[标签缺失] {name}) continue with open(lbl_path, encodingutf-8) as f: lines [ln.strip() for ln in f if ln.strip()] if not lines: problems.append(f[空标签] {stem}.txt) for idx, line in enumerate(lines, 1): parts line.split() if len(parts) ! 5: problems.append(f[字段数错误] {stem}.txt 第{idx}行: {line}) continue cid int(float(parts[0])) cx, cy, w, h map(float, parts[1:]) if not (0 cid num_classes): problems.append(f[类别越界] {stem}.txt 第{idx}行: class{cid}) if not (0.0 cx 1.0 and 0.0 cy 1.0): problems.append(f[中心未归一化] {stem}.txt 第{idx}行: {cx},{cy}) if not (0.0 w 1.0 and 0.0 h 1.0): problems.append(f[宽高异常] {stem}.txt 第{idx}行: {w},{h}) cls_counter[cid] 1 return problems, cls_counter if __name__ __main__: probs, stats check_dataset(dataset/images/train, dataset/labels/train, 3) for p in probs[:50]: print(p) print(f\n问题总数: {len(probs)}) print(各类别实例数:, dict(sorted(stats.items())))跑完之后重点看两件事一是问题总数为 0二是各类别实例数别出现极端失衡。如果某一类只有十几条实例那这一类基本学不会得补数据或者考虑合并类别。经验值是每个类别至少 150 个实例、最好 300 以上低于这个数量级模型对该类的召回率会非常不稳定。3. 训练配置参数不是抄来的是算出来的3.1 目录结构与配置文件的标准写法YOLO 的目录约定其实很宽松images和labels分开、内部按train/val/test分好是最常见的做法my_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml关键点在于labels下的目录名必须和images完全一致并且.txt文件名和图片文件名不含扩展名严格对应。很多人卡在训练不报错但 mAP 一直是 0最后一个查出的原因就是路径里的图片和标签对不上模型读到的全是背景。配置文件的写法path: /data/my_dataset train: images/train val: images/val test: images/test nc: 3 names: 0: scratch 1: stain 2: crack这里最容易出错的是names的顺序。它必须和你标注时用的数字一一对应而且一旦开始训练就不要再改顺序否则旧标签全部错位。我的习惯是在标注工具里就把类别名定死然后在标注结束后导出一次classes.txt让配置文件和它逐行核对避免手敲出错。3.2 显存和 batch size 的估算过程显存爆掉是训练阶段最高频的报错。与其一次一次试不如先估算。训练时的显存占用大致由三部分构成模型权重和优化器状态、前向传播的中间激活值、以及数据加载的缓冲。第一项在推理时也要占但训练时要乘上大约 3 倍权重 动量 优化器二阶矩如果用 Adam 系列还要再多一倍。第二项跟 batch size 和输入分辨率近似成正比是调 batch 时的主要矛盾。给一个我实测出来的经验参考值输入 640、混合精度开启的情况模型规模单张图训练显存约8G 卡可跑 batch16G 卡可跑 batchn0.25 GB8 ~ 1224 ~ 32s0.40 GB4 ~ 816 ~ 24m0.70 GB2 ~ 48 ~ 12l1.10 GB1 ~ 24 ~ 8这个表误差可能有正负 30%因为实际占用还受图像长宽比、是否开启多尺度训练、框架版本等因素影响。但它足以帮你定一个好起点避免从 batch64 开始一路往下试。调整顺序建议是先降 batch 到能跑再考虑降 imgsz最后才换小模型。因为降低输入分辨率对小目标检测的伤害是直接且不可逆的而 batch 变小只是让梯度噪声大一点配合学习率调整基本能补回来。3.3 关键超参几个必须自己算的学习率不要照抄。业界常用的基准是 batch64 时 SGD 的初始学习率取 0.01如果你实际 batch 是 16按线性缩放规则应该是0.01 × 16 / 64 0.0025。这个规则在大 batch 场景下不完全成立但作为起点够用了。用 AdamW 系列优化器时初始学习率通常取 0.001 左右比 SGD 小一个数量级。学习率设大了的典型表现是损失剧烈震荡甚至变成 NaN设小了的表现是损失下降极慢前 10 个 epoch 几乎是一条平线。训练轮数取决于数据量。经验公式是epochs ≈ 2000 × max(类别数, 1) / 图片数这个量级不太可靠我一般直接在 100~300 之间取配合早停机制。早停的耐心值设 30~50 个 epoch 比较稳设太小容易在正常的学习率衰减周期里被误判。输入分辨率要和目标尺寸挂钩。粗略的判断是目标在图像中的典型短边像素数最好不低于输入分辨率的 1/30。也就是说 640 输入下目标短边至少得有 20 像素左右才比较稳。如果你的目标普遍只有 10 像素那就得考虑提到 960 或 1280或者改用带高分辨率检测头的结构。增强策略是最容易开过头的地方。默认的 Mosaic、随机缩放、色彩抖动对小目标数据集其实是双刃剑——Mosaic 会把四张图拼一起进一步缩小目标。我的做法是如果目标偏小把 Mosaic 在最后 10~20 个 epoch 关掉很多实现有这个开关并且把缩放范围从默认的 0.5~1.5 收窄到 0.8~1.2。这个改动我在三个不同的项目里都验证过小目标场景下 mAP 能稳定提升 2~4 个点。3.4 训练过程怎么看盯这三条曲线启动训练后不要只盯着进度条。我只看三样东西第一是分类损失和框回归损失是否同步下降。如果分类损失降了但框损失平着走通常是框的标注有系统性问题比如标注偏松或者坐标有偏移。第二是验证集的 mAP 曲线是否还在涨。如果验证 mAP 连续 20 个 epoch 不涨但训练损失还在降那是过拟合的信号早停该触发了。第三是各类别的 AP 分布。整体 mAP 0.7 听着不错但如果某一类 AP 只有 0.2整体数字就是被另外几类拉起来的实际部署时会集中暴露在这一类上。一个容易被忽略的点训练日志里的显存占用和实际时间。如果每个 epoch 的时间波动很大比如忽快忽慢差两三倍通常是数据加载成了瓶颈把workers调大或者改用更快的存储能让整体训练时间缩短三四成。这个细节在数据量上万张时影响很明显。4. 预测推理从一张图到真正能用的输出4.1 四种推理入口的实际用法训练完拿到best.pt之后推理入口按使用场景分四种我按使用频率排单图快速验证用来肉眼确认模型有没有学到东西yolo predict modelruns/detect/train/weights/best.pt sourcesamples/test1.jpg conf0.25 save_txtTrue整个目录批量跑用于跑测试集、生成评估数据yolo predict modelruns/detect/train/weights/best.pt sourcedataset/images/test conf0.25 iou0.45 save_txtTrue save_confTrue视频或摄像头流一定要加vid_stride跳帧否则处理速度跟不上yolo predict modelruns/detect/train/weights/best.pt sourcestream.mp4 vid_stride2 conf0.3Python 脚本内调用这是接业务系统时最常用的# 导入方式以你所用的框架版本官方说明为准 model YOLO(runs/detect/train/weights/best.pt) results model.predict( sourcedataset/images/test, imgsz640, conf0.25, iou0.45, max_det100, agnostic_nmsFalse, halfTrue, device0, )注意save_txtTrue这个参数它会把检测结果按 YOLO 标签格式写到labels目录下方便你后续做误差分析或者直接拿去当补充训练数据。这个功能很多人不知道用完能省下自己写序列化的功夫。4.2 conf 和 iou 到底怎么调这两个阈值是推理阶段影响结果最大的参数但绝大多数人都是复制别人的 0.25 和 0.45从来没根据业务调过。conf置信度阈值决定多确定才算检出。调低它召回率升高、误检增多调高它精确率升高、漏检增多。怎么定看你业务的代价结构。如果是宁可错杀不可放过的场景比如缺陷初筛、可疑目标提示可以压到 0.15~0.2如果是误报成本很高的场景比如自动告警、自动剔除建议提到 0.4~0.5 甚至更高。别凭空想拿验证集跑一遍不同阈值下的精确率和召回率画个曲线按业务需求取点这个动作花半小时收益比调一星期模型还大。iouNMS 重叠阈值决定两个框重叠到什么程度算同一个目标。默认 0.45 对大多数场景OK但两种情况必须改目标密集、彼此贴得很近调低到 0.3~0.4避免相邻目标被误抑制。反过来说如果调低了发现同一目标出现多个框那要往回调。同一目标可能出现高度重叠的真框比如堆叠的物品这时候用agnostic_nms不一定对更合适的做法是保留同类多框把 iou 提到 0.6~0.7。我踩过最典型的一个坑是在密集小目标场景下用默认 0.45 跑出来漏检一大片一度以为是模型不行。后来把 iou 调到 0.35召回率直接涨了 12 个点。这个参数在官方文档里只是一行说明但实际影响比很多人想象的大。4.3 把检测结果变成业务能用的结构化数据模型输出的是张量业务系统要的是表格或 JSON。这段转换代码我基本每个项目都会写一遍直接抄走改改就能用import csv model YOLO(runs/detect/train/weights/best.pt) results model.predict(sourcedataset/images/test, conf0.25, iou0.45, streamTrue) rows [] for r in results: names r.names for box in r.boxes: cls_id int(box.cls.item()) conf round(float(box.conf.item()), 4) x1, y1, x2, y2 [round(float(v), 2) for v in box.xyxy[0].tolist()] rows.append({ image: r.path, class: names[cls_id], confidence: conf, x1: x1, y1: y1, x2: x2, y2: y2, width: round(x2 - x1, 2), height: round(y2 - y1, 2), center_x: round((x1 x2) / 2, 2), center_y: round((y1 y2) / 2, 2), }) with open(detect_result.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnameslist(rows[0].keys())) writer.writeheader() writer.writerows(rows) print(f共输出 {len(rows)} 条检测记录)几个实操细节值得说。utf-8-sig这个编码是为了让表格软件打开中文路径不乱码我吃过太多次亏。streamTrue在处理大量图片时能避免一次性把所有结果堆在内存里几万张图跑下来内存曲线是平的。还有个容易忽略的点保存的坐标是相对于原始图片尺寸的但如果推理时做了 letterbox 缩放有些接口返回的是缩放后的坐标你需要在后处理时按比例还原。这一条不同版本行为不完全一致第一次用时一定拿一张已知尺寸的图验证一下别等业务侧反馈框位置偏了才发现。4.4 推理速度优化的几个实际手段训练完成后要上实时场景速度往往是硬约束。按收益从大到小排降低输入分辨率。这是最直接的一招640 降到 480速度大概能提升 40% 左右代价是精度下降小目标先遭殃。所以要先测精度损失能不能接受。启用半精度推理。halfTrue在支持张量核心的显卡上基本是免费的加速速度提升 30%~50%精度损失通常在 0.5 个点以内性价比极高。但要注意不是所有硬件都支持跑不起来就关掉。导出成静态图格式。ONNX 或更进一步的 TensorRT 能带来显著的加速尤其在批量推理时。导出的关键是输入尺寸必须和训练时保持一致动态 shape 虽然灵活但会牺牲一部分性能。我之前一个项目导出时用了动态尺寸速度比静态慢了近一倍改成固定 640 后立刻追平。批量推理代替逐张推理。GPU 的并行能力在 batch1 时利用率很低把batch设成 8 或 16吞吐量通常能翻两三倍。代价是单次延迟变高这就要看你的场景是重吞吐还是重延迟。跳帧。视频流场景下如果目标移动不快设置vid_stride2或 3处理速度直接翻倍对结果影响很小。实时的业务场景里30fps 降到 15fps 的检测结果人眼基本察觉不出差别。5. 常见问题速查与排查实录5.1 训练阶段的典型异常训练阶段的问题症状往往很模糊——不报错、能跑完、指标难看。我整理了一张速查表按症状倒推原因症状高概率原因排查动作mAP 全程为 0类别索引与配置不匹配标签未归一化验证集路径为空跑第 2.4 节的自查脚本手动打印一条标签核对损失不降或剧烈震荡学习率过大batch 过小权重未正确加载预训练学习率降 10 倍重跑确认迁移学习的权重路径有效训练损失降、验证 mAP 不涨过拟合数据划分泄漏验证集分布与训练集差异大检查是否为同一批次数据加增强、减轮数某一类 AP 极低该类实例数过少该类内部形态差异过大统计类别分布考虑合并类别或补采数据显存溢出batch 或 imgsz 过大未开混合精度按 3.2 节的表下调开启 AMP训练中途崩溃数据集中有损坏图片或非标准通道如 CMYK用 PIL 遍历一遍把坏图剔除或统一转 RGB关于损坏图片这一条我要多说一句。JPEG 文件头损坏、颜色模式是 CMYK 或灰度、带 alpha 通道的 PNG这几种情况在混合来源的数据集里非常常见。它们不会在加载时报错但会在某个 epoch 突然抛异常而且报错信息指向的往往是底层库看不出是哪张图的问题。我的做法是在数据整理阶段就统一转一遍全部转成 RGB 三通道 JPG短边截到固定尺寸这个预处理脚本十分钟就能写完能消掉后面几小时的诡异报错。5.2 推理阶段的典型异常推理阶段的问题更隐蔽因为模型不报错只是框不对。几个高频的框位置整体偏移。第一嫌疑是缩放还原也就是 4.3 节提过的 letterbox 坐标没有正确映射回原图。第二嫌疑是训练和推理的预处理不一致——训练时做了 letterbox 保持长宽比推理时直接 resize 拉伸变形模型看到的物体比例变了框自然歪。这条我在两个项目里都遇到过统一预处理之后立刻正常。同一目标出多个重叠框。NMS 没生效或者 iou 阈值设太高调到 0.4 附近试试。也有一种情况是模型确实没学好对同一目标输出了多个低置信度的框这时候应该回去看训练数据里是不是有大量重复标注。该检出的都没检出。先确认conf是不是设得过高把阈值压到 0.05 跑一遍如果这时能检出、只是置信度低那说明模型学到了但不够自信问题在训练而不是推理。如果压到 0.05 还是什么都没有那就是模型压根没学到这一类回去查标签。中文路径读不到图。经典问题某些图像读取库对非 ASCII 路径支持不好。绕法是先用字节流读入、再解码成图像import cv2 import numpy as np def imread_unicode(path): data np.fromfile(path, dtypenp.uint8) return cv2.imdecode(data, cv2.IMREAD_COLOR)这个函数我在几乎所有涉及中文路径的项目里都会写一份省事。5.3 上线后效果打折的原因排查训练和验证都很好一部署到真实环境就不行这是最让人挫败的一类问题。按我遇到的频率排序原因通常是图像来源变了。训练用的是手机拍的图部署用的是监控摄像头分辨率、色彩、镜头畸变全不一样。这种情况没有捷径必须用部署点位的真实图像重新采集一批补训。我的做法是上线前先用部署设备采 100 张图人工看一下评估分布差异有多大别等上线后再返工。预处理链路不一致。训练框架内部有一套默认的预处理缩放方式、归一化参数、通道顺序你自己写的部署代码如果没完全对齐精度就会莫名下降。最稳的验证方法拿同一张图分别用官方接口和你自己的部署代码跑一遍对比输出的框坐标差异超过 2 个像素就说明有问题。光照和季节变化。户外场景特别明显夏天训的模型冬天用会掉点。这个只能靠定期补数据和增量训练解决没有一劳永逸的方案。阈值沿用了训练时的默认值。训练评估用的 conf 通常设得很低比如 0.001以便算完整的 PR 曲线直接搬到线上会导致满屏低置信度框。上线前必须按 4.2 节的方法重新定阈值。6. 评估指标怎么看以及怎么持续迭代6.1 mAP、PR 曲线和混淆矩阵的读法mAP50是 IoU 阈值 0.5 时的平均精度mAP50-95是在 0.5 到 0.95 之间每隔 0.05 取一个阈值再平均后者更严格也更接近实际使用体感。如果一个模型 mAP50 有 0.85 但 mAP50-95 只有 0.45说明它能检测到但框得不准定位精度是短板这时候该考虑提高标注质量而不是继续调分类相关的参数。PR 曲线看的是精确率和召回率的权衡关系。曲线上那个拐点精确率开始快速下降的位置对应的置信度通常就是业务上比较合适的默认阈值。混淆矩阵则能直接暴露类别混淆比如把划痕大量判成裂纹这时候要么补这两类的区分性样本要么干脆合并类别。我个人的习惯是永远不只看一个总指标。总 mAP 是给汇报用的实际迭代要看的是每类的 AP、每类的召回、以及测试集上置信度最低的那 50 个框长什么样。最后这一项信息量最大往往一眼就能看出系统性偏差。6.2 badcase 回流让模型越用越准的闭环一个能长期用的检测系统一定有回流机制。具体做法是推理时把低置信度的框比如 conf 在 0.1~0.3 之间的和明显误检的结果单独存一份人工过一遍把其中确实有目标的重新标注加到训练集里定期重训。这个循环跑上两三轮模型在你实际场景下的表现会明显好于第一版。要注意的是重训时别把旧数据全丢掉。新补的数据往往集中在某些难例上如果只用新数据训模型会在原场景上退化这就是所谓的灾难性遗忘。实用比例是旧数据保留 70%~80%新数据占 20%~30%并且新旧混合后重新做一次数据划分避免泄漏。还有一个成本更低的手段用一个更高的 conf 阈值生成伪标签人工只做修正。对于已经有较好基础的模型这个方法能把标注效率提升三四倍。前提是模型本身已经比较准否则伪标签会把错误固化下来越训越偏。最后分享一个我个人用得最多的调试习惯每次改完配置或数据都先只跑 5 个 epoch看看损失曲线的前几个点是不是正常。正常的起点是分类损失在 1~3 之间、框损失在 1 附近如果开局就是 NaN 或者几十的量级那一定是有配置错误别等跑完 200 个 epoch 才发现。这五分钟的检查帮我省下的时间大概能按天算。