
简介面向遥感、地理信息及计算机视觉研究者这套基于CNN的Landsat影像地物分类Python源码包针对传统分类方法依赖手工特征、精度与鲁棒性有限的问题提供从样本制作到模型预测的完整工程实现。压缩包共10个文件约14.49MB包含3个Python脚本、1个已训练好的HDF5格式模型、2幅tif样例影像及配套xml/tfw投影信息文件、README说明等脚本依次实现影像切片、模型训练、新数据预测三个环节h5模型可被直接加载无需重新训练即可输出地物分类结果。已有181人学习下载适用于土地覆盖变化检测、农作物分类、城市规划等任务的快速验证。借助TensorFlow/PyTorch与OpenCV等生态用户可复现完整流程也可基于现有代码二次开发调整网络结构与样本参数以适配自己的数据对科研与教学均有较高参考价值。1. 遥感地物分类为什么绕不开 CNN从一景 Landsat 影像说起手里拿到一景 Landsat 8 OLI 影像9 个波段十几个 GB你要回答的问题其实很简单这块地是农田、树林还是建筑区逐像素打上类别标签就是地物分类。传统方法用支持向量机或随机森林靠人工设计的波段指数和纹理特征在小范围测试区能到 85% 的精度换一景影像、换一个季节精度立刻掉下来。CNN 解决的就是特征自动提取这件事它把波段间的光谱关系和空间邻域的纹理关系一起学习分类结果比传统方法稳定得多尤其在建筑区和不透水面这类光谱混淆严重的类别上优势很明显。这个标题里的东西翻译成工程语言就是一套用 Python 写的、基于 CNN 深度学习的 Landsat 影像逐像素分类代码附带一组已经训练好的模型权重。适合谁用做土地利用变化监测的科研人员、GIS 相关专业的学生、以及刚接触遥感深度学习但不想从零开始训模型的从业者。你不需要精通 PyTorch 底层原理但得知道模型输入什么、输出什么以及哪些参数不能乱调。下面从数据准备开始一步步把这套流程讲透。2. 模型吃的是影像块而不是整幅图波段整理、标注与滑窗切块的落地做法2.1 拿到 Landsat 数据之后先做波段整理而不是直接扔给模型Landsat 8 OLI 有 9 个波段但做地物分类时没人把 9 个波段全用上。热红外波段空间分辨率是 100 米重采样到 30 米会和可见光波段产生配准误差沿海波段噪声大对常见地物类别贡献有限。我一般只用多光谱的 6 个波段蓝B2、绿B3、红B4、近红外B5、短波红外 1B6、短波红外 2B7。建筑区和裸土在短波红外上差异明显水体在近红外上吸收强烈这 6 个波段组合已经覆盖了绝大多数地物分类需求。预处理阶段有两个容易忽略的点。第一Landsat 数据默认存的是 DN 值Digital Number不同影像之间的 DN 值范围受太阳高度角和大气条件影响不一致直接拿去做训练会让模型学到这一景影像的特殊亮度而不是地物的光谱特征。我通常转成表面反射率Surface Reflectance产品或者至少做一步简单的辐射定标。第二每个波段在代码里读取之后要做统一的裁剪和重采样保证所有波段的行列数一致。这一步用 rasterio 处理比 GDAL 命令行更顺手直接以数组形式读进来方便后续堆叠成多通道输入。import rasterio import numpy as np band_paths [B2.TIF, B3.TIF, B4.TIF, B5.TIF, B6.TIF, B7.TIF] bands [] with rasterio.open(band_paths[0]) as src: meta src.meta.copy() # 记录投影、变换参数后续写分类结果要用 height, width src.height, src.width for path in band_paths: with rasterio.open(path) as src: arr src.read(1).astype(np.float32) # Landsat 的 nodata 值通常是 0直接用 0 填充会对训练产生干扰 arr[arr 0] np.nan bands.append(arr) stack np.stack(bands, axis-1) # 形状 (height, width, 6)这段代码做的是把 6 个单波段文件读成数组并按最后一维堆叠得到 (height, width, 6) 的数据立方体。注意 0 值被替换成了 nan因为在影像边缘和云掩膜区域DN 值为 0 不代表地物真实反射率是 0保留它会让模型学到一个黑色区域的错误类别。后续切块时如果某个块里 nan 占比过高直接跳过不让它进训练集。2.2 训练样本标注ROI 标注与数据增强的常见做法地物分类的标签制作是个体力活。常见做法是在 QGIS 里加载影像手动画 ROIRegion of Interest多边形给每个多边形指定类别比如建设用地耕地林地水体裸土然后栅格化成与影像同尺寸的标签图每个像素的值是对应的类别编号。你也可以用 ESA WorldCover 或 FROM-GLC 这类公开产品当初始标签再手动修正明显错分的区域这个半自动标注的路径在时间紧的时候很实用。这里有个专业上的提醒Landsat 是 30 米分辨率一个像素覆盖 30×30 米的地面范围所以标注的最小单元是一个像素。你在 QGIS 里画多边形时边界要尽量对齐像素网格否则标签边缘会包含混合像元。混合像元是遥感分类里绕不开的话题——建筑物阴影、水体边缘、农田边界这些位置一个像素里可能有多种地物模型在小块训练数据上能记住但到整幅影像推理时就会在边界处输出椒盐状的噪声。应对方法是在训练时做标签平滑Label Smoothing或者干脆在评估时忽略边界像素后者更省事。数据增强对遥感影像同样有效。除了常规的随机翻转、旋转 90 度我还会加两样随机亮度扰动和随机波段缩放。前者模拟不同季节太阳高度角差异后者模拟大气条件变化等效于扩大训练集的光谱多样性。import random def augment(image, label): # 随机 90 度旋转系列规则共 8 种变换 k random.randint(0, 3) image np.rot90(image, k, axes(0, 1)) label np.rot90(label, k, axes(0, 1)) if random.random() 0.5: image np.fliplr(image) label np.fliplr(label) # 光谱扰动对每个波段乘一个近似于 1 的系数 scale np.random.uniform(0.9, 1.1, size(1, 1, image.shape[-1])) image image * scale # 类别标签不做插值保持整数 return np.clip(image, 0, 1), label.astype(np.int64)augment 函数放在 DataLoader 里调用每次迭代随机用掉一组变换。scale 系数的范围 0.9 到 1.1 是我常用的设置再大容易让光谱特征失真再小起不到增强效果。注意标签图旋转时不能做插值所以用 np.rot90 而不是 scipy 的旋转函数保持像素值严格对应。2.3 用 Python 把整景影像切成训练块滑窗采样与标签对齐CNN 不能直接把一整景 Landsat 影像塞进显存常规做法是切成 128×128 或 256×256 的影像块patch作为训练样本。之所以不切更小是因为地物分类需要足够的空间上下文一个像素是建筑区还是裸土看它自身的光谱可能区分不开但看它周围 128 个像素范围内的纹理和邻域关系就清楚了。我的经验是 128×128 是效率和精度的平衡点显存不够再用 96×96不要小于 64×64。滑窗采样要考虑步长。如果训练块之间有重叠相当于对同一地物采样了多次数据量变大但信息重复度高如果完全不重叠对大面积类别比如连片的森林会采出大量几乎一样的块。我的做法是同时保证两点采样步长取 patch 尺寸的一半让相邻块有 50% 重叠对样本量最大的类别设置一个上限避免模型被单一类别淹没。import numpy as np from osgeo import gdal def generate_patches(image, label, patch_size128, stride64, max_per_class500): h, w image.shape[:2] patches_img, patches_lbl [], [] class_count {} for y in range(0, h - patch_size 1, stride): for x in range(0, w - patch_size 1, stride): img_patch image[y:ypatch_size, x:xpatch_size] lbl_patch label[y:ypatch_size, x:xpatch_size] # 跳过包含无效值超过 10% 的块 if np.isnan(img_patch).sum() 0.1 * patch_size * patch_size: continue # 统计标签分布控制每个类别的块数量 unique, counts np.unique(lbl_patch, return_countsTrue) dominant unique[np.argmax(counts)] class_count[dominant] class_count.get(dominant, 0) 1 if class_count[dominant] max_per_class: continue # 归一化到 [0, 1]训练更稳定 img_patch (img_patch - np.nanmin(img_patch)) / ( np.nanmax(img_patch) - np.nanmin(img_patch) 1e-6) patches_img.append(img_patch) patches_lbl.append(lbl_patch) return np.stack(patches_img), np.stack(patches_lbl)这个采样函数里最关键的是类别数量上限控制。Landsat 分类数据里林地和农田经常占到影像面积的 70% 以上如果不设上限模型看到的就是全是林地的训练集建设用地和水体几乎没有样本。max_per_class 按类别统计主导类别的块数超过 500 就不再采样这个值可以根据分类类别数调整。归一化用了每个块独立的 min-max 拉伸比全图统一归一化更能适应不同光照条件下的影像块。3. 用 PyTorch 搭 U-Net 模型遥感影像逐像素分类的核心结构3.1 为什么选 U-Net 而不是普通分类网络标题里写的是 CNN但 CNN 是一个大的技术范畴到了遥感影像逐像素分类这个具体任务我首选 U-Net 结构。原因有两个一是 U-Net 的编码器-解码器结构天然适合输入整块影像、输出同尺寸标签的密集预测任务不需要像图像分类那样在末端接全连接层再上采样避免了细节丢失二是 U-Net 的跳跃连接skip connection把编码器的细节特征和解码器的语义特征拼在一起对地物边界保持有奇效。普通分类网络最后得到的特征图只有输入的 1/32 大小直接放大回原尺寸做逐像素分类边界会糊成一团。遥感地物分类还有一个特殊性类别之间存在明显的空间尺度差异。一栋建筑物可能只占 30×30 米的一个像素而一片森林可能是几万个像素连在一起。U-Net 的多尺度特征融合机制让模型同时保留小目标的局部细节和大目标的全局上下文比单纯加深网络层数或者堆 ResNet 结构更实用。3.2 U-Net 模型代码编码器-解码器的核心实现模型代码不复杂核心是两个卷积块组成的编码器每层下采样一次同时把通道数翻倍到最底层之后开始上采样每一步和对应编码器层的特征图拼接。输入通道数设为 6对应 6 个 Landsat 波段输出通道数设为类别数。import torch import torch.nn as nn import torch.nn.functional as F class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1, biasFalse), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1, biasFalse), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), ) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, in_channels6, num_classes6, features[64, 128, 256, 512]): super().__init__() self.downs nn.ModuleList() self.ups nn.ModuleList() self.pool nn.MaxPool2d(2) for f in features: self.downs.append(DoubleConv(in_channels, f)) in_channels f self.bottleneck DoubleConv(features[-1], features[-1] * 2) for f in reversed(features): self.ups.append(nn.ConvTranspose2d(f * 2, f, kernel_size2, stride2)) self.ups.append(DoubleConv(f * 2, f)) self.final nn.Conv2d(features[0], num_classes, kernel_size1) def forward(self, x): skip_connections [] for down in self.downs: x down(x) skip_connections.append(x) x self.pool(x) x self.bottleneck(x) skip_connections skip_connections[::-1] for idx in range(0, len(self.ups), 2): x self.ups[idx](x) skip skip_connections[idx // 2] # 处理因下采样产生的尺寸不一致裁剪到相同大小 if x.shape ! skip.shape: x F.interpolate(x, sizeskip.shape[2:], modebilinear, align_cornersTrue) x torch.cat((skip, x), dim1) x self.ups[idx 1](x) return self.final(x)这段代码里有几个参数需要关注。features 列表控制每层的通道数[64, 128, 256, 512] 是内存和精度的折中显存低于 6GB 时可以缩成 [32, 64, 128, 256]。输入大小必须是 16 的整数倍因为这里下采样了 4 次。forward 里做了一次尺寸对齐的 F.interpolate 处理这是为了应对非严格倍数尺寸的输入否则拼接时会直接报错。卷积层设置了 biasFalse配合 BatchNorm 使用——加了 bias 会和 BatchNorm 的均值偏移功能重复反而影响收敛。3.3 损失函数与评估指标交叉熵、IoU 与 Kappa 系数的取舍模型输出的是每个像素在各个类别上的分数形状是 (batch, num_classes, height, width)。训练时最常用的是交叉熵损失但遥感分类里的类别不平衡问题会直接让交叉熵偏向大类别。假设建设用地只占训练数据的 5%模型只要把所有像素都预测成林地就能得到 90% 以上的准确率但这显然不是你要的结果。解决方案有两个层面。第一个层面是加权交叉熵按类别样本比例的倒数给损失加权小类别权重高、大类别权重低。第二个层面是 Lovász-Softmax 损失它直接优化 IoU 而不是像素准确率对分割任务更友好缺点是收敛速度稍慢。我通常的做法是两者加权求和交叉熵保证训练稳定Lovász 保证最终 IoU 更高。评估指标千万别只看 accuracy要同时看各类别的 IoU 和 Kappa 系数Kappa 能反映分类结果和真实标签的一致性是否优于随机猜测这是遥感论文审稿人最常盯的指标。class_weights torch.tensor([1.0, 1.0, 2.0, 3.0, 1.5, 2.5]) # 按类别逆频率估算 criterion nn.CrossEntropyLoss(weightclass_weights)class_weights 的数值要先统计训练标签里每个类别的像素占比再用总样本数除以各类别样本数做归一化。不要凭感觉写否则小类别可能矫枉过正把大量背景像素错分成小类别。4. 训练与推理从能跑到跑得稳的关键参数控制4.1 训练脚本骨架数据加载、训练循环与模型保存训练脚本的框架和普通图像分割任务区别不大核心区别在数据读取上。每个训练样本是 (影像块, 标签块) 的配对数据量通常是几千到几万个块全部加载进内存不现实需要写一个 Dataset 类在每次迭代时按索引读取。from torch.utils.data import Dataset, DataLoader class LandsatDataset(Dataset): def __init__(self, images, labels): self.images images # (N, 128, 128, 6) self.labels labels # (N, 128, 128) def __len__(self): return len(self.images) def __getitem__(self, idx): img torch.from_numpy(self.images[idx]).permute(2, 0, 1).float() lbl torch.from_numpy(self.labels[idx]).long() if self.augment and random.random() 0.5: img, lbl augment(img, lbl) return img, lbl dataset LandsatDataset(train_images, train_labels) loader DataLoader(dataset, batch_size16, shuffleTrue, num_workers4)batch_size 的选择取决于显存大小。6 波段输入、128×128 尺寸的样本batch_size16 大约需要 8GB 显存训练时如果报 CUDA out of memory先减半而不是去改模型结构。num_workers 设为 4 到 8 可以加快数据读取但 Windows 下如果数据增强函数里用了 numpy 的随机数可能会遇到多进程重复随机的问题建议把 seed 固定或者在增强函数内部使用独立的随机数生成器。训练循环本身保持常规写法关键点是学习率的设置和模型的保存策略。Adam 优化器初始学习率 1e-4 是我的常用起点配合 ReduceLROnPlateau 调度器当验证集损失连续 5 个 epoch 不降时把学习率乘以 0.5。保存模型时不要只存最后一轮的权重要跟踪验证集上 IoU 最高的那个 epoch存成 best_model.pth这就是标题里说的训练好的模型。加载时用 torch.load 加 map_location 参数控制设备避免在无 GPU 的机器上加载报错。4.2 用训练好的模型做推理整幅影像的预测与拼接训练得到模型权重后要对一整景 Landsat 影像做预测。推理阶段的处理方式和训练时一样把影像切成 128×128 的块逐块预测再把结果拼接回原图尺寸。但这里有个训练时没有的问题——块与块的边界处预测结果会出现接缝因为每个块边缘的上下文信息和完整影像中该位置的真实上下文不同。model.eval() with torch.no_grad(): for y in range(0, img_h - patch_size 1, stride): for x in range(0, img_w - patch_size 1, stride): patch image[y:ypatch_size, x:xpatch_size] patch_tensor torch.from_numpy(patch).permute(2, 0, 1).unsqueeze(0).float() logits model(patch_tensor) pred torch.argmax(logits, dim1).squeeze().cpu().numpy() output[y:ypatch_size, x:xpatch_size] pred推理时要用 model.eval() 关闭 dropout 和 BatchNorm 的训练行为否则同样的输入每次会得到不同结果这是新手最常犯的错。拼接到 output 数组时注意覆盖顺序如果设置了 stride 小于 patch_size后面预测的块会覆盖前面块的边缘部分这其实是一种隐式的多数投票机制能在一定程度上平滑边界噪声。推理速度方面一景 7000×7000 像素的 Landsat 影像切成 128 块逐个预测在 RTX 3060 上大约需要 5 到 8 分钟属于可接受范围。4.3 模型训崩了怎么办学习率、归一化与类别不均衡的诊断顺序训练过程中 Loss 不降或者验证集指标振荡这类问题几乎人人都遇到过。按以下顺序排查能省下大量时间。第一检查输入数据有没有做归一化。Landsat 的反射率数据范围是 0 到 1但如果直接从 DN 值读进来范围 0 到 65535没做归一化梯度更新会非常不稳定表现为 Loss 在初期剧烈震荡。解决办法是把输入归一化到 [0,1]如果用的是预训练模型要按模型的归一化统计值来而不是重新算一遍。第二检查学习率。1e-4 的 Adam 学习率对 U-Net 来说一般不会出错但如果 Loss 在前几个 step 内出现 inf 或 nan大概率是学习率过大或者输入里有异常值先降到 1e-5 试试。第三检查类别权重。加权交叉熵的权重如果设置不当模型会陷入只学大类别的局部最优验证集 IoU 涨到某个值后停滞。我的做法是先不加类别权重训练一轮确认 Loss 能正常下降再加上权重微调。第四检查 BatchNorm 的 batch_size。batch_size2 以下时 BatchNorm 的统计量波动很大训练和验证效果差异明显这时候要么加大 batch_size要么改用 GroupNorm。5. 遥感分类的避坑手册五个最容易翻车的地方5.1 影像和标签错位投影不一致导致的正确率低于随机现象训练集准确率很高验证集准确率也不错但整幅影像一推理分类结果和真实地物明显错位建筑区整体偏移了几个像素。原因Landsat 各波段文件之间、Landsat 影像和标签栅格之间的投影或地理变换参数不一致读取时没有做对齐重采样导致影像像素和标签像素在地理位置上差了几十米甚至上百米。解决在使用 GDAL 或 rasterio 读取标签文件时不仅要确保投影坐标系相同比如都是 WGS84 UTM 50N还要确认 transform 参数里的分辨率、旋转角度、左上角坐标完全一致。最稳妥的方法是用 gdal.Warp 把标签重采样到影像的网格上用最近邻插值保持类别值不变然后对比几个已知地物点如道路交叉口在影像和标签上的坐标是否吻合。5.2 归一化方式不对每个 patch 单独拉伸导致的亮度断层现象推理结果里出现了明显的方块状接缝每个 128×128 的块内部色调一致块与块之间却有一道明显的边界线。原因这就是第 2 章提到的逐块 min-max 归一化带来的后遗症。每个块的光照条件和地物组成不同min 和 max 差异悬殊同一个地物在这个块里被拉伸成 0.8在另一个块里被拉伸成 0.2模型输出自然不一致。解决推理阶段不要对每个块独立归一化而是要么用训练集统计好的全局均值和标准差做标准化要么把整幅影像一次性读入内存用全图的 min 和 max 做归一化后再切块。每块独立归一化这个操作只允许出现在训练阶段作为数据增强的一种形式推理时用了就是在给自己挖坑。5.3 忽略云和云影模型把云判成了建设用地现象分类结果的白色区域大面积分布于影像的云覆盖处这些区域被预测成了建设用地或裸土。原因云在可见光波段的反射率极高光谱特征和建筑屋面的金属顶、混凝土表面非常接近。如果训练数据里的云区域没有掩膜而推理影像上有云模型必然会把云分为高亮地物类别。解决训练和推理前都要做云掩膜。Landsat 的 QA 波段Quality Assessment里自带了云和云影的标记位读取后把云区域的标签设为 ignore_index在交叉熵损失里通过 ignore_index 参数跳过这些像素。如果 QA 波段被裁剪掉了可以用 Fmask 工具或简单的波段阈值蓝光 0.3 且近红外 0.25先粗筛一遍。5.4 训练集中水体样本太少模型学不会水体的光谱特征现象整幅影像里河流和湖泊区域被分成了阴影或森林水体类别几乎没被预测出来。原因Landsat 影像中水体占比通常不高训练集中水体样本不足且水体在近红外和短波红外波段上吸收强烈和山体阴影的光谱曲线有相似性模型无法区分。解决在标注阶段额外补充水体样本或者在损失函数里提高水体类别的权重。另一个有效的办法是加入 NDWI 指数作为额外的输入通道水体在 NDWI 上显著为正模型可以非常容易地学会这个特征。把 NDWI 加到输入里不是作弊在很多实操项目中额外输入人工设计的指数特征能显著降低样本需求尤其是对阴影和水体的区分。5.5 模型保存与加载的黑匣子state_dict 和模型结构不匹配现象拿到一个训练好的 .pth 文件加载时报错 Missing key(s) in state_dict: encoder.0.conv.0.weight或者加载成功但推理结果全是噪声。原因保存的只是模型的权重字典state_dict它和模型结构的每一层一一对应。如果加载时模型的层名字对不上比如输入通道数从 6 改成了 4或者类别数改了PyTorch 就会报 missing keys如果权重是随机初始化的模型上叠加了部分预训练权重输出就会是噪声。解决保存模型时把模型结构相关的信息一并存进字典里例如输入通道数、类别数、归一化参数加载时先根据这些信息重新实例化模型再加载权重。这个习惯能省掉大量因为拿到模型但不知道输入格式而浪费的时间。checkpoint { state_dict: model.state_dict(), in_channels: 6, num_classes: 6, features: [64, 128, 256, 512], global_mean: global_mean, # 训练集的波段均值 global_std: global_std, # 训练集的波段标准差 } torch.save(checkpoint, best_model.pth)6. 分类结果的精度验证与推理加速两个值得多花时间的技巧模型训练完、推理图也出了距离能用还差最后两步量化评估分类精度和消除推理结果的碎斑噪声。先看精度评估只报一个总准确率是不专业的遥感地物分类领域的惯例是报每类 IoU、总体 IoU 和 Kappa 系数。IoU 的意义是预测区域和真实区域的交并比对边界偏移非常敏感比逐像素准确率更能反映分类质量。Kappa 系数大于 0.8 在遥感分类里就算优秀结果0.6 到 0.8 属于可用范围。精度评估代码里有一个容易忽略的细节评估样本的选择。随机抽样验证点在空间上往往聚集在少数几块区域尤其是当影像里地形复杂时。更合理的做法是分层抽样每个类别固定抽样数量并且让样本点之间的空间距离不少于两个像素避免相邻像素的强自相关性导致评估指标虚高。这一点在写论文或做项目验收时尤其重要评审人最爱问的就是你的验证点是怎么选的。from sklearn.metrics import confusion_matrix, cohen_kappa_score y_true valid_labels.flatten() y_pred valid_preds.flatten() cm confusion_matrix(y_true, y_pred) # 每个类别的 IoU 对角线 / (该类真实和预测的并集) iou_per_class np.diag(cm) / ( cm.sum(axis1) cm.sum(axis0) - np.diag(cm) 1e-6) kappa cohen_kappa_score(y_true, y_pred)推理加速方面一个非常实用的技巧是重叠滑窗 中心区域拼接。常规滑窗推理直接取每个块的完整预测结果覆盖到输出图上块与块之间无重叠边界处容易产生条带噪声。改为步长取 patch_size 的一半推理时只保留每个块中心 64×64 的预测区域其余部分丢弃下一个块覆盖相邻区域。这样做相当于对每个像素进行了多次预测结果更平滑噪声大幅减少。代价是推理时间翻倍但用 torch.no_grad() 和半精度推理torch.cuda.amp可以弥补回来。还有一个小技巧是类别后处理。分类结果里经常出现零星的椒盐噪声——单个像素被分成和周围完全不同的类别这在地理学上不合理因为地物通常是有空间连续性的。用 scipy 的 median_filter 对分类结果做一次 3×3 的中值滤波或者用 majority_filter 做众数滤波能有效消除孤立的错分点。注意标签图是离散类别编码而不是 0 到 1 的连续值不能用均值滤波否则会产生不存在的类别值。一路做下来我的感受是遥感 CNN 分类的瓶颈多半不在模型结构而在数据处理的严谨度。一套再好的 U-Net喂进去错位的标签和未归一化的 DN 值结果也不会比随机森林强多少。所以我现在的习惯是每做一景影像的分类先抽几个典型地物点检查影像和标签的对齐情况再跑一轮小规模的快速训练验证数据管线没问题最后才上全量训练。这个流程帮我少翻了很多次车也把调参的玄学成分压缩到最低。希望这篇整理能帮你少走一段弯路让自己的模型真正从能跑走向可信。本文还有配套的精品资源点击获取