☰
680张图也能训好YOLO:PCB缺陷检测实战指南
2026/10/1 23:52:58 网站建设 项目流程

简介:一份面向工业视觉入门与电子质检场景的 PCB 缺陷检测数据集,包含 680 张产线实拍高清图像,覆盖桥接、缺失、短路、断路、偏移、极性错误 6 类典型瑕疵,标注遵循 YOLOv5/v8/v9 格式,按 7:2:1 划分训练、验证与测试集,可直接接入 Ultralytics 训练流程。压缩包共 1376 个文件,含 680 张 jpg 原图、693 个 txt 标注文件及 data.yaml 配置与缓存文件,整包 77.85MB,轻量易用,适合快速完成目标检测实训。已有 26 人学习。资源额外提供中文 readme,说明采集设备、标注规范与训练命令,并附带缺陷可视化分析工具,可生成类别分布、标注密度等报告;经 YOLOv8n 实测 mAP@0.5 达 86.3%,能支撑从数据加载、模型微调到性能评估的全流程实践。

1. 只有680张图的PCB缺陷数据集,凭什么能训出能用的YOLO模型

PCB缺陷检测数据集(6类,680张图像,YOLO格式)这个规模在工业质检项目里其实是常态,不是特例。产线上短路、开路、缺孔、鼠咬、多余铜、毛刺六类缺陷,形态固定但真出现时往往一批板子也就留得下几十张有效图像。这份数据集的姿势很直接:txt标注、归一化坐标、类别编号齐全,拿到手就能往YOLO训练脚本里填。680张图听着少,配合YOLO的预训练权重迁移、数据增强和合理的验证集划分,按常见路径推进时mAP能稳定在0.85以上,对刚接触缺陷检测的工程师,这是一份能快速跑通全流程的最小数据集;对熟手,它是测试增强策略和调参手感的试验场。接下来从数据格式、训练配置到翻车现场一步步拆开讲。

2. 先读懂PCB六类缺陷与YOLO标注格式,再动手训练

2.1 六类缺陷定义与成像特征:从类别名到标注规则

拿到数据集第一件事不是急着跑训练,先把labels目录打开看一遍。这份数据集把PCB缺陷分成六类,它们的成像特征差异明显,标注规则也各有侧重。

class_id类别名成像特征典型成因
0missing_hole焊盘中央该有的孔却没有,反光形状和正常孔明显不同钻孔偏移、钻头磨损
1mouse_bite导线边缘出现半月形缺口,像被老鼠咬过蚀刻过度、铜箔损伤
2open_circuit铜箔导线中间断裂,信号通道不连续蚀刻断裂、搬运划伤
3short两条导线之间的铜箔粘连,间距被桥接蚀刻不足、残铜未清
4spurious_copper非线路区域残留的零星铜箔,形状不规则显影残留、蚀刻不均
5spur导线边缘突出的尖角状铜箔,细长且尖锐蚀刻方向异常、铜箔毛刺

这张表不只是给人看的,它直接决定后续增强策略。比如mouse_bite和spur在成像上都是导线边缘的缺损,模型很容易混淆,所以标注时框要紧贴缺陷像素,不能把整段导线框进去。我见过不少团队在标注阶段图省事,框比实际缺陷大两三倍,模型学到的变成“导线边缘”而不是“缺陷本身”。open_circuit的断裂点往往只有几个像素宽,标注偏移一个像素,IoU就掉到0.5以下,这也正是很多PCB检测项目在验证集上mAP始终上不去的隐形原因。

标注框还有个容易被忽略的规则:如果一个缺陷横跨两条导线,比如short桥接了相邻导线,应该用一个框把整个桥接区域框住,而不是分别标两个小框。一个框对应一个独立缺陷,这是YOLO格式处理检测任务的基本约定。如果你拿到手的数据集里发现同一个缺陷被切成多个框,训练前要合并回去,否则模型学到的是残缺目标,推理时会出现一个缺陷打出两三个框的现象,NMS后检出的置信度也会被摊薄。

2.2 YOLO格式的txt标注:归一化坐标与类别编号对应关系

这份数据集的标注是YOLO格式,每张图像对应一个同名txt文件。txt里每一行代表一个缺陷框,内容格式是:

class_id center_x center_y width height

center_x和center_y是缺陷框中心点相对于图像宽高的比例,width和height是框宽高相对图像宽高的比例。全部是0到1之间的浮点数,不是像素坐标。之所以用归一化坐标而不直接用像素值,是因为YOLO训练时会做随机缩放和裁剪,归一化坐标在resize后不需要重新计算,框架内部通过比例映射就能还原真实位置。

labels目录里的txt文件打开长这样:

0 0.5234 0.6125 0.0432 0.0287 3 0.3108 0.4257 0.0185 0.0154

第一行是类别0(missing_hole)的缺陷框,中心点在图像横向52.34%、纵向61.25%的位置,框宽占图像宽度4.32%,框高占图像高度2.87%。第二行是类别3(short)的缺陷框,数值含义相同。看这个数值分布就知道缺陷框的尺寸占比很小,最大的框也不到图像面积的5%,这是PCB缺陷数据集最典型的特点,也决定了训练时必须把imgsz设到640以上,否则小目标直接就消失在特征图里。

第一次跑YOLO时我踩过坐标写错的坑:标注脚本把center_x算成了左上角x,导致所有框偏移了半个图像宽度,训练出来的模型在验证集上mAP直接归零。所以训练前我会先写一段脚本检查标注和图像是否对得上:

import cv2 import os def verify_annotation(image_path, label_path, min_size=5): """检查YOLO标注是否在图像边界内且尺寸正常""" img = cv2.imread(image_path) if img is None: print(f"图像读取失败: {image_path}") return False h, w = img.shape[:2] ok = True with open(label_path, 'r') as f: for line in f.readlines(): parts = line.strip().split() if len(parts) != 5: print(f"格式错误: {label_path} -> {line.strip()}") continue cls_id = int(parts[0]) cx, cy, bw, bh = map(float, parts[1:]) if not (0 <= cls_id < 6): print(f"类别编号越界: {label_path} -> {cls_id}") ok = False x1 = (cx - bw / 2) * w y1 = (cy - bh / 2) * h x2 = (cx + bw / 2) * w y2 = (cy + bh / 2) * h if x1 < 0 or y1 < 0 or x2 > w or y2 > h: print(f"越界框: {label_path} -> cls={cls_id}") ok = False if (x2 - x1) < min_size or (y2 - y1) < min_size: print(f"过小框: {label_path} -> cls={cls_id}") ok = False return ok base = '/data/pcb_defect' for split in ['train', 'val']: img_dir = os.path.join(base, 'images', split) lbl_dir = os.path.join(base, 'labels', split) for file in os.listdir(img_dir): name = file.rsplit('.', 1)[0] img_path = os.path.join(img_dir, file) lbl_path = os.path.join(lbl_dir, name + '.txt') if not os.path.exists(lbl_path): print(f"缺少标注: {lbl_path}") continue verify_annotation(img_path, lbl_path)

这段脚本只用了OpenCV和os两个标准库,没有额外依赖。verify_annotation函数做的事有三件:检查每行是否正好五个字段;检查类别编号是否在0到5之间;把归一化坐标反算回像素坐标,确认框没有越界、没有小于5像素的废框。

之所以要在训练前跑一遍这个脚本,是因为这类数据集经常从转换工具或人工标注平台导出,归一化坐标在四舍五入时会出现x2比图像宽度多一个像素的情况。图像边缘的缺陷框一旦越界,YOLO训练时Dataloader做马赛克拼接会把这些框裁掉,模型等于丢失了一部分正样本。而小于5像素的框经过640×640下采样后,在特征图上不到一个像素,基本学不到任何特征,相当于噪声标注,直接过滤掉反而干净。我每次检查完这个脚本,多多少少都能挑出几条问题标注,这份680张图像的数据集也不例外。

如果你训练时发现某些类别AP偏低,回头先跑一遍这个脚本,把越界框和过小框清理掉,AP往往能回升三到五个点。这是处理数据集用于yolov8训练的第一步,也是被跳过得最多的一步。

2.3 680张图像的目录划分:随机切还是按组切

目录划分直接决定了训练评估的可信度。常见做法是按8:1:1随机切,但我不建议直接随机切。PCB缺陷图往往来自同一批板卡的不同拍摄位置,如果随机切,同一块板的不同区域会同时出现在训练集和验证集,模型“见过”的语义信息被带进了验证集,mAP会虚高两到三个点,等部署到新板卡上立刻露馅。

如果680张图像是按板卡编号命名的,先按编号分组,保证同一组图像不会被切到两个集合里。划分逻辑用Python实现很简单:

import os import random from collections import defaultdict base = '/data/pcb_defect' img_dir = os.path.join(base, 'images') img_paths = os.listdir(img_dir) # 按文件名中板卡编号分组,假设文件名格式为 board_001_patch_1.jpg groups = defaultdict(list) for p in img_paths: board_id = p.split('_')[0] groups[board_id].append(p) random.seed(42) group_list = list(groups.keys()) random.shuffle(group_list) train_ratio, val_ratio = 0.8, 0.1 n_train = int(len(group_list) * train_ratio) n_val = int(len(group_list) * val_ratio) train_groups = group_list[:n_train] val_groups = group_list[n_train:n_train + n_val] test_groups = group_list[n_train + n_val:] # 输出划分结果,后续按这个清单移动图像和标注 for split, gids in [('train', train_groups), ('val', val_groups), ('test', test_groups)]: for gid in gids: for f in groups[gid]: print(f"{split}\t{f}")

注意代码里的board_id提取逻辑是按文件名格式写的,实际使用时要改成你数据集的实际命名规则。如果文件名里没有板卡编号,退一步的做法是按patch的坐标位置分组——比如文件名包含图像在整板上的行列号,那么同一行相邻patch也属于同板区域,应尽量归到同一个集合。这个细节决定了验证集mAP的可信度,值得花十分钟处理。

划分之后记得检查每个集合里的类别分布。680张图总缺陷框数大约在1200到1500之间,如果test集里missing_hole只有两三个框,那测试结果就没有统计意义。我会手动从val或者train里调几张包含missing_hole的图像进test,保证每类在test里至少有5个框。另外,划分时不要只划图像,要同步移动labels目录下的同名txt。一个常见错误是移动了jpg但漏了txt,YOLO训练时会反复警告image without labels,浪费排查时间。

3. 用YOLOv8训练这份数据集:yaml配置、命令与参数调优

3.1 数据集yaml配置文件:路径结构、nc与names

YOLOv8训练的第一步是写数据集配置yaml。这个文件告诉ultralytics框架去哪里读图像、标注有几类,以及类别名称。yaml写在数据集根目录下,内容如下:

# pcb_defect.yaml path: /data/pcb_defect # 数据集根目录 train: images/train # 训练图像相对路径 val: images/val # 验证图像相对路径 test: images/test # 测试图像相对路径 nc: 6 # 缺陷类别数 names: 0: missing_hole 1: mouse_bite 2: open_circuit 3: short 4: spurious_copper 5: spur

path字段是绝对路径,train/val/test写相对于path的路径。yaml里的类别编号和顺序必须与txt标注文件里的class_id一一对应,这是最常见的配置错误来源。举例来说,如果yaml里把short写成第3类,但txt标注文件里short的class_id是2,训练时模型会把两类缺陷完全学反,验证集mAP看起来却不会太差,因为你拿错标注去比错了检测结果,整个评估失去意义。

ultralytics框架启动时会解析yaml并拼接图像路径,所以保证images目录下的jpg文件和labels目录下的txt文件数量一致、文件名一致。用以下命令快速核对:

ls /data/pcb_defect/images/train | wc -l ls /data/pcb_defect/labels/train | wc -l

两个数字必须相等。如果labels比images少,说明有图没标;反过来则是有标注没图,都要先补齐。yaml里我习惯不写test字段,只在验证时用val,test留到最终评估时单独写,避免训练过程误用test做早停判断。

3.2 训练启动命令:imgsz、batch与epochs的选择逻辑

配置好yaml后,训练命令非常简洁。如果你装好了ultralytics包,最小可运行的训练命令是:

yolo detect train \ data=/data/pcb_defect/pcb_defect.yaml \ model=yolov8n.pt \ epochs=150 \ imgsz=640 \ batch=16 \ project=/output/pcb_yolov8 \ name=run1

这段命令里每个参数都值得细说。model=yolov8n.pt是预训练权重,n代表nano,是YOLOv8系列里参数量最小的模型,约310万参数。680张图的数据量,用nano是合理起点——用yolov8x这种大模型训练几乎必然过拟合,验证集mAP反而更差。imgsz=640表示训练时把图像统一缩放到640×640,PCB缺陷是小目标,这个分辨率不能降,降到416的话missing_hole这种几个像素宽的缺陷就彻底消失了。batch=16是每次迭代同时处理16张图,显存足够就设16,不够就下调用梯度累积兜底。epochs=150是训练轮数,配合早停一般足够收敛。

我通常还会额外追加几个参数,让训练更可控:

yolo detect train \ data=/data/pcb_defect/pcb_defect.yaml \ model=yolov8n.pt \ epochs=150 \ imgsz=640 \ batch=16 \ patience=30 \ lr0=0.005 \ seed=42 \ project=/output/pcb_yolov8 \ name=run1

patience=30是早停策略,连续30轮验证集mAP没有提升就停止训练,防止后段过拟合。lr0=0.005是初始学习率,比默认0.01低一半。迁移学习场景下用小学习率可以避免预训练权重被大幅破坏,PCB图像和COCO图像差异大,学习率稍低能让模型在稳定区域内微调。seed=42固定随机种子,让每次训练结果可复现。参数调优时如果不固定随机种子,你改了某个参数但效果变化可能只是随机波动,根本无法判断哪个改动起了作用。

训练完成后验证命令是:

yolo detect val \ data=/data/pcb_defect/pcb_defect.yaml \ model=/output/pcb_yolov8/run1/weights/best.pt

验证输出会给出mAP50、mAP50-95以及每个类别的Precision和Recall。看到哪些类别AP明显低于均值,再回去查该类别的样本量和标注质量。

3.3 迁移学习与预训练权重:小数据集训练的胜负手

680张图像直接从头训练YOLO基本不可行。CNN特征提取器需要大量数据才能学会基础纹理、边缘和颜色特征,两三百张图训练出来的backbone学到的全是噪声。所以必须用预训练权重做迁移学习。YOLO预训练模型下载渠道很成熟,yolov8n.pt是COCO数据集上预训练好的权重,COCO有80类自然图像,虽然和PCB缺陷域差异很大,但底层卷积核学到的边缘、纹理、颜色渐变特征是可以迁移的。

训练时冻结前几层是一个常见调优手段。前几层学到的是通用特征,在PCB图像上同样适用,冻结它们不更新梯度,训练更稳定、显存占用更小。实际操作中用freeze参数指定冻结层数:

yolo detect train \ data=/data/pcb_defect/pcb_defect.yaml \ model=yolov8n.pt \ epochs=150 \ imgsz=640 \ batch=16 \ freeze=10 \ lr0=0.003 \ project=/output/pcb_yolov8 \ name=run_freeze10

freeze=10表示冻结前10层,主要是backbone前段的卷积模块。我处理PCB缺陷数据时一般从freeze=10起步,然后对比验证集mAP再决定加大还是减小。如果PCB图像是在暗场、低对比度下拍摄的,和COCO自然图像的特征分布差得远,冻结层反而限制了特征提取,需要把freeze降到5甚至0。这里有一个判断技巧:训练到30轮左右时看一眼验证集的Precision曲线,如果曲线持续很低且不抬头,多半是冻结层数过多导致模型学不到PCB的底层特征,果断减少freeze层数重跑。

预训练权重属于典型的“黑匣子”依赖,用之前记录版本和来源。不同版本的yolov8n.pt结构一致但权重数值不同,混用可能导致效果偏差。我会在模型目录里写一个weights_notes.txt,记录权重文件名、下载时间、训练配置和对应mAP,换权重时方便回溯。

4. 680张不够用?数据增强与伪标签撑起泛化能力

4.1 离线增强:用albumentations扩充缺陷样本

680张原始图像直接训练,模型见过的缺陷形态太有限。离线增强是最直接的办法,在训练前用图像处理库生成扩充样本。albumentations是目前工业视觉最常用的增强库,支持同步变换图像和边界框,YOLO格式的归一化坐标可以直接传入。

下面是一段针对PCB缺陷数据的增强脚本,按常见做法把小样本训练里最有效的几类增强都配置好了:

import albumentations as A import cv2 import os aug = A.Compose([ A.RandomBrightnessContrast(p=0.8, brightness_limit=0.15, contrast_limit=0.15), A.HueSaturationValue(p=0.5, hue_shift_limit=5, sat_shift_limit=10, val_shift_limit=10), A.RandomGamma(p=0.5, gamma_limit=(80, 120)), A.ShiftScaleRotate(p=0.5, shift_limit=0.05, scale_limit=0.1, rotate_limit=15), A.GaussNoise(p=0.3, var_limit=(10.0, 40.0)), A.CLAHE(p=0.3, clip_limit=2.0, tile_grid_size=(8, 8)), ], bbox_params=A.BboxParams( format='yolo', label_fields=['class_labels'], min_visibility=0.3, min_area=4 )) def augment_one_image(img_path, label_path, save_dir, idx): img = cv2.imread(img_path) boxes, labels = [], [] with open(label_path) as f: for line in f: cls, cx, cy, bw, bh = map(float, line.strip().split()) boxes.append([cx, cy, bw, bh]) labels.append(cls) augmented = aug(image=img, bboxes=boxes, class_labels=labels) out_img = augmented['image'] out_name = f"aug_{idx}.jpg" cv2.imwrite(os.path.join(save_dir, out_name), out_img) with open(os.path.join(save_dir, out_name.replace('.jpg', '.txt')), 'w') as f: for bbox, cls in zip(augmented['bboxes'], augmented['class_labels']): cx, cy, bw, bh = bbox f.write(f"{int(cls)} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n")

代码里的bbox_params是核心。format='yolo'告诉albumentations输入的是归一化坐标,它会在内部转换成像素坐标做变换,再转回归一化坐标输出。min_visibility=0.3表示变换后如果边界框可见面积少于30%,这个标注就丢弃,避免生成一半缺陷被裁掉的无效样本。min_area=4过滤掉变换后面积小于4像素的框,和训练前的检查脚本逻辑一致。

增强的每一项都有针对性。RandomBrightnessContrast模拟工厂不同班次的光照差异;ShiftScaleRotate的rotate_limit=15限制了旋转角度,PCB缺陷中的short和spurious_copper对旋转方向没有严格依赖,但旋转过大会引入大量背景干扰;GaussNoise模拟工业相机低照度下的传感器噪点。离线增强只对训练集做,val和test保持原始图像,这样评估结果才真实反映泛化能力。生成增强样本后建议把原图和增强图混在一起,比例控制在1:2以内,增强图太多了模型会过度适应噪声分布。

4.2 在线增强:YOLOv8内置增强参数调整

YOLOv8本身集成了在线增强,训练时动态变换图像,不额外占磁盘空间。ultralytics的增强参数可以在训练命令里直接覆盖默认值。

参数名默认值推荐值作用
hsv_h0.0150.02色调增强强度
hsv_s0.70.8饱和度增强强度
hsv_v0.40.5亮度增强强度
degrees0.05.0随机旋转角度
translate0.10.1平移比例
scale0.50.6缩放比例
fliplr0.50.5水平翻转概率
mosaic1.00.8Mosaic增强概率
mixup0.00.2Mixup增强概率

对应训练命令:

yolo detect train \ data=/data/pcb_defect/pcb_defect.yaml \ model=yolov8n.pt \ epochs=150 \ imgsz=640 \ batch=16 \ hsv_h=0.02 \ hsv_s=0.8 \ hsv_v=0.5 \ degrees=5 \ scale=0.6 \ mosaic=0.8 \ mixup=0.2

这些推荐值基于PCB缺陷数据的特点。PCB图像背景通常是绿色阻焊层,色调单一,所以hsv增强可以适当放开。degrees=5是因为PCB板在夹具里可能有轻微旋转但不会大角度倾斜,超过10度的旋转会产生明显背景变形。mosaic从默认1.0降到0.8要说明一下:PCB缺陷面积很小,mosaic拼接四张图后单个缺陷的像素占比进一步缩小,不利于小目标学习,适度降低mosaic概率反而有助稳定收敛。

如果离线增强已经扩充了大量样本,在线增强的强度要相应调低,两者叠加太多会让模型面对的变换过于剧烈,训练难度增大且收敛变慢。我一般离线增强做一套保底,在线增强用偏保守的参数,两条路都走,但都不走极端。

4.3 伪标签半监督:让未标注PCB图像参与训练

产线最不缺的就是未标注的图像。标注680张图要一两天,但拍几千张未标注图只要半天。伪标签(pseudo labeling)的思路是:先用680张标注数据训练一个初始模型,再用这个模型给未标注图像打标注,把高置信度的检测结果当成训练数据。

操作流程分两步。第一步推理生成伪标签:

yolo detect predict \ model=/output/pcb_yolov8/run1/weights/best.pt \ source=/data/pcb_unlabeled/images \ conf=0.6 \ save_txt=True \ save_conf=True \ project=/data/pcb_unlabeled/pseudo \ name=round1

conf=0.6是伪标签的置信度阈值,这个值很关键。阈值高于0.8,保留的标注太少,扩展作用有限;低于0.4,大量误检框被当成标注,污染训练数据。PCB缺陷背景单一,模型置信度往往呈两极分化,0.6到0.7是可接受区间。save_txt=True把检测结果存成和源图像同名的txt文件,格式和数据集标注一致,可以直接进训练。save_conf=True会在txt每行末尾附加置信度,方便后续筛选低质量伪标签。

第二步把伪标签混入训练集重新训练。我一般按伪标签:真实标签=1:2.5的比例混合,伪标签占比太高会把模型的偏差固化。第二轮训练完成后,用新模型回到第一步生成更高质量的伪标签,形成自训练循环。迭代三轮左右,mAP通常还能再提升三到五个百分点。这个收益在小数据集上比任何超参调优都明显,代价只是多跑几轮训练时间。

如果预算允许,更稳的做法是让模型挑出低置信度样本,人工快速复核后加入标注集。这属于人机协同标注,比纯伪标签更可靠,人工成本也远低于从零标注。

5. 小样本训练避坑:680张图像上最常见的五个翻车现场

5.1 Loss降到0.2但mAP只有0.3,模型学了个寂寞

现象:训练150轮后训练loss降到0.2以下,但验证集mAP只有0.3出头,Precision和Recall都惨不忍睹。

原因:这是典型的过拟合。680张训练图像经过150轮迭代,模型参数量足以把训练样本“背”下来,却没有任何泛化能力。验证集图像和训练集只要有一点光照或角度差异,检测性能立刻崩塌。

解决:先看训练日志里val_loss和train_loss的差距,如果val_loss始终是train_loss的两倍以上,从三个方向同时下手——增强离线增强强度(尤其是光照和噪声类)、增加数据多样性(用第4.4.3节的伪标签扩展)、把freeze层数加到10让浅层特征更通用。还有一个立竿见影的土办法:把epochs从150砍到80,配合patience=20早停,让模型没有足够时间去背样本。

5.2 验证集mAP震荡剧烈,每隔10轮波动超过5个点

现象:训练日志里mAP50曲线像锯齿,上一轮0.7下一轮0.62,再一轮又回到0.71,完全无法判断模型收敛状态。

原因:两个因素叠加。一是batch size太小,梯度估计噪声大;二是验证集图像太少。680张图切出的val通常只有68张,这68张里如果恰好包含几张角度偏差大的异常图,mAP就会随批次不同剧烈波动。batch size=4时BN层的统计量不稳定,训练过程本身也在震荡。

解决:val集扩容到100张以上,从train匀一部分图过来。学习率降到0.002以下让训练更平滑。如果显存允许,batch size提到32。还有一个评估层面的调整:用mAP50代替mAP50-95作为主要监控指标,后者对框的精确位置更敏感,在小验证集上更容易震荡。

5.3 short和spurious_copper几乎检测不到,其他四类正常

现象:验证结果显示missing_hole和open_circuit的AP都在0.7以上,但short和spurious_copper的AP只有0.2,接近随机猜测。

原因:两类缺陷的像素面积小,是典型的小目标。在640×640输入下,原始图像里只有几十个像素的缺陷经过下采样后在特征图上可能只剩一两个点。其次这两类缺陷的样本量往往最少,模型没有足够的正样本去学习形态特征。

解决:针对小目标缺陷,把imgsz从640提高到960甚至1280,代价是训练速度变慢、显存占用翻倍,但小目标的AP通常能提升十个百分点以上。如果不方便提分辨率,用切片推理策略——训练时保持640,推理时把原图切成多个640×640的patch分别检测,再合并结果。样本量层面,对这两类做针对性离线增强,把已有样本随机粘贴到背景板的其他位置生成新样本。

5.4 显存只有8GB,batch size只能设4,训练又慢又不稳

现象:显卡显存不够,batch size设到16直接OOM,只能用4硬撑。训练速度慢,而且验证集mAP波动明显。

原因:batch size过小时,BN层的均值和方差估计不准,梯度更新方向噪声大,模型不收敛。

解决:最常见做法是梯度累积,让模型每积累多次反向传播的梯度再更新一次参数,等效于增大batch size。在ultralytics中通过nbs参数实现:

yolo detect train \ data=/data/pcb_defect/pcb_defect.yaml \ model=yolov8n.pt \ epochs=150 \ imgsz=640 \ batch=4 \ nbs=16 \ lr0=0.003 \ project=/output/pcb_yolov8 \ name=run_gradacc

nbs是normalizer batch size,当batch=4而nbs=16时,框架会自动累积4次梯度再更新参数,等效于batch size=16。学习率要相应调低,这里lr0=0.003是配合nbs=16的取值。显存实在不够时,把imgsz降到480,效果通常比硬撑着batch=2要好。小分辨率模型初始精度略低,但收敛更稳定,后期可以再微调到640。

5.5 用了预训练权重反而比从头训练效果更差

现象:加载yolov8n.pt预训练权重训练150轮,mAP只有0.4,而不加载权重从头训练反而能到0.55。

原因:预训练模型的BN层参数适应了COCO数据分布,在PCB图像上微调时如果学习率过高或冻结层数不合理,BN统计量没有被正确更新,特征分布错乱,模型反而被“带偏”了。

解决:把freeze降到5以下,让backbone后面的层更快适应PCB图像。同时把lr0降到0.001或0.0005,用小梯度步长微调。另一个容易被忽略的问题是图像通道:PCB图像如果是灰度图,预训练模型的第一卷积层是3通道输入,和灰度单通道不匹配。解决方法是读图时把灰度图复制成3通道,用np.repeat(img, 3, axis=-1)或者用cv2.cvtColor转成BGR。如果数据集的图像本身就是三通道但颜色单一,这个问题不存在,但要确认所有图像通道数一致,避免训练中途报shape不匹配的错。

6. 验证与落地:从mAP曲线到产线质检的最小闭环

6.1 混淆矩阵:哪两类缺陷最容易互相误判

训练完成后不要只看整体mAP。打开ultralytics输出目录里的confusion_matrix.png,看哪两类缺陷互相误判最严重。PCB六类缺陷中,mouse_bite和spur在形态上都是边缘缺损,误判率通常最高。这里有个容易误解的细节:YOLO的混淆矩阵默认不画背景类,漏检的目标被归到background列,所以每一行总量不等于该类真实框数。看到行列对不上先别慌,是框架的统计口径,不是bug。如果spur被大量预测成mouse_bite,优先检查标注质量而不是继续调参。

6.2 导出ONNX:从训练服务器到产线相机

训练好的模型要部署到产线,ONNX是最通用的中间格式:

yolo export \ model=/output/pcb_yolov8/run1/weights/best.pt \ format=onnx \ imgsz=640 \ simplify=True

导出后交给ONNX Runtime或TensorRT推理。这里有一个踩过的坑:导出时的imgsz必须和训练时一致,否则推理性能明显退化。训练用640,导出就是640,不要图快改成416。如果现场相机是2000万像素的整板图像,推理前先做切片,切成640×640的patch逐块检测,再把检测框映射回原图坐标,避免小目标在整图缩放中丢失。

6.3 置信度阈值:漏检和误报的权衡

产线部署时的置信度阈值需要单独调。默认conf=0.25针对自然场景,但PCB缺陷检测中漏检成本远高于误检——一个缺陷漏过去就是整批板子报废。我一般先用conf=0.1跑一遍验证集,看能检出多少真实缺陷,再逐步提高阈值,在漏检率和误报率之间找平衡。缺陷率低于0.5%的产线,宁可把误报放宽到3%到5%,也要把漏检率压到0.1%以下。

最后说一个我的习惯:每次训练完,best.pt和last.pt都保留,两者对比能发现是不是早停太早,这相当于给自己留了一颗后悔药。验证集的fail case截图归档到固定目录,下次训练前先翻一遍历史失败案例,确认它们有没有被修复再决定调参方向。调参很多时候是玄学,但记录每一次实验的心得之后,玄学慢慢会变成手感,这套流程跑顺之后,680张图的数据集真的够撑起一条初步的PCB缺陷检测产线。希望帮到你。

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

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

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

立即咨询