资讯详情 CNN火灾识别实战:从数据集整理到模型训练与部署
📅 2026/10/31 21:37:17
简介基于CNN深度学习的火灾识别资源包面向入门深度学习的开发者与学生覆盖图像分类任务中数据预处理、模型训练与可视化界面完整流程。代码基于PyTorch实现数据集图片经过短边添加灰边转正方形与随机旋转增强可提升模型泛化能力适用于火灾检测相关课程设计或项目演练。压缩包共250个文件以200张png、44张jpg图片构成训练数据3个python脚本分别完成数据集文本生成、模型训练与PyQt界面展示3个txt文件记录环境依赖与数据划分信息。整包约173.55MB资源目录结构清晰已有172人浏览学习。通过依次运行三个脚本可获得本地保存的模型权重及每个epoch的验证集损失与准确率日志并能利用可视化界面上传图片进行识别便于对照理解CNN训练全流程与调参思路。1. 拿到“基于CNN深度学习的火灾识别-含数据集.zip”先别急着解压跑训练这个压缩包几乎是火灾识别方向最经典的落地形态一个CNN卷积神经网络分类项目附上一个可以直接开练的数据集。它的核心任务很直接——给定一张图片模型输出“有火”还是“没火”难点却在数据集质量、类别平衡和真实场景误报上。我接过不少类似的包多数人第一步就双击打开README找训练命令结果往往卡在数据目录混乱、标签对不上、训练完精度虚高但一测就翻车。这篇文章就按我自己的操作顺序把数据集整理、模型选型、训练调参和排障讲清楚。适合正在做毕设、课程设计或消防监控原型验证的人照着做能把跑通时间压缩到一晚上。2. 数据集解压与目录整理把zip里的零散图片变成CNN能吃的标准结构2.1 zip包解压与数据分布摸底文件数量、类别比例怎么查拿到压缩包先做两件事解压、摸底。别一上来就开Notebook写模型火灾识别这类项目的数据集往往是从多个来源凑出来的目录名可能是中文、类别可能叫fire/not_fire也可能叫smoke/normal先看清真实结构比什么都重要。unzip 基于CNN深度学习的火灾识别-含数据集.zip -d fire_dataset cd fire_dataset find . -type f | wc -l第一条命令把压缩包解压到fire_dataset目录-d指定输出目录find . -type f | wc -l统计一共多少个文件先对数据规模有个数。然后看目录结构find . -maxdepth 2 -type d只看两层目录一般就能发现是“按类别分文件夹”还是“所有图片混在一起带标签文件”。如果解压出来乱码多半是压缩包用GBK编码在macOS或Linux上解压时可以用unzip -O gbkWindows下一般直接右键解压没问题。类别数量一定要量化不要靠肉眼。用Python统计比一条条shell命令更直观尤其是文件名不规律的情况import os from collections import Counter base fire_dataset exts (.jpg, .jpeg, .png, .bmp) counts Counter() for root, _, files in os.walk(base): for f in files: if f.lower().endswith(exts): cls os.path.basename(root) counts[cls] 1 for cls, num in counts.most_common(): print(f{cls}: {num}张)这段代码按“图片所在文件夹名”作为类别标签exts限定只统计图片格式避免把README或标注json计入。输出后你就知道正负样本大概什么比例。火灾识别数据集最常见的坑是类别极度不平衡比如fire类2000张、no_fire类8000张这种数据直接训练会让模型倾向于把所有图都判成no_fire因为这样准确率也很高。2.2 把目录结构统一成标准数据集格式ImageFolder要求与数据划分脚本CNN训练最省事的组织方式是PyTorch的ImageFolder格式它要求目录严格分层train/类别名/图片、val/类别名/图片。很多zip包里已经是“fire”和“no_fire”两个文件夹但train、val、test没分开或者测试集和训练集混在一起。这时候我一般写一个划分脚本固定随机种子保证结果可复现并按8:1:1切成训练、验证、测试三份。import os, random, shutil from collections import defaultdict random.seed(42) # 固定种子保证每次划分结果一致 base fire_dataset # 解压后的原始目录 target dataset # 整理后的标准目录 exts (.jpg, .jpeg, .png, .bmp) ratios {train: 0.8, val: 0.1, test: 0.1} images defaultdict(list) for root, _, files in os.walk(base): cls os.path.basename(root) if cls in (fire, no_fire): # 只处理真正有意义的类别目录 for f in files: if f.lower().endswith(exts): images[cls].append(os.path.join(root, f)) for cls, paths in images.items(): random.shuffle(paths) # 打乱顺序避免同类图片扎堆 n_train int(len(paths) * ratios[train]) n_val int(len(paths) * ratios[val]) for phase, part in [ (train, paths[:n_train]), (val, paths[n_train:n_train n_val]), (test, paths[n_train n_val:]) ]: for idx, src in enumerate(part): ext os.path.splitext(src)[1] dst_dir os.path.join(target, phase, cls) os.makedirs(dst_dir, exist_okTrue) dst os.path.join(dst_dir, f{cls}_{idx:05d}{ext}) shutil.copy2(src, dst)这里有两个细节值得注意一是重命名了图片避免不同目录里的001.jpg互相覆盖二是copy2保留原始文件元信息如果你的磁盘比较紧张也可以改成shutil.move但那样原始目录就被拆掉了做数据回查时不太方便。划分完之后用find dataset -type d确认目录层级是不是预期的样子。提示如果zip里已经存在train/val/test三套目录就别套这个脚本了。直接检查每个split里的类别文件夹是否齐全缺哪类补哪类这才是最快的路。3. 搭建一个能真正收敛的CNN模型结构与训练代码3.1 自己写CNN还是用预训练模型火灾数据集大小决定选型CNN之所以适合火灾识别是因为火焰在图像里呈现的是局部纹理特征——橙红色区域、高频边缘、烟雾的半透明渐变这些都能被卷积核提取出来。全连接网络看的是全局像素关系没有空间局部性先验数据量一大就容易学偏。选型先看数据量。压缩包里的数据集规模通常在几千到一两万张这种情况下我的建议很明确优先用预训练模型做迁移学习而不是从零训练一个深层网络。数据量小时ResNet18、MobileNetV3这类模型用ImageNet预训练权重初始化再冻结前几层只训练分类头效果远好于自己搭的网络数据量只有几百张时自己搭一个3层小CNN反而更稳迁移学习容易过拟合。数据规模推荐方案理由每类少于500张自建轻量CNN 强数据增强模型小不容易直接把噪声背下来每类500~5000张ResNet18/MobileNetV3迁移学习预训练特征泛化能力强收敛快每类超过5000张ResNet50/EfficientNet微调数据够多深层模型能学到细粒度火焰特征如果后续要部署到监控摄像头常见做法是先用分类模型做帧级判断再在视频流里做时序平滑。标题这个项目是CNN分类但实际场景里如果要做目标定位就该换YOLOv8这类检测模型训练自己数据集的流程又不一样了。3.2 最小可运行的CNN训练脚本Dataset、模型、训练循环一次跑通先写数据加载。这里用ImageFolder所以第2章整理的目录结构必须严格正确。from torchvision import datasets, transforms from torch.utils.data import DataLoader transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_ds datasets.ImageFolder(dataset/train, transformtransform) val_ds datasets.ImageFolder(dataset/val, transformtransform) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) print(train_ds.classes) print(len(train_ds), len(val_ds))Resize((224, 224))先把尺寸统一Normalize里的mean和std用的是ImageNet统计值已经训练好的预训练模型要求输入分布一致迁移学习时一定要保持这套参数。num_workers4是数据加载的并行进程数Windows上如果卡死就在主程序入口加if __name__ __main__:保护。打印train_ds.classes非常重要ImageFolder会按文件夹名的字典序给类别编号比如fire是0、no_fire是1后面分析混淆矩阵时要对上。模型部分为了演示完整逻辑先写一个自建的轻量CNN代码结构清楚每层都能看到尺寸变化import torch import torch.nn as nn class FireCNN(nn.Module): def __init__(self, num_classes2): super(FireCNN, self).__init__() self.features nn.Sequential( nn.Conv2d(3, 16, kernel_size3, stride1, padding1), nn.BatchNorm2d(16), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(16, 32, kernel_size3, stride1, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, stride1, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(64, 128), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(128, num_classes) ) def forward(self, x): return self.classifier(self.features(x))kernel_size3, padding1让卷积不改变特征图尺寸真正缩小尺寸的是MaxPool2d(2)每经过一次池化宽高减半。BatchNorm2d必须加在卷积和ReLU之间它能让深层网络训练更稳定不用手动调学习率也能快速收敛。AdaptiveAvgPool2d((1,1))把最后一层特征图压成64x1x1好处是不管输入尺寸怎么变全连接层的输入维度都不变。Dropout(0.3)是0.3概率随机失活神经元防止小数据集过拟合。训练循环就是一个标准的监督学习流程但有几个细节会影响成败import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model FireCNN().to(device) criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max20) epochs 20 for epoch in range(1, epochs 1): model.train() total_loss, correct, total 0.0, 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() total_loss loss.item() * images.size(0) preds outputs.argmax(dim1) correct (preds labels).sum().item() total labels.size(0) train_acc correct / total avg_loss total_loss / total model.eval() val_correct, val_total 0, 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) preds outputs.argmax(dim1) val_correct (preds labels).sum().item() val_total labels.size(0) val_acc val_correct / val_total print(fepoch {epoch:2d} | loss {avg_loss:.4f} | ftrain_acc {train_acc:.4f} | val_acc {val_acc:.4f}) scheduler.step()AdamW相比Adam多了解耦的权重衰减weight_decay1e-4是L2正则对小数据集尤其有用。CosineAnnealingLR让学习率在20个epoch里从1e-3余弦下降到接近0前5个epoch学习率较高用来快速收敛后面慢慢精细调整。验证阶段必须写model.eval()和torch.no_grad()前者关闭Dropout和BatchNorm的统计更新后者关闭梯度计算省显存也防止验证结果被随机化干扰。outputs.argmax(dim1)取每个样本概率最大的类作为预测结果。4. 训练参数怎么设从学习率、batch size到数据增强全是细节4.1 一套安全起步的超参数值以及怎么判断该往哪个方向调训练CNN最让新手头疼的是超参数组合太多。我一般给一个保底的起步配置先把模型跑通再根据损失曲线微调参数建议值调整方向输入尺寸224x224太小丢特征太大显存吃紧batch size32显存不够就降到16或8学习率1e-3损失震荡就降收敛太慢就升epochs20~50以early stopping为准优化器AdamW最终调优可换SGDmomentum损失函数CrossEntropyLoss类别不平衡时给class_weight如果训练损失在3~5个epoch后还是纹丝不动先检查是不是学习率太小如果损失直接变成nan或者一步从2.0跳到0.5再跳回2.0那就是学习率太大降到1e-4再看。学习率这个东西在黑匣子里占一半的权重与其猜不如拿出训练日志画图。4.2 数据增强策略火焰识别的增强和一般分类不一样火灾识别里的数据增强有个特殊约束火焰的颜色和纹理是核心特征增强太猛会把橙红色区域扭曲成无法识别的样子。比如ColorJitter的hue参数如果设成0.2火焰可能偏成紫色模型学到的就是“紫红色块是火”真实场景一测就废。train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees10), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.05), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])RandomResizedCrop的scale(0.8, 1.0)表示缩放比例不低于80%相当于轻微裁切保留大部分火焰区域RandomRotation(degrees10)只做小角度旋转火焰形状不像普通物体那样有固定朝向转个10度完全合理hue0.05是保守值只让色相轻微偏移。验证集和测试集不要做任何随机增强只用Resize和Normalize保证指标可比。如果是正负样本不平衡光靠增强不够还要在采样器上做文章from torch.utils.data import WeightedRandomSampler labels [s[1] for s in train_ds.samples] class_counts torch.bincount(torch.tensor(labels)) weights 1.0 / class_counts[labels].float() sampler WeightedRandomSampler(weights, num_sampleslen(labels), replacementTrue) train_loader DataLoader(train_ds, batch_size32, samplersampler)weights计算方式很直白样本属于类别0就除以类别0的总数属于类别1就除以类别1的总数。少的类别每个样本被抽到的概率变大多的类别被降权。replacementTrue允许重复抽样这样每个epoch里样本数是均衡的但代价是同一张图可能在一个epoch里出现多次。4.3 训练日志与早停损失曲线就像心电图要学会看异常训练代码里那只print还不够我习惯把每个epoch的损失和精度存下来最后画成曲线。训练完画图也能网上复盘但训练过程里就要早停不然最后几个epoch在过拟合。history {train_loss: [], val_loss: [], val_acc: []} best_val_acc 0 patience 5 bad_epochs 0 # 在训练循环内部记录 history[train_loss].append(avg_loss) # 验证阶段结束后 history[val_loss].append(val_loss) history[val_acc].append(val_acc) if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), best_fire_cnn.pth) bad_epochs 0 else: bad_epochs 1 if bad_epochs patience: print(early stop at epoch, epoch) breakbest_fire_cnn.pth只保存验证集最优的模型权重这是防止过拟合的后悔药。判断过拟合有个简单规律训练损失持续下降验证损失先降后升两线逐渐拉开典型的“把训练集背下来了”。这时候回退到验证损失最低的那个epoch同时加大Dropout或数据增强强度。5. 火灾识别训练常见问题与避坑现象、原因、解决5.1 验证准确率很高但拿真实场景测试完全不行现象是最典型的“翻车现场”训练完准确率96%开心地拿小区监控截图一测识别出大量误报。原因有两层第一是数据集划分出问题train和val来自同源场景模型看到的背景都差不多等于提前知道了答案第二是数据增强太弱或太强导致模型没学会火焰的本质特征。解决方法是先用原始压缩包里的图片按场景分组不同摄像机、不同光照条件各拿一部分进验证集保证val里的场景和train没有明显重合。如果压缩包里的图本身就来自同一个地方那测试准。更实际的做法是训练完成后额外收集10~20张真实火灾新闻图当冒烟测试集一条条看置信度。5.2 白天效果好夜间和人工光源干扰下误报率飙升这是火灾识别最典型的问题夜间场景的火焰和路灯、车灯、手机闪光灯在CNN眼里颜色几乎一样。原因是训练集里缺乏夜间负样本模型没学过“亮橙红色但不一定是火”这个边界。解决分两步第一步去收集夜间街景、篝火晚会、灯光秀这类hard negatives补充到no_fire类里重新训练第二步在后处理层加条件判断比如单帧“火”置信度必须超过0.7且连续3帧都判火才报警这个时序平滑能过滤掉绝大多数灯光闪烁。很多实际工程的分类模型都能达到90%以上准确率但真正能用靠的都是阈值和后处理。5.3 显存不够报OOM或者训练速度慢到没法忍现象是刚开始训练就报CUDA out of memory或者一个epoch跑十几分钟。先看batch size和输入尺寸这两个对显存影响最大。batch_size32在6GB显存用不下是正常的降到8或16再跑。还有一种情况是num_workers4在Windows上因为多进程问题卡死或非常慢可以先改成num_workers0牺牲一点数据加载速度排除进程阻塞问题。如果显存还是不够用混合精度训练能省将近一半显存scaler torch.cuda.amp.GradScaler() for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() with torch.cuda.amp.autocast(): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()autocast把前向计算的精度降到FP16反向传播依然用FP32GradScaler防止梯度下溢。这个改动对2G小显存机器是救命级别的而且对精度的影响基本可以忽略。5.4 loss变成nan或者训练损失不降反升训练时loss直接变nan或者前几个epoch降了一点后面一路飞升回2.0以上。loss变成nan最常见原因是学习率过大导致梯度爆炸1e-3在某些小模型上也可能太大先降到1e-4试。第二种常见原因是输入数据里混入了损坏的图片比如0字节的jpgToTensor读不出来直接nan。第三个是数据增强里有除以零的操作比如RandomResizedCrop配合过小的scale有时候会裁出一张0像素图。排查时先去掉增强只跑几个epoch如果正常了就是增强的问题如果还是nan检查图片是不是都能被正常解码写个循环单独加载每张图片报错的那个就是问题样本。5.5 同一张图每次跑预测结果不一样模型的Dropout层在推理时还在生效导致同一个输入每次输出的概率不一样尤其是阈值刚好在0.5附近时忽正忽负。原因是PyTorch里只要调用了model.train()Dropout就随机失活神经元推理时必须切换回model.eval()。很多人写训练代码时最后一个epoch没把model.eval()放进去保存的模型状态还算正常但预测脚本里没有调用就会看到这个灵异现象。解决方法是预测脚本固定写两行model.eval() with torch.no_grad(): outputs model(images)另外保存模型时我不建议只存state_dict最好把类别名和预处理参数一起存进一个checkpointtorch.save({ model_state: model.state_dict(), classes: train_ds.classes, transform: str(transform), }, best_fire_cnn.ckpt)这样过一个月回来还能知道当时的类别顺序和预处理方式不至于在推理时因为classes顺序反了把fire和no_fire搞混。6. 模型上线前的验证与导出一张图一张图确认再转成能部署的格式训练完不等于结束部署前必须做两件事画混淆矩阵确认是否偏科转ONNX确认能上推理引擎。from sklearn.metrics import confusion_matrix, classification_report y_true, y_pred [], [] model.eval() with torch.no_grad(): for images, labels in val_loader: images images.to(device) outputs model(images) preds outputs.argmax(dim1).cpu().tolist() y_pred.extend(preds) y_true.extend(labels.tolist()) print(confusion_matrix(y_true, y_pred)) print(classification_report(y_true, y_pred, target_namestrain_ds.classes))如果混淆矩阵显示no_fire被大量判成fire说明模型偏保守宁误报不漏报这在消防场景可以接受但误报率太高运维会崩溃。我的习惯是把阈值从默认的0.5往上调比如调到0.7以上再统计误报与漏报的代价。消防场景里漏报一票否决实际工程往往做两路分类模型输出低置信度时告警高置信度时直接触发中间地带丢给视频流时序判断。导出ONNX是部署的关键一步。PyTorch模型在服务端可以直接用但边缘设备或者用OpenCV的DNN模块加载时ONNX几乎是最好的中间格式import torch.onnx model.eval() dummy torch.randn(1, 3, 224, 224).to(device) torch.onnx.export( model, dummy, fire_cnn.onnx, input_names[input], output_names[logits], dynamic_axes{input: {0: batch}, logits: {0: batch}}, opset_version11 )dynamic_axes声明batch维度可变这样单张图片预测和批量预测都可以用同一个onnx文件。opset_version11是兼容性比较好的版本很多推理框架对它的支持最完整。导出后用onnxruntime跑一遍验证输出是否和PyTorch一致才算真正完成。我个人做火灾识别项目到最后一定会留一个“场景冒烟测试集”放十几张从来没见过的火灾新闻图、路灯夜景、夕阳和厨房煎锅照片。模型在测试集上分数再高也要在这批图上一张张过一遍看置信度分布是否合理。这套方法帮我在好几个项目里避免了“交上去指标好看、现场一测就废”的尴尬。希望这篇笔记能把你的火灾识别项目从“解压zip”推到“训练出可用模型”这一步少走几个我走过的弯路。数据清理花一小时后面训练和调参能省一整天值得。本文还有配套的精品资源点击获取