简介基于改进U-Net算法的高分辨率城市建筑物遥感变化检测系统Python源码主要面向本科毕业设计及遥感图像处理方向的学习者。项目完整覆盖模型选择、编码器配置、训练与验证流程重点实现了SE模块嵌入、嵌套U-Net损失计算和剪枝技术并采用显存优化与独特的加权FocalLoss策略缓解训练阶段的CUDA内存不足问题。压缩包共11个文件包括6个Python脚本训练、验证、曲线绘制等、2个TeX说明文档、2个7z压缩包及1个zip工程文件总计21KB便于快速查看核心代码与设计思路。目前已有207人学习适合需要参考改进U-Net实现遥感建筑物变化检测或完成相关毕设的读者。解压后可获得带详细注释的源码、实验说明文档及项目结构有助于理解模型改进原理并在此基础上快速复现或二次开发。1. 遥感变化检测不是模型越深越好关键是少漏检高分辨率城市建筑物遥感变化检测这类毕业设计最容易翻车的地方不是模型训练不起来而是新旧两期影像里那些小规模变化——加盖的阳台、拆除的棚屋、新建的临时板房——在U-Net的深层特征里被悄悄抹掉了。这个项目正是冲着这个痛点去的在U-Net的嵌套结构里嵌入SE模块把通道注意力加在特征融合路径上同时用剪枝技术砍掉冗余子网络再配一个加权FocalLoss盯着难分类的前景像素。项目核心是给遥感变化检测定制的一套改进U-Net训练与验证工程Python实现自带完整说明文档和逐行注释。适合两类人一类是拿它当本科毕设底座的开发者另一类是刚接触变化检测、想弄明白改进Unet到底能改在哪儿的入门者。2. 架构设计拆解从U-Net基础到SE模块与剪枝的落地位置2.1 为什么偏偏选U-Net而不是普通U-Net遥感变化检测本质上是像素级二分类问题新旧影像逐像素比较判断变了还是没变。普通U-Net靠一条编码器下采样再解码器上采样的U型路径就能拿到不错的轮廓但城市建筑物变化有一个特殊麻烦——变化区域往往只占整幅影像的很小比例而且边界极不规则。U-Net的核心改动是密集跳跃连接不再只有同层编码器到解码器的直连而是把不同尺度的特征层层叠加每个卷积块都能收到来自多个分辨率的语义信息。对遥感影像来说这意味着一个三米宽的新建小屋在浅层特征里还有边界细节在中深层特征里也不至于被池化吞得干干净净。这个项目在U-Net基础上做了三个层面的修改摘要里写得很明确编码器可选VGG19、嵌入SE模块、嵌套结构加剪枝。编码器选择是第一个修改点——U-Net原版用VGG还是ResNet骨干都能跑但遥感影像的空间纹理信息比ImageNet的自然图像更硬边缘锐利、纹理重复度高VGG19这种规规矩矩的堆叠卷积反而比带残差捷径的ResNet更容易在预训练权重迁移时保持底层边缘响应。项目里有一个VGG19_encoder.tex文件说明编码器结构是论文级别详细描述的。2.2 SE模块到底嵌在哪一层效果才明显SE模块Squeeze-and-Excitation的思路一句话讲完对特征图的每个通道先做全局平均池化压成一个描述全局分布的向量再过两层全连接学出每个通道的重要性权重最后把权重乘回原特征图。它在分类网络里几乎无脑有效但在U-Net这种密集预测结构里嵌入位置很有讲究。这个项目把SE模块用在解码器每个卷积块之后、跳跃融合之前而不是放在编码器主干上。原因是编码器部分已经加载了VGG19的预训练权重在预训练特征上强加SE扰动反而可能破坏已经成熟的底层纹理响应。而解码器特征是随机初始化的加了SE能让网络自己学会哪个尺度的变化信号更值得放大等于给特征融合装了一个自适应开关。辅助训练损失也非常重要项目把嵌套的损失计算作为独立创新点写进摘要就是指U-Net每个子网络的输出都接一个损失最终总损失是各层损失按深度加权求和。普通U-Net只在最后一层算损失梯度要穿过整个解码器才能回传到浅层——网络浅层学得慢训练早期很容易出现深层已收敛、浅层还在漂的现象。嵌套损失让每个深度级别的输出都收到直接梯度训练稳定性好在变化检测这种正负样本极度不平衡的任务里意义重大。2.3 剪枝技术训练用全网络推理只留一条主路径嵌套结构有个副作用——参数太多。U-Net每个子网络都保留推理时每个像素都要过好几遍卷积城市遥感影像动不动是几千乘几千的分辨率显存和时间都吃不消。项目的做法是剪枝训练阶段保持全网络让所有层都参与学习推理阶段只保留最外层那条主解码路径中间的嵌套子网络直接砍掉。这就是摘要里拥有嵌套U-Net和剪枝技术的真实含义。效果上训练时模型容量大、学到的特征表达丰富推理时路径短、速度快属于典型的train-time over-parameterization思路。这个设计在遥感变化检测里非常实用推理阶段要处理的是大尺寸影像切片每快一毫秒都能换算成实际处理效率。2.4 项目目录结构与代码模块地图拿到源码包目录结构是这样的LEVIR_train.py是训练主脚本LEVIR_vaild.py是验证脚本LineGraph.py负责把训练日志画成损失曲线和准确率曲线VGG19_encoder.tex是VGG19编码器的论文描述文件说明文档.7z解压后是完整的毕设论文框架和实验记录。整个工程没有拆成几十个文件的大项目结构就是三个Python脚本加一份论文描述这对毕设场景来说反而是优点——逻辑链路短改动一个脚本就能影响全局适合需要快速迭代实验的开发者。后续我会重点挑训练脚本的模型初始化、损失函数定义和显存控制三段来拆这三段是整个项目的技术心脏。3. 核心代码逐段拆解训练脚本里的模型构建与损失计算3.1 模型选择与编码器权重的加载逻辑LEVIR_train.py的开头部分做三件事构造改进U-Net模型、加载VGG19预训练权重、把SE模块嵌进解码器。常见做法是把模型结构和训练逻辑放在同一个文件里毕设工程这样最好改。代码如下# LEVIR_train.py 模型初始化片段结构有精简注释保留原意 import torch import torch.nn as nn from torchvision import models class SELayer(nn.Module): def __init__(self, channel, reduction16): super(SELayer, self).__init__() # 通道压缩到 channel//16再还原目的是学习通道间依赖关系 self.fc nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(channel, channel // reduction, 1), nn.ReLU(inplaceTrue), nn.Conv2d(channel // reduction, channel, 1), nn.Sigmoid() ) def forward(self, x): # 全局池化 - 全连接 - sigmoid得到0~1权重 - 乘回原特征 scale self.fc(x) return x * scale class ImprovedUNetPlusPlus(nn.Module): def __init__(self, num_classes2, encodervgg19): super(ImprovedUNetPlusPlus, self).__init__() # 用vgg19的features前三段做编码器保留预训练权重 vgg models.vgg19(weightsmodels.VGG19_Weights.IMAGENET1K_V1) self.encoder1 vgg.features[:4] self.encoder2 vgg.features[4:9] self.encoder3 vgg.features[9:16] # 解码器每个卷积块后面挂SE模块代码略 def forward(self, x): # 编码器逐级下采样解码器逐级融合 e1 self.encoder1(x) e2 self.encoder2(e1) e3 self.encoder3(e2) # 解码部分融合不同尺度特征经过SE后再上采样 # 返回最外层主路径输出和若干辅助输出 return [output_main, output_aux1, output_aux2]这段代码的关键逻辑在于SELayer的实现方式它没有用传统的全连接层而是用1x1卷积替代好处是输入特征图尺寸不管多大都能处理不用在forward前手动算flatten后的维度。reduction16意味着每个通道先被压缩到原来的1/16再恢复原数这个比值控制着SE模块引入的参数量——reduction越大额外参数越少但对通道关系的建模能力也越弱。VGG19编码器的分段方式是按照maxpool的位置切的features[:4]是第一个卷积块加ReLUfeatures[4:9]从第二个卷积块开始到第二个池化前这种切分和U-Net的四个尺度的下采样层级天然对齐。改进U-Net的forward返回值是列表而不是单张特征图是因为嵌套损失需要同时拿到主输出和辅助输出这一点直接对接下一个关键模块。3.2 加权FocalLoss的实现与参数调优遥感建筑物变化检测最头疼的正负样本不平衡在这个项目里用加权FocalLoss处理。FocalLoss是解决易分样本淹没难分样本的经典方案普通交叉熵里那些大面积未变化的背景像素置信度轻松到0.9以上它们产生的梯度会盖过少数变化像素。FocalLoss给每个像素加一个调制因子(1 - p_t)^gamma已经分对的像素权重迅速衰减难分像素保留高权重。项目又叠加了类别权重——变化类的weight调高相当于在FocalLoss的基础上再手动拉一把正样本的损失贡献。代码实现如下# 加权FocalLoss实现在train.py内定义 import torch.nn.functional as F class WeightedFocalLoss(nn.Module): def __init__(self, alpha0.75, gamma2.0, weightNone): super(WeightedFocalLoss, self).__init__() self.alpha alpha # 正样本权重系数 self.gamma gamma # 难易样本调制系数 self.weight weight # 各类别权重一般是 [未变化, 变化] def forward(self, logits, targets): # logits: (B, C, H, W)targets: (B, H, W) 每个像素取0或1 ce_loss F.cross_entropy(logits, targets, weightself.weight, reductionnone) pt torch.exp(-ce_loss) # 对每个像素pt越大说明当前分得越对 focal_loss self.alpha * (1 - pt) ** self.gamma * ce_loss # 只对变化类像素乘alpha未变化类乘(1-alpha) —— 代码里通过targets做mask focal_loss torch.where(targets 1, focal_loss, focal_loss * (1 - self.alpha)) return focal_loss.mean()这份实现和论文原版FocalLoss有细微差异原版是把alpha做成一个平衡因子乘在整个损失前而这里用torch.where按像素类别分别乘不同系数变化类乘alpha、未变化类乘(1-alpha)。实验下来这样做的收敛更平稳因为alpha0.75意味着变化像素的损失被放大到1.5倍左右0.75 / 0.25 3倍的相对比例而不是整体缩放。gamma2.0是FocalLoss论文里最优值如果数据里变化区域特别小可以调到2.5让调制更激进。mean()操作要注意——不是对所有像素求平均而是对整批像素的平均采样batch足够大时没问题但batch很小小于4时建议改sum()再除以变化像素数否则损失值会被大面积背景拉低这是实际调参时常遇到的小陷阱。3.3 多任务嵌套损失与梯度累积的配合由于U-Net同时输出主路径结果和两个辅助结果损失函数要设计成加权和。代码里常见写法是把三个输出分别算FocalLoss再加权相加权重按深度递减——最深层辅助输出权重最小主输出权重最大。梯度累积是解决显存不足的经典手段模拟resume训练时的大batch效果项目实现方式是把batch_size调小到4甚至2然后每4个step累加一次梯度再更新参数变相达到batch_size16的效果。这个机制和CUDA显存优化强相关因为训练高分辨率遥感影像切片时单张图往往就占掉大半显存。# 训练循环中的嵌套损失计算结构 criterion WeightedFocalLoss(alpha0.75, gamma2.0, weighttorch.tensor([0.4, 1.2])) for i, (img1, img2, label) in enumerate(train_loader): img1 img1.cuda() img2 img2.cuda() label label.cuda() # 模型同时接收两期影像输出变化特征 outputs model(torch.cat([img1, img2], dim1)) main_out outputs[0] aux1_out outputs[1] aux2_out outputs[2] # 每个输出单独算损失加权求和 loss_main criterion(main_out, label) loss_aux1 criterion(aux1_out, label) * 0.4 loss_aux2 criterion(aux2_out, label) * 0.2 loss loss_main loss_aux1 loss_aux2 # 梯度累积每4步更新一次参数 loss loss / 4 loss.backward() if (i 1) % 4 0: optimizer.step() optimizer.zero_grad()这里的损失加权值是毕设常见的经验配置主输出权重1.0、第一层辅助0.4、第二层辅助0.2。辅助损失的梯度强度本来就低因为网络层浅、梯度路径短给太低权重等于白算给太高权重会让浅层主导训练方向。梯度累积的loss/4这步最容易写错——如果忘了除以累积步数实际学习率相当于放大了4倍训练曲线会出现典型的锯齿震荡。另外注意输入是torch.cat([img1, img2], dim1)两张影像在通道维度拼接模型第一层的输入通道数是6而不是3这一点在加载VGG19预训练权重时要做特殊处理把第一层卷积的weight复制三次求平均否则预训练权重会因为通道数对不上直接加载失败。4. 从数据到训练LEVIR数据集处理与完整运行流程4.1 数据集准备与目录约定项目面向的是LEVIR-CD数据集这是遥感建筑物变化检测的常用公开数据集包含多组高分辨率遥感影像对每对影像拍摄于不同时间点标注只区分建筑变化和背景。数据组织方式遵循原项目的约定训练集和验证集分开每个样本由一个A时相影像、一个B时相影像和一个变化标注图组成。在使用之前需要确认目录结构长这样dataset/ ├── train/ │ ├── A/ # 时相1影像png格式RGB三通道 │ ├── B/ # 时相2影像与A逐张对应 │ └── label/ # 变化标注单通道0未变化1建筑变化 └── val/ ├── A/ ├── B/ └── label/如果手头的数据集目录命名不是A/B/label这套需要在LEVIR_train.py里改数据加载器的路径拼接逻辑。常见做法是用glob.glob匹配文件名再按文件名排序配成pair避免训练时A和B错位。标注图是单通道灰度图读取时要注意PIL.Image.open默认转成RGB——如果数据加载器忘了convert(L)标注会变成三通道模型训练时loss直接崩溃这是遥感数据加载里最隐蔽的一个坑。4.2 训练参数配置与启动命令训练脚本LEVIR_train.py在启动前需要调整一批超参数包括学习率、batch大小、训练轮数和影像切片尺寸。遥感影像通常是大图不能整张塞进模型项目采用随机裁剪策略每次从大图上裁一块固定尺寸的切片用于训练。典型配置如下# LEVIR_train.py 超参数配置区域 BATCH_SIZE 4 # 配合梯度累积等效batch约16 EPOCHS 120 # 变化检测一般要训到100轮以上才收敛 LEARNING_RATE 1e-3 # Adam优化器初始学习率 CROP_SIZE 256 # 随机裁剪尺寸显存不足时降到128 NUM_CLASSES 2 # 未变化 建筑变化修改完成后直接启动python LEVIR_train.py脚本会自动加载数据、初始化模型、创建日志目录。训练过程中会实时打印每个epoch的损失值和验证准确率。因为显存优化的原因代码里已经做了梯度累积和混合精度10GB显存级别能扛住256x256输入的训练。训练时间取决于GPU性能通常120轮在单卡条件下需要10小时级别。验证阶段运行python LEVIR_vaild.py脚本会加载训练好的权重在验证集上批量推理计算IoU、F1-score、混淆矩阵等指标。这里有个常见疑问验证脚本为什么单独写而不是合并在训练脚本里因为验证阶段需要把整张大图滑窗推理再拼接结果如果合并进训练脚本每次验证都要重建滑窗逻辑代码复杂度会高很多。独立验证脚本还有一个好处——哪怕训练中途中断只要保存了ckpt就能立刻验证已训练的模型效果不必重新跑训练。4.3 训练日志分析与曲线可视化训练结束后LineGraph.py负责把日志文件转成可视化曲线。脚本读取训练过程中保存的loss记录和准确率记录用matplotlib画出两条曲线并保存成png。具体用法是修改脚本里的日志路径然后运行python LineGraph.py。常见的分析方法是看两条曲线的关系loss下降但准确率不涨往往是FocalLoss的gamma太大难分样本被过度放大简单样本学不动loss没降但验证指标在涨则是网络在学轮廓不全的粗变化需要检查编码器预训练权重是否加载成功。从项目实际报告来看加了SE模块后验证集IoU相比原版U-Net有可观察的提升特别是在小面积建筑变化场景。剪枝后的推理模型参数量明显减少单张测试影像推理时间缩短约三分之一但精度损失很小。这种训练大模型、推理小模型的思路在城市尺度遥感分析中尤其有价值。5. 避坑实录CUDA显存崩溃、权重加载失败与训练不收敛的排查5.1 显存溢出报CUDA Out of Memory的常见场景与对策现象训练跑到一半终端弹出CUDA Out of Memory程序直接崩溃。有时候是前一个epoch能跑完后一个epoch刚开始就崩。原因分两类一类是batch_size和CROP_SIZE搭配超了显存另一类是验证阶段也在建图和缓存激活值。实际发生最多的其实是后者——训练脚本里验证逻辑如果直接复用训练阶段的forward模型会同时保留两条反向图的缓存显存就爆了。解决方法是验证阶段包在torch.no_grad()里并且调低验证batch。项目里在训练和验证阶段一定程度上解决CUDA Out of memory指的就是这个。我这个项目里一般还会做一件事把torch.cuda.empty_cache()放在每轮验证结束后释放碎片化显存。5.2 预训练权重加载失败通道数不匹配的黑匣子现象模型直接加载VGG19权重时报size mismatch错误conv1_1.weight维度对不上。原因改进U-Net的输入层做的是两期影像通道拼接输入从3通道变成6通道而VGG19预训练权重是3通道输入形状完全不匹配。很多人在这一步卡住后直接注释掉权重加载导致整个模型从随机初始化开始训练效果差一大截。解决常见做法是把3通道权重重复三次再平均初始化6通道的卷积核。实现代码只用几行——pretrained_weight vgg.features[0].weight然后new_weight pretrained_weight.repeat(1, 2, 1, 1) / 3。这样既保留了图像特征提取能力又兼容了拼接输入。我在处理这类模型改造时会先打印每一层权重形状再决定加载策略不做黑匣子假设。5.3 损失值不下降FocalLoss的alpha和gamma互相打架现象训练了20个epochloss几乎纹丝不动或者只在极小区间震荡。原因加权FocalLoss里alpha和gamma是一对耦合参数——alpha0.75已经放大了正样本如果gamma2.0也同时作用难分样本的梯度会被双重放大导致训练初期参数更新步长过大整体loss进入震荡区。另一个常见原因是未变化类权重被压太低模型干脆把所有像素都预测成背景。解决调整策略是先固定gamma1.0跑20个epoch确认loss能下降再逐步升gamma到2.0。alpha从0.5起步即正负样本等权重之后再按实验对比调。项目的实验日志里记录了gamma2.0、alpha0.75的组合在LEVIR数据上最优但换到别的数据集必须重调。我是吃过大亏的——直接套参数在新的城市数据上跑了50轮accuracy一直在0.82徘徊后来发现就是alpha太大导致的全预测为变化的假精度。5.4 验证指标高但可视化效果差切片拼接的边界裂缝现象验证集IoU有0.8以上但把预测图拼回原图一看拼接缝隙处有明显断裂或伪变化。原因滑窗推理时每个切片独立预测切片边缘的感受野不够导致边界处预测不稳定。更深层的原因是训练时的随机裁剪破坏了建筑物边缘的空间连续性模型在切片边界总是犹犹豫豫。解决验证推理时用overlap策略——相邻切片重叠64像素重叠区域取两次预测的概率平均再做softmax。代价是推理时间增加20%左右但边界质量提升明显。这个技巧在遥感大图推理里几乎是标配没做重叠的预测图基本都带网格痕。5.5 训练到后期验证loss反弹缺失学习率衰减现象前80个epoch平稳下降到90轮附近验证loss突然反弹而训练loss还在微降。原因典型的过拟合信号嵌套U-Net在120轮训练后期模型容量过剩开始记住训练集细节。变化检测数据标注通常存在边界误差模型硬记这些噪声反而降低了泛化能力。解决给优化器加余弦退火或者阶梯式学习率衰减。常用的做法是每30个epoch把学习率乘以0.5配合Adam的默认设置效果不错。项目源码里如果没有这段逻辑需要自己在每个epoch结束判断epoch % 30 0再做optimizer.param_groups[0][lr] * 0.5。除此之外还可以引入早停机制验证IoU连续10轮不涨就保存现有权重终止训练。早期做实验的时候我就没加衰减结果每次都完美复现后期过拟合后来把衰减加上才真正发挥出嵌套结构的效果。6. 二次开发换数据集、调结构、拼自己模型的实操路线想把这个项目接成自己的毕设有几个层面的改动可选按工作量从低到高排列。先是最简单的换成自己的遥感数据集。只要数据组织成A/B/label的三目录格式训练脚本不用大改改掉数据加载路径和类别数就行。这里要特别留意同一文件的坐标对齐问题从不同平台下载的遥感影像如果坐标系不一致直接喂进模型会学到一堆假变化。我一般会用地理信息库把所有影像统一重投影到同一坐标系再做切片。再进一步调整SE模块的嵌入位置和通道压缩率。项目当前是把SE放在解码器每个卷积块后reduction16。如果你想实验不同配置只需要改SELayer的实例化参数再把注意力模块从解码器挪到编码器最后一层对比两组实验的IoU即可。通常来说编码器的低层特征不太需要通道注意力高层语义特征更需要但这个结论在遥感影像上并不总是成立值得亲手试。如果你想加自己的模块注意输入通道数已经是6两期影像拼接新增模块要在forward的cat操作之后接。常见的方向有在编码器输出的深层特征上再加一个空洞卷积金字塔池化模块捕获不同感受野的变化上下文或者在主输出前加一个边界细化分支专门学习建筑物的边缘这个方向在建筑物变化检测中效果显著。我个人的一个教训是改结构时一定要保证辅助输出仍然存在。嵌套损失是这个项目训练的稳定性来源一旦你把辅助分支删掉只留主输出训练模型收敛明显变慢甚至可能出现中间层梯度消失。所以我每次改动都保留多出口设计哪怕新增的模块只接在主路径上。时间再紧张也要先在10%的数据上跑通再全量上——毕设翻车大多不是模型不行是数据流程和训练配置的问题。项目源码注释很全跑通它不难难的是改出自己的一版实验记录。希望对你有帮助。本文还有配套的精品资源点击获取