
简介面向时间序列分析预测任务的完整LSTM项目以PM2.5浓度预测为典型应用场景适合计算机、人工智能、自动化等专业学生用于课程设计、毕业设计或大作业。项目共6个文件包含3个Python脚本、2个CSV数据集和1个README说明文档压缩包仅929KBPython脚本分别覆盖主预测流程、数据预处理与序列可视化CSV数据提供原始与整理后的输入README则说明启动方式与项目结构整体轻量直接、便于快速运行与二次修改。代码在作者毕设答辩中平均分达到96分上传前已完整测试通过可直接执行读者既能照脚本理解LSTM时序建模的基本环节也能利用现有数据和预处理逻辑尝试不同的预测目标。已有96人学习下载适合作为初学者的进阶代码范本也可作为高评分作业、课设或毕设初期的参考实现。1. 把 LSTM 时间序列预测做成一次能跑完的大作业先解决四个问题一门课的期末作业如果是 LSTM 时间序列预测通常的要求是给一份真实数据集提交能直接运行的 python 源码、文档说明和预测效果图。很多人卡在“老师机器上没有训练好的权重”这一步因为模型文件太大、相对路径写错、归一化参数没保存。其实 95 分以上的作业并不追求模型创新而是做到三件事数据切分可复现、训练过程可控、评估指标和图表能支撑结论。常见的数据集包括径流、负荷、交通流量和股价序列这类数据长度几百到几千条LSTM 不比 Transformer 差反而更容易被讲清楚。下面按数据预处理、模型调参、评估出图的顺序把直接可跑的代码路径写完整。2. 先想清楚 LSTM 在时间序列预测里的适用边界再写代码2.1 门控为什么能记住“昨天的趋势”从 RNN 到 LSTMRNN 在时间步之间共享权重本意是让每个历史状态都影响当前输出。问题是反向传播时梯度需要沿着时间步连乘序列一长梯度要么指数衰减要么爆炸。LSTM 在隐状态之外增加一条细胞状态通道由遗忘门、输入门、输出门分别控制丢弃、写入和读取。用在水文径流预报这类场景里遗忘门学到的往往是“昨天雨量大今天基流要续多少”这类跨时间步的规律这比普通 RNN 在几十步后还保留得住。三个门里遗忘门决定上一时刻细胞状态保留多少输入门决定当前候选值写进去多少输出门决定当前隐状态对外暴露多少。训练时真正学的是三组权重矩阵连接输入到门、连接上一隐状态到门、以及输出层的投影。由于门控是逐元素操作参数数量只和隐状态维度和输入维度相关不会随 lookback 长度线性膨胀这就是 LSTM 能在中等长度序列上稳定的原因。2.2 输入形状和输出形状直接决定数据切分方式在用 TensorFlow 或 PyTorch 写代码之前先记住一个关键点LSTM 的输入形状是三维的分别是样本数、时间步数和特征数。很多直接运行失败的报告问题不是网络写错而是喂进去的是二维数组。预测类型输入形状输出形状典型损失函数单变量单步(batch, lookback, 1)(batch, 1)MSE单变量多步(batch, lookback, 1)(batch, horizon)MSE多变量多步(batch, lookback, n_features)(batch, horizon)MSE这里的 lookback 是滑窗长度horizon 是未来要预测多少步batch 是训练时一次喂的样本数。例如一批 32 个样本每个样本看过去 24 小时数据里只有流量一个字段那么 X 的形状就是 (32, 24, 1)。先用 NumPy 验证形状再去搭网络能省掉大半排错时间。import numpy as np X_batch np.random.randn(32, 24, 1) # 32 个样本24 个历史时刻1 个特征 y_batch np.random.randn(32, 1) # 预测未来 1 个时刻的目标值 print(X_batch.shape, y_batch.shape)这段代码的第二个维度 24 就是 lookback第三个维度 1 是特征数量。如果改成多变量输入比如同时用上游水位和雨量预测流量就把第三维改成 2y 仍然是流量这一列。Keras 的Dense输出层会自动把最后一维压到神经元数量所以 y 的形状大多数情况下是 (样本数, 输出步数)不需要手动 reshape 成三维。2.3 单变量、多变量与多步预测的建模差异单变量预测直接用目标列自身的历史值做特征适用于数据内在规律较强、外部影响不明确的序列。多变量预测需要额外准备对齐的特征列要注意不同字段的量纲差异和缺失时间点通常要先做插值或剔除。多步预测有两种常见做法。一种是直接多步即让最后一层 Dense 输出 horizon 个值适合 3 到 7 步以内的短期预测另一种是滚动预测把前一步预测值当作下一步输入缺点是误差会累积。课程大作业里用直接多步更稳妥因为训练和预测的输入输出形状一致代码更短验证集指标也更直观。真正需要长序列预测时再考虑 seq2seq 结构但那是另一个复杂度级别了。3. 数据读取、归一化和滑动窗口切分LSTM 时间序列预处理直接可跑的写法3.1 用 pandas 读取 CSV 并检查时间索引数据准备是 LSTM 时间序列预测 Python 实现里最容易出错的部分。常见的数据集是 CSV 格式第一列时间第二列或若干列观测值。读取时不能让 pandas 默认把日期当成字符串否则按时间顺序切分时会出现“看起来连续、实际乱序”的问题。import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler df pd.read_csv(data/runoff.csv, parse_dates[date]) df df.sort_values(date).reset_index(dropTrue) # 检查缺失值和时间间隔 print(df.isna().sum()) print(df[date].diff().value_counts()) # 做线性插值时间序列不建议直接删除缺失行 df[flow] df[flow].interpolate(methodlinear) data df[flow].values.reshape(-1, 1)parse_dates让第一列变成 pandas 的 datetime 类型sort_values保证时间从旧到新。date.diff()能看出采样间隔是否固定比如正常情况下都是 1 小时如果出现 2 小时说明中间缺数据。缺失值用interpolate线性填充比dropna更适合时间序列因为保留时间轴的连续性。reshape(-1, 1)是为了让数据变成列向量后面喂给 MinMaxScaler 时不会报 shape 错误。3.2 MinMaxScaler 必须在训练集上 fit再 transform 验证集和测试集时间序列预测的归一化有一个隐藏陷阱如果先对整条序列做scaler.fit_transform再划分训练集和测试集那么测试集的信息已经参与了训练阶段的最大值和最小值计算验证指标会偏乐观。课程作业里老师可能不深究但被问到数据泄露时容易扣分。正确做法是先按时间顺序切段再分别归一化。scaler MinMaxScaler(feature_range(0, 1)) train_size int(len(data) * 0.7) val_size int(len(data) * 0.15) test_size len(data) - train_size - val_size train_raw data[:train_size] val_raw data[train_size:train_size val_size] test_raw data[train_size val_size:] train_scaled scaler.fit_transform(train_raw) val_scaled scaler.transform(val_raw) test_scaled scaler.transform(test_raw)这里先按 70%、15%、15% 切成三段然后只对训练段调用fit_transform。fit会记录训练集里的最小值和最大值transform用同一组参数去缩放验证集和测试集。这种写法保证测试集的最大值不会影响归一化结果也保证scaler.inverse_transform可以还原真实数值。3.3 用滑动窗口把序列拼成有监督样本LSTM 不能直接吃一维序列需要把序列切成 (样本, 时间步, 特征) 的三维数据。切窗口时还要注意一个顺序问题验证集和测试集应该各自连续切不能从整段归一化序列里统一切否则验证集窗口会包含训练集尾部的数据造成信息穿越。def make_sequences(input_data, lookback24, horizon1): X, y [], [] for i in range(len(input_data) - lookback - horizon 1): X.append(input_data[i:i lookback]) y.append(input_data[i lookback:i lookback horizon]) return np.array(X), np.array(y) lookback 24 horizon 1 X_train, y_train make_sequences(train_scaled, lookback, horizon) X_val, y_val make_sequences(val_scaled, lookback, horizon) X_test, y_test make_sequences(test_scaled, lookback, horizon) print(X_train.shape, y_train.shape) print(X_val.shape, y_val.shape) print(X_test.shape, y_test.shape)循环里X取当前位置往后 lookback 个点y取后面 horizon 个点。因为 LSTM 只看过去不看未来所以窗口严格保持时间先后。三个集合分开切验证集和测试集的开头会各损失 lookback 个点这是正常现象不需要额外处理。如果数据量很大可以用sliding_window_view替代循环加速但中小型课程数据集用循环更直观也不会慢到哪去。4. 构建 LSTM 模型并调出“95 分”的四个关键参数4.1 用 TensorFlow/Keras 搭一个可复现的 LSTM 模型环境建议是 Python 3.10 以上TensorFlow 2.13 或 2.15 都能直接跑安装时用pip install tensorflow。如果不想让首次运行等太久可以先用 CPU 训练数据量在几千条时单轮通常只要几秒到十几秒。下面这段代码是课程作业里最常见、也最容易解释清楚的两层 LSTM 结构。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dropout, Dense from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau model Sequential([ LSTM(64, return_sequencesTrue, input_shape(lookback, 1)), Dropout(0.2), LSTM(32, return_sequencesFalse), Dropout(0.2), Dense(1) ]) model.compile(optimizeradam, lossmse, metrics[mae]) model.summary() history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs100, batch_size32, callbacks[ EarlyStopping(monitorval_loss, patience15, restore_best_weightsTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience5, min_lr1e-5) ], verbose1 )第一层LSTM(64, return_sequencesTrue)输出的是每个时间步的隐状态给第二层 LSTM 继续处理第二层return_sequencesFalse只返回最后一个时间步的输出再接一个Dense(1)产生预测值。input_shape不写 batch 维度只写(lookback, 1)和前面切出来的 X 形状对齐。EarlyStopping是拿到高分的关键。它监控验证集 loss如果连续 15 轮没有下降就停止训练并恢复到验证集 loss 最低时的权重。ReduceLROnPlateau在验证集 loss 连续 5 轮不降时把学习率减半通常能避免 loss 卡在高位震荡。训练结束后history对象里保存了每一轮的训练和验证 loss可以直接拿来画曲线。4.2 units、lookback、dropout、batch_size 的取舍这四个参数是 LSTM 时间序列预测大作业里最常被问到的调参项直接决定了模型是欠拟合还是过拟合。参数建议范围效果偏小效果偏大units32 到 128学不到复杂周期训练 loss 高过拟合验证 loss 升高lookback12 到 48看不到日周期或周周期输入噪声太多训练变慢dropout0.1 到 0.3正则化不足模型欠拟合训练 loss 降不下去batch_size16 到 64梯度抖动收敛慢内存占用大收敛不稳定units 决定 LSTM 的“记忆容量”。流量预测这类数据用 32 和 64 通常已经足够超过 128 反而容易把噪声也记住。lookback 应该结合数据本身的周期来定小时数据至少看 24 步如果做周预测就再看 168 步但 lookback 过大时序列样本数量会减少需要根据数据集长度权衡。dropout 加在 LSTM 层之间而不是输入层不要一开始就加到 0.5否则模型很难收敛。batch_size 一般取 32如果训练集只有几百条可以降到 16。4.3 训练曲线和早停怎么看训练结束后用plt.plot(history.history[loss])和plt.plot(history.history[val_loss])画两条曲线。正常情况是两者都下降然后趋于平缓。如果训练 loss 一直降但验证 loss 先降后升说明在第 10 到 20 轮左右开始过拟合这时把EarlyStopping的 patience 调小或者加大 dropout。一种容易被忽视的情况是验证 loss 一开始就是nan。常见原因有两个一是训练数据里出现无穷大值归一化前没有做np.isinf排查二是学习率太大导致梯度爆炸。可以把ReduceLROnPlateau的初始学习率设成1e-3再在 compile 时传入optimizertf.keras.optimizers.Adam(learning_rate1e-3)。如果 loss 还是震荡就把clipnorm1.0加进 Adam 优化器梯度范数超过 1 就会被缩放这是 LSTM 训练里稳定性的最后一道保障。5. 预测结果还原、误差指标和 LSTM 时间序列大作业文档说明的加分写法5.1 预测结果逆归一化与误差指标模型输出的是 0 到 1 之间的归一化值要还原成原始流量单位必须用训练时保存的scaler做逆变换。注意scaler是在整个train_raw上拟合的所以逆变换时可以传入整列预测值。pred model.predict(X_test, verbose0) pred_flat pred.reshape(-1, 1) y_test_flat y_test.reshape(-1, 1) pred_inv scaler.inverse_transform(pred_flat) y_test_inv scaler.inverse_transform(y_test_flat) mae np.mean(np.abs(pred_inv - y_test_inv)) rmse np.sqrt(np.mean((pred_inv - y_test_inv) ** 2)) ss_res np.sum((y_test_inv - pred_inv) ** 2) ss_tot np.sum((y_test_inv - np.mean(y_test_inv)) ** 2) r2 1 - ss_res / ss_tot print(fMAE: {mae:.3f}, RMSE: {rmse:.3f}, R2: {r2:.3f})MSE在训练时作为损失函数但汇报时用MAE和RMSE更直观数值单位与原数据相同。R²越接近 1 说明预测越接近真值但要注意如果测试集本身波动很小R² 会虚高所以最好同时报告 MAE。绘图时只画测试集前 200 个真实值与预测值对比避免序列太长导致曲线糊成一团。5.2 文档说明里写清四个部分课程大作业的文档说明决定最终分数上限代码之外最值得花时间的是一张数据划分表和一段参数实验记录。按顺序写四个部分第一问题定义。说明预测对象是什么、时间粒度是多少、为什么选 LSTM 而不是 ARIMA。第二数据与预处理。写清楚原始数据集来源字段含义、缺失值处理方式、训练验证测试的划分比例以及划分时是否打乱顺序。第三模型结构。给出网络层数和每层参数数量贴出model.summary()的输出再列一张超参数表。第四实验结果。放预测对比图、loss 曲线、MAE/RMSE/R² 三个指标最后写一句“在当前数据集上模型在汛期峰值处误差偏大可能原因是训练样本中高流量样本较少”这种老实的局限分析比一味说“模型效果很好”更拿分。交付时把源码、文档说明、数据集和已训练权重放在同一个目录使用相对路径读取数据并把随机种子固定为np.random.seed(42)。这样老师按顺序运行 ipynb 单元格就能得到完全一致的图整份作业的可信度会明显超过只贴一段训练代码的提交。本文还有配套的精品资源点击获取