简介:面向高校期末大作业与课程设计场景的时间序列预测项目源码,基于LSTM模型实现,同时提供RNN和Transformer对比模型。代码结构清晰,曾获高分验证,适合深度学习初学者快速理解不同循环神经网络在序列建模中的差异。压缩包共31个文件,包含Python脚本、CSV时序数据集、说明文档、运行截图与项目配置,整体大小28.48MB。其中CSV数据涵盖ETTh1、ETTm1、pollution等常见负荷与空气质量序列;模型代码覆盖数据预处理、训练、评估和预测全流程;README附有环境配置与运行说明,图片直观展示训练曲线和预测效果,解压后即可按说明部署运行。目前已有1026人学习下载,无论用于课程设计、期末答辩还是代码复用,都能提供完整可跑的参考实现。
1. 时间序列预测(LSTM 模型)是玄学还是送分题:一份高分期末源码的完整拆解
时间序列预测(LSTM 模型)这门课在很多人眼里是玄学,同样的数据,有人调三天还在原地打转,有人一把过还拿了高分。差别往往不在模型多高级,而在数据和流程的细节对不对。这份源码是已经拿高分通过的期末大作业:一个可直接打开的 PyCharm 工程,内置 ETTh1、ETTm1、pollution 三套公开数据集,RNN、RNN_gpu、Transformer 三个模型文件齐整,README 连运行截图都配好了,解压装好依赖就能复现。它解决的不是读论文,而是交作业:期末要做时间序列课设的学生,或者想快速跑通 LSTM 完整流程的入门开发者,都能直接抓起来用。下面按数据、模型、跑通、踩坑的顺序,把这套工程的关键细节一次讲透。
2. 数据先行:ETTh1、ETTm1、pollution 三套数据集怎么选、怎么做滑窗
2.1 三套数据的出身、字段与适用选题
ETTh1.csv 和 ETTm1.csv 来自同一个公开的电力变压器温度数据集,前者每小时一条(h 后缀表示 hourly),后者每 15 分钟一条(m 后缀表示 minutely)。特征以电力负荷、油温、风速这类物理量为主,适合"基于 LSTM 的变压器油温预测"或"电力负荷短期预测"这类选题。pollution.csv 是北京 PM2.5 多变量数据集,字段包括 pm2.5、DEWP(露点)、TEMP(温度)、PRES(气压)、cbwd(风向)、Iws(风速)、Is(积雪时间)、Ir(降雨量),做空气质量预测最合适。
| 数据集 | 采样粒度 | 典型选题 | 变量特征 |
|---|---|---|---|
| ETTh1.csv | 1 小时 | 电力负荷 / 油温预测 | 多变量,周期以天、周为尺度 |
| ETTm1.csv | 15 分钟 | 分钟级短期预测 | 多变量,周期特征更密集 |
| pollution.csv | 1 小时(含完整日期时间) | PM2.5 浓度预测 | 8 个特征,含分类风向 |
选数据只有一个原则需要弄清:作业要求单变量预测,就把目标列单独提出来当 y,其余列要么当外部特征、要么直接舍弃;要求多变量预测,就整表进特征。这份工程把三套数据都配好了,切换只需要改代码里的 csv 路径。
2.2 归一化与划分顺序:数据泄露是期末课设的第一个坑
很多人在这一步翻车:读 csv 之后先对整个 DataFrame 做 MinMaxScaler,再切训练集和测试集。这个顺序会让测试集的 min/max 信息提前暴露给训练过程,训练时相当于偷看到了未来序列的分布,测试 RMSE 虚低,等答辩现场换一段新数据预测立刻露馅。正确做法是先按时间切分,再分别归一化,scaler 只在训练集上 fit。
import pandas as pd from sklearn.preprocessing import MinMaxScaler df = pd.read_csv('data/pollution.csv', index_col=0, parse_dates=True) # 前 80% 时间做训练,后 20% 做测试;时间序列不能随机 shuffle split = int(len(df) * 0.8) train_df, test_df = df.iloc[:split], df.iloc[split:] scaler = MinMaxScaler(feature_range=(0, 1)) scaled_train = scaler.fit_transform(train_df) # 只在训练集上 fit scaled_test = scaler.transform(test_df) # 测试集只 transformfit_transform 和 transform 的分工是这套流程的核心:fit 学的是训练集的 min 和 max,transform 只是套用;测试集一旦参与 fit,就等同于提前看到了未来数据的取值范围。归一化完成后再做滑窗,喂给 LSTM 的每个窗口都在同一尺度下,梯度更新才稳定。
2.3 滑动窗口:lookback 是整天作业里最值得调的参数
LSTM 不是把整条序列直接丢进网络,而是构造"用过去 lookback 个时间步预测下一个时间步"的样本。我一般会把滑窗函数单独抽出来,方便后面换 lookback 做对照实验:
import numpy as np def make_sequences(data, lookback=24): X, y = [], [] for i in range(len(data) - lookback): X.append(data[i:i + lookback]) # 过去 24 个时刻 y.append(data[i + lookback]) # 未来 1 个时刻 return np.array(X), np.array(y) lookback = 24 X_train, y_train = make_sequences(scaled_train, lookback) X_test, y_test = make_sequences(scaled_test, lookback) print(X_train.shape, y_train.shape)输出形状是 (样本数, lookback, 特征数) 和 (样本数, 特征数)。lookback 的选择必须跟着采样粒度走:ETTh1 是小时级、预测下一天,24 起步,想看周周期拉 168;ETTm1 是 15 分钟级、一天有 96 步,取 48 或 96 更合理;pollution 小时级,24 小时是默认起点。lookback 太大训练变慢且容易过拟合,太小学不到周期,这笔账用验证集跑两组对照再定。
pollution.csv 原始数据存在缺失值,常见做法是先沿时间方向 ffill 前向填充、再丢掉头部空行,最后归一化:
df = df.ffill().dropna()3. 模型实现:RNN.py、RNN_gpu.py、Transformer.py 三份代码分别承担什么角色
3.1 基础 RNN 为什么打不过 LSTM:先弄懂门控在干什么
这份工程里有 RNN.py 和 RNN_gpu.py 两个文件。基础 RNN 的短板在于梯度沿时间步逐层相乘,序列一长,梯度要么小到消失、要么大到爆炸,结果模型对窗口早期信息几乎没记忆。LSTM 用遗忘门、输入门、输出门配合 sigmoid 和 tanh,决定哪些信息扔掉、哪些写进长期记忆,梯度在细胞状态路径上有相对平缓的通道,长序列也能稳定训练。课设把 LSTM 当主角,不是因为它概念新,而是同样的数据和训练轮数下,验证 loss 通常比基础 RNN 低一截。
拿到源码后建议按角色分工理解:RNN.py 跑 baseline,RNN_gpu.py 是 LSTM 或 GRU 的 GPU 加速版本,装好 GPU 版 TensorFlow 后直接跑,Transformer.py 是答辩加分项。先跑通 LSTM 版本,再回头补 baseline 和 Transformer 的对比,报告才有内容可写。
3.2 搭一个能出成果的 LSTM:units、return_sequences、dropout 怎么设
工程里的 LSTM 是双层的典型课设结构,第一层输出完整时间步给第二层,第二层只留最后一个时间步接全连接:
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout n_features = X_train.shape[2] model = Sequential([ LSTM(64, return_sequences=True, input_shape=(lookback, n_features), dropout=0.2), LSTM(32, dropout=0.2), Dense(n_features) ]) model.compile(optimizer='adam', loss='mse', metrics=['mae']) model.summary()参数含义逐个说清楚:第一个 LSTM 的 units=64 是隐藏神经元数,return_sequences=True 表示输出每个时间步的隐藏状态给下一层;第二层 return_sequences 不设,默认 False,只输出最后一步的向量再接 Dense。input_shape=(lookback, n_features),即滑窗长度和特征数。dropout 放在 LSTM 内部,抑制过拟合,课设从 0.2 起步稳妥。优化器 adam、损失 mse 是回归预测的默认组合,mae 作为辅助评估指标。单变量预测时 n_features=1,多变量时是 8 或更多,Dense 输出维度跟着改。
模型保存的约定是 model 目录放权重文件,state 目录放训练状态或归一化 scaler 信息:
model.save('model/lstm_pollution.h5')3.3 Transformer.py 的定位:对照实验怎么做才不显得堆材料
Transformer 对时间序列的优势是并行处理整个窗口,用自注意力直接建模任意两个时间步的关系。这类课设工程里的 Transformer.py 通常是简化版编码器结构:位置编码、多头注意力、前馈网络、层归一化。答辩时用白板画清楚:输入先加位置编码,再过多头注意力层,注意力权重表示"第 3 小时的油温变化会强烈影响第 20 小时的预测"——这是 LSTM 很难直观解释的一点。
from tensorflow.keras.layers import Layer, MultiHeadAttention, LayerNormalization, Dense class TransformerBlock(Layer): def __init__(self, d_model=64, num_heads=4, ff_dim=128): super().__init__() self.att = MultiHeadAttention(num_heads=num_heads, key_dim=d_model) self.ffn = Sequential([ Dense(ff_dim, activation='relu'), Dense(d_model), ]) self.layernorm1 = LayerNormalization(epsilon=1e-6) self.layernorm2 = LayerNormalization(epsilon=1e-6) def call(self, inputs): attn_output = self.att(inputs, inputs) # 自注意力 out1 = self.layernorm1(inputs + attn_output) # 残差 + 层归一化 out2 = self.layernorm2(out1 + self.ffn(out1)) return out2注意 Transformer 对数据量的要求比 LSTM 高,这份工程在 pollution.csv 上能出合理结果,是因为序列长度和样本量正好够用。换更小的数据集时,先降 d_model 和 num_heads,否则训练 loss 压不下去。做对照组时,固定同一批数据、同一个 lookback、同一个 scaler,只换模型结构,答辩老师才挑不出毛病。
4. 完整复现路径:从配环境、跑通三份代码到画出预测曲线
4.1 环境准备:Python 版本与依赖清单
工程里带着 .idea 目录,用 PyCharm 直接打开项目根目录即可,运行配置都在包里,不用手动建环境再逐个配参数。Python 建议用 3.8 到 3.10,TensorFlow 2.x 在这几个版本下最稳定。依赖就四件套:
pip install numpy pandas matplotlib scikit-learn tensorflow只跑 LSTM 的话,CPU 版 TensorFlow 完全够用;要跑 RNN_gpu.py 的 GPU 路径,需要确认机器的 CUDA 和 cuDNN 版本跟 TensorFlow 匹配。遇到 Python 版本太新装不上 TensorFlow 的情况,最省事的解法是建一个 3.10 以下的虚拟环境,再在这个环境里重装依赖。
4.2 按 README 顺序跑通三套模型
README.md 里每一步截图都放好了,照着顺序执行就行:
python RNN.py # baseline python RNN_gpu.py # GPU/CPU 均可跑,内部自动判断设备 python Transformer.py # 对照模型每个脚本独立运行,输出统一写到 model 和 state 目录。第一次跑建议只跑 RNN_gpu.py,确认数据路径和 TensorFlow 版本没问题后,再补跑另外两个。脚本里写的是相对路径 data/ETTh1.csv,解压后如果把数据文件移走了,会直接报文件不存在,先检查当前工作目录。
4.3 训练参数速查表与调参顺序
| 参数 | 常见值 | 说明 |
|---|---|---|
| lookback | 24 / 48 / 96 / 168 | 滑窗长度,小时级数据 24 起步 |
| units | 32 / 64 / 128 | 第一层 LSTM 神经元数 |
| dropout | 0.1 - 0.3 | 防过拟合,课设 0.2 起步 |
| batch_size | 16 / 32 / 64 | 越大批次训练越快但可能收敛到较差解 |
| epochs | 50 - 150 | 配合早停使用 |
| loss | mse / mae | 回归任务默认 mse |
| optimizer | adam | 学习率 0.001 起步 |
调参顺序我习惯先固定 lookback,配上 epochs 和早停,再动 units,最后才碰 dropout。一上来同时改三个参数,最后训完根本说不清效果变化是谁带来的。
4.4 训练完看什么:loss 曲线和真实值预测值曲线
README 截图里最核心的图是真实值和预测值的曲线对比。自己验证时用这段代码出图:
import matplotlib.pyplot as plt inv_pred = scaler.inverse_transform(pred.reshape(-1, n_features)) inv_true = scaler.inverse_transform(y_test.reshape(-1, n_features)) plt.figure(figsize=(12, 4)) plt.plot(inv_true[:200], label='true') plt.plot(inv_pred[:200], label='pred') plt.legend() plt.title('LSTM Prediction vs True') plt.show()pred 是模型输出的归一化值,画图前必须用训练时的 scaler 反归一化回去,否则纵轴范围只有 0-1,跟真实值完全对不上。反归一化之前要把 pred reshape 成 (样本数, 特征数) 再调 inverse_transform。两条曲线趋势一致、局部波动有一点相位差是正常的;看到预测曲线几乎是平移的直线,说明训练环节出了问题,大概率是数据泄露或滑窗构造错了,下一章逐个排查。
5. 避坑实录:这份源码复现路上最常见的 5 个翻车点
5.1 预测曲线是一条笔直的"躺平线"
现象:训练 loss 还在降,验证 loss 也正常,画出来的预测曲线却像把上一天的值平移了一下,变化极小。
原因:绝大多数情况是归一化和划分顺序做反了,scaler 在切分前对整个数据集 fit,造成数据泄露;也可能是 lookback 太短,模型退化成只学会复制最近一个点;还有可能是 Dense 输出维度对应不上特征数,模型拟合到均值上去。
解决:回到第 2 章的流程,确认 scaler 只在训练集上 fit;把 lookback 从 24 往上调,试 48、96,逼模型看更长历史;检查数据有没有排序错乱的行,时间序列所有操作都不能 shuffle。
5.2 inverse_transform 报 ValueError:维度对不上
现象:报错类似 "Found input variables with inconsistent numbers of samples",或者 reshape 时长度正好差一个 lookback。
原因:训练集和测试集构造滑窗后样本数不一致,测试样本数等于原始测试行数减 lookback,画图和计算指标时拿错长度。
解决:所有涉及 y_test 和 pred 的地方统一用同一个切片长度;predict 返回的结果先 reshape 成 (n_samples, n_features),再传给 scaler.inverse_transform。
5.3 模型构建直接报 incompatible:expected ndim=3, found ndim=2
现象:构建 Sequential 时,TensorFlow 报 "Input 0 of layer 'lstm' is incompatible: expected ndim=3, found ndim=2"。
原因:LSTM 要求输入三维,格式是 (样本数, lookback, 特征数);把二维数组直接丢进去了,说明滑窗没做,或数据被 reshape 压平了。
解决:打印 X_train.shape 确认是三维;滑窗函数没写对,回第 2 章重新构造;多变量预测时把 n_features 和 Dense 输出维度对齐。
5.4 验证 loss 先降后升,训练 loss 还在掉
现象:前几十个 epoch 两条 loss 一起降,往后验证 loss 开始反弹,训练 loss 却继续走低。
原因:典型过拟合。LSTM 参数量不小,pollution 这类数据量也就几千行,训练太久模型把训练集噪声当成了规律。
解决:把 dropout 加到 0.2 以上;加早停是更省事的办法,patience 设 10-15,监控 val_loss,连续没改善就回滚到最佳权重:
from tensorflow.keras.callbacks import EarlyStopping early_stop = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True) model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=100, batch_size=32, callbacks=[early_stop])5.5 加载保存的模型后预测全 NaN
现象:训练完保存了 model/lstm_pollution.h5,换个脚本重新加载,预测输出大面积 NaN。
原因:加载和训练用了不同版本的 TensorFlow,或者保存的是自定义 Transformer 层、加载脚本没注册对应的层定义;另一个常见原因是归一化前的数据本身带 NaN,训练时表面正常,保存时权重已经被污染。
解决:加载和训练必须同一环境;自定义层对象要在加载脚本里先 import 进来;回到第 2 章,df.ffill().dropna() 先把数据清干净再归一化。这条习惯能躲掉九成 NaN 闹鬼。
6. 把作业从"能跑"改到"能答辩":多步预测、回归指标和 baseline 对照
6.1 单步改多步:滚动预测和它的误差累积
单步预测是"用过去 24 小时预测下 1 小时",答辩时老师通常会追问能不能预测未来 24 小时。最小改动是滚动预测:把上一步的预测结果当成新窗口输入,循环 24 次。
def forecast(model, last_window, steps=24): preds = [] window = last_window.copy() for _ in range(steps): p = model.predict(window[np.newaxis, :, :], verbose=0)[0] preds.append(p) window = np.concatenate([window[1:], p[np.newaxis, :]], axis=0) return np.array(preds)window 形状是 (lookback, n_features),每预测一步把窗口往前推一格,末尾补上刚预测的值。多步预测的坑是误差累积:第 24 步的误差远大于第 1 步,答辩时主动承认这一点,反而显得你理解序列预测的本质。
6.2 回归三件套:RMSE、MAE、R2 出一张指标表
光有曲线不够看,答辩还需要指标表。计算后按 RMSE、MAE、R2 排三行直接进报告:
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score rmse = mean_squared_error(inv_true, inv_pred, squared=False) mae = mean_absolute_error(inv_true, inv_pred) r2 = r2_score(inv_true, inv_pred) print(f'RMSE={rmse:.3f}, MAE={mae:.3f}, R2={r2:.3f}')新版 sklearn 里 squared=False 是老版 mean_squared_error 的替代写法,老接口会报警告但不影响数值。R2 接近 1 说明拟合好,负值说明模型比直接预测均值还差,指标表里顺手把 baseline 也算上。
6.3 朴素 baseline:让 LSTM 的优势有对照
答辩被问"凭什么说 LSTM 有效"时,最实在的回答是给一个基线:用"过去 24 小时的均值"当预测结果,算同一套 RMSE、MAE 对比。通常 LSTM 能把 RMSE 压到基线的一半左右,这个数字比讲任何原理都有说服力。建议做实验时固定同一个 scaler 和后处理流程,算出 LSTM、RNN baseline、Transformer 三行指标,形成一张干干净净的对比表,课程报告直接把表贴进去。
这套工程解压后对照 README 用,省掉的配置时间能把三个模型都跑一遍。从那以后我每次跑序列预测课设,都强制自己先打印数据行列数、确认归一化只在训练集上 fit、再检查滑窗 shape,三个检查项过完才谈调参,这套流程帮我避开了绝大多数翻车现场,希望帮到你。
本文还有配套的精品资源,点击获取