简介:面向有 TensorFlow 基础并希望将 Transformer 迁移到语义分割任务的中高级开发者,资源提供了一套完整的端到端分割网络实现。整体方案先通过两层卷积提取基础特征,随后将特征图切分为四份,分别送入四个可调节注意力头数的并行 Transformer 支路,拼接后再经一个 Transformer 与逐层解码器输出,适合作为图像分割方向实验、课程设计或算法改进的起点。资源共554个文件,压缩包仅2.75MB,核心代码集中在 main.py、transformer.py、builders.py 三个脚本中,其余主要是 Python 缓存、txt 说明、png 示例图以及运行所需的 C/C++ 头文件等依赖,解压后目录层次不复杂,便于定位关键模块。已有660人学习下载,说明该实现具备一定的参考价值。读者可同时获得网络模块封装、训练测试入口、数据集划分参数、评价指标设置,以及 conda 环境创建、JPG 转 PNG、图片统一缩放为 256×256 等预处理细节,能快速跑通训练流程并在此基础上调整网络结构。
1. Transformer语义分割:全局上下文与稠密预测的一次对齐
如果把语义分割简化成逐像素分类,DeepLabV3 这类 CNN 架构早就能把指标推到可用的水平;真正让 Transformer 进入语义分割的原因,不是单个像素分类得更准,而是全局上下文建模。分割任务里最典型的错分往往来自感受野不足——一大片草地和远处山坡颜色相近,分类器需要一个足够大的上下文才能分辨它们属于哪一类。Vision Transformer 从第一层起就让所有位置两两交互,这种能力与分割任务的诉求形成了直接对齐。下面按从 ViT 到 Swin、再到混合架构的路线,说清选型逻辑、可运行代码和训练中会踩的参数坑。阅读前提很简单:你会跑 PyTorch,能看懂 CNN 分割模型的结构。
2. 语义分割与自注意力机制的契合边界:CNN 输在哪里
2.1 从感受野说起:分割比分类更需要全局视野
语义分割常常被当成“逐像素分类”来理解,但真实标注规则决定了它并不是一个点独立的任务。在自动驾驶语义分割里,同一个像素在晴天和傍晚的 RGB 值几乎相同,要正确判定它的类别,必须看到它是否在车道线内部、是否与前方车辆形成遮挡关系、周边的边缘线是否连续。这些线索散布在以目标像素为中心的大范围区域中,这就意味着分割任务天然地要求模型具备全局视野。
CNN 通过卷积核的滑动来积累视野,理论感受野随层数增加不断增大,但经验观察表明,有效感受野远小于理论值,且随距离衰减近似服从高斯分布。也就是说,卷积网络即使在最深层的特征图上,也主要受局部区域影响。DeepLabV3 引入 ASPP 模块,本质上是用多个不同膨胀率的空洞卷积强行扩大感受野,但这些膨胀卷积之间没有动态交互,不能根据输入内容灵活决定“看哪里”。
Transformer 语义分割的出发点正是补上这一环。Vision Transformer 把图片切成 Patch 并映射为 Token,自注意力对任意两个位置一次性建立交互,跨距离沟通不再依赖堆叠层数。数据规模足够大时,基于 Transformer 的主干网络在 ADE20K、Cityscapes 这类数据集上的 mIoU,普遍能比同参数量的 CNN 高 2 到 4 个百分点。
| 维度 | CNN(ResNet/DeepLabV3) | Transformer(ViT/SegFormer) |
|---|---|---|
| 感受野增长方式 | 靠堆叠卷积层,有效感受野非线性增长 | 每层直接全局交互 |
| 局部先验 | 卷积核滑动窗口内隐式注入 | 无先验,靠数据学习 |
| 内部结构 | 权值共享、平移等变性 | 池化与嵌入结合,输入顺序敏感 |
| 小数据场景 | 稳定,收敛快 | 容易过拟合,需要预训练 |
| 大数据场景 | 精度趋于饱和 | 持续提升,上限更高 |
这张表不是绝对结论,而是训练集只有几千张图和几十万张图这两种情形下最常遇到的取舍。Transformer 在数据量不足时优势并不明显,甚至可能打不过 ResNet 系骨架,这一点在落地时比模型选择本身更值得先确认。
2.2 自注意力在分割任务里的开销与两个补救方向
TransformerBlock 里单个自注意力层的前向计算可以精简成下面这段代码:
import torch def self_attention(x, W_q, W_k, W_v, d_k): # x 是 (B, N, d) 的 token 序列,分类时 N 可能只有 196 # 分割任务输入 512x512 时,patch 为 16x16,N = 1024 q = x @ W_q # (B, N, d_k) k = x @ W_k v = x @ W_v attn = (q @ k.transpose(-2, -1)) / (d_k ** 0.5) # (B, N, N) attn = torch.softmax(attn, dim=-1) return attn @ vW_q、W_k、W_v 分别是查询、键、值的投影矩阵,d_k 是键向量的维度。除以根号 d_k 是为了防止点积结果过大导致 softmax 饱和。中间生成的 attn 矩阵大小是 N×N,这就是显存开销的主要来源。512×512 的输入按 16×16 的 Patch 划分后 N=1024,再用 8 个头,就有 800 万个注意力分数;分辨率翻倍到 1024×1024 时 N=4096,这个数字要乘 16 倍。
所以原始 ViT 直接拿来做语义分割几乎不可行,业界主流的两条路是窗口注意力和金字塔结构。Swin 把注意力限制在局部窗口内,只在特定层做跨窗口交换;SegFormer 和 PVT 则把网络分成四个阶段,低分辨率阶段做全局建模,高分辨率阶段用局部操作保留纹理细节。工程上选哪条路,往往取决于目标分辨率和显存预算,而不是论文指标。
2.3 归纳偏置缺失的代价:何时该回退到 CNN
Transformer 没有内置“相邻像素相关”的先验,这是它在大数据上精度更高的原因,也是它在小数据场景下容易过拟合的原因。ViT 原论文已经明确说明:直接在 ImageNet-1K 上从头训练的 ViT 不如同规模 ResNet,只有在 3 亿级别的数据集上预训练之后才能反超。
落到工程上的建议是:如果手里的分割标注图只有几千张,不要一上来就换 Transformer 骨干。先用 DeepLabV3 跑通流程做基准,再换 ImageNet-22K 预训练的 Swin 或 SegFormer 做迁移,对比 mIoU。实践中,小数据集下 Transformer 分割模型的 mIoU 比同规模 CNN 低 2 到 5 个点是常见现象,这时候问题通常在数据量撑不起全局建模所需的统计量,而不是模型实现有问题。
3. 用 Vision Transformer 搭建最小可运行的语义分割流程
3.1 为什么优先选 SegFormer 的思路而不是原始 ViT
原始 ViT 的序列长度恒定,最终特征图分辨率固定为输入的 1/16。语义分割要求输出与输入同分辨率,这导致解码器要么用简单双线性插值,要么堆叠卷积层恢复细节,效果都不理想。SegFormer 提出的 Mix Transformer 是更常见的做法:四个阶段输出不同分辨率的特征图,Patch 尺寸从 4×4 逐渐扩大到 32×32,解码器是轻量 MLP,直接做多尺度融合后上采样。省去 FPN 的设计,结构简单,在迁移学习时也更稳。
另一个容易被忽视的是位置编码。ViT 的位置嵌入是在固定分辨率上训练的,换到 512 或 640 分辨率时,插值会引入扰动,训练初期容易出现 Loss 抖动。SegFormer 直接去掉位置编码,改用零填充 patch embedding 来隐式编码位置信息,这在实际工程里省掉很大一块调试量。下面用手写方式搭建一个最小可运行模型,不依赖 mmsegmentation 或 HuggingFace,方便看清每个组件的输入输出尺寸。
3.2 手写一个精简版 Mix Transformer 分割模型
import torch import torch.nn as nn import torch.nn.functional as F class OverlapPatchEmbed(nn.Module): """重叠 patch embedding,SegFormer 使用,避免硬切 patch 导致边界信息丢失""" def __init__(self, in_ch=3, embed_dim=64, patch_size=7, stride=4): super().__init__() self.proj = nn.Conv2d(in_ch, embed_dim, kernel_size=patch_size, stride=stride, padding=patch_size // 2) self.norm = nn.LayerNorm(embed_dim) def forward(self, x): B, C, H, W = x.shape x = self.proj(x) # 下采样并投影 x = x.flatten(2).transpose(1, 2) # B, N, C x = self.norm(x) return x, (H // 4, W // 4) class TransformerBlock(nn.Module): """单层自注意力,带残差与 LayerNorm""" def __init__(self, dim, num_heads=4, mlp_ratio=4.0): super().__init__() self.norm1 = nn.LayerNorm(dim) self.attn = nn.MultiheadAttention(dim, num_heads, batch_first=True) self.norm2 = nn.LayerNorm(dim) self.mlp = nn.Sequential( nn.Linear(dim, int(dim * mlp_ratio)), nn.GELU(), nn.Linear(int(dim * mlp_ratio), dim) ) def forward(self, x): x = x + self.attn(self.norm1(x), self.norm1(x), self.norm1(x))[0] x = x + self.mlp(self.norm2(x)) return x class TinySegFormer(nn.Module): """简化版三层编码器 + MLP 解码器,用来跑通 Transformer 语义分割流程""" def __init__(self, in_ch=3, num_classes=21, embed_dims=[32, 64, 128]): super().__init__() self.stage1 = OverlapPatchEmbed(in_ch, embed_dims[0], patch_size=7, stride=4) self.stage2 = OverlapPatchEmbed(embed_dims[0], embed_dims[1], patch_size=3, stride=2) self.stage3 = OverlapPatchEmbed(embed_dims[1], embed_dims[2], patch_size=3, stride=2) self.blocks = nn.ModuleList([ TransformerBlock(embed_dims[0]), TransformerBlock(embed_dims[1]), TransformerBlock(embed_dims[2]), ]) self.head = nn.Conv2d(sum(embed_dims), num_classes, kernel_size=1) def forward(self, x): B, _, H, W = x.shape feats = [] x1, _ = self.stage1(x) x1 = self.blocks[0](x1) feats.append(x1) x1 = x1.transpose(1, 2).reshape(B, -1, H // 4, W // 4) x2, _ = self.stage2(x1) x2 = self.blocks[1](x2) feats.append(x2) x2 = x2.transpose(1, 2).reshape(B, -1, H // 8, W // 8) x3, _ = self.stage3(x2) x3 = self.blocks[2](x3) feats.append(x3) out = [] for i, f in enumerate(feats): f = f.transpose(1, 2) if i == 0: f = f.reshape(B, -1, H // 4, W // 4) elif i == 1: f = f.reshape(B, -1, H // 8, W // 8) f = F.interpolate(f, size=(H // 4, W // 4), mode='bilinear') else: f = f.reshape(B, -1, H // 16, W // 16) f = F.interpolate(f, size=(H // 4, W // 4), mode='bilinear') out.append(f) x = torch.cat(out, dim=1) x = self.head(x) x = F.interpolate(x, size=(H, W), mode='bilinear') return x model = TinySegFormer(num_classes=21) out = model(torch.randn(1, 3, 256, 256)) print(out.shape) # torch.Size([1, 21, 256, 256])核心逻辑在 OverlapPatchEmbed 的 stride 参数上:stride 控制了每个阶段的降采样倍数,padding 使用零填充而非镜像填充,是为了保留一定位置信息。每个阶段先做卷积投影,再经过一层 TransformerBlock,输出被保存为多尺度特征。解码器部分把三个尺度的特征统一上采样到 1/4 分辨率,再通过 1×1 卷积输出类别数。整体网络把 256×256 的输入逐步降到 1/4、1/8、1/16,再恢复回输入尺寸。
要把它扩展成真正的 SegFormer,只需要改三处:给每个阶段加多层 TransformerBlock 的深度控制,把解码器换成逐层上采样后再拼接的 MLP 结构,以及加上阶段间的特征融合。Tiny 版本不追求指标,只验证“Transformer 编码器 + 上采样解码器”在分割任务上的可行性。实际项目里建议直接加载预训练权重,从零训练不仅慢,loss 曲线也会非常不稳定。
3.3 数据准备、训练循环与参数速查
语义分割数据集的制作成本通常被低估。训练前只需要准备好两类文件:原图和单通道 mask 图。mask 的每个像素值对应类别索引,未标注区域统一设为 255。生产环境里一般先用多边形标注工具导出 JSON,再把多边形逐类填充成灰度索引图;如果之前只有检测框标注,还需要人工补画边缘,这个过程通常占整个项目一半以上工时。下面这段代码是训练循环的模板:
from torch.utils.data import Dataset, DataLoader import torch.optim as optim class SegDataset(Dataset): def __init__(self, image_paths, mask_paths, img_size=256): self.image_paths = image_paths self.mask_paths = mask_paths self.img_size = img_size def __getitem__(self, idx): from PIL import Image img = Image.open(self.image_paths[idx]).convert('RGB') mask = Image.open(self.mask_paths[idx]) img = img.resize((self.img_size, self.img_size)) mask = mask.resize((self.img_size, self.img_size)) img_t = torch.tensor(np.array(img) / 255.).permute(2, 0, 1).float() mask_t = torch.tensor(np.array(mask), dtype=torch.long) return img_t, mask_t model = TinySegFormer(num_classes=21) optimizer = optim.AdamW(model.parameters(), lr=6e-5, weight_decay=0.01) criterion = nn.CrossEntropyLoss(ignore_index=255) for epoch in range(50): model.train() for images, masks in DataLoader(train_set, batch_size=8, shuffle=True, num_workers=4): optimizer.zero_grad() preds = model(images) loss = criterion(preds, masks) loss.backward() optimizer.step()训练部分的注意点集中在几个参数上。AdamW 的权重衰减在 Transformer 上比 SGD 稳健得多,学习率初始值建议 1e-5 到 1e-4 之间,过大容易发散。CrossEntropyLoss 的 ignore_index=255 是分割数据集的通用约定,未标注区域不参与梯度计算。实际使用时要加随机翻转和随机裁剪,否则模型在输入尺寸变化较大的情况下 mIoU 会出现明显波动。
| 参数 | 常见取值 | 调整方向 |
|---|---|---|
| 骨干学习率 | 6e-5 到 3e-4 | 加载预训练时用低值,随机初始化时提 10 倍 |
| 解码器学习率 | 骨干的 10 倍 | 收敛慢时单独提解码器 |
| warmup steps | 总迭代的 5% 到 10% | 大 batch 下需要更长 warmup |
| batch size | 8 到 64 | 与学习率线性缩放:batch 翻倍,lr 也翻倍 |
| 训练分辨率 | 512² 到 1024² | 分辨率提高时 batch 相应减半 |
4. Swin Transformer、HGFomer 与点云语义分割的进阶路线
4.1 Swin 的窗口注意力为何能保住高频细节
全局自注意力在低分辨率下效果好,但一旦分辨率升高,会出现一个容易被忽略的问题:每个 token 要跟全图其他 token 计算相关性,大量无关位置会稀释注意力权重。语义分割里高频细节恰恰需要局部信息,比如物体边缘、细长结构,全局注意力反而会把这些信息抹平。Swin 的思路是把特征图划分为不相交的窗口,在每个窗口内部计算自注意力,下一层再把窗口移动半个窗口大小,让信息在相邻窗口间流动。窗口内注意力保留了局部建模能力,同时抑制了计算量膨胀。
Swin 的另一个贡献是金字塔结构。它借鉴 CNN 的多尺度思想,由四个阶段逐步降低分辨率,输出 1/4、1/8、1/16、1/32 的特征图,天然适配 FPN 这类分割解码器。相比 ViT,Swin 在处理高分辨率输入时表现更稳定,这也是它在语义分割竞赛和工业落地中更常见的原因。如果你发现 SegFormer 在特定数据集上边界模糊,先检查是不是训练分辨率太低,再考虑换成 Swin 骨干多尺度融合。
4.2 HGFomer 的角度:超图学习给 Transformer 加一条拓扑旁路
HGFomer 全称是 topology-aware vision transformer with hypergraph learning,思路是从图学习里借来超图概念。普通图的一条边只能连接两个顶点,超图的超边可以连接任意多个顶点,能够显式表达三元及以上的组间关系。语义分割里像素常常以组为单位协同判定:同一辆车上的多个像素、同一条车道线的连续像素、同一栋楼的墙面,这些都属于组关系而不是成对关系。
自注意力天然建模成对依赖,组关系必须经过多个成对关系间接传递。HGFomer 的做法是加一条超图学习分支,先根据视觉特征构造拓扑图,再用超图卷积更新节点表示,最后与 Transformer 分支融合。从实现角度,它并不需要替换主干,只需要在最后几层加入一个可插拔的分支:
def hypergraph_message_passing(x, incidence, W): # incidence: 超边数 x 节点数 的关联矩阵,每条超边连接多个节点 # 第一步:超边内特征聚合 edge_feat = incidence @ x # (num_edges, dim) edge_feat = F.relu(edge_feat @ W) # (num_edges, dim) # 第二步:超边特征回传节点 node_update = incidence.T @ edge_feat # (num_nodes, dim) return node_update这个简化算子没有做度归一化,真实实现里需要对关联矩阵做行和列的归一化,防止特征尺度漂移。超图分支出现在网络最后两层时,通常会给 mIoU 带来微小但稳定的提升,尤其是对大型连通区域的分割。代价是训练时间增加约 10% 到 15%,因为需要维护关联矩阵和图构建开销。
4.3 点云语义分割的 Token 化与最小验证流程
点云语义分割的输入不是规则网格,不能直接套 2D 卷积。Transformer 在这里的用法和图像完全不同:先要把点云变成 Token 序列。常见做法是先用最远点采样选出中心点,再在中心点周围用 KNN 找邻域,把邻域内每个点的坐标、RGB、法向量拼接起来作为 Token 的特征。中心点数量决定了 Token 数量,通常把 10 万个点采样成 4096 个中心点,这样注意力矩阵的大小才可控。
与图像 Transformer 不同,点云里很少做全局自注意力,而是做局部注意力:每个 Token 只与它邻域内的 k 个 Token 交互,这个 k 一般是 16 或 32。在自动驾驶语义分割中,点云与摄像头图像的融合也开始引入 Transformer,做法是先把图像特征投影到点云坐标系,再在 Transformer 里做跨模态注意力,让点云 query 去图像特征图上检索信息。这个方向目前还在快速演变,但它已经把 Transformer 的应用场景从二维图片扩展到了三维空间。
5. 训练语义分割 Transformer 的核心参数与排错信号
5.1 学习率、warmup 与 batch size 的联动经验
Transformer 的收敛对学习率极度敏感,跟 CNN 的训练习惯很不一样。CNN 用 1e-2 的 SGD 也能稳定训练,Transformer 用 1e-3 的 AdamW 就可能直接发散。分割任务里常用的是线性 warmup 加多项式衰减:前几步把学习率从极小值慢慢升到目标值,再按迭代次数衰减到接近零。warmup 的本质是让 LayerNorm 和注意力矩阵在初始阶段稳定下来,避免早期梯度震荡。
学习率与 batch size 之间存在近似线性关系。batch size 从 8 提升到 16 时,学习率也翻倍,否则大 batch 下的梯度更新步长会相对变小,收敛变慢。加载预训练权重时,骨干学习率要低于随机初始化的头部,常见做法是骨干 6e-5、解码器 6e-4。如果训练曲线一直不降,先不要动网络结构,把学习率调到 1e-5 跑 20 个迭代观察 loss 变化,这个信号比任何监控指标都直接。
5.2 从 CrossEntropy 到 Dice:长尾类不均衡的常规解法
语义分割数据集的类别分布极不均衡,道路、建筑这类背景占比高,行人、交通标志占比很低。CrossEntropy 优化的是逐像素准确率,对长尾类别不敏感,导致 mIoU 里某些类别长期在 10% 以下。一个有效做法是给类别加权,但权重设置本身需要反复试。更常见的做法是叠加 Dice loss,它按类别计算区域重叠度,对小类别更友好:
class CombinedLoss(nn.Module): def __init__(self, num_classes, weight=None, ignore_index=255): super().__init__() self.ce = nn.CrossEntropyLoss(weight=weight, ignore_index=ignore_index) self.num_classes = num_classes def forward(self, pred, target): ce_loss = self.ce(pred, target) pred_softmax = F.softmax(pred, dim=1) # B, C, H, W target_onehot = F.one_hot(target, self.num_classes) target_onehot = target_onehot.permute(0, 3, 1, 2).float() intersection = (pred_softmax * target_onehot).sum(dim=(0, 2, 3)) union = pred_softmax.sum(dim=(0, 2, 3)) + target_onehot.sum(dim=(0, 2, 3)) dice = 1 - 2 * intersection / (union + 1e-6) return ce_loss + dice.mean()需要说明的是,忽略区域(255)在 one-hot 里所有类别都是 0,Dice 计算时该像素的贡献会消失,不会污染梯度。Dice loss 的缺点是在小目标上梯度不稳定,所以权重不要太高,一般 0.1 到 0.5 之间。如果你发现加了 Dice 后 mIoU 反而下降,多半是权重过大导致背景类被过度惩罚。
5.3 mIoU 计算时最常见的两个计数顺序错误
验证阶段最容易出的问题不是模型,而是 mIoU 的计算方式。第一个错误是把每张图片的 IoU 分别计算再取平均,正确做法是先累加所有图片的混淆矩阵,再统一计算 IoU。两者结果会有 1 到 3 个百分点的差距。第二个错误是没有过滤 ignore_index=255 的像素,导致混淆矩阵里出现一个永远预测不对的“类别”。
def compute_miou(preds, targets, num_classes, ignore_index=255): preds = preds.argmax(dim=1).flatten() targets = targets.flatten() valid = targets != ignore_index preds, targets = preds[valid], targets[valid] matrix = torch.zeros(num_classes, num_classes) for c in range(num_classes): idx = targets == c matrix[c] = (preds[idx][:, None] == torch.arange(num_classes)).sum(dim=0).float() intersection = torch.diag(matrix) union = matrix.sum(dim=0) + matrix.sum(dim=1) - intersection iou = (intersection + 1e-6) / (union + 1e-6) return iou.mean().item()这段代码按真实类别逐行填充混淆矩阵,行表示真实类别,列表示预测类别。验证时如果某个类别在数据中完全没出现,平滑项 1e-6 会把它当成 IoU 为 0,拖低整体分数。正确做法是先把没有样本的类过滤掉再取平均。另一个建议是验证阶段只用单尺度、不加增强,多尺度测试对 Transformer 的收益不稳定,但会让结果难以复现。
5.4 显存溢出与 loss=nan 的排查顺序
显存溢出时不要一上来就换小模型,按这个顺序排查:先减 batch size,把分辨率降到 512;再用梯度累积模拟原 batch size,保持有效学习率不变;还不够时给主干层加 gradient checkpointing,用重计算换显存。最后再考虑是否把注意力实现换成 memory-efficient 版本,这一步通常能省 30% 到 50% 显存,但需要验证梯度精度。
loss=nan 则优先怀疑学习率过大,Transformer 对学习率的敏感度远高于 CNN。把学习率降到 1e-5 跑几十步,如果 loss 恢复正常,就确认是学习率问题。如果仍然 Nan,检查数据增强是否引入了异常像素值,例如随机失真后 RGB 出现负数或 NaN。再检查混合精度训练时 LayerNorm 在 fp16 下是否溢出,用纯 fp32 跑一次对比 loss 曲线即可定位。
提示:训练曲线不下降时,第一个排查目标永远是学习率和数据,不是网络结构。Transformer 模型结构的问题通常表现为 loss 正常下降但 mIoU 不涨。
6. 推理阶段的三个实用调优:半精度、固定尺寸与滑动窗口
6.1 半精度推理:先算 logits 再取 argmax
部署阶段最常见的加速手段是半精度推理。PyTorch 里用 autocast 即可,但要注意一个细节:不要在 fp16 下先做 softmax 再取整。softmax 在 fp16 下的精度足够,但省掉这一步可以少一次指数计算:
@torch.inference_mode() def predict(model, img, device): model.eval() img_t = img.to(device) with torch.autocast(device_type='cuda', dtype=torch.float16): logits = model(img_t) pred = logits.argmax(dim=1) return pred输入归一化要在进入模型前完成,fp16 的范围有限,标准化能避免极端值。这里没有做 softmax,因为 argmax 只关心类别间的大小关系,logits 的相对顺序不会因常数偏移改变。LayerNorm 在 fp16 下偶尔会出问题,如果发现某个类别的预测结果在 fp16 和 fp32 下不一致,把 LayerNorm 单独切回 fp32。
6.2 固定输入尺寸带来的两个收益
Transformer 的推理输入尺寸最好是固定的。原因有两个:一是很多工程化实现里位置编码或窗口划分对尺寸有隐式依赖,输入尺寸变化会触发重新插值,带来额外开销;二是 TensorRT 和 ONNX Runtime 对动态形状的支持仍然不如静态形状高效。固定尺寸的常见做法是把输入短边缩放到 512,再中心裁剪或 padding 到 512×512。分割任务里 padding 区域要在后处理时裁掉,否则会产生大面积伪类别。
如果对边界质量要求更高,可以牺牲一部分速度,改用滑动窗口推理:把大图切成 512×512 的块分别预测,再按像素位置拼回原图。窗口重叠 64 像素,只在重叠区域取非重叠中心的预测结果,这样可以避免物体在窗口边缘被切碎。这个技巧在遥感影像和病理切片分割里几乎是标配。
Transformer 分割模型的导出要注意 LayerNorm 的算子映射,部分推理后端对 LayerNorm 的 fp16 实现不完整,导出 ONNX 后逐算子检查即可。半精度导出的注意力输出与 fp32 的余弦相似度如果低于 0.99,优先检查 LayerNorm 的精度设置,再决定是否回退到 fp32 推理。
本文还有配套的精品资源,点击获取