简介这份数据集面向深度学习与计算机视觉方向的开发者及学习者尤其适合正在开展图像分类实践、灾害场景识别研究的人群。资源包含洪水、地震、火灾、台风四个类别共约4400张已标注自然灾害图像jpg图片按类别归档json文件记录分类标签py可视化脚本可快速预览样本分布。压缩包共2000个文件整体160.05MB结构简洁便于纳入自定义训练流程。数据已预先划分好训练集与测试集可直接送入CNN等分类网络开展训练与评估json文件中的类别定义也方便后续扩展或修改运行show.py即可直观核对四类样本形态与数量适合作为入门分类实验、课程设计或网络改进研究的基准数据。目前已有241人学习下载对需要构建灾害识别模型、探索CNN分类网络改进方案的读者来说是一份可直接落地的数据支撑。1. 4400张标注灾害图像能把图像分类做到什么程度先给结论地震、台风这类自然灾害图像分类数据集标注量在4400张是一个“刚好能跑迁移学习、但不足以让你在类间平衡上偷懒”的量级。它的典型应用场景是灾情快速评估、无人机/监控图像的分拣归档以及给后续目标检测任务做类别预筛。灾害图像的“图像分类”难点并不在分类本身而在同一类别内部尺度差异极大——同样是“台风”可能是漫天乌云的远景也可能是房屋屋顶被掀翻的特写。这种差异会让模型在训练集上轻松跑到95%以上到了新场景却直接翻车。适合用这个数据集的人有三类正在做灾情评估系统原型验证的算法工程师需要给大量历史灾害照片做自动归档的行业从业者以及想从零走一遍数据标注到模型部署全流程的学生或转行者。4400张意味着你不必从零训练但也意味着数据清洗、划分策略和验证方式必须认真对待否则后面每一步都在给模型挖坑。2. 拿到数据集先别训练目录、标注与类别分布的验货脚本很多人拿到数据集的第一反应是直接写训练脚本这是最容易后悔的做法。灾害图像数据集的标注质量参差不齐类别分布可能极不均衡目录里可能混着打不开的损坏文件或重复图片。花二十分钟做一次验货能省下后面几天的排错时间。2.1 两种常见组织方式按目录分类与CSV标注怎么解析自然灾害图像分类数据集的常见组织方式一般是两种。第一种是按目录分类形如train/地震/xxx.jpg、train/台风/xxx.jpg类别名就是文件夹名这种格式对 PyTorch 的ImageFolder最友好但你没法附带额外的元信息比如拍摄时间、地点、事件编号。第二种是图片平铺在images/目录下另外提供一个 CSV 或 JSON 标注文件每行记录文件名, 类别, 可选的事件ID或地区。后一种更常见于需要防数据泄漏的场景。先写个脚本把两种格式都摸清楚from pathlib import Path from collections import Counter import pandas as pd # 场景A按目录分类。root目录下是 train/val/test 三个split每个split下是类别文件夹 root Path(natural_disaster_dataset) img_exts {.jpg, .jpeg, .png, .webp} def count_by_dir(split_dir: Path) - Counter: cnt Counter() for cls_dir in split_dir.iterdir(): if not cls_dir.is_dir(): continue n sum(1 for p in cls_dir.rglob(*) if p.suffix.lower() in img_exts) cnt[cls_dir.name] n return cnt total Counter() for split in [train, val, test]: c count_by_dir(root / split) total c print(f[{split}] 类别数{len(c)}总样本{sum(c.values())}) for cls, n in total.most_common(): print(f{cls}: {n}张占比{n / sum(total.values()) * 100:.1f}%)这段代码做的事情很简单遍历每个 split 下的类别目录统计每类图片数量并打印占比。参数说明rglob(*)是为了兼容“类别目录下还有子目录”的情况有些数据集把同一事件的照片放在子目录里img_exts集合决定了哪些后缀被计入如果数据集混入了 TIFF 或 BMP需要手动补充后缀。这里最值得关注的是占比那一列——如果最大的类别占了60%以上后面训练时必须处理不平衡而不是直接套用默认的交叉熵损失。如果拿到的是 CSV 标注解析逻辑不一样import pandas as pd from pathlib import Path df pd.read_csv(labels.csv, sep,) # 有些数据集用\t分隔注意sep参数 print(df.head(10)) # 先看列名filepath? label? event_id? print(df[label].value_counts(normalizeTrue)) # 类别占比 # 检查标注文件里有多少路径实际存在 missing [p for p in df[filepath] if not Path(p).exists()] print(f标注中存在但文件缺失的记录数: {len(missing)})CSV 格式的坑通常在分隔符和列名上。sep,遇到文件名里含逗号的数据集会解析错位所以第一行输出前先df.head()确认结构比直接跑训练踏实得多。value_counts(normalizeTrue)直接输出各类别占比一眼看出有没有类别被标注成空目录或只有一两张的极端情况。2.2 用脚本核对类别分布与文件完整性验货的第二步是核对文件完整性这一步比很多人都想象的重要。真实的数据集在打包、压缩、传输过程中可能丢失文件、损坏图片、或者混入非图片文件。PyTorch 的 DataLoader 遇到坏图只会在读取时报错而且报错信息往往不指明确切文件排查成本极高。from PIL import Image from pathlib import Path root Path(natural_disaster_dataset) corrupted [] for p in root.rglob(*): if p.suffix.lower() not in {.jpg, .jpeg, .png}: continue try: with Image.open(p) as img: img.load() # 强制解码验证文件是否真的能读 if img.width 32 or img.height 32: corrupted.append((str(p), 尺寸过小)) except Exception as e: corrupted.append((str(p), str(e))) for path, reason in corrupted[:20]: print(f{reason}: {path}) print(f问题文件总数: {len(corrupted)})这段脚本的核心是img.load()——它不只是打开文件头而是真正解码像素数据。有些图片预览正常但解码到一半会报Image file is truncated这类文件在训练时会随机中断流程。尺寸过滤条件width 32 or height 32针对的是缩略图或损坏后残留的小尺寸文件灾害数据集中偶尔会有从网页爬下来的低分辨率缩略图混入这些图对分类训练没有正向帮助反而会干扰模型学习。我一般会把这个脚本的结果保存成日志然后把问题文件移动到_rejected/目录而不是直接删除。原因是后面如果发现某个类别数量异常偏少可能还需要回过头来检查被拒文件是不是被误判了。2.3 顺带检查“时间与事件”维度防止后面验证集失效这一点是灾害图像数据集特有的坑而且是最容易被忽略的。普通的图像分类数据集比如 CIFAR、ImageNet每张图片彼此独立随机划分训练集和验证集没问题。灾害图像不一样——同一个台风事件拍出来的几百张照片在背景、光照、拍摄设备上高度相似如果这些照片被同时分进训练集和验证集模型在验证集上的表现会虚高。后面真正部署到新场景时面对的是没见过的背景和拍摄条件准确率会断崖式下跌。如果你拿到的 CSV 里有event_id或date字段值得做一次快速检查# 检查每个事件下的样本分布为后续按事件分组划分做准备 if event_id in df.columns: event_stats df.groupby(event_id)[label].value_counts().unstack(fill_value0) print(event_stats) # 输出每个事件涉及多少个类别、每类多少张 print(event_stats.sum(axis1).describe())这段代码按event_id分组列出每个事件里各类别的样本数。describe()输出的均值和中位数可以帮你判断“大部分事件只有几十张”还是“少数事件占了绝大多数数据”。如果后者成立后面训练时的划分方式需要从随机划分改成按事件分组划分具体做法在第4章会展开。现在只要确认这个维度的信息存在即可——很多数据集根本不提供 event_id这时候只能在后续训练中用特征相似度检查数据泄漏成本高得多。3. 用迁移学习跑通基线ResNet50在4400张上的完整训练流程验完货之后就可以跑第一个模型。我的建议是第一版不要追求花哨直接拿 ImageNet 预训练的 ResNet50 做迁移学习跑通流程、拿到可信的评估数字再谈优化。4400张数据训练一个全量 ResNet50 会严重过拟合但微调最后几层加上适当的数据增强完全可以得到一个可用的基线。3.1 划分策略分层划分比随机划分更靠得住划分数据是最容易被轻视的步骤但它直接决定你后面所有评估数字是否可信。4400张数据如果随机切分小类别可能全部掉进验证集或训练集导致验证集里某个类别只有几张图指标震荡剧烈。分层划分Stratified Split保证每个类别在训练集、验证集、测试集中的比例与原数据集一致。import numpy as np from sklearn.model_selection import StratifiedShuffleSplit # df.columns [filepath, label]已经过验货清洗 X df[[filepath]].values y df[label].values # 第一层分出测试集20%剩下80%用于训练验证的后续划分 sss StratifiedShuffleSplit(n_splits1, test_size0.2, random_state42) train_val_idx, test_idx next(sss.split(X, y)) # 第二层从剩余80%里再分出12.5%作为验证集占全量10% X_train_val X[train_val_idx] y_train_val y[train_val_idx] sss2 StratifiedShuffleSplit(n_splits1, test_size0.125, random_state42) train_idx, val_idx next(sss2.split(X_train_val, y_train_val)) train_df df.iloc[train_val_idx[train_idx]] val_df df.iloc[train_val_idx[val_idx]] test_df df.iloc[test_idx] print(f训练集 {len(train_df)}验证集 {len(val_df)}测试集 {len(test_df)}) print(训练集类别分布:, train_df[label].value_counts().to_dict())test_size0.2再加上第二轮划分时乘上的0.125最终得到 70% / 10% / 20% 的划分比例。4400张的情况下测试集大约 880 张验证集 440 张这个量级足够让评估指标稳定。random_state42保证可复现——同一个数据集、同一个随机种子训练结果才有可比性这是之后做任何实验对比的前提。这里建议把划分结果保存成 CSV 文件后续所有实验都复用同一份划分不要每次运行都重新切否则不同实验之间的指标差异会混入数据划分的随机性。3.2 数据增强针对灾害场景的扰动参数怎么设灾害图像和普通场景图像有个明显区别拍摄环境不可控亮度、清晰度、拍摄角度变化极大。同一场地震无人机航拍是俯视图地面拍摄是平视图手机照片可能带强烈的暗角或运动模糊。数据增强的目标就是模拟这种多样性。from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomAffine(degrees10, translate(0.1, 0.1)), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.2), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])逐项说明参数选择。Resize((256, 256))之后接RandomResizedCrop(224, scale(0.7, 1.0))先放大再随机裁剪一个区域模拟同一场景的不同取景范围scale下限设为0.7是因为灾害主体在画面中的占比浮动大——如果裁剪范围太激进可能会裁掉实际的损毁区域模型学到的是“碎砖纹理”而不是“倒塌建筑”。ColorJitter(brightness0.3, contrast0.3)模拟不同天气和光照条件下的色彩变化但饱和度扰动只设 0.2原因是灾害图像的色彩本身偏灰暗过度扰动饱和度会让模型更难区分正常建筑和废墟。验证集不用数据增强只做缩放和中心裁剪这是为了保证评估时的输入一致性——验证集加随机扰动会让指标每次运行都不一样没法做比较。3.3 冻结与微调4400张规模下ResNet50的参数组合迁移学习的核心问题是“冻结哪些层、微调哪些层”。4400张数据量不算大我的建议是第一轮冻结骨干网络的大部分层只训练分类头和最后两个残差块layer4等验证集指标稳定后再解冻更多层做微调。这样做的原因是低层特征边缘、颜色、纹理在 ImageNet 上已经学得很好灾害图像和自然图像在这些基础特征上没有本质差异需要调整的是更高层的语义特征组合。import torch import torch.nn as nn from torchvision import models num_classes len(train_df[label].unique()) device cuda if torch.cuda.is_available() else cpu model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) # 冻结全部参数然后只解冻 layer3 和 layer4 for param in model.parameters(): param.requires_grad False for param in model.layer3.parameters(): param.requires_grad True for param in model.layer4.parameters(): param.requires_grad True # 替换分类头新分类头默认 requires_gradTrue model.fc nn.Linear(model.fc.in_features, num_classes) model model.to(device) # 用两组学习率分类头学快一点骨干层学慢一点 optimizer torch.optim.SGD([ {params: model.fc.parameters(), lr: 1e-3}, {params: model.layer3.parameters(), lr: 1e-4}, {params: model.layer4.parameters(), lr: 1e-4}, ], momentum0.9, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30, eta_min1e-6) criterion nn.CrossEntropyLoss()scheduler选了CosineAnnealingLRT_max30对应训练轮数——余弦退火的动机是把学习率平滑地从初始值降到最低在后期让模型在小学习率下做精细调整这比固定学习率更不容易在末尾来回震荡。weight_decay1e-4是经验值太小起不到约束作用太大会让已学到的低层特征被过度衰减。Loss 先用默认的CrossEntropyLoss不做类别加权因为第一版基线需要看到真实的过拟合程度和类别表现直接加权会掩盖不平衡问题的严重程度。等基线跑出来发现某类召回率低再针对性地引入第4章的加权方案。训练循环本身用标准写法即可但有一个细节值得注意每个 epoch 结束后在验证集上的评估不能用loss做唯一判断要同时记录每类的召回率。4400张的小数据集上loss 可能稳定下降但某个类别已经崩了——比如“山体滑坡”类被全部预测成“地震”因为两者在灰度纹理上高度相似。这类问题只有看分类别指标才能发现。3.4 评估指标不平衡数据里accuracy会骗人灾害图像分类数据集的类别分布天然不平衡地震和台风的样本量可能是山体滑坡的十倍。这种场景下整体 accuracy 会掩盖一切问题——假设模型把“山体滑坡”全部预测成“地震”只要地震类占多数准确率依然能到90%以上但这个系统在实际上是完全不可用的。import torch from sklearn.metrics import classification_report, confusion_matrix model.eval() all_preds, all_labels [], [] with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) class_names list(train_df[label].unique()) print(classification_report(all_labels, all_preds, target_namesclass_names)) print(confusion_matrix(all_labels, all_preds))classification_report会输出每个类别的 precision、recall、F1这是判断模型是否“偏科”的最快方式。我通常用 macro-F1 作为总指标——它把每个类别的F1取平均不受类别样本量影响。一个 macro-F1 在 0.85 但 accuracy 在 0.93 的模型比一个 accuracy 在 0.96 但小类别召回率只有 0.1 的模型可靠得多。confusion_matrix用来找“混淆对”——如果地震和山体滑坡互相误报严重后面就要考虑是否把这两个类别合并或者给模型增加区分这两个类别的专项数据。4. 把准确率做成可用系统类别加权、事件分组与阈值决策基线跑通之后接下来做的事情是把模型从“能分类”推向“能用于实际判断”。这一章里的四个调整是灾害图像分类从实验室走向业务的关键彼此独立但建议按顺序做。4.1 类别不平衡的三档解法从class weight到Focal Loss如果第3章的 basline 暴露了小类别召回率低的问题第一档解法是给交叉熵损失加类别权重。权重设置的原则是不平衡越严重权重越大但也不是简单地用逆频率——逆频率会把小类别的权重拉得非常大导致模型在小类别上过拟合把大量正常样本误判成灾害。from sklearn.utils.class_weight import compute_class_weight import torch # 计算每个类别的样本数 class_counts train_df[label].value_counts().sort_index() # 方法一直接逆频率 weights 1.0 / class_counts.values # 方法二中位数频率平衡抑制极端权重 median_count np.median(class_counts.values) weights median_count / class_counts.values # 做归一化控制幅度 weights weights / weights.sum() * len(weights) class_weights torch.tensor(weights, dtypetorch.float32).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)compute_class_weight是 sklearn 提供的便捷函数但它默认返回严格逆频率。我更推荐median_count / class_counts这种中位数频率平衡因为它不会让最大类别和最小类别的权重差距超过一个数量级。如果类别数量悬殊达到 30 倍以上直接逆频率会让模型把训练重点完全放在小类别上破坏整体精度。如果做了类别加权后小类别仍然不达标第二档是换 Focal Loss。Focal Loss 会降低“易分类样本”的损失权重让模型更关注那些置信度不高的难样本。这在灾害图像里很有用因为大量样本根本不是“明确的可分类对象”——半倒塌的建筑从某个角度看可能像正常建筑。import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, alphaNone, gamma2.0): super().__init__() self.alpha alpha # 类别权重优先用中位数频率平衡后的值 self.gamma gamma def forward(self, logits, targets): ce_loss F.cross_entropy(logits, targets, reductionnone) pt torch.exp(-ce_loss) # 当前样本的预测置信度 focal (1 - pt) ** self.gamma * ce_loss if self.alpha is not None: alpha self.alpha.to(logits.device) focal focal * alpha[targets] return focal.mean()gamma2.0是论文里的默认值实际使用中我会在 1.0 到 2.5 之间调。gamma 越大模型越聚焦于难样本但过大容易让小噪声样本主导训练。Focal Loss 的前提是样本类别不平衡且难易程度差异大如果你的数据集整体类别分布还算均匀它的收益反而不如 class weight 明显不必盲目上。4.2 按事件分组划分避免同一次灾害的数据泄漏第2章检查过 event_id 之后这里要落地实现。如果同一事件的照片同时出现在训练集和验证集验证集的作用就失效了——模型不是在做泛化而是在“回忆”同一个场景里的背景特征。from sklearn.model_selection import GroupShuffleSplit # df 必须包含 event_id 列用于标识同一次灾害事件 groups df[event_id].values gss_train GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_val_idx, test_idx next(gss_train.split(df, df[label], groupsgroups)) # 验证集也要按事件组划分保证同一事件的照片不会跨 train/val gss_val GroupShuffleSplit(n_splits1, test_size0.125, random_state42) train_idx, val_idx next( gss_val.split(df.iloc[train_val_idx], df.iloc[train_val_idx][label], groupsdf.iloc[train_val_idx][event_id]) )这段代码与第3章分层划分的区别在于GroupShuffleSplit的分组对象是event_id而不是单个样本。它保证同一事件的所有照片被分到同一侧训练集和验证集之间不存在来自同一事件的相似图片。注意这里test_size的语义已经变了——它控制的是事件组的比例而不是样本的比例所以验证集的样本量可能偏离 10% 不少要做好心理准备。如果某个事件的照片特别多比如一次台风拍摄了800张它会在测试集里占很高比例导致测试结果被这一事件主导。这种情况可以考虑按事件做上限截断或者在事件维度上重新加权评估指标。严格按事件分组会让验证集指标比随机划分低 3 到 8 个百分点这是正常的不要因为指标下降就改回随机划分。下降的那部分才是模型真实泛化能力的检测。4.3 ViT / EfficientNet 对照什么时候值得换骨干网络ResNet50 作为基线稳妥但它不是唯一选择。EfficientNet 在同等计算量下精度更高ViT 在数据量足够时分类表现更好但两个网络在 4400 张数据下的表现都需要实测验证。from torchvision import models # 方案一EfficientNet-B0相比ResNet50参数更少适合小数据集 model models.efficientnet_b0(weightsmodels.EfficientNet_B0_Weights.IMAGENET1K_V1) model.classifier[1] nn.Linear(model.classifier[1].in_features, num_classes) # 方案二ViT-B/16Transformer结构需要更多数据但特征更全局 model models.vit_b_16(weightsmodels.ViT_B_16_Weights.IMAGENET1K_V1) model.heads.head nn.Linear(model.heads.head.in_features, num_classes)我的判断标准是如果类别数在 6 到 10 个之间先跑 EfficientNet-B0 做对照它的参数量只有 ResNet50 的一半左右过拟合风险更低。如果发现模型出现“对损毁纹理过拟合、对整体结构理解不足”——典型现象是废墟和正常建筑在局部纹理相似时误判——再上 ViT因为 Transformer 的全局注意力机制能更好地整合建筑结构和损毁区域的空间关系。但 4400 张数据对 ViT 来说偏少需要依赖更强的数据增强和正则化否则 ViT 在验证集上的表现可能反而不如 ResNet50。我一般会把三个模型各跑一遍把所有结果放在一张表里对比 macro-F1、每类召回率和推理时间用数据和业务场景做决定。泛泛地说“ViT 更好”在灾害图像上不成立。4.4 置信度阈值与决策把模型输出交给业务分类模型的输出不是一个类别而是一组概率。直接把argmax的结果当作最终判断在灾害场景里很危险——低置信度的预测可能是错判而这种错判在紧急情况下会带来实际影响。更合理的做法是用置信度做分级决策。model.eval() threshold 0.75 # 可调参数根据验证集上的置信度-精度曲线确定 with torch.no_grad(): logits model(images) probs torch.softmax(logits, dim1) conf, pred torch.max(probs, dim1) # 把低置信度样本单独标记出来不参与自动分类 uncertain_mask conf threshold print(f本次批次不确定样本: {uncertain_mask.sum().item()}/{len(conf)}) # 这些样本应该进入人工复核队列而不是直接给一个硬标签threshold0.75是一个起点值更严谨的做法是在验证集上绘制“置信度 vs 准确率”曲线找到准确率开始下降的拐点作为阈值。这个阈值的意义是模型告诉我们“它不确定”这件事本身是有价值的信息。在 4400 张这样的小数据集上模型的不确定性往往集中在训练样本覆盖不足的场景把这些样本挑出来做人工复核比盲目相信模型判断更可靠。5. 灾害图像分类的避坑记录六个常见翻车现象与修复这一章的每一条都来自真实项目里的血泪经验。现象写得具体是因为只有你能对着自己的数据集找到相同症状修复方案才有意义。5.1 验证集95%新照片直接翻车现象模型在验证集上准确率 95%拿到一批新拍摄的灾害照片准确率掉到 60% 出头错误集中在“地震”和“正常建筑”之间。原因灾害图像数据集的两个特点放大了过拟合——同一事件的照片在背景、拍摄设备、天气条件上高度相似随机划分时这些高度相似的照片被同时分进训练集和验证集同时数据量只有 4400 张模型很容易记住训练集里的背景特征比如某场地震的特定街区而不是损毁本身。解决第一步按第4.2节改成 event_id 分组划分让验证集只包含模型没见过的灾害事件。第二步数据增强要加 RandomAffinedegrees10, translate(0.1, 0.1)让模型对拍摄角度和画面偏移不敏感。第三步如果还不行把训练集的背景多样性做可视化——随机抽 100 张训练图片看背景是否过于集中如果集中需要补充更多不同地区、不同季节的正常建筑照片作为负类。5.2 模型学的是“救援车”而不是“废墟”现象模型把“地震”类别的召回率做到 0.9但错误分析时发现它判断地震的依据是画面里的救援卡车、帐篷和消防员而不是倒塌的建筑物。原因灾害数据集的天然偏见——救援现场的车辆和设备出现在大部分地震照片里模型用最简单的视觉线索车轮、帐篷形状代替了真正的结构化特征。这在用 Grad-CAM 可视化时看得非常清楚模型的注意力集中在车辆上而不是建筑裂缝上。解决第一步做数据清洗统计训练集里每张图片是否包含明显的车辆或帐篷可以用简单目标检测器辅助把这类图片单独建一个 “rescue_logistics” 类别或者直接从训练集里删除避免模型把“车辆出现”等同于“地震发生”。第二步扩充负类样本让正常的建筑场景和车辆出现在“正常”类别里。如果数据集不允许增加外部数据退而求其次是类别加权时降低“地震”类的权重强行抑制模型对高频特征的依赖。5.3 小类别召回率个位数accuracy却很高现象山体滑坡类别只有 160 张训练样本测试集召回率 5%但整体准确率仍然有 92%因为山体滑坡在所有测试样本里只占 8%。原因交叉熵损失默认对所有样本一视同仁模型为了让总体损失最小化策略是放弃小类别把所有样本都预测到大类别上。这个问题的细心之处在于它不是模型坏了而是优化目标本身和业务目标不匹配。解决先按第4.1节加中位数频率平衡的类别权重把山体滑坡的损失权重提上去。观察两轮训练后的召回率变化——如果提升不明显再换 Focal Lossgamma 从 2.0 开始。如果小类别仍然起不来检查一下这个小类别的样本质量是不是标注有噪声、或者各类别间纹理过于相似。最后手段是对小类别做过采样复制样本到与实际数量 1:1数据增强里对小类别单独加扰动。5.4 雨天正常街景被误报为“台风”或“内涝”现象验证集里大量“正常”类别的图片被预测成“台风”这些图片的共同特点是阴天、雨水、路面反光出现在台风季前后。原因模型的判断依据是“天气暗淡路面反光”这个强视觉线索而不是“房屋损坏、树木倒伏”这些结构性损毁特征。这和第5.2条同源——模型总倾向于找最容易区分的表面特征而灾害图像里这些表面特征往往和天气相关。解决把 ColorJitter 增强加强为 brightness0.4、contrast0.4强迫模型不能依赖固定的光照模式。同时检查“正常”类别里的训练样本如果正常类别全部来自晴朗天气模型当然会把“阴天和暗光”当成灾害特征。从公开数据集里补充一些阴天、雨天的正常街景图片到“正常”类别让这个类别的特征多样性覆盖到灾害天气的视觉范围。5.5 测试集划分太小训练五次差四个点现象同一个人同一个代码同一个随机种子连续训练五次验证集 accuracy 在 0.88 到 0.92 之间波动。原因4400 张数据按 70/10/20 划分后验证集只有 440 张小类别在验证集里可能只有 10 到 20 张。每张图的误判都会让指标跳动 0.5% 到 1%如果误判恰好集中在小类别macro-F1 的波动会更明显。解决两个方向。第一把验证集和测试集合并成 30%用 5 折交叉验证评估模型稳定性报告均值和标准差第二固定随机种子并多次训练取中位数不要只跑一次就下结论。交叉验证在 4400 张的数据量下是更可靠的评估方式只是要接受训练时间变成原来的五倍但这对判断一个模型到底行不行是必要的投入。最终的模型用全部数据重新训练一遍再用单独的测试集做最终评估。5.6 一张图同时存在两种灾害粗粒度标签打架现象台风引发了洪水一张照片里房屋被泡在水里屋顶也被掀翻。这个样本在数据集中被标成“台风”但模型预测成“洪水”训练时不断在这两个类别之间震荡两个类别的指标都不稳定。原因灾害图像分类数据集的粗粒度标签假设“一张图只有一个类别”但自然灾害的实际场景往往是链式发生的——地震引发海啸台风引发洪水和滑坡。强行为一个多灾害样本分配单一标签标签本身就存在噪声。解决如果确认数据集里混入较多这类样本有两个方案。方案一是把常伴随发生的灾害合并成组合类别比如“台风洪水”让类别变成互斥事件。方案二是改成多标签分类用BCEWithLogitsLoss替代CrossEntropyLoss让一张图可以同时被预测为“台风”和“洪水”。后者更符合实际业务需求但评估指标要从准确率换成每个标签的 AUC复杂度会上一个台阶。如果项目周期紧先用方案一合并类别让标签自洽。6. 用 Grad-CAM 验证模型到底在看什么训练完模型、指标也满意了先别急着部署。在灾害图像分类这类对判断依据有实际要求的任务里模型“为什么这么判断”和“判断得对不对”同等重要。Grad-CAM 通过梯度计算生成热力图把模型决策时的注意力区域可视化——你一眼就能看出来模型是在看倒塌的墙体还是在看车道线。import torch from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image import cv2 import numpy as np # target_layers 选择最后一个卷积层包含最高层语义特征 # ResNet50 用 layer4[-1]EfficientNet 用 features[-1]ViT 不适用传统 Grad-CAM target_layers [model.layer4[-1]] cam GradCAM(modelmodel, target_layerstarget_layers) model.eval() with torch.no_grad(): logits model(image_tensor.unsqueeze(0).to(device)) pred_class torch.argmax(logits, dim1).item() # 生成热力图并叠加到原图上 targets [ClassifierOutputTarget(pred_class)] grayscale_cam cam(image_tensor.unsqueeze(0), targetstargets) heatmap show_cam_on_image(np.float32(image_np) / 255.0, grayscale_cam[0], use_rgbTrue) cv2.imwrite(gradcam_output.jpg, heatmap)这段代码的核心参数是target_layers——选哪个层决定了热力图反映“低层纹理线索”还是“高层语义线索”。灾害图像上用最后一个卷积层最合适低层特征边缘、颜色不足以解释模型为什么把一个场景分成地震而最后一个卷积层已经把局部纹理抽象成了结构语义。ClassifierOutputTarget(pred_class)指定只对模型的预测类别做解释如果你想研究“为什么没有把它预测成洪水”可以改成ClassifierOutputTarget(洪水的类别ID)看模型在看到这张图时对洪水类别的注意力分布。我拿到训练好的模型后习惯性会做两件事第一把测试集里所有误报样本的 Grad-CAM 图集中保存下来按错误类型分类看模型是“看到了正确区域但判断错误”还是“注意力完全跑偏”。前者说明特征表达不够后者说明模型被表面特征误导。第二从每个类别抽 20 张正确预测的样本确认模型判断依据的稳定性——如果同一类别的热力图分布差异过大说明模型对这类别的判断路径不统一这类样本再多也没有形成一个稳定的判别模式。这两个检查做完模型才能放心交给业务方。灾害图像分类这个方向的价值不在于把 accuracy 刷到 99%而在于模型在关键时刻的判断依据是可信的、稳定的。判断依据不对准确率再高也只是个黑匣子碰巧答对了题目。4400 张数据注定了这一行不会像大规模预训练那样“力大砖飞”但如果能把数据验货、划分策略、类别处理和可视化验证这四件事做扎实这套方法论可以照搬到任何一个小样本图像分类项目上。希望帮到你。本文还有配套的精品资源点击获取