☰
YOLO自定义数据集训练全流程:数据标注、训练配置与推理后处理避坑指南
2026/10/26 13:45:03 网站建设 项目流程

把自己的数据集喂给 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, encoding="utf-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 卡可跑 batch
n0.25 GB8 ~ 1224 ~ 32
s0.40 GB4 ~ 816 ~ 24
m0.70 GB2 ~ 48 ~ 12
l1.10 GB1 ~ 24 ~ 8

这个表误差可能有正负 30%,因为实际占用还受图像长宽比、是否开启多尺度训练、框架版本等因素影响。但它足以帮你定一个好起点,避免从 batch=64 开始一路往下试。调整顺序建议是:先降 batch 到能跑,再考虑降 imgsz,最后才换小模型。因为降低输入分辨率对小目标检测的伤害是直接且不可逆的,而 batch 变小只是让梯度噪声大一点,配合学习率调整基本能补回来。

3.3 关键超参:几个必须自己算的

学习率不要照抄。业界常用的基准是 batch=64 时 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 model=runs/detect/train/weights/best.pt source=samples/test1.jpg conf=0.25 save_txt=True

整个目录批量跑,用于跑测试集、生成评估数据:

yolo predict model=runs/detect/train/weights/best.pt source=dataset/images/test conf=0.25 iou=0.45 save_txt=True save_conf=True

视频或摄像头流,一定要加vid_stride跳帧,否则处理速度跟不上:

yolo predict model=runs/detect/train/weights/best.pt source=stream.mp4 vid_stride=2 conf=0.3

Python 脚本内调用,这是接业务系统时最常用的:

# 导入方式以你所用的框架版本官方说明为准 model = YOLO("runs/detect/train/weights/best.pt") results = model.predict( source="dataset/images/test", imgsz=640, conf=0.25, iou=0.45, max_det=100, agnostic_nms=False, half=True, device=0, )

注意save_txt=True这个参数,它会把检测结果按 YOLO 标签格式写到labels目录下,方便你后续做误差分析或者直接拿去当补充训练数据。这个功能很多人不知道,用完能省下自己写序列化的功夫。

4.2 conf 和 iou 到底怎么调

这两个阈值是推理阶段影响结果最大的参数,但绝大多数人都是复制别人的 0.25 和 0.45,从来没根据业务调过。

conf(置信度阈值)决定"多确定才算检出"。调低它召回率升高、误检增多;调高它精确率升高、漏检增多。怎么定?看你业务的代价结构。如果是宁可错杀不可放过的场景(比如缺陷初筛、可疑目标提示),可以压到 0.15~0.2;如果是误报成本很高的场景(比如自动告警、自动剔除),建议提到 0.4~0.5 甚至更高。别凭空想,拿验证集跑一遍不同阈值下的精确率和召回率,画个曲线按业务需求取点,这个动作花半小时,收益比调一星期模型还大。

iou(NMS 重叠阈值)决定"两个框重叠到什么程度算同一个目标"。默认 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(source="dataset/images/test", conf=0.25, iou=0.45, stream=True) 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="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=list(rows[0].keys())) writer.writeheader() writer.writerows(rows) print(f"共输出 {len(rows)} 条检测记录")

几个实操细节值得说。utf-8-sig这个编码是为了让表格软件打开中文路径不乱码,我吃过太多次亏。stream=True在处理大量图片时能避免一次性把所有结果堆在内存里,几万张图跑下来内存曲线是平的。还有个容易忽略的点:保存的坐标是相对于原始图片尺寸的,但如果推理时做了 letterbox 缩放,有些接口返回的是缩放后的坐标,你需要在后处理时按比例还原。这一条不同版本行为不完全一致,第一次用时一定拿一张已知尺寸的图验证一下,别等业务侧反馈框位置偏了才发现。

4.4 推理速度优化的几个实际手段

训练完成后要上实时场景,速度往往是硬约束。按收益从大到小排:

降低输入分辨率。这是最直接的一招,640 降到 480,速度大概能提升 40% 左右,代价是精度下降,小目标先遭殃。所以要先测精度损失能不能接受。

启用半精度推理。half=True在支持张量核心的显卡上基本是免费的加速,速度提升 30%~50%,精度损失通常在 0.5 个点以内,性价比极高。但要注意不是所有硬件都支持,跑不起来就关掉。

导出成静态图格式。ONNX 或更进一步的 TensorRT 能带来显著的加速,尤其在批量推理时。导出的关键是输入尺寸必须和训练时保持一致,动态 shape 虽然灵活但会牺牲一部分性能。我之前一个项目导出时用了动态尺寸,速度比静态慢了近一倍,改成固定 640 后立刻追平。

批量推理代替逐张推理。GPU 的并行能力在 batch=1 时利用率很低,把batch设成 8 或 16,吞吐量通常能翻两三倍。代价是单次延迟变高,这就要看你的场景是重吞吐还是重延迟。

跳帧。视频流场景下,如果目标移动不快,设置vid_stride=2或 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, dtype=np.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 才发现。这五分钟的检查,帮我省下的时间大概能按天算。

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

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

立即咨询