简介这是一份基于 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, encodingutf-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 为什么一定要做 MinMaxScalerLSTM 内部用的是 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, lookback7): 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如果样本量特别大才考虑更长的窗口。作业里如果你想体现思考过程可以对比 lookback7、14、30 三组实验的 loss挑一组最好的写进报告。这个实验不复杂但很能体现你对模型的理解。4. 用 Keras 搭一个 LSTM 模型的最小可运行版本4.1 模型结构两层 LSTM 加一个 DenseKeras 在 TensorFlow 2.x 里已经成为标准接口期末作业代码里见到 tf.keras.layers.LSTM 的频率远高于其他框架。最稳妥的结构是两层 LSTM 堆叠第一层返回完整序列第二层只返回最后一步再接一个全连接层输出预测值。第一层为什么要 return_sequencesTrue因为如果你不返回完整的序列第二层 LSTM 就接收不到每个时间步的隐藏状态只能拿到最后一步的结果这会让两层结构名存实亡效果和一层几乎一样。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model Sequential([ LSTM(units64, return_sequencesTrue, input_shape(lookback, 1)), Dropout(0.2), LSTM(units32, return_sequencesFalse), Dropout(0.2), Dense(units1) ]) model.compile(optimizeradam, lossmse, 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), epochs100, batch_size32, verbose1 )epochs100 对期末作业的数据规模是够用的如果你的样本量只有几百50 轮左右就能收敛。batch_size32 是内存和速度的折中数据量小的时候没必要改。validation_data 用来在每个 epoch 结束后计算验证集的损失方便观察模型有没有过拟合。verbose1 表示打印进度条跑的时候能看到每个 epoch 的 loss 变化。还有一个不经常写进代码但很有用的参数是 EarlyStopping。它的作用是当验证集损失连续若干轮不下降时自动停止训练避免白白浪费算力也防止过拟合越来越严重。from tensorflow.keras.callbacks import EarlyStopping early_stop EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs100, batch_size32, callbacks[early_stop], verbose1 )monitor 设置为 val_loss表示监控验证集损失patience10 表示连续 10 轮没有提升就停止restore_best_weights 会在停止时自动把模型权重恢复为验证集损失最小的那一轮。这样设置下来代码既能在作业环境中跑得动又能体现出你的专业性。如果老师问起来你可以理直气壮地说这是为了防止训练后期过拟合。5. 预测、反归一化和画图作业分数好看不好看全看这三步5.1 模型预测完了别忘了把数据还原成原始量纲训练完成后用测试集做预测这一步本身不复杂但随后非常容易出错。预测结果是归一到 [0, 1] 区间的数值发布于图上和原始数据完全对不上就必须用训练时拟合好的 scaler 做 inverse_transform。y_pred_scaled model.predict(X_test, verbose0) 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 层改成 units7。期末作业如果要求预测未来多天我建议用第一种递归方式实现代码逻辑容易解释清楚效果也不差。5.2 loss 曲线图和预测对比图怎么画得像论文配图作业报告里至少要放两张图模型训练过程中的 loss 曲线以及测试集真实值和预测值的对比图。你会发现凡是高分作业图都画得异常讲究低分作业则经常是两张图比例失调、坐标轴不标单位、图例缺失。其实用 matplotlib 画一张好看又不费力的图并不难关键是规范。import matplotlib.pyplot as plt plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_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], labelTrue, linewidth2) plt.plot(y_pred[:100], labelPred, linewidth2, linestyle--) plt.xlabel(Time Step) plt.ylabel(Value) plt.legend() plt.title(Test Set Prediction Comparison) plt.show()linestyle-- 让预测曲线变成虚线真实曲线用实线一眼就能分辨。加上 linewidth2打印成黑白纸质版也能看清。如果你想把图做得更专业可以加上网格也就是 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, squaredFalse) mae mean_absolute_error(y_true, y_pred) mape np.mean(np.abs((y_true - y_pred) / (y_true 1e-8))) * 100 print(fRMSE: {rmse:.4f}) print(fMAE: {mae:.4f}) print(fMAPE: {mape:.2f}%)注意 mean_squared_error 的 squared 参数sklearn 比较新的版本里默认是 True返回的是均方误差不是均方根误差所以要拿到 RMSE 需要显式传 squaredFalse。如果你的环境里 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_rate0.001, clipnorm1.0) model.compile(optimizeroptimizer, lossmse)clipnorm1.0 的意思是限制每一个参数的梯度范数不超过 1超过就缩放到 1。这是处理梯度爆炸的通用手段写进代码里老师也看得出你是有备而来的。6.2 训练集损失很低验证集损失高得离谱现象train_loss 一直在下降val_loss 下降到一定程度后掉头上涨两者差距越拉越大。原因模型参数过多训练样本过少模型把训练集里的噪声也背了下来这就是彻头彻尾的过拟合。期末作业的数据量通常只有几百到一千条却上了两层 6432 的 LSTM非常容易过拟合。解决先增加 Dropout 比例到 0.3 或 0.4如果还不行就减小 units 量级把第一层从 64 改成 32第二层从 32 改成 16。记住一个朴素原则参数量的上限应该远小于训练样本量。300 条数据你塞进去几万个参数它必然是把训练集背下来了谈不上任何泛化能力。6.3 使用 train_test_split 随机切分模型成绩虚高现象测试集上 RMSE 小得惊人曲线完美贴合仿佛模型已经预测到了未来。原因sklearn 的 train_test_split 默认 shuffleTrue时间序列被打乱后测试集的数据分布严格混入了训练集模型在测试时相当于开卷考试。解决不用 train_test_split改用下标切片严格按时间顺序划分。在报告里写明这一点反而能让老师眼前一亮——大多数同学都没意识到随机切分的问题。6.4 反归一化时用了不同名字的 scaler预测结果严重偏移现象预测值和真实值的形状几乎相同但是整体上平移了一段距离或者缩放了某个比例。原因训练时用了 scaler_a MinMaxScaler()测试时又手滑定义了另一个 scaler_b 重新 fit 了一遍测试数据导致两者的数据范围不同inverse_transform 之后就出现了系统性偏移。解决ManBetX 整个程序里只定义一个 scaler训练前 fit之后所有数据变换都用同一个对象。如果你写成函数就把 scaler 作为参数传进去不要在函数内部重复创建。6.5 画图时横坐标错位真实值和预测值错开了一个点现象对比图里真实曲线和预测曲线走势一致但总是错了一个点看起来像模型慢半拍。原因create_sequences 生成样本时用 data[i:ilookback] 预测 data[ilookback]测试集的 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), verbose0) 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。这些回答都能从今天的代码里找到答案。希望帮到你。本文还有配套的精品资源点击获取
