简介:这是一份基于CNN深度学习的遥感Landsat影像地物分类算法Python完整源码,适合计算机、遥感、人工智能等相关专业学生用于毕业设计、课程设计或项目实战。代码经过运行验证,涵盖从影像切片生成、模型训练到新数据预测的完整流程,包含三个Python脚本、预训练h5权重模型、示例tif影像、分类结果输出文件及配套xml/tfw辅助文件,共10个文件,压缩包仅14.88MB,轻量易部署。已有470人学习下载,资源附带README说明,便于快速上手。通过此源码包可掌握CNN在遥感和地物分类中的数据处理、模型构建与预测输出方法,熟悉Landsat影像波段设定与地物类别标注思路,为同类算法复现和实验扩展提供直接参考。
1. 基于CNN的Landsat影像地物分类:这套源码包值不值得你花时间
拿到Landsat影像想做地物分类,很多人的第一反应是上随机森林或者支持向量机,但真到了大范围、多时相的应用场景就会发现,传统方法光是做光谱指数组合和纹理特征提取就能把人磨到怀疑人生。基于CNN深度学习的这套Python源码,恰好是冲着这个痛点来的:它把Landsat多波段数据直接喂进卷积网络,让模型自己从光谱和空间邻域里学习地物特征,省掉了人工设计特征这层中间环节。我拆过几套同类型的遥感分类源码,也在某生态监测项目X里跑过类似的流程,可以负责任地说,这类项目的目录结构和训练逻辑,是遥感方向入门深度学习最值得照着复现的一类样板。它适合遥感或GIS方向的学生、刚转深度学习的开发者也适合需要快速给某区域做土地覆盖底图的技术人员。下面把源码涉及的波段选择、样本制作、网络设计、训练评估和踩坑点完整梳理一遍,尽量让你拿到的不是一堆跑不通的代码,而是一条能复现、能改参数、能换数据继续用的完整路径。
2. 数据准备与样本制作:从Landsat原始影像到可训练的切片数据集
2.1 Landsat波段组合与地物类别的对应关系
Landsat 8/9的OLI传感器常规拿到的是9个波段,做地物分类时并不会全用。热红外波段分辨率是100米重采样到30米,对地表覆盖分类帮助有限;卷云波段主要用于大气校正,放在分类特征里反而是噪声。所以源码里最常见的是取6个多光谱波段:蓝(0.45-0.51μm)、绿(0.53-0.59μm)、红(0.64-0.67μm)、近红外(0.85-0.88μm)、短波红外1(1.57-1.65μm)、短波红外2(2.11-2.29μm)。这六个波段组合之所以能支撑绝大多数地物分类任务,是因为不同地物在这几个波段的反射特性差异足够大。水体在近红外和短波红外上几乎完全吸收,反射率极低;健康植被在红光波段吸收强、近红外波段反射陡升,这是判断植被覆盖最稳的光谱特征;裸土和建筑在短波红外1、短波红外2上普遍偏高,但建筑区域往往带有规则纹理和阴影,裸地则比较平滑。
| 波段 | 波长范围(μm) | 主要响应地物 |
|---|---|---|
| 蓝 | 0.45-0.51 | 水体、建筑、大气散射 |
| 绿 | 0.53-0.59 | 植被、浅水 |
| 红 | 0.64-0.67 | 植被吸收带、裸土 |
| 近红外 | 0.85-0.88 | 植被、水体强吸收 |
| 短波红外1 | 1.57-1.65 | 土壤湿度、建筑 |
| 短波红外2 | 2.11-2.29 | 岩石、裸地、干旱区 |
波段顺序是这套源码第一个隐藏坑。不同数据源给出的Landsat GeoTIFF波段排列并不一致,有的按蓝绿红近红外短波红外排列,有的文件里还混着卷云波段或者沿海波段。如果直接按固定下标读波段,训练时模型看到的通道顺序和实际物理波段对不上,轻则精度偏低,重则loss完全降不下去。我自己的习惯是先用rasterio把每个波段的文件名或者元数据里的波长描述打出来,确认顺序后再写读取代码。
2.2 辐射定标、归一化与切片:把GeoTIFF变成训练样本
拿到Landsat原始数据后,第一个问题是数据级别。L1级产品存的是大气表观反射率,没有做过大气校正,不同时间的影像之间辐射差异很大,直接混在一起训练会让网络去拟合大气条件而不是地物本身。常见做法是两个方向:一是用遥感处理软件里的FLAASH大气校正模块做一遍处理,二是直接使用表面反射率产品,省掉定标和校正步骤。对只想跑通源码流程的人来说,我建议直接用表面反射率数据,把精力留给后面的训练环节。
切片之前必须做归一化。遥感影像的像素值范围波动很大,表面反射率产品虽然是0到1的小数,但存储成uint16后可能是0到10000的整数,如果直接拿原始值喂CNN,卷积核的梯度计算会被大数值主导,网络收敛极其缓慢。源码里普遍的做法是按波段做z-score标准化,也就是每个波段减去均值再除以标准差,把特征分布拉到0附近。这一步看似简单,却有个容易被忽略的细节:标准化参数一定要在整个训练集上统计,不能拿单个切片自己算,否则推理阶段拿到新影像时会因为归一化参数不一致而预测紊乱。
import rasterio import numpy as np def load_landsat_tiles(tiff_path, patch_size=128, stride=64, band_indices=(2, 3, 4, 5, 6, 7)): with rasterio.open(tiff_path) as src: # band_indices 按1起始的下标读取蓝绿红近红外短波红外1短波红外2 bands = src.read(band_indices).astype(np.float32) # bands shape: (6, height, width),转成(height, width, 6) bands = np.transpose(bands, (1, 2, 0)) h, w = bands.shape[:2] tiles = [] for i in range(0, h - patch_size + 1, stride): for j in range(0, w - patch_size + 1, stride): tile = bands[i:i + patch_size, j:j + patch_size, :] tiles.append(tile) return np.stack(tiles, axis=0)这段代码做的事情很直接:把一景Landsat影像按指定波段组合读进来,再按固定步长滑动切片。patch_size和stride这两个参数决定了训练样本的空间覆盖方式。patch_size取128时,在30米分辨率的Landsat影像上对应3.84公里见方的区域,能覆盖大面积农田和连续水体,但对细小地物边界来说太大;取64则更轻量,样本数量翻四倍,显存压力小,但感受野变小。我一般倾向于64到128之间,具体看目标地物的最小图斑尺寸。stride等于patch_size时切片不重叠,样本独立性强;stride小于patch_size时相邻切片有重叠,相当于一种数据增强,能显著增加样本量,但也要注意重叠会导致训练集和验证集之间的空间自相关。
2.3 样本标注与数据集划分:先分区块再切片,避免空间自相关
Landsat地物分类属于监督学习,每个训练切片都要有对应的类别标签。标注方式常见有两种。第一种是在影像上直接勾画多边形ROI,把多边形内部的像素归类为某个地物类别,再栅格化成与影像同尺寸的标签图;第二种是拿已有的土地覆盖产品做自动标注,再人工抽查修正。对于源码复现来说,第一种更可控,样本质量心里有底。每个切片的标签怎么定也有讲究,如果按整块分类的思路走,可以用切片内出现次数最多的类别作为该样本的标签;如果模型设计成端到端分割的类型,那每个切片要保留完整的像素级标签图,这意味着数据准备的代码要多写一层。
def prepare_dataset(feature_tiles, label_tiles, val_ratio=0.2, test_ratio=0.1): # 对每个波段做z-score标准化,统计参数来自整个样本集 mean = feature_tiles.mean(axis=(0, 1, 2), keepdims=True) std = feature_tiles.std(axis=(0, 1, 2), keepdims=True) + 1e-6 feature_tiles = (feature_tiles - mean) / std n = len(feature_tiles) n_val = int(n * val_ratio) n_test = int(n * test_ratio) n_train = n - n_val - n_test train_x, train_y = feature_tiles[:n_train], label_tiles[:n_train] val_x, val_y = feature_tiles[n_train:n_train + n_val], label_tiles[n_train:n_train + n_val] test_x, test_y = feature_tiles[n_test:], label_tiles[n_test:] return train_x, train_y, val_x, val_y, test_x, test_y数据划分这里藏着遥感分类最容易翻车的一个细节:不能把全部切片混在一起后随机打乱划分。Landsat影像的空间分辨率是30米,同一地物在相邻切片里高度相似,如果A切片进训练集而紧挨着它的B切片进验证集,验证精度会被严重高估。更好的流程是先把一景影像按地理区域切成几大块,比如按左上、右上、左下、右下分成四块,取其中三块的全部切片做训练,剩下一块做验证,测试集只在完全独立的一景影像上生成。train、val、test的比例按照7比2比1或者8比1比1都常见,关键是保证空间上的独立性。
3. CNN网络结构与训练脚本:三层卷积的模型怎么把精度跑到85%以上
3.1 网络结构设计:为什么是小卷积核加池化而不是堆层数
遥感地物分类和自然图像分类有一个本质区别:地物类别往往没有清晰的物体轮廓,农田、裸地、建筑、水体之间的边界是渐变的,判别信息主要来自光谱曲线形态和局部纹理统计。CNN之所以适合这类任务,是因为卷积层的局部感受野能同时捕捉光谱通道之间的组合关系,以及小邻域内的空间纹理模式,而权值共享机制让模型在训练样本有限时不会因为参数过多而迅速过拟合。
源码里最稳妥的网络结构是三组卷积加池化,再接一个全局池化和全连接分类头,而不是一味堆层数。原因很现实:遥感训练样本通常只有几千到几万张切片,一个50层的残差网络在这么小的数据规模上很容易把训练集背下来,验证精度却一路走低。三层卷积每层滤波器数量从32递增到128,参数总量控制在几十万量级,对Landsat这种光谱纹理相对简单的输入已经足够。
import torch import torch.nn as nn class LandsatCNN(nn.Module): def __init__(self, in_channels=6, num_classes=5): super().__init__() self.features = nn.Sequential( nn.Conv2d(in_channels, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.AdaptiveAvgPool2d(1) ) self.classifier = nn.Linear(128, num_classes) def forward(self, x): x = self.features(x) x = x.view(x.size(0), -1) return self.classifier(x)网络里有几个细节值得展开。卷积核全部用3乘3加padding保持空间尺寸不变,这是VGG系列验证过的组合:两个3乘3卷积堆叠等价于一个5乘5卷积的感受野,但参数量更小、非线性更强。BatchNorm放在卷积和激活函数之间,作用是对每个通道做标准化,这个操作在遥感数据上比在自然图像上更重要,因为不同时间、不同区域获取的影像辐射值差异可能很大,BatchNorm能把这些差异在训练过程中逐渐拉平。AdaptiveAvgPool2d把任意尺寸的特征图压缩成1乘1,这样网络在推理阶段可以接受不同大小的输入切片,不需要重新训练。
3.2 训练循环核心代码与关键参数解读
网络定义完之后,训练脚本是整个源码里最需要仔细读的部分。很多人拿到代码直接跑,发现loss曲线一直在高位震荡,第一反应是调学习率或者换优化器,但大多数情况下问题出在数据而不是训练参数。训练循环里几个关键操作必须对齐:输入张量的维度顺序要从h、w、c转成c、h、w,标签要是长整型,损失函数用交叉熵,优化器选Adam。
import torch import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset def train_model(model, train_x, train_y, epochs=30, batch_size=32, lr=1e-3): device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) train_dataset = TensorDataset(torch.from_numpy(train_x), torch.from_numpy(train_y)) train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4) optimizer = optim.Adam(model.parameters(), lr=lr) criterion = nn.CrossEntropyLoss() for epoch in range(epochs): model.train() total_loss = 0.0 for x_batch, y_batch in train_loader: # 输入是NHWC格式,卷积层需要NCHW,所以做一次维度交换 x_batch = x_batch.permute(0, 3, 1, 2).to(device) y_batch = y_batch.long().to(device) optimizer.zero_grad() outputs = model(x_batch) loss = criterion(outputs, y_batch) loss.backward() optimizer.step() total_loss += loss.item() avg_loss = total_loss / len(train_loader) print(f'Epoch [{epoch+1}/{epochs}], Loss: {avg_loss:.4f}')epochs设30是一个起步值,样本量小的时候20轮就能收敛,样本量达到几万时可能需要50轮以上。判断标准是验证loss开始回升或长时间不再下降,而不是训练loss是不是趋近于0。batch_size取32在大多数8GB显存的显卡上都能跑动,如果显存溢出就降到16,或者把切片尺寸从128缩到64。num_workers=4表示用4个进程预加载数据,这个参数在Windows上可能会因为多进程启动慢而报错,Linux环境下问题不大。permute那行尤其重要——训练数据在预处理阶段是按h、w、c排列的,而PyTorch的卷积层要求c、h、w,忘记转置会出现通道错乱,典型表现是loss刚开始下降很慢,然后直接nan。
checkpoint保存是训练脚本的必备环节。训练结束后要把模型权重存下来,推理脚本再加载,常见做法是torch.save(model.state_dict(), 'cnn_landsat.pth'),加载时先实例化相同结构的网络再load_state_dict。不要图省事把整个模型对象存下来,后续修改网络结构或跨机器部署时容易遇到版本兼容问题。
3.3 损失函数、优化器与学习率:训练不收敛时先改哪个
损失函数和优化器的选择在遥感分类任务里其实有固定的套路,不需要做太多花活。多分类任务用交叉熵损失是公认的默认选项,它把模型输出的类别概率和真实标签之间的距离作为优化目标,梯度计算简单稳定。优化器用Adam,初始学习率1e-3,这个组合在绝大多数小规模遥感分类任务上都能快速收敛到不错的位置。如果训练过程出现loss来回震荡或者直接不下降,优先检查数据归一化和标签对齐,而不是急着改参数,这是我跑了多轮项目后的血泪经验。
类别不平衡是Landsat分类里绕不开的问题。某区域如果大部分是农田,少部分是水体,那网络天然会倾向于把不确定的像素判成农田,因为这样能让整体loss最小化。解决办法是给交叉熵加类别权重,让少数类样本的梯度贡献变高。权重设置的基本公式是总样本数除以类别数再除以该类别样本数,通俗说就是样本越多权重越小、样本越少权重越大。
def get_class_weights(train_y, num_classes): # 按类别统计样本频率,再用中位数频率平衡法计算loss权重 counts = np.bincount(train_y.flatten(), minlength=num_classes).astype(np.float32) freq = counts / counts.sum() median_freq = np.median(freq[freq > 0]) weights = median_freq / (freq + 1e-8) return torch.from_numpy(weights).float() criterion = nn.CrossEntropyLoss(weight=get_class_weights(train_y, num_classes))学习率调度在训练后期能给精度带来明显提升。我一般会在训练脚本里加一个ReduceLROnPlateau,当验证loss连续3个epoch没有下降时把学习率乘以0.1,这个方法比固定epoch后手动降学习率更省心。需要注意的是,Adam的适应性学习率机制本身已经做了很多调整,前期不收敛时不要反复调大初始学习率,那只会让loss在前期波动更大。先把数据问题排除干净,再考虑换优化器或调调度策略。
4. 模型评估与全图推理:混淆矩阵、Kappa系数和Landsat专题图生成
4.1 混淆矩阵与Kappa系数:分类精度不是只有一个数字
地物分类项目里,只看一个总体精度数字很容易被表面的华丽数据骗到。最常见的例子是某区域95%的面积是农田,模型把所有像素都标成农田,总体精度直接到95%,但这个模型实际上什么都没学会。所以源码里评估部分一定要有混淆矩阵和Kappa系数。混淆矩阵的每一行代表真实类别,每一列代表预测类别,对角线上的值才是正确分类的样本数;Kappa系数则衡量了分类结果和随机分类相比的改善程度,数值到0.75以上才算基本可靠。
from sklearn.metrics import confusion_matrix, classification_report, cohen_kappa_score def evaluate_model(model, val_x, val_y, class_names): device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model.eval() preds = [] with torch.no_grad(): for i in range(0, len(val_x), 64): batch = torch.from_numpy(val_x[i:i + 64]).permute(0, 3, 1, 2).to(device) out = model(batch) preds.extend(torch.argmax(out, dim=1).cpu().numpy()) cm = confusion_matrix(val_y, preds) kappa = cohen_kappa_score(val_y, preds) report = classification_report(val_y, preds, target_names=class_names, digits=3) return cm, kappa, report评估脚本里有几个隐含的雷要注意。预测时要用torch.no_grad()关闭梯度计算,否则显存会被不断累积的计算图撑爆;每批64个样本是为了避免一次性把整个验证集都塞进显存。classification_report里除去总体精度,用户精度和生产者精度才是判断每个类别表现的关键。生产者精度对应漏分误差,是某个真实类别里有多少被正确识别了;用户精度对应错分误差,是模型分出来的这个类别里有多少真的是这个类别。比如水体漏分严重时生产者精度会很低,裸地大面积错分成建筑时用户精度会很低。
4.2 全图滑动窗口推理:重叠预测与接缝消除
模型验证通过后,真正的考验是对整景Landsat影像做推理出图。如果直接用模型处理一整幅数千乘数千像素的影像,显存和内存都会瞬间爆掉,所以推理阶段同样要用滑动窗口的方式逐块预测再拼接。这个步骤看着和切片训练差不多,但有两个额外问题:拼接接缝和推理效率。接缝问题的典型表现是分类结果图上出现规则的棋盘格状错位,原因是窗口边缘的预测置信度比中心低。解决方法是让窗口重叠,同一位置被预测多次,最终按投票或者平均决定类别。
def predict_full_image(model, tiff_path, patch_size=128, stride=64, device='cuda'): with rasterio.open(tiff_path) as src: bands = src.read([2, 3, 4, 5, 6, 7]).astype(np.float32) # 注意:归一化参数必须来自训练时的全局统计量 bands_norm = (bands - train_mean) / train_std h, w = bands.shape[1], bands.shape[2] result = np.zeros((h, w), dtype=np.uint8) count = np.zeros((h, w), dtype=np.int16) model.eval() with torch.no_grad(): for i in range(0, h - patch_size + 1, stride): for j in range(0, w - patch_size + 1, stride): patch = bands_norm[:, i:i + patch_size, j:j + patch_size] patch_tensor = torch.from_numpy(patch).unsqueeze(0).to(device) pred = torch.argmax(model(patch_tensor), dim=1).squeeze().cpu().numpy() result[i:i + patch_size, j:j + patch_size] += pred count[i:i + patch_size, j:j + patch_size] += 1 # 重叠区域取多数投票结果,避免接缝错位 result = (result / np.maximum(count, 1)).astype(np.uint8) return resultstride取patch_size的一半时,每个像素会被预测4次,接缝两侧的预测误差会被投票机制平均掉。如果嫌推理太慢,可以把stride放大到和patch_size相同,速度提升明显但可能出现细线状接缝;更优雅的优化方案是把多张切片堆成一个batch并行推理,我在实际项目中常用batch_size=16的滑动窗口,推理速度提升接近10倍。推理脚本最需要注意的是归一化参数,train_mean和train_std必须是训练集里统计出来的全局变量,如果换到另一景影像上用完全相同的参数,大概率会在某些区域产生系统性错分。
4.3 分类后处理与制图:去掉椒盐噪声,输出带坐标的专题图
模型直接输出的分类结果图上通常布满椒盐噪声——单个像素随机被分到完全不同的类别,这种细碎的错误在图面上表现为密密麻麻的杂点,视觉上非常难看。解决方法是做多数滤波:对每个像素取它周围邻域内出现频率最高的类别作为新值,窗口大小一般取3乘3或5乘5。注意不能用中值滤波,因为类别编号只是名义变量,不是有数学意义的数值,中值滤波会把不存在的类别编号算出来。
类别数据里另一种常见问题是孤立小图斑。一块面积只有几个像素的水体夹在农田中间,可能是真实的小水塘,也可能是误判的噪声。常见做法里有一个最小图斑面积阈值处理,比如把小于20个像素且与周围类别不一致的区域合并到相邻的最大类别中,这个操作可以用连通域分析来实现。
from skimage.filters.rank import majority from skimage.morphology import disk # 对分类结果做5x5邻域多数滤波,消除椒盐噪声 filtered = majority(result.astype(np.uint8), disk(2)) # 把结果写回GeoTIFF,保留原来的坐标参考和地理范围 with rasterio.open(tiff_path) as src: profile = src.profile profile.update(dtype=rasterio.uint8, count=1) with rasterio.open('classified.tif', 'w', **profile) as dst: dst.write(filtered, 1)写回GeoTIFF时最容易丢的就是坐标参考信息。理想状态下应该直接复制原始影像的profile文件,里面包含了投影坐标系、仿射变换参数和像元大小等关键元数据,自动沿用原影像的空间属性。如果分类结果实际输出的地理范围相差了几十米,往往就是在这一步把坐标信息写丢了。图面展示时给每个类别配固定颜色是一个好习惯:水体蓝色、植被绿色、裸地棕色、建筑灰色、阴影深灰,这样图一出来就能直接做质量检查。
5. 避坑指南:Landsat地物分类项目里五个高发踩坑记录
5.1 训练loss不降:先查波段顺序而不是调学习率
现象:模型训练到十几个epoch,loss值一直在0.6到0.7之间来回震荡,验证精度稳定在30%上下,和随机猜测差不多。换更小的学习率、换不同优化器都毫无起色。
原因:细查下来是波段顺序搞错了。数据源给的GeoTIFF波段排列里,下标4对应的接近红外而不是短波红外,但读取代码按固定下标把红波段和近红外波段的位置对调了。模型看到的近红外数据被当成了红光,植被的光谱特征完全扭曲,网络当然学不到任何有效规律。
解决:写代码前先打印rasterio读取后每个波段的统计特征和对应波长信息,和已知Landsat波段定义对照。更稳妥的做法是按波段名称而不是下标读取,或者在数据预处理阶段就把波段顺序固定成源码约定的排列,并加一个断言检查输入张量的通道数和预期一致。
5.2 换一景影像预测就崩:辐射归一化参数不能跨景迁移
现象:在一景影像上训练出的模型,验证集精度达到88%,换到相邻区域的另一景Landsat影像做全图推理,分类结果出现大面积系统性错分,原先的水体区域变成了裸地,植被区域变成了农田。
原因:两景影像的成像时间不同,大气条件和太阳高度角都有差异,训练集的归一化参数来自特定的辐射分布,直接套到新影像上等于把数据分布硬生生扭曲了。特别是当训练影像和预测影像来自不同季节时,这种辐射差异会放大到模型无法容忍的程度。
解决:推理前先对目标影像单独统计每个波段的均值和标准差,再做标准化。更根本的做法是在训练数据里混入多个时相、多个区域的影像,让模型见过更多的辐射变化。如果条件允许,直接使用表面反射率产品能从数据源头缩小这种差异。
5.3 水体被完全漏分:类别不平衡让交叉熵失效
现象:模型总体精度有91%,看起来不错,但查看每个类别的用户精度才发现,水体这一类的召回率只有2%,几乎所有水体都被分成了农田或阴影。
原因:训练集里水体样本占比不到1%,交叉熵损失被农田这类多数类别主导。模型只要把所有像素都判成农田,整体loss就能降低很多,水体那部分微小占比的损失根本不足以推动权重朝识别水体方向更新。
解决:给交叉熵损失加类别权重,权重按中位数频率平衡法计算。也可以对水体样本做过采样,在每次迭代中保证每个batch里都有一定比例的少数类样本。我一般在源码里把这两个方法同时用上,精度提升非常明显。
5.4 训练中断显存溢出:batch_size和切片尺寸的取舍
现象:训练到中途直接报显存溢出错误,进程被杀掉,之前几个epoch的权重白训。把batch_size从32降到16后能跑,但速度明显变慢,训练时间拉长。
原因:128乘128乘6的输入张量在32的batch下占用的显存远高于自然图像分类任务,因为遥感影像的通道数是6而不是3,加上卷积层中间产生的特征图也要占用显存,8GB的显卡在这种配置下捉襟见肘。
解决:降低batch_size是最直接的办法,但会让训练时间变长。更好的组合是保持batch_size不变,把patch_size从128缩到64,样本数量不变但单样本显存占用降为原来的四分之一。如果还想进一步压缩显存,可以加一句torch.backends.cudnn.benchmark优化卷积计算的显存分配策略。
5.5 验证精度虚高:验证集和训练集混进了同一块地
现象:训练日志里验证精度一路高歌到95%,模型评估报告各种指标都无可挑剔,但拿到另一景独立影像上一试,精度直接掉到60%以下。
原因:数据划分时把全部切片混在一起随机打散了。Landsat影像里相邻切片的纹理和光谱高度相似,同一个农田地块被同时切进了训练集和验证集,模型其实早就见过这些验证样本,评估结果自然虚高。
解决:先按地理区块划分数据,保证训练集、验证集、测试集来自不同空间位置。每景影像按四象限切分,取三个象限做训练,一个象限做验证,测试集则使用完全独立的一景影像。空间独立性远比样本数量重要,这是我在这类项目里最大的后悔药总结。
6. 进阶方向:多尺度融合与迁移学习还能怎么提升地物分类精度
6.1 多尺度特征与空洞卷积
如果基础模型已经能稳定跑到85%以上精度,再往上提升就值得尝试多尺度思路。Landsat影像上的地物天然存在尺度差异:农田地块有几公里大小,但田埂和零星水体只有几米到几十米。单个patch_size只能覆盖一个尺度范围,一个很自然的做法是训练两个模型,一个输入64乘64切片捕捉细节,一个输入128乘128切片捕捉上下文,推理时把两个模型的预测结果按权重平均。空洞卷积是另一个更优雅的方案,在不增加参数量的前提下扩大感受野,只需把最后一层卷积的dilation参数从1改成2或3,网络就能看到更大的空间范围而不损失分辨率。
6.2 迁移学习和多时相堆叠
样本量不足是遥感分类项目最常见的瓶颈,这时候迁移学习能救场。把在ImageNet上预训练好的ResNet前几层拿来复用,把第一层卷积的输入通道从3改为6,新加的3个通道用随机初始化,其余的权重全部沿用预训练结果,只微调分类头。遥感影像的纹理特征和自然图像有部分共性,这种迁移往往能让模型在少量样本下快速收敛。
多时相堆叠则是更贴近Landsat数据特性的做法:把不同季节的影像按通道方向拼接,比如春季、夏季、秋季各取6个波段,拼成18通道输入。这样模型能学到生长季变化特征,农田和自然植被的分类精度往往比单时相高出不少。某开发者在模拟项目X里用这个方法把总体精度从86%推到了92%,代价只是训练时间增加两倍。
从一个容易忽略的细节说起:我自己的习惯是,不管训练结果看起来多漂亮,拿到推理图后做的第一件事永远是打开原始影像和分类结果叠加对比,目视检查主要地物的边界是否对齐。坐标参考、投影、影像配准这些环节如果出错,模型的预测再准也只是黑匣子里打转。养成这个习惯之后,我在后续多个遥感项目里都没有再犯过数据错位的低级错误。希望帮到你。
本文还有配套的精品资源,点击获取