小样本图像识别实战:240张火焰烟雾数据训练三分类模型

发布时间:2026/10/5 16:26:39
小样本图像识别实战:240张火焰烟雾数据训练三分类模型 简介这份图像识别数据集面向深度学习初学者与计算机视觉入门项目共标注约240张图片覆盖火焰、烟雾、正常三类场景适合用于火灾预警、安全监控等分类任务的前期实验与模型训练。资源共243个文件主体为240张jpg样本图片并附带类别划分json文件、可运行的可视化脚本以及说明图整个7z压缩包仅504KB轻量易获取。数据已按训练集与测试集划分同类图片集中存放目录结构直观运行配套show脚本即可查看样本json文件则记录了各类别标签便于直接对接CNN或YOLOv5分类项目进行训练验证。目前已有112人学习下载适合作为分类网络入门练习或快速搭建火情识别方案的数据基础。1. 240张的火焰、烟雾、正常图像识别数据集到底能做出什么东西收到一个火焰、烟雾、正常图像识别数据集一看标注数量只有约240张很多人第一反应是这能训出什么来但我的结论是如果标注规范、类别覆盖到位这批数据足以让你在一个晚上跑通完整的图像识别流程拿到一个可靠的指标基线而不是让项目卡在“不知道有没有戏”的评估阶段。对于正在做消防预警demo、毕业设计选型或刚转视觉算法的人来说这类已标注数据集的价值在于省掉了整个领域里最贵的环节——数据标注。这里的三类目标要理解成火焰样本必须含明火区域烟雾样本是有烟但未必有明火正常样本则是无可疑目标的工作、生活场景也就是负样本。它的边界同样明确240张适合做分类验证和流程验证距离经过消防验收的生产系统还有一段路要走。2. 训练前先做数据体检一个脚本看清目录、标注与划分很多人拿到图像识别数据集第一件事就是开训练脚本直接跑但我一般会先花十分钟做数据体检。240张的体量经不起折腾如果标注里有漏框、错类、损坏文件训练跑得越久浪费的时间越多。下面这套流程能帮你把目录结构、标注格式和划分策略一次摸清。2.1 目录体检脚本一次性看清类别分布与损坏文件如果数据集按“火焰 / 烟雾 / 正常”三个文件夹组织那它就是分类格式PyTorch的ImageFolder可以直接读。不管哪种组织方式先跑下面的脚本统计类别张数、分辨率范围和损坏文件。import os from collections import Counter from PIL import Image def inspect_data(root_dir): counts Counter() widths, heights [], [] broken [] for cls in sorted(os.listdir(root_dir)): cls_dir os.path.join(root_dir, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): if not fname.lower().endswith((.jpg, .jpeg, .png, .bmp)): continue path os.path.join(cls_dir, fname) counts[cls] 1 try: w, h Image.open(path).size widths.append(w) heights.append(h) except Exception as e: broken.append((fname, str(e))) print(类别分布:, dict(counts)) if widths: print(宽度范围:, min(widths), -, max(widths)) print(高度范围:, min(heights), -, max(heights)) if broken: print(损坏文件:) for fname, err in broken: print( , fname, err) if __name__ __main__: inspect_data(/path/to/dataset)这段脚本假定一级目录就是类别名240张数据通常不需要深层遍历这个假设在大部分已标注数据集里成立。输出里先看类别比例如果火焰有150张、烟雾只有30张说明类别不均衡后面训练要在损失函数里做类别加权或者对烟雾类单独加强数据增强。再看分辨率范围火焰和烟雾目标尺度差异很大如果所有图都是1920x1080的监控截图训练时的Resize策略需要格外注意缩小后小火焰区域的细节会丢得很快。损坏文件则必须处理DataLoader读到坏图时往往是在几十个epoch之后才抛错排查成本比现在高得多。体检完成后把类别比例、分辨率范围、损坏列表三个结果存下来这就是这份小数据集的基线档案。后续每次调整数据划分都拿它做对比能避免误删样本或合并场景。2.2 标注格式读取分类目录与检测框的不同处理路径如果数据集的标注不是文件夹分类而是带坐标的检测框比如VOC格式的XML或JSON读取方式要换一套。常见做法是把XML全部解析出来逐张核对每张图片里有哪些目标、框坐标是否越界、有没有空标注。import glob import xml.etree.ElementTree as ET def parse_voc_xml(xml_path): tree ET.parse(xml_path) root tree.getroot() objects [] for obj in root.findall(object): name obj.findtext(name).strip() box obj.find(bndbox) xmin int(float(box.findtext(xmin))) ymin int(float(box.findtext(ymin))) xmax int(float(box.findtext(xmax))) ymax int(float(box.findtext(ymax))) objects.append({name: name, bbox: (xmin, ymin, xmax, ymax)}) return root.findtext(filename), objects for xml_path in glob.glob(/path/to/annotations/*.xml): filename, objects parse_voc_xml(xml_path) print(filename, len(objects), [obj[name] for obj in objects])这个脚本的价值不在于把标注搬到某个训练框架里而在于做标注一致性检查。解析后要关注三件事第一有没有图片对应的XML里没有任何object这种空标注会让检测训练出现空正样本损失直接异常第二bbox坐标是否超出图像边界xmax不能大于图片宽度ymax不能大于图片高度越界框会让模型学习到错误的定位信号第三类别名是否统一常见问题是在标注过程中“火焰”被写成fire、flame、明火等多个版本训练时会被当成不同类别样本量本来就不多经不起这种分裂。如果发现数据集是分类目录而不是检测框也有一个常规套路跑完2.1的统计后再随机抽几张正常、火焰、烟雾图人工看一眼确认正常类里没有混入小火苗。对图像识别数据集来说负样本的干净程度往往比正样本数量更影响模型表现。2.3 240张样本的划分策略按场景切而不是按文件名随机切很多人拿到240张数据习惯用sklearn的train_test_split按文件名随机切但小样本下这个做法容易翻车。同一批监控视频里抽出的连续帧背景和光照几乎一样随机切会让训练集和验证集里出现大量“长得一样”的图验证指标虚高模型到了现场换成新场景立刻打回原形。正确做法是先识别数据里的场景结构再划分。如果文件名带摄像头编号、日期或场景ID直接按场景ID分组没有ID时可以用感知哈希对图片做相似度聚类把互相接近的图片放在同一组再按组分配。这是小数据集划分里最值得花时间的一步。import os from PIL import Image import imagehash # 需要 pip install imagehash def group_by_phash(root_dir, hash_size8): groups {} for cls in sorted(os.listdir(root_dir)): cls_dir os.path.join(root_dir, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): if not fname.lower().endswith((.jpg, .jpeg, .png)): continue path os.path.join(cls_dir, fname) h imagehash.phash(Image.open(path), hash_sizehash_size) key (cls, h) groups.setdefault(key, []).append(path) return groups这个脚本把每张图转成一个64位的感知哈希值完全相同或极度相似的图会落到同一个key得到的groups就是“场景候选组”。划分时让同一个组只出现在训练集或验证集中而不是拆散到两边。hash_size8生成64bit哈希默认值即可不用刻意调大否则会把同一场景的不同亮度帧拆成多个组失去合并的意义。240张数据通常能分出几十个场景组按8:1:1切到train、val、test每个集合里仍保留火焰、烟雾、正常三个类别的相对比例。最后test集合不要反复使用否则它慢慢就变成了第二个验证集指标会失去参考价值。3. 迁移学习训练三分类模型240张数据也能跑出可用基线数据体检做完接下来进入正式训练。小样本图像识别最稳的路线是迁移学习用ResNet18做骨干网络替换最后一层全连接为三类输出。这一章把选型理由和完整训练代码一次讲透。3.1 为什么用ResNet18迁移学习而不是从零训练图像识别里有个直觉数据量少就换小模型。这句话方向对但执行上有个更关键的前提——别从零训练。从零初始化的卷积网络在240张图上很难学到有判别力的边缘和纹理组合训练集loss会降得很快验证集却一动不动这是典型的欠拟合加过拟合同时发生。预训练模型在ImageNet上已经学会通用视觉特征包括边缘、纹理和物体部件的组合我们只需要替换分类层让模型把“已经学会的特征”映射到火焰、烟雾、正常这三个类别上。模型选型上我一般先用ResNet18。理由有三参数量适中CPU也能完成验证不需要一上来就折腾GPU环境PyTorch官方权重可以直接加载省去很多版本适配问题层结构规整方便后面做热力图和分段微调。如果显存紧张或者想部署到边缘盒子再换EfficientNet-B0或MobileNetV3也不迟但第一版跑通流程时不必追求最轻的模型先拿ResNet18把baseline立住。3.2 三分类训练代码数据加载、模型替换与训练循环下面这份训练脚本是图像识别数据集训练的常用骨架把路径换成自己的目录就能跑。它一次性解决数据增强、迁移学习和三分类输出的问题。import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, models, transforms train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(10), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), 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]) ]) train_dataset datasets.ImageFolder(/path/to/train, transformtrain_transform) val_dataset datasets.ImageFolder(/path/to/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size16, shuffleTrue, num_workers2) val_loader DataLoader(val_dataset, batch_size16, shuffleFalse, num_workers2) model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) in_features model.fc.in_features model.fc nn.Linear(in_features, 3) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-4) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemax, patience2, factor0.5) epochs 15 best_val_acc 0.0 for epoch in range(epochs): 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() * images.size(0) # 验证阶段 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: outputs model(images) _, predicted torch.max(outputs, 1) correct (predicted labels).sum().item() total labels.size(0) val_acc correct / total train_loss_avg running_loss / len(train_dataset) scheduler.step(val_acc) print(fEpoch {epoch1}: train_loss{train_loss_avg:.4f}, val_acc{val_acc:.4f}) if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), fire_smoke_normal_resnet18.pth)这段代码有四处值得细看。第一ImageFolder要求一级目录就是类别名所以目录要整理成train/火焰、train/烟雾、train/正常这种结构建议用英文目录名中文名在部分服务器和容器里容易出现编码问题。第二ResNet18的fc层原来输出1000类这里替换成3类网络其余部分全部沿用ImageNet预训练权重。第三学习率用1e-4而不是Adam默认的1e-3预训练权重已经处于一个较优的特征空间学习率过大会把这些特征直接破坏掉。第四ReduceLROnPlateau监控验证准确率两个epoch不涨就把学习率减半这是小样本训练里最省心的调度方式。epochs设15只是一个起点。判断标准以val_acc为准如果模型在第10轮还在明显上升就说明训练没结束如果连续5个epoch没有改善直接停掉。batch_size16对应224分辨率单卡内存占用不高显存紧张可以降到8但不要为了速度开64小数据集上大batch会加剧过拟合因为同一个类别的图片很容易被塞进同一批迭代里。想保持宽高比的话把Resize改成Resize(256)加CenterCrop(224)也可以代价是保留更多画面裁剪。3.3 三个必调参数分辨率、微调粒度与保存策略训练跑通后真正决定模型最终分数的往往不是网络结构而是三个参数。第一个是输入分辨率。224x224是常见默认值但烟雾是弥散的、边界模糊的目标火焰边缘又往往偏小224下很容易把小火苗压缩成十几个像素。如果显存允许把Resize改成320或448通常比调任何超参数都有效。改法就是直接动train_transform和val_transform里的Resize注意ColorJitter等增强在Resize之后执行顺序不要乱。第二个是微调粒度。第一版全量微调能跑通流程但火焰、烟雾特征和ImageNet里的通用物体差异较大只训练最后的fc层可能学不到火烧边缘的特殊纹理。常见做法是分两阶段第一阶段把backbone的requires_grad设为False只训fc层几个epoch找到合适学习率第二阶段解冻layer4参与微调学习率降到1e-5避免破坏底层特征。用代码表达就是把model.layer4.requires_grad置为True优化器里只传入需要更新的参数。第三个是保存策略。保存时用model.state_dict()而不是torch.save(model)后者会把网络结构和训练状态一起存下来换机器时要求代码环境完全一致很麻烦。加载时先实例化ResNet18并替换fc层再load_state_dict这个习惯能少踩很多部署的坑。4. 小样本踩坑记录240张数据训练火焰烟雾识别的高频问题训练过程看着顺利不代表模型真的学到了火焰和烟雾的本质。下面这五条踩坑记录按“现象→原因→解决”列出每一条都是小样本火焰烟雾识别项目里真正会遇到的。4.1 训练集准确率冲上98%验证集一直在60%徘徊现象第一轮训练结束训练准确率已经比验证集高一截到第五轮训练集接近100%验证集却卡在60%左右两条曲线像拉不开的剪刀。原因最常见的不是模型问题而是数据划分问题。如果训练集和验证集里有大量来自同一视频流的连续帧模型只需记住背景就能拿到高分验证时遇到真正的新场景立刻失效。其次是数据增强强度不够ColorJitter只给到0.1几乎等于没加。解决先回看2.3把同源图片按场景组切分让验证集和训练集彻底分开。然后把ColorJitter的brightness、contrast提到0.3RandomRotation提到15度必要时加MixUp。对240张的小数据集验证集准确率能稳定在75%以上已经是合格基线不必追求90%。4.2 烟雾大量误判为正常现象火焰的召回率能做到95%左右烟雾的召回率可能只有五成模型宁可把淡烟判成正常也不愿意给出预警。原因烟雾的单帧特征太弱。静态图里烟雾接近半透明和天空、白色墙面很容易混淆而且烟雾样本往往浓烟占多数淡烟的分布几乎没有覆盖到。解决训练阶段对烟雾类单独加强颜色扰动适当调大色相和饱和度扰动让网络不能只靠灰白色块判断。划分时按“浓烟、淡烟、夜间烟”分组检查保证每组的样本都被验证集覆盖到。如果数据来自视频更救命的做法是取连续两帧做差分把静止背景去掉拿运动区域进分类器这种帧间特征比单帧稳定得多。4.3 白天一切正常晚上路灯把正常场景误报成火焰现象白天验证集表现不错一到夜间场景红色车灯、路灯、霓虹灯频繁触发火焰预警误报率直接拉满。原因颜色空间上火焰和红色光源高度重叠模型学到的可能只是“一块红色区域”而不是火焰特有的分布、边缘和亮度关系。白天正常样本里没有这种红色干扰晚上自然崩。解决把正常类里的硬负样本补进来从现场或常见素材里找出红色卡车、夕阳、红色警示灯加入正常类一起训练。这是比调任何参数都有效的方案。同时在模型外加一道颜色前置规则完全不含高亮红色像素的图直接判为正常不用进模型有可疑红色区域的图再交给分类器。别把希望全押在模型上图像识别在工业场景里常常需要规则和模型并行。4.4 标注有问题但训练不报错背景区域被当成正样本现象训练loss掉得很稳验证集偶尔出现一张碎片图被识别成火焰人工看不明白模型依据在哪。原因分类格式的数据集可能存在整张图标注过粗的问题比如一张火焰只占画面很小部分标注时把整张图都归为火焰类或者检测框把大片无关背景圈了进去。已标注数据不等于标注零错误。解决用2.2节的解析脚本把所有标注信息导出成表格重点查三件事空标注、坐标越界、类别名不统一。发现整图类别有问题的样本宁可直接删掉不要让它留在训练集里污染loss。我一般会在训练前让脚本每次随机打印20张图的路径人工扫一遍这一步用不了十分钟但对小数据集来说能省下大量排错时间。4.5 同一张图重新Resize后预测结果不一致现象训练时用224分辨率demo里却拿原图直接推理同一张火焰图有时预测为火焰有时预测为正常结果还不稳定。原因这不算bug而是训练和推理的预处理没有保持一致。原图1920x1080缩到224后火焰区域从几百像素变成十几像素特征被抹平如果Resize方式不同比如直接拉伸和等比缩放后填充又会改变火焰的宽高比和位置分布。解决推理时固定使用和验证集一致的预处理流程Resize、Normalize都要一致。如果现场需要识别画面中的小火焰把输入分辨率提高到320或448并且用等比缩放加padding不要直接拉伸。项目交付前拿20张训练集外图片分别跑两次推理确认输出稳定再谈上线。5. 进阶用热力图验证模型到底在看什么再决定扩数据还是部署模型训练完先别急着调参数用热力图看一下它到底在看图片的哪个区域。这是火焰烟雾识别项目里最依赖的验证手段比任何准确率数字都能说明问题。5.1 用热力图把模型注意力抽出来看PyTorch生态里torchcam可以直接从ResNet18的layer4抽取CAM核心代码很短。import torch from torchcam.methods import CAM from torchvision import models, transforms from PIL import Image model models.resnet18(weightsNone) model.fc torch.nn.Linear(model.fc.in_features, 3) model.load_state_dict(torch.load(fire_smoke_normal_resnet18.pth)) model.eval() 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]) ]) input_tensor transform(Image.open(test_fire.jpg)) cam_extractor CAM(model, layer4) out model(input_tensor.unsqueeze(0)) acts cam_extractor(0, out)把acts用双线性插值上采样回原图尺寸叠加到原图上输出就能看到模型注意力集中在哪个区域。如果热力图高亮区域集中在火焰的红色边缘和烟雾的弥散区域说明模型学的是目标本身如果高亮到背景杂物上多半是标注噪声或划分泄漏问题。这一张图给评审看最有说服力省去大量解释。5.2 从分类往检测和边缘部署走如果后续要接工业现场分类只是第一步。数据若带检测框按归一化坐标把XML转成YOLO的txt格式就能用YOLOv8训练自己的检测模型小数据集上先训一个检测baseline再逐步扩充现场数据。如果算力受限制比如想用ESP32-S3这类设备做前端识别常见做法是把ResNet18换成MobileNetV3做int8量化和通道剪枝再转TFLite Micro但不要指望240张数据量化后还能保持实验室指标。更稳妥的路线是摄像头采样后把画面送到边缘盒子或服务端推理前端只做主循环和报警逻辑。5.3 一个交付习惯我个人的交付习惯是最后一步拿20张现场照片用训练好的模型跑一遍同时把每张图的CAM热力图打印出来和现场人员一起过一遍。有一次我只看acc指标就交付结果现场把夕阳误报了一整晚后来才发现模型注意力全在红色区域上而不是火焰形状。从那以后涉及火焰和烟雾识别的项目我坚持先跑热力图再谈准确率。这个习惯也推荐给你希望帮到你。本文还有配套的精品资源点击获取