遥感影像滑坡场景分类实战:基于ResNet迁移学习的完整指南

发布时间:2026/9/24 18:12:40
遥感影像滑坡场景分类实战:基于ResNet迁移学习的完整指南 简介基于Python的遥感影像滑坡场景分类任务代码与配套项目文档面向毕业设计、课程设计及项目开发场景适合具备Python与机器学习基础、希望快速上手图像分类完整流程的开发者。压缩包共21个文件大小仅1.86MB包含4个Python源码、4个pkl模型/数据文件、4个txt标签文本、LDA模型与id2word词典、visual_dict视觉词典、npy状态文件及README说明代码模块按“特征提取-聚类-主题分析-分类”拆分清晰。项目利用SVM分类器完成滑坡场景识别先提取光谱特征平均值、标准差与GLCM纹理特征再经K-Means聚类得到视觉单词进而通过LDA主题分析生成高级特征并转为libsvm格式最后调用Libsvm完成分类可帮助理解词袋模型与主题模型在遥感图像分类中的应用。源码经过严格测试可直接参考并在此基础上扩展使用配套文档说明详细运行方式。目前已有55人学习下载。1. 遥感影像滑坡场景分类这活儿到底在分什么先给结论遥感影像滑坡场景分类本质上是给一张裁剪好的遥感图像块打标签判断它属于“含滑坡”还是“不含滑坡”的二分类问题最多扩展成“滑坡、裸地、植被、水体、居民地”的多类别场景识别。它和像素级的滑坡提取不一样——分类只回答“这张图里有没有滑坡”不需要把滑坡边界画出来这让数据标注成本大幅降低也是很多本科生毕业设计和课程设计选它当题目的直接原因。适合做这个方向的人有两类。一类是需要快速出成果的毕设/课设学生用迁移学习加公开数据集两到三周就能跑出可展示的结果另一类是刚入门遥感深度学习的新人想用一个有明确业务背景的任务练手把数据加载、模型训练、评估、文档整理的完整链路走一遍。它不涉及复杂的多尺度融合或时序分析技术栈收敛在 Python PyTorch 加图像分类模型上几乎是为“短周期项目开发”量身定制的题目。下面按我自己的做法从数据、模型、训练到避坑把这条线的完整落地路径拆开讲。2. 数据准备是第一步把公开影像切成模型能吃的样本2.1 数据从哪来公开数据集与人工裁剪的搭配跑滑坡场景分类数据是第一个卡脖子的地方。常见的做法是优先用公开的滑坡影像数据集这类数据往往直接给出了滑坡区域的矢量标注或像素掩膜你只需要按“有滑坡/无滑坡”生成标签即可。没有现成分类数据集时就用公开遥感影像如高分系列、Sentinel-2 的可见光波段叠加滑坡分布标注自己裁剪样本。裁剪的流程我一般分三步先按滑坡矢量外接矩形生成正样本中心点再在非滑坡区域随机采样生成负样本最后以每个中心点为中心裁剪固定尺寸的影像块并保存。import numpy as np import rasterio from shapely.geometry import box from shapely.ops import unary_union def generate_samples(satellite_path, landslide_shp, patch_size224, stride112): landslide_geom unary_union(landslide_shp.geometry) # 合并滑坡矢量 with rasterio.open(satellite_path) as src: bounds src.bounds # 按滑动窗口切出候选块再判断是否与滑坡区域相交 for x in np.arange(bounds.left, bounds.right - patch_size, stride): for y in np.arange(bounds.bottom, bounds.top - patch_size, stride): patch_box box(x, y, x patch_size, y patch_size) label 1 if patch_box.intersects(landslide_geom) else 0 window rasterio.windows.from_bounds( x, y, x patch_size, y patch_size, transformsrc.transform ) img src.read(windowwindow, out_shape(3, patch_size, patch_size)) yield img.transpose(1, 2, 0), label这段代码的核心逻辑是用滑动窗口切影像并用几何相交判断正负样本。patch_size是裁剪边长224 是目前绝大多数分类网络的默认输入尺寸显存有限时用 224 最稳妥stride是滑动步长步长小于边长会产生重叠样本能在数据量少时起到隐式数据增强作用。注意这里用shapely.geometry的intersects判断相交而非包含因为真实滑坡的矢量边界往往与影像块边界不完全重合只要有重叠就算正样本实际训练效果更好。2.2 数据划分与目录组织train/val/test 别混在一起拿到样本后很多人直接按文件夹把正负样本丢进去让框架自动读取这没错但要注意三点一是正负样本要按比例打进 train/val/test 三个目录不能全量丢进 train二是划分时要以“影像块”为粒度随机划分避免同一块区域的高重叠样本同时出现在训练集和验证集三是在多类别场景下要保证每个类别在三个集合里的分布近似一致否则验证集只看到部分类别指标会虚高。目录结构我习惯按 torchvision 的ImageFolder约定来组织这样后续加载只需要一行代码。import random import shutil from pathlib import Path def split_dataset(src_dir, dst_dir, train_ratio0.7, val_ratio0.15): random.seed(42) src_dir, dst_dir Path(src_dir), Path(dst_dir) for cls_dir in src_dir.iterdir(): if not cls_dir.is_dir(): continue imgs list(cls_dir.glob(*.jpg)) random.shuffle(imgs) n_train int(len(imgs) * train_ratio) n_val int(len(imgs) * val_ratio) for split_name, subset in zip([train, val, test], [imgs[:n_train], imgs[n_train:n_trainn_val], imgs[n_trainn_val:]]): out_dir dst_dir / split_name / cls_dir.name out_dir.mkdir(parentsTrue, exist_okTrue) for img_path in subset: shutil.copy(img_path, out_dir / img_path.name)这里的random.seed(42)很关键遥感数据的分布不均匀不固定随机种子每次划分出的数据集不同模型结果就没有可比性后面写文档时也不好交代。train_ratio0.7、val_ratio0.15是图像分类项目的常见配置比 0.8/0.1/0.1 更稳留出足够多的验证样本做模型筛选。我一般会再留 15% 的测试集这个测试集在整个调参过程中只跑一次防止验证集参与选模型后指标失真。3. 模型选型与加载迁移学习是毕设项目的命根子3.1 为什么从 ResNet 而不是 Transformer 起步入门遥感影像分类任务ResNet 系列是我们的最优先选择没有之一。原因很简单遥感数据集规模通常只有几千到几万张从头训练 ResNet-50 这种百万参数模型会面临严重的过拟合而视觉 TransformerViT、Swin Transformer虽然上限更高但它在小数据集上的收敛速度慢预训练权重也更难对齐遥感影像的分布。ResNet-18 或 ResNet-34 在 ImageNet 上的预训练权重可以无缝迁移到三通道 RGB 遥感影像上且网络结构足够简单出问题时也容易调试。再说一句毕业设计视角的实话答辩老师关心的不是你用了多新的模型而是你能不能说清楚“为什么选这个模型”——ResNet 的残差结构解决了深层网络退化问题这一点本身就够你讲三分多钟。与其在 Swin Transformer 里调参调得昏天黑地不如把 ResNet 的迁移学习吃透把功夫放在数据清洗和评估分析上产出更扎实。3.2 用 torchvision 加载预训练权重的最小代码import torch import torch.nn as nn from torchvision import models def get_model(num_classes2, baseresnet34, pretrainedTrue): weights ( models.ResNet34_Weights.IMAGENET1K_V1 if base resnet34 else models.ResNet18_Weights.IMAGENET1K_V1 ) model getattr(models, base)(weightsweights if pretrained else None) # 把最后一层全连接换成与类别数匹配的新层 in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) return modelgetattr(models, base)让你可以通过字符串切换模型主干调试阶段先用 ResNet-18最后测试用 ResNet-34代码不用改。model.fc.in_features是 ResNet 全连接层的输入维度不同深度的 ResNet 这个值有差异写死 512 或 2048 都会出问题——这是很常见的坑。加nn.Dropout(0.3)做一点正则能明显缓解小数据集的过拟合比直接单层全连接泛化好。pretrainedFalse的情况只在你想做“是否用预训练”的消融实验时才用正常训练务必保持True。需要注意 torchvision 新版本对预训练权重的加载方式做了调整。老代码写models.resnet34(pretrainedTrue)在新版本里会弹出警告甚至报错正确做法就是用models.ResNet34_Weights.IMAGENET1K_V1枚举传入。你的项目文档里写清楚这一点指导老师会觉得你确实踩过坑、填过坑。3.3 训练时的三个关键配置优化器、学习率与损失函数import torch.optim as optim model get_model(num_classes2, baseresnet34) criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-2) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30)损失函数在二分类和多分类时都用CrossEntropyLoss它内部把 softmax 和交叉熵合在一起做了数值稳定处理不要在最后一层再手动加 softmax 然后接 NLLLoss那样梯度会不稳定。优化器我推荐AdamW而不是传统 Adam它在权重衰减的实现上做了修正对遥感影像这种带噪数据效果更稳。学习率1e-4是迁移学习微调阶段的稳定起点新手最容易犯的错是用1e-3十有八九会在前几个 epoch 就出现 loss 震荡或直接发散。CosineAnnealingLR按余弦曲线把学习率从初始值降到接近 0这种退火方式比固定学习率更适合迁移学习因为前期权重还在大幅调整后期只做精细微调。这里的T_max30要和你的 epoch 总数一致否则余弦周期没走完训练就停了末尾的学习率降不到最低点模型可能没收敛到最好的状态。4. 训练全流程用 30 个 epoch 跑出一个可信的结果4.1 一个完整的训练循环长什么样很多毕设项目的训练代码是直接从 GitHub 复制来的“大杂烩”有的没有验证集逻辑有的每个 epoch 都往 tensorboard 写一堆东西但核心的训练-验证-保存链条是断的。我写训练循环的原则是控制台输出能看清每一步模型保存只留最优和最后一个指标记录必须同时包含 loss、准确率、召回率和 F1。def train_one_epoch(model, dataloader, loss_fn, optimizer, device): model.train() total_loss, correct, total 0, 0, 0 for imgs, labels in dataloader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss loss_fn(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * imgs.size(0) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) return total_loss / total, correct / total def validate(model, dataloader, loss_fn, device): model.eval() total_loss, correct, total 0, 0, 0 with torch.no_grad(): for imgs, labels in dataloader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) loss loss_fn(outputs, labels) total_loss loss.item() * imgs.size(0) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) return total_loss / total, correct / total训练函数里的loss.item()取出张量里的 Python 标量用于累加imgs.size(0)是当前 batch 的大小用总样本数做加权平均防止最后一个 batch 不足量时对平均 loss 产生偏差。验证函数里两个细节model.eval()会关闭 dropout 和 batch norm 的训练行为torch.no_grad()关闭梯度记录以节省显存和计算时间——这两个不写验证指标会偏高或波动。前向时不要重复学习率调整逻辑scheduler 的 step 要放在每个 epoch 结束时调用。4.2 在测试集上计算 F1 与混淆矩阵训练过程中的 accuracy 只能反映整体正确率在滑坡场景里正负样本比例经常失衡——假设负样本占 90%模型全部预测负样本也能拿到 90% 准确率但一点实际价值都没有。所以测试集上的评估必须看混淆矩阵、精确率、召回率和 F1 分数。from sklearn.metrics import confusion_matrix, classification_report import numpy as np def evaluate_test(model, dataloader, device): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in dataloader: imgs imgs.to(device) outputs model(imgs) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) report classification_report( all_labels, all_preds, target_names[non-landslide, landslide], digits4 ) print(Confusion Matrix:\n, cm) print(report) return cm, reportclassification_report一次输出精确率、召回率、F1 和各类别样本数省去你手动计算。注意这里调用preds.cpu().numpy()是必须的因为 GPU 上的张量不能直接转 numpy。如果做的是三类以上的多分类比如滑坡、植被、水体、居民地target_names参数按类别顺序写对应名称即可。保存这个测试输出到 txt 文件毕设论文的实验结果章节直接有素材可用。5. 五个必踩的坑从数据泄漏到显存崩溃的复盘5.1 正负样本分错——把滑坡附近影像当成“无滑坡”负样本现象训练时 loss 下降正常验证集准确率却始终在 70% 上下徘徊测试集的结果也不理想。原因滑坡发生区域周边的地质环境裸土、碎石坡面和滑坡体高度相似如果把严格意义上的非滑坡负样本都放在远离滑坡的区域采样模型学到的是“和滑坡长得完全不像”而真实应用场景里待判别的遥感块往往就是滑坡与周边地物混合的区域。负样本太难模型不会泛化。解决负样本采集要在滑坡边界外扩一段距离比如 50 到 200 米的缓冲区内也采样一部分让模型见过“像滑坡但不是滑坡”的样本。同时用训练完成后的模型对验证集里的负样本做一次预测把预测概率高于 0.7 的负样本挑出来人工检查——很多情况下你会发现标注错误或采样位置偏差。5.2 数据泄漏——切片重叠导致验证集虚高现象测试集 F1 高达 0.98但换到另一景影像上测试性能急剧掉到 0.6。原因用滑动窗口裁剪时步长小于 patch 边长同一区域生成了大量重叠样本。如果随机划分数据集训练集和验证集中会出现来自同一地理位置的重叠兄弟样本模型等于提前见过答案。解决裁样本时按“区域”划分而不按“样本”划分。具体做法是先给每个滑坡 block 编号把整个 block 分配进训练集或验证集再在 block 内部裁样本。或者更简单在裁剪时设置stridepatch_size不产生任何重叠样本牺牲一点数据量换取可靠的验证指标。小数据场景下我更推荐前者数据量损失可控且指标可信。5.3 显存溢出——单卡负载拉满的典型触发器现象batch size 设 32 跑 ResNet-50前几轮正常第三个 epoch 报CUDA out of memory。原因显存占用不只是模型参数还包括激活值、梯度、优化器状态。ResNet-50 输入 224×224、batch size 32训练时的激活值占用通常在 5-8 GB 之间轻则触发显存告警重则直接杀掉进程。解决先换小模型跑通流程比如 ResNet-18batch size 从 16 起步。如果必须用 ResNet-50把 batch size 降到 8 并用梯度累积补足有效 batch size再不够就打开混合精度训练PyTorch 的 AMP 模块显存占用能降低约一半。不用强上大模型——多数毕设场景 ResNet-34 已经足够。scaler torch.cuda.amp.GradScaler() for imgs, labels in dataloader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() with torch.cuda.amp.autocast(): outputs model(imgs) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()这段是混合精度训练的标准写法。GradScaler防止梯度下溢autocast在 forward 阶段自动选择 float16 和 float32注意力机制和归一化层自动保持浮点精度数值稳定性不用额外担心。我在自己的实验中开启 AMP 后显存占用从 11 GB 降到 6 GB 左右训练速度还快了 40%属于成本最低的性能优化手段。5.4 新版本 PyTorch 的 API 变动——老代码直接报错现象从 GitHub 拉的代码在本地环境跑不起来报错集中在pretrained参数和torchvision.models相关位置。原因torchvision 从 0.13 开始把pretrainedTrue改为弃用推荐显式传入weights枚举。部分更老的代码还用了model.fc nn.Linear(...)单层替换但新版 TorchVision 的 ResNet 实现把分类头封装成了fc属性直接替换没毛病但某些魔改版本里分类头是classifier属性对新用户很不友好。解决统一把模型获取代码改为torchvision.models官方文档里的新写法用weights参数并用getattr动态获取模型类。环境固定建议在项目文档里写清楚torch2.x、torchvision0.x的版本组合并用pip freeze requirements.txt锁定全量环境这是设备间迁移踩坑最少的方式。5.5 准确率指标欺骗——类别不平衡下的“躺赢”陷阱现象训练日志里准确率一路升到 90%但打开混淆矩阵发现滑坡类别的召回率只有 40%。原因负样本非滑坡远多于正样本滑坡模型通过全部预测为多数类获得高准确率。更隐蔽的是验证集如果也继承了这种不平衡分布指标看起来还“稳定”问题就被掩盖了。解决用 F1 分数而非准确率作为模型选择的依据并在训练阶段对正样本做加权。PyTorch 的交叉熵损失里可以直接传weight参数nn.CrossEntropyLoss(weighttorch.tensor([1.0, 1.5]))让少数类的损失占比变大模型被迫关注滑坡类别的判别特征。配合测试集上的分类报告逐类检查才算真正评完了模型。6. 让项目可复现、可评分文档与交付清单的写法跑通代码只是毕设和课设的及格线真正拉开差距的是你交付的代码结构和项目文档。我见过太多人把训练脚本写成单文件 500 行的“屎山”没有requirements.txt没有 README连自己在跑什么数据集都不写清楚答辩现场演示翻车概率极高。下面这套组织方式是我反复调整后用着最顺手的照着搭即可。项目根目录分割成几个逻辑清晰的模块data/放数据集和划分脚本models/放网络结构定义utils/放数据加载和评估函数config.py集中管理超参数train.py只负责主流程。核心原则是“训练脚本只做训练”不要在里面写数据增强、额外打印日志、测试等多个业务逻辑。这样一来每组实验只需要改config.py里的参数即可。# config.yaml 也可以但毕设答辩场景里 Python 文件加注释比 YAML 更容易解释 # config.py 示例结构 DATA_ROOT ./data/landslide_split # 已划分好的数据集根目录 PATCH_SIZE 224 BATCH_SIZE 16 EPOCHS 30 LR 1e-4 MODEL_NAME resnet34 NUM_CLASSES 2 DEVICE cuda:0 LOG_INTERVAL 20 # 每 20 个 batch 打印一次训练日志DATA_ROOT指向划分好的目录绝对路径和相对路径的选择上建议用相对路径加Path(__file__).parent锚定项目根目录这样代码拷到别的机器上不用改路径。DEVICE也不要写死成cuda:0用torch.device(cuda if torch.cuda.is_available() else cpu)做一次判断至少在 CPU 机器上代码不会直接崩。把这套东西在项目文档里写清楚你的项目已经从“一堆脚本”升级成了“可交付的工程”。文档方面README 必须包含四个部分项目背景与任务定义、数据集来源与预处理步骤、运行环境与快速开始命令、实验结果与模型文件说明。很多同学把 README 写成“代码抄了谁的、模型是什么”的流水账应该反过来写——先告诉读者“这个项目解决什么问题”再给“复制粘贴就能跑”的三条命令最后附上测试集 F1 和混淆矩阵截图。让任何一个陌生人拿着你的 README 能完整复现实验这才达到项目文档的及格线。最后说一个我的习惯每个项目我都会在代码里留一个seed_everything()函数在训练脚本的最开头调用。固定随机种子后别人复现你的结果时才不会出现“跑出来和文档里的数字不一样”的尴尬。这个细节不值钱但答辩时被问“你的结果可复现吗”你直接现场重新跑一遍训练并给出相同的 F1那一瞬间的确定性比任何口头解释都有说服力。我用这个方案已经带过不少学弟学妹做遥感方向的毕设。他们把项目文档写好、代码按模块拆完、测试集指标如实记录之后答辩的注意力基本都放在了“为什么选这个模型”“数据怎么清洗”这类能讲深的问题上而不是被追问“你这结果是不是假的”。希望你也能走通这条路径希望帮到你。本文还有配套的精品资源点击获取