简介:这套YOLO目标检测配套钢材表面缺陷数据集,包含1800张已标注图像,覆盖crazing、inclusion、patches、pitted_surface、rolled-in_scale、scratches六类典型表面缺陷,适合计算机、电子信息工程、数学等专业学生用于课程设计、期末大作业或毕业设计中的目标检测模型训练与验证,也适合初学者快速上手缺陷检测项目。资源共3608个文件,主体为1800张jpg原图与1800个txt格式标注文件,另配yaml类别配置、py脚本和cache缓存文件,可直接接入YOLO系列训练流程;压缩包约25.95MB,结构紧凑、下载使用方便。已有1592人学习/下载。标注文件采用YOLO格式,txt与图像一一对应,yaml定义了类别名称,py脚本保留了参数化编程方式,参数便于修改且注释详细,可帮助读者快速调整训练配置。借助这批数据,可完成从数据准备、模型训练到缺陷检测效果评估的完整实验,尤其适合以钢材表面检测为背景的课程设计或毕设项目。
1. 为什么说“已标注的钢材缺陷数据集”是 YOLO 项目最容易省心的起点
做工业质检的视觉工程师,最崩溃的时刻往往不是模型不收敛,而是数据集还停留在“几百张原始图片 + 一个实习生手动画框”的阶段。所以当看到“YOLO目标检测+钢材表面缺陷数据集已标注可以直接使用(1800张图像+对应已标注文件).zip”这个标题时,我的第一反应是:这省掉的是整个项目里最贵的一步。钢材表面缺陷检测本身就是目标检测里典型的“小目标 + 强纹理背景”场景,没有一套干净带标签的数据,后面的模型选型、调参全都没有意义。这个 zip 适合两类人:一类是刚入门目标检测、想用真实工业数据跑通全流程的学生;另一类是要在一周内做钢材缺陷检测可行性验证的工程师。但先说破一件事:标注文件并不等于标注正确,拿到 zip 的第一件事不是解压训练,而是验证里面的标签到底能不能用。
2. 解压前先搞懂:这个 zip 里的 YOLO 标注到底长什么样
2.1 钢材表面缺陷检测要解决的真实问题
钢材表面的缺陷种类很多,常见的有裂纹、夹杂、麻点、氧化铁皮压入、凹坑和划伤。这些缺陷的共同点,用一句话说是“长在纹理复杂的背景上,边界还模糊”。比如裂纹这种缺陷,在灰度图像上就是一条断断续续的暗色细线,跟钢材本身的轧制纹理混在一起,人工标注时都要放大到像素级才敢下笔。这就决定了目标检测模型要面对的不是那种“一个清晰的物体放在干净背景里”的任务,而是“目标小、对比度低、类别之间还可能互相混淆”的困难场景。
这类数据集的规模也很有意思。公开的钢材缺陷检测数据集,常见组织方式是每类缺陷约 300 张图像,六类加起来正好 1800 张,与这个标题的数字对得上。但标题里没有写具体类别清单,所以解压之后第一件事就是核对 labels 目录里的类别编号,不要默认它就一定包含六类。如果只有五类或者七类,data.yaml 的 names 要按实际内容改,编号错了训练也能跑,但检出来的类别名全是错位的。
2.2 YOLO 标注格式与 VOC、COCO 的差异
YOLO 系列的标注格式是一张图对应一个同名 txt 文件,文件里每一行代表一个目标框,格式是:类别ID、中心点 x、中心点 y、框宽度、框高度。关键是后面四个值全部做了归一化,也就是除以图像的宽和高之后的值,范围落在 0 到 1 之间。用目标检测常用标注工具如 LabelImg 时,默认导出的是 VOC 格式的 xml 文件,每个目标是一个<bndbox>节点,记录框左上角和右下角的像素坐标。如果用的是 LabelMe,导出的又是 JSON 格式。这些格式在送到 YOLO 训练之前,都得写脚本转成上面那种 txt。
为什么 YOLO 系列非要统一成 txt?一是读取效率高,一个文件逐行解析就能拿到所有框;二是归一化坐标与图像分辨率解耦,训练时不管把图缩放到 640 还是 1280,都不用重新算框的坐标。这也是这个数据集声称“已标注可以直接使用”的底气所在。但正因为格式简单,数据集的“水”也最容易藏在里面。比如一行只有四个数字、坐标值超出 0-1 范围,或者 txt 里混着中文类别名,这些在训练时不一定立刻报错,但结果就是某些类别的 AP 直接为 0。
下面是我拿到这类数据集之后第一段会跑的代码,把一条标注从归一化坐标换算回像素坐标,再在图上画出来,用眼睛确认框是不是真的套在缺陷上。
# 读取一行YOLO标注并换算成像素坐标,用于可视化抽查 def yolo_to_pixel(txt_line, img_w, img_h): cls, cx, cy, bw, bh = map(float, txt_line.split()) x1 = int((cx - bw / 2) * img_w) y1 = int((cy - bh / 2) * img_h) x2 = int((cx + bw / 2) * img_w) y2 = int((cy + bh / 2) * img_h) return int(cls), x1, y1, x2, y2 # 读一张图像和它的同名标注,打印前3个框的像素坐标 import cv2 img = cv2.imread("steel_defect/images/00001.jpg") h, w = img.shape[:2] with open("steel_defect/labels/00001.txt", encoding="utf-8") as f: lines = [ln.strip() for ln in f if ln.strip()] for ln in lines[:3]: cls, x1, y1, x2, y2 = yolo_to_pixel(ln, w, h) print(f"class={cls}, bbox=({x1}, {y1}, {x2}, {y2})") cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.imwrite("check_visual.jpg", img)这段代码有两个作用。第一是验证坐标换算逻辑本身没有写反,x/y 和宽高的对应关系是这套格式里最容易搞错的地方。第二是抽查标注质量,如果发现大量框只框住了缺陷的一半,或者框比目标大出好几倍,说明标注员的画框习惯有问题,这种数据集直接拿去训练,模型学到的框精度会很差。注意这里我用了 utf-8 读文件,如果 txt 是带 BOM 的编码,第一行的类别 ID 可能解析失败,这个问题到第 5 章避坑清单里再展开。
2.3 拿到 zip 后第一步:结构与完整性检查
我见过太多人拿到数据集压缩包直接解压就开训,结果训练跑到一半报错说图片数量对不上。所以不管是谁发的包,我第一步永远是检查 zip 的内部结构,看这个包是不是“空壳数据集”。先不要急着解压,用 zipinfo 看一眼里面有哪些目录、文件数量大概是多少。
# 先看压缩包内部结构,不解压就能列出所有文件 zipinfo -l "YOLO目标检测+钢材表面缺陷数据集已标注可以直接使用(1800张图像+对应已标注文件).zip" | head -30输出里能直接看到 images 目录和 labels 目录的文件排列方式。如果发现 images 下面有子目录而 labels 是统一平铺的,后面写训练脚本时就要注意路径拼接。确认结构没问题之后,正式解压。
# 解压到独立的目录,避免和别的项目文件混在一起 unzip "YOLO目标检测+钢材表面缺陷数据集已标注可以直接使用(1800张图像+对应已标注文件).zip" -d steel_defect # 统计图片和标注文件的数量,先做一个粗略体检 find steel_defect -name "*.jpg" | wc -l find steel_defect -name "*.txt" | wc -l如果两张图对应的数字不是 1800 和 1800,说明有缺失,不能直接开训。还有一种情况是图片是 jpg 而标注是 JPG 后缀,大小写不一致会导致按文件名匹配时对不上。这个阶段发现问题,改起来成本最低,等训练跑起来才发现就是几个小时白等。
提示:这个 zip 文件名带中文,在 Linux 服务器上解压一般没问题,但如果是从 Windows 网盘下载后再传到服务器,文件名可能会变成乱码。先改成一个纯英文名再解压,能省掉后面很多路径相关的麻烦。
3. 处理数据集用于 yolov8 训练:从 zip 到一次完整训练的最小闭环
3.1 校验图片和标注是否一一对应
现在目录里躺着 1800 张图和 1800 个 txt,但“数量对得上”只是最表层的要求。真正要确认的是:每张图都有一个内容不是空的同名 txt,txt 里每一行都是合法的五列数值,坐标没有超出归一化边界。这一步我会用一段独立的 Python 脚本做完,而不是直接开训练,因为训练器报出来的错误信息往往不够直观。
import os import glob img_dir = "steel_defect/images" label_dir = "steel_defect/labels" imgs = glob.glob(os.path.join(img_dir, "*.jpg")) label_stems = { os.path.splitext(os.path.basename(p))[0] for p in glob.glob(os.path.join(label_dir, "*.txt")) } missing_label = [] # 有图像但没有对应标注文件 empty_label = [] # 标注文件存在但是空文件 bad_line = [] # 行内字段数不等于5 out_of_range = [] # 坐标不在 [0,1] 区间 for p in sorted(imgs): stem = os.path.splitext(os.path.basename(p))[0] if stem not in label_stems: missing_label.append(p) continue lp = os.path.join(label_dir, stem + ".txt") with open(lp, encoding="utf-8") as f: lines = [ln.strip() for ln in f if ln.strip()] if not lines: empty_label.append(lp) continue for ln in lines: parts = ln.split() if len(parts) != 5: bad_line.append((lp, ln)) continue try: cls, cx, cy, bw, bh = map(float, parts) except ValueError: bad_line.append((lp, ln)) continue if not (0 <= cx <= 1 and 0 <= cy <= 1 and 0 < bw <= 1 and 0 < bh <= 1): out_of_range.append((lp, ln)) print(f"total images: {len(imgs)}") print(f"missing label: {len(missing_label)}") print(f"empty label: {len(empty_label)}") print(f"bad line: {len(bad_line)}") print(f"out of range: {len(out_of_range)}")这段脚本的输出就是一张“体检报告”。missing_label 的处理方式是检查是不是有同名文件但扩展名不同,比如图片叫 00001.jpg,标注叫 00001.jpg.txt,那就是上一环节的命名规范问题。empty_label 直接看文件为什么为空,有些标注工具会在导出时给没有目标的图片生成空文件。bad_line 是五列的格式要求,多一列少一列都说明不是标准 YOLO 格式。out_of_range 是数值问题,归一化坐标大于 1 基本可以断定是从 VOC 格式转换时没有除以图像宽高。这四个问题全为 0,数据集才配得上“已标注可以直接使用”这句话。
3.2 按 8:1:1 拆分训练集、验证集和测试集,并保持类别比例
很多人做目标检测数据集拆分是直接把所有文件 shuffle 一下按比例切分。这在类别分布均匀的分类任务里问题不大,但在缺陷检测这种类别不平衡的任务里会埋雷。比如某种缺陷总共只有几十个实例,随机切分后验证集里可能一个都没有,那 val 上的 mAP 就失去了参考意义。我一般会换一种做法:先按每张图片包含的类别组合来分桶,同一个组合内部的图片再按比例随机分到三个集合,保证每一类缺陷在三个集合里都存在。
import os import random import glob import shutil from collections import defaultdict random.seed(42) img_dir = "steel_defect/images" lbl_dir = "steel_defect/labels" split_root = "steel_defect/split" # 先创建三个集合的目录结构 for split in ("train", "val", "test"): os.makedirs(os.path.join(split_root, split, "images"), exist_ok=True) os.makedirs(os.path.join(split_root, split, "labels"), exist_ok=True) # 按“图片包含的类别组合”分桶 buckets = defaultdict(list) for img in glob.glob(os.path.join(img_dir, "*.jpg")): stem = os.path.splitext(os.path.basename(img))[0] with open(os.path.join(lbl_dir, stem + ".txt"), encoding="utf-8") as f: classes = sorted({ln.split()[0] for ln in f if ln.strip()}) buckets[tuple(classes)].append(img) # 每个桶内独立做 8:1:1 随机切分 for key, imgs in buckets.items(): random.shuffle(imgs) n = len(imgs) n_val = max(1, round(n * 0.1)) n_test = max(1, round(n * 0.1)) n_train = n - n_val - n_test assignments = [ ("train", imgs[:n_train]), ("val", imgs[n_train:n_train + n_val]), ("test", imgs[n_train + n_val:]), ] for split, part in assignments: for img in part: stem = os.path.splitext(os.path.basename(img))[0] shutil.copy(img, os.path.join(split_root, split, "images", os.path.basename(img))) shutil.copy( os.path.join(lbl_dir, stem + ".txt"), os.path.join(split_root, split, "labels", stem + ".txt"), )这段脚本是纯文件复制操作,不修改标注内容,所以即使跑错了也不会损坏原始数据,这点很重要。我习惯在拆分之后再看一眼每个集合里的类别分布,确认一下有没有哪一类在某个集合里数量是 0。另外注意随机种子,我把 random.seed(42) 写死是为了让拆分结果可复现,方便后面几位同事在同一个数据划分下对比实验。如果你是在产线场景拿到的连续帧图像,还要注意同一块钢的相邻帧可能几乎一样,单纯随机切分会造成信息泄露,这个第 5 章展开说。
3.3 生成 data.yaml 并用一条命令跑通 yolov8 训练
数据目录结构定下来之后,需要写一个 data.yaml 告诉 YOLO 训练器三件事:数据集根路径在哪里、训练和验证图片在哪里、类别编号对应什么名字。这个文件是整个配置环节最容易手滑的地方,尤其类别 names 的排序必须和标注文件里的数字 ID 完全一致。
# 训练配置文件 data.yaml path: steel_defect/split train: train/images val: val/images test: test/images names: 0: crazing 1: inclusion 2: patches 3: pitted_surface 4: rolled-in_scale 5: scratchespath 字段写的是相对当前工作目录的路径,train 和 val 写的是相对 path 的子目录。这里不要用绝对路径,因为换了机器或者 docker 容器路径一变就要改。names 是类别名和 ID 的映射表,如果你解压出来的数据集类别清单和上面六类不同,直接按实际情况修改顺序即可。注意不要改动 txt 里的数字 ID,只改 names 列表的顺序,让索引保持一致。
配好 yaml 之后,训练就变成一条命令了。我一般会先拿 nano 模型跑 100 个 epoch,用最便宜的配置验证整个数据链路通不通通,再决定要不要换大模型。
# 训练最小可用基线:nano模型 + COCO预训练权重 + 640分辨率 yolo train \ data=steel_defect/split/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0model=yolov8n.pt 表示用 nano 级别的预训练权重做初始化,文件如果在本地不存在,ultralytics 会自动从官方仓库拉取。这就是热词里常说的 yolo 预训练模型下载,但注意如果没有外网环境,需要提前把权重文件拷到项目根目录。device=0 是使用第一块 GPU,没有 GPU 环境就改成 device=cpu,但 1800 张图的钢材缺陷训练在 CPU 上会很煎熬,建议至少准备一块哪怕是消费级的 GPU。跑起来之后如果前几个 epoch 没有报错,说明数据链路已经通了。
4. 训练阶段最值得盯的三个参数与结果验证
4.1 YOLO 版本怎么选,预训练权重和从头训练差多少
钢材缺陷检测这个任务,YOLO 家族里哪个版本更合适?YOLOv5 的生态最成熟,网上能找到大量部署和量化资料,但官方新特性迭代基本停了。YOLOv8 是目前多数项目的默认起点,训练接口统一,数据增强和超参配置都写在配置系统里,改起来方便。YOLOv10、v11 这些新版本在推理速度和精度上确实有提升,但对 1800 张图这个规模的数据集,带来的提升远远不如把数据质量搞干净来得大。所以我的建议是:主力用 YOLOv8,把它跑熟,再去看新版本。
预训练权重是另一个不能省的环节。用 COCO 上训出来的权重做初始化,跟从随机权重开始训的差距,在小数据集上会非常明显。COCO 模型虽然没见过钢材缺陷,但它已经学会了边缘、纹理、局部对比度这些通用的视觉底层特征。钢材缺陷检测正好吃这些特征,比如裂纹在局部就是一条低对比度的边缘,划伤就是一组有方向的纹理。用 COCO 预训练权重相当于带着已经练好的“眼睛”进新领域,随机初始化则是从零开始,在小数据量下很难收敛到同样的水平。
yolo 系列对比里还有一个容易被忽略的点:模型 size 的选择。钢材缺陷是小目标,理论上模型越大特征提取能力越强,但 1800 张图的规模限制了模型容量,yolov8n 或者 yolov8s 通常是性价比最高的区间。直接上 yolov8x,很容易在训练集上表现得非常好,但验证集的损失从某个 epoch 开始回头向上,这是典型的过拟合。
4.2 imgsz、batch、epoch 三个参数怎么设才有意义
这三个参数是训练阶段最容易引发玄学讨论的,我直接给出实际经验值和背后的判断思路。
| 参数 | 推荐值 | 判断依据 |
|---|---|---|
| imgsz | 640 或 1280 | 缺陷目标的小尺寸决定分辨率,先 640 跑基线,再看小目标漏检情况 |
| batch | 16 或 32 | 显存允许的前提下尽量大,V100 32G 可以开 64,但 16 更稳 |
| epoch | 100 到 300 | 看验证集 loss 是否还在下降,下降就继续,平台期就停 |
imgsz 是影响钢材缺陷检测最直接的一个参数。640 是 YOLO 系列的默认值,推理速度快,但如果缺陷在原始图上只有十几个像素,缩放到 640 之后就只剩几个像素,基本等于消失。我在 V100 上做过对比,同样一个数据集,imgsz 从 640 调到 1280,小缺陷类别的 AP 能涨 5 到 10 个点,代价是训练时间几乎翻倍、显存占用翻两番。如果你的 GPU 显存只有 8G,建议先 640 跑通,再用切图方案弥补小目标问题,这个第 5 章讲。
batch 在 16 到 32 之间对最终精度的影响没有想象中大,但它决定训练的稳定性。batch 太小,loss 曲线震荡剧烈,BN 层的统计量也不准。batch 太大,显存不够就只能调小 imgsz,得不偿失。epoch 不要拍脑袋定死 300,我一般的习惯是训练时开着验证集评估,如果 val/box_loss 连续 30 个 epoch 没有下降,就提前停,省下来的时间去跑下一组实验。
训练日志里还有一条曲线值得盯:cls_loss。YOLOv8 的损失函数由三部分组成,边框损失用 CIoU 衡量预测框和真实框的重合程度,分类损失用 BCE 判断类别是否正确,DFL 损失负责分布聚焦。如果 epoch 过了 50,cls_loss 还在高位震荡不下降,大概率不是参数问题,而是数据的问题,比如某类缺陷的标注本身就不一致,或者类别 ID 有错位。yolo 损失函数三个分量的下降趋势,比总 loss 更能说明问题,我每次训练都会单独看这三条曲线。
4.3 用混淆矩阵和 mAP 看缺陷检测的真实水平
训练结束之后,yolo val命令会在输出目录生成一堆评估文件,包括 confusion_matrix.png、PR_curve.png、results.csv 等。先看 mAP50 和 mAP50-95 两个指标。mAP50 是 IOU 阈值取 0.5 时的平均精度,工业场景里更关注能不能检出,所以这个指标可读性最强。mAP50-95 是把 0.5 到 0.95 的阈值平均,它更苛刻,一个框只要标得稍微偏一点,分数就会掉。钢材缺陷的标注框有很多是套在缺陷团块上的不规则矩形,本身就和真实边缘不完全贴合,所以 mAP50-95 偏低不一定是模型的问题,也可能是标注风格的问题。
验证命令本身很简单:
# 用 best.pt 在 test 集合上做最终评估 yolo val \ model=steel_defect/split/runs/detect/train/weights/best.pt \ data=steel_defect/split/data.yaml \ imgsz=640 \ batch=16打开 confusion_matrix.png 之后,很多人会困惑为什么行和列加起来对不上。YOLO 的混淆矩阵默认按行归一化,每一行代表该类别真实样本被分到各个预测类别的比例,所以每行之和是 100%,但列方向上没有归一化,各列加起来当然不等于 100%。看漏检要读对角线之外的行元素,尤其看最右边“background”列,如果某一类有 20% 的真实样本被分到了背景,说明这个类漏检很严重。看误检则要关注背景那一行,大量背景被误判为某一类缺陷,说明模型学到的特征里混入了环境纹理。
这里顺带说一个判断数据质量的硬指标:如果混淆矩阵里多个类别互相混叠严重,比如 inclusion 和 patches 之间经常搞混,先不要急著换模型,回去抽查这两个类的标注图片,很多时候是标注本身就不一致,同一个缺陷在两张图里一个标成 inclusion 一个标成 patches。模型学到的是标注里的不一致,而不是缺陷本身的差异。
5. 钢材缺陷检测的避坑清单:五个值得写进备忘的翻车记录
5.1 坑一:图片和标注对不上,训练启动就报 assert 错误
现象:yolo train命令刚跑起来,日志抛出类似AssertionError: train: labels not found或者Dataset is empty的提示,有时候是训练中途突然报图片索引越界。
原因:zip 包解压后图片数量和标注文件数量对得上,但里面有几张图的标注文件名字拼写不一致,比如图片叫00023.jpg,标注却叫00023 .txt多了一个空格,或者图片是 .jpg 而标注里混入了 .JPG 的历史遗留文件。YOLO 是按文件名匹配图片和标注的,差一个字符就等于没有标注。
解决:这个坑最好在训练前就用第 3 章的校验脚本挡掉。已经报错了也别慌,脚本跑一遍,把 missing_label 列表里的图片直接移到 backups 目录,不要让训练集里存在没有标注的图片。如果缺失的标注很少,手动补一下也可以,但注意补的框要和同类缺陷的标注风格一致。
5.2 坑二:类别不平衡让少数类永远不收敛
现象:训练结束后打开混淆矩阵,其他类别 AP 都在 0.8 左右,唯独 rolled-in_scale 和 crazing 这两类的 AP 只有 0.2 甚至 0。再看每个 epoch 的验证日志,这两个类的 loss 曲线从头到尾几乎是一条水平线。
原因:标题说“1800 张图像 + 已标注文件”,但 1800 张图不等于每个类别均衡分配。如果数据集里某一类缺陷只有几十个标注实例,而其他类有上千个实例,模型在训练时绝大多数更新都被多数类主导,少数类几乎学不到东西。比这更隐蔽的情况是类别数量均衡,但某一类的实例面积特别小,模型在特征提取阶段就把这些目标过滤掉了。
解决:先统计每个类别的实例总数,用 txt 文件的行数就能算。如果确实出现极端不平衡,不要直接增加图片数量,而是先试数据增强里对少数类做过采样。具体做法是训练时把包含少数类缺陷的图片重复多读几遍,相当于变相提高它的采样权重。在 ultralytics 里没有直接的按类采样参数,我一般是在预处理阶段把少数类图片在目录里多复制几份,注意文件名不能重,或者写一个自定义 Dataset 做重采样。另一个有效手段是 copy-paste 增强,把少数类的缺陷小块贴到正常钢材区域,这个第 6 章讲。
5.3 坑三:小缺陷目标在 640 分辨率下被直接吞掉
现象:验证集指标看起来还行,mAP50 有 0.7 以上,但把模型部署到实际产线图像上,那些只有十几个像素宽的细小裂纹全部漏检,而训练集里这类缺陷可能占了接近一半。
原因:这是钢材缺陷检测最核心的矛盾。输入图像缩放到 640 之后,一个原本只有 20 像素宽的划伤在特征图里可能只剩下 2 个像素点,经过几次下采样后特征直接消失。模型不是没学会,是根本看不到。钢材表面的裂纹、划伤这类缺陷天然就是细长条结构,比普通目标检测里的“小物体”还要极端。
解决:有两个可行的方案。方案一,把原图切成 512x512 的 patch 再训练,缺陷在 patch 里相对尺寸变大,模型能学到有效特征,代价是训练样本数量膨胀、标注需要跟着切。方案二,直接用 imgsz=1280 训练,YOLOv8 支持这个大分辨率输入,在 V100 这类显存充足的卡上跑得动,推理时也用同样的分辨率,速度会慢一些,但对小目标的效果提升立竿见影。我现在的习惯是先用 640 跑基线确认数据没问题,再切到 1280 做正式模型,两条链路都保留。
5.4 坑四:验证集被污染,指标虚高得不敢信
现象:val 上面的 mAP 到了 0.92,模型表现得近乎完美,但拿一段完全没参与训练的产线视频去测,mAP 直接掉到 0.55,漏检一堆。
原因:最常见的原因是数据集划分时没有考虑图像之间的相关性。钢材缺陷数据集如果是产线采集的,很可能同一块钢的连续几张图只是相机位置稍有移动,背景和缺陷几乎一样。随机划分时,这些“长得很像”的图被同时分进训练集和验证集,模型等于提前看到了验证集的答案。另一个污染源是划分脚本里忘记设置随机种子,不同机器上拆出来的集合不一致,实验之间没法对比。
解决:按“组”而不是按“张”划分。如果图像文件名里有批次编号或者序列号,先把同一批次的图归为一组,整个组要么全部进训练集,要么全部进验证集,不要拆散。没有编号也没关系,可以按文件名前缀聚类,或者用感知哈希计算图像的相似度,把相似度高的图强制分到同一集合。每次划分时固定 random.seed,让所有实验在同一数据划分下公平对比。这个坑最难排查,因为指标本身不会报错,完全靠对数据的理解。
5.5 坑五:中文路径和标注文件编码问题
现象:在 Windows 上解压 zip,数据目录名带中文,或者图片放在了带中文的父路径下,训练时图片读取失败,报错信息是cv2.error: OpenCV(4.x) ... is not a valid file,但同样的代码在 Linux 上又能跑。
原因:ultralytics 底层依赖 OpenCV 和 matplotlib,而 OpenCV 的 imread 函数对中文路径支持很差,这是从 OpenCV 2.x 时代就有的老毛病,到现在也没完全解决。另一个编码问题是标注文件如果是用 Windows 记事本另存的,可能带 UTF-8 BOM 头,Python 用 utf-8 读的时候第一个字符会变成\ufeff,导致第一行的类别 ID 解析失败。
解决:第一道防线是把整个项目路径改成纯英文,目录名用 steel_defect 而不是“钢材缺陷数据集”,zip 文件名也提前改掉。这道防线能解决 90% 的问题。BOM 的问题在读取标注时用encoding="utf-8-sig",它可以自动吞掉开头的 BOM 字符。如果你已经写了很多脚本用的是utf-8,统一全局替换一下,改动量不大。这个坑属于典型的“环境坑”,代码没错,错在数据文件的出身。
6. 再抠几个百分点的召回率:钢材小缺陷的增强与推理 trick
数据链路通了,基线也跑了,这时候再往上提指标,靠的不是换更大的模型,而是在增强和推理环节做针对性调整。先说数据增强。Ultralytics 默认开启 mosaic 增强,把四张图拼在一起训练。这个增强在通用目标检测里非常有效,但在钢材表面缺陷这个小目标场景里反而有害:四张图拼完,每个缺陷的相对尺寸又缩小了四倍,模型更难学到特征。我现在的做法是关闭或大幅调低 mosaic 的概率,让模型看到完整尺寸的缺陷。如果训练时发现小目标召回率上不去,这个操作经常立竿见影地往回拉。
另一个有效的增强是 copy-paste。从标注框里把缺陷区域抠出来,贴到没有缺陷的钢材背景图上,同时给它生成对应的标注框。这样做的好处是既增加了少数类的实例数,又让模型学会在“干净的背景”上识别缺陷,而不是依赖整张图的纹理上下文。具体实现可以直接操作数据集,批量生成增强图片和标注,不用改训练器内部逻辑。
推理阶段同样有可调的余地。默认置信度阈值是 0.25,在工业场景下漏检成本远高于误检成本,我会把阈值降到 0.05,让模型把更多“模棱两可”的区域也预测出来,再用 NMS 去重,最后按实际业务需求决定保留多少。必要的时候打开测试时增强,让模型在多个尺度上预测再综合结果,召回率还能再涨一点,但推理耗时增加明显,是否启用取决于产线的节拍要求。
收个尾,分享一个我现在固定的习惯:每次训练完,第一眼不看 mAP,而是打开混淆矩阵,看每一行真实样本有多少比例落在了 background 列。这个数字比任何指标都能说明标注质量和模型能力的边界。如果超过 15%,我不会急着换架构,而是回头重新检查这批标注里到底有多少框是错的,或者有多少缺陷本身就细微到人眼都难确认,这是做质检项目最值钱的经验。希望帮到你,踩过这些坑之后,你会感谢这个 1800 张的 zip 让你把精力花在了真正该花的地方。
本文还有配套的精品资源,点击获取