☰
多变量时序分类实战:LSTM/CNN/CNN-LSTM在Matlab中的实现与对比
2026/10/21 6:33:30 网站建设 项目流程

我最近刚在一个多变量时序分类项目里,把LSTM、CNN、CNN-LSTM三种模型在Matlab中完整跑了一遍对比实验。项目背景是某工业设备的运行状态分类,传感器采集了温度、压力、振动、电流四个维度的时序数据,目标是从连续采样窗口中识别出设备的五种工作状态。先说结论:三种模型各有适用场景,单纯LSTM在长时间依赖上有优势,CNN在局部特征提取上更快,CNN-LSTM则在分类精度和鲁棒性上综合表现最好,但训练耗时最明显。这篇就围绕这个项目,把数据组织、模型搭建、参数计算、训练调优和踩坑记录全部展开,给正在做类似任务的你一份可以直接参考的Matlab实现方案。

如果你手头的数据也是多变量、分窗口、做分类标签,这篇文章能帮你省掉不少试错时间。文章不会堆理论,重点放在可复现的代码结构、维度推导和那些文档里不会明说的坑。

1. 项目背景与整体设计思路

1.1 为什么需要多变量分类预测

很多入门教程都喜欢用单变量序列做预测,比如根据历史股价预测未来股价,根据历史流量预测未来流量。但实际工程里几乎不会遇到这么干净的数据。拿我这次的项目来说,设备的状态不能只看温度,也不能只看振动,温度和振动在正常工况下可能都有波动,但联合起来看才能区分是轴承磨损、负载异常还是散热劣化。这就是多变量分类预测的典型场景:输入是一个时间窗口内多个通道的观测值,输出是一个离散的类别标签。

多变量分类预测的难点在于特征之间的耦合关系。四个传感器通道之间可能存在相位差、量级差甚至互相关,单纯把每个通道单独建模再投票,往往会丢失交互信息。神经网络的优势就在于它能在训练过程中自动学习通道组合特征,不需要人工设计交叉特征。

1.2 三个模型选型逻辑

选LSTM、CNN、CNN-LSTM这三个模型,不是因为它们最新,而是因为它们在多变量时序分类任务里代表三条经典技术路线,对比起来最有参考价值。

LSTM解决的是长期记忆问题。它通过门控机制保留长时间跨度的信息,适合捕捉序列里的时序依赖,比如某类故障在发生前十几秒就有微弱的前兆特征。缺点是训练慢,参数多,对局部突变特征不敏感。

CNN在时序任务里做的是"滑动窗口扫描",卷积核在时间维度上滑动,提取局部短期的模式,比如振动信号的冲击尖峰、电流波形的畸变。CNN参数共享、计算效率高,但对长距离依赖建模能力弱。

CNN-LSTM就是把两者串联:CNN先做局部特征提取,把原始多变量序列压缩成更抽象的特征序列,再把特征序列交给LSTM建模时序关系。这相当于先帮LSTM把输入里的噪声滤掉、把重点区域标记出来,效果通常比单独用其中一种更好。

1.3 整体流程规划

整个项目我拆成了四个阶段:数据准备与切窗、模型搭建、训练优化、评估对比。数据准备阶段要把原始连续采集的数据切成固定长度的窗口,每个窗口对应一个标签;模型搭建阶段分别定义LSTM、CNN、CNN-LSTM三种网络结构;训练阶段重点处理学习率、批次大小和防止过拟合;评估阶段用混淆矩阵和各类别F1分数去对比,而不是只盯着整体准确率。

这里要特别强调一个容易被忽略的问题:数据的窗口切分方式。窗口太短,信息不足;窗口太长,样本数变少,而且可能跨越状态切换点。我在项目中通过滑动窗口方式切数据,步长小于窗口长度,这样样本量可以扩大,同时窗口之间保持部分重叠。

2. 数据准备与预处理细节

2.1 多变量数据组织方式

多变量时序数据在Matlab中通常组织成元胞数组(cell array),每个元胞存放一个样本的特征矩阵。特征矩阵的尺寸是特征维度×时间步长,注意这个顺序和Python里常见的"时间步×特征维度"是相反的。很多人第一次写Matlab的LSTM代码就在这里踩坑。

举个例子,如果每个样本窗口是4个通道、200个时间步,那么在Matlab中每个样本就是一个4×200的矩阵。假设总共有1500个样本,那么训练数据就是一个1×1500的元胞数组,每个元胞里装一个4×200的双精度矩阵。标签则是一个1500×1的分类向量,可以用categorical类型,也可以用整数向量。

我建议在数据加载后先做一个维度检查脚本,打印出每个样本矩阵的尺寸是否正确。我在项目中就遇到过部分样本因为采集断点导致通道数不一致的问题,如果没有这一步检查,会在训练时报诡异的维度错误。

2.2 归一化与划分策略

多变量数据最大的坑是通道之间的量纲差异。温度可能在20到80之间,振动加速度可能在0.01到5之间,电流可能在0到20之间。如果不做归一化,梯度更新会被量级大的通道主导,网络几乎学不到小量级通道上的模式。

归一化我用的是Z-score标准化,也就是每个通道减去均值除以标准差。注意一个关键问题:标准化参数必须在训练集上计算,然后用同样的参数去处理验证集和测试集,不能单独对每个数据集做标准化。很多基础教程为了让代码看起来简洁,会对整个数据集做一次性归一化,这在时序预测里属于数据泄漏,会让评估结果虚高。

数据集划分上我没有用随机划分,而是按时间顺序划分。先用前60%的窗口做训练,中间20%做验证,最后20%做测试。这是因为时序数据存在自相关性,随机打乱会让相邻窗口同时出现在训练集和测试集里,模型相当于见过答案再考试。

2.3 标签处理与格式转换

标签必须转成categorical类型才能用于trainNetwork。转换方法是categorical(classID),其中classID是整数标签向量。此外还要确认类别顺序,建议通过categories函数查看,避免后续混淆矩阵画出来顺序和预期不一致。

另一个细节是类别不平衡。设备正常状态的数据量往往远大于故障状态,直接在训练时用原始分布会导致模型把所有样本都预测成多数类。我在项目里采用了加权交叉熵损失,通过classWeights选项给少数类更高的权重。具体做法是统计每个类别的样本数,然后让权重与样本数成反比并归一化。

3. 三个模型的核心原理与Matlab架构搭建

3.1 LSTM分类网络

LSTM的基础组件是lstmLayer,在Matlab中一条命令就能加一层。但"能用"和"用得好"之间差距很大。我在项目里测试了单层和多层LSTM,单层128个隐含单元在训练集上就已经能有不错表现,但验证集表现略差;加了第二层LSTM后验证准确率有所提升,但训练时间几乎翻倍。

LSTM层的几个核心参数需要注意。

  • numHiddenUnits:隐含单元数量,我建议先从64或128开始,再逐步增加,不要一次给到512,否则训得很慢还容易过拟合。
  • 'OutputMode':分类任务用'last',只取最后时间步的输出;回归或序列标注用'sequence'。这里踩过坑,默认是'sequence',如果忘改,后面接fullyConnectedLayer时维度会算不对。
  • 'Dropout':可以在LSTM层之后加一个dropoutLayer来抑制过拟合,很多老教程没有这个建议,但现代网络结构里dropout几乎是标配。

我的基准LSTM结构是:sequenceInputLayer(4) → lstmLayer(128, 'OutputMode', 'last') → dropoutLayer(0.3) → fullyConnectedLayer(5) → softmaxLayer → classificationLayer。

3.2 CNN分类网络

CNN在时序分类中通常是一维卷积,Matlab对应convolution1dLayer。卷积核在时间步方向上滑动,对每个通道做特征提取。第一层卷积的输入通道数必须等于输入特征维度,也就是传感器通道数。

我在项目中试过两种CNN结构。一种用两个卷积层加两个池化层,卷积核大小分别是5和3,滤波器的数量分别是16和32。另一种是单层卷积直接接全连接,效果差不少。多层级联的优势在于低层卷积提取局部细节,高层卷积组合出更复杂的模式,这和图像分类里从边缘到纹理再到形状的特征层级是类似的。

CNN结构里要注意池化窗口大小。maxPooling1dLayer的PoolSize设太大会把时间步压得太狠,丢失短时特征;设太小则参数冗余,训练变慢。我用的窗口大小是2,也就是每两个时间步保留一个最大值,这样既能下采样一半维度,又不会过度丢失信息。

3.3 CNN-LSTM混合网络

CNN-LSTM的结构是先把原始序列输入CNN部分,经过卷积、池化、归一化和激活后,得到一个压缩后的特征序列,再送入LSTM层。这里有一个容易搞混的地方:CNN输出的特征序列长度变了,但在送入LSTM前不需要展平,要保持序列维度。

我的实现结构是sequenceInputLayer(4) → convolution1dLayer(5, 16) → batchNormalizationLayer → reluLayer → maxPooling1dLayer(2) → convolution1dLayer(3, 32) → batchNormalizationLayer → reluLayer → maxPooling1dLayer(2) → lstmLayer(64, 'OutputMode', 'last') → dropoutLayer(0.3) → fullyConnectedLayer(5) → softmaxLayer → classificationLayer。

CNN部分在这里扮演的是一个自动特征工程器的角色。原始输入是4×200,经过两次池化后时间步从200降到50,每个时间步的特征从4维升到32维。LSTM再去处理这个50步长的32维特征序列,计算负担比直接处理200步长的4维序列更大,但信息密度更高,效果也更好。

3.4 训练选项设置要点

Matlab中训练选项用trainingOptions函数设置。我初始设置是solver为'adam',MaxEpochs为30,MiniBatchSize为32,InitialLearnRate为0.001。这些参数不是随手填的,需要和模型复杂度匹配。

LSTM参数多,学习率太大容易震荡,太小则收敛慢。CNN参数相对少,可以适当加大学习率到0.005。CNN-LSTM是两者串联,学习率我一般取0.001作起点,如果损失曲线下降太慢再降到0.0005。

验证频率ValidationFrequency也要注意。默认是每50次迭代验证一次,但如果数据量小,可能整个训练过程只有几十次迭代,这样永远触发不了验证。我建议设置为总迭代次数的十分之一左右,或者直接用'ValidationPatience'来控制早停。

防止过拟合的一个实用技巧是开启'Shuffle',默认是'once',也就是每个epoch开始时打乱数据一次。这个默认设置其实已经够用,真正要注意的是不要在每个mini-batch内部打乱,那会导致相邻时间窗口样本乱了套。

4. 关键代码实现与参数计算演示

4.1 数据加载与序列格式化代码

以下是我实际用的数据预处理核心代码思路。这里用虚构的"某设备状态数据集"命名,数据结构是:原始数据矩阵data,每行是一个时间点,每列是一个传感器通道,对应标签向量labels。

% 假设data大小为 N*4,labels 为 N*1 整数向量 % 窗口长度 windowLen,步长 step samples = {}; classID = []; idx = 1; while idx + windowLen - 1 <= size(data, 1) seg = data(idx:idx+windowLen-1, :)'; % 转置成 4*windowLen samples{end+1, 1} = seg; classID(end+1, 1) = labels(idx + windowLen - 1, 1); idx = idx + step; end classID = categorical(classID);

这里我特意把标签取的是窗口最后一个时间点的类别,而不是窗口中间或开头。原因在于设备状态变化通常有一个过程,窗口末尾的标签更接近当前采集时刻的真实状态。如果你的数据里状态切换点有明确标注,可以按标注来切,但我的经验是用末尾标签更稳定。

4.2 模型定义代码对比

三个模型定义代码放在一起看最清晰。

% LSTM 网络 layersLSTM = [ sequenceInputLayer(4) lstmLayer(128, 'OutputMode', 'last') dropoutLayer(0.3) fullyConnectedLayer(5) softmaxLayer classificationLayer ]; % CNN 网络 layersCNN = [ sequenceInputLayer(4) convolution1dLayer(5, 16, 'Padding', 'same') batchNormalizationLayer reluLayer maxPooling1dLayer(2, 'Stride', 2) convolution1dLayer(3, 32, 'Padding', 'same') batchNormalizationLayer reluLayer maxPooling1dLayer(2, 'Stride', 2) fullyConnectedLayer(5) softmaxLayer classificationLayer ]; % CNN-LSTM 网络 layersMixed = [ sequenceInputLayer(4) convolution1dLayer(5, 16, 'Padding', 'same') batchNormalizationLayer reluLayer maxPooling1dLayer(2, 'Stride', 2) convolution1dLayer(3, 32, 'Padding', 'same') batchNormalizationLayer reluLayer maxPooling1dLayer(2, 'Stride', 2) lstmLayer(64, 'OutputMode', 'last') dropoutLayer(0.3) fullyConnectedLayer(5) softmaxLayer classificationLayer ];

CNN里的'Padding', 'same'很重要,它保证卷积输出和输入时间步一致,避免尺寸漂移。没有这个设置,每过一层卷积时间步就变小,到后面维度对不上会报错。

4.3 训练与评估代码

训练和评估过程如下。

options = trainingOptions('adam', ... 'MaxEpochs', 30, ... 'MiniBatchSize', 32, ... 'InitialLearnRate', 0.001, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropFactor', 0.3, ... 'LearnRateDropPeriod', 10, ... 'ValidationData', {XVal, YVal}, ... 'ValidationFrequency', 30, ... 'Shuffle', 'every-epoch', ... 'Verbose', true, ... 'Plots', 'training-progress'); net = trainNetwork(XTrain, YTrain, layersMixed, options); % 测试集预测 YPred = classify(net, XTest); accuracy = mean(YPred == YTest); C = confusionchart(YTest, YPred);

训练过程中我会打开'Plots', 'training-progress',实时看训练集和验证集的准确率曲线。如果训练集准确率先到90%而验证集还在70%附近晃,说明过拟合开始出现,这时候要优先调整dropout和正则化,而不是盲目增加层数。

4.4 特征维度与层参数的计算思路

维度计算是新手最容易懵的地方,我直接讲一个计算实例。假设输入窗口是4通道×200时间步,进CNN-LSTM模型:

第一步,convolution1dLayer(5, 16, 'Padding', 'same')输出16×200,因为padding保持长度不变但特征通道数从4变成16。第二步,maxPooling1dLayer(2)之后变成16×100。第三步,第二个卷积同理变32×100,池化后32×50。此时LSTM的输入是32维特征序列,序列长度50。所以lstmLayer的numHiddenUnits和输入维度没有直接关系,它表示隐含状态向量维度,填64或128都行。关键是不要在这里试图去匹配32这个数。

全连接层输出必须是类别数5,不能多不能少。很多时候维度报错都出在LSTM层OutputMode没设成'last',导致LSTM输出的是一个序列而不是单个向量,全连接层收到一个三维张量后直接抛错。这个错误信息很隐蔽,但排查一下就明白。

对于CNN模型,池化后的时间步是50,特征维度是32,flatten后送到全连接层的特征总数是32×50=1600。如果池化层设置不同,这个数要重新算。我建议在定义完网络后,用analyzeNetwork函数检查每一层的输出尺寸,这个可视化工具能帮你提前发现问题。

5. 三模型对比实验与调参经验

5.1 基准实验设置

为了公平对比,三个模型用了相同的训练集、验证集、测试集,相同的数据预处理,相同的损失函数。训练选项里除了学习率和模型本身不同,其余参数保持一致。这里有一个容易忽略的公平性问题:LSTM层数不同导致参数量差异巨大。为了减小这种偏差,我尽量把三个模型的参数量控制在同一量级,LSTM用128个隐含单元,CNN-LSTM里的LSTM用64个隐含单元,而CNN部分负责主要特征提取。

训练环境我用的是单张中端GPU,显存有限,所以MiniBatchSize从32起调。如果显存不够,优先减小批次大小而不是减小序列长度,因为序列长度直接影响模型感受野。

5.2 实验结果对比分析

跑完三组实验后,结果比较有意思。LSTM单独用在验证集上大概能达到82%的准确率,CNN单独用反而略高,能到85%。这个结果和很多人的直觉不一样,因为我这个数据的特点是局部脉冲特征特别明显,比如振动冲击和电流畸变,这些恰好是CNN的强项。LSTM的优势更多体现在跨周期趋势上,但这个数据里趋势特征并不突出。

CNN-LSTM的验证准确率达到了91%,比两个单模型都高。这不是因为我调参调得更好,而是因为CNN先做了局部特征提取后,LSTM处理的特征序列更干净,避免了原始序列里大量噪声对长期依赖学习的干扰。但代价是训练时间大约是纯LSTM的1.7倍,纯CNN的2.3倍。

测试集上的混淆矩阵暴露出一个典型问题:正常状态和轻微负载异常之间容易混淆,因为两者在全采样窗口上的统计特征差异不大,只有局部细节不同。CNN-LSTM在这个类别对上错分样本数最少,正说明CNN部分确实提取到了判别的细节特征。

5.3 超参数调整优先级

三模型对比实验后的调参,我总结出一个优先级排序。

第一优先级是学习率。先用一个稍小的学习率跑通网络,观察训练损失是否能下降。如果损失纹丝不动,不是学习率太大而是太小,或者网络结构搭错了。第二优先级是批次大小,它影响梯度估计的噪声水平。第三优先级是隐含单元数和卷积核数量。第四才是dropout比例这类正则化项。

具体到我这个项目,LSTM模型的InitialLearnRate从0.01改成0.001之后,验证准确率直接提升了4个百分点。CNN模型的卷积核数量从32改成64后提升不大,反而增加训练时间,说明16到32就已经足够。CNN-LSTM模型则是把dropout从0.2加到0.3后,验证集和测试集之间的差距缩小了,这就是过拟合在缓解的一手信号。

另外我要提醒一个搜索陷阱:不要一上来就做网格搜索。先固定一批"大概率可行"的参数跑通流程,然后每次只改一个参数做对比实验,记录结果。这样做出来的结论才可信,而且你能真正理解每个参数的影响。

6. 常见问题与排查技巧

6.1 训练集准确率高但测试集差

这是我在项目里遇到最频繁的问题。一开始LSTM模型的训练准确率达到96%,测试准确率只有73%。排查思路是这样的:先看验证集准确率是否随着训练上升后开始下降,如果验证集曲线出现先升后降的拐点,说明过拟合。其次检查数据划分是否随机打乱了,如果是随机划分,相邻时间窗口可能跨集合,评估结果虚高。

解决过拟合我按顺序尝试了三个方法。第一是在网络里加dropoutLayer,LSTM之后通常加0.3到0.5。第二是把LSTM的隐含单元数减小,从256降到128降到了96。第三是调整数据切窗步长,增大步长能减少重叠样本,降低相邻窗口的相似度。三步下来测试准确率从73%提到了82%。

6.2 维度不匹配报错

Matlab的报错信息有时很长,但核心原因往往就是维度对不上。我遇到过一种典型场景:序列长度是200,卷积核大小是5,池化后是100,但如果卷积层忘了设置'Padding', 'same',长度会变成196,再池化变98,第二个卷积层用完变94,池化变47,整个链路的维度完全乱套。

排查时用analyzeNetwork查看每层输出尺寸是最快的方式。如果发现某一层输出维度异常,优先检查卷积层Padding、池化层Stride、LSTM层OutputMode这三个位置。这三个位置我几乎每次搭新结构都会至少踩中一次,现在已经成为固定检查项。

6.3 GPU内存不足

训练CNN-LSTM时我遇到过out of memory。模型本身不算大,但MiniBatchSize设成了128,一次要往显存里放128个4×200的矩阵,加上计算图中的中间变量,显存很快被占满。解决方法是把MiniBatchSize降到64甚至32,同时减少验证集样本数,验证时也占用显存。

另一个技巧是在训练前清空不必要的变量,比如原始数据的大数组。如果数据不是特别大,也可以考虑用'ExecutionEnvironment', 'auto'让Matlab自动选择CPU或GPU,数据小的时候CPU反而更快,因为数据在CPU和GPU之间的拷贝时间可能比计算时间还长。

6.4 训练不收敛

训练不收敛通常表现为损失曲线一直在高位震荡或者干脆变成NaN。我在项目里让CNN-LSTM第一次训练时损失直接变成NaN,原因是我把学习率设成了0.05,对LSTM部分来说太大了。改成0.001之后问题消失。

如果损失变成NaN但不是学习率问题,就要检查输入数据是否有NaN或Inf。时序数据在采集过程中经常出现短暂掉线导致的空值,我写了一个预处理函数,先用fillmissing按通道填充,再用标准差的3倍阈值检测异常值并剔除对应窗口。这个步骤帮我避免了很多不明所以的训练崩溃。

还有一类问题是类别标签不连续,比如标签是1、2、3、5,缺了4,categorical会自动把不存在的类别忽略掉,但全连接层输出节点数和真实类别数对不上就会报错。稳妥做法是在转categorical前先用unique查看所有类别。

重要提示:网络结构改完以后,第一步不是训练,而是分别用analyzeNetwork和summary检查各层尺寸和参数量。这个习惯帮我省下了至少3次无意义的训练等待。

6.5 系列备忘小技巧

除了上述大问题,还有几个小技巧值得分享。第一,训练过程中定期存checkpoint,可以用trainingOptions里的'CheckpointPath'设置,这样哪怕训练中途宕机,也不至于前功尽弃。我在训练CNN-LSTM时设置每200次迭代保存一次,最终用了5个检查点才找到最优模型。

第二,做对比实验时最好固定随机种子,用rng(0)这样的命令。不然每次跑出来的模型都是随机初始化的,三模型精度差异的前后对比会变得不可信。我一开始没加随机种子,LSTM跑了两遍,验证集结果差了3个百分点,差点得出错误结论。

第三,如果结果特别好或者特别差,先怀疑代码bug再怀疑模型能力。我在实验中发现CNN准确率比LSTM高很多,第一反应是代码写错了,检查后发现是LSTM的OutputMode忘了改,导致它输出序列而不是分类结果,准确率被低估。输出层一个参数改回来之后,LSTM恢复到合理水平。

最后再分享一个后续可能用到的扩展方向。这次的项目数据是单一工况下采集的,如果换工况、换设备,模型泛化能力可能还会下降。后续可以考虑加入域自适应方法,或者在数据层面做更多的数据增强,比如时间扭曲和通道遮罩,这两种增强方式在时序分类任务里被验证能显著提升鲁棒性。根据我个人的经验,CNN-LSTM这种结构还有一个被很多人忽略的扩展用法:先冻结CNN部分的参数,只微调LSTM和全连接层,这样换到新的数据集时收敛速度会快很多,尤其在目标数据规模不大的时候。你可以把这个思路保留到实际项目中试试看。

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

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

立即咨询