基于Python的火灾检测CNN模型设计与源码实战
发布时间:2026/10/1 4:10:09来源:尧图网络
简介面向深度学习开发者和火灾监测研究人员这份源码包提供基于Python的火灾检测CNN模型完整实现包含FireNet与InceptionV1、V3、V4的OnFire变体可应用于图像火灾识别、监控预警等场景适合有一定神经网络基础并希望参考或二次开发实际检测方案的读者。压缩包共23个文件大小仅1.9MB其中8个Python脚本分别覆盖模型定义、训练验证与格式转换6张PNG图片展示各模型结构及超像素分割效果另配Shell下载脚本、Markdown说明、YAML工作流配置、依赖列表和许可证文件结构清晰、模块划分清晰且完整。借助随附的数据集下载与模型下载脚本可快速准备数据与权重依赖清单帮助一键重建环境项目还提供问题模板与持续集成工作流便于团队协作与后续维护。已有444人学习下载整体适合作为火灾检测入门实践、模型对比或算法扩展的高性价比基线。1. “基于Python的火灾检测CNN模型设计源码”这到底是个什么项目搜这个标题的人我猜你多半不是第一次接触深度学习。要么是课程设计选了火灾检测这个方向要么是毕业设计想找一个能跑通、能复现、论文里有东西可写的题目。这个标题看上去朴素其实把一条完整的技术链路和交付物都点了出来“基于Python”指实现语言与生态“火灾检测”指应用场景与数据来源“CNN模型设计”指网络结构需要自己搭、自己改、自己训练“源码”指要交付一套能运行、能二次开发的代码。这项目真正的价值不在“检测火灾”这个业务本身而在于它用一个看似具体的场景逼着你走完图像分类的完整流程数据预处理、网络设计、训练调参、评估分析、结果可视化。我会把这篇文章写成你照着做就能独立复现一套代码的实战笔记包含完整结构、训练参数的设定逻辑、以及我踩过的几个坑。新手能跟得上步骤老手可以直接看边界条件和参数设计思路。2. 火灾检测到底用CNN解决什么问题分类与定位的边界很多初学者拿到这个题目第一反应是“用CCTV画面框出火焰位置”——这是目标检测不是分类。标题写的是“火灾检测CNN模型”我一般会先把它收敛成图像分类任务给一张图片判断里面是否包含火焰或烟雾。后面要改造成定位模型可以在分类模型基础上做feature map可视化或替换成YOLO那是第二步的事第一步把分类做扎实。2.1 火焰图像特征和普通物体的本质差别火焰不是“固态物体”没有稳定的边缘和纹理形态连续变化。但在像素层面它有极强的统计特征颜色集中在红黄白区间饱和度偏高亮度在局部区域显著超出平均高频纹理在火焰边缘密集内部反而有一定平滑性。这些特点决定了CNN能学会的不是“火焰长什么样”而是“火焰对应的颜色分布与纹理统计模式”。这里要强调一个选型理由不用SVM或传统颜色阈值法是因为火焰在真实场景下受光照、干扰光源夕阳、红灯、灯光反射影响极大阈值法在控制场景有效放真实监控画面误报率会到不能用的程度。CNN的优势在于把颜色、纹理、邻域关系联合建模把“橙色圆形发光体”和“橙色火焰”区分开来。2.2 二分类还是三分类一个影响标注成本的决策常见的公共数据集有两种组织方式。一种是二分类正样本为含火焰/烟雾的图像负样本为场景相似但无火灾的图像另一种是三分类把火焰、烟雾、正常分开。我建议你的课程设计做三分类理由有三一是论文里可以多写一个类别判别分析二是训练时模型能学到“烟”的特征而不会被统一压进“火”的语义里三是答辩时拿混淆矩阵能讲出东西。三分类带来的代价是标注成本变高尤其是烟雾样本公共数据集中数量少、质量差需要手动筛选。后面我会讲我怎么处理数据不平衡的问题。3. 数据从哪里来和组织成什么样火灾检测源码的地基3.1 数据集获取的正规渠道和常见翻车方式不要直接去搜索引擎随便拉图。常见做法是使用公开数据集以“fire dataset”为关键词能找到若干来源。我用过的包括Corsican Fire Database、FireNet的公开子集以及Kaggle上的Fire and Smoke Detection数据集。如果你需要写论文记得在参考文献里写数据集来源并标注版本。顺序很重要先整理数据再确定网络结构最后才写训练代码。很多源码跑不通问题不在模型代码而是数据目录乱、标签错、图片格式混。我建议你严格按下面目录组织dataset/ train/ fire/ smoke/ normal/ val/ fire/ smoke/ normal/ test/ fire/ smoke/ normal/这份结构看似普通但有两个隐含好处一是torchvision的ImageFolder可以直接加载不需要写自定义Dataset二是类别名即标签人工抽查时一眼就能发现放错文件夹的问题。3.2 清洗数据的必要性哪些图片必须删掉我踩过最大的坑是数据集里混了大量“夕阳”“红色灯光夜景”“橘色渐变海报”等难例。不清理直接训练验证集精度可能很高但跑到真实监控画面会疯狂误报。清洗规则我总结为三条删掉纯色背景只有微小火焰的图片这种图在监控中占比极低反而会让模型学到“小面积亮斑即火焰”删掉重复图不同数据集间常有完全相同的图删掉分辨率过低的图低于200×200的直接放弃因为火焰边缘在高频细节里低分辨率损失严重给一个标准化的图像预处理脚本import os from PIL import Image source_root ./raw target_root ./dataset/train/ # 按目标类别组织清洗后的图片 category_map {fire: fire, smoke: smoke, normal: normal} os.makedirs(target_root, exist_okTrue) for category in category_map: os.makedirs(os.path.join(target_root, category), exist_okTrue) src_dir os.path.join(source_root, category) for fname in os.listdir(src_dir): fpath os.path.join(src_dir, fname) try: img Image.open(fpath).convert(RGB) w, h img.size if w 200 or h 200: continue img.save(os.path.join(target_root, category, fname)) except Exception: print(fcorrupted file: {fpath})这段代码做的事不多但很关键统一转成RGB防止灰度图或带Alpha通道的PNG图在数据加载时报错过滤小分辨率跳过损坏文件。我在实际处理数据集时那批图片里总有几张是截断的JPEG不处理的话Torch训练会中途中断。3.3 类别不平衡的应对策略真实公共数据集中Normal类往往远多于Fire和Smoke。如果你直接训练模型会学成“永远输出Normal”因为整体准确率已经足够高。需要做两类操作一是训练时用加权采样器二是做数据增强。加权采样的手段PyTorch原生支持from torch.utils.data import WeightedRandomSampler # 统计每个类别的样本数 counts [len(os.listdir(fdataset/train/{c})) for c in [fire, smoke, normal]] total sum(counts) weights [1.0 / c for c in counts] sample_weights [] for i, c in enumerate([fire, smoke, normal]): sample_weights [weights[i]] * counts[i] sampler WeightedRandomSampler(sample_weights, num_sampleslen(sample_weights), replacementTrue)这里的逻辑是以“每个样本被抽中的概率与其类别样本总数成反比”的方式做重采样让fire和smoke类别在训练中不被淹没。一个细节num_samples保持等于整个训练集大小用可放回抽样。这比简单复制少数类样本要好因为它不改变原始图像的分布只改变了抽样的频率。4. 用PyTorch从零搭一个能跑的CNN结构设计与参数选择4.1 为什么不用ResNet直接迁移学习课程设计里最常见的偷懒方式是直接加载ResNet50预训练权重换全连接层后训练。这个做法不能说错但它暴露不出你对CNN结构的理解。而且ResNet50参数量大在没有GPU的机器上训练耗时很长在论文里要写清楚结构、参数和为什么这样设计难度更高。我建议的做法是自己搭一个浅层CNN保持在5~8个卷积层以内参数量控制在1M左右配合BatchNorm和Dropout。这样一方面在CPU上也能在合理时间内跑完另一方面结构简单每一层的设计理由可以写清楚。下面给出一个我在火灾检测任务上验证过的网络结构import torch.nn as nn class FireCNN(nn.Module): def __init__(self, num_classes3, dropout_rate0.5): super(FireCNN, self).__init__() # 第一层提取颜色分布与低频边缘 self.conv1 nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2) ) # 第二层提取火焰特有高频纹理 self.conv2 nn.Sequential( nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2) ) # 第三层扩大感受野捕获烟雾弥散特征 self.conv3 nn.Sequential( nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2) ) self.conv4 nn.Sequential( nn.Conv2d(128, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2) ) self.global_pool nn.AdaptiveAvgPool2d(1) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(128, 64), nn.ReLU(inplaceTrue), nn.Dropout(dropout_rate), nn.Linear(64, num_classes) ) def forward(self, x): x self.conv1(x) x self.conv2(x) x self.conv3(x) x self.conv4(x) x self.global_pool(x) x self.classifier(x) return x设计逻辑说明第1层卷积核数量少负责捕捉输入图像在颜色通道上的基本分布。第2层和第3层逐渐加深通道数目的是学习火焰边缘的高频响应。第4层带有全局平均池化用AdaptiveAvgPool2d(1)把任意尺寸的feature map压缩成1×1好处是输入图像尺寸不需要固定训练和推理时可以有不同的输入分辨率。注意一个参数padding都为1且kernel为3因此每个卷积层不改变feature map尺寸尺寸变化只由MaxPooling层带来。输入224×224的图像经过四个池化层后变成14×14最后一层得到14×14×128的特征图全局池化后变成128维向量这个维度刚好衔接后面的全连接层。4.2 输入尺寸、Batch Size和图片变换的算力联动输入尺寸不建议照搬ResNet时代的224×224。在我的实践里火灾检测用144×144或者160×160就能达到不错的效果。理由有两个火焰区域识别不需要极高频细节降低分辨率能大幅减少训练时间更大的输入尺寸意味着更大的batch size才能占满GPU显存不够梯度更新就不稳定。一个可用的数据加载和增强管道from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((160, 160)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((160, 160)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])这里使用了ImageNet数据集的统计量来做标准化。你可能会问火灾图像的像素分布与ImageNet差异很大直接用ImageNet的均值合理吗答案是合理。归一化的目的不是匹配数据分布而是把输入缩放到一个适合优化器的范围用统一均值和标准差能保证初始化网络时激活值的尺度是稳定的。如果你自己统计火灾数据集的均值方差效果通常会变差因为数据集中不同类别的亮度差异极大。另一个容易踩坑的点验证集不要用随机增强。这一点踩过的人不少验证集只用Resize和标准化否则验证精度会有很大波动尤其当RandomHorizontalFlip把火焰位置翻转后会影响模型对空间结构的判断。4.3 损失函数和优化器的选择为什么不用SGD三分类任务首选交叉熵损失。PyTorch的nn.CrossEntropyLoss()内部包含了softmax和log运算所以网络最后一层不需要额外加softmax。优化器我推荐AdamW权重衰减建议设1e-4到5e-4之间。这个选择的原因在于SGD需要精细调节学习率和动量对课程设计来说调参成本高AdamW自适应调整每个参数的学习率收敛速度明显快于SGD适合小数据集和浅层网络。训练循环的基本框架import torch import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model FireCNN(num_classes3).to(device) criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr1e-3, weight_decay5e-4) # 学习率余弦退火避免后期震荡 scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) def train_one_epoch(model, loader, criterion, optimizer, device): model.train() running_loss 0.0 correct 0 total 0 for images, labels in 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) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() epoch_loss running_loss / total epoch_acc correct / total return epoch_loss, epoch_acc这里有一个代码层面的细节loss.item() * images.size(0)再把总和除以样本数算的是整个epoch平均loss而不是每个batch的简单平均。后者会因为最后一个batch大小不足而偏小。打印日志时建议同时输出loss和acc它们对应的本质不同loss下降但acc不升说明模型在“变自信但判断错误”常见于类别不平衡没处理好。5. 训练全流程从自己电脑到答辩演示的完整落地路径5.1 断点续训你不想在训练到第25个epoch时崩溃课程设计训练量通常不大但依然建议加断点续训。理由不是显存不够而是你很可能需要反复调参后接着训而不是从头再来。断点保存代码checkpoint { model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), epoch: epoch, best_acc: best_acc, scheduler_state_dict: scheduler.state_dict() } torch.save(checkpoint, fcheckpoints/fire_cnn_epoch{epoch}.pt) # 加载 checkpoint torch.load(checkpoints/fire_cnn_epoch25.pt) model.load_state_dict(checkpoint[model_state_dict]) optimizer.load_state_dict(checkpoint[optimizer_state_dict]) epoch checkpoint[epoch] best_acc checkpoint[best_acc]两个容易翻车的点加载optimizer和scheduler状态时必须和原来训练时的配置完全一致——包括优化器类型、学习率初始值、T_max值。如果改了这些参数再加载状态恢复会错乱恢复训练时如果使用了不同的学习率设置模型基本会被带偏。另一个问题是PyTorch新版本默认torch.load带weights_onlyTrue加载包含完整训练状态的checkpoint到CPU上时记得设定map_locationcpu。5.2 训练参数怎么设一组可以无脑跑通初始值表格给出一组我觉得“不保证最优但保证能收敛”的初始参数后续根据实验再调整参数推荐值理由输入尺寸160×160平衡细节与训练速度Batch Size32在8GB显存或16GB内存上都能跑初始学习率1e-3AdamW常用起始点权重衰减5e-4防止过拟合训练轮次30配合余弦退火刚好收敛Dropout0.5全连接层防过拟合数据增强翻转颜色抖动增加火焰形态多样性这个组合的收益在于前10个epoch模型会明显下降loss第15到25个epoch之间验证精度会小幅波动上升。如果30个epoch结束验证精度还在上升就把T_max加到40再续训10个epoch如果第5个epoch验证精度就停滞请先检查数据预处理和标签。5.3 保存最优模型而不是最后一轮一个防后悔药的做法建议每轮验证后比较val_acc只有提高才覆盖保存。这是我在训练很多次后养成的习惯best_model_path best_model.pth if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model_path) print(fsave best model at epoch {epoch}, acc {val_acc:.4f})这里保存的是model.state_dict()不是整个模型对象。后者在加载时会绑定网络结构一起序列化一旦你改了FireCNN类里的层参数旧模型就无法加载。只存state_dict加载时先实例化模型再填参数灵活得多。5.4 测试集上要输出什么不只是算个准确率答辩时老师不会只问准确率他更可能问“在什么条件下失效”。因此建议保存测试集预测结果、置信度和真实类别做三件事输出分类报告、绘制归一化混淆矩阵、挑出置信度低于阈值的错分样本单独查看。from sklearn.metrics import classification_report, confusion_matrix model.eval() all_preds [] all_labels [] all_confidences [] with torch.no_grad(): for images, labels in test_loader: images images.to(device) outputs model(images) probs torch.softmax(outputs, dim1) conf, preds torch.max(probs, dim1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) all_confidences.extend(conf.cpu().numpy()) print(classification_report(all_labels, all_preds, target_names[fire, smoke, normal]))软输出是有讲究的。测试阶段不直接取argmax作为最终判断你会看到很多肉眼几乎无法分辨的图片模型给的概率可能只有0.4左右。这给论文讨论部分提供了素材哪些误报在可接受范围哪些不可以。6. 火灾检测CNN源码的避坑手册5个我会反复检查的问题6.1 训练loss快速降为0但验证accuracy很低现象是训练集上准确率接近100%验证集准确率只有60%典型过拟合。原因有两个数据量太少而网络过深网络选择性地记住了训练集的纹理特征或者是你的训练集和验证集来自不同数据集火焰风格差异过大。解决办法先检查验证集是否混入了训练集图片这个最常见也最隐蔽。用一个脚本统计相同文件名和相同图像哈希值确认没有重叠。确认没有泄漏后增加增强强度尤其是ColorJitter的值提高或者减少网络层数。我的经验里火灾检测的数据量低于1000张时4层卷积已经偏深删掉conv4反而效果更好。6.2 混淆矩阵显示smoke类别完全被压成normal现象是烟雾识别结果惨不忍睹而火焰识别效果很好。原因是烟雾本质上是半透明的模糊区域边缘极不清晰在低分辨率下和模糊背景差异极小同时烟雾样本数量少类别权重不足。解决办法不止一个角度把训练图像分辨率改成192×192保留更多烟雾纹理细节单独的烟雾类别做额外的数据增强比如高斯模糊和轻微随机遮挡也可以修改损失函数给烟雾类别手动加权重weights torch.tensor([1.0, 2.5, 0.8]) # fire, smoke, normal criterion nn.CrossEntropyLoss(weightweights.to(device))烟雾权重设2.5和normal权重设0.8是经验值你要根据自己数据集的实际分布做微调。注意权重总和不需要等于1交叉熵内部会做归一化。6.3 换机器换环境后代码报错找不到模型文件现象是换到答辩用的机器后torch.load找不到模型路径或者能加载但报错。原因是模型路径写的是相对路径不同目录下启动Python脚本时相对位置变了另一个常见原因是最佳模型保存在checkpoint文件里只拷了best_model.pth而没拷整个目录。把路径规范化是一种通用做法用os.path.join并做好根目录配置import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) MODEL_PATH os.path.join(BASE_DIR, checkpoints, best_model.pth)另一个易错点训练时用了GPU保存checkpoint在无GPU机器上加载会报RuntimeError: Attempting to deserialize object on a CUDA device。常规解法是加载时加map_locationmodel.load_state_dict(torch.load(MODEL_PATH, map_locationcpu))6.4 训练过程中DataLoader内存溢出或CPU占用奇高现象是训练到某个epoch时程序崩溃或者风扇狂转但GPU利用率极低。原因是你把num_workers设太大python多进程在每个epoch重新加载图片时内存分配峰值过高另一个原因是图像没有在加载时做标准化。train_loader torch.utils.data.DataLoader( train_dataset, batch_size32, shuffleTrue, samplertrain_sampler, num_workers2, pin_memorytorch.cuda.is_available() )num_workers设置本身是一种取舍Windows系统建议设为0Linux下设为2~4即可。不要为了追求加载速度盲目调到8内存不够时系统开始swap训练速度反而下降。如果设置了sampler参数shuffle必须设为False否则两者冲突会报错。6.5 最终模型在真实图片上误报率极高现象是测试集准确率90%拿手机拍一张夕阳图丢进去预测成了fire。原因是你的负样本只选了“普通室内/街道场景”没有包含容易混淆的难例或者训练集本身正样本太理想化——要么大火焰占满画面要么清晰可见和真实监控中火焰占比很小的场景差别太大。我的解决思路是“负样本掺假”从训练集的fire类别里随机挖一些小块比如原图中心区域裁剪出来的小块粘贴到normal样本的角落让模型学到“局部强亮斑不一定危险”。这本质上是一种数据增强手段可以手工做代码量不复杂import random from PIL import Image def paste_flame_patch(background_img, fire_img, paste_ratio(0.05, 0.2)): bg background_img.convert(RGB).copy() fire fire_img.convert(RGB) patch_w int(bg.width * random.uniform(*paste_ratio)) patch_h int(bg.height * random.uniform(*paste_ratio)) fire_resized fire.resize((patch_w, patch_h)) x random.randint(0, bg.width - patch_w) y random.randint(0, bg.height - patch_h) bg.paste(fire_resized, (x, y)) return bg往负样本里贴真的火焰小块这操作听着有点反直觉但它确实有效模型必须学会区分“小面积橙色区域”和“确实需要报警的火焰”没法靠局部颜色糊弄分类。写论文时这部分可以描述成“难例挖掘与局部区域对抗训练”也是加分项。7. 把训练好的模型变成能演示的推理接口最后一步的工程化7.1 单张图片预测函数不要每次都现敲加载代码课程设计和论文答辩都需要现场演示。写一个独立的predict.py接受一张图片路径输出类别和置信度。核心代码import torch import torch.nn.functional as F from PIL import Image from torchvision import transforms from FireCNN import FireCNN CLASS_NAMES [fire, smoke, normal] def predict_single_image(model, image_path, devicecpu): transform transforms.Compose([ transforms.Resize((160, 160)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) img Image.open(image_path).convert(RGB) tensor transform(img).unsqueeze(0).to(device) model.eval() with torch.no_grad(): logits model(tensor) probs F.softmax(logits, dim1)[0] conf, idx torch.max(probs, dim0) return CLASS_NAMES[idx.item()], conf.item() # 入口 model FireCNN(num_classes3) model.load_state_dict(torch.load(./best_model.pth, map_locationcpu)) result, confidence predict_single_image(model, ./test_fire_001.jpg) print(f类别: {result}, 置信度: {confidence:.4f})这段代码的作用是把整个推理流程压缩成一个函数。特别值得说明的是unsqueeze(0)模型输入要求是一个batch维度单张图片取出来是[C,H,W]三维必须增加一维变成[1,C,H,W]。没有加model.eval()的话BatchNorm和Dropout仍然处于训练模式推理结果每次都不一样这是一个很多人都不知道的高干扰陷阱。7.2 用混淆矩阵决定演示时展示哪些图训练完成后我会从测试集里找出最典型的成功案例和最典型的失败案例各3张分别存到demo/success/和demo/failure/里。成功案例对应高置信度高正确率失败案例对应错误但高置信度或低置信度的图。这套演示组件在答辩时非常有用。演示逻辑是先放一批测试集结果用混淆矩阵说明模型整体表现再放失败案例主动说明模型当前边界在哪——你对模型边界的表达比模型本身更让答辩老师信服。7.3 把代码整理成可提交的工程结构而不是一团散文件到最后提交代码或者放进论文附录时按下面结构组织一套文案比较清晰fire_detection_cnn/ dataset/ src/ # 网络结构、训练、验证脚本 checkpoints/ demo/ README.md requirements.txt predict.pyREADME.md里不要写作文写运行命令、数据集来源、环境依赖。我的习惯是把完整训练命令直接放在第一行让后来的人能在自己机器上一步复现pip install torch torchvision pillow scikit-learn python train.py --data ./dataset --epochs 30 --batch 32 --lr 1e-3一个细节requirements.txt里不要锁版本锁得过死写torch2.0.0比写torch2.0.1更稳妥因为对方环境大概率和你不一样。不锁版本的风险在深度学习项目里其实很小PyTorch的API在2.x系列内基本兼容。8. 评估指标别只算accuracy怎么验证模型真的可用8.1 精确率、召回率和F1分数比整体准确率更诚实火灾检测场景里假阴性真的着火了没报警的代价远大于假阳性没着火报警了。所以评估的核心指标不能只看acc要重点看fire和smoke两个类别的召回率。我建议至少打印两个指标macro-F1和fire类别的recall。from sklearn.metrics import f1_score, recall_score recall_fire recall_score(all_labels, all_preds, labels[0], averagemacro) macro_f1 f1_score(all_labels, all_preds, averagemacro) print(ffire recall: {recall_fire:.4f}, macro-f1: {macro_f1:.4f})计算recall时用labels[0]限制只看fire类别这样评估结果会更直接。在我的经验里这个数字至少要到0.85才说明模型在“能发现火”这件事上基本合格。8.2 置信度阈值改一个数字就能改变行为模型输出的是three分类概率分布取最大值对应的类别作为结果。但你完全可以设置一个阈值当最大置信度低于0.6时输出“不确定”这在实际演示中相当实用——那些夕阳和灯光干扰图会被归到“不确定”而不是“fire”避免误报很尴尬的场景。实现方式是在推理函数里加一个分支if confidence 0.6: result uncertain, need manual check阈值0.6不是固定标准你可以拿验证集数据画出置信度分布选择能覆盖绝大多数正确分类的阈值。一般思路是让绝大多数正确样本的confidence在阈值以上同时压住错误样本的confidence。8.3 自己录一段视频做端到端测试静态图片测完以后建议最后一步跑一个视频测试因为真实应用场景是连续帧。可以简单用OpenCV循环读帧把模型推理封装成函数对每帧做分类。实践中发现一个有意思的现象静态图测试效果好的模型在视频上不一定表现好因为视频帧之间有强相关性单帧偶发误报在连续帧上会被放大成持续误报。解决思路是加入滑动窗口判定连续5帧中至少3帧被判定为fire才真正报警。import cv2 frame_results [] ALARM_FRAMES 5 THRESHOLD_HIT 3 cap cv2.VideoCapture(./demo_fire_video.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break # 调用推理函数预测当前帧 result, _ predict_single_image(model, frame, devicecpu) frame_results.append(1 if result fire else 0) if len(frame_results) ALARM_FRAMES: frame_results.pop(0) if sum(frame_results) THRESHOLD_HIT: print(fire alarm triggered) break cap.release()这种平滑策略在论文里可以写成“时间维度上的决策融合”是加分的亮点。它的本质是牺牲单帧响应速度换可靠性对于火灾检测场景完全值得。慢一两秒钟判断远远好过每帧都在误报。8.4 我的个人习惯与收尾说句实在话我每次拿到一个“检测”类题目第一步永远是先问“误报和漏报哪个代价更大”。把这个问题的答案写进README、写进代码注释、也写进论文讨论部分整个项目的设计就立住了。没有固定答案但必须有明确取舍。这也是我看一个火灾检测项目是认真做还是交差做的核心区别。希望这篇笔记帮到你按里面的路径走完你收获的不只是一套能跑的源码而是以后接到任何图像分类任务都不慌的完整套路。本文还有配套的精品资源点击获取
网站建设高端定制企业官网