最近我在做一个圆环产品的工业视觉检测,遇到了一个比较绕的问题:数据里只标注了一种缺陷,测试时却出现了其他类别的误检,而且有些框出现在产品旁边的背景区域。
当时我的标注类别编号是1,部署软件固定保留0~20共21个类别。我的需求很明确:只检测目标缺陷,并让软件按照业务类别1处理结果。
但实际测试中,既有类别1的命中,也出现了类别0的误检。同一张图片反复测试,结果还出现过不一致。
排查过程中,我补充过误判图片,也尝试关闭 AMP。关闭 AMP 后,我当时再次验证推理,之前的问题没有再出现,结果恢复正常。
不过,“调整后恢复正常”和“已经证明唯一根因”是两回事。这篇文章记录我遇到的现象,以及这次排查中需要分清的几个问题:训练类别编号、部署类别映射、预训练权重和混合精度。
一、先把实际问题说清楚
这个项目要检测的是圆环产品上的螺纹特征。按照当时的判定标准,无螺纹以及允许范围内的浅纹属于合格品,达到不良标准的螺纹才是需要检测的目标。
我遇到的情况主要有下面几个:
| 项目 | 当时的情况 |
|---|---|
| 实际标注内容 | 一种目标缺陷 |
| 标签文件中的类别编号 | 1 |
| 软件中的类别列表 | 0~20,共21类 |
| 模型初始化 | 使用预训练权重 |
| 异常表现 | 合格品或背景区域出现0类误检,同图测试结果出现过不一致 |
| 排查中的变化 | 关闭 AMP 后,当次验证推理恢复正常 |
我最初怀疑过:是不是预训练模型原本有很多类别,训练后还保留了其他类别的识别能力,所以会冒出0类?
要判断这个猜测,首先得弄清楚:我说的“只训练一类”,到底是只标了一类,还是模型实际上只有一个输出类别。
二、只标注一种缺陷,不等于模型只有一个类别
这是这次问题里最容易混淆的地方。
在普通的 YOLO 检测训练中,类别编号从0开始。如果没有启用single_cls等类别重映射逻辑,可以这样理解:
| 数据配置与标签 | 含义 |
|---|---|
| 配置1个类别,标签编号为0 | 正常的单类别配置 |
| 配置1个类别,标签编号为1 | 编号越界,需要修正配置或标签 |
| 配置21个类别,标签只使用1 | 声明了21个类别,只给其中一类提供了正样本 |
第三种情况不能叫“模型只有一类”。模型不会因为标签文件只出现过1,就自动把其他20个输出类别全部删掉。反过来,声明21类也不能直接证明它就是误检的原因。
Ultralytics 的检测数据集格式说明明确要求类别编号从0开始。对于真正只有一种目标的检测任务,我会使用下面这种配置:
# 示例:按实际数据目录修改 path path: /data/ring_thread train: images/train val: images/val nc: 1 names: 0: thread_defect对应的检测标签可以是:
0 0.500000 0.500000 0.200000 0.100000第一列是类别编号,后面四个数是归一化后的框中心坐标、宽和高。这一行只是格式示例,实际坐标要来自标注。
这里还要强调一点:**YOLO检测里的类别0不是自动预留的“背景类”。**在上面的单类别配置中,0就是thread_defect。正常图片没有目标,应当没有目标框,而不是专门画一个框标成0。
所以,如果我把模型正确改成了单类别,之后出现0类检测框本身完全正常。需要判断的是它有没有框到真正的缺陷。
三、软件固定21类,训练标签是不是就必须用1?
我当时的实际限制是:软件已经保留了21个类别,业务上希望使用类别1。
这就需要把两套编号分开看:
- 模型类别编号:模型内部用来区分检测目标;
- 业务类别编号:软件用来显示、统计、输出判定结果。
如果软件接口允许适配,我更倾向于让训练端保持单类别,再在识别结果进入业务逻辑之前做映射:
| 环节 | 编号及含义 |
|---|---|
| 数据标注 | 0 = thread_defect |
| 模型检测结果 | class_id = 0 |
| 软件业务结果 | business_class_id = 1 |
映射逻辑本身很简单:
# 仅示意编号转换,实际要接入软件的结果处理流程 model_to_business = {0: 1} model_class_id = 0 business_class_id = model_to_business[model_class_id]但这里有一个现场限制,不能省略。
如果软件只是界面上保留21个业务类别,可以考虑在结果层做映射;如果底层推理代码把输出按21类写死,就必须同时调整输出解析。
后一种情况下,只改标签或者只改names,并不能让旧软件正确读取新模型。要先确认模型输出形状、类别数和后处理接口,再安排训练与部署一起修改。
单类别加业务映射是我建议的整理方向,不能把它当成这次已经完成全部现场验证的修复结果。
四、使用预训练权重,会不会把原来的类别带进来?
我当时确实怀疑过预训练权重,但不能只凭“用了官方权重”就下结论。
在标准的 Ultralytics 检测训练流程中,模型会依据目标数据集的类别数构建,再加载可用的预训练权重。官方的检测训练器实现可以看到这个流程。
因此,一个正确构建的单类别检测模型,不会因为初始化来自多类别权重,就在输出里自动多出其他类别。
比起猜测,我会先检查实际加载的文件、类别表和检测头:
import torch import ultralytics from ultralytics import YOLO # 在能够加载该模型自定义模块的原环境中运行 model = YOLO("best.pt") print("Ultralytics版本:", ultralytics.__version__) print("PyTorch版本:", torch.__version__) print("类别表:", model.names) print("类别表数量:", len(model.names)) # 适用于常见的 Ultralytics YOLO11 检测模型结构 head = model.model.model[-1] print("检测头nc:", getattr(head, "nc", "请按自定义结构核对"))如果类别表仍然有21项,就需要结合检测头的nc、训练日志和输出形状继续核对。
如果模型确认只有一类,标准解析得到的类别ID应当是0;软件却显示其他编号,就要追踪是不是业务映射、错误解析,或者加载了其他模型文件。
**修改model.names只是修改类别名称信息,不能把一个21类模型直接变成单类别模型。**对于改过网络或训练器的项目,我会以本地源码和实际输出为准。
五、AMP是什么,为什么我会尝试关闭它?
AMP 是 Automatic Mixed Precision,也就是自动混合精度。
按照 PyTorch 的说明,它会让不同运算使用合适的数值类型。例如,在常见的 CUDA FP16 混合精度训练中,一部分运算使用 FP16,一部分仍使用 FP32,训练时还会配合梯度缩放。
它的用途是提高计算效率、减少部分显存开销,并不是“开了就会误检”的选项。
不过,混合精度会改变部分计算的数值行为。如果网络中存在对数值精度比较敏感的计算,就值得把它列为排查变量。这是一个需要验证的方向,不能直接当作我这次已经证明的底层原因。
在 Ultralytics 训练中,可以通过下面的参数关闭训练 AMP:
amp=False参数含义可参考官方训练文档。不同版本的接口可能变化,项目里应以实际安装版本为准。
这次关闭 AMP 后,我当时验证推理正常,这是一个有用的结果。但如果要进一步确认原因,还需要把数据、初始权重、其他训练参数和推理设置固定下来,再比较开启与关闭 AMP 的结果。
尤其不能忽略这一点:重新训练会得到另一组权重。两次训练的结果不同,不等于已经找到了某个算子的精度错误。
另外,训练日志里的AMP checks passed也不是项目验收结论。官方的 AMP 检查实现包含通用模型和示例图片的对照,并没有用我的全部产品数据验证最终效果。
训练AMP和部署FP16,要分别检查
这两个设置容易混在一起,但它们作用在不同阶段:
| 阶段 | 检查内容 | 影响范围 |
|---|---|---|
| 训练 | model.train()中的amp | 训练计算过程以及得到的权重 |
| 验证、预测 | 当前版本的推理精度设置,例如half等参数 | 这一次运行使用的计算精度 |
| TensorRT部署 | 引擎构建时的精度配置 | 已构建引擎的执行行为 |
推理精度的参数名称需要查对应版本的预测文档。
在训练脚本里改成amp=False,不会自动修改已经存在的best.pt,也不会改变已经构建好的 TensorRT 引擎。重新训练后的权重如果要用于现场,还需要重新导出、构建并完成部署验证。
六、如果再做一次严格排查,我会怎么对照?
我会先保存当前能正常工作的模型和配置,再建立一组可以重复的对照实验。
第一步,确认类别配置合法。
核对data.yaml、实际标签、训练日志、模型类别表和部署映射。只有一个类别时,不能一边配置nc=1,一边继续使用标签1。
如果修改了标签,还需要确认训练实际重新扫描了修改后的数据。必要时清理对应数据集的标签.cache缓存,让它重新生成。
第二步,只改变AMP这一项。
下面是关闭 AMP 的训练示例,模型路径、图片尺寸和训练参数需要按项目调整:
from ultralytics import YOLO if __name__ == "__main__": model = YOLO("yolo11n.pt") model.train( data="data.yaml", imgsz=1024, batch=2, epochs=100, device=0, amp=False, seed=0, deterministic=True, resume=False, name="ring_amp_off", )这段代码演示参数位置,不能把里面的默认模型和参数当成我的现场最优配置。实际对照时,两组都要使用相同的项目结构、初始权重、数据划分和其他训练参数,只改变amp,并用不同实验名称保存。
如果一组用自定义网络,另一组改用官方网络,或者同时换了优化器、batch和数据,就又增加了变量。
两组都应该从同一份初始权重开始,不能一组从头训练,另一组接着前一组的best.pt继续训练。固定随机种子有助于比较,但也不能保证跨版本、跨硬件完全复现,具体限制可参考 PyTorch 的可复现性说明。
第三步,把两组权重放在同一推理设置下比较。
我会固定测试图片、输入尺寸、置信度阈值、NMS参数和推理精度,再对比误报与漏检。这样才能减少“训练设置和推理设置一起变了”的干扰。
如果需要排查推理精度,则保持同一份权重,再单独比较不同推理精度的结果。
第四步,单独检查同图重复推理。
同一张图片的结果明显反复变化,不能只用“训练时开过AMP”来解释。
我会先在固定环境中,用同一模型、同一张本地图片、单路推理重复测试,记录原始类别ID、置信度和框坐标。可以连续运行100次作为一次复现测试,但这不是最终稳定性验收标准。
如果单路稳定、接入视觉软件后才异常,我会重点追踪输入是否被改动、模型是否混用,以及并发任务有没有共用输入输出缓冲区等问题。
七、补充负样本时,不能把正常区域也标成缺陷
排查过程中,我补充过误判图片。这一步有价值,但要先分清图片里到底有没有需要检测的目标。
以这次圆环检测为例,我会分别整理:
- 无螺纹的合格产品;
- 在验收标准允许范围内的浅纹产品;
- 曾经引发误报的背景、反光或纹理区域;
- 真正达到不良标准的螺纹产品。
对于确实没有目标缺陷的图片,应该作为负样本处理。在 Ultralytics 的 YOLO 检测数据格式中,无目标图片可以没有对应标签文件;使用空标签文件时,也要确认标注和数据处理工具能够正确识别。
**不能为了“告诉模型这张图是好的”,就在正常区域画一个框并标成0。**如果0代表目标缺陷,这个操作等于告诉模型:正常区域也是缺陷。
如果一张图里既有真实缺陷,又有容易误检的背景,我会保留真实缺陷的标注,不会把整张图当成负样本。
还有一点容易让验证结果看起来很好:把误判图片加入训练后,又只拿同一批图来证明模型提升。
这些图片适合做回归检查,但还需要另外保留没有参与训练的正常品和不良品。划分时,我也会尽量让同一件产品的连续拍摄图片留在同一组,避免相似图片同时进入训练集和测试集。
八、最后我会看哪些结果?
对于工业检测,我更关心最终有没有把正常品误判成不良,以及真实缺陷有没有漏掉。
下面这张表,是我给这类问题整理的复查清单:
| 检查项 | 我会怎样判断 |
|---|---|
| 类别编号 | 标签、模型类别表、检测头与部署映射是否一致 |
| 正常品误报 | 无目标缺陷的图片,是否仍被判为不良 |
| 不良品漏检 | 达到判定标准的目标,是否仍然能被检出 |
| 旧问题回归 | 原来出错的图片,重新测试是否正常 |
| 新样本表现 | 没有参与训练的图片,是否同样稳定 |
| 同图重复推理 | 类别、框和置信度是否出现无法解释的明显变化 |
| 部署一致性 | 同一批图片在本地模型与现场软件中的结果是否一致 |
统计时也要统一口径。按图片统计的误报率,和产线上按整件产品统计的误剔率,不一定是同一个数。多相机项目尤其需要说明结果如何汇总。
我也不会只靠提高置信度阈值来判断问题是否解决。误报可能少了,但边界缺陷的漏检也可能增加,两边都要看。
这次我能确认的是:**关闭 AMP 后,当次验证推理恢复正常。**类别编号、软件映射和精度设置,是后续需要分别核对的环节;现有结果还不足以证明它们共同构成了唯一根因。
以后再遇到“只标了一类,却出现其他类别”的情况,我会先确认模型到底有几个输出类别,再去追踪类别映射和数值精度。对于目前已经验证正常的配置,我会先保留它;是否重新开启 AMP,则用固定测试集和重复对照的结果来决定。