简介:基于LSTM的股票预测是金融时间序列分析的热点方向,这份PDF论文正是围绕该主题展开的完整研究资料。文档从LSTM神经网络的基本原理出发,结合最高价、最低价、收盘价、开盘价、日成交量与成交金额六个关键属性,设计了以过去15天数据预测第16天最高价的实验方案,并对比了真实值与预测值的拟合效果。资源包内仅含单个PDF文件,大小约897KB,内容涵盖数据爬取、Z-score标准化、PyTorch搭建模型、参数微调及梯度爆炸问题处理等关键技术点,配有清晰的流程图、网络结构图和公式推导,便于读者按图索骥复现实验。目前已有294人浏览学习,对于从事机器学习、数据建模或量化交易的读者而言,既能补充LSTM网络的理论认知,也能获得股票预测项目的实操参考,是一份兼具学术性与实用性的专业指导资料。
1. 基于LSTM神经网络的股票预测算法研究:一篇能直接落地复现的时序预测方案
做股票价格预测的人大多绕不开一个现实问题:股价数据本质上是带噪声的时间序列,用普通的前馈网络去拟合,结果往往差强人意。这份研究资源的核心价值在于,它把 LSTM 神经网络完整地应用到了股票最高价的短期预测上——输入前 15 天的六维行情数据(开盘价、收盘价、最高价、最低价、日成交量、成交金额),输出第 16 天的最高价,并在两支股票指数和四支国内个股上给出了实测误差数据。换句话说,这不是一篇只讲概念的理论文章,而是从数据采集、预处理、模型搭建到误差评估都走通了的完整技术路线。适合正在做时序预测相关毕设或课题的人,也适合刚接触 PyTorch 和 LSTM、想找一个最小可复现项目入门的从业者。它不能让你精准抄底逃顶,但能让你少走大量弯路。
2. 为什么是 LSTM 而不是 BP 网络:从梯度消失到三扇门的选型逻辑
2.1 股票价格的时间序列特性决定了模型必须能「记住」历史
股票价格不是独立随机事件,今天的收盘价和过去几天的价格、成交量之间存在明显的依赖关系。论文里明确提到股票价格是随机的时间序列,但「随机」不等于「无规律」——短期内趋势、均值回归这些现象都是可观测的。传统 BP 神经网络的问题在于,它默认所有输入是独立的,相邻两天的数据在它眼里没有任何顺序关系。你丢给它 15 天的数据,它会把这 15 天当作 15 个互不相干的特征,完全丢失了时间先后顺序这一层信息。
LSTM 则天然为序列数据设计。它的隐藏状态在时间步之间传递,这意味着模型在处理第 15 天的数据时,理论上还记得第 1 天到第 14 天的信息。对于股票这种「历史影响未来」的数据形态,这种结构上的契合度远高于 BP 网络。论文实验也验证了这一点:四支个股的预测误差控制在 7.9% 到 18.9% 之间,曲线走势与真实值基本吻合。
2.2 RNN 的梯度困境:BP 网络在时序任务上的致命短板
如果只是需要「记忆」,普通的循环神经网络(RNN)也能做,那为什么论文直接跳过 RNN 选用了 LSTM?这就涉及深度学习里一个经典的工程问题——梯度消失与梯度爆炸。
RNN 在处理长序列时,反向传播需要沿着时间步展开,每一层梯度都要乘以同一个权重矩阵 W。如果 W 的特征值小于 1,梯度的模会随步数指数级衰减,最终消失;如果大于 1,梯度会指数级增长,最终爆炸。梯度消失的直接后果是:网络离当前时刻越远的记忆越无法被有效学习,早期的关键信息(比如 10 天前的放量信号)在训练中根本传递不到输出层。梯度爆炸则会让 loss 在某个 step 突然跳到 NaN,训练直接崩溃。
论文里给出的解决思路非常直白——LSTM 靠三道「门」控制信息的增删,让梯度在时间轴上有一条「高速公路」可以直接穿过,而不是每一步都乘以同一个矩阵。这是在结构层面解决问题的,不是靠调参调出来的,所以稳定性远好于原版 RNN。
2.3 LSTM 的核心机制:细胞状态与遗忘门、输入门、输出门
把论文第 4.1 节的三道门展开,其实逻辑并不复杂。LSTM 在每个时间步维护两个东西:细胞状态 C_t(长期记忆)和隐藏状态 h_t(短期记忆,也是输出)。
遗忘门决定上一时刻的细胞状态 C_{t-1} 有多少要被丢弃,输入门决定当前时刻的新信息有多少要写入细胞状态,输出门决定当前细胞状态有多少要输出到隐藏状态。数学上就是论文里的公式(2)(3)(4):
f_t = sigmoid(W_f · [h_{t-1}, x_t] + b_f) # 遗忘门 i_t = sigmoid(W_i · [h_{t-1}, x_t] + b_i) # 输入门 C̃_t = tanh(W_C · [h_{t-1}, x_t] + b_C) # 候选细胞状态 C_t = f_t * C_{t-1} + i_t * C̃_t # 更新细胞状态 o_t = sigmoid(W_o · [h_{t-1}, x_t] + b_o) # 输出门 h_t = o_t * tanh(C_t) # 更新隐藏状态三个门都是 sigmoid 激活,输出 0 到 1 之间的值,相当于一个可微分的「开关」。细胞状态更新是加法的形式,梯度在这个路径上可以稳定传播,这就是 LSTM 能避免梯度消失的结构原因。理解了这个机制,你就明白了为什么论文把 LSTM 叫做「具有选择记忆性」——它确实能在训练中自主学会哪些历史信息要保留、哪些要遗忘。
2.4 LSTM 与 BP 网络的横向对照:论文实验给出的选型佐证
论文在引言部分明确指出,国内外股票预测以往多用 BP 神经网络,而 LSTM 主要用于自然语言处理,金融领域较少使用。从实验结果看,这个选型是成立的。四支个股中浦发银行误差最低(0.079),平安银行和工商银行在 0.123 和 0.126 左右,贵州茅台误差最高(0.189),整体都在工程可接受的范围内。
一个值得注意的细节是:不同股票的预测误差差异明显。这说明模型的效果不仅取决于模型本身,还跟标的的数据特性有关。茅台股价高、波动大,同样的模型误差就偏高。这在选型上是个提醒——LSTM 适合趋势性相对稳定、波动不过于剧烈的标的,遇到暴涨暴跌的行情,任何时间序列模型都会力不从心。
3. 数据工程:爬虫采集、z-score 标准化与前 15 天输入窗口的构造
3.1 行情数据采集:论文的做法与工程上的常见替代方案
论文提到数据获取采用网络爬虫的方式,通过安装相应的库函数模块快速抓取网页信息并保存到本地。具体实现上,常见做法是用 requests 拉取财经网站的历史行情 JSON 接口,或者用 akshare、tushare 这类现成库直接拿日线数据。下面这段是典型的爬虫写法,按天循环拉取并写入 CSV:
import requests import pandas as pd import time def fetch_stock_data(symbol, start_date, end_date): """从行情接口拉取日线数据,返回 DataFrame""" url = "https://example.com/api/stock_daily" params = { "symbol": symbol, "start": start_date, "end": end_date, "fields": "open,close,high,low,volume,amount" } resp = requests.get(url, params=params, timeout=10) data = resp.json()["data"] df = pd.DataFrame(data) df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date") # 必须按时间升序排列,LSTM 依赖顺序 return df df = fetch_stock_data("600519", "2015-01-05", "2018-12-08") df.to_csv("stock_600519.csv", index=False) print(df.head())两点说明:一是fields参数里指定了论文用的六个字段——开盘价、收盘价、最高价、最低价、成交量、成交金额,volume和amount分别对应该资源中的日成交量和成交金额;二是排序这步最容易漏,如果接口返回的数据不是严格按日期升序,后面的时间窗口构造会全部错位。
3.2 z-score 标准化:为什么成交量不能和价格直接拼在一起
把原始数据打印出来看,量纲差异大得离谱:贵州茅台的股价在几百元这个量级,成交量却是以万手为单位,成交金额更是动辄几十亿。如果不做处理,直接把六个维度的数据丢进 LSTM,模型会优先拟合数值大的特征,价格信息被成交量直接淹没。论文采用的是 z-score 标准化,公式是 x' = (x - μ) / σ,其中 μ 是训练集均值,σ 是训练集标准差。
这一步有极其关键的细节——μ 和 σ 只能从训练集计算,不能混入测试集数据。如果先用全部数据计算均值方差再标准化,测试集的分布信息就提前泄露到了训练过程中,模型评估会虚高,这在金融时序预测里属于严重的作弊行为。代码实现如下:
import numpy as np def zscore_fit_transform(train_df, test_df): """只在训练集上计算均值标准差,测试集用同一套参数变换""" train_stats = {} features = ["open", "close", "high", "low", "volume", "amount"] for col in features: mu = train_df[col].mean() sigma = train_df[col].std() train_stats[col] = (mu, sigma) train_df[col + "_scaled"] = (train_df[col] - mu) / sigma test_df[col + "_scaled"] = (test_df[col] - mu) / sigma return train_df, test_df, train_stats3.3 构造监督学习样本:前 15 天六个特征映射第 16 天最高价
论文里的监督方式是前 15 天的 6 个属性作为输入层数据、第 16 天作为标签。这个窗口长度是把原始数据转成监督学习样本的核心操作。对连续的日线数据,用一个滑窗切分即可:
def create_sequences(data, seq_len=15, target_col="high"): """ data: 标准化后的 DataFrame,按时间升序排列 seq_len: 15,用前 15 天数据预测第 16 天 target_col: "high",预测目标是最高价 """ feature_cols = [c for c in data.columns if c.endswith("_scaled")] feature_values = data[feature_cols].values target_values = data[target_col + "_scaled"].values X, y = [], [] for i in range(len(data) - seq_len): X.append(feature_values[i : i + seq_len]) y.append(target_values[i + seq_len]) return np.array(X), np.array(y)这里有几个参数要注意。seq_len=15是论文明确给出的窗口大小,不是经验参数而是实验设定;理论上窗口越长模型看到的「历史」越久,但也会带来更长的训练时间和更大的过拟合风险,改动这个参数需要重新做多组对照实验才能确定优劣。目标变量用的是标准化后的最高价,因为模型输出范围是 0-1 左右,训练更稳定,反归一化留到预测完了再做。
3.4 样本划分:时间序列预测为什么不能用随机打乱
论文明确写了「前 80% 作为训练集数据,后 20% 作为测试集数据」。这个划分方式与普通的分类任务完全不同——分类任务里训练集和测试集通常是随机抽样的,但时间序列如果随机打乱,训练集里会出现「未来数据」预测「过去数据」的荒谬场景,评估结果完全失真。
对于 LSTM 样本,划分时不能直接对 X 和 y 做train_test_split,必须按原始序列的时间顺序切片:
train_size = int(len(X) * 0.8) X_train, X_test = X[:train_size], X[train_size:] y_train, y_test = y[:train_size], y[train_size:]论文实验的时间范围是 2015/1/5 到 2018/12/8,约 950 个交易日,前 80% 差不多是 760 天用于训练,后 190 天用于测试。这个比例在时间序列预测里是比较主流的做法——训练数据太少模型学不到足够规律,太多又容易过拟合最近期的走势。如果数据量更大,还可以考虑滚动验证的方式,即多次用不同时间段做训练测试,取误差均值,得到更稳健的模型评估。
4. PyTorch 模型搭建与训练:网络结构、参数微调与自定义误差函数
4.1 定义 LSTM 网络结构:输入维度、隐藏层大小与输出的衔接
论文在 PyTorch 框架下搭建 LSTM 模型。网络结构的设计要点是:输入维度 input_size=6(六个行情属性),时间步数 seq_len=15,隐藏层维度 hidden_size 是第一个需要反复尝试的超参数。输出端由于任务是对最高价做回归预测,不是分类,所以最后一层是线性层而不是 softmax,输出维度是 1。
import torch import torch.nn as nn class LSTMStockPredictor(nn.Module): def __init__(self, input_size=6, hidden_size=128, num_layers=1, output_size=1): super(LSTMStockPredictor, self).__init__() self.hidden_size = hidden_size self.num_layers = num_layers 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, output_size) def forward(self, x): # x 形状: (batch_size, 15, 6) h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size) c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size) out, _ = self.lstm(x, (h0, c0)) # out 形状: (batch_size, 15, hidden_size) last_output = out[:, -1, :] # 取最后一个时间步的输出 prediction = self.fc(last_output) # (batch_size, 1) return prediction几个关键点。batch_first=True让输入形状变成(batch, seq_len, feature)而不是 PyTorch 默认的(seq_len, batch, feature),可读性更好,也省去转置的麻烦。num_layers是 LSTM 堆叠的层数,论文实验没有明确说用几层,工程上从单层起步,如果欠拟合再加到 2 层,超过 3 层在股票这种数据量级上几乎没有收益却显著增加训练时间。取最后一个时间步的输出是时序预测的标准做法——整个序列的信息都汇聚到了最后一步的隐藏状态里。
4.2 训练循环与超参数微调:从学习率到 epoch 的调试路径
训练部分用的是均方误差(MSE)作为损失函数,配合 Adam 优化器。论文强调「通过不断微调参数,不断降低误差」,这个微调过程在 PyTorch 里主要是调三个东西:学习率、batch_size 和隐藏层维度。
import torch.optim as optim def train_model(model, X_train, y_train, epochs=100, lr=0.001): criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=lr) dataset_size = X_train.shape[0] batch_size = 64 for epoch in range(epochs): model.train() total_loss = 0 for i in range(0, dataset_size, batch_size): x_batch = torch.FloatTensor(X_train[i:i+batch_size]) y_batch = torch.FloatTensor(y_train[i:i+batch_size]).view(-1, 1) optimizer.zero_grad() output = model(x_batch) loss = criterion(output, y_batch) loss.backward() optimizer.step() total_loss += loss.item() * len(x_batch) avg_loss = total_loss / dataset_size if (epoch + 1) % 20 == 0: print(f"Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.6f}")一个值得注意的工程习惯:batch 是顺序切片的,不是随机抽样的。虽然 PyTorch 的DataLoader默认支持shuffle=True,但在 LSTM 股票预测里通常不开启 shuffle。原因在于按时间顺序的 batch 保留了序列的先后结构,模型在每个 batch 内学到的是一段连续时间窗口内的关系,更接近真实交易场景。如果数据序列本身噪声很大导致训练不稳定,需要优先降低学习率而不是开 shuffle——调学习率的操作是每 20 个 epoch 观察 loss 是否下降,不降就把学习率除以 10。
4.3 自定义相对误差:绝对误差在股票预测里的陷阱
论文在误差评估上做了一个很有行业经验的决策——没有直接用预测值和真实值的差来衡量模型好坏,而是定义了相对误差 error = |prediction - y| / y。论文给出的理由非常实际:如果股价是万元量级,预测差 10 块钱算非常准,但如果股价是十元量级,同样差 10 块钱就是灾难性的。用绝对误差评估模型,等于默认高价股和低价股的误差标准一样,这显然不合理。
def relative_error(y_true, y_pred): """按论文公式 (5) 计算相对误差""" return torch.abs(y_pred - y_true) / y_true # 使用示例 errors = relative_error(y_batch, output) mean_error = errors.mean().item() print(f"平均相对误差: {mean_error:.4f}")这里有个容易踩的暗坑:训练时候用的损失函数是 MSE(绝对误差的平方),评估时候用的是相对误差,两者并不完全一致。实际工程中如果想让模型直接优化相对误差,可以把损失函数也改成相对误差的均值,但这样做梯度在优化后期容易振荡,因为当真实值接近零时分母趋近于零会让梯度爆炸。稳妥的做法是训练仍用 MSE,评估统一用相对误差,论文的实验数据也是这样处理的。
4.4 实验数据对照:四支个股的误差分布说明了什么
论文给出了四支个股在 2015/1/5 到 2018/12/8 期间的误差数据,值得逐条分析:
| 实验对象 | 误差 |
|---|---|
| 平安银行 | 0.123 |
| 工商银行 | 0.126 |
| 贵州茅台 | 0.189 |
| 浦发银行 | 0.079 |
四支股票的误差从高到低排列,贵州茅台最差、浦发银行最好。这个现象暴露了 LSTM 模型的一个真实边界:股价绝对值高的股票,模型预测的相对误差偏大。茅台的股价一度在 600 元以上,同样的相对波动对应的绝对金额更大,模型学习到的特征规律更难稳定捕捉高价股的日内波动。浦发银行的误差低到 7.9%,说明在低价股上这个模型的表现是可用的。对于想复现实验的人,这条数据可以作为模型是否调好的「参考系」——如果你的模型在浦发银行股票上误差高于 0.1,大概率是数据预处理或超参数设置有偏差,不是模型本身的问题。
5. 避坑指南:LSTM 股票预测最容易翻车的五个细节
5.1 标准化参数混入了测试集信息
现象:模型在训练集上 loss 一路下探,但测试集误差始终降不下来,甚至出现训练误差远小于测试误差的异常差距。
原因:在数据标准化时,直接用整个数据集(包含测试集)的均值和标准差做 z-score 变换,测试集的分布信息提前泄露到了训练过程中。模型「见过」测试样本的统计特征,训练时相当于开了外挂,一旦换到真实场景预测未来数据就无法复用。
解决:严格按照论文第 3 节的逻辑,只在X_train切片上计算mean和std,测试集用同一组参数变换。写完代码后检查一下:scaler.fit()是否只用了训练数据,如果用了fit_transform而不是fit再transform,就基本可以确认踩了这个坑。
5.2 训练集和测试集划分时用了随机打乱
现象:预测曲线和真实曲线看起来完美贴合,误差低到不可思议,甚至趋近于零。
原因:用 sklearn 的train_test_split默认参数切分数据时会shuffle=True,样本被随机打乱。LSTM 样本之间有严重的时间重叠——第 i 个样本的标签恰好是第 i+1 个样本的部分输入。随机划分之后,测试集里可能存在与训练集几乎是同一时段的数据,模型不需要泛化能力就能「记忆」出结果。
解决:永远按时间顺序切片,前 80% 训练后 20% 测试。更保险的做法是用 Pandas 按日期排序后,用df.iloc[:int(len(df)*0.8)]做切片,切片完再确认测试集的最小日期晚于训练集的最大日期。
5.3 预测结果忘记反归一化
现象:预测出来的「最高价」是一个 0 到 1 左右的小数,跟实际股价完全对不上号,画出来的曲线跟真实曲线数值相差几个量级。
原因:输入数据做了 z-score 标准化,模型输出自然也是标准化空间的值。如果直接把这个值当作预测股价拿去用,得到的结果是「标准化的最高价」,不是真实股价。
解决:预测完成后要做反变换 y_true = y_pred * σ + μ。注意这里的 σ 和 μ 必须和标准化训练数据时用的完全一致。工程上标准化时把统计量存成字典返回,预测时读取同一个字典做反归一化,不要重新计算,否则结果会偏差。
5.4 多步预测被当作单步预测来评估
现象:单日预测误差表现不错,但连续预测未来 10 天,误差快速累积,曲线严重偏离真实值。
原因:LSTM 训练时是用真实的历史 15 天去预测第 16 天,属于「teacher forcing」模式。但实际用做多步预测时,第 17 天的输入需要第 16 天的预测值来补位,预测误差会作为输入传递到下一步,随着步长增加误差指数级放大。论文的实验只做了下一日最高价的预测,明确提到「长时间的预测真实值与预测值个别相差较大」,这正是误差累积的表现。
解决:如果业务上确实需要多步预测,常见做法是滚动预测——每次只预测明天,把明天预测值拼到输入序列末尾,同时丢弃最早一天的数据,再预测后天。还有一种做法是直接训练多步输出模型,让 LSTM 最后一步接多个输出头,分别对应未来 1 到 5 天的预测值,但数据量和模型复杂度都要相应调整。
5.5 用绝对误差评估不同股价标的的模型
现象:在贵州茅台(单价高)上模型误差数值很大,在浦发银行(单价低)上误差很小,得出「模型适合便宜股票」的错误结论。
原因:绝对误差和股价量级直接挂钩,用绝对误差横向量级差异很大的股票,本质上是在拿「金额」而不是「准确率」做评估。论文明确提到了这个陷阱——以万为单位股价相差 10 块问题不大,以十为单位相差 10 块就是灾难,所以定义了相对误差公式。
解决:把评估指标统一换成相对误差。代码上直接用np.mean(np.abs(pred - true) / true)这一行计算。关注模型在四支个股上误差的相对排序(论文里是浦发 0.079 < 平安 0.123 < 工商 0.126 < 茅台 0.189),而不是各自的绝对差值。
6. 从复现到验证:反归一化、滚动预测与模型可用的判断标准
论文实验里纵坐标用的是归一化后的数据,没有反归一化就展示拟合曲线了。但实际做复现的人拿到模型,最终想看到的是「预测明天的最高价是 12.5 元」这种可操作的结果,所以反归一化这步必须补上。做法是拿到模型输出的标准化预测值后,用训练集的最高价均值 μ_high 和标准差 σ_high 做逆变换:pred_price = pred_scaled * σ_high + μ_high。这里的 μ_high 和 σ_high 是保存下来的,不能重新用全量数据算。
验证模型是否真正可用,我有一个自己习惯的检查流程:第一步,把测试集的预测结果反归一化后和真实最高价画在同一张图上,肉眼确认两条曲线的趋势是否跟随、峰值是否滞后。第二步,把误差拆到「上涨日」和「下跌日」两组分别计算——如果模型在下跌日误差远大于上涨日,说明它对市场情绪突变不敏感,这在实际应用里风险很大。第三步,在未参与训练的历史时段上做滚动预测,也就是模拟真实交易环境中的「昨天只知道昨天的数据」,连续预测 20 个交易日,观察误差是否在可接受范围内膨胀。
滚动预测的具体做法是:模型训练完进入 eval 模式,保持输入窗口长度为 15 天,每次只预测一个时间点,将预测值追加到序列末尾,同时丢弃序列最前端的真实数据。核心注意点是在预测过程中禁止访问未来数据,只能使用截止到当前时刻的真实观测值和之前时刻的预测值。这个验证方式和论文实验的区别在于,它更接近真实使用场景,也更严格。
关于「模型到底靠不靠谱」这件事,我从这个项目里学到的教训是:评估一个股票预测模型不能用单一指标,更不能只看整体平均误差,要看最差情况下的误差分布。浦发银行平均误差 7.9%,看起来很漂亮,但如果你能画出每天误差的分布图,会发现某些极端行情日的误差可能超过 30%,而恰好在那些天你做了交易决策,结果就是完全被市场打脸。从那以后我每次评估时序预测模型,都强制把测试集的误差分成区间统计——误差小于 10% 的样本占比多少、10% 到 20% 的多少、超过 20% 的多少,低于 60% 的样本落在误差 15% 以内,这个模型我就不敢用于实盘,只当作研究验证。希望帮到你。
本文还有配套的精品资源,点击获取