
简介这是一份基于Python实现的遥感影像滑坡场景分类完整项目面向毕业设计、课程设计及项目开发的初学者与进阶者。项目采用SVM分类器完成滑坡场景识别覆盖光谱特征均值、标准差与纹理特征GLCM统计值提取、K-Means特征聚类形成视觉单词、LDA主题分析构建高级特征并保存为LibSVM格式最终完成分类的完整流程思路清晰便于迁移到其他遥感分类任务。资源包共21个文件包含4个Python脚本特征提取、样本生成、数据工具与分类模块、4个pkl模型文件、训练测试文本数据、LDA模型及其状态文件、说明文档等压缩包大小1.86MB结构简洁可直接对照运行与二次开发。项目源码已经严格测试README文档中说明了运行方式与扩展建议。目前已有56人浏览学习读者可借此获得完整的遥感影像分类处理链、LibSVM样本格式转换方法以及视觉词袋LDA分类器的组合实践项目文件按功能划分清晰有助于快速上手并在此基础上延伸改进。1. 滑坡场景分类从毕业设计到真实遥感落地的第一道分水岭用 Python 做遥感影像的滑坡场景分类听起来是一条标准的毕业设计路线下载影像、切图、训练 CNN、出准确率、写论文。但真正动手的人会发现这项目的难点根本不在“分类”两个字上而是藏在数据侧——滑坡体在一景影像里往往只占几个百分点负样本多到能把模型压死标注边界稍微潦草一点训练曲线就开始随机抖动用 ImageNet 预训练权重直接迁移输入的波段均值一错前面几个 epoch 的 loss 全是 NaN。这篇文章就是基于 python 实现遥感影像滑坡场景分类的完整落地笔记面向做毕业设计、课程设计或者第一次接遥感项目的开发者。我会从影像切割、标签制作、模型选型讲到训练参数的调整逻辑最后给出验证模型到底学了什么的两个手段。全程不绕弯直接给能复现的代码和参数。2. 数据是关键遥感影像下载、切割与滑坡标签制作的最小闭环很多人拿到遥感影像项目的第一反应是找模型结构先把 ResNet 调通再考虑数据。这个顺序在滑坡分类里是反的。滑坡场景分类本质上是一个“小目标占比极低的二分类问题”它的上限由标签质量和正样本采样策略决定模型结构只是把数据里已有的信息提取出来。所以第一章先把数据闭环做扎实。2.1 影像来源与标注策略先标好五十景再谈模型遥感影像的获取渠道并不神秘常见的公开数据源就能覆盖毕设需求。中等分辨率用 Landsat-8/9 的 30 米多光谱局部细节更清楚的需求用 Sentinel-2 的 10 米波段想突出地形起伏可以叠加 ALOS 或 SRTM 的 DEM 做辅助波段。国产的高分系列影像在滑坡研究里也更常被拿来用但获取门槛略高需要走数据申请流程。对毕业设计来说优先推荐能直接下载的公开光学影像省去预处理时间。标注环节容易出现一个认知偏差觉得深度学习可以端到端手动勾绘是多余工作。实际经验是滑坡场景分类的公开标签非常分散很多论文里的数据集不直接开放最稳妥的方案是自己标。常见做法是用 QGIS 加载影像底图新建矢量图层沿着滑坡体的边界手动勾绘 polygon再把 Shapefile 栅格化为 0/1 掩膜。别一次标几百景先标 50 景每景切出来的 patch 数量已经足够训练一个基线。标注有一个值得借鉴的辅助技巧类似于“局部聚焦算法辅助标记”QGIS 里把视图放大到 1:5000 左右打开局部直方图拉伸滑坡体与周围植被的亮度差异会明显拉开。人工标注时盯着局部增强窗口而不是整景影像勾出来的边界噪声会少很多。这个细节看起来不起眼但对后面的训练稳定性有直接影响。2.2 滑动窗口切割把大影像切成可训练的小样本并控制正负样本比例一景遥感影像动辄上万乘上万像素不可能直接塞进 GPU。标准做法是滑窗切割把整景影像切成 256 或 512 像素的 patch。切割时必须同时读影像和掩膜用同一个窗口坐标切保证 patch 和标签严格对齐。下面这段代码是用 rasterio 做的滑窗切割同时统计每个 patch 里滑坡像素的占比用来控制正负样本比例。import rasterio import numpy as np from rasterio.windows import Window import random def slide_crop(img_path, mask_path, out_img_dir, out_mask_dir, win_size512, stride256, min_pos_ratio0.05, bg_keep_rate0.15): with rasterio.open(img_path) as src, rasterio.open(mask_path) as msk: width, height src.width, src.height for y in range(0, height - win_size, stride): for x in range(0, width - win_size, stride): window Window(x, y, win_size, win_size) img src.read([1, 2, 3], windowwindow) # 取RGB三波段 mask msk.read(1, windowwindow) # 跳过黑边或全空值区域 if np.max(img) 10: continue pos_ratio float((mask 0).mean()) # 纯背景patch只保留一部分避免负样本淹没正样本 if pos_ratio min_pos_ratio and random.random() bg_keep_rate: continue img img.transpose(1, 2, 0) # (C, H, W) - (H, W, C) out_name fpatch_{y}_{x} np.save(f{out_img_dir}/{out_name}.npy, img.astype(np.uint8)) np.save(f{out_mask_dir}/{out_name}.npy, (mask 0).astype(np.uint8))这里的 min_pos_ratio 是正样本占比阈值低于这个值的 patch 被认为是背景按 bg_keep_rate 概率抽样保留。为什么不能全部丢弃因为模型需要认识“什么是正常的山坡”完全没有纯背景 patch推理时会把大片正常地表误判成滑坡。stride 小于 win_size 会产生重叠 patch相当于数据增广对 512 窗口用 256 步长同一位置最多出现四次能显著提升小目标的召回率。保存成 npy 而不是直接存图片是为了训练时省去重复解码时间代价是磁盘占用会大一些毕设规模的几百个 patch 完全能接受。2.3 目录组织与训练集划分让 DataLoader 可以直接吃切出来的 patch 需要一个清晰的目录规范避免训练脚本里到处写魔数路径。我一般按下面的结构组织ImageFolder 可以直接加载不需要额外写 Dataset 逻辑data/ train/ landslide/ # 含滑坡的patch background/ # 纯背景patch val/ landslide/ background/原始 patch 本身是带掩膜的按什么规则归类到 landslide 文件夹我的习惯是mask 中滑坡像素占比大于 5% 的放进滑坡目录其余放背景目录。这样模型看到的是一个“patch 级是否有滑坡”的二分类信号符合滑窗推理时的真实使用方式。5% 这个阈值可以按需调整阈值越低正样本里混入的背景噪声越多阈值越高正样本数量越少训练越容易欠拟合。划分 train/val 时记住一个原则同一个原始影像切出来的所有 patch 必须放进同一侧不能按 patch 随机分。否则相邻 patch 内容高度相似验证集会泄漏训练信息评估出来的指标虚高。这个泄漏问题在答辩时被问到会很难解释清楚。3. 训练一条可靠的滑坡分类基线ResNet/Swin 选型与参数说明数据闭环搞定后进入模型训练环节。滑坡场景分类目前没有专用的“标准模型”从业者习惯的做法是在 ImageNet 预训练模型基础上微调。这里先解决选型问题再给出训练脚本和参数调整逻辑。3.1 先跑 ResNet50 基线再用 Swin-T 看收益预算驱动的选型思路选型不应该拍脑袋。我对第一次接触遥感项目的建议是先定一个“最不容易出错的基线”再逐步换更强或更轻的模型。滑坡分类的输入是 512×512 的 RGB patch其中滑坡体可能只有几十像素属于典型的小目标识别场景需要模型有足够的感受野和细节保持能力。对比下来三条路线的差别很清晰。ResNet18 训练最快显存占用低但深层语义信息不够对小滑坡的召回率明显偏弱ResNet50 是性价比最稳的选择训练四五个小时就能收敛配合 ImageNet 预训练权重可以拉到不错的效果Swin-T 在小目标上比 ResNet50 有明显的优势原因是自注意力能自适应地聚焦到滑坡区域但训练时间翻倍且数据量少时容易过拟合。经费和算力都有限的场景我的建议是ResNet50 出基线Swin-T 做对比实验。两张表放进论文里导师那边基本能过关。具体选型可以用下面这张表快速对照模型参数量512×512 显存占用batch8小滑坡召回率训练时间相对ResNet1811.7M约 2.5GB中1xResNet5025.6M约 4.5GB中高1.8xSwin-T28.3M约 6.2GB高3xMobileNetV35.4M约 1.8GB低0.8x3.2 训练脚本主干Focal Loss 处理正负样本失衡AdamW 配 Cosine 退火选好模型后训练脚本是整个项目能不能跑通的关键。滑坡分类里最容易被忽略的是 Loss 设计。用普通的交叉熵直接训练模型会很快学会“全部预测为背景”因为负样本占比太高loss 下降但滑坡一个都检不出来。解决手段有两个一是采样阶段控制正负样本比例二是 Loss 层用 Focal Loss它降低易分类样本的权重让模型被迫关注那些难分的滑坡 patch。下面这个训练脚本是完整可运行的核心参数都写在其中import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR from torch.cuda.amp import autocast, GradScaler import timm # 数据增强遥感影像不做随机裁剪避免把滑坡挖掉 transform transforms.Compose([ transforms.Resize((512, 512)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomVerticalFlip(p0.5), transforms.ToTensor(), # 归一化必须与ImageNet预训练权重匹配 transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_ds datasets.ImageFolder(data/train, transformtransform) val_ds datasets.ImageFolder(data/val, transformtransform) train_loader DataLoader(train_ds, batch_size16, shuffleTrue, num_workers4, pin_memoryTrue) model timm.create_model(resnet50, pretrainedTrue, num_classes2) model.cuda() class FocalLoss(nn.Module): def __init__(self, alpha0.75, gamma2.0): super().__init__() self.alpha alpha self.gamma gamma def forward(self, logits, targets): ce nn.functional.cross_entropy(logits, targets, reductionnone) pt torch.exp(-ce) # alpha 加权正样本权重更高 alpha_t torch.where(targets 1, self.alpha, 1 - self.alpha) loss alpha_t * (1 - pt) ** self.gamma * ce return loss.mean() criterion FocalLoss(alpha0.75, gamma2.0) optimizer AdamW(model.parameters(), lr1e-4, weight_decay0.05) scheduler CosineAnnealingLR(optimizer, T_max30) scaler GradScaler() for epoch in range(30): model.train() running_loss 0.0 for imgs, labels in train_loader: imgs, labels imgs.cuda(), labels.cuda() optimizer.zero_grad() with autocast(): out model(imgs) loss criterion(out, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() running_loss loss.item() * imgs.size(0) scheduler.step() print(fepoch {epoch1}: train_loss{running_loss/len(train_ds):.4f}) torch.save(model.state_dict(), fcheckpoints/resnet50_epoch{epoch1}.pth)几个参数值得单独说明。学习率 lr 设为 1e-4这是微调预训练模型的通用安全值如果你先解冻所有层再从头训可以试 1e-3但滑坡分类的数据量撑不住这个学习率loss 很容易震荡。weight_decay 设成 0.05配合 AdamW 是针对小数据集防过拟合的常用组合。Focal Loss 里的 alpha0.75 意味着正样本的损失权重是 0.75负样本是 0.25gamma2 是让模型对“容易分对”的负样本降权对“难分”的滑坡强关注。alpha 和 gamma 是超参数如果训练集正负样本比例悬殊到 1:20 以上alpha 可以往 0.85 调如果模型误检很多gamma 往 1.5 降。3.3 训练监控与“假装收敛”判据看验证集召回率而不是训练 loss训练脚本跑起来后最容易让新手误判的是“收敛”概念。训练集 loss 降到 0.1 以下并不代表模型学好了滑坡分类里更可靠的判据是验证集上的召回率和 F1 分数。我一向的做法是每个 epoch 结束后在验证集上算一遍混淆矩阵再结合 TensorBoard 画出 recall 和 precision 曲线。模型真正的收敛信号是验证集的 F1 不再上升且训练集和验证集的 F1 差距不明显扩大。如果训练集 F1 已经到 0.9 而验证集只有 0.5说明过拟合了。这时先别急着换模型检查一下正样本数量是否太少以及是否做了足够的增强。翻转增强对遥感影像有效但随机裁剪要慎重——把滑坡体截掉一半的 patch 会让模型学到错误的上下文。遥感训练惯用的增强是水平翻转、垂直翻转、小角度旋转和亮度扰动这些都能在保持滑坡结构完整的前提下增加多样性。训练环境方面建议直接用 conda 建独立环境Python 3.10 配 PyTorch 2.x 的稳定组合避免系统 Python 环境被各种依赖搞得一团糟。Windows 下注意 CUDA 版本与 PyTorch 的对应关系Linux 服务器上用 conda 创建环境后激活再装包。代码调试期推荐用 VS Code 的 Python 插件设置好解释器路径不然 import timm 报错时很难定位是哪一层环境的问题。4. 滑坡分类训练避坑指南5 个让模型翻车的常见问题这部分是血泪经验合集。滑坡分类和普通图像分类不一样的地方在于数据特性太特殊很多在常规分类任务里不影响结果的问题在这里会直接让项目进度停滞。每一条都按“现象→原因→解决”来写方便排查时对号入座。4.1 模型一直输出“背景”训练 loss 不下降现象训练跑完 10 个 epoch训练集准确率 90% 以上但手动抽查验证集输出发现模型对包含滑坡的 patch 也全部预测为背景。训练 loss 看起来正常收敛验证集的准确率却接近纯背景比例。原因这是典型的正负样本极端失衡。滑坡体在影像中的占比很低如果滑窗切割后没有做采样控制负样本可能是正样本的 50 倍以上。模型发现把所有 patch 预测为背景就能拿到很低的 loss于是梯度被大量易分类的负样本主导正样本的损失信号被淹没。准确率指标在这种情况下完全没有参考价值。解决从两个方向同时下手。数据侧采样阶段把正负样本比例控制在 1:3 到 1:10 之间优先保证每个 batch 里都含有滑坡 patchLoss 侧换 Focal Loss并调整 alpha 值让正样本的损失权重更大。判断是否修好的标准是验证集上滑坡类别的召回率开始上升到 0.5 以上而不是准确率数字的浮动。# 快速验证打印每个batch中正样本数量 pos_count sum(labels).item() print(fbatch pos{pos_count}, neg{len(labels)-pos_count}) # 若多数batch的pos0说明采样策略失效4.2 验证集 F1 随训练震荡模型越训越不稳定现象前 10 个 epoch 验证集 F1 稳步上升第 12 个 epoch 突然骤降再跑两个 epoch 又回升反复振荡权重文件保存后无法判断哪个轮次最可靠。原因最常见的是标注噪声触发的。滑坡体的边界是手工勾绘的边缘像素在掩膜里被标成正样本但对应的影像内容其实是植被或裸岩过渡带。这些边界像素占比虽小却会在训练后期成为“难分样本”把梯度方向带得来回摆动。另一个可能的原因是学习率在后期仍然偏大步长跨过了 loss 曲面上的窄谷。解决先清洗标签对掩膜做一次形态学腐蚀消除边界上的孤立噪声点。用 OpenCV 的 erode 算子核大小 3×3 即可。再做标签平滑把硬标签从 0/1 改成 0.05/0.95削弱模型对边界像素的“过度自信”。如果这两个手段做了还震荡把 AdamW 的初始学习率从 1e-4 降到 5e-5并检查 Cosine 退火的 T_max 是否与 epoch 数量吻合。import cv2 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) mask cv2.erode(mask.astype(np.uint8), kernel, iterations1)4.3 大影像切成 512×512 后直接 OOMbatch 调小还是爆显存现象用 ResNet50 训练batch_size16 时提示 CUDA out of memory。改成 8 仍然爆最后只能 batch2训练速度慢到无法接受。原因不是模型太大而是输入的 patch 尺寸大。512×512 的输入张量单张图在前向传播中的中间特征图显存占用远高于 224×224同时滑窗切割后踩了黑边区域读取的影像带没有裁剪干净额外增大了内存压力。解决先确认 DataLoader 的 num_workers 是否占用了大量内存建议设成 4 到 8 而不是默认 0。开启 pin_memoryTrue 减少 CPU/GPU 传输开销。训练时启用混合精度用 torch.cuda.amp 把计算精度切成 FP16显存占用可以下降 40% 左右。如果还不行把训练输入尺寸降到 384×384保持滑窗推理时的窗口大小不变只在训练时做随机缩放。注意同时要改 model 输入前的 Resize 逻辑保证训练和推理的 patch 内容一致性。4.4 加载 ImageNet 预训练权重后 loss 为 NaN现象模型换成 ResNet50 并设置 pretrainedTrue 后第一个 epoch loss 直接输出 nan或训练几步后 loss 变成 NaN 不再恢复。原因大概率是输入数据的归一化参数与预训练权重不匹配。ImageNet 预训练权重的归一化均值是 [0.485, 0.456, 0.406]标准差是 [0.229, 0.224, 0.225]。如果直接用 [0,1] 归一化或忘了归一化输入分布和权重期望的分布完全错位激活值溢出导致 NaN。另一个常见原因是输入影像本身含有 NaN 或极端高值像素例如未处理的卫星影像原始 DN 值高达几千直接进网络自然爆掉。解决统一影像预处理流程读取 patch 后先做 Clip把像素值裁剪到 [0, 10000] 之类的合理范围再归一化到 [0,1]最后按 ImageNet 的 mean/std 标准化。写一个预处理函数并固定下来训练和推理共用同一套逻辑不要各写各的。用单波段灰度影像的人要特别注意把单波段复制成三通道时三个通道的归一化参数应该一致而不是套用 RGB 的各自均值。4.5 答辩被问“漏检了多少滑坡”拿着 accuracy 答不上来现象训练完成测试集准确率 95%自我感觉良好。答辩时老师问滑坡 patch 的召回率是多少现场算不出来因为整篇代码里没有统计过这个指标。原因准确率在这个场景下是误导性最强的指标。负样本占比 90% 以上时模型把全部 patch 判断为背景也能拿到 90% 的准确率。滑坡分类真正要回答的问题是“有滑坡的地方我捡出来了多少”这只能靠召回率和 IoU 来回答。解决训练脚本里固定输出四类指标精确率、召回率、F1、滑坡类别的 IoU。滑坡类别 IoU 的定义是预测为滑坡且真实为滑坡的像素数除以两个集合的并集它比 patch 级准确率更贴近实际检测效果。我通常还会保存每个 batch 中误预测为正样本的 patch 路径便于人工复盘模型学到的“假阳性”到底是什么。from sklearn.metrics import confusion_matrix cm confusion_matrix(y_true, y_pred) # 滑坡类别索引为1 TP cm[1, 1]; FP cm[0, 1]; FN cm[1, 0] recall TP / (TP FN) iou TP / (TP FP FN) print(frecall{recall:.3f}, iou{iou:.3f})5. Grad-CAM 和混淆矩阵验证模型到底学了什么并筛选硬样本模型训练完别急着写结论。遥感分类里最怕的就是模型“抄了近道”学了纹理特征之外的环境线索。滑坡影像里常见两种假线索一是滑坡体旁边的道路或河流阴影亮度特征相似模型可能把阴影当滑坡二是固定拍摄区域里每次影像都出现的同一块裸岩模型记住位置而不是形态特征。验证模型是否学对了我靠两个工具混淆矩阵和 Grad-CAM。混淆矩阵先读行和列。比如滑坡类别被大量误判为背景说明召回率不足需要增加正样本权重或降低 min_pos_ratio如果背景被大量误判为滑坡说明模型学到的滑坡特征太宽泛可能是标注时把滑坡边缘的裸露地表也标进去了。矩阵里数值最大的非对角元就是模型最容易混淆的两个方向后续数据层面的修正就瞄准它。Grad-CAM 用来回答“模型为什么这么判断”。实现上不需要额外依赖重库PyTorch 里注册 hook 就能拿到最后一层卷积的梯度def save_gradient_hook(module, grad_input, grad_output): model.register_hook grad_output[0] # 保存梯度 model.eval() output model(img.unsqueeze(0)) target output.argmax(dim1) model.zero_grad() one_hot torch.zeros_like(output) one_hot[0, target] 1 output.backward(gradientone_hot) # 取梯度均值作为通道权重对特征图加权求和即可得到热力图把热力图叠到原图上一眼就能看出模型关注的是滑坡体本身还是旁边的阴影。如果高亮区域集中在阴影或河道说明模型走了捷径需要重新清洗训练数据把这些带混淆的区域人工修正标签。我每训练一版模型都会固定抽 20 张包含滑坡的验证集 patch生成热力图存档。这个习惯在调参时帮了很大忙能直观发现某次数据增广是否把滑坡纹理破坏了。最后一个习惯是固定随机种子。遥感分类训练涉及数据采样、数据增强、模型权重的随机初始化任何一个环节的随机性都会导致结果无法复现。我习惯在训练脚本开头写入 torch.manual_seed(42) 并配置 cuDNN 的确定性模式这样每次跑出来的指标一致论文里的实验对比才有说服力。希望你读完这篇笔记后能少走这些弯路。如果第一次训练出来的模型 F1 不太理想先回去检查正负样本比例和标签质量方向对了再调模型。希望帮到你。本文还有配套的精品资源点击获取