恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Res2Net海陆分割实战:多尺度骨干与边界感知训练全解析
首页
资讯中心
/
Res2Net海陆分割实战:多尺度骨干与边界感知训练全解析
Res2Net海陆分割实战:多尺度骨干与边界感知训练全解析
发布时间:2026/10/11 2:46:51
简介遥感影像海陆分割是海岸线提取、海岛礁识别和近岸目标检测的重要基础但复杂背景与弱边界常导致分割不准。这份docx文档围绕该问题提出并系统介绍了基于Res2Net的多尺度海陆分割网络MSRNet。文档先概述传统阈值分割、活动轮廓模型等方法的不足再重点阐述MSRNet的编码-解码结构利用Res2Net提取多尺度特征借助压缩与注意力模块强化海陆弱边界信息并通过深度监督策略增强各尺度预测结果最终获得更清晰完整的边界输出。文中还包括两组不同海岸类型数据集的实验对比与评价指标涉及定量精度和边界完整性分析能为遥感图像处理、深度学习语义分割方向的研究者提供完整的方法思路与实验设计参考。资源为单篇docx文档压缩包内共1个文件大小约345KB当前已有128人学习下载适合作为论文写作、技术调研或方法复现的参考资料。1. 海陆分割为什么需要 Res2Net一个被低估的预处理枢纽海陆分割在遥感影像语义分割里看起来是最“简单”的任务只有海、陆两个类别不涉及复杂语义。但真实项目里把一张两亿像素的影像扔进模型跑出 99% 的像素准确率很容易交付时却可能被打回重做——因为海岸线偏了几十米或者浑浊的近岸水体被划成了陆地。这类任务的瓶颈从来不是“分类”而是多尺度下的边界表达几公里宽的大陆轮廓和几米宽的养殖塘堤坝出现在同一张图里。Res2Net 把多尺度感受野做进残差块内部配合解码器和边界感知损失是当前落地海陆分割最稳的骨干之一。这篇笔记写给正在做海岸带遥感、海洋测绘或 GIS 数据生产的人从选型、实现到调参和排坑一次讲透。2. Res2Net 为什么能压住海陆分割的尺度问题从残差块内部重新设计感受野2.1 海陆分割难在哪不止是“分出哪里是海”海陆分割的传统做法并不少。NDWI 水体指数、Otsu 阈值、边缘检测、区域生长在干净的外海影像上都能跑出不错的效果。但这些方法有一个共同弱点一旦遇到近岸环境就开始翻车。海水养殖塘、盐田、滩涂、防波堤、泥沙含量高的浑浊水体这些区域的光谱特征和陆地建筑、裸土高度相似纯靠阈值或指数根本没有稳定的可分性。更麻烦的是云影和不同太阳高度角带来的同物异谱同一个水塘在不同时相的影像里反射率能差出一大截。从标注角度看海陆分割的数据本身就带主观性。晒盐场的卤水池算海还是陆潮间带露出水面的滩涂算海还是陆我见过标注团队为这类问题开会对齐标准最后不得不定一条“以影像时相的水边线为准”的规则。也就是说这个任务本质上是遥感影像语义分割里的一个二分类问题但它比绝大多数多分类分割都更依赖边界质量。内部大块区域怎么分都行海岸线附近差一个像素在 GF-2 这种 0.8 米分辨率的影像上就是 0.8 米放到 1:1 万制图里就是不可接受的错位。另一个容易被忽略的难点是尺度跨度。一段完整的海岸带影像里大陆架轮廓可能是几千像素的连续用地类而养殖塘的堤坝只有三五个像素宽。常规语义分割网络靠堆层数扩大感受野但堆出来的感受野是“均匀”的它没法在同一层里同时响应粗轮廓和细结构。这就是 Res2Net 这类多尺度骨干的价值所在——它不需要靠加深网络来被动获取大感受野而是在残差块内部主动构造多个尺度。2.2 Res2Net 的多尺度机制一个 Bottleneck 内部发生了什么Res2Net 的核心改动非常小。标准 ResNet 的 Bottleneck 是 1x1 卷积降维、3x3 卷积提特征、1x1 卷积升维3x3 卷积的感受野是固定的。Res2Net 把中间这个 3x3 卷积替换成一组按通道分割的子卷积并且让它们首尾相接、逐级融合。以 scale4 为例主干特征会被拆成 4 份每一份的宽度变成原来的四分之一。第一份直接透传第二份经过一个 3x3 卷积第三份先把第二份的输出与自己相加再过 3x3 卷积第四份同理。这样在同一个残差块内不同分支就获得了不同大小的等效感受野。下面是我在 PyTorch 里写的一个简化版 Res2Net Bottleneck去掉了 stem 和后续 stage 的细节只保留最核心的机制import torch import torch.nn as nn import torch.nn.functional as F class Res2NetBottleneck(nn.Module): def __init__(self, in_channels, out_channels, scale4, stride1): super().__init__() width out_channels // 4 # 每个子分支的基础宽度 self.scale scale self.width width # 1x1 降维输出通道数是 width * scale self.conv1 nn.Conv2d(in_channels, width * scale, 1, biasFalse) self.bn1 nn.BatchNorm2d(width * scale) # 中间 3x3 卷积组共 scale - 1 个 self.convs nn.ModuleList([ nn.Conv2d(width, width, 3, stride if i 0 else 1, # 下采样只放在第一个子分支上 1, biasFalse) for i in range(scale - 1) ]) self.bns nn.ModuleList([ nn.BatchNorm2d(width) for _ in range(scale - 1) ]) # 1x1 升维 self.conv2 nn.Conv2d(width * scale, out_channels, 1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) # 残差短路通道数或分辨率不一致时用 1x1 投影对齐 self.shortcut None if stride ! 1 or in_channels ! out_channels: self.shortcut nn.Sequential( nn.Conv2d(in_channels, out_channels, 1, stride, biasFalse), nn.BatchNorm2d(out_channels), ) def forward(self, x): identity self.shortcut(x) if self.shortcut else x out self.conv1(x) out self.bn1(out) out F.relu_(out) # 按通道分成 scale 份逐级融合 spx torch.split(out, self.width, dim1) y spx[0] for i in range(1, self.scale): sp self.convs[i - 1](spx[i] y) # 前一级输出加到当前分支 sp self.bns[i - 1](sp) sp F.relu_(sp) y torch.cat([y, sp], dim1) out self.conv2(y) out self.bn2(out) return F.relu_(out identity)这段代码里最值得关注的是两个细节。第一个scale4时中间只有 3 个 3x3 卷积第一分支不经过任何卷积直接进入拼接这意味着残差块内部天然存在“浅层特征”和“深层特征”的混合。第二个stride只作用在第一个子分支的卷积上其余分支保持原分辨率因此下采样不会破坏细尺度信息。对比标准 ResNetRes2Net 在几乎不增加 FLOPs 的情况下把单层的等效感受野从固定的 3x3 扩展到近似 3x3、7x7、11x11、15x15 的序列。海陆分割最需要的正是这种同一层内的多尺度响应——粗看大陆轮廓细看堤坝边缘不需要等网络层数堆够才能看到大范围上下文。2.3 与 ResNet、HRNet、DeepLab 的取舍为什么我最终选 Res2Net做海陆分割的骨干选择市面上常见三个方向ResNet 系列、HRNet 系列、DeepLab 系列的空洞卷积。三者各有适用场景但放到海陆分割这个特定任务上Res2Net 的性价比优势很明显。ResNet 的问题在于感受野增长依赖网络深度底层特征根本看不到大范围上下文近岸水体和陆地的全局判别只能靠顶层硬扛。DeepLabV3 用空洞卷积解决感受野问题但空洞率设得越大权重越稀疏对密集边界不够友好而且空洞卷积吃显存遥感影像切片往往还要留空间给解码器。HRNet 的高分辨率分支确实能保住细边界但多分支并行结构在推理时有额外开销部署到批量生产环境时显存占用和推理延迟都不占优。对海陆分割这种只需要两类输出的任务HRNet 属于“杀鸡用牛刀”。下面是骨干选型的对比表按我的实际生产经验给参考骨干方案多尺度来源边界细节保持推理成本适合场景ResNet-50 特征金字塔跨层特征融合一般低通用分割基线Res2Net-50 轻量解码器残差块内部级联好低二分类海陆分割HRNet-W48多分辨率并行最好高细小目标、关键点DeepLabV3空洞卷积中中一般语义分割我一般会把 Res2Net-50 作为第一选择并不是因为它精度一定最高而是它在“参数量、推理速度、边界质量”三个维度上最均衡。后面几章会讲到解码器只需要一个很轻的融合头就能达到生产可用水平整个模型在单张 1080Ti 上推理 512x512 切片大约 30 毫秒批量跑整景影像时这个速度差异会被放大得非常明显。3. 基于 Res2Net 搭建海陆分割网络模型代码、损失函数与边界感知设计3.1 用 timm 加载 Res2Net 预训练权重两行代码拿到骨干自己从零实现 Res2Net 并加载 ImageNet 预训练权重工作量不小但 timm 已经把这件事做好了。模型名里的“26w_4s”代表 base_width26、scale4这是 Res2Net 论文里最常用的配置参数量和 ResNet-50 基本持平。加载时用 features_only 模式可以直接拿到多尺度的特征图省去手动截断分类头的麻烦。import timm import torch backbone timm.create_model( res2net50_26w_4s, pretrainedTrue, features_onlyTrue, out_indices[2, 3, 4], # 取 stride 8、16、32 的三层特征 ) backbone.eval() x torch.randn(1, 3, 512, 512) feats backbone(x) for f in feats: print(f.shape) # 预期输出类似 # torch.Size([1, 512, 64, 64]) stride 8 # torch.Size([1, 1024, 32, 32]) stride 16 # torch.Size([1, 2048, 16, 16]) stride 32这里有个关键参数配置out_indices 选的是后三个 stage。stride 8 的特征保留较多边界细节stride 32 的特征负责全局上下文后面解码器会把它们融合起来。如果用哨兵二号这类多光谱影像做 4 波段输入第一个卷积层需要替换成 4 通道版本ImageNet 预训练权重的前三个通道直接复制第四个通道用前三个通道的均值初始化。这个操作在遥感影像语义分割里很常见能保留底层纹理特征的同时兼容红外波段。3.2 轻量级解码器设计ASPP 加双线性上采样拿到了多尺度特征之后解码器不需要设计得太复杂。海陆分割只有两个类别解码器过于厚重反而容易在小样本上过拟合。我常用的做法是对 stride 32 的特征过一个精简版 ASPP扩大感受野再逐级上采样和低层特征拼接最后用一个 1x1 卷积输出单通道 logits。这里的核心思想是——高层的语义信息负责判断“这是海还是陆”低层的空间信息负责把边界“抠”到像素级。import torch.nn as nn import torch.nn.functional as F class ASPP(nn.Module): def __init__(self, in_channels, out_channels256, rates(6, 12, 18)): super().__init__() self.conv0 nn.Conv2d(in_channels, out_channels, 1, biasFalse) self.conv1 nn.Conv2d(in_channels, out_channels, 3, paddingrates[0], dilationrates[0], biasFalse) self.conv2 nn.Conv2d(in_channels, out_channels, 3, paddingrates[1], dilationrates[1], biasFalse) self.conv3 nn.Conv2d(in_channels, out_channels, 3, paddingrates[2], dilationrates[2], biasFalse) self.gap nn.AdaptiveAvgPool2d(1) self.conv_gap nn.Conv2d(in_channels, out_channels, 1, biasFalse) self.bn nn.BatchNorm2d(out_channels * 5) self.relu nn.ReLU(inplaceTrue) def forward(self, x): x0 self.conv0(x) x1 self.conv1(x) x2 self.conv2(x) x3 self.conv3(x) x4 self.gap(x) x4 self.conv_gap(x4) x4 F.interpolate(x4, sizex0.shape[-2:], modebilinear, align_cornersFalse) out torch.cat([x0, x1, x2, x3, x4], dim1) return self.relu(self.bn(out)) class SeaLandDecoder(nn.Module): def __init__(self, feat_channels(512, 1024, 2048), out_channels1): super().__init__() self.aspp ASPP(feat_channels[-1], out_channels256) self.fuse nn.Sequential( nn.Conv2d(256 feat_channels[1] feat_channels[0], 256, 3, padding1, biasFalse), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.Conv2d(256, out_channels, 1), ) def forward(self, feats): s8, s16, s32 feats x self.aspp(s32) x F.interpolate(x, sizes16.shape[-2:], modebilinear, align_cornersFalse) x torch.cat([x, s16], dim1) x F.interpolate(x, sizes8.shape[-2:], modebilinear, align_cornersFalse) x torch.cat([x, s8], dim1) return self.fuse(x)这套解码器的参数量很小训练时收敛也快。设计时有一个容易被忽略的点拼接前低层特征没有经过额外卷积处理直接和高层语义拼接会引入大量纹理噪声。我的做法是在骨架的 stride 8 特征后面加一个 1x1 卷积把通道压缩到 128某种程度上相当于给低层特征做了“语义提纯”。如果训练数据比较干净不加也能跑但加了之后边界稳定性会好一截。3.3 损失函数与类别不均衡Dice BCE 加边缘权重海陆分割的类别不均衡程度取决于影像覆盖范围。纯外海影像里海面可能占 90% 以上纯城市近岸可能陆地占 70%但无论如何不平衡都是常态。单独用交叉熵会导致模型倾向于把模糊区域全部判给占比高的类别这在海岸线附近尤其致命。我通常用 Dice Loss 和 BCE 的加权组合再叠加一个边缘感知权重项让模型对海岸线附近的像素投入更多注意力。import torch import torch.nn as nn import torch.nn.functional as F class EdgeAwareDiceBCE(nn.Module): def __init__(self, dice_weight0.5, edge_weight5.0): super().__init__() self.dice_weight dice_weight self.edge_weight edge_weight def forward(self, logits, target, edge_mapNone): prob torch.sigmoid(logits) # 逐像素 BCE边缘区域额外加权 bce F.binary_cross_entropy(prob, target, reductionnone) if edge_map is not None: bce bce * (1 self.edge_weight * edge_map) bce bce.mean() # Dice 项缓解类别不平衡 smooth 1.0 inter (prob * target).sum() dice 1 - (2 * inter smooth) / (prob.sum() target.sum() smooth) return (1 - self.dice_weight) * bce self.dice_weight * diceedge_map 的生成很简单对标签图用 Sobel 算子算梯度梯度幅值大于阈值的像素视为边界区域乘上一个放大系数。这个思路的效果非常直接——模型不需要额外解码边界只要在损失层面把海岸线附近的分错代价放大训练出的概率图在边界处就会更锐利。dice_weight 我一般固定在 0.5edge_weight 从 3 调到 10 之间根据验证集表现选择。如果数据里岛屿特别多edge_weight 可以适当调大因为岛屿边界占全部边界的比例更高模型学到的边界先验也更丰富。4. 数据集切分与训练参数配置从原始影像到分割模型4.1 影像切片与标签对齐512 窗口加 20% 重叠遥感影像的原始尺寸动辄上万像素不可能整图进模型切片是必经步骤。切片的窗口大小要同时考虑显存和上下文512x512 是平衡点BatchSize 8 在 16G 显存上可以跑1024 能提供更多上下文但显存占用指数上涨。重叠更是必须的不然一个养殖塘恰好被切成两半模型在窗口边缘大概率误判。我一般用 512 窗口、128 像素重叠相当于重叠率 25%既能抑制拼接缝又不会让有效计算量膨胀太多。import rasterio from rasterio.windows import Window import numpy as np import os def make_tiles(src_path, label_path, dst_dir, crop512, overlap128): os.makedirs(dst_dir, exist_okTrue) stride crop - overlap with rasterio.open(src_path) as src_img, \ rasterio.open(label_path) as src_lab: width, height src_img.width, src_img.height idx 0 for row in range(0, height - crop 1, stride): for col in range(0, width - crop 1, stride): win Window(col, row, crop, crop) img src_img.read(windowwin, out_shape(src_img.count, crop, crop)) lab src_lab.read(windowwin, out_shape(1, crop, crop)) # 丢弃 nodata 占比过高的切片 if img.shape[1:] (crop, crop) and (lab 0).mean() 0.5: np.save(f{dst_dir}/img_{idx:06d}.npy, img.astype(np.float32)) np.save(f{dst_dir}/lab_{idx:06d}.npy, lab.astype(np.uint8)) idx 1注意这里有两个细节。一是 out_shape 参数它会让 rasterio 在读取时做重采样如果影像和标签的空间分辨率不一致必须在写入前统一成同一个 grid否则标签和影像会出现像素级错位。二是 nodata 的处理遥感影像边缘经常有黑边或 0 值区域这些切片扔进训练集会教坏模型我习惯直接丢弃。另一个容易翻车的点是标签对齐。如果标签是 shp 矢量转出来的栅格必须确认栅格化的 transform 和影像完全一致。我一般用 gdal.Rasterize 指定 outputBounds 和 target_resolution保证矢量和栅格在同一个空间参考下对齐。这个坑很少有人提前注意到等训练完发现边界整体偏移两三个像素再回头就晚了。4.2 数据增强与归一化的细节分位截断比均值方差更稳自然图像分割的归一化习惯是套 ImageNet 的 mean 和 std但遥感影像的数值分布和自然图像差异很大。GF 系列产品的 DN 值范围是 0 到 2047哨兵二号 L2A 地表反射率通常带 1/10000 的缩放因子不同传感器差异明显。我最早直接套 RGB mean/std结果发现模型对光照和大气条件极其敏感换一景影像精度就掉三四个点。后来改用逐波段 2% 到 98% 分位截断归一化效果稳定很多。import numpy as np def normalize_percentile(img, low2, high98): 逐波段分位截断到 0-1适合遥感影像而非自然图像。 out np.zeros_like(img, dtypenp.float32) for b in range(img.shape[0]): band img[b] p_low np.percentile(band, low) p_high np.percentile(band, high) out[b] np.clip((band - p_low) / (p_high - p_low 1e-6), 0, 1) return out分位截断的直观效果是压掉了云、高亮建筑、极深水体这些极端值让模型更关注中间段的地物反射差异。注意这里有个陷阱训练时每个切片单独分位截断推理时也用同样的方式处理但如果训练集和测试集来自不同传感器或不同时相分位统计量会漂移。稳妥做法是统计整景影像的 p2 和 p98保存成 json训练和推理都用同一套统计量。数据增强方面遥感影像的增强策略和自然图像不完全一样。平移、裁剪会改变地理空间关系我不能用但旋转和翻转对海陆分割是安全的因为海陆语义不依赖方向。随机旋转 90 度的整倍数、水平垂直翻转、亮度对比度小幅度抖动是主力。高斯噪声加一点点可以帮助模型抵抗传感器噪声但强度要控制好不然会破坏细边界。增强操作概率参数范围随机水平垂直翻转0.5两种独立选择随机旋转 90 度整数倍0.250/90/180/270亮度对比度抖动0.3幅度在正负 10% 内高斯噪声0.1sigma 0.01 到 0.024.3 训练超参数速查表一组可直接照抄的配置训练配置这块我踩过不少坑尤其是学习率和骨干的微调策略。Res2Net 的 ImageNet 预训练权重对遥感影像来说只是“半熟”状态骨干微调学习率一定要比解码器低否则前面几个 stage 会被破坏。我常用 AdamW初始学习率 1e-3骨干学习率乘 0.1配合多项式衰减后期学习率降到 1e-6 附近。切片尺寸 512、BatchSize 8 在 16G 显存上配合 AMP 刚好能跑显存不够就 BatchSize 减半再用梯度累积不要让 BatchSize 小于 4。超参数推荐值备注输入尺寸512x512切片重叠 128 像素BatchSize816G 显存 AMP小卡减半优化器AdamWweight_decay 1e-4初始学习率1e-3骨干部分乘 0.1学习率调度Poly 衰减power0.9min_lr 1e-6训练轮数100前 5 轮 warmup混合精度torch.cuda.amp显存省一半速度提升明显训练循环里 AMP 的写法有一个容易忽略的地方loss 的缩放和梯度裁剪必须配合 scaler 使用。直接对放大后的梯度做 clip 会导致裁错数值要先调用 scaler.unscale_ 再做 clip。训练到第 30 轮左右可以暂停一下用当时的模型对验证集做一次推理挑出错误像素密集的切片把它们补充到训练集里继续训练。这个难例挖掘流程往往比加模型复杂度更有效。5. 海陆分割训练与推理避坑五次翻车换来的排查清单5.1 近岸浑浊水体被误判成陆地现象验证集上整体 IoU 不低但放大看近岸有大量条带状陆地误检尤其集中在河流入海口、滩涂、水库边缘。原因这类区域的反射特性和裸土、建筑太接近模型在缺少上下文的情况下只能靠纹理猜。更深层的原因是训练样本里“干净水体”占比过高浑浊水体样本不足模型天然会把模糊区域偏向训练集中占多数的类别。解决我做三件事。第一把误判严重的切片挑出来单独建一个难例集合放回训练集加权重重复训练一般一轮难例挖掘能拉回 3 到 5 个点的边界精度。第二如果影像有近红外波段一定要把它作为第四通道输入NIR 对水体的吸收特性是 RGB 无法替代的。第三检查归一化参数是否在训练和推理之间保持一致统计量漂移会造成同样的水色在不同的切片里被拉伸到不同范围。5.2 海岸线预测结果锯齿严重现象模型输出的掩膜边界有明显锯齿甚至出现 1 到 2 像素的椒盐噪点形态学处理后边界平滑度仍不如传统方法。原因这通常是损失函数缺少边界约束导致的。普通 BCE 是逐像素独立计算模型不需要让相邻像素的预测保持协同因此边界处会出现独立跳变。还有一个原因是测试时的窗口大小和训练不一致感受野分布变化导致边界置信度降低。解决训练时启用 EdgeAwareDiceBCE 的边缘权重让边界像素的分错代价放大。推理时对 sigmoid 概率图先做一次 3x3 中值滤波再取 0.5 阈值对最终的二值图做一次闭运算加开运算可以同时消除细小孔洞和孤立噪点。如果锯齿还是严重检查训练是否用到了足够多的重叠切片窗口边缘的预测本身就不稳定推理时重叠区要按权重融合而不是硬切。5.3 大影像推理显存溢出与拼接缝现象单卡 16G 跑全图推理时 OOM或者滑窗推理拼出来的结果有明显网格状拼缝。原因显存溢出多半是 BatchSize 设置不合理或者输入尺寸超过模型设计范围。拼接缝的根源是重叠区像素被不同窗口推理了多次背骨网络在窗口边缘的感受野不足置信度低直接取 argmax 拼接必然出现不连续。解决显存问题用两层手段推理时 BatchSize 固定为 1打开 torch.cuda.amp 的自动混合精度再不够就把切片降到 384。拼接缝问题要写一个羽化权重叠加的逻辑重叠区按权重比例混合而非硬切换下面是我常用的羽化权重生成方式import numpy as np def make_alpha(crop, fade): 生成一个中心高、边缘低的权重图用于重叠区羽化拼接。 w np.ones((crop, crop), dtypenp.float32) w[:fade] np.linspace(0, 1, fade) # 上边缘渐变 w[-fade:] np.linspace(1, 0, fade) # 下边缘渐变 w[:, :fade] * np.linspace(0, 1, fade) # 左边缘渐变 w[:, -fade:] * np.linspace(1, 0, fade) # 右边缘渐变 return wfade 一般取 64和重叠量 128 的一半匹配。每个窗口的预测概率乘上这个权重图再累加到大图上最后除以权重累加和得到的拼接结果几乎看不到窗口边界。这一步对最终交付图的观感影响非常大血泪经验告诉我拼缝问题比模型精度问题更容易被甲方注意到。5.4 换骨干后 loss 不收敛或直接出 NaN现象把 ResNet 换成 Res2Net 后训练 loss 不降或者前几步就出现 NaN验证集全预测成同一类。原因最常见的是骨干输出的 stride 和解码器预期不匹配。Res2Net 的 stem 和 ResNet 一样是 stride 2但 stage 内部的下采样位置不同如果直接把 ResNet 版解码器的输入索引沿用过来特征图尺寸对不上拼接时就会产生维度错误。另一个常见原因是 ImageNet 预训练权重在遥感 4 通道输入下没有正确扩展第一个卷积层导致前向传播结果异常。解决拿到骨干后先跑一次前向传播打印每一层输出 shape确认 stride 8/16/32 的索引和解码器预期一致。如果是 4 通道输入按前面讲的方法替换第一层卷积并复制权重。batch size 小于 4 时把骨干的 BatchNorm 冻结用requires_grad_(False)或backbone.eval()防止小批量统计量抖动。多卡训练改用torch.nn.SyncBatchNorm.convert_sync_batchnorm同步 BN 对遥感这种 batch 内差异大的数据有帮助。5.5 验证集 IoU 虚高但边界错位现象验证集 IoU 到了 0.92看起来很好但把预测边界和真值叠在一起肉眼可见整体偏移几个像素岛屿面积系统性偏大或偏小。原因IoU 这个指标被大块内部区域主导了。一景影像里陆地内部像素可能有几百万个海岸线附近边界像素只有几万个边界偏移 2 个像素在 IoU 里只体现为 0.5% 的变化肉眼却已经能看出系统性错位。如果标签本身就存在矢量转栅格时的偏移模型学到的边界就会带着同样的偏置。解决在验证指标里额外加入边界 F-score专门评估 1 到 2 像素级别的边界质量。下面是这个指标的参考实现from skimage.morphology import binary_erosion def boundary_f1(pred, gt): 边界 F1只评估预测与真值的 1 像素边界重合度。 def edge_mask(m): return m ~binary_erosion(m) pe, ge edge_mask(pred), edge_mask(gt) inter (pe ge).sum() precision inter / (pe.sum() 1e-6) recall inter / (ge.sum() 1e-6) f1 2 * precision * recall / (precision recall 1e-6) return f1用这个指标跑一次验证你会立刻看到真实水平。IoU 0.92 的模型边界 F1 可能只有 0.7这时候优先排查标签对齐问题而不是继续调模型参数。我也建议把真值边界和预测边界画在同一张图上按 10 像素宽度切片对比能直接定位是系统性偏移还是局部随机抖动。6. 把模型精度再往上推多尺度推理与边界验证的落地技巧6.1 多尺度推理 TTA用推理时间换最后两个点训练做完之后如果离交付指标还差一两个点最先做的应该是多尺度推理而不是换模型。这个技巧和标题里的“多尺度”正好呼应同一张切片分别以 0.75、1.0、1.25 倍尺度输入模型把三次 sigmoid 概率取平均再上采样回原尺寸。多个尺度在边界处的投票会相互补位一个尺度在窗口边缘置信度低时另一个尺度可能正好能看到完整上下文。import torch import torch.nn.functional as F torch.no_grad() def predict_tta(model, img, scales(0.75, 1.0, 1.25), use_flipTrue): 多尺度推理返回平均概率图img 形状为 (B, C, H, W)。 probs [] for s in scales: x F.interpolate(img, scale_factors, modebilinear, align_cornersFalse) logit model(x) prob torch.sigmoid(logit) if use_flip: x_flip torch.flip(x, dims[3]) prob_flip torch.sigmoid(model(x_flip)) prob 0.5 * (prob torch.flip(prob_flip, dims[3])) prob F.interpolate(prob, sizeimg.shape[-2:], modebilinear, align_cornersFalse) probs.append(prob) return torch.stack(probs).mean(dim0)参数上scales 取三个尺度加水平翻转一个窗口要做 6 次前向推理耗时涨到原来的 6 倍但边界 F1 通常能提升 2 到 3 个点。如果推理资源紧张可以只保留 (0.75, 1.0, 1.25) 不加翻转或者只保留 (1.0, 1.25)。我在实际项目里通常把多尺度 TTA 用在最终交付的那一轮推理上中间训练验证阶段不开否则调参周期会变得非常难熬。6.2 用公开海岸线数据对预测边界做外部校验精度验证不能只靠自己标注的测试集最好用公开海岸线数据做一次独立的边界偏差评估。GSHHG 是常用的全球高精度海岸线数据集虽然它基于历史测绘数据可能和当前时相的影像存在真实偏差但好处是不依赖标注员主观判断适合用来量化模型的系统性偏移。做法是先把预测的栅格掩膜矢量化提取边界线再和公开海岸线做距离对比。下面是把二值掩膜转成边界矢量的核心逻辑import geopandas as gpd import rasterio from rasterio.features import shapes from shapely.geometry import shape def mask_to_boundary_gdf(mask, transform, crs): mask 为 0/1 的二维数组返回值为边界线的 GeoDataFrame。 geoms [] for geom, value in shapes(mask.astype(uint8), transformtransform): if value 1: polygon shape(geom) if polygon.geom_type Polygon: geoms.append(polygon.boundary) elif polygon.geom_type MultiPolygon: geoms.extend([p.boundary for p in polygon.geoms]) return gpd.GeoDataFrame(geometrygeoms, crscrs)拿到预测边界和公开海岸线后把它们统一投影到 UTM 投影坐标系再用空间距离函数统计每个预测边界点到参考海岸线的最近距离均值。注意不能用 WGS84 地理坐标系直接算距离单位是度而不是米结果没有任何意义。这个流程做一次你就能得到一张距离分布直方图比任何一个单一指标都更能说明模型能不能交付。回到开头那个问题海陆分割到底难在哪里难在大多数人以为它简单。我第一次做海陆分割时直接整景影像塞进早期 FCN显存当场崩掉后来老老实实从切片、归一化、骨干选型一路重做才明白这类任务的本质是边界工程。Res2Net 解决多尺度感受野损失函数解决边界注意力切片和推理策略解决工程稳定性——每一步都不花哨但每一步都缺不得。希望帮到你。本文还有配套的精品资源点击获取