ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于CNN深度学习的火灾识别实战:从数据集处理到模型部署

基于CNN深度学习的火灾识别实战:从数据集处理到模型部署 简介基于 PyTorch 深度学习框架的卷积神经网络CNN火灾识别项目资源面向图像分类入门与科研实践场景适合需要完成数据增强、模型训练和可视化识别全流程的开发者。压缩包内含 250 个文件以 200 张 PNG 与 44 张 JPG 火灾/非火灾数据集为主体另附 3 个 Python 脚本和 3 个 TXT环境依赖与标签说明整体大小约 173.55MB。项目自带一整套数据预处理思路对较短边增加灰边将图片统一为正方形并通过旋转和翻转扩充样本避免原始图片尺寸不一影响训练三个脚本分别实现数据集标签文本生成、基于训练集/验证集的 CNN 模型训练与本地保存、以及 PyQt 界面加载图片识别训练日志会记录每个 epoch 的验证集损失值和准确率便于复盘与调参。目前已有 172 人学习下载覆盖从环境配置到可视化识别的完整路径对学习 PyTorch 图像分类与火灾识别应用具有直接参考价值。1. 从“含数据集.zip”说起这个火灾识别项目到底能跑多远接过一个写着“基于CNN深度学习的火灾识别-含数据集.zip”的压缩包第一反应不是解压而是想清楚里面装的是什么。这类项目通常包含三样东西一批火灾与非火灾的图片、一份CNN训练代码、还有让人又爱又恨的不完整注释。CNN指的是卷积神经网络是深度学习里做图像识别最主流的那套方法火灾识别在这里是一个二分类问题——判断一张图里有没有火。能解决什么对初学者来说这是把图像分类从理论落到代码的完整训练场对要做消防预警的人来说它是判断“这套路子在真实场景可不可用”的起点。适合谁适合手里有数据、想跑通一个端到端深度学习流程的人适合被理论书劝退、需要用代码建立信心的从业者。但先说结论数据决定这个项目的上限模型只是把数据里的规律挖出来。2. 把数据集从zip里解放出来目录结构、坏图清洗与训练集划分拿到项目第一步永远不是看模型而是看数据。火灾识别项目里数据组织方式通常就两种一种是按类别建立文件夹fire/和no_fire/各放一摞图片PyTorch 的ImageFolder能直接读另一种是给一张 CSV 标注表文件名映射到标签。最常见的做法是前者因为它对新手最友好也最容易出问题——文件夹里混进了不该有的图片、有些图根本打不开、类别比例严重失衡。这些坑会在训练阶段集中引爆所以解压之后先做三件事看结构、验完整性、划数据集。2.1 解压与目录结构先知道你的数据长什么样在 Linux 或 Windows 的 Git Bash 环境下解压和检查目录基本是两条命令unzip 基于CNN深度学习的火灾识别-含数据集.zip -d fire_dataset find fire_dataset -maxdepth 3 -type d | head -50-d fire_dataset指定了解压到哪个目录不写就会把一堆散文件直接倒进当前目录到时候想归类就麻烦了。find配合-maxdepth 3只看前三层目录目的是确认类别文件夹是不是按fire、no_fire这种名字组织。如果看到data/train/fire/01.jpg这种结构说明已经划分过了但也别高兴太早——顺手用du -sh fire_dataset看看总大小如果只有几十 MB很可能图片分辨率不高或者样本数量少到会让模型严重欠拟合。Windows 用户用自带解压工具也能做但我不推荐一是解压嵌套 zip 需要手动操作好几层二是命令行环境下find和du这种检查方式没法复用。环境方面注意一点整个项目的代码、数据集路径不要带中文目录名。PyTorch 的 DataLoader 遇到中文路径在部分老版本 Windows 系统上会直接报编码错误这是真实翻车现场。2.2 图片完整性核查坏图比想象中多解压完毕、结构清楚之后先做一个全量坏图扫描。火灾识别数据集来源比较杂——有的图是从监控视频里抽帧出来的有的是爬虫抓的这两类数据特别容易出现半截图、截断图和魔改后缀的图。用一段 Python 把每张图都真实地打开一次from PIL import Image import os data_dir fire_dataset bad_images [] total_images 0 for root, _, files in os.walk(data_dir): for f in files: if f.lower().endswith((.jpg, .jpeg, .png, .bmp)): total_images 1 path os.path.join(root, f) try: img Image.open(path) img.load() # 真正把图片解压到内存 except Exception as e: bad_images.append((path, str(e))) print(ftotal images: {total_images}) print(fbad images: {len(bad_images)}) for p, e in bad_images[:10]: print(p, e)这里最关键的是img.load()。只调用Image.open()并不会真实读取文件内容它只是读取了头部信息在训练循环里那个坏图才炸出来——到时候报错信息会指向 DataLoader让你误以为是数据加载的问题。循环里用os.walk递归遍历后缀名大小写都判一遍因为从 zip 里解压出来的文件名经常有JPG和jpg混着来的情况。扫描出坏图后直接删掉或移动到bad/目录别留在原地。如果说有什么血泪经验那就是永远不要因为几张坏图手动改文件名硬凑。删掉它们不影响大局火灾识别需要的是“能稳定加载、内容清晰”的图不是纠结坏图修补。2.3 划分训练集、验证集和测试集固定随机种子才有后悔药原始 zip 里的数据如果只有fire和no_fire两个文件夹没有划分训练验证测试那就要自己动手。这里我建议按 80/10/10 划分成 train/val/test 三份而且用复制而不是移动原图——划分完训练不理想原图还在还能重新划分这是你的后悔药。划分脚本import os import random import shutil src fire_dataset train_dir fire_split/train val_dir fire_split/val test_dir fire_split/test for cls in [fire, no_fire]: files os.listdir(os.path.join(src, cls)) random.Random(42).shuffle(files) # 固定种子保证每次划分结果一致 n len(files) n_train, n_val int(n * 0.8), int(n * 0.1) for i, f in enumerate(files): if i n_train: dest os.path.join(train_dir, cls, f) elif i n_train n_val: dest os.path.join(val_dir, cls, f) else: dest os.path.join(test_dir, cls, f) os.makedirs(os.path.dirname(dest), exist_okTrue) shutil.copy2(os.path.join(src, cls, f), dest) print(done) for split in [train, val, test]: for cls in [fire, no_fire]: folder os.path.join(ffire_split/{split}, cls) print(split, cls, len(os.listdir(folder)))random.Random(42)这句是核心。用全局random.shuffle的话每次脚本运行结果都会变换台机器结果又变模型对比和论文报告里的数字就说不清了。建议直接把这个脚本存成split_data.py以后换数据集改个路径就能复用。注意这里类别名要跟实际文件夹名一致如果你的数据集里写的是fire和nofire就别改成no_fire否则后面构造数据集时会读到空目录。3. 模型选型为什么要用CNN、自定义网络与迁移学习怎么选数据准备好之后才轮到模型。CNN 在火灾识别这个任务上的优势非常直接火焰的橘红色调、烟雾的纹理、火光边缘的亮度梯度这些特征是有局部相关性的。全连接网络把每个像素当成独立的输入学不到“像素和附近像素一起构成火焰轮廓”这种关系而卷积层天然就是局部连接加权重共享所以 CNN 在图像任务里碾压传统 MLP 并不玄学是结构设计决定的。3.1 自定义浅层CNN为什么说火灾识别不需要很深很多人一上来就搬 ResNet50但先冷静算一笔账如果你手里的数据只有几千张图一个 ResNet50 参数量两千多万训练起来不仅慢而且极容易过拟合。火灾识别跟 ImageNet 那种一千类分类比赛不一样它本质上是二分类且特征火焰颜色、烟雾形态足够鲜明一个 3 到 5 层的浅层 CNN 完全能打。用 PyTorch 定义一个可用的结构import torch.nn as nn class FireCNN(nn.Module): def __init__(self, num_classes2): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(128, 64), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(64, num_classes), ) def forward(self, x): return self.classifier(self.features(x))这里有两个设计细节值得解释。一是用了BatchNorm2d跟在每个卷积层后面它的作用是稳定中间特征的分布让模型对学习率不那么敏感这对没有调参经验的新手是很大的保护你可以放心用默认参数。二是AdaptiveAvgPool2d((1, 1))把最后一层特征图直接池化成 1x1这意味着不管输入图片是 224 还是 128全连接层的输入维度都不会变省去了torch.nn.Linear尺寸算不对的经典翻车。每层padding1是为了让图片尺寸在卷积后减半只在池化层发生这个习惯能避免特征图缩得过快。3.2 迁移学习用 ResNet18 兜底如果你不追求自己从零训练的全部乐趣迁移学习就是那个更稳的选择。用 torchvision 里预训练好的 ResNet18把最后一层全连接换成二分类输出模型权重在 ImageNet 上已经学到了丰富的底层特征——边缘、纹理、颜色块。火灾烟雾这种复杂的纹理特征直接继承比从头学要划算得多。import torchvision.models as models model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_ftrs model.fc.in_features model.fc nn.Linear(num_ftrs, 2) # 换成二分类输出num_ftrs model.fc.in_features这行有人会写成2048那是 ResNet50 的值ResNet18 是 512写死就报错。weightsmodels.ResNet18_Weights.IMAGENET1K_V1会从网络下载预训练权重到本地缓存离线环境需要提前准备好权重文件否则跑起来才发现卡在下载步骤。我一般会先用自定义 CNN 跑通流程再用迁移学习做对比两个都跑一遍才能看出数据本身的难度。3.3 选型对比数据量、训练时长与效果的权衡选型方案参数量训练时间千级数据最低数据要求最终效果自定义浅层CNN约50万分钟级每类500张可起步分类精度可达90%ResNet18迁移学习约1100万十几分钟每类200张就有不错效果通常比浅层CNN高2%-5%ResNet50迁移学习约2500万半小时以上需要更多数据支撑数据少时反而过拟合给读者一个可执行推荐数据量在千张以内、追求快速出结果的上 ResNet18 迁移学习打底数据量上万、有充足 GPU 时再考虑让浅层 CNN 从零训练做对照。别一上来就追大模型火灾识别这任务撑不起那么大的容量这不是显卡够不够的问题是数据根本喂不饱模型训练损失一路降到零、验证集却不动那叫过拟合不叫识别能力强。4. 训练配置与参数调优从训练循环到学习率、数据增强模型结构定了接下来才是真正决定成败的部分——训练配置。深度学习训练说到底是“损失函数下降的过程”但怎么让损失稳定地降、在合适的时候停下全靠 DataLoader 读取、优化器选择、学习率调度这三个环节配合。火灾识别里有几个特有的参数雷区这一节逐一拆开讲。4.1 最小可运行的训练循环框架训练循环代码是整篇的骨架每个人写的细节不同但核心逻辑固定。给一个能直接放到 PyTorch 1.13 环境跑的版本import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_ds datasets.ImageFolder(fire_split/train, transformtransform) val_ds datasets.ImageFolder(fire_split/val, transformtransform) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers2, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers2, pin_memoryTrue) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size8, gamma0.1) for epoch in range(30): model.train() running_loss 0.0 for inputs, labels in train_loader: optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() scheduler.step() model.eval() correct 0 total 0 with torch.no_grad(): for inputs, labels in val_loader: outputs model(inputs) _, preds torch.max(outputs, 1) total labels.size(0) correct (preds labels).sum().item() val_acc correct / total print(fepoch {epoch1}: loss{running_loss/len(train_loader):.4f}, val_acc{val_acc:.4f})transform里三个关键点Resize((224, 224))强制统一输入尺寸因为混合分辨率在 batch 训练时会让张量拼接直接报错Normalize用的均值方差是 ImageNet 的统计值如果使用迁移学习模型就必须沿用这套参数否则预训练权重的分布对不上自己从零训练时可以替换成自己算的数据集均值方差但 ImageNet 参数通常也能凑合。训练阶段model.train()与验证阶段model.eval()缺一不可——BatchNorm 和 Dropout 在两种模式下的行为完全不同漏掉eval()会导致验证集准确率忽高忽低这就是所谓的“训练准、验证飘”现象。4.2 学习率、batch size与epoch怎么搭配学习率是整个训练过程最敏感的参数。用 Adam 优化器时lr1e-3是最常见的默认值但迁移学习微调时我一般会降到1e-4因为预训练权重已经很好了学习率太大相当于拿到一件半成品还疯狂修改训练损失降得快验证集效果反而不行。batch size 同样影响结果显存允许的话设 32图片分辨率高或者显卡老就设 16。双重教训说明batch size 小于 16 时 BatchNorm 的统计量会很不稳定loss 曲线震荡剧烈并不是玄学是样本量太少算出来的均值方差失真。epoch 数量的判断标准不是跑满固定轮数而是看验证集准确率开始不再上升甚至下降的时刻——那是过拟合的临界点。StepLR(optimizer, step_size8, gamma0.1)表示每 8 个 epoch 学习率乘以 0.1初期快速收敛后期小步微调。一个平时常用的做法前三轮先用默认学习率试探如果 loss 波动极大就终止把 lr 除以 10 重跑这比死等训练结束然后看结果要好得多。训练过程本身要有“随时能停、调完再续”的心态。4.3 数据增强与正则化火灾场景的两个特殊限制数据增强是提升模型泛化能力最有效的免费手段。对火灾识别来说随机水平翻转、随机裁剪、随机旋转不超过 15 度都可以用。但注意不要用颜色抖动这是火灾识别特有的坑火焰的橙红色是核心特征颜色抖动会随机改变色相和饱和度让模型学到“颜色变化等于有火”这种错误的因果。烟雾是灰白色的色调参数抖得太狠烟雾样本也被搅乱。只保留几何增强和光照微调transform_train transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees10), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])RandomResizedCrop常用于 ImageNet但对火灾识别我却推荐先用Resize(256)再RandomCrop(224)因为前者随机裁剪比例会让火焰区域被裁掉大部分等于生成了错误标签的样本。这是一个很隐蔽的坑如果不看训练集可视化结果根本发现不了。正则化方面weight_decay1e-4加在优化器里就够Dropout 已经在分类器里放了 0.5不需要更多冗余手段。训练时的关键习惯是每个 epoch 结束打印验证集准确率和训练损失存下最优模型。最好在内存里维护一个变量记录验证集最高的准确率达到新高就保存一份权重文件这样就算后面训练崩了也有一个最优版本兜底。5. 避坑指南火灾识别训练中的典型问题与排查路径训练一轮就能顺利收敛是理想情况现实中更多的是莫名其妙的问题loss 变成 NaN、验证集准确率纹丝不动、训练时表现完美部署时乱报。这些问题大多不是模型结构造成的而是数据、参数、环境三个层面的细节遗漏。下面把火灾识别训练里最常遇到的 5 个问题按“现象 → 原因 → 解决”写清楚每一条都来自真实运行的典型场景排查顺序按频率从高到低排列。5.1 训练损失降到很低但验证准确率只有60%上下现象训练集准确率一路冲到 98% 以上验证集却卡在 60% 附近不动看训练曲线像是在做两个不同的任务。原因绝大多数火灾数据集的类别是不均衡的。如果fire类有 2000 张、no_fire类只有 300 张模型学到的最优策略就是全部预测为 fire整体准确率看着不低但验证集里 no_fire 几乎全错。再加上图片背景差异明显比如正样本都是暗色监控截图负样本都是明亮风景照模型记住的是背景而不是火焰。解决先打印数据集的类别分布确认比例。如果不均衡给CrossEntropyLoss加权重参数import torch.nn as nn class_counts [2200, 350] # 依次为 fire, no_fire 的样本数 total sum(class_counts) weights [total / c for c in class_counts] criterion nn.CrossEntropyLoss(weighttorch.tensor(weights, dtypetorch.float))weight越大代表该类别的损失惩罚越高模型会偏向学习少样本类别。解决后如果验证集还是低就要怀疑是数据集划分时原图来自不同分布——去检查切分脚本的随机种子是否真的固定了。5.2 训练到一半 loss 变成 NaN现象前几个 epoch 损失还正常下降某一步突然变成 NaN之后再也回不来只能重新跑。原因学习率过大导致梯度爆炸是最常见的一种。火灾图像里如果有亮度极高的火焰区域像素值经过归一化后仍可能很大再加上深层网络梯度累积数值溢出就出现 NaN。另一个隐藏原因是数据里出现了损坏过度的图片——比如全黑图或者纯白图经过多层卷积后特征值异常放大。解决第一反应是降低学习率从1e-3降到1e-4重跑。同时排查训练数据里是否存在极端异常图把像素值几乎全为 0 或全为 255 的样本筛掉。还有一个平时容易忽略的点如果用了torch.FloatTensor计算且数据加载时没有归一化到 [0,1]RGB 值直接以 0-255 送进网络BatchNorm 内部计算精度很容易溢出。确认ToTensor()在Normalize之前执行这个顺序错了就会把整数像素当浮点数用。5.3 验证准确率不错但实际摄像头识别频繁误报现象测试集上准确率超过 95%部署后用手机或监控摄像头对着家里环境扫一圈红色窗帘、橙色灯光、甚至落日余晖都会被报成火警。原因测试集和真实场景的数据分布不一致。数据里的负样本可能是简单的风景、室内图而真实场景里有各种颜色的光源、玻璃反射、红色物体。CNN 学到了“橘红色等于火”的肤色逻辑而不是真正的火焰特征——动态闪烁、烟雾扩散、火焰形态变化。解决这一步需要在数据层面做补充而不是换模型。收集 100 到 200 张真实场景中纯红色、纯橙色物体的图片加入训练负样本让模型区分“红色的静态物体”和“火焰的动态区域”。另外调高预测阈值默认argmax是只看最大概率改成只有当 fire 类概率超过 0.7或更高才报警会更安全。import torch prob torch.softmax(outputs, dim1)[0, 1] # fire 类的概率 if prob 0.7: print(ffire detected, confidence{prob.item():.2f}) else: print(fno fire, fire prob{prob.item():.2f})softmax输出的概率和模型置信度是两码事一个没有见过红色窗帘的模型给红色窗帘打出 0.98 的火警概率是合理的它就是这么被训练出来的。阈值调节能缓解误报但治本方向永远是补充负样本。5.4 从零训练的 CNN 准确率始终比迁移学习低一大截现象自己搭的浅层 CNN 训练到验证集准确率 85% 就上不去了同样的数据 ResNet18 迁移学习能做到 93%。原因数据量不够。神经网络学底层特征边缘、纹理、颜色块需要大量样本如果你只有几千张图从零训练等于让模型在狭窄的数据里同时学“什么是边缘”和“什么是火焰”两层任务叠在一起容量不够。迁移学习替你把底层特征全部准备好了只需要学高层语义对数据的需求量少了一个数量级。解决如果坚持用自定义 CNN先把训练集每个类别的样本扩充到 3000 张以上通过数据增强加上合理的数据采集如果不具备扩充数据的条件就接受迁移学习作为主力模型。自定义 CNN 可以留存作理解原理的学习工具部署用 ResNet18这不是认输是把算力和数据花在刀刃上。5.5 训练和验证时图片类别标签与文件名对不上现象训练曲线完全正常验证准确率却持续在 50% 附近波动像随机猜测。原因ImageFolder的标签是根据文件夹名称的字母顺序自动生成的fire排在no_fire前面标签 0 是 fire标签 1 是 no_fire。如果某处代码里手动指定了标签顺序反了模型学和验证的根本是倒置的。解决每次训练前先跑一段最小验证代码打印train_ds.class_to_idx确认标签映射关系from torchvision import datasets ds datasets.ImageFolder(fire_split/train) print(ds.class_to_idx) print(ds.classes)输出如果显示{fire: 0, no_fire: 1}那么在计算准确率的代码里preds 1对应的就是 no_fire。我见过最离谱的一个坑是在推理时把argmax结果直接当成索引去类名列表取名字而类名列表没按字母顺序排列模型判断全对但打印出来的名字全部反了。这个验证脚本加进训练流程里总共花不到一分钟能省掉半小时的自我怀疑。6. 验证方法与进阶部署技巧从混淆矩阵到时间平滑推理模型训练完验证不只是看一个准确率数字。火灾识别场景中漏报的代价——真实火情被忽略——远高于误报所以衡量模型要看混淆矩阵。计算测试集上每个类别的召回率和精确率确认 fire 类的召回率在 95% 以上才算合格否则就调低报警阈值或者补数据。进阶部署时逐帧推理视频流会遇到新的问题单帧误报率高且结果闪烁上一帧报火警、下一帧又消失。常见做法是加一个时间窗口投票from collections import deque class FireDetector: def __init__(self, frame_window5, fire_threshold0.7): self.history deque(maxlenframe_window) self.threshold fire_threshold def update(self, fire_prob: float) - bool: self.history.append(fire_prob) if len(self.history) self.history.maxlen: return False avg_prob sum(self.history) / len(self.history) return avg_prob self.threshold窗口长度设为 5 帧平均置信度超过阈值再报警。这个技巧能消掉大部分由单帧噪声引起的误报代价是报警延迟约 1 到 2 秒对火灾预警来说完全可接受。我自己第一次做的时候直接把单帧阈值拉高到 0.9结果系统彻底变成瞎子——真正有火的画面因为角度、遮挡置信度只有 0.85 左右全给否决了。后来改成低阈值加时间平滑误报和漏报反而同时降了。最后一个建议是保留每一轮的验证结果和模型权重写个简单的实验记录文本数据集划分方式、增强策略、学习率、验证准确率、阈值。下次调参时翻翻记录就知道哪个方向有效不用每次从头猜。这个习惯会让你的深度学习迭代速度快一倍。数据能跑通、曲线能收敛、阈值会调整这套流程就算是真正属于你自己的了以后再看别的视觉项目结构再复杂也是同一套逻辑的延伸。希望帮到你。本文还有配套的精品资源点击获取
返回列表