☰
基于CNN的火灾识别:PyTorch完整项目复现与部署指南
2026/10/1 18:22:50 网站建设 项目流程

简介:这份压缩包提供一套基于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: # 标签用数字表示:fire=0, non_fire=1 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", encoding="utf-8") as f: f.write("\n".join(train_lines)) with open("val.txt", "w", encoding="utf-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, transform=None): self.samples = [] with open(txt_path, "r", encoding="utf-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", transform=transform) val_dataset = FireDataset("val.txt", transform=transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)

FireDataset的核心就是__getitem__:按行解析路径和标签,用PIL.Image.open()读图,然后经过预设的 transform 变成网络需要的张量格式。这里要强调一个隐藏细节:Resize((224, 224))是在灰边填充之后执行的,如果省略了填充步骤,宽图会被直接压缩变形,训练出来的模型鲁棒性会差不少。

训练主循环是典型的 PyTorch 写法:

device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = SimpleFireCNN(num_classes=2).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-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(f"Epoch {epoch+1}/{epochs}, Train Loss: {train_loss/len(train_loader):.4f}, " f"Val 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_size=32是比较中庸的选择,显存不够时可以降到 16;lr=1e-3搭配 Adam 对这个小项目来说足够稳定,不需要额外做学习率预热;epochs=50看起来多,但每次验证集准确率提升后才覆盖保存最佳模型,所以训练结束时会得到一个在验证集上表现最好的权重,而不是最后一个 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.pth:

model = SimpleFireCNN(num_classes=2) model.load_state_dict(torch.load("best_model.pth", map_location="cpu")) model.eval()

map_location="cpu"的作用是:即使训练时用了 GPU,UI 脚本在无显卡环境下也能加载模型权重。如果你在部署机上没有 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 PyQt5==5.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_memory=True打开。但这台机器如果本身是旧款显卡或共享显存,那就只能接受慢的现实,把数据增强频率降下来会显得“快”一些。

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,冻结前几层只微调后面几层,通常能把验证集准确率再往上推一到两个百分点。具体操作是在初始化模型时把pretrained=True打开,然后把最后一个全连接层换成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 环境。

最后说一个我自己的习惯:每次训练完,我都会把训练集里随机挑出的样本和验证集里容易分错的样本单独存一个文件夹,定期翻一翻。这个方法没有多高深,但能逼着我审视数据质量问题,而不是只看准确率数字。从那以后,我每次跑这种小型视觉识别项目,都会强制自己过一遍“模型能识别,不代表它理解”——宁可多花时间在数据和部署细节上,也不要让模型成为一个只会说“我准了”的黑匣子。希望这个项目能帮你在火灾识别这条路上少踩一些坑,做出真正能落地的东西。

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

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

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

立即咨询