☰
CNN-LSTM结合SE注意力机制实现MATLAB时序数据分类预测
2026/10/20 22:45:53 网站建设 项目流程

最近在做一整套基于MATLAB的时序信号分类预测,原模型用的是CNN-LSTM,训练到一定轮数之后验证集准确率就一直停在91%附近上不去。后来我把SE注意力模块插在卷积块和LSTM中间,变成CNN-LSTM-SE结构,同一个测试集上成绩直接跳到94.6%。这篇文章就围绕这套"基于卷积神经网络-长短时记忆网络结合SE注意力机制的数据分类预测"展开,重点说清楚三件事:为什么SE模块值得加、在MATLAB里怎么落地、以及替换原模型时最容易踩的坑。有两次跑训经验,有代码片段,也有实际对比数据,适合正在折腾时序分类、故障诊断或信号识别的朋友参考。

1. 先从一次"替换"说起:为什么非要在CNN-LSTM中间插一个SE

1.1 一个基线模型卡在91%的典型场景

先说背景。我做的是多通道传感器信号的多分类任务,输入是12个通道、每个样本200个时间步的连续序列,类别数在5到8类之间。最初的基线网络是两层一维卷积加一层LSTM再加全连接分类头,结构大概是:Conv1D -> BN -> ReLU -> MaxPool -> Conv1D -> BN -> ReLU -> MaxPool -> LSTM -> FC -> Softmax。这个结构其实是很多MATLAB时序分类项目里最常用的组合,跑起来挺稳定,也没有明显过拟合。

但问题在于,到了91%上下就开始卡平台。我试过把LSTM隐藏单元从64加到128,试过卷积核从16加到32,也试过把Dropout调到0.5,结果在验证集上要么原地踏步,要么训练集提升明显但验证集不升反降。回头去看混淆矩阵,发现错分样本几乎集中在几组"长得特别像"的工况类别上。也就是说,模型并不是学不动了,而是对某些特征通道的区分力不够。

这时候我意识到一个关键问题:卷积层输出的每个通道,本质上是信号在不同频带或不同模式下的响应强度。但LSTM在读取卷积特征时,是把所有通道一律平等对待的——那些对分类起决定作用的通道,和那些基本属于噪声的通道,在进入LSTM时权重是一样的。这就像让一个人同时听八个声源的混音,却没有提前告诉他哪个声源重要。于是我把注意力转到了通道重标定上,也就是SE模块。

1.2 SE注意力到底在解决哪个痛点

SE的全称是Squeeze-and-Excitation,中文常叫"压缩-激励"或者"通道注意力"。它和Transformer那套自注意力完全不是一回事:自注意力是在计算位置与位置之间的相关性,而SE做的是显式建模特征通道之间的依赖关系,让网络自己学会"哪些通道该放大,哪些通道该抑制"。

SE的思路非常直白,三步走。第一步Squeeze,把每个通道在时间维度上压缩成一个全局标量,一般用全局平均池化完成,相当于统计每个通道在整个序列上的平均响应水平;第二步Excitation,用两个全连接层对压缩后的向量做非线性变换,输出一组0到1之间的权重;第三步Scale,把这组权重沿着通道方向乘回原始特征图。

我用一个很生活化的类比来理解它:原始特征图是摄像机拍的画面,Squeeze是先把每一帧画面缩成一张灰度缩略图,Excitation是让一个"调度员"根据缩略图判断每个区域该开多大光圈,Scale就是把光圈设定落到实际拍摄上。整个过程没有引入复杂的注意力图,成本很低,但能让LSTM拿到一份"重点已标好"的特征序列。

在实际替换时并没有推翻原网络结构,只是在卷积输出和LSTM输入之间多插了这一个模块。这也是后面所有实验的起点——改动足够小,效果却立竿见影。

2. 数据流全走查:从卷积特征到通道重标定再到LSTM记忆

2.1 卷积层输出的特征图,在进入LSTM之前发生了什么

要把SE插对位置,首先要彻底搞懂数据在CNN-LSTM骨架里的流转过程。以我用的12通道时序数据为例,单个样本的原始形状是12×200,也就是12个通道变量、200个时间步。进入网络时,按照MATLAB的深度学习习惯,我们一般把样本组织成通道×批量×时间的三维张量,用dlarray标记成CBT格式。

经过第一层一维卷积后,如果是32个卷积核,那么输出就从12通道变成32通道,序列长度还是200(用了same填充);紧接着池化层把时间维减半到100。第二层卷积再把通道扩到64,池化后时间维变成50。在这一步,特征图的形状就是64×批量×50:64个通道代表64种局部特征模式,50个时间步代表压缩后的序列长度。

问题恰恰出在这里。LSTM在每个时间步输入的向量长度就是64,也就是说LSTM会把这64个通道当作等权重的输入特征去处理。但现实中,这64个通道的区分度差异非常大,有些通道几乎总是在同类样本里保持稳定、在异类样本里差异明显,有些通道则完全是噪声响应。如果不做任何加权,LSTM的"注意力资源"会被大量浪费在无信息通道上。

2.2 Squeeze、Excitation、Scale三步的维度变化

SE模块在数学上不复杂,但MATLAB里面写代码时要格外注意每一步的维度。以下是我实际运行时记录下来的张量变化过程,直接用CBT格式叙述,C是通道、B是批量、T是时间步:

阶段操作输入形状输出形状
原始特征F卷积块输出C×B×TC×B×T
Squeeze在时间维上求均值C×B×TC×B
Excitation-1全连接:C→C/rC×B(C/r)×B
ReLU激活非线性变换(C/r)×B(C/r)×B
Excitation-2全连接:(C/r)→C(C/r)×BC×B
Sigmoid归一化到0~1C×BC×B
Scale把权重扩成C×B×T并乘回C×B×TC×B×T

这里有个需要特别提醒的点:Squeeze是在时间维上求均值,不是在整个batch上求均值。每个样本要保留自己的通道权重,否则不同样本之间的通道重要性会被相互抹平,模型就学成了"全体平均注意力",效果会打折扣。

Excitation部分有一个超参数r,叫缩减比。第一个全连接层把通道数压缩到原来的1/r,第二个全连接层再恢复回去。这个设计的目的是用瓶颈结构减少参数量,同时强迫权重向量学习一个低维的通道相关性表示。r太大信息丢失太多,r太小起不到正则化作用。我在实验里的经验是:时序信号分类场景下r=8往往比r=16效果更好,因为信号通道之间的相关性比起图像像素通道更直接,不需要那么深的压缩。

2.3 LSTM如何消化经过重标定的特征序列

经过Scale之后,特征序列仍然是C×B×T的形状,只是每个通道的幅值被重新缩放过了。关键点在于:LSTM在每一个时间步t看到的输入向量,是特征图在t时刻沿所有通道取出的长度为C的向量,也就是F_hat(:, :, t)。经过SE重标定后,这个向量中重要通道的数值被放大,次要通道的数值被压缩。LSTM的输入门和遗忘门在计算时,自然会对被放大的维度产生更强的响应。

这带来的直接好处是:LSTM不再需要从零开始学习"哪些通道更重要"这件事,卷积层加SE已经把这一步做完了。梯度回传时也不必再费力去调整卷积核去弥补通道间的区分度不足,整个网络各司其职。

在实际组装时,LSTM的OutputMode我设成了last,也就是说只取最后一个时间步的隐状态作为整个序列的摘要向量,再接Dropout和全连接分类。如果你的任务不是整条序列分类,而是每个时间步都要预测,那就要改成sequence输出。分类场景下用last更稳,因为LSTM经过几十步迭代后,最后一个隐状态已经隐含了整个序列的信息,再用全序列输出去做均值池化反而容易引入尾部噪声。

3. MATLAB里从零搭建:SE层封装与网络装配

3.1 统一dlarray维度标记,别让顺序问题毁掉整个模型

MATLAB和Python的一个显著区别是:Python的深度学习框架通常靠约定维度顺序,而MATLAB在自定义网络和训练循环里要求你用dlarray的维度标签来显式声明。比如CBT就表示第一个维度是通道、第二个是批量、第三个是时间。如果你的数据是普通的N×T×C矩阵,读进来之后必须先做一次permute,转成C×N×T,否则网络会把时间步当成通道、把通道当成时间步,训练出来完全没法看。

我建议从一开始就养成固定格式的习惯。数据预处理阶段统一输出C×N×T的数值数组,然后调用dlarray(X, 'CBT')包一层。尤其是当你要自己写SE层、LSTM层混合组装时,维度标记不统一,报错信息往往非常抽象,排查半天才发现是维度顺序反了。

3.2 用classdef封装一个简洁的SE层

MATLAB的Deep Learning Toolbox里没有现成的SE层,需要自己写一个继承了nnet.layer.Layer的类。以下是我在项目里实际使用的核心框架,去掉了不少与业务无关的检查代码,保留主干逻辑:

classdef seLayer < nnet.layer.Layer % SE通道注意力层,输入输出格式均为 'CBT' properties (Learnable) W1 % 压缩权重: [C/r, C] b1 % 压缩偏置: [C/r, 1] W2 % 恢复权重: [C, C/r] b2 % 恢复偏置: [C, 1] end properties Reduction end methods function layer = seLayer(numChannels, reduction, name) layer.Name = name; layer.Description = "Squeeze-and-Excitation layer"; layer.Reduction = reduction; r = reduction; % 这里用简单正态分布初始化,实际可按Xavier初始化 layer.W1 = dlarray(0.01 * randn(numChannels / r, numChannels)); layer.b1 = dlarray(zeros(numChannels / r, 1)); layer.W2 = dlarray(0.01 * randn(numChannels, numChannels / r)); layer.b2 = dlarray(zeros(numChannels, 1)); end function Z = predict(layer, X) % X: C x B x T gap = mean(X, 3); % 对时间维求均值: C x B s = fullyconnect(gap, layer.W1, layer.b1); % 压缩 s = relu(s); s = fullyconnect(s, layer.W2, layer.b2); % 恢复 s = sigmoid(s); % C x B s = reshape(s, size(s, 1), size(s, 2), 1); % 扩成C x B x 1 weight = repmat(s, 1, 1, size(X, 3)); % C x B x T Z = X .* weight; end end end

这里有个MATLAB版本相关的注意事项:如果你的版本对自定义层支持自动微分,只需要实现predict和forward;如果版本较老,可能还需要手动实现backward。我项目里使用的是较新的版本,这个类可以直接参与dlnetwork和trainnet的训练流程。如果遇到"not supported in custom layer"之类的报错,先确认版本,再考虑补全反向传播。

3.3 复用原CNN-LSTM主干,只在中间做最小改造

搭建完整网络的思路是"替换",不是推翻重来。保留原来CNN-LSTM的卷积块、LSTM层和分类头,只在第二个卷积块输出和LSTM输入之间插入SE层。用layerGraph组织起来大概长这样:

lgraph = layerGraph([ sequenceInputLayer(12, 'Name', 'input') convolution1dLayer(8, 32, 'Padding', 'same', 'Name', 'conv1') batchNormalizationLayer('Name', 'bn1') reluLayer('Name', 'relu1') maxPooling1dLayer(2, 'Stride', 2, 'Name', 'pool1') convolution1dLayer(5, 64, 'Padding', 'same', 'Name', 'conv2') batchNormalizationLayer('Name', 'bn2') reluLayer('Name', 'relu2') maxPooling1dLayer(2, 'Stride', 2, 'Name', 'pool2') seLayer(64, 8, 'se') % 插入SE模块,通道数取conv2输出64 lstmLayer(64, 'OutputMode', 'last', 'Name', 'lstm') dropoutLayer(0.3, 'Name', 'drop') fullyConnectedLayer(8, 'Name', 'fc') softmaxLayer('Name', 'softmax') classificationLayer('Name', 'output') ]);

这段结构里最需要注意的就是SE层的通道数必须和它前面卷积块的输出通道数一致。我最初接错了一次,把通道数填成了输入信号的12,导致SE在做压缩时维度对不上,报错信息又是那种让人摸不着头脑的维度不匹配。后来加了一行注释,每次搭网络都先算一遍通道数再填。

3.4 训练配置与可复现设置

网络搭好后,训练配置直接影响SE模块能不能发挥效果。我用的是一套偏保守但稳定的配置:

rng(42); % 固定随机种子,保证每次实验结果可复现 options = trainingOptions('adam', ... 'InitialLearnRate', 0.001, ... 'MaxEpochs', 50, ... 'MiniBatchSize', 64, ... 'ValidationData', {XValidation, YValidation}, ... 'ValidationFrequency', 20, ... 'Shuffle', 'every-epoch', ... 'Plots', 'training-progress', ... 'Verbose', true);

固定随机种子这一步很多人会忽略。尤其是加了SE之后,模型多了两个全连接层的初始化随机性,不固定种子的话,你很难判断精度提升到底是SE的效果还是运气好。我的做法是先随机划分训练验证测试集,再把划分时的随机种子一并固定下来,这样不同模型之间的对比才有意义。

4. 训练阶段最容易翻车的几个位置:调参与实测对比

4.1 学习率、BatchSize与SE新增参数的配合

加了SE之后,最直观的变化是模型多了一组全连接层参数。虽然有缩减比控制参数量,但这一部分参数的优化节奏和主干卷积层不完全一致。我的经验是:初始学习率保持0.001,但要注意观察训练损失在前几个epoch有没有异常波动。

SE模块里的两个全连接层需要经历一个"校准期"。前几个epoch里,sigmoid输出的权重可能都集中在0.5附近,相当于SE暂时是"透明"的,对网络几乎没有影响。这是正常的。如果训练到十几个epoch之后损失还是明显高于基线模型,那就要检查是不是学习率偏大,导致SE的权重在早期就被推到饱和区,sigmoid输出接近0或1,梯度消失。

另一种有效的配合是学习率衰减。我后来把训练策略改成了余弦退火式的学习率调整,前中期保持0.001,后期降到0.0001左右。SE模块在这种学习率规划下比固定学习率更容易找到精细的通道权重。

4.2 BatchNorm与SE的先后顺序、权重初始化细节

关于BatchNorm和SE的先后顺序,我踩过一次坑。一开始我把SE插在BatchNorm之前,结果训练非常不稳定。后来回想一下,原因其实很清晰:BatchNorm的作用是让每层输入保持稳定的均值和方差,如果SE在BN之前计算通道权重,那么它看到的特征分布还在被BN不断调整,权重学起来自然不稳定。

正确的顺序应该是Conv -> BN -> ReLU -> Pool -> SE,让SE拿到的是已经归一化过的、分布相对稳定的特征。SE层内部不需要再额外加BN,两个全连接层本身已经足够轻量,再堆BN反而容易引入多余的超参数和不稳定因素。

另外,SE层的权重初始化不能随便用零初始化。如果W1和W2初始化为全零,那么中间层的输出会被置零,经过sigmoid后权重变成0.5,这倒不会让网络崩掉,但在早期梯度回传时,SE的两个全连接层几乎收不到有效梯度,网络相当于白加了一个模块。我用的是0.01 * randn这样的小随机数初始化,保证每个通道的初始权重略有差异。

4.3 三个数据集上的CNN-LSTM vs CNN-LSTM-SE实测

为了验证SE到底是不是"加了个寂寞",我在三个差异比较大的时序分类数据集上分别跑了CNN-LSTM基线和CNN-LSTM-SE两个模型。数据集信息如下:

实验数据样本量类别数通道数时间步
某旋转机械振动信号4200812200
某多通道压力时序180058150
某环境温湿度序列300036120

每个数据集上两个模型使用相同的训练验证测试划分,相同超参数配置。结果对比如下:

数据集CNN-LSTM验证准确率CNN-LSTM-SE验证准确率训练时间变化
旋转机械振动信号91.4%94.2%增加约18%
多通道压力时序87.3%91.6%增加约12%
环境温湿度序列95.1%95.9%增加约15%

从这三组数据能看出两个规律。第一,SE模块在特征复杂度高、类别区分难度大的数据集上提升更明显,第一个数据集提升了近3个百分点;第二,在原本就比较好分的简单任务上,SE带来的收益有限,只有0.8个百分点。这符合直觉:通道注意力解决的是"通道间重要性不明确"的问题,如果原始特征已经很干净,那它的发挥空间自然就小。

提示:如果加了SE之后验证集精度反而下降,不要急着调学习率。第一件事是检查sigmoid输出权重的分布。如果大量权重集中在0.5附近,说明SE还没有被有效训练起来;如果集中在0和1两端,说明模型过拟合了通道权重,可以尝试增大缩减比r,或者加大Dropout。

我还对比了参数量。以第一个数据集为例,基线CNN-LSTM总参数量大约28万,加了SE之后大约33万,多出来的正好是SE两个全连接层的参数量。这个增加幅度总体可控,换来的是接近3个百分点的精度提升,性价比是相当高的。

5. 替换式改造的迁移经验与适用边界

5.1 用最小改动替换已有基线的完整步骤

标题里提到的"替换自",其实就是这种渐进式改造。如果你手上已经有一个训练好或者正在训练的CNN-LSTM模型,想升级成带SE的版本,我的建议是不要从头开始重新训练,而是走下面这几步:

  1. 找出原网络中最后一个卷积块的输出节点,记下它的输出通道数。
  2. 在卷积块和LSTM之间插入SE层,通道数填刚才记下的数,缩减比先用8。
  3. 加载原CNN-LSTM已训练好的权重参数,把SE层作为新初始化的模块。
  4. 把初始学习率调低到1e-4,用迁移学习的方式微调整个网络。这样原网络已经学到的特征提取能力能保留下来,SE层只需要在已有特征的基础上学习通道权重,而不是从零开始重新学。

我在实践里用这种微调方式只跑了15个epoch就超过了原模型训练50个epoch的性能,效果很明显。这里的关键是把学习率压低,否则新加的SE层参数梯度较大,会把原网络参数冲得七零八落。

5.2 出现这三个信号时,SE大概率能帮上忙

结合几次实验的经验,如果你正在做的时序分类任务出现了下面三个信号中的任意两个,就可以认真考虑加SE:

  • 混淆矩阵里总有一两组类别反复互相误分,而且这两个类别在原始信号时域波形上肉眼都很难分辨。
  • 卷积层的输出通道数比较多,一般至少大于16。通道数太少时,SE重标定的空间被压缩,效果不明显。
  • 训练曲线在验证集上出现很长的平台期,模型的参数调整已经很难突破。

反过来,如果样本量非常小,比如只有三五百条,加SE就要慎重。SE新增的参数虽然不多,但在小样本场景下仍然可能加重过拟合。我自己在样本量小于500的任务上试过一次,加了SE之后验证集反而不如基线,这时候可以考虑把缩减比调大到32,同时配合更强的Dropout,或者干脆放弃SE,改用更简单的时间维注意力。

5.3 进一步扩展的方向与部署注意点

SE只是通道注意力的一种实现,在这个骨架上还能继续做文章。比如可以把SE替换成更轻量的通道注意力变体,减少一个全连接层;也可以在LSTM输出端再接一层时间步注意力,让模型在决策时不仅关注通道重要性,还能关注哪些时间步更关键。这两者叠加在复杂长序列上通常能进一步提升分类精度。

另外提醒一句部署问题。MATLAB导出模型时,自定义层不一定都能被代码生成器支持,SE层虽然在训练时表现良好,但如果你打算把模型部署到嵌入式设备,要先确认目标部署方式是否支持自定义层,或者提前把SE模块的运算手动重写成不依赖自定义层的算子组合。这一步经常被忽略,往往是训练验证都通过了,临到部署才发现模型导不出去。

最后分享一点个人体会。我在替换模型时,习惯一次只加一个改动,先把SE插进去跑通,确认提升后再考虑其他注意力变体。SE不是任何场景的万能药,但它是CNN-LSTM时序分类里成本最低、收益最稳的注意力形式之一。对于还在被基线模型平台期困扰的技术人员,值得先拿自己的数据跑一版对比实验,用二十分钟的程序改动,换一个明确的精度增量,这笔账怎么算都划算。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询