简介:这是一份基于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_size=512, stride=256, min_pos_ratio=0.05, bg_keep_rate=0.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], window=window) # 取RGB三波段 mask = msk.read(1, window=window) # 跳过黑边或全空值区域 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 = f"patch_{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 显存占用(batch=8) | 小滑坡召回率 | 训练时间(相对) |
|---|---|---|---|---|
| ResNet18 | 11.7M | 约 2.5GB | 中 | 1x |
| ResNet50 | 25.6M | 约 4.5GB | 中高 | 1.8x |
| Swin-T | 28.3M | 约 6.2GB | 高 | 3x |
| MobileNetV3 | 5.4M | 约 1.8GB | 低 | 0.8x |
3.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(p=0.5), transforms.RandomVerticalFlip(p=0.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", transform=transform) val_ds = datasets.ImageFolder("data/val", transform=transform) train_loader = DataLoader(train_ds, batch_size=16, shuffle=True, num_workers=4, pin_memory=True) model = timm.create_model("resnet50", pretrained=True, num_classes=2) model.cuda() class FocalLoss(nn.Module): def __init__(self, alpha=0.75, gamma=2.0): super().__init__() self.alpha = alpha self.gamma = gamma def forward(self, logits, targets): ce = nn.functional.cross_entropy(logits, targets, reduction="none") 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(alpha=0.75, gamma=2.0) optimizer = AdamW(model.parameters(), lr=1e-4, weight_decay=0.05) scheduler = CosineAnnealingLR(optimizer, T_max=30) 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(f"epoch {epoch+1}: train_loss={running_loss/len(train_ds):.4f}") torch.save(model.state_dict(), f"checkpoints/resnet50_epoch{epoch+1}.pth")几个参数值得单独说明。学习率 lr 设为 1e-4,这是微调预训练模型的通用安全值,如果你先解冻所有层再从头训,可以试 1e-3,但滑坡分类的数据量撑不住这个学习率,loss 很容易震荡。weight_decay 设成 0.05,配合 AdamW 是针对小数据集防过拟合的常用组合。Focal Loss 里的 alpha=0.75 意味着正样本的损失权重是 0.75,负样本是 0.25,gamma=2 是让模型对“容易分对”的负样本降权,对“难分”的滑坡强关注。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 里都含有滑坡 patch;Loss 侧,换 Focal Loss,并调整 alpha 值让正样本的损失权重更大。判断是否修好的标准是:验证集上滑坡类别的召回率开始上升到 0.5 以上,而不是准确率数字的浮动。
# 快速验证:打印每个batch中正样本数量 pos_count = sum(labels).item() print(f"batch pos={pos_count}, neg={len(labels)-pos_count}") # 若多数batch的pos=0,说明采样策略失效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, iterations=1)4.3 大影像切成 512×512 后直接 OOM,batch 调小还是爆显存
现象:用 ResNet50 训练,batch_size=16 时提示 CUDA out of memory。改成 8 仍然爆,最后只能 batch=2,训练速度慢到无法接受。
原因:不是模型太大,而是输入的 patch 尺寸大。512×512 的输入张量,单张图在前向传播中的中间特征图显存占用远高于 224×224,同时滑窗切割后踩了黑边区域,读取的影像带没有裁剪干净,额外增大了内存压力。
解决:先确认 DataLoader 的 num_workers 是否占用了大量内存,建议设成 4 到 8 而不是默认 0。开启 pin_memory=True 减少 CPU/GPU 传输开销。训练时启用混合精度,用 torch.cuda.amp 把计算精度切成 FP16,显存占用可以下降 40% 左右。如果还不行,把训练输入尺寸降到 384×384,保持滑窗推理时的窗口大小不变,只在训练时做随机缩放。注意同时要改 model 输入前的 Resize 逻辑,保证训练和推理的 patch 内容一致性。
4.4 加载 ImageNet 预训练权重后 loss 为 NaN
现象:模型换成 ResNet50 并设置 pretrained=True 后,第一个 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(f"recall={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(dim=1) model.zero_grad() one_hot = torch.zeros_like(output) one_hot[0, target] = 1 output.backward(gradient=one_hot) # 取梯度均值作为通道权重,对特征图加权求和即可得到热力图把热力图叠到原图上,一眼就能看出模型关注的是滑坡体本身还是旁边的阴影。如果高亮区域集中在阴影或河道,说明模型走了捷径,需要重新清洗训练数据,把这些带混淆的区域人工修正标签。我每训练一版模型都会固定抽 20 张包含滑坡的验证集 patch,生成热力图存档。这个习惯在调参时帮了很大忙,能直观发现某次数据增广是否把滑坡纹理破坏了。
最后一个习惯是固定随机种子。遥感分类训练涉及数据采样、数据增强、模型权重的随机初始化,任何一个环节的随机性都会导致结果无法复现。我习惯在训练脚本开头写入 torch.manual_seed(42) 并配置 cuDNN 的确定性模式,这样每次跑出来的指标一致,论文里的实验对比才有说服力。希望你读完这篇笔记后,能少走这些弯路。如果第一次训练出来的模型 F1 不太理想,先回去检查正负样本比例和标签质量,方向对了,再调模型。希望帮到你。
本文还有配套的精品资源,点击获取