ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Keras LSTM实现股票价格预测:从数据预处理到模型训练全流程

用Keras LSTM实现股票价格预测:从数据预处理到模型训练全流程 简介面向量化交易与深度学习入门者一套基于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, ..., c9c101c1, c2, ..., c10c112c2, c3, ..., c11c12每行样本就是沿着时间轴滚动一格得到的这种构造方式决定了模型能看到的“记忆范围”。2.1 滑动窗口的三个边界lookback、horizon、features窗口大小在代码里通常叫lookback它表示模型每次能用多少天或多少根分钟线的历史信息。标签和窗口之间的距离叫horizon股票日线预测里最常见的是 horizon1即用历史窗口预测下一个交易日。第三个边界是特征数量只放收盘价时 features1加入成交量、开盘价、最高最低价后 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(dropTrue)这里使用对数收益率而不是简单差分是因为对数收益率在数学上可加且数值范围比价格序列收敛得多。注意dropna会把第一行缺失值删掉后续构造窗口时样本总数会比原始数据少lookback 1行这是正常现象。2.4 单向 LSTM 够用不要在做回归时偷看未来一些人在模型结构上犹豫是不是要上双向 LSTM。双向结构在训练时可以让每个时间步同时看到窗口前后的信息这对文本情感分类这类任务很有效但用在股票预测上有一个逻辑漏洞训练阶段模型读了窗口后面的数据预测阶段却拿不到后面几天的真实值。当然可以用双向结构配合完整的过去数据做特征提取但对日线级别的单变量预测来说收益很小反而增加参数量。因此这套代码里用的是单向 LSTMreturn_sequencesFalse直接输出最后一个时间步的隐藏状态再接全连接层完成回归。这也是时间序列预测里最常见、最不容易出错的配置。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(dropTrue) df df.dropna(subset[close]).reset_index(dropTrue) print(df.shape) print(df.head())读取之后的排序和去空行是标准的清洗步骤。reset_index(dropTrue)的作用是让行号从 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, lookback20, horizon1): 把时间序列切成 (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这个位置的值当 horizon1 时就是用第i-lookback到i-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(units64, activationtanh, input_shape(lookback, n_features), return_sequencesFalse), Dropout(0.2), Dense(32, activationrelu), Dropout(0.1), Dense(1) ]) model.summary()代码里input_shape(lookback, n_features)不包含 batch 维度Keras 会自动在首维补上。return_sequencesFalse表示 LSTM 只输出最后一个时间步的结果这一层输出形状是 (batch, 64)后续才能接 Dense 层。如果误设为 True输出会变成三维再连 Dense 时就会报维度错误。LSTM 层内部的激活函数默认是 tanh一般不需要改。Dropout放在 LSTM 输出之后作用是随机丢弃 20% 的神经元连接来抑制过拟合。最后接的Dense(1)没有指定激活函数默认是线性输出因为这是回归任务不需要 sigmoid 或 softmax 压缩结果。4.2 编译参数回归任务里 mae 和 mse 怎么选模型编译时最常见的配置是lossmse也就是均方误差。它会放大较大误差的惩罚训练初期收敛快缺点是遇到极端行情时 loss 会被个别样本拉高。mae则对异常值更抗干扰收敛略微慢一些但数值上更直观可以直接理解成“平均预测偏差了几个百分点”。参数推荐值使用场景lossmse默认选择适合大多数回归实验lossmae数据含有明显极端值时更抗异常值optimizeradam无需手动调学习率通用效果最好metricsmae训练日志里能直接看平均误差model.compile(optimizeradam, lossmse, metrics[mae])optimizeradam自带自适应学习率在这类小规模 LSTM 回归任务里几乎不需要换。如果你后续发现训练后期 loss 震荡剧烈可以手动把学习率从默认的 0.001 降到 0.0005做法是替换成keras.optimizers.Adam(learning_rate0.0005)。4.3 训练代码与两个必挂的回调训练过程只用model.fit一行当然可以但股票序列短、样本少模型很容易在后期过拟合。因此一般会挂上 EarlyStopping 和 ReduceLROnPlateau 两个回调前者在验证 loss 不再下降时提前停止后者在学习率停滞时自动减半让训练过程更平稳地收敛。from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau early_stop EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) lr_reduce ReduceLROnPlateau( monitorval_loss, factor0.5, patience5, min_lr1e-5 ) history model.fit( X_train, y_train, batch_size64, epochs100, validation_data(X_valid, y_valid), callbacks[early_stop, lr_reduce], verbose1 )patience代表连续多少个 epoch 没有改善才触发动作。restore_best_weightsTrue会让训练结束后模型回滚到验证 loss 最低的那组权重而不是保留最后一个 epoch 的结果这个参数在早期停止时特别关键。batch_size64对几百到几千条样本都适用如果数据量特别少可以降到 32 或 16。4.4 训练日志里看什么loss 下降曲线和过拟合信号一个正常的训练过程表现为训练 loss 和验证 loss 同步下降随后逐渐走平。如果训练 loss 持续下降而验证 loss 在某个 epoch 后转头上升就是过拟合的典型信号。代码里可以用matplotlib画两条 loss 曲线对比它们的距离。import matplotlib.pyplot as plt plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_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), verbose0 )[0, 0] future_pred.append(p) last_window np.roll(last_window, -1, axis0) last_window[-1, 0] pnp.roll将整个窗口向前平移一格最旧的时间步被丢弃空出的最后一个位置填入新预测值。这样做避免了重新跑一遍数据集也保持了每个时间步只依赖过去信息的约束。注意多步预测的误差会逐级累积预测步数越长结果越倾向于向训练集的均值回归这是模型的固有行为。最后提醒一点用上述代码做出来的结果只适合作为回归实验和技术验证不要直接用于实盘交易。股票预测是一个典型的强噪声时间序列问题任何模型给出的预测都存在巨大不确定性用滞后修复后的曲线做交易决策同样有不可控的风险。本文还有配套的精品资源点击获取
返回列表