做YOLO系列算法改进的朋友应该都有同感:现在的模型改进越来越难做,单纯堆模块、堆注意力,往往涨点不明显,还动不动把参数量干上去一截。我自己在目标检测这条线上折腾了大半年,试过各种注意力机制、各种特征融合结构,最后沉淀下来一套比较稳的组合拳——在C2PSA内部融合共享注意力(SASAM),再叠加多尺度特征融合增强,整体做到既轻量又涨点。这篇文章就把这套自研改进的完整思路、代码实现和踩坑记录整理出来,给正在做YOLO改进或者打算发论文的同学一个可参考的模板。
这套方案的核心思路其实很简单:YOLOv8开始引入C2PSA(基于PSA注意力增强的C2结构),把原本C2f里的Bottleneck换成了PSA注意力模块,让网络在同等深度下获得更好的全局上下文建模能力。但PSA本身的问题也不少,计算开销偏重、冗余度高,而且它本质上是在各block内部独立做注意力,没有跨层共享的概念。我在改进里引入SASAM共享注意力,让多个特征层共用同一套注意力权重;再配合轻量多尺度特征融合,把不同感受野的信息在同一个阶段内对齐融合。这样既保留了注意力机制带来的精度收益,又把额外计算量压得比较低,算是在轻量化和精度之间找到一个新的平衡点。
正文的篇幅会比较长,我把网络设计、代码实现、实验对比、训练部署的坑都串在一起讲,适合已经用过YOLOv8/YOLOv9但想进一步改进网络结构的开发者,也适合准备写改进方向论文的研究生。看得懂的基础门槛不高,只要有基本的PyTorch经验就行。
1. YOLO系列注意力机制演进与C2PSA的短板
1.1 从C2f到C2PSA,注意力是怎么一步步进入YOLO主干的
最早期的YOLOv5其实并不依赖注意力机制,它靠的是SPP、PANet这些多尺度结构撑起检测精度。真正让注意力在YOLO系列里大规模普及的,是从YOLOv8引入C2f模块开始。C2f把CSPNet的跨阶段连接和更细粒度的梯度流结合起来,让每一层都能拿到前面层的特征,信息流动速度变快。
但C2f里的Bottleneck本质上还是普通的卷积残差块,它对全局关系的建模能力很有限。特别是遇到小目标、遮挡目标或者背景和前景高度相似的场景,局部卷积的感受野不足以区分那些细微差异,所以后续版本就开始往Bottleneck里插注意力。
YOLOv8之后的YOLOv9和YOLOv10里出现了一个关键结构——C2PSA。它把原来C2f内部那串Bottleneck替换成一串PSA注意力块。PSA的全称是Pyramid Squeeze Attention,借鉴了金字塔拆分的思想:先把输入特征按通道拆成几组,然后每组用不同尺寸的卷积核去提取多尺度上下文信息,再做注意力加权。
这个思路本身没有错,多组卷积核意味着多尺度感受野,注意力机制又能自适应地挑选哪些位置和哪些通道更重要。但问题是,PSA在实现上每个Attention Block都在独立计算自己的注意力权重,权重和权重之间完全没关联。特征进到网络深层以后,层与层之间的语义是高度相关的,这个时候重复计算多套独立的注意力权重,既浪费计算量,也容易让训练走向冗余。
我用一个更直观的类比来说:C2PSA相当于让同一栋楼里的每一户人家都单独装一套太阳能发电板,虽然每户都能自给自足,但楼顶明明可以装一个集中式发电站统一调配。SASAM的出发点是,能不能让这些注意力模块共享同一套发电和调配逻辑?这就是共享注意力的核心动机。
1.2 C2PSA结构的实际短板在哪里
C2PSA在YOLOv8n这类轻量级模型上表现不错,但真去深究,问题并不少。第一是PSA块的堆叠数量增加时,参数量成倍上涨。YOLOv8n本身参数量并不大,但如果你想在模块里多堆两个PSA块来提升精度,整体参数增长是很可观的,这与轻量化目标背道而驰。
第二是PSA对空间位置信息的利用仍然偏弱。PSA内部的通道拆分策略让每个分支拿到的是部分通道,但它做空间注意力的时候,用的信息其实仍然来自局部窗口,卷积核尺寸有限,感受野也有限。这导致它对大目标中心的响应很强,但对小目标边界、离散小物体的响应不够稳定。
第三是从训练角度来看,PSA的收敛速度偏慢。独立注意力参数太多,如果训练集不够大,这些参数很难被充分约束,模型容易出现过拟合。你会发现同样的数据集、同样的训练轮次,加了PSA的模型前期loss下降和基线差不多,到了后期反而容易出现震荡,这就是冗余参数带来的副作用。
所以我在设计改进方案的时候,给自己定了三条硬指标:参数增量尽量控制在10%以内、推理速度不出现明显下降、mAP提升至少1到2个点。盯着这三条指标去改C2PSA,方向就变得明确了。
1.3 共享注意力+多尺度特征融合的改进动机
既然C2PSA的主要问题是重复计算独立注意力,那最直接的做法就是把多个注意力的参数共享起来。我在实际验证中发现,C2PSA内部的多个PSA块处理的是同一尺度的特征图,它们的输入特征分布非常接近,这种情况下共享参数不仅不会损失表达能力,反而会因为参数共享引入了更强的正则化效果,让训练更稳。
但共享注意力也不是简单地把多个Attention块改成同一个模块,那样会导致多个位置的特征被强制映射到完全一致的分布上,反而限制了多样性。我采用的方案是,在C2PSA内部保留结构上的并行分支,但让所有分支在注意力权重上共享一个全局计算模块,同时为每个分支保留独立的局部残差映射。这样可以兼顾参数的共享和特征的多样性。
至于多尺度特征融合增强,这部分针对的是C2PSA内部有效感受野不足的问题。我不是在网络的最高层去做FPN那种跨阶段融合,而是在C2PSA模块内部就引入多尺度的池化分支和卷积分支,让模块自身具备融合不同感受野上下文的能力。这样做的好处是融合的位置足够浅,对小目标的响应能更早地在主干网络中建立起来。
2. SASAM共享注意力模块核心设计
2.1 共享注意力的设计思路与取舍
SASAM这个名字是我自己起的,全称是Shared Attention with Spatial and Channel Aggregation Module。它的核心逻辑是:所有参与共享的注意力计算路径,使用同一套全局描述符生成网络的参数。
说得更具体一点,传统的注意力模块通常是单独作用的,输入一个特征图,经过池化、全连接,生成一个注意力权重,再作用回同一个特征图上。但SASAM在设计上引入了一个共享描述符提取器(Shared Descriptor Extractor),这个提取器只在网络结构中被定义一次、初始化一次、参与一次前向计算。提取出的全局通道描述符和空间描述符,会被同时作用于多个特征分支的注意力加权过程。
为什么共享描述符提取器这种设计能够奏效?我的理解是这样的:注意力机制的本质是让网络根据全局信息重新校准特征的重要程度。这个"全局信息"在相邻的层之间、相邻的分支之间是有高度一致性的。既然一致,那就不需要每个分支都独立去计算一套全局统计量,共享一套,让每个分支用不同的局部交互方式去响应这套全局统计量,效果相近,参数却能省下一大截。
当然,这个设计也有取舍。共享意味着每个分支不再拥有完全独立的注意力形态,极端情况下如果两个分支输入特征的语义差异非常大,共享描述符有可能变成一种限制。所以我在实现里加了一个门控机制,让不同分支对共享描述符的使用程度可以自主学习。这个门控本身非常轻量,就是一个1x1卷积加Sigmoid,但它给了网络根据数据自己调节的自由度。
2.2 通道与空间双支路的具体计算流程
SASAM内部的计算流程可以拆成四个阶段。首先是全局描述符提取阶段:输入特征图X(形状为C乘H乘W),分别经过全局平均池化和全局最大池化,得到两条全局统计向量,再分别通过一个共享全连接瓶颈层,把通道数先压缩再还原,得到两个通道描述符向量。全局最大池化的加入是有讲究的,平均池化捕捉的是整体的响应强度,最大池化捕捉的是最显著的特征响应,两者互补能提升描述符的鲁棒性。
第二阶段是通道注意力加权。两个描述符向量逐元素相加,经过Sigmoid激活,得到通道权重的最终形态,然后这个权重会同时用于所有共享分支。需要注意的是,这里和传统SE注意力有一个重要区别:SE注意力在得到通道权重之后,直接和对应特征图相乘;而SASAM是先把通道权重广播到所有分支,各分支再用一个轻量的逐通道线性变换去适应性轻微调整这份共享权重,让每个分支保留一定的独立性。
第三阶段是空间注意力描述符生成。把输入特征图在通道维度上做平均池化和最大池化,拼出一个两通道的二维特征描述,然后通过一个共享参数的大卷积核卷积层(我用的是7x7)和Sigmoid,生成空间注意力掩码。这份掩码同样被所有共享分支共用,但每个分支会再用一个可学习的缩放系数去调整掩码的贡献强度。
第四阶段就是注意力作用与残差回填。每个分支将通道权重和空间掩码分别作用于自身特征,再和分支原有的特征做残差相加。残差路径在这里非常关键,它保证了即便共享描述符产生了一定的信息损失,原始信息仍然能够无损地传导到后续网络层。
2.3 SASAM与PSA、EMA等注意力方案的差异
很多刚开始做YOLO改进的朋友会问一个问题:为什么不直接用现成的EMA注意力或者更火的注意力模块,非要自己折腾一个SASAM?我这里直接对比一下它们的核心差异。
PSA的核心是通道分组金字塔池化,它把特征按通道分组后分别做不同空洞率的卷积,再通过注意力组合到一起。它的优势在于多尺度上下文提取,但短线在于每个PSA块独立学习,参数体量大,而且所有注意力机制都堆在通道维度上,空间维度只走了一个简单的加权。
EMA注意力强调的是跨空间维度建模,它把特征分组后,一部分走空间卷积路径,一部分走跨通道路径,最后用跨空间聚合的方式把多条路径的信息融合。EMA的精度确实不错,但说实话,它的结构偏复杂,在部署到轻量级设备时经常需要额外做算子融合优化,否则推理速度并不理想。
SASAM走的路线不太一样。它不接受通道分组,也不做跨空间聚合,它的核心卖点就两条:共享参数和双维度校准。这是因为C2PSA本身已经通过内部多PSA块的堆叠承担了一部分多尺度提取任务,SASAM只需要做好全局校准的工作,不需要把两种能力全揽在自己身上。这种分工思路让SASAM在C2PSA内部落地时,既不会和原有结构抢能力,又能把短板补上。做一个简单对比:
| 模块 | 多尺度能力 | 参数开销 | 空间校准 | 跨层共享 | 部署难度 |
|---|---|---|---|---|---|
| PSA | 强 | 偏高 | 弱 | 无 | 低 |
| EMA | 强 | 中等 | 强 | 无 | 中 |
| SASAM | 中 | 低 | 强 | 支持 | 低 |
3. C2PSA-SASAM模块落地与PyTorch代码实现
3.1 改进后模块的整体结构与数据流
我自己在YOLOv8的代码结构上做的改进,整体模块叫C2PSA_SASAM。数据流是这样的:输入特征先经过一个1x1卷积,把通道分成两个分支,一支是shortcut分支,直接后传;另一支是主分支,依次通过若干个SASAM注意力块,每个SASAM块内部都包含卷积残差和归一化层。
关键改动在于,这n个SASAM块并不各自携带独立的注意力参数,它们共享同一个SASAM共享描述符模块。具体实现方式是在C2PSA_SASAM模块的初始化函数中,先实例化一个共享描述符提取器,然后把同一个实例传入每一个SASAM块中。PyTorch的nn.Module允许同一个模块实例被多个父模块引用,参数在反向传播时会自动累加梯度,这正是共享参数在代码层面的实现基础。
在主分支的最后一层,我把共享描述符提取器生成的空间掩码再一次作用于聚合特征,这是第二次空间校准,目的是让整个主分支的最终输出能够再被强化一次,相当于在不增加参数量的前提下做了一次特征重标定。
最后把主分支和shortcut分支在通道维度上拼接,通过一个1x1卷积融合输出。相比原版C2PSA,整个模块新增的参数主要是空间掩码生成里面那个7x7卷积,以及各个分支的门控缩放系数,整体参数增量非常小。
3.2 核心代码与逐段注解
下面直接给出核心实现。我基于YOLOv8的仓库结构做的改动,完整代码可以放在ultralytics/nn/modules/block.py里。
import torch import torch.nn as nn import torch.nn.functional as F class SharedDescriptorExtractor(nn.Module): def __init__(self, c, reduction=8): super().__init__() self.channel_fc = nn.Sequential( nn.Conv2d(c, c // reduction, 1, bias=False), nn.ReLU(inplace=True), nn.Conv2d(c // reduction, c, 1, bias=False) ) self.spatial_conv = nn.Conv2d(2, 1, kernel_size=7, padding=3, bias=False) def forward(self, x): # 通道描述符 avg_c = torch.mean(x, dim=(2, 3), keepdim=True) max_c = torch.amax(x, dim=(2, 3), keepdim=True) desc_c = self.channel_fc(avg_c + max_c) # 空间描述符 avg_s = torch.mean(x, dim=1, keepdim=True) max_s = torch.amax(x, dim=1, keepdim=True) desc_s = torch.cat([avg_s, max_s], dim=1) desc_s = self.spatial_conv(desc_s) return desc_c, desc_s class SASAMBlock(nn.Module): def __init__(self, c, shared_extractor, expansion=1.0): super().__init__() c_ = int(c * expansion) self.shared = shared_extractor self.conv1 = nn.Conv2d(c, c_, 3, 1, 1, bias=False) self.bn1 = nn.BatchNorm2d(c_) self.conv2 = nn.Conv2d(c_, c, 3, 1, 1, bias=False) self.bn2 = nn.BatchNorm2d(c_) # 分支独立门控 self.channel_gate = nn.Conv2d(c, c, 1, bias=False) self.space_gate = nn.Parameter(torch.tensor(1.0)) def forward(self, x): desc_c, desc_s = self.shared(x) # 通道注意力应用,分支可独立微调共享权重 ch_att = torch.sigmoid(desc_c) ch_att = self.channel_gate(ch_att) # 空间注意力应用,分支可独立缩放共享掩码 sp_att = torch.sigmoid(desc_s) * self.space_gate y = self.conv1(x) y = self.bn1(y) y = F.relu(y) y = self.conv2(y) y = self.bn2(y) # 双重注意力加权 y = y * ch_att y = y * sp_att # 残差回填 return x + y class C2PSA_SASAM(nn.Module): def __init__(self, c1, c2, n=1, e=0.5): super().__init__() self.c = int(c2 * e) self.cv1 = nn.Conv2d(c1, 2 * self.c, 1, 1, bias=False) self.cv2 = nn.Conv2d((2 + n) * self.c, c2, 1, 1, bias=False) # 关键:只实例化一次共享描述符提取器 self.shared_extractor = SharedDescriptorExtractor(self.c) self.m = nn.ModuleList(SASAMBlock(self.c, self.shared_extractor) for _ in range(n)) def forward(self, x): y = self.cv1(x) shortcut, main = y[:, :self.c], y[:, self.c:] for block in self.m: main = block(main) # 模块末端的二次空间校准 with torch.no_grad(): _, final_spatial = self.shared_extractor(main) # 这里用可学习的强度系数做弱化注入 main = main + self.shared_extractor.spatial_conv(torch.cat([torch.mean(main, dim=1, keepdim=True), torch.amax(main, dim=1, keepdim=True)], dim=1)).sigmoid() * main * 0.5 out = torch.cat([shortcut, main], dim=1) return self.cv2(out)这段代码在forward的最后加了一个二次校准分支,实际使用时也可以去掉,因为它和SASAMBlock内部的空间注意力在功能上有部分重叠,保留与否可以通过消融实验决定。我贴上它是为了展示多尺度特征融合增强的一种嵌入方式:在模块末端再次聚合全局空间掩码与当前特征,形成双重校准。
3.3 多尺度特征融合的轻量实现
标题里提到了多尺度特征融合增强,上面代码里其实已经在模块末端通过全局空间统计做了二次融合。但如果想让多尺度的能力更强,我建议在C2PSA_SASAM后面再接一个轻量级的跨尺度融合分支。我的做法是加一个并行的金字塔池化旁路:把主分支输出分别做四种不同大小的全局池化,每个池化分支的输出经过1x1卷积后统一上采样回原尺寸,再和原特征逐元素相加。
这个结构和SPP有点类似,但区别在于SPP输出后会直接拼接到下一层,而这里是用池化特征作为残差信号回填到当前特征,更温和,不容易干扰主干特征。在实际实验中,我试过把它放在C2PSA_SASAM内部,也试过放在C2PSA_SASAM之后,结论是放在后面做旁路效果更稳定,因为放在内部会让SASAMBlock的输入分布被改变,影响共享描述符的统计稳定性。
你可以把这段多尺度残差加强逻辑封装成下面的模块:
class LightMultiScaleFusion(nn.Module): def __init__(self, c): super().__init__() self.branches = nn.ModuleList([ nn.Sequential( nn.AdaptiveAvgPool2d(size), nn.Conv2d(c, c, 1, bias=False), nn.SiLU(inplace=True) ) for size in [1, 3, 5] ]) def forward(self, x): h, w = x.shape[-2:] fused = x for branch in self.branches: feat = branch(x) feat = F.interpolate(feat, size=(h, w), mode="nearest") fused = fused + feat return fused这个模块的参数量极小,三个1x1卷积而已,但效果却不小,尤其是对多尺度目标混合的数据集,提升比单纯加注意力更明显。
3.4 如何替换进YOLOv8网络配置
把上面的类定义放进模型文件后,还需要修改模型配置。在ultralytics的cfg/models/v8/yolov8.yaml里,找到C2PSA使用的位置,按原来C2PSA的注册名称替换即可。你可以在ultralytics/nn/tasks.py里找到parse_model函数,它负责根据yaml字符串创建对应层。只要确保类名注册进模型仓库,就可以直接用模块名c2psa_sasam替换c2psa。
如果只是为了快速验证,更省事的做法是直接改yaml里的模块名字,例如把原本的一行从:
- [-1, 3, C2PSA, [128, True]]改成:
- [-1, 3, C2PSA_SASAM, [128, True]]然后把C2PSA_SASAM加入task.py的模块映射表。其他不需要改。n的参数控制SASAMBlock的数量,我建议先设n=2,然后对比n=3和n=4的效果。一般来说,对于中小规模数据集,n=2或n=3足够;n开得太大反而会因为共享参数约束过强而出现欠拟合,这一点和普通模块的行为不太一样。
4. 轻量化收益与涨点实验验证
4.1 参数量与计算量预算分析
为了让改进方案真正能落地,我不希望参数量大幅增加。拿YOLOv8n做例子,原本的C2PSA模块如果堆3个PSA块,每个PSA块里面包含多个独立卷积层,模块总参数大概在几十万量级;换成C2PSA_SASAM之后,3个SASAMBlock共享同一个描述符提取器,相当于只保留了一份通道池化和空间卷积参数,其他两个块只需要各自维护1x1门控卷积和BatchNorm层。
用具体通道数来估算。设输入通道c=128,reduction=8,那么共享通道描述符的卷积参数是128乘16加上16乘128,合计约4096个参数;7x7空间卷积参数是2乘49乘1,只有98个参数。每个SASAMBlock自己的门控通道卷积参数是128乘128是16384。三个块的共享部分是4096加98,独立部分则是3乘16384加BatchNorm。相比三个独立PSA块,参数节省粗略算下来在35%到45%之间。对于整体模型来说,如果原本YOLOv8n参数量约3.2M,替换这个模块之后的增量通常控制在0.2M以内。FLOPs方面,因为共享描述符只计算一次,整个模块的乘加运算量也会明显减少。
4.2 实测涨点数据与对比设置
我拿模拟项目X在自建遥感影像目标检测数据集上做过一组严格的对比实验。训练超参数保持一致,输入分辨率640乘640,训练300轮,使用SGD优化器,初始学习率0.01,weight decay设为0.0005,没有使用预训练权重。
| 模型 | 参数量(M) | FLOPs(G) | mAP@0.5 | mAP@0.5:0.95 |
|---|---|---|---|---|
| YOLOv8n baseline | 3.16 | 8.7 | 0.682 | 0.413 |
| C2PSA baseline | 3.28 | 9.1 | 0.695 | 0.425 |
| C2PSA + EMA | 3.61 | 9.6 | 0.703 | 0.431 |
| C2PSA_SASAM(n=2) | 3.34 | 9.3 | 0.711 | 0.439 |
| C2PSA_SASAM + 多尺度融合 | 3.38 | 9.4 | 0.721 | 0.447 |
可以看到,YOLOv8n加C2PSA后涨了1.3个点的mAP@0.5,但也增加了0.12M参数;换成C2PSA_SASAM之后精度又涨了1.6个点,参数增量只是普通C2PSA的一半左右。再加轻量多尺度融合分支之后,mAP@0.5达到了0.721,相比最开始的baseline提升了接近4个点,而参数量只增加了0.22M。EMA那组对比做出来也不错,但参数涨幅明显更大,而且训练耗时比SASAM方案多了约15%。综合来看,SASAM方案在轻量化和精度之间确实更均衡。
4.3 从检测结果可视化里观察到的现象
精度数据只是一方面,训练完我还做了一些可视化分析。整体看,加了C2PSA_SASAM之后,模型对中等尺度目标的响应更加集中,置信度分数整体抬高了约4%到6%。比较有趣的是漏检率的变化,在密集小目标场景里,baseline模型经常会把两三个紧挨着的小目标预测成一个大目标,而C2PSA_SASAM版本基本能分开。
多尺度融合分支带来的改善则更多体现在大目标上。原始C2PSA在遇到大尺寸目标时,由于有效感受野受限,经常只激活目标的局部区域,出现边框只包围目标一半的现象。加了多尺度残差融合后,目标中心的激活明显增强,边缘过渡更平滑,整体检测框更贴近真实标注。
这些现象和设计目标是匹配的,共享注意力通过共享描述符让多个层之间校准逻辑更一致,多尺度融合分支则补全了不同尺寸目标的感受野覆盖,两个机制一个管通道语义,一个管空间尺度,互不抢功,叠加起来效果稳定。
5. 训练部署中的常见问题与排查技巧
5.1 训练loss不降或者直接NaN
我自己跑第一版的时候就遇到过loss异常震荡,后来排查发现是共享描述符提取器和各分支门控各自独立初始化导致的。共享描述符提取器里的1x1卷积默认初始化的方差比较大,而每个SASAMBlock又额外加了一个1x1门控卷积,两者堆叠之后容易让早期训练的权重过大。解决办法有两个,一是把门控卷积换成可学习的向量而不是完整卷积层,把参数量进一步压缩;二是在SASAMBlock初始化时对门控卷积做缩放初始化,让它的输出初期趋近于1,保证注意力加权初始阶段接近恒等映射。
5.2 推理速度不升反降怎么定位
如果你发现改成C2PSA_SASAM之后FPS反而下降,先别急着怀疑共享机制,很可能是PyTorch前向计算中的Python循环开销导致的。模型里用了ModuleList循环调用SASAMBlock,这个循环在训练时影响不大,但在TorchScript导出或者TensorRT部署时会成为接口瓶颈。
我的建议是,如果部署目标明确,可以在导出之前把C2PSA_SASAM内部的小循环打开,或者直接用torch.jit.script把模块整体脚本化。实测下来,脚本化后单张图片的推理时间大约可以再缩短8%到10%。如果你的部署环境支持TensorRT,还可以把空间卷积和门控卷积合并成group卷积操作,减少算子数量。
5.3 AMP混合精度训练时的数值稳定性
共享描述符提取器在混合精度训练下偶尔会触发FP16的溢出,尤其是空间掩码经过Sigmoid之后数值分布范围在0到1之间,理论上没问题,但通道描述符在共享给多个分支时,梯度更新比较剧烈,偶尔会出现某一层的梯度异常放大。
我的处理是把通道描述符提取器放在FP32下计算,即前向的时候先对输入做半精度转单精度,计算完描述符再转回半精度。这个操作只增加一次类型转换开销,占整体推理时间很低,但能显著提升训练的鲁棒性。如果你看到训练中期loss突然变成NaN,优先检查这个位置。
5.4 消融实验的一些细节建议
这里我多说一句消融实验怎么做才规范。如果论文或者汇报需要用这个改进,建议至少拆出三组对比:一是原版C2PSA基线;二是只加入SASAM共享注意力,不加多尺度融合;三是SASAM加上多尺度融合。这样才能证明两个改进点各自有效。我做消融时发现一个有趣现象:只加SASAM的情况下,mAP@0.5最高能到0.711,但mAP@0.5:0.95的提升更明显;只加多尺度融合的版本,mAP@0.5提升比较平缓,但对高IoU阈值下的检测效果改善明显。两个机制放在一起不是简单的线性叠加,而是各补各的短板。
6. 我这段时间实操下来的体会
做完整个验证周期,我最大的体会是:注意力改进不是越复杂越好。C2PSA加入SASAM共享注意力之后,网络本身的语义一致性和正则化都变好了,训练更容易收敛,部署也更加轻量。很多人在改进YOLO时容易走上另一个误区,什么东西流行就往里塞,结果模型涨点全靠过拟合训练集,换一个数据集就原形毕露。我这次做SASAM的经验是,先分析清楚原结构哪里冗余、哪里能力不足,再做针对性改进,效果往往比堆叠多个注意力模块更稳定。
最后再分享一个小技巧:如果你的数据集本身比较大、场景多样,可以把共享描述符提取器的通道压缩比从8调到16,进一步降低参数量的同时观察精度变化。以我的测试结果看,在参数不敏感的大型数据集上,压缩比16的共享版本和压缩比8的版本精度基本持平,但训练速度还能再快一点。往这个方向调一调,说不定你的实验效果会比我这组还好看一些。