简介:面向遥感图像语义分割任务,这份zip资源提供了结合自注意力机制、膨胀卷积与高分辨率网络HRNet的完整实现,适合研究生、算法工程师及遥感应用开发者用于地物分类、变化检测等精细解译场景。包内共100个文件,以61个Python源码为主,包含网络定义、训练与推理脚本;另有26个编译后的pyc文件,以及C++/CUDA扩展源码、XML/IML工程配置、LICENSE等,便于直接运行或二次开发,压缩包仅145KB,轻量易部署。已有191人学习下载。资源内可找到网络核心结构、训练入口与底层加速实现等关键模块,理解“注意力+膨胀卷积+HRNet”的融合方式,并能加载代码库对自有遥感影像执行语义分割;同时,通过阅读底层扩展源码可深入优化加速细节,为后续调整模型结构、膨胀率或训练流程提供完整参考。
1. 高分辨率还能不能打:把注意力机制和膨胀卷积装进 HRNet
遥感图像语义分割和普通自然图像分割最大的差异在于,目标尺度跨度极大且背景极度复杂。一颗树的阴影可能只有几个像素,而一片农田的边界却要依赖上下文纹理来闭合。把 512×512 的输入直接扔进分类网络后接上采样,边缘细节基本就没救了。所以遥感赛道里,捍卫高分辨率特征不是风格问题,是命门。HRNet 从第一层开始就把高分辨率分支保留到底,天然适合遥感这种“小目标密度高、边界语义弱”的数据。但 HRNet 也吃亏——感受野偏小,多尺度上下文不够用,纯靠卷积抓不住大范围依赖。
把通道注意力、空间注意力和膨胀卷积按正确的姿势组合进去,等于在高分辨率骨架上同时补了“看哪里”和“看多大”两块短板。这篇文章直接把一条能落地的方案讲透:结构怎么改、损失怎么配、膨胀率怎么设、推理时影像太大怎么切块。不整花活,只讲遥感语义分割里真正会遇到的取舍。
2. HRNet 能成为遥感语义分割骨架,靠的不是“高分辨率”三个字
2.1 HRNet 和 ResNet 系骨架在高分辨率保持上的本质差异
先看 ResNet 系的做法:输入图像逐步降采样,到 1/32 分辨率提语义,再靠上采样把空间信息找回来。这一去一回,边缘和细碎目标的响应已经被平均掉了。遥感图像里的房屋边缘、裸土和植被过渡带,恰恰是这类高频信息。
HRNet 把这条路反过来了。它不是先降再升,而是从一开始就用一个高分辨率分支,后续每下一层分辨率就新开一个分支,四个分支彼此并行。关键操作是重复的多分辨率融合:每个 stage 结束,不同分辨率的特征互相交换信息。高分辨率分支接收低分辨率分支带来的语义上下文,低分辨率分支则拿到高分辨率的空间细节。因为高分辨率分支从第一层到最后一层始终存在,所以空间位置信息从来没被整体丢掉。
# 以 torch 伪代码表示 HRNet 分支融合的核心理念 def fuse_branches(branches): # branches 是不同分辨率的特征列表 target_shape = branches[0].shape[-2:] # 以最高分辨率分支为准 fused = branches[0] for feat in branches[1:]: if feat.shape[-2:] != target_shape: feat = F.interpolate(feat, size=target_shape, mode='bilinear', align_corners=False) fused = fused + feat return fused这段代码代表的是最朴素的融合方式:直接上采样后相加。真实 HRNet 里用的是可学习的 1×1 卷积完成分辨率对齐,但思想一致——把低分辨率分支的语义信息逐步“喂”回高分辨率分支。参数上,HRNetV2-W18 大约 21M 参数,HRNetV2-W48 约 66M,比同量级 ResNet 略重,但换来的是特征图分辨率整体抬高。
2.2 HRNet 输出特征应该怎么取:V1 和 V2 的差异
语义分割任务上,到底取哪个分支的输出,很多人一开始会搞错。HRNetV1 只保留高分辨率分支输出,用来做关键点检测没问题;HRNetV2 把四个分支全部上采样到最高分辨率再 concat,再接分割头。遥感语义分割要用 V2 的输出,原因很简单:低分辨率分支带的是全局语义,高分辨率分支带的是局部细节,concat 之后分割头能在同一组特征里同时看到两者。
实际使用时建议这样组织解码器输入:
# 取 HRNetV2 的四个层输出 feat1, feat2, feat3, feat4 = hrnet(x) # 分辨率分别为 1/4, 1/8, 1/16, 1/32 for i, feat in enumerate([feat2, feat3, feat4]): size = feat1.shape[-2:] feat = F.interpolate(feat, size=size, mode='bilinear', align_corners=False) features.append(feat) # 输出通道数 = sum(各分支通道数),再接分割头这里有个参数细节值得注意:align_corners=False是分割任务的标准选择,原因是它把像素中心对齐,而不是角点对齐,对边缘的偏移影响更小。
2.3 遥感场景下 HRNet 的已知短板:感受野不足与计算量问题
HRNet 高分辨率分支的卷积核基本是 3×3,堆叠再多层,感受野也是线性增长。遥感图像里一个 1024×1024 的影像,可能同时包含几米宽的屋顶和几百米宽的湖泊。要让网络“看到”湖泊的整体边界,感受野必须覆盖足够大的区域——这是 HRNet 原版架构最别扭的地方。
另一个问题是计算量。高分辨率分支始终保持在 1/4 分辨率,以 512×512 输入为例,这个分支的特征图是 128×128×C,通道数又不可能太少,整体 FLOPs 明显高于 ResNet-50 类骨架。所以遥感场景里直接替换原版 HRNet 会发现训练速度和显存消耗都上了一个台阶。这不是 HRNet 的缺陷,而是高分辨率策略本身的代价,解决方向不是砍掉高分辨率分支,而是用注意力机制和膨胀卷积把有限的计算用到刀刃上。
3. 注意力机制不是“加个模块”那么简单:从通道到空间的遥感适配
3.1 SE、CBAM、CA 和自注意力到底各自适合解决什么问题
很多人在 HRNet 的每个 block 后面无脑接一个 CBAM,训完发现涨点有限。原因很现实——注意力机制的作用范围不同,解决的问题也不同。
SE 注意力机制只做通道维度的重标定:全局平均池化得到通道描述向量,经过两个全连接层学习通道间的依赖关系。对遥感图像而言,SE 能帮网络区分“植被绿”和“水体蓝”这些通道响应模式,但它完全不做空间上的选择。CBAM 在 SE 的基础上加了空间注意力分支,用通道均值池化和最大值池化拼出空间注意力图,能告诉网络“哪个位置更重要”——但它的空间注意力本质上是局部操作,对长距离依赖没有建模能力。
CA 注意力机制则是把空间信息分别沿水平和垂直方向编码,对遥感图像特别有意义。因为遥感目标往往具有方向性,比如道路沿某一方向延展,CA 能把这种位置感知编码进通道权重。自注意力机制(包括多头自注意力原理)能建模任意像素之间的长程依赖,但计算复杂度是 O(HW)²,遥感影像动辄上百万像素,直接用在特征图上会直接爆显存。
# CA 注意力的核心:对空间维度做两个方向的池化 def coordinate_attention(x): b, c, h, w = x.shape # 沿高度方向全局平均池化,输出形状 [b, c, h, 1] x_h = x.mean(dim=3, keepdim=True) # 沿宽度方向全局平均池化,输出形状 [b, c, 1, w] x_w = x.mean(dim=2, keepdim=True) # 后续拼接、卷积、分离,再与原始特征相乘CA 对道路、河流这类定向目标非常有效,而 CBAM 更适合目标尺度均匀的场景。这意味着选择注意力模块前,要先看你的标注数据里哪种目标占主导,而不是照搬论文里的结构。
3.2 在 HRNet 的哪个位置插入注意力最划算
这是整个结构设计里最关键的问题。在 HRNet 的每个 basic block 后加 CBAM,计算开销增加 10% 到 20%,但涨点可能只有 1 个 mIoU。我通常会分三档来插:
第一档是在 1/8 分辨率及其以下分支的 block 后加轻量 SE 或 CA。理由是这些分支负责语义信息提取,通道重标定能强化类别相关的响应。第二档是在融合阶段之后、进入下一 stage 之前加一个 CBAM 或坐标注意力,这时特征图包含了多分辨率信息,注意力能直接做跨尺度的特征选择。第三档是分割头前——这是收益最稳定的一处,因为解码器的输入来自四个分支的 concat,通道数多、信息冗余大,在分割头前加一个通道注意力机制来压缩冗余,效果会在 mIoU 上稳定体现。
需要注意的是别在一个分支里反复堆叠多种注意力模块。通道注意力机制和自注意力机制各有建模侧重,叠多了只会让梯度传播路径变长,遥感数据量本来就有限,很容易过拟合。
3.3 一个可复现的 CBAM 实现与参数设置
CBAM 的实现不复杂,但两个全连接层的压缩比设置直接影响效果:
import torch import torch.nn as nn class ChannelAttention(nn.Module): def __init__(self, in_channels, reduction=16): super().__init__() self.mlp = nn.Sequential( nn.Conv2d(in_channels, in_channels // reduction, 1, bias=False), nn.ReLU(inplace=True), nn.Conv2d(in_channels // reduction, in_channels, 1, bias=False), ) self.sigmoid = nn.Sigmoid() def forward(self, x): avg_out = self.mlp(x.mean(dim=(2, 3), keepdim=True)) max_out = self.mlp(x.amax(dim=(2, 3), keepdim=True)) return self.sigmoid(avg_out + max_out) class SpatialAttention(nn.Module): def __init__(self, kernel_size=7): super().__init__() self.conv = nn.Conv2d(2, 1, kernel_size, padding=kernel_size // 2, bias=False) self.sigmoid = nn.Sigmoid() def forward(self, x): avg_out = x.mean(dim=1, keepdim=True) max_out = x.amax(dim=1, keepdim=True) return self.sigmoid(self.conv(torch.cat([avg_out, max_out], dim=1)))reduction=16是经验值,遥感图像类别多且语义相近时,比如区分草地和耕地,建议把 reduction 降到 8,保留更多通道描述能力。空间注意力卷积核大小设为 7,覆盖范围更大,对边缘过渡带更敏感;但如果目标是道路这类细长结构,kernel_size=3 反而更好,因为大卷积核会把相邻的干扰信息也融合进来。
4. 膨胀卷积怎么接才不破坏 HRNet 的高分辨率优势
4.1 膨胀卷积的语义:感受野扩大而不降低分辨率
普通卷积在 stride=1 的情况下,每层只让感受野线性增长。膨胀卷积通过在卷积核内填充空洞来跳着采样输入,膨胀率为 d 的 3×3 卷积,实际感受野相当于 (2d+1)×(2d+1),而参数量和计算量不变。
对 HRNet 而言,膨胀卷积是补感受野短板最自然的手段——因为它的特征图分辨率本来就高,不需要通过降采样来扩大感知范围。问题在于怎么选膨胀率。
我用一个很直观的准则来处理:每个 level 的分支配一组膨胀率,1/4 分辨率分支用小膨胀率,因为它的特征图大,盲目用大膨胀率会引入太多无效背景信息;1/16 和 1/32 分支用大膨胀率,它们是语义信息的主载体。
# 建议的分支膨胀率配置 dilation_config = { 'low_level': [1, 2], # 1/4 分辨率分支 'middle_level': [2, 4], # 1/8 分辨率分支 'high_level': [4, 8], # 1/16 和 1/32 分支 }4.2 用 ASPP 还是手动替换卷积:一个结构上的取舍
直接把 HRNet 里所有 3×3 卷积换成膨胀卷积,会带来一个严重问题——网格伪影。当多个膨胀卷积串联且膨胀率互为倍数时,特征图会出现棋盘格状的信息丢失。这是遥感图像分割里最容易被忽略的结构性坑。
解决方式有两种,一种是在低分辨率分支上叠加 ASPP 模块,用并行多膨胀率的卷积组合,最后融合;另一种是手动把特定位置的卷积替换为膨胀卷积,但必须控制膨胀率的组合方式。实操中 ASPP 更稳,因为它把不同膨胀率的特征融合起来,网格伪影可以被其他分支的信息填充。
# 简易 ASPP 模块,用于替换 HRNet 最小分辨率分支后的解码器输入 class ASPP(nn.Module): def __init__(self, in_channels, out_channels, rates=[1, 6, 12, 18]): super().__init__() self.branches = nn.ModuleList() for rate in rates: if rate == 1: self.branches.append(nn.Conv2d(in_channels, out_channels, 1, bias=False)) else: self.branches.append( nn.Conv2d(in_channels, out_channels, 3, padding=rate, dilation=rate, bias=False)) self.project = nn.Sequential( nn.Conv2d(out_channels * len(rates), out_channels, 1, bias=False), nn.BatchNorm2d(out_channels), nn.ReLU(inplace=True)) def forward(self, x): feats = [branch(x) for branch in self.branches] return self.project(torch.cat(feats, dim=1))ASPP 的四个膨胀率固定为 1、6、12、18,这是 DeepLab 系列沉淀下来的配置。它在 512×512 输入下能覆盖大约 60 到 100 像素范围的感受野跨度,对遥感图像里的农田地块、道路交叉口这类中尺度目标足够用。如果目标更大,比如要分割整片水域,更建议把 input 尺寸调大,而不是无限加大膨胀率。
4.3 膨胀卷积和注意力的协同方式:先筛选再扩大
结构顺序上,我建议在 HRNet 融合后的特征上先做注意力加权,再接膨胀卷积提取多尺度上下文。逻辑是注意力先告诉网络哪些位置值得看,膨胀卷积再把这些位置周围的信息放大收集进来。反过来先膨胀后注意力也行,但效果往往不如前者——膨胀卷积会把噪声也放大,注意力来不及做筛选。
5. 遥感数据集的标注与预训练策略,决定了模型上限
5.1 遥感图像标注的现实约束与最小样本方案
遥感语义分割数据集标注成本比自然图像高得多。一般流程是先在目标区域用简易工具勾出类别边界,再用 GEE(Google Earth Engine)导出历史影像做预标注,最后人工修正。但精度要求高的任务,比如建筑物提取,还是得手动标注。样本量不足时,常见做法是在 ImageNet 预训练的 HRNet 基础上微调。然而遥感图像的光谱分布和自然图像相差很大,尤其多光谱影像,通道数都对不上,直接复用 ImageNet 权重在首层是无效的。
一个有效的最小样本方案是:如果数据量少于 5000 张,优先冻结 HRNet 前两个 stage,只训练后面的 stage 和解码器,让网络先学遥感特有的纹理结构,再解冻全部层做整体微调。这里有个参数经验供参考:初始学习率用 0.001 的十分之一,即 0.0001。
# 冻结前两个 stage 的参数 for name, param in hrnet.named_parameters(): if name.startswith('stage1') or name.startswith('stage2'): param.requires_grad = False5.2 训练过程的必备设置:Loss、学习率与类别不均衡
遥感语义分割最常见的训练问题是类别不均衡。建筑物、道路这类目标在整幅影像里占比往往不到 10%,而植被、裸土可能占 60% 以上。光用交叉熵损失,模型会偏向多数类。实践上我常用 Lovasz-Softmax 或 Dice Loss 与交叉熵按 1:1 加权组合。
# 组合损失:交叉熵 + Dice Loss def combined_loss(logits, target, epsilon=1.0): ce = nn.functional.cross_entropy(logits, target) pred = torch.softmax(logits, dim=1) target_onehot = F.one_hot(target, num_classes=pred.shape[1]).permute(0, 3, 1, 2).float() dice = 1 - (2.0 * (pred * target_onehot).sum() + 1.0) / (pred.sum() + target_onehot.sum() + 1.0) return ce + epsilon * diceepsilon控制 Dice Loss 的比重,建议从 1.0 开始,如果验证集 mIoU 提升缓慢,可以加大到 2.0 甚至 3.0。学习率的调整策略推荐用 poly 衰减,lr * (1 - iter/total_iter)^0.9,比 StepLR 在遥感数据上更平滑。优化器选 AdamW,weight decay 设为 0.01 或 0.05,比 Adam 默认的 0 更稳。
6. 推理阶段的几个硬经验:切块策略、评价指标与模型导出
遥感影像通常是几千乘几千的大图,HRNet 加上注意力机制和膨胀卷积,整图推理显存会直接爆掉。标准做法是滑窗切块:将大图切成 512×512 或 1024×1024 的重叠块,重叠率一般设为 10% 到 20%,推理完再拼回去。拼回去时重叠区域取平均值,可以减轻边缘伪影。
切块大小直接决定 mIoU 指标的表现。一个小目标是道路,块越小越容易保留局部连续性;大目标是水域,块太小会让模型丢失上下文,导致分割结果支离破碎。我的经验是,如果目标尺度特征差异极大,先用 1024×1024 跑一遍,把预测置信度低的地方再切小块重跑——这个策略比单纯调模型结构涨点更明显。
验证阶段不要只看 mIoU,遥感语义分割还要额外关注边界 F1-score 和类别 IoU 的方差。边界 F1-score 能反映注意力机制对边缘的改善程度,类别 IoU 方差则能暴露模型是整体均衡地变强,还是只提升了那几个占比大的类。
# 推理结束后统计各类别的 IoU,用 torchmetrics 输出报告 python eval.py --checkpoint best_model.pth --input_dir ./val_imgs --output_dir ./pred_masks --patch_size 1024 --overlap 0.2最后提醒一个经常翻车的地方:分块推理时 BatchNorm 的统计量会变化。一旦模型在单张图上做切块推理,BatchNorm 的 running_mean 和 running_var 会受单 batch 统计影响,结果和训练时不一致。解决方案是推理前把 BatchNorm 切换成 eval 模式(model.eval()),并确保用预训练时累积的全局统计量,而不是直接沿用训练模式下的 batch 统计。如果显存允许,用梯度累积等价增大 batch size,比调低分辨率更有利于稳定 BatchNorm 行为。
本文还有配套的精品资源,点击获取