ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

一套源码玩转12种时间序列预测模型:LSTM到Encoder-Decoder完全指南

一套源码玩转12种时间序列预测模型:LSTM到Encoder-Decoder完全指南 简介基于LSTM、CNN与堆叠式LSTM的时间序列预测Python源码包面向人工智能、数据科学等计算机相关专业学生和开发者重点解决多模型结构下输入输出数据形状构造与网络参数配置难题既适合课程作业、毕业设计参考也能作为时序预测入门到进阶的通用模板。压缩包共39个文件以24个Python脚本为核心覆盖单步/多步预测、单变量/多变量组合、双向长短期记忆、堆叠式长短期记忆、卷积神经网络长短期记忆、卷积长短期记忆及编码器-解码器长短期记忆等典型结构另含13个示例数据包和2个说明文档整体仅68KB目录按模型分块便于按需检索与复现实验。目前已有646人学习下载内容经过运行验证、注释详细12个独立示例覆盖不同输入输出组合场景允许读者直接复制调整结构、损失函数与超参数以适配自身数据对于小白可解决“怎么把数据做成模型能吃的形状”这一常见难点对于进阶用户则可对比不同网络在时序预测任务上的表现。项目价值体现在完整度和可复用性上是课程大作业或初期立项演示的实用素材。1. 一套源码横跨 12 种时间序列预测组合从单步 LSTM 到 Encoder-Decoder 的完整对照做时间序列预测课程大作业时最折磨人的不是 LSTM 原理看不懂而是同一份数据要在十几种模型间来回切换输入输出形状各不相同改一处崩三处。这份源码把这条路走通了01 号脚本负责把原始序列切成监督学习样本02 到 06 是单层 LSTM、堆叠式 LSTM、双向 LSTM、CNNLSTM、ConvLSTM 五种结构在同一基准任务上的对照07 到 12 号脚本补齐多变量输入、多步输出、多变量输出的各类组合Encoder-Decoder 兜底多步预测。每份代码都带超详细注释装好 TensorFlow/Keras 就能跑适合课程设计、大作业和毕设初稿也适合想快速对比神经网络在时间序列预测上差异的从业者。下文按数据构造、基础模型、混合模型、多步输出四条线拆解给出可直接抄的参数写法。2. 先把数据形状造对监督学习转换的三个关键参数2.1 为什么原始序列必须先转成监督学习LSTM 本身不知道什么叫时间序列它只认(样本数, 时间步, 特征数)的三维张量。原始数据是一列数必须用滑动窗口把它切成「过去 n_in 步 未来 n_out 步」的样本对这一步在时间序列预测里叫监督学习转换也是整套源码里 01 号脚本存在的意义。这一步要是蒙混过去后面所有模型都是空中楼阁报错只会一个接一个。# 01 号脚本的核心滑动窗口构造函数 import numpy as np import pandas as pd def series_to_supervised(data, n_in3, n_out1, dropnanTrue): df pd.DataFrame(data) cols, names [], [] # 过去 n_in 个时间步作为输入特征 for i in range(n_in, 0, -1): cols.append(df.shift(i)) names [t-%d % i] # 未来 n_out 个时间步作为输出标签 for i in range(0, n_out): cols.append(df.shift(-i)) names [t%d % i] agg pd.concat(cols, axis1) agg.columns names if dropnan: agg.dropna(inplaceTrue) return agg # 10 个点的原始序列n_in3, n_out1得到 6 个完整样本 raw np.arange(1, 11, dtypefloat) print(series_to_supervised(raw, n_in3, n_out1))shift(i) 表示把整列向下平移 i 行shift(-i) 向上平移这样同一行里就同时装着「过去 3 个时刻」和「未来 1 个时刻」模型要的 X 和 y 就是这么对齐的。dropnanTrue 会丢弃首尾窗口不满的行这是必须的否则模型拿 NaN 训练loss 第一轮就能给你 nan。三个参数里 n_in 是回看步长n_out 是预测步长dropnan 一般不动。2.2 单变量与多变量差别只在最后一维02 到 06 号脚本全部是单变量输入特征数 n_features107、08、11、12 号脚本换成多变量输入同一时刻有多列特征n_features 变成特征列数。数据切成样本对之后都要 reshape 成 LSTM 要的三维形状这一步做错Keras 会直接甩给你维度错误。# result 来自 2.1 的滑动窗口结果列名为 t-3, t-2, t-1, t0 n_in, n_features 3, 1 X result.iloc[:, :n_in].values # 前三列是输入窗口 y result.iloc[:, n_in:].values # 最后一列是输出标签 # LSTM 要求三维输入 (样本数, 时间步, 特征数) X X.reshape((X.shape[0], n_in, n_features)) print(X 形状:, X.shape) # (6, 3, 1) print(y 形状:, y.shape) # (6, 1)多变量场景下 n_features 不再是 1 而是特征列数。比如数据里有气温、湿度、风速三列同时参与预测X 形状就变成 (样本数, n_in, 3)。最容易犯的错是把二维的 (样本数, n_in) 直接塞进模型Keras 提示expected ndim3, found ndim2看到这个报错第一反应就应该是回去检查 reshape而不是怀疑模型写错了。2.3 切分与归一化时间序列的隐藏规则时间序列切训练集和测试集有两条铁律违反任何一条后面跑出来的指标都没有参考价值第一不能随机打乱第二归一化只能在训练集上 fit。from sklearn.preprocessing import MinMaxScaler # 顺序切分时间序列禁止随机打乱 split_idx int(len(X) * 0.8) X_train, X_test X[:split_idx], X[split_idx:] y_train, y_test y[:split_idx], y[split_idx:] # 特征归一化scaler 只 fit 训练集 scaler_x MinMaxScaler() X_train scaler_x.fit_transform(X_train.reshape(-1, n_in)).reshape(-1, n_in, n_features) X_test scaler_x.transform(X_test.reshape(-1, n_in)).reshape(-1, n_in, n_features) # 标签归一化单独一个 scaler预测结果要靠它反变换回原始尺度 scaler_y MinMaxScaler() y_train scaler_y.fit_transform(y_train).ravel() y_test scaler_y.transform(y_test).ravel()如果直接调用 sklearn 的 train_test_split默认 shuffleTrue 会把时间顺序打乱模型相当于偷看了未来数据验证集 loss 很好看换到真实场景立刻翻车。归一化同理在全量数据上 fit 再切分测试集的信息已经泄漏进训练过程这种错误很隐蔽指标还异常地好反而最危险。习惯做法是只对训练集 fit scaler测试集用同一个 scaler 做 transform保证测试集全程不参与任何统计量计算。n_in 的取值决定了模型能看多长的历史。经验值是覆盖数据的一个完整周期日数据取 7 或 14月度数据取 12 或 24。n_in 太小模型学不到周期依赖预测曲线会明显滞后太大则参数量膨胀、训练变慢小数据集上非常容易过拟合。这个参数值得单独做一轮网格搜索它比调 hidden units 的影响大得多。3. LSTM 家族三件套单层、堆叠式、双向的模板与选型3.1 单层 LSTM02 号脚本是最小可运行模板02 号脚本对应「多步单变量输入 → 单步单变量输出」的基准任务是整个资源里结构最简单的 LSTM 模型也是建议第一个跑通的脚本。网络只有两层LSTM 加 Dense 输出层没有任何花活适合作为后面所有对照实验的基线。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense from tensorflow.keras.callbacks import EarlyStopping model Sequential() model.add(LSTM(50, activationtanh, input_shape(n_in, n_features))) model.add(Dense(n_out)) model.compile(optimizeradam, lossmse) early_stop EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) history model.fit(X_train, y_train, epochs200, batch_size32, validation_split0.2, callbacks[early_stop], verbose1)LSTM(50) 的 50 是隐藏单元数也是这个模型最主要的旋钮。单元越多容量越大但课程数据集通常只有几百到几千条样本50 和 100 的差距往往不明显反而是 100 更容易过拟合。activation 我用默认的 tanh很多网课喜欢写 relu但在序列任务里 relu 遇上大学习率很容易梯度爆炸tanh 稳得多。input_shape 固定写 (n_in, n_features)不要写样本数Keras 会自动补 batch 维度。这里没写 return_sequences所以 LSTM 只输出最后一个时间步的隐藏状态形状是 (batch, 50)Dense 层直接接住并吐出 n_out 个数。loss 用 mse 是时序回归的标配分类任务才用交叉熵。EarlyStopping 的 patience10 表示验证集 loss 连续 10 个 epoch 不下降就停restore_best_weightsTrue 保证最后拿到的权重是最优的那一份而不是最后一次迭代的这是防止过拟合性价比最高的一个参数。3.2 堆叠式 LSTM03 号脚本的层间衔接03 号脚本在单层基础上又叠了一层 LSTM就是堆叠式 LSTM。堆叠的意义是让高层网络学到更抽象的时序模式但代价是参数量和训练难度同时上升。这里的关键在 return_sequences它决定上一层的输出是保留完整时间步序列还是只保留最后一个时间步的隐藏状态。model Sequential() model.add(LSTM(50, activationtanh, return_sequencesTrue, input_shape(n_in, n_features))) model.add(LSTM(50, activationtanh)) # 最后一层不写 return_sequences model.add(Dense(n_out))第一层必须写 return_sequencesTrue这样输出形状才是 (batch, n_in, 50)才能作为第二层 LSTM 的序列输入最后一层不写输出只剩最后时间步的隐藏状态形状是 (batch, 50)。漏写第一层的 return_sequences 是堆叠 LSTM 最常见的报错来源报错通常是 ndim 不匹配第二层期望三维输入结果拿到了二维。小数据集上堆两层足够堆到三层以上验证集 loss 几乎必然开始反弹这时候不是模型不够深是数据不够多。3.3 双向 LSTM04 号脚本的适用边界04 号脚本把 LSTM 包进 Bidirectional 包装器。双向的含义是同时从前向后、从后向前各读一遍序列把两个方向的结果拼起来作为输出。对时间序列预测要冷静看待它如果输入就是「过去 n_in 步」双向等于把同样的历史读两遍再拼接预测任务的增益有限参数量却是单层的两倍。from tensorflow.keras.layers import Bidirectional model Sequential() model.add(Bidirectional(LSTM(50, activationtanh), input_shape(n_in, n_features))) model.add(Dense(n_out))真正适合双向的场景是序列补全、缺失值插值这类左右都有上下文的任务或者在特征构造里本来就有未来信息的时候。课程大作业里如果只是为了在 02 到 06 之间凑结构对照双向 LSTM 跑出来的指标通常不会比单层好太多但作为结构对比写进报告是合格的。训练时它的参数量翻倍EarlyStopping 的 patience 可以调小到 5避免模型在验证集上来回震荡浪费时间。三种结构的选型边界我用一个表格收一下结构参数量训练速度适合的数据规模典型用途单层 LSTM低快小样本即可基线模型最稳的起点堆叠式 LSTM中高中中大规模单层欠拟合时的加深方向双向 LSTM高慢中大规模补全插值特征含未来上下文4. 混合模型实战CNNLSTM 与 ConvLSTM 的维度衔接4.1 CNNLSTMConv1D 沿时间步做局部卷积05 号脚本把一维卷积和 LSTM 串起来思路是先用卷积在时间维度上提取局部模式比如连续三天的变化趋势再把卷积结果交给 LSTM 学长程依赖。模型本身很直观但维度衔接是最大的坑十个人里有八个翻在形状上。from tensorflow.keras.layers import Conv1D, MaxPooling1D model Sequential() model.add(Conv1D(filters64, kernel_size3, activationrelu, paddingsame, input_shape(n_in, n_features))) model.add(MaxPooling1D(pool_size2)) model.add(LSTM(50, activationtanh)) model.add(Dense(n_out))Conv1D 的输入就是 (batch, n_in, n_features)卷积核只在时间步维度上滑动。kernel_size3 表示每次看连续 3 个时间步filters64 是输出通道数也就是卷积后每个时间步的特征维度。这里我写了 paddingsame卷积后时间步保持不变如果不写默认是 validn_in 会变成 n_in-kernel_size1。这个变化会影响后面 LSTM 的输入形状属于隐蔽翻车点。真正必踩的坑在 MaxPooling1Dpool_size2 把时间步直接砍半。假设 n_in10卷积后还是 10池化后变成 5LSTM 实际接收的时间步是 5 而不是你心里想的 10。很多人模型编译过了但预测结果异常回头查才发现时间步对不上。我的习惯是每改一次结构就打印一次 model.summary()看每一层的 Output Shape全部对齐后再训练。提示任何维度报错先打印 X.shape 和 model.summary()把每层输入输出形状一对齐问题一般当场就能定位。4.2 ConvLSTM一维序列塞进五维张量的 reshape 技巧06 号脚本用的是 ConvLSTM2D这是 ConvLSTM 最常见的实现。它把卷积运算嵌进 LSTM 单元内部每个时间步的信息更新都带卷积操作最初是为视频帧序列设计的所以输入要求五维。一维时间序列要进去必须做一次 reshape 手术。from tensorflow.keras.layers import ConvLSTM2D, Flatten # 把 n_in 拆成 (子序列数, 子序列步长) n_seq, n_steps 2, n_in // 2 X_seq X.reshape((X.shape[0], n_seq, n_steps, n_features, 1)) model Sequential() model.add(ConvLSTM2D(filters32, kernel_size(1, 1), activationrelu, input_shape(n_seq, n_steps, 1, n_features))) model.add(Flatten()) model.add(Dense(n_out))ConvLSTM2D 的输入形状是 (samples, timesteps, rows, cols, channels)。一维序列没有行列概念所以 rows 和 cols 都设成 1kernel_size 用 (1,1)等价于在每个时间步上做逐点卷积。reshape 这步最容易翻车X 原来是 (样本, 10, 1)要先用n_in % n_seq 0确认能整除再拆成 (样本, 2, 5, 1, 1)。维度数从 3 变 5很多人直接在原 X 上多塞一维就喂进去报错说 expected 5 dimensions, got 3 dimensions原因就是少拆了子序列这一维。4.3 混合结构的输入输出对照表脚本网络结构输入形状输出形状一句话选型02单层 LSTM(样本, n_in, 特征数)(样本, n_out)默认起点03堆叠式 LSTM同上同上单层欠拟合时加深04双向 LSTM同上同上需要反向上下文05CNNLSTM同上同上想捕获局部趋势06ConvLSTM2D(样本, 子序列数, 子步长, 1, 特征数)(样本, n_out)时空结构任务10Encoder-Decoder(样本, n_in, 特征数)(样本, n_out, 1)多步预测首选这张表列的是每种结构在源码里的标准形状照着填就不会错。值得注意的是 02 到 06 五份脚本共用同一个基准任务输入输出形状完全一致唯一区别在网络中间层。这意味着跑完一遍就可以直接横向对比五种结构的 MAE课程报告里的对照表就是这么来的。5. 常见问题排查多步预测与 Encoder-Decoder 的五个高频坑5.1 多步预测的三条路线09/11/12 号脚本的差别09、11、12 号脚本分别对应单变量、多变量输入下做多步输出。多步输出在 Keras 里有两种简单做法一种是让最后一层 Dense(n_out) 一次吐出未来 n_out 个值网络自己决定怎么分配09、11、12 号脚本走的是这条路线另一种是 Encoder-Decoder 结构先压缩再展开10 号脚本就是后者的标准模板。前者实现简单后者在预测步长较长时误差累积更小这是多步预测场景里最需要想清楚的一个选型问题。5.2 Encoder-Decoder 拆解RepeatVector 把上下文复制 n_out 份from tensorflow.keras.layers import RepeatVector, TimeDistributed model Sequential() model.add(LSTM(100, activationtanh, input_shape(n_in, n_features))) # 编码器 model.add(RepeatVector(n_out)) # 复制成 n_out 个时间步 model.add(LSTM(100, activationtanh, return_sequencesTrue)) # 解码器 model.add(TimeDistributed(Dense(1))) # 逐时间步输出 model.compile(optimizeradam, lossmse)编码器 LSTM 把整个输入序列压成一个固定长度的上下文向量形状是 (batch, 100)。RepeatVector(n_out) 把这个向量复制 n_out 份拼成 (batch, n_out, 100) 的序列让解码器 LSTM 能沿着时间维度展开生成。TimeDistributed(Dense(1)) 是最后的机关它让同一个全连接层独立作用在每一个时间步上输出形状是 (batch, n_out, 1)正好对应未来 n_out 个时刻。如果不包 TimeDistributedDense 会把整个序列压成一个向量维度立刻对不上。训练时注意 y 也要 reshape 成 (batch, n_out, 1)保持和模型输出一致。5.3 五个高频坑的排查记录坑一训练一开始 loss 就是 NaN现象model.fit 打印的 loss 第一轮就出现 nan后面全是 nan预测值也是 nan。 原因最常见是数据没归一化直接喂给 LSTM配合 Adam 默认的 0.001 学习率发生梯度爆炸其次是 LSTM 里用了 relu 激活网络稍深就数值不稳定。 解决先做 MinMaxScaler 归一化再进模型把优化器换成 Adam(lr1e-4)激活函数换回 tanh。归一化后还偶发 nan就检查原始序列里有没有 inf 或缺失值没清干净这一步经常被忽略。坑二预测曲线是一条水平直线现象训练 loss 收敛得很好但 predict 出来的几乎是一条直线数值接近训练集均值。 原因序列非平稳、趋势项太强MSE 损失下模型发现「输出常数」的 loss 最低或者 n_in 太小模型根本没学到周期。 解决先对序列做一阶差分或对数变换压掉趋势项把 n_in 放大到覆盖一个完整周期增加 epochs 并同时看 val_loss排除欠拟合。水平线还有一种隐蔽情况测试集取值落在训练集范围边缘模型本就缺乏外推能力这时要先看验证集曲线而不是怪模型。坑三ValueError: expected ndim3, found ndim2现象model.fit 直接报错提示 LSTM 层期望三维输入实际给了二维。 原因X 没 reshape 成 (样本数, n_in, n_features)把二维窗口矩阵直接塞进去了。 解决检查是否执行了 X.reshape((X.shape[0], n_in, n_features))。反过来还有一种报错 expected ndim2, found ndim3那是堆叠 LSTM 或 Encoder-Decoder 漏写了 return_sequences 或 RepeatVector三维序列没压成二维就强行接了 Dense。坑四Conv1D MaxPooling1D 后 LSTM 报维度错现象05 号脚本编译报错或者编译能过但预测结果离谱。 原因MaxPooling1D(pool_size2) 把时间步砍半LSTM 实际接收的时间步比设计的 n_in 小Conv1D 默认 valid 卷积也会缩短时间步。 解决建模前用 model.summary() 打印每层 Output Shape确认进入 LSTM 前的时间步数要么 Conv1D 写 paddingsame 保持时间步不变要么在代码里按池化后的时间步去衔接后续逻辑。坑五inverse_transform 后预测值跟原始数据不是一个数量级现象模型预测的 y 在 0 到 1 之间画在原始数据图上完全对不上。 原因y 做了归一化但预测结果没有反变换或者 MinMaxScaler 在整段数据上 fit测试集尺度信息泄漏反变换结果偏移。 解决scaler 只 fit 训练集预测时用同一个 scaler 对输出做 inverse_transform。多变量输出时特别注意 scaler 是按列 fit 的反变换要取对列取错列的结果比不反变换还难排查。6. 验证与改造路线半小时跑通三天内换上自己的数据6.1 最小验证实验01 → 02 → 指标三步确认环境没问题拿到源码先别急着改模型解压后按编号顺序跑三个脚本先跑 01 生成监督学习数据再跑 02 训练单层 LSTM最后加一段指标输出。这一步的目的是确认环境、数据、模型三板斧都是通的再往上叠别的结构才有参照系否则后面 12 个脚本对比出来的差异你根本说不清是模型差异还是数据差异。from sklearn.metrics import mean_absolute_error, mean_squared_error y_true scaler_y.inverse_transform(y_test.reshape(-1, 1)).ravel() y_pred scaler_y.inverse_transform(model.predict(X_test).reshape(-1, 1)).ravel() print(MAE :, mean_absolute_error(y_true, y_pred)) print(RMSE:, mean_squared_error(y_true, y_pred, squaredFalse))MAE 的单位和原始数据一致方便跟业务指标对照RMSE 对大误差更敏感能暴露个别预测点严重偏离的情况。两个指标一起看MAE 小但 RMSE 大说明大部分点预测得不错但存在极端离群点优先去查那段数据是不是有异常值或突变点。跑通过这一轮02 号脚本就是你的基准线后面每个模型的指标都拿它来比。6.2 换成自己数据时的四步清单换数据时要动的不是模型结构而是数据管道我一般按固定顺序过一遍。第一步清掉原始序列里的缺失值和 inf用前后均值插值或直接丢弃这一步不做后面全是玄学报错。第二步确认列顺序多变量场景下特征列顺序决定 n_features 和 reshape 的维度改错一列全错。第三步n_in 先按数据周期设置日数据设 14周数据设 8跑通后再做网格搜索微调。第四步遇到星期几这类离散特征先 one-hot 展开再拼进特征矩阵别把离散整数当连续值直接喂给模型。我自己被 06 号脚本坑过一次当时直接把 (样本, n_in, 1) 的三维数组喂给 ConvLSTM2D报错提示期望五维输入我花了半小时查资料才意识到要把 n_in 拆成子序列和子步长。那之后我养成一个固定习惯每换一个模型先打印数据形状和 model.summary()确认输入输出对齐再按训练按钮。这个习惯让我后来换数据集时几乎没再因为维度问题浪费过时间也建议你从这份源码开始就把它固化下来希望帮到你。本文还有配套的精品资源点击获取
返回列表