
简介该数据集面向火灾预警、安防监控等场景中的图像分类任务包含火焰、烟雾、正常三个类别共约240张已标注图片并划分好训练集与测试集适合初学者练习CNN或YOLO分类模型时直接使用。压缩包共243个文件主体为240张jpg图像另附1个py脚本、1个png示意图和1个json标注文件py脚本可用于可视化数据集json文件记录类别划分信息整体体积仅504KB轻量易下载。目前已有112人学习浏览。通过这份数据读者可以快速验证图像分类流程无需自己爬图与清洗数据配合博主提供的一系列CNN分类与YOLOv5分类项目博客还能进一步掌握火焰、烟雾识别模型搭建、训练和评估的具体思路是开展分类实验或课程设计的实用小样本资源。1. 火焰、烟雾、正常三分类240张已标注图片打开的图像识别实战做图像识别的都知道模型效果的上限往往在数据手里而不是在代码里。这个火焰、烟雾、正常三分类数据集一共约240张已标注图片按类别文件夹组织训练集和测试集已经划分好JSON文件里放着每一类的对应关系。对于想做火焰烟雾检测、安防监控场景分类或者刚接触图像分类想跑通完整流程的人来说这是一个可以直接上手的数据集不需要自己清洗标注打开就能训练。数据集本身不大但结构完整。三个类别——火焰、烟雾、正常——各自独立存放数据集还带一个show脚本用来可视化跑一下就能看到图片和标签是否对得上。适合的对象很明确正在学CNN分类的、想试试YOLOv5分类分支的、或者需要一份小规模基准数据来验证网络改动的从业者。说白了这份资源解决的是「有数据可练、有标准可对照」的问题配套的CSDN专栏从CNN分类到YOLOv5分类都有对应代码。下面我把数据集的底层结构、标注JSON怎么读、两条训练路线怎么选以及实际跑的时候会踩的坑一条条拆开讲。2. 拆解数据集底层结构目录划分、JSON标注与show脚本可视化开始训练之前先花十分钟把数据集的家底摸清楚。很多人在小数据集上翻车不是模型不对而是压根没搞明白数据是怎么组织的。这个数据集的目录结构其实很典型和ImageNet的按类分文件夹方式同构搞清楚它后面接PyTorch的ImageFolder或者YOLOv5都能少走弯路。2.1 train/test目录与类别文件夹的对应关系这个数据集的核心划分逻辑是先把全部约240张图片按三个类别——火焰fire、烟雾smoke、正常normal——分开然后在每个类别内部再划分训练集和测试集。文件命名上火焰图片以fire开头加编号比如fire69.jpg、fire43.jpg烟雾图片以smoke开头比如smoke01.jpg正常类别图片从摘要描述看也应有独立文件只是文件清单里没有完整展示。从文件清单里能看到的规律是fire69.jpg、fire68.jpg、fire59.jpg、fire03.jpg、fire49.jpg、fire43.jpg、fire45.jpg、fire04.jpg、fire74.jpg这批都属于火焰类smoke01.jpg属于烟雾类。但这里要提醒一句不要只靠文件名判断类别最终务必以JSON标注为准。文件名可以被误改JSON标注才是工程上的可靠依据。拿到数据集第一件事先跑一个统计脚本把每个文件夹里的图片数量、尺寸分布、格式列出来。我一般会写这样一段import os from collections import Counter from PIL import Image root fire_smoke_normal_dataset # 改成你解压后的实际目录名 counter Counter() size_map {} for split in [train, test]: split_path os.path.join(root, split) if not os.path.exists(split_path): continue for cls in os.listdir(split_path): cls_path os.path.join(split_path, cls) if not os.path.isdir(cls_path): continue for fname in os.listdir(cls_path): if not fname.lower().endswith((.jpg, .jpeg, .png)): continue counter[f{split}/{cls}] 1 with Image.open(os.path.join(cls_path, fname)) as img: w, h img.size size_map[(w, h)] size_map.get((w, h), 0) 1 print(各类别图片数量) for k, v in sorted(counter.items()): print(f {k}: {v}) print(\n图片尺寸分布Top5) for k, v in sorted(size_map.items(), keylambda x: -x[1])[:5]: print(f {k}: {v}张)这段脚本做的事情很简单遍历train和test两个目录按“split/类别”组合统计图片数量同时用PIL读取每张图片的尺寸统计最常见尺寸分布。为什么要看尺寸分布因为CNN输入尺寸通常要固定如果数据集里混着1920×1080和224×224两种极端尺寸预处理策略完全不一样。参数说明root变量指向数据集解压后的根目录目录名不同就改这一个变量。split列表控制遍历哪几个划分有的数据集还有val直接加进列表即可。尺寸统计里我取出现次数最高的5个尺寸如果Top5占比很低说明图片尺寸很杂后面训练时Resize策略要保守最好先统一缩放到短边256再中心裁剪。提示文件清单里只展示了jpg结尾的图片如果解压后发现还有png或bmp同样会被统计脚本捕捉到。之前我处理一份标注240张的数据集实际解压后多出5张隐藏缩略图直接把训练集弄脏了。拿到数据先跑统计永远是对的。2.2 标注JSON怎么读类别映射与文件名对照摘要描述里明确说了「分类个数3火焰、烟雾、正常具体查看json文件」说明类别不是靠文件夹名约定而是有一份正式的JSON标注文件做映射。典型的标注格式大概是这样的{ num_classes: 3, classes: [fire, smoke, normal], train: { fire: [fire69.jpg, fire68.jpg, fire59.jpg], smoke: [smoke01.jpg], normal: [] }, test: { fire: [fire43.jpg, fire45.jpg], smoke: [], normal: [] } }读这个JSON要做三件事第一确认classes数组的元素顺序这直接决定模型输出的类别索引索引0对应fire、索引1对应smoke、索引2对应normal第二核对train和test下每个类别的文件列表是否与目录实际内容一致第三检查有没有图片同时出现在train和test里这种情况叫数据泄漏在只有240张样本的小数据集上危害被放大好几倍。实际项目中我一般会写一个校验脚本把JSON里的文件名与目录里的实际文件做差集对比。它能拦截三类问题JSON里写了但目录里没有的文件数据缺失、目录里有但JSON没写的文件标注遗漏、两边都有但文件名大小写不一致的Windows和Linux跨平台导致的错乱。从使用角度看JSON标注的价值在于把类别与文件名的对应关系固定下来。哪怕你把fire文件夹里的图片挪到别处只要文件名不变JSON就能还原出真实标注。这也是为什么我不建议只依赖文件夹名判断类别——文件夹可以被误改JSON标注在工程上更可信。2.3 show脚本可视化30秒确认样本与标注是否对上资源里附带的show脚本作用就是可视化数据集。具体来说它从每个类别抽出几张图片展示出来同时把对应类别标签打在图上。这个脚本的价值在于把「标注对不对」从抽象的数字校验变成肉眼可见的直观判断。火焰类图片应该是明显的明火形态烟雾类是弥散的烟团或者烟柱正常类就是没有火焰也没有烟雾的普通场景。如果show脚本跑出来发现某个类别的图片内容明显不对——比如normal类别里出现一张大火照片——那说明标注有问题这个数据集要谨慎处理。我自己写可视化脚本时习惯用matplotlib因为输出美观且能直接保存为png归档。资源里如果没有现成的show脚本或者你想自己控制展示逻辑可以参考这个版本import matplotlib.pyplot as plt import os from PIL import Image root fire_smoke_normal_dataset/train class_names [fire, smoke, normal] fig, axes plt.subplots(3, 3, figsize(9, 9)) for i, cls in enumerate(class_names): cls_dir os.path.join(root, cls) files [f for f in os.listdir(cls_dir) if f.lower().endswith(.jpg)] for j in range(3): # 每类展示前3张 if j len(files): axes[i][j].axis(off) continue img_path os.path.join(cls_dir, files[j]) axes[i][j].imshow(Image.open(img_path)) axes[i][j].set_title(f{cls}: {files[j]}) axes[i][j].axis(off) plt.tight_layout() plt.show()这段代码从train目录下每个类别取前3张图片展示标题显示类别名和文件名。参数上两个注意点一是root路径要和你的实际目录结构匹配二是如果某个类别目录里图片不足3张要用长度判断兜底否则直接索引越界报错——这个坑我踩过不止一次。可视化这步很多人跳过但我强烈建议不要省。火焰烟雾这类数据集图片质量参差是常态有的烟雾图片光线很暗有的火焰图片其实是火光反射。肉眼扫一遍你就能大致判断数据集的难度和可用性也能提前预判模型训练后应该达到什么水平。3. CNN分类实战用PyTorch把240张图片训成可用的三分类器数据集摸清了接下来就是训练。这一章以CNN分类路线为主线把数据加载、模型构建、训练参数逐段讲清楚。为什么先讲CNN而不是直接上YOLOv5分类因为对于240张图片的小规模数据集CNN分类是性价比最高的起点——训练快、调试直观、出问题容易定位。3.1 小数据集为什么首选CNN分类而不是大模型240张图片三个类别平均每个类别约80张。这个规模放到今天的深度学习环境里属于典型的小样本分类问题。大型预训练模型比如ViT、EfficientNet确实能用但参数规模动辄几千万在没有充足数据微调的情况下很容易过拟合——训练集上准确率99%测试集上掉到60%。CNN的优势在于先验结构强卷积的局部感受野和权值共享特性让它在小数据集上也能学到有意义的特征。同时训练成本低一张普通GPU甚至CPU都能在几分钟内完成训练迭代试错效率高。对于新手CNN的调试链路也更短——loss曲线、准确率曲线、预测错例每一步都可解释。这里要区分一个概念CNN分类和YOLOv5分类分支在模型结构上都有卷积骨干但训练目标和数据组织方式不同。CNN分类是标准的图像分类——输入一张图输出一个类别概率分布YOLOv5分类分支则继承了目标检测框架的数据处理逻辑目录命名有自身约定。对于纯分类需求我用CNN如果后续要扩展成检测任务再考虑YOLO系列。两条路线不冲突但要明确各自适用场景。用这份数据集能做和不能做的事心里要有数任务类型是否适用说明火焰/烟雾/正常三分类适用CNN分类或YOLOv5分类分支均可火焰/烟雾目标检测不适用需要框级标注本数据集没有火焰/烟雾像素分割不适用需要逐像素标注本数据集不具备所以用这份数据集做分类是最合理、最完整的路径。3.2 数据加载与预处理从目录到DataLoaderPyTorch加载这种按文件夹组织的分类数据集最直接的方式是torchvision.datasets.ImageFolder。它要求目录结构是 root/类别名/图片文件刚好与这份数据集的train/test目录结构吻合。import torch import torchvision import torchvision.transforms as transforms IMG_SIZE 224 MEAN [0.485, 0.456, 0.406] STD [0.229, 0.224, 0.225] # 训练集增强尺寸对齐 随机翻转 颜色抖动缓解小数据集过拟合 train_transform transforms.Compose([ transforms.Resize((IMG_SIZE, IMG_SIZE)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(MEAN, STD) ]) # 测试集只做尺寸对齐和归一化不做增强 test_transform transforms.Compose([ transforms.Resize((IMG_SIZE, IMG_SIZE)), transforms.ToTensor(), transforms.Normalize(MEAN, STD) ]) train_dataset torchvision.datasets.ImageFolder( rootfire_smoke_normal_dataset/train, transformtrain_transform ) test_dataset torchvision.datasets.ImageFolder( rootfire_smoke_normal_dataset/test, transformtest_transform ) train_loader torch.utils.data.DataLoader( train_dataset, batch_size16, shuffleTrue, num_workers2 ) test_loader torch.utils.data.DataLoader( test_dataset, batch_size16, shuffleFalse, num_workers2 ) print(类别映射:, train_dataset.class_to_idx) print(训练集样本数:, len(train_dataset)) print(测试集样本数:, len(test_dataset))代码逻辑分三段第一段定义transforms训练集增强包含随机水平翻转和颜色抖动目的就是让模型在80张每类的小样本上看到更多变化测试集只有Resize和归一化不加增强是为了评估结果稳定可复现。第二段用ImageFolder加载数据它会自动读取子目录名作为类别标签。第三段创建DataLoader设置batch大小、是否打乱和并行加载线程数。参数说明IMG_SIZE设为224兼容ImageNet预训练模型的标准输入尺寸MEAN和STD是ImageNet的归一化值如果用随机初始化的CNN可以替换成数据集自算的均值和标准差batch_size在小数据集上16或32均可太大梯度更新次数太少太小训练不稳定num_workers在Windows上建议设0避免多进程报错。有一个细节要特别留意ImageFolder的类别排序按照文件夹名字母顺序不是文件夹创建顺序。如果测试集目录里只放了fire和smokeclass_to_idx就只有两个类别与训练集不一致后面模型评估必然出错。所以训练前的第一行输出就要检查类别映射。3.3 CNN训练配置ResNet18迁移学习与超参数选择对于小数据集我的默认选择是ResNet18预训练权重加迁移学习。ResNet18参数适中残差结构稳定在图像分类任务上是通用性很强的baseline。有两种做法一是微调全部参数二是冻结骨干只训分类头。240张图片这种规模我一般先冻结骨干用分类头快速验证数据可用性效果不理想再解冻微调。import torch.nn as nn import torch.optim as optim from torchvision import models # 加载ResNet18预训练权重冻结骨干 model models.resnet18(pretrainedTrue) for param in model.parameters(): param.requires_grad False # 替换最后全连接层为3类输出 num_ftrs model.fc.in_features model.fc nn.Linear(num_ftrs, 3) model.fc.requires_grad_(True) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.fc.parameters(), lr1e-3) epochs 30 best_acc 0.0 for epoch in range(epochs): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) # 每轮结束在测试集上评估 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in test_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() epoch_acc correct / total epoch_loss running_loss / len(train_dataset) print(fEpoch {epoch1}/{epochs} | Loss: {epoch_loss:.4f} | Acc: {epoch_acc:.4f}) if epoch_acc best_acc: best_acc epoch_acc torch.save(model.state_dict(), best_model.pth)训练代码的结构很清晰加载预训练权重冻结所有参数只把最后的全连接层替换成输出3类的线性层并且只对这一层设置requires_grad。损失函数用交叉熵优化器用Adam配1e-3学习率训练30轮每轮结束后在测试集上评估保存最高准确率的模型。几个参数的调整逻辑冻结骨干时学习率可以设大一点1e-3因为只有一层在更新不会震荡如果解冻全部参数微调学习率要降到1e-4或更低否则预训练特征会被破坏。epochs在30轮左右比较稳妥小数据集收敛很快10轮以后准确率基本稳定30轮足够如果30轮还没收敛说明预处理或数据有问题先别急着加轮数。保存模型时我用state_dict()而不是整个模型对象前者是纯参数字典体积小、跨环境加载方便后者容易遇到PyTorch版本不兼容。加载时注意要先按同样结构实例化模型再调load_state_dict。4. YOLOv5分类分支数据改造、训练命令与推理验证如果你不是单纯做分类而是想要一个后续能平滑过渡到目标检测的方案YOLOv5的分类分支值得试。YOLOv5从v5.0开始支持分类训练数据组织方式和ImageFolder类似但训练命令和预处理逻辑与CNN路线有明显差异。这一章把YOLOv5路线的完整流程和参数讲透。4.1 YOLOv5分类与CNN分类的差异训练入口、数据组织与模型结构同样是分类任务YOLOv5分类和上一章的CNN分类有三个层面的区别。第一训练入口不同CNN路线是自己写PyTorch训练循环YOLOv5用官方仓库的classify/train.py脚本命令行为主第二数据组织细节不同虽然都是按类别分文件夹但YOLOv5分类对目录命名有约定通常要求train和val两个目录类别文件夹放在各自目录下第三模型结构不同YOLOv5分类用的是它的backbone加分类头输入尺寸默认224但可以配置成256或320。选择YOLOv5路线的理由通常是你已经在用YOLO系列做检测不想再维护一套CNN分类代码或者后续计划把火焰烟雾分类任务升级成检测任务提前统一技术栈。如果你已经在用YOLOv8数据组织思路完全一致只是命令参数做了简化懂v5再看v8会很快。数据集组织方面YOLOv5分类要求目录结构为 train/fire、train/smoke、train/normal、val/fire 这样的形式。这份数据集已经划分了训练集和测试集但目录名可能是test而不是val需要做一步对齐# 在数据集根目录下执行 # 先创建YOLOv5分类的标准目录 mkdir -p yolov5_classify/val cp -r fire_smoke_normal_dataset/test/* yolov5_classify/val/ mkdir -p yolov5_classify/train cp -r fire_smoke_normal_dataset/train/* yolov5_classify/train/这段命令把原始数据集的test目录复制为YOLOv5分类要求的val目录train保持原名。注意用的是cp不是mv保留原始数据集不动避免操作失误把原数据搞坏。完成后yolov5_classify目录就是YOLOv5分类的标准输入。参数说明val的命名在YOLOv5里是硬约定用test会识别不了类别文件夹名称会自动生成类别映射如果你想控制类别顺序可以在数据加载参数里显式传入class_names不过一般靠文件夹名自动生成就够了。4.2 YOLOv5分类训练命令关键参数逐项说明YOLOv5官方仓库的classify目录下有一个train.py训练命令大概是这样的python classify/train.py \ --model yolov5s-cls.pt \ --data yolov5_classify \ --epochs 50 \ --img 224 \ --batch 32 \ --lr 0.01 \ --project runs/classify \ --name fire_smoke_normal \ --pretrained逐项说明--model指定模型架构yolov5s-cls是分类版的小型模型参数量约710万比ResNet18还轻量--data指向整理好的数据集根目录YOLOv5自动识别train和val子目录--img输入图片尺寸224是默认值原始图片分辨率高可以试256--batch在小数据集上32没问题显存不够降到16--lr 0.01是YOLOv5分类训练的默认学习率用SGD优化器这和上一章Adam的1e-3不同因为YOLOv5内部有学习率调度配合直接换优化器反而可能不收敛。--pretrained表示加载ImageNet预训练权重不加的话模型从头训练对小数据集效果很差不建议。训练结束后结果输出到runs/classify/fire_smoke_normal目录里面有训练曲线、混淆矩阵、验证集预测结果图。分类任务的混淆矩阵是看类别间混淆情况最直观的工具比如smoke被预测成fire多还是normal多一眼定位问题。4.3 YOLOv5分类推理与验证predict.py使用与结果解读训练完的模型用classify/predict.py做推理验证命令如下python classify/predict.py \ --weights runs/classify/fire_smoke_normal/weights/best.pt \ --source fire_smoke_normal_dataset/test/smoke \ --img 224 \ --save-dir runs/classify/predict_smoke这个命令加载训练好的best.pt权重对测试集里smoke类别的图片做预测结果保存到指定目录。重点看两样东西一是输出的置信度分数二是预测出的类别标签。如果smoke图片被大量预测成fire说明两类特征在模型眼里太接近需要回头检查数据增强和训练策略。推理时跑单张图片把--source改成具体图片路径跑视频用mp4文件路径。YOLOv5分类推理还支持--augment参数做测试时增强通常能在验证集提升1-2个点准确率代价是推理时间变长线上部署一般不开。注意YOLOv5分类模型的输出类别顺序严格跟随训练时文件夹的字母排序。如果训练时文件夹顺序是fire、normal、smoke模型输出的索引0/1/2就对应这个顺序。推理时打印的预测索引要和训练时的class_names对应表放在一起看否则会误判结果。这个坑我在做类别映射时踩过训练时类别顺序没记录推理时全乱了。5. 火焰烟雾分类项目避坑指南五个实战中容易翻车的细节训练代码跑通只是开始真实项目里有一堆细节等着你。这一章把过去在这个数据集和相似小规模数据集上踩过的坑集中列出来每一条都按「现象→原因→解决」的结构写方便对照排查。5.1 训练集准确率接近100%测试集只有60%现象模型在训练集上表现完美loss趋近于零准确率接近100%。但一跑测试集准确率掉到60%甚至更低明显不对劲。原因这是典型的过拟合。240张图片的数据规模太小模型可学习参数多训练轮次一多就会把训练样本的细节特征记住而不是学到泛化模式。另外如果训练集和测试集的图片来自不同场景比如训练集全是户外火焰测试集有室内烟雾分布偏移会加剧这个问题。解决优先加数据增强随机翻转、颜色抖动、随机裁剪都用上其次换更小的模型或者用预训练权重冻结骨干只训分类头最后把训练轮次降下来配合早停验证集准确率不再提升就停止训练。5.2 烟雾和火焰类别互相误判现象测试集上的混淆矩阵显示smoke被预测成fire的比例很高或者反过来fire被预测成smoke。原因火焰和烟雾在视觉特征上有天然重叠——火焰边缘有烟雾烟雾在特定光照下颜色接近火焰。加上数据集只有240张每个类别样本量大约80张类内多样性不足模型很难学到区分性强的特征。解决一方面调整类别定义如果两类边界本来就模糊可以接受一定混淆率或者合并成「异常」类别另一方面增强数据增强特别是颜色抖动和对比度变化让模型看到更多颜色变化更实际的做法是补充数据把公开数据集中类似场景的火焰、烟雾图片迁移进训练集注意保持类别平衡不要让某一类样本数超过其他类的两倍。5.3 训练时报错ImageFolder类别数量不一致现象训练代码跑起来训练集加载正常但测试集评估时报错提示目标类别超出范围或者模型输出维度不是3。原因训练集和测试集目录下的类别文件夹数量和名称不一致。比如训练集有fire、smoke、normal三个文件夹测试集只有fire、smoke两个ImageFolder自动构建类别索引时就少了normal。这类错误在手工划分数据集时很常见。解决训练前先打印train_dataset.class_to_idx和test_dataset.class_to_idx对比两边是否一致。不一致就统一文件夹结构补全缺失类别目录。更保险的做法是直接用JSON标注里的classes列表构建类别映射不依赖目录名推断。5.4 YOLOv5分类训练到一半中断显存被占满现象YOLOv5分类训练启动后跑了几十个epoch突然报CUDA out of memory训练中断之前的进度全部丢失。原因小数据集上容易忽略的一个问题——batch设得过大输入尺寸调高后显存需求成倍增长。yolov5s-cls模型本身不大但如果你把--img设成320甚至640显存占用会翻好几倍在6G显存的卡上很容易爆掉。解决先确认GPU显存大小6G显存建议img224、batch168G以上可以batch32。另外YOLOv5支持--cache参数把图片缓存到显存会额外吃显存小数据集没必要开。如果已经爆了把batch降一半重新启动数据量不大的话训练时间增加很有限。5.5 show脚本跑出来是空白图或者图片显示不全现象资源自带的show脚本或者自己写的可视化代码运行后窗口里全是白屏或者只有少数几张图片看起来像是数据坏了。原因大概率是图片通道顺序问题——OpenCV读取的图片是BGR顺序matplotlib期望RGB顺序如果混用两个库读图再显示颜色就错乱。还有一种情况是图片本身是RGBA四通道matplotlib默认按RGB处理会出问题。解决如果用了OpenCV显示前做通道转换cv2.cvtColor(img, cv2.COLOR_BGR2RGB)。如果图片含Alpha通道用PIL打开后调用img.convert(RGB)去掉。我的习惯是稳定性优先先读单张图片打印它的shape属性确认通道数再批量可视化别一上来就show。6. 模型上线前的验证习惯混淆矩阵、遮挡测试与阈值调优训练结束、测试集准确率看得过去不代表模型可以直接上线。火焰烟雾分类这种安防类场景误报和漏报的成本完全不同——漏报一场火情可能酿成事故误报一次消防系统会引来一趟无效出警。所以上线前我有一套强制验证流程花不了多少时间但能帮你避开绝大部分线上事故。6.1 混淆矩阵与报警阈值选择第一个必做的是混淆矩阵的可视化。不要只看总体准确率要看normal被误判成fire的比例。这个比例直接决定误报率——如果系统一天处理一万帧画面误报率1%也会有100次无效报警这是现场接受不了的。画混淆矩阵用sklearn几行代码就能完成from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import torch y_true [] y_pred [] model.eval() with torch.no_grad(): for images, labels in test_loader: images images.to(device) outputs model(images) _, predicted torch.max(outputs, 1) y_true.extend(labels.cpu().numpy()) y_pred.extend(predicted.cpu().numpy()) cm confusion_matrix(y_true, y_pred) disp ConfusionMatrixDisplay(cm, display_labelstrain_dataset.classes) disp.plot(cmapBlues)这段代码在测试集上收集真实标签和预测标签用sklearn生成混淆矩阵并可视化。display_labels直接取train_dataset.classes保证顺序与训练一致。看混淆矩阵时优先看对角线以外的数值集中在哪两个类别之间——fire和smoke互混严重是数据问题normal和fire互混严重是阈值问题。阈值调优的做法是模型输出概率分布后不对fire概率超过0.5的样本直接报警而是遍历不同阈值找出误报和漏报的平衡点import torch import numpy as np thresholds np.arange(0.5, 0.96, 0.05) fire_idx train_dataset.class_to_idx[fire] model.eval() fire_probs [] true_labels [] with torch.no_grad(): for images, labels in test_loader: images images.to(device) probs torch.softmax(model(images), dim1) fire_probs.extend(probs[:, fire_idx].cpu().numpy()) true_labels.extend(labels.cpu().numpy()) for thresh in thresholds: preds (np.array(fire_probs) thresh).astype(int) fire_true (np.array(true_labels) fire_idx).astype(int) tp ((preds 1) (fire_true 1)).sum() fp ((preds 1) (fire_true 0)).sum() fn ((preds 0) (fire_true 1)).sum() precision tp / (tp fp) if tp fp 0 else 0 recall tp / (tp fn) if tp fn 0 else 0 print(fthreshold{thresh:.2f} | precision{precision:.3f} | recall{recall:.3f})代码对每个测试样本取出fire类别的概率在不同阈值下把大于阈值视为正样本计算精确率和召回率。fire_idx从class_to_idx里取不能写死成0否则类别顺序一错整个验证就无效。6.2 遮挡测试与上线检查清单第二个验证是遮挡测试专门用来发现模型是不是在「抄捷径」。做法是把一张正常图片切成网格逐块用灰色方块遮挡分别送入模型看预测结果。如果遮挡住某个区域后预测突然从fire变成normal说明模型可能只是记住了那个区域的颜色特征而不是真正理解火焰的形状和上下文。在火焰烟雾检测里颜色捷径太容易走了——火焰和烟雾的颜色特征太明显模型很容易只靠颜色判断遇到反光、橙色灯光就翻车。从那以后我每次在火焰烟雾数据集上训练完模型都会强制走一遍这三步混淆矩阵看结构、遮挡测试看捷径、阈值遍历看精度。这套流程不是论文里的花架子是真实部署场景里能直接减少线上事故的操作。240张图片的数据集虽然小但走完这套验证你至少能知道模型的「能」和「不能」这就比盲目上线强得多。希望帮到你。本文还有配套的精品资源点击获取