火焰烟雾小数据集迁移学习实战:从240张图到可靠识别
发布时间:2026/10/5 5:21:04 作者:尧图编辑部 阅读量:1,286

简介这是一个面向图像分类任务的火焰、烟雾与正常场景识别数据集包含约240张已标注图片类别分为火焰、烟雾、正常三类适合用于火灾预警、安全监控等场景的深度学习实践。资源共243个文件压缩包约504KB主体为240张jpg图像已划分训练集与测试集目录同类图片集中存放便于直接加载训练另附1个json标注文件、1个Python可视化脚本及1张说明图片运行show脚本即可预览标注与分类效果。目前已112人学习/下载适合有一定CNN或目标检测基础的学习者快速上手。读者既可直接用于训练自己的分类模型也可结合作者博客中CNN分类网络或基于YOLOv5的分类项目对比不同网络的识别表现作为课程设计或算法实验的数据支撑。1. 240张火焰烟雾数据值得做但别急着开训手头拿到一套「火焰、烟雾、正常图像识别数据集约240张、已标注」第一反应往往是这也太小了能训出什么但你真正要解决的是生产环境里的火警早期预警——摄像头拍到的是正常场景还是已经冒烟起火模型要在几十毫秒内给出判断。240张做不了从零训练却足够做一次迁移学习的可行性验证先证明这套特征在有监督小样本下能否收敛、能否区分开烟雾和正常光线再决定要不要扩数据、上检测。这篇文章就沿着「数据体检 → 选型 → 训练 → 踩坑 → 进阶」把这条路走一遍适合正在做安全监控、消防预警、边缘盒子图像识别的从业人员也适合第一次拿小数据集练手的学生。2. 数据到手先别急着训练把240张图的“家底”摸清楚2.1 标注文件长什么样先解析再动手标注过的数据一般有两种形态要么是每个类别一个文件夹文件名即标签要么是一个 CSV/JSON/VOC XML 的标注文件记录图像路径与类别。拿到手第一件事不是开训练而是写脚本把标注读出来统计类别分布、检查坏图。import os import csv import json from collections import Counter from PIL import Image # 假设标注是 CSV列名: image_path, label def parse_csv_annotation(csv_path): samples [] with open(csv_path, r, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: samples.append((row[image_path], row[label])) return samples # 如果是 VOC 格式的 XML用 ElementTree 解析 import xml.etree.ElementTree as ET def parse_voc_xml(xml_path): tree ET.parse(xml_path) root tree.getroot() folder root.find(folder).text filename root.find(filename).text objects [] for obj in root.findall(object): name obj.find(name).text objects.append(name) # VOC 一个文件里可能有多个目标这里做分类任务取第一个主类别 return os.path.join(folder, filename), objects[0] data parse_csv_annotation(annotations.csv) print(总样本数:, len(data)) print(类别分布:, Counter(label for _, label in data)) # 坏图检查打不开、全黑、尺寸过小 bad [] for path, label in data: try: with Image.open(path) as im: w, h im.size if w 32 or h 32: bad.append((path, too_small)) except Exception as e: bad.append((path, corrupted)) print(异常图像:, len(bad), bad[:10])逻辑上先统一走 CSV 解析再用 PIL 逐一验证图像完整性。参数说明Image.open只是打开文件头不会真正解码全部像素要确认图像没有损毁最好在后续读取时用im.load()强制解码。这里不做的原因是遍历全部像素太慢先筛掉文件级异常就够了。类别分布用Counter一眼就能看清是否平衡——这一步能帮你判断后面训练要不要加类别权重。如果标注文件不是 CSV 而是 JSON把csv.DictReader换成json.load再按 key 取路径与标签即可结构大同小异。实际项目中我还遇到过标注文件里混着smoke、Smoke、fire_with_smoke这种同义不同名的情况解析完要先做一次标签归一化否则类别统计全乱。2.2 目录结构怎么摆Train / Val 划分与类别平衡图片分类训练最常见、也最不容易出错的目录形态是train/类别名/*.jpg与val/类别名/*.jpg。这样 PyTorch 的ImageFolder可以直接加载不用手写 Dataset。240 张数据切分时验证集不能太大否则训练集只剩一百多张所以我一般按照 8:2 划分同时用随机种子固定划分结果保证每次实验可复现。import os import shutil import random from collections import defaultdict random.seed(42) def split_by_class(samples, val_ratio0.2): by_label defaultdict(list) for path, label in samples: by_label[label].append(path) train_files, val_files [], [] for label, paths in by_label.items(): random.shuffle(paths) n_val max(1, int(len(paths) * val_ratio)) val_files.extend([(p, label) for p in paths[:n_val]]) train_files.extend([(p, label) for p in paths[n_val:]]) return train_files, val_files train_files, val_files split_by_class(data) print(训练集:, len(train_files), 验证集:, len(val_files)) def organize_to_folder(samples, dest_root): for src, label in samples: label_dir os.path.join(dest_root, label) os.makedirs(label_dir, exist_okTrue) dst os.path.join(label_dir, os.path.basename(src)) shutil.copy(src, dst) organize_to_folder(train_files, data/train) organize_to_folder(val_files, data/val)按类别先分组再划分避免随机切分时某一类全部落到训练集或验证集。参数说明val_ratio0.2对 240 张而言意味着每类约留 16 张给验证偏少但可接受再降低到 0.1 会让验证指标抖动剧烈8:2 是一个相对稳的中间值。random.seed(42)是复现的关键同一批数据多人协作时没有固定种子会出现“你跑你的、我跑我的”没法对齐的现象。补充一点如果原始数据本身带标注框在整理成分类目录后标注框信息就丢失了。做分类不需要框但后续要转检测时还得回头找原始标注所以整理后的副本单独放一份原始文件不要动。2.3 类别不平衡检查240张里“正常”可能占了一半真实项目拿到的数据几乎永远不平衡——正常画面最容易采集火焰次之烟雾最少。用上面的Counter统计后如果发现三类的比例到了 5:3:2意味着烟雾只有 48 张验证集里每类可能只有个位数准确率的波动会非常大。常见做法不是强行把数据补齐而是在训练里给少样本类别更高的权重。PyTorch 里直接用WeightedRandomSampler按样本数的倒数设置采样概率让每个 epoch 里少样本类别被抽到的次数接近多样本类别。from torch.utils.data import WeightedRandomSampler, DataLoader labels [label for _, label in train_files] label_counts Counter(labels) total len(labels) weights [1.0 / label_counts[label] for label in labels] sampler WeightedRandomSampler(weights, num_samplestotal, replacementTrue) train_loader DataLoader(dataset, batch_size16, samplersampler, num_workers2)给每个样本分配1 / 该类总数的权重分布越少的类别单样本权重越高。参数说明num_samplestotal表示每个 epoch 总采样次数与训练集大小一致replacementTrue允许同一张图在一个 epoch 里重复被抽到这是过采样实现的基础。采样器会打乱顺序所以DataLoader里shuffle要设为False避免双重打乱造成逻辑混乱。如果连训练集内部各类都相差 10 倍以上加权采样也救不回来唯一出路是回头补数据。小数据集最怕的不是总量少而是某一个类别只有十几张那无论怎么调权重都容易过拟合到那几张图的背景噪声上。3. 为什么这种小数据集我不用YOLO而是先上分类网络3.1 检测与分类的边界240张能做什么、不能做什么搜索热词里有一大把「yolov8训练自己的数据集」很多人的第一反应是火焰烟雾识别直接上 YOLO 检测框不是更好确实最终生产系统往往需要框出火焰区域、判断火势蔓延范围检测是终态。但一套 YOLO 检测数据集的标注成本远高于分类数据集每张图要画矩形框类别是fire还是smoke还要逐框确认。240 张图即使全标了框平摊到三类每类只有几十个目标训练出来的框位置会严重过拟合换一个机位的摄像头就抓不到目标。我的判断标准是先想清楚当前阶段要回答的问题。如果只是验证「这路摄像头拍的画面里烟雾和正常厂区光照能不能区分开」分类网络就够了。分类任务只需要图级标签240 张可以启动检测任务需要目标级标签240 张连起步线都够呛。实际部署时常见做法是先用分类网做粗筛把疑似帧截下来再跑检测框定位两级串联反而比直接上检测更稳。3.2 迁移学习是唯一正确的起跑姿势240 张从零随机初始化训练 ResNet结果基本是 loss 降不下去val acc 在 33% 上下反复横跳这就是小数据集的黑匣子效应——模型根本没有足够的监督信号去学习边缘、纹理、形状这些底层特征。迁移学习的逻辑是ImageNet 预训练权重里已经装好了通用的特征提取能力火焰的橙红色纹理、烟雾的半透明边缘这些底层视觉模式在 ImageNet 的千万张自然图像里早就见过。你只需要把最后几层分类头换掉在 240 张图上微调。后端的参数可以少学一点前端的 backbone 参数用很小的学习率带动。一个直观比喻预训练权重相当于一个已经学会看世界的成年人你要教他区分三种新东西只需要在顶层加几句描述而不是让他重新睁开眼。3.3 模型选型ResNet18 还是 MobileNetV3小数据集不需要大模型。ResNet50 参数量 2500 万ResNet18 只有 1100 万在 240 张数据上两者的精度差距几乎可以忽略但 ResNet18 在 CPU 上推理速度快一倍训练时显存占用也更低。如果最终要部署到摄像头边缘盒子MobileNetV3 的参数量只有 ResNet18 的约三分之一单张推理在树莓派级别的设备上也能跑到几十毫秒考虑优先用 MobileNetV3 做原型验证后期再蒸馏。模型参数量Top-1 参考精度CPU 推理相对耗时小数据集适用性ResNet18约 1100 万约 70%1x最稳首选MobileNetV3-Small约 250 万约 68%0.4x轻量适合边缘部署ResNet50约 2500 万约 76%2.5x不建议240张喂不饱ResNet18 在 PyTorch 里有torchvision官方预训练权重加载方便不容易在版本上翻车。MobileNetV3 的预训练权重同样官方提供但需要注意输入尺寸是 224×224 还是 192×192不同版本不一致。我的选择倾向先用 ResNet18 跑通全流程等验证集精度稳定了再换 MobileNetV3 做部署侧优化两头兼顾。4. 用ResNet18在本地跑通火焰/烟雾/正常三分类训练4.1 数据增强小数据集的救命稻草240 张图直接训练模型会把背景里的电线杆、厂房窗框当作判别特征。数据增强的本质是人为制造样本多样性让模型学到「火焰是橙红色的、烟雾是灰白半透明的、正常场景没有这些」而不是死记某几张图的背景。from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.3), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_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]) ])RandomResizedCrop是这里最关键的一步随机裁掉一部分背景再缩放到 224模型被迫关注目标本身而不是整张图的场景布局。参数说明scale(0.7, 1.0)控制裁剪面积占原图比例0.7 意味着允许裁掉 30% 的边缘区域大一些更激进、也能更强地抗过拟合但火焰目标本身可能被裁掉一半ColorJitter(brightness0.3, contrast0.3, saturation0.3)是三通道颜色扰动用来模拟白天黑夜不同光照下火焰和烟雾的颜色漂移。Normalize的均值方差必须用 ImageNet 的标准值因为预训练权重是按这个分布训练的用错会导致 loss 一开始就震荡。4.2 训练配置学习率、batch size、epoch 的合理设定小数据集的训练参数跟大数据集截然不同。batch size 建议 8 或 16240 张的训练集每 epoch 只有 15 到 30 个 batchbatch 太大梯度更新次数太少模型还没来得及学就过拟合了。学习率要从1e-4起步而不是常用的1e-3因为预训练权重已经很接近最优解区域用大学习率一步就把特征冲坏了。import torch import torch.nn as nn from torchvision import models model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_ftrs model.fc.in_features model.fc nn.Linear(num_ftrs, 3) # 3 类: fire / smoke / normal # 骨干网络用小学习率微调分类头用大学习率快学 optimizer torch.optim.AdamW([ {params: model.conv1.parameters(), lr: 1e-5}, {params: model.layer1.parameters(), lr: 1e-4}, {params: model.layer2.parameters(), lr: 1e-4}, {params: model.layer3.parameters(), lr: 1e-4}, {params: model.layer4.parameters(), lr: 1e-4}, {params: model.fc.parameters(), lr: 1e-3}, ], weight_decay1e-4) criterion nn.CrossEntropyLoss() scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max40) best_acc 0 for epoch in range(40): model.train() running_loss 0.0 for images, labels in train_loader: optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() # 验证 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: outputs model(images) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) acc correct / total print(fEpoch {epoch1:03d} | Loss {running_loss/len(train_loader):.4f} | Val Acc {acc:.4f}) if acc best_acc: best_acc acc torch.save(model.state_dict(), best_fire_smoke.pth)分层学习率是这段代码的精髓。conv1和layer1学习率最低只做微调fc层是完全随机初始化的新层给1e-3让它快速收敛。参数说明T_max40与epoch数一致让学习率从初始值余弦衰减到接近 0weight_decay1e-4加一点 L2 正则防止过拟合小数据集上不要超过1e-3过大反而压低了有效特征的规模。保存模型用best_acc判断而不是最后一个 epoch因为训练后期往往出现过拟合最后一个 checkpoint 反而不如中间某个 epoch 的结果。4.3 从日志判断训练没有翻车跑起来之后要学会读日志。一个健康的训练过程是前 5 个 epoch 的 train loss 从 1.2 左右快速降到 0.8val acc 从 40% 左右稳步爬到 70% 以上第 10 到 20 个 epoch 增速放缓loss 降到 0.4 附近val acc 在 80% 上下波动。如果 train loss 降了但 val acc 始终停在 33% 附近大概率是标签错位或者预处理不对如果 train loss 降到 0.1 以下而 val acc 还在 60% 徘徊过拟合已经开始了需要增加增强强度或者提前停止。我自己习惯把每个 epoch 的 loss 和 acc 存成 CSV训练完画一条曲线比盯着终端输出直观得多。5. 小数据集实训最容易踩的四个坑5.1 踩坑一验证集 acc 很高但新摄像头画面几乎全错现象训练时 val acc 到 95%换一路新摄像头的画面做测试准确率跌到五成以下。原因小数据集最容易出现隐性的数据泄漏。比如同一场景连续拍摄的多帧画面被同时分进训练集和验证集验证集里出现了与训练集几乎相同的背景和光照模型实际记住的可能是背景而不是火焰烟雾。解决划分数据集之前先按「场景或拍摄批次」分组保证同一来源的图像只出现在训练集或验证集里而不是随机打散。240 张数据如果来自 3 个不同场景就应该按场景组划分而不是按单张图随机划。5.2 踩坑二train loss 降到 0.1val acc 卡在 60% 不动现象训练集 loss 一路走低但验证集准确率上不去两者的差距越拉越大。原因过拟合 类别不平衡的双重作用。模型把训练集里烟雾类样本的背景特征比如灰白色的天空学成了烟雾本身验证集里的烟雾出现在不同背景时就认不出来。解决增强里加入更激进的随机裁剪甚至用RandomErasing随机抹掉图像的一部分来强迫模型关注局部特征同时把学习率降到 1e-5 再做 10 个 epoch 的收尾训练。这也解释了为什么要保存best_acc的 checkpoint——过拟合后期那个中间状态的模型反而是泛化最好的。5.3 踩坑三烟雾和正常类混淆严重火焰倒是很准现象混淆矩阵里smoke - normal的误判率特别高fire类的识别准确率却接近满分。原因三类样本本身分布不均——火焰有鲜明的橙红色特征强烟雾是半透明的灰色在弱光下与正常场景的阴影高度相似。更关键的是烟雾训练样本太少模型没有见过足够多的烟雾形态。解决短板补数据优先于调参。我的常用做法是把模型在验证集上预测错误的烟雾图全部挑出来人工分析到底是因为目标太小还是对比度太低然后针对性地做数据扩充。5.4 踩坑四训练时换了一台机器精度突然降了 5 个点现象代码完全一样换 GPU 或换 CPU 推理同一张图的预测结果变了。原因不是玄学而是图像预处理差异。有些机器用 OpenCV 读图返回 BGR 通道有些用 PIL 返回 RGB同一张 JPEG 在不同解码库下像素值会有细微差异。解决把数据加载、Resize 插值方式PIL 默认BILINEARPyTorchResize默认也是双线性但要显式指定、归一化参数全部固定任何设备跑之前先对同一张输入图做一次逐像素比对确认预处理链路完全一致再谈模型部署。这个坑我在实际项目中踩过排查了两天才定位到是一台机器用了 OpenCV 读图。6. 把240张用到极致的三个进阶技巧6.1 用混淆矩阵和错误样本反推数据短板训练完成只是开始。我会把验证集里每个错误预测的样本单独存到一个文件夹按「真实类别 / 预测类别」命名然后逐一肉眼浏览。看 20 张错图花 5 分钟收获比调 5 个小时参数都大。同时打印三类的精确率和召回率如果烟雾的召回率只有 40%说明漏报太多这时调阈值比调学习率直接有效——把烟雾类的预测概率阈值从 0.5 降到 0.3牺牲一点正常类的误报率换取更少的漏报这在消防场景里是划算的权衡。6.2 用高置信度策略找到下一批可标注数据240 张只是第一桶金。用训好的模型去跑历史监控录像注意这里指自有服务器的录像文件不涉及任何网络链路筛出模型对某一类预测置信度超过 0.9 的帧当作候选伪标注数据。人工复核这些帧——重点看置信度高的原因到底是目标清晰还是背景残留——把确认正确的帧加入训练集。这个半监督循环每轮能扩充几十张高质量样本比重新从零采集标注效率高一个量级。我自己在做类似项目时用这个办法把数据集从 200 多张扩到 800 张误检率降了一半。6.3 用模型蒸馏保住精度、压小体积如果最终目标是部署到低成本边缘设备ResNet18 可能还是嫌大。常见做法是以训练好的 ResNet18 或者更大的网络做 teacher以 MobileNetV3-Small 做 student用软标签蒸馏teacher 输出的概率分布比 one-hot 硬标签带更多信息告诉 student「正常类和烟雾类其实有点接近、火焰类和它们差异很大」。student 模型蒸馏后体积可以压到原来的四分之一精度只掉两三个点。240 张数据做蒸馏没什么特别的门槛PyTorch 官方蒸馏示例改一下数据路径就能跑。最后说一条我自己的习惯小数据集项目的日志和配置永远比模型权重值钱训练完把预处理代码、划分种子、每个 epoch 的指标都归档好三个月后回来看能省下大把重建环境的时间。希望这些经验能帮你在同样的数据规模下少走几段弯路。本文还有配套的精品资源点击获取