news 2026/10/5 5:20:58

火焰烟雾小数据集迁移学习实战:从240张图到可靠识别

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
火焰烟雾小数据集迁移学习实战:从240张图到可靠识别

简介:这是一个面向图像分类任务的火焰、烟雾与正常场景识别数据集,包含约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', encoding='utf-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_ratio=0.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_ok=True) 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_ratio=0.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_samples=total, replacement=True) train_loader = DataLoader(dataset, batch_size=16, sampler=sampler, num_workers=2)

给每个样本分配1 / 该类总数的权重,分布越少的类别单样本权重越高。参数说明:num_samples=total表示每个 epoch 总采样次数与训练集大小一致;replacement=True允许同一张图在一个 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(p=0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.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(brightness=0.3, contrast=0.3, saturation=0.3)是三通道颜色扰动,用来模拟白天黑夜不同光照下火焰和烟雾的颜色漂移。Normalize的均值方差必须用 ImageNet 的标准值,因为预训练权重是按这个分布训练的,用错会导致 loss 一开始就震荡。

4.2 训练配置:学习率、batch size、epoch 的合理设定

小数据集的训练参数跟大数据集截然不同。batch size 建议 8 或 16,240 张的训练集每 epoch 只有 15 到 30 个 batch,batch 太大梯度更新次数太少,模型还没来得及学就过拟合了。学习率要从1e-4起步而不是常用的1e-3,因为预训练权重已经很接近最优解区域,用大学习率一步就把特征冲坏了。

import torch import torch.nn as nn from torchvision import models model = models.resnet18(weights=models.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_decay=1e-4) criterion = nn.CrossEntropyLoss() scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=40) 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(f'Epoch {epoch+1: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_max=40与epoch数一致,让学习率从初始值余弦衰减到接近 0;weight_decay=1e-4加一点 L2 正则防止过拟合,小数据集上不要超过1e-3,过大反而压低了有效特征的规模。保存模型用best_acc判断而不是最后一个 epoch,因为训练后期往往出现过拟合,最后一个 checkpoint 反而不如中间某个 epoch 的结果。

4.3 从日志判断训练没有翻车

跑起来之后要学会读日志。一个健康的训练过程是:前 5 个 epoch 的 train loss 从 1.2 左右快速降到 0.8,val 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.1,val 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 默认BILINEAR,PyTorchResize默认也是双线性,但要显式指定)、归一化参数全部固定,任何设备跑之前先对同一张输入图做一次逐像素比对,确认预处理链路完全一致再谈模型部署。这个坑我在实际项目中踩过,排查了两天才定位到是一台机器用了 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 的指标都归档好,三个月后回来看,能省下大把重建环境的时间。希望这些经验能帮你在同样的数据规模下少走几段弯路。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 5:20:32

任意边界圆柱壳振动求解:Sanders理论与切比雪夫多项式

简介&#xff1a;面向具备固体力学与数值分析基础、熟悉MATLAB的研究生、科研人员及工程技术人员&#xff0c;这份PDF聚焦任意边界条件下圆柱壳的自由振动与模态求解。内容以Sanders壳体理论构建弹性应变能&#xff0c;通过端部人工弹簧模拟不同边界条件&#xff0c;系统比较改…

作者头像 李华
网站建设 2026/10/5 5:20:14

GraphRAG 社区发现落地:Leiden 算法如何聚类超长文本全局主题

GraphRAG 社区发现落地&#xff1a;Leiden 算法如何聚类超长文本全局主题在传统的向量检索 RAG 系统中&#xff0c;最让算法工程师感到挫败的提问&#xff0c;莫过于用户的“全局概括性总结”。 当业务高管指着包含数万份客服会话、故障工单或战略研报的知识库提问&#xff1a;…

作者头像 李华
网站建设 2026/10/5 5:19:04

DeepSeek语义理解+多目标优化:能源企业碳减排路径落地实战

简介&#xff1a;这份197页PDF面向能源行业低碳转型从业者、算法工程师与研究人员&#xff0c;围绕DeepSeek语义理解与多目标优化技术&#xff0c;系统讲解碳减排路径优化的落地方法。内容从能源行业碳减排的紧迫性与技术瓶颈切入&#xff0c;依次展开语义理解需求解构、模型底…

作者头像 李华
网站建设 2026/10/5 5:18:35

NEC红外协议精讲:从时序原理到RK3576解码实战

前阵子帮朋友调一块RK3576开发板上的红外遥控&#xff0c;板子明明收到了红外接收头吐出来的波形&#xff0c;键值却怎么都对不上。折腾了一下午&#xff0c;最后发现不是驱动问题&#xff0c;而是对NEC协议的时序理解出了偏差——他把遥控器按键抬起时的重复码当成了一帧完整数…

作者头像 李华
网站建设 2026/10/5 5:18:34

Ponytail动效:轻量级动态进度指示器实现指南

1. 项目概述&#xff1a;从“ponytail”这个词出发&#xff0c;我们到底在聊什么&#xff1f;“ponytail”这个词最近在社交平台和内容社区里反复出现&#xff0c;但它的语义正在悄然发生偏移——它不再只是教科书里那个“马尾辫”的基础释义。我翻了近三个月的主流平台热榜、小…

作者头像 李华