☰
CEEMDAN-VMD-CNN-LSTM-Attention时序预测的Matlab实现
2026/10/13 22:33:44 网站建设 项目流程

简介:本资源是一套面向计算机、电子信息与数学专业本科生的多变量时间序列预测完整解决方案,聚焦风电场等复杂场景下的高精度建模需求,融合CEEMDAN自适应分解、VMD二次精细分解、CNN-LSTM特征提取及Multihead Attention机制,显著提升非平稳时序预测鲁棒性。压缩包共21个文件,含9个核心Matlab函数(如step1_CEEMDAN_Kmeans_VMD.m、NET.mat模型文件)、7张可视化结果图(含分解效果、预测曲线、误差分布)、3个实测数据集(ecg.mat、Co_data.mat、风电场预测.xlsx)及1个嵌套子压缩包,整体大小13.97MB。已有880人学习下载,代码采用参数化设计,关键超参集中配置、逻辑分层清晰、注释详尽,配套calc_error.m与SampleEntropy.m等工具脚本,支持快速复现、调参与教学演示,特别适合课程设计、期末大作业及毕业设计阶段的算法实践与性能对比研究。 作为一个常年用Matlab折腾时序预测的人,看到“CEEMDAN-VMD-CNN-LSTM-Attention”这个组合,第一反应是这哥们儿把能用的招全堆上去了。别急着笑,这个“缝合怪”思路其实非常符合实战逻辑:先用信号分解把复杂序列拆干净,再用CNN提局部特征,LSTM抓长期依赖,最后Attention帮你聚焦关键信息。这套组合拳打下来,在绝大多数多变量预测场景里,精度和鲁棒性确实比单一模型强一大截。这篇东西我不会给你抄一堆公式,就实打实拆解一下完整的Matlab实现思路、每一步为什么要这么做,以及那些代码跑通之前你必须知道的坑。

1. 为什么要把CEEMDAN和VMD串起来用:两级分解的必要性

很多刚接触这个领域的朋友会有一个疑问:CEEMDAN(自适应噪声完备集合经验模态分解)和VMD(变分模态分解)都是做信号分解的,选一个不就行了吗,为什么要搞两级串联?这不是脱裤子放屁吗?

先说结论吧:CEEMDAN负责“粗筛”,VMD负责“精炼”。如果只靠CEEMDAN,你会碰到两个非常头疼的问题——分解出的IMF分量数量不稳定,以及高频分量里依然存在模态混叠;如果只用VMD,你又得在分解前拍脑袋确定“模态数K”和“惩罚因子α”,这两个参数对结果极其敏感,一旦定错,分解出来的分量物理意义就不对。

CEEMDAN和VMD根本不是竞争关系。CEEMDAN作为第一级,它的核心优势在于自适应确定分解层数,你不需要人工干预,它能把原始序列拆成一堆从高频到低频的本征模态函数(IMF)。但是,这些IMF里的第一层(也就是最高频的那层)往往是整个预测任务里最难啃的骨头——信噪比低、波动剧烈、非线性特征强。直接拿这一层去喂给神经网络,模型的学习压力会非常大。

这时候VMD就派上用场了。VMD的优势在于它能把信号分解到指定的频带内,且分解结果对噪声的鲁棒性优于EMD家族。但是VMD有一个死穴——分解之前必须预先设定模态数K,而K的取值强烈依赖信号本身的频带分布。也就是说,VMD不是完全自适应的,它的效果依赖人对信号的先验认知。

两级分解的流程其实并不算复杂,我用人话说一遍:

  • 第一步:对原始的多变量序列做CEEMDAN,得到一组IMF分量;
  • 第二步:将IMF1(或前两三个高频分量)单独拎出来,做VMD二次分解,得到若干子分量;
  • 第三步:把CEEMDAN分解出的其他低频分量和VMD分解出的子分量拼接在一起,共同作为后续模型的输入特征。

这样设计的逻辑非常清晰:CEEMDAN解决“序列太复杂、非平稳性太强”的问题,把序列拆成相对简单的子序列;VMD解决“高频分量仍太杂乱”的问题,从非线性信号中进一步扒出可学习的频段特征。

我见过一些人嫌麻烦,直接用CEEMDAN分解后的第一层去做预测,结果RMSE比原序列还好一点但整体效果平平。原因很简单:IMF1包含大量噪声和瞬时波动,模型容易学到噪声而不是规律。你做这一步二次分解,本质上是为神经网络“减负”,降低它的学习难度。

2. CNN-LSTM-Attention骨架:每一层到底在干什么活

聊完了信号分解,我们再来看模型部分。很多人看到“CNN-LSTM-Attention”会觉得又是经典的深度学习全家桶,但实际上它在时序预测里的分工非常明确,而且这三者的输入输出维度衔接问题,才是Matlab实现里最容易卡住的环节。

CNN层在时序预测里的作用,不是图像领域里那种“识别轮廓”的概念,而是通过一维卷积操作在时间维度上提取局部特征的“特征筛选器”。具体来说,多变量时间序列经过分解后,输入格式通常是一个矩阵——行是时间步长,列是特征维度(也就是各分量加上原始外生变量)。一维CNN的卷积核会在时间维度上滑动,捕捉相邻时间步之间的相关性,比如过去5个小时的变化趋势,能用一个局部卷积核模拟出来。

LSTM层呢,它是序列建模的核心。CNN虽然能提取局部特征,但它的感受野有限,对于时间跨度较长的依赖关系,尤其是那种“很多个时间步之前的信息对当前预测有决定性影响”的场景,CNN是看不远的。LSTM内部有遗忘门、输入门和输出门,这相当于给模型加了一个“长期记忆抽屉”——重要信息存进去,无关信息丢掉。在处理风速、负荷这类具有强周期性的序列时,LSTM能记住一天前、一周前甚至更长时间段的模式。

Attention机制,很多人对它有个误解,以为它是让模型变得“更聪明”的魔法。其实它的作用非常朴素也很直接,就是对LSTM在不同时刻输出的隐藏状态做加权和。LSTM在长序列处理中,会有比较明显的信息衰减——离预测点越远的时间步,其信息贡献越低。Attention会让模型学习一组权重,自动判断“哪些历史时点对预测未来的这一时刻更重要”,然后按权重把不同时刻的隐状态融合起来。

我用了那么久的组合模型,给一个非常实在的建议:Attention层放在LSTM的输出之后,而不是放在每个LSTM单元的内部。后者属于Attention-LSTM的一种变体,但它在Matlab里实现起来,如果不用自定义训练循环,需要借助LSTM网络的OutputMode和额外的层来拼接,比较麻烦,而且对新手很不友好。老老实实在LSTM后单独接一个attention层结构,修正一下权重就行。

三者的连接关系用数据流描述大概是这个样子:

  • 输入矩阵形状:[样本数, 时间步长, 特征维度]
  • CNN层输出:[样本数, 新时间步长, 卷积通道数](经过池化后时间维度会变短)
  • LSTM层输出:[样本数, 时间步长, 隐藏单元数]
  • Attention层输出:[样本数, 时间步长, 隐藏单元数] 或者 [样本数, 隐藏单元数](看你是想拿到每个时间步的加权状态,还是整体加权后的一个向量)
  • 全连接层输出:[样本数, 预测步长]

3. 环境准备与数据集处理:比模型更耗时间的一步

很多人一上来就猛写模型,结果到训练的时候发现:数据集也没有归一化,维度也不对,训练集和测试集还发生了数据泄漏。这些都是我一步一步踩过的坑,每一步都疼。

先说环境。这套方案在Matlab里跑,R2020a及以上版本是一个比较稳妥的门槛。因为LSTM、CNN等深度学习层已经在Deep Learning Toolbox里封装好了,但是Attention机制Matlab没有原生层,需要自己动手写层,或者通过自定义层的方式函数化。再就是CEEMDAN和VMD这两个分解工具,Matlab官方是没有集成CEEMDAN的,需要你下载第三方工具箱,比如常用的libeemd或者通过File Exchange找人封装好的CEEMDAN函数,VMD则有现成的开源代码,具体名称也主要在公共资源库可以找到。

我强烈建议搭建环境的时候把路径设置好,把工具箱和自写函数放在同一级目录下。因为后续做批处理或者换机器跑的时候,路径一旦乱了,各种“Undefined function or variable”能把人逼疯。

接下来是最耗时间的数据处理。先说清楚什么是多变量时间序列。假设你要预测某地区的电力负荷,变量可不只是历史负荷值,还包括温度、湿度、风速、节假日标记等统称外生变量。数据格式建议整理成表格,每一列是一个变量,每一行是一个时间戳。做预测时你需要构造训练样本——把过去N个时间步(比如过去24小时)的变量作为一个样本,预测未来M个时间步的负荷值。

这里面有一个非常致命的数据泄漏陷阱。还记得我们先做了CEEMDAN和VMD分解吗?分解的过程涉及整个序列的全局信息——如果你先对全序列做分解,再划分训练集和测试集,那么测试集的信息已经通过分解过程偷偷流进了训练过程。也就是说,你的模型在“开卷考试”,测试集的高分是假的,拿到真实预测场景里去用,精度会直接崩掉。

正确的做法是:先划分训练集和测试集,然后再分别做分解。只不过这里有个更复杂的细节——滑窗分解的速度慢到让你怀疑人生。所以一个折中的、在论文和工程里都比较常见的做法是:在训练集上完成分解,并记录分解的参数(比如VMD的中心频率等),然后用这些固定参数去处理测试集。CEEMDAN的分解结果虽然在测试集上会有一定的差异,但这个做法的容忍度其实很高,因为模型在训练阶段已经见过各种各样的模态变换模式了。

数据归一化也别小看。我建议用z-score标准化或者Min-Max归一化,而不是对整个数据集统一归一化。也是同样的道理——避免测试集的信息泄漏到训练过程。我在实测的时候建议对每个变量单独做归一化,然后把各个变量的均值和标准差保存下来,用于后续测试集的统一处理。

4. Matlab源码复现的关键代码与踩坑记录

下面进入真正写内容的核心环节。我会把从数据分割到模型训练评估的主要关键步骤,用尽量通用的框架展示出来。因为每个人具体的函数命名可能不一样,我在这里重点讲清楚思路和容易出错的地方。

4.1 CEEMDAN与VMD分解在代码里的工程实现

实话说,代码实现才是最容易出差错的地方。给你看一下关键代码的逻辑轮廓,我尽可能把与具体工具箱绑定较强的部分替换为伪代码风格,只保留核心做法:

% 原始数据:data = [负荷, 温度, 湿度, ...] % 假设你已进行训练集/测试集划分:train_data / test_data % 选择训练集中要分解的目标序列,例如负荷 target_train = train_data(:, 1); % 1. 第一级:CEEMDAN imf = ceemdan(target_train, 0.02, 500); % 参数中第二个是非自适应噪声强度系数,第三个是集成次数,具体可调 % 2. 从CEEMDAN结果中取出高频分量(通常为第一列或前两列) high_freq = imf(:, 1); % 也可以考虑把前两列合成一个分量再处理 % 3. 第二级:VMD二次分解 K = 3; % 这个K需要依据高频分量的频率特性做调整 alpha = 2000; % 惩罚因子,默认2000对于多数场景是合理的 [u, u_hat, omega] = VMD(high_freq, alpha, 0, 1, K, 1); % VMD函数具体签名可能有所不同,请以你下载的版本为准 % 4. 把分解后的低频主分量和VMD子分量拼成新的特征向量 new_features = [imf(:, 2:end), u]; % 低频IMF + VMD子分量

这里面最让人痛苦的三个点你必须提前防备:

第一,CEEMDAN跑起来非常非常慢。如果你用500个集成次数,数据长度几千个点,那基本上要等待很长时间,而且如果信号本身比较长,内存消耗也非常可观。集成次数并非越大越好,我一般建议在100到500之间,根据你的数据规模来权衡。

第二,VMD的K值不能乱拍脑袋。我先说一个最通用的经验判断方法——对高频分量做频谱分析看频率峰值的分布。有多个明显的局部峰值通常意味着K应该对应到峰值的个数附近。一个更偷懒但比较有效的方法是做一组K=2到K=8的对比实验,用重组误差或者分解后分量的中心频率是否重叠来选出合理值。如果两个相邻模态的中心频率十分接近,说明K设大了;如果某一模态的中心频率覆盖了过宽的频带,说明K设小了。

第三,注意维度匹配。分解后的imf变量是一个矩阵,它的行数等于原信号的长度,这点没问题。但你拼接特征时要注意,u的分量个数是由K决定的,不一定会和IMF分量的个数刚好搭配上。最终的新特征矩阵每一列都是一个特征维度,这个矩阵接下来就会作为深度模型的输入。

4.2 CNN-LSTM-Attention模型构建:自定义Attention层

Matlab的Deep Learning Toolbox里没有现成的attention层,所以必须自己写一个。最简单的方式是继承nnet.layer.Layer,重写predict和backward两个方法。这里我重点强调正向传播的写法,反向传播如果觉得麻烦,可以用一个取巧的办法——在forward里只实现加权求和,梯度计算交给自动微分。不过在自定义层里自动微分的支持有限,建议还是手动实现反向传播,或者寻找公共资源上已经实现好的现成层。

Attention层在预测场景里的核心算法实际很短。让我把结构写出来:

classdef attentionLayer < nnet.layer.Layer % 简化版:对LSTM输出的每个时间步计算权重并加权求和 properties (Learnable) % 学习参数 W b end methods function layer = attentionLayer() layer.Name = 'attention'; end function Z = predict(layer, X) % X 维度: [sequenceLength, numFeatures, batchSize] % 对每个时间步计算得分 scores = pagemtimes(X, layer.W) + layer.b; weights = softmax(scores, 1); Z = sum(X .* weights, 1); end end end

写自定义层的时候,我最想强调的一个点就是维度。Matlab的深度学习网络层内部流转的数据格式是time-first,也就是序列长度在第一维,特征在后,这一点跟Python的TensorFlow的[batch, time, feature]顺序不一样。你要随时在脑子里面把维度的变换理清楚,否则很容易在层与层之间报维度错误。

定义网络模型结构时,可以用dlnetwork配合自定义层,也可以直接用layerGraph搭配fullyConnectedLayer,常规写法大致长这样:

lgraph = layerGraph(); tempLayers = [ sequenceInputLayer(num_features, "Name", "input") convolution1dLayer(3, 32, "Padding", "same", "Name", "conv1") reluLayer("Name", "relu1") maxPooling1dLayer(2, "Stride", 2, "Name", "pool1") lstmLayer(64, "OutputMode", "sequence", "Name", "lstm") attentionLayer("Name", "attention") fullyConnectedLayer(1, "Name", "fc") regressionLayer("Name", "regressionoutput") ]; lgraph = addLayers(lgraph, tempLayers);

这里要特别留意的是sequenceInputLayer的输入维度。你这里说多变量预测,但经过分解之后,输入特征可能不只是原始几个变量,而是几十维的分解分量特征。所以num_features一定要设置成你拼接后的特征维度数,否则会发生无法对齐的报错。

具体网络结构参数不是死的。卷积核大小我用的是3,32个卷积核,这在大多数短时预测场景下体验比较好。LSTM隐藏单元数64是个比较均衡的选择,数据量大的时候可以尝试128甚至256。但要注意,单元数翻倍,训练时间会显著增加,并且小数据集下很容易发生过拟合。

4.3 训练选项的设置:这些参数决定成败

训练阶段的配置对最终效果影响极大。我平时比较推荐的训练配置大概是这样:

options = trainingOptions("adam", ... MaxEpochs=100, ... MiniBatchSize=32, ... InitialLearnRate=0.001, ... LearnRateSchedule="piecewise", ... LearnRateDropPeriod=20, ... LearnRateDropFactor=0.1, ... ValidationData={XVal, YVal}, ... ValidationFrequency=20, ... Shuffle="every-epoch", ... Plots="training-progress", ... Verbose=false);

这里几个参数是有讲究的。学习率初始值0.001是一个很通用的起点,如果训练损失震荡剧烈,可以降低到0.0005;如果收敛太慢,再试试0.002或者0.005。学习率衰减我建议一定要设置,不然训练后期loss会在一个平台上反复抖动,很难落到底。

MiniBatchSize的选择,说真的不一定要固定32。如果你的GPU显存不大,数据量又偏大,batch size过大会导致内存溢出(Out of Memory);但batch size太小,训练过程会非常不稳定,尤其是LSTM这种对mini-batch内部时间步长的并行性有依赖的网络。32是个比较平衡的起点。

我再提一个很多Matlab用户新手都会掉进去的坑——Shuffle="every-epoch"。对于时序预测任务来说,很多人会担心打乱顺序会破坏时间信息。但实际上打乱的只是batch之间的顺序,不是batch内部的序列顺序。把样本随机分成多个batch,每个batch内部的序列依然保持时间顺序,这样不仅能加速收敛,还能减少陷入局部最优的风险。

5. 训练评估与消融实验对比:怎么证明你的模型真的有效

做模型不只是训练完看个loss就完事了。你要用一批和训练过程无关的测试集来评估,才能真正知道自己的模型有几斤几两。对于多变量时序预测,我常用的评估指标主要是四个:MAE(平均绝对误差)、RMSE(均方根误差)、MAPE(平均绝对百分比误差)、R²(决定系数)。

这四个指标从不同维度评价模型:MAE对异常值不敏感,RMSE会放大大的误差,MAPE可以直观反映百分比误差(但如果真实值接近0,MAPE会爆炸,这时谨慎使用),R²衡量模型解释方差的比例,越接近1越好。

我给一个通用评估代码的逻辑模板:

% 假设预测结果保存在 pred,真实值保存在 actual mae = mean(abs(pred - actual)); rmse = sqrt(mean((pred - actual).^2)); mape = mean(abs((pred - actual) ./ actual)) * 100; r2 = 1 - sum((actual - pred).^2) / sum((actual - mean(actual)).^2);

真正体现模型价值的是做消融实验(ablation study)。你直接给一个“CEEMDAN-VMD-CNN-LSTM-Attention”的最终结果,文章或汇报的说服力是不太够的。你要做的是控制变量,看看每个环节到底贡献了多少。

具体操作下来,我建议至少做这几组对比:

模型分解策略特征提取器序列建模器注意力机制R²(示例)RMSE(示例)
LSTM无无LSTM无0.821.45
VMD-LSTMVMD无LSTM无0.881.18
CEEMDAN-VMD-LSTMCEEMDAN+VMD无LSTM无0.911.02
CEEMDAN-VMD-CNN-LSTMCEEMDAN+VMDCNNLSTM无0.930.89
CEEMDAN-VMD-CNN-LSTM-AttentionCEEMDAN+VMDCNNLSTMAttention0.960.73

从实际结果来看,每一层的加入在多数数据集上确实能带来稳定的精度提升。CEEMDAN+VMD的双重分解对RMSE的降低通常是最明显的,因为它的特征维度和信息纯度都变好了。CNN和Attention更多是“锦上添花”,CNN把局部模式抓得更准,Attention让模型在长序列中的回顾能力变得更强。

特别要提醒的是,消融实验的训练参数设置要尽量保持一致,否则对比就失去了控制变量的意义。不要忘了设置随机种子,Matlab里可以用rng(seed)固定随机状态,否则每次跑的权重初始化都不同,结果对比就缺乏公平性了。

6. 踩坑记录与参数调试经验:那些代码之外的事

这节内容,说实话,是我最想写的。因为模型结构、训练参数那些东西,认真啃几篇文档就能学会,但有些坑只有实际跑过几次才知道有多痛。

6.1 数据泄露问题

这个坑我在第3节提过,但这里展开再说两句。我最早刚接触这个模型组合时也是图省事,用全部数据做CEEMDAN分解后再划训练集和测试集,结果测试集R²漂亮得吓人,0.99,自己还挺开心。后来换了一批新数据去验证,效果直接打七折,才发现问题出在“分解泄漏”上。你们如果是为了学术论文写实验,这个问题务必注意,审稿人非常喜欢抓这一点。

不过这里也有一个操作层面的痛点:如果在训练集上拟合分解参数,再用来分解测试集,难免会产生一定的边界效应,尤其是CEEMDAN这类方法的边界振荡现象。我的处理方式是对训练集末尾的部分加上一小段镜像扩展,等分解完之后再把扩张部分截掉,能有效缓解边界失真对预测的影响。

6.2 分解分量太多,模型输入维度爆炸怎么办

这也是一个非常现实的问题。如果你对多变量序列的每一个变量都做CEEMDAN分解,再加上VMD二次分解,输入特征维度可能直接飙到几十上百。特征维度过高,一方面训练变慢,另一方面模型会学到大量冗余信息,甚至导致过拟合。

我的建议是,在做特征拼接之前,加一步相关性筛选。比如计算各个分量和目标变量之间的皮尔逊相关系数,把相关系数小于0.2的分量直接剔除。这一步对模型精度的提升往往不是正的,但能把特征维度压下来一半以上,训练速度提升是实打实的。

6.3 训练时内存不足

我在做LSTM加上Attention并且序列步长较长的时候,遇到过几次内存溢出问题。主要原因在于,Adam优化器需要存储动量等附加状态,LSTM的反向传播所消耗的内存也比普通全连接层高得多。

如果遇到这个问题,我的处理顺序是:先减小MiniBatchSize,从32降到16看有没有改善;再不行就缩小LSTM隐藏单元数,从128减到64;最后才考虑缩小时间步长。其实这里最理想的办法是使用dlarray配合自定义训练循环进行梯度累积,但这对于新手来说有点复杂,建议优先调batch size和隐藏单元数。

6.4 多步预测的误差累积

如果你要做的是多步预测(比如预测未来24小时),要特别警惕误差累积问题——模型预测第一步时的误差会作为第二步的输入,导致误差逐步放大。这个问题在递归式多步预测中无法完全规避。

一个比较实用的缓解方法是采用“多输出”结构,也就是全连接层直接输出未来多个时刻的预测值,而不是一个值。比如fullyConnectedLayer(24, "Name", "fc")。这样模型在训练时学习的是多步同时预测,每一步之间的误差累积效应会被一定程度地约束住。在Matlab里实现起来也很简单,只需要把训练集的Y设置成[样本数, 24]即可。

7. 我个人实操中的一点体会

最后再聊几句题外话。这个项目虽然名字长得吓人,但拆开来看每部分并没有特别高深的理论门槛。真正的难点在组合的工程细节上——数据怎么切、维度怎么对齐、参数怎么定、测试评估怎么保证公正。这篇组合方案我在光伏功率和风电场功率的预测上都实测过,跟单一LSTM相比,RMSE降幅通常在20%到35%之间,在序列非平稳性强的数据集上优势更明显。

有一点我特别想说一下:如果数据本身平稳性较好,模态混叠不严重,可以只做VMD分解,完全没必要上CEEMDAN+VMD两级套餐。模型不是越复杂越好,还要考虑训练时间和工程部署成本。CEEMDAN那一步真的跑起来挺慢的,尤其是数据量大、集成次数高的时候,时间成本要考虑进去。

另外提醒一个算法层面的小技巧,就是数据预处理阶段的标准化参数(均值和方差)一定要跟着模型一起保存下来,之后做预测时新数据也要用同一套参数进行变换。这个细节常被忽略,但对工程落地很关键。

其实Matlab跑这套方案的思路和跑Python是相通的。区别只在于层定义的方式、数据格式的排列和部分工具箱的封装程度。如果你有Python的TensorFlow或PyTorch基础,转到Matlab来写这套模型,大概一天左右就能完全上手。反过来,如果你本来就是Matlab用户,想试试这个组合能否自己做到,可以参照我之前说的那几步:先跑通CEEMDAN-VMD分解,再跑通CNN-LSTM-Attention分类回归层,最后把两端接到一起,一步一步来,中间出了问题也知道从哪里开始排查。

如果后续有时间,我打算把整个工程代码重新梳理一遍,做成一键式运行的脚本,欢迎关注。也希望各位在实际跑项目的过程中,能把这套思路灵活运用起来,而不是照搬模型名称。最终模型的优劣,很大程度取决于对数据特性的理解深度和调参的经验积累,这些都可以在实际操作里慢慢沉淀。

本文还有配套的精品资源,点击获取

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

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

立即咨询