直接说结论:FocalNet 这个骨干网络,放在目标检测和实例分割里用,配置环境、调数据集、跑训练、做改进,一条链路下来踩坑是免不了的,但收益也确实可观。我最近帮别人调了一套基于 FocalNet 的检测分割方案,从环境到落地折腾了整整一周,今天把整个过程重新梳理一遍,把那些文档里不会写、但实际一定会踩的细节全部放出来。这篇内容适合手里有检测或分割任务、想换更强骨干的人参考,也适合正在纠结 FocalNet 和 Swin 选哪个的人。
FocalNet 全称 Focal Modulation Networks,是 2022 年提出的视觉骨干网络。它最核心的卖点是用 focal modulation 机制替代了 self-attention,在保持全局建模能力的同时,大幅降低了计算复杂度。配合 Mask R-CNN 或者 RetinaNet 这类检测框架,FocalNet 的精度表现非常能打,尤其在小模型段位,比同规模的 Swin Transformer 更稳。下面我按照实际项目推进的顺序,把这四个环节讲透:环境配置、数据集调整、训练代跑、改进创新。
1. 内容整体设计与思路拆解
1.1 FocalNet 的核心机制到底是怎么回事
要理解 FocalNet 在检测分割任务里为什么好用,先得知道它做了什么。传统 Transformer 骨干靠 self-attention 捕捉全局关系,但 self-attention 的复杂度是 O(n²),图像分辨率一上来就直接爆炸。Swin Transformer 用窗口注意力缓解了这个问题,但窗口之间的信息交流又成了新的瓶颈。FocalNet 的思路是:用 focal modulation 分三个粒度聚合特征——局部细粒度、周围上下文、全局粗粒度——然后把它们融合在一起。这个过程复杂度只有线性级别,但能覆盖的感受野和 self-attention 相当。
这个机制有一个很直观的好处:它在不引入位置编码的情况下也能感知空间位置关系,因为它本身就在不同大小的窗口内做聚合。对于目标检测这种需要精准定位的任务来说,特征的局部细节和全局上下文缺一不可,FocalNet 正好两者兼顾。而且它的实现非常干净,拆掉注意力机制之后,整个网络的计算模式变得极其规整,工程上优化空间很大。
我实际用下来的感受是:FocalNet 不只是精度高,更重要的是它的计算效率太稳定了。Swin 在高分辨率下窗口计算会因为 padding 和 mask 产生各种开销波动,FocalNet 没有这些问题,训练和推理的速度都更好预估。
1.2 检测分割任务里选型 FocalNet 的几条理由
选骨干网络不能只看指标榜单,还得结合自己的任务场景。我总结 FocalNet 特别适合下面这几种情况:
- 你手里的数据集不是海量规模,FocalNet 比 Swin 更容易收敛,小数据集上差距尤其明显
- 你要做实例分割,Mask R-CNN 这类两阶段框架对骨干的特征质量非常敏感,FocalNet 的多粒度聚合对掩膜预测帮助很大
- 你需要在性能和速度之间找平衡点,FocalNet-T/S 这种小模型,既能跑得快,精度又不掉链子
- 你想做改进创新,FocalNet 的模块设计非常规整,focal modulation 完全可以当作一个模块嵌入到其他网络里,而不是只能整体替换骨干
当然,FocalNet 也不是万能药。如果你的任务本身就是密集小目标、对局部纹理极其敏感,那纯卷积骨干可能依然更合适。FocalNet 的全局粗粒度聚合在某些极端小目标场景下会产生特征稀释,这个后面讲改进的时候会提到对策。
1.3 FocalNet 关键配置参数解析
FocalNet 的实现里有四个参数几乎是每次都要碰的,官方配置里默认值如下:
- focal_levels:控制 focal modulation 的层级数量,默认 3,含义是三种粒度窗口
- focal_windows:每一层对应的窗口大小,默认 [1, 3, 5],从小到大对应局部到全局
- use_postln:是否在模块末尾加 LayerNorm,默认 false
- use_iFF:是否使用 invertible Fully-connected Fusion,这个在 tiny 和 small 模型上默认 false,但 base 往上的模型开启后性能有提升
这四个参数直接决定了骨干的容量和感受野覆盖方式。我用一个类比来解释 focal_levels 的作用:它就像你用不同焦距的镜头同时看一个场景,特写、中景、全景各来一张,然后合成一张信息量最大的图。focal_windows 则控制每个镜头的焦距范围。
实际调参的时候,我不建议一上来就动这四个参数。先跑通整体流程,确认基线没问题,然后再根据任务特点调整。比如你的目标是中大型物体,可以适当把 focal_windows 整体调大;如果是小目标数据集,把 focal_levels 增加一层,让网络有机会聚合更细粒度的局部信息。
2. 环境配置全流程与避坑指南
2.1 硬件和基础软件选型
FocalNet 的显存占用比同规模 Swin 要低一些,但依然是 Transformer 家族的骨干,对显存有要求。我实测下来:
- FocalNet-T 作为 Mask R-CNN 骨干,batch size 2 训练,大约占用 8-9GB 显存,一张 2080Ti 或者 3060 都能跑
- FocalNet-S 做检测任务,batch size 2,大约 11-13GB,建议 3080 以上的卡
- FocalNet-B 配合 Cascade Mask R-CNN,建议至少 24GB 显存,否则要梯度累积
操作系统方面,Ubuntu 20.04 及以上是最好的选择,Windows 也能跑,但分布式训练和数据处理这块会闹心一些。CUDA 版本推荐 11.3 到 12.1 之间,太老或太新都会有一堆兼容性问题。
2.2 Conda 环境搭建细节
我的习惯是每次项目都用 conda 新建独立环境,不混装。创建命令:
conda create -n focalnet python=3.8 -y conda activate focalnetPython 版本选 3.8 不是因为 3.10 不行,而是很多旧版检测库对 3.10+ 的支持有暗坑。接下来安装 PyTorch,这一步很关键,一定要根据自己的 CUDA 版本选择对应的轮子:
pip install torch==1.13.1 torchvision==0.14.1 --index-url https://download.pytorch.org/whl/cu117注意这里我用的是 cu117,表示 CUDA 11.7 对应的 PyTorch 版本。如果你的 CUDA 是 12.x 但从 conda 环境里 nvcc -V 查不到,别慌,PyTorch 是自带 CUDA runtime 的,只要显卡驱动够新,torch 能正常调用 GPU 就行。判断方法是进入 Python 后跑:
import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))实测能输出 True 和显卡名称就说明没问题。如果 False,大概率是 PyTorch 版本装成了 CPU 版,卸载重来。
2.3 检测框架与编译安装
FocalNet 本身是一个骨干网络实现,要跑检测和分割,需要配合 MMDetection 或者 Detectron2。我这边更推荐 MMDetection,因为 FocalNet 官方就是基于 MMDetection 做的实验,配置生态完整,遇到问题更容易搜到解决方案。
安装顺序是:先装 mmcv,再装 mmdetection。mmcv 是必须编译的,直接 pip install 经常踩版本坑。用 OpenMIM 工具最省心:
pip install openmim mim install mmcv-full==1.7.2注意 mmcv-full 和 mmdetection 的版本匹配关系,mmdet 2.25.1 配 mmcv-full 1.7.2,这是我验证过比较稳的组合。mmdetection 本体直接 git clone 然后 pip install 就行:
git clone https://github.com/open-mmlab/mmdetection.git cd mmdetection git checkout v2.25.1 pip install -r requirements/build.txt pip install -v -e .这里用的是 editable 模式安装,好处是改代码后不需要重新编译,对于后面做改进创新非常关键。我记得第一次装的时候漏了 pycocotools,跑数据加载直接报错,后来单独补上:
pip install pycocotools==2.0.62.4 下载预训练权重与目录组织
FocalNet 官方在 GitHub 上提供了 ImageNet 预训练权重,分为 FocalNet-T、S、B、L 等规格,下载后建议统一放在一个目录里,比如:
├── checkpoints │ ├── focalnet_tiny_imagenet.pth │ ├── focalnet_small_imagenet.pth │ └── focalnet_base_imagenet.pth ├── configs ├── data └── work_dirs权重文件注意要检查哈希值,我碰上过一次下载损坏的权重,加载时模型初始化完全错乱,又想不起来哪儿有问题,排查了很久。建议用 md5sum 校验一下,官方页面会给出哈希值。
预训练权重放好后,加载到 MMDetection 有几种方式,最简单的是在配置文件的 load_from 字段直接指定路径。这里有个细节:FocalNet 官方权重是纯 backbone 权重,MMDetection 加载时会自动匹配 backbone 部分的 key,如果你的模型结构里 backbone 命名空间和配置不一致,会报 unexpected key 的警告。遇到这种情况,需要把官方权重重新保存一份,只保留 backbone 前缀的 key:
import torch ckpt = torch.load('focalnet_tiny_imagenet.pth') state_dict = ckpt if 'state_dict' not in ckpt else ckpt['state_dict'] new_state_dict = {} for k, v in state_dict.items(): new_key = 'backbone.' + k new_state_dict[new_key] = v torch.save(new_state_dict, 'focalnet_tiny_backbone.pth')2.5 环境配置阶段的高频报错与处理
我把实操中遇到的报错按概率排序,做一个速查表:
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
| No module named 'mmcv' | mmcv 没装上或者装在别的环境 | 确认环境激活,用 mim install 重装 |
| AssertionError: MMCV==... is incompatible with MMDetection==... | 版本错配 | 按上述版本组合重装 |
| KeyError: 'backbone' 或 load_state_dict 报错 | 权重 key 不匹配 | 用脚本重写权重 key 前缀 |
| CUDA out of memory | 显存不足 | 降低 batch size 或开启梯度累积 |
| AttributeError: 'NoneType' object has no attribute 'shape' | 数据路径错误导致图片没加载 | 检查 data_root 和 ann_file 路径 |
| torch.cuda.OutOfMemoryError 但复现时又好了 | 缓存碎片 | 设置 PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128 |
有一个很隐蔽的坑:如果系统的 GCC 版本太老,mmcv 编译过程会直接中断或者编译出来性能异常。Ubuntu 20.04 自带的 GCC 9 是没问题的,但 CentOS 7 自带 GCC 4.8 强烈建议先升级 GCC。编译 mmcv 报错关键看最后一行,出现 unrecognized command line option 这类基本都是编译器问题。
3. 数据集调整与配置实操
3.1 数据格式统一到 COCO 标准
FocalNet 基于 MMDetection 训练,无论是目标检测还是实例分割,数据集最佳选择是 COCO 格式。不管你的原始数据是 VOC、YOLO 的 txt 还是自己标注的 JSON,先统一转成 COCO 标注格式。
COCO 格式的核心结构有三个字段:
- images:存放每张图片的信息,包括 id、file_name、width、height
- annotations:存放每个目标的标注信息,包括 image_id、category_id、bbox、area、segmentation、iscrowd
- categories:存放类别列表,从 id=1 开始(注意 COCO 的 categories id 从 1 开始,不是 0)
对于实例分割任务,segmentation 字段可以是多边形 polygon 格式(逐点坐标列表),也可以是 RLE 编码。如果是从 LabelMe 这类工具导出的数据,转 COCO 时建议保留 polygon 格式,避免转换精度损失。从 LabelMe 转向 COCO 的 JSON 时,polygon 点坐标是按 (x, y) 顺序排列的 float 数组,而 COCO 官方要求的是扁平化的 float 列表,这点很多人会忘记做维度展平。
检测任务只需 bbox 字段,格式是 [x, y, width, height],注意是左上角坐标加宽高,而不是中心点坐标。我在转换脚本里遇到最多的问题就是 bbox 坐标被算成中心点格式,训练时 loss 直接崩掉或者收敛极差。
3.2 修改数据加载配置
假设你已经把数据整理成 COCO 格式,目录结构如下:
data/ ├── train/ │ ├── images/ │ └── annotations/ │ └── instances_train.json ├── val/ │ ├── images/ │ └── annotations/ │ └── instances_val.json配置文件的 data 部分需要做两件事:第一,把 ann_file 和 img_prefix 指到正确的路径;第二,如果自定义数据集类别不是 80 类,必须覆盖 dataset 里的 metainfo 或旧版配置里的 CLASSES。
MMDetection 2.x 的写法是在配置里直接定义 CLASSES 变量:
CLASSES = ('person', 'cat', 'dog', 'car')这个 CLASSES 会被传给 CocoDataset 作为类别列表,顺序必须和标注 JSON 里的 categories 完全一致。我发现一个高频错误:改了 category_id 但忘了改 CLASSES,导致训练时目标类别错位,模型收敛后测试结果张冠李戴。
3.3 模型头部类别修改
FocalNet 作为骨干,本身不负责输出类别,具体的分类和回归预测头在检测框架里。使用 Mask R-CNN 时,主要改这两个地方:
- bbox_head 的 num_classes
- mask_head 的 num_classes
如果是 Cascade Mask R-CNN,有三组 bbox_head,每一组的 num_classes 都要改。这个细节不留意就会报维度不匹配的错:分类头输出的类别数和标注数据的标签数对不上,loss 直接出现 index out of range。
关于类别数有一个容易忽略的点:两个以上的类别时,num_classes 就是真实的类别数量;但如果是单类检测,也就是只有一个目标类别,部分老代码里需要设置 num_classes=1 然后配合特定 loss 处理。我在实际项目里踩过这个坑,最稳妥的做法是直接加一个背景类作为第 0 类,即 num_classes 设为 2,这样就不需要改 loss 逻辑。如果追求严格的单类检测精度,再考虑二分类 loss 方案。
3.4 图像尺寸与数据增强策略调整
FocalNet 对输入分辨率比较敏感,因为它的 focal windows 是固定大小的,如果输入尺寸太小,全局粗粒度聚合能看到的范围也小,优势体现不出来。以 FocalNet-T + Mask R-CNN 为例,默认输入是 1333x800(长边 1333,短边 800),这个分辨率对一张 1080Ti 来说压力很大。
实际跑的时候可以这样调整:
img_norm_cfg = dict( mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) train_pipeline = [ dict(type='LoadImageFromFile'), dict(type='LoadAnnotations', with_bbox=True, with_mask=True), dict(type='Resize', img_scale=(1333, 800), keep_ratio=True), dict(type='RandomFlip', flip_ratio=0.5), dict(type='Normalize', **img_norm_cfg), dict(type='Pad', size_divisor=32), dict(type='DefaultFormatBundle'), dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels', 'gt_masks']) ]img_scale 这里我建议保留 (1333, 800) 作为基准,如果你的 GPU 显存吃紧,可以改到 (1000, 600)。还有一点,FocalNet 内部的 padding 逻辑要求输入尺寸是 32 的倍数,配置里已经有 size_divisor=32,这个值不要乱改,否则最终输出的 feature map 尺寸不一致,FPN 融合时会报 tensor size mismatch。
数据增强方面,很多人只开 RandomFlip,但对于小数据集,我强烈建议开启多尺度训练:
dict(type='Resize', img_scale=[(800, 480), (1000, 600), (1333, 800)], multiscale_mode='range', keep_ratio=True)多尺度训练对 FocalNet 来说几乎是免费的精度提升,因为它本身多粒度聚合的结构和尺度变化天然契合。实测同样的训练配置,多尺度比单一尺度提升 1-2 个点 AP。
3.5 小数据集下的数据集调整策略
如果你手里的数据只有几百张甚至更少,直接全量训练 FocalNet 大概率会过拟合。我在这种情况下有几条经验:
第一,冻结 backbone 的前几个 stage。FocalNet 有四个 stage,前两个 stage 学到的都是通用特征,小数据集里微调容易破坏这些特征。配置里可以通过 frozen_stages 参数控制,我一般设 frozen_stages=2。
第二,增强数据多样性。除了几何增强(翻转、旋转),建议加上颜色抖动、随机擦除,对检测和分割的鲁棒性有帮助。FocalNet 对灰度变化不敏感,但颜色抖动有助于避免过拟合。
第三,把类别不均衡的问题放在数据集层面解决。如果你的数据集中某类只有几十个样本,可以考虑在 Dataset 层面做类别重采样,而不是只依赖 loss 里的权重。MMDetection 里可以通过 dataset 配置里的 class_balancing 相关参数做处理,或者更直接地自己写一个重采样 Dataset。
4. 代跑训练的全流程与实战调优
4.1 从配置文件到启动训练
环境配置好、数据路径改好之后,接下来就是跑训练。MMDetection 的训练入口是 tools/train.py,单卡启动非常简单:
python tools/train.py configs/focalnet/mask_rcnn_focalnet_tiny_1x_coco.py多卡训练用分布式脚本:
bash tools/dist_train.sh configs/focalnet/mask_rcnn_focalnet_tiny_1x_coco.py 8这里的 8 是 GPU 数量。我在最开始跑训练时犯过一个低级错误:明明只有两张卡,却写成了 8,任务直接卡死。先确认显卡数量再填。
FocalNet 官方配置里已经写好了 schedule,一般 1x 表示 12 epoch,2x 表示 24 epoch。如果你的数据量比 COCO 小很多,12 epoch 可能不够模型收敛。我建议小数据集直接上 2x 甚至 3x,配合中途的学习率衰减阶段观察 loss 曲线是否已经平台期。
4.2 学习率与优化器调整实录
MMDetection 默认用的是 SGD,FocalNet 官方实验里也是 SGD 为主。配置里的学习率基准是 0.02,这个值是针对 8 卡 batch size 16 的情况。如果你只有单卡、batch size 只有 2,线性缩放规则告诉我们实际学习率应该调整为:
lr_new = 0.02 * batch_size_new / 16单卡 batch size 2 的话,理论上 lr = 0.0025。但我实测直接按这个规则有时候收敛太慢,更实用的策略是先用 lr=0.005 跑 2-3 个 epoch 试试,观察 loss 下降速度,再决定是否上调或下调。
optimizer 配置里还有 weight_decay,默认 0.0001 即可,一般不用动。如果你发现训练后期 loss 震荡厉害,可以先把 weight_decay 微调到 0.00005 看看。
对于 FocalNet 这种带复杂模块的骨干,我还建议开启 warmup,MMDetection 默认已经有 warmup 配置,但默认的 warmup_iters 是 500。如果你用的是小数据集,建议把 warmup_iters 减到 200,否则刚开始训练的大部分时间都浪费在预热阶段。
4.3 训练过程监控与日志解析
训练过程中要重点看的是 loss 的几个分量:
- loss_cls:分类损失,如果这一项不下降,几乎可以断定类别配置错了
- loss_bbox:回归损失,过高说明 bbox 格式或 anchor 配置有问题
- loss_mask:分割损失,实例分割任务专用
- loss_rpn:RPN 网络的损失,如果 RPN 不收敛,整个两阶段模型也会跟着崩
观察这些值有个技巧:不要只看最后一个 step 的 loss,要看平滑后的趋势。MMDetection 训练时会周期性打印均值,关注每 50 或 100 次迭代打印出来的曲线。如果 loss 出现突然的尖峰然后恢复到正常,不用管;如果持续居高不下甚至上升,优先检查数据加载是否正常,比如是否加载了大量空白标注图。
训练日志默认保存到 work_dirs 目录下,如果日志里的 Iter 数长时间不增长,大概率是 dataloader 卡住了。这时候检查数据目录是否在机械硬盘上、是否被其他程序占用,以及 num_workers 是否被设置得过大导致内存爆掉。num_workers 我建议 4 到 8 之间,太大反而会因为进程竞争导致加载变慢。
4.4 断点续训与训练中途调整
训练不是总能一次跑完,断电、显存报错、手动中断都是常事。好在 MMDetection 自带断点续训功能,启动时加上 --resume-from:
python tools/train.py configs/exp.py --resume-from work_dirs/exp/latest.pth中断恢复时要注意一个问题:如果你的配置文件改了但数据没变,直接 resume 会导致学习率、epoch 等设置错乱。我的习惯是中断恢复只用于不改配置的情况。如果确实改动了配置,建议保留优化器状态手动调整。
训练中途想调整学习率,除了停掉重新跑,也可以直接修改配置后 resume。但这样会导致 warmup 重复执行,反而破坏当前的学习状态,因此不要这么干。想调学习率,正确做法是等当前阶段结束,或者说在平台期手动中断后,修改学习率并配合 --auto-scale-lr 重新开始。
4.5 显存优化与混合精度训练
小显存跑 FocalNet 是很多人的痛点,我有一套优先级明确的优化手段:
第一步,降低 batch size。这是最简单有效的方案,但也可能影响 BN 统计量。FocalNet 用的是 LayerNorm 而不是 BN,所以 batch size 减小对归一化层的影响远小于 CNN 骨干,这点 FocalNet 确实很友好。
第二步,开启梯度累积。配置里:
optimizer = dict(type='SGD', lr=0.005, momentum=0.9, weight_decay=0.0001) optimizer_config = dict(grad_clip=None)梯度累积需要自己在训练 loop 里改,或者直接复用 mmdetection 的 runner 扩展。我提供一个简单做法:把 batch size 设为 1,有效 batch size 设为 8,那么每 8 个 step 更新一次梯度。学习率依然按有效 batch size 来算。
第三步,混合精度训练。MMDetection 2.x 里设置:
fp16 = dict(loss_scale=512.)开启 fp16 后显存占用能减少约一半,但有时会出现梯度溢出导致 loss 变成 NaN。如果出现这种情况,把 loss_scale 调大,或者检查数据里是否含 NaN 值标注。FocalNet 的 focal modulation 对 fp16 的敏感度比纯卷积略高,所以开启后务必观察前几百个 iteration 的 loss 是否正常下降。
4.6 训练后评估与可视化
训练结束后第一个跑的命令是:
python tools/test.py configs/exp.py work_dirs/exp/latest.pth --eval bbox segm--eval bbox 是目标检测评估,--eval segm 是实例分割评估,两者可以同时用。输出结果中核心看 mAP 和 AP50、AP75。如果 mAP 很低但 AP50 还行,说明定位精度不足,优先检查回归损失和 NMS 设置;如果 AP50 都很低,大概率是训练配置有问题,重新审视数据。
评估之后建议把检测结果可视化出来,MMDetection 提供了现成的工具:
python tools/test.py configs/exp.py work_dirs/exp/latest.pth --show-dir vis_results可视化结果一定要看,尤其是实例分割的掩膜质量。我遇到过一次训练 loss 很漂亮但掩膜边缘粗糙的情况,最后发现是数据标注里的 polygon 坐标精度不够,转成 mask 时丢失了细节。这种问题只有可视化才能发现。
5. FocalNet 改进创新方向与实战
5.1 基于可变形卷积的模块替换
FocalNet 原始的局部聚合用的是固定窗口的卷积或池化,对几何形变大的目标不够灵活。一个很实用的改进是把其中的局部聚合层替换成可变形卷积(DCN v2)。具体做法是:把 FocalNet stage 里负责局部细粒度聚合的 3x3 卷积替换为 3x3 的 DeformConv,并为每个 focal level 学习独立的 offset。
这个改动的收益在实例分割任务上很可观,尤其是对于形态不规则的物体(比如家具、动物、车辆),掩膜质量提升明显。代价是显存占用增加约 15%-20%,训练速度下降 10% 左右。我在实现时发现一个关键点:DCN 的 offset 网络需要足够的感受野来预测形变方向,所以在 stage 3 和 stage 4 替换效果远好于 stage 1 和 stage 2。别看论文里说所有 stage 都可以改,实战中最稳妥的是只改最后两个 stage。
5.2 结合注意力机制的多尺度特征融合
FocalNet 的输出是 C2-C5 四个层级的特征图,一般会送入 FPN 进行融合。原始 FPN 是简单的 Top-Down 路径,自顶向下逐层融合,对浅层细节和深层语义的生物融合效果粗糙。这里可以做一个改进:把 FPN 的每层融合权重从等权求和改成可学习的注意力权重。
以 Mask R-CNN 的 FPN 实现为例,改造 FPN 的 lateral connection:
class AttentionFPN(nn.Module): def __init__(self, in_channels, out_channels): super().__init__() self.lateral_convs = nn.ModuleList() self.attention_weights = nn.ParameterList() for i in range(len(in_channels)): self.lateral_convs.append(nn.Conv2d(in_channels[i], out_channels, 1)) self.attention_weights.append(nn.Parameter(torch.ones(1))) def forward(self, inputs): laterals = [conv(x) for conv, x in zip(self.lateral_convs, inputs)] # 自顶向下融合时乘以可学习权重 ...这种改动小、效果好,而且基本不改变原有计算量。FocalNet 本身输出的多层级特征已经具有较强表达能力,注意力 FPN 能选出每一层最值得传递的信息,对遮挡严重的场景帮助显著。我在一个行人检测任务上试过,AP 提升约 1.3 个点,而推理速度几乎没有下降。
5.3 轻量化方向:减少 focal level 与剪枝
如果你的应用场景对实时性要求高,FocalNet-T 可能还是嫌大。一个有效做法是降低 focal_levels 从 3 减到 2,同时 focal_windows 从 [1,3,5] 改为 [1,3]。这样参数量大约减少 20%,推理速度提升明显。代价是长距离建模能力下降,小目标检测精度会受影响。
另一种思路是结构化剪枝。FocalNet 的每个 Focal Modulation 模块由多个分支组成,不同分支的重要性差异很大。用 L1 范数评估每个分支的输出贡献,把贡献低的分支直接剪掉。这个方案我在某个边缘设备项目上验证过,剪掉 10% 参数后精度下降在 0.5 AP 以内,配合知识蒸馏甚至可以做到无损失压缩。
还要提一个点:FocalNet 不同 stage 的计算量分布不均匀,前排 stage 处理高分辨率特征,计算量占比很高。如果做实时优化,可以把 stage 3 的 focal_windows 从 [1,3,5] 调整为 [1,3],减少窗口数量,而 stage 4 保持不变。这个操作对整体精度的伤害非常小,因为高层的全局建模能力对检测至关重要,低层多窗口只是锦上添花。
5.4 和检测头的联合改进:动态 RoI 策略
FocalNet 的改进不仅限骨干本身,还能和检测头联动。在两阶段检测器里,RoI Align 之后的特征来自骨干的某个 layer,FocalNet 的多粒度特征天然适合做动态 RoI 特征选择。具体做法是:在 RoI Align 后,通过一个小型预测网络给不同 focal level 的特征图分配权重,再加权融合后送入 bbox head 和 mask head。
我画不出流程图,但可以描述清楚整体逻辑。假设 Mask R-CNN 从 FPN 的 P2-P5 层做 RoI Align,每个 RoI 会得到一组多尺度特征。原始做法是对这些特征逐层 RoI Align 后 sum 或 concat。改进后,用一个小型全连接网络对每个 RoI 预测一组权重,权重与特征本身相关,从而实现动态融合。实验证明,这个改进对大小目标混杂的场景非常有用,因为模型可以自己学会给不同尺寸的 RoI 分配不同层的特征权重。
5.5 训练技巧上的改进:更好的正则化与长训练策略
很多人在做改进创新时只改网络结构,忽略训练策略。FocalNet 对小数据集非常敏感,一个常见的改进是引入更强的正则化。我在训练 FocalNet 时尝试过在 stage 3 和 stage 4 的输出特征上加入随机深度(Drop Path),效果比在全网络加 Dropout 好很多。
随机深度的配置在 FocalNet 官方代码里有现成实现,只需要在训练配置里打开:
model = dict( backbone=dict( type='FocalNet', drop_path_rate=0.2, ... ) )drop_path_rate 从 0.1 到 0.3 之间调节。增大这个值相当于提高了隐式的模型集成效果,对泛化能力有正向帮助。如果训练精度挺好但验证精度上不去,优先调大 drop_path_rate。
长训练策略方面,直接从 1x schedule 改成 3x schedule + 余弦退火学习率,往往能带来 1-2 个点的免费提升。Cosine annealing 相比 step decay 让模型在后期更平滑地收敛到更优解。配置写起来很简单:
lr_config = dict( policy='CosineAnnealing', min_lr_ratio=0.01, warmup='linear', warmup_iters=500, warmup_ratio=1.0 / 3)5.6 改进案例复盘
我之前在某公司做过一个汽车零部件缺陷检测项目,用的就是 FocalNet-B + Cascade Mask R-CNN。原本的基线是以 ResNet-101 为骨干的 Cascade R-CNN,mAP 大概 56.2。替换成 FocalNet 后,什么都不改直接训练,mAP 提升到 58.7。后来我做了两个改进:一是把 FPN 改成注意力 FPN,二是把 stage 4 的局部聚合换成 DCN v2,最终 mAP 到了 61.3,推理速度还比 ResNet-101 快约 20%。
这个项目的训练数据只有 4000 张左右,属于明显的小数据集。之所以 FocalNet 能稳定超过 ResNet-101,我认为核心原因是 focal modulation 的全局建模能力让 RPN 的 proposal 质量大幅提升,错误候选框变少,整个两阶段流程的负担明显减轻。相比之下,ResNet-101 的感受野有限,很多离群目标框根本生成不出来。
当然也要提一下不成功的尝试。我试过把 FocalNet 的 focal_windows 全部等比例放大两倍,想获得更大的感受野,结果小目标 AP 反而掉了 1.4 个点。原因不难理解:窗口放大后,局部细粒度聚合的粒度变粗,对像素级细节的保留能力下降。所以调 FocalNet 参数的规律不是越大越好,而是要和数据集的目标尺度分布相匹配。
6. 实操心得与经验沉淀
跑完整个 FocalNet 项目,我最大的体会是:这类 Transformer 类骨干的工程细节,比模型结构本身更能决定项目成败。很多人照着论文改配置,结果精度怎么都上不去,多半是环境或数据环节出了问题,而不是模型不行。
有几个具体的经验想分享给大家:
第一,FocalNet 对输入 normalization 很严格,ImageNet 预训练的 mean 和 std 不要乱改。有些检测任务觉得图像亮度不均就调这些参数,实际上是帮倒忙。改 mean 和 std 意味着所有预训练权重都要在完全不同的数据分布下重新适应,收敛速度会大幅下降。
第二,数据增强的随机种子要固定。FocalNet 的 focal modulation 有随机性(如果开了 drop path 的话),如果数据加载的随机种子不固定,每次重启训练都会得到略有不同的曲线,排查问题时会变得非常困难。配置里加上seed=2024能让你自己的实验有可比性。
第三,分布式训练时,FocalNet 的权重初始化很敏感。如果多卡训练一开始就出现某些卡 loss 偏高,先把--launcher pytorch的参数确认好,以及init_dist是否正常。大部分分布式训练的诡异问题都源自 NCCL 通信异常,而不是模型本身。
最后建议你对配置做版本控制。FocalNet 的改进通常不是一次到位的,中间会有大量 trial and error。每跑通一个有价值的改动,就把配置单独存一份,并记录对应的指标结果。等积累了一定的实验记录后,你会发现后续改进和回归分析有了依据,不再像无头苍蝇一样试。
FocalNet 本身是一个非常有工程潜力的骨干网络,它的设计简洁、性能出色、改进空间也大。希望这篇内容能帮你把环境、数据、训练、创新这几个环节都理顺,少走一些我走过的弯路。如果你正在某个环节卡住,可以顺着文章里的步骤再检查一遍——大部分坑,都是从一个看似不起眼的细节开始的。