用Keras LSTM实现股票价格预测:从数据预处理到模型训练全流程
2026/9/10 6:32:30 网站建设 项目流程

简介:面向量化交易与深度学习入门者,一套基于Keras的LSTM股票预测实战资源包,完整演示股票收益率预测流程:从原始行情数据清洗与时间序列构造,到多变量特征准备、模型训练,再到预测结果反归一化还原,覆盖数据预处理、特征工程、建模与评估的完整链路。以600000.SH股票2016年3月至2017年12月为回测区间,提供损失与RMSE计算结果,可直观对比不同参数下的预测误差。压缩包共4个文件,含2个zip数据包(覆盖20支股票原始行情、损失曲线数据)、1个Python源代码脚本及1份背景介绍文档,整体约3.87MB,结构清晰。目前已有4797人学习下载,适合作为量化交易课程实验、毕业设计或自学LSTM预测应用的参考,可帮助读者减少数据准备与代码调试时间,快速复现一套可运行的股票预测基线方案,并为后续改进模型提供对照。

1. 股票预测的第一直觉为什么选 LSTM

跑通第一版 LSTM 股票预测代码的人,评价往往走两个极端:要么觉得窗口数据一喂就能画出漂亮曲线,要么发现预测值总比真实行情“慢一拍”。这两种印象都不算错,因为 LSTM 能记住序列里的短期形态,而股票数据一步一噪声,它最擅长的是在给定窗口内做非线性回归,而不是真的看穿下一根 K 线。对 Keras 用户来说,真正的门槛不在model.add,而在把行情数据整理成 (samples, time_steps, features) 这个形状。这篇文章讲的是一条完整链路:从 CSV 数据集清洗,到构造监督学习样本,再到 Keras 训练和预测结果修正。适合刚接触时间序列预测、想用 Python 做回归实验的开发者照着跑一遍。

2. 用 Keras 之前,先把股票序列改成监督学习样本

LSTM 本身不是一个“股票预测器”,它做的事情是把一段序列映射成一个输出。要让 Keras 处理股票数据,得先把连续的价格序列切成一堆“窗口 + 标签”的配对,也就是把时间序列问题改写成监督学习问题。下面这个表展示的是一组典型样本:用前 10 天的收盘价预测下一天。

样本索引X 特征(窗口内 10 个值)y 标签
0c0, c1, ..., c9c10
1c1, c2, ..., c10c11
2c2, c3, ..., c11c12

每行样本就是沿着时间轴滚动一格得到的,这种构造方式决定了模型能看到的“记忆范围”。

2.1 滑动窗口的三个边界:lookback、horizon、features

窗口大小在代码里通常叫lookback,它表示模型每次能用多少天(或多少根分钟线)的历史信息。标签和窗口之间的距离叫horizon,股票日线预测里最常见的是 horizon=1,即用历史窗口预测下一个交易日。第三个边界是特征数量,只放收盘价时 features=1;加入成交量、开盘价、最高最低价后 features 会变成 3 或 5,输入样本的宽度随之变宽。

这三个值必须在造数据集时就固定下来。常见做法是lookback取 10 到 60 之间,horizon取 1,特征的增加靠拼接多个序列列实现。实际写代码时,我一般先用单特征跑通,再逐步加特征,避免一开始就面对多维输入带来的排查困难。

2.2 Keras 的 LSTM 输入形状:为什么是三维而不是两维

刚接触 Keras 的人最容易在这里报错:Input 0 of layer "lstm" is incompatible with layer。原因是 LSTM 层期望三维输入,而 DataFrame 或 numpy 二维数组直接喂进去自然不匹配。三维的含义分别是:batch size(一次喂多少条样本)、time steps(每条样本的窗口长度)、features(每条样本每个时间步有几个数值)。

import numpy as np # 模拟 1000 条样本,每条样本含 20 个时间步,每个时间步 3 个特征 X = np.random.randn(1000, 20, 3) y = np.random.randn(1000, 1)

这里的X.shape就是 LSTM 需要的形状。第一维 1000 在训练时会按 batch 切分,第二维 20 对应lookback,第三维 3 对应特征数。写代码时很多问题不在于模型参数,而在于X_train.reshape(-1, 20, 1)这一步有没有正确还原三个维度。

2.3 预测收益率而不是价格:避开数值范围漂移

把收盘价原封不动喂给 LSTM 虽然能跑,但训练过程通常比较难受。原因是股价序列的数值范围会随着时间不断变化,比如一只票从 10 元涨到 50 元,网络要同时拟合 10 附近和 50 附近的波动,梯度会被大数值样本带走。更常见的做法是先对价格做差分或取收益率,让序列变成相对平稳、围绕零附近波动的形态。

import pandas as pd df = pd.read_csv("stock.csv") close = df["close"] # 用对数收益率替代原始价格 df["log_return"] = np.log(close).diff() df = df.dropna(subset=["log_return"]).reset_index(drop=True)

这里使用对数收益率而不是简单差分,是因为对数收益率在数学上可加,且数值范围比价格序列收敛得多。注意dropna会把第一行缺失值删掉,后续构造窗口时样本总数会比原始数据少lookback + 1行,这是正常现象。

2.4 单向 LSTM 够用:不要在做回归时偷看未来

一些人在模型结构上犹豫是不是要上双向 LSTM。双向结构在训练时可以让每个时间步同时看到窗口前后的信息,这对文本情感分类这类任务很有效,但用在股票预测上有一个逻辑漏洞:训练阶段模型读了窗口后面的数据,预测阶段却拿不到后面几天的真实值。当然可以用双向结构配合完整的过去数据做特征提取,但对日线级别的单变量预测来说,收益很小,反而增加参数量。

因此这套代码里用的是单向 LSTM,return_sequences=False,直接输出最后一个时间步的隐藏状态,再接全连接层完成回归。这也是时间序列预测里最常见、最不容易出错的配置。

3. 股票数据集构建:从 CSV 清洗到标准化切分

标题里说的“数据集”,在工程上落到两个环节:一是准备一份至少几百条的日线或分钟线行情数据,二是把原始 CSV 处理成模型直接能用的 numpy 数组。数据质量直接决定训练能否收敛,模型结构反而是相对固定的那一部分。

3.1 拿到 CSV 后的前几步:排序、加日期索引、检查缺失

公开的行情 CSV 一般至少有 date、open、high、low、close、volume 这几列。读取时先把日期列转成 datetime 并排序,避免数据源里按任意顺序存储导致窗口错位。随后要检查有没有停牌导致的空行,这些缺失值在构造样本前必须处理掉。

import pandas as pd df = pd.read_csv("stock.csv", parse_dates=["date"]) df = df.sort_values("date").reset_index(drop=True) df = df.dropna(subset=["close"]).reset_index(drop=True) print(df.shape) print(df.head())

读取之后的排序和去空行是标准的清洗步骤。reset_index(drop=True)的作用是让行号从 0 开始连续排列,后面做时间切分和窗口切片时才不会出现索引跳跃。如果数据集里有极端异常值,比如某天收盘价明显错误,可以在这一步用分位数截断处理,但幅度要控制,避免把真实行情削掉。

3.2 先切分再标准化:MinMaxScaler 只能落在训练段

数据泄漏是时间序列任务里最容易被忽略又最致命的问题。图像分类任务里随机划分训练测试集没有风险,但股票数据一旦用全量数据去拟合标准化参数,测试段的统计信息就会提前进入训练过程。常见做法是先把序列按时间顺序切成三段,再用训练段的数据去 fit 标准化器。

from sklearn.preprocessing import MinMaxScaler total = len(df) train_end = int(total * 0.7) valid_end = int(total * 0.85) train_data = df["close"].iloc[:train_end].values.reshape(-1, 1) valid_data = df["close"].iloc[train_end:valid_end].values.reshape(-1, 1) test_data = df["close"].iloc[valid_end:].values.reshape(-1, 1) scaler = MinMaxScaler(feature_range=(0, 1)) scaler.fit(train_data) # 只能 fit 训练段 train_scaled = scaler.transform(train_data) valid_scaled = scaler.transform(valid_data) test_scaled = scaler.transform(test_data)

注意这里先拟合训练段的 min 和 max,再用同一套参数去转换验证集和测试集。reshape(-1, 1)是因为 sklearn 的 scaler 不接受一维数组,必须显式变成单列二维矩阵。如果你后续要加入成交量、涨跌幅等多个特征,应该对每个特征分别 fit 对应的 scaler,而不是把整个矩阵一起缩放。

3.3 时间切分比例与收益率的取舍

常见的切分比例是 7:1.5:1.5,也就是训练集 70%、验证集 15%、测试集 15%。这个比例对几百条日线数据来说足够看到模型是否过拟合。如果用的是分钟线,数据量有几万条,可以适当把验证集压缩到 10%,训练集扩到 80%。

数据周期建议窗口长度 lookback最少样本量
分钟线60 ~ 24010000 条以上
日线10 ~ 30500 条以上
周线5 ~ 10200 条以上

如果数据集本身只有一两百条,LSTM 很难学到有价值的时间依赖,这时候把 lookback 设成 5 到 10,并把 LSTM 单元数降到 16 或 32,模型还能做基础性的演示。不要在小数据集上强行堆大网络,验证集 loss 会明显发散。

3.4 可复用的造样本函数:lookback 与 horizon 分开设计

这是整个源代码里最值得复用的函数。它把标准化后的序列变成监督学习用的 X 和 y,同时让 lookback 和 horizon 成为独立参数,方便后面做多步预测实验时直接调整。

import numpy as np def make_sequences(features, lookback=20, horizon=1): """ 把时间序列切成 (X, y) 监督学习样本。 features: 二维数组,shape = (样本数, 特征数) lookback: 每个样本包含多少个历史时间步 horizon: 标签与窗口末尾的距离,1 表示预测下一时刻 """ X, y = [], [] total = len(features) for i in range(lookback, total - horizon + 1): X.append(features[i - lookback:i, :]) y.append(features[i + horizon - 1, 0]) # 默认预测第一个特征 return np.array(X), np.array(y)

这里X的 shape 是 (样本数, lookback, 特征数),正好符合 LSTM 的三维输入要求。y取的是i + horizon - 1这个位置的值,当 horizon=1 时,就是用第i-lookbacki-1的窗口预测第i天。把 horizon 改成 2 或 5,就能验证“用过去一个月预测未来一周”这类实验,而不用改写核心循环。

4. LSTM 预测模型的 Keras 落地:从层结构到训练回调

数据集准备好之后,模型部分反而简单。一个能完成日线预测任务的 Keras 模型通常只有三到四层:LSTM 负责提取时间依赖,Dropout 抑制过拟合,Dense 负责输出预测值。这里给出一个可以直接运行的完整结构。

4.1 能直接跑的 Sequential 模型

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dropout, Dense model = Sequential([ LSTM(units=64, activation="tanh", input_shape=(lookback, n_features), return_sequences=False), Dropout(0.2), Dense(32, activation="relu"), Dropout(0.1), Dense(1) ]) model.summary()

代码里input_shape=(lookback, n_features)不包含 batch 维度,Keras 会自动在首维补上。return_sequences=False表示 LSTM 只输出最后一个时间步的结果,这一层输出形状是 (batch, 64),后续才能接 Dense 层。如果误设为 True,输出会变成三维,再连 Dense 时就会报维度错误。

LSTM 层内部的激活函数默认是 tanh,一般不需要改。Dropout放在 LSTM 输出之后,作用是随机丢弃 20% 的神经元连接来抑制过拟合。最后接的Dense(1)没有指定激活函数,默认是线性输出,因为这是回归任务,不需要 sigmoid 或 softmax 压缩结果。

4.2 编译参数:回归任务里 mae 和 mse 怎么选

模型编译时最常见的配置是loss="mse",也就是均方误差。它会放大较大误差的惩罚,训练初期收敛快;缺点是遇到极端行情时 loss 会被个别样本拉高。mae则对异常值更抗干扰,收敛略微慢一些,但数值上更直观,可以直接理解成“平均预测偏差了几个百分点”。

参数推荐值使用场景
lossmse默认选择,适合大多数回归实验
lossmae数据含有明显极端值时更抗异常值
optimizeradam无需手动调学习率,通用效果最好
metricsmae训练日志里能直接看平均误差
model.compile(optimizer="adam", loss="mse", metrics=["mae"])

optimizer="adam"自带自适应学习率,在这类小规模 LSTM 回归任务里几乎不需要换。如果你后续发现训练后期 loss 震荡剧烈,可以手动把学习率从默认的 0.001 降到 0.0005,做法是替换成keras.optimizers.Adam(learning_rate=0.0005)

4.3 训练代码与两个必挂的回调

训练过程只用model.fit一行当然可以,但股票序列短、样本少,模型很容易在后期过拟合。因此一般会挂上 EarlyStopping 和 ReduceLROnPlateau 两个回调,前者在验证 loss 不再下降时提前停止,后者在学习率停滞时自动减半,让训练过程更平稳地收敛。

from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau early_stop = EarlyStopping( monitor="val_loss", patience=10, restore_best_weights=True ) lr_reduce = ReduceLROnPlateau( monitor="val_loss", factor=0.5, patience=5, min_lr=1e-5 ) history = model.fit( X_train, y_train, batch_size=64, epochs=100, validation_data=(X_valid, y_valid), callbacks=[early_stop, lr_reduce], verbose=1 )

patience代表连续多少个 epoch 没有改善才触发动作。restore_best_weights=True会让训练结束后模型回滚到验证 loss 最低的那组权重,而不是保留最后一个 epoch 的结果,这个参数在早期停止时特别关键。batch_size=64对几百到几千条样本都适用,如果数据量特别少,可以降到 32 或 16。

4.4 训练日志里看什么:loss 下降曲线和过拟合信号

一个正常的训练过程表现为训练 loss 和验证 loss 同步下降,随后逐渐走平。如果训练 loss 持续下降而验证 loss 在某个 epoch 后转头上升,就是过拟合的典型信号。代码里可以用matplotlib画两条 loss 曲线,对比它们的距离。

import matplotlib.pyplot as plt plt.plot(history.history["loss"], label="train_loss") plt.plot(history.history["val_loss"], label="val_loss") plt.legend() plt.show()

两条曲线离得越远,说明泛化能力越弱。股票预测数据噪声大,验证 loss 波动是正常的,不要因为某个 epoch 的抖动就停下来;重点看趋势,而不是单点。

5. 预测结果慢半拍:单步 LSTM 的滞后修正与多步扩展

很多人训练完模型后画出预测曲线,发现它几乎贴着真实值走,但整体沿着同一方向偏移。这个“慢半拍”的观感,主要是因为单步预测本身是一个滚动外推过程:模型每预测一个点,都基于前面的真实窗口,因此下一时刻的预测和上一时刻的真实值天然高度相关。这不是模型故障,而是任务定义导致的。

5.1 画图前先对齐索引:预测序列要右移一个窗口

如果训练标签构造正确,预测值本身代表的是未来时刻的估计。绘图时要让预测点落在它对应的时刻上,而不是和输入窗口的最后一个点对齐。常见做法是构造一个和原始序列等长的数组,把预测段放到测试区间的对应位置上。

pred = model.predict(X_test).flatten() aligned_pred = np.full(len(scaled_close), np.nan) test_start = train_end # 测试集起点在标准化序列里的位置 aligned_pred[test_start + lookback: test_start + lookback + len(pred)] = pred

这里aligned_pred用 NaN 填充非预测区间,绘图时 matplotlib 会自动跳过 NaN。把预测位置平移lookback的原因是模型看到的是从test_start之前的窗口开始的历史数据,输出自然要落在test_start + lookback这个时间戳。

5.2 用方向准确率评估:回归 loss 在股票场景里不够直观

在股票预测实验里,loss 小不意味着模型有用。更贴近实际的是方向准确率:模型预测的涨跌方向和真实涨跌方向是否一致。这个指标通过np.sign(np.diff())计算相邻两个点的变化方向,再与预测序列的变化方向做比较。

pred_signed = np.sign(np.diff(pred)) test_signed = np.sign(np.diff(y_test)) direction_accuracy = np.mean(pred_signed == test_signed) corr = np.corrcoef(y_test, pred)[0, 1] print(f"方向准确率: {direction_accuracy:.3f}") print(f"预测与真实相关系数: {corr:.3f}")

方向准确率在 0.5 附近意味着模型基本没有区分涨跌的能力,明显高于 0.55 才有参考价值。相关系数大于 0.9 时,预测曲线与真实曲线形态高度相似,但这也暗示了滞后特征明显:因为相邻时刻股价天然相关,模型相当于学到了“下一时刻接近当前时刻”。这两个指标要放在一起看,不要只看其中一个。

5.3 多步预测要改的三处代码:horizon、输出维度、迭代预测

想把“预测下一天”扩展成“预测未来五天”,只需要改三个地方。第一是把make_sequences里的horizon参数从 1 改成 5,让标签变成窗口之后第五个交易日的值。第二是把输出层的Dense(1)改成Dense(horizon),让模型一次输出多个未来时刻的预测。第三是推理时的预测方式,要用迭代预测:把新预测值当作下一轮窗口的最后一帧,逐步向后滚动。

last_window = X_test[-1].copy() # 最后一个测试样本,(lookback, features) future_pred = [] for _ in range(5): p = model.predict( last_window.reshape(1, lookback, n_features), verbose=0 )[0, 0] future_pred.append(p) last_window = np.roll(last_window, -1, axis=0) last_window[-1, 0] = p

np.roll将整个窗口向前平移一格,最旧的时间步被丢弃,空出的最后一个位置填入新预测值。这样做避免了重新跑一遍数据集,也保持了每个时间步只依赖过去信息的约束。注意多步预测的误差会逐级累积,预测步数越长,结果越倾向于向训练集的均值回归,这是模型的固有行为。

最后提醒一点:用上述代码做出来的结果只适合作为回归实验和技术验证,不要直接用于实盘交易。股票预测是一个典型的强噪声时间序列问题,任何模型给出的预测都存在巨大不确定性,用滞后修复后的曲线做交易决策同样有不可控的风险。

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

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

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

立即咨询