恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

改进U-Net++遥感变化检测:SE注意力与FocalLoss工程实践

  • 首页
  • 资讯中心
  • /
  • 改进U-Net++遥感变化检测:SE注意力与FocalLoss工程实践

相关资讯

diagram-design实战:从需求分析到团队图库的信息设计方法论 2026/10/19 21:15:24
diagram-design:从草图到可维护图表资产的设计方法论 2026/10/19 21:15:24
从知道到稳定输出:构建可验证、可迁移的个人技能体系 2026/10/19 21:15:24

最新资讯

DeskcommCRM实战:轻量级客户管理与销售跟进全流程落地指南
以沟通为核心的轻量级CRM系统设计与实践
Kafka全链路性能调优:吞吐量与延迟的实战权衡
ZXing.Net 0.16.8.0 条码识别实战:从解码链路到性能调优
从线程池失控到全局限流:高并发系统稳定性实战指南
STC单片机范例程序实战指南:从点灯到多外设工程搭建

今日推荐

本周热门

基于GMM的语音识别系统原理与MATLAB实现详解
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

改进U-Net++遥感变化检测:SE注意力与FocalLoss工程实践

发布时间:2026/10/19 21:20:25
改进U-Net++遥感变化检测:SE注意力与FocalLoss工程实践 简介面向城市遥感智能解译的本科毕业设计项目基于改进U-Net算法构建高分辨率建筑物变化检测系统解决传统方法在复杂场景下建筑物提取精度不足、边界模糊等痛点。资源内含完整Python源码涵盖LEVIR数据集上的训练、验证与损失曲线可视化等环节并融入SE注意力模块、嵌套U-Net损失计算、剪枝技术、加权FocalLoss等创新点能够增强特征通道关注缓解正负样本不均衡同时通过深层监督与网络剪枝平衡精度和速度训练阶段还针对CUDA Out of Memory做了优化便于常规显存设备运行。编码器可灵活选择并支持VGG19等预训练主干适配不同任务。压缩包共11个文件包括6个py源码、2个tex说明文档、2个7z补充资料和1个汇总代码zip整体仅21KB轻量便携。目前已有207人浏览/学习读者可获得详细注释的改进模型代码、说明文档与算法改进思路整理既可辅助毕设论文撰写也能作为遥感变化检测方向的开源基线进行二次开发与拓展。1. 项目速览一份能直接跑的改进U-Net遥感变化检测毕设每年毕业设计遥感建筑物变化检测都是热门方向。很多人拿到源码第一步就卡住环境装不上、显存不够、训练loss不降最后只能把别人的模型硬套在自己数据集上。这个项目不太一样它是一份把改进算法真正落到代码里的Python工程——在U-Net骨架上把SE通道注意力、嵌套深度监督、剪枝推理、加权FocalLoss和显存优化全部串进了训练与验证流程解压后照着说明文档就能跑。适合两类人一是做遥感变化检测相关毕设需要一套能复现、能在答辩时讲清楚创新点的代码二是想研究改进U-Net的开发者想看看SE模块、嵌套损失、剪枝这些改动到底怎么和训练管线接在一起。接下来我会从工程结构、核心算法、实操运行三个角度把它拆开最后聊二次开发的改造思路。2. 工程结构拆解从压缩包里的文件看完整运行链路拿到压缩包先别急着解压跑训练花五分钟把文件认一遍能省掉后面很多麻烦。这个项目不复杂核心就是两个训练验证脚本加一个绘图脚本再加一份说明文档但每一份文件都对应一个明确的阶段搞清楚整体结构不管训练还是改代码都不会晕。2.1 文件清单与职责划分先看压缩包里第一层有哪些东西我用表格梳理一下文件作用什么时候用project_code_upload.zip完整工程代码打包解压后作为工作目录LEVIR_train.py训练入口负责数据加载、构建模型、损失计算、参数更新跑训练时LEVIR_vaild.py验证入口加载训练好的权重算指标、输出结果图训练完成后验证时LineGraph.py读取训练日志画loss曲线和F1曲线任何时候想看曲线时说明文档.7z论文正文、环境配置、使用说明配环境、写论文时VGG19_encoder.texLaTeX格式的VGG19编码器结构参数表写论文插图时注意看一下LEVIR_vaild.py里vaild是valid的拼写变体属于作者笔误但不影响Python导入和执行不用改。脚本名里的LEVIR对应那套公开的高分辨率遥感建筑物变化检测数据集训练脚本和验证脚本就是围绕这套数据组织的。说明文档.7z解压后一般会包含环境要求、数据目录结构、训练参数说明和实验结果分析第一次跑之前建议先把文档里环境配置那一节读完比直接执行脚本省事得多——很多同学上来就python LEVIR_train.py结果torchvision版本对不上跑两行就翻车。注意优先把说明文档.7z解压出来核对版本要求而不是先跑代码。这类项目最常见的启动失败原因就是依赖版本冲突。2.2 训练-验证-可视化三段式流程如何串起来整个项目的执行链路是先跑LEVIR_train.py得到权重文件和日志再跑LEVIR_vaild.py加载权重做验证并输出指标最后用LineGraph.py把日志画成曲线图直接放进论文。三个脚本各管一段中间靠权重文件 日志文件衔接不需要人工干预。数据流方向是前时相图像 后时相图像 → 数据增强 → 输入改进U-Net → 输出变化概率图 → 与标签计算损失 → 反向传播 → 保存最优权重 → 验证阶段输出检测结果图和F1 / IoU指标。这里面最关键的设计是训练和验证共用同一套模型定义在训练脚本里改了网络结构验证脚本不用同步改避免了两边模型不一致的低级错误。数据加载部分遥感变化检测的数据组织方式和普通分类任务不太一样。常见做法是每个样本包含两张同一地点不同时间的图像和一张二值变化标签三个文件按名字一一对应。Dataset的__getitem__里要做的是同时读两张图、做同步的数据增强比如同时翻转然后返回给训练循环。参考实现长这样import os import torch from PIL import Image from torch.utils.data import Dataset from torchvision import transforms class ChangeDetectionDataset(Dataset): 遥感变化检测数据集每次返回一个图像对和对应的变化标签 def __init__(self, image_dir, label_dir, transformNone): self.image_dir image_dir self.label_dir label_dir self.transform transform # 假设文件名格式为 A_001.png前时相、B_001.png后时相、L_001.png标签 self.names sorted([f[2:] for f in os.listdir(image_dir) if f.endswith(.png)]) def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] img1 Image.open(os.path.join(self.image_dir, A_ name)).convert(RGB) img2 Image.open(os.path.join(self.image_dir, B_ name)).convert(RGB) label Image.open(os.path.join(self.label_dir, L_ name)).convert(L) # 同一组数据用同一个随机种子做增强保证前后时相和标签的空间位置对齐 if self.transform is not None: seed torch.randint(0, 10000, (1,)).item() torch.manual_seed(seed) img1 self.transform(img1) torch.manual_seed(seed) img2 self.transform(img2) torch.manual_seed(seed) label self.transform(label) return img1, img2, label这段代码里最值得注意的就是增强的同步前时相、后时相、标签三张图必须用同一个随机种子做翻转或裁剪否则图像对的空间位置就错位了模型学到的全是错位噪声。很多刚接触变化检测的同学直接在Dataset里对三张图分别transform结果标签和图像对不上训练loss怎么都降不下去查半天才发现是数据增强不同步。这个坑在遥感任务里属于高频翻车点。除了同步增强还有个细节label用convert(L)转成单通道灰度图因为变化标签本质就是二值图0表示没变1表示变了不需要三通道。有的实现会在这一步把label二值化比如把大于127的像素全部置为1这也是常见做法取决于数据集原始标注格式。2.3 为什么选U-Net做底座而不是普通U-Net项目名称里写的是改进UNet但摘要里明确提到嵌套U-Net嵌套的损失计算指向的就是U-NetNested U-Net结构。U-Net和普通U-Net的核心区别在于解码器不再只从同层编码器拿特征而是把中间层用密集跳跃连接全部串起来每个解码节点都接收来自不同分辨率的特征。这个设计对遥感建筑物变化检测有两个直接好处。第一建筑物边界模糊、阴影遮挡多多尺度特征融合能让模型同时看到高分辨率的边界细节和低分辨率的语义信息对小目标变化更敏感。第二U-Net天然支持深度监督deep supervision每个子网络输出都能算一次损失梯度可以直接传到浅层编码器收敛更快。项目里说的嵌套的损失计算就是这个机制。编码器选VGG19核心原因是迁移学习。VGG19在ImageNet上训练好的权重非常成熟torchvision一行就能加载感受野较大、层级规整作为遥感特征的编码器效果稳定。而且项目里专门给了VGG19_encoder.tex说明作者在论文里对编码器结构做了完整描述方便你复现和改写。后面在核心算法章节我会具体讲VGG19编码器怎么嵌进模型以及怎么换掉它。3. 核心算法拆解五处改进分别改在哪、为什么这么改这一章是整个项目最值钱的部分。毕设答辩时老师问你的创新点是什么答案就在这一章里。我把项目的五处改进按照编码器 → 注意力 → 网络结构 → 损失函数 → 工程优化的顺序拆开讲每一处都给出代码逻辑和参数含义方便你对照项目源码看。3.1 VGG19编码器迁移学习怎么省下大量训练时间项目里专门有一个VGG19_encoder.tex文件说明文档里用LaTeX写了VGG19编码器的结构表。代码里VGG19承担的是编码器角色负责把输入图像逐步下采样成高维特征图。之所以不随机初始化直接从零训练是因为遥感图像虽然和自然图像有差异但底层特征边缘、纹理、形状是通用的用ImageNet预训练权重做初始化相当于让模型站在一个已经会看图的起点上继续学。加载VGG19的常见做法是直接用torchvision然后按MaxPool的位置把features切成5个stage每个stage对应编码器的一层分辨率输出import torch import torch.nn as nn from torchvision.models import vgg19_bn def build_vgg19_encoder(pretrainedTrue): 构建VGG19编码器按MaxPool位置切分成5个stage vgg vgg19_bn(pretrainedpretrained) features vgg.features stages [] current [] # 遇到MaxPool2d就认为当前stage结束把收集到的层打包成一个Sequential for layer in features: if isinstance(layer, nn.MaxPool2d): current.append(layer) stages.append(nn.Sequential(*current)) current [] else: current.append(layer) return nn.ModuleList(stages) def freeze_encoder_stages(encoder, frozen_stages4): 冻结前 frozen_stages 个stage的参数只训练后面的层 for i, stage in enumerate(encoder): if i frozen_stages: for p in stage.parameters(): p.requires_grad False return encoderpretrainedTrue表示加载ImageNet预训练权重如果网络下载困难可以先把权重文件下载到本地再把pretrained设为False最后手动load_state_dict。frozen_stages控制冻结前几个stage按我的经验编码器前4个stage可以冻结只微调最后一个stage和整个解码器这样显存占用和训练时间都能明显下降且不会让浅层特征被遥感图像的大分布差异带偏。这个值是经验参数项目里可能直接固定了某个数你可以按自己的数据集规模来调。注意遥感图像如果包含近红外等额外波段输入通道就不是3了。此时需要把VGG19第一个卷积层改成对应通道数并将该层权重做平均或复制处理。项目里如果用的是普通RGB影像则不需要这一步。3.2 SE模块在哪个位置插入通道注意力SE模块是Squeeze-and-Excitation的缩写核心思想是让网络自动学习每个特征通道的重要程度把重要通道放大、不重要通道抑制。在遥感变化检测里建筑物和道路、裸地在某些波段上特征很像通道注意力能帮助模型提高区分性强的特征通道的权重。嵌入位置的选择比SE模块本身实现更关键。常见位置有三个编码器每个stage输出后、解码器每次上采样后、以及跳跃连接上。项目里采用的是第一类思路在每个编码器stage输出后插入SE模块。理由是编码器特征经过注意力重标定后再传给解码器能在特征融合前就把噪声通道压下去。SE模块的实现不算复杂import torch import torch.nn as nn class SELayer(nn.Module): Squeeze-and-Excitation通道注意力模块 def __init__(self, channel, reduction16): super(SELayer, self).__init__() # Squeeze全局平均池化把每个通道压缩成一个标量 self.avg_pool nn.AdaptiveAvgPool2d(1) # Excitation两个全连接层学习通道权重中间用ReLU最后用Sigmoid输出0~1权重 self.fc nn.Sequential( nn.Linear(channel, channel // reduction, biasFalse), nn.ReLU(inplaceTrue), nn.Linear(channel // reduction, channel, biasFalse), nn.Sigmoid() ) def forward(self, x): b, c, _, _ x.size() # 得到每个通道的权重形状为 (b, c, 1, 1) y self.avg_pool(x).view(b, c) y self.fc(y).view(b, c, 1, 1) # 与原始特征逐通道相乘 return x * y.expand_as(x)reduction参数控制中间层压缩比例16是论文常用的默认值代表第一个全连接层把通道数压缩到1/16。这个值不是越大越好通道数本来就少的浅层stage用16可能导致中间层维度太小信息损失过多所以有些实现会对不同stage做适配浅层用8深层用16。SE模块的参数量很小一个stage插入一个整个网络显存开销增加不到1%属于性价比非常高的改进。3.3 嵌套U-Net与剪枝训练和推理用两套结构项目里说的嵌套U-Net嵌套的损失计算和剪枝技术是U-Net最标志性的两个设计。理解这两个基本就理解了整个模型骨架。嵌套结构指解码器不是简单的倒金字塔而是有多条不同深度的解码路径。模型同时维护多个尺度的特征节点每个节点既接收编码器同尺度的特征也接收来自相邻更小尺度上采样后的特征。这种密集连接让每个解码节点都拥有多感受野信息。训练阶段模型对多个子网络分支都计算损失并求和梯度从多个深度回传推理阶段则只保留最深层主路径其余子网络全部剪掉。这样做的结果是训练更充分、收敛更稳而推理速度几乎和普通U-Net一样。def forward(self, x1, x2, is_trainingTrue): # 双时相输入在通道维拼接变成6通道输入 x torch.cat([x1, x2], dim1) # 编码器逐stage提取特征x_0_0到x_4_0表示不同分辨率层级 x_0_0 self.encoder_stage1(x) x_1_0 self.encoder_stage2(x_0_0) x_2_0 self.encoder_stage3(x_1_0) x_3_0 self.encoder_stage4(x_2_0) x_4_0 self.encoder_stage5(x_3_0) # 解码器密集跳跃连接x_i_j表示第i层第j个节点 x_0_1 self.up_conv1_2(torch.cat([x_0_0, self.up_1_0(x_1_0)], dim1)) x_1_1 self.up_conv2_2(torch.cat([x_1_0, self.up_2_0(x_2_0)], dim1)) x_2_1 self.up_conv3_2(torch.cat([x_2_0, self.up_3_0(x_3_0)], dim1)) x_3_1 self.up_conv4_2(torch.cat([x_3_0, self.up_4_0(x_4_0)], dim1)) x_0_2 self.up_conv1_3(torch.cat([x_0_0, x_0_1, self.up_1_1(x_1_1)], dim1)) x_1_2 self.up_conv2_3(torch.cat([x_1_0, x_1_1, self.up_2_1(x_2_1)], dim1)) x_2_2 self.up_conv3_3(torch.cat([x_2_0, x_2_1, self.up_3_1(x_3_1)], dim1)) x_0_3 self.up_conv1_4(torch.cat([x_0_0, x_0_1, x_0_2, self.up_1_2(x_1_2)], dim1)) x_1_3 self.up_conv2_4(torch.cat([x_1_0, x_1_1, x_1_2, self.up_2_2(x_2_2)], dim1)) x_0_4 self.up_conv1_5(torch.cat([x_0_0, x_0_1, x_0_2, x_0_3, self.up_1_3(x_1_3)], dim1)) if is_training: # 训练阶段返回多个子网络输出分别参与损失计算 return [x_0_1, x_0_2, x_0_3, x_0_4] else: # 推理阶段剪枝只保留最深层主路径速度和普通U-Net一致 return x_0_4这个forward逻辑就是U-Net的浓缩版。is_training是关键开关训练时返回四个不同深度的输出验证或推理时只返回最深的x_0_4剪枝就在这一步体现。实际项目里代码可能会用循环和模块列表来组织避免写这么长一串但核心思想完全一致——训练多监督、推理单路径。需要提醒的是训练时返回多个输出意味着损失函数也要对应写成嵌套损失这一点下一节会细讲。3.4 加权FocalLoss类别不平衡的应对方案遥感变化检测里没有变化的像素通常占绝对多数变化区域可能只占整张图的百分之几甚至更少。用普通交叉熵损失模型只要全部预测成没变化就能拿到很低的loss但实际检测能力为零。FocalLoss就是为这种极端类别不平衡设计的。FocalLoss在交叉熵基础上加了一个调制因子(1-p_t)^gamma让模型更关注难分类样本也就是那些预测概率不高但确实是变化的像素。项目里还提到了加权我的理解是在FocalLoss基础上再叠加一个类别权重系数给正类别更高的权重双管齐下import torch import torch.nn as nn import torch.nn.functional as F class WeightedFocalLoss(nn.Module): 加权FocalLossalpha控制正负样本权重gamma控制难易样本权重 def __init__(self, alpha0.75, gamma2.0): super(WeightedFocalLoss, self).__init__() self.alpha alpha self.gamma gamma def forward(self, pred, target): # pred是sigmoid输出target是0/1标签 bce F.binary_cross_entropy(pred, target, reductionnone) p_t pred * target (1 - pred) * (1 - target) # 预测正确的概率 modulating_factor (1.0 - p_t) ** self.gamma # 类别权重alpha给正样本(1-alpha)给负样本 alpha_weight self.alpha * target (1 - self.alpha) * (1 - target) loss alpha_weight * modulating_factor * bce return loss.mean()alpha0.75的意思是正类别变化区域的损失权重是0.75负类别是0.25相当于把变化像素的重要性放大了三倍。gamma2.0是论文里的标准值gamma越大模型对已经分对的简单样本越不敏感。实际调参时我会先用alpha0.5、gamma0跑一遍这等价于普通加权交叉熵再逐步调大gamma观察验证集F1变化而不是一上来就跳到一个激进组合。项目摘要里注明部分实验已完成说明加权FocalLoss还不是最终定稿版本这部分改起来空间很大后面二次开发章节我会展开讲。3.5 显存优化从CUDA OOM到稳定训练摘要里特别写了一条在训练和验证阶段一定程度上解决CUDA Out of memory这几乎是每个跑遥感分割的人都会撞上的问题。高分辨率遥感图像动辄几百MB一张即使裁剪成256x256batch稍大就会爆显存。常见解法按性价比排序是梯度累积、混合精度训练、验证时关闭梯度计算、减小输入尺寸。项目里一定程度上解决我判断是组合了其中两种以上。梯度累积的思路是把一个大batch拆成多个小batch先算梯度不更新累积几步后再更新一次参数效果上接近大batch但显存峰值只有小batch的水平accumulation_steps 4 # 累积4个batch再更新一次参数 optimizer.zero_grad() for i, (img1, img2, label) in enumerate(train_loader): img1, img2, label img1.cuda(), img2.cuda(), label.cuda() outputs model(img1, img2, is_trainingTrue) # 取最后一个子网络输出算主损失浅层分支的损失按嵌套损失逻辑叠加 loss weighted_focal_loss(outputs[3], label) / accumulation_steps loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()注意loss除以了accumulation_steps如果不除等效batch size变大但学习率没变训练会不稳定。混合精度训练是另一个高性价比方案用torch.cuda.amp可以把浮点计算降到16位显存几乎减半训练速度在支持Tensor Core的显卡上还能提升。项目大概率用了AMP因为这是解决OOM最直接的手段之一。验证阶段则务必用with torch.no_grad():包住整个验证循环这是最常见的显存杀手下一章实操部分会再提到。4. 训练与验证实操两个脚本加一个绘图脚本怎么配合这一章解决的是怎么把人家的项目跑起来。我会从训练入口的参数配置讲起再到验证阶段的指标计算最后是LineGraph.py怎么把日志变成论文插图。照着这个顺序走一遍基本就能把整个项目跑通。4.1 训练入口参数配置与数据准备打开LEVIR_train.py从上往下扫一般依次是导入依赖、定义全局参数、加载数据、构建模型、训练循环。全局参数通常集中在文件开头定义下面列一下典型的参数和合理取值参数常见取值说明learning_rate1e-3 到 1e-4用了预训练编码器时建议从1e-4起步batch_size4 到 16取决于显存梯度累积可以让它设小num_epochs100 到 200遥感变化检测数据量不大训练一般不会太久image_size256 或 512裁剪尺寸越大细节越好但显存占用越高loss_typeweighted_focal 或 bcedice项目核心损失是加权FocalLosssave_dir./weights权重和日志输出目录训练循环里建议重点关注三件事模型切换train模式、梯度清零和更新的时机、每个epoch结束保存最优权重。最优权重的判定标准一般是验证集F1最高而不是训练loss最低因为loss低只代表拟合了训练集。训练脚本的骨架常见这样model.train() for epoch in range(start_epoch, num_epochs): epoch_loss 0.0 for i, (img1, img2, label) in enumerate(train_loader): img1 img1.cuda() img2 img2.cuda() label label.cuda().float() outputs model(img1, img2, is_trainingTrue) # 嵌套损失四个子网络输出都参与深层分支权重更大 loss 0.1 * criterion(outputs[0], label) \ 0.1 * criterion(outputs[1], label) \ 0.3 * criterion(outputs[2], label) \ 0.5 * criterion(outputs[3], label) loss.backward() optimizer.step() optimizer.zero_grad() epoch_loss loss.item() scheduler.step(epoch_loss) torch.save(model.state_dict(), f./weights/epoch_{epoch}.pth)嵌套损失的权重分配是按常见做法给的项目里可能直接取平均也可能只取最后一层的loss。核心逻辑是浅层子网络输出分辨率更高但语义偏弱所以权重低深层子网络语义强权重高。如果你在二次开发时改了模型深度这组权重也要相应调整否则梯度会被某个分支主导。训练过程中建议同时把每个epoch的loss和F1记录到txt或csv文件里后面LineGraph.py要用。4.2 验证阶段指标计算与结果保存LEVIR_vaild.py要做的事情是加载训练好的权重对验证集逐张预测计算Precision、Recall、F1和IoU。变化检测本质上是逐像素二分类所以这些指标和语义分割完全一致。注意一个细节模型输出的是概率图不是最终的0/1标签必须选一个阈值把概率图二值化。固定阈值0.5最省事但更稳妥的做法是在验证集上遍历0.1到0.9选F1最高的那个阈值。这个操作往往能让F1涨1到2个点项目里如果没做你完全可以自己补上。def validate(model, val_loader, threshold0.5): model.eval() # 切到eval模式让BN层用全局统计量 f1_scores [] with torch.no_grad(): # 关闭梯度计算省显存且加快速度 for img1, img2, label in val_loader: img1, img2, label img1.cuda(), img2.cuda(), label.cuda() prob model(img1, img2, is_trainingFalse) # 剪枝后的单路径输出 prob torch.sigmoid(prob) pred (prob threshold).float() tp ((pred 1) (label 1)).sum().float() fp ((pred 1) (label 0)).sum().float() fn ((pred 0) (label 1)).sum().float() precision tp / (tp fp 1e-6) recall tp / (tp fn 1e-6) f1 2 * precision * recall / (precision recall 1e-6) f1_scores.append(f1.item()) return sum(f1_scores) / len(f1_scores)threshold既是阈值参数也是调试参数预测结果里变化区域明显偏多时调大threshold能减少虚检漏检严重时调小一些。这个参数在写论文做对比实验时很有用你可以在相同模型下通过调整阈值得到P-R曲线的不同位置甚至画出PR曲线放在论文里。验证阶段最常见的问题有两个一是忘了model.eval()导致BN层统计量错乱二是忘了torch.no_grad()导致显存被翻倍占用。项目既然专门提了解决验证阶段的OOM说明这两点已经处理过了你改代码时要保持住。4.3 可视化LineGraph.py把日志变成论文插图LineGraph.py一般负责画两类曲线训练/验证loss曲线以及验证F1曲线。它通常直接读取训练时保存的日志文件用matplotlib画图。对毕设来说这个脚本的价值是省去手动记录数据的麻烦论文里的模型收敛性分析那一节正好需要这几张图。运行方式一般是python LineGraph.py它会读取指定路径下的日志文件然后生成png图片。如果执行后提示找不到数据先检查日志文件路径和文件名是否匹配。这类脚本通常把路径写死在文件开头找不到就改成实际路径。画图时建议把训练loss和验证loss画在同一张图里用两个不同颜色区分这样能直观看到模型是否过拟合——如果验证loss先降后升而训练loss一直降就是典型的过拟合信号需要加正则或者缩小模型容量。到现在为止训练、验证、可视化三个环节已经全部串起来了。整个实验流程基本不需要手工干预接下来就可以开始改模型、跑对比实验了。5. 避坑记录遥感变化检测从源码到跑通的五个常踩问题这一章把我自己复现这类项目时踩过的坑整理一下全部按照现象 → 原因 → 解决来写方便你遇到问题时快速对照排查。5.1 验证阶段CUDA out of memory现象训练没崩模型加载进验证脚本却直接报out of memoryGPU显存瞬间被吃满。原因验证循环里没有开torch.no_grad()模型前向计算仍然保存了中间梯度信息显存占用和训练时一样高或者验证时batch_size设置得比训练还大另一个常见原因是训练和验证交替进行前一个epoch的显存缓存没有释放。解决验证循环外面包with torch.no_grad():把验证batch_size降到1在训练代码里每个epoch结束用torch.cuda.empty_cache()清理缓存。跑之前用nvidia-smi确认没有其他进程占用显存这也是个容易被忽略的坑。5.2 训练loss在降验证F1却一直不动现象loss曲线稳步下降到0.2以下但验证F1始终在0.3左右徘徊模型看起来学会了但实际上没用。原因类别不平衡导致的假收敛。数据集里不变像素占绝大比例模型把所有像素预测成不变就能拿到很低的loss但变化区域几乎没被召回F1自然上不去。这是变化检测最典型的翻车方式。解决分别看验证集的precision和recall。如果recall极低说明模型在正类别上几乎失效把加权FocalLoss的alpha调大让正样本的梯度占比更高。同时检查threshold是否过大很多情况下F1卡住只是二值化阈值选得太高了。5.3 预测概率图看不出变化区域现象模型输出的概率图颜色非常单一要么全部接近0要么全部在0.5附近看不到明显的变化区域高亮。原因模型最后一层没有配合sigmoid做输出。常见错误是在训练时损失函数内部做了sigmoid计算但验证时直接输出logits没有再过sigmoid或者权重加载路径不对加载了还在训练早期的中间权重。解决确认训练和验证对模型输出的处理一致。训练时如果用了带sigmoid的损失函数验证时就要对logits显式做sigmoid如果想看中间结果加载最后一个epoch保存的权重而不是看第一个保存的。这类问题排查顺序先确认权重路径再确认sigmoid位置最后确认数据归一化方式一致。5.4 加载预训练权重报size mismatch现象加载VGG19或模型权重时报size mismatch for xxx: expected channel ... got channel ...。原因最常见的是改动过模型输入通道数或解码器输出维度导致当前模型定义和保存的权重对不上。比如把三通道输入改成四通道加了近红外波段或者修改了解码器结构后输出通道数和旧权重不一致。解决如果只是输入通道变了可以先加载预训练权重再对被改动的卷积层做通道复制或截断处理。更省事的做法是冻结编码器前四层跳过与改动层相关的权重让它们从头学。如果模型结构和原版完全一致则优先检查是不是加载了不同epoch的权重文件。5.5 换了更大的GPU反而训练变慢现象从4GB显存换到12GB的卡batch调大了但每个epoch时间反而变长甚至明显变慢。原因batch变大后迭代次数变少但每次迭代的数据加载和增强时间也变长了GPU处理完一批后在等数据训练变成IO密集型而不是计算密集型。加上学习率没按batch比例调整收敛速度也不理想。解决batch_size翻倍后学习率通常也要按线性缩放规则相应调大比如batch从4变8学习率从1e-4调到2e-4。同时把DataLoader的num_workers调高让数据加载并行。显存有富余时优先加大输入分辨率而不是batch因为分辨率对遥感小目标检测的收益更直接。6. 二次开发思路改结构、换损失、做对比实验的具体步骤把这套代码改造成你自己的实验框架核心就三件事换编码器、组合损失函数、管理好每次实验。这三件事做扎实毕设的对比实验和消融实验就都有素材了。6.1 把VGG19换成ResNet34或EfficientNet做消融实验答辩时老师几乎必问为什么选VGG19所以你需要对比实验来支撑选择。换编码器的方式很直接把build_vgg19_encoder里面的vgg19_bn换成resnet34去掉最后的avgpool和fc把stage1到stage4作为编码器的四个阶段通道数分别是64、128、256、512需要与U-Net解码器的通道定义一一对齐。如果换EfficientNet用timm库更省事timm.create_model(efficientnet_b0, features_onlyTrue)会自动返回每个stage的输出。我一般会写一个编码器工厂函数用一个字符串参数控制加载哪个backbone这样对比实验只需要改配置不用改模型代码。6.2 损失函数改成FocalLoss Dice组合加权FocalLoss处理类别不平衡很有效但缺点是对区域一致性和边界平滑没有约束。DiceLoss对前景区域的重叠度敏感两者组合可以互补。最终loss可以写成criterion 0.7 * focal_loss 0.3 * dice_loss两个权重用验证集来调。改的时候注意DiceLoss在变化区域为空的图上会算出0/0实现时要加平滑项防止反向传播出现NaN。建议先单独跑加权FocalLoss做基线再跑组合损失两边F1对比才有说服力。6.3 实验管理每个实验留下可回溯的记录最后分享三个让实验不迷路的习惯第一每个实验建一个独立目录保存模型权重、训练日志和当时的参数配置不要所有权重堆在一个文件夹里第二在训练脚本里记录文件哈希或者手动写一行注释说明本次改动方便回溯到底改了哪些地方第三验证集上除了F1还要保存precision和recall这样能判断模型是偏向漏检还是虚检调参时方向更明确。很多人在毕业设计里吃亏不是模型不够好而是改了一堆东西最后说不清楚自己到底改了什么。从那以后我每次改网络结构都强制先写一行注释说明改动目的再动代码每个实验跑到一半把当时的loss曲线截图存档方便对比。希望帮到你。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号