VMD+Attention+LSTM:时间序列预测的分解注意力混合模型实战
2026/9/13 18:54:07 网站建设 项目流程

简介:基于VMD-Attention-LSTM的时间序列预测模型Python实现,面向深度学习与时间序列方向初学者及课程设计、毕业设计场景。方案融合变分模态分解与注意力机制,给出从数据处理、模型构建到训练预测的完整参考流程,内含详细代码注释与项目说明,便于按步骤理解算法细节。压缩包共28个文件、约5.26MB,包含6个Python源码、9份Excel原始数据、模型权重文件、处理后的CSV数据、图文说明与项目报告等,覆盖数据拆分、窗口构造、模型搭建、训练评估与预测等环节。已有635人学习/下载。代码采用前30天数据预测第31天的滑动窗口设计,内置128单元LSTM、点积注意力与Dropout防过拟合机制,并提供训练好的模型权重与单序列预测脚本,用户可直接运行结果,也可基于小数据集快速验证算法,便于后续扩展。

1. 为什么时间序列预测要叠 VMD、Attention 和 LSTM

做时间序列预测的人,十有八九会遇到同一个尴尬:数据明明是干净的,LSTM 也跑起来了,可 loss 下降得像个假模型,val loss 却一动不动的——典型过拟合。这个资源给了一个很实在的解法,不是把网络堆得更深,而是在进 LSTM 之前先用 VMD 把序列拆开,再用 Attention 把拆出来的模态按重要程度加权。思路很直接:VMD 负责把非线性、非平稳的信号分解成有限带宽的模态分量,Attention 负责让模型自己决定看哪几个时间步、哪几个特征更重要,LSTM 再负责把这些信息按时间顺序递推下去。整套代码用 Python 写,有训练脚本、预测脚本、训练好的权重和一份湖北数据的预测结果,结构清楚得可以直接抄作业。适合正在做课程设计、毕业设计或者刚开始接触深度学习时序预测的从业者,也适合那些已经跑通基础 LSTM、想看看加分解和加注意力到底能带来什么变化的人。

2. 数据准备与 VMD 分解:从原始序列到 (840, 30, 15) 的训练张量

2.1 数据源与滚动窗口的构造逻辑

压缩包里放了北京、湖北、天津、上海、深圳、广东、重庆、福建等多个省市的时间序列表,格式是 xlsx,处理后的数据被整理成了一份统一的 CSV。第一次打开处理后的数据表.csv时,你看到的应该是按日期排序的数值型序列,每一列是一个地区或一个指标,没有缺失值也不用再清洗。真正麻烦的是怎么把一维序列变成 LSTM 能吃进去的三维张量。

代码里给出的答案是:用前 30 天的数据预测第 31 天,窗口每次向后滑动一天。也就是说,一个样本的输入形状是(30, 特征个数),标签是第 31 天的值。连续滑动得到样本后,再按 8:2 左右切分训练集和测试集,于是就有了代码里写死的(840, 30, 15)(205, 30, 15)这两个张量形状。第一步要理解 30 这个时间步的含义,它对应的是循环核时间展开步数,太小了抓不住趋势周期,太大了训练样本数会骤减,代码选择 30 属于时序预测里比较稳妥的起点。

import pandas as pd import numpy as np def build_samples(series, time_step=30): X, y = [], [] for i in range(len(series) - time_step): X.append(series[i : i + time_step]) y.append(series[i + time_step]) return np.array(X), np.array(y) # series 是某省市经过 VMD 分解并构造好的特征矩阵,形状为 (总天数, 15) X, y = build_samples(series, time_step=30) # 手动切分,保持时间顺序不乱 train_X, train_y = X[:840], y[:840] test_X, test_y = X[840:1045], y[840:1045] print(train_X.shape, test_X.shape) # (840, 30, 15) / (205, 30, 15)

这段代码里的build_samples是标准的滑窗函数,time_step=30决定循环核展开多少步;X的第二个维度永远是时间步,第三个维度是每个时间步上的特征数。切分时我特意没有用train_test_split,因为时间序列一旦随机打乱,未来信息就会漏进训练集,验证结果会虚高。后面那个840205不是拍脑袋定的,而是总样本数减去时间步后再按比例切出来的,你换数据集的时候应该按int(len(X) * 0.8)这种方式计算,别把边界写死。

2.2 VMD 分解的代码实现与 K 值怎么定

VMD(变分模态分解)和 EMD 最大的区别是 VMD 把分解问题放到变分框架里求解,每个模态被约束在中心频率附近,不容易出现 EMD 那种模态混叠。在这个资源里,VMD 承担的任务不是直接预测,而是先把原始序列拆成若干个平稳性更好的子序列,再把这些子序列作为 LSTM 的特征输入。常见实现是用vmdpy这个库,安装方式就是pip install vmdpy,核心参数就四个:alpha是惩罚项,K是模态个数,tau是噪声容忍度,DC决定是否把直流分量单独拆出来。

from vmdpy import VMD import numpy as np def vmd_decompose(series, K=5, alpha=2000, tau=0.0, DC=0, init=1, tol=1e-7): # series: 一维数组,长度为 T # 返回 u,形状为 (K, T),每一行是一个模态分量 u, u_hat, omega = VMD(series, K, alpha, tau, DC, init, tol) return u # 示例:对湖北原始序列做 5 模态分解 raw = pd.read_excel('14-22.7 湖北.xlsx').iloc[:, 1].values modes = vmd_decompose(raw, K=5) print(modes.shape) # (5, 总天数)

K是最值得调的参数。K太小,分解不充分,趋势和噪声还搅在一起;K太大,会出现虚假模态,相邻两个分量的中心频率重叠。判断办法很朴素:画出各模态的频谱,如果两个模态的中心频率几乎重合,说明K过大;如果某个模态里还明显残留高频毛刺,说明K偏小。项目里默认用了 5 个模态,并且只取部分模态构成特征,背后还有一层考虑,后面展开。

2.3 15 维特征是怎么构造出来的

训练数据特征维度是 15,很多人拿到代码后第一反应是:VMD 分解不是只有 5 个模态吗,15 从哪来的?注意项目说明里那句关键的话:每个时间段只对应五个原数据的 VMD 分解特征,而不是把所有分解结果全部丢进去。实验也证明了,如果把全部模态一股脑全送进网络,特征被压缩得太厉害,模型会严重过拟合;按当前方式截取五个原始序列的对应分解特征,预测结果基本不受影响,过拟合问题却明显缓解。

我按资源里的思路给一个可落地的构造方法:选 5 条原始序列(比如 5 个省市的数据),每条序列做 K=3 的 VMD 分解,取每条的 3 个模态,5×3=15,正好凑成每个时间步的 15 维特征。你也可以把 K 设为 5,每条取前 3 个主能量模态,同样能得到 15 维。关键不在于具体怎么拼,而在于每个时间步上的 15 个特征必须在时间上对齐,不能前 30 天用的是 A 序列的分解结果,第 31 天却混进了 B 序列的分解结果。

def build_feature_matrix(series_list, K=3): # series_list: 5 个一维序列组成的 list feature_list = [] for s in series_list: modes = vmd_decompose(s, K=K) feature_list.append(modes[:3]) # 取前 3 个主模态 # 拼接成 (T, 15) feature_matrix = np.vstack(feature_list).T return feature_matrix

拼接时注意np.vstack的方向,modes[:3]得到的是(3, T),五个序列拼起来是(15, T),转置后才是(T, 15),对应 LSTM 要求的「每个时间步一个 15 维向量」。这个 15 维特征里既有不同序列自身的模态信息,又有它们之间的横向关联,Attention 层后面做的事,就是在这些特征里挑出当前预测点最该看的那些维度。

3. Attention-LSTM 模型结构:点积注意力与双层 LSTM 怎么组合

3.1 点积注意力为什么适用在这个场景

时序预测里加 Attention,最朴素的理解是:LSTM 虽然能记住历史,但对第 1 天和第 29 天的信息权重是均等的,而现实中往往是某几个关键时间点决定了第 31 天的值,比如节假日前后的突变、政策发布的当天。点积注意力通过计算 Query 和 Key 之间的相似度,把这种「哪个时间步更重要」变成可训练的权重。资源里实现了attention_3d_block,输入形状是(batch_size, time_steps, features),它做的事情就是典型的 scaled dot-product attention:先生成 Q、K、V,然后算 Q 和 K 的点积,除以特征维度的平方根做缩放,再 softmax 得到权重,最后用权重加权 V。

import tensorflow as tf from tensorflow.keras import layers def attention_3d_block(inputs): # inputs shape: (batch_size, time_steps, features) features = inputs.shape[-1] # 用三个独立的全连接层生成 Q、K、V q = layers.Dense(features, activation='relu')(inputs) k = layers.Dense(features, activation='relu')(inputs) v = layers.Dense(features, activation='relu')(inputs) # 点积相似度 + 缩放 score = tf.matmul(q, k, transpose_b=True) score = score / tf.sqrt(tf.cast(features, tf.float32)) # softmax 归一化成权重 weight = tf.nn.softmax(score, axis=-1) # 加权求和得到上下文向量 context = tf.matmul(weight, v) return context

注意score矩阵的形状是(batch, timesteps, timesteps)score[i][j]表示第 i 个时间步应该给第 j 个时间步分配多少注意力。Dense(features)的作用不是改变维度,而是让模型学习从原始特征到 Q/K/V 空间的映射。这里没有用多头,因为数据集规模只有一千多条样本,多头会把参数量撑得过大,单头点积注意力已经够用。

3.2 双 LSTM 层加 Dropout 的完整模型

Attention_lstm的组装顺序是:Attention 模块 → 两个 128 单元的 LSTM 层 → 每个 LSTM 后接 Dropout(0.5) → Flatten 展平 → 全连接输出层。输入形状在代码里写死为(30, 15),其中 30 是时间步,15 是特征维度。两层 LSTM 都设置return_sequences=True,这很关键——第一层输出的仍然是完整的时间步序列,这样第二层 LSTM 才能继续按时间递推;如果第一层只返回最后一步,序列的时序信息到第二层就断了。

from tensorflow.keras import Model, Input from tensorflow.keras import layers def Attention_lstm(input_shape=(30, 15)): inputs = Input(shape=input_shape) # 注意力层先处理原始输入 att = attention_3d_block(inputs) # 第一层 LSTM,返回完整序列 x = layers.LSTM(128, return_sequences=True, activation='tanh')(att) x = layers.Dropout(0.5)(x) # 第二层 LSTM,同样返回序列 x = layers.LSTM(128, return_sequences=True, activation='tanh')(x) x = layers.Dropout(0.5)(x) # 展平后接全连接,输出第 31 天的预测值 x = layers.Flatten()(x) outputs = layers.Dense(1)(x) model = Model(inputs, outputs) return model model = Attention_lstm() model.summary()

两个 LSTM 单元数都取 128,不是随便定的。项目说明里明确提到,经过测试 128 神经元数量在精度和训练成本上最均衡。activation='tanh'是 LSTM 默认的门控激活,内部状态用 tanh 压缩到 [-1,1] 可以避免梯度爆炸。Dropout(0.5)放在 LSTM 输出之后而不是内部循环之间,这种是标准 dropout;如果要在时间步维度上做变体 dropout,需要用LSTM(..., dropout=0.5)参数,两种位置作用不同,不要混淆。

3.3 为什么输出层只有 1 个神经元

整个任务的标签是第 31 天的值,一个连续数值,所以输出层是一个无激活函数的全连接层,等价于线性回归头。这里有个新手容易踩的坑:以为是回归问题就在输出层加sigmoid或者tanh,结果预测值全被限制在 [-1,1] 或 [0,1] 区间里,完全对不上原始量纲。正确的做法是不加激活函数,让输出层的线性组合直接映射到目标值范围;如果你做了归一化,记得预测完再做反归一化还原。

4. 训练细节:损失函数、回调与过拟合控制

4.1 超参数表与选型理由

训练阶段的设计比模型结构更能看出作者有没有实际调过参。学习率取1e-4,Batch Size 取 128,优化器用 Adam,损失函数用 Huber,最大迭代次数 500。这套组合放在小数据集上非常稳,下面是完整的超参数表:

超参数取值选择说明
LSTM 单元数128测试后精度与训练速度折中
学习率1e-4小数据集上偏保守,避免前期震荡
Batch Size128与 840 条训练样本配合,约 7 个 batch/epoch
Dropout0.5抑制过拟合,力度较大但要配合 Huber
损失函数Huber对异常值不敏感,比 MSE 稳
优化器Adam自适应学习率,免去手动衰减
Epochs500配合回调取最优权重,不需要手动卡 epoch

Batch Size 128 看起来和 840 条训练样本不匹配,一个 epoch 只有 6 个 batch,反而成了一种隐式正则化——梯度更新次数少、噪声大,不容易陷入局部最优。1e-4的学习率在这种 batch 数量下不会跳得太猛。Huber 损失是新引入的关键点:MSE 对离群点会给出平方级别的惩罚,一旦某天数据出现尖峰,梯度会被拉得很大;Huber 在误差小于阈值delta时表现如 MSE,大于阈值时退化为 MAE,既保留了对正常样本的敏感度,又限制异常样本的破坏力。

4.2 Callback 保存最优模型权重

500 个 epoch 不代表最终模型用的是最后一个 epoch 的权重。很多情况下,训练集 loss 还在降,验证集 loss 早就开始回升了,这就是过拟合的信号。资源里用了 CallBatch 机制里的 ModelCheckpoint 来监控验证集指标,只把最优点保存下来。具体做法是让回调监控验证集 Huber 损失,mode='min',一旦验证损失创新低,就把当前权重写到 checkpoint 文件里。

from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping from tensorflow.keras.optimizers import Adam from tensorflow.keras.losses import Huber model.compile(optimizer=Adam(learning_rate=1e-4), loss=Huber(delta=1.0), metrics=['mae']) checkpoint = ModelCheckpoint( 'checkpoint/my_model.ckpt', monitor='val_loss', mode='min', save_best_only=True, save_weights_only=True, verbose=1 ) early_stop = EarlyStopping( monitor='val_loss', patience=30, restore_best_weights=True ) history = model.fit( train_X, train_y, validation_data=(test_X, test_y), batch_size=128, epochs=500, callbacks=[checkpoint, early_stop] )

逻辑说明:save_best_only=True保证磁盘上始终只有最优权重一组文件,不会像默认行为那样每个 epoch 都存一遍。save_weights_only=True只存权重不存结构,文件小,加载时用代码里重新定义的模型结构再load_weights就行。EarlyStoppingpatience=30说直白点就是验证损失连续 30 个 epoch 不创新低就提前终止,400 个 epoch 跑完的情况其实很少见,资源里标注 500 是上限。

4.3 为什么 VMD 特征能缓解过拟合

回到项目说明里那个实验现象:直接用所有时间原数据 VMD 分解后的特征作为输入,数据量被压得过小,网络严重过拟合;改成五个原始序列的对应模态后,过拟合明显缓解。这个现象背后的原理是:特征维度从「全量分解的高维拼接」降到 15 维之后,模型的自由度被迫减小,LSTM 和 Attention 只能学习有限特征里最稳定的映射关系。相当于给模型加了一个隐式的特征选择器——VMD 分解本身也滤掉了一部分高频噪声,LSTM 拿到的是相对干净的模态输入,不会花大量参数去拟合噪声细节。

验证过拟合是否被控制住,直接看训练集 loss 和验证集 loss 的差距。我一般会画一个双纵轴图,左边是训练 Huber loss,右边是验证 Huber loss,如果两条曲线都在下降且末端差值小于 20%,说明模型在泛化;如果训练 loss 一路冲到 0.1 以下而验证 loss 停在 0.5 上下,说明 Attention 的权重没有学到有意义的分布,多半是特征没对齐或者 K 值不对。

5. 预测应用与扩展技巧:加载最优权重后的三件事

5.1 用后 100 个时间点做预测验证

资源里把湖北原数据预处理后的后 100 个时间点专门拿来测试,目的是模拟「模型没见过的最新鲜数据」。加载权重后,输入形状必须是(100, 30, 15)而不是(100, 15)——因为每个预测点都需要前面 30 个时间步的历史特征。一个常见的做法是保留原始序列最后 130 个时间步,前 30 个作为首个预测窗口的输入,预测出第 31 个点后,把这个点拼到序列末尾作为新的历史,再切出 30 个时间步预测下一个点,这样滚动下去直到生成 100 个预测值。

import numpy as np def rolling_predict(model, history, steps=100, time_step=30): # history: (T, 15) 特征矩阵 preds = [] tmp = history.copy() for _ in range(steps): # 取最后 30 个时间步作为输入 x = tmp[-time_step:].reshape(1, time_step, 15) y_pred = model.predict(x, verbose=0)[0, 0] # 构造下一时间步的特征向量并拼回历史 next_feature = build_next_feature(y_pred) # 生成 15 维特征 tmp = np.vstack([tmp, next_feature.reshape(1, 15)]) preds.append(y_pred) return np.array(preds)

这里build_next_feature是特征构造函数的反向过程:把预测出的标量值还原成当天的 15 维特征,再拼回历史。注意如果训练前做了归一化,预测出的y_pred是归一化区间内的值,计算误差指标前必须反归一化到原始量纲,否则 RMSE 和 MAPE 完全没有可解释性。资源里的pred_hubei_set.npy就是这组预测结果的 Numpy 数组,你可以直接和真实值相减,算一下最大误差出现在哪个区域,往往能发现 Attention 权重在那些位置确实会重新分配注意力。

5.2 三个实战级调优建议

第一个建议是改大数据集再谈模型能力。这份代码只用了一个较小数据集训练,结构上是完整的,但 840 条样本对双 LSTM 加 Attention 来说还是偏少,有能力的可以通过文件里的13-21.6北京.xlsx13-22.8天津.xlsx等多省市数据做多序列联合训练,把样本量扩到几千条,过拟合会进一步缓解。

第二个建议是调K值时要配合模态能量看。VMD 分解后计算每个模态的方差贡献率,去掉累计贡献率低于 90% 的模态再进特征矩阵,比盲目固定 K=3 或者 K=5 更合理。实现时可以在vmd_decompose函数里加一行variance_ratio = np.var(u, axis=1) / np.sum(np.var(u, axis=1)),然后根据这个比值选择保留前几个模态。

第三个建议是不要迷信测试集指标。pred_hubei_set.npy里的 100 个点是滚动预测出来的,误差会随着步数增加而累积,所以只看第 1 步的预测误差是不够的。多步预测的误差曲线如果呈现单调上升,说明模型把历史预测值当作真实值回传时误差在扩散,这时候应该把time_step加大到 60 或者改用 seq2seq 结构在解码端重新生成特征,而不是继续加大训练轮数。

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

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

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

立即咨询