ARTICLE DETAIL

资讯详情

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

基于CNN的火灾识别:PyTorch完整项目复现与部署指南

基于CNN的火灾识别:PyTorch完整项目复现与部署指南 简介这份压缩包提供一套基于CNN深度学习的火灾识别完整代码使用PyTorch实现内置标注好的数据集和3个Python脚本适合入门图像分类或做火灾检测练习的开发者也可以通过PyQt界面直观查看识别效果。压缩包共250个文件以200张png、44张jpg图片为主对应火灾/非火灾样本3个txt文件包含环境安装说明与训练路径记录3个py文件分别负责数据集文本生成、模型训练和可视化UI整体大小约173.55MB已有170人学习下载。内容覆盖从数据准备到推理的完整链路预处理会在图片短边补灰边并旋转角度完成正方形化和数据增强训练结束后保存模型日志记录每个epoch的验证集损失与准确率PyQt界面可加载本地图片进行识别。整个项目可直接作为深度学习和图像分类的练习范本按说明配置环境即可运行。1. 基于CNN的火灾识别一个完整PyTorch项目的可复现路径搞火灾识别项目很多人一开始就把精力砸在模型结构上觉得 CNN 不够新、不够深结果数据没处理好训练一轮下来验证集准确率还在 60% 晃悠。这份基于 PyTorch 的 CNN 火灾识别资源恰好把最容易被忽略的部分——数据预处理、训练流程、可视化界面——完整地串成了一条线先对图片做灰边填充和旋转增强再按类别生成训练文本最后用 PyTorch 训练出二分类模型并通过 PyQt5 界面加载图片识别。它不追求模型复杂度而是让你用最少的环境依赖把一个可演示、可继续迭代的火灾识别流程跑通。不管你是刚接触深度学习的本科生还是想快速做个消防预警演示的工程师这套代码都值得照着过一遍。下面我会按实际运行顺序把每个脚本拆开讲顺带把最容易踩的坑列出来而不是只给你看一个“读 README 就能跑”的假象。2. 环境与数据准备图片在进入 CNN 之前都经历了什么2.1 项目结构与运行顺序拿到压缩包之后第一件事不是急着运行而是先看清文件组织方式。这个项目的结构非常清晰解压后你会看到三个 Python 脚本和一个数据集文件夹外加一个requirement.txt环境依赖文件。核心运行顺序是固定的先跑 01 脚本生成数据集文本再跑 02 脚本训练模型最后跑 03 脚本启动 PyQt5 界面。如果把顺序颠倒02 脚本会读不到训练列表03 脚本加载不到模型文件报错信息还不一定友好。一般解压后的目录结构长这样项目根目录/ ├── 01_数据集文本生成制作.py ├── 02_深度学习模型训练.py ├── 03_pyqt_ui界面.py ├── requirement.txt └── 数据集/ ├── fire/ │ ├── fire.1.jpg │ ├── fire.13_rotated45.jpg │ └── ... └── non_fire/ ├── non_fire.1.jpg ├── non_fire.13_rotated45.jpg └── ...注意看文件名里的_rotated45和_flip后缀这是数据增强留下的痕迹。rotated45表示该图片由原图旋转 45 度得到flip表示水平翻转得到。通过这种方式同一张原始图片派生出了多个训练样本这在样本量有限时是非常实用的扩增手段。我对这类项目的习惯是先看数据集图片的命名规律再决定要不要沿用原作者的预处理方式。如果图片后缀混乱01 脚本生成的文本也会跟着出错这就是为什么第 3 章会重点拆解脚本逻辑。2.2 环境依赖与 PyTorch 安装requirement.txt里通常会列出项目需要的 Python 包常见组合大致是torch、torchvision、numpy、Pillow、opencv-python、PyQt5这几项。安装命令很简单pip install -r requirement.txt但这里有一个必须单独处理的问题PyTorch 的安装方式和 CUDA 版本强相关。如果你直接pip install torch默认装到的是 CPU 版本训练一个小型 CNN 模型可能还感觉不明显但一旦图片分辨率偏高或 epoch 数量增加训练速度会慢到让你怀疑人生。我一般会先确认显卡支持哪个 CUDA 版本再去 PyTorch 官网选对应安装命令# 以 CUDA 11.8 为例实际版本以你本机为准 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118如果你的机器没有独立显卡CPU 版本也能跑只是训练时间会拉长。这个项目本身是二分类、图片数量级在几百到几千张CPU 训练虽然慢但不会出现“等一天都训练不完”的极端情况。环境装好后建议跑一句简单的验证命令python -c import torch; print(torch.__version__, torch.cuda.is_available())返回True表示 CUDA 可用后面训练时会自动调用 GPU。如果返回False程序也能运行只是设备会退回 CPU。这个环节最容易翻车的是 Python 版本和 PyTorch 版本不匹配表现为import torch时报 DLL 加载失败或找不到指定模块此时优先考虑重装对应版本的 PyTorch而不是盲目升级 Python。2.3 预处理策略为什么用灰边填充而不是直接拉伸这个项目在预处理上有一个很值得学习的细节当你把一张宽高比例不是 1:1 的图片送入 CNN 时最常见的做法是直接resize到固定尺寸但这样做会把图片拉伸变形物体的长宽比例被破坏CNN 提取到的特征也会失真。这个项目采用的办法是在较短边两侧填充灰色像素先把图片变成正方形再统一缩放。如果图片本来就是正方形则不填充。这一步的实际意义在于对火灾这种“颜色和纹理特征远大于形状特征”的目标拉伸变形的影响可能没那么致命但一旦你以后把同一套代码迁移到行人检测、车辆分类等形状敏感任务时灰边填充的优势就会立刻体现出来。此外填充灰色一般用 114、128 这类中性灰度值不会引入额外的颜色分布偏移也不容易干扰 BatchNorm 层的统计量。数据增强方面项目用的是旋转 45 度加水平翻转。旋转能模拟摄像头安装角度不统一的情况水平翻转则直接让样本量乘以 2。相比随机裁剪和颜色抖动这两种增强方式几乎不会改变火焰的本质视觉特征非常适合火灾识别这种场景。我在实际复现时还会额外加一点随机亮度扰动后面第 6 章会展开讲。3. 训练链路拆解从 01 脚本生成文本到 02 脚本保存模型3.1 01 数据集文本生成制作.py路径扫描与标签归档这个脚本的目标很简单把数据集文件夹下每个类别的图片路径和对应标签写入纯文本文件供后续训练脚本读取。它本质上是在做“数据集描述文件”的生成工作类似我们熟悉的train.txt和val.txt格式。核心逻辑拆开来看是这样一段流程import os import random dataset_root 数据集 classes [fire, non_fire] train_ratio 0.8 train_lines [] val_lines [] for label, cls in enumerate(classes): cls_dir os.path.join(dataset_root, cls) img_names os.listdir(cls_dir) # 按比例切分训练集和验证集 split_idx int(len(img_names) * train_ratio) # 这里用随机打乱替代顺序切分避免同类样本扎堆 random.shuffle(img_names) train_names img_names[:split_idx] val_names img_names[split_idx:] for name in train_names: # 标签用数字表示fire0, non_fire1 train_lines.append(f{os.path.join(cls_dir, name)} {label}) for name in val_names: val_lines.append(f{os.path.join(cls_dir, name)} {label}) with open(train.txt, w, encodingutf-8) as f: f.write(\n.join(train_lines)) with open(val.txt, w, encodingutf-8) as f: f.write(\n.join(val_lines))这里有两个关键点需要注意。第一标签的类别顺序直接决定了模型输出的 0 和 1 分别代表什么后续 03 脚本的 UI 显示也要保持同一个映射关系。第二随机打乱这步非常重要如果原始文件夹里图片是按时间或场景顺序排列的不做随机切分会导致训练集和验证集的场景分布严重不均衡。脚本执行完后会在项目根目录生成train.txt和val.txt。每行内容格式是“图片路径 空格 标签”。你可以用文本编辑器打开确认路径里的分隔符是正斜杠还是反斜杠。如果在 Windows 下运行os.path.join生成的路径可能带反斜杠而后续训练脚本在用os.path.split()或字符串拼接时一旦处理不当很容易出现路径解析错误。我自己的经验是如果遇到这类问题优先在 01 脚本里把路径统一替换成正斜杠一劳永逸。3.2 02 深度学习模型训练.py模型构建与训练主循环02 脚本是这个项目的核心。它读取上一步生成的train.txt和val.txt构建自定义 Dataset然后进入训练循环。我复现时发现项目采用的模型结构是典型的 CNN 二分类网络若干卷积层提取特征接全连接层输出两个类别的置信度。相比直接套用 ResNet18 做迁移学习这种轻量级网络在小数据集上的优势是训练速度快、不易过拟合劣势是准确率上限可能略低。下面是我按项目逻辑整理的训练主循环框架实际代码与此基本一致import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import Dataset, DataLoader from PIL import Image import torchvision.transforms as transforms class FireDataset(Dataset): def __init__(self, txt_path, transformNone): self.samples [] with open(txt_path, r, encodingutf-8) as f: for line in f: path, label line.strip().split() self.samples.append((path, int(label))) self.transform transform def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] img Image.open(path).convert(RGB) if self.transform: img self.transform(img) return img, label # 数据变换先填充灰边变正方形再缩放到网络输入尺寸 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_dataset FireDataset(train.txt, transformtransform) val_dataset FireDataset(val.txt, transformtransform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse)FireDataset的核心就是__getitem__按行解析路径和标签用PIL.Image.open()读图然后经过预设的 transform 变成网络需要的张量格式。这里要强调一个隐藏细节Resize((224, 224))是在灰边填充之后执行的如果省略了填充步骤宽图会被直接压缩变形训练出来的模型鲁棒性会差不少。训练主循环是典型的 PyTorch 写法device torch.device(cuda if torch.cuda.is_available() else cpu) model SimpleFireCNN(num_classes2).to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3) best_acc 0.0 epochs 50 for epoch in range(epochs): model.train() train_loss 0.0 for inputs, labels in train_loader: inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() train_loss loss.item() # 每个 epoch 结束跑一次验证集 model.eval() correct 0 total 0 val_loss 0.0 with torch.no_grad(): for inputs, labels in val_loader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) loss criterion(outputs, labels) val_loss loss.item() _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() acc correct / total print(fEpoch {epoch1}/{epochs}, Train Loss: {train_loss/len(train_loader):.4f}, fVal Loss: {val_loss/len(val_loader):.4f}, Val Acc: {acc:.4f}) if acc best_acc: best_acc acc torch.save(model.state_dict(), best_model.pth)几个值得琢磨的参数配置batch_size32是比较中庸的选择显存不够时可以降到 16lr1e-3搭配 Adam 对这个小项目来说足够稳定不需要额外做学习率预热epochs50看起来多但每次验证集准确率提升后才覆盖保存最佳模型所以训练结束时会得到一个在验证集上表现最好的权重而不是最后一个 epoch 的权重。这个“保存最优模型而非最后模型”的习惯是这个项目做得最专业的地方。3.3 训练日志与结果验证训练完成之后项目会输出 log 日志文件里面记录了每个 epoch 的验证集损失值和准确率。这不仅是给你看的进度报告也是判断模型是否收敛的依据。常见健康曲线是训练损失持续下降验证损失先降后平验证准确率逐步稳定在 90% 以上。如果验证准确率在某个 epoch 后开始波动明显比如 70% 和 95% 交替出现说明学习率可能偏大或者验证集样本太少、分类不够稳定。我一般会把学习率降到 3e-4 并重新训练一轮看曲线是否平滑。还有一种情况是训练损失很低但验证准确率上不去这基本就是过拟合了需要在数据增强上做文章而不是调整模型结构。best_model.pth保存的是模型权重不是完整模型。这意味着在 03 脚本里加载时需要先实例化一个结构相同的模型再调用load_state_dict()。如果两个脚本对模型的定义不一致加载时会直接报Missing key(s) in state_dict之类的错误这一点第 5 章会重点讲。4. PyQt5 可视化界面把训练好的模型变成能点的按钮4.1 UI 脚本的加载逻辑03 脚本启动后会弹出一个 PyQt5 窗口界面上通常包含“选择图片”按钮、“识别”按钮、图片显示区域和结果标签。这里的关键点不是 UI 布局本身而是模型加载的部分。常见的实现方式是先重建一个与训练时结构完全相同的 CNN 类然后加载best_model.pthmodel SimpleFireCNN(num_classes2) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval()map_locationcpu的作用是即使训练时用了 GPUUI 脚本在无显卡环境下也能加载模型权重。如果你在部署机上没有 CUDA却直接torch.load(best_model.pth)很可能报错找不到设备。这是一个非常典型的部署坑。4.2 单张图片识别流程用户在界面上选择一张图片后后端处理流程基本是固定的读图 → 灰边填充 → 缩放到 224×224 → 转张量 → 归一化 → 前向传播 → 取 argmax → 映射回类别文字。下面这段是我整理的推理逻辑def predict_image(image_path, model, transform): img Image.open(image_path).convert(RGB) # 灰边填充保证送入网络前图片为正方形 img pad_to_square(img, fill(114, 114, 114)) img transform(img).unsqueeze(0) # 加 batch 维度 with torch.no_grad(): outputs model(img) _, pred torch.max(outputs, 1) return fire if pred.item() 0 else non_fire这里给刚接触 PyTorch 的朋友提个醒unsqueeze(0)是必不可少的。模型的输入维度是(batch_size, channel, height, width)单张图片只有(channel, height, width)不补上 batch 维模型会直接报维度不匹配。如果不熟悉 PyTorch 的张量操作这是 SDK 使用中最容易卡住的地方之一。4.3 把 UI 接到摄像头或批量图片上UI 脚本提供的按钮加载方式适合单张图片验证效果。如果你想做实时演示比如对着摄像头预测那可以直接在 03 脚本里加一个 OpenCV 的读取循环把每一帧丢进predict_image里再在 UI 上刷新结果。这个改造不会影响已有功能只需要注意帧率控制避免 CPU 满载。思路是import cv2 cap cv2.VideoCapture(0) while True: ret, frame cap.read() # 对 frame 做同样的预处理后送入模型 # 在窗口上绘制识别结果 if cv2.waitKey(1) 0xFF ord(q): break对于批量识别可以直接遍历文件夹里的所有图片把每张图的预测结果写进 CSV 文件方便统计准确率和误报率。这两种扩展方式都不需要改动模型训练代码属于 UI 脚本的增量修改对原项目的侵入性很小。5. 常见问题排查与避坑从环境安装到模型加载的血泪经验5.1 问题一pip install PyQt5 之后启动 UI 报错现象运行 03 脚本后窗口一闪而过控制台提示Qt platform plugin windows could not be found或者直接崩溃。原因多数情况下是 PyQt5 和 PyQt5-Qt5 的版本不匹配或者系统缺少 Visual C 运行库。另一个常见情况是 Python 3.10 及以上版本与某些旧版 PyQt5 的兼容问题。解决重新安装 PyQt5 的完整依赖并保证版本一致pip uninstall PyQt5 PyQt5-Qt5 pip install PyQt55.15.7如果问题依旧检查是否缺少platforms/qwindows.dll文件必要时把 PyQt5 的site-packages/PyQt5/Qt5/plugins/platforms路径加到环境变量QT_QPA_PLATFORM_PLUGIN_PATH中。5.2 问题二训练过程中报FileNotFoundError现象02 脚本运行到中途提示某个图片路径找不到训练直接中断。原因01 脚本生成 txt 时用了相对路径但你在另一个工作目录下运行 02 脚本导致相对路径失效。还有可能是 Windows 下路径分隔符处理不当。解决我的习惯是在 01 脚本生成路径时就直接使用绝对路径或者在 02 脚本开头统一拼接项目根路径。最简单的方式是保证三个脚本都在项目根目录下运行不要从其他目录跳转执行。如果你非要在别的目录下跑可以在 02 脚本里加一句import os os.chdir(os.path.dirname(os.path.abspath(__file__)))这句话会把工作目录强制切到脚本所在目录路径问题基本能规避一半。5.3 问题三CUDA 可用但训练速度异常慢现象torch.cuda.is_available()返回True但训练时 GPU 利用率一直在 0% 10% 徘徊速度甚至不如 CPU。原因要么是模型和输入数据没有正确搬到 GPU 上要么是DataLoader的num_workers设置不当导致数据加载成为瓶颈。很多初学 PyTorch 的人只调用.to(cuda)移动模型却忘了移动训练数据。解决检查训练循环里inputs, labels inputs.to(device), labels.to(device)是否真的执行了。如果确认已经搬移就尝试把DataLoader的num_workers从 0 调到 2 或 4同时把pin_memoryTrue打开。但这台机器如果本身是旧款显卡或共享显存那就只能接受慢的现实把数据增强频率降下来会显得“快”一些。5.4 问题四模型识别准确率很高但实际场景乱报现象验证集准确率 95% 以上一放到现场测试非火灾图片频繁误报或者远处的小火苗识别不出来。原因这是典型的数据分布不一致问题。训练集里的火灾图片大多是近距离、大火焰、纯色背景而真实场景中火焰面积可能只占画面很小一部分或者有大量干扰光斑。CNN 学到的可能是“大块橙色区域”这种特征而不是“烟雾与火苗”的语义特征。解决扩充数据集时引入多尺度样本。具体做法是对部分图片按随机比例裁剪缩放模拟远近变化再增加一些带有橙色灯光的夜间街道图片作为负样本逼着模型学会区分“真正的火”和“看着像火的东西”。这个思路是从“让模型记住正例”转向“让模型理解类别边界”属于数据策略层面的调整不用改任何代码。5.5 问题五加载模型权重时报 key 不匹配现象03 脚本执行load_state_dict时爆出一长串Missing key(s)和Unexpected key(s)错误。原因模型结构定义不一致。训练时的 CNN 类里卷积层命名是conv1、conv2而 UI 脚本里写的类改成了layer1、layer2键名对不上。解决复制模型定义代码确保 02 和 03 脚本用的是同一个类。检查方法是打印出model.state_dict().keys()看键名是否一致。如果只多了一层 Dropout 或只改了个激活函数也建议直接重新训练或均匀初始化对应层不要手动改权重名字那样很容易引入隐藏 bug。6. 进一步扩展从二分类演示走向更可靠的识别方案当你把这个项目完整跑通之后我建议再做三件事让这个小模型真正进入可用状态。第一件是迁移学习实验。把SimpleFireCNN换成在 ImageNet 上预训练过的 ResNet18冻结前几层只微调后面几层通常能把验证集准确率再往上推一到两个百分点。具体操作是在初始化模型时把pretrainedTrue打开然后把最后一个全连接层换成nn.Linear(512, 2)。迁移学习的优势在于即便你的数据集只有几百张图片预训练权重也能让特征提取器一开始就处在合理状态收敛速度和最终准确率都会更好。第二件值得做的事是混淆矩阵分析。训练完成后把验证集所有图片过一次模型统计哪些火灾图片被误判成了非火灾哪些反而误报。这个分析的价值在于你能直观看到模型的错误模式。如果误报主要出现在偏暗的室内场景说明负样本中缺少这类图片如果漏报主要出现在小面积火焰说明你对小目标特征不敏感。这条路径比盲目堆数据更高效。第三件是把模型完全离线部署到一台没有 Python 环境的机器上。用torch.jit.script或onnx.export将模型导出再配合 OpenCV 的 DNN 模块做推理这样就不再依赖 PyTorch 运行时。导出 ONNX 的常见做法是dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export(model, dummy_input, fire_cnn.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}})导出后可以用onnxruntime做一次快速验证确认前后结果一致。这一步对做嵌入式部署或上位机集成的工程师会非常实用因为工业现场的工控机往往不希望你为了一个识别功能装一整套 Python 环境。最后说一个我自己的习惯每次训练完我都会把训练集里随机挑出的样本和验证集里容易分错的样本单独存一个文件夹定期翻一翻。这个方法没有多高深但能逼着我审视数据质量问题而不是只看准确率数字。从那以后我每次跑这种小型视觉识别项目都会强制自己过一遍“模型能识别不代表它理解”——宁可多花时间在数据和部署细节上也不要让模型成为一个只会说“我准了”的黑匣子。希望这个项目能帮你在火灾识别这条路上少踩一些坑做出真正能落地的东西。本文还有配套的精品资源点击获取
返回列表