PCB缺陷检测YOLOv8实战:小目标、标签分配与部署调优

发布时间:2026/10/30 1:38:10
PCB缺陷检测YOLOv8实战:小目标、标签分配与部署调优

简介:这是一份面向计算机科学与技术等专业毕业生的完整毕业论文文档,聚焦基于YOLOv8的PCB板缺陷检测系统的开发与实现,适合正在准备深度学习、计算机视觉方向毕业设计,需要论文框架与实验论证参考的学生及指导教师。文档按绪论、关键技术、系统分析、系统设计等章节展开,涵盖Python语言、YOLOv8框架与PyCharm工具的选型说明,并逐项落实可行性与功能需求分析,深入讲解YOLOv8的基本思想、网络结构、损失函数、训练与推理流程及数据集准备等核心内容,可直接对照借鉴章节编排、技术论述与写作表述,也可作为同类检测系统的设计模板。压缩包共1个docx文件,约3.66MB,含中英文摘要、关键词与完整目录,便于直接编辑修改。目前已有502人学习下载。

1. 换更大的模型没用:PCB 缺陷检测的瓶颈不在骨干网络

有个反直觉的结论:在这类 PCB 缺陷检测任务里,把骨干从最轻的一档一路换到最重的一档,mAP 往往只挪动一两个点;而把输入分辨率从 640 抬到 1024,或者把验证集的划分方式改对,收益立刻翻倍。原因不复杂——600 张图、6 类缺陷(缺失孔、老鼠咬痕、开路、短路、毛刺、铜渣),最小的毛刺在全图里只占几十个像素,模型容量再大,特征图上没剩下信息就是没剩下。真正决定成败的是三件事:小目标在 P3 特征图上能不能活到检测头、正样本分配得合不合理、以及「漏检一块板」和「误报一块板」的代价根本不对称。搞懂这套机制,比在毕设里堆参数有用得多。

2. 单阶段与 Anchor-Free:YOLOv8 凭何不再依赖先验框

2.1 从划格子到每个位置都参与预测

早期的 YOLO 把图像切成 S×S 网格,每个格子预测 B 个边界框和置信度,这套「划格子」的思路有个隐含前提:目标的尺度和形状能被预先设定的 anchor 覆盖。anchor 的宽高通常靠 k-means 在训练集上聚出来,聚出来的是一组「平均形状」。问题在于 PCB 缺陷的形状分布极端不平均——开路是又细又长的线,缺失孔是规整的小圆,老鼠咬痕是边缘不规则的缺口,毛刺是贴在线边上的短刺。你聚类得到的 9 组 anchor,很可能一组都对不上毛刺,于是毛刺这个类别从标签分配阶段就被判了死刑,后面损失函数再怎么调都救不回来。

YOLOv8 走的是 Anchor-Free 路线,把「预设形状」这件事彻底删掉。它的检测头在每个特征图位置上直接预测四条边到该位置的距离,也就是 left、top、right、bottom 四个量,再由这四个量还原出边界框。这样做的直接后果是:候选框数量从「网格数 × anchor 数」变成「网格数」,参数少了,但每个位置都必须自己学会判断「我离左边界多远」。对于形状千奇百怪的 PCB 缺陷,这个改变是决定性的——模型不再被 anchor 的形状先验束缚,细长开路和不规则毛刺可以在同一个位置预测头上共存。

代价是回归任务变难了。Anchor-Based 方法里,网络只需要回归「相对于 anchor 的偏移量」,偏移量通常在一个很小的范围内波动,数值稳定;Anchor-Free 要直接回归绝对距离,一个 640 输入下 P3 特征图的格子对应原图 8 个像素,目标可能有 200 像素宽,回归值的动态范围被拉得很大。这也是后面 DFL 存在的理由:把一个大范围的连续回归拆成若干个小区间的分类问题,梯度就稳了。

2.2 C2f 与 PANet:小目标在哪一层特征图上活下来

要理解小目标为什么容易丢,得先看清三个检测尺度的分辨率。输入固定 640×640 时,骨干网络的下采样倍率分别是 8、16、32,对应产生三张特征图:P3 是 80×80,P4 是 40×40,P5 是 20×20。一个在原始图像上占 16×16 像素的毛刺,映射到 P3 上大约占 2×2 个格子,映射到 P4 上只剩 1×1,映射到 P5 上连一个完整格子都填不满,特征响应会被周围的背景稀释掉。

所以小目标的检测几乎完全依赖 P3。这也是为什么在这类任务里,把 imgsz 从 640 提到 1024 往往比换骨干更有效——1024 输入下 P3 变成 128×128,同一个毛刺能占到 3×3 甚至 4×4 个格子,特征响应不会被下采样抹平。血泪经验是:先看你的缺陷在 P3 上还剩几个像素,再决定要不要动网络结构。

YOLOv8 的骨干里有个叫 C2f 的模块,它的作用是把浅层特征尽量完整地送到检测头。常见的实现思路是:输入先经过一次 1×1 卷积做通道压缩,然后 split 成两半,一半直接短路连到输出,另一半经过若干个 Bottleneck 后再和短路分支 concat 起来。这种「多分支 + 跨层短路」的结构让梯度有更多条路径可以回传,浅层的高分辨率信息不会被深层卷积层层稀释。相比前几代的 C3 模块,C2f 的旁路更多,对细小纹理的保留更友好,代价是显存占用和推理时间略有增加。

颈部负责把不同尺度的特征融合起来。YOLOv8 用的是自顶向下加自底向上的双向结构:先把 P5 上采样后和 P4 相加,再把融合结果上采样和 P3 相加,这一步把语义信息带到了高分辨率层;随后反向下采样,把 P3 的细节信息回传给 P4 和 P5。对 PCB 检测来说,这个双向结构的意义在于:P3 拿到了「这是不是缺陷」的语义判断,P5 拿到了「缺陷边界在哪」的细节线索,两者缺一不可。

下面的代码片段展示的是三个检测头的输出通道构成,理解这个形状是后面调损失函数和做后处理的前提:

# 输入张量形状:[B, 3, 640, 640] # 三个尺度的检测头输出(nc 为类别数,本项目 nc = 6) # P3: [B, 64 + nc, 80, 80] # P4: [B, 64 + nc, 40, 40] # P5: [B, 64 + nc, 20, 20] # # 64 的来历:4 条边 × reg_max(16) 个离散区间 # nc 的来历:每个位置对 6 类缺陷的分类得分(sigmoid,非 softmax) REG_MAX = 16 NC = 6 for level, size in zip(("P3", "P4", "P5"), (80, 40, 20)): reg_channels = 4 * REG_MAX # 64:四条边各 16 个 bin cls_channels = NC # 6:缺失孔/老鼠咬痕/开路/短路/毛刺/铜渣 print(f"{level}: {size}x{size}, 通道 {reg_channels + cls_channels}")

这段输出说明了两件事。第一,回归和分类的通道是拼在一起的,但在网络内部它们由两条独立的分支产生,下一节会讲为什么必须分开。第二,每个位置只输出一组框,没有 anchor 维度,所以后处理时不需要按 anchor 做 decode,直接对这 8400 个位置(80²+40²+20²)做阈值筛选和 NMS 就够了。8400 这个数字在小目标密集的 PCB 图上会带来另一个麻烦:重叠框变多,NMS 的 IoU 阈值调不好就会把相邻的两个真实缺陷合并成一个。

2.3 Decoupled Head:分类与回归为何必须分别处理

在早期的单阶段检测器里,分类和回归共用一个卷积头,输出通道直接拼在一起。这种做法在小数据集上问题不大,但在 PCB 这种「类别判断靠纹理、位置判断靠边缘」的场景里会互相拖累。分类分支关心的是这块区域有没有焊盘缺失的圆形空洞、有没有铜渣的粗糙质感,它需要的是语义层面的、感受野更大的特征;回归分支关心的是缺陷的边界到底压在第几个像素上,它需要的是位置敏感的、高分辨率的特征。两种需求挤在同一个卷积核里,梯度方向会打架。

Decoupled Head 的做法是把两条分支拆开:每个检测位置上,先各自过两层 3×3 卷积,然后分类分支输出 nc 个通道,回归分支输出 4×16 个通道。拆开之后,分类分支可以专心学「这像不像毛刺」,回归分支专心学「边界在左边第 3 个像素还是第 4 个像素」。

拆开还有一个容易被忽略的好处:训练和推理的一致性更好。分类分支输出的分数直接参与正样本分配的对齐度量(下一章会讲),回归分支输出的分布直接参与 DFL 计算,两条链路互不干扰,调参时也更容易定位问题——如果分类分数普遍偏低,去看分类分支;如果框的位置普遍偏移,去看回归分支。耦合在一起的时候,你只能看到「mAP 掉了」,然后靠猜。

3. 损失函数与标签分配:0.92 的 mAP 是如何分配出来的

3.1 Task-Aligned Assigner:正样本不按 IoU 一刀切

标签分配决定了「哪些预测位置需要为哪个真实框负责」。这一步在目标检测里比损失函数本身更影响最终精度,因为它决定了模型在训练时看到的是什么样的监督信号。

传统做法有两种。一种是固定 IoU 阈值,比如 IoU 大于 0.5 就算正样本,问题是 PCB 上缺陷密集时,一个预测框可能和两个真实框的 IoU 都超过 0.5,强行二选一会引入噪声。另一种是 ATSS 那样的自适应阈值,按 IoU 分布的均值和方差定阈值,比固定阈值稳,但仍然只看位置重合度,不看分类置信度。

YOLOv8 用的是 Task-Aligned Assigner,核心是把分类和回归的对齐程度合成一个度量。对每个真实框,它对所有预测位置算一个对齐分数:

t = s^alpha * u^beta

其中 s 是该位置对真实框所属类别的分类预测分数,u 是该位置与真实框的 IoU,alpha 和 beta 是两个指数,常见取值是 0.5 和 6.0。beta 远大于 alpha 意味着位置重合度在筛选里占主导,分类分数起微调作用。算出 t 之后,对每个真实框取 t 最大的前 10 个位置作为正样本。

这个设计对 PCB 检测的意义在于:一个位置即使 IoU 很高,如果分类分数很低(模型认为这里是背景),它也不会被选为正样本,避免了「用错的监督信号教模型」。反过来,一个分类分数很高但 IoU 一般的位置会被选进来,帮助模型在边界模糊的缺陷上收敛。

简化后的实现逻辑大致如下:

def task_aligned_assign(cls_score, iou, gt_cls, topk=10, alpha=0.5, beta=6.0): """ cls_score: [N, C] 每个预测位置对每类的 sigmoid 分数 iou: [N, M] 每个预测位置与每个真实框的 IoU gt_cls: [M] 每个真实框的类别索引 """ # 只取与该真实框类别对应的分类分数,而不是 max over all classes s = cls_score[:, gt_cls] # [N, M] align = (s ** alpha) * (iou ** beta) # 对齐度量 t # 每个真实框独立取 topk,正样本可以重叠(一个位置服务多个 GT) topk_idx = align.topk(topk, dim=0).indices # [topk, M] pos_mask = torch.zeros_like(align, dtype=torch.bool) pos_mask.scatter_(0, topk_idx, True) return pos_mask

这段代码有两个细节值得注意。第一,s 取的是真实框对应类别的分数,不是所有类别里的最大值,这样才不会出现「预测成毛刺的位置被分配去学开路」的错位监督。第二,每个真实框独立取 topk,允许同一个位置同时是多个真实框的正样本,这对 PCB 上紧挨着的两个缺陷很重要——强行做一对一匹配会让其中一个缺陷失去监督。

3.2 VFL + DFL + CIoU:三个损失各自负责什么

YOLOv8 的损失是三部分加权和,每一部分解决一个特定问题。

分类损失用的是 VFL(Varifocal Loss)。PCB 检测里正负样本比例极端失衡,一张 640 图上的 8400 个候选位置里,真正对应缺陷的可能只有几十个。普通的交叉熵在负样本占 99.5% 的情况下,梯度会被负样本主导,模型倾向于全部预测为背景。VFL 的做法是对正样本按 IoU 加权、对负样本做分数衰减,让难负样本的梯度不至于淹没正样本。公式形式是:正样本的损失按预测分数和 IoU 的乘积降权,负样本的损失按预测分数的幂次降权。结果是模型不再被海量负样本牵着走。

回归损失由 DFL 和 CIoU 两部分组成。DFL 解决的是「边界距离怎么表示」的问题。前面说过,Anchor-Free 要直接回归绝对距离,数值范围大、梯度不稳。DFL 把每条边的距离建模成 16 个离散区间的概率分布,然后用期望值还原成连续距离。这样做的好处是梯度只更新相邻的两个 bin,不会因为一个像素的偏差导致整个回归值剧烈跳变,对小目标的边界尤其友好。

import torch import torch.nn as nn class DFL(nn.Module): """把 16 个离散 bin 的分布积分成一条边的连续距离""" def __init__(self, reg_max=16): super().__init__() self.reg_max = reg_max # project 是固定权重 [0, 1, ..., 15],注册为 buffer,不参与梯度更新 self.register_buffer("project", torch.arange(reg_max).float()) def forward(self, x): # x: [B, 4*reg_max, H, W] b, _, h, w = x.shape # 拆成四条边,每条边做 softmax 得到概率分布 prob = x.view(b, 4, self.reg_max, h, w).softmax(dim=2) # 与 [0..15] 加权求和,得到期望距离,形状 [B, 4, H, W] return (prob * self.project.view(1, 1, -1, 1, 1)).sum(dim=2)

project用 buffer 而不是 Parameter 是关键——它代表的是 bin 的中心位置偏移,是人为定义的坐标,不应该被学习。如果误写成可训练参数,训练初期这个权重会漂移,四条边的距离整体偏移,表现就是「所有框都往一个方向偏」。踩过这个坑的人不少,因为代码看起来完全正常,只是框永远偏几个像素。

CIoU 负责的是框的整体质量,它同时考虑重叠面积、中心点距离和长宽比一致性。对 PCB 缺陷来说,长宽比一致性这项特别有用:开路是细长条,模型如果预测出一个接近方形的框,长宽比惩罚会把它拉回细长形状,避免框把周围的铜箔也包进来。

3.3 500 张训练图怎么不训练崩溃

数据集是 600 张图按 5:1 划分,500 张训练、100 张验证。这个量级在深度学习里属于极小样本,训练策略稍有不慎就会过拟合或者根本不收敛。

学习率方面,批量大小 16、总轮数 200 的情况下,初始学习率 0.01 配余弦退火是常见起点。关键是 warmup,前 3 个 epoch 从极小学习率线性爬升,避免一开始梯度爆炸把预训练权重冲垮。如果是从头训练而不是加载预训练权重,warmup 的轮数还要加长。

数据增强方面,PCB 图像有个特殊性:板子可以任意旋转,旋转 90 度、180 度不会改变缺陷的语义,所以 degrees=180 是安全的增强,能有效扩充样本。但缩放要克制。缺陷的相对尺寸是有物理意义的,一个 20 像素的毛刺放大到 40 像素之后,模型学到的尺寸先验就乱了,所以 scale 一般不超过 0.3。Mosaic 增强把四张图拼成一张,能显著提升小目标的出现频率,但在最后 20 个 epoch 必须关掉,让模型适应真实的单图分布,否则验证集的 mAP 会明显低于训练集。

还有一个容易被忽略的点:分类别的样本数。六类缺陷里,短路和开路在真实产线上出现频率远高于老鼠咬痕和铜渣,如果直接按原始分布训练,稀有类别的召回率会很低。可行的做法是在数据加载器里对稀有类别做重采样,或者用 copy-paste 增强把稀有缺陷贴到正常板图上。这两种做法都会改变类别分布,验证集必须保持原始分布才能反映真实性能。

4. 从 600 张图到可用系统:数据管线与四种推理场景

4.1 数据集构建与 5:1 划分的隐藏问题

600 张图、六类缺陷、500 训练 100 验证,这个划分看似标准,但有个坑:如果按文件名顺序或者随机打乱直接切分,很可能出现某些类别在验证集里只有三五个实例。mAP 是按类别算的,一个类别只有 3 个实例时,一个漏检就能让这类 AP 掉 30 个点,整体 mAP 的波动会大到没法比较模型优劣。

稳妥的做法是分层划分:先按类别统计每类的实例总数,然后在每类内部按 5:1 切分,保证每类在验证集里的实例数至少占总数的 15% 以上。如果某类总实例数太少(比如铜渣只有 20 个),那就要么补充数据,要么在报告结果时单独说明这个类别的置信区间。

标注质量同样关键。PCB 缺陷的边界有时并不锐利,比如老鼠咬痕的边缘是渐变的,标注框画大一点还是小一点,IoU 会差出 0.1 以上。经验是:框紧贴缺陷的可见边界,不要为了「包住整个可疑区域」而外扩,外扩会让模型学到「框比缺陷大」的偏差,推理时框会把周围正常的铜箔也算进去,表现为误检率偏高。

4.2 四种推理场景共享同一套前处理流程

系统支持单图片、批量图片、视频文件、摄像头四种输入,看起来是四个功能,实质上只有一套核心流程:读入帧 → letterbox 前处理 → 模型推理 → NMS 后处理 → 坐标映射回原图 → 绘制结果 → 输出。差别只在调度方式。

letterbox 这一步值得单独讲,因为直接 resize 是新手最常犯的错误:

import cv2 def letterbox(img, new_size=640, color=(114, 114, 114)): """等比缩放 + 灰边填充,保持缺陷长宽比不变""" h, w = img.shape[:2] r = min(new_size / h, new_size / w) nh, nw = int(round(h * r)), int(round(w * r)) pad_h, pad_w = new_size - nh, new_size - nw top, left = pad_h // 2, pad_w // 2 if (w, h) != (nw, nh): img = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_LINEAR) img = cv2.copyMakeBorder(img, top, pad_h - top, left, pad_w - left, cv2.BORDER_CONSTANT, value=color) # 返回缩放比例和偏移量,后处理时用它把框映射回原图 return img, r, (left, top)

直接 resize 会把圆形的缺失孔压成椭圆,模型看到的形状和训练时的形状不一致,回归分支输出的边界距离就会偏;更隐蔽的是,长宽比变了之后,CIoU 里的长宽比惩罚项会算出一个错误的梯度。返回的r和(left, top)必须一路传到后处理,用(x - left) / r把框还原,否则画在原图上的框会整体偏移。

批量检测和单图检测的差别在于内存管理。一次性把几百张图读进列表再逐张推理,在 8GB 显存的卡上跑到一半就会 OOM。可行的做法是分批读取、推理完立刻释放中间张量,或者干脆用生成器逐张吞吐。

视频检测的难点不在模型,而在调度。如果按帧率同步处理——读一帧、推理一帧、写一帧——处理速度跟不上视频帧率时,缓冲区会不断堆积,延迟越跑越大,最后内存爆掉。常见的实现思路是维护一个长度为 1 的队列:新帧到来时直接覆盖旧帧,处理线程永远只取最新的那一帧。这样会丢帧,但延迟恒定,对实时监控场景来说丢帧比延迟累积可接受得多。

摄像头检测还多一层协商问题:打开设备时要先设分辨率再设编码格式,顺序反了某些设备会回退到默认的 640×480,导致实际输入比预期小,小目标更难检出。

4.3 置信度阈值:训练指标和部署参数是两回事

训练结束时报告的 mAP@0.5 平均 0.92、各类在 0.86 以上,这个数字是在置信度阈值极低(通常 0.001)、NMS IoU 固定 0.7 的条件下算出来的,目的是让 PR 曲线覆盖完整。直接把这个配置搬到系统里,界面上会画出一堆重叠的框,用户第一眼就会觉得「这系统不靠谱」。

部署时的阈值要按业务代价来定。漏检一块有开路的板子流到下一道工序,损失可能是整块板报废;误检一块好板,代价只是人工复检一次。所以优先保证召回率,置信度阈值放到 0.25 到 0.35 之间,NMS IoU 调到 0.5 到 0.6,宁可多框几个,也不要漏。如果界面上要显示缺陷总数,还要考虑重叠框的合并逻辑,否则同一个毛刺会被数成两个。

5. 常见问题排查:从漏检到卡顿的现场记录

5.1 小毛刺集体失踪,mAP 卡在 0.6 上不去

现象:训练日志里缺失孔、开路的 AP 都在 0.9 以上,毛刺和铜渣始终在 0.5 附近徘徊,验证集可视化图上小缺陷一个框都没有。

原因:毛刺在原图上只占十几到几十个像素,640 输入下映射到 P3 只剩 1 到 2 个格子,特征响应被周围铜箔的纹理淹没;同时训练集里毛刺实例数最少,正样本分配时经常分不到足够的位置。

解决:先把 imgsz 提到 1024,观察毛刺的 AP 是否上升,这一步能确认是不是分辨率问题。如果上升明显,就固定 1024 训练,代价是推理变慢,可以在部署时用半精度或者 TensorRT 补回来。如果上升不明显,说明是样本问题,用 copy-paste 把毛刺实例贴到不同的板图背景上,把实例数扩到和其他类别同一量级。

5.2 置信度在 0.4 到 0.6 之间反复震荡

现象:同一个缺陷,连续推理十次,置信度在 0.42、0.55、0.48 之间跳,画出来的框位置也有一两个像素的抖动。

原因:验证集只有 100 张,模型在训练末期还在轻微过拟合,权重对输入的微小扰动敏感;另外如果推理时忘了切 eval 模式,BatchNorm 用训练批次的统计量,输出就会随 batch 内容变化。

解决:先确认推理代码里有model.eval()和torch.no_grad(),这两个漏掉是最常见的玄学来源。确认无误后,检查数据增强是否在验证阶段被误开启,验证必须关掉 mosaic 和随机翻转。如果抖动仍然存在,把训练末期的学习率再降一个量级,或者用指数滑动平均保存权重。

5.3 批量检测跑到一半显存溢出

现象:单张检测正常,选一个包含 300 张图的文件夹批量跑,跑到第 80 张左右报显存不足。

原因:把整个文件夹的图片一次性读进内存并转成张量列表,每张 640×640×3 的 float32 张量占约 4.9MB,300 张就是 1.5GB,再加上模型和前向传播的中间激活,显存很快见底。

解决:改成流式处理,用生成器逐张读取、推理、保存、释放。如果一定要批处理提速,把 batch 固定成 8 或 16,每批处理完显式del中间变量并调用torch.cuda.empty_cache()。注意empty_cache()有性能开销,不要每张图都调。

5.4 视频检测越跑越卡,延迟从 200ms 涨到几秒

现象:刚打开视频时流畅,跑一两分钟后画面明显滞后,任务管理器里内存持续上涨。

原因:读帧线程和推理线程速度不匹配,帧缓存队列无限增长;或者每一帧的结果图都被追加到一个列表里等着最后统一保存,列表越攒越大。

解决:把帧队列长度限制为 1,新帧覆盖旧帧;结果保存改成边处理边写视频文件,用cv2.VideoWriter逐帧写入而不是攒在内存里。如果还是卡,检查是不是每帧都在重建模型或者重复加载权重,这种低级错误在集成阶段很常见。

5.5 离线指标 0.92,上线后误检率明显偏高

现象:验证集上表现很好,接到实际产线的图片上,正常的过孔和丝印被大量识别成缺失孔和毛刺。

原因:训练数据来自网络搜集,光照条件、板子颜色、拍摄角度和产线相机差异很大;模型学到的是「这批图上的缺陷长什么样」,而不是「缺陷的本质特征」。这是小样本数据集最根本的泛化边界。

解决:从产线相机采集一批正常板图和已知缺陷板图,哪怕每类只有几十张,混进训练集做微调,效果通常比调任何超参都明显。同时把误检样本单独存下来,做成一个负样本集,定期回流到训练流程里。这套数据闭环才是这类系统能长期用的关键,模型本身反而是次要的。

6. 设计取舍:精度、速度与那 600 张图的边界

这套系统的技术选择里,有几处取舍值得单独想一想。

单阶段加 Anchor-Free 的组合,换来了推理速度和形状适应性,代价是小目标的召回天花板偏低。如果场景换成大片缺陷为主的工业检测,两阶段或者带高分辨率分支的结构可能更合适;但在需要实时反馈、缺陷以中小目标为主的 PCB 场景里,这个取舍是划算的。真正需要警惕的是把「mAP 0.92」当成系统能力的证明——这个数字建立在 100 张验证图、六类缺陷、特定光照条件之上,换一批图就可能掉到 0.7。小样本任务里,指标的置信区间比指标本身更重要,报告结果时把每类的实例数一起列出来,比只写一个平均值诚实得多。

C2f 和双向颈部的选择也是同样的道理。多分支结构保留了浅层细节,但显存和延迟都上去了;如果部署在边缘设备上,可能要把 P5 分支砍掉,只保留 P3 和 P4,牺牲大目标的检测能力换取帧率。参数没有绝对的对错,只有和场景匹配与否。

还有一处容易被忽视的取舍:置信度阈值。训练阶段追求的是 PR 曲线下的面积,部署阶段追求的是业务代价最小化。这两个目标在数值上并不一致,把训练配置直接搬到系统里,界面效果一定不好看。我现在做任何检测系统,都会把阈值做成可调参数暴露在界面上,而不是写死在代码里。

从那以后我每次交付这类系统,都会强制走一遍「产线图回测」——拿至少 50 张现场图跑一遍,看误检和漏检的实际分布,再决定阈值和是否需要补数据,而不是只看验证集的 mAP。这份毕业设计里完整的训练流程、六类缺陷的数据组织方式、四种推理入口的集成代码和测试记录,正好可以作为这套流程的起点,拿去改改就能跑在自己的数据上。希望帮到你。

本文还有配套的精品资源,点击获取