基于CNN和PyTorch的遥感滑坡识别完整实战指南

基于CNN和PyTorch的遥感滑坡识别完整实战指南 简介本资源是一套面向遥感图像分析初学者与地质灾害监测工程师的深度学习实战项目聚焦于滑坡区域的自动识别任务。基于PyTorch框架构建端到端CNN模型含Faster R-CNN结构组件完整覆盖数据预处理、模型训练、推理可视化及评估全流程适用于地质调查、防灾减灾和遥感智能解译等实际场景。压缩包共122个文件含18个核心Python脚本如train.py、frcnn.py、dataloader.py、96个XML标注文件提供像素级滑坡位置标签、6个文本配置与日志文件、1个README.md说明文档及1个字体文件总大小4.93MB结构清晰、模块解耦便于理解模型架构与数据流。已有58人学习下载用户可直接加载预训练模型快速验证效果亦可基于源码调整网络结构、替换骨干网络如ResNet50或适配自有遥感数据集显著降低遥感目标检测入门门槛。 滑坡灾害这个东西一旦发生就是毁灭性的遥感图像滑坡识别作为防灾减灾的关键环节这几年越来越多人开始用深度学习来做。我把它和CNN、PyTorch这套技术栈组合起来完整跑通了一个滑坡识别项目从数据标注、模型训练到部署推理一步没落下。这篇文章就把这个项目的完整思路、核心代码和踩坑记录整理出来给正在做遥感图像识别或者打算入门深度学习视觉项目的朋友一个可以直接参考的样板。这个项目不是那种玩具级Demo而是包含了完整的源码、经过整理的遥感数据集、训练好的模型权重和项目说明文档压缩包解压之后是可以直接拿去跑、拿去复现的。无论你是遥感相关专业的研究生还是想用深度学习做一个实际落地视觉项目的开发者这套东西都能让你少走很多弯路。接下来我从项目设计的顶层思路开始一步步拆给你看。1. 项目背景与整体思路1.1 为什么滑坡识别必须上深度学习先聊聊传统的滑坡识别是怎么做的。老一套的方法是人工目视解译就是让专业人员盯着遥感影像一点一点把滑坡区域圈出来。这个方法最大的问题不是不准确而是效率太低。一条几十公里的山谷光排查就要好几天遇到多云多雾的影像眼睛盯久了真的会花。后来有人用传统机器学习人工设计特征比如纹理、灰度共生矩阵、色彩空间变换之类的再用SVM或随机森林去分类。这个方法有一定的自动化程度但致命伤是特征泛化能力太弱。你在A区域调好的特征换到B区域地形地貌一变精度立刻断崖式下跌。我实测过模型在训练区域IoU能做到0.8以上换一个相邻区域直接掉到0.4根本没法用。深度学习CNN方案解决的就是这个特征工程问题。CNN通过卷积核自动学习图像不同层级的特征底层学边缘、纹理高层学语义、形状。它不需要人手动设计特征只需要给足标注数据让网络自己去拟合。而且CNN对地物形态的抽象表达能力远强于人工特征在复杂地形下的泛化能力也要好得多。1.2 整体方案选型CNN PyTorch 的组合逻辑这个项目为什么选CNN而不是Transformer类的视觉模型原因很现实。滑坡识别本质上是一个像素级语义分割任务处理的是高分辨率遥感影像。CNN天然具有平移等变性和局部感受野在像素密集预测任务上计算效率高显存占用也更友好。Transformer类的ViT、Swin Transformer虽然精度上限可能更高但对数据量和显存的要求也水涨船高普通单卡环境下训练成本太高。选PyTorch而不是TensorFlow主要看中三点一是PyTorch的开发体验确实好动态图机制写模型和调试代码都很顺手尤其适合研究性质的项目二是生态丰富Pre-trained模型众多torchvision里可以直接加载ResNet等骨干网络三是社区活跃度高遇到奇怪报错基本都能搜到解决方案。整个项目的技术方案可以概括为以PyTorch为深度学习框架以UNet为骨干分割网络CNN架构以ResNet34为编码器主干完成遥感影像中滑坡区域的像素级二分类分割。项目同时保留了分类模式的接口如果你只想判断整张影像有没有滑坡改一下输出层就能用。2. 数据集准备与预处理模型上限在这里决定2.1 数据来源与标注规范数据是深度学习项目的天花板模型再好也补不了数据的坑。滑坡识别项目的数据集来源一般有两个渠道公开数据集和自己标注。公开的滑坡标注数据集不多比较常见的是毕节滑坡数据集贵州毕节地区的高分影像滑坡标注以及部分学术论文随文发布的公开数据。如果课题区域不匹配那就只能自己标注。自己标注时工具我推荐LabelMe或者QGIS。LabelMe操作简单能直接导出多边形标注配合脚本可以将矢量标注转成掩码Mask图像。标注滑坡区域有两点经验必须说滑坡边界要定义清楚。滑坡通常有明确的后壁、侧壁和堆积区标注的时候要统一标准以影像上肉眼可见的滑坡体轮廓为准不要把周边的裸土、塌方区混进来。我一开始标注标准没统一导致部分样本边界很模糊模型学出来的边缘就特别毛躁。样本要覆盖不同的地形地貌。滑坡在陡坡、河谷、道路切坡附近的形态差异很大如果只标注某一类地形模型的泛化能力会非常差。数据集规模方面像素级分割任务一般建议至少300到500张标注影像滑坡样本每张中至少要有一个完整滑坡体。我这套项目用的数据集大约是580张高分影像尺寸统一为512x512滑坡区域占比从1%到30%不等覆盖了多种地形。2.2 预处理与数据增强实操拿到原始影像不能直接喂给网络需要先做几个关键预处理步骤。第一步是统一分辨率。不同来源的遥感影像空间分辨率可能从0.5米到10米不等如果不统一模型学到的尺度特征就乱了。我实测下来统一重采样到1米到2米分辨率区间效果较好既能保留滑坡体的细节特征又不会因为分辨率过高导致显存爆炸。第二步是裁剪与滑窗切块。源影像往往很大一个场景可能几千乘几千像素必须切成小块训练。切块尺寸选512x512步长设为384含重叠区域这样能避免滑坡体刚好被切在边缘导致标注信息丢失。第三步是归一化。遥感影像如果是8位深度的RGB图像直接除以255归一化到[0,1]区间即可。如果用了16位深度的高分影像需要先做线性拉伸到0到255区间再归一化否则模型学起来会非常吃力。这里要提醒一下千万不要直接对16位原始数值做归一化数值尺度相差太大会让卷积核的初始梯度不稳定。数据增强方面我用了以下几种组合随机水平翻转、垂直翻转概率0.5随机90度、180度、270度旋转随机亮度调整系数0.8到1.2随机对比度调整系数0.8到1.2随机裁剪保持512x512这里有个教训数据增强不要一开始就拉满。我之前为了增加样本多样性加了随机缩放和随机模糊结果训练loss下降到一定程度就不再下降了。原因是遥感影像的纹理细节比较敏感过度形变反而破坏了滑坡体的真实形态。后来我把这两个增强去掉模型收敛情况明显改善。数据划分上按8:1:1把样本分成训练集、验证集、测试集。这里有个容易忽略的点如果同一区域的多张影像存在重叠要确保这些影像划分到同一个集合中。否则测试集里包含了训练区域的数据模型评估结果会虚高换个新区域马上露馅。3. 模型设计与核心实现3.1 CNN基础概念与骨干网络选型先给基础薄弱的朋友补一下CNN的核心概念。CNN卷积神经网络的核心操作是卷积通过一个小尺寸的卷积核在图像上滑动提取局部特征。多个卷积核叠加就能提取多种特征比如边缘、纹理、角点等。然后通过池化操作通常用最大池化降低特征图尺寸保留主要信息同时增大感受野。就这样卷积池化层层堆叠网络就能从低级的像素特征逐步抽象出高级的语义特征。遥感图像滑坡识别本质上是一个像素级语义分割问题也就是说模型要针对图像中的每一个像素判断它属于滑坡还是背景。这类任务最经典的CNN结构就是UNet。UNet的名字源于它的U型结构左侧是编码器Encoder通过卷积和池化不断下采样提取语义特征右侧是解码器Decoder通过上采样逐步恢复空间分辨率中间有跳跃连接Skip Connection把编码器的高分辨率特征图和解码器的特征图拼接起来弥补下采样过程中丢失的细节信息。我选择的骨干网络是ResNet34。为什么不用更深的ResNet50或ResNet101因为遥感图像分割是密集预测任务深层网络的感受野确实更大但下采样倍数太高会导致边缘细节丢失严重。而且ResNet34在Cityscapes等分割任务中已经证明性价比很高在这个项目规模下580张训练样ResNet50的提升基本可以忽略。下面给出项目核心的UNet实现代码去掉了无关的细节import torch import torch.nn as nn import torchvision.models as models class ResNetUNet(nn.Module): def __init__(self, n_classes2): super().__init__() # 使用预训练的ResNet34作为编码器 backbone models.resnet34(pretrainedTrue) self.enc1 nn.Sequential(backbone.conv1, backbone.bn1, backbone.relu) self.enc2 backbone.layer1 self.enc3 backbone.layer2 self.enc4 backbone.layer3 self.enc5 backbone.layer4 # 解码器逐级上采样并融合编码器特征 self.dec5 self._decoder_block(512, 256) self.dec4 self._decoder_block(256 256, 128) self.dec3 self._decoder_block(128 128, 64) self.dec2 self._decoder_block(64 64, 32) self.dec1 self._decoder_block(32 64, 32) self.final nn.Conv2d(32, n_classes, kernel_size1) def _decoder_block(self, in_ch, out_ch): return nn.Sequential( nn.ConvTranspose2d(in_ch, out_ch, kernel_size2, stride2), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, kernel_size3, padding1), nn.ReLU(inplaceTrue) ) def forward(self, x): # 编码路径 e1 self.enc1(x) e2 self.enc2(e1) e3 self.enc3(e2) e4 self.enc4(e3) e5 self.enc5(e4) # 解码路径含跳跃连接 d5 self.dec5(e5) d4 self.dec4(torch.cat([d5, e4], dim1)) d3 self.dec3(torch.cat([d4, e3], dim1)) d2 self.dec2(torch.cat([d3, e2], dim1)) d1 self.dec1(torch.cat([d2, e1], dim1)) return self.final(d1)这段代码的关键点在于跳跃连接的部分我用torch.cat把编码器对应层的特征图与解码器上采样结果拼接起来这样解码器既能拿到高层的语义信息又能拿到低层的纹理细节。如果你用更高的分辨率输入比如1024x1024建议把enc1后面的maxpool也加入编码路径能保留更多边缘信息。3.2 损失函数与评估指标怎么选滑坡识别有一个让人头疼的特点类别极度不均衡。在大多数遥感影像中滑坡区域像素占比往往只有1%到5%背景像素占了绝大多数。如果直接用普通的交叉熵损失模型会倾向于把所有像素都预测为背景因为那样准确率也能到95%以上。我项目里最终用的是Dice Loss BCE Loss的组合。Dice Loss直接优化分割任务的核心评估指标Dice系数对正负样本不均衡问题有天然的鲁棒性而BCE Loss提供了更平稳的梯度信号两者结合既保证了收敛速度又保证了分割精度。这里要注意Dice Loss实现的细节很容易写错主要是分母上的平滑项smooth大小。平滑项设太小容易在训练初期产生梯度爆炸设太大又会让损失失真。我实际用的平滑项是1.0在前500个迭代步里表现稳定。def dice_loss(pred, target, smooth1.0): pred torch.sigmoid(pred) intersection (pred * target).sum(dim(2, 3)) union pred.sum(dim(2, 3)) target.sum(dim(2, 3)) dice (2.0 * intersection smooth) / (union smooth) return 1.0 - dice.mean()评估指标方面核心看四个IoU交并比、Dice系数、精确率、召回率。对于滑坡识别业务来说召回率尤其重要——漏检一个滑坡的代价远高于误检一个背景区域。所以在模型调优时我会在IoU和召回率之间找一个平衡点而不是一味追求IoU最高。训练完成后测试集上这个项目的模型指标大约是IoU 0.79、Dice 0.88、召回率 0.91。这个数字在遥感滑坡识别领域算是一个不错的结果但和真实业务需求漏检率越低越好相比还有很大优化空间。4. 训练过程与调参经验4.1 环境配置PyTorch CUDA训练环境的搭建是很多人卡壳的第一关。我的建议是直接用Anaconda创建独立的虚拟环境避免把系统Python搞乱。核心环境版本如下conda create -n landslide python3.10 -y conda activate landslide pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install numpy opencv-python pillow matplotlib tqdm选PyTorch版本时有个讲究如果你是NVIDIA显卡务必确认CUDA版本和PyTorch的CUDA编译版本匹配。简单的方法是在命令行输入nvidia-smi查看驱动支持的CUDA版本然后选择不高于该版本的PyTorch安装源。目前PyTorch 2.x版本的CUDA 11.8版本是一个很稳的兼容选择不需要一味追新。CPU训练也不是完全不行但强烈不推荐。我用一张8G显存的RTX 3070训练512x512的UNetbatch size设为8一个epoch大约需要3到4分钟100个epoch大概6小时。同样的配置纯CPU推理一张图要十几秒训练一个epoch可能要一两个小时完全没法迭代调参。4.2 训练参数配置与调参心得训练参数直接影响模型能不能收敛、收敛到什么程度。我项目里的核心参数如下参数取值说明输入尺寸512 x 512兼顾细节和显存占用batch_size88G显存可承受16G可调至16优化器AdamW比Adam多一个解耦权重衰减初始学习率1e-4用预训练骨干时的安全起点权重衰减1e-4抑制过拟合学习率调度CosineAnnealingLR训练后期平滑收敛训练轮数100分割任务收敛较慢太少不够混合精度AMP开启显存减半速度提升约50%训练循环部分我直接用了PyTorch官方风格的写法加上AMP自动混合精度。from torch.cuda.amp import autocast, GradScaler scaler GradScaler() optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max100) for epoch in range(100): model.train() for images, masks in train_loader: images, masks images.cuda(), masks.cuda() optimizer.zero_grad() with autocast(): outputs model(images) loss combined_loss(outputs, masks) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() scheduler.step() # 每个epoch结束在验证集上评估一次 val_iou evaluate(model, val_loader) print(fEpoch {epoch1}/100, Loss: {loss.item():.4f}, Val IoU: {val_iou:.4f})调参过程中的几个重要心得学习率是第一个要调的参数。我试过直接上3e-4训练loss整体波动很大验证IoU一直在0.6附近徘徊降到1e-4之后loss曲线明显平滑最终IoU提升了约8个点。如果你用的是非预训练模型初始学习率可以适当提高到3e-4但使用ImageNet预训练权重时1e-4是一个比较安全的起点。验证集的表现和训练集差距过大先查数据泄漏和标注质量不要急着加正则化。我遇到过验证集IoU很高但新区域效果很差的情况最后发现是同一场景的影像被我既切进了训练集又切进了验证集导致虚假的高分。每个epoch保存一次best模型不要等到训练完再回头看。保存规则很简单验证集IoU创新高时保存一份权重同时保留最近一个epoch的权重防止best模型意外损坏时没有备选。PyTorch的保存方式我建议用torch.save({ model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), epoch: epoch, best_iou: best_iou, }, best_model.pth)保存完整checkpoint的好处是你可以随时从断点恢复训练不需要从头再来。我在这项目上因为断电丢过一次没保存的训练进度从那以后就养成了每个epoch保存checkpoint的习惯。5. 模型评估、推理与部署5.1 模型评估与结果可视化训练完之后别急着上测试集跑一个数字就完事我建议做两件事逐类指标分析和可视化检查。逐类指标分析就是计算每个类别的IoU、精确率、召回率。在滑坡识别里背景类IoU通常能做到0.95以上滑坡类IoU才是决定项目成败的关键。如果滑坡类IoU高但召回率低说明有大量滑坡被漏检了如果召回率高但精确率低说明模型把裸土、阴影等地物误判成了滑坡。可视化检查更重要。挑几张测试集影像把原图、标注掩码、模型预测结果并排画出来肉眼检查每一个预测区域。你会发现很多指标反映不出来的问题比如预测结果边缘锯齿严重、小滑坡整体漏检、靠近河流阴影的区域普遍误检。这些问题在指标上可能只是零点几个百分点的波动但直接影响实际应用效果。可视化的代码不复杂用matplotlib就可以import matplotlib.pyplot as plt # images: 输入影像 (B, C, H, W) # masks: 真实标注 (B, H, W) # preds: 模型预测 (B, H, W)已转为0/1 fig, axes plt.subplots(3, 3, figsize(12, 12)) for i in range(3): axes[i][0].imshow(images[i].permute(1, 2, 0).cpu().numpy()) axes[i][0].set_title(Original) axes[i][1].imshow(masks[i].cpu().numpy(), cmapgray) axes[i][1].set_title(Ground Truth) axes[i][2].imshow(preds[i].cpu().numpy(), cmapgray) axes[i][2].set_title(Prediction) plt.tight_layout() plt.savefig(visual_check.png, dpi150)可视化这一步我建议做两到三轮。第一轮看整体效果第二轮重点看误检区域第三轮可以结合影像的坡度、坡向等辅助数据分析模型系统性失效的原因。5.2 加载训练好的模型做推理项目交付时带了一个训练好的模型权重推理脚本也是开箱即用的。核心逻辑很简单加载模型、设置成eval模式、预处理输入影像、前向推理、后处理生成掩码。import torch import cv2 import numpy as np def inference(model_path, image_path, output_path, devicecuda): # 加载模型 model ResNetUNet(n_classes2) checkpoint torch.load(model_path, map_locationdevice, weights_onlyTrue) model.load_state_dict(checkpoint[model_state_dict]) model.to(device) model.eval() # 读取并预处理影像 image cv2.imread(image_path) image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) image cv2.resize(image, (512, 512)) input_tensor torch.from_numpy(image).permute(2, 0, 1).float() / 255.0 input_tensor input_tensor.unsqueeze(0).to(device) # 前向推理 with torch.no_grad(): output model(input_tensor) pred torch.argmax(output, dim1).squeeze(0).cpu().numpy() # 将预测结果放大回原图尺寸并保存 pred_resized cv2.resize(pred.astype(np.uint8), (image.shape[1], image.shape[0]), interpolationcv2.INTER_NEAREST) cv2.imwrite(output_path, pred_resized * 255)推理阶段有几个细节必须注意。第一是model.eval()这个一定要调用否则BN层BatchNorm和Dropout的行为会不一致造成预测结果不稳定。第二是torch.no_grad()推理阶段不需要计算梯度不开的话内存占用会成倍增加。第三是后处理的插值方式把预测掩码放大回原图尺寸时一定要用INTER_NEAREST最近邻插值如果用INTER_LINEAR会导致滑坡边界出现渐变的中间值后续做面积统计时会出错。还有一个兼容性问题值得单独提出来说。PyTorch 2.6开始torch.load的weights_only参数默认值从False改成了True这意味着直接加载带有自定义类或优化器状态的checkpoint会报错。我用的checkpoint是字典形式并且保存了多个字段所以在torch.load时显式指定了weights_onlyTrue。如果你是从老版本迁移过来的训练代码里保存的是torch.save(model.state_dict(), ...)这种纯state_dict格式那weights_onlyTrue完全没问题但如果保存的是包含优化器状态的完整checkpoint读取时就要注意区分。如果后续有部署到服务端的需要我建议把模型导出成ONNX格式这样可以用ONNX Runtime或者OpenVINO做CPU推理加速。一张512x512的遥感影像ONNX Runtime CPU推理大约需要0.5秒相比Pytorch原生CPU推理速度快了3到5倍。导出代码也就几行dummy_input torch.randn(1, 3, 512, 512).to(device) torch.onnx.export(model, dummy_input, landslide_unet.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}})6. 常见问题与排查技巧实录6.1 典型问题速查表项目开发过程中一定会遇到各种问题我把常见问题整理成一张速查表方便你对照排查。问题现象可能原因解决方案训练时报CUDA out of memorybatch_size过大或输入分辨率过高减小batch_size到4或2或缩小输入尺寸到384x384直接崩溃报错Sizes of tensors must match输入尺寸不是模型要求的2的幂次倍数把输入尺寸对齐到模型下采样倍数的整数倍常见为16或32Loss下降缓慢或停滞学习率过小或数据集样本太少适当增大学习率到3e-4或检查数据量是否需要扩充训练初期Loss就很大且抖动剧烈学习率过大或Dice Loss的平滑项太小调低学习率到5e-5增大平滑项到1.0验证IoU高但新区域泛化很差数据泄漏或数据多样性不足检查训练/验证划分是否有重叠增加不同地貌样本预测结果全是背景没有滑坡区域类别极度不均衡模型学会了“偷懒”换用Dice Loss或Focal Loss或对滑坡区域做过采样滑坡边缘预测结果特别粗糙下采样倍数太高或输入分辨率不足换用更浅的骨干网络或提高输入分辨率推理时加载模型报weights_only相关错误PyTorch 2.6默认安全检查加载时显式指定weights_onlyFalse代码必须可信或保存纯state_dictCPU推理一张图要十秒未做模型量化和算子优化导出ONNX并用ONNX Runtime推理或转INT8量化6.2 避坑心得总结最后分享几个我在这个项目上花了大代价换来的避坑经验。关于标注问题我想重点说一个点标注的边界非常影响模型的边缘质量。遥感影像上滑坡体和周边裸土有时颜色非常接近尤其在阴影区域。我前两版标注中滑坡边界画得比较随意有的大块“留白”被标成了背景结果模型在这些区域出现了很多小洞预测为背景的像素点。后来我重新规定了标注规范滑坡后壁和侧壁的裸露区域、堆积区都算滑坡植被覆盖区域不算。重新标注后预测边缘的毛躁问题明显改善。关于训练策略我的感受是别一上来就训练一个超复杂的模型。在数据量只有几百张的情况下与其纠结换更深的骨干网络不如先把UNetResNet34这套基线跑到极致把数据增强、学习率调度、损失函数这些基础环节调好。我见过太多人一上来就上Swin Transformer结果因为显存不够、训练不稳定最后效果还不如一个调好的UNet。关于业务落地还有一个小建议模型预测结果一定要做基于地理信息的后处理。滑坡在自然条件下只会出现在坡度较陡的区域平坦的农田里绝不可能有滑坡。你可以准备一个坡度数据DEM派生在推理后把坡度小于10度的预测区域直接过滤掉这一步可以显著降低误检率。我用这个方法把测试集上的误检率降低了约15%而且完全不需要重新训练模型。这个项目本身当然还有不少可以继续扩展的地方。比如把单时相识别升级为多时相变化检测引入更多波段的遥感数据或者用少量标注样本加半监督学习来降低标注成本。不过这些都建立在一个稳定可靠的基线上希望这套基于CNN和PyTorch的方案能给你一个扎实的起点。本文还有配套的精品资源点击获取