简介:这是一份面向高校时间序列预测课程设计与期末大作业的 LSTM 模型 Python 实战资源,适合需要完成股票收盘价预测任务、并希望获得可运行代码与高分参考范式的学习者。压缩包共 30 个文件、1.83MB,核心包括可运行的 Python 预测脚本、3 份源数据 Excel、1 份分析报告 Markdown,以及 16 张展示 LSTM 与 RNN 网络结构、计算过程的原理图,配合配置文件可快速还原完整项目环境。资源以“用时间序列模型学习股票收盘价、预测未来价格”为主线,从数据文件、模型讲解到报告输出均按模块整理,便于边看原理边对照代码理解训练与预测流程。已有 1792 人学习下载,适合为期末答辩或课程报告准备一份完整、清晰、可复现的时间序列预测方案。
1. 用 LSTM 预测股票收盘价:这份期末大作业代码,拆开看值不值得跑
每年期末都会有一批人卡在同一个题目上:给定一只股票的收盘价历史数据,用 LSTM 做时间序列预测,要求代码可运行、有分析报告、分数还得过得去。这份 95 分以上的 LSTM 时间序列预测 python 代码实现,恰好把整套东西打包齐了:LSTM 原理讲解图、源数据 Excel、完整的项目代码和一篇分析报告。对急着交大作业、又不想从零推导门控公式的人来说,它最直接的价值是省掉了“搭框架”的时间,你拿到手能先跑通一条完整链路:读数据、构造序列、训练模型、预测未来价格、画图写结论。
我拆这份资源时最关心的不是分数,而是三件事:模型代码能不能脱离作者环境直接跑、数据处理有没有时间顺序上的硬伤、报告里的结论是不是真的来自实验结果。下面按原理图、数据预处理、核心代码、典型坑、验证方法这条线展开,新手能照着复现,熟手可以直接拿关键参数和边界条件去改自己的任务。
2. LSTM 与 RNN 的原理差异:从四张原理图看懂门控机制,再决定参数怎么设
2.1 从 RNN 节点图看循环网络的共享权重
资源里有一张“一个 RNN 节点.png”和一张“RNN 神经网络.png”,这两张图看似基础,却是理解后面所有代码的起点。RNN 的核心是隐藏状态 (h_t) 沿着时间轴不断传递,同时每个时间步接收当前输入 (x_t)。关键点在于:整个循环过程用的是同一组权重矩阵,也就是参数共享。这正是 RNN 能处理变长序列的原因,也是它在长序列上梯度消失的根源——误差反向传播要穿过时间维度上同一个矩阵的多次连乘,连乘次数一多,梯度按指数级衰减,前面时间步的参数就基本收不到更新信号了。
对应的代码层表现非常直观:如果你在 PyTorch 里定义一个nn.RNN,它只有一个weight_ih_l0和weight_hh_l0,不管序列长度是 30 还是 300,都靠这两个矩阵在时间步上反复计算。股票收盘价这类数据,历史信息往往要在几十个交易日前就产生影响,标准 RNN 很难把这个跨度的依赖学明白。这就是为什么期末大作业普遍要求用 LSTM 而不是 RNN,也是资源里专门放一张“RNN vs LSTM 节点区别”对比图的原因。
从实现角度看,你只需要记住一个结论:LSTM 在 RNN 的基础上额外维护了一条“细胞状态”传送带 (C_t),它像一条高速公路,让信息可以很少衰减地跨时间步传递。后面所有门控计算都是为了决定“这条高速公路上装什么、删什么、放行什么”。
2.2 LSTM 的中间变量与门控计算:ft、it、ot 到底在做什么
资源里的“LSTM-2-中间变量.png”和“LSTM-3-计算过程.png”把 LSTM 的内部计算拆成了几个中间量。很多入门者看公式头晕,我习惯把 LSTM 理解成三个决策开关:
第一个是遗忘门 (f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f)),它决定过去的细胞状态 (C_{t-1}) 有多少要被保留。sigmoid 输出接近 1 就全保留,接近 0 就几乎全忘。对于股票数据,这个门在学“哪些历史价格走势不再有参考价值”,比如某个突发性的跳空缺口,之后几天就不该持续影响预测了。
第二个是输入门 (i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i)) 与候选记忆 (\tilde{C}t = \tanh(W_C \cdot [h{t-1}, x_t] + b_C)),两者相乘后决定新增什么信息写入细胞状态。注意 (i_t) 是 sigmoid 输出,负责“决定要不要写”;(\tilde{C}_t) 是 tanh 输出,负责“提供新内容”,两者的角色完全不冲突。
第三个是输出门 (o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o)),它控制当前细胞状态 (\tanh(C_t)) 中有多少要暴露给下一层和当前时间步的隐藏状态。最终 (h_t = o_t \cdot \tanh(C_t))。
这里有一个非常容易被忽略的点:LSTM-4-神经网络层.png这张图画的是时间维展开后的结构,横向是时间步,纵向是网络层。你在 PyTorch 里设num_layers=2,实际是在纵向堆叠两个 LSTM 单元——第一层的输出序列变成第二层的输入序列。这意味着第二层的输入维度不再是你原始数据的特征数,而是第一层的hidden_size。资源里代码如果默认用两层 LSTM,你改层数时一定要同步检查第一层和第二层的维度衔接。
2.3 这些原理图对应到代码里的 nn.LSTM 参数
把原理图翻译成 PyTorch 代码,其实就一行:
import torch.nn as nn lstm_layer = nn.LSTM( input_size=1, # 单变量序列,每个时间步只有收盘价一个特征 hidden_size=64, # 隐藏状态维度,也是细胞状态维度 num_layers=2, # 纵向堆叠层数 batch_first=True # 输入形状为 (batch, seq_len, input_size) )逻辑说明:input_size=1对应源数据里只用“收盘价”这一列做预测,如果你想加入成交量、开盘价等额外特征,就把这个值改成特征列的数量。hidden_size=64是 LSTM 内部中间变量的维度,也是资源里那张“LSTM-4-神经网络层”图中每个时间步输出向量的长度,它直接决定了模型容量,但不是越大越好——后面讲训练时你会发现 hidden_size 过大在样本量小的时候非常容易过拟合。num_layers=2是从原理图到代码最直接的映射,两层 LSTM 比单层多了一组门控参数,对复杂序列的拟合能力更强,但训练时间和过拟合风险同步上升。
batch_first=True是新手最容易漏掉的参数。默认情况下 PyTorch 的 LSTM 输入形状是(seq_len, batch, input_size),也就是序列长度在最前面;设成True之后,你喂进去的(batch, seq_len, input_size)才符合直觉,否则X[i]取的是第 i 个时间步而不是第 i 个样本,调试时数据形状会非常绕。资源里的代码如果用了 DataLoader,这个参数基本是必设的,你复现时先检查这一行。
从原理到代码,核心结论就一句话:你能控制的不是门控的内部细节,而是input_size(用什么特征)、hidden_size(容纳多少信息)、num_layers(多少层传递)。这三个参数定了,LSTM 内部的 W、b 全是由训练数据学习出来的,这就是为什么后面数据预处理比模型本身更容易决定项目成败。
3. 数据与预处理:从源数据.xlsx 到监督学习样本,90% 的人第一步就做错
3.1 股票收盘价序列的读取与缺失值处理
资源里的数据文件是“源数据.xlsx”,打开后大概率是两列:日期和收盘价。第一步是把它读成 DataFrame,并确认数据类型和时间顺序:
import pandas as pd df = pd.read_excel("源数据.xlsx") df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date").reset_index(drop=True) print(df.head()) print(df.isna().sum())逻辑说明:sort_values按日期升序重排是关键一步——很多 Excel 手工整理的数据并不是严格时间顺序,LSTM 训练依赖序列顺序,乱序数据会让模型学到完全错误的时间依赖关系。isna().sum()用来确认缺失值数量,如果缺失不多,常见做法是用前一个有效值填充或直接删除,我一般会用df["close"].ffill()前向填充,因为股票收盘价的缺失通常是停牌导致,用前一日价格填充比插值更接近真实市场逻辑。
参数说明:read_excel需要openpyxl库,如果没装会直接报ImportError。运行前先执行pip install openpyxl pandas numpy matplotlib torch,这是这份资源跑通需要的最小依赖集。另外要确认列名到底是close还是收盘价,资源包内的代码是英文列名还是中文列名,以 zip 里实际代码为准,自己跑的时候把列名统一一下即可。
3.2 MinMaxScaler 归一化:为什么要做,以及最常见的泄露坑
LSTM 内部用的是 sigmoid 和 tanh 激活函数,输入范围落在 0 附近时梯度最健康。股票价格动辄几十到几百,如果不做归一化,输入进 LSTM 的大数值会让 sigmoid 直接饱和,梯度趋近于零,训练半天 loss 纹丝不动。所以归一化不是可选项,是必选项。
from sklearn.preprocessing import MinMaxScaler import numpy as np # 正确做法:只对训练集 fit close = df["close"].values.reshape(-1, 1) split_idx = int(len(close) * 0.8) scaler = MinMaxScaler(feature_range=(0, 1)) train_scaled = scaler.fit_transform(close[:split_idx]) test_scaled = scaler.transform(close[split_idx:])逻辑说明:fit_transform先在训练集上计算最小值和最大值,transform再用同一组参数转换测试集。这是时间序列预测里最容易踩的坑之一——如果对全部数据先fit再切分,测试集的 min/max 信息已经参与了训练集的归一化,相当于模型在训练时“见过”了未来数据的取值范围,测试集上的误差会虚假地偏小。期末大作业答辩时老师常问这个问题,正确写法本身就是加分项。
参数说明:feature_range=(0, 1)不是唯一选择,有人用(-1, 1)配合 tanh 输出,效果差别不大。关键是不管用哪个区间,训练好模型后做预测时,必须用同一个scaler做inverse_transform把预测结果还原成真实价格,否则画出来的预测曲线数值严重失真。这点在第四章的预测代码里会再强调。
3.3 用 look_back 时间步把序列转成 X、y
归一化之后,数据还是一条时间线,不能直接喂给 LSTM。LSTM 的输入是(batch, seq_len, input_size),所以要把长序列切成一个个固定长度的窗口:每个窗口的前look_back个时间步作为X,第look_back + 1个时间步作为y。这个窗口长度就是时间步(seq_len),资源里的代码一般管它叫look_back或time_step。
def create_dataset(data, look_back=10): X, y = [], [] for i in range(len(data) - look_back): X.append(data[i:i + look_back, 0]) y.append(data[i + look_back, 0]) return np.array(X), np.array(y) look_back = 10 X_train, y_train = create_dataset(train_scaled, look_back) X_test, y_test = create_dataset(test_scaled, look_back) # 调整形状:LSTM 需要 (样本数, 时间步, 特征数) X_train = X_train.reshape(X_train.shape[0], X_train.shape[1], 1) X_test = X_test.reshape(X_test.shape[0], X_test.shape[1], 1)逻辑说明:create_dataset做的事就是滑窗。以look_back=10为例,第 0 到第 9 个收盘价预测第 10 个,第 1 到第 10 个预测第 11 个,依次滑动。注意这里不是预测未来很多天,而是滚动地用最近 10 天预测下一天。如果你想预测未来 30 天,常见做法是先预测第 11 天,再把预测值拼进输入窗口预测第 12 天,这种递归预测方式的误差会逐日累积,第五章会细讲。
参数说明:look_back是个玄学参数,选大了输入维度高、信息冗余,模型更难训练;选小了模型看不到足够的趋势信息。我一般会试 5、10、20、30 这几个档位,对比测试集误差来定,资源里如果默认值能跑出 95 分左右的报告效果,大概率选的是 10 到 20 之间的某个值。切分比例0.8也是常见设置,时间序列不能像分类任务那样随机乱切,必须保留时间顺序,这也意味着训练集永远是较早的数据,测试集永远是较晚的数据。
4. 核心代码与训练流程:从模型定义到预测画图,把调用链捋顺
4.1 LSTM 模型类:PyTorch 写法与 Keras 对应关系
资源包里的项目代码文件名是“用时间序列机器学习模型做股票收盘价格预测”,核心模型类在 PyTorch 下的标准写法如下:
import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True ) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): # x 形状: (batch, seq_len, input_size) out, (h_n, c_n) = self.lstm(x) # 取最后一个时间步的隐藏状态 last_hidden = out[:, -1, :] # (batch, hidden_size) prediction = self.fc(last_hidden) return prediction逻辑说明:nn.LSTM返回两个值,out是所有时间步的输出序列,形状是(batch, seq_len, hidden_size);(h_n, c_n)是最后一个时间步的隐藏状态和细胞状态。这里我们取out[:, -1, :],也就是序列最后一步的输出,再经过一个全连接层映射成 1 个价格值。选择最后一步是因为它浓缩了前面所有时间步的信息,这是 LSTM 做序列预测最主流的做法,资源里的代码大概率也是这个结构。
参数说明:nn.Linear(hidden_size, 1)的输入维度必须等于hidden_size,如果你改了 LSTM 的hidden_size,这里要同步改,否则直接形状报错。用 Keras 版本的同学直接对应keras.layers.LSTM(units=64, return_sequences=False)加keras.layers.Dense(1),注意 Keras 的units就是 PyTorch 的hidden_size,return_sequences=False等价于只取最后一步输出。
4.2 训练循环:loss 函数、优化器、epoch 与 batch 的取舍
模型定义好之后,训练循环是整个流程里最需要关注参数的地方。回归任务的 loss 用均方误差(MSE),优化器优先 Adam,学习率从 0.001 起步:
import torch.optim as optim from torch.utils.data import TensorDataset, DataLoader model = LSTMPredictor(input_size=1, hidden_size=64, num_layers=2) criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=0.001) # 构造 DataLoader dataset = TensorDataset( torch.FloatTensor(X_train), torch.FloatTensor(y_train).view(-1, 1) ) dataloader = DataLoader(dataset, batch_size=32, shuffle=False) num_epochs = 100 for epoch in range(num_epochs): model.train() total_loss = 0 for x_batch, y_batch in dataloader: optimizer.zero_grad() y_pred = model(x_batch) loss = criterion(y_pred, y_batch) loss.backward() optimizer.step() total_loss += loss.item() if (epoch + 1) % 10 == 0: print(f"Epoch {epoch+1}/{num_epochs}, Loss: {total_loss/len(dataloader):.6f}")逻辑说明:shuffle=False是时间序列训练和普通分类任务最大的区别。分类任务为了让每个 batch 的数据分布均匀,通常要随机打乱;但序列预测的样本是按时间顺序生成的,打乱后虽然每条样本内部的时间顺序没变,样本之间的先后关系会被破坏,模型看到的梯度方向会在时间维度上跳跃,训练反而更不稳定。资源里的代码如果跑出平滑下降的 loss 曲线,多半是保留了顺序。
参数说明:batch_size=32在股票数据这种千级样本量下偏大,如果你发现训练时 loss 震荡剧烈,可以降到 16 或 8。num_epochs=100是我给的初始值,实际使用中可以加一个早停(Early Stopping):当测试集 loss 连续 10 个 epoch 不降时停止训练,防止过拟合。判断过拟合的典型信号是训练 loss 持续下降但测试集预测曲线越来越平,这是 LSTM 在序列预测上最常见的翻车现场。
4.3 预测与反归一化:从模型输出到真实价格曲线
训练完成后,用测试集做预测,注意模型输出是 0 到 1 之间的归一化值,必须用之前保存的scaler还原成真实价格:
model.eval() with torch.no_grad(): X_test_tensor = torch.FloatTensor(X_test) y_pred_scaled = model(X_test_tensor).numpy() # 反归一化 y_pred = scaler.inverse_transform(y_pred_scaled) y_test_actual = scaler.inverse_transform(y_test.reshape(-1, 1)) # 画图对比 import matplotlib.pyplot as plt plt.figure(figsize=(12, 5)) plt.plot(y_test_actual, label="真实收盘价", color="black") plt.plot(y_pred, label="LSTM 预测", color="red", linestyle="--") plt.legend() plt.title("测试集 LSTM 预测 vs 真实价格") plt.savefig("prediction_result.png", dpi=150)逻辑说明:model.eval()切换模型到推理模式,关闭 dropout 和 batch normalization 的训练行为,这一步漏掉会导致每次预测结果不一致。torch.no_grad()告诉 PyTorch 不需要计算梯度,推理时省内存且速度更快。scaler.inverse_transform是反归一化的唯一正确姿势,它会把模型输出的数值映射回原始价格量纲,比如从 0.532 还原成 158.20 元。
参数说明:预测时输入X_test的形状必须与训练时完全一致,即(样本数, look_back, 1)。如果你在第 3.3 节里改了look_back,这里的数据形状会跟着变,不需要额外调整。画图保存时dpi=150是我常用的设置,期末报告里插图清晰度够用,资源包里的images文件夹里那几张截图应该就是这么生成的。
到这里,完整的调用链已经闭环:读数据 → 归一化 → 滑窗构造样本 → 定义模型 → 训练 → 预测 → 反归一化 → 画图。资源里的分析报告“用时间序列模型学习股票收盘价,预测未来价格.md”写的就是这条链路的结果,你跑通代码后,只需要把自己的预测图和 loss 曲线替换进报告模板即可。
5. LSTM 时间序列预测避坑实录:五个常见翻车现场与对应解法
5.1 翻车现象 1:预测曲线整体滞后,看起来像“昨天收盘价”
现象:测试集预测曲线和真实曲线形状几乎一致,但整体向右平移了一天,预测值约等于前一天的真实值,R² 看着还行,实际毫无前瞻性。
原因:股票收盘价接近随机游走,序列本身可预测性极低。LSTM 在 MSE 损失下学到的“最优策略”就是直接复制上一个时间步的值,因为这样误差最小。这不是模型 bug,而是任务本身的难度决定的。
解决:改用差分序列,把原始价格变成“今天减去昨天”的收益率序列再训练,模型要学的就从“预测绝对值”变成“预测变化方向”,难度更合理。预测出差分值后,最后累加还原成价格。报告里如实写清楚这一点,说明你理解模型边界,反而比吹嘘预测精度更能拿高分。
5.2 翻车现象 2:多步预测后期曲线趋向水平直线
现象:用递归方式预测未来 30 天,前几天还有变化,越往后预测值越接近一个常数,曲线趋于水平。
原因:递归预测时,每一步的输入都包含上一步的预测值,误差逐日累积,且 LSTM 的隐藏状态会逐步趋向训练集序列的均值,这本质上是一种“回归到均值”的行为。
解决:不要做长距离递归预测,改成“滚动回测”——用测试集已知的真实价格更新输入窗口,只预测下一步。期末大作业展示两步或三步以内的预测即可,长周期预测直接承认 LSTM 的局限,并用 ARIMA 或线性回归做对比,证明你做过模型选型评估。
5.3 翻车现象 3:训练 loss 变成 NaN
现象:训练到某个 epoch,loss 突然变成nan,之后一直无法恢复。
原因:最常见的是学习率过大导致梯度爆炸,或者输入数据里存在 NaN 值没清理干净,少数情况是 LSTM 内部数值不稳定。
解决:先把数据里的 NaN 处理干净,再用torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)做梯度裁剪,最后把学习率从 0.001 降到 0.0005 或 0.0001。检查顺序按这个来,90% 的情况是学习率问题,别一上来就怀疑模型结构。
5.4 翻车现象 4:提前用全部数据做归一化,预测结果异常准
现象:测试集预测曲线和真实值贴合得完美,几乎看不出误差,这种“好结果”反而有问题。
原因:在 3.2 节里提到过,如果scaler是在全部数据上fit的,测试集的 min/max 已经参与训练数据的缩放,相当于模型在训练时见过未来数据的分布范围,测试误差是被严重低估的。
解决:严格按时间顺序,只在训练集上fit_transform,测试集只做transform。同时检查代码里 shuffle 参数,确保没有在训练时把测试样本混进 DataLoader。这两个是期末大作业答辩时老师最常挑的刺,改对了代码,报告里的诚实验证反而更有说服力。
5.5 翻车现象 5:报告里只有 loss 下降图,没有预测对比图
现象:分析报告写了一大堆,唯独缺少“真实价格 vs 预测价格”的实际对比,只有训练 loss 曲线。
原因:很多人觉得 loss 下降到 0.001 就是模型表现好,直接照抄输出。但 loss 数值本身无量纲,经过归一化后 0.001 的 MSE 代表什么,不画图根本看不出来。
解决:报告里至少放两张图:一是训练集和测试集的 loss 曲线,二是测试集上真实和预测的价格曲线。再加两个数字:测试集 RMSE 和 MAPE(平均绝对百分比误差)。资源里的分析报告模板已经预留了这些位置,你跑完代码直接填数据就行。这张图比你写一千字都有说服力。
6. 进阶验证:用滚动预测和朴素基线证明你的 LSTM 真的有用
期末大作业拿高分的关键不在模型多复杂,而在于验证是否扎实。我给这份资源补两个进阶验证手段,做完之后报告深度会明显不一样。
第一个是滚动预测(rolling forecast),它模拟的是真实使用场景:给出截止到今天的历史数据,预测明天的收盘价,第二天有了真实数据后再更新窗口继续预测。这种验证方式能看出模型的稳定性和误差累积情况:
def rolling_forecast(model, initial_window, scaler, n_steps): model.eval() history = initial_window.copy() predictions = [] with torch.no_grad(): for _ in range(n_steps): # history 最后 look_back 个数据作为输入 input_seq = history[-look_back:].reshape(1, look_back, 1) input_tensor = torch.FloatTensor(input_seq) pred_scaled = model(input_tensor).item() predictions.append(pred_scaled) # 用真实值更新窗口,模拟每日收盘后更新数据 history = np.append(history, scaler.transform( [[test_actual[len(predictions)-1]]]).flatten()) return scaler.inverse_transform(np.array(predictions).reshape(-1, 1)) # 使用:initial_window 取测试集起点前 look_back 个真实值 init_window = test_scaled[:look_back].flatten() y_roll_pred = rolling_forecast(model, init_window, scaler, len(test_scaled) - look_back)逻辑说明:滚动预测与传统直接预测的区别在于,每一步都用真实观测值更新历史窗口,不让预测误差参与下一轮输入。这样评估的是模型“基于已知信息预测未来一步”的能力,而不是“用预测预测预测”的累计误差,结果更接近真实落地场景。
第二个是朴素基线对比。LSTM 在股票这类高噪声数据上,如果预测效果还不如“用昨天收盘价当作今天预测值”的朴素策略,说明模型根本没有学到有效信息。计算方式非常简单:naive_pred[i] = test_actual[i-1],然后对比两者的 MAPE:
from sklearn.metrics import mean_absolute_percentage_error naive_pred = test_actual[:-1] actual_aligned = test_actual[1:] mape_lstm = mean_absolute_percentage_error(actual_aligned, y_roll_pred[:-1]) mape_naive = mean_absolute_percentage_error(actual_aligned, naive_pred) print(f"LSTM MAPE: {mape_lstm:.4f}, 朴素基线 MAPE: {mape_naive:.4f}")如果 LSTM 的 MAPE 只是略低于朴素基线,不用沮丧,这是序列预测的正常水平;如果反而不如基线,那就回到第五章检查数据预处理和超参数,或者换用差分序列重新训练。报告里把这两组数字并排写出来,比任何华丽描述都有说服力——老师会看到你不仅会调包,还知道怎么验证模型有效性。
从那以后,我每次接手序列预测项目,无论多急,都强制走一遍“时间顺序切分、训练集单独归一化、滚动预测、朴素基线对比”这四步才敢下结论。这套验证习惯帮我在不止一个项目里提前嗅到了模型失效的信号,也让我在交期末大作业时心里有底。希望帮到你。
本文还有配套的精品资源,点击获取