小样本吸烟检测实战:991张PASICAL VOC XML数据集的YOLOv8训练与调优
简介这份吸烟数据集面向计算机视觉方向的学习者与算法开发者适用于目标检测模型的训练、微调与性能评测尤其适合需要快速验证吸烟行为识别方案的场景。资源包共收录1982个文件由991张原始图片与991个PASCAL VOC XML格式标注文件一一对应组成图片覆盖多种吸烟姿态与背景标注信息可直接用于主流检测框架的数据加载与训练流程压缩包整体约44.92MB体积轻便、便于快速下载与本地部署。目前已有262人学习下载说明该数据集在实际项目中具备一定的参考与复用价值。基于该数据训练得到的模型平均识别率可达88.3%读者可据此搭建吸烟检测基线、对比不同网络结构的精度表现或将其作为数据增强与迁移学习的起点省去从零采集与标注的成本较快进入模型调优与落地验证阶段。1. 吸烟数据集落地991 张原始图片、88.3% 平均识别率背后的真实工程账手上只有 991 张原始图片标注是 PASICAL VOC XML 格式官方口径平均识别率 88.3%——这个数字放在论文里好看放到产线里能不能用是两回事。我见过太多团队拿到这类小数据集直接套一个 YOLO 默认配置跑一遍mAP 出来 0.6 出头然后开始怀疑数据有问题。问题往往不在数据在于没搞清楚 991 张图意味着什么它属于典型的小样本目标检测场景类别少、场景单一、标注格式老但恰好是验证「数据增强 迁移学习 标注清洗」这条链路的最佳试验田。这篇笔记面向两类人一类是手上有 PASICAL VOC XML 标注、想快速跑通吸烟行为检测的算法同学另一类是评估这个方向值不值得投入的工程负责人。我会把从数据体检、格式转换、训练参数到避坑的完整路径拆开讲参数给到能直接抄的程度坑也标清楚在哪一步最容易翻车。2. 先给 991 张图做体检PASICAL VOC XML 到底藏着什么拿到一个 PASICAL VOC XML 数据集第一件事不是写训练脚本是搞清楚它的分布。991 张这个量级如果类别分布再偏一点某些类可能只有几十个实例直接训就是灾难。PASICAL VOC XML 的结构本身不复杂每张图对应一个同名 xml里面object节点记录类别和bndbox坐标但实际拿到的数据里坑往往藏在细节坐标越界、宽高为 0、类别名大小写不一致、图片和 xml 对不上号。这些不体检出来后面训练 loss 不降你都不知道为什么。2.1 用一段脚本把数据集分布摸清楚我一般会先写一个统计脚本把类别数、每类实例数、图片尺寸分布、标注框宽高分布一次性打出来。这一步花十分钟能省后面几小时的瞎调参。import os import xml.etree.ElementTree as ET from collections import Counter, defaultdict ANNO_DIR annotations # PASICAL VOC XML 存放目录 IMG_DIR images # 原始图片目录 cls_counter Counter() size_counter Counter() wh_list defaultdict(list) bad_files [] for xml_file in os.listdir(ANNO_DIR): if not xml_file.endswith(.xml): continue path os.path.join(ANNO_DIR, xml_file) try: tree ET.parse(path) except Exception as e: bad_files.append((xml_file, parse_error)) continue root tree.getroot() # 图片尺寸 size root.find(size) if size is not None: w int(size.find(width).text) h int(size.find(height).text) size_counter[(w, h)] 1 # 遍历目标框 for obj in root.findall(object): name obj.find(name).text.strip() cls_counter[name] 1 bbox obj.find(bndbox) xmin int(float(bbox.find(xmin).text)) ymin int(float(bbox.find(ymin).text)) xmax int(float(bbox.find(xmax).text)) ymax int(float(bbox.find(ymax).text)) bw, bh xmax - xmin, ymax - ymin if bw 0 or bh 0: bad_files.append((xml_file, zero_bbox)) wh_list[name].append((bw, bh)) print(类别分布:, cls_counter) print(图片尺寸分布 top5:, size_counter.most_common(5)) for c, whs in wh_list.items(): ws [w for w, _ in whs] hs [h for _, h in whs] print(f{c}: 实例数{len(whs)}, 平均宽{sum(ws)/len(ws):.1f}, 平均高{sum(hs)/len(hs):.1f}) print(异常文件:, bad_files[:20], 共, len(bad_files))这段脚本做三件事统计类别实例数、统计图片尺寸分布、检查零面积框。逻辑上先解析 xml再逐 object 取 name 和 bndbox最后汇总。参数上ANNO_DIR和IMG_DIR按你实际目录改bad_files里出现的文件要么修要么删别留着。跑完你会得到几个关键判断如果某个类实例数低于 100后面增强要重点照顾如果图片尺寸五花八门训练时的 resize 策略就得统一如果有零面积框训练时 IoU 计算会出 NaN必须提前清掉。2.2 类别不平衡和尺寸分布决定你的增强策略991 张图里吸烟行为检测通常只有「吸烟」「未吸烟」或者「手持烟」「嘴含烟」这类 2 到 4 个类。如果统计出来某一类占比超过 70%那就是典型的长尾。这时候不要急着上 focal loss先把数据增强做对对少数类做随机裁剪、亮度扰动、小角度旋转对多数类做随机丢弃一部分。尺寸分布也很关键如果标注框平均宽高只有几十像素说明目标偏小训练分辨率就不能压太低输入尺寸至少 640否则小目标特征在 backbone 下采样几次后就没了。提示PASICAL VOC XML 里difficult标记为 1 的框训练时建议保留但评估时忽略很多开源框架默认不处理这个字段需要自己在 dataloader 里过滤。3. 把 PASICAL VOC XML 转成训练框架能吃的格式PASICAL VOC XML 是标注格式不是训练格式。不管你用 YOLO 系列、Detectron2 还是 PaddleDetection第一步都是转格式。转格式看着简单但坐标越界、图片名带空格、类别名映射错位这三个坑几乎每个项目都会踩一遍。我一般会写一个转换脚本同时做坐标裁剪和类别映射转完再抽样可视化验证。3.1 转 YOLO txt 格式的完整脚本与坐标裁剪YOLO 格式要求每张图一个 txt每行class_id cx cy w h全部归一化到 0 到 1。PASICAL VOC XML 给的是绝对坐标 xmin ymin xmax ymax转换时最容易翻车的是坐标超出图片边界——标注员手一抖xmax 写成 2000图片宽只有 1920归一化后大于 1训练时框就飞了。import os import xml.etree.ElementTree as ET from PIL import Image ANNO_DIR annotations IMG_DIR images OUT_DIR labels CLASSES [smoking, not_smoking] # 按你的实际类别改顺序固定 os.makedirs(OUT_DIR, exist_okTrue) def clip(v, lo, hi): return max(lo, min(v, hi)) for xml_file in os.listdir(ANNO_DIR): if not xml_file.endswith(.xml): continue stem os.path.splitext(xml_file)[0] img_path os.path.join(IMG_DIR, stem .jpg) if not os.path.exists(img_path): print(缺图:, stem) continue with Image.open(img_path) as im: iw, ih im.size tree ET.parse(os.path.join(ANNO_DIR, xml_file)) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in CLASSES: continue cid CLASSES.index(name) bbox obj.find(bndbox) xmin clip(int(float(bbox.find(xmin).text)), 0, iw - 1) ymin clip(int(float(bbox.find(ymin).text)), 0, ih - 1) xmax clip(int(float(bbox.find(xmax).text)), 0, iw - 1) ymax clip(int(float(bbox.find(ymax).text)), 0, ih - 1) if xmax xmin or ymax ymin: continue cx (xmin xmax) / 2.0 / iw cy (ymin ymax) / 2.0 / ih bw (xmax - xmin) / iw bh (ymax - ymin) / ih lines.append(f{cid} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) with open(os.path.join(OUT_DIR, stem .txt), w) as f: f.write(\n.join(lines))脚本核心是clip函数把坐标卡在图片范围内再算归一化中心点和宽高。CLASSES列表顺序必须和训练配置里的类别顺序完全一致错一位整个模型就学歪。转换完建议随机抽 20 张用 PIL 把框画回图上肉眼检查这一步别省。3.2 划分训练验证集时别用随机划分991 张图如果按 8:2 随机划分很可能同一场景的连续帧被分到训练和验证两边验证集精度虚高。正确做法是按场景或按视频来源分组划分同一组的图要么全在训练要么全在验证。如果数据里没有场景标识退而求其次按图片文件名前缀分组。划分比例上小数据集我一般用 7:2:1验证集留 200 张左右测试集留 100 张测试集只在最后跑一次不要拿来调参。# 假设图片名格式为 scene01_0001.jpg按 scene 前缀分组划分 python split_dataset.py --img_dir images --ratio 0.7 0.2 0.1 --group_by prefix划分脚本的逻辑是按前缀聚合再在组级别打乱分配保证同组不跨集。参数--group_by prefix表示按文件名下划线前部分分组如果你的命名规则不同改成对应字段即可。4. 训练参数怎么设从 88.3% 识别率倒推配置88.3% 这个平均识别率如果指的是 mAP0.5那对应的是一个中等偏上的小样本检测模型如果指的是分类准确率那参考价值有限。不管哪种你要复现或超过它核心在三点backbone 选预训练权重、输入分辨率别低于 640、学习率用 warmup 加余弦退火。991 张图从头训必然过拟合迁移学习是唯一出路。4.1 用 YOLOv8 在 991 张图上跑通最小训练命令以 YOLOv8 为例先把数据组织成它要求的目录结构然后写一个 data.yaml再跑训练。下面是最小可复现的命令和配置。# data.yaml path: ./smoking_dataset train: images/train val: images/val test: images/test nc: 2 names: [smoking, not_smoking]yolo detect train \ datadata.yaml \ modelyolov8s.pt \ epochs150 \ imgsz640 \ batch16 \ lr00.001 \ lrf0.01 \ warmup_epochs3 \ cos_lrTrue \ patience30 \ augmentTrue \ mosaic1.0 \ mixup0.1 \ degrees10 \ translate0.1 \ scale0.5 \ fliplr0.5 \ projectruns/smoking \ nameexp01参数逐个说modelyolov8s.pt用预训练权重小数据集别用 l 或 x参数量大更容易过拟合imgsz640是下限如果显存够可以上 768lr00.001配合lrf0.01做余弦退火初始学习率别超过 0.01小数据集容易震荡warmup_epochs3让前 3 个 epoch 学习率线性上升避免一开始就破坏预训练特征patience30是早停验证集 30 轮不涨就停增强参数里mosaic1.0和mixup0.1对小数据集提升明显但mixup别开太大0.1 到 0.2 够了开大了小目标会被混没。4.2 学习率和 batch size 的联动关系很多人只调学习率不看 batch size这是玄学调参的根源。实际有效的 batch size 等于单卡 batch 乘梯度累积步数学习率应该和有效 batch size 成正比缩放。比如你单卡 batch 只能开到 8想等效 batch 16就加--accumulate 2同时学习率保持 0.001 不变。如果你把 batch 从 16 降到 8 却不调学习率训练会变得不稳定loss 曲线毛刺明显。991 张图这个量级batch 16 加 150 epoch 是比较稳的组合显存不够就降 batch 加累积别硬撑。注意验证集 mAP 在训练中期突然掉一大截八成是学习率太大导致跳出最优区域先把 lr0 砍半再跑别急着换模型。5. 避坑与排查991 张图训练时最容易翻车的 5 个点小数据集训练翻车是常态不翻车才奇怪。下面这 5 个是我在吸烟数据集这类项目里反复遇到的每条按现象、原因、解决写清楚你对照自己的训练日志看。5.1 现象loss 从第一轮就不降一直卡在 0.7 左右原因通常是标注类别名和配置里的names对不上或者转换后的 txt 里 class_id 越界。PASICAL VOC XML 里类别名可能带空格或大小写不一致转换时没做 strip 和统一映射就错了。解决重新跑一遍统计脚本打印所有出现的类别名和data.yaml里的names逐字比对确认 class_id 从 0 开始连续。5.2 现象验证集 mAP 很高但测试集一跑就崩这是典型的场景泄漏。随机划分把同一场景的图分到了训练和验证两边模型记住了背景而不是目标。解决按场景或文件名前缀分组划分确保同一来源的图不跨集。如果数据里没有场景标识用图片的感知哈希做聚类同簇的图分到同一侧。5.3 现象小目标框全部漏检大目标正常原因在输入分辨率。991 张图里如果吸烟目标平均只有 40 像素宽resize 到 640 后可能只剩 20 多像素经过 backbone 三次下采样就没了。解决把imgsz提到 768 或 896同时在增强里关掉scale的下限别让图缩得太小。如果显存不够用切片推理把大图切成小块分别检测再合并。5.4 现象训练到 80 轮后验证集精度开始下降训练集还在涨过拟合。991 张图对任何检测模型都偏少模型开始背训练集。解决加大增强强度mosaic保持 1.0mixup提到 0.2加copy_paste0.1同时把weight_decay从默认 0.0005 提到 0.001如果还压不住换更小的 backbone比如从 yolov8s 降到 yolov8n。5.5 现象推理时框的位置整体偏移坐标转换时用了int截断而不是四舍五入或者归一化时除错了宽高。PASICAL VOC XML 的坐标是 1-based有些框架要求 0-based差 1 像素在大目标上看不出来小目标上就是明显偏移。解决转换脚本里统一用float计算最后再取整并且用可视化脚本抽检 50 张确认框贴合目标边缘。6. 把 88.3% 再往上推小样本吸烟检测的三个进阶技巧88.3% 不是天花板991 张图通过合理的技巧还能再榨出几个点。第一个技巧是伪标签半监督先用当前模型对未标注图片推理挑置信度高于 0.9 的框加入训练集迭代两轮通常能涨 2 到 3 个点。第二个技巧是类别平衡采样写一个自定义 sampler让每个 batch 里少数类实例占比不低于 30%比 focal loss 更直接。第三个技巧是测试时增强推理时对同一张图做水平翻转和不同尺度把结果做 NMS 融合mAP 能再涨 1 个点左右。# 测试时增强的简化实现 import cv2 import numpy as np def tta_predict(model, img, scales(1.0, 1.25), flipTrue): all_boxes, all_scores, all_cls [], [], [] for s in scales: h, w img.shape[:2] resized cv2.resize(img, (int(w * s), int(h * s))) for f in ([False, True] if flip else [False]): inp cv2.flip(resized, 1) if f else resized boxes, scores, clses model(inp) # 还原坐标到原图尺度 boxes boxes / s if f: boxes[:, [0, 2]] w - boxes[:, [2, 0]] all_boxes.append(boxes) all_scores.append(scores) all_cls.append(clses) # 合并后做 NMS return merge_nms(all_boxes, all_scores, all_cls)这段代码的逻辑是对不同尺度和翻转的推理结果做坐标还原再统一 NMS。参数scales别设太多两三个够了多了推理时间线性涨。flip对吸烟检测这种左右对称场景有效如果目标有方向性就别开。验证方法上我习惯在测试集上同时看 mAP0.5 和 mAP0.5:0.95前者看整体检出后者看框的紧致程度。如果两者差距超过 0.2说明框回归不准回去检查标注质量。另外按类别分开看 AP如果某一类明显低单独对那一类做增强或补标注。最后说个我自己的习惯每次改完参数先把训练日志里的 loss 曲线和 mAP 曲线截图存档标注清楚改了什么。991 张图这个量级调参空间不大但记录能让你少走回头路。小数据集项目最怕的不是精度低是不知道哪次改动起了作用。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →