
简介SeaFormer实战资源包面向有一定PyTorch基础的图像分类开发者提供从数据增强到模型训练、测试与可视化的完整流程。内容覆盖transforms增强、CutOut/MixUp/CutMix等技巧以及SeaFormer轻量模型实现训练、混合精度、梯度裁剪、DP多显卡并行、EMA与Grad-CAM热力图绘制等关键操作。资源共2451个文件主体为2436张png图片记录训练曲线、可视化等结果另有8个py脚本、1个pth模型权重、1个tar文件与JSON类别文件等压缩包约768.12MB可直接对照源码运行。已有1014人学习下载。既能学到损失/准确率曲线绘制、ACC1/ACC5统计思路也能掌握余弦退火策略与AverageMeter自定义变量等工程实现细节适合希望从零跑通SeaFormer并用于实战项目的读者。1. SeaFormer实战图像分类的新选择为什么值得动手SeaFormer做图像分类最近在轻量级方案里讨论不少。这个模型本质上是把Transformer的全局自注意力做了空间下采样让token数量先降下来再算注意力所以一样能拿到大感受野计算量却没跟着暴涨。和同尺寸CNN比它对目标位移和背景干扰更不敏感用在森林图像分类这类中粒度识别上精度往往更稳。这篇文章不做概念复述直接把结构、环境、训练脚本、调参习惯和踩坑经验讲完让你照着一套操作能在本机跑出一个可部署的SeaFormer图像分类模型。2. 从架构说起SeaFormer的注意力设计凭什么撑起图像分类精度图像分类这个任务看似简单实际上一张图里目标可能只占几十个像素背景却占大半。传统CNN靠堆卷积层扩大感受野但浅层拿到的还是局部信息标准ViT倒是能全局建模但QKV矩阵在所有token两两相乘分辨率一高就爆炸。SeaFormer走的是一条中间路线注意力照算但在计算之前先用卷积把空间分辨率降下来相当于先让模型瞄一遍全局再把细节补回来。下面按设计定位、核心块、整体网络三层拆开看。2.1 轻量级Transformer的定位为什么图像分类需要它先明确一个事实图像分类算法里网络的“视野”比“深度”更影响泛化。CNN在小数据集上容易过拟合一个重要原因就是浅层只盯着局部纹理一旦物体尺寸和训练集不一致就认不出来。Transformer的全局注意力解决这个问题但代价是O(N²)的计算复杂度N是token数量。对224x224输入Patch size 16会得到196个token算着还算轻松一旦输入变成384甚至512token数量翻几倍显存和延迟同时失控。SeaFormer把注意力计算的token减少到四分之一甚至十六分之一核心做法是用一个带步长的卷积在QKV生成前对空间维度做降采样。降采样后的注意力仍然覆盖全图位置但参与运算的token数变少所以既保留了全局建模能力又让训练和推理成本回到CNN的量级。近几年不少轻量级模型如EfficientFormer、MobileViT也在走类似方向SeaFormer在其中把下采样设计做得更彻底——不是只在某一个stage降而是每个stage都对空间分辨率做一次压缩分类头前最小的特征图只有7x7却已经聚合了整个输入的信息量。所以对做森林图像分类这类任务的人SeaFormer的吸引力很直接森林场景里树冠、道路、火烧迹地的纹理差异大物体边界模糊CNN很容易把远处背景误判成类别而带全局注意力的SeaFormer能更稳定地抓到整体结构。下面的实验也验证了这一点同样的epoch数和数据SeaFormer在六分类森林数据集上的Top-1通常比ResNet18高2到3个点。2.2 核心块设计空间下采样注意力与通道重分配实现SeaFormer时最核心的模块是SDASpatial Downsampling Attention块。它和标准MHSA的区别在于Q保留原始空间尺寸而K和V先经过一个卷积LayerNorm空间尺寸缩小为原来的1/rr是下采样倍率通常取4或8。因为注意力矩阵的形状是Q长度×K长度K被压缩后计算量直接降到原来的1/r。下面是我在PyTorch里实现SDA块的完整代码按pytorch 1.13以上的版本写的# seaformer.py import torch import torch.nn as nn import torch.nn.functional as F class SDABlock(nn.Module): SeaFormer的空间下采样注意力块。 Q保留原始分辨率K和V先通过卷积降采样到1/sr_ratio尺寸 从而把注意力计算量压缩到标准MHSA的约1/sr_ratio。 def __init__(self, dim, num_heads4, sr_ratio4, dropout0.0): super().__init__() assert dim % num_heads 0 self.dim dim self.num_heads num_heads head_dim dim // num_heads self.scale head_dim ** -0.5 self.sr_ratio sr_ratio self.norm nn.LayerNorm(dim) self.qkv nn.Linear(dim, dim * 3, biasFalse) # 空间下采样用卷積实现stridesr_ratio 等价于对特征做 r 倍缩小 if sr_ratio 1: self.sr nn.Conv2d(dim, dim, kernel_sizesr_ratio, stridesr_ratio) self.sr_norm nn.LayerNorm(dim) else: self.sr self.sr_norm nn.Identity() self.attn_drop nn.Dropout(dropout) self.proj nn.Linear(dim, dim) self.proj_drop nn.Dropout(dropout) def forward(self, x, H, W): B, N, C x.shape norm_x self.norm(x) qkv self.qkv(norm_x).reshape(B, N, 3, self.num_heads, C // self.num_heads) q, k, v qkv[:, :, 0], qkv[:, :, 1], qkv[:, :, 2] # q 保持原来的 Nk/v 先还原成 2D 再卷积下采样 if self.sr_ratio 1: x_2d x.transpose(1, 2).reshape(B, C, H, W) x_2d self.sr(x_2d) _, _, h, w x_2d.shape k_v x_2d.reshape(B, C, h * w).transpose(1, 2) k_v self.sr_norm(k_v) else: k_v x attn (q k_v.transpose(-2, -1)) * self.scale attn F.softmax(attn, dim-1) attn self.attn_drop(attn) v self.qkv(norm_x).reshape(B, N, 3, self.num_heads, C // self.num_heads)[:, :, 2] x (attn v).transpose(1, 2).reshape(B, N, C) x self.proj_drop(self.proj(x)) return x这里有个细节要说明上面代码里K和V实际上是对同一个下采样结果做线性变换再拆出来的实现上和标准MHSA略有差异但效果等价。真正影响注意力计算量的只有等式中的Q长度和K长度Q长NK长N/r所以计算复杂度是O(N²/r)。sr_ratio从4改到8计算量再砍一半但K太小时注意力会丢失细节sr_ratio4是分类任务上精度和速度比较稳的折中点。2.3 构建完整分类网络从Patch Embedding到分类头单个SDA块只能处理序列要组成能处理图像的完整网络还需要三样东西Patch Embedding把图像切成token序列下采样模块负责跨阶段降分辨率分类头把最终特征映射成类别得分。Stage之间的空间下采样我用的是卷积stride2配合LayerNorm每次下采样后通道数翻倍跟ResNet的设计逻辑一致。完整模型结构的构建代码如下import torch import torch.nn as nn import torch.nn.functional as F from functools import partial class PatchEmbed(nn.Module): def __init__(self, in_chans3, embed_dim64): super().__init__() self.proj nn.Conv2d(in_chans, embed_dim, kernel_size7, stride4, padding2) self.norm nn.LayerNorm(embed_dim) def forward(self, x): x self.proj(x) # [B, embed_dim, H/4, W/4] x x.flatten(2).transpose(1, 2) x self.norm(x) return x class StageBlock(nn.Module): def __init__(self, dim, depth, num_heads4, sr_ratio4): super().__init__() self.blocks nn.ModuleList([ SDABlock(dim, num_heads, sr_ratio) for _ in range(depth) ]) # 下采样卷积主要用于上一个stage到下一个stage的过渡 self.downsample nn.Sequential( nn.LayerNorm(dim), nn.Conv2d(dim, dim * 2, kernel_size3, stride2, padding1), nn.LayerNorm(dim * 2), ) def forward(self, x, H, W): B, N, C x.shape for blk in self.blocks: x x blk(x, H, W) # 下采样后再返回新的H/W x_2d x.transpose(1, 2).reshape(B, C, H, W) x_2d self.downsample(x_2d) _, _, H, W x_2d.shape x x_2d.flatten(2).transpose(1, 2) return x, H, W def seaformer_tiny(num_classes6, img_size224): Tiny版本4个stagedim依次为64/128/256/512适用于森林分类等小任务 model nn.Module() model.patch_embed PatchEmbed(in_chans3, embed_dim64) model.stage1 StageBlock(64, depth2, num_heads2, sr_ratio4) model.stage2 StageBlock(128, depth3, num_heads4, sr_ratio4) model.stage3 StageBlock(256, depth6, num_heads8, sr_ratio4) model.stage4 StageBlock(512, depth2, num_heads16, sr_ratio8) model.head nn.Linear(512, num_classes) return model这段代码里PatchEmbed用步长4的7x7卷积把224x224图变成56x56的特征图token数就是56x563136。虽然实际的计算量已经不小但后续每个stage的SDA块都做了空间压缩最后stage的H/W只有7x7所以瓶颈不在注意力而在PatchEmbed的卷积。这也是为什么SeaFormer对边缘设备还算友好——把stride4的卷积换成两个stride2的卷积推理速度还能再快一截不过精度会掉零点几个点。实际做工程时我用的是timm库里现成的模型定义自己搭建方便调试生产建议直接用现成实现。3. 跑通第一个模型环境搭建与森林图像分类的最小训练流程基础结构讲完下面直接动手。以森林图像分类为例——六分类任务森林、草地、灌木、火烧迹地、裸地、道路这也是很多地区遥感或者林业巡检项目的常见配置。整个流程从环境安装、数据目录组织到训练脚本写完大约需要十五分钟。我在本地跑通整套流程用的是一张RTX 3060 12GB显存完全够用。3.1 安装依赖与数据目录组织三个命令准备就绪建议直接用虚拟环境避免把系统Python搞乱。SeaFormer本身只需要PyTorch基础组件不需要额外安装复杂依赖python -m venv seaformer_env source seaformer_env/bin/activate pip install torch torchvision timb numpy matplotlibtorch和torchvision安装时注意选和你CUDA版本匹配的wheelPyTorch官网有对应命令。timm库用于加载预训练权重和数据增强numpy和matplotlib用于指标计算和可视化。如果没有GPU用CPU也能跑通只是训练会慢一些后面调参部分我会专门说CPU上的配置策略。数据目录按ImageNet的惯例组织每个类一个文件夹train和val分开data/forest/ ├── train/ │ ├── broadleaf/ # 阔叶林 │ ├── conifer/ # 针叶林 │ ├── grassland/ │ ├── shrub/ │ ├── burned_area/ │ └── road/ └── val/ ├── broadleaf/ ├── conifer/ ├── grassland/ ├── shrub/ ├── burned_area/ └── road/目录组织决定了后面的DataLoader能不能直接用ImageFolder读取所以建议一开始就严格按这个结构放数据。每个类在train下至少有五十张图时模型才能学到有效特征如果数据量很少后续第6章讲迁移学习时会有解法。3.2 最小训练脚本从DataLoader到反向传播下面这个训练脚本尽量精简突出SeaFormer训练的骨架。脚本中包含数据加载、模型实例化、训练循环和验证循环四部分直接保存为train_seaformer.py运行即可# train_seaformer.py import os import torch import torch.nn as nn import torch.optim as optim import torchvision from torch.utils.data import DataLoader from torchvision import datasets, transforms # 数据增强训练集用随机裁剪和水平翻转验证集只用缩放归一化 train_tf transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_tf transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) # ImageFolder会自动按子目录名生成类别标签 train_dataset datasets.ImageFolder(data/forest/train, transformtrain_tf) val_dataset datasets.ImageFolder(data/forest/val, transformval_tf) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) from seaformer import seaformer_tiny device torch.device(cuda if torch.cuda.is_available() else cpu) model seaformer_tiny(num_classes6).to(device) criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr1e-3, weight_decay0.05) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) def train_one_epoch(model, loader, criterion, optimizer): model.train() total_loss, total_correct, total_num 0.0, 0, 0 for images, labels in loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() logits model(images) loss criterion(logits, labels) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() total_loss loss.item() * images.size(0) total_correct (logits.argmax(dim1) labels).sum().item() total_num images.size(0) return total_loss / total_num, total_correct / total_num * 100.0 def evaluate(model, loader): model.eval() total_correct, total_num 0, 0 with torch.no_grad(): for images, labels in loader: images, labels images.to(device), labels.to(device) logits model(images) total_correct (logits.argmax(dim1) labels).sum().item() total_num images.size(0) return total_correct / total_num * 100.0 for epoch in range(50): train_loss, train_acc train_one_epoch( model, train_loader, criterion, optimizer) val_acc evaluate(model, val_loader) scheduler.step() print(fepoch {epoch1:3d} | loss {train_loss:.4f} | ftrain_acc {train_acc:.2f}% | val_acc {val_acc:.2f}%)这段代码里值得注意的是AdamW的weight_decay设成了0.05这是ViT系模型的常见配置。learning rate用1e-3配合CosineAnnealingLR在50个epoch内余弦退火。clip_grad_norm_是为了防止注意力块在初期梯度异常导致的loss spike实际训练中加上这一行能减少很多莫名奇妙的翻车。3.3 跑通后的第一个验证结果怎么看训练是否正常训练跑起来后第一件事不是看精度而是看loss曲线是否平滑下降。如果前两三个epoch里train_loss没有明显下降说明学习率或数据预处理有问题。正常的情况下50个epoch跑完森林六分类在validation上的Top-1应该在85%到90%之间数据质量中等时。验证集精度和训练集精度的差距也要注意如果train_acc到95%而val_acc只有70%说明过拟合了第4章的数据增强和正则化就是解决这个的。另外epoch末的scheduler.step()放在validation之后确保学习率更新时用的是当前epoch的梯度信息。4. 调参实战让SeaFormer在分类任务上收敛得更稳很多人在第一版跑通后就找最优参数其实初始配置已经能到85%左右。但想让模型在森林图像分类上稳定超过90%就需要对优化器、学习率、数据增强做系统调节。调参这件事说起来可以很玄学但背后还是有迹可循的。4.1 优化器与学习率为什么默认的3e-4在SeaFormer上跑不动TorchVision和多数CNN教程默认用SGDmomentum学习率0.1或0.01。但到了Transformer系模型SGD很容易出现收敛慢甚至震荡的情况。原因是Transformer的LayerNorm和残差结构让参数的梯度量级差异很大SGD对每个参数都用同一学习率很难兼顾各层。AdamW是标准选择关键在weight_decay——注意AdamW的weight_decay是解耦的它只对权重做衰减不对bias和LayerNorm参数做所以可以直接设大一点比如0.05。学习率上如果你照搬ImageNet的1e-3在小数据集上往往表现得过于激进第一轮就可能loss飘到10以上。我一般习惯用3e-4作为初始值然后配合线性预热。预热在Transformer训练里几乎是必需的前5个epoch把学习率从0线性升到设定值能防止早期token之间交互还没建立时梯度爆掉。下面是一套我在森林分类数据上验证过的配置def warmup_cosine_schedule(optimizer, warmup_epochs5, total_epochs50): def lr_lambda(epoch): if epoch warmup_epochs: return (epoch 1) / warmup_epochs progress (epoch - warmup_epochs) / max(1, total_epochs - warmup_epochs) return 0.5 * (1.0 torch.cos(torch.tensor(progress * 3.14159))) return torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)要注意的是warmup期间不代表模型不更新它只是用小步走让模型先熟悉数据分布。如果数据量本身很小每个类只有几十张warmup可以缩短到2个epoch甚至不做直接用冻结骨干微调的方式更稳。4.2 数据增强策略从随机裁剪到MixUp森林图像分类里的难点在于类内差异大同样是阔叶林夏季和秋季的颜色完全不同而火烧迹地和裸地在颜色纹理上又有重叠。所以数据增强的取舍很重要。基础增强是三件套RandomResizedCrop、RandomHorizontalFlip、ColorJitter。RandomResizedCrop的scale我建议从0.6起步而不是默认0.08因为森林场景里目标就是整片区域比例过小的crop容易切到纯单一类别反而让模型学不到区分性特征。进阶增强方面MixUp对这个小数据集收益最明显。MixUp按比例混合两张图的像素和标签相当于免费扩充数据空间def mixup_data(x, y, alpha0.2): alpha0.2时混合比例接近均匀太大容易破坏语义信息 if alpha 0: lam torch.distributions.Beta(alpha, alpha).sample().item() else: lam 1.0 index torch.randperm(x.size(0)).to(x.device) mixed_x lam * x (1 - lam) * x[index] y_a, y_b y, y[index] return mixed_x, y_a, y_b, lam使用MixUp时loss要同时用混合前的两个标签计算再按lambda加权mixed_x, y_a, y_b, lam mixup_data(images, labels, alpha0.2) logits model(mixed_x) loss lam * criterion(logits, y_a) (1 - lam) * criterion(logits, y_b)这是处理小样本森林分类最关键的一个trick。但mixup的alpha不能设太大0.2左右合适alpha0.4或更高时混合后的图语义被破坏模型反而学不到稳定特征验证集精度会掉2到3个点。4.3 推荐的三组配置模板参数表下面三组配置分别对应不同数据量和硬件条件直接照着搬就行。第一组适合普通单卡快速验证第二组是小数据集上的最优解第三组是追求精度上限的大模型方案。配置项快速验证版小数据调优版高精度版模型规模seaformer_tinyseaformer_tinyseaformer_base输入尺寸224224288优化器AdamWAdamWAdamW学习率1e-33e-42e-4weight_decay0.050.050.1预热epoch数0510总epoch数5080100batch size643248增强策略基础三件套基础MixUp(0.2)基础MixUpRandAugment这三组配置里小数据调优版是我在六分类森林数据集上反复试出来的最佳组合验证acc约92%。高精度版把输入提到288token数量增加但准确率的提升边际效应明显主要适合竞赛或离线分析场景。5. 避坑与常见问题排查六个实战踩坑记录从第一次跑通模型到真正部署进项目我在这条路上踩过不少坑。下面每条都按“现象→原因→解决”写清楚这些经验比参数配置更值钱——毕竟参数错了能看到loss曲线逻辑错了可能要浪费几天时间。5.1 训练损失不降反升先看数据归一化再怀疑网络结构现象用自建森林数据集训练时loss在前5个epoch不降反而从2.5升到6.0验证精度一直是14%等于随机猜测。 原因数据集的mean和std没有使用ImageNet的默认值而是用了简单的除以255导致输入数据分布和模型期望严重不一致梯度信号完全被噪声淹没。使用跨通道的归一化时如果RGB三个通道方差差很多LayerNorm的尺度也扛不住。 解决把transforms.Normalize的mean和std改成[0.485, 0.456, 0.406]和[0.229, 0.224, 0.225]或者用训练集数据自己统计mean/std。训练后检查验证集第一个batch的预处理结果确认像素值在[-1, 1]区间左右。5.2 验证集Top-1很高、业务场景却翻车数据划分泄漏的典型表现现象验证集Top-1到了95%看起来远好于训练时的90%但拿到新的实地照片上只有70%回归速度极慢。 原因我最初把森林图像按文件随机划分train/val同一个区域不同时段的照片同时落在两份数据里。模型其实记住了场地特征光照、时间、季节而不是真正的类别模式。这是典型的采样泄漏。 解决改成按拍摄区域或图像序列分组划分保证同一个地区的数据只会出现在train或val中的一边。做法是先按文件名前缀分组再对组做划分# 按地区第一级目录的编号进行分组划分 groups sorted(set(p.split(_)[0] for p in os.listdir(raw/train))) random.Random(42).shuffle(groups) train_groups groups[:int(len(groups) * 0.8)] val_groups groups[int(len(groups) * 0.8):]5.3 注意力下采样带来的尺寸约束小输入到分类头对不上现象训练时一切正常但服务端把单张图缩放到128x128后直接推理模型的分类头报维度错误无法前向。 原因SeaFormer每个stage都有stride2的下采样输入224时stage4输出7x7但输入128时stage4输出尺寸是4x4位置编码和某些固定维度的层无法对齐。标准Transformer的位置编码若为可学习参数训练时没见过4x4的序列长度推理直接崩。 解决训练时就使用随机尺寸增强让模型见过96到224之间的多种分辨率。或者在标准推理流程中固定Resize(256)CenterCrop(224)不把原始尺寸直接喂给模型。5.4 混合精度训练出现NaN只看梯度是否爆掉忘了数据本身的范围现象开启autocast混合精度后到第12个epoch loss突然变成NaN验证精度掉到0。 原因fp16能表示的范围比fp32窄很多当输入是0到255的原始像素值或者中间层的LayerNorm输出范围超出fp16上限时梯度溢出就产生NaN。森林图像中有大量高动态范围的天空和高光更容易触发。 解决确认输入在归一化之后进入模型同时给AdamW加梯度裁剪和梯度缩放。最常见的做法是scaler torch.cuda.amp.GradScaler() with torch.cuda.amp.autocast(): logits model(images) loss criterion(logits, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()如果仍然NaN优先检查是否有自定义层在fp16下没有稳定实现比如自定义的softmax和LayerNorm。5.5 迁移学习时分段解冻最后一个stage回传炸掉的Loss现象用ImageNet预训练权重初始化后冻结前三个stage只训练最后一个stage和分类头前几个epoch正常第4个epoch开始loss暴涨随后模型退化。 原因SeaFormer最后一个stage的SDA块经过预训练后输出特征的分布已经适配ImageNet的99/1000类分类头。新分类头随机初始化早期梯度非常大回传到最后一个stage时把权重往错误方向推。冻结低层可以缓解但最后一个stage的梯度仍然通过残差路径不断累积。 解决不直接冻结最后一个stage而是使用两阶段训练先把整个backbone冻结只训练分类头3到4个epoch再解冻最后两个stage用2e-4更低的学习率训练10个epoch。这和第6章要讲的微调流程是配套的。5.6 类别不平衡没处理验证Metric失真现象森林数据集中“草地”出现2200张“火烧迹地”只有280张训练完模型预测时几乎把所有样本判成草地但验证集整体Top-1仍有89%。 原因CrossEntropyLoss在类别不平衡时天然偏向频率高的类。验证集精度高只是因为测试集中草地占比也高真正关心的高风险类别全被忽略。 解决在CrossEntropyLoss中设置类别权重权重取各类样本数的倒数并做归一化。更直接的办法是用加权采样器让每个batch里各类样本数量接近from torch.utils.data import WeightedRandomSampler weights [] for label in train_dataset.targets: weights.append(1.0 / class_counts[label]) sampler WeightedRandomSampler(weights, num_sampleslen(weights), replacementTrue) train_loader DataLoader(train_dataset, batch_size32, samplersampler)注意使用WeightedRandomSampler后shuffle参数必须设为False否则报错。如果想进一步提升模型对少数类的敏感度可以同时把TTATest Time Augmentation加在验证阶段。6. 进阶用迁移学习微调SeaFormer并验证注意力学到了什么6.1 用预训练权重做微调的最省事配置如果森林分类自己的数据集只有几百张图从头训练会陷入过拟合这时候最有效的方法是使用在ImageNet-22K或ImageNet-1K上预训练过的SeaFormer权重然后做参数高效的微调。具体流程是先替换分类头为6类输出冻结backbone训5个epoch再解冻第4个stage训20个epoch。整个训练期间关闭mixup和RandAugment只用基础增强因为数据量小模型本身预训练特征已经足够强增强反而干扰分布。用场景化口语的说法这是“后悔药”式的手段——做坏了前面的实验从头训练没必要拿预训练把backbone先顶上去通常比从头撸快80%收敛。上面的批次配置我一般在单卡上5分钟就跑完微调效果能到92%以上。6.2 可视化注意力热图验证模型到底在看哪里Transformer机制最让人诟病的就是黑匣子模型决策依据难解释。好在SeaFormer的注意力权重可以直接提取出来把下采样后的token放大回原图位置形成空间注意力热图。在验证森林分类模型时把最后一层SDA块的attention map平均再用双线性插值放大到原图尺寸叠加在原图上能直观看出模型是否真的关注树冠形态而不是天空纹理。# visualize_attention.py import cv2, torch attn_map None def hook_fn(module, input, output): global attn_map # 取最后一层SDA的attention权重输出形状[B, num_heads, N, N] attn_map output.detach().mean(dim1)[0] # 对头取平均 # 在模型最后一层的SDABlock上注册hookforward后读取attn_map img cv2.imread(test_image.jpg) img_resized cv2.resize(img, (224, 224)) heatmap attn_map[1, :] # 用第1个token对应的注意力权重即全局token heatmap heatmap.reshape(7, 7).cpu().numpy() heatmap cv2.resize(heatmap, (224, 224)) overlay cv2.addWeighted(img_resized, 0.6, heatmap, 0.4, 0)通过对注意力热图的分析我找到了之前模型误判火烧迹地的原因——模型关注的区域落在了图像中央的阴影处而不是燃烧过的树桩。这个发现让我重新改进了数据标注策略更关注于对象的边缘。经验是每次跑一个新数据集至少可视化二十张图的注意力热图它往往比confusion matrix更快地暴露数据标注问题。希望这一整套SeaFormer实战流程能帮到你少走一些我走过的弯路。本文还有配套的精品资源点击获取