
简介基于VMD-Attention-LSTM的时间序列预测模型完整项目包面向深度学习初学者及需要完成课程设计、毕业设计的学生。内含VMD变分模态分解、Attention注意力机制与双层LSTM网络的完整搭建代码以及数据预处理、训练、预测和模型权重保存逻辑并配套项目说明文档与训练报告可帮助快速理解时序预测建模全流程。包体共28个文件以Python源码.py、Excel原始数据.xlsx、NumPy中间特征.npy、TensorFlow模型权重.ckpt及Word报告.docx为主压缩包仅5.26MB结构清晰便于直接运行与二次开发。已有635人学习下载。资源重点演示了“用前30天数据预测第31天”的滑动窗口思路并针对小数据集给出防止过拟合的设计方案如使用Huber损失、Dropout层及五个VMD分解特征输入等。同时提供测试完成的训练与预测脚本适合在现有代码基础上修改特征或网络结构作为毕设、课设或项目初期演示的起点。1. 先用问题把 VMD-Attention-LSTM 这个组合讲清楚时间序列预测最让人头疼的不是模型不够深而是同一个序列里混着好几种不同性质的波动日周期、周周期、缓慢漂移的趋势、随机毛刺。LSTM 的记忆单元虽然有门控但要它在同一组参数里同时拟合这些不同尺度的成分结果往往是“趋势跟上了、细节糊了”。VMD 先把序列按频率拆开让每个子序列相对平稳Attention 再告诉 LSTM 在预测当前值时输入窗口里哪几个历史时刻最重要最后把各模态的预测叠加回去得到完整预测。这套 VMD-Attention-LSTM 的 python 源码组合本质上是把“信号分解”和“序列建模”两件事拆开做每个环节都更容易调试。适合已经用 LSTM 做过基础预测、但卡在精度上的人也适合想把论文方法改为工程实现的读者。我会讲清三个组件各自的边界给出可运行的源码和参数调整思路。2. 三个组件各自解决什么问题为什么偏偏是它们2.1 VMD 不是过滤器是把序列拆成带物理含义的模态VMD 的基本假设是一段时间序列可以写成 K 个调幅-调频信号的叠加每个信号有独立的中心频率和有限带宽。与 EMD 靠极值点包络逐步筛选不同VMD 把分解写成约束变分问题用交替方向乘子法迭代求解把每个模态约束在中心频率附近的窄带内。这样做的直接好处是模态之间不会像 EMD 那样轻易混叠中心频率分离得清楚每个 IMF 的物理含义更明确。为什么先分解再预测会提升 LSTM 的效果因为 LSTM 对“多尺度并存”的输入并不擅长。给模型一段既包含小时级波动、又包含天级趋势的序列它的门控单元需要同时追踪两种不同的时间常数梯度回传时相互干扰最后学到的往往是占能量最大的那个尺度。把信号拆成低频趋势、周期项和随机余项后每个模态内部相对平稳LSTM 只需专注单一时空尺度训练难度明显下降。需要澄清一个常见的误解VMD 不是低通滤波器。低通滤波只留下低频成分高频信息会被直接删除VMD 会把高频分量也作为独立的模态输出仍可对其单独建模。真正的问题出在高频模态上——它的信噪比很低模型很容易把噪声的形状当成规律。我一般会把能量占比很小的几个高频分量合并成一个残余项不单独预测这样能减少整体模型的过拟合风险。VMD 的分解质量高度依赖参数尤其是模态数 K 和惩罚因子 alpha这两个参数在第四章展开。这里先记住一个原则模态应该是可解释的而不是数学上漂亮。如果分解出的某个模态中心频率跳动频繁说明 K 设多了或 alpha 设小了。2.2 Attention 在 LSTM 里到底加在哪一层LSTM 最后一个时间步的隐状态只代表序列末尾的信息。对时间序列预测而言目标值往往只与输入窗口中的某几个历史时刻强相关——比如预测早高峰车流量时前一天相同时段的数据比紧接着的前一个时间点更有参考价值。普通 LSTM 只能隐式地记住这些相关性信息经过多步传递后会被稀释。Attention 的作用是把这种相关性显式化。常见做法是在 LSTM 的输出层之后加一个注意力层计算每个时间步隐状态的得分softmax 归一化成权重再对全部隐状态做加权求和得到一个上下文向量最后接全连接层输出预测值。等价的公式是 context sum(softmax(score(h_t)) * h_t)其中 score 通常是一个单层全连接网络。另一种做法是把 Attention 放到 decoder 端用于 encoder-decoder 架构。但需要注意对单变量、单步预测来说decoder 端没有额外的语义向量作为 query注意力分数只能基于 decoder 自己的隐状态计算信息增益有限。我一般建议在 encoder 输出端加注意力也就是把 LSTM 输出序列整体压缩成一个带权重的上下文向量这样实现更直接反向传播也更稳定。Attention 层的位置和维度同样影响训练行为。如果加在每层 LSTM 之后而不是只在最后一层参数数量会明显上升小数据下更容易过拟合。先从最后一层加起验证有效后再考虑多层注意力这是更稳妥的工程路径。2.3 数据泄露是这类模型最容易翻车的地方组合模型比单一 LSTM 多了一个分解环节也就多了一个数据泄露入口。最常见的问题是先把整段序列做 VMD 分解再切训练集和测试集。VMD 在求解变分问题时用到的是整段信号的统计信息包括未来段的频谱特征这相当于让模型在训练时就知道了测试段的模态分布。这样得到的测试集指标会异常好看但上线后立刻原形毕露。正确的分解时机是“先切分后分解”。具体做法是只在训练集上做 VMD测试集的模态用训练好的分解参数近似计算。第二个常见问题是归一化。归一化用的均值和标准差必须只从训练集计算否则测试集的数值范围会泄漏到训练过程中。同样如果用 sklearn 的 StandardScaler务必先 fit 训练集再用同一个 scaler transform 测试集。第三个容易忽略的点是模态叠加的顺序。单独预测每个模态后要先反归一化到原始量纲再求和。如果顺序反了先求和再反归一化由于每个模态的方差不同反归一化会叠加错误。这三个坑都发生在数据流转的边界和模型结构无关但影响比调参大得多。提示先切分后分解再归一化这个顺序能排除 90% 的数据泄露问题。3. 搭建 VMD-Attention-LSTM 的最小 python 源码3.1 环境与数据准备这一节给出一个能直接运行的最小骨架。依赖库包括 vmdpyVMD 的 python 实现、PyTorch、numpy 和 pandas。安装命令是 pip install vmdpy torch numpy pandas。数据只需要一列按时间升序的数值比如电力负荷、水位或交易量。先读取数据并把原始序列转成浮点数组。import numpy as np import pandas as pd from vmdpy import VMD import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset # 假设 CSV 里有 date 和 value 两列 df pd.read_csv(series.csv, parse_dates[date], index_coldate) values df[value].to_numpy(dtypenp.float32) # 只用前 80% 计算均值方差避免测试集信息泄漏 train_len int(len(values) * 0.8) mu values[:train_len].mean() sigma values[:train_len].std() normalized (values - mu) / sigma这段代码的关键点是归一化统计量只取自训练段。很多开源时间序列代码直接对整个序列做归一化短期测试精度会偏高但换到新数据上就崩。normalized 序列的长度和原序列一致但统计口径已经和模型隔离。3.2 VMD 分解与模态重组下一步把归一化后的序列做 VMD 分解。vmdpy 的接口稳定性很好核心参数是 K、alpha、tau。# u: (K, N) 每个模态一行omega: 中心频率估计 u, u_hat, omega VMD( normalized, K5, alpha2000, tau0, DC0, init1, tol1e-7, )分解完成后u 的第一行是低频趋势越往下频率越高。我通常会检查 omega 的最后一组值是否明显分离。中心频率打架的解决办法在第四章说明。验证分离效果的实用命令是打印 omega 的最后一行并观察低频模态是否平滑。也可以用 np.fft.rfft 查看各模态的频谱峰值是否错开这一步虽不是必需但能帮助你判断 K 是否设大。下表是 vmdpy 各参数的说明和推荐范围。参数含义常见取值调整方向K模态数量310中心频率重叠则减小 Kalpha惩罚因子控制模态带宽5003000带宽过大则加大 alphatau噪声容忍度0 表示无噪声约束0 或 0.1数据噪声大时可适当调大DC是否保留直流分量0 或 1趋势明显时设为 1init中心频率初始化方式1 表示均匀分布默认即可tol迭代收敛容忍度1e-7收敛困难时适当放宽3.3 Attention-LSTM 模型结构模型结构不复杂输入先过一个单变量窗口LSTM 编码每个时间步的隐状态Attention 层对隐状态加权重最后全连接层输出单值。class AttentionLayer(nn.Module): def __init__(self, hidden_size): super().__init__() self.score nn.Linear(hidden_size, 1) def forward(self, lstm_output): # lstm_output: (batch, seq_len, hidden_size) weights torch.softmax(self.score(lstm_output), dim1) context torch.sum(weights * lstm_output, dim1) return context class VMDAttentionLSTM(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout, ) self.attention AttentionLayer(hidden_size) self.fc nn.Linear(hidden_size, 1) def forward(self, x): out, _ self.lstm(x) # (batch, seq_len, hidden) context self.attention(out) # (batch, hidden) return self.fc(context).squeeze(-1)这里的 LSTM 设置 batch_firstTrue输入形状为 (batch, seq_len, input_size)。attention 用单层线性打分softmax 维度是 1也就是对 seq_len 求权重。num_layers 和 dropout 在 LSTM 内部只在层间生效最后一层不做 dropout这一点和 Transformer 的实现逻辑一致。3.4 训练、验证与反归一化训练数据要按每个模态单独构造。look_back 表示用过去多少个时间点预测下一点数值应与数据的自然周期相关例如小时级电力数据取 24 或 48。def make_samples(modal, look_back48): X, y [], [] for i in range(look_back, len(modal)): X.append(modal[i - look_back:i]) y.append(modal[i]) return np.array(X, dtypenp.float32), np.array(y, dtypenp.float32) X, y make_samples(u[0], look_back48) # 先处理第一个模态 split int(len(X) * 0.8) train_x, train_y X[:split], y[:split] val_x, val_y X[split:], y[split:]拆分后每个模态都会得到自己的训练集。注意 VMD 分解已经消耗了整个 normalized 序列的一部分信息因此这里的训练/验证划分同样不能跨越原先的切分边界。实践中如果训练数据不足可以保留小部分作为最终测试其余内部的 80/20 划分只用于选参。训练循环里需要做梯度裁剪因为 LSTM 在较长序列上容易出现梯度爆炸。loader DataLoader( TensorDataset(torch.tensor(train_x), torch.tensor(train_y)), batch_size128, shuffleTrue, ) model VMDAttentionLSTM(input_size1, hidden_size64, num_layers2, dropout0.2) optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, factor0.5, patience5 ) loss_fn nn.MSELoss() for epoch in range(120): model.train() for xb, yb in loader: optimizer.zero_grad() pred model(xb.unsqueeze(-1)) loss loss_fn(pred, yb) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() model.eval() with torch.no_grad(): val_pred model(torch.tensor(val_x).unsqueeze(-1)) val_loss loss_fn(val_pred, torch.tensor(val_y)).item() scheduler.step(val_loss)把 xb.unsqueeze(-1) 的原因是要补上 input_size 维度。clip_grad_norm_ 中的 1.0 是梯度的全局范数上限可以按需要放大或缩小。scheduler 使用验证损失来调整学习率乘性因子 0.5、patience 5 在中小型数据集上效果稳定。预测阶段要把每个模态的输出反归一化再叠加。由于每个模态训练时用的是同一个 mu 和 sigma预测值也统一乘 sigma 加 mu 即可。preds [] with torch.no_grad(): for xb, yb in loader_test: preds.append(model(xb.unsqueeze(-1)).numpy()) pred np.concatenate(preds) * sigma mu到这里一个可运行的 VMD-Attention-LSTM 最小流程就完整了。实际使用中模态数量 K 和 look_back 对结果的影响远大于模型结构层面的差异下一章专门讨论这些参数怎么定。4. 关键参数怎么定改了会怎样4.1 VMD 的 K 和 alpha 最影响天花板VMD 分解出的模态数 K 直接决定模型能区分出多少种频率成分。K 设小了趋势和周期混在一个模态里退化成普通 LSTMK 设大了某些模态会成为拼凑出来的残余没有物理意义。判断依据有两个一是看 omega 最后一行的中心频率是否均匀分布在频带上二是看各模态的相关性矩阵如果两个模态的相关系数接近 1说明过分解。alpha 的作用是控制模态带宽。alpha 越大每个模态的带宽越窄中心频率越稳定但高频细节会被削平alpha 越小带宽越宽模态彼此的频率区间容易重叠。vmdpy 默认值是 2000对多数分钟级或小时级数据都够用。可以把 alpha 拉高到 3000 试试中心频率是否漂移更小但代价是某些瞬态波动会被滤掉。这里给一个可抄的调参思路先固定 alpha2000让 K 从 3 到 8 依次跑分解打印 omega 的最后一列选择中心频率开始出现重叠的前一个 K。之后保持 K 不变把 alpha 按 500 的步长在 15003000 之间扫描看验证集误差曲线的拐点。这种两段式扫描比盲目网格搜索快得多。下面是分解质量的快速判断表配合 omega 和模态能量使用。观察点可能原因调整方式相邻模态中心频率接近K 过大或 alpha 过小先降 K再升 alpha所有模态都和原序列高度相关几乎没有分解效果增大 K检查 DC 设置某个模态能量占比低于 0.1%高频残余噪声合并到相邻模态或丢弃tau 非 0 时模态凌乱噪声容忍度过高改回 tau04.2 attention 维度、dropout 和序列长度的取舍注意力的学习能力集中在 AttentionLayer 的线性层维度上。这个 score 层的输出维度固定为 1输入维度来自 LSTM 的 hidden_size。hidden_size 从 32 往上调注意力分布会更细腻但也更容易把权重集中在极少数时间步上造成过拟合。对中短序列look_back 2472hidden_size64 是个安全起点没必要追大。dropout 在 LSTM 里只在多层之间生效所以我个人从不在单层 LSTM 上设置 dropout。如果 num_layers2dropout0.2 或 0.3 是常见范围超过 0.5 会让训练明显变慢且收益递减。Attention 层本身不接 dropout想要在上下文向量与全连接之间加正则可以在全连接前插入一个 nn.Dropout。look_back 与数据的周期强相关而不是越大越好。电力负荷按小时采样look_back24 能抓住日内周期48 能补充前一天同刻信息径流预报按天采样look_back7 或 30 更合理。把 look_back 拉到 200LSTM 的记忆负担和训练时长都会暴涨而注意力权重在远距离上通常会稀疏化实际信息量并没有增加。4.3 训练策略先整体后微调早停不要只看 val_loss因为每个模态都是独立训练的模型参数数量乘以 K 以后会非常大。常见做法是第一步让所有模态共享同一套超参数分别训练到收敛第二步把 K 个模态的预测结果作为多通道输入合并进一个 LSTM 重新微调让模型学习模态间的交叉信息。第二种做法在数据集较大的时候效果明显小数据下反而容易过拟合。早停的条件建议写成相对阈值当验证损失连续 10 个 epoch 下降幅度小于当前值的 0.5% 时停止。只看 val_loss 是否下降会忽略噪声绝对值又依赖量纲相对阈值更通用。此外不同模态的 loss 量级差别很大比如高频模态的方差小loss 天然低如果拿所有模态的平均 loss 做早停高频模态会被低频模态的数值掩盖。best_loss float(inf) trigger 0 patience 10 for epoch in range(120): # 训练与验证省略 if val_loss best_loss * 0.995: best_loss val_loss trigger 0 else: trigger 1 if trigger patience: print(fearly stop at epoch {epoch}) break这段代码中的 0.995 就是相对阈值含义是验证损失需要比历史最优下降至少 0.5%否则算一次无效 epoch。连续 10 次无效就停止。调参时建议把每个模态单独训练时的 loss 曲线画出来确认哪几个模态贡献了主要误差。通常低频模态的拟合误差最小高频模态误差最大。如果去掉高频模态后验证集结果反而变好就不要单独预测它用它和其他高频分量构成一个聚合项或直接把该模态置零靠整体重构时的平滑性补足。这个细节决定了你的模型在真实数据上有没有泛化能力比换网络结构重要得多。5. 真实数据上的效果与对比5.1 用电力负荷数据做实验以小时级的电力负荷数据为例序列长度通常在 8000 到 20000 个点之间。按前面给出的流程先做归一化和 VMD 分解K 设 5、alpha 设 2000。观察各模态IMF1 是平缓的趋势IMF2 和 IMF3 的周期大约是 24 小时和 12 小时IMF4 和 IMF5 中心频率更高形状也更碎。这说明分解结果是符合负荷数据的物理常识的。分别对每个模态训练 Attention-LSTMlook_back 取 48。实际对比中VMD 分解带来的收益主要落在 IMF2 和 IMF3 上——这两个模态的规律性强LSTM 在平稳周期信号上的拟合明显好于在混叠信号上。趋势模态 IMF1 即使换成简单的线性外推也差别不大高频模态则经常出现预测曲线比真实值“多抖”的现象。实验中的另一个发现是多模态独立预测再加权求和其误差不是各模态误差的简单叠加。低频模态的偏差会直接进入最终结果高频模态的误差则可能因为符号随机而被部分抵消。这决定了最后的评估不能只看总体 RMSE还要看误差在哪个频率区间集中。5.2 和纯 LSTM、EMD-LSTM 对比对照实验至少要有三组纯 LSTM、EMD-LSTM、VMD-Attention-LSTM。纯 LSTM 输入不做任何分解EMD-LSTM 用 PyEMD 做经验模态分解其余训练设置完全一致。这样能分离出两个变量分解方法本身以及 Attention 层的贡献。表格记录三组模型在验证集上的指标具体数值随数据变化但排序有参考意义。模型RMSEMAE训练时长纯 LSTM高高短EMD-LSTM中中中等VMD-Attention-LSTM低低长这只是大致排序不是固定结论。VMD 的优势在序列的频谱成分清晰且稳定时最明显如果序列本身接近随机游走分解出的模态没有规律可学组合模型可能反而比纯 LSTM 差。EMD 的问题在于模态数不固定且容易出现端点飞翼对样本外预测不利。值得强调的是 Attention 在对比中的位置。去掉 Attention、保留 VMD 和 LSTM模型通常仍然优于纯 LSTM但提升幅度大约只有 VMD-Attention-LSTM 的一半。Attention 的收益主要体现在预测曲线在转折点附近的跟随时滞更小这在 RMSE 上反映不大在业务上却很重要。5.3 效果好的标准是什么别只看 RMSE时间序列预测的评估要分用途。如果预测结果用于调度或备货转折点是否滞后比平均误差更关键。RMSE 对离群点敏感一个尖峰就能让它变差而 MAE 更能反映中位数水平的误差。更严格的做法是计算 pinball loss 来评估分位数预测或用 MASE 把误差归一化到朴素预测之上这样不同数据量的结果可以横向比较。模态分解模型还有个特有指标模态还原度。把预测的各模态求和后对原始序列做一次 VMD再看两个分解的差异。如果预测结果的频谱结构扭曲即使 RMSE 看着还行后续业务上的高阶特征也是错的。这个指标在论文里不常见但在工程上能防住“数值好看、物理不对”的情况。import numpy as np def pinball_loss(y_true, y_pred, tau0.8): err y_true - y_pred return np.mean(np.maximum(tau * err, (tau - 1) * err))tau0.8 时 pinball loss 更关注高分位误差适合电力负荷这类需要偏保守估计的场景tau0.5 则退化为 MAE。用这个指标挑选模型比单看 RMSE 更容易选出在实际调度中好用的配置。最后检查一下多步预测的表现。单步预测精度高不代表滑动多步预测稳定。如果前几步误差逐步放大甚至出现振荡常见的对策是降低高频模态的权重或在训练时引入 teacher forcing。6. 让这套东西真正能上线的一个配置技巧6.1 模态叠加预测的前置步骤上线前先把流程固化成函数避免每一步手动拼装。第 3 章的每个模态预测结果都要乘 sigma 加 mu再累加。可以把这条逻辑封装成一个 predict_series 函数内部循环模态集合外部只接受原始序列和预测步数。叠加前务必检查每个模态预测的起始时间对齐否则相位一旦错开求和后会出现奇怪的跳变。6.2 滚动多步预测的窗口设定上线时往往要预测未来 24 步而不是一步。常见做法是滚动预测每预测出一个值就把它拼到窗口末尾同时丢弃窗口最前面的值再预测下一步。这样误差会累积但每一步都让 LSTM 看到最新信息。def rolling_predict(model, window, steps, look_back48): window window.copy() preds [] for _ in range(steps): x torch.tensor( window[-look_back:], dtypetorch.float32 ).view(1, -1, 1) with torch.no_grad(): p model(x).item() preds.append(p) window.append(p) return np.array(preds)使用这段代码时要额外传入当前已预测的值作为下一次输入。注意训练时模型从未见过自己生成的输入分布所以长时间滚动预测的误差会逐步偏离训练分布这属于正常现象。如果发现第 10 步以后误差明显膨胀可以把训练方式改成 sequence-to-sequence 或多步直接回归。6.3 验证方法滚动回测上线的最终验证应该用滚动回测而非单次切分。每次只把窗口向前移动一个真实值重新预测记录误差然后继续。这样模拟了线上“每来一个新时刻预测下一个窗口”的真实节奏。代码上只需要复用上面的 rolling_predict外层循环从测试集起点开始移动即可不需要每次重训模型。滚动回测的结果如果比固定切分的指标差说明模型存在数据自适应过拟合。此时回到 VMD 参数优先减小 K 而不是调节注意力结构往往更有效。本文还有配套的精品资源点击获取