☰
基于Python的火灾检测CNN模型设计与源码实战
2026/10/1 4:10:07 网站建设 项目流程

简介:面向深度学习开发者和火灾监测研究人员,这份源码包提供基于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_ok=True) for category in category_map: os.makedirs(os.path.join(target_root, category), exist_ok=True) 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(f"corrupted file: {fpath}")

这段代码做的事不多,但很关键:统一转成RGB,防止灰度图或带Alpha通道的PNG图在数据加载时报错;过滤小分辨率;跳过损坏文件。我在实际处理数据集时,那批图片里总有几张是截断的JPEG,不处理的话Torch训练会中途中断。

3.3 类别不平衡的应对策略

真实公共数据集中,Normal类往往远多于Fire和Smoke。如果你直接训练,模型会学成“永远输出Normal”,因为整体准确率已经足够高。需要做两类操作:一是训练时用加权采样器,二是做数据增强。加权采样的手段PyTorch原生支持:

from torch.utils.data import WeightedRandomSampler # 统计每个类别的样本数 counts = [len(os.listdir(f"dataset/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_samples=len(sample_weights), replacement=True)

这里的逻辑是以“每个样本被抽中的概率与其类别样本总数成反比”的方式做重采样,让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_classes=3, dropout_rate=0.5): super(FireCNN, self).__init__() # 第一层:提取颜色分布与低频边缘 self.conv1 = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2) ) # 第二层:提取火焰特有高频纹理 self.conv2 = nn.Sequential( nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2) ) # 第三层:扩大感受野,捕获烟雾弥散特征 self.conv3 = nn.Sequential( nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2) ) self.conv4 = nn.Sequential( nn.Conv2d(128, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2) ) self.global_pool = nn.AdaptiveAvgPool2d(1) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128, 64), nn.ReLU(inplace=True), 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(p=0.5), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.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_classes=3).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=5e-4) # 学习率余弦退火,避免后期震荡 scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30) 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, f"checkpoints/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_only=True,加载包含完整训练状态的checkpoint到CPU上时,记得设定map_location='cpu'。

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(f"save 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, dim=1) conf, preds = torch.max(probs, dim=1) 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(weight=weights.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_location:

model.load_state_dict(torch.load(MODEL_PATH, map_location="cpu"))

6.4 训练过程中DataLoader内存溢出或CPU占用奇高

现象是训练到某个epoch时程序崩溃,或者风扇狂转但GPU利用率极低。

原因是你把num_workers设太大,python多进程在每个epoch重新加载图片时内存分配峰值过高;另一个原因是图像没有在加载时做标准化。

train_loader = torch.utils.data.DataLoader( train_dataset, batch_size=32, shuffle=True, sampler=train_sampler, num_workers=2, pin_memory=torch.cuda.is_available() )

num_workers设置本身是一种取舍,Windows系统建议设为0,Linux下设为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, device="cpu"): 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, dim=1)[0] conf, idx = torch.max(probs, dim=0) return CLASS_NAMES[idx.item()], conf.item() # 入口 model = FireCNN(num_classes=3) model.load_state_dict(torch.load("./best_model.pth", map_location="cpu")) 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.py

README.md里不要写作文,写运行命令、数据集来源、环境依赖。我的习惯是把完整训练命令直接放在第一行,让后来的人能在自己机器上一步复现:

pip install torch torchvision pillow scikit-learn python train.py --data ./dataset --epochs 30 --batch 32 --lr 1e-3

一个细节:requirements.txt里不要锁版本锁得过死,写torch>=2.0.0比写torch==2.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], average="macro") macro_f1 = f1_score(all_labels, all_preds, average="macro") print(f"fire 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, device="cpu") 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、写进代码注释、也写进论文讨论部分,整个项目的设计就立住了。没有固定答案,但必须有明确取舍。这也是我看一个火灾检测项目是认真做还是交差做的核心区别。希望这篇笔记帮到你,按里面的路径走完,你收获的不只是一套能跑的源码,而是以后接到任何图像分类任务都不慌的完整套路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询