自然灾害图像分类实战:4400张数据集从预处理到迁移学习全流程
发布时间:2026/10/1 8:49:15来源:尧图网络
简介这是一份面向图像分类学习者和深度学习实践者的自然灾害图像分类数据集涵盖洪水、地震、火灾、台风四类约4400张已标注图片。资源已划分训练集与测试集目录按类别存放并附带用于可视化的show脚本和类别说明json文件适合入门CNN分类模型训练、迁移学习或数据增强实验。压缩包共2000个文件主体为1998张jpg图像辅以1个Python脚本和1份json配置整包约160MB结构清晰便于直接加载使用。已有241人学习下载读者可获得一套带标注的完整数据划分方案以及配套的可视化与配置工具省去自行爬取、清洗和标注的时间可直接用于灾害场景识别模型的训练与验证。若结合CNN分类网络改进系列博客还能进一步探索模型优化思路。1. 自然灾害图像分类数据集4400 张已标注图像能解决什么我接过不少应急相关的小项目最头疼的不是模型结构而是数据从哪里来。地震、台风这类灾害图像散落在新闻图和现场拍摄里想凑一批能训练的图像分类数据集光清洗就够熬几个通宵。这份“地震、台风等自然灾害图像分类数据集”恰好补上这个缺口约 4400 张已标注图像按灾害类型分好类拿来就能喂给 ResNet、ViT 这类图像分类模型做监督训练省掉最耗时的采集与标注环节。它适合三类人一是做应急管理信息化的一线工程师需要快速搭一个灾害场景分类基线二是研究方向与遥感、灾害图像识别相关的学生三是想完整走一遍“数据集划分 → 模型训练 → 结果分析”流程的从业者用这份数据练手迁移学习和图像分类的调参套路比在 CIFAR-10 上反复跑更有业务实感。4400 张不是什么大数字但配合迁移学习足够训练出可用的分类器。拿到压缩包别急着写训练脚本先把标签体系和类别分布盘清楚后面才不会翻车。2. 先盘点再训练把 4400 张图的类别分布与标签质量理清楚2.1 拿到压缩包先看目录结构标签体系藏在文件名里图像分类数据集的标注方式常见有三种按类别分文件夹、CSV/JSON 记录图像路径与标签、或者带边界框的检测格式。这份数据集从标题看是“已标注”但只有拆开压缩包才能确认具体是哪种。我拿到任何一份新数据集第一步永远是列目录结构而不是直接写 DataLoader。# 递归列出两层目录看清根目录布局 find . -maxdepth 2 -type d | sort # 排除缩略图等隐藏文件后统计每个子目录下的图片数量 find . -maxdepth 2 -type f \( -name *.jpg -o -name *.png -o -name *.jpeg \) -exec dirname {} \; | sort | uniq -c | sort -n第一条命令确认类别是不是以子目录形式存在第二条命令统计每个类别的样本量。这里的-maxdepth 2限制只查两层避免把缓存目录也统计进来uniq -c的输出顺序用sort -n排列能一眼看出哪几个类别样本多、哪几个少。如果根目录下没有类别子目录而是带labels.csv或annotations.json那就改用 pandas 检查标注字段。类别分布长什么样直接决定后面用不用class_weight这一步偷懒训练时就会在样本少的类别上付出代价。2.2 核对图像质量与标注一致性类别文件夹建好了不代表能直接训练常见隐患包括某几张图打不开、分辨率参差、混入黑白图更麻烦的是标注标准不一致——比如“台风”类别里混进了暴雨积涝图这种语义重叠会直接干扰模型学习。用一段脚本把图像可读性和基本尺寸提前筛出来from PIL import Image from pathlib import Path valid_ext {.jpg, .jpeg, .png} broken, gray, small [], [], [] for p in Path(./dataset).rglob(*): if p.suffix.lower() not in valid_ext: continue try: with Image.open(p) as im: im.verify() # 只校验文件头与完整性不加载像素 w, h im.size if min(w, h) 128: # 太小的图喂进网络是纯噪声 small.append(str(p)) if im.mode L: # 灰度图需要单独记录 gray.append(str(p)) except Exception as e: broken.append(f{p} - {e}) print(损坏:, len(broken), 小图:, len(small), 灰度图:, len(gray))用im.verify()而不是直接im.load()是因为 verify 模式不加载像素数据几千张图跑完也就几秒。min(w, h) 128这个阈值按你的网络输入来定如果只用 ResNet50 的 224×224 输入128 以下的图上采样后模糊严重建议直接剔除。灰度图不一定废弃转成 RGB 三通道复制也能用但我一般会单独记录防止它们成为模型学不到纹理的干扰样本。标注一致性这里有个土办法把每一类的代表图拼成一张 contact sheet人眼扫一遍就大致知道标注边界是否统一。地震灾后图和建筑倒塌图最容易混台风图和洪水图也容易混——这些语义重叠后面会反映在混淆矩阵里提前在标注阶段留个心眼。2.3 按类别分层划分训练集、验证集、测试集4400 张的规模不算大划分策略直接影响验证指标的可信度。常用的比例是 70/15/15关键在于必须按类别做分层抽样而不是随手random.shuffle。分层抽样的作用是让每个类别在训练集和验证集中保持相近的占比避免某个小类在验证集里只分到一两张——那样验证分数的波动会大到毫无参考价值。import pandas as pd from sklearn.model_selection import train_test_split df pd.read_csv(./dataset/labels.csv) # 或由类别目录构建出 image_path label 两列 train, tmp train_test_split( df, test_size0.3, stratifydf[label], random_state42 ) val, test train_test_split( tmp, test_size0.5, stratifytmp[label], random_state42 ) train.to_csv(./split/train.csv, indexFalse) val.to_csv(./split/val.csv, indexFalse) test.to_csv(./split/test.csv, indexFalse)stratifydf[label]保证划分后各类别比例和原始数据一致random_state42固定随机种子让划分结果可复现。我习惯把划分结果落成 CSV 文件而不是每次临时生成这样后续调试时只要报错信息里带着样本路径就能从 CSV 反查到它来自哪个集合排查加载 bug 会快很多。这里还有一个容易忽略的点测试集最好在来源上跟训练集独立。如果这批数据里的台风图像都来自同一批新闻图模型可能学到的是“新闻图色调”而不是“台风特征”。理想做法是从中抽出一部分来源不同的图像单独当测试集接现场采集的图像做外场验证这一步放在第 5 章避坑部分细说。提示每次划分都固定同一个随机种子并且把划分后的 CSV 提交进版本管理否则换个机器重跑一遍数据分布变了指标对比就失去意义。3. 从标注到可训练单标签分类的基线代码与损失选择3.1 先定任务类型单标签还是多标签图像分类数据集最常见的是单标签多分类——每张图只对应一个灾害类别。但灾害图像有个现实问题一张图可能同时出现倒塌建筑和火焰语义上既是“地震”也是“火灾”。要先确认标注文件里每张图是只有一条记录还是有多条只有一条就按单标签处理有多条就得走多标签训练。判断方法很直接打开标注文件看每行的结构。如果是image_path,label两列一行一张图那就是单标签。如果一张图出现多行不同 label说明标注方已经把多标签信息留了出来。以这份数据集的定位来看做单标签多分类是稳妥起点——先跑通基线再看测试集上的混淆集中在哪些类别这些混淆对就是潜在的多标签或细粒度分类优化点。一来基线的正确率能快速验证数据质量二来后面的迁移学习、数据增强调参都有对照对象。3.2 把标注转成训练能用的索引格式不管原始标注是类别目录还是 CSV训练代码里统一要一个“路径 类别索引”的映射。这里有个常见坑直接用中文类别名当天数写入 CSV 时编码一乱整个 DataLoader 直接报错。更规范的做法是给每个类别固定一个整数索引映射表单独存成一份 JSON。import json import pandas as pd from pathlib import Path # 假设标注是类别子目录结构 data_dir Path(./dataset) samples [] for cls_dir in sorted(data_dir.iterdir()): if not cls_dir.is_dir(): continue cls_name cls_dir.name for img_path in cls_dir.glob(*.jpg): samples.append([str(img_path), cls_name]) df pd.DataFrame(samples, columns[image_path, label]) classes sorted(df[label].unique()) class_to_idx {c: i for i, c in enumerate(classes)} with open(./split/class_to_idx.json, w, encodingutf-8) as f: json.dump(class_to_idx, f, ensure_asciiFalse, indent2) df[label_idx] df[label].map(class_to_idx) df.to_csv(./split/train_samples.csv, indexFalse) print(df.groupby(label)[label_idx].count())class_to_idx用sorted排序后编号保证同一份数据的映射在不同机器上一致ensure_asciiFalse让中文类别名以明文写入 JSON排查时一眼能看明白。把类别数量和各组样本量打印出来是对数据资产的第二次确认——这里的类别数和第 2 章目录统计对不上说明标注有缺失或重复趁早回去找原始数据别等模型训完才发现。3.3 ResNet50 迁移学习基线4400 张图从零训练一个深网络基本是浪费算力常规做法是加载 ImageNet 预训练权重后微调。ResNet50 是这类规模最稳的基线模型参数量适中在中等数据集上不容易像 ViT 那样欠拟合或过拟合两极摇摆。我用 PyTorch 写训练脚本时习惯把模型构建和数据增强拆开方便后面换模型。import torch from torch.utils.data import Dataset, DataLoader from torchvision import models, transforms from PIL import Image class DisasterDataset(Dataset): def __init__(self, csv_path, transformNone): self.df pd.read_csv(csv_path) self.transform transform def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img Image.open(row[image_path]).convert(RGB) label int(row[label_idx]) if self.transform: img self.transform(img) return img, label model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) num_classes len(json.load(open(./split/class_to_idx.json))) model.fc torch.nn.Linear(model.fc.in_features, num_classes) train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])weightsmodels.ResNet50_Weights.IMAGENET1K_V2直接拉官方预训练权重比自己下载再加载省事。model.fc替换成输出维度等于类别数的全连接层是迁移学习最标准的改法。RandomResizedCrop(224, scale(0.8, 1.0))是数据增强的核心项scale控制裁剪面积占原图的比例设成 0.8 到 1.0 是保守做法——灾害图像里关键结构往往分布在全图裁剪太狠反而把判别区域裁掉了。Resize 256再Crop 224是 ImageNet 时代的通用搭配给随机裁剪留了空间。训练参数方面batch_size取 32 或 64 都行4400 张的规模一个 epoch 大概几十个 step重载成本很低。优化器我常用 AdamW初始学习率 1e-4 起搭配余弦退火如果用 SGD学习率要放到 1e-3 附近并加 momentum。训练轮次控制在 15 到 20 轮就够这个规模下多了容易过拟合少了 val 指标还没稳定。3.4 类别不均衡要不要换损失函数类别分布不均时第一步不是换损失函数而是给 CrossEntropyLoss 传class_weight。计算方式是total_samples / (num_classes * class_samples)让样本少的类别在 loss 里获得更高权重代码量最小、效果最可预期。如果加权重后样本少的类别正确率还是上不去再考虑 Focal Loss。Focal Loss 的 γ 参数会让 hard example 的梯度占比变大非常适合难例集中的场景。但这里有个血泪经验类别严重不均衡时Focal Loss 调参比 CrossEntropy 加权重难得多γ 设 2.0 常有奇效但也可能让训练早期 loss 震荡得厉害。我的习惯是先用 CrossEntropy class_weight 跑通基线确认瓶颈确实在难例上再换 Focal而不是一上来就上复杂损失。Label smoothing 是另一个值得开的选项把 one-hot 标签变成 0.9/0.1 的软标签能抑制模型对训练集过于自信。灾害图像类间相似度本来就高软标签能让概率输出温和一些对后面第 6 章的置信度阈值思路也有利。4. 模型选型与迁移学习4400 张怎么用好 ResNet 与 ViT4.1 数据量决定模型天花板什么时候该碰 Transformer4400 张图像放在图像分类任务里属于“小规模但够用”的区间。ResNet50 在这个规模下训练稳定、收敛快是默认选择。EfficientNet 系列有更好的参数效率但调参空间更大作为第二候选可以作为基线没必要。ViT 这类纯 Transformer 模型对小数据量极不友好——原生 ViT 没有卷积的归纳偏置4400 张完全不足以让它从零学到局部纹理模式硬上只会得到一个验证集上震荡不安的模型。我并不是说 Transformer 不能用而是要用它的预训练版本。torchvision 里带了 ViT-B/16 的 ImageNet 权重加载后做全量微调在小数据集上是可行的。用不用取决于你对推理速度和精度的取舍ViT 的特征更全局化对一些“语义需要上下文”的灾害场景比如从大面积积水判断洪水可能比 ResNet 更准但参数量更大、训练更慢效果提升却不总是明显。模型预训练加载4400 张预期表现训练成本ResNet50容易稳定推荐做基线低EfficientNet-B4容易参数效率高略强于 ResNet50中ViT-B/16容易依赖微调技巧方差大高我的建议是拿 ResNet50 做基线把指标跑出来后再试 EfficientNet 或 ViT用同一个验证集对比。小数据集上模型间的差距往往没有“数据增强策略”带来的差距大把时间花在增强和数据清洗上更值。4.2 迁移学习的两种打开方式冻结骨干还是全量微调迁移学习有两种常见做法全量微调和分阶段解冻。全量微调适合目标数据和预训练数据分布差距较大的情况但训练时容易破坏已经学好的底层特征。分阶段解冻是先冻结骨干只训练分类头让新分类器先适应目标数据再解冻网络后半部分做微调——这是我处理小数据集时更常用的方式。# 第一阶段冻结骨干只训练分类头 for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True # 训练 5 个 epoch 后解冻最后两个 stage 微调 def unfreeze_last_stages(model, stages_to_unfreeze2): children list(model.children()) for param in children[-stages_to_unfreeze - 1].parameters(): param.requires_grad True # 同时把 fc 层保持可训练 for param in model.fc.parameters(): param.requires_grad True第一阶段学习率放到 1e-3 也安全因为只有一层在更新解冻后要降到 1e-4否则底层特征会被大步长更新扰乱。控制stages_to_unfreeze2是经验值——ResNet 有四个 stage解冻最后两个前面那些提取边缘纹理的底层特征保持原样中间层学到的抽象特征再做针对性调整。4400 张这个规模解冻面越大越容易过拟合这不是玄学而是参数量与样本量的直接矛盾。4.3 数据增强要克制不是越多越好小数据集的过拟合风险高数据增强是必需品但灾害图像有特殊性判别信息可能在图像中的任何位置——断裂的路面、倾斜的建筑、大面积积水。过强的随机裁剪或遮挡类增强会把这些关键线索直接裁掉模型学到的就是残缺特征。# 推荐的自然灾害分类增强策略 train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.7, 1.0)), # 保留足够上下文 transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15, fill0), # 小角度旋转模拟拍摄姿态差异 transforms.ColorJitter(brightness0.2, contrast0.2), # 轻微颜色扰动 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])和 3.3 的代码相比这里加了RandomRotation(15)和ColorJitter。角度限制在 15 度以内是因为无人机和手持拍摄的图像通常不会有大幅旋转填充值设 0旋转产生的黑边在 Normalize 后不会产生突兀的高亮噪声。ColorJitter 只扰动亮度和对比度不动色相——灾害图像里火焰的橙红色、洪水的土黄色是有判别力的乱动色相容易制造虚假颜色分布。CutMix、MixUp 这类的强增强在 4400 张数据上要谨慎。它们本质上是正则化手段适合大数据集和长训练周期小数据集用它容易欠拟合。如果你坚持要用把 CutMix 的概率控制在 0.3 以下并且观察训练 loss 是否能在前几个 epoch 正常下降——下降过慢多半是增强强度超过了数据本身的承受能力。提示验证集和测试集上不要用任何随机增强只做 Resize、ToTensor、Normalize。这是数据增强最基础的分寸错了的话验证指标会被训练数据污染看起来虚高实际无效。5. 常见翻车与排查分类训练到上线的 5 个坑5.1 训练 loss 降得很低验证准确率却卡在 60%现象训练集上 loss 一路降到 0.1 以下准确率接近 100%验证集却始终在 60% 上下波动。原因典型的过拟合但也可能是标签本身有噪声——某个类别内部混入了大量语义不一致的图。4400 张规模的训练集模型容量足够把这些错误标签“背下来”loss 自然很低验证却暴露了泛化问题。解决先停训练把训练集中预测置信度最高和最低的样本各抽 50 张看一遍。置信度最高且预测正确的多是重复背景图置信度最低的往往就是错标样本。如果是标注错误比例超过 5%返回标注环节修数据比调模型有效如果标注没问题再把第 4 章的冻结骨干训练方式加回来同时把 epoch 减少到 12-15 轮。5.2 验证集准确率很好换到真实场景立刻下降现象在数据集上训练测试准确率 90% 以上部署到现场拍摄的图像上准确率跌到一半以下。原因数据分布漂移。这份数据集里的图可能大多来自新闻图片分辨率高、构图规范、画面干净现场拍摄的图则可能是手机随手拍、逆光、模糊、有遮挡。模型学到的其实是“新闻图的色调与构图”而不是灾害本身的特征。解决从真实场景收集一批带标注的现场图单独作为外场测试集。这部分数据量有几百张就够不用参与训练只做最终评估。如果现场测试集准确率远低于数据集测试集说明这个模型当前不能直接上线需要补充现场数据进行二次微调或者退回到“模型初筛 人工复审”的半自动流程。这一步没有捷径数据的来源多样性必须由真实分布决定。5.3 模型对大部分样本都预测“地震”这一类现象训练的模型在推断时倾向输出某一个类别比如一半以上的测试图都被划到“地震”其他类别几乎不出现。原因类别不均衡之外更大的可能是标注文件里某个类的样本绝对量占优比如 4400 张里“地震”占了近一半模型学到的先验分布被带偏了。另一个隐蔽原因是 DataLoader 里类别索引和映射表对不上导致某类样本被重复加载或丢失。解决给损失函数加class_weight方法在第 3.4 节。同时用验证集打印每类样本数量确认是不是某个类在划分时被抽空了。最有效的排查工具是交叉矩阵——把测试集的预测结果按类别统计看预测都集中在哪一类再把该类对应的 feature 图可视化确认模型到底在学什么特征。5.4 Transformer 直接训不收敛loss 降不下去现象用预训练 ViT 微调时不冻结骨干学习率设成 1e-3loss 在初始值附近抖动几个 epoch 都下不来。原因ViT 对学习率极其敏感小数据集上尤其明显。它的优化器状态和 CNN 完全不同AdamW 默认学习率直接照搬 ResNet 的参数必然翻车。解决ViT 微调时把学习率降到 1e-5 到 5e-5 之间并加一层 warmup——前 500 步线性增加到目标学习率。冻结所有 patch embedding 层和前几个 transformer block只训练最后几个 block 和分类头。这样做的原理是视觉 Transformer 的低层 patch embedding 已经从 ImageNet 学到了通用局部特征灾害图像虽然域不同但底层纹理特征没有本质差异微调高层就足够。5.5 拿分类数据集直接喂给 YOLO 做检测训练现象有人看到“已标注”两个字就假设有边界框直接按 YOLO 格式组织数据结果训练报错或者 loss 很大没收敛。原因这份数据的标注是“分类标注”——每张图对应一个类别没有边界框坐标。分类标注和检测标注是两回事检测训练必须有x_center, y_center, width, height缺了就是缺了不能拿类别标签硬凑。解决先确认标注文件格式再选任务。如果业务需求确实是检测那需要额外补标注用 CVAT 或 labelImg 在类别图上重新画框这一步的时间和人力成本要提前评估。如果只是做“图像属于哪类灾害”的判定就用分类模型不要被检测检测工作流带偏。6. 上线前的最后一步用混淆矩阵和置信度阈值兜底训练完成不等于可以交付。我每做完一个分类项目都会在测试集上跑一遍混淆矩阵再统计错误样本的置信度分布。需要先判断分类器能不能直接上线还是只能做“自动初筛”把拿不准的丢给人。很多灾害图像分类的应用场景里有一个人工复核环节因为图片语义复杂、涉及后续应急决策不可能完全依赖模型。所以模型输出一个置信度分数阈值以上的自动分类阈值以下的进入人工队列这是实际业务里最常用的闭环。import numpy as np import seaborn as sns import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix y_true, y_pred, y_conf [], [], [] with torch.no_grad(): for imgs, labels in test_loader: logits model(imgs) probs torch.softmax(logits, dim1) conf, pred torch.max(probs, dim1) y_true.extend(labels.tolist()) y_pred.extend(pred.tolist()) y_conf.extend(conf.tolist()) cm confusion_matrix(y_true, y_pred) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelsclasses, yticklabelsclasses) plt.xlabel(Predicted) plt.ylabel(True) plt.savefig(./metric/confusion_matrix.png, dpi150) # 找低置信度样本准确率与阈值的关系 y_true, y_pred, y_conf np.array(y_true), np.array(y_pred), np.array(y_conf) for th in [0.5, 0.7, 0.9]: mask y_conf th acc (y_true[mask] y_pred[mask]).mean() * 100 print(fthreshold{th}: coverage{mask.mean()*100:.1f}%, acc{acc:.2f}%)这段代码的核心是把softmax的概率值当作置信度画出不同阈值下的覆盖率和准确率。coverage指有多少样本的置信度超过阈值acc是这些样本里的正确率。一个好的业务配置是让 coverage 在 70%-80%剩下的 20%-30% 进入人工复核。常见的实际情况是某个类别内部高度相似比如台风与洪水类模型会把两者混在一起此时把这类样本的阈值调低让它们多数进入人工队列比硬调模型参数性价比高得多。混淆矩阵的作用是定位类别间的系统性混淆。如果 80% 的“台风”被预测成“洪水”就说明这两类的视觉特征边界模糊——要么是标注标准本身不统一要么是两类图像在真实场景中确实存在叠合。这时候回到第 1 节用 contact sheet 重新检查这两类的标注标准比继续优化模型结构见效更快。从那以后我每次拿到新的图像分类数据集都强制走一遍这五步统计类别分布、核对标注一致性、分层划分、跑基线模型、画混淆矩阵和置信度曲线全部通过才谈部署上线。这套流程谈不上惊艳但每一次都能在数据集里找到预期之外的问题也帮我从“训练任务”快速切换到“业务交付”的视角希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网