
如果你最近开始碰遥感影像分割估计十有八九都会被推荐先跑一遍 LoveDA。可真正上手之后你会发现这套数据集的英文网站要填申请表、下载回来还要折腾目录结构和标签好不容易把数据塞进模型训练时又可能因为标签值偏移和显存问题翻车。我前前后后在这个数据集上踩了不少坑也帮几个师弟师妹排过问题下面这份从下载到训练的完整流程直接照着做就能少走弯路。LoveDA 的全称是 Land-cOver Domain Adaptation是武汉大学团队发布的高分辨率土地覆盖语义分割数据集。它不仅帮你解决“一个分割模型怎么搭”的问题更核心的是它把城市与乡村两类差异巨大的场景放在了一起天然就是为研究跨域语义分割准备的。对于想入门遥感影像语义分割、或者要做无监督域适应UDA的同学来说这是目前最适合用来练手和发论文的数据集之一。我默认你至少会一点 PyTorch知道什么是 dataset、dataloader不需要你精通迁移学习但我会把每一步为什么要这么做讲清楚方便你做选择。1. 先看清 LoveDA 的底细7 个类别、跨域设计和真正难点1.1 数据规模、分辨率和 7 类语义标签LoveDA 提供的是 0.3 米左右空间分辨率的高分遥感影像图像以瓦片形式提供单张尺寸大约 1024×1024 像素。整个数据集大约有 4778 张影像其中训练集 2523 张、验证集 1287 张、测试集 968 张覆盖了中国多个城市的多样化地物场景。关注点是 7 类地物语义标签背景background、建筑物building、道路road、水域water、草地grass、灌丛shrub和耕地cropland。类别编号类别名称典型特征1Background未定义区域、边缘区域或其他零星地类2Building人工建筑屋顶、密集居民区建筑3Road城市主路、乡村道路、裸土路肩4Water河流、湖泊、池塘、水田积水区5Grass公园草地、河岸草地6Shrub低矮灌丛、林地过渡带7Cropland规则田地、大棚、农田田块单看类别数量7 类确实不算多和人造数据集几十上百类没法比。但遥感影像分割的难度从来不只在类别数量而在于类间视觉差异小、类内差异巨大。比如草地和灌丛在高分影像上有时只是纹理疏密不同耕地里面不同作物、不同生育期的颜色千差万别道路穿过乡村土路之后又被两边耕地和裸土包围。这些都会让普通的语义分割模型很头痛也正因如此直接用 LoveDA 验证模型好坏是有说服力的。1.2 为什么“域”设计是 LoveDA 最特别的地方很多新手把 LoveDA 当成普通分割数据集分好 train/val 就直接训练这其实完全浪费了它最有价值的设计。LoveDA 特意把样本组织成城市Urban、乡村Rural和随机Random三种域不同域之间地物形态差异非常大。你在城市域里看到的建筑是密集的高楼和整齐街区道路是柏油路面但在乡村域里建筑物变成了低矮村屋道路可能还是未硬化的土路耕地和灌丛占据了绝对主导地位。这种位移差异正好对应真实遥感业务的痛点一个在上海训练好的模型直接拿到四川农村大概率性能暴跌。所以LoveDA 的官方设定更适合做无监督域适应比如用城市域样本作为 source乡村域或随机域作为 target考察模型在目标域上没有标注时的表现。你如果只是做监督训练也要明白自己验证集上的分数会明显虚高跨域评估才是更真实的水平。1.3 最容易被高估的两个入门误区误区一“7 类太简单了随便跑跑就能 90 mIoU”。实际上LoveDA 上完全监督训练的主流模型 mIoU 一般也就在 55-65 这个区间。原因显而易见全图 1024×1024 的标注图里类别像素数量极度不均衡背景和耕地占了大头建筑物和灌丛只占很小比例mIoU 一平均就把分数拉下来了。误区二“只要把训练集吃透测试集自然没问题”。由于 LoveDA 的测试集没有公开标注而且它和训练集来自不同城市区域模型在训练集上的过拟合会直接暴露在最终测试分数上。这也是为什么我强烈建议你在本地固定一个验证集每轮都看全类别的 IoU不要只盯着总 loss。2. 下载这一步就有坑申请、解压、目录结构和标签值确认2.1 官方申请流程与邮箱选择LoveDA 数据集需要在官方页面填写申请表不是点个按钮就能直接下载的。这些表单项大概包括你的姓名、单位、邮箱和用途说明。我第一次申请时图省事填了个临时邮箱等了三天都没收到回复后来换成机构邮箱第二天就收到下载链接了。建议直接用学校或单位的正式邮箱同时在用途说明里写清楚打算用来做什么比如“用于遥感图像分割模型的研究与对比实验”比光写“research”通过率高不少。收到回复邮件后里面一般会提供下载链接。数据文件整体在几个 GB 级别网速一般的话需要一点时间。下载完不要急着解压到默认路径先检查一下文件大小和压缩包完整性我曾经遇到下载中断但压缩包还能解压出一部分文件的情况训练到一半才发现样本数量不对排查浪费了半天。2.2 解压后的目录结构必须核对的三件事解压完成后标准目录结构应该是这样LoveDA/ ├── Train/ │ ├── Image/ # 训练图像000001.png │ └── Mask/ # 训练标签000001_mask.png ├── Val/ │ ├── Image/ │ └── Mask/ └── Test/ └── Image/ # 只有图像没有公开 Mask第一件要核对的事情是训练图像和标签的文件名是否一一对应。正常情况下训练图像名为000001.png对应的标签图名为000001_mask.png我在用 glob 方式读取时建议直接基于图像文件名去生成 mask 文件名同时做一次存在性检查避免因为某一张图缺 label 导致运行时崩溃。小组命名上没有做好对齐常见做法是把Image路径里的.png替换成_mask.png但这依赖路径结构。我更推荐用 pandas 或单纯的列表推导构建映射表然后打印前 5 条确认别想当然。第二件要核对的是图片尺寸是否统一。LoveDA 名义上是 1024×1024但为了保证边界完整实际解压出来的图像可能有少量尺寸偏差。如果你打算直接喂给模型务必在 dataset 里读取后做形状断言或者统一 resize。否则训练时到了某一张图batch 维度不一致PyTorch 会直接报错。第三件要核对的是测试集。测试集只有Test/Image文件夹没有任何标注。所以本地验证泛化性能时正确做法是从Train里分出一部分样本做验证集或者用官方Val集。我建议直接用官方 Val因为它和 Train 是不同位置的影像更能反映真实泛化水平。2.3 掩码的像素值不是 0 到 6而是 1 到 7这是 LoveDA 新手最容易踩的大坑。很多人在写数据加载代码时会下意识认为七分类的 mask 像素值就是 0 到 6结果训练出来的模型要么 loss 不降要么可视化时发现预测结果整体错位。LoveDA 官方把类别像素值从 1 标到了 7其中 1 是背景7 是耕地。像素值为 0 的区域主要是边界填充或无效区域建议当作 ignore 处理。所以在训练前需要把像素值做一个映射。2.4 掩码用看图软件打开黑乎乎一片如果你解压之后直接用普通看图软件打开mask文件会发现整张图几乎全黑只有极少数亮斑。这不是文件坏了而是像素值 1 到 7 在一张 8 bit 图像里亮度差异太微弱。用 QGIS 或者 Python 里做一次 histogram 拉伸才能看到有颜色的地物标注区域。批量确认标签值分布可以用下面这段代码import numpy as np from PIL import Image mask np.array(Image.open(/path/to/LoveDA/Train/Mask/000001_mask.png)) print(unique values:, np.unique(mask))我第一次检查时看到 unique values 是[0, 1, 2, 3, 4, 5, 6, 7]立刻就明白标签偏移的问题了。这一步不要跳解压后第一时间跑一遍。3. 训练前的冷启动环境、模型选型和一份极简训练代码3.1 选 MMSegmentation 还是自己写 DataLoader面对 LoveDA 训练你第一个要做的决定是用现成框架还是自己写一套小脚本。很多人迷信 MMSegmentation一上来就配置文件调成 LoveDA结果被版本依赖、config 继承、pipeline 定义这些复杂机制卡住了一个星期。我当然承认 MMSegmentation 是好东西它对很多公开数据集都提供了现成 config复现经典模型效率极高。但只要 LoveDA 不在它内置数据集列表里你就得自己写 dataset class 和 config这个学习曲线对新手并不友好。而且 LoveDA 的标签偏移问题在 MMSegmentation 里一般通过reduce_zero_label来配置但这样会把像素值为 0 的区域当成 ignore处理不好还会让背景类从原来的 1 变成 0整个类别顺序乱掉。我的建议很直接第一阶段先用一个几十行的 PyTorch Dataset 子类把数据加载搞明白模型用一个 U-Net 或 DeepLabV3把训练流程跑通确认模型真的能学到东西第二阶段再切换到 MMSegmentation 做消融对比或复现那时候你已经知道每个参数在干什么就不会被框架牵着鼻子走。3.2 环境依赖不需要花里胡哨我推荐的环境组合是 Python 3.10、PyTorch 2.x、CUDA 11.8配合opencv-python、albumentations、timm。如果是 NVIDIA 显卡训练前先确认可用import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果 CUDA 不可用不要开始任何训练先把驱动和 PyTorch 版本对齐。3.3 模型怎么选先跑一个能收敛的再谈 SOTA我在 LoveDA 上最先跑通的模型是 U-Net原因很简单显存占用低、训练速度快、代码不容易出错。但它作为基础 encoder-decoder 架构对大目标和大背景类别表现还行遇到细碎的建筑物边缘和道路连接处就明显拉胯。之后换成 DeepLabV3 的 ResNet50 和 ResNet101整体 mIoU 有 3-5 个点的提升。模型骨干网络大致显存占用512×512, batch 4适合阶段U-Net无预训练约 4-6 GB跑通流程、快速验证DeepLabV3ResNet50约 8-10 GB正式训练、效果提升SegFormerMiT-B2约 8-12 GB跟风试新模型MMSegmentation 全家桶任选取决于 config复现论文、大规模消融如果你显卡只有 8 GB 显存先上 U-Net 跑流程如果有 16 GB 以上直接上 DeepLabV3 不亏。不要拿一张入门卡上来就跑 SegFormer-B4给自己找不痛快。3.4 一份能直接跑的极简训练骨架不要嫌代码简单贪多嚼不烂。我的习惯是先写一个能 5 分钟跑通一个 batch 的脚本确认输入输出维度没问题后再逐步加正则化和各种 trick。import torch import torch.nn as nn from torch.utils.data import DataLoader, Dataset from PIL import Image import numpy as np import os, glob class LoveDADataset(Dataset): def __init__(self, img_dir, mask_dir, img_size512): self.img_paths sorted(glob.glob(os.path.join(img_dir, *.png))) self.mask_paths [p.replace(img_dir, mask_dir).replace(.png, _mask.png) for p in self.img_paths] for p in self.mask_paths: assert os.path.exists(p), fmask not found: {p} self.img_size img_size def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img np.array(Image.open(self.img_paths[idx]).convert(RGB)) mask np.array(Image.open(self.mask_paths[idx])) # resize 到统一尺寸注意 mask 用最近邻 img np.array(Image.fromarray(img).resize((self.img_size, self.img_size))) mask np.array(Image.fromarray(mask).resize((self.img_size, self.img_size), resampleImage.NEAREST)) # 关键1~7 映射到 0~60 作为 ignore mask mask - 1 mask np.where(mask 0, 255, mask) img img.astype(np.float32) / 255.0 img torch.from_numpy(img).permute(2, 0, 1) mask torch.from_numpy(mask).long() return img, mask if __name__ __main__: ds LoveDADataset(/path/to/LoveDA/Train/Image, /path/to/LoveDA/Train/Mask) dl DataLoader(ds, batch_size2, shuffleTrue) for x, y in dl: print(x.shape, y.shape, torch.unique(y)) break这里有三个细节值得解释mask 的 resize 必须用NEAREST不能做双线性插值否则类别边界会出现不存在的浮点过渡值产生灰色噪声标签。mask - 1之后把负值替代为 255这样在交叉熵损失里就能通过ignore_index255把无效边界区域排除掉。图像归一化直接除以 255 是保底方案如果你用 ImageNet 预训练权重最好改用mean[0.485, 0.456, 0.406]、std[0.229, 0.224, 0.225]的标准化方式配合预训练主干效果更好。4. 把数据喂进 DataLoader掩码偏移、归一化、增强和显存控制4.1 掩码偏移的两种处理流派上一章节我给的代码是标准的mask mask - 1再ignore_index255。这样处理后原来的背景类 1 变成了训练时的 0 类耕地类 7 变成了 6 类类别之间刚好连续。这种方式简单直接绝大多数开源实现也是这么处理的。另一种流派是把背景直接忽略也就是删除 mask 中类别 1 的像素当成 6 类的纯地物分割来做。这种做法在某些特定任务里可行但我不推荐因为背景类并非完全无意义它代表的“不是七种地物之一”的区域对边界判断是有帮助的。如果你用 MMSegmentation又想用官方的 7 类设定最好在 dataset 配置里把reduce_zero_labelFalse并自己写 pipeline 里的Normalize前后的类别映射不要偷懒。4.2 要不要用 ImageNet 预训练归一化这是一个非常容易被忽略但影响挺大的点。如果你用的骨干网络是 ImageNet 预训练权重比如 ResNet50那么输入图像的通道均值、方差一定要用 ImageNet 的统计值否则骨干网络第一层卷积的特征分布会错乱开始训练时 loss 可能抖动得厉害。如果你用的是自己从零初始化的 U-Net那么用img / 255.0的简单归一化就够了没必要硬套 ImageNet 统计量。我见过不少同学代码里混着用训练出来的模型在验证集上表现不稳定最后排查下来就是归一化不匹配的问题。4.3 数据增强少而精别把遥感图搞花LoveDA 影像的语义信息对颜色和纹理很敏感所以增强策略要克制。我实测下来最有用的是随机水平翻转和垂直翻转遥感图像没有绝对的“上下”语义翻转让模型对方向不敏感涨点有效。随机裁剪到 512×512 或 640×640解决大图显存压力也相当于做数据扩充。轻度随机缩放0.75 到 1.5 倍模拟不同成像高度和尺度。颜色抖动看情况加强度不要太高特别是耕地和草地的颜色容易因为色彩增强而混淆。不要做无谓的旋转任意角度增强。遥感图像虽然不是完全“上北下南”但任意角度旋转会让规则的建筑物边界和道路方向变得混乱给分割模型增加大量无效学习负担。我建议只做 90 度倍数旋转比如随机选 0、90、180、270。4.4 两大显存爆炸场景与解决方案场景一是全图直接进模型。1024×1024 的输入加上 8 的 batch哪怕 ResNet50 也会让你直接 OOM。解决方案就是随机裁剪成 512×512或者 640×640然后配合梯度累积。场景二是推理时整张预测。训练时你用 512 的裁剪到测试推理时自然也想用同样的输入尺寸但这样会丢失全图的上下文尤其对大块耕地和背景区域不友好。可以做一个重叠滑窗推理简单实现如下def sliding_window_infer(model, image, window512, stride256): h, w image.shape[-2:] pred torch.zeros((1, num_classes, h, w), deviceimage.device) count torch.zeros((1, 1, h, w), deviceimage.device) for i in range(0, h, stride): for j in range(0, w, stride): patch image[:, :, i:iwindow, j:jwindow] patch F.pad(patch, (0, max(0, jwindow-w), 0, max(0, iwindow-h))) out torch.softmax(model(patch), dim1) pred[:, :, i:iwindow, j:jwindow] out count[:, :, i:iwindow, j:jwindow] 1 return pred / count滑窗重叠的目的是减少拼接接缝处的预测不一致。你如果只是先跑通训练流程可以先不写这个等做验证集评估时再用。5. 第一次训练的超参与验证不要一上来就对标 SOTA5.1 类别不均衡损失函数如何设置直接套一个普通的CrossEntropyLoss也能跑但 LoveDA 里背景和耕地面积很大建筑物和灌丛面积较小最后出来的 mIoU 会被小类别严重拉低。我会推荐在损失函数里加入类别权重最简单的是按频率的倒数做归一化当然不要用原始倒数否则权重比过于极端。我习惯取weight 1 / np.log(1.02 freq)既放大小类别贡献又不会让权重爆炸。class_weights torch.tensor([1.0, 1.2, 1.0, 1.0, 1.5, 2.0, 1.2]).cuda() criterion nn.CrossEntropyLoss(ignore_index255, weightclass_weights)你也可以在训练一个 epoch 后统计所有 batch 的类别分布动态更新权重。但对第一次跑通训练来说没必要用一个相对合理的固定权重就好。5.2 学习率、迭代轮次和优化器的选择LoveDA 单张图较大常规训练建议跑 40 到 60 个 epoch。优化器我一般用 AdamW初始学习率 6e-5 到 1e-4配合多项式学习率衰减也就是lr base_lr * (1 - iter / total_iter) ** 0.9。如果你用的是 U-Net 这类从零训练的模型也可以用 SGDmomentum 0.9初始学习率 0.01但个人体验是 AdamW 更省心对新手更友好。batch size 不要一味求大。我实测在单张 RTX 3090 上DeepLabV3 配 ResNet50 用 512×512 输入batch 设 8 刚刚好batch 18 到 16 虽然能塞进去但训练不稳定。小 batch 配合梯度累积两到三步效果不比大 batch 差。一个比较实用的训练脚本片段optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.PolynomialLR(optimizer, total_iterstotal_iters, power0.9) scaler torch.cuda.amp.GradScaler() for epoch in range(epochs): model.train() for img, mask in train_loader: img, mask img.cuda(), mask.cuda() optimizer.zero_grad() with torch.cuda.amp.autocast(): logits model(img) loss criterion(logits, mask) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() scheduler.step()混合精度训练在 30 系及以上显卡上能明显提速而且显存占用降低让 batch 可以稍微调大一点。如果碰到 loss 出现 NaN先关掉 AMP 试试很多情况下是学习率太大导致梯度溢出。5.3 验证不是只看 loss逐类别 IoU 和可视化缺一不可训练损耗下降只代表模型在训练集上拟合了不代表它对每个类别都学得好。我强烈建议每 5 个 epoch 在验证集上计算一次逐类别 IoU 以及 mIoU保存最新的最优权重。否则你可能训练到第 50 个 epoch 才发现模型把灌丛类完全学成了草地类白白浪费时间。验证阶段最简单也最有效的三行代码pred logits.argmax(dim1) iou_per_class [] for cls in range(num_classes): intersection ((pred cls) (target cls)).sum() union ((pred cls) | (target cls)).sum() iou_per_class.append((intersection 1e-6) / (union 1e-6)) miou np.mean(iou_per_class)同时每次验证取 4 到 6 张验证图把原图、标注、预测并排保存成图片打开看一眼就知道模型是在哪些地方犯错。我发现 LoveDA 上最常见的错误是建筑物边缘被道路或背景吃掉草地和灌丛互相污染耕地在不同季节影像里颜色变化明显模型会碎成很多小块。6. 结果解读与避坑清单再训练前最好看这里6.1 监督训练结果意味着什么我在这里先说个容易让人焦虑的现象。你真拿 LoveDA Train 去训练、Val 去验证可能发现 mIoU 只有 50 多换了一个稍微复杂的模型能到 60 多但再往上走就非常吃力了。这不是你调参能力不行而是 LoveDA 本身的域差异和类不均衡决定了它在监督学习下的上限就在这个区间。看到这个结果不要灰心。你如果去看官方论文和其他以 LoveDA 为基准的论文就会发现大家在完全监督设定下的 baseline 并不高很多所谓 SOTA 其实是在无监督域适应设定下拿到的提升。所以第一次训练跑出 55 附近是个非常正常的起点。6.2 最容易踩的坑汇总表我把这些坑按照“表现、原因和解决方式”整理成了表训练前后都对照一遍。表现根本原因解决方案mask 标签读出来全是 0 或 1像素值偏移没处理读取后打印 unique训练前做 mask-1resize 后标签出现灰色噪点mask resize 用了双线性插值改用Image.NEAREST训练时显存溢出1024 全图加过大的 batch 直接进 GPU随机裁剪到 512 或使用 AMPloss 下降慢或震荡归一化与预训练权重不匹配统一用 ImageNet mean/std或换无预训练模型类别数对不上维度报错mask 没从 1-7 映射到 0-6确认 mask 的 unique 值做减法映射验证 mIoU 低验证集与训练集来自不同城市区域正常现象做跨域对比时不要慌验证集上单类 IoU 为 0该类别在训练集中样本极少加入类别权重或采用更强的增强用测试集时找不到 mask官方测试集不公开标签本地分验证集或走官方评测流程6.3 想再深入的话LoveDA 的正确打开方式是域适应训练过一次全流程之后你就已经具备了继续往下走的基础。LoveDA 之所以值得深入不是因为它能让你把 U-Net 调得很好而是它给了一个天然的任务场景如何在城市、乡村、随机三种域之间迁移。我自己在复现过程中最喜欢的一步是把训练集按域标签分离用城市域训练模型然后在乡村域验证集上评估观察跨域性能下降。这个体验很直观地告诉你为什么现在遥感分割领域大家都在研究域适应真实业务里标注数据永远不够而地物分布永远在变。如果你想继续进阶可以考虑三步第一步尝试在数据加载时把域标签也传进模型辅助分割头第二步改用对抗式域适应框架比如基于对抗的特征对齐第三步扩展到 MMSegmentation 生态找 LoveDA 相关的开源配置文件做更系统的实验。我个人的经验是做深度学习项目最怕的不是模型难调而是数据集没吃透。LoveDA 的标签偏移、域划分、混淆类别这些问题几乎在所有遥感分割数据集里都会遇到。你把这个数据集完整跑通一遍之后切换到自己研究领域的数据集时会发现大部分代码和思路都不用重写只需要替换数据加载和类别定义部分。这份实战的价值比单纯调高几个点 mIoU 要大得多。