☰
遥感影像滑坡场景分类实战:从数据预处理到PyTorch训练评估
2026/9/28 12:52:22 网站建设 项目流程

简介:一套基于Python的遥感影像滑坡场景分类完整实现,面向毕业设计、课程设计与项目开发,适合需要掌握机器学习处理遥感影像的初学者与开发者。压缩包共21个文件,包含Python源码、模型文件、训练/测试文本数据及说明文档等,其中4个py文件覆盖特征提取、样本生成与分类等核心流程,4个pkl及model、state等文件用于保存聚类器、LDA模型与视觉词表,整体仅1.86MB,轻量易部署。项目按照“特征提取—K-Means聚类—LDA主题分析—Libsvm分类”流程组织,提取光谱统计值和GLCM纹理特征作为视觉词袋,再经LDA抽象为主题分布高级特征,最终用SVM完成滑坡场景识别。已有56人学习下载。随包附有项目说明文档,可帮助读者快速复现实验、理解遥感影像分类中的特征工程与模型训练细节,并在此基础上扩展应用。

1. 做毕业设计选到「基于 Python 实现的遥感影像滑坡场景分类」这个题目,很多人第一反应是:这不就是个图像分类吗,跑个 ResNet 就完了。真上手才发现,遥感影像不是普通照片,滑坡也不是猫狗那样边界清晰的目标。滑坡场景分类要做的,是给一张覆盖某片山区的遥感影像,判断里面有没有发生滑坡。看起来是二分类,但多光谱波段、云雾遮挡、季节差异、正样本稀缺,会一个个跳出来卡住你。这套任务的价值在于:它是少数能把深度学习、遥感数据处理和工程化文档串在一起的入门题目,毕业设计、课程设计、小规模项目开发都能用同一套流程。下面按我做这类题目的顺序讲:数据怎么准备、模型怎么选、指标怎么看、坑在哪、文档怎么写。

2. 动手前先定任务边界:滑坡场景分类的两种定义与遥感数据准备

2.1 图级场景分类还是像素级分割:先选一个,代码量差一倍

滑坡识别在遥感领域其实有两条路线。一条是图级场景分类,输入一张影像图块,输出一个类别标签,判断这个区域有没有发生滑坡;另一条是像素级语义分割,输入一张影像,输出每个像素属于滑坡还是背景的掩码。标题写的是「场景分类」,所以默认走图级分类,这也是绝大多数毕业设计和课程设计的选题范围。

这两条路线的差别直接体现在代码量上。图级分类用 torchvision 的 ImageFolder 加 ResNet 就能跑,加载数据、训练、评估加起来不到 200 行;分割任务要自己写 Dataset 读掩码、改 U-Net 结构、算 IoU,工作量和坑都翻倍。如果你所在的课程要求里写了「识别滑坡区域」「提取滑坡边界」,那才需要走分割;只写「判断是否存在滑坡」「滑坡场景识别」,老老实实做图级分类。

还有一个常见误解是把场景分类当成目标检测。检测要画框定位滑坡的具体位置,需要多边形标注,正样本标注成本高;场景分类只需要给整张图打标签,标注成本低,而且和 Python 生态里的 ImageFolder、预训练模型、迁移学习能无缝配合,适合短期出成果。

2.2 遥感影像下载与预处理:把多波段 GeoTIFF 切成 RGB 图块

数据是这一题目最大的门槛。普通分类数据集从网上下载解压就能用,遥感影像你得自己下载、合成、切片。常见的数据源包括 Sentinel-2、Landsat 系列、国产高分系列,以及地理空间数据云这类面向国内用户的数据平台。做滑坡场景分类,Sentinel-2 的 10 米分辨率在多数情况下够用,而且免费开放,复现门槛低。

下载时注意两件事。第一,文件格式基本都是 GeoTIFF,一个文件里包含多个波段,不是普通图片;第二,不同传感器的波段顺序不一样,比如 Sentinel-2 真彩色合成用的是 B4(红)、B3(绿)、B2(蓝),而 Landsat 8 的 RGB 波段是 B4、B3、B2,顺序恰好相同但编号体系不同。做预处理前先确认波段描述,别想当然。

拿到原始影像后,第一步是切成固定大小的图块。遥感影像动辄上万像素宽,不可能整张喂进模型。我一般用 rasterio 读波段,然后按 256×256 或 512×512 的窗口切片,存成 JPEG 供训练使用:

import os import numpy as np from PIL import Image import rasterio def tif_to_rgb_tiles(tif_path, output_dir, tile_size=256, stride=256): """把多波段 GeoTIFF 切成 RGB 图块,直接供 PyTorch ImageFolder 使用""" with rasterio.open(tif_path) as src: # 先打印 src.descriptions 确认波段顺序,再决定读哪几个波段 print("波段顺序:", src.descriptions) # 以 Sentinel-2 为例:B4 红、B3 绿、B2 蓝 red = src.read(4) green = src.read(3) blue = src.read(2) rgb = np.stack([red, green, blue], axis=-1) # (H, W, 3) h, w, _ = rgb.shape os.makedirs(output_dir, exist_ok=True) idx = 0 for y in range(0, h - tile_size + 1, stride): for x in range(0, w - tile_size + 1, stride): tile = rgb[y:y + tile_size, x:x + tile_size] # 跳过全黑或无数据区域,减少无效样本 if tile.max() == 0: continue Image.fromarray(tile.astype(np.uint8)).save( os.path.join(output_dir, f"tile_{idx:05d}.jpg")) idx += 1 print(f"切片完成: 共 {idx} 个图块 -> {output_dir}")

几个参数说明。stride 等于 tile_size 时切出的图块没有重叠,图块数量最少,适合数据量还够的情况;stride 小于 tile_size 时产生重叠图块,能扩充样本量,但同一区域会出现在多张图里,后面划分数据集时要注意数据泄漏,这个坑在第 5 章详细讲。tile_size 从 256 起步比较稳,图块太小滑坡特征看不全,图块太大又浪费显存。波段读取用 src.read(4) 而不是 src.read(4),rasterio 的波段索引从 1 开始,这一点经常有人搞混导致通道错乱。

远感影像下载下来之后做不做大气校正,取决于你的场景。滑坡识别靠的是地表形态和色调差异,如果只是做课程设计,拿到的是 L2A 级表面反射率产品,直接用就行;只有当你需要跨时相对比或做定量分析时,才需要考虑辐射定标和大气校正。预处理阶段真正该花时间的是直方图拉伸,很多影像原始数值范围很窄,直接转 uint8 会整体发灰,我一般用 2% 线性拉伸把对比度拉开再做切片。

2.3 数据集划分与目录组织:ImageFolder 结构让训练代码少写一半

数据切完图块,接下来是标注和划分。标注没有捷径,只能靠人工目视判读:逐个看切片,有明显滑坡痕迹(滑体、滑坡后壁、碎屑堆积)的放进 landslide 文件夹,没有的放进 non_landslide 文件夹。判读时有一个容易忽略的点:植被已经恢复的老滑坡痕迹很淡,这类样本要不要算正样本,直接决定你的模型行为。建议在文档里写清楚你的正样本定义,比如「目视可辨的滑坡体或滑坡痕迹」,否则答辩时老师拿一张老滑坡影像问你「这算不算滑坡」,你答不上来。

标注完成后,目录按 ImageFolder 约定组织,训练代码几乎不用写数据加载逻辑:

data/ ├── train/ │ ├── landslide/ │ └── non_landslide/ ├── val/ │ ├── landslide/ │ └── non_landslide/ └── test/ ├── landslide/ └── non_landslide/

划分脚本用 Python 写很直接,关键是固定随机种子,保证每次运行结果一致,这也是实验可复现的基本要求:

import os import shutil import random from glob import glob def split_dataset(source_dir, output_dir, train_ratio=0.7, val_ratio=0.15): """按类别把已标注影像划分成 train/val/test 三份""" random.seed(42) # 固定种子,确保可复现 os.makedirs(output_dir, exist_ok=True) for cls in os.listdir(source_dir): cls_dir = os.path.join(source_dir, cls) if not os.path.isdir(cls_dir): continue imgs = glob(os.path.join(cls_dir, "*.jpg")) + glob(os.path.join(cls_dir, "*.tif")) random.shuffle(imgs) n_train = int(len(imgs) * train_ratio) n_val = int(len(imgs) * val_ratio) parts = { "train": imgs[:n_train], "val": imgs[n_train:n_train + n_val], "test": imgs[n_train + n_val:], } for split_name, split_imgs in parts.items(): dst = os.path.join(output_dir, split_name, cls) os.makedirs(dst, exist_ok=True) for img_path in split_imgs: shutil.copy(img_path, dst) print(f"{cls}: {split_name} {len(split_imgs)} 张") # 用法 split_dataset("labeled_tiles", "data")

提示:如果滑坡正样本总共只有几十张,按 0.7/0.15/0.15 划分后测试集可能只剩几张,不够评估。正样本少于 100 张时,建议只分 train/val 两份,把 val 当测试用,或者做 K 折交叉验证,别为了形式硬切三份。

划分完的目录配合 torchvision 的 ImageFolder,两行代码就把数据集和类别映射建好了:

from torchvision import datasets train_ds = datasets.ImageFolder("data/train") print(train_ds.classes) # ['landslide', 'non_landslide'] print(train_ds.class_to_idx) # {'landslide': 0, 'non_landslide': 1}

注意 class_to_idx 是按文件夹名字母序排列的,landslide 排在 non_landslide 前面,所以正类是索引 0。后面写评估代码时别把正负类搞反,混淆矩阵的解读全靠这个映射。

3. 模型选型与训练骨架:用 ResNet 迁移学习把基线跑起来

3.1 为什么选 ResNet18 做基线:参数量、复现难度与预期准确率

滑坡场景分类的样本量通常在几百到几千张,这种规模下,模型选型的核心不是「谁准确率最高」,而是「谁能在小数据上稳定收敛、容易复现、好解释」。几个候选模型放在一起对比就很清楚:

模型参数量几百张样本的预期复现难度建议
ResNet18约 11M稳定 85% 以上低,PyTorch 直接调用首选
ResNet50约 25M容易过拟合低样本上千再考虑
ViT-B/16约 86M需要大量数据,容易崩中不推荐做基线
自建 CNN不定难到 80%中不推荐

ResNet18 的残差结构在遥感小数据集上有天然优势:网络不深,参数量适中,配合 ImageNet 预训练权重做迁移学习,几百张样本就能收敛到可用的准确率。ViT 虽然在新数据集上表现亮眼,但 transformer 需要大量数据拟合归纳偏置,滑坡正样本往往只有一两百张,用它做基线纯属给自己添堵。ResNet50 比 18 深一倍,数据不够时验证集损失先降后升,过拟合会很典型。

另一个选型理由是 PyTorch 生态对 ResNet 的支持最完整。torchvision.models 里 resnet18 一行调用,预训练权重自动下载,后续换 ResNet50 或 101 只需要改一行代码,实验对比成本极低。答辩时老师问「为什么选这个模型」,你可以回答:残差结构缓解梯度消失,适合中小规模数据集微调;预训练权重提供了良好的初始化,缓解遥感样本不足的问题。这个回答逻辑是完整的。

3.2 train.py 最小骨架:PyTorch 训练主循环与参数说明

选型定了,直接写训练脚本。下面这份 train.py 是完整的、能跑的骨架,我按「数据增强 → 模型构建 → 训练循环 → 验证保存」四段组织,方便你在答辩时对着代码讲思路:

import os import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms, models def get_model(num_classes=2): """加载 ImageNet 预训练的 ResNet18,替换最后分类头""" model = models.resnet18(pretrained=True) model.fc = nn.Linear(model.fc.in_features, num_classes) return model def evaluate(model, loader, device): """验证集准确率""" model.eval() correct, total = 0, 0 with torch.no_grad(): for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) preds = torch.argmax(model(imgs), dim=1) correct += (preds == labels).sum().item() total += labels.size(0) return correct / total def main(): device = "cuda" if torch.cuda.is_available() else "cpu" batch_size, epochs, lr = 16, 30, 1e-4 # 训练集用增强,验证集只用缩放归一化,保证评估稳定 train_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomVerticalFlip(p=0.5), transforms.RandomRotation(degrees=15), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) val_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) train_ds = datasets.ImageFolder(os.path.join("data", "train"), train_tf) val_ds = datasets.ImageFolder(os.path.join("data", "val"), val_tf) train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=2) val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=2) model = get_model(len(train_ds.classes)).to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=lr) best_acc = 0.0 for epoch in range(epochs): model.train() total_loss = 0.0 for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() loss = criterion(model(imgs), labels) loss.backward() optimizer.step() total_loss += loss.item() val_acc = evaluate(model, val_loader, device) print(f"Epoch {epoch+1:02d}/{epochs} | loss={total_loss/len(train_loader):.4f} | val_acc={val_acc:.2%}") # 只在验证准确率创新高时保存权重,防止最后几轮过拟合覆盖好模型 if val_acc > best_acc: best_acc = val_acc os.makedirs("checkpoints", exist_ok=True) torch.save(model.state_dict(), "checkpoints/best.pth") print(f"训练完成,最佳验证准确率 {best_acc:.2%}") if __name__ == "__main__": main()

几个参数的使用逻辑说清楚。学习率 1e-4 是迁移学习的常见起点,直接从头训练才需要 1e-3 量级;用 1e-4 微调 ImageNet 权重,既能让新分类头学起来,又不会把预训练特征冲掉。batch_size 16 在 224×224 输入下,6GB 显存的显卡基本够跑,显存不足优先减半而不是改模型。epochs 30 对小数据集偏多,但配合「验证准确率创新高才保存」的逻辑,多跑几轮也没有副作用,反而是训练曲线更完整,写实验报告时更有素材。

有两个容易翻车的小点。第一,Windows 上 DataLoader 的 num_workers 设为 0,否则多进程加载会在 Jupyter 或某些 IDE 环境里反复报错;Linux 服务器上设 2 或 4 能明显提速。第二,Normalize 的均值和标准差用的是 ImageNet 统计值,这是迁移学习的标准做法,你不需要自己重新统计遥感影像的均值,除非你打算完全从头训练。

3.3 正样本不足和多波段输入:两个必须提前处理的选型问题

训练起来之后,最先暴露的两个问题几乎必然出现:正样本少导致模型偏置,多波段影像和预训练权重不匹配。

正样本少的典型表现是 loss 降得很顺,但验证时 landslide 类的召回率极低。原因是交叉熵损失对多数类偏向,模型发现全预测为非滑坡就能拿到很低的 loss。最常见的解法是给交叉熵损失加类别权重,让少数类的错误付出更大代价:

# 按训练集样本数计算类别权重,正样本少就给它更高的权重 class_counts = [train_ds.targets.count(i) for i in range(len(train_ds.classes))] max_count = max(class_counts) weights = [max_count / c for c in class_counts] class_weights = torch.tensor(weights, dtype=torch.float).to(device) criterion = nn.CrossEntropyLoss(weight=class_weights)

这段代码放在 get_model 之后、训练循环之前。注意 train_ds.targets 是 ImageFolder 按文件夹顺序生成的标签列表,和 class_to_idx 对应。权重计算方法很朴素:多数类权重为 1,少数类权重 = 多数类样本数 / 少数类样本数。如果两类样本数是 500 比 50,滑坡类权重就是 10,相当于把滑坡样本的 loss 放大 10 倍。

多波段的问题是另一类翻车现场。Sentinel-2 有 13 个波段,很多人想保留近红外波段提升植被和裸地的区分度,但 ImageNet 预训练的 ResNet 第一层卷积只接受 3 通道输入。两条路:大多数情况直接取 RGB 三波段,简单稳定,配合合适的拉伸效果不差;想用 4 波段(RGB+NIR),需要修改第一层卷积并复制权重:

import torch.nn as nn def adapt_conv1_for_bands(model, in_channels=4): """把 ResNet 首层卷积改成支持 4 波段输入,保留预训练权重""" old_conv = model.conv1 # 预训练权重 shape: (64, 3, 7, 7) new_conv = nn.Conv2d(in_channels, 64, kernel_size=7, stride=2, padding=3, bias=False) with torch.no_grad(): new_conv.weight[:, :3] = old_conv.weight # 第 4 个通道用前 3 个通道的均值初始化,而不是随机初始化 new_conv.weight[:, 3] = old_conv.weight.mean(dim=1) model.conv1 = new_conv return model

为什么要用均值初始化第四个通道而不是随机值?随机初始化会在一开始给模型引入剧烈噪声,破坏预训练权重的稳定性,均值复制至少让前向传播的输出量级和原模型接近。不过说句实在话,课程设计和毕业设计的样本量下,RGBN 四波段带来的提升通常不显著,还要额外写预处理逻辑。我一般建议先跑 RGB 基线,如果 F1 卡在瓶颈再尝试加 NIR 波段,把结论写进实验报告,反而显得你考虑周全。

4. 训练评估与推理闭环:指标、调参顺序与 predict.py

4.1 评估指标:混淆矩阵、F1 与滑坡类召回率才是关键

滑坡场景分类有一个典型陷阱:Accuracy 虚高。假设验证集里 500 张非滑坡、50 张滑坡,模型把所有图都判成非滑坡,Accuracy 有 90.9%,看起来「效果不错」,实际上滑坡一张都没识别出来。所以训练脚本里的 evaluate 只能算中间检查,项目交付时必须上混淆矩阵和分类报告:

from sklearn.metrics import confusion_matrix, classification_report, f1_score def detailed_evaluate(model, loader, device, target_names): """输出混淆矩阵、Precision/Recall/F1 分类报告""" model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) preds = torch.argmax(model(imgs), dim=1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.cpu().numpy()) cm = confusion_matrix(all_labels, all_preds) print("混淆矩阵 (行=真实, 列=预测):") print(cm) print("\n分类报告:") print(classification_report(all_labels, all_preds, target_names=target_names)) print(f"滑坡类 F1 分数: {f1_score(all_labels, all_preds):.4f}") return all_preds, all_labels # 调用示例 # detailed_evaluate(model, val_loader, device, ["non_landslide", "landslide"])

实际使用中,我重点看三个数:滑坡类的召回率(漏检率直接关系应用价值)、滑坡类的 F1(精度和召回的综合)、非滑坡类的精度(误报率)。混淆矩阵则帮你定位错误模式——如果非滑坡被误判为滑坡的样本大量集中在裸地、河流冲积扇、采石场这些视觉上与滑坡相似的地物,这就是一个值得写进文档的发现。

特别提醒一个阈值问题。模型的输出是 softmax 概率,默认取 0.5 作为分类阈值,但对滑坡这类「漏检代价高」的任务,可以主动调低阈值提升召回率:

# 不用 argmax,改用概率阈值,滑坡召回率不够时把阈值从 0.5 降到 0.4 probs = torch.softmax(outputs, dim=1)[:, 1] # 索引 1 是滑坡类 preds = (probs >= 0.4).long()

阈值降多少,看你对漏检和误报的权衡。答辩时能说出「我把阈值从 0.5 调到 0.4,滑坡召回率从 82% 提到 91%,代价是非滑坡误报率从 6% 升到 9%,这个代价在实际应用中可接受」,这种表述比单纯报一个准确率有说服力得多。

4.2 训练参数范围与调参顺序:先稳住 loss 再谈准确率

调参不是玄学,但乱调确实很浪费时间和算力。我的习惯是固定一套顺序:先确认 loss 能下降,再调学习率,最后调数据增强和数据量,模型结构尽量不动。

超参数推荐范围失败时的排查方向
学习率1e-4 ~ 5e-4loss 震荡则减半,loss 不降则先冻结 backbone
batch size8 ~ 32显存不够就减半,BN 层在小 batch 下不稳定
epochs20 ~ 50连续 5 轮 val_acc 不涨就早停
优化器Adam betas=(0.9, 0.999)想提上限可换 SGD momentum=0.9

loss 迟迟不降时,最常见的做法是先冻结 backbone,只训练分类头几个 epoch,让新分类头先适应预训练特征的分布,再解冻全模型微调:

def freeze_backbone(model, freeze=True): """冻结或解冻 ResNet 除 fc 外的全部参数""" for name, param in model.named_parameters(): if name.startswith("fc"): param.requires_grad = not freeze else: param.requires_grad = freeze # 先冻结训练 5 轮 freeze_backbone(model, freeze=True) # ... 训练 5 个 epoch ... # 再解冻全部参数训练 freeze_backbone(model, freeze=False)

冻结 backbone 的阶段建议把学习率调到 1e-3,因为此时只训练 fc 层,参数量小,收敛快;解冻后再降回 1e-4 微调,避免破坏预训练特征。这个「先粗后细」的调参顺序在遥感小数据集上非常管用,也是答辩时能讲清楚的工程经验。

4.3 模型保存与独立推理脚本:predict.py 写得让答辩老师能直接跑

训练产出的 best.pth 只是权重文件,项目要能交付,必须有一个独立的预测脚本。所谓独立,指的是不依赖训练代码,只要有 PyTorch 环境就能跑单张影像的推理。我写的 predict.py 会把模型路径、输入影像、置信度阈值都做成命令行参数,答辩现场拿一张没见过的影像直接跑:

import argparse import torch import torch.nn as nn from PIL import Image from torchvision import transforms, models CLASS_NAMES = ["non_landslide", "landslide"] # 和 train_ds.classes 顺序一致 def load_model(model_path, num_classes=2): """加载权重并切到推理模式,注意 map_location 保证无 GPU 也能跑""" model = models.resnet18(pretrained=False) model.fc = nn.Linear(model.fc.in_features, num_classes) model.load_state_dict(torch.load(model_path, map_location="cpu")) model.eval() return model def predict_single(image_path, model, threshold=0.5): """对单张影像做推理,返回类别和置信度""" tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) img = Image.open(image_path).convert("RGB") tensor = tf(img).unsqueeze(0) with torch.no_grad(): prob = torch.softmax(model(tensor), dim=1)[0] landslide_prob = prob[1].item() label = CLASS_NAMES[1] if landslide_prob >= threshold else CLASS_NAMES[0] return label, landslide_prob def main(): parser = argparse.ArgumentParser(description="遥感影像滑坡场景分类推理") parser.add_argument("--image", required=True, help="待预测的影像路径") parser.add_argument("--model", default="checkpoints/best.pth", help="权重路径") parser.add_argument("--threshold", type=float, default=0.5, help="滑坡判定阈值") args = parser.parse_args() model = load_model(args.model) label, prob = predict_single(args.image, model, args.threshold) print(f"影像: {args.image}") print(f"滑坡概率: {prob:.2%} | 判定结果: {label}") if __name__ == "__main__": main()

命令行用法是python predict.py --image test_001.jpg --model checkpoints/best.pth --threshold 0.4。三个参数的意义分别对应:影像路径支持 jpg/png/tif(tif 会被 PIL 自动转 RGB)、模型权重路径、置信度阈值。用map_location="cpu"是因为演示机器大概率没装 GPU 版 PyTorch,提前兼容能避免答辩时当场报 CUDA 错误。

这个脚本的价值在于它把「训练」和「使用」解耦了。答辩时老师不会关心你怎么训练的,但会关心「你训练出来的东西能不能用」,predict.py 就是回答这个问题的最好证据。

5. 滑坡场景分类五个常见翻车点:现象、原因与排查手记

下面这五个问题是我和身边人做遥感分类题目攒下来的血泪经验,每一条都按「现象 → 原因 → 解决」的顺序写,照着排查能省好几天。

5.1 通道数不匹配:4 波段影像直接喂进 ResNet 报错

现象:训练脚本在 DataLoader 加载第一个 batch 时报RuntimeError: Given groups=1, weight of size [64, 3, 7, 7], expected input to have 3 channels, but got 4 channels instead,或者类似 shape mismatch 的报错。

原因:GeoTIFF 默认保留了所有波段,预处理时如果只做了切片没做波段选择,读出来的图块就是 4 通道(RGB + NIR)甚至 13 通道的数组,而 ResNet 的第一层卷积只接受 3 通道。

解决:回到第 2.2 节的切片脚本,确认用src.read(4), src.read(3), src.read(2)只取三个波段再合成 RGB;如果坚持用 4 波段,走第 3.3 节的adapt_conv1_for_bands改模型结构。排查时在切片脚本里加一行print(tile.shape),看到(256, 256, 4)就说明波段没选对。

5.2 模型把所有影像都判成非滑坡:正负样本失衡

现象:训练 loss 正常下降,验证准确率能到 90% 以上,但打开混淆矩阵发现滑坡类精度和召回率全为 0,模型把所有输入都输出为非滑坡。

原因:滑坡正样本通常只有几十到一两百张,非滑坡可能上千张,交叉熵损失在样本比例悬殊时倾向多数类。准确率虚高就是被这类样本失衡骗了,只盯着 Accuracy 根本发现不了问题。

解决:先加类别权重,nn.CrossEntropyLoss(weight=class_weights)是最快的修正;如果加权重后训练震荡,再用 WeightedRandomSampler 对少数类过采样,每次迭代有更高概率抽到滑坡样本。判断是否修好,不看准确率,看混淆矩阵里 landslide 行有没有非零值。

5.3 验证集 95% 测试集 60%:切块造成的数据泄漏

现象:训练时验证准确率一路涨到 95%,自我感觉良好;拿独立测试集一测只有 60%,答辩现场直接翻车。

原因:第 2.2 节切片时如果 stride 小于 tile_size,相邻图块有重叠;更常见的是同一幅大影像的不同区域被随机分到了 train 和 val。模型在训练时见过来自同一场景的像素,验证时当然「眼熟」,一遇到真正没见过的影像就露馅。

解决:切块时记录每张图块来自哪幅原始影像,在文件命名里加原始影像 ID;划分数据集时按影像 ID 分组,保证同一幅影像的所有图块进同一个划分。最简单的做法是切片时用tile_{image_id}_{idx:05d}.jpg命名,划分脚本里按前缀分组而不是逐张 shuffle。这个坑最隐蔽,也最致命,一定要在文档里写明你的划分策略。

5.4 loss 不降、验证准确率卡在 50%:迁移学习在遥感影像上失效

现象:预训练权重加载成功,但训练十几轮后 loss 还在 0.69 附近,验证准确率稳定在 50% 左右,跟随机猜一样。

原因:多数情况下不是模型坏了,而是输入分布和预训练数据差异太大。常见诱因有三个:RGB 波段选错导致颜色通道错乱、影像没有做拉伸对比度过低、学习率设太高把预训练权重冲坏了。

解决:按顺序排查。先保存一张预处理后的训练样本图片,肉眼确认颜色正常;再检查预处理里有没有做Resize((224, 224)),原始影像分辨率太高直接进模型会触发缩放异常;最后把学习率降到 1e-5 试几个 epoch,如果 loss 开始下降,就说明之前是学习率问题,回到 1e-4 继续。冻结 backbone 只训练分类头也是一个有效的兜底手段,能把问题范围缩小一半。

5.5 文档和代码对不上:答辩被问一句就穿

现象:答辩时老师问「你的学习率为什么是 1e-4」「loss 曲线为什么有毛刺」「这个阈值 0.4 怎么定的」,学生翻代码翻半天,最后说「我试出来的」,然后就没有然后了。

原因:训练过程中参数随手改,改完没记录,代码里只留了最终版本,中间过程没有存档。这是课程设计和毕业设计最常见的失分点,不是技术问题,是工程习惯问题。

解决:从第一次跑通基线开始,维护一份实验记录表,每改一个参数就记一行,包含日期、模型、学习率、数据增强策略、验证准确率、F1、备注;代码里每个关键参数旁边写注释说明为什么取这个值。答辩被问参数时,直接翻实验记录表回答「第 3 轮实验从 1e-4 调到 5e-4,验证 F1 下降 4 个点,所以维持 1e-4」,这个回答的杀伤力远大于「我试出来的」。

6. 交稿前补齐三件事:文档主线、实验记录表与答辩现场验证

6.1 项目文档的一页式主线与四张必备图

项目文档最常见的毛病是写成流水账:先写环境安装,再写代码结构,最后贴几个结果,老师看完不知道你解决了什么问题。更好的写法是沿着一条主线组织:背景与问题定义 → 数据来源与预处理 → 方法选型理由 → 实验设计与结果 → 结论与不足。这条线就是你在第 2 到第 4 章做的事情,文档只是把它用文字固化下来。

文档里必须放四张图:数据样本图(正负样本各几张,带标注说明)、模型结构图(用 PyTorch 的模型结构打印或手绘 ResNet 示意)、loss 和验证准确率曲线、混淆矩阵热力图。这四张图对应数据处理、模型设计、训练过程、评估结果四个环节,答辩 PPT 可以直接复用,比大段文字高效得多。

6.2 实验记录表:让老师一眼看到你的调参过程

实验记录表放在项目文档的附录里,格式不用复杂,一个表格就够:

实验编号模型输入波段学习率数据增强验证准确率滑坡 F1备注
E01ResNet18RGB3e-4基础翻转87.2%0.81基线
E02ResNet18RGB1e-4增强+亮度89.5%0.85增强有效
E03ResNet18RGBN1e-4增强+亮度88.1%0.834 波段无提升
E04ResNet50RGB1e-4增强+亮度90.2%0.86有过拟合风险

这个表本身就在回答问题:为什么最终选 ResNet18 而不是 ResNet50?因为 E04 验证集涨了但测试集没涨,过拟合。为什么用 1e-4?因为 E01 和 E02 对比说明 1e-4 更稳。老师的追问,一张表全部兜住。

6.3 答辩现场验证:拿三张没见过的影像当场跑推理

答辩前准备三张模型没见过的影像:一张典型滑坡、一张典型非滑坡、一张边缘场景(比如裸岩或采石场)。现场跑python predict.py --image 那张图,重点不是结果全对,而是你能解释为什么。

典型滑坡判对了,说「滑坡体呈浅色调、边界不规则,模型在滑坡类上召回率达到 91%」;边缘场景如果判错,就借机说「这类样本和裸岩视觉特征相似,训练数据里这类负样本偏少,这是当前方案的已知局限,后续可以补充数据或加注意力机制」。把翻车现场变成方案讨论,比假装模型完美无缺更让老师信服。

我养成的习惯是:每次实验先在记录表里补一行,再动下一个参数。看起来每次只多花两分钟,答辩前能省出大量返工时间。这套流程从数据到文档走完一轮,你就知道遥感影像滑坡场景分类真正花时间的不是模型训练,而是数据整理和实验管理——把这两块做扎实,项目自然立得住。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询