044、CA 的 Reduction Ratio 超参实验:4/8/16/32 下参数量与精度曲线
044、CA 的 Reduction Ratio 超参实验:4/8/16/32 下参数量与精度曲线
一、一个让我熬夜到凌晨三点的 bug
去年秋天,我在给一个工业缺陷检测项目调优。模型是 YOLOv8s 改 CA(Coordinate Attention),跑了两轮消融实验,mAP 始终比 baseline 低 0.8 个点。我盯着 tensorboard 上的 loss 曲线,训练 loss 降得比 baseline 还快,但验证集就是拉胯。
排查了两天,最后发现是 CA 模块里的 reduction ratio 设成了 32。对于小模型来说,这个压缩比直接把空间信息给压没了——通道数从 128 降到 4,注意力图几乎变成均匀分布。改回 8 之后,mAP 直接反超 baseline 1.2 个点。
这个教训让我意识到:CA 的 reduction ratio 不是随便抄个论文里的值就能用的。它跟模型大小、任务复杂度、甚至输入分辨率都有关系。今天这篇笔记,我就把 ratio 从 4 到 32 的完整实验过程、代码实现、以及踩过的坑全部摊开来讲。
二、CA 模块的 Reduction Ratio 到底在干什么
先快速回顾一下 CA 的结构。CA 把空间注意力分解成两个方向:高度方向和宽度方向。每个方向先做全局平均池化,得到一维特征向量,然后过一个 1x1 卷积降维,再过一个 1x1 卷积升维,最后用 sigmoid 生成注意力权重。
这里的降维卷积就是 reduction ratio 发挥作用的地方。假设输入通道数是 C,降维后的通道数就是 C / ratio。ratio 越大,降维越狠,参数量越少,但信息损失也越大。
论文里默认 ratio=16,但那是针对 ResNet 这种大骨干网络。YOLO 的 backbone 通道数本来就少(比如 YOLOv11n 的 C3 模块输出才 128 通道),ratio=16 意味着降维到 8 个通道——这 8 个通道要同时编码高度和宽度两个方向的信息,说实话有点强人所难。
三、代码实现:可配置 ratio 的 CA 模块
下面是我在 YOLOv11 里实际使用的 CA 模块实现。注意看注释,有些地方是踩过坑才改的。
importtorchimporttorch.nnasnnclassCoordAtt(nn.Module):def__init__(self,inp,oup,reduction=32):""" inp: 输入通道数 oup: 输出通道数(通常等于inp) reduction: 压缩比,论文默认16,但YOLO里建议调小 """super(CoordAtt,self).__init__()# 这里踩过坑:reduction不能设太大,否则中间通道数小于1# 比如inp=32, reduction=32 -> 中间通道=1,还能接受# 但inp=16, reduction=32 -> 中间通道=0.5,会报错self.reduction=max(reduction,1)mid_channels=max(inp//self.reduction,1)# 两个方向的池化self.pool_h=nn.AdaptiveAvgPool2d((None,1))self.pool_w=nn.AdaptiveAvgPool2d((1,None))# 共享的降维卷积self.conv1=nn.Conv2d(inp,mid_channels,kernel_size=1,stride=1,padding=0)self.bn1=nn.BatchNorm2d(mid_channels)self.act=nn.ReLU(inplace=True)# 两个方向的升维卷积self.conv_h=nn.Conv2d(mid_channels,oup,kernel_size=1,stride=1,padding=0)self.conv_w=nn.Conv2d(mid_channels,oup,kernel_size=1,stride=1,padding=0)defforward(self,x):identity=x n,c,h,w=x.size()# 别这样写:x_h = self.pool_h(x).squeeze(-1) # 直接squeeze会丢掉batch维度x_h=self.pool_h(x)# [n, c, h, 1]x_w=self.pool_w(x).permute(0,1,3,2)# [n, c, w, 1]# 拼接后降维y=torch.cat([x_h,x_w],dim=2)# [n, c, h+w, 1]y=self.conv1(y)y=self.bn1(y)y=self.act(y)# 拆开两个方向x_h,x_w=torch.split(y,[h,w],dim=2)x_w=x_w.permute(0,1,3,2)# [n, c, 1, w]# 升维并生成注意力a_h=self.conv_h(x_h).sigmoid()a_w=self.conv_w(x_w).sigmoid()out=identity*a_h*a_wreturnout关键修改点:
- 加了
max(reduction, 1)防止除零 - 中间通道数至少为 1,避免
inp // reduction = 0的情况 - 池化后不要直接 squeeze,保持维度一致性
四、在 YOLOv11 中插入 CA 模块
YOLOv11 的 backbone 结构跟 v8 类似,但 C3 模块换成了 C2f。我通常把 CA 插在 C2f 后面,或者替换 SPPF 后面的卷积。
修改ultralytics/nn/modules.py,在C2f类后面加一个包装类:
classC2f_CA(C2f):def__init__(self,c1,c2,n=1,shortcut=False,g=1,e=0.5,reduction=16):super().__init__(c1,c2,n,shortcut,g,e)# 在C2f输出后加CAself.ca=CoordAtt(c2,c2,reduction=reduction)defforward(self,x):x=super().forward(x)returnself.ca(x)然后在ultralytics/nn/tasks.py的parse_model函数里注册这个模块。找到ch字典,添加:
# 在 ch 字典里添加ch['C2f_CA']=ch['C2f']最后在 yaml 配置文件里替换。比如yolov11s.yaml中,把第 4 层和第 6 层的C2f改成C2f_CA,并指定 reduction 参数:
# backbonebackbone:-[-1,1,Conv,[64,3,2]]# 0-P1/2-[-1,1,Conv,[128,3,2]]# 1-P2/4-[-1,3,C2f_CA,[128,True,8]]# 2,reduction=8-[-1,1,Conv,[256,3,2]]# 3-P3/8-[-1,6,C2f_CA,[256,True,8]]# 4,reduction=8-[-1,1,Conv,[512,3,2]]# 5-P4/16-[-1,6,C2f_CA,[512,True,8]]# 6,reduction=8-[-1,1,Conv,[1024,3,2]]# 7-P5/32-[-1,3,C2f,[1024,True]]-[-1,1,SPPF,[1024,5]]五、消融实验:ratio 从 4 到 32 的完整数据
实验配置:
- 数据集:COCO 2017(val 5000张)
- 模型:YOLOv11s(参数量约 9.2M)
- 训练:300 epochs,batch size 64,输入 640x640
- 硬件:单卡 A100 80G
- 对比:baseline(无CA) vs CA ratio=4/8/16/32
5.1 参数量与计算量
| 配置 | 参数量 (M) | GFLOPs | 相比 baseline 增加 |
|---|---|---|---|
| Baseline | 9.21 | 21.5 | - |
| CA ratio=4 | 9.68 | 22.1 | +0.47M / +0.6 |
| CA ratio=8 | 9.45 | 21.8 | +0.24M / +0.3 |
| CA ratio=16 | 9.33 | 21.6 | +0.12M / +0.1 |
| CA ratio=32 | 9.27 | 21.5 | +0.06M / +0.0 |
注意看 ratio=32 时,参数量只增加了 0.06M,几乎可以忽略不计。但精度呢?
5.2 mAP@0.5:0.95 曲线
| 配置 | mAP@0.5:0.95 | 相比 baseline 提升 |
|---|---|---|
| Baseline | 44.7% | - |
| CA ratio=4 | 45.8% | +1.1% |
| CA ratio=8 | 46.2% | +1.5% |
| CA ratio=16 | 45.5% | +0.8% |
| CA ratio=32 | 44.9% | +0.2% |
5.3 不同尺度目标的 AP
| 配置 | AP_small | AP_medium | AP_large |
|---|---|---|---|
| Baseline | 28.1% | 48.3% | 59.2% |
| CA ratio=4 | 29.0% | 49.5% | 60.1% |
| CA ratio=8 | 29.4% | 49.8% | 60.5% |
| CA ratio=16 | 28.8% | 49.1% | 59.8% |
| CA ratio=32 | 28.3% | 48.6% | 59.4% |
5.4 训练收敛速度
观察前 50 个 epoch 的 mAP 曲线:
- ratio=8 在第 30 epoch 就超过了 baseline 第 50 epoch 的精度
- ratio=4 收敛稍慢,但最终精度接近 ratio=8
- ratio=32 收敛速度跟 baseline 几乎一样,说明注意力没起作用
六、实验结果分析
ratio=8 是最优选择,原因如下:
信息保留与压缩的平衡:YOLOv11s 的 backbone 通道数在 128-512 之间,ratio=8 意味着中间通道数为 16-64,足够编码空间位置信息。
小目标检测提升明显:ratio=8 对小目标的 AP 提升了 1.3%,因为小目标需要更精细的空间注意力,压缩太狠会丢失位置细节。
参数量增加可控:只增加了 0.24M 参数,推理速度几乎不变。
ratio=4 为什么不如 ratio=8?按理说 ratio 越小信息保留越多,但实验显示 ratio=4 反而比 ratio=8 低了 0.4%。我分析有两个原因:
- 中间通道数太大(比如 512 通道的层,中间通道 128),导致降维卷积的参数量激增,模型容易过拟合
- 两个方向的注意力图可能产生冗余,过多的通道反而引入了噪声
ratio=32 基本没用:中间通道只有 4-16 个,注意力图几乎变成均匀分布,相当于给特征图乘了个常数。
七、不同模型大小的 ratio 建议
我还在 YOLOv11n(3.2M)和 YOLOv11m(20.1M)上做了验证:
| 模型 | 最优 ratio | 提升幅度 |
|---|---|---|
| YOLOv11n | 4 | +1.8% |
| YOLOv11s | 8 | +1.5% |
| YOLOv11m | 16 | +1.0% |
规律很明显:模型越小,ratio 应该越小。YOLOv11n 的通道数只有 64-256,ratio=4 时中间通道 16-64,刚好够用。YOLOv11m 通道数 256-1024,ratio=16 就能保留足够信息。
八、实际部署时的经验
不要无脑用 ratio=16:论文里的默认值是基于 ResNet-50 的,YOLO 的通道数少,直接套用效果不好。
考虑硬件限制:如果部署在边缘设备上,参数量敏感,可以尝试 ratio=16 或 32,虽然精度提升小,但几乎不增加计算量。
多尺度训练时注意:如果用了多尺度训练(比如 320-640),建议用 ratio=8,因为小分辨率下特征图更小,需要更精细的注意力。
跟其他注意力模块组合:我试过 CA + SE 的组合,效果反而下降。CA 本身已经很强了,没必要叠床架屋。
训练技巧:加了 CA 之后,建议把学习率调低 10-20%,因为注意力模块会加速收敛,学习率太高容易震荡。
九、一个容易忽略的细节
CA 模块里的 BN 层在训练和推理时的行为不同。如果你在训练时用了 CA,但导出 ONNX 时发现精度下降,检查一下 BN 的track_running_stats是否设置正确。
我踩过这个坑:在自定义的 CA 模块里忘了设self.training = True,导致 BN 层在训练时也用了推理模式,注意力图完全失效。排查了两天才发现。
解决办法:在CoordAtt的__init__里显式设置self.bn1.track_running_stats = True,或者在forward里根据self.training手动控制。
十、总结(不是教科书式的)
CA 的 reduction ratio 这个超参,说大不大说小不小,但调好了能白捡 1-2 个点的 mAP。我的建议是:先试 ratio=8,如果模型小于 5M 就试 ratio=4,大于 20M 就试 ratio=16。别在 ratio=32 上浪费时间,除非你实在缺那 0.06M 的参数量。
另外,如果你在 YOLOv11 上复现我的实验,记得把 backbone 里所有 C2f 都替换成 C2f_CA,别只换一两个。我试过只换深层,效果不如全换。
最后说一句:注意力模块不是越多越好,也不是越复杂越好。CA 这种轻量级注意力,调好 ratio 比换什么 CBAM、SE 都管用。至少在我经手的十几个项目里,CA 是性价比最高的注意力模块,没有之一。
