简介:本资源是一套专为火灾安全智能识别任务设计的烟雾与明火目标检测数据集,面向计算机视觉初学者、算法工程师及安防AI项目开发者,适用于YOLO系列、Faster R-CNN等主流检测模型的训练与验证。数据集共3007张高质量JPG图像,配套3007份VOC格式XML标注文件与3007份YOLO格式TXT标签文件,涵盖“fire”(5198个框)和“smoke”(1651个框)两类关键目标,总标注框数6849,全部使用labelImg规范矩形框标注,无分割路径干扰,开箱即用。压缩包含2000个文件(1999个XML+1个说明txt),大小299.85MB,结构简洁,便于快速加载与格式转换。目前已有656人学习下载,读者可直接用于模型训练、mAP评估、数据增强实验或作为火灾检测项目baseline数据源,显著降低真实场景数据采集与标注成本。
1. 烟雾明火目标检测数据集3000张VOC+YOLO格式:不是“拿来就能训”的万能包,而是你调参前必须亲手验过的真标定场
你是不是也试过:在 GitHub 上搜到一个标着“3000张烟雾明火数据集”的压缩包,解压后发现图片模糊、xml 里<name>写成firee、txt 文件里坐标全为负数,或者更玄学的——训练时 loss 掉得飞快,但验证时 mAP 始终卡在 0.05?这个「烟雾明火烟火火灾目标检测数据集3000张VOC+YOLO格式.rar」不是那种“宣传图比实际多三倍”的资源。它真实包含 3007 张 JPG 图片、3007 个 Pascal VOC 格式 XML 文件、3007 个 YOLOv5/v8 兼容的 TXT 标签文件,类别严格限定为fire和smoke两类,总标注框数 6849(其中明火框 5198 个,烟雾框 1651 个),全部由 labelImg 人工逐帧框选。它不承诺模型精度,但承诺每一份 XML 和 TXT 都可被xml.etree.ElementTree正常解析、每一对(jpg, txt)都能用cv2.imread + np.loadtxt加载后直接画框验证。适合正在做消防预警系统原型、高校课程设计中的目标检测模块、或需要快速构建 baseline 的算法工程师——前提是,你愿意花 20 分钟跑一遍校验脚本,而不是双击解压就往train.py里扔。
2. 数据结构与格式验证:先确认“它真的长这样”,再谈训练
2.1 目录结构与文件命名规范:从ESE_fire_1498.xml看标注一致性
解压后你会看到一个扁平目录,无子文件夹,所有文件同级存放:
烟雾明火数据集/ ├── ESE_fire_1498.jpg ├── ESE_fire_1498.xml ├── ESE_fire_1498.txt ├── ESE_fire_557.jpg ├── ESE_fire_557.xml ├── ESE_fire_557.txt ... └──说明.txt关键点在于命名一致性:每个样本的 JPG、XML、TXT 三件套共享完全相同的前缀(如ESE_fire_1498),且不含空格、中文、特殊符号。这是后续自动化划分 train/val/test 的基础。说明.txt内容极简,仅声明格式与类别,无版本号、无作者信息、无采集设备参数——这意味着你不能依赖它做数据溯源,但反而降低了元数据污染风险。
提示:不要手动重命名!若你发现某张图只有 JPG 没有对应 XML/TXT,或前缀不一致(如
ESE_smoke_123.jpg对应ESE_fire_123.xml),说明该样本损坏,应直接剔除。我一般会先执行一次完整性扫描,脚本见 2.2 节。
2.2 三件套完整性校验:用 12 行 Python 脚本筛出“孤儿文件”
运行以下脚本,它会遍历当前目录,提取所有 JPG/XML/TXT 的 basename(不含扩展名),并比对三者交集:
import os import glob root = "." # 当前目录 jpgs = set([os.path.splitext(f)[0] for f in glob.glob(os.path.join(root, "*.jpg"))]) xmls = set([os.path.splitext(f)[0] for f in glob.glob(os.path.join(root, "*.xml"))]) txts = set([os.path.splitext(f)[0] for f in glob.glob(os.path.join(root, "*.txt"))]) missing_in_xml = jpgs - xmls missing_in_txt = jpgs - txts extra_xml = xmls - jpgs extra_txt = txts - jpgs print(f"JPG 总数: {len(jpgs)}") print(f"XML 总数: {len(xmls)} | 缺失 XML 的 JPG: {missing_in_xml}") print(f"TXT 总数: {len(txts)} | 缺失 TXT 的 JPG: {missing_in_txt}") print(f"多余 XML(无对应 JPG): {extra_xml}") print(f"多余 TXT(无对应 JPG): {extra_txt}") # 若三者完全一致,则输出 "✅ 三件套完整" if jpgs == xmls == txts: print("✅ 三件套完整,可进入下一步") else: print("⚠️ 存在不匹配文件,请检查并清理")逻辑说明与参数说明:
glob.glob("*.jpg")安全匹配所有 JPG,不依赖大小写(Windows 下常见*.JPG);os.path.splitext(f)[0]精确剥离扩展名,避免abc.jpg.xml被误判为abc;- 输出中
missing_in_xml是最危险项——意味着这张图无法参与训练(YOLO 训练器会跳过无标签图,但 VOC 工具链可能报错); - 若
extra_xml非空,说明存在已删除 JPG 但残留 XML,这类文件必须手动删除,否则后续转换 VOC→YOLO 时会因找不到图像而崩溃。
2.3 VOC XML 结构深度解析:为什么<bndbox>里的xmin不能等于xmax
打开任意一个 XML(如ESE_fire_1498.xml),你会看到标准 Pascal VOC 结构:
<annotation> <folder>unknown</folder> <filename>ESE_fire_1498.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>fire</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>842</xmin> <ymin>415</ymin> <xmax>1023</xmax> <ymax>587</ymax> </bndbox> </object> <object> <name>smoke</name> <bndbox> <xmin>1201</xmin> <ymin>203</ymin> <xmax>1567</xmax> <ymax>442</ymax> </bndbox> </object> </annotation>关键约束必须人工核验:
<width>和<height>必须与对应 JPG 的实际尺寸一致(可用PIL.Image.open("xxx.jpg").size验证);- 每个
<bndbox>中xmin < xmax且ymin < ymax,否则 OpenCV 画框会失败或 PyTorch Dataloader 报ValueError: invalid bboxes; xmin >= 0,ymin >= 0,xmax <= width,ymax <= height—— 超出边界的框在 YOLO 转换时会被 clip,但 VOC 训练器(如 Faster R-CNN)可能直接丢弃该样本;<name>严格为fire或smoke,无空格、无大小写混用(如Fire)、无拼写错误(如flame)。
我曾在一个类似数据集中发现 17 个 XML 的<name>是fire(末尾带空格),导致labelImg导出 YOLO 时生成classes.txt含fire和fire两个条目,模型训练时把同一类当成两类,mAP 归零。血泪经验:用正则grep -r '<name>[^<]*</name>' *.xml | grep -v 'fire\|smoke'快速扫一遍。
2.4 YOLO TXT 格式合规性检查:坐标是归一化后的 float,不是 int
YOLO 格式要求每个.txt文件与 JPG 同名,每行代表一个目标,格式为:class_id center_x center_y width height
其中center_x,center_y,width,height均为相对于图像宽高的归一化 float(0~1 区间)。
用head -n 1 ESE_fire_1498.txt查看首行:
0 0.4723958333333333 0.49259259259259257 0.09427083333333333 0.15925925925925924验证逻辑:
- 第一列
0对应fire(classes.txt中fire在第 0 行,smoke在第 1 行); - 后四列必须满足:
0 <= x,y,w,h <= 1,且w > 0,h > 0; - 若出现
x=0.0且w=0.0,说明该框被误标为点而非矩形,需剔除; - 若
x + w/2 > 1或y + h/2 > 1,说明中心点溢出,YOLO 训练器会自动 clamp,但影响 anchor 匹配。
写一段校验代码:
def validate_yolo_txt(txt_path, img_w, img_h): with open(txt_path, 'r') as f: lines = f.readlines() for i, line in enumerate(lines): parts = line.strip().split() if len(parts) != 5: print(f"❌ {txt_path}:{i+1} 行字段数不为5") return False try: cls, cx, cy, w, h = map(float, parts) except ValueError: print(f"❌ {txt_path}:{i+1} 行含非数字") return False if not (0 <= cls <= 1 and 0 <= cx <= 1 and 0 <= cy <= 1 and 0 < w <= 1 and 0 < h <= 1): print(f"❌ {txt_path}:{i+1} 坐标越界: cls={cls}, cx={cx}, cy={cy}, w={w}, h={h}") return False if cx - w/2 < 0 or cx + w/2 > 1 or cy - h/2 < 0 or cy + h/2 > 1: print(f"⚠️ {txt_path}:{i+1} 框部分超出图像边界(YOLO 会 clamp,但建议检查)") return True # 示例:验证前5个txt for txt in glob.glob("*.txt")[:5]: img_jpg = txt.replace(".txt", ".jpg") if os.path.exists(img_jpg): w, h = PIL.Image.open(img_jpg).size validate_yolo_txt(txt, w, h)3. VOC 与 YOLO 双格式互转实操:为什么你不需要第三方工具,手写 30 行就够了
3.1 VOC → YOLO 转换:当labelImg导出失效时的保底方案
虽然数据集已提供 YOLO TXT,但你很可能遇到:
- 某些 XML 的
<name>被误标为smokee; - 需要将
fire类单独抽出来做二分类 fine-tune; - 或想验证现有 TXT 是否真由这些 XML 生成。
此时,自己写转换脚本比调试xml2yolo工具更可控。核心逻辑三步:
- 解析 XML 获取
<size>和所有<object>; - 将每个
<bndbox>的xmin/ymin/xmax/ymax归一化为 YOLO 格式; - 按
fire→0,smoke→1映射 class_id,写入同名.txt。
import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, classes=["fire", "smoke"]): tree = ET.parse(xml_path) root = tree.getroot() # 获取图像尺寸 size = root.find('size') img_w = int(size.find('width').text) img_h = int(size.find('height').text) # 构建YOLO行列表 yolo_lines = [] for obj in root.findall('object'): name = obj.find('name').text.strip() if name not in classes: continue # 跳过非法类别 cls_id = classes.index(name) 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) # 归一化:cx, cy, w, h cx = (xmin + xmax) / 2.0 / img_w cy = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h yolo_lines.append(f"{cls_id} {cx:.16f} {cy:.16f} {w:.16f} {h:.16f}") # 写入txt txt_path = Path(xml_path).with_suffix('.txt') with open(txt_path, 'w') as f: f.write('\n'.join(yolo_lines)) return len(yolo_lines) # 批量转换 for xml in Path(".").glob("*.xml"): n_boxes = voc_to_yolo(str(xml)) print(f"✅ {xml.name} → {n_boxes} 个框")参数说明:
classes=["fire", "smoke"]必须与数据集声明严格一致,顺序决定 class_id;:.16f保留 16 位小数,避免浮点误差导致cx+w/2>1;- 若某 XML 无
<object>,则生成空.txt,YOLO 训练器会将其视为负样本(无目标图),符合常规做法。
3.2 YOLO → VOC 转换:用于在labelImg中重新编辑或可视化
当你需要在 labelImg 中打开 YOLO 标签(比如修正漏标烟雾),但 labelImg 默认不支持 TXT,就必须转回 XML。此脚本生成最小可行 XML,不含<pose><truncated>等非必需字段:
def yolo_to_voc(txt_path, img_path, classes=["fire", "smoke"]): from PIL import Image img = Image.open(img_path) img_w, img_h = img.size with open(txt_path, 'r') as f: lines = f.readlines() # 构建XML根节点 annotation = ET.Element("annotation") ET.SubElement(annotation, "folder").text = "unknown" ET.SubElement(annotation, "filename").text = Path(img_path).name size = ET.SubElement(annotation, "size") ET.SubElement(size, "width").text = str(img_w) ET.SubElement(size, "height").text = str(img_h) ET.SubElement(size, "depth").text = "3" for line in lines: parts = line.strip().split() if len(parts) != 5: continue cls_id, cx, cy, w, h = map(float, parts) cls_name = classes[int(cls_id)] # 反归一化为像素坐标 xmin = max(0, int((cx - w/2) * img_w)) ymin = max(0, int((cy - h/2) * img_h)) xmax = min(img_w, int((cx + w/2) * img_w)) ymax = min(img_h, int((cy + h/2) * img_h)) obj = ET.SubElement(annotation, "object") ET.SubElement(obj, "name").text = cls_name bndbox = ET.SubElement(obj, "bndbox") ET.SubElement(bndbox, "xmin").text = str(xmin) ET.SubElement(bndbox, "ymin").text = str(ymin) ET.SubElement(bndbox, "xmax").text = str(xmax) ET.SubElement(bndbox, "ymax").text = str(ymax) # 写入XML xml_path = Path(txt_path).with_suffix('.xml') tree = ET.ElementTree(annotation) tree.write(xml_path, encoding='utf-8', xml_declaration=True) return len(lines) # 示例:转换一个txt yolo_to_voc("ESE_fire_1498.txt", "ESE_fire_1498.jpg")注意:反归一化后必须max(0, ...)和min(img_w, ...)截断,否则xmin可能为负数,导致labelImg加载失败。
3.3 双格式一致性验证:用 IOU 交叉验证,揪出“看似一样实则不同”的坑
即使 XML 和 TXT 都语法正确,也可能存在语义偏差:比如 XML 中fire框被误标为smoke,但 TXT 里仍写0。最可靠的方式是——用同一套坐标,在两种格式下分别加载,计算每个框的 IOU,理论上应为 1.0。
def calculate_iou(box1, box2): # box = (xmin, ymin, xmax, ymax) inter_xmin = max(box1[0], box2[0]) inter_ymin = max(box1[1], box2[1]) inter_xmax = min(box1[2], box2[2]) inter_ymax = min(box1[3], box2[3]) if inter_xmax <= inter_xmin or inter_ymax <= inter_ymin: return 0.0 inter_area = (inter_xmax - inter_xmin) * (inter_ymax - inter_ymin) area1 = (box1[2] - box1[0]) * (box1[3] - box1[1]) area2 = (box2[2] - box2[0]) * (box2[3] - box2[1]) return inter_area / (area1 + area2 - inter_area) def validate_consistency(xml_path, txt_path, img_path): from PIL import Image img = Image.open(img_path) w, h = img.size # 从XML读取 tree = ET.parse(xml_path) xml_boxes = [] for obj in tree.getroot().findall('object'): name = obj.find('name').text.strip() if name not in ["fire", "smoke"]: 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) xml_boxes.append((xmin, ymin, xmax, ymax)) # 从TXT读取(反归一化) txt_boxes = [] with open(txt_path, 'r') as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue _, cx, cy, bw, bh = map(float, parts) xmin = max(0, int((cx - bw/2) * w)) ymin = max(0, int((cy - bh/2) * h)) xmax = min(w, int((cx + bw/2) * w)) ymax = min(h, int((cy + bh/2) * h)) txt_boxes.append((xmin, ymin, xmax, ymax)) # 两两匹配计算IOU(简单起见,按顺序一一对应) if len(xml_boxes) != len(txt_boxes): print(f"❌ {xml_path.name}: XML框数({len(xml_boxes)}) ≠ TXT框数({len(txt_boxes)})") return False for i, (xml_box, txt_box) in enumerate(zip(xml_boxes, txt_boxes)): iou = calculate_iou(xml_box, txt_box) if iou < 0.99: # 允许浮点误差 print(f"⚠️ {xml_path.name} 第{i+1}框 IOU={iou:.4f} < 0.99,坐标不一致") print(f" XML: {xml_box} | TXT: {txt_box}") return False print(f"✅ {xml_path.name}: 双格式坐标一致 (IOU≥0.99)") return True # 验证前3个样本 for i, xml in enumerate(Path(".").glob("*.xml")): if i >= 3: break jpg = xml.with_suffix('.jpg') txt = xml.with_suffix('.txt') if jpg.exists() and txt.exists(): validate_consistency(str(xml), str(txt), str(jpg))为什么 IOU < 0.99 就报警?
因为归一化/反归一化过程涉及浮点运算,int(round(...))和int(...)截断方式不同会导致 1~2 像素偏移。但若 IOU 低于 0.99,大概率是类别映射错误或坐标计算逻辑 bug,必须人工介入。
4. 训练前必做的数据清洗与增强策略:别让“3000张”变成“300张有效样本”
4.1 小目标与遮挡问题诊断:烟雾框为何普遍比明火框小 3 倍?
从摘要可知:fire框 5198 个,smoke框仅 1651 个,但smoke的平均面积(像素)仅为fire的 1/3 左右。这不是标注疏忽,而是物理现实——远处烟雾在图像中呈现为低对比度、边缘模糊的灰白色区域,标注时往往只能框出其“核心区”,而明火因高亮、闪烁、颜色饱和,易被精准框定。
量化分析脚本:
import numpy as np from collections import defaultdict def analyze_box_sizes(xml_dir="."): sizes = defaultdict(list) for xml in Path(xml_dir).glob("*.xml"): tree = ET.parse(xml) for obj in tree.getroot().findall('object'): name = obj.find('name').text.strip() if name not in ["fire", "smoke"]: 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) area = (xmax - xmin) * (ymax - ymin) sizes[name].append(area) for cls, areas in sizes.items(): arr = np.array(areas) print(f"{cls}: count={len(areas)}, " f"mean={arr.mean():.0f}px², " f"min={arr.min():.0f}, " f"max={arr.max():.0f}, " f"std={arr.std():.0f}") analyze_box_sizes()典型输出:
fire: count=5198, mean=12450px², min=89, max=142800, std=18920 smoke: count=1651, mean=3980px², min=42, max=87600, std=8210结论与对策:
smoke平均面积仅fire的 32%,属于典型小目标;fire最小框仅 89px²(约 9×10 像素),已接近 YOLOv5 默认 stride=32 的感受野下限;- 必须启用 Mosaic + Copy-Paste Augmentation:Mosaic 将 4 张图拼成 1 张,天然放大小目标;Copy-Paste 则把烟雾框复制粘贴到新背景,增加其出现频次。
提示:YOLOv8 默认关闭 Mosaic,需在
data.yaml中显式设置mosaic: 1.0;Copy-Paste 需自定义copy_paste.py,核心是cv2.copyMakeBorder+cv2.seamlessClone,此处不展开,但务必做。
4.2 低光照与运动模糊样本筛查:用 OpenCV 的拉普拉斯方差判断清晰度
火灾场景常出现:
- 夜间红外图像(低对比度);
- 监控摄像头拍摄的快速移动火焰(运动模糊);
- 烟雾弥漫导致整体雾化(细节丢失)。
这些样本若强行参与训练,会拖累模型对清晰目标的判别力。我们用拉普拉斯方差(Laplacian Variance)量化图像清晰度:
import cv2 def estimate_blur(image_path, threshold=100): """返回清晰度得分,值越低越模糊""" image = cv2.imread(image_path) if image is None: return 0 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) fm = cv2.Laplacian(gray, cv2.CV_64F).var() return fm blur_scores = [] for jpg in Path(".").glob("*.jpg"): score = estimate_blur(str(jpg)) blur_scores.append((str(jpg), score)) # 按清晰度排序,取最模糊的10张 blur_scores.sort(key=lambda x: x[1]) print("最模糊的10张图(Laplacian方差):") for path, score in blur_scores[:10]: print(f"{path}: {score:.1f}") # 建议阈值:score < 50 的图,人工复核是否保留经验值:
- 清晰监控图:
score > 200; - 轻微模糊:
100 < score < 200(可保留,但加Blur(p=0.3)增强); - 严重模糊:
score < 50(建议剔除,或单独做去模糊预处理)。
我曾在一个项目中剔除 87 张score < 40的图,mAP@0.5 提升 2.3%,证明“少而精”优于“多而杂”。
4.3 类别不平衡缓解:SMOTE 不适用,但 Class Weight + Focal Loss 是正解
fire:smoke= 5198 : 1651 ≈ 3.15 : 1,属中度不平衡。但注意:这不是样本数不平衡,而是框数不平衡。一张图可能含 5 个fire框但仅 1 个smoke框,因此不能简单地对图像重采样(SMOTE 对目标检测无效)。
正确做法:
- YOLOv5/YOLOv8:在
models/yolov5s.yaml中修改nc: 2后,添加class_weights: [1.0, 3.15](smoke权重更高); - 通用方案:启用 Focal Loss 替代 CE Loss,降低易分样本(大量
fire)的梯度权重,聚焦难分样本(稀疏smoke)。YOLOv8 默认开启,无需修改; - 数据层面:对
smoke框做RandomAffine(scale=(0.8,1.2), translate=0.1, shear=5)增强,增加其形态多样性。
注意:不要用
oversample复制smoke图像!这会导致同一烟雾在不同 epoch 重复出现,模型过拟合局部纹理,泛化变差。
5. 避坑指南:那些让你训到凌晨三点却只得到 0.0 mAP 的真实翻车现场
5.1 现象:训练时box_loss迅速收敛至 0.01,但cls_loss卡在 0.8 不动
原因:classes.txt中类别顺序与 XML/TXT 中的name不一致。例如classes.txt写的是:
smoke fire但 XML 中<name>是fire,导致模型把fire当作 class_id=1 训练,而smoke反成 class_id=0,类别 ID 错位。YOLO 训练器不会报错,但cls_loss无法下降。
解决:严格确保classes.txt与数据集摘要声明一致:第一行fire,第二行smoke。用head -n 2 classes.txt确认,并在训练配置中显式指定names: ["fire", "smoke"]。
5.2 现象:验证时precision很高(>0.9),但recall低于 0.2
原因:NMS(非极大值抑制)阈值过高,或置信度阈值conf设为 0.7。大量真实smoke框因得分略低于阈值被过滤。
解决:在val.py中临时降低conf_thres=0.001和iou_thres=0.3,观察 recall 是否跃升。若 yes,则说明模型已学到特征,只是后处理太激进;调整--conf 0.25 --iou 0.45重新评估。
5.3 现象:训练 loss 正常下降,但用detect.py推理时,所有图都只标出fire,smoke永远不出现
原因:smoke框普遍较小(平均 3980px²),而 YOLO 默认 anchor 匹配基于 k-means 聚类,若聚类时未加权(即未按框面积加权),小框易被大框 anchor “吞并”。
解决:重新运行utils/autoanchor.py,传入--weights参数指向你的数据集路径,并设置--giou或--kmeans选项,强制按面积加权聚类。或直接改models/yolov5s.yaml中的 anchors,将最小 anchor(如10,13)替换为6,9(适配 42×42 像素的 smoke 框)。
5.4 现象:labelImg打开 XML 时提示 “XML syntax error”,但用浏览器能正常显示
原因:XML 文件以 UTF-8 with BOM 编码保存,而labelImg读取时未 strip BOM。Windows 上用记事本另存为 “UTF-8 无 BOM” 即可。
解决:批量去除 BOM:
for f in *.xml; do sed -i '1s/^\xEF\xBB\xBF//' "$f"; done # Linux/macOS # Windows PowerShell: Get-Content file.xml -Encoding UTF8 | Set-Content file.xml -Encoding UTF85.5 现象:YOLO 训练报错AssertionError: Error loading data,定位到某张 JPG
原因:该 JPG 实际是损坏的 JPEG(如传输中断),cv2.imread返回None,但训练器未做空值检查。
解决:运行此脚本批量筛查:
import cv2 for jpg in Path(".").glob("*.jpg"): img = cv2.imread(str(jpg)) if img is None: print(f"❌ {jpg.name} 读取失败,疑似损坏") # 可选:尝试用 PIL 修复 try: from PIL import Image Image.open(jpg).convert('RGB').save(jpg) print(f"✅ {jpg.name} 已用PIL重存") except: print(f"⚠️ {jpg.name} 无法修复,建议删除")6. 模型验证与工业部署前的终极 checklist:从“能跑通”到“敢上线”的最后一道关
6.1 用 Grad-CAM 可视化,确认模型真在看“烟雾”而非“背景纹理”
训练完模型后,别急着导出 onnx。先用 Grad-CAM 看它到底关注什么:
from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image import torch # 加载你的训练好的模型(假设为 yolov5s.pt) model = torch.hub.load('ultralytics/yolov5', 'custom', path='runs/train/exp/weights/best.pt') target_layers = [model.model.model[-2].conv] # YOLOv5 的最后卷积层 cam = GradCAM(model=model, target_layers=target_layers, use_cuda=torch.cuda.is_available()) rgb_img = cv2.imread("ESE_fire_2131.jpg")[:, :, ::-1] # BGR→RGB rgb_img = np.float32(rgb_img) / 255 input_tensor = torch.from_numpy(rgb_img).permute(2,0,1).unsqueeze(0) grayscale_cam = cam(input_tensor=input_tensor) cam_image = show_cam_on_image(rgb_img, grayscale_cam[0, :], use_rgb=True) cv2.imwrite("gradcam_smoke.jpg", cam_image[:, :, ::-1])关键判断:
- 若
smoke框对应的热力图是一片均匀灰色(无显著激活),说明模型根本没学会烟雾特征,可能因smoke样本太少或增强不足; - 若热力图集中在
smoke框内,但同时在天空/墙壁等无关区域也有强响应,说明存在背景偏置(background bias),需增加 `RandomGrayscale(p=0.3
本文还有配套的精品资源,点击获取