简介CrossFormer是面向图像分类任务的视觉Transformer实战资源核心围绕跨尺度注意力机制展开适合有一定深度学习基础、希望理解并快速上手该模型的开发者和研究人员。压缩包共2000个文件、约835.34MB主体为1986张png图像便于配合代码查看数据样本、过程可视化与结果样例另有7个py脚本负责数据加载、模型定义与训练评估配合1个json类别映射文件、1个txt说明文件和1个pth模型权重便于直接复现与二次调优。目前已有191人学习下载。整套资源提供从数据准备、跨尺度特征交互到分类结果输出的完整代码与配套文件。读者可获得可运行的训练/推理脚本、类别文件、预训练权重和大量可视化结果借此理解CrossFormer的关键机制同时清晰的目录结构与脚本分工也方便按需修改为后续迁移到检测、分割等视觉任务打下基础。1. CrossFormer图像分类的落脚点跨尺度Token与长短距离注意力做森林图像分类做到怀疑人生的时候很多人会以为问题出在训练技巧上但真正卡住精度的往往是token生成方式ViT固定patch大小细碎纹理被平均成一块色斑全局注意力又让计算量随分辨率平方上涨。在图像分类模型的更新迭代里CrossFormer这种把跨尺度嵌入和长短距离注意力同时塞进骨干网络的设计解决的就是这个痛点。它让token在生成阶段就携带多感受野信息再用长短分工的注意力把计算成本压下来。这篇实战笔记按原理→跑通→调参→踩坑→进阶的顺序讲清楚CrossFormer怎么用于图像分类以及替换ViT/Swin时容易翻车的地方。2. 理解CrossFormer的核心设计跨尺度嵌入与长短距离注意力2.1 跨尺度嵌入层CEL把不同感受野的卷积特征融进tokenCrossFormer在图像分类任务上最打动我的一点是它把多尺度这步从网络深处提到了最前端。ViT的做法是把图像切成长宽16或32的patch每个patch直接展平映射成向量。问题在于patch一旦定下来感受野就定死了16x16的patch里如果同时混着树干的纹理和叶片的边缘卷积嵌入会把这两种信号平均成一块没有区分度的区域。森林图像里细碎目标多这种信息损失是实打实的精度损失。CEL的常见做法是用3x3、5x5、7x7三个不同大小的卷积核并行扫一遍输入得到三份感受野不同的特征图在通道维拼接后用1x1卷积融合。这样每个token在生成时既看到了小块范围内的锐利边缘也看到了更大范围的上下文。融合后的特征图按crs_interval步长做unfold采样得到带跨尺度信息的token序列。我在项目里把这段写成独立模块结构大致是import torch import torch.nn as nn class CrossScaleEmbedding(nn.Module): 跨尺度嵌入并行多分支卷积输出在通道维拼接后融合 def __init__(self, in_ch, embed_dim, interval4, kernels(3, 5, 7)): super().__init__() self.interval interval # 每个分支输出 embed_dim/3 通道保证拼接后总维度不变 self.branches nn.ModuleList([ nn.Conv2d(in_ch, embed_dim // len(kernels), k, paddingk // 2) for k in kernels ]) self.fuse nn.Conv2d(embed_dim, embed_dim, 1) def forward(self, x): feats [branch(x) for branch in self.branches] x torch.cat(feats, dim1) x self.fuse(x) return x # 特征图后续会被 unfold 成 token 序列逻辑说明branches里三个卷积的padding分别取1、2、3保证不同kernel size下输出分辨率一致否则无法在通道维拼接。interval扮演patch size的角色控制token密度interval越小token越多注意力计算量越大。fuse层把三个分支的信息按权重混合让跨尺度特征在进Transformer之前先对齐。参数说明embed_dim要能被分支数整除用embed_dim // 3是三个分支时的写法加分支数就要同步改这里。interval在CrossFormer的4个stage里分别是4、2、1、1意味着浅层token稀疏、深层token密集这种非均匀采样比每层都密集要省算力。CEL做对的关键点是先融合多尺度、再采样token顺序不能反。如果先采样成patch再做多尺度卷积等价于对每个小patch做特征提取跨尺度信息就丢了这也是CrossFormer跟PVT、CvT在理念上最大的差异。2.2 长短距离注意力LSDA一个多头注意力里的局部与全局分工有了多尺度token后注意力本身也要跟着改。标准全局自注意力确实能让每个token看到全图但特征图分辨率稍大就吃不消Swin用固定窗口切分注意力虽然省了计算量但跨窗口信息只能靠shift间接传递长距离依赖表达得不直接。LSDA把注意力头分成两组短距离注意力组用较小的窗口专注边缘、纹理这类高频局部特征长距离注意力组用大窗口负责对象关系、全局布局这类低频结构。两组头各看各的最后拼接结果。工程价值在复杂度上很直观注意力矩阵的规模由窗口内token数的平方决定长距离组用大窗口但头数少短距离组头数多但窗口小整体计算量被控制在可控范围。长短距离的头数分配不是固定的我拿到的开源实现里short和long两组头数默认各占一半。如果任务里纹理细节特别重要可以把short组比例提到三分之二代价是长距离建模弱一点。这个比例我一般会跑两组实验对比着看选验证集更高的一组不需要纠结理论。实际使用里LSDA对图像分类的贡献不只在精度还在收敛速度。短距离组在训练前20个epoch就快速收敛到位长距离组在后期慢慢补齐类别间的关系这是单窗口模型不容易出现的训练曲线。2.3 CrossFormer与ViT、Swin的选型对比既然要拿CrossFormer做图像分类先得知道它在什么情况下值得替换现有基座。我把三个模型放在同一张表里对比模型token生成注意力视野最适合的场景明显的短板ViT固定16x16 patch全局数据量大的通用分类小纹理信息丢失训练要吃大量数据Swin固定patch局部窗口shift通用分类与检测跨窗口信息传递慢长距离建模弱CrossFormer跨尺度融合长短距离分组森林、遥感等纹理密集任务token多时显存占用偏高选型时我一般看两个条件。第一任务里是否有大量同类目标密集出现比如森林图像里的不同树种、遥感图里的密集房屋这类场景CrossFormer的短距离头明显比Swin更敏感第二是否有跨区域上下文依赖比如判断一片区域是湿地还是农田既要看局部植被纹理也要看全局地形走势CrossFormer的长距离头正好接管这部分。CrossFormer还常被用作双流基座两路输入分别送进两个共享或独立的CrossFormer分支最后融合特征做分类。比如RGB加红外波段红外流用较粗的crs_interval提取宏观纹理RGB流用较细的间隔捕获细节两路特征融合后在森林图像分类里比单流能提升不少。这个用法在遥感赛事里很常见第6章会讲落地细节。3. 本地跑通CrossFormer图像分类从环境到最小训练闭环3.1 环境准备PyTorch与依赖安装CrossFormer的开源实现依赖PyTorch和einops模型结构文件可以独立放进自己项目不用把整个仓库拖进来。我习惯先建一个干净的conda环境避免跟其他项目的torch版本互相污染conda create -n crossformer python3.9 -y conda activate crossformer pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install timm einops tensorboard逻辑说明第一行创建虚拟环境并锁定Python 3.9Transformer类模型在3.9上兼容性最好第二行安装pytorch和torchvisioncu118是CUDA 11.8的wheel索引如果你的显卡驱动不同去PyTorch官网换对应的index-url即可第三行安装的timm提供大量Transformer训练工具调度器、Mixup、EMAeinops是模型里做张量重排的底层库。参数说明tensorboard用于记录训练曲线建议一开始就装。调参全靠看趋势不要凭感觉改学习率。3.2 构建CrossFormer模型加载结构与配置说明从开源实现里拿到crossformer_model.py后模型入口是一个CrossFormer类。我习惯不直接改源码而是在使用端写配置文件把模型尺寸参数集中管理。CrossFormer-T是最小配置单卡就能跑import torch from crossformer_model import CrossFormer def create_crossformer(cfg): model CrossFormer( img_sizecfg[img_size], in_chanscfg[in_chans], num_classescfg[num_classes], group_sizecfg[group_size], crs_intervalcfg[crs_interval], embed_dimscfg[embed_dims], depthscfg[depths], num_headscfg[num_heads], mlp_ratioscfg[mlp_ratios], qkv_biasTrue, drop_path_ratecfg[drop_path_rate], ) return model # CrossFormer-T 配置单卡能跑的最小尺寸 cfg { img_size: 224, in_chans: 3, num_classes: 10, group_size: [7, 7, 7, 7], crs_interval: [4, 2, 1, 1], embed_dims: [64, 128, 320, 448], depths: [2, 2, 8, 14], num_heads: [2, 4, 10, 14], mlp_ratios: [8, 8, 4, 4], drop_path_rate: 0.1, }逻辑说明group_size决定LSDA里每个窗口包含多少token7代表7x7局部窗口crs_interval是CEL的采样间隔4个stage逐步从4减到1token越来越密embed_dims对应4个stage的输出通道64到448是T尺寸的典型取值depths是每个stage的Transformer block数量num_heads是每个stage的注意力头数需要与embed_dims匹配通常是embed_dim/32。参数说明如果显卡显存小于8G把img_size改成160或把batch调小模型结构不用动。drop_path_rate是随机丢弃Transformer层的概率小数据集建议0.05ImageNet级别用0.1到0.2。3.3 数据准备从目录结构到DataLoader图像分类任务里数据组织方式我统一用torchvision的ImageFolder目录结构必须是data/train/类别名/图片文件。准备阶段把训练集和验证集分开验证集不做随机裁剪只做缩放和归一化from torchvision import datasets, transforms train_tf transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.05, 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]), ]) train_set datasets.ImageFolder(data/train, train_tf) val_set datasets.ImageFolder(data/val, val_tf) from torch.utils.data import DataLoader train_loader DataLoader(train_set, batch_size64, shuffleTrue, num_workers8, pin_memoryTrue) val_loader DataLoader(val_set, batch_size64, shuffleFalse, num_workers8, pin_memoryTrue)逻辑说明train_tf里的RandomResizedCrop从原图随机裁剪并缩放到224scale设为0.05到1.0意味着裁剪面积可以很小这是从DeiT训练策略继承的强增强对Transformer基座很关键val_tf先Resize到256再CenterCrop到224是图像分类评估的通用做法。ImageFolder自动把子目录名映射为类别ID省去手写标签表。参数说明num_workers设为8的前提是CPU核数够如果训练时数据加载成为瓶颈再配合pin_memory一起调。batch_size64在CrossFormer-T上大约占用7到8G显存显存不够时优先减batch而不是改模型。3.4 最小训练循环单卡跑通一个Epoch把前三块拼起来最小训练循环几十行就够了。第一次跑通时先不接任何花哨trick保证数据链路、模型、优化器三个环节转起来import torch import torch.nn as nn model create_crossformer(cfg).cuda() optimizer torch.optim.AdamW(model.parameters(), lr5e-4, weight_decay0.05) criterion nn.CrossEntropyLoss(label_smoothing0.1) for epoch in range(1): model.train() for step, (images, labels) in enumerate(train_loader): images, labels images.cuda(), labels.cuda() logits model(images) loss criterion(logits, labels) optimizer.zero_grad() loss.backward() optimizer.step() if step % 50 0: print(fepoch {epoch} step {step} loss {loss.item():.4f})逻辑说明AdamW替代传统SGD是Transformer训练的共识weight_decay作用在参数而非梯度更新上能有效抑制过拟合label_smoothing把One-Hot标签向均匀分布拉一点CrossFormer在中小数据集上配合它验证集准确率通常能涨0.5到1个点。训练循环里只有前向、Loss、反向、更新四步没有做梯度裁剪和warmup先验证链路通畅。参数说明lr5e-4是batch_size64下的经验值batch翻倍时学习率也要翻倍。这版代码没有加验证逻辑跑通一个Epoch后马上接第4章的调度器和EMA。训练启动命令python train_crossformer.py --data ./data --epochs 100 --batch-size 64启动后先观察前100个step的loss是否从4.x往下走。如果loss不降且NaN频出优先检查数据是否归一化干净再看学习率是否过大。4. 调优关键参数让CrossFormer在分类任务上收敛得更稳4.1 学习率与warmupTransformer基座的共同脾气CrossFormer和ViT、Swin一样对学习率比对CNN敏感得多。CNN训练里lr从0.1掉到0.01可能只是慢一点Transformer模型lr设为1e-3以上基本就是震荡甚至NaN。我在这类模型上拿得准的起点是lr5e-4配线性warmup和cosine衰减而不是step decay。warmup的作用是让token之间的注意力分布先稳定下来直接上大学习率会把Attention矩阵在头几个batch里打散。timm自带的CosineLRScheduler直接可用省去自己写from timm.scheduler import CosineLRScheduler scheduler CosineLRScheduler( optimizer, t_initial100, # 总训练epoch数 warmup_t5, # 前5个epoch线性预热 warmup_lr_init1e-6, # 预热起点 lr_min1e-6, # 最低学习率 ) for epoch in range(100): train_one_epoch(model, train_loader, optimizer) scheduler.step(epoch)逻辑说明CosineLRScheduler把学习率从5e-4按余弦曲线降到1e-6比固定步长衰减平滑适合Transformer这类loss曲面陡峭的模型warmup_t5在100个epoch里占5%是经验值epoch总数翻倍时warmup也跟着翻倍。参数说明如果只有1张卡且数据量小于1万把warmup_t提到10防止模型前期在小数据上过拟合lr_min设成0容易让后期loss震荡保留1e-6的量级更稳。4.2 batch size、梯度累积与EMA小显存下的保命配置CrossFormer的LSDA长距离组在特征图大时显存偏高8G卡上batch_size64不一定扛得住。不建议直接换小模型训练动态就变了。我的做法是先跑一版batch32或16再用梯度累积把等效batch拉回64accum_steps 4 # 实际batch16时等效batch64 for step, (images, labels) in enumerate(train_loader): loss criterion(model(images.cuda()), labels.cuda()) loss loss / accum_steps # 先除再反传保证梯度量级不变 loss.backward() if (step 1) % accum_steps 0: optimizer.step() optimizer.zero_grad()逻辑说明梯度累积是把多个step的梯度累加后更新一次参数等效增大batch。除以accum_steps是因为CrossEntropyLoss默认对batch求平均直接把4个小batch的loss相加梯度会变成等效batch的4倍学习率就过大了。参数说明accum_steps越大更新频率越低训练时间拉长但显存几乎不涨配合EMA指数移动平均可以进一步平滑参数更新EMA衰减系数0.9999时验证集往往比最终checkpoint高0.3到0.5个点。timm里有现成的ModelEma可以直接接入。4.3 数据增强与正则化Mixup、CutMix和DropPath的搭配CrossFormer吃数据增强但不像CNN那样依赖RandomResizedCrop打天下。我在图像分类任务里固定用三件套RandAugment做颜色和几何扰动、Mixup做样本插值、CutMix做局部裁剪混合。timm把它们封装得很好from timm.data import Mixup mixup_fn Mixup( mixup_alpha0.8, # mixup强度 cutmix_alpha1.0, # cutmix强度 label_smoothing0.1, # 必须与loss里的设置一致 ) samples, targets next(iter(train_loader)) samples, targets mixup_fn(samples, targets) # targets是软标签后续用CE即可不要再加softmax逻辑说明Mixup把两张图按比例混合标签也按同样比例混合强制模型学线性插值CutMix把一张图的某块区域用另一张图覆盖标签按面积比例混合。两者一起能给CrossFormer在中小数据集上带来明显的泛化提升。参数说明mixup_alpha0.8和cutmix_alpha1.0是ImageNet训练的标准值如果是1000到5000张的小数据集把两个alpha都降到0.2太强会让模型学不到细节DropPath通常取0.1到0.2已经在配置参数里预留了drop_path_rate不需要额外开开关。4.4 模型尺寸选择T/S配置与计算量取舍CrossFormer的规格里T和S是两个最常用的档位。T的embed_dims是64/128/320/448depths是2/2/8/14适合单卡快速验证S把depths加深到2/2/12/20参数量和计算量增加约30%精度通常能换1到1.5个点。再往上走是加宽embed_dims并同步加深depths显存占用上升明显一般需要多卡或大显存。选择时我遵循两条经验。一是先用T在数据子集上跑通全流程确认任务信号能被模型捕获二是精度不够时先加数据增强和EMA最后才换S。很多人在小数据集上直接上S结果过拟合反而比T差。另一个容易忽略的点是group_size和crs_interval共同决定token密度T配置的crs_interval[4,2,1,1]在224输入下token分布已经比较均衡不要为了追求大感受野把第一层interval改成8浅层token过疏会把细粒度纹理直接弄丢。5. CrossFormer实战避坑5个会中断训练或悄悄掉点的典型案例5.1 输入尺寸不是32的倍数窗口分区直接报错现象训练能启动第一个Epoch还没跑完就报错错误信息类似shape mismatch in window partition位置在LSDA的reshape操作附近。原因CrossFormer的CEL用unfold按crs_interval采样tokenLSDA又按group_size切分窗口两者都要求特征图尺寸能被interval和group_size整除。输入224时4个stage的尺寸分别是56、28、14、7group_size7正好整除如果数据集里混入了尺寸不规范的图或者resize时用了非对齐尺寸某个stage就会出现余数。解决在数据流水线里强制对齐训练和推理用同一套尺寸逻辑。我在dataset里加断言发现问题图直接打印路径for p in train_set.samples: img Image.open(p[0]) if img.width % 32 ! 0 or img.height % 32 ! 0: print(bad size:, p[0], img.size)坏图直接跳过Resize到256再CenterCrop后一定能被32整除因为256和224都是32的倍数。另一个隐蔽坑是验证集不做随机裁剪时原图被Resize到非对齐尺寸同样会炸务必统一。5.2 长距离注意力组显存溢出注意窗口大小的平方增长现象用CrossFormer-S训练batch64直接OOM错误卡在attention的bmm操作上用nvidia-smi看显存在第二个Epoch突然拉满。原因LSDA长距离组窗口较大注意力矩阵大小是窗口内token数的平方。group_size[14,14,14,14]的配置在stage4特征图上每个窗口的token数会很大注意力矩阵直接爆掉。group_size表面只是分组大小实际直接决定显存水位。解决第一选择是减batch到32并开梯度累积等效batch不变第二选择是换group_size[7,7,7,7]的T配置对224输入已经足够第三选择是开AMP混合精度但注意先看5.4节的坑。用torch.cuda.amp.autocast()把attention计算放到FP16显存基本能省一半。5.3 加载预训练权重时num_classes对不上分类头被截断现象下载CrossFormer在ImageNet上的预训练权重后model.load_state_dict(state_dict)报错缺了head.weight和head.bias两个key。原因预训练模型的分类头是1000维输出自定义分类任务的num_classes不是1000最后一层线性层shape不同PyTorch按key严格匹配时直接报错。很多人卡在这里其实是把加载权重和微调两件事混在了一起。解决加载时分两步走先屏蔽分类头再加载然后让分类头随机初始化state torch.load(crossformer_t_224.pth, map_locationcpu) filtered {k: v for k, v in state.items() if not k.startswith(head.)} model.load_state_dict(filtered, strictFalse)参数说明filtered用startswith跳过head相关参数strictFalse允许缺失key。加载后head保持随机初始化先冻结backbone、只训练head和最后1个stage跑20个epoch等loss降下来再解冻全部参数学习率从1e-4换回5e-4。这个小流程能让迁移学习的收敛稳定很多。5.4 AMP混合精度下loss震荡LayerNorm在FP16下的数值问题现象开启AMP后前几轮正常某一步loss突然变NaN重跑又不一定复现看起来跟玄学一样黑匣子排错法在这一步经常失灵。原因LayerNorm在FP16下的均值方差计算精度不够小数值被截断后梯度爆炸。CrossFormer每个block里都有LN它是整个模型里对数值精度最敏感的位置。梯度scaler虽然能防梯度下溢但无法修复LN内部的上溢。解决两条路线。路线一显存允许时用bfloat16代替float16BF16的指数范围和FP32一致LN数值问题基本消失代价是只有A100、4090等新卡才支持路线二保留FP16但给LN单独走float32在模型forward里把LN的输入显式转换class SafeLayerNorm(nn.Module): def __init__(self, dim): super().__init__() self.ln nn.LayerNorm(dim) def forward(self, x): return self.ln(x.float()).type_as(x)参数说明type_as把结果转回原精度保证后续算子不用处理混合类型。我一般优先试bfloat16改动最小且效果干净老卡只能做SafeLayerNorm或者干脆关AMP毕竟NaN一出现整个训练就前功尽弃。5.5 森林图像分类里的小类别过拟合长尾分布的三个动作现象训练集准确率跑到99%验证集整体还行但某个样本很少的类别比如某种病树的早期病斑准确率只有20%左右模型把它全部预测成大类。森林图像分类数据集的典型特征是标签长尾这种翻车在单类样本不足50张时几乎必然发生。原因CrossEntropyLoss对小类别的梯度贡献被大类淹没Transformer又擅长记忆训练集小类被直接过拟合掉。这不是CrossFormer独有而是所有深度学习模型的共性但Transformer在数据少时更严重。解决三个动作。第一用采样器把小类权重拉起来from torch.utils.data import WeightedRandomSampler counts torch.bincount(torch.tensor(train_set.targets)) weights 1.0 / counts[torch.tensor(train_set.targets)].float() sampler WeightedRandomSampler(weights, num_sampleslen(train_set), replacementTrue)逻辑说明weights按1除以类别样本数构造样本越少的类别被抽到的概率越高每个epoch里小类都能被充分看到replacementTrue允许重复采样否则无法覆盖到每个样本的权重。参数说明对小类加额外增强可以但别把RandomResizedCrop的scale下限调太低病斑纹理本身细小过度裁剪会让模型学不到有效特征。第二epoch从100减到50配合EMA降低过拟合窗口第三损失函数换成Focal Loss或给CE加class weightFocal Loss对大类的置信度惩罚更大能缓解小类被淹没。这三步做完小类准确率通常能从20%拉到50%以上继续提就得靠补数据了。6. 进阶用CrossFormer做双流图像分类的迁移习惯如果你是奔着森林图像分类或遥感场景分类来的CrossFormer最强的用法之一是把双流结构和迁移学习结合。双流基座里两个分支可以用同一个CrossFormer骨干分别处理不同输入——典型的组合是RGB流加NDVI红外流两路在最后一个stage后做特征拼接再分类。CrossFormer在这里的好处是两个流可以设置不同的crs_intervalRGB流用[4,2,1,1]保留细粒度纹理红外流用[8,4,2,1]拉大感受野捕获植被覆盖的大尺度分布两路特征互补性比用同配置高不少。迁移学习的标准习惯是从粗到细先把预训练权重按5.3节方式加载冻结前两个stage只训练head和最后两个stage学习率降到1e-4等验证集loss不再下降再解冻全部stage学习率回到5e-4并关掉warmup此时注意力已经稳定不需要重新预热。每次换数据集我会先拿整个流程在128x128低分辨率下跑10个epoch确认数据链路、类别分布、loss下降趋势都没问题再正式切到224训练。这个习惯帮我省掉了无数次在224分辨率上跑到一半才发现数据集标签错位的返工。另一个值得养成的小习惯是每5个epoch存一次checkpoint并记录验证集准确率同时把EMA影子权重也存一份。CrossFormer在训练后期会出现验证集和训练集同步上涨的平稳期EMA权重往往落在更平滑的位置最终提交前把EMA权重和在线权重各跑一遍验证集选更高的那个。训练日志统一写tensorboard按项目分目录半年后回看旧实验还能对比出当时的调参轨迹。如果你是从ViT或Swin迁移过来的不要直接沿用旧训练脚本先把第2章的核心参数过一遍再动手CrossFormer在token生成和注意力上的两个变化会联动影响所有超参。跑过几个任务后你大概率会同意它不是神秘的新玩具而是一个能在图像分类任务里稳定复现收益的骨干模型。希望我的这些踩坑和习惯能帮到你少走几趟弯路。本文还有配套的精品资源点击获取