CrossFormer图像分类实战:多尺度注意力与训练调参全解析

发布时间:2026/10/5 8:06:15
CrossFormer图像分类实战:多尺度注意力与训练调参全解析 简介CrossFormer图像分类实战包面向有一定深度学习基础、希望将视觉Transformer落地的开发者提供从数据准备、模型搭建到训练评估的完整工程实现。压缩包共2000个文件含1986张PNG图像、7个Python脚本、1个JSON类别映射文件、1个pth权重文件及说明文档整体约835MB目录清晰便于按模块复用。已有191人浏览学习适合作为图像分类任务的技术验证与模型对比基线。CrossFormer独有的跨尺度注意力机制能有效融合不同层级特征在图像细节与整体结构上均有良好表现资源内的训练好权重与现成脚本可省去大量数据准备和预训练时间。通过完整工程与JSON类别对应关系使用者能快速跑通训练流程、开展调参优化并迁移到其他视觉任务是一份可直接落地的实战资料。1. CrossFormer凭什么在图像分类里站稳脚跟做过几年图像分类的工程师大概都有这种感觉ResNet系列吃透了卷积的局部归纳偏置但感受野始终受限ViT把全局注意力拉满小数据集上却经常训得比CNN还差。CrossFormer属于“最新的图像分类模型”里比较务实的一类——它不跟Transformer结构硬刚而是把多尺度信息做进了Token生成这一步再用动态位置偏置缓解平移不变性的丢失。我最初用CrossFormer替换手头森林图像分类任务的Baseline在同样的224分辨率下Top-1精度比同量级ViT高了两到三个点显存开销还更小。这篇笔记就从模型设计、数据准备、完整训练脚本到避坑经验整理一遍适合想把手头分类任务从CNN迁移到新架构的读者。2. 先看懂CrossFormer的设计跨尺度嵌入与动态位置偏置2.1 跨尺度嵌入Cross-scale Embedding是怎么把Token变“聪明”的ViT把图像切成一堆16×16的Patch每个Patch里的局部结构差异直接被平均掉小目标在Token里基本消失。CrossFormer的思路是先保留多尺度再融合而不是从头到尾只用一种Patch尺寸。跨尺度嵌入层在每个Stage入口并行做几次卷积分别模拟小尺度、中尺度、大尺度的局部特征再把它们Concat到一起展平成Token序列。具体到实现常见做法是用GroupNorm加多个不同核大小的卷积。比如Stage 1里我用4×4、8×8、16×16三种卷积核Stride分别设置为4、8、16这样三种尺度的输出在空间分辨率上保持一致都是H/4 × W/4最后沿着通道维度拼起来再接一个LayerNorm。这样做的好处是一个小目标只要在4×4的卷积核范围内有响应它的信息就不会被大核卷积平均掉而大目标又能在16×16的核里看到更完整的纹理。import torch import torch.nn as nn class CrossScaleEmbedding(nn.Module): def __init__(self, img_size224, patch_size4, in_chans3, embed_dim32): super().__init__() self.num_patches (img_size // patch_size) ** 2 self.proj_list nn.ModuleList() for rate in [1, 2, 4]: kernel_size patch_size * rate stride patch_size * rate proj nn.Sequential( nn.Conv2d(in_chans, embed_dim // 3, kernel_sizekernel_size, stridestride, biasFalse), nn.GroupNorm(1, embed_dim // 3) ) self.proj_list.append(proj) def forward(self, x): # x: [B, C, H, W] feats [] for proj in self.proj_list: feats.append(proj(x).flatten(2).transpose(1, 2)) token torch.cat(feats, dim-1) return token # [B, num_patches, embed_dim]这里的embed_dim // 3是为了让三种尺度最终拼接后等于目标维度实际实现里如果embed_dim不能被3整除官方会更精细地分配通道数比如Tiny模型是32维分成12、10、10。我在自己实验时直接改成可配置维度避免除不尽导致维度对不上。注意GroupNorm(1, C)等价于InstanceNorm对每个样本独立做归一化能保留对比度信息比BatchNorm在Token生成阶段更稳。2.2 长短距离注意力LSDA与动态位置偏置DPBCrossFormer的Transformer Block分两半短距离注意力管局部纹理长距离注意力管全局关系。短距离注意力把每个7×7窗口内做自注意力和Swin的Window Attention类似但CrossFormer的窗口是分组的——奇数层用短距离偶数层用长距离长距离那层不切窗口直接全局注意力。这样交替使用局部和全局信息都能往前走不靠Shift Window那种复杂的窗口移动。另一个关键设计是动态位置偏置DPBDynamic Positional Bias。ViT只加一次绝对位置编码之后每层都用同一份位置信息这对平移不变性是有害的——同一只鸟往左挪了10个像素位置编码变了注意力权重也跟着变。DPB的做法是在计算Query和Key的相对偏移量之后用一个两层MLP把偏移量映射成偏置值加进Attention的logits里。因为MLP是动态计算的模型能学到“偏移多远该给多少偏置”而不是固定查表。import torch import torch.nn as nn class DynamicPositionalBias(nn.Module): def __init__(self, num_heads, max_offset7): super().__init__() self.mlp nn.Sequential( nn.Linear(2, 512, biasTrue), nn.ReLU(inplaceTrue), nn.Linear(512, num_heads, biasTrue) ) self.max_offset max_offset def forward(self, relative_coords): # relative_coords: [num_heads, q_patches, k_patches, 2] bias self.mlp(relative_coords) return bias.squeeze(-1)实现细节里最容易被忽略的是相对坐标的归一化。我的做法是把相对偏移除以窗口尺寸再乘一个缩放因子让offset落在[-1,1]区间否则MLP在数值大的坐标上输出不稳定。实际训练中DPB让模型在测试时对物体的位置变化更鲁棒尤其在小数据集上能明显看到验证集精度抖动变小。2.3 CrossFormer系列模型怎么选Tiny/Small/Base/Large的参数差异CrossFormer系列参数配置和Swin类似从Tiny到LargeEmbedding维度、深度、注意力头数都在涨。Tiny的embed_dims是[32,64,128,256]Small是[48,96,192,384]Base是[64,128,256,512]Large是[96,192,384,768]。对我来说Tiny适合探路显存占用小224分辨率下Batch Size 128在单张2080Ti上能跑几分钟就能看一次验证集。Small是性价比最高的选择精度比Tiny高两到三个点训练时间只多60%。Base一般用在数据集超过10万张的场景。Large不推荐除非你有A100集群它的参数量较大推理延迟在CPU上尤其感人。# 以crossformer_tiny_224为例的配置参考 model_cfg { img_size: 224, patch_size: 4, embed_dims: [32, 64, 128, 256], depth: [2, 2, 4, 2], num_heads: [1, 2, 4, 8], group_size: [7, 7, 7, 7], }GroupSize在四个Stage都设成7这是官方参数我试着改成[7,7,14,14]想在大图上多给窗口一些上下文发现小数据集反而泛化变差。GroupSize不是越大越好它影响短距离注意力的局部性调它之前先确认你的任务真的需要更大局部感受野。3. 准备图像分类环境与数据集从timm到森林图像3.1 环境配置torch、timm与CrossFormer的版本搭配CrossFormer的完整训练环境我建议直接用PyTorch 1.12以上加timm 0.6.12以上。timm从0.6.0开始把CrossFormer放进模型库提供预训练权重这对没有太多机器资源跑预训练的人很友好。我一般这样建环境conda create -n crossformer python3.9 conda activate crossformer pip install torch1.13.1 torchvision0.14.1 --extra-index-url https://download.pytorch.org/whl/cu117 pip install timm0.9.2 pip install tensorboard tqdm scikit-learn版本搭配上有两个坑。第一个是torch和torchvision必须配套不然DataLoader会报奇奇怪怪的错误第二个是timm版本别太新0.9.2附近对CrossFormer的注册表最完整再新的版本我发现有时会丢失一些预训练权重入口。如果你要加载官方释放的.pth文件注意官方代码是基于他们自己的models模块写的和timm里的实现虽然结构一致但权重键名有差异这个在第5章避坑里细说。3.2 数据集组织以森林图像分类为例的目录结构准备数据集的常规结构就是ImageFolder格式训练集和验证集分开放类别名作为子目录名。森林图像分类任务里我处理的类别包括“红杉”“云杉”“桦树”“松树”等每个类别大概1500到3000张图片。目录结构如下:forest_dataset/ ├── train/ │ ├── conifer/ │ ├── broadleaf/ │ └── mixed/ └── val/ ├── conifer/ ├── broadleaf/ └── mixed/整理数据的时候有几个要点。第一类别样本数尽量均衡某类超过另一个类三倍的时候精度会被大类别带偏第二验证集不要从训练集里直接随机抽最好按拍摄地点或时间段划分防止同一场景的重复帧让验证集虚高第三图片格式统一转成RGB三通道否则有灰度图混进来预处理时会产生尺寸匹配错误。3.3 数据增强与预处理参数CrossFormer对数据增强的敏感度比ViT低但比ResNet高。我的标准增强管线是RandomResizedCrop(224)、RandomHorizontalFlip、ColorJitter(0.4, 0.4, 0.4)、CutMix和MixUp。CutMix和MixUp的开启方式是用timm的mixup_fn统一封装不要在Dataset里手动做否则会和RandomResizedCrop的顺序打架。from timm.data import create_transform from timm.data import Mixup train_transform create_transform( input_size224, is_trainingTrue, color_jitter0.4, auto_augmentrandaug-m9-mstd0.5, re_prob0.25, re_modepixel, re_count1, ) mixup_fn Mixup( mixup_alpha0.8, cutmix_alpha1.0, prob1.0, switch_prob0.5, label_smoothing0.1, num_classes3, )我建议不要一上来就全套增强。先只用RandomResizedCrop和Flip跑10个Epoch确认模型能过拟合训练集精度上到95%以上再开Mixup和AutoAugment。直接在欠拟合状态下叠加增强你根本分不清是模型容量不够还是增强太强导致训不动。森林图像数据里很多是无人机俯拍图颜色变化不大我把ColorJitter的饱和度部分调到0.3避免树影变化被过度增强。4. 用CrossFormer训练图像分类模型从加载预训练权重到完整脚本4.1 加载CrossFormer模型timm与官方代码的两种方式加载预训练权重有两种常见做法。第一种直接用timm一行代码搞定适合快速验证第二种用官方GitHub仓库里的CrossFormer定义适合你要改结构做实验的场景。我平时先用timm跑通流程后期要细调某个Stage的Attention就会切到官方实现。import timm import torch # 方式一timm加载 model timm.create_model( crossformer_tiny_224, pretrainedTrue, num_classes3, ) # 方式二官方代码风格 # from models.crossformer import CrossFormer # model CrossFormer( # img_size224, # in_chans3, # num_classes3, # embed_dims[32, 64, 128, 256], # depths[2, 2, 4, 2], # num_heads[1, 2, 4, 8], # group_size[7, 7, 7, 7], # )加载预训练权重时有一个很容易忽略的点如果num_classes改了最后的分类头是随机初始化的而前面的主干是ImageNet预训练的。这种情况下分类头的学习率应该设为主干的10倍或者在前5个Epoch用较小的学习率只热身分类头。我习惯用param_group把分类头单独拎出来给它设lr * 10这样前期收敛快主干权重不会被打乱。4.2 训练主脚本优化器、学习率调度与混合精度训练CrossFormer的优化器我推荐LAMB或AdamW学习率从1e-4到5e-4之间做网格搜索Batch Size越大学习率越高。下面这个脚本是我在森林图像分类任务上走通的最小版本单卡V100Batch Size 128训练50个Epoch。import torch from torch import nn from timm.optim import Lamb from timm.scheduler import CosineLRScheduler from timm.loss import SoftTargetCrossEntropy from tqdm import tqdm def train_one_epoch(model, loader, optimizer, criterion, mixup_fn, scaler): model.train() total_loss 0.0 correct 0.0 total 0 for images, labels in tqdm(loader, desctrain): images, labels images.cuda(), labels.cuda() if mixup_fn is not None: images, labels mixup_fn(images, labels) with torch.cuda.amp.autocast(): outputs model(images) loss criterion(outputs, labels) # 这里labels是经过mixup的软标签不能用argmax计算acc optimizer.zero_grad() scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() total_loss loss.item() * images.size(0) if mixup_fn is None: correct (outputs.argmax(dim1) labels).sum().item() total images.size(0) return total_loss / total, correct / total if mixup_fn is None else 0.0 model timm.create_model(crossformer_tiny_224, pretrainedTrue, num_classes3).cuda() optimizer Lamb(model.parameters(), lr2e-3, weight_decay0.02) scheduler CosineLRScheduler( optimizer, t_initial50, warmup_t5, warmup_lr_init1e-6, ) criterion SoftTargetCrossEntropy() scaler torch.cuda.amp.GradScaler()注意这个脚本里Lamb的lr2e-3是配合大Batch Size 128来设的Batch Size小一半时学习率也要跟着减半否则很容易在头几个Epoch就发散。这里用SoftTargetCrossEntropy是因为Mixup产生了软标签如果你关掉Mixup直接用nn.CrossEntropyLoss()。混合精度在V100和A100上是必须的训练速度快三成且在我的实验里没有出现精度下降如果是老卡如P100fp16的支持不完整会报错或者损失不收敛那就改成fp32。4.3 评估与混淆矩阵验证不只靠Top-1验证时不能只看Top-1 Acc还要看混淆矩阵。森林图像分类里“针叶林”和“混交林”之间经常互相误判Top-1高了也可能是因为类别不平衡。我每个Epoch结束后跑一次验证集统计准确率、召回率和Kappa系数。from sklearn.metrics import confusion_matrix, cohen_kappa_score import numpy as np torch.no_grad() def evaluate(model, loader): model.eval() all_preds [] all_labels [] for images, labels in tqdm(loader, desceval): images images.cuda() outputs model(images) preds outputs.argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) kappa cohen_kappa_score(all_labels, all_preds) acc np.mean(np.array(all_preds) np.array(all_labels)) return acc, kappa, cm验证的时候要关掉Mixup和CutMix但要不要开多尺度测试是有争议的。我的经验是224分辨率训练验证时跑一次原始尺寸和一次224取平均Softmax分数大约能提0.3到0.5个点。但如果验证集很大多尺度推理会让时间翻倍工业落地上不划算我只在交报告结论时才用多尺度。5. CrossFormer实战常见问题排查越过这三个坑模型才能收敛5.1 坑一预训练权重加载报错——键名不匹配现象是timm.create_model(..., pretrainedTrue)时某些键对不上或者加载官方.pth时报Missing key(s)和Unexpected key(s)。原因很简单官方仓库的classifier层叫headtimm里的分类层叫head或fc这个还能对上但官方实现里的norm层命名和timm里不一样。解决方法是先加载剔除分类头的权重再重新初始化分类头checkpoint torch.load(crossformer_tiny.pth, map_locationcpu) state_dict checkpoint[model] if model in checkpoint else checkpoint # 剔除分类头相关键 state_dict {k: v for k, v in state_dict.items() if not k.startswith(head)} model.load_state_dict(state_dict, strictFalse)严格来说strictFalse会放过缺失键但你要警惕它可能放过了主干里的某个Norm层导致后面训练时数值不稳定。我的习惯是把加载完的模型跑一次推理看输出是不是有限值且不是全零做一次冒烟测试再开始训练。5.2 坑二小数据集训不动——学习率与Warmup没配好现象是损失降不下来或者验证精度一直在几个百分点晃悠。森林图像分类数据集总共几千张直接上lr5e-4CrossFormer的前几层特征很快就被打乱。原因在于视觉Transformer对小数据集的学习率极敏感预训练权重的特征分布是ImageNet的学习率太大一个Step就把原本好好的特征洗掉了。解决方法是把Warmup从3个Epoch加到10个Epoch让学习率从1e-6缓缓爬升前几个Epoch相当于只训练分类头。同时把主干参数的学习率设成分类头的三分之一。这样做的原理是主干在ImageNet上学到的通用特征边缘、颜色、纹理不应该被几个小数据集的梯度粗暴修改分类头才是需要快速适应新任务的部分。5.3 坑三显存爆掉——输入分辨率与Batch Size的取舍现象是CUDA Out Of Memory或者训练途中某个Batch突然崩溃。CrossFormer的注意力复杂度不是纯二次方短距离注意力按窗口计算长距离注意力全局算显存占用比ViT好一些但比CNN还是高。单卡16G显存Batch Size 128跑224分辨率没问题换到384分辨率Batch Size要降到32。处理显存爆掉的常用做法是将Batch Size减半同时把学习率按比例往下调否则Batch Size变了优化轨迹也跟着变。还有一招是开启torch.utils.checkpoint梯度检查点把某些Attention层的激活值不保存反向传播时重新计算能省三成显存代价是训练时间多15%。from torch.utils.checkpoint import checkpoint # 在CrossFormerBlock的forward中按需包裹 # def forward(self, x): # x checkpoint(self.short_attn, x) # x self.norm1(x) # x checkpoint(self.long_attn, x) # return x用梯度检查点的时候注意它只适合显存紧张但时间充裕的场景分布式训练里配合DDP可能每张卡的显存占用不均需要额外同步。5.4 坑四验证集精度抖动大——EMA与多尺度测试的取舍现象是训练损失还在下降验证集精度却上蹿下跳或者某一轮突然跌了三四个点又涨回来。原因是单轮权重对最后一轮Batch的噪声过于敏感。解决方式是加EMA指数移动平均把过去N个Epoch的权重做平均作为最终模型。我在CrossFormer上加EMA后森林图像分类的验证Kappa系数从0.86提升到0.89提升幅度在这个数据集上算很明显。class EMA: def __init__(self, model, decay0.9999): self.model model self.decay decay self.shadow {k: v.clone().detach() for k, v in model.state_dict().items()} def update(self): for k, v in self.model.state_dict().items(): self.shadow[k].mul_(self.decay).add_(v.detach(), alpha1 - self.decay) def apply_shadow(self): for k, v in self.model.state_dict().items(): v.data.copy_(self.shadow[k])EMA的decay参数在0.999到0.9999之间数据集越小decay应该越大因为小数据集上权重震荡更剧烈。注意EMA只是验证时用训练中不要直接替换模型权重否则梯度更新节奏会被打乱。6. 验证模型真的“懂了”图像注意力可视化与特征图检查6.1 用forward_hook导出注意力权重训练完不能只看一组指标就收工我始终认为可视化能暴露指标掩盖的问题。CrossFormer的每个Block里都有注意力权重用forward_hook把这些权重捞出来挑测试集里分类正确的和错误的各几张看模型到底在关注什么。attention_maps {} def hook_fn(module, input, output): # 输出里包含attention weights需要按模块内部结构索引 attention_maps[module.name] output for name, module in model.named_modules(): if attn in name and isinstance(module, nn.Module): module.name name module.register_forward_hook(hook_fn)可视化时最典型的发现是分类错误的图片注意力Map往往破碎成几个孤立的小块或者集中在背景纹理上。这时候我会回到训练集里检查对应类别的样本多半是标注有噪声或者增强管线过度裁剪让目标只露出不到一半。顺带说一句这不是CrossFormer独有的问题我用Swin和ViT做同样检查时也会遇到类似现象但CrossFormer的错误注意力Map通常更集中修复标注后回升更快。6.2 特征图差异判断模型是否学到多尺度信息另一种验证方法是导出Stage 1和Stage 4的特征图对比它们的空间分辨率与语义。Stage 1的特征图应该是边缘、颜色块等低级特征Stage 4靠近分类头特征图应该能明显区分出树冠的轮廓与空地背景。如果Stage 4的特征图还像噪声一样分布均匀说明模型在最后几个Block里没做有效的全局聚合这时要回头看长距离注意力的配置或者训练是否充分。# 注册hook输出中间层特征并保存为tensor def extract_feature_maps(model, image): feats {} def hook_fn(module, input, output): feats[module.name] output.detach().cpu() for name, module in model.named_modules(): if block.0 in name: # Stage 1 第一个block module.name name module.register_forward_hook(hook_fn) model.eval() with torch.no_grad(): _ model(image.unsqueeze(0).cuda()) return feats一个值得记住的教训是Stage 1的跨尺度嵌入层如果有三个并行分支你可以直接把三个分支的输出各打印成一张图看看是不是一个分支在学颜色、一个分支在学纹理、一个分支在学轮廓。我实验时发现如果三个分支学出来的特征高度一致说明跨尺度融合退化了常见原因是GroupNorm的初始化或者卷积核尺寸分布不合理。这时把最大的卷积核往下调一档重新训练多尺度区分度就回来了。6.3 一个值得坚持的习惯每次完成一个实验我会把训练曲线、混淆矩阵、注意力热图、特征图样例四个东西放进一个以日期命名的目录。没有这些记录三个月后回看模型你只知道精度数字根本不记得当时为什么把GroupSize从7改成14。做图像分类这行模型能跑通只是开始能复现、能解释、能迁移才是真的掌握了。这是我自己栽过跟头之后养成的习惯希望帮到你。本文还有配套的精品资源点击获取