空间注意力网络(Spatial Attention Neural Network, SANN)把空间注意力机制嵌进 CNN,在处理时序数据时能动态聚焦关键片段。我在 UCI HAR 数据集上跑了一把,96.31% 准确率,单次推理 1.61ms,轻量又能打。
为什么需要空间注意力
普通 CNN 处理多通道时序数据时,每个时刻、每个通道的特征都给一样的权重。实际上,重要的模式只在某些时间窗出现,噪声却无处不在。尤其像加速度计、陀螺仪这类传感器数据,静态姿势和动态动作的差异往往集中在几个关键区间,一视同仁地卷积等于浪费计算。
我最初直接用纯 CNN 跑 UCI HAR,验证集卡在 92% 左右死活上不去,仔细看了混淆矩阵,发现 Standing 和 Sitting 这两个静态类经常互相误分 —— 它们的信号本来就平坦,CNN 抓不住细微的波动。所以我想,如果让网络自己学会'看哪里',也许能改善这类问题。空间注意力刚好就是这个思路。
核心模块:SpatialAttentionModule
SANN 的创新点全在一个轻量的空间注意力模块里。代码很简单:
class SpatialAttentionModule(nn.Module):
def __init__(self):
super().__init__()
self.att_fc = nn.Sequential(
nn.Conv2d(1, 1, (3, 1), (1, 1), (1, 0)),
nn.Sigmoid()
)
def forward(self, x):
# x: [batch, channel, series, modal]
att = torch.mean(x, dim=1, keepdim=True) # 通道压缩
att = self.att_fc(att) # 生成空间权重
out = x * att
return out

设计上有几个考量:
- 通道取平均压缩:把多通道信息压成一张二维空间图,再在上面学权重。省参数,也避免通道维度的干扰。
- 只在时序轴做注意力:因为输入的 shape 是 [batch, channels, series, modal],我把卷积核设成
(3,1),只在 series 维度滑动,modal 维度保持不变。这是因为传感器数据的模态轴(比如三轴加速度)天然就有物理意义,强行卷积可能会破坏空间结构。 - Sigmoid 保证权重 0~1:软性选择,训练稳定。
模块本身参数量极小,几乎不影响推理速度,但带来的增益很明显。
完整网络结构
主网络是四层卷积,每层后跟一个空间注意力模块。通道数按 64 → 128 → 256 → 512 递增,每层卷积核 (3,1),步长 (2,1) 只在时序轴下采样。
class SpatialAttentionNeuralNetwork(nn.Module):
def __init__(self, train_shape, category):
super().__init__()
self.layer = nn.Sequential(
nn.Conv2d(1, 64, (3, 1), (2, 1), (1, 0)),
SpatialAttentionModule(),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Conv2d(64, 128, (3, 1), (2, 1), (1, 0)),
SpatialAttentionModule(),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.Conv2d(128, 256, (3, 1), (2, 1), (1, 0)),
SpatialAttentionModule(),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.Conv2d(256, 512, (3, 1), (2, 1), (1, 0)),
SpatialAttentionModule(),
nn.BatchNorm2d(512),
nn.ReLU()
)
self.ada_pool = nn.AdaptiveAvgPool2d((1, train_shape[-1]))
self.fc = nn.Linear(512 * train_shape[-1], category)

几点实际体会:
- 注意力放在 BatchNorm 前还是后? 我试过两种,发现先注意力再 BN 效果更好 —— 注意力权重先修正特征,BN 再稳定分布,梯度回传更顺畅。
- 模态维度保留:最后自适应池化成
(1, modal)再全连接,这样不同输入序列长度都能适配。 - 下采样只在时序轴:模态维度从一开始就保留原始尺寸,这是传感器数据特有的设计,如果换成图像任务,肯定要改卷积核。
UCI HAR 实验实录
在 UCI HAR 数据集上训练了 50 个 epoch,batch size 64,Adam 默认参数。训练和验证曲线很接近,没有明显过拟合。
最终结果:
Val Acc: 96.31% | Accuracy: 96.31%
Macro Precision: 0.9643 | Macro Recall: 0.9639 | Macro F1: 0.9641
Inference Time: 1.61 ms

各类别表现:
- Walking: Precision=0.9685, Recall=0.9826, F1=0.9755
- Jogging: Precision=0.9711, Recall=0.9773, F1=0.9742
- Sitting: Precision=0.9964, Recall=0.9751, F1=0.9856
- Standing: Precision=0.9144, Recall=0.9298, F1=0.9220
- Upstairs: Precision=0.9358, Recall=0.9186, F1=0.9272
- Downstairs: Precision=1.0, Recall=1.0, F1=1.0
Downstairs 完美分类,Sitting 和 Walking 也很高。Standing 和 Upstairs 相对弱一些,但对比纯 CNN 已有明显提升(之前这两个类 F1 不到 0.89)。
静态活动的混淆主要发生在 Standing 和 Sitting 之间,因为它们在三轴加速度上都是近乎常值,区别只在于方向轴的偏置。注意力模块能在时序上聚焦那些微小的姿态切换瞬间,比如坐下时短暂的失重,所以 Sitting 的召回率能达到 97.5%。
设计与调参取舍
- 为什么选择平均池化而不是最大池化压缩通道? 最大池化对噪声敏感,平均池化更平滑,能保留全局空间分布趋势,这在时序数据上更稳定。
- 注意力的卷积核为什么是 3×1? 1×1 的话没上下文,大于 3 感受野过大,可能模糊了关键时刻。3 是平衡点,局部相邻几帧就够了。
- 没有使用全局池化? 我想保留模态维度的空间关系,自适应池化到固定的 (1, modal) 再全连接,比直接压成向量丢失的信息更少。
总结
SANN 用很小的计算代价,换来了可观的性能提升。它的注意力模块可以无缝插到任何类似结构的 CNN 里。在实时传感器数据分析这种对速度和精度都有要求的场景,相当实用。
完整代码我已经整理在 GitHub,有兴趣可以跑跑看,换成自己的数据集也只需要调整一下数据加载和最后的全连接输出维度。


