遥感语义分割毕设指南:UNet数据管线与训练避坑

发布时间:2026/10/2 14:02:56
遥感语义分割毕设指南:UNet数据管线与训练避坑 简介这份资源面向计算机相关专业的本科生与课程设计学习者提供一套基于UNet网络的遥感图像语义分割完整项目可用于毕业设计、期末大作业或课程设计场景难度适中适合具备一定Python与深度学习基础的同学上手。压缩包共68个文件约46.94MB包含6个Python源码文件与3个Jupyter Notebook覆盖数据制作、模型搭建、训练与预测全流程另有32张png与6个svg用于结果图和网络结构展示5个tex与1个pdf构成论文正文与参考文献辅以字体、脚本和说明文档结构清晰。目前已有369人学习下载。项目源码经本地编译可运行评审分达98分内容经助教老师审定读者可据此掌握UNet编解码结构、遥感影像多类别分割的数据集构建与训练调参思路并直接参考论文写作框架与图表组织方式快速完成从实验到成文的全过程。1. 遥感语义分割毕设选 UNet为什么它仍是性价比最高的起点遥感图像语义分割这个方向每年毕业季都有大量同学扎进来。原因很直接数据公开、任务清晰、指标好量化而且 UNet 这套编码器-解码器结构在中小规模数据集上表现稳定训练成本可控。如果你手里只有一张 8G 显存的消费级显卡又想在三个月内跑出能写进论文的结果UNet 几乎是绕不开的基线。但这里有个反直觉的事实大部分毕设翻车不是因为模型不够新而是因为数据管线没搭对。遥感图像和自然图像不一样它的通道数可能是 4 通道甚至更多标签可能是单通道灰度图类别极度不均衡图像尺寸动辄 512×512 以上。这些细节处理不好UNet 再优雅也跑不出像样的结果。这篇内容面向正在做遥感语义分割毕设的本科生和刚入门的研究生从数据准备、模型搭建、训练调参到论文写作把每个环节的可复现步骤和踩坑点讲清楚。读完你应该能独立跑通一套完整的 UNet 遥感分割流程并且知道论文里哪些部分该重点写、哪些坑必须提前避开。2. 数据管线从原始遥感影像到可训练张量2.1 遥感数据集的选择与标签格式统一做毕设第一步不是写模型是找数据。遥感语义分割常用的公开数据集有几个方向土地利用分类如 LoveDA、DeepGlobe、建筑物提取如 WHU Building、Massachusetts Buildings、道路提取如 DeepGlobe Road、农作物分类如 iSAID。选哪个取决于你论文的切入角度但不管选哪个都要先确认三件事影像通道数、标签编码方式、空间分辨率。以 LoveDA 为例它的影像有两种分辨率0.3m 和 0.6m标签是单通道 PNG像素值直接对应类别 ID1-70 是背景。而 WHU Building 的标签是 0/1 二值图影像可能是 RGB 三通道。如果你打算把多个数据集混在一起做实验第一步就是统一标签编码。常见做法是写一个标签重映射脚本把所有数据集的标签统一成 0 到 N-1 的连续整数0 固定为背景或忽略类。下面是一个通用的重映射函数import numpy as np def remap_labels(mask, mapping_dict): mask: 单通道标签图np.ndarray mapping_dict: {原始像素值: 新类别ID} remapped np.zeros_like(mask, dtypenp.uint8) for old_val, new_val in mapping_dict.items(): remapped[mask old_val] new_val return remapped # LoveDA 示例把 1-7 映射到 0-60 保持为 255忽略 loveda_mapping {1: 0, 2: 1, 3: 2, 4: 3, 5: 4, 6: 5, 7: 6, 0: 255}这段代码的逻辑很直白遍历映射字典把原始标签中等于 old_val 的像素全部替换成 new_val。参数说明上mapping_dict 的键必须是原始标签中实际出现的像素值值建议从 0 开始连续编号255 留给忽略区域。注意 remapped 的 dtype 用 uint8 就够了遥感分割类别数一般不超过 20。提示重映射之后一定要用 np.unique 检查一遍新标签的像素值分布确认没有遗漏的原始类别。我见过有人漏了某个类别训练时 loss 一直不降排查半天才发现标签里有个孤立像素值没映射。2.2 滑动窗口切图与数据增强的工程细节遥感图像尺寸通常很大比如 5120×5120 甚至更大直接缩放到 256×256 会丢失大量空间细节。主流做法是滑动窗口切图把大图切成带重叠的小块。重叠的作用是避免边缘目标被切断推理时再拼回去。切图参数一般这样设窗口大小 256×256 或 512×512步长取窗口大小的 1/2 到 3/4。步长越小重叠越多样本量越大但冗余也越多。我一般用 256 窗口配 128 步长这样每个像素至少出现在两个块里。import numpy as np def sliding_window_crop(image, mask, patch_size256, stride128): image: H×W×C mask: H×W 返回: patches_img, patches_mask H, W mask.shape patches_img, patches_mask [], [] 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, :] mask_patch mask[y:ypatch_size, x:xpatch_size] patches_img.append(img_patch) patches_mask.append(mask_patch) return np.array(patches_img), np.array(patches_mask)逻辑说明双重循环遍历所有窗口位置每次切出 image 和 mask 的对应区域。参数上patch_size 必须小于等于图像最短边stride 控制重叠程度。如果图像边缘有剩余不足一个窗口的区域要么丢弃要么做 padding 补齐。我一般选择丢弃因为遥感图像边缘往往是无效区域。数据增强方面遥感图像有几个特殊点不能随意做垂直翻转因为太阳光照方向有物理意义旋转要谨慎建筑物方向信息可能被破坏。比较安全的增强组合是随机水平翻转、随机 90 度旋转、颜色抖动亮度/对比度微调、随机裁剪。不要用 CutMix 或 Mosaic 这类强增强遥感分割对空间结构敏感强增强容易引入噪声标签。2.3 Dataset 与 DataLoader 的写法与常见错误PyTorch 的 Dataset 类是整个数据管线的核心。写的时候注意三个点返回的 image 要归一化到 [0,1] 或标准化到均值方差mask 要是 LongTensorgetitem里不要做太重的计算。import torch from torch.utils.data import Dataset, DataLoader import numpy as np class RemoteSensingDataset(Dataset): def __init__(self, images, masks, transformNone): self.images images # N×H×W×C self.masks masks # N×H×W self.transform transform def __len__(self): return len(self.images) def __getitem__(self, idx): img self.images[idx].astype(np.float32) / 255.0 mask self.masks[idx].astype(np.int64) if self.transform: img, mask self.transform(img, mask) img torch.from_numpy(img).permute(2, 0, 1) # HWC - CHW mask torch.from_numpy(mask) return img, mask # 使用示例 dataset RemoteSensingDataset(train_imgs, train_masks) loader DataLoader(dataset, batch_size8, shuffleTrue, num_workers4, pin_memoryTrue)参数说明batch_size 根据显存调8G 显存跑 256×256 的 UNet 一般能到 8 或 16。num_workers 设成 CPU 核心数的一半左右太多反而拖慢。pin_memoryTrue 在 GPU 训练时能加速数据传输。注意 mask 转 LongTensor 之前不要归一化类别 ID 必须是整数。常见错误是 transform 里对 mask 做了插值。mask 只能用最近邻插值用双线性会把类别 ID 变成小数训练直接崩。另一个坑是 num_workers 大于 0 时Windows 上可能报错需要把主训练代码放在 ifname main 里面。3. UNet 模型搭建从论文结构到可训练代码3.1 编码器-解码器结构与跳跃连接的本质UNet 的核心思想不复杂编码器逐层下采样提取语义特征解码器逐层上采样恢复空间分辨率跳跃连接把编码器的高分辨率特征直接拼到解码器对应层。这样做的原因是下采样会丢失空间细节而分割任务恰恰需要精细的边界信息。跳跃连接相当于给解码器开了一条捷径让它能同时看到深层语义和浅层纹理。原始 UNet 的编码器是 4 次下采样每次通道数翻倍64→128→256→512→1024解码器对称上采样最后 1×1 卷积输出类别数。这个结构在遥感分割上依然有效但有几个调整点输入通道数要改成你数据的实际通道数比如 4 通道多光谱输出通道数改成类别数如果类别不均衡严重可以在最后一层加 Dropout。import torch import torch.nn as nn 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), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, in_channels3, num_classes7): super().__init__() self.enc1 DoubleConv(in_channels, 64) self.enc2 DoubleConv(64, 128) self.enc3 DoubleConv(128, 256) self.enc4 DoubleConv(256, 512) self.pool nn.MaxPool2d(2) self.bottleneck DoubleConv(512, 1024) self.up4 nn.ConvTranspose2d(1024, 512, 2, stride2) self.dec4 DoubleConv(1024, 512) self.up3 nn.ConvTranspose2d(512, 256, 2, stride2) self.dec3 DoubleConv(512, 256) self.up2 nn.ConvTranspose2d(256, 128, 2, stride2) self.dec2 DoubleConv(256, 128) self.up1 nn.ConvTranspose2d(128, 64, 2, stride2) self.dec1 DoubleConv(128, 64) self.out nn.Conv2d(64, num_classes, 1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) e4 self.enc4(self.pool(e3)) b self.bottleneck(self.pool(e4)) d4 self.dec4(torch.cat([self.up4(b), e4], dim1)) d3 self.dec3(torch.cat([self.up3(d4), e3], dim1)) d2 self.dec2(torch.cat([self.up2(d3), e2], dim1)) d1 self.dec1(torch.cat([self.up1(d2), e1], dim1)) return self.out(d1)逻辑说明forward 里每一步上采样后都和编码器对应层的输出做通道维拼接这就是跳跃连接。参数上in_channels 根据数据改num_classes 等于类别数含背景。ConvTranspose2d 的 kernel_size2, stride2 是最简单的 2 倍上采样也可以用 nn.Upsample 加卷积替代效果差不多。3.2 损失函数选择交叉熵、Dice 与组合损失的适用场景遥感分割的类别不均衡是常态。比如建筑物提取背景像素可能占 90% 以上用纯交叉熵训练模型会倾向于全预测背景准确率看着很高但 IoU 惨不忍睹。解决办法是用 Dice Loss 或组合损失。交叉熵适合类别相对均衡的情况它对每个像素一视同仁。Dice Loss 直接优化预测和标签的重叠度对前景少的情况更友好。我一般用 0.5×交叉熵 0.5×Dice 的组合兼顾像素级准确和区域重叠。import torch.nn.functional as F class DiceLoss(nn.Module): def __init__(self, smooth1.0): super().__init__() self.smooth smooth def forward(self, logits, targets): probs F.softmax(logits, dim1) targets_onehot F.one_hot(targets, num_classeslogits.shape[1]) targets_onehot targets_onehot.permute(0, 3, 1, 2).float() intersection (probs * targets_onehot).sum(dim(0, 2, 3)) union probs.sum(dim(0, 2, 3)) targets_onehot.sum(dim(0, 2, 3)) dice (2.0 * intersection self.smooth) / (union self.smooth) return 1.0 - dice.mean() # 组合损失 class CombinedLoss(nn.Module): def __init__(self, weight_ce0.5, weight_dice0.5): super().__init__() self.ce nn.CrossEntropyLoss(ignore_index255) self.dice DiceLoss() self.w_ce weight_ce self.w_dice weight_dice def forward(self, logits, targets): return self.w_ce * self.ce(logits, targets) self.w_dice * self.dice(logits, targets)参数说明ignore_index255 让交叉熵忽略标签为 255 的像素这个值要和数据管线里设置的忽略类一致。DiceLoss 的 smooth 防止分母为零一般取 1.0。权重方面如果前景极少可以把 Dice 权重提到 0.7。注意Dice Loss 在类别数很多时计算量会上升因为要对每个类别单独算。如果类别超过 20建议只用交叉熵加类别权重或者改用 Focal Loss。3.3 训练循环与学习率调度的实操配置训练循环本身不复杂但有几个参数直接影响收敛速度和最终精度。优化器我一般用 AdamW学习率初始 1e-3 或 1e-4配合 CosineAnnealingLR 或 ReduceLROnPlateau。Batch size 在显存允许下尽量大8 或 16 都行。import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR device torch.device(cuda if torch.cuda.is_available() else cpu) model UNet(in_channels3, num_classes7).to(device) criterion CombinedLoss(weight_ce0.5, weight_dice0.5) optimizer optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max50, eta_min1e-6) for epoch in range(50): model.train() epoch_loss 0.0 for imgs, masks in loader: imgs, masks imgs.to(device), masks.to(device) optimizer.zero_grad() logits model(imgs) loss criterion(logits, masks) loss.backward() optimizer.step() epoch_loss loss.item() scheduler.step() print(fEpoch {epoch1}, Loss: {epoch_loss/len(loader):.4f})逻辑说明每个 epoch 遍历一次 DataLoader前向传播算 loss反向传播更新参数最后调整学习率。参数上weight_decay 用 1e-4 做 L2 正则防止过拟合。CosineAnnealingLR 的 T_max 设成总 epoch 数eta_min 是最小学习率。常见问题是 loss 震荡不降。排查顺序先看学习率是不是太大改成 1e-4 试试再看数据标签有没有问题用 np.unique 检查最后看模型输出有没有 NaN加 torch.autograd.set_detect_anomaly(True) 定位。4. 避坑与排查遥感 UNet 训练中最容易翻车的五个地方4.1 现象loss 从第一个 epoch 就不降准确率卡在背景类原因标签编码和损失函数的 ignore_index 不匹配。比如标签里背景是 0但你设了 ignore_index0等于把所有背景都忽略了模型只学前景但前景又太少梯度几乎为零。解决确认 ignore_index 设的是你真正想忽略的值通常是 255背景类要正常参与训练。用 np.unique(mask) 打印标签值分布和损失函数参数对一遍。4.2 现象验证集 IoU 比训练集低 20 个点以上原因过拟合或者训练集和验证集的数据分布不一致。遥感图像常见的情况是训练集和验证集来自不同区域光照、地物类型差异大。解决先加数据增强水平翻转、颜色抖动再加 Dropout在 bottleneck 后面加 nn.Dropout2d(0.5)最后考虑减小模型容量把编码器通道数减半。如果还不行检查验证集是不是包含了训练集里没出现过的类别。4.3 现象推理时拼接回去的大图有网格状伪影原因滑动窗口切图时有重叠但推理拼接时没有做重叠区域的平均或投票直接覆盖导致边界不一致。解决推理时记录每个像素被预测的次数重叠区域取平均概率再 argmax。代码上用一个计数矩阵累加最后除以计数。def inference_large_image(model, image, patch_size256, stride128, num_classes7): model.eval() H, W, C image.shape prob_map np.zeros((num_classes, H, W), dtypenp.float32) count_map np.zeros((H, W), dtypenp.float32) for y in range(0, H - patch_size 1, stride): for x in range(0, 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().to(device) with torch.no_grad(): logits model(patch_tensor) probs F.softmax(logits, dim1).squeeze(0).cpu().numpy() prob_map[:, y:ypatch_size, x:xpatch_size] probs count_map[y:ypatch_size, x:xpatch_size] 1.0 count_map[count_map 0] 1.0 prob_map / count_map[np.newaxis, :, :] return np.argmax(prob_map, axis0)4.4 现象训练到一半 loss 突然变成 NaN原因学习率太大导致梯度爆炸或者 Dice Loss 的 smooth 太小遇到空类别时分母为零。解决加梯度裁剪 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)把学习率降到 1e-4Dice Loss 的 smooth 提到 1.0。如果用了混合精度训练检查 loss scale 是否正常。4.5 现象多光谱数据4 通道以上训练效果远差于 RGB原因多光谱波段的值域差异大比如近红外波段数值范围可能和可见光差一个量级直接除以 255 归一化不合理。解决对每个波段单独做标准化用训练集的均值和标准差。代码上在 Dataset 的getitem里按通道减均值除标准差而不是统一除以 255。5. 论文写作与实验设计让毕设结果经得起追问5.1 论文框架怎么搭从摘要到实验的写作顺序毕设论文的框架其实有固定套路但写的时候建议不要按顺序写。先写实验部分因为数据出来了才好编故事。实验部分包括数据集介绍、评价指标、对比方法、消融实验、可视化结果。写完实验再写方法方法部分把 UNet 结构、损失函数、训练策略讲清楚。最后写引言和摘要这两部分要反复改因为审阅老师最先看的就是它们。摘要控制在 300 字以内结构是问题背景一句话、方法两句话、实验结果两句话、结论一句话。引言要回答三个问题为什么做遥感分割、为什么选 UNet、你的改进点在哪。如果只是用原始 UNet 跑了个数据集没有改进那引言就要强调应用价值比如某个具体场景的落地意义。5.2 消融实验怎么做才有说服力消融实验是论文里最能体现工作量的一部分。不要只做“完整模型 vs 去掉某个模块”这一组至少做三组损失函数对比交叉熵 vs Dice vs 组合、编码器深度对比3 层 vs 4 层 vs 5 层、数据增强对比无增强 vs 基础增强 vs 强增强。每组实验固定其他变量只改一个因素。评价指标用 IoU 和 F1 为主辅以 Overall Accuracy。注意 OA 在类别不均衡时虚高不要只报 OA。表格里每个指标保留四位小数加粗最优值。如果某个改进只提升了 0.5 个点要分析原因不要硬吹。5.3 可视化结果图的选取与排版技巧论文里的分割结果图不要随便截几张。选图原则覆盖所有类别、包含难例小目标、边界模糊、类别混淆区域、对比方法的结果要放在同一行。每张图下面标注类别颜色图例用统一配色。排版上建议用 2×3 或 3×3 的网格每列是一个方法每行是一个场景。图注写清楚第一行是原图第二行是标签后面是各方法结果。如果某张图你的方法明显更好可以在正文里用“如图 X 第三行所示”引导读者看。5.4 回复审稿意见的实用话术审稿意见常见的有三类创新性不足、实验不充分、写作问题。创新性不足的回复思路是强调应用场景的特殊性或者补充消融实验证明每个模块的必要性。实验不充分的回复是补做对比实验或者引用已有工作说明你的设置是合理的。写作问题的回复最简单逐条改就行。回复信的结构感谢审稿人、逐条回复、标注修改位置。每条回复先复述意见再写“感谢指出我们已做如下修改”然后说明改了什么、在论文第几页。不要和审稿人争论即使你觉得意见不合理也要用实验数据说话。做完毕设最大的教训是不要等到最后一个月才开始跑实验。遥感数据预处理可能就要花一周训练调参再花两周写论文至少两周。时间留够心态才不崩。希望帮到你。本文还有配套的精品资源点击获取