简介:一套可直接用于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 | 常见类别名 | 破损形态 |
|---|---|---|
| 0 | cigarette_broken | 烟体有明显折痕或断裂 |
| 1 | filter_damaged | 滤嘴端部缺损、撕裂 |
| 2 | paper_crack | 卷烟纸纵向裂开 |
| 3 | pack_dented | 烟盒外观变形、压扁 |
| 4 | seal_torn | 透明膜、封口贴撕裂 |
| 5 | mold_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 写着 nc=6,那类别 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,对大多数工业缺陷检测场景已经够用。需要手动干预的通常只有这几个:
| 超参数 | 默认值 | 什么情况下调 |
|---|---|---|
| lr0 | 0.01 | 数据集偏小或迁移效果差时,降到 0.005 |
| fl_gamma | 0.0 | 6 个类别数量明显不平衡时,设为 1.5 左右 |
| mosaic | 1.0 | 训练后半程把 mosaic 降到 0.5 或关闭,防止过拟合 |
| scale | 0.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 mAP@0.5 mAP@0.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 有两档:mAP@0.5 表示 IoU 阈值 0.5 时的平均精度,mAP@0.5:0.95 表示从 0.5 到 0.95 每隔 0.05 取一次阈值的平均。在香烟破损检测这种场景,我更信任 mAP@0.5。原因是破损区域的边界本来就模糊,人工标注的框很难做到高度一致的 IoU,mAP@0.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 虚高:训练和验证数据划分不当
现象:训练结束后跑验证,mAP@0.5 高达 0.98,看起来模型已经接近完美。但把训练好的权重接进摄像头或者视频流里实测,漏检一塌糊涂,跟验证结果完全不成比例。
原因:数据集划分时按文件名随机分,而制作方采集的是同一包烟的多视角连续帧。这些帧在光照、角度、背景上高度相似,随机划分后,验证集里很可能混入了跟训练集几乎相同的画面。模型在验证的时候就等于见过答案,mAP 当然虚高。
解决:拿到数据集后先看图片命名和采集规律。常见做法是按拍摄批次或场景维度划分,而不是按文件名随机打散。如果数据集自带的 val 目录已经确定且划分不合理,可以把 val 里的图片重新抽样,确保每个拍摄批次只出现在一个集合里。另外,你可以在训练结束后从验证集里按置信度从低到高排序,挑出预测最差的几十张图,人工确认它们是否与训练集内容高度相似。这一步做不了假,也是最直接的验证集质量检验方法。
4.3 小目标漏检严重:破损部区域太小,标注框过小
现象:模型对正常大小的烟盒变形检测得不错,但裂口、霉斑这类小区域几乎全漏。不是分错类,而是预测框完全没有出现。
原因:破损区域在原始图片里可能只有几十个像素,经过--img 640缩放到 640 后,目标区域在特征图上可能只剩一个点。加上默认超参数里 scale=0.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 就再也找不回来。这点不是技术难点,但往往是项目回退时的后悔药。希望上面的这些经验能帮你在香烟破损检测这个方向上少走几步弯路。
本文还有配套的精品资源,点击获取