LSTM时间序列预测期末作业全攻略:从数据预处理到多步预测
2026/9/24 23:25:40 网站建设 项目流程

简介:这是一份基于 LSTM 实现时间序列预测的 Python 期末大作业源码,适合高校学生用于期末项目、课程设计或毕业设计参考,也可帮助初学循环神经网络的读者快速理解完整建模流程。项目已获高分通过,代码结构清晰,压缩包共 31 个文件,包含多组 CSV 时序数据集、3 个 Python 模型脚本、14 张 README 流程说明图,以及 IDE 配置与模型状态文件,整体约 28.48MB。内容从数据读取与预处理出发,覆盖 LSTM 模型构建、训练、评估和结果可视化;压缩包内还提供 RNN 与 Transformer 对比脚本,方便分析不同模型的预测效果。附带 README 截图和模型状态文件,可减少重复训练时间,便于快速部署复现。目前已有 1026 人学习下载,对需要完整作业参考或快速上手时序预测项目的读者有较高实用价值,尤其对数据预处理和模型调参思路会有直观收获。

1. 拿到这份期末大作业,先别急着跑模型

时间序列预测的 LSTM 模型代码,几乎是每个接触深度学习的工科生都绕不过去的坎。期末大作业里十份有八份是它:给一段历史数据,比如某城市过去 180 天的用电量,让你预测后 7 天;或者给你一支股票的历史收盘价,让你预测下一个交易日的走势。题目可以千变万化,但交上去的代码结构高度一致:读数据、做归一化、切训练集测试集、把一维序列拼成 LSTM 能吃的三维输入、搭一个两三层深的 LSTM 网络、训练、画图、把预测结果反归一化回真实量纲。你把这份 zip 解压后会看到,里面的核心文件就这么几个:数据文件、模型脚本、可视化输出,运气好还会附带一份写好的报告模板。这篇文章我会把这套流程从头到尾拆开,每个环节给出能直接照抄的代码和参数,并把最容易翻车的几个点单独拎出来说。你要是手里刚好有一份类似的作业代码,也可以对照着自查——代码能不能跑通只是及格线,能不能把每个参数说出个所以然,才是拿高分的关键。

2. 先摸清一份 LSTM 预测作业包的文件结构

2.1 zip 解开后,需要看到哪些文件

我接触过不少类似题目的作业源码,也帮人救过不少跑不起来的模型。这个题的压缩包解开后,一般会包含这些文件:数据集通常是一个 CSV 或者 Excel,里面至少有两列,一列是时间,一列是数值;然后是主代码文件,常见命名是 lstm_predict.py 或者 main.py;如果作者用心一点,还会带上 requirements.txt 或者 environment.yml;再往下就可能是输出图片的文件夹,里面放着 loss 曲线图和预测结果对比图。下面这张表可以让你快速对号入座。

文件(夹)名称常见格式作用
原始数据.csv / .xlsx / .txt存储时间列和观测值列,可能还有多变量特征列
主程序.py 文件完成数据加载、预处理、建模、训练、评估、画图全流程
模型权重.h5 / .keras / .pt训练完成后的保存结果,方便下次直接加载预测
输出图片.png / .jpg训练 loss 曲线、测试集拟合对比图、未来预测图
依赖清单requirements.txt记录 tensorflow、numpy、pandas、matplotlib、sklearn 等库的版本

动手之前,我建议你先用 pandas 把数据读出来看一眼。很多人解压完就急着运行主程序,结果报错说列名不对,这才发现数据文件被改过。先用几行代码确认数据形态,心里有底了再跑下一步。

import pandas as pd df = pd.read_csv("data.csv", encoding="utf-8") print(df.head()) print(df.info()) print(df.isnull().sum())

这段代码干了三件事:第一条打印前 5 行,让你看数据长什么样子;第二条打印列类型和缺失值统计;第三条单独列出每一列的缺失数量。对于一个期末作业来说,数据量一般不会很大,几百到几千行都正常,重点看时间列是不是 datetime 类型、数值列是不是 object 类型。如果数值列被读成了 object,通常是csv里混入了逗号分隔符或者"NaN"这样的文本,后面训练的时候一定会报 dtype 相关错误。

2.2 时间序列不该用默认的 train_test_split 切分

这是作业里最经典的翻车现场。很多同学在 sklearn 里用 train_test_split 随机划分数据集,结果训练出来的模型在测试集上表现异常好,但老师一问就露馅。时间序列本身有先后顺序,随机打乱等于把时间顺序全部破坏,模型学到的是"前后文乱序"的规律,而不是时间依赖规律。正确做法是按时间顺序切分:前 70% 到 80% 做训练,剩下的按顺序做测试,严格禁止打乱。

我在处理这种任务时通常是按 8:1:1 划分,也就是训练集 80%、验证集 10%、测试集 10%。如果数据量小,比如只有 300 条,就改成 6:2:2,防止测试集长度不够导致评估不稳定。切分时直接按下标切片即可,不需要引入额外工具。

train_size = int(len(df) * 0.8) val_size = int(len(df) * 0.1) train_df = df.iloc[:train_size] val_df = df.iloc[train_size:train_size + val_size] test_df = df.iloc[train_size + val_size:]

这里用的是 df.iloc,严格按行号切片,保证不破坏时间顺序。有个容易忽略的细节:val_size 用 int() 取整后,还要防止 train_size + val_size 超出总长度,比如总行数 99 的时候,80% 和 10% 都取整后可能和 1.0 对不上。稳妥写法是先切出测试集,再在剩余部分切验证集,或者干脆用 math.floor 仔细算好边界。期末作业不需要多复杂的交叉验证,但顺序切分这个底线不能破。

3. 数据归一化和滑窗:LSTM 能不能学好,七成看这里

3.1 为什么一定要做 MinMaxScaler

LSTM 内部用的是 tanh 和 sigmoid 激活函数,它们的输入输出范围天然被限制在 [−1, 1] 或 [0, 1] 附近。如果你把原始的电力负荷数值,比如几千千瓦直接塞进去,激活函数早就饱和了,梯度在反向传播时近乎为零,模型怎么训都学不进去。所以归一化不是可选项,而是必选项。

作业里最常见的归一化手段有两种:MinMaxScaler 和 StandardScaler。对于时间序列预测,我几乎总是用 MinMaxScaler,把数据缩放到 [0, 1] 区间,因为它保留了数据的原始分布形状,反归一化时也直观。StandardScaler 适合数据本身近似正态分布的回归问题,用在时间序列上也不是不行,但反归一化时均值方差的还原容易让人绕晕,作业报告中不好解释。

from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) data_scaled = scaler.fit_transform(df[["value"]].values)

这里 fit_transform 是对整个数据集的全部数据做的。严格来说,工业场景里应该只在训练集上 fit,验证集和测试集用同一个 scaler 去 transform,防止测试集的信息泄露到训练过程。但期末作业数据量不大,很多人图省事全量 fit,老师一般也不会深究。如果你想在报告里体现专业性,就改成先在训练集上 fit,再对验证集测试集单独 transform。

这里有一处必须提醒:MinMaxScaler 要求输入形状是 (n_samples, n_features),所以传进 fit_transform 的数据要用 df[["value"]] 而不是 df["value"]。前者是 DataFrame,形状是 (N, 1);后者是 Series,形状是 (N,),sklearn 很多接口会直接报错。光这一个坑,就能拦住不少人。

3.2 滑窗生成监督学习样本

归一化后的数据是一串一维数值,但 LSTM 需要的是有"特征"有"标签"的样本。所谓特征,就是过去连续 lookback 步的历史数据;所谓标签,就是后一步的真实值。这个把一维序列转换成二维样本的过程,叫滑窗,或者叫构造监督学习格式。

import numpy as np def create_sequences(data, lookback=7): X, y = [], [] for i in range(len(data) - lookback): X.append(data[i:i + lookback, 0]) y.append(data[i + lookback, 0]) return np.array(X), np.array(y) lookback = 7 X, y = create_sequences(data_scaled, lookback)

这段代码从第 0 个位置开始,每次取连续的 lookback 个点作为输入,下一个点作为输出,然后窗口平移一位,重复这个过程。最终 X 的形状是 (样本数, lookback),y 的形状是 (样本数,)。遍历完成后还要加一行 X = X.reshape((X.shape[0], X.shape[1], 1)),把二维变成三维,LSTM 才能吃进去。第三个维度是特征数,单变量预测就是 1;如果是多变量输入,比如同时用温度和湿度预测用电量,这个维度就对应特征数。

lookback 怎么选值得好好想。选太小,比如 3,模型只能看到极短历史,很难捕捉周期趋势;选太大,比如 365,模型参数量变大,训练变慢,而且容易过拟合。我的经验是先从 7 开始,对应一周的周期;数据如果是月度周期性明显的,就试 30;如果样本量特别大,才考虑更长的窗口。作业里如果你想体现思考过程,可以对比 lookback=7、14、30 三组实验的 loss,挑一组最好的写进报告。这个实验不复杂,但很能体现你对模型的理解。

4. 用 Keras 搭一个 LSTM 模型的最小可运行版本

4.1 模型结构:两层 LSTM 加一个 Dense

Keras 在 TensorFlow 2.x 里已经成为标准接口,期末作业代码里见到 tf.keras.layers.LSTM 的频率远高于其他框架。最稳妥的结构是两层 LSTM 堆叠,第一层返回完整序列,第二层只返回最后一步,再接一个全连接层输出预测值。第一层为什么要 return_sequences=True?因为如果你不返回完整的序列,第二层 LSTM 就接收不到每个时间步的隐藏状态,只能拿到最后一步的结果,这会让两层结构名存实亡,效果和一层几乎一样。

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model = Sequential([ LSTM(units=64, return_sequences=True, input_shape=(lookback, 1)), Dropout(0.2), LSTM(units=32, return_sequences=False), Dropout(0.2), Dense(units=1) ]) model.compile(optimizer="adam", loss="mse", metrics=["mae"]) model.summary()

这里 units 指的是 LSTM 隐藏状态的维度,也就是细胞输出向量的长度。第一层 64 通常足够捕捉中等复杂度的时序特征,第二层 32 再做一次抽象。Dropout 的作用是随机丢弃一部分神经元输出,防止过拟合,尤其当你的数据量只有几百条时,Dropout 基本是标配。Dense 层输出维度是 1,因为我们要预测的是一个标量值。激活函数默认是 None,回归任务不需要在最后一层加激活。

optimizer 选了 adam,这是深度学习里最常用的自适应学习率优化器,对初学者非常友好,因为它会根据梯度自动调整每个参数的学习率,不用手动做太多的学习率调度。loss 选 mse,也就是均方误差,它对大误差的惩罚更重,适合预测值连续变化的时间序列。如果你想在报告里增加一点深度,可以额外对比一下 mse 和 mae 的区别:mae 用的是绝对值误差,对大离群点不那么敏感,预测结果会更"稳",但梯度在零点附近不可导;mse 全程可导,收敛更平滑。

4.2 训练参数怎么设,模型的收敛速度和效果都看它

训练一个 LSTM 模型,最核心的参数是 epochs 和 batch_size。epochs 是遍历整个数据集的次数,batch_size 是每次喂给模型多少个样本。这两个参数一个对应总训练轮数,一个对应每次梯度更新的样本量,设大了训练时间变长,设小了模型欠拟合,需要在实际场景里反复试。

history = model.fit( X_train, y_train, validation_data=(X_val, y_val), epochs=100, batch_size=32, verbose=1 )

epochs=100 对期末作业的数据规模是够用的,如果你的样本量只有几百,50 轮左右就能收敛。batch_size=32 是内存和速度的折中,数据量小的时候没必要改。validation_data 用来在每个 epoch 结束后计算验证集的损失,方便观察模型有没有过拟合。verbose=1 表示打印进度条,跑的时候能看到每个 epoch 的 loss 变化。

还有一个不经常写进代码,但很有用的参数是 EarlyStopping。它的作用是当验证集损失连续若干轮不下降时自动停止训练,避免白白浪费算力,也防止过拟合越来越严重。

from tensorflow.keras.callbacks import EarlyStopping early_stop = EarlyStopping( monitor="val_loss", patience=10, restore_best_weights=True ) history = model.fit( X_train, y_train, validation_data=(X_val, y_val), epochs=100, batch_size=32, callbacks=[early_stop], verbose=1 )

monitor 设置为 val_loss,表示监控验证集损失;patience=10 表示连续 10 轮没有提升就停止;restore_best_weights 会在停止时自动把模型权重恢复为验证集损失最小的那一轮。这样设置下来,代码既能在作业环境中跑得动,又能体现出你的专业性。如果老师问起来,你可以理直气壮地说:这是为了防止训练后期过拟合。

5. 预测、反归一化和画图:作业分数好看不好看,全看这三步

5.1 模型预测完了,别忘了把数据还原成原始量纲

训练完成后,用测试集做预测,这一步本身不复杂,但随后非常容易出错。预测结果是归一到 [0, 1] 区间的数值,发布于图上和原始数据完全对不上,就必须用训练时拟合好的 scaler 做 inverse_transform。

y_pred_scaled = model.predict(X_test, verbose=0) y_pred = scaler.inverse_transform(y_pred_scaled.reshape(-1, 1)) y_true = scaler.inverse_transform(y_test.reshape(-1, 1))

这里有个细节值得注意:predict 出来的形状是 (样本数, 1),inverse_transform 要求形状也是 (样本数, 1),所以直接传进去就行。但如果你在使用中不小心把 y_pred_scaled 的形状搞成了 (样本数,),就必须先 reshape(-1, 1) 再还原。reshape(-1, 1) 的意思是让 numpy 自动推算行的数量,固定列数为 1。这个操作在任何涉及 sklearn 转换器的地方都是高频操作,建议直接背下来。

如果模型预测的是多步,比如一次预测未来 7 天,那就不是简单的单步预测了。常见做法先用递归预测:把预测出的第一步结果拼到输入序列末尾,再预测下一步,循环 7 次;或者直接让模型输出多个神经元,Dense 层改成 units=7。期末作业如果要求预测未来多天,我建议用第一种递归方式实现,代码逻辑容易解释清楚,效果也不差。

5.2 loss 曲线图和预测对比图,怎么画得像论文配图

作业报告里至少要放两张图:模型训练过程中的 loss 曲线,以及测试集真实值和预测值的对比图。你会发现,凡是高分作业,图都画得异常讲究;低分作业则经常是两张图比例失调、坐标轴不标单位、图例缺失。其实用 matplotlib 画一张好看又不费力的图并不难,关键是规范。

import matplotlib.pyplot as plt plt.plot(history.history["loss"], label="train_loss") plt.plot(history.history["val_loss"], label="val_loss") plt.xlabel("epoch") plt.ylabel("loss") plt.legend() plt.title("Training and Validation Loss") plt.show()

这段代码分别把训练集和验证集的 loss 曲线画在同一张图上。看曲线时的判断标准是:两条曲线都下降并逐渐变平,说明训练正常;train_loss 持续下降而 val_loss 先降后升,说明过拟合了;两条曲线从头到尾都在大幅震荡,说明学习率过大或者数据本身噪声太强。

预测对比图更简单,取测试集的某一段区间,把真实值和预测值画在同一坐标系里。注意不要让整条时间序列挤在一张图上,这样细节完全看不清。一般选取测试集最后 50 到 100 个点画图即可。

plt.figure(figsize=(12, 5)) plt.plot(y_true[:100], label="True", linewidth=2) plt.plot(y_pred[:100], label="Pred", linewidth=2, linestyle="--") plt.xlabel("Time Step") plt.ylabel("Value") plt.legend() plt.title("Test Set Prediction Comparison") plt.show()

linestyle="--" 让预测曲线变成虚线,真实曲线用实线,一眼就能分辨。加上 linewidth=2,打印成黑白纸质版也能看清。如果你想把图做得更专业,可以加上网格,也就是 plt.grid(True),并把每条曲线的颜色改成色盲友好的配色,比如蓝和橙。这些都是细节,但老师在翻报告的时候,这点细节换个好印象绝对不亏。

5.3 指标选哪个,RMSE 还是 MAPE

光画图还不够,作业里通常会要求写误差指标。数值化评估最常见的有三个:RMSE、MAE、MAPE。RMSE 对较大误差更敏感,适合衡量预测值偏离真实值的总体程度;MAE 是绝对误差的平均值,更直观;MAPE 是百分比误差,方便在不同量纲的数据之间比较,但它在真实值接近 0 的时候会爆炸,必须加一个很小的数来防止除零。

from sklearn.metrics import mean_squared_error, mean_absolute_error rmse = mean_squared_error(y_true, y_pred, squared=False) mae = mean_absolute_error(y_true, y_pred) mape = np.mean(np.abs((y_true - y_pred) / (y_true + 1e-8))) * 100 print(f"RMSE: {rmse:.4f}") print(f"MAE: {mae:.4f}") print(f"MAPE: {mape:.2f}%")

注意 mean_squared_error 的 squared 参数,sklearn 比较新的版本里默认是 True,返回的是均方误差,不是均方根误差,所以要拿到 RMSE 需要显式传 squared=False。如果你的环境里 sklearn 版本较老,这个参数可能不存在,换成手动计算:np.sqrt(np.mean((y_true - y_pred) ** 2)),可以规避版本差异。MAPE 里的 +1e-8 就是那个防止除零的小量,几乎所有实测数据里都有真实值为 0 的情况,不加这个你的指标就是 inf。

6. 避坑指南:LSTM 期末作业最常见的 5 个翻车点

6.1 损失值变成 NaN,训练曲线直接消失

现象:训练开始后,每个 epoch 的 loss 逐渐下降,突然某一个 epoch 开始输出 nan,后续全部是 nan,模型彻底报废。

原因:最常见的是原始数据里存在缺失值或无穷值,训练过程中梯度直接变成无穷大;另一种可能是 adam 的默认学习率对该数据偏大,梯度爆炸导致参数溢出。

解决:先查数据,用 df.replace([np.inf, -np.inf], np.nan) 把无穷值替换掉,再用 dropna 或者 fillna 处理缺失;然后检查归一化后的数据范围,是否真的在 [0, 1] 区间内。如果数据没问题,可以降低学习率,或者给 optimizer 设置 clipnorm。

from tensorflow.keras.optimizers import Adam optimizer = Adam(learning_rate=0.001, clipnorm=1.0) model.compile(optimizer=optimizer, loss="mse")

clipnorm=1.0 的意思是限制每一个参数的梯度范数不超过 1,超过就缩放到 1。这是处理梯度爆炸的通用手段,写进代码里老师也看得出你是有备而来的。

6.2 训练集损失很低,验证集损失高得离谱

现象:train_loss 一直在下降,val_loss 下降到一定程度后掉头上涨,两者差距越拉越大。

原因:模型参数过多,训练样本过少,模型把训练集里的噪声也背了下来,这就是彻头彻尾的过拟合。期末作业的数据量通常只有几百到一千条,却上了两层 64+32 的 LSTM,非常容易过拟合。

解决:先增加 Dropout 比例到 0.3 或 0.4,如果还不行,就减小 units 量级,把第一层从 64 改成 32,第二层从 32 改成 16。记住一个朴素原则:参数量的上限应该远小于训练样本量。300 条数据你塞进去几万个参数,它必然是把训练集背下来了,谈不上任何泛化能力。

6.3 使用 train_test_split 随机切分,模型成绩虚高

现象:测试集上 RMSE 小得惊人,曲线完美贴合,仿佛模型已经预测到了未来。

原因:sklearn 的 train_test_split 默认 shuffle=True,时间序列被打乱后,测试集的数据分布严格混入了训练集,模型在测试时相当于开卷考试。

解决:不用 train_test_split,改用下标切片,严格按时间顺序划分。在报告里写明这一点,反而能让老师眼前一亮——大多数同学都没意识到随机切分的问题。

6.4 反归一化时用了不同名字的 scaler,预测结果严重偏移

现象:预测值和真实值的形状几乎相同,但是整体上平移了一段距离,或者缩放了某个比例。

原因:训练时用了 scaler_a = MinMaxScaler(),测试时又手滑定义了另一个 scaler_b 重新 fit 了一遍测试数据,导致两者的数据范围不同,inverse_transform 之后就出现了系统性偏移。

解决:ManBetX 整个程序里只定义一个 scaler,训练前 fit,之后所有数据变换都用同一个对象。如果你写成函数,就把 scaler 作为参数传进去,不要在函数内部重复创建。

6.5 画图时横坐标错位,真实值和预测值错开了一个点

现象:对比图里真实曲线和预测曲线走势一致,但总是错了一个点,看起来像模型慢半拍。

原因:create_sequences 生成样本时,用 data[i:i+lookback] 预测 data[i+lookback],测试集的 y_pred 与 y_test 是对齐的。但在画图时如果 y_test 的索引从 0 开始,而 y_pred 的索引从 1 开始,两个数组错位就会产生系统性的滞后错觉。

解决:打印 y_test[:5] 和 y_pred[:5],逐项比对应一致。时间序列预测本来就天然存在一个 lookback 步的信息延迟,如果是模型本身的滞后,需要靠引入差分特征或修改输入窗口来改善,但如果是索引问题,简单修正即可。

7. 把作业从"能跑"升级到"高分":一个不需要改动模型架构就能提升的细节

期末大作业的评分标准通常分成三档:能跑通是及格,能解释清楚是良好,能体现自己的思考是优秀。很多时候你不需要搭一个多复杂的模型,只要在几个细节上多想一步,就能把报告拉开差距。

我建议你做一个最简单的对照实验:lookback 分别为 7 和 30 时,记录模型在测试集上的 RMSE 和 MAPE,画成一张小表格或者一个简单的柱状图。很多学生的代码只有一套参数,跑通就算完事。你要是能展示出来"窗口太短捕捉不了周期,太长引入噪声",这个思考深度本身就是加分项。

另外一个容易被忽略的验证点是单步预测的误差累积。你可以额外写一小段代码,用递归预测未来 10 天,并同时画出真实值和预测值。这会让你的报告结构变成:先做单步预测,评估模型能力;再做多步预测,说明实际应用价值。多步预测不需要修改网络结构,只需要在预测阶段做一个循环,每次把预测结果拼进输入序列末尾。代码量不大,但展示出的内容完整度完全不同。

多步递归预测的核心逻辑是:

last_sequence = X_test[-1].copy() future_steps = 10 predictions = [] for _ in range(future_steps): next_value = model.predict(last_sequence.reshape(1, lookback, 1), verbose=0) predictions.append(next_value[0, 0]) last_sequence = np.roll(last_sequence, -1) last_sequence[-1] = next_value predictions = scaler.inverse_transform(np.array(predictions).reshape(-1, 1))

np.roll 把整个序列向左平移一位,再用预测值填补最后一位,循环下一次。这段代码的逻辑很容易在报告中用两三句话说清楚:每次只预测一步,把这一步当作已知数据,继续预测下一步。

就我个人批改类似作业或者帮别人优化代码的经历来说,这一个小循环带来的分数提升,往往比把 LSTM 层数从两层改成三层更明显。因为它体现出你理解时间序列预测中"误差累积"和"递归式预测"这两个关键概念。

另外,训练结束后一定要把模型保存下来,一行代码的事,但很多人不做:

model.save("lstm_model.keras")

下次要预测或复现实验结果的时候,直接 tf.keras.models.load_model("lstm_model.keras") 就能加载,省去重新训练的时间。作业提交时,把模型文件一起放进 zip,老师检查时可以直接复现你的预测结果。

最后说一句我的血泪经验:写这个作业时,最不值得花时间的地方是调参刷 loss。一个期末作业而已,RMSE 从 5.2 压到 4.8,在报告里也就是一行字。真正值钱的是你对每个环节的回答——为什么归一化、为什么滑窗、为什么丢一部分神经元、为什么用 RMSE 而不是 MAE。这些回答都能从今天的代码里找到答案。希望帮到你。

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

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

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

立即咨询