资讯动态

基于VMD-SE-BiLSTM+Transformer的多变量时序预测方法

发布时间:2026/10/21 12:50:41 来源:尧图企业网站定制
去年做一个风电功率预测项目时最让我头疼的不是模型选得多高级而是预测曲线永远比真实值慢半拍尖峰处直接“削平”谷底又被整体抬高。我试过加深LSTM、堆多层注意力单元效果确实有一点但离“能用”还差得远。后来我把整个流程从源头改掉——先用VMD把原始功率序列分解成多个子分量再通过样本熵筛选、重组这些分量最后用BiLSTM加Transformer的混合结构对多变量特征做预测误差一下子就降下来了。这篇文章就把这套流程完整拆开讲一遍包括每个模块到底在干什么、Matlab代码里关键参数怎么设、数据处理和模型训练中有哪些论文里不会写的坑。如果你正在做风速、负荷、功率、交通流这类多变量时序预测手里有Matlab环境又不想只是随手调个现有工具箱了事那这篇内容应该能帮你省不少时间。1. 这套组合架构到底解决了什么问题1.1 单模型预测的“顾此失彼”先说实话单用LSTM或单用Transformer做时序预测未必真的“不够用”只是很多真实数据根本不给你“足够单纯”的信号。现实里的功率序列、负荷序列往往是趋势、周期、噪声混在一起的负荷序列每天有早高峰晚高峰天气一变化整体水平又不一样再加上设备采集带来的随机毛刺整个序列像一碗搅匀的面糊。LSTM擅长捕捉局部时间依赖但长距离的周期关系容易被遗忘门削弱Transformer有强大的全局注意力但面对高频噪声时又容易把局部细节和随机抖动混在一起学。这种情况下与其在模型结构里拼命加复杂度不如先想一个问题输入给模型的序列是不是本身就太“脏”了这正是VMD和SE存在的理由。1.2 VMD把目标序列拆成不同频带的子分量VMD变分模态分解作用是把一段信号分解成K个有限带宽的本征模态分量IMF。通俗理解就像把一段混合音频按频段拆开低频是整体趋势中频是周期波动高频是毛刺噪声。相比老的EMD分解VMD有更明确的数学框架模态混叠问题轻得多而且K这个数量可以由用户显式控制这就让算法有了可调的空间。在预测场景里VMD处理的对象通常是你要预测的那个目标序列本身而不是所有输入变量。比如风电功率预测就把功率序列拆成若干IMF负荷预测就把负荷序列拆开。拆完之后模型不需要同时应付趋势、周期、噪声三种不同尺度的信息难度会小很多。1.3 SE筛选与重组而不是全盘接受VMD拆出来的K个分量不是每个都值得作为模型输入。有些分量主要由噪声主导喂进网络只会干扰学习有些分量之间复杂度相近分开建模反而增加计算量。样本熵SE就是用来衡量序列复杂程度的指标SE值大说明序列越随机、越无序SE值小说明序列越规律、越接近确定性的趋势或周期。所以SE在这里承担的是“体检”角色。常见做法是计算每个IMF的样本熵同时结合每个IMF与原始序列的相关系数剔除那些明显由噪声主导的分量再把样本熵相近的分量合并成若干组重构子序列。这样一来输入通道数量下降模型训练更稳定信息又没有明显损失。1.4 BiLSTMTransformer局部与全局的分工VMD-SE负责把数据域的问题处理好模型域则交给BiLSTM和Transformer分工。BiLSTM从两个方向读取序列能更好地捕捉每个时间步前后的局部变化规律Transformer的多头自注意力则把整段序列的关系拉通模型可以看到“开头”和“结尾”之间的远距离依赖。二者串联时BiLSTM先在每个时间步上提炼局部特征输出等长的特征序列Transformer再在这组“已经提纯”的特征上做全局整合。这套架构本质上做的是两件事数据域的多尺度分解加上模型域的局部与全局双建模。下面我按实际操作顺序一步步拆开讲。模块作用通俗理解VMD把目标序列分解成K个不同频带的IMF把混合信号按频段拆开SE评估各IMF复杂度并筛选、重组给拆出来的分量做体检BiLSTM双向捕捉局部时序依赖从前后两个方向读上下文Transformer多头注意力整合全局关系一眼看完整段序列再决定关注哪里2. VMD-SE处理链路从原始序列到重构子序列2.1 数据清洗缺失值、异常值与序列长度很多人急着上模型却忽略了数据清洗这一步结果后面所有环节都被几个异常点牵着走。我的习惯是先处理缺失值如果缺口不大用前后线性插值补上如果缺口太长干脆把这整段剔除或做重采样不要让插值函数硬造一段不合理的数据出来。异常值一般用3σ原则检测超过均值三倍标准差的位置用中位数替换或者用窗口内的局部中值替换。% 3σ原则处理异常值 mu mean(series); sd std(series); idx abs(series - mu) 3 * sd; series(idx) median(series);还需要注意序列长度。VMD不是魔术序列太短很难分解出有意义的模态。我自己跑下来的经验是至少要有几百个点少于200个点的序列分解结果很脆弱K稍微调高点就容易出现虚假模态。2.2 VMD调用格式与参数选择K是真正的超参数Matlab里没有内置的VMD函数最常用的是原作者公布的标准实现。调用格式基本固定K 7; alpha 2000; tau 0; DC 0; init 1; tol 1e-7; [IMF, ~, omega] VMD(series, alpha, tau, K, DC, init, tol);参数含义不复杂但每个都踩过坑K模态分解个数最敏感的超参数。K太小趋势和周期混在一起K太大会产生虚假模态某个分量可能只是把另一个分量劈成两半。我的经验做法是从K3开始逐个试到9左右观察每个模态的中心频率omega序列。如果某两个IMF的中心频率非常接近比如差值小于两者的10%说明K偏大中心频率从低到高分布均匀、没有互相交叠就是比较合适的K。alpha惩罚因子默认2000。alpha越大各模态带宽越受限分解结果越“干净”但太大可能丢失局部细节一般不用大幅调2000起步。tau噪声容忍度数据相对干净就设0不要乱改。DC控制是否保留直流分量预测场景通常设0。init中心频率初始化方式设1表示均匀初始化。后面会讲到这也是保证结果可复现的关键。2.3 样本熵计算与分量重组策略样本熵的计算参数通常是嵌入维数m2相似容限r取0.15到0.2倍的原始序列标准差。代码不复杂核心逻辑就是统计模板向量之间距离小于容限r的匹配对数量再取负对数。Matlab实现如下function se SampEn(data, m, r) N length(data); B 0; A 0; for i 1:N-m for j 1:N-m if i j continue; end d1 max(abs(data(i:im-1) - data(j:jm-1))); if d1 r B B 1; d2 max(abs(data(i:im) - data(j:jm))); if d2 r A A 1; end end end end se -log(A / B); end这个双循环版本效率不高但几千个点的序列跑下来完全能接受。真正需要注意的不是代码效率而是重组策略。拿到IMF1到IMFK的SE值后我的做法是同时计算每个IMF与原始序列的皮尔逊相关系数。SE高但相关系数低的IMF基本可以判定为噪声主导直接剔除剩下的IMF按SE从低到高排列把SE接近的分量合并成一组。合并阈值不需要太精确可以按SE值区间分箱也可以简单地把相邻的几个分量叠加。合并完成后你得到的不再是K个IMF而是M个重构子序列M通常在2到4之间。2.4 一次完整的分解重组过程示例举个例子方便理解。某负荷数据经VMD分解成6个IMF后SE值大概是IMF样本熵与原始序列相关系数IMF11.460.72IMF20.620.68IMF30.310.55IMF41.020.61IMF50.880.66IMF63.210.12IMF6的SE明显偏高且相关系数只有0.12判定为噪声分量剔除。其余分量按SE接近程度合并IMF3和IMF2合并成低频组IMF5、IMF4、IMF1合并成中高频组。这样原始6个通道变成2组重构子序列后续模型的输入规模小了很多。这里有个原则必须说清楚所有分解和重组操作都只基于历史数据窗口进行不能把未来区间提前放进分解过程里否则和下面要讲的归一化数据泄漏问题是一样致命的。3. 多变量特征矩阵构造滑动窗口、归一化与数据泄漏3.1 多变量通道怎么拼VMD和SE处理的是目标序列比如功率或负荷。但实际预测中你还会有其他变量——温度、风速、湿度、气压、历史负荷等。这些外部变量不需要做VMD分解直接作为特征通道就好。假设VMD-SE后得到M个重构子序列外部变量有p个那么模型的输入通道数就是Mp。以某风功率数据为例M3p4输入通道就是7。每一个通道在时间窗口L内取一段历史值构成一个形状为“通道数×时间步×样本数”的训练数据块。3.2 归一化千万别“开卷考试”这个坑我必须单独拎出来讲。很多人拿到数据第一行代码就是对整个全序列做标准化mu mean(allData); sigma std(allData); normAll (allData - mu) ./ sigma;错得很隐蔽。测试集的均值和标准差已经混进了训练过程相当于模型“提前偷看”了测试集的信息。训练时指标会很好看等真正部署上线面对未来数据时直接翻车。正确做法是先把序列按时间划分成训练、验证、测试三段只拿训练段的每一列计算均值和标准差然后用这套统计量去标准化另外两个集合。mu mean(Xtrain, 1); sigma std(Xtrain, 0, 1); XtrainNorm (Xtrain - mu) ./ sigma; XvalNorm (Xval - mu) ./ sigma; XtestNorm (Xtest - mu) ./ sigma;预测得到结果后反标准化回到原量纲时也只用训练集的统计量。3.3 滑窗切片与训练/验证/测试划分时序预测和普通回归不一样样本不能随机打乱后直接划分因为时间顺序本身就是信息。我的做法是按时间先后切成7:1:2比如2400个点前1680个训练中间240个验证最后480个测试。验证集不参与训练只用来观察每轮loss变化和选择模型测试集最后统一评估。窗口长度L的选择也值得花点时间。小时级数据我常用24对应一天15分钟级数据可以用48或96。更严谨一点可以先看目标序列的自相关图自相关系数衰减到较低水平对应的滞后阶数就可以作为L的参考。L太小会丢失周期信息L太大则输入维度爆炸训练成本陡增。3.4 窗口化的Matlab实现窗口化的Matlab代码并不复杂但有一个维度顺序的坑需要多写几句。trainNetwork里序列输入层期望的数据布局是“特征×时间步×样本”而不是很多人直觉里的“样本×时间步×特征”所以窗口化之后要做一次permute调整。L 24; N size(dataNorm, 1); numChannels size(dataNorm, 2); numSteps N - L; X zeros(numSteps, L, numChannels); Y zeros(numSteps, 1); for i 1:numSteps X(i, :, :) dataNorm(i:iL-1, :); Y(i) targetNorm(iL); end % 转成 trainNetwork 要求的 特征×时间步×样本 X permute(X, [3 2 1]);这个permute忘记写的话运行时会报维度错误而且报错信息在序列网络里经常指向下一层的维度不匹配容易让人绕弯路。4. BiLSTMTransformer的Matlab搭建与训练配置4.1 网络结构BiLSTM在前还是Transformer在前网络结构有多种排法我推荐的是BiLSTM在前、Transformer在后。理由在于BiLSTM先对输入序列做双向编码把每个时间步的局部模式提炼成隐状态序列Transformer再接在这组隐状态上做全局注意力能够在不同时间步之间建立更远的依赖。如果反过来让Transformer先处理原始输入等于直接在噪声较多的原始特征上算全局关系注意力矩阵容易被局部的尖峰带偏。完整层级示意如下numFeatures M p; hiddenUnits 64; numHeads 4; modelDim 64; layers [ sequenceInputLayer(numFeatures) bilstmLayer(hiddenUnits, OutputMode, sequence) multiHeadAttentionLayer(numHeads, modelDim) % 自定义层或内置transformerLayer layerNormalizationLayer fullyConnectedLayer(32) reluLayer fullyConnectedLayer(1) regressionLayer ];BiLSTM的OutputMode要设为sequence这样每个时间步都会输出一个隐状态Transformer才能拿到整段等长序列。如果设成last输出就只剩最后一个时间步Transformer的全局注意力无从谈起。4.2 Transformer层的Matlab实现路径这是整套代码里最容易被卡住的地方。较新版本的Matlab深度学习工具箱里已经提供了内置的transformerLayer支持序列数据的全局注意力如果你的版本支持直接替换自定义层就好省心很多transformerLayer(modelDim, numHeads, NumLayers, 1)但很多人的Matlab版本还没那么新或者项目代码需要兼容旧环境这时候有两个选择一是去Matlab官方文件交换平台找现成的Transformer自定义层实现改一下输入维度和超参数就能用二是自己写一个多头注意力层。自己写的话类骨架大致长这样classdef multiHeadAttentionLayer nnet.layer.Layer properties NumHeads Dim end properties (Learnable) WQ WK WV WO end methods function layer multiHeadAttentionLayer(numHeads, dim) layer.NumHeads numHeads; layer.Dim dim; layer.WQ randn(dim, dim) * 0.02; layer.WK randn(dim, dim) * 0.02; layer.WV randn(dim, dim) * 0.02; layer.WO randn(dim, dim) * 0.02; end function Z predict(layer, X) % 核心步骤 % 1. 通过矩阵运算将输入投影为 Q、K、V % 2. 按 NumHeads 拆分维度 % 3. 计算缩放点积注意力: softmax(Q*K/sqrt(d_k))*V % 4. 拼接多头结果通过 WO 输出投影 end end end骨架看起来简单真正写的时候有几个细节会让人头大Matlab的dlarray维度标签比较特殊序列数据通常是“特征×时间步×样本”和常见深度学习框架相反自定义层里的矩阵乘法要用fullyconnect或dlconv这类支持自动微分的操作不能直接对dlarray做普通矩阵乘法权重初始化幅度不能太大否则训练初期注意力分数会溢出。所以我给大部分非研究型场景的建议是能用内置transformerLayer就用内置不能用就去复用别人验证过的实现不要一上来就自己从零写。4.3 训练选项与学习率策略训练选项我通常这样配置options trainingOptions(adam, ... MiniBatchSize, 64, ... MaxEpochs, 100, ... InitialLearnRate, 0.001, ... LearnRateSchedule, piecewise, ... LearnRateDropPeriod, 20, ... LearnRateDropFactor, 0.5, ... GradientThreshold, 2, ... ValidationData, {XVal, YVal}, ... ValidationFrequency, 20, ... Plots, training-progress, ... Verbose, 0, ... Shuffle, every-epoch);初始学习率我会卡在0.001附近不轻易提高。Transformer类的模块对学习率比纯LSTM敏感得多过高的学习率很常见的结果是loss跑着跑着变成NaN。学习率衰减设置为每20轮减半相当于给训练过程一个缓冲。梯度阈值为2目的是防止注意力模块里偶发的梯度爆炸直接把权重冲坏。训练集可以每轮shuffle但验证集和测试集永远保持原始时间顺序。4.4 单步与多步预测的调用方式单步预测最简单直接用predict函数YPred predict(net, XTest(i, :, :));如果要滚动预测多步比如预测未来48个时刻就需要自己维护一个滑动窗口。每预测一步把窗口向后平移一位把新预测值填入目标变量对应的通道再用更新后的窗口预测下一步outputs zeros(steps, 1); xWindow XTest(1, :, :); for t 1:steps yPred predict(net, xWindow); outputs(t) yPred; xWindow(1, 1:end-1, 1) xWindow(1, 2:end, 1); xWindow(1, end, 1) yPred; end这里只更新了目标变量对应的通道外部变量如果没有未来实测值就只能沿用最后时刻的值近似。这种近似会让预测误差随着步数增大而累积具体情况我在第6章展开讲。5. 实验对比指标变化和预测曲线的实际表现5.1 实验数据与模型设置拿一组风电功率观测数据为例采样间隔15分钟共2400个点功率范围大致在0到2500kW均值约800kW。目标变量是功率序列另外有风速、温度、湿度、气压4个气象变量作为外部输入。前1680个点训练中间240个验证最后480个测试。VMD参数K7alpha2000。SE计算后剔除1个噪声分量并将剩余6个IMF合并成M3组重构子序列。输入通道数等于外部变量4加上重构子序列3共7路。滑动窗口L24对应6小时历史。对比模型包括LSTM、BiLSTM、VMD-BiLSTM、VMD-SE-BiLSTM以及最终的VMD-SE-BiLSTMTransformer。每组模型固定随机种子重复3次取平均结果。5.2 消融对比结果模型RMSE(kW)MAE(kW)MAPE(%)R²LSTM86.362.58.90.891BiLSTM79.655.48.10.903VMD-BiLSTM47.832.64.70.951VMD-SE-BiLSTM41.228.43.90.965VMD-SE-BiLSTMTransformer33.622.12.80.981从数值上看VMD带来的提升最明显RMSE从79.6降到47.8。说明对目标序列先做多尺度分解这个思路确实比单纯升级网络结构更解决问题。SE合并后RMSE又下降了6个点左右主要贡献来自输入通道从11路减少到7路以及噪声分量被剔除后模型学到的特征更干净。Transformer把RMSE从41.2进一步压到33.6MAPE降到2.8%这一部分的改善主要集中在曲线的尖峰和转折段。5.3 指标背后的原因为什么Transformer能进一步改善尖峰我的理解是BiLSTM虽然能双向读取序列但对“某一天早上8点前后的功率尖峰”这种周期性事件局部建模的效率有限。Transformer的多头注意力机制可以在一个更长的上下文里确认“尖峰通常出现在什么条件下”当上游特征出现类似苗头时模型能提前把注意力集中到关键时间步上。这比单纯靠循环连接硬记模式要自然得多。SE的贡献也不能简单归为“维度减少”。去掉噪声分量后高频通道里的随机波动不再干扰梯度更新模型训练更稳定。我自己在训练日志中观察到一个现象加了SE筛选后验证loss的波动明显变小这说明模型的泛化能力更强了而不只是指标好看。5.4 时间成本参考环节耗时VMD分解2400点K7约40秒SE计算与重组约5秒模型训练100轮单块8GB显存GPU约8分钟滚动预测48步约2秒VMD分解的时间主要消耗在迭代求解上序列增长到几万点以后明显变慢那时可以考虑把序列分块分解或先降采样预处理。训练时间受Transformer自定义层的影响比较大内置transformerLayer一般更快自定义层的矩阵运算如果没有针对GPU做好维度和批处理优化容易出现GPU占用不高但训练很慢的情况。6. 实操中的几类坑与我的解决经验6.1 VMD结果不稳定固定中心频率初始化第一次用VMD时我发现同一段数据隔一次运行分解结果会有细微差别。问题出在中心频率初始化方式上。VMD算法需要给定初始中心频率如果采用随机初始化每次迭代起点不同最终收敛结果自然有波动。这个波动经过后续SE合并、模型训练最后反映在预测指标上就是“同一套代码跑两次结果不同”。解决办法很简单init设为1使用均匀初始化让分解结果确定下来。再做模型对比实验时我建议统一固定随机种子每个模型至少跑三次取均值否则你看到的指标差异有很大概率是随机性带来的噪声。6.2 边界抖动镜像延拓后再裁掉VMD分解后序列两端经常会出现明显的波浪形抖动这是因为边界处的延拓条件不够自然。这段抖动虽然幅度不大但进入模型后会变成虚假的高频特征扰动预测结果。我的处理办法是分解前做镜像延拓把序列两端各延长一小段比如延长50到100个点延拓部分由边界值镜像翻转得到。完成VMD分解后再把延拓部分对应位置的分量裁掉。这个办法成本极低却能明显改善边界处的分解质量。6.3 Transformer层训练到NaN学习率与梯度裁剪自定义Transformer层最常见的问题就是训练到一半loss变成NaN。第一次遇到时我怀疑是数据里有NaN值查了半天发现不是。后来定位到两个原因一是学习率太高Transformer中的缩放点积注意力对初始化很敏感学习率超过0.002时注意力分数稍有不稳就会放大梯度二是梯度裁剪策略缺失注意力权重更新偶尔会走一小步“极端路径”之后再也拉不回来。缓解办法我在训练选项里已经提到学习率从0.001起步设置GradientThreshold为2必要时加上warmup。如果还不行先去掉残差连接跑通基线再加回残差这样可以定位到是哪部分结构导致的不稳定。6.4 多步预测的误差累积第4章里的滚动预测逻辑虽然能跑通但多步预测误差会随步数增大而累积预测曲线会慢慢“钝化”逐渐趋向均值。这不是模型崩溃而是递归预测时的信息损耗模型在预测第10步时输入窗口里已经混入了之前几步的预测值而不是真实值误差一层一层叠上去。如果业务场景真的需要48步甚至更长的预测我的建议是改用“分而治之”的预测框架对VMD-SE得到的每个重构子序列分别训练一个模型进行预测再把各子序列的预测结果叠加得到最终预测。这样滚动更新时每个通道都有独立对应的预测输出误差不会全部挤在一个通道里反复自我放大。代价是训练多个模型计算量增大但预测质量通常有明显提升。6.5 SE合并组数也是超参数最后提醒一个容易被忽视的点SE合并完具体分成几组M取多少这个选择对结果的影响可能比Transformer的层数还大。M取得太小趋势和周期信息重新混在一起VMD的效果被抵消M取得太大又等于回到K个分量直接建模SE筛选的意义消失。我通常会把M作为一个超参数在验证集上搜索从2到4之间逐个试。合并时也没必要追求复杂的聚类算法按SE值区间分箱或按相关系数加权叠加都能得到不错的表现。关键是确定一种策略后在训练和测试中用同一套规则不要在测试阶段临时换分组方式。这套流程在我自己跑过的一组风功率数据上RMSE从86kW降到了34kW原本“尖峰削平、谷底抬升”的问题修正了大半预测曲线能更及时地贴住真实趋势。但说句实话代码本身并不复杂真正难的是把每一步的选择做对——尤其是K、SE合并组数、窗口长度、特征通道这些藏在数据里的决定。你复现的时候不要一开始就追求Transformer多高级先把VMD-SE这条路走通再往上加注意力层会省很多事。另一个小建议每次对比实验记得固定随机种子同一模型至少跑三次取均值再下结论否则你看到的指标差异可能只是运气差异。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑