
简介面向计算机视觉与深度学习学习者的森林火灾图像分类数据集包含约一万三千张已标注图片按“有火”与“无火”两个类别组织可用于火灾检测、图像分类等模型的训练与算法验证。压缩包共两千个文件主体为一千九百九十八张jpg图像附带1个Python可视化脚本和1个json标注配置文件整体大小约338.73MB图像按类别存放并已划分训练集与测试集方便加载到深度学习框架中直接开展训练和评估。json文件记录了类别划分与标注信息运行配套的show脚本可快速浏览样本分布直观理解数据构成与分类边界为网络结构调试、超参数调优提供依据。目前已有439人学习既适合初学者入门图像分类任务也可作为森林火情监测相关课题的基准数据集具备良好的复用价值。1. 森林火灾图像分类数据集13,000张已标注图像到底值不值得用搞森林防火报警或林区巡检落地的人拿到数据的第一步不是急着开训而是先看清楚这套标注好的图集能不能扛住真实场景。森林火灾图像分类数据集规模约13,000张、已标注刚好卡在一个很舒服的位置直接训练一个图像分类模型做火情判断完全够用又不至于像上百万级数据集那样先花半个月做分布式清洗才能动手。它的任务很聚焦输入一张林区或山野画面判断里面有没有烟火这也是业内常说的森林图像分类。无论是做摄像头实时预警、无人机巡检初筛还是验证自己熟悉的图像分类算法这个量级都能支撑起一个完整的工程闭环。它适合刚入门的工程师也适合要把火灾场景迁移到自有监控体系的团队。值不值得用取决于你拿到之后怎么解剖它。2. 拿到数据集先别训练类别分布、文件质量与划分方法2.1 先摸清类别体系和数量分布火灾图像分类数据集的标注方式比通用数据集更多样。常见的有三种二分类只分 fire/no-fire适合做快速预警闸口三分类把 smoke/flame/no-fire 分开能区分冒烟和明火适合早期发现还有把消防处置、无关干扰也拆出来的多分类。标题写的是“已标注约13,000张”没有写明类别数所以我不会假设它是二分类更不会一上来就按两个文件夹去组织训练。第一步永远是统计。下面这个脚本按文件夹统计每个类别的图片数量同时把 0 字节或打不开的文件抓出来import os from collections import Counter root path/to/fire_dataset exts {.jpg, .jpeg, .png, .bmp, .webp} counts Counter() broken [] for label in sorted(os.listdir(root)): label_dir os.path.join(root, label) if not os.path.isdir(label_dir): continue images [f for f in os.listdir(label_dir) if os.path.splitext(f)[1].lower() in exts] counts[label] len(images) for f in images: fp os.path.join(label_dir, f) try: if os.path.getsize(fp) 0: broken.append(fp) except OSError: broken.append(fp) print(类别分布:, dict(counts)) print(空文件数量:, len(broken))这里两个小细节值得说os.path.splitext(f)[1].lower()是为了统一大小写后缀很多数据集里同时存在.JPG和.jpg不转小写会漏统计os.path.getsize只能抓空文件文件头裁掉一半的图它看不出来那部分要靠后面的解码检查。统计结果决定后面怎么做如果某个类别只有几百张而另一个类别上万张训练时就必须做类别权重补偿第 4 章避坑部分还会再碰到。2.2 图片尺寸、色彩空间与损坏样本检查约13,000张图里最怕的不是标注不干净而是格式混杂。真实的火灾采集场景里有网络摄像头截的 JPEG有无人机拍的 PNG 序列还有网页抓的 WebP。如果数据加载阶段不做归一把灰度图、RGBA 图和普通 RGB 图混在一起送进ToTensor()训练到一半就会以各种诡异方式报错。我一般用 PIL 做一次“能否完整体解码”的体检from PIL import Image from glob import glob imgs glob(path/to/fire_dataset/**/*.jpg, recursiveTrue) sizes {} failed [] for p in imgs: try: im Image.open(p) im.load() sizes[im.size] sizes.get(im.size, 0) 1 except Exception as e: failed.append((p, str(e))) print(最常见的5种尺寸:, sorted(sizes.items(), keylambda x: -x[1])[:5]) print(损坏样本:, len(failed), failed[:3])Image.open(p)只是读文件头im.load()才真正解压像素数据所以解码错误会在这里现形。尺寸统计的意义在于这个数据集的图片从 224×224 缩略图到 4000×3000 无人机原图都可能出现如果最大尺寸和最小尺寸差太多训练时做随机缩放就要把裁剪比例下限放宽否则容易把林区的大场景误切成局部特写。检查中如果发现非 RGB 模式直接在读取阶段统一转不用等预处理im Image.open(p).convert(RGB)这一步在开发期可能觉得多余但等你把模型换到 TensorRT 或者 ONNX 部署、突然被灰度图卡一晚上的时候就会知道它值多少钱。2.3 划分训练集/验证集/测试集不要按文件名随机切这是我翻车最狠的地方。最早我用random_split按单张图片切分验证集精度刷到 97%上线后在同一个摄像头角度布新点位精度直接从 97 掉到 80。后来排查才发现火灾数据集的图像大多是同一场火在不同时刻的连拍帧相邻帧之间几乎只有像素级差异。随机切分等于把答案提前塞进了验证集。火灾场景的特殊性在于“空间连续、时间连续”切分边界应该按“事件/场景”走而不是按单张图片走。常见的做法是先根据文件名或子目录找出场景分组再按组切分import os import random def scene_key(img_path: str) - str: sub, fname os.path.basename(os.path.dirname(img_path)), os.path.basename(img_path) # 归一化取文件名前两段做场景ID例如 2024_0715_fire_001.jpg - 2024_0715 return sub / _.join(fname.split(_)[:2]) groups {} for img_path, label in all_samples: # [(path, cls_id), ...] groups.setdefault(scene_key(img_path), []).append((img_path, label)) scene_ids list(groups.keys()) random.Random(42).shuffle(scene_ids) n_train int(len(scene_ids) * 0.8) n_val int(len(scene_ids) * 0.9) train_pairs [x for sid in scene_ids[:n_train] for x in groups[sid]] val_pairs [x for sid in scene_ids[n_train:n_val] for x in groups[sid]] test_pairs [x for sid in scene_ids[n_val:] for x in groups[sid]]这段代码的核心是把“分组”当作最小划分单元。80% 组做训练10% 组做验证10% 组做测试组内图像天然被放到同一侧。random.Random(42)里手动传 seed是为了跟全局的random.seed(42)隔离开避免别处调用随机数影响这次划分结果。切分完之后把路径和标签写到三个文本文件里后面每一次迭代都读同一份清单这就是整个训练过程的“后悔药”——模型权重丢了可以重训划分结果乱了才是灾难。3. 训练火灾图像分类模型数据加载、模型选型和关键参数3.1 PyTorch 数据加载用 ImageFolder 还是自定义 Dataset如果这个数据集的目录结构就是“类别文件夹→图片”直接用 torchvision 的ImageFolder最省事但现实里经常遇到标签写在 CSV、或者需要跳过第 2 章过滤掉的坏文件的情况。这种情况下我建议直接写一个自定义Dataset这样做不会多花多少时间反而能给后面做负样本扩充、数据卡校验留出操作空间。import os from PIL import Image from torch.utils.data import Dataset class FireDataset(Dataset): def __init__(self, root, split_file, transformNone): self.root root self.transform transform self.samples [] with open(split_file, r) as fp: for line in fp: rel, label line.strip().split(,) self.samples.append((rel, int(label))) def __len__(self): return len(self.samples) def __getitem__(self, idx): rel, label self.samples[idx] img Image.open(os.path.join(self.root, rel)).convert(RGB) if self.transform: img self.transform(img) return img, label这段代码的关键在构造参数split_file训练、验证、测试三份划分清单都通过这个文件传入Dataset本身不关心目录结构。convert(RGB)放在__getitem__而非预处理中能让所有异常通道在图进入模型前就归一。后续如果要做迁移学习、查漏补缺样本改的是 CSV 文件而不是重建数据集对象省去很多重跑成本。配合的 transform 要按火灾数据的特性来配我常用的是from torchvision import transforms train_tf transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])注意RandomResizedCrop的scale下限我设在 0.6不是 0.08 这种常规值。火灾图像有很强的尺度不确定性有时候火焰铺满全屏有时候只是一片树丛里的烟雾裁剪范围放太大反而会让模型学会“局部特写一律报警”的坏习惯。ColorJitter 的亮度、对比度、饱和度抖动控制在 0.3 左右是为了适应不同时段的光线分寸感很关键——抖动太狠画面偏到不像真实火灾模型就会在部署时把火烧原图判成“被调过色的图”。3.2 模型选型先跑 ResNet 别急着上最新结构约 13,000 张不是一个大得没边的数据规模。我自己的选型次序是这样的先跑通一个 ResNet 级别的模型拿基准再决定要不要换更大网络如果你一上来就上了参数量上百 M 的模型训练时间和调参成本都会翻倍而且大概率精度收益不到一个点。模型参数量单张224推理相对耗时这个数据集上的定位ResNet-1811M低用来快速验证数据质量和 pipelineResNet-5025M中默认选择精度/速度最平衡EfficientNet-B05M低嵌入式边缘部署优先MobileNetV3-Large5.5M低摄像头端轻量化场景Swin-T28M中高想对比 Transformer 和 CNN 时再上现在大家常说的最新的图像分类模型很多是基于 Vision Transformer 的架构它们在 ImageNet 上表现不差但森林火灾分类这种“颜色纹理 强上下文”的场景CNN 的归纳偏置仍然很稳而且调参和推理部署的工具链更成熟。用这个数据集做实验完全可以但我不建议把它作为验证“最新模型”的第一站。说句题外话分类任务跑通之后如果你觉得单分类不够用想往目标检测走完全可以直接把这个数据集整理成 YOLO 格式然后按 yolov8 训练自己的数据集的流程接着做。反正前期的数据检查、划分、增强逻辑是共通的这里踩的坑在那边一样躲不掉。3.3 训练脚本与关键参数预训练、余弦退火与类别权重下面是一段最精简但可跑的训练循环。默认用 ImageNet 预训练的 ResNet-50 做迁移学习这是 13,000 张规模下最稳的起点。import torch import torch.nn as nn from torchvision import models num_classes 2 # 用第2.1节的统计结果替换 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, num_classes) criterion nn.CrossEntropyLoss(weightclass_weights) optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) for epoch in range(30): model.train() total_loss 0.0 for images, labels in train_loader: images, labels images.cuda(), labels.cuda() optimizer.zero_grad() out model(images) loss criterion(out, labels) loss.backward() optimizer.step() total_loss loss.item() scheduler.step() print(fepoch {epoch:02d} loss {total_loss / len(train_loader):.4f})三个参数你要根据数据集的实际情况调整。lr3e-4是基于预训练权重做全量微调比较稳的起点如果从零开始训练一般要降到1e-4甚至更低。T_max30跟epoch30对齐让余弦退火在整个训练周期里把学习率平滑拉到底。class_weights来自类别样本数的倒数归一化比如 fire 有 3000 张、no-fire 有 10000 张那权重就设成约 1.67 和 0.5把少数类往上抬。这个权重别拍脑袋写直接按第 2.1 节的统计结果算。训练时我还会顺带保存验证集上 acc 最高的权重val_acc evaluate(model, val_loader) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_fire.pth)为什么要单独存 best因为余弦退火的最末端模型未必是验证集最优存最后一个 epoch 的权重万一落在一个局部抖动上等于白训 30 轮。4. 森林火灾分类训练的5个典型翻车现场现象、原因和处置4.1 烟雾被识别成云云被识别成烟雾这是森林火灾分类里最常见的混淆没有之一。现象是早晨或阴天的大片白烟被判成 cloud积云边缘又被报成 fire。原因也直白烟和云在视觉特征上高度重合都是高亮度、低饱和、边缘模糊的半透明区域。模型如果只是在亮度上做文章几乎分不开它们。我的处置方式是三层并进第一层数据层面必须加入大量“云、雾、霾、山间水汽”这类负样本并且把负样本的标注质量过一遍确认标注人员没有把远山雾气标成烟第二层类别设计上不要把 smoke 和 cloud 合成一个大类宁可回到三分类让模型学会用“纹理和边界”而非颜色去区分第三层后处理里用地理先验做一个保守规则比如画面顶部天空区域出现烟状目标时不直接触发报警等连续帧稳定后再处理。4.2 类别不均衡时模型直接“躺平”现象非常直观验证集 loss 在降但混淆矩阵里模型几乎把所有样本都判成 no-fire准确率看起来很高实际正样本召回率只有十几个点。原因是负类样本太多交叉熵损失在数学上鼓励模型去把大局判成负类因为它能用一个很小的“权重代价”换回大部分样本的正确分类。从数据侧我先做随机欠采样把负样本压到正样本的 1.5 倍以内如果还偏再用损失函数补偿。CrossEntropyLoss的weight是现成方案class_weights 总样本数 / (类别数 * 每类样本数)。再激进一点就是换Focal Loss让模型在训练中聚焦在那些难分样本上。对约 13,000 张的数据集来说我建议先欠采样再考虑 Focal Loss顺序不要反因为 Focal Loss 在正负比极其悬殊时有一堆额外的超参要调。4.3 验证集虚高同场景照片混进训练集现象前面提到过验证 acc 0.97上线后换个摄像头就崩。根本原因就是划分时按单张图片随机切同一场火的连拍帧被平分到了训练集和验证集模型等于提前看过答案。这和数据质量无关是划分策略的锅。解决方式就是第 2.3 节说的按场景/事件分组。这个操作在通用数据集的 benchmark 里不太常见在火灾数据集里却是生死线。还有一点补充测试集也要按同样原则单独隔离最好里面包含从未在地面验证阶段出现过的拍摄点位这样评估结果才有说服力。4.4 夜间和暗光场景的小火苗识别不出来现象很明显训练时白天样本召回率很高到夜间部署就漏检尤其是远处暗背景上的小火苗模型几乎不响应。原因是数据集里白天的样本占大头夜间样本少、亮度低而 ResNet 这类模型在下采样的过程中会把小尺寸火焰的细节一点点磨掉。处置一般分两步。第一步是增强把 gamma 校正和亮度抖动加进训练管线亮度下限可以拉到 0.5专门模拟夜间和逆光同时对夜间样本单独做一次上采样复制哪怕只是把同一张图复制两份放进训练集召回率也会有可见提升。第二步是分辨率夜间小火苗需要更精细的空间细节把输入分辨率从 224 提到 384只换输入尺寸不换模型结构往往就能把 recall 拉回好几个点。4.5 模型记住了相机位置和天空颜色这是最隐蔽的一个坑。现象是模型在训练场景里的测试精度很高但换一个拍摄角度、换一个镜头色彩风格就明显退化。原因在于火灾图像数据集的采集高度同源同一台无人机或者同一批摄像头拍出来的画面背景纹理、天空色温、树木形态都高度相似模型很容易把“这个相机位和这种天色”当成火的替身。我用来揪这个问题的办法就是 Grad-CAM 可视化第 5 章会展开。数据解决上随机水平翻转会破坏固定机位的镜像关系ColorJitter 和灰度化能打断天空色带的稳定依赖更彻底的方案是用 CutMix 随机把两张图的区域混合强制模型不能只依赖某个固定位置的背景。要是做完这些模型在测试集上还能有一个稳定的 acc 提升那基本可以认为它学到的是“火”而不是“场景”。5. 进阶用 CAM 和硬负样本把精度再往上抬一截5.1 用 Grad-CAM 验证模型到底在看哪儿from torchcam.methods import GradCAM cam GradCAM(model, target_layerlayer4) with torch.no_grad(): out model(img.unsqueeze(0).cuda()) cam_out cam(out.argmax().item(), out) heatmap cam_out.squeeze().cpu().numpy()把热力图叠回原图如果模型注意力集中在火焰轮廓和烟雾扩散的方向说明它学到了真正可迁移的火灾特征如果集中在天空、树干、相机固定位置的水印那基本就是背景过拟合对应第 4.5 节的坑。这是我在每个数据集上做的最后一道验收效果比看准确率直观得多。5.2 给负样本“加料”晚霞、红叶、黄沙、云现实里让我觉得最头疼的误报源不是普通森林而是那些“长得像火”的东西傍晚的晚霞、秋日红叶、红砂岩山体、沙尘暴以及清晨大片水雾。通用数据集里极少专门关注这类样本所以我会额外准备 500 到 1000 张这样的硬负样本塞进训练集里重采样让它出现的比例不低于 20%。每次迭代按一定概率把这些图片做一次随机增强再送进去。调这类样本会明显改变模型的误报率但对准确率的影响可能要两三个 epoch 才能体现。不要因为头几个 epoch 的准确率没有涨就把它撤掉误报率的改善往往滞后。5.3 我的收尾习惯把训练结果固化成一份“数据集体检报告”一个长期的经验每次训练结束都存在一份从数据集统计开始、到类别混淆矩阵、到 Grad-CAM 样例图都包含的“体检报告”。下次模型精度出问题、要换场景复用、或者团队内部复盘直接翻这份报告的结论比重新跑一天脚本再讨论省太多时间。约13,000张的森林火灾图像分类数据集本身只提供一个起点真正的价值在于能不能用它沉淀出可复用的数据管线——我在这类数据上翻车的五六个问题基本都是吃了“数据集没摸清就开训”的亏。希望这个流程能帮你在做火灾图像分类或类似森林场景时少走弯路也希望你那份数据集的 13,000 张图像能被用得值回票价。本文还有配套的精品资源点击获取