简介:这是面向图像分类初学者的三分类标注数据集,包含火焰、烟雾、正常三类实景图片,约240张已标注样本,并完成训练集与测试集划分,可直接用于CNN或YOLOv5分类模型的训练与效果验证。压缩包内共243个文件,主体为240张jpg格式图片,另附1个Python脚本(show.py)用于可视化数据集,1张png示例图便于快速预览,1个json文件保存类别划分与标注信息,整体体积仅504KB,轻量易部署。资源发布后已有112人学习浏览,适合需要真实火情场景数据进行火灾预警、烟雾检测等分类实验的开发者。借助配套可视化脚本,使用者无需额外标注即可快速了解数据分布与图像内容,并可直接对接博主后续的CNN分类或YOLOv5分类项目,节省数据整理时间,专注模型设计与调优。
1. 240张火焰烟雾图像数据集:小样本图像识别怎么做才不翻车
做工业视觉这几年,最常被问的不是“哪个模型精度高”,而是“手头就一两百张已标注图片,能不能直接拿去训练深度学习图像识别模型”。这套火焰、烟雾、正常图像识别数据集正好属于这种情况——约240张,三类,带标注。数量不大,但真实场景里这种小数据样本才是常态:机房摄像头拍到一段烟雾、厂区角落出现火光、巡检图上多了一团模糊的白色区域,能拿到的先验样本往往就这么点。
用这类数据集,最忌讳拿个ResNet从头开始训,更别一上来就套YOLOv8跑目标检测。240张图喂给大网络,验证集准确率能到98%,部署到真实摄像头里立刻现原形。正确做法是把它当分类任务先跑通,用预训练模型做迁移学习,再把数据增强、分层抽样、类别权重这些手段全部用上。这篇文章就按这个顺序拆开讲清楚:数据怎么看、模型怎么训、参数怎么调、哪些坑必须绕开。适合手里有类似小标注集、想快速验证可行性,或者做毕业设计、比赛Demo的人直接照着做。
2. 先看数据再谈模型:火焰、烟雾、正常图像的构成与标注
2.1 240张小数据集的典型分布
拿到任何数据集,第一步永远是打开目录看分布,而不是急着写训练脚本。火焰、烟雾、正常三类图像识别任务里,240张通常不是均匀分配,更常见的是火焰100张、烟雾70张、正常70张,或者火焰80张、烟雾80张、正常80张。有的还会包含少量接近误判的“难例”,比如红色灯光下的管道、白色蒸汽、夜晚的橙色路灯,这些图对模型很关键,直接影响边界划分。
以最常见的100/70/70分布来算,火焰占41.7%、烟雾占29.2%、正常占29.2%,类别不平衡已经存在。如果按默认的方式随机切分训练集和验证集,烟气类还可能分配更少。给这类小数据集写脚本时,最先要处理的就是这个不平衡问题,后续所有的epoch设置、batch组成、评估指标都要围绕它来设计。
另外要确认标注文件的实际形态。火焰、烟雾、正常图像识别数据集的“已标注”一般指两种形式:一种是目录结构式标注,即火焰/、烟雾/、正常/三个文件夹,文件夹名即标签,这是分类任务最常见的方式;另一种是JSON或XML文件,记录每个目标的边界框坐标,主要用于目标检测。如果只有第一种,那就是个纯分类数据集,直接用torchvision的ImageFolder就能读。如果是第二种,则已经是PASCAL VOC或YOLO格式,可以走目标检测流程,但240张数据量对检测模型来说更吃紧,至少要再配合其他来源的负样本一起扩。
2.2 标注格式与目录结构:先确认手里拿的是什么
这份数据集如果按目录分类,结构大致是这样:
dataset/ ├── train/ │ ├── fire/ 火焰图像,约80张 │ ├── smoke/ 烟雾图像,约56张 │ └── normal/ 正常图像,约56张 ├── val/ │ ├── fire/ 约20张 │ ├── smoke/ 约14张 │ └── normal/ 约14张 └── labels/ └── markup_info.json 标注说明或文件名对照这种划分方式在小型数据集里很常见,但有个隐患:train和val的比例是用户手工切的,而不是按算法分层抽样,训练集和验证集里的场景分布很可能不一致。比如val/fire里恰好全是山林火焰,而训练集里全是室内火焰,模型验证分数就会被低估。
看标注信息时还要对照图片本身,确认文件命名是否连续、图像尺寸是否统一。很多小数据集从不同摄像头截帧而来,分辨率从640×480到1920×1080都有,少数还是BMP、PNG混排。处理时统一缩放到224×224或256×256,并且训练时用RandomResizedCrop而非CenterCrop来保留空间信息的多样性。
2.3 三类图像的难点:火焰无固定轮廓、烟雾半透明
从模型角度说,火焰、烟雾、正常三者并不是同一个难度级别。火焰有相对明确的颜色特征,橙色到红色区域在RGB空间里分布集中,单靠颜色直方图就能获得不错的baseline。但火焰轮廓不定、闪烁变化大,同一张火焰图在不同光照下的特征偏移明显。
烟雾是最难的一类。它半透明、边缘模糊、颜色从白色到灰色渐变,和背景混在一起时人眼都容易分辨失误。烟雾图像里真正起判别作用的往往是背景的“被遮挡感”和整体的“灰白雾化”,而不是一个具体目标区域。这就意味着模型必须从全局特征里学信号,不能只依赖局部Patch。所以用预训练模型做迁移学习时,不推荐只更新最后一层全连接,常见做法是微调backbone的后半段,保持底层纹理特征的同时让高层语义适应烟雾的全局性。
正常图像相对容易,但正常类内部差异巨大——室内、室外、白天、夜晚都有,如果“正常”全是室内管道,而部署时摄像头对着室外,模型会把所有非室内图都判成烟雾。这个问题在小样本里特别突出,解决办法只有两个方向:要么把正常类换成更贴近真实部署场景的图,要么在评估环节单独计算每类的召回率,别只看总准确率。
3. 用迁移学习在本地跑通火焰/烟雾/正常分类:最小可行训练脚本
3.1 环境准备与数据加载的完整代码
我的惯例是用PyTorch,在CPU上也能跑通完整流程,只是训练时间会长一些。有GPU的情况下训练240张小图非常快,30个epoch大概3-5分钟。下面是完整的数据加载部分,包含标签读取、图像缩放、批处理划分。
import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms from torch.utils.data import WeightedRandomSampler import numpy as np # 数据增强 + 归一化,ImageNet的均值和标准差直接用 train_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.1), 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]) ]) # ImageFolder 直接按 train/fire, train/smoke, train/normal 子目录读取 train_dataset = datasets.ImageFolder('dataset/train', transform=train_transform) val_dataset = datasets.ImageFolder('dataset/val', transform=val_transform) # 统计每类样本数,计算样本权重,解决类别不平衡 class_counts = np.bincount(train_dataset.targets) class_weights = 1.0 / torch.tensor(class_counts, dtype=torch.float32) sample_weights = class_weights[train_dataset.targets] sampler = WeightedRandomSampler(weights=sample_weights, num_samples=len(sample_weights), replacement=True) train_loader = DataLoader(train_dataset, batch_size=16, sampler=sampler, num_workers=2) val_loader = DataLoader(val_dataset, batch_size=16, shuffle=False)这里有个细节要说明:RandomResizedCrop前面的Resize((256, 256))不是无用步骤。240张小数据集的原始图像尺寸参差不齐,直接送进RandomResizedCrop时,如果原图只有320×240,裁剪窗口的尺度比例会和1920×1080完全不同,导致增强效果不稳定。先统一到256再做随机裁剪,能让增强行为对每张图更一致。
权重采样器的作用是在每个batch里让三类样本出现概率接近。火焰100张、烟雾70张、正常70张时,烟雾和正常的采样概率会被抬高到火焰的1.43倍,确保每个batch都能见到足够多的少数类别样本。replacement=True表示允许同一张图在一个epoch里被重复抽到,这是小数据集下常用的做法,等价于做了在线过采样。
3.2 迁移学习:冻结低频特征,微调高层语义
构建模型时用预训练ResNet18而不是自己写卷积层,这是小数据集最稳妥的方案。ResNet18在ImageNet上学到的纹理、边缘、颜色特征同样适用于火焰和烟雾图像,直接取用可以省掉大量训练时间,也避免过拟合。
import torchvision.models as models # 加载预训练权重,默认分类头是1000类 model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) num_features = model.fc.in_features # 替换成3类分类头,火焰/烟雾/正常 model.fc = torch.nn.Linear(num_features, 3) # 冻结除了 layer3、layer4 和 fc 之外的所有参数 for name, param in model.named_parameters(): if not (name.startswith('layer3') or name.startswith('layer4') or name.startswith('fc')): param.requires_grad = False optimizer = torch.optim.Adam([ {'params': filter(lambda p: p.requires_grad, model.layer3.parameters()), 'lr': 1e-5}, {'params': filter(lambda p: p.requires_grad, model.layer4.parameters()), 'lr': 1e-5}, {'params': model.fc.parameters(), 'lr': 1e-3} ], weight_decay=1e-4) criterion = torch.nn.CrossEntropyLoss()为什么不把卷积层全部冻结、只训练最后的全连接层?火焰识别的颜色和纹理特征确实可以从ImageNet里直接迁移,但烟雾的“半透明”、“背景雾化”这类全局语义特征在ImageNet预训练模型的高层表示里并不直接存在。只训练分类头的话,模型只能依赖原有特征重组,很难产生烟雾专属的抽象概念。
我刚跑这类数据集时经常把整个网络全部解冻,结果训练到第10个epoch时训练集准确率直接拉满,验证集却开始回落,明显过拟合。冻结backbone前两段后,收敛慢了一点,但验证集曲线平稳得多。上述代码里layer3和layer4用1e-5的极小学习率,相当于只在原有特征空间里做局部调整,分类头用正常的1e-3学习率去完成新类别的映射,这个组合对240张规模的数据非常稳定。
3.3 三个必调参数:batch size、epoch、类权重
- batch size设为16而不是32或64。240张图分成30个batch仍然偏少,梯度估计噪声大,16是折中值。如果有显卡可以设到8再看效果,batch太小BN层统计不稳定,但小数据集里大batch会加剧过拟合。
- epoch设在30-50之间。第1个epoch结束模型可能只有50%准确率,第15个epoch后开始快速爬升,之后波动明显。用验证集监控,保存最优模型而不是最后模型。50个epoch后即便看起来还在上升,也要停,因为240张图没有足够数据支撑继续学。
- 类权重用
CrossEntropyLoss(weight=...)而不是只在采样器里做。仅用WeightedRandomSampler时训练集批次分布均衡了,但验证集仍然是不均衡分布,交叉熵默认对所有类别一视同仁,少数类被误判的罚分不够大。显式给loss加权重可以确保烟雾这类难分类别在优化方向上占据更大份量。
还有一个容易忽略的点:model.fc = torch.nn.Linear(num_features, 3)这一步一定要在优化器创建之前执行。如果先建了优化器再改分类头,新分类头的参数不会出现在优化器的参数列表里,训练过程中这个层永远不更新,模型输出会一直没有变化。归类头后要重新创建或更新优化器。
4. 扩数据与验证:让240张数据学得动也测得准
4.1 数据增强组合:小分类任务的合理配置
数据增强是小数据集训练里价值最大的一环,但增强不是无脑堆叠。火焰和烟雾的判别特征差异极大,前者依赖色彩,后者依赖纹理与全局透明度,增强策略要分别照顾。
我常用的训练增强组合是:随机缩放裁剪、随机水平翻转、颜色抖动。具体参数上,RandomResizedCrop的scale设为(0.8, 1.0)而不是默认的(0.08, 1.0),因为尺度变化过大会把烟雾这类目标切碎,模型学不到完整轮廓;ColorJitter里亮度和对比度抖动±0.2就够了,亮度变化过大会让烟雾的灰度特征和正常类混淆。
烟雾增强还有一种低成本方案:用albumentations库里的RandomFog或RandomShadow模拟不同光照下的烟雾环境。这类操作在真实部署场景里比随机裁剪更有价值,因为烟雾检测在夜晚和白天的差异本质上是背景光照差异。如果数据里有室内外混合图像,把训练增强补上一档光照变化会让验证集分数更可信。
4.2 分层抽样:小数据集验证集的正确切法
240张数据里,验证集切多少是个关键决定。切20%是合理的,也就是训练192张、验证48张,但如果随机切分,火焰和烟雾在验证集里的数量可能只有十几张,一个误判就让准确率掉5个百分点。更稳的做法是先用 StratifiedShuffleSplit 保持每类比例后再切分:
from sklearn.model_selection import StratifiedShuffleSplit # 从原图路径和标签构造数组 X = np.array([p for p in train_dataset.imgs]) y = np.array(train_dataset.targets) # 固定随机种子,保证多次运行切分一致 splitter = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(splitter.split(X, y)) # 严格区分原图和增强图 # 所有增强都在train_loader内部在线执行,不与val共享同一份变换这里有个容易出错的细节:原数据集中如果train目录和val目录已经分好,我会直接把两个目录合并后重新做分层切分,而不是沿用给定的划分。因为原划分可能某类在val里只有个位数样本,连基础评估都不够。重新切分后需要在脚本外维护一份label映射表,方便后续对比模型对不同类别的表现。
验证时不用增强,只Resize到224,这个操作不能省但也不要过度:有的做法会在验证时做TenCrop一刀,这种方式会显著提升准确率但会掩盖模型真实部署水准,建议只使用CenterCrop或直接Resize整图。
4.3 评估指标:小样本不能只看总准确率
240张数据的分类任务里,总准确率是最容易骗人的指标。假设火焰100张、烟雾70张、正常70张,模型全部预测为火焰,总准确率也能到41.7%,看着不低,实际没有任何可用性。正确做法是打印每个类别的precision、recall和F1,尤其关注烟雾的recall。
其次要看混淆矩阵。烟雾被误判为正常的比例如果超过30%,说明模型学到的是“白色物体”而不是“烟雾特征”,此时需要检查训练集中是否有白色背景的管道、灯光、雾气样本。这类混淆矩阵信息在样本量小的时候比loss曲线直观得多。使用sklearn的classification_report配合confusion_matrix就能搞定。
还有一点值得做:把预测置信度导出出来,观察fire类中置信度低于0.6的样本长什么样,这些“低置信度火焰”往往会暴露出数据集标注错误。标注错误在小数据集里占比通常在5%左右,人工复查时重点关注这部分。
5. 避坑记录:240张火焰烟雾数据训练与部署的5个常见坑
5.1 模型全部预测为火焰,准确率却还行
现象:训练结束后打印分类报告,发现烟雾和正常类的recall是0,总准确率40%左右。
原因:类别不平衡下,模型学会了“无脑输出多数类”。有个隐藏推手是损失函数在火焰类的梯度更大,压过了其他类的错误信号。
解决:先加WeightedRandomSampler,再用CrossEntropyLoss(weight=...)把少数类的损失权重抬上来。如果两个都加完仍没改善,检查火焰图像里是否混入大量烟雾图——有些数据集的火焰和烟雾是同一批图像按时间截帧的,前一帧还算烟、后一帧就起了明火,这会让模型学到“亮的就是火焰”。重新标注或用聚类方法区分颜色分布后重新训练。
5.2 验证集准确率高于训练集,还在攀升
现象:训练集准确率75%,验证集85%,验证集曲线在训练集中后期仍在缓慢上升。
原因:验证集和训练集之间存在数据泄漏。常见三类情况,一是在整体划分前做了增强并把增强后图片写回了原目录,二是同一个摄像头连续帧被同时切进训练和验证,三是验证集里包含了和训练集几乎同场景但亮度略有不同的截图。
解决:合并目录后用StratifiedShuffleSplit一次性划分,确保同一类场景的视频帧不被拆分到两边。划分之后再做增强,增强只加在train_loader内部,验证集绝对不用增强。如果条件允许,把验证集换成另一个摄像头独立拍摄的图像,这是最可信的测试方式。
5.3 换了自定义分类头后训练报维度错误
现象:RuntimeError: Expected input batch_size (16) to match target size (3)。
原因:数据集的标签是数值0/1/2,batch里16张图对应16个标签,但模型输出的维度是3,交叉熵预期目标也是一维索引。问题通常在于分类头输出维度和类别数对不上,或者标签在读取时被解析错了。
解决:先打印model.fc.out_features确认是3;再打印train_dataset.classes确认子目录顺序是[‘fire’, ‘smoke’, ‘normal’],顺序必须和数据集的标签含义一致;最后手动检查一个样本的target值和对应图像。如果用的是迁移学习,还要确认model.fc的初始化发生在构造优化器之前。
5.4 数据增强过度,训练集准确率反而不如验证集
现象:训练集准确率止步在60-70%,验证集在80%左右,或者训练和验证曲线都偏低且松散。
原因:增强组合里ColorJitter的幅度调得过大,亮度±0.5、饱和度±0.5,火焰的橙色被改成绿色,烟雾的白色被改成紫色,模型学到的是“被强行改变的带噪分布”,真实特征信息丢失了。
解决:把增强幅度砍半,亮度、对比度、饱和度均控制在±0.2以内,保留翻转和裁剪。更强的增强可以配合更长的epoch使用,在240张数据上epoch只有30-50的话,强增强会让模型还没稳定就停止了。增强参数应该作为超参数和lr一起实验,不能一套配置用到所有数据集。
5.5 标注错误:火焰和烟雾边界被人为混淆
现象:混淆矩阵里fire和smoke互为误判的比例很高,且出错的图片集中在几类特定场景,例如灯光下的黄白色烟雾、夜晚的火焰反光。
原因:小数据集标注一般是人工进行的,火焰区域反光和淡黄色烟雾在视觉上确实难区分。240张规模下5%左右的标注误差已能明显影响模型决策边界。
解决:让两个标注人员分别独立过一遍,对不一致的图片做二次判定。如果无法消耗人力复查,就用训练好的模型找出置信度在0.4-0.7之间的样本,人工集中检查这些低置信度图,通常能筛出大部分错标数据。
6. 进阶:把分类模型转成可用的火焰烟雾报警服务
这一章讲两个最值得做的落地技巧:加置信度阈值而不是全盘接受模型输出,以及把模型导出成轻量推理格式。两者都直接决定你的成果在真实场景中能不能用。
先加阈值。分类模型的输出是softmax后的概率,三个类分别代表火焰、烟雾、正常的置信度。很多初版训练代码直接argmax取最大分类作为最终结果,这在报警场景里是错误的做法。一张图中如果火焰概率只有0.35,正常概率0.33,模型只是勉强猜了火焰,部署到现场早晚会误报。常见做法是设两个阈值:当火焰或烟雾的置信度超过0.7时触发报警,所有类别概率都低于0.5时输出“待人工确认”。这能让误报率下降一半以上。阈值不宜低于0.5,火焰和烟雾视觉特征明确,在图上真出现时模型置信度通常很高。
其次是导出ONNX格式。训练阶段用的PyTorch模型在部署时依赖Python环境和torch库,不适合直接嵌入摄像头端或边缘设备。导出ONNX后可以脱离PyTorch运行,也方便用ONNX Runtime做推理:
import torch.onnx model.eval() dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, "fire_smoke_model.onnx", input_names=["input"], output_names=["score"], dynamic_axes={"input": {0: "batch"}, "score": {0: "batch"}} )导出的onnx模型大概是几十MB级别,用onnxruntime在CPU上推理单张图不到20毫秒,足够满足视频帧抽样的实时性要求。加载后记得做一次session.get_inputs()[0].name检查输入名称,实际推理时按名称喂数据。
我自己的习惯是到这一步后不再继续加新模型结构,而是先拿真实摄像头拍一小时的视频帧,人工标注50个“值得报警”的画面和100个“不该报警”的画面,跑一遍推理脚本看阈值是否命中。240张数据训练出来的模型,离线测试通过只能说明数据集拟合得好,现场采样才是检验边界的地方。这个环节暴露出的大多是光照变化或摄像头角度造成的误判,解法是补充现场负样本到训练集里重新训练。
240张样本的分类模型能做到报警服务级别、且能稳定运行,核心不是模型多深多新,而是数据分布是否贴合真实场景、阈值设计是否留了余地。每一步都有简化空间,但阈值和导出这两件事不要省,它们是对模型之外系统稳定性的兜底。希望这篇笔记能帮你把同样的小数据集完整地推到部署这一步。
本文还有配套的精品资源,点击获取