
简介基于VMD-Attention-LSTM的时间序列预测完整项目包面向需要处理非线性、非平稳时序数据的算法学习者和科研人员。源码针对LSTM输入特性将数据设计为样本数时间步长特征数结构采用前30天数据预测第31天并将VMD分解得到的五组特征作为输入特征有效缓解了因数据压缩过小而引发的严重过拟合问题。模型由两层128单元LSTM、点积注意力层、展平层和全连接输出层组成并使用Dropout提升泛化能力。压缩包共28个文件、约5.25MB包含6个py源码、9个xlsx原始数据集及处理后的csv数据、模型checkpoint与权重文件、npy预测结果以及docx实验报告和README说明文档代码附详细注释训练集与测试集形状分别为840,30,15和205,30,15可直接运行复现实验。已有381人学习下载适合具备Python与LSTM基础、想掌握VMD分解结合注意力机制深度应用的进阶学习者参考。1. VMD-Attention-LSTM一套把负荷预测过拟合按下去的组合方案如果你跑过LSTM做日负荷或日客流预测大概率遇到过这种情况训练loss一路降到0.000几验证集前200天也很乖结果一碰到节假日或气温骤变的样本就突然“翻车”误差直接拉高两三倍。这套基于VMD-Attention-LSTM的时间序列预测模型就是针对这个场景设计的先用VMD把原始非线性序列拆成不同频段的模态分量再用两层128单元LSTM配合点积注意力抓取关键时间步最后用Dropout和早停把过拟合压住。资源里带python源码、训练好的checkpoint权重、从2013到2022年全国及多省市的电力负荷Excel原始数据还有一份从数据处理写到模型结构的说明报告。适合正在做电力短期负荷预测、风速预测或任何日粒度回归任务的人也适合想找一份带完整数据流和注释的VMD-LSTM入门工程的人直接改自己的数据。2. VMD分解与数据预处理从原始Excel到(840, 30, 15)的npy2.1 VMD分解的核心参数与封装调用这个项目里真正的“第一个黑匣子”不是LSTM而是VMD分解。VMD变分模态分解做的事情是把一条非平稳、非线性的负荷曲线分解成若干个具有有限带宽的模态分量低频分量对应长期趋势高频分量对应天气突变或假日扰动。把它放在LSTM前面等于先替网络把“该学的趋势”和“难学的噪声”拆分好LSTM只需要对每个模态分别建模收敛速度和精度都会好很多。项目源码里封装在utilt/VMD.py中底层用的就是常见的VMD迭代算法调用方式如下# 项目里VMD的封装在 utilt/VMD.py # f: 一维原始序列(比如某个省份的日负荷曲线) # alpha: 带宽惩罚因子, 越大模态带宽越窄 # tau: 噪声容忍度, 0表示严格保真 # K: 模态分解个数 # DC: 是否将第一个模态作为中心频率为0的分量 # init: 中心频率初始化方式, 1表示均匀初始化 # tol: 迭代停止阈值 from utilt.VMD import VMD f df[load].values.astype(np.float64) u, u_hat, omega VMD(f, alpha2000, tau0, K5, DC0, init1, tol1e-7) # 返回的 u 形状为 (K, len(f)), 每一行是一个模态分量 for k in range(5): df[fimf_{k1}] u[k]这里最值得关注的是K和alpha这两个参数。K是模态个数对一个以年为周期、带有周末和节假日波动的负荷序列我在实际复现时一般取5~7。K太小趋势和波动拆不干净LSTM还是会学到一堆噪声K太大会出现两个模态中心频率挨得很近的情况反而引入混叠。alpha是带宽惩罚因子常见经验值是1500~3000代码里取2000是比较稳的中间档。tau0表示完全不考虑噪声容忍做数据的强迫分解这样每个模态加起来能还原出原始序列方便后面把分解特征当输入。注意VMD分解只负责构造特征不是把目标值也拆开单独预测。项目里最终预测的还是原始负荷值VMD分量只是作为输入特征送进网络这一点和“先分解-再分别预测-最后叠加”的做法不一样数据处理时别搞混。2.2 滑窗构造训练样本为什么是(840, 30, 15)项目摘要里说得比较明确循环核时间展开步数是30也就是用前30天的数据预测第31天每个时间步的输入特征个数是15。这个15维特征不是随手定的摘要里专门提到一个反直觉的结论——如果把所有时间点的VMD分解特征全塞进去数据会被压缩得过小网络严重过拟合改成“每个时间段只对应当天五个原数据的VMD分解特征”之后过拟合得到明显缓解。也就是说这15维大概率是几个原始变量各自VMD分解出的5个主要分量拼接而成具体哪些列需要对照处理后的数据表.csv和README.md确认。构造滑窗样本的代码逻辑如下import numpy as np import pandas as pd df pd.read_csv(处理后的数据表.csv, parse_dates[date]) df df.sort_values(date).reset_index(dropTrue) # 假设 df 已包含 VMD 分解后的 imf_1 ~ imf_5 以及原始负荷等多列 feature_cols [imf_1, imf_2, imf_3, imf_4, imf_5] [temp, humidity, load] # 这里只是示例, 具体列名以项目的 处理后的数据表.csv 为准 data df[feature_cols].values.astype(np.float32) TIME_STEPS 30 # 用前30天 FEATURES len(feature_cols) # 这里应为15 X, y [], [] for i in range(TIME_STEPS, len(data)): X.append(data[i - TIME_STEPS : i]) # 第 i-30 到 i-1 天的特征 y.append(data[i, -1]) # 第 i 天(即第31天)的负荷值 X np.array(X).reshape(-1, TIME_STEPS, FEATURES) y np.array(y).reshape(-1, 1) # 按时间顺序切分: 前840个样本训练, 后205个样本测试 X_train, X_test X[:840], X[840:840 205] y_train, y_test y[:840], y[840:840 205] print(X_train.shape, y_train.shape, X_test.shape, y_test.shape) # (840, 30, 15) (840, 1) (205, 30, 15) (205, 1)这里的滑窗方式有一个细节每一行样本都包含连续30天、每天15个特征目标值是第31天的负荷。因为VMD分解是对整条序列一次性做的滑窗前必须保证数据无空值且时间索引连续。对原始Excel数据我一般会用asfreq(D)把缺失日期补全再用前向填充补空白避免滑窗把两个不相邻日期的数据硬拼在一起。另外一个容易踩的点是归一化的时机。常见做法是先做滑窗再对每个特征维度分别做MinMaxScaler而且scaler只能fit训练集再用同一个scaler去transform测试集。如果对全量数据先归一化再滑窗测试集信息会泄漏到训练过程里验证集指标会虚高。项目里已经处理好的train_vmd_af.npy和test_vmd_af.npy是直接可用的加载后形状就是上面那两组省掉这一步。3. Attention-LSTM模型实现点积注意力与128单元双层LSTM怎么写3.1 attention_3d_block点积注意力的TensorFlow实现项目模型代码在models/vmd_attention_lstm.py下分成两个部分attention_3d_block是点积注意力模块Attention_lstm是组合好的最终模型。先看注意力部分。点积注意力的核心思路是让每个时间步先计算一个得分再通过softmax变成权重把LSTM输出按权重加权求和让网络自己学会“重点看哪几天”。代码用的是TensorFlow 2的Keras函数式APIimport tensorflow as tf from tensorflow.keras.layers import Dense, Lambda def attention_3d_block(inputs, d_k128): 点积注意力层 inputs: (batch_size, TIME_STEPS, units), 即LSTM层的输出 d_k: 注意力键的维度, 这里取128与LSTM单元数一致 # 生成 Q, K, V 三个向量 q Dense(d_k)(inputs) # (batch, TIME_STEPS, d_k) k Dense(d_k)(inputs) # (batch, TIME_STEPS, d_k) v Dense(d_k)(inputs) # (batch, TIME_STEPS, d_k) # 点积得分: Q * K^T / sqrt(d_k) scores Lambda( lambda x: tf.matmul(x[0], x[1], transpose_bTrue) / tf.sqrt(tf.cast(d_k, tf.float32)) )([q, k]) # (batch, TIME_STEPS, TIME_STEPS) # 对每个时间步做softmax, 得到归一化注意力权重 weights Lambda(lambda x: tf.nn.softmax(x, axis-1))(scores) # 加权求和得到注意力上下文向量 context Lambda(lambda x: tf.matmul(x[0], x[1]))([weights, v]) return context代码里几个参数需要解释一下。d_k128不是随便拍的项目里两个LSTM层都用了128单元注意力内部维度对齐可以避免信息降维损失。更直白地说d_k就是Q和K的投影维度它决定注意力能“分辨”多少种不同的时间步模式d_k太小所有时间步的得分会趋同d_k过大权重分布会变得尖锐几乎只看一两天反而丢掉上下文信息。我一般会让d_k等于LSTM的units这个项目就是这么设计的。weights输出能拿来做注意力可视化这一点在后面的进阶章节会再展开。因为它形状是(batch, TIME_STEPS, TIME_STEPS)对某个样本取weights[i]就能看到模型预测第31天时对前30天每一天分别给了多少权重。3.2 Attention_LSTM从双层LSTM到全连接输出的完整结构模型整体结构是两层128单元LSTM都保留时间步输出接注意力层再展平接一个全连接层最后是单输出回归层。两层LSTM都使用return_sequencesTrue这点很关键——注意力层需要拿到每个时间步的LSTM输出如果第二层LSTM只返回最后一步注意力就没了作用对象。from tensorflow.keras.models import Model from tensorflow.keras.layers import Input, LSTM, Dropout, Flatten, Dense def Attention_LSTM(input_shape(30, 15), units128, dropout_rate0.2): inputs Input(shapeinput_shape) # 第一层LSTM, 返回每个时间步输出 x LSTM(units, return_sequencesTrue, activationtanh)(inputs) x Dropout(dropout_rate)(x) # 第二层LSTM, 同样返回每个时间步输出 x LSTM(units, return_sequencesTrue, activationtanh)(x) x Dropout(dropout_rate)(x) # 点积注意力, 输出 (batch, TIME_STEPS, d_k) x attention_3d_block(x, d_kunits) # 展平 全连接 输出 x Flatten()(x) x Dense(64, activationrelu)(x) x Dropout(dropout_rate)(x) outputs Dense(1, activationlinear)(x) model Model(inputsinputs, outputsoutputs) return model model Attention_LSTM((30, 15), units128, dropout_rate0.2) model.summary()注意力层输出形状是(batch, 30, 128)Flatten之后变成(batch, 3840)接64单元全连接做特征压缩最后线性输出第31天的预测值。结构上值得注意的一点是两个LSTM层之间都放了Dropout但注意力层后面没有再放LSTM而是直接展平因为注意力已经把所有时间步的信息做了加权汇聚再堆LSTM反而会把聚焦后的信息重新打散。这里的dropout_rate0.2也是项目控制过拟合的重要旋钮。如果训练集只有840个样本LSTM参数动辄十几万不加Dropout几乎必过拟合。我试过把dropout调到0.1训练loss照样降得漂亮但验证loss会在30轮左右开始反弹调到0.2~0.3配合早停才稳住。具体数值可以按自己的数据量微调但0.15以下在这个数据规模下不建议尝试。3.3 编译配置与训练参数Adam、MSE和早停模型编译和训练在train_vmd_af.py里损失函数用MSE优化器用Adam初始学习率0.001。对回归任务来说MSE是最稳妥的选择它放大大误差让模型优先学对突变样本。下面这段是训练主流程的核心逻辑from tensorflow.keras.optimizers import Adam from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint model.compile(optimizerAdam(learning_rate0.001), lossmse) checkpoint ModelCheckpoint( checkpoint_/my_model.ckpt, monitorval_loss, save_weights_onlyTrue, save_best_onlyTrue, modemin ) early_stop EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) history model.fit( X_train, y_train, validation_split0.15, epochs200, batch_size32, callbacks[checkpoint, early_stop] )batch_size32对应840个训练样本每个epoch大约26个batch梯度更新频率适中。如果想快速验证代码能不能跑通可以先把epochs改成20、patience改成3跑一次确认loss下降和checkpoint保存没问题后再上完整训练。项目里checkpoint_文件夹下已经有了my_model.ckpt.index和my_model.ckpt.data-00000-of-00001说明发布者已经把训练好的权重附带在资源里不想花时间重新训练的话可以直接跳到下一章的加载验证步骤。4. 训练与验证加载checkpoint、跑通val_models_pred并与报告对账4.1 从npy加载数据到用checkpoint恢复权重资源里的train_vmd_af.npy和test_vmd_af.npy是已经过VMD分解并滑窗好的特征数组直接np.load就能使用。比起重新从Excel跑到VMD再滑窗这套现成npy至少帮读者节省了半小时预处理调试时间。加载并校验维度的写法如下import numpy as np X_train np.load(train_vmd_af.npy) X_test np.load(test_vmd_af.npy) # 注意: 如果npy里同时包含特征和目标, 需要按脚本里的拆分逻辑处理 # 这里以X_train形状(840, 30, 15)为准 print(X_train:, X_train.shape) print(X_test:, X_test.shape)如果你的npy里只有特征没有目标那就需要从处理后的数据表.csv按同样滑窗逻辑生成y_train和y_test。项目摘要里给出的形状是(840, 30, 15)和(205, 30, 15)这里的15是输入特征数输出是单个值所以y的形状应该是(840, 1)和(205, 1)。加载模型权重时有一个易错点必须先实例化模型再load_weights否则TensorFlow会报“变量不存在”或权重形状不匹配model Attention_LSTM((30, 15), units128, dropout_rate0.2) model.load_weights(checkpoint_/my_model.ckpt) print(权重加载完成)如果报KeyError或者Unexpected keys大概率是模型定义和保存时的结构不一致。常见原因是我在改模型代码时调整了d_k或units导致Dense层的权重形状对不上。遇到这种情况最稳的办法是直接沿用models/vmd_attention_lstm.py里的原始定义不要自己改结构。4.2 运行验证脚本pred_hubei_set.npy是怎么来的项目的val_models_pred.py负责加载测试集、加载权重、做预测并保存结果。文件名pred_hubei_set.npy说明发布者最后是用湖北的数据作为演示验证对象。验证阶段的代码逻辑大致如下# val_models_pred.py 的核心流程 import numpy as np from sklearn.metrics import mean_absolute_error, mean_squared_error # 预测测试集 pred model.predict(X_test) # 输出形状 (205, 1) # 保存预测结果 np.save(pred_hubei_set.npy, pred) # 计算误差指标 mae mean_absolute_error(y_test, pred) rmse np.sqrt(mean_squared_error(y_test, pred)) mape np.mean(np.abs((y_test - pred) / y_test)) * 100 print(fMAE: {mae:.3f}, RMSE: {rmse:.3f}, MAPE: {mape:.3f}%)这里要特别提醒一点如果原始数据做过归一化pred是归一化空间里的值必须用训练时保存的scaler反归一化再和真实负荷对比。很多读者一上来直接拿pred和原始y算误差算出来的MAPE小得离谱其实是量纲都没对齐。项目报告基于VMD.docx里的误差对比表是在反归一化后的原始尺度上计算的所以你自己复现时也要走一遍反归一化。4.3 报告里该看什么误差口径和数据结构对齐报告的核心价值不是告诉你“这个模型精度多高”而是交代清楚了数据组织方式和模型设计理由尤其是那句“全部VMD特征喂进去会过拟合”的实验结论。我建议拿到报告后重点看两段一是数据部分里VMD分解特征和15维输入的对应关系二是实验对比部分里纯LSTM、Attention-LSTM、VMD-Attention-LSTM三者的验证误差差别。报告里的误差对比可以作为你复现结果是否“对账”的参考如果同一个数据集上你的MAE和报告的MAE差了一个数量级先检查是否漏了反归一化再检查train_test切分是否也是前840后205。另外资源里的原始Excel文件跨度很杂北京是2013-2021.6湖北是2014-2022.7上海是2013-2022.8全国是2021-2022福建是2017-2021.1还有广东、重庆、天津、深圳。这说明发布者做过多个省份的对比实验但最终只保留了湖北的预测结果npy。如果你想在别的省份上跑需要自己把对应Excel按处理后的数据表.csv的列格式处理好再做VMD分解和滑窗。5. 避坑过拟合、模态混叠、形状报错与权重恢复的五个现场5.1 训练loss掉到0.000x验证loss却一路走高现象训练过程loss下降很快前50轮就逼近0但验证loss在30轮附近开始反弹最终预测曲线几乎成了一条水平线。原因840个样本对双层128单元LSTM来说太少模型把训练集里的噪声和高频波动背了下来。这个项目摘要里已经明确写了——当初把全部VMD分解特征当作输入时数据量被压缩得过小引发严重过拟合。解决严格按摘要的做法每个时间步只保留当天的5个VMD分解特征拼成15维并配合dropout_rate0.2和EarlyStopping。我第一次复现时自作主张把15维特征换成30维结果就是在第60轮开始崩。5.2 K值不匹配导致模态混叠现象设置K10时打印omega发现后两个中心频率非常接近预测结果出现周期性抖动设置K3时预测曲线又过于平滑节假日突变根本捕捉不到。原因VMD的模态数K需要和序列本身的频带结构匹配。K过大算法会把一个完整频带强行切成两段产生虚假模态K过小高频有用信息混进低频模态。解决分解后先打印中心频率检查如果相邻omega差值小于某个阈值我一般关注最后一个值是否异常小就降K。项目默认K5是经过实验的先不要改。# 每次调整K后, 先看中心频率分布 u, u_hat, omega VMD(f, alpha2000, tau0, K5, DC0, init1, tol1e-7) print(中心频率:, omega) # 如果omega[-1]与omega[-2]差值在数量级内接近, 说明K太大5.3 训练时报“Shapes (32, 15) and (32, 1) are incompatible”现象代码在model.fit阶段直接报维度不匹配或者报ValueError: Input 0 of layer lstm is incompatible。原因LSTM要求的输入是三维(batch, time_steps, features)很多人从处理后的数据表.csv读进来后直接df.values得到的是二维(样本数, 列数)没做滑窗或者做了滑窗但目标值y是二维(840, 30)而不是(840, 1)。解决把X先reshape(-1, 30, 15)再送进模型。一个习惯性动作是每次训练前打印X_train.shape, y_train.shape确认第一维是样本数、第二维是30、第三维是15y最后一维是1。5.4 load_weights后预测输出全是同一个值现象能加载权重但model.predict(X_test)输出的结果几乎都是同一个常数loss曲线也纹丝不动。原因模型结构定义和训练时的结构不一致常见是自定义的attention_3d_block里d_k变了或者LSTM层数改了导致权重错位加载。TensorFlow的load_weights有时不会严格报错而只是把能匹配的权重加载进去。解决加载前先用model.predict(X_test[:1])跑一次前向确认输出是合理数值然后完全复用models/vmd_attention_lstm.py里的模型结构不要手动改动任何层的units或d_k。5.5 多省份Excel时间轴不连续导致滑窗串数据现象用湖北省的Excel做预测时误差尚可换深圳的Excel后验证误差突然暴涨。原因各地文件时间跨度不同有的从2013年1月到2021年6月有的到2022年7月中间存在缺失日期。滑窗时如果行之间并不是连续的“前一天和后一天”模型等于是看了断档的序列。解决对每份Excel先统一时间索引再滑窗。常见做法是用pandas的date_range生成完整日期序列然后reindex并前向填充再进入VMD分解流程。我一般还会把序列末尾不足30天的尾巴直接丢弃避免生成残缺样本。6. 进阶注意力权重可视化和两种预测模式的取舍注意力权重是这套模型里最值得利用的“副产品”。attention_3d_block里的weights输出形状是(batch, 30, 30)每一行代表预测某个目标日时前30天各天对它的关注程度。把某一行的权重取出来画折线图能直观看到模型在预测第31天时到底更依赖近3天、近7天还是同期历史。我一般会在验证集上挑几个特殊日期——比如春节前后、国庆节——单独看它们的注意力分布。如果模型在特殊日期的权重分布和普通工作日差异明显说明Attention学到的不是死记硬背。提取权重的方法很简单就是重写一个只输出weights的模型from tensorflow.keras.models import Model # attention_model 输出注意力权重 attention_model Model(inputsmodel.input, outputsmodel.get_layer(attention_3d_block).output) att_weights attention_model.predict(X_test[:1]) # (1, 30, 30) print(att_weights[0].shape)另一种更贴近业务的用法是滚动预测。原设计是“前30天预测第31天”适用于离线批量预测。但实际业务往往是今天预测明天然后明天用真实值更新窗口。两种模式误差差距很大:离线预测每个样本都用真实历史误差不累积滚动预测把预测值回填进窗口误差会随时间叠加。如果要做滚动预测我习惯把模型封装成下面这样def rolling_predict(model, data, steps7): data: 最近30天的真实特征, 每次往后推一步 results [] window data.copy() for _ in range(steps): # window 形状 (1, 30, 15) pred model.predict(window)[0, 0] results.append(pred) # 新窗口 旧窗口去掉第一天 新增一天(用预测值填充对应的特征列) new_row window[:, -1, :].copy() new_row[0, -1] pred window np.concatenate([window[:, 1:, :], new_row[:, None, :]], axis1) return np.array(results)注意这段代码里的new_row[0, -1] pred只是示意。实际拼接时如果15维特征里包含温度等外部变量需要把外部变量的真实预报值填进去只有负荷类的目标列才用预测值回填。至于调参顺序我的血泪经验是不要一上来就改网络层数。先固定一个纯LSTM基线然后单独加VMD分解特征再看加Attention后的增量。这个项目报告里的实验就是按这个思路做的每一层的贡献单独可衡量。从那以后我每次跑时间序列项目都强制先看一眼omega中心频率再检查X_train.shape是不是(样本, 30, 特征)最后才动学习率。这套流程帮我避掉了八成玄学报错希望帮到你。本文还有配套的精品资源点击获取