ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

YOLOv5香烟破损检测数据集验证与训练实践

YOLOv5香烟破损检测数据集验证与训练实践 简介一套可直接用于YOLOv5训练流程的香烟破损检测数据集面向工业质检、目标检测学习及算法验证场景覆盖头部破损、滤嘴破损等6类缺陷。压缩包为zip格式共803个文件内含401张JPEG原图、401个TXT标签文件和1个Python可视化脚本整体大小388.19MB数据量适中便于快速跑通训练与评估流程。目录结构严格遵循YOLOv5的images与labels约定训练集位于datasets-images-train含320张图片与320个标签文件验证集位于datasets-images-val含80张图片与80个标签文件图片与标签一一对应且划分互不交叉。图像为3024×4032高分辨率RGB大图可清晰保留烟支破损边缘、纹理等细节每个txt标签记录目标类别与归一化坐标另附6类类别文本文件可直接修改模型配置后开始训练。已有140人学习附带的Python脚本无需修改即可运行随机传入一张图片即可自动绘制边界框并保存至当前目录方便快速核查标注质量与数据划分合理性。1. 拿到一份 YOLOV5 目录格式的香烟破损检测数据集先别急着训练收到“目标检测数据集YOLOV5目录格式香烟破损检测6类别包含训练集、验证集”这类标题时我的第一反应不是打开训练脚本而是先把它当成一批待验收的文件来查。真正让项目卡住的很少是网络结构或损失函数常常是目录层级对不上、标注类别 id 越界、验证集里混进了训练集图片这类看不见的问题。YOLOv5 目录格式之所以能成为事实标准是因为它把图片、标签、类别定义和数据划分用文件夹加一个 yaml 文件固定下来train.py 拿到就能直接跑。下面这篇文章就按这套格式从目录结构、标注字段、训练命令讲到最常见的验证翻车点最后给出我自己的验证和迭代习惯。2. 拆解 YOLOv5 目录格式数据集images、labels 与 6 类破损标注2.1 目录结构为什么一定是 images/labels 与 train/val 双层划分一份规范的 YOLOv5 数据集目录结构长这样cigarette_damage/ ├── images/ │ ├── train/ │ │ ├── 00001.jpg │ │ ├── 00002.jpg │ │ └── ... │ └── val/ │ ├── 01001.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── 00001.txt │ │ ├── 00002.txt │ │ └── ... │ └── val/ │ ├── 01001.txt │ └── ... └── data.yaml注意一个容易误解的点images 和 labels 是平级目录而不是在 train 下面再分 images 和 labels。这套约定的由来是 YOLOv5 的训练代码在读取数据时会根据图片路径自动把/images/替换成/labels/再把后缀从.jpg换成.txt。所以只要图片和标签文件名一致两边目录保持平行就能彼此找到。这个约定从 YOLOv5 一直延续到 Ultralytics YOLOv8 和 YOLOv11只是读取代码的 API 变了目录规则没变。很多人拿到数据集后第一件事就是改这个结构比如把标签按类别建子目录或者把所有图片混到一个目录里、标签单独放——这样 YOLOv5 反而找不到标签。我一般会先保留原始结构只在数据根目录外面做软链或者复制一份到我项目里的 datasets 目录尽量不动内部层级。还有一个很常见的坑train 和 val 的划分方式。如果数据集的制作方是按文件名随机切分的而原始采集又是同一包烟多个角度的连续拍摄那很可能同一根烟的不同视角分别出现在 train 和 val 里。这种划分会让验证结果虚高后面在 4.2 节我会专门展开。拿到数据集后的第一件落地动作我建议先核对文件一一对应。最简单的做法是在数据集根目录执行for split in train val; do echo $split diff (ls images/$split | sed s/\.[^.]*$// | sort) \ (ls labels/$split | sed s/\.[^.]*$// | sort) | head -20 done这个命令的作用是把每个 split 下的图片文件名和标签文件名去掉后缀后做差集。如果 diff 输出为空说明两边一致如果有输出说明存在“有图没标签”或者“有标签没图”的情况。前者会让那张图在训练时被跳过后者会让标签被浪费两种情况都会影响最终 mAP 的统计口径。2.2 标注文件内容类别 id、中心坐标与宽高YOLO 格式的每个标注文件里每一行代表一个目标框内容固定为 5 个数类别 id、框中心点 x、中心点 y、框宽度 w、框高度 h。其中 x、y、w、h 全部是相对于图片宽高的归一化值取值范围 0 到 1。举例某张图的前三行标注可能是head -3 labels/train/00001.txt1 0.5237 0.3821 0.1542 0.0883 2 0.3844 0.6250 0.2011 0.0647 0 0.7120 0.2145 0.1736 0.0712第一行的类别 id 是 1表示这是一个“滤嘴破损”目标中心点落在图片横向 52.37%、纵向 38.21% 的位置框宽是图宽的 15.42%框高是图高的 8.83%。这里最容易翻车的地方是把它和 VOC 格式搞混。VOC 的标注是左上角和右下角两个点YOLO 则是中心点加宽高。不少转换脚本在转格式时忘了做归一化或者归一化时除错了方向导致训练时 loss 一开始就不正常下降。我拿到带标注的数据集会先抽一张图把标注画回去看看框的位置对不对得起烟支形状。下面这段脚本可以直接用# verify_boxes.py import cv2 from pathlib import Path img_path Path(cigarette_damage/images/train/00001.jpg) img cv2.imread(str(img_path)) h, w img.shape[:2] # 根据图片路径推导对应标签路径把 /images/ 换成 /labels/后缀换 .txt label_path Path(str(img_path).replace(/images/, /labels/).rsplit(., 1)[0] .txt) with open(label_path) as f: for line in f: cls, cx, cy, bw, bh map(float, line.split()) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, str(int(cls)), (x1, max(0, y1 - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imwrite(check_00001.jpg, img)脚本逻辑不复杂读取标注把归一化坐标乘回图片宽高再用 OpenCV 画框和类别 id。如果你看到框明显偏离烟支或者框的大小完全不合理那就说明标注数据的坐标基准有问题趁早返工别等到训练损失异常才回头查。至于 6 个类别具体指什么数据集的 data.yaml 里通常会有明确 names。一套在香烟破损检测里比较常见的定义是0 烟支断折、1 滤嘴破损、2 烟纸开裂、3 烟盒压扁、4 封口破损、5 霉斑污渍。如果你手里的 yaml 写的不是这套以 yaml 为准这里的关键是理解 class id 只是索引真正的语义在 names 列表里。class_id常见类别名破损形态0cigarette_broken烟体有明显折痕或断裂1filter_damaged滤嘴端部缺损、撕裂2paper_crack卷烟纸纵向裂开3pack_dented烟盒外观变形、压扁4seal_torn透明膜、封口贴撕裂5mold_spotted表面霉斑或污渍2.3 data.yaml 配置路径、类别数与类别名YOLOv5 训练时真正读的不是图片目录本身而是一个 data.yaml。这个文件是训练和验证的入口内容通常长这样# data.yaml train: /home/user/cigarette_damage/images/train val: /home/user/cigarette_damage/images/val nc: 6 names: 0: cigarette_broken 1: filter_damaged 2: paper_crack 3: pack_dented 4: seal_torn 5: mold_spottedtrain 和 val 指向的是图片目录标签目录不需要写YOLOv5 会自动按/images/到/labels/的规则去替换。nc 必须和 names 列表长度一致且 names 的索引顺序必须和标注文件里的 class id 一一对应。这个顺序一旦错了模型照样能训练、能收敛验证时 mAP 也未必难看但部署阶段你拿到的“类别”语义全是错的输出 id 和实际破损类型对不上。路径写法也要注意。YOLOv5 接受相对路径但相对路径是相对于你执行 python train.py 时的工作目录而不是 yaml 文件所在的目录。这个差异很容易让人困惑尤其是从项目子目录启动训练命令的时候。我的做法是直接写绝对路径或者用环境变量在前面拼一层根目录。Windows 下如果路径里有反斜杠YAML 会把\t、\n之类当成转义字符处理所以统一用正斜杠/最省事。这一步让我讲个题外话很多标注工具导出数据集时会在 yaml 里带上绝对路径换机器后路径失效训练报 “No images found”。拿到数据后第一件事就是把路径改掉再跑 3.1 的检查。3. 把这份数据集跑通YOLOv5 训练前的检查与训练命令3.1 数据完整性检查图片与标注一一对应在真正执行 yolov5 训练自己的数据集之前我习惯先跑一个完整性检查脚本而不是上来就训练。原因很简单训练报错的成本高而统计检查的成本低。把检查放在第一步能提前暴露大部分低级错误。# check_data.py from pathlib import Path base Path(cigarette_damage) for split in [train, val]: img_dir base / images / split lbl_dir base / labels / split img_names {p.stem for p in img_dir.glob(*.*) if p.suffix.lower() in {.jpg, .jpeg, .png}} lbl_names {p.stem for p in lbl_dir.glob(*.txt)} only_img img_names - lbl_names only_lbl lbl_names - img_names # 检查标签文件里是否存在空文件或越界类别 id max_id -1 empty_cnt 0 for p in lbl_dir.glob(*.txt): lines [ln.strip() for ln in p.read_text().splitlines() if ln.strip()] if not lines: empty_cnt 1 for ln in lines: cls int(ln.split()[0]) max_id max(max_id, cls) print(f[{split}] images{len(img_names)}, labels{len(lbl_names)}) print(f 有图但无标签: {len(only_img)}, 有标签但无图: {len(only_lbl)}) print(f 空标签文件: {empty_cnt}, 最大类别 id: {max_id}) if only_img: print( 示例:, list(only_img)[:3]) if only_lbl: print( 示例:, list(only_lbl)[:3])这个脚本做三件事统计各 split 的图片标签数量找出只有图或只有标签的文件检查空标签和类别 id 最大值。空标签文件在 YOLOv5 里会导致 dataloader 对该图片的处理变慢甚至让某些增强操作报错。最大类别 id 是个硬指标——如果你的 data.yaml 写着 nc6那类别 id 合法范围是 0 到 5任何大于 5 的标注都会直接让训练中断。跑完检查之后再配合前面 2.1 里的 diff 命令基本就能判定这份数据集能不能进训练流程。3.2 训练命令与超参数从 COCO 预训练权重出发数据检查通过后就到了训练这一步。训练命令本身不复杂复杂的是参数怎么设。一个在香烟破损检测上足够好用的起点命令是python train.py \ --data data/cigarette_damage.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --project runs/cigarette_damage \ --name v5s_640解释一下参数--data指向 2.3 节写好的 yaml 文件--weights yolov5s.pt使用 COCO 预训练权重做迁移学习。不要用随机初始化权重去训烟支破损迁移学习带来的收敛速度差距非常大。--img 640是输入图片尺寸的短边YOLOv5 会等比缩放后填充到 640。--batch 16在 8G 显存以下的卡上比较稳如果显存只有 6G 可以降到 8。--project和--name用来组织输出目录训练结果会写在runs/cigarette_damage/v5s_640下面。关于超参数YOLOv5 的默认配置文件是数据目录下data/hyps/hyp.scratch-low.yaml对大多数工业缺陷检测场景已经够用。需要手动干预的通常只有这几个超参数默认值什么情况下调lr00.01数据集偏小或迁移效果差时降到 0.005fl_gamma0.06 个类别数量明显不平衡时设为 1.5 左右mosaic1.0训练后半程把 mosaic 降到 0.5 或关闭防止过拟合scale0.5小目标漏检严重时降到 0.3 左右避免过度缩放让破损区域消失这里要特别说的是 fl_gamma。破损检测普遍存在类别不平衡断折样本可能上千张霉斑样本可能只有几十张。开启 focal loss 可以让模型把注意力往少数类上倾斜。做法是训练时附加参数或者直接改 hyp 文件python train.py \ --data data/cigarette_damage.yaml \ --weights yolov5s.pt \ --hyp data/hyps/hyp.scratch-low.yaml \ --img 640 --batch 16 --epochs 100如果你改动了 hyp 文件里的 fl_gamma记得先看一眼 train.py 启动日志确认它加载的是你改过的那个 yaml而不是默认配置。YOLOv5 的权重名称、模型大小选择也有讲究yolov5s适合快速验证yolov5m和yolov5l适合最终把精度往上推。我一般先用 s 跑通全流程确认数据和代码没有低级问题再换 m 或 l 做正式训练。这个顺序能省下不少反复试错的时间。3.3 训练日志指标解读box_loss、obj_loss、mAP 的变化曲线训练开始后控制台会滚动输出一列指标类似这样Epoch GPU_mem box_loss obj_loss cls_loss Instances Size mAP0.5 mAP0.5:0.95 49/100 4.21G 0.05218 0.01837 0.01572 84 640 0.9621 0.7410 50/100 4.21G 0.05134 0.01755 0.01498 61 640 0.9715 0.7552box_loss 是预测框与标注框的回归误差。它下降说明框的位置越来越准。obj_loss 是目标性损失衡量模型对“这个位置有没有目标”的判断破损检测里最常见的问题就是 obj_loss 降不下去模型总觉得背景里没有东西表现出来就是漏检。cls_loss 是 6 个类别的分类损失一般会在训练前 20 轮快速下降后续趋于平缓。mAP 有两档mAP0.5 表示 IoU 阈值 0.5 时的平均精度mAP0.5:0.95 表示从 0.5 到 0.95 每隔 0.05 取一次阈值的平均。在香烟破损检测这种场景我更信任 mAP0.5。原因是破损区域的边界本来就模糊人工标注的框很难做到高度一致的 IoUmAP0.5:0.95 会过分惩罚那些边界标注存在微小偏差的框。你只要把训练日志里的两个 mAP 曲线同时打开对比就很容易看到这一点。还有一类情况值得警惕loss 一直在降但 mAP 从某一轮开始不再上升。这时候别盲目加轮次多半是数据本身的问题比如某些类别样本太少、或验证集里存在和训练集过于相似的图片。这类问题的排查方法放在下一章。4. 训练香烟破损模型常见问题与排查4 条真实踩坑记录4.1 报错 class out of range标注类别 id 越界现象训练刚开始dataloader 抛异常错误信息类似AssertionError: class 9 specified for label ... but class number is only 6或者直接崩在读取某个 txt 的时候。原因标注文件里出现了大于等于 6 的类别 id而 data.yaml 的 nc 写的是 6。这类情况常见于标注工具导出的类别列表和 yaml 不一致或者转换脚本对类别做了重新映射但漏改了一部分文件。解决先用 3.1 节检查脚本扫一遍或者直接用 find 命令把可疑文件揪出来grep -RlE ^[6-9] cigarette_damage/labels --include*.txt这个 grep 匹配所有以 6 到 9 开头、后跟空格的行把命中的文件列出来。找到后回到标注工具里修正类别 id再把 yaml 里的 names 顺序核对一遍。这里有一个细节grep 只覆盖 6 到 9万一标注里出现10这样的两位数需要用^([6-9]|[1-9][0-9])这样的扩展表达式或者干脆用 Python 检查脚本一次性处理。4.2 验证集 mAP 虚高训练和验证数据划分不当现象训练结束后跑验证mAP0.5 高达 0.98看起来模型已经接近完美。但把训练好的权重接进摄像头或者视频流里实测漏检一塌糊涂跟验证结果完全不成比例。原因数据集划分时按文件名随机分而制作方采集的是同一包烟的多视角连续帧。这些帧在光照、角度、背景上高度相似随机划分后验证集里很可能混入了跟训练集几乎相同的画面。模型在验证的时候就等于见过答案mAP 当然虚高。解决拿到数据集后先看图片命名和采集规律。常见做法是按拍摄批次或场景维度划分而不是按文件名随机打散。如果数据集自带的 val 目录已经确定且划分不合理可以把 val 里的图片重新抽样确保每个拍摄批次只出现在一个集合里。另外你可以在训练结束后从验证集里按置信度从低到高排序挑出预测最差的几十张图人工确认它们是否与训练集内容高度相似。这一步做不了假也是最直接的验证集质量检验方法。4.3 小目标漏检严重破损部区域太小标注框过小现象模型对正常大小的烟盒变形检测得不错但裂口、霉斑这类小区域几乎全漏。不是分错类而是预测框完全没有出现。原因破损区域在原始图片里可能只有几十个像素经过--img 640缩放到 640 后目标区域在特征图上可能只剩一个点。加上默认超参数里 scale0.5 会给训练图片做随机缩放小目标进一步被缩小最终变成无法学习的噪声。解决第一步把--img提到 1280每张图保留更多的原始细节。显存不够时可以改用 SAHI 这类切图推理方案把大图切成小块分别检测。第二步把 hyp 里的 scale 从 0.5 降到 0.3减少训练时对小目标的“二次伤害”。第三步也是很容易被忽略的一点标注破损区域时不要把框紧贴着裂口像素。我一般会把框放大到“能看清破损周边烟体纹理”的范围让模型有更多上下文可学。标注框过紧在小目标场景下反而是负面因素。4.4 某个类别 mAP 明显偏低类别数量不平衡现象训练日志里的整体 mAP 不错但单独看某个类别的 mAP 很低比如霉斑这一类只有 0.3而其他几类都在 0.9 以上。跑到最后几轮loss 也没有明显改善。原因6 个类别里霉斑样本数量太少正负样本比例严重失衡。模型在训练时大部分“注意力”都被样本量大的类别占据了少数类学不出稳定的特征。解决两个方向。第一个是数据层面对少数类做复制粘贴增强把霉斑区域贴到无破损的烟支图片上。注意只能在训练集内部做绝对不要碰验证集否则又会产生 4.2 节那种虚高问题。第二个是损失函数层面在 hyp 文件里把 fl_gamma 设为 1.5 或 2.0让模型更关注少数类的难例。如果这两招都用了还是提不上去那就要回到采集环节补拍几十张真实霉斑样本。数据增强能帮的东西是有上限的真实样本才是最终解。5. 用验证集做最后的验证与迭代mAP、混淆矩阵与坏样本复盘5.1 从验证集输出里看 mAP 和混淆矩阵训练完成后用官方验证脚本跑一遍验证集是我固定的动作python val.py \ --data data/cigarette_damage.yaml \ --weights runs/cigarette_damage/v5s_640/weights/best.pt \ --img 640 \ --conf 0.5val.py 会在输出目录下生成 confusion_matrix.png、labels.jpg、results.png 等文件。看混淆矩阵时我重点盯 background 那一列。破损检测场景里大多数漏检不是“检出来了但分错类”而是“压根没检出来”这类错误最终都体现在 background 列上。如果那一列特别亮说明模型对烟体纹理的背景理解还不够正确的动作是补负样本或者调整训练数据的背景多样性而不是急着换更大的模型。5.2 用 badcase 复盘决定下一轮迭代动作把验证集里 mAP 最低的类别挑出来逐张看预测结果。不同的坏法对应不同的修法框中心偏离说明定位回归不够提高--img并降低 scale 超参数框有大有小不稳定说明标注本身不一致需要回头统一标注标准某个小破损被完全漏掉先试 1280 输入或者切图推理。如果验证集质量过关且 badcase 分析定位到具体类别下一步动作就非常明确了。这套数据集在生产环节的决策会走向哪一步完全取决于验证集和 badcase 的质量。我现在的习惯是跑正式实验前先确保数据集目录结构不变训练的权重和 data.yaml 都单独按日期存档。否则一旦动了标注前面那版能用的 best.pt 就再也找不回来。这点不是技术难点但往往是项目回退时的后悔药。希望上面的这些经验能帮你在香烟破损检测这个方向上少走几步弯路。本文还有配套的精品资源点击获取
返回列表