做多变量序列分类,特别是想对比CNN、LSTM和CNN-LSTM谁更合适的时候,很多人第一反应是打开Python框架。但如果你手头的数据是表格化、硬件采集或者已有大量业务代码在Matlab里,重开一套技术栈反而麻烦。我最近正好用Matlab把三套模型在同一份多变量数据集上完整跑了一遍,从数据整理、模型搭建到结果对比,把过程中遇到的坑和关键选择都整理出来了。这篇文章适合正在做设备状态识别、信号分类、时间序列多指标分类预测的同学,尤其是不想完全脱离Matlab工作流、又想快速出一个多模型对比结论的从业者。
先说结论:Matlab的深度学习工具箱(Deep Learning Toolbox)配合自定义网络层,搭建CNN、LSTM和CNN-LSTM的复杂度远低于预期,核心难点其实大部分在数据组织和维度对齐上,而不是模型本身。下面直接进入正题。
1. 项目整体设计与三模型对比思路
1.1 这个任务到底在解决什么问题
多变量分类预测,本质上就是把一条多维时间序列映射到一个离散类别标签上。比如你采集了设备的振动、电流、温度三个通道的信号,每段信号切一条样本,经过模型后输出"正常/故障A/故障B"这样的结果。和回归预测最大的区别是输出是类别而不是数值,所以网络最后通常接softmax层,用分类交叉熵作为损失函数。
这类任务在工业场景里非常常见,关键难点有三层:第一是多个变量之间不是独立存在的,通道之间存在耦合关系;第二是故障特征经常藏在局部窗口里,比如某个瞬间的冲击信号;第三是类别之间可能存在混淆,仅仅看单一时间步的数值很难区分。三种模型解决的侧重点不同,所以放在同一份数据上对比才有意义。
1.2 三个模型的选型逻辑
为什么偏偏是这三个模型,而不是上Transformer或者GRU?这里有一个很实际的项目选型逻辑。
- CNN:擅长提取局部特征,通过卷积核在时间轴上滑动,能抓住短窗口内的模式。它的优势是训练快、参数少、不容易过拟合,缺点是对时间顺序的长程依赖建模能力弱。
- LSTM:通过门控机制记住长距离信息,适合处理"前面某段时间的状态决定了后面的类别"这类问题。缺点是训练较慢,对小样本不友好,且单独使用时局部特征捕捉能力不足。
- CNN-LSTM:先让卷积层把原始多变量信号压缩成更高层、更紧凑的特征序列,再把这些特征送入LSTM学习时间依赖。这样既减轻了LSTM的输入压力,又保留了时序建模能力,在多通道、低信噪比的数据上通常效果最好。
实际动手之前,最好先用小样本把你的数据和任务梳理清楚,否则后面调参很容易抓瞎。
1.3 为什么用Matlab做这个实验
Matlab在工程诊断、信号处理领域的使用率一直很高,硬件在环、代码生成、Simulink等生态是Python短期取代不了的。而且Deep Learning Toolbox现在已经支持sequenceInputLayer、convolution1dLayer、lstmLayer等原生的序列建模层,配置训练选项只需要一行函数式调用,比手写训练循环省事很多。
另外Matlab在数据可视化上也有天然优势:训练过程曲线可以直接交互查看,混淆矩阵有专门的chart函数,这对快速判断模型是否收敛非常实用。如果你只是想做算法对比验证、不想维护一套Python推理环境,Matlab几乎是效率最高的一条路。
2. 数据准备的完整流程与预处理细节
2.1 多变量序列的分类数据应该以什么格式组织
在Matlab的trainNetwork中,输入多变量序列有固定的组织要求:每一个观测(observation)是一个二维矩阵,尺寸是特征数×时间步数。所有观测放在一个列cell数组里。对应的标签是categorical类型向量,与观测一一对应。
这个格式特别容易搞混的是"特征数和时间步数谁在前"。先说结论:必须以特征数为行、时间步数为列。比如四个特征、长度200的序列,每个观测就是4×200的矩阵。如果你不小心用200×4,trainNetwork一般不会明确告诉你"你转置错了",而是会在某个卷积层或LSTM层报维度错误,排查起来非常难受。
2.2 用合成数据验证整个流程
为了在没有现成工业数据的情况下先验证流程,我做了一份合成数据,用来模拟三个类别的多变量时间序列。核心思路是:每个类别使用不同频率的基础信号,加上噪声和幅值偏移,通过频率特征来区分类别。代码如下:
% 生成三类多变量时间序列,每类120条样本,每条样本200个时间步,4个特征 rng(11); numFeatures = 4; timeSteps = 200; numObsPerClass = 120; numClasses = 3; XTrain = {}; YTrain = []; for c = 1:numClasses for i = 1:numObsPerClass t = (0:timeSteps-1) / timeSteps; % 每个类别基础频率不同,加入随机扰动增加难度 freq = c * 3 + randn * 0.3; signal = zeros(numFeatures, timeSteps); for f = 1:numFeatures signal(f, :) = sin(2*pi*freq*t + f) + c * 0.2 + 0.3 * randn(1, timeSteps); end XTrain{end+1, 1} = signal; YTrain(end+1, 1) = categorical(c); end end生成之后先把数据随机打乱,再按比例划分训练集和验证集:
idx = randperm(numel(YTrain)); XTrain = XTrain(idx); YTrain = YTrain(idx); numTrain = floor(0.7 * numel(YTrain)); XValidation = XTrain(numTrain+1:end); YValidation = YTrain(numTrain+1:end); XTrain = XTrain(1:numTrain); YTrain = YTrain(1:numTrain);如果换成真实数据,只要保证每个观测都是"特征数×时间步数"的矩阵,标签是分类变量或者数字,逻辑完全一致。
2.3 归一化与数据集划分的坑
多变量序列数据的归一化建议按"特征"来做。什么意思?就是四个通道各自的均值和方差分别处理,而不是把整个矩阵拉成一个向量去做min-max。
这里有一个很容易踩的坑:测试集归一化必须复用训练集的统计量。很多人在把训练集、验证集放一起做归一化后再切分,这在工程上是数据泄漏,会让验证集准确率虚高。正确做法是先切分数据集,然后在训练集上算出每个特征的均值、标准差,再把这个统计量同步应用到验证集或测试集。
另一个关键点是真实场景里类别可能不平衡,划分时最好用stratify方式按类别比例抽样,保证训练集和验证集的类别分布接近。我在数据处理阶段踩过的正是这些看似基础的坑,后来都变成了排查模型问题的前置检查项。
3. 三个分类模型的Matlab搭建与训练全流程
3.1 训练选项与通用配置
三个模型用的输入数据格式是一样的,所以训练选项也可以共用。我习惯统一设置成Adam优化器,初始学习率0.001,最大轮数60,MiniBatchSize固定32,并加上验证集监控:
options = trainingOptions('adam', ... 'MaxEpochs', 60, ... 'MiniBatchSize', 32, ... 'InitialLearnRate', 0.001, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropFactor', 0.1, ... 'LearnRateDropPeriod', 25, ... 'ValidationData', {XValidation, YValidation}, ... 'ValidationFrequency', 20, ... 'Plots', 'training-progress', ... 'Verbose', true);经验之谈:对于分类任务,学习率不要一开始就上0.01,非常容易产生震荡,特别是CNN-LSTM这种层数较深的模型。用0.001起步,等训练曲线稳定后如果收敛太慢再往上调。Matlab的training-progress图可以直接看到训练集和验证集的损失曲线,一旦验证曲线开始往上走而训练曲线还在下降,基本就是过拟合信号。
3.2 CNN模型搭建步骤
针对固定长度的多变量序列,CNN使用一维卷积层(convolution1dLayer),卷积核沿着时间轴滑动。第一层用sequenceInputLayer指定输入特征数,这个数字必须和数据样本的行数一致。
layersCNN = [ sequenceInputLayer(numFeatures) convolution1dLayer(3, 32, 'Padding', 'same') batchNormalizationLayer reluLayer maxPooling1dLayer(2, 'Stride', 2) convolution1dLayer(3, 64, 'Padding', 'same') batchNormalizationLayer reluLayer globalAveragePooling1dLayer fullyConnectedLayer(numClasses) softmaxLayer classificationLayer ];这里的结构我解释一下:第一层卷积用32个卷积核,每个卷积核宽度为3,padding设置为same,这样卷积后时间步数不变;池化层把时间步压缩一半,降低后续计算量;第二个卷积块用64个卷积核提取更深层特征。最后用globalAveragePooling1dLayer把特征图平均成一个向量,再连接全连接层。
使用这个小技巧可以减少需要逐个调整的全连接输入尺寸问题。如果你在低版本Matlab里没有这个层,就把每个池化后的时间步数与通道数手动算清楚,用flattenLayer来连接,也能达到目的。CNN的优势在这个流程中体现得很明显:从头到尾只需要关注卷积核数量和层数,不需要考虑太多时间步长对齐的问题,模型结构非常直观。
3.3 LSTM模型搭建步骤
LSTM部分比CNN还要简单,但设置上有几个细节需要注意。第一个是lstmLayer的OutputMode参数。如果后面直接接全连接分类层,必须设置OutputMode为'last',表示只把最后一个时间步的输出拿出来做分类;如果设置成'sequence',输出维度变成了序列长度×隐含单元数,再接全连接层大概率报维度错误。
第二个是dropout,我加了0.2的丢包率,对小样本来说这是性价比很高的正则化手段。代码:
layersLSTM = [ sequenceInputLayer(numFeatures) lstmLayer(128, 'OutputMode', 'last') dropoutLayer(0.2) fullyConnectedLayer(numClasses) softmaxLayer classificationLayer ];单个LSTM层在这个任务上通常能跑出不错的基线,但有个现象:它对数据归一化特别敏感。如果输入特征之间的量纲差距很大,比如电压是220、温度是80、振动是0.1,不归一化会让LSTM的收敛速度变得极慢。所以LSTM工作前,我基本会先单独检查一下训练集各通道的分布,再决定是否需要标准化。
3.4 CNN-LSTM混合模型搭建步骤
混合模型是我整个项目里最花时间调试的部分。结构上不能直接把CNN的序列输出丢给LSTM,因为经过卷积和池化之后,序列的时间长度已经变了。好在卷积层不要求输入的具体时间步数,只要池化后输出是一个特征序列,LSTM就能接受。
layersCNNLSTM = [ sequenceInputLayer(numFeatures) convolution1dLayer(3, 32, 'Padding', 'same') batchNormalizationLayer reluLayer maxPooling1dLayer(2, 'Stride', 2) convolution1dLayer(3, 64, 'Padding', 'same') batchNormalizationLayer reluLayer lstmLayer(64, 'OutputMode', 'last') dropoutLayer(0.2) fullyConnectedLayer(numClasses) softmaxLayer classificationLayer ];这里我直接把第二个卷积块后的特征序列送到LSTM,没有加globalAveragePooling,因为平均池化会把时间维压掉,LSTM就没有序列可学了。混合模型的核心思路是:CNN部分负责降维和提取局部特征,LSTM部分负责在压缩后的时间维度上学习长期关系。
实际运行中,这个模型训练时间大约是纯CNN的3-4倍,但在我测试的数据上准确率明显更高。有一点需要注意:CNN部分的池化步长不能过大,否则时间步数太少,LSTM能学到的时序信息有限。
3.5 多模型循环训练与结果保存
三个模型可以分别调用trainNetwork训练,也可以用循环统一跑,便于后续对比。我的做法是把网络层放在struct里,写成cell数组自动训练:
allLayers = {layersCNN, layersLSTM, layersCNNLSTM}; modelNames = {'CNN', 'LSTM', 'CNN-LSTM'}; trainedNets = cell(1, 3); for i = 1:3 fprintf('正在训练模型: %s\n', modelNames{i}); trainedNets{i} = trainNetwork(XTrain, YTrain, allLayers{i}, options); end训练结束后用save把网络和验证信息都保存下来,方便后面统一评估。如果不想每个模型都重新训练一遍,也可以逐个运行,反正options可以复用。我在这个环节养成了一个习惯:每个模型训练完立刻记录当时的训练时间,用tic/toc包起来,后面做选型对比时能一并分析。
4. 训练实测中的常见报错与排查记录
4.1 输入维度错误全家桶
这个环节必须单独拿出来说,因为在我反复调试中最常遇到的就是维度问题。
第一个报错是"Input data has incorrect number of dimensions"。出现这个错误,第一反应就是序列Data的格式不对。trainNetwork对sequence输入要求cell数组,每个cell是"特征数×时间步数"的矩阵。如果你传成普通三维数组,哪怕数值完全一样,也会触发这个错误。
第二个是卷积层报"Expected input to have 4 channels"之类的问题。正常情况下sequenceInputLayer第一个参数是特征数,如果你的数据是4个特征,这里必须写4。很多人误把时间步数当成了输入特征数,结果卷积层直接拒绝输入。要记住:sequenceInputLayer只关心每个观测矩阵的第一维,也就是特征数,时间步数是可以动态变化的。
第三个是LSTM层和全连接层之间的维度问题。如果lstmLayer设置了'OutputMode', 'sequence',输出形状是"隐含单元数×时间步数",你要接全连接层就必须把序列浓缩成一条向量,否则连接处报维度不匹配。最简单的做法是设置'OutputMode', 'last'。
4.2 不收敛、过拟合与类别失衡
验证集准确率始终保持在某一个常数,比如一直在66.7%(三类分布均匀时的随机水平),这种情况多半不是模型结构的问题,而是学习率设置不合理或者数据归一化没做对。优先检查三点:学习率、MiniBatchSize、是否有类别严重失衡。
类别不平衡是我真实踩过的坑。某类样本数量是另一类的三倍,模型很快就学会把所有样本都预测为多数类,整体准确率看起来还行,但少数类一个都识别不出来。这种情况下可以给分类层加ClassWeights参数:
classWeights = [1.2, 0.8, 1.0]; layersCNNLSTM = [ % 前面结构保持不变 fullyConnectedLayer(numClasses) softmaxLayer classificationLayer('ClassWeights', classWeights) ];ClassWeights会放大少数类的损失贡献,让模型更重视少数类。这是我做故障数据时学会的一个重要调整,比单纯加数据有效得多。
过拟合则表现为验证损失在下降后又反弹。解决办法按优先级排序:增大dropout比率、减少LSTM隐含单元数、降低训练轮数、加L2正则化。Matlab训练选项里有个'L2Regularization'参数,默认是1e-4,数据量少时我会调到1e-3。
4.3 训练效率问题
CNN-LSTM训练慢是一个普遍现象。除了减少LSTM隐含单元数外,可以先把MiniBatchSize加大,前提是你的GPU显存受得了。如果显存不足报OOM,不要直接换更大的机器,先检查是不是把验证集也一次性放进GPU了,可以把ValidationFrequency调大,减少验证频次,或者把输入数据改成single类型,能省不少显存。
数据读取方面,如果每次都在循环里切片cell数组,训练效率会很低。最好一次性把训练集放进变量,让trainNetwork在内部做batch划分。另外如果样本是等长的,可以先把所有样本堆叠成"特征数×时间步数×样本数"的三维数组,再用mat2cell转换,处理速度明显快过逐条赋值cell。
5. 三模型结果对比怎么看
5.1 从混淆矩阵和数据指标看差异
训练结束后,我三步走进行评估:整体准确率、混淆矩阵、逐类精确率和召回率。整体准确率容易算:
YPredCNN = classify(trainedNets{1}, XValidation); accCNN = mean(YPredCNN == YValidation);混淆矩阵用confusionchart展示最直观:
figure; confusionchart(YValidation, YPredCNN); title('CNN验证集混淆矩阵');从我这组合成数据的结果来看,趋势很有代表性:纯CNN的准确率在80%上下浮动的可能性较高,因为信号类别主要由频率区分,局部感受野已经够用,但遇到噪声较大、时域特征重叠的样本时会错分;LSTM大约能到85%以上,对时间模式更敏锐;CNN-LSTM在同样条件下往往能到90%以上,尤其对容易混淆的边界样本改善明显。实话说,这只是合成数据下的趋势,换成真实数据差别会很大,但这个对比思路是通用的。
训练时间上,LSTM是CNN的2倍左右,CNN-LSTM是CNN的3到4倍。做报告或者选型时,把训练时间、验证准确率、分类边界情况放在一张表里,比只贴一个准确率数字有说服力得多。
5.2 不同数据规模下的选型建议
如果你有几十到几百条样本,量级不大,我建议先跑CNN,原因非常简单:它最不容易过拟合,而且训练快,能快速判断你的特征工程和标签是否合理。如果CNN都跑不出一个可用的基线,说明数据本身问题大了,码LSTM和混合模型就是在错误的地基上盖楼。
如果数据量中等,且确认类别与时间序列的长程依赖强相关,LSTM开始显现优势。可以尝试把滑动窗口的长度调大,让LSTM有更多上下文可用。
如果数据量充足,而且你有GPU资源,CNN-LSTM是效果上限最高的选择。但从工程角度看,它并不是万能药:当序列很长时,LSTM部分的计算量会明显上升,需要平衡CNN的池化步长和信息保留程度。
我个人的选择策略是:先用CNN建立baseline,再用CNN-LSTM做最终模型,LSTM则作为中间验证,看时序建模对任务到底贡献了多大的收益。
5.3 从结果反推数据质量
做模型对比的一个副产品是能反推你的数据质量。如果三个模型在验证集上的准确率都很低,但训练集准确率都接近100%,那基本是数据划分和归一化泄漏的问题,或者数据本身类间重叠太严重。如果CNN和CNN-LSTM准确率接近,但LSTM明显更差,这说明可能数据的时间依赖并不强,加LSTM是在无用功。如果CNN-LSTM显著优于CNN,说明数据里确实存在需要跨时间窗口才能捕捉的特征,LSTM部分是值得保留的。
抱着这个思路去分析结果,就不会陷入一味的精度攀比,而是能从实验数据中得到对业务问题更有价值的判断。
做完全部三组模型之后,我个人最大的体会是:Matlab做这类多变量分类预测的难点从来不在调库,而在于对sequence数据格式的理解和维度流的把控。用CNN-LSTM也好、纯LSTM也好,底层逻辑都是把你手里的表格或波形整理成模型能消化的矩阵序列,再通过合适的网络结构把特征提炼出来。
最后分享一个小技巧:在搭建网络之前,先单独构造一个随机的小数据集,跑一遍完整的trainNetwork流程,只用一个batch,确认前向传播没有维度问题,再上真实数据。这个习惯帮我省下了大量调试报错的时间。三模型的对比实验做完,你得到的其实远不止一个准确率,而是一套对数据、对问题、对网络结构的判断依据。