ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

遥感语义分割训练避坑:SegFormer、mIoU与数据切块

遥感语义分割训练避坑:SegFormer、mIoU与数据切块 简介这套遥感城市图像语义分割数据集面向需要标准训练数据的深度学习学习者可用于城市地物、海陆区域等场景的多类别语义分割实验。图像与标签均已预处理完毕无需额外清洗即可直接训练覆盖8个常见地物类别并明确划分为约800张的训练集和约300张的验证集方便开展模型训练与效果评估。压缩包共有2000个文件以png和jpg图片为主png对应图像与掩码标签jpg保存原始遥感影像另有txt类别说明和一个Python可视化脚本整个资源包约42.05MB目录结构简洁便于按需调用。目前已有129人学习下载适合作为Unet、SwinUnet等分割网络改进实验的基础数据集。压缩包内还附可视化脚本可随机抽取一张样本将原始图、GT标签以及GT在原图上的蒙版效果同时展示能直观核对标注质量与预测结果。1. 拿到一份“已处理完”的遥感语义分割数据集第一件事别急着训遥感城市图像的语义分割和自然图像分割最大的区别在于“地物尺度”。一张遥感影像动不动就是 1024、2048 甚至更大里面既有整片屋顶也有只有几个像素宽的马路和电线杆。很多人拿到一份“约1000张、8类别、已处理完”的数据集第一反应是直接扔进 U-Net 开训然后发现 Loss 降不下去、小类别全被吞掉、验证集 mIoU 只有 0.3。问题不在模型在于没搞清楚这批数据“处理完”到底处理了什么。这类数据集的真正价值不在 1000 这个数字而在“可以跳过标注和预处理”这两个最耗人力的环节。遥感影像的标注成本远高于普通图片——一个 512x512 的切块就要人工描几十个地物轮廓而原始大图往往需要先切块再标注切块策略还会直接影响标签质量。所以这份数据的核心卖点是“省下的时间”不是“数据量”。在往下读之前先想清楚你要用它做什么学术对比、算法验证、还是落地到具体城市区域的识别任务目标不同切块尺寸、类别权重、验证集划分方式都不一样。下面要讲的内容围绕三条线展开这份数据应该怎么验收和拆解、怎么搭一套能直接训练的分割 pipeline、以及 8 类别任务里最容易翻车的地方在哪里。每一节都给可复现的参数和命令不是泛泛而谈。2. 先验收数据8类别和约1000张背后的隐藏信息2.1 类别分布比总张数更值得先看拿到任何语义分割数据集我第一件事不是打开图片看效果而是统计类别分布。一份“约1000张”的数据如果 8 个类别里有一个类别只出现几十张那这个类别在交叉熵 Loss 里基本会被背景类淹没。常见做法是用 Python 脚本统计每个类别的像素占比写出来大概是这样的import numpy as np from glob import glob from PIL import Image label_paths sorted(glob(labels/*.png)) class_pixels np.zeros(8, dtypenp.int64) for lp in label_paths: label np.array(Image.open(lp)) # 语义分割标签图里像素值就是类别ID0通常为背景 for c in range(8): class_pixels[c] int((label c).sum()) total class_pixels.sum() for c in range(8): print(fclass {c}: {class_pixels[c]} pixels, {class_pixels[c] / total * 100:.2f}%)这段代码做的事情很简单遍历所有标签图统计每个类别 ID 出现的像素总数最后打印占比。注意这里没有做类别平衡处理只是先摸底。如果某个类别占比低于 1%后面训练时必须给这个类别提高 Loss 权重否则模型会直接把这个类别学成背景。除了像素占比还要看每一张图里类别是否共现。遥感城市影像里道路、建筑、植被通常同框出现但停车场、水体这类类别可能只集中在少量样本中。如果某个类别只存在于前 200 张图里随机划分训练集和验证集时就要用 stratify 策略按类别分布切分不能纯随机否则验证集里可能根本没有这个类别。2.2 标签格式、调色板和类别顺序的检查清单“已处理完”不等于“格式一定符合你的训练代码”。我遇到过标签图是 RGB 三通道的彩色 PNG但训练代码用np.array(Image.open(path))读出来是 (H,W,3)和模型输出的 (H,W) 对不上也遇到过类别 ID 从 1 开始而不是从 0 开始。所以写训练代码之前先花十分钟跑一段检查脚本from PIL import Image import numpy as np img Image.open(labels/0001.png) label np.array(img) print(shape:, label.shape, dtype:, label.dtype) print(unique values:, np.unique(label)) # 如果shape是(H,W,3)说明是RGB标签需要转成单通道index if label.ndim 3: # 常见调色板映射读取颜色表并映射到类别ID palette np.array(img.getpalette()).reshape(-1, 3) # 以第一个像素的颜色查找类别ID # 更稳妥的方式是直接用灰度模式打开 label_gray np.array(Image.open(labels/0001.png).convert(L)) print(gray unique values:, np.unique(label_gray))这里的关键判断标准是unique values是否正好是 0 到 7 的连续整数。如果出现 255 或者其他异常值说明标签里有 ignore 区域或者标注残留。遥感数据里很常见的现象是建筑物边缘的标签和影像有 1~2 个像素的偏移导致边界像素被标错。这类问题在 512 分辨率下影响不大但如果你计划做高分辨率推理或边缘精修就要特别留意。2.3 图像尺寸不统一带来的切块策略问题约1000张遥感图尺寸不太可能完全一致。有的可能是 1024x1024 切好的有的可能是 2000x1500 的原始大图。这两种情况对训练的影响完全不同。如果是后者你没法直接把整图送进 GPU必须切块。常见做法有两种无重叠切块和滑窗切块。无重叠切块简单高效但在图幅边缘会丢掉信息而且如果目标物体恰好跨在切块边界上标注会被切成两半模型推理时容易出现“边界断裂”。滑窗切块带 overlap 能缓解这个问题但会成倍增加样本数量。我一般用 512 的窗口、128 的 overlap样本量大约是 5x4 的网格1000 张大图切出来可能变成 2 万个小块这对显存和训练时间是压力但准确率收益明显。# 用开源工具切块示例用 gdal_retile 思路但遥感分割更常用的是自定义滑窗 python split_remote_sensing.py \ --input images/ \ --label labels/ \ --window 512 \ --overlap 128 \ --out_dir train_512/这个命令里的--window和--overlap是两个最需要调节的参数。窗口太小大建筑看不全上下文信息不足窗口太大小类别道路、车辆的像素占比更低模型更容易忽略它们。512 是语义分割里相对稳妥的起点2080Ti 8G 显存也能跑得动。3. 用 SegFormer 还是 U-Net训练脚本的基线与参数设计3.1 为什么 8 类别遥感分割不优先选 U-Net 裸跑很多教程一上来就让人用 U-Net 训遥感分割这个选择在样本量极小几十张时是合理的但你有约1000张数据已经脱离了“小样本”范畴。U-Net 的编码器是 VGG 或者 ResNet 变体感受野有限对“大目标包裹小目标”的城市遥感场景——比如一片建筑区中间有一小块绿地——分割结果容易出现内部空洞或者边缘锯齿。更贴合遥感场景的基线是 SegFormer 或 DeepLabV3。SegFormer 的分层 Transformer 编码器能同时保留高分辨率细节和低分辨率语义在 Cityscapes 这类户外场景上表现好而遥感城市图像和 Cityscapes 的高度结构化特征比较接近。DeepLabV3 的优势是 ASPP 空洞卷积池化模块对大目标的上下文建模稳定训练速度比 SegFormer 快。下面我用 SegFormer 做基线给出一个可直接替换的完整训练脚本。3.2 最小可运行训练脚本数据加载、损失函数与评估import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader from transformers import SegformerForSemanticSegmentation import numpy as np from PIL import Image import albumentations as A # 自定义数据集读取图像和标签返回 tensor class RemoteSensingDataset(Dataset): def __init__(self, img_dir, label_dir, transformNone): self.img_paths sorted(glob(f{img_dir}/*.png)) self.label_paths sorted(glob(f{label_dir}/*.png)) self.transform transform def __len__(self): return len(self.img_paths) def __getitem__(self, idx): image np.array(Image.open(self.img_paths[idx]).convert(RGB)) label np.array(Image.open(self.label_paths[idx]).convert(L)) if self.transform: aug self.transform(imageimage, masklabel) image, label aug[image], aug[mask] # 归一化到 [0,1]SegFormer 的预训练权重要求特定 mean/std image image.astype(np.float32) / 255.0 image torch.from_numpy(image).permute(2, 0, 1) label torch.from_numpy(label).long() return image, label # 数据增强遥感分割最怕过拟合这里加翻转和随机亮度 train_transform A.Compose([ A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.RandomBrightnessContrast(p0.3), A.RandomCrop(512, 512, p0.8), ]) model SegformerForSemanticSegmentation.from_pretrained( nvidia/mit-b3, num_labels8, ignore_mismatched_sizesTrue, ) criterion nn.CrossEntropyLoss(ignore_index255) optimizer torch.optim.AdamW(model.parameters(), lr6e-5, weight_decay0.01)代码里的关键点标签用convert(L)转成单通道灰度确保 shape 是 (H,W)ignore_index255是给标签里的边界残影兜底如果标签里没有 255 也不影响。RandomCrop 到 512 是配合 SegFormer 的输入分辨率实际推理时可以用整图但训练时固定尺寸能显著提高 GPU 利用率。lr 用 6e-5 是 SegFormer 微调比较稳的起点超过 1e-4 容易出现 Loss 震荡。训练循环不需要太多定制但有两个细节值得注意每个 epoch 结束后在验证集上算 mIoU而不是只盯着 Loss遥感图像的 Loss 下降曲线通常比自然图像慢——前 5 个 epoch mIoU 可能只有 0.1 到 0.2这是正常的不要因为前几个 epoch 没起色就调大学习率。3.3 训练参数表从 8G 显存到 24G 显存的配置参考显存batch_size训练尺寸SegFormer 模型epoch建议8G4512x512mit-b040先验证 pipeline 通不通12G8512x512mit-b250常规实验配置24G8640x640mit-b360追求精度的主力配置24G4768x768mit-b330注意过拟合风险小显存不要强行拉大 batch_size可以梯度累积。但如果梯度累积超过 4 步BN 层的统计量会失真SegFormer 这类 Transformer 影响相对小但也要注意。1000 张数据训练 50 个 epoch以 12G 显存为例大约 3~5 小时能跑完这个时间成本在合理范围内。4. mIoU 的陷阱8类别里的“背景”和“小目标”要分开看4.1 背景类占比 70% 时mIoU 会骗人遥感城市图像的语义分割背景类往往占比极大。如果 8 个类别里第 0 类是背景占比 70%模型把所有像素都预测成背景整体 Accuracy 就是 0.7但 mIoU 只有 0.09 左右。这个数字看着惨但恰恰说明 mIoU 比 Accuracy 更有参考价值。不过 mIoU 还有一个更隐蔽的问题如果一个类别占 0.5% 像素另一个类别占 30% 像素mIoU 是两类 IoU 的算术平均小类别的精度波动对整体 mIoU 的影响被稀释了。所以评估时至少要打印每个类别的 IoU不能只看总体 mIoU。我自己评估遥感分割模型的标准是道路 IoU 必须单独看因为道路是城市遥感里最细长、最容易被切断的类别它的 IoU 直接决定模型能不能用于实际场景。4.2 验证集划分按区域划分而不是按文件随机划分约1000张遥感图如果来自同一个城市的相邻区域随机划分训练集和验证集会带来严重的数据泄露训练集里某栋建筑的纹理特征验证集里也有几乎一样的建筑。这是一个很常见的翻车点——“验证集 mIoU 0.7换一块新区只有 0.35”的根因就在这里。解决方案是按拍摄区域分组。如果数据集文件有目录或前缀区分区域按区域划分没有的话看图像的地物重叠度把空间相邻的图像归为同一组。遥感数据的验证集划分没有完美答案但至少要保证验证集里每一个类别都有足够多的像素否则评估结果方差极大。4.3 后处理CRF 和形态学操作什么时候该用分割模型输出的概率图直接 argmax 得到标签图边界通常是锯齿状的。对遥感图像我一般会在模型输出后接一个轻量的形态学后处理先去除面积小于阈值的零散连通域再用scipy.ndimage.binary_opening修掉细长噪声。CRF 全连接条件随机场在自然图像分割里效果好但对遥感大图计算量太大——一张 1024x1024 的图像 CRF 推理可能要几十秒不划算。道路断裂的问题靠 CRF 救不回来得靠损失函数和推理策略解决。5. 训练避坑指南从 Loss 不降到你以为的“玄学”问题5.1 标签类别号和调色板不一致导致的“看似正常实则全错”现象Loss 正常下降训练 mIoU 在 40 个 epoch 后到 0.75但随便抽一张推理可视化发现建筑被标成了绿地水体被标成了道路。原因标签 PNG 是带调色板的 RGB 图convert(L)读出来的灰度值和实际类别 ID 对不上或者类别顺序和模型输出头不一致。解决先用 2.2 节的脚本检查unique values确认类别 ID 范围再随机抽 5 张标签图用调色板还原成可视化图和原图叠在一起看边缘是否贴合。这种情况在“已处理完”的数据集里最常见也是最难排查的——因为 Loss 曲线一切正常。我的经验是训练第一个 epoch 结束后就抽 20 张图做可视化不要等训练完再看。5.2 模型把所有像素预测成同一类的“退化模式”现象训练 10 个 epoch 后验证集输出全是背景类或者全是某一个占比较大的类别。原因类别极度不平衡加上学习率偏大模型快速收敛到“全预测背景”这个局部最优。解决先降低学习率从 6e-5 降到 2e-5再给 Loss 加类别权重。遥感分割里最常见的权重设置是weight 1 / np.log(1.02 class_pixel_ratio)它可以放大低频类别的梯度贡献又不会像1/ratio那样把噪声也放大。# 按像素占比计算类别权重避免模型退化成全背景预测 pixel_ratio class_pixels / total_pixels class_weight 1.0 / np.log(1.02 pixel_ratio) class_weight torch.from_numpy(class_weight.astype(np.float32)).cuda() criterion nn.CrossEntropyLoss(weightclass_weight, ignore_index255)这段代码里1.02是平滑项防止某个类别占比极小时权重爆炸。如果加了权重后训练 Loss 出现震荡说明权重过激把平滑项调到 1.05 再试。5.3 切块后目标被切断导致道路和建筑边缘断裂现象训练时切块是 512x512推理时用整图结果道路在切块边界处断开建筑边缘出现明显接缝。原因滑窗切块时同一个目标物体被分到两个相邻块中模型在推断时看不到完整上下文。解决推理时用 overlapping window多个窗口的预测结果取平均soft voting代码逻辑如下def sliding_predict(model, image, window512, overlap128): h, w image.shape[:2] scores np.zeros((h, w, 8), dtypenp.float32) count np.zeros((h, w, 1), dtypenp.float32) for y in range(0, h, window - overlap): for x in range(0, w, window - overlap): y2, x2 min(y window, h), min(x window, w) patch image[y:y2, x:x2] # 短边补零到 window 大小推理后裁掉 pad_h, pad_w window - (y2 - y), window - (x2 - x) patch np.pad(patch, ((0, pad_h), (0, pad_w), (0, 0))) with torch.no_grad(): out model(torch.from_numpy(patch).permute(2,0,1).unsqueeze(0).float().cuda()) out torch.nn.functional.softmax(out.logits, dim1)[0].cpu().numpy() scores[y:y2, x:x2] out[:, :y2-y, :x2-x].transpose(1,2,0) count[y:y2, x:x2] 1 final scores / count return final.argmax(axis2)overlap128会让每个边界像素被多个窗口预测到能显著改善拼接痕迹代价是推理时间变成原来的两倍左右。如果你要在城市级大图上推理这个时间成本是值得的否则接缝问题会在后续矢量化处理时被放大成“断头路”。5.4 验证集 mIoU 高但实际效果差地物分布不一致现象训练时留出的验证集 mIoU 有 0.72但把模型放到另一幅完整遥感图上推理整体效果明显变差。原因约1000张的训练数据往往来自同一批次影像色调、光照、建筑风格高度一致模型学到的是“这批数据的风格”而不是通用的城市地物特征。解决引入基础的数据增强之外还要做色域扰动——随机改变亮度、对比度、色调饱和度甚至用A.ToGray(p0.1)让模型不对某种色调产生依赖。这个技巧能让模型在不同传感器来源的遥感图上泛化性明显提升。5.5 训练时内存溢出不是显存问题是标签文件过大现象数据加载时 MemoryError或者 DataLoader 卡死。原因某些标签 PNG 文件尺寸异常大或者图片读取时没有做尺寸归一化直接把 5000x4000 的大图读进内存。解决在 Dataset 的__getitem__里先做中心裁剪或者 resize 到统一尺寸不要让原始大图进 DataLoader。遥感大图读取本身是 I/O 瓶颈建议把所有样本预先切块成 512x512 存到 SSD 上训练时直接读切块不做实时切块。6. 从 0.5 到 0.8 的三个进阶技巧类别加权、伪标签和多尺度推理我训练遥感分割最常用的一个习惯是“两阶段训练”。第一阶段用类别加权后的交叉熵跑 40 个 epoch把模型训到 mIoU 0.55 左右第二阶段加载第一阶段的权重把 Loss 换成 Dice CrossEntropy 的混合损失用更低的学习率1e-5继续训 20 个 epoch。混合损失里 Dice 项对类别不平衡不敏感能专门把道路这类小类别的 IoU 拉高但纯 Dice 在训练初期容易震荡所以不能从头用。伪标签是半监督方向的思路对约1000张的数据量尤其管用用第一阶段的模型对一小批未标注的遥感图做预测挑选置信度高于 0.9 的像素作为软标签混入训练集继续训练。这个操作能把训练数据量扩到 1500 张左右的等效效果但前提是未标注图像和训练集的区域特征接近不能完全是另一种地貌。挑选置信度时要注意背景类通常置信度偏高道路和车辆的中等置信度像素更有训练价值所以按类别分别设定置信度阈值而不是全局一个阈值。多尺度推理在老生常谈里算性价比最高的一个。SegFormer 这类模型对输入分辨率较敏感同一张图分别用 0.5 倍、1.0 倍、1.5 倍分辨率做推理把三个概率图 resize 回原尺寸再取平均mIoU 通常能提升 1~3 个百分点。我做过一个实验在 8 类别遥感数据上单尺度 mIoU 0.742三尺度平均后到 0.771提升主要来自道路和建筑边缘。代价是推理时间变成原来的 3 倍如果在乎延迟可以在关键区域只对 0.5 倍和 1.0 倍做双尺度平均效果接近但时间减半。最后补充两个细节第一ignore_mismatched_sizes在加载预训练权重时一定要加否则 SegFormer 分类头输出维度对不上会直接报错第二训练日志里除了 mIoU 之外额外记录每个类别的 IoU 曲线如果某个类别的 IoU 在训练中期开始下降说明过拟合了要及时加增广或者提前停止。这些细节看起来琐碎但把 0.5 提升到 0.8 往往就是靠它们一个个叠出来的希望帮到你。本文还有配套的精品资源点击获取
返回列表