简介:这是一份面向计算机视觉初学者、算法工程师及安全监控项目开发者的吸烟检测数据集,集中覆盖室内办公、走廊、室外街道等多种真实场景,包含一千余张已标注图片。所有图像均配套VOC格式的XML标签文件,类别信息清晰,可直接用于训练YOLO、SSD、Faster R-CNN等主流目标检测模型,也可用于模型精度对比、训练流程验证或论文实验。资源总文件数为2000个,其中1507个XML标签文件与486张JPG、7张PNG图像一一对应,压缩包整体约862MB,目录组织直观,便于按图像名称快速检索与划分训练集、验证集。当前已有240人学习下载,适合希望跳过数据采集和标注环节、直接进入模型训练的开发者使用,尤其适合开展吸烟行为识别、智慧安防巡检等场景的算法落地验证。
1. 一千张室内外吸烟检测数据,真正值钱的不是数量而是标签规范
标题里“1000多张室内室外场景人员吸烟检测数据集,voc格式标签”这句话,我反复看了几遍,想提醒你一个反直觉的点:这1000多张图,决定它能不能用的,不是张数,而是VOC格式的标签是否被严格校验过。做过目标检测的人都懂,标注数据里十张坏图就能让你训练出来的模型在推理时集体翻车,室内外光照差异更会把小目标检测的玄学放大好几倍。这篇笔记会从格式拆解、质量排查、转YOLO到训练调参,把一条能复现的路径完整走一遍,适合准备做禁烟检测、又在为数据和标签头疼的工程师。
2. 把 VOC 格式拆开看:目录结构、XML 字段与标注工具的配置
VOC 格式是目标检测领域最经典的数据组织方式,Faster R-CNN、SSD、YOLO 早期版本都原生吃这套格式。拿到一个声称是 VOC 格式的吸烟检测数据集,第一件事不是打开图片看烟头标得准不准,而是先核实目录结构完不完整。很多从网上下载或同事交接来的数据集,往往只有 JPEGImages 和 Annotations 两个文件夹,ImageSets 直接缺失,训练脚本一跑就报找不到训练集划分文件。
2.1 JPEGImages 与 Annotations:先搞清谁是谁的索引
VOC 格式的标准布局是三个目录加一个划分文件组:JPEGImages 放原始图片,Annotations 放与图片同名的 XML 标注文件,ImageSets/Main 下放 train.txt、val.txt、trainval.txt 这些纯文本列表。文件名就是索引,一张smoking_001.jpg对应一个smoking_001.xml,严格同名才认。我见过不少数据集用图片压缩软件批量改名后,XML 文件名没跟着改,结果训练时一半样本读不出来。
# 先跑一遍目录核对,缺文件一目了然 cd smoking_dataset find JPEGImages -type f | wc -l find Annotations -type f | wc -l find ImageSets/Main -type f | wc -lfind分别统计三个目录的文件数时,如果 JPEG 和 XML 数量对不上,先别急着补文件,要把差出来的名单打出来看。常见情况是标注遗漏,图片存在但没人标;另一种可能是重复标注,同一张图存在两份 XML。用find列出文件名做 diff,把缺的、多的都定位出来,这一步是后面所有训练的前提。
提示:ImageSets/Main 下的 txt 文件里每行是文件名主部,不带 .jpg 也不带 .xml,YOLO 的转换脚本和训练脚本都依赖这个约定,不要自己在 txt 里加扩展名。
2.2 XML 里真正有用的五个字段
VOC 的 XML 结构不复杂,但字段含义容易被忽略。外层是<annotation>根节点,里面<filename>对应图片名,<size>记录宽高和通道数,<object>标签块里是目标的类别名和<bndbox>坐标。真正决定训练效果的,是<bndbox>里 xmin、ymin、xmax、ymax 四个值,它们必须和图片像素坐标系一致。
| 字段 | 作用 | 检查重点 |
|---|---|---|
filename | 关联图片 | 与 JPEGImages 下文件名完全一致 |
size/widthsize/height | 图片真实尺寸 | 与图片文件头一致,不能是标注工具的缩放窗口尺寸 |
object/name | 类别名 | 全数据集统一,不能“smoking”和“smoke”混用 |
bndbox/xmin | 目标左边界 | 大于 0,小于 xmax |
bndbox/ymax | 目标下边界 | 不超过图片高度 |
解析 XML 时我习惯用xml.etree.ElementTree,一个容易踩的细节是:有些标注工具会把<truncated>、<occluded>、<difficult>这些字段省略,而不是显式写 0。解析脚本里如果不做get()带默认值,直接取find('difficult').text就会抛 NoneType 异常。更隐蔽的是<size>里的值如果是从旧文件复制的,和图片实际宽高差一点点,转成 YOLO 归一化坐标后,整个框的位置就偏了。
import xml.etree.ElementTree as ET tree = ET.parse('Annotations/smoking_001.xml') root = tree.getroot() size = root.find('size') width = int(size.find('width').text) height = int(size.find('height').text) for obj in root.findall('object'): name = obj.find('name').text bbox = obj.find('bndbox') xmin = int(bbox.find('xmin').text) ymin = int(bbox.find('ymin').text) xmax = int(bbox.find('xmax').text) ymax = int(bbox.find('ymax').text) print(name, xmin, ymin, xmax, ymax)这段代码是标准的 XML 读取流程,width和height是从 XML 里取的值,后面所有坐标校验都依赖它们。常见做法是把这一步封装成独立函数,因为后续转换脚本还要复用。需要特别注意的是xmax和ymax的取值惯例,LabelImg 这类工具习惯从 1 开始计数,但 VOC 官方评测代码按像素起始 0 处理,转换成 YOLO 时统一按 0 起算,避免坐标整体偏移一个像素。
2.3 用 LabelImg 打开 1000 张图之前先改这三个配置
如果数据集的标注需要补漏或修正,LabelImg 是处理 VOC 格式最顺手的工具。安装完成后别急着打开图片,先改三处配置,否则后面每标一张图都想砸键盘。第一,在Save菜单里选PascalVOC格式,默认可能是 YOLO 格式,存出来的是一堆 txt,和 VOC 的 XML 对不上。第二,在View里打开Auto Saving,自动保存能让你标完一张就切下一张,不会因为手滑丢标签。第三,如果数据集只有一个类别,在标注前把标签写成一个classes.txt,别看它简单,多人协作时命名不一致的问题多半出在这里。
还有一个容易忽略的点:LabelImg 的默认界面里有个difficult标志位,如果你在标注时不小心勾上,生成的 XML 里就会有<difficult>1</difficult>。后面训 YOLO 时如果你的转换脚本忽略了困难样本,这些框直接被丢弃,数据量和类别分布就变了。对吸烟检测来说,远处模糊的烟头恰恰是最需要保留的困难样本,我一般不建议跳过。
启动 LabelImg 的命令也简单,但要在 Python 环境里先装好依赖库:
# LabelImg 基于 PyQt5,安装后直接启动 pip install labelImg labelImg . --classes classes.txt第二个参数--classes指定类别文件,启动后左侧标签列表里就只有你定义好的类别,避免手输标签时打出smoking、Smoking、吸烟这种不一致的名字。labelImg .后面的点表示打开当前目录,它会自动加载 JPEGImages 下的图片和 Annotations 下的 XML。
2.4 室内外样本差异:为什么室外图 300 张就够,室内却要多备
同样是“吸烟检测”,室内和室外两个场景的视觉特征差异巨大。室内环境以荧光灯、白炽灯为主,光照稳定,但背景里白墙、瓷砖、玻璃反光多,烟头发出的红光在浅色背景下对比度反而低。室外则是逆光、树影、黄昏、夜晚霓虹灯轮番上场,烟头这种只有十几个像素的小目标,在强逆光下很容易和背景融为一体。
数据分布上,一个相对稳妥的比例是:如果总数据量只有 1000 多张,室内约占六到七成,室外占三到四成,室外里再刻意补一些黄昏和夜晚的样本。否则模型训练完后,大概率出现室内准确率尚可、室外漏检率飙高的情况。标注时也要按场景区别对待:室内图把 bbox 卡紧烟支本身,别把手指、嘴唇框进来;室外逆光图允许适当放宽边界,因为烟头边缘本身就不清晰,框小了反而给模型制造噪音。
3. 标注质量排查:五条能把训练直接带翻车的坑
拿到数据集先别高兴,VOC 格式只能说明标签长那个样,不代表标签是对的。我这几年代码敲下来,一半的训练事故不是模型结构问题,而是数据里的暗伤。以下五条按杀伤力排序,每一条都是先给现象,再讲原因,最后给解决办法,照着排查一遍,能省下后面好几个调参的夜晚。
3.1 文件名大小写不一致:数据在 Windows 好好的,到 Linux 训练就丢一半
现象:训练脚本报 FileNotFoundError,报错文件是smoking_014.jpg,但目录里明明躺着SMOKING_014.JPG。或者干脆不报错,但训练 loss 降不下去,因为数据加载时跳过了一批图片,有效样本少了一截。
原因:VOC 格式里图片是 jpg,XML 文件名却是 .JPG 或 .JPEG 混用。Windows 文件系统大小写不敏感,资源管理器能正常打开;Linux 严格区分大小写,转换脚本和训练框架按小写去找文件就匹配不上。这个问题在多人协作、跨平台拷数据时最常见。
解决:写个几行的脚本统一把文件名转成小写并统一扩展名。注意顺序:先改图片文件名,再改 XML 里的<filename>字段,最后用 XML 文件名反向核对一遍。
import os, glob # 先把所有 jpg 统一成小写 .jpg,避免 .JPG/.jpeg 混用 for path in glob.glob('JPEGImages/*'): directory = os.path.dirname(path) filename = os.path.basename(path) new_name = filename.lower().replace('.jpeg', '.jpg') if new_name != filename: os.rename(path, os.path.join(directory, new_name)) # 再改 XML 里的 filename 节点,保持索引一致 import xml.etree.ElementTree as ET for xml_path in glob.glob('Annotations/*.xml'): tree = ET.parse(xml_path) root = tree.getroot() old = root.find('filename').text new = old.lower().replace('.jpeg', '.jpg') root.find('filename').text = new tree.write(xml_path, encoding='utf-8')这段脚本先把图片名统一成小写,再把 XML 里的<filename>同步改成新名字。换完之后养成一个习惯:拿 XML 文件名列表当作准绳,逐个检查对应图片是否存在,因为标注过程中丢掉一张图很容易,补起来却很麻烦。
3.2 XML 的<size>和图片实际尺寸对不上,bbox 全被拉偏
现象:模型训练出来,预测框整体往右下或左上偏移,尤其是大目标偏得更明显,小目标的框干脆飘到物体旁边。
原因:标注员在 LabelImg 里打开图片时,如果系统缩放显示,工具记录的<size>是基于原始文件还是显示窗口因版本而异。更常见的是图片在标注后被批量压缩过、裁剪过,但 XML 没同步更新<size>和坐标。归一化时宽度和高度用错,自然全盘错位。
解决:转换前做一次批量比对,用 OpenCV 读图片真实尺寸,和 XML 里的<size>对照,不一致的直接报出来。
import cv2, glob, xml.etree.ElementTree as ET for xml_path in glob.glob('Annotations/*.xml'): tree = ET.parse(xml_path) root = tree.getroot() img_path = os.path.join('JPEGImages', root.find('filename').text) h, w = cv2.imread(img_path).shape[:2] xml_w = int(root.find('size/width').text) xml_h = int(root.find('size/height').text) if (w, h) != (xml_w, xml_h): print(f'{xml_path}: xml size {xml_w}x{xml_h}, real {w}x{h}')打印出来的每一行都是一张需要重新导出的图。处理方式看时间成本:错得不多就改<size>然后按比例缩放 bbox 坐标;错得离谱就直接删掉这张样本,1000 张数据里去掉几张坏图,比带着错误坐标训练更划算。
3.3 烟头只有 12 个像素:小目标直接超出 YOLO 默认 anchor 的感知范围
现象:mAP50 数值还行,mAP50-95 一路走低;推理时近处的烟能框住,画面里两三米外的烟头直接漏掉。
原因:YOLO 系列骨干网络下采样 32 倍,一张 640 像素宽的图在特征图上只有 20 像素宽。烟头在原始图上可能才 12 像素,落到特征图上不足 1 像素,检测头根本感知不到。VOC 格式本身没有小目标专属标注,全看原始抽烟标注时有没有把这种小框标出来。
解决:训练时把输入分辨率提到 1280,相当于给模型一张更大尺寸的图来放大微小烟头;或者对原始图片做滑窗切块,把大图切成若干 640 小图再训练。前者显存占用翻倍,后者训练时间变长,但小目标召回率通常能明显改善。数据层面还要确认:训练集里有没有大量把“手夹着烟”整个框进去的标注,框越大,模型越学不到烟头本身的纹理。
3.4 把“烟”和“吸烟”混成一类,模型学出来的是“手指夹物”
现象:测试集里有人拿笔、拿筷子甚至拿手机,模型也报 smoking,置信度还不低。
原因:标注规则没定义清楚。VOC 格式允许标注任意语义,但算法不区分“烟”和“吸烟”,它学的是标注框内所有像素的共性。如果框里既包含烟头又包含手指、嘴唇,模型记住的其实是“手指夹一截浅色物体”的组合特征,而不是“烟头燃烧”的特征。
解决:把类别名收敛成一个,全数据集统一写smoking,并且只框烟头或烟支本身,绝对不要把整只手框进去。如果场景里需要区分点燃和未点燃的烟,那就开两个类,smoking和cigarette,不要用同一个类硬扛。改完标签后要重新检查一遍类别文件和 XML 里的 name 字段。
3.5 数据划分没固定随机种子,验证集在作弊
现象:训练时 loss 曲线很漂亮,跑完测试 mAP 却对不上号,同一套参数一次比一次好,一次比一次差。
原因:数据集划分脚本用了random.shuffle但没设random.seed(42),每次跑出来的 train.txt 和 val.txt 都不一样。最严重的时候,一张图的增强版本或者同一个场景的连拍图被同时分进训练集和验证集,相当于训练时已经见过了“答案”。
解决:划分脚本里先random.seed(42),划分完打印训练集和验证集的类别统计,确认每个 set 里都有足够多的阳性样本。更稳妥的做法是直接按 ImageSets/Main 下已有的 trainval.txt 和 val.txt 划分,不重新造轮子。检查一下 val.txt 里的图片名和训练集有没有交集,一行命令的事,别让玄学背这个锅。
4. 把 VOC 转成 YOLO 格式:转换脚本与四个边界坑
YOLO 训练不认 XML,只认每个图片对应一个.txt,每行写类别 id、归一化后的中心点坐标。转换脚本本身不难,真正的坑全在边界条件里。这一章先给可抄的脚本,再逐个讲清楚容易出错的地方。
4.1 先定目录骨架:训练前把数据落成 YOLO 默认结构
YOLO 默认的数据组织方式是一个父目录下分images和labels两个子目录,各自再按 train、val 细分。这样训练脚本的配置最简单,不会因为路径层级问题多绕弯。
# 以 smoking 为项目目录,建立 images/labels 下的 train/val 结构 mkdir -p smoking_dataset/images/train smoking_dataset/images/val mkdir -p smoking_dataset/labels/train smoking_dataset/labels/val四行命令建好基础目录。VOC 的 JPEGImages 全部对应训练集时,就直接把图片复制到 images/train 下;如果有 val 划分,按 val.txt 里的文件名主部去复制对应图片。labels 目录的结构要和 images 完全一致,否则训练时找不到标签文件。
4.2 XML 转 txt:坐标归一化脚本
核心转换逻辑是读 XML 里的 bndbox 坐标,除以图片宽高得到 0 到 1 之间的归一化值,类别 id 按你定义的类别顺序从 0 开始。
import os, glob import xml.etree.ElementTree as ET # 类别顺序要固定,这个顺序会写进数据集的 yaml 文件 class_names = ['smoking'] class_to_id = {name: idx for idx, name in enumerate(class_names)} def convert_xml_to_txt(xml_path, txt_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') width = int(size.find('width').text) height = int(size.find('height').text) with open(txt_path, 'w') as f: for obj in root.findall('object'): # 跳过 difficult 样本,默认不跳过则删掉这行 if obj.find('difficult') is not None and int(obj.find('difficult').text) == 1: continue name = obj.find('name').text if name not in class_to_id: continue bbox = obj.find('bndbox') xmin = int(bbox.find('xmin').text) ymin = int(bbox.find('ymin').text) xmax = int(bbox.find('xmax').text) ymax = int(bbox.find('ymax').text) # 坐标越界保护,避免 xmax 超过图片宽度 xmin = max(0, min(xmin, width)) xmax = max(0, min(xmax, width)) ymin = max(0, min(ymin, height)) ymax = max(0, min(ymax, height)) center_x = (xmin + xmax) / 2 / width center_y = (ymin + ymax) / 2 / height box_w = (xmax - xmin) / width box_h = (ymax - ymin) / height f.write(f'{class_to_id[name]} {center_x:.6f} {center_y:.6f} {box_w:.6f} {box_h:.6f}\n') for xml_path in glob.glob('Annotations/*.xml'): filename = os.path.basename(xml_path).replace('.xml', '.txt') convert_xml_to_txt(xml_path, os.path.join('labels/train', filename))class_to_id这个字典决定了类别 id 的映射,smoking是 0,如果后续加了cigarette就是 1,顺序必须和训练时 yaml 里 names 列表保持一致。坐标归一化时把中心点、宽、高都除以图片宽高,得到 0 到 1 的浮点数,YOLO 训练脚本读的就是这种格式。越界保护那段max和min是我后来加上去的,因为你永远不知道标注工具会生成什么样的小数坐标,防止 xmax 比 width 大 1 个像素这种问题直接让训练崩溃。
参数说明:convert_xml_to_txt函数接收 XML 路径和输出 txt 路径,脚本主体遍历所有 XML 后写入 labels 目录。生成的 txt 和原始图片同名,只是扩展名不同。
4.3 转换结果自检:把 YOLO 标签画回图上
转换完别急着训练,先画回去看一遍。很多人省了这一步,结果训练过程中发现框全画在天上,回头看是某个 XML 的尺寸字段是错的。这个自检脚本一次能把所有 txt 的框画到图片上,攒成一张大图扫一眼。
import cv2, os, glob def draw_yolo_boxes(img_path, txt_path, class_names, output_path): img = cv2.imread(img_path) h, w = img.shape[:2] if not os.path.exists(txt_path): cv2.imwrite(output_path, img) return with open(txt_path) as f: lines = f.readlines() for line in lines: parts = line.strip().split() cls_id, cx, cy, bw, bh = int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) xmin = int((cx - bw / 2) * w) ymin = int((cy - bh / 2) * h) xmax = int((cx + bw / 2) * w) ymax = int((cy + bh / 2) * h) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, class_names[cls_id], (xmin, ymin - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(output_path, img) os.makedirs('check_visual', exist_ok=True) for txt_path in glob.glob('labels/train/*.txt'): img_path = os.path.join('JPEGImages', os.path.basename(txt_path).replace('.txt', '.jpg')) draw_yolo_boxes(img_path, txt_path, class_names, os.path.join('check_visual', os.path.basename(img_path)))关键是最后一步用 OpenCV 把归一化坐标还原成像素坐标再画矩形,这一步能直观看到标签是否贴住烟头。如果你发现有些 box 特别大,把整个人都框进去了,不是脚本错,是原始标注的框就不准,得回去改 XML。
4.4 四个边界坑:difficult、翻转、空标签、类别 id 重排
第一个坑是 difficult 和 truncated 字段。以前在 VOC 时代这两个字段用于区分困难样本和截断目标,YOLO 转换时要不要跳过看你的场景。吸烟检测里远处的不清晰烟头本身是困难样本,舍弃会削弱模型的远距离检测能力,所以我的建议是:修正这些框的位置而不是直接删掉。
第二个坑是数据增强里的图片翻转。YOLO 训练时开启 mosaic 和随机翻转后,坐标由训练框架自己重算,没问题;但如果有人在数据层面先做了水平翻转增强,翻转后的图片必须对应翻转后的 txt,不能用原来的坐标。VOC 转 YOLO 之后,增强脚本和训练时数据加载的翻转逻辑可能会叠加,导致模型看到的框错位。
第三个坑是空标签问题。如果某张图确实没有目标或所有目标都被过滤掉,生成的 txt 文件就是零字节。YOLO 默认会跳过这种图片不参与训练,如果你期待“负样本”参与训练,需要在 yaml 里额外配置,或者把这类图单独放一个背景类。常见做法是把空标签文件直接删掉,避免训练框架报警。
第四个坑是类别 id 重排。VOC 类别名是字符串,YOLO 只认数字。如果你从别人那里拿到的数据集自带的labels.txt顺序是smoking在前,而你转换脚本里 class_names 把smoking放在第二个位置,id 就全错了。每个转换脚本的 class_names 要和最终训练 yaml 里的 names 完全一致,这是最容易在交接时埋雷的地方。
5. 用 YOLOv8 跑通吸烟检测最小训练流程
数据格式处理好之后,剩下的就是训练。YOLOv8 是目前把配置和训练命令封装得最省心的检测框架,一个.yaml加一行命令就能跑起来。这一章给的是最小可用方案,每个参数都代表一个你需要做决策的点。
5.1 配置数据集 yaml:路径用绝对还是相对
YOLOv8 的数据配置用 YAML 文件描述,三个键:path 指向数据集根目录,train 和 val 分别指向训练集和验证集图片目录的相对路径或绝对路径。
# smoking.yaml path: /data/smoking_dataset train: images/train val: images/val names: 0: smoking写完 yaml 检查一遍,path写成绝对路径最稳妥,尤其用脚本训练的时候,千万别用相对路径然后被当前工作目录坑了。names的键值映射要和转换脚本里 class_names 的顺序一致,这里只有 0 对应 smoking,如果以后加了类,需要同步修改 yaml 和转换脚本。
5.2 加载预训练权重,跑最小训练命令
YOLOv8 的预训练权重在首次使用时自动下载,训练命令简洁,但参数作用必须心里有数。
yolo detect train \ data=smoking.yaml \ model=yolov8n.pt \ imgsz=1280 \ epochs=100 \ batch=16model=yolov8n.pt是 nano 版本预训练权重,参数最少、速度最快,适合先跑通流程。imgsz=1280前面说过,对烟头这种小目标很重要,代价是显存占用变成 640 分辨率下的四倍。epochs=100对 1000 多张的数据集来说稍微偏多,配合后续的早停策略可以接受。batch=16在 1280 分辨率下大概需要 16GB 以上显存,显存不够降到 8 或 4,不要硬撑。
注意:如果第一次训这类数据集,先跑通流程再谈优化。模型和参数可以在训完一轮后换大版本、加分辨率,数据格式的问题越早暴露越省钱。
5.3 四个必调参数:imgsz、batch、mosaic、close_mosaic
除了上面的基础命令,YOLOv8 训练还有几个参数是针对室内外混合数据特别值得调的。
mosaic=1.0默认开启马赛克增强,把四张图的随机区域拼成一张训练图,对提升泛化能力很有帮助。但马赛克会缩放大图中每个目标的尺寸,烟头这种小目标经过拼接和缩放,经常缩得更小甚至消失,造成模型对小目标更不敏感。常见做法是保留 mosaic 但降低概率,比如mosaic=0.5,让模型一半时间看真实尺寸的图。
close_mosaic=10表示训练最后 10 个 epoch 关闭马赛克增强,让模型适应真实的物体尺寸分布。这个参数我建议从 10 开始,如果 loss 曲线在最后阶段抖得厉害就增加到 15。
degrees=0控制旋转增强的角度范围。吸烟检测场景里烟头本身有各种朝向,但如果标注框是正矩形,旋转角度太大会让框里混进大量非目标像素,建议先保持degrees=0,训完一轮再决定要不要开。
hsv_h、hsv_s、hsv_v控制色调饱和度亮度的扰动范围。室内外的光照差异主要落在亮度通道,适当调大hsv_v=0.6让模型对暗光、逆光更鲁棒,代价是收敛变慢一点。
yolo detect train \ data=smoking.yaml \ model=yolov8n.pt \ imgsz=1280 \ epochs=100 \ batch=16 \ mosaic=0.5 \ close_mosaic=10 \ hsv_v=0.6 \ patience=20patience=20是早停参数,连续 20 个 epoch 验证集指标不提升就自动停止。这组参数组合是我在类似小目标场景里的常用起点,具体数值要看着 loss 曲线微调。训完看runs/detect/train/目录下的results.csv,里面记录了每个 epoch 的 box_loss、cls_loss 和验证集指标。
5.4 训练过程看什么:loss 曲线哪个信号要先停
训练不是等它跑完就完事,每轮结束后要瞄一眼两个信号。第一个是 box_loss 和 cls_loss 的下降曲线,如果 cls_loss 在某个 epoch 后不再下探甚至反弹,说明类别学够了,再训会记住背景噪音。第二个是 val/box_loss 和 train/box_loss 的差距,如果差距越拉越大,明显过拟合,这种时候提前停掉,用验证集指标最好的那轮权重。
吸烟检测是安全监控类用途,评价指标上我习惯主要看召回率。漏掉一次真实吸烟行为比多报一次误检严重,所以验证时别只盯着 mAP,把 Precision 和 Recall 看全。YOLOv8 训练结束后会在runs/detect/train/下生成confusion_matrix.png,里面能清晰看到烟头被分到了哪个类别、有没有大量和背景混淆。
6. 室内外分开验证与半自动标注扩数据
训完模型先别急着部署,把验证集按室内和室外分开各跑一遍推理,你会发现两个场景的 mAP 可能差十个百分点以上。这个差异才是后续数据扩充方向的依据。
6.1 把测试集按室内室外拆开,一张图看穿泛化缺陷
在测试图片的命名或元数据里提前打上indoor、outdoor的标记,推理时分别统计。室外组如果漏检集中在远距离小烟头,就重点补远距离样本;室内组如果误检集中在白色反光区域,就补充反光背景的负样本。这一步比你闷头调参数效率高得多,因为数据缺口往往是模型上限的天花板。
6.2 用半自动标注把 1000 张扩到 5000 张
训练的模型稳定后,用它去标注新增图片,输出一套 yolo 格式标签,人工只修正框的位置和漏检的样本。这个流程能把你从标注地狱里捞出来,重点补三类图:室内夜晚、室外逆光、镜头远端的烟头。半自动标注产出后,重新走一遍校验脚本,把置信度低的框全部人工重标。
# 用训练好的模型批量推理新图片,输出 YOLO 格式标签供人工修正 from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') results = model.predict(source='new_capture/', imgsz=1280, conf=0.25, save_txt=True, save_conf=True)save_txt=True会把预测框写为 YOLO 格式的 txt,save_conf=True额外保留置信度。人工修正时优先看置信度低于 0.5 的框和漏检的大图块,修正完再和原标注合并,生成一份更大的训练集。
做这类项目时我最大的教训是:花三天修标签,胜过花三周调模型参数。数据里的脏标签不清理,后面所有训练都是拿玄学当工程。室内外场景差异已经够模型喝一壶了,别再让标注错误掺一脚。这 1000 多张 VOC 数据跑通流程后,你会更清楚下一批数据该补什么,而不是盲目堆数量。希望帮到你。
本文还有配套的精品资源,点击获取