
简介基于长短期记忆网络LSTM的股票预测Python实现面向对量化交易和深度学习感兴趣的开发者。资源完整演示了从数据获取、缺失值清洗、标准化到0-1区间、训练集和测试集时间序列切分再到构建含输入门/遗忘门/输出门的LSTM层并配合Dense与Dropout层完成模型编译、训练评估和未来价格预测的完整流程适合作为入门项目参照。压缩包共12个文件其中3个Python脚本负责数据预处理与模型构建训练2个CSV文件存放上证综合指数及单支股票历史数据checkpoint/meta/index构成已训练好的LSTM模型另有docx说明文档整体仅959KB小巧实用。目前已有两万余人学习下载热度较高代码含stock_predict.py、stock_predict_2.py和demo.py可直接运行复现也可基于真实数据自行调整参数是理解递归神经网络门控机制与时序预测的直观样例。1. 为什么用LSTM做股票预测它到底比传统模型强在哪做股票预测的人常有一个执念想知道明天是涨是跌。ARIMA和线性回归对非线性、长期依赖的行情数据表现很差。在人工智能的序列建模任务里LSTM通过输入门、遗忘门、输出门三套门控机制把长期依赖保留下来这正是股票序列最需要的特性。这份基于LSTM的股票预测模型用Python和Keras实现完整流程行情数据获取、归一化、时间窗口构造、LSTM训练、预测还原和可视化评估整套流程能在notebook里直接跑通。适合三类人想系统学时序预测的Python开发者、做量化入门研究的学生、想快速验证LSTM在金融场景效果的人。下面把关键代码和参数选择逻辑拆开讲。2. 数据预处理与特征工程LSTM输入格式的构建细节2.1 数据源选择与基本清洗先解决数据从哪来。常见的数据接口能直接拉日线行情包括开高低收、成交量等字段没有接口时用本地CSV也可以。我一般会把数据整理成统一的DataFrame字段至少包含date、open、high、low、close、volume。拿到数据后先做三步清洗删除全空行、检查有无重复日期、对缺失值用前一天的收盘价填充。股票数据里偶尔出现停牌导致的Null值直接drop会打断时间连续性用向前填充反而更符合交易逻辑。import pandas as pd df pd.read_csv(stock_daily.csv) df[date] pd.to_datetime(df[date]) df df.drop_duplicates(subsetdate).sort_values(date) # 缺失值向前填充停牌日沿用上一交易日数据 df df.ffill() # 后向填充开头的空值 df df.bfill() print(df.shape) print(df.head())这段代码先清洗再排序保证时间序列单调递增。ffill的作用是让缺失值继承前一个有效值适用于停牌、数据源漏行等场景bfill处理数据最前面的空档。需要注意如果缺失区间过大比如连续几十天这种填充方式会引入虚假平台数据后续训练时模型会把“连续同价”学成一种模式影响预测。2.2 归一化先划分数据还是先整体缩放LSTM对输入尺度非常敏感。股票的收盘价从几元到几百元不等如果不归一化梯度下降会非常不稳定。常见做法是MinMaxScaler把数据压到0到1之间。但这里有个关键取舍是先整体归一化还是先划分数据再归一化严格的做法是先划分训练集和测试集再用训练集的统计量min和max去变换测试集。如果先对全量数据做fit_transform测试集的min/max信息会泄漏到训练过程中模型在评估时等于偷看了未来的数值范围这在时间序列场景里是典型的数据泄露。from sklearn.preprocessing import MinMaxScaler # 先按时间顺序切分再做归一化 train_size int(len(df) * 0.8) train_df df.iloc[:train_size] test_df df.iloc[train_size:] scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_df[[close]]) test_scaled scaler.transform(test_df[[close]])提示归一化必须放在划分训练集之后否则min和max会泄露测试集信息。这段代码用fit_transform只拟合训练集的min/maxtransform直接把同样的缩放规则套到测试集上。这样测试集的分布信息不会污染训练过程。后面做逆归一化时也用同一个scaler实例来inverse_transform才能还原成真实价格。2.3 时间窗口与特征维度LSTM的输入是三维张量samples, time_steps, features。time_steps就是lookback窗口表示用过去多少天的数据预测下一天。窗口长度直接影响模型感受野太短学不到中期趋势太长引入过多噪声。用日线数据时我常用20到60天初学者可以直接从30天起步。def create_sequences(data, lookback30): X, y [], [] for i in range(lookback, len(data)): X.append(data[i-lookback:i, 0]) y.append(data[i, 0]) return np.array(X), np.array(y) lookback 30 X, y create_sequences(train_scaled, lookback) X X.reshape((X.shape[0], X.shape[1], 1)) print(X shape:, X.shape, y shape:, y.shape)这里X的每一行是连续30天的收盘价序列y是对应的第31天价格。features维度设为1表示只用收盘价一个特征。如果想加入成交量或MACD等其他指标就把对应的归一化列拼接进来把最后一维改为特征数量。注意y的构造只用未来一天不要一次性构造多步目标否则模型的任务会从回归变成序列生成复杂度完全不同。注意lookback窗口不是越大越好过长的窗口会显著减少样本量也容易引入无关噪声。2.4 数据划分的坑时序数据不能随机打乱普通机器学习任务习惯用train_test_split并设置shuffleTrue但时序数据一旦打乱模型就失去了时间顺序的概念。训练集学到的是“从任意截断点预测下一帧”的模式测试时却面对连续的历史窗口效果必然偏离。常见做法是按时间点直接切片前80%训练中间10%验证最后10%测试。验证集用于early stopping测试集只评价最终模型。切片后记得用原始索引对齐日期方便后续画图对比。val_size int(len(train_scaled) * 0.85) X_train, X_val X[:val_size], X[val_size:] y_train, y_val y[:val_size], y[val_size:] # 测试集用同样的方式构造序列 X_test, y_test create_sequences(test_scaled, lookback) X_test X_test.reshape((X_test.shape[0], X_test.shape[1], 1))这里的val_size是按照“训练集的85%”算出来的也就是约占全量数据的68%。我用两层切分而不是直接三份目的是让验证集紧挨着训练集之后保证验证时使用的都是模型没见过的时期。金融数据有很强的非平稳特性如果验证集和训练集相隔很远分布差异会干扰early stopping的判断。3. 模型构建与训练流程Keras中LSTM层的关键参数3.1 网络结构设计units、层数与DropoutLSTM网络的结构直接决定模型容量。单个LSTM层加一个Dense输出层已经能拟合不少序列问题但股票数据维度高、噪声大单层往往欠拟合。常见做法是堆叠两层LSTM第一层return_sequencesTrue输出完整的序列给第二层第二层return_sequencesFalse只输出最后一个时间步的隐状态。units表示隐藏单元数我一般从50开始。太小拟合不了复杂模式太大训练时间长且容易过拟合。Dropout放在每层LSTM之后随机丢弃一部分神经元的输出防止模型把训练集的随机噪声当成规律。from keras.models import Sequential from keras.layers import LSTM, Dense, Dropout model Sequential() model.add(LSTM(units50, return_sequencesTrue, input_shape(lookback, 1))) model.add(Dropout(0.2)) model.add(LSTM(units50, return_sequencesFalse)) model.add(Dropout(0.2)) model.add(Dense(units1)) model.compile(optimizeradam, lossmean_squared_error) model.summary()input_shape(lookback, 1)表示每个样本有lookback个时间步、每步1个特征。第一层LSTM的return_sequencesTrue是为了把每个时间步的隐状态传给下一层第二层只需要最后一个输出所以是False。units50是经验值数据量小可以减到32数据量大可以加至64或128。损失函数用均方误差MSE适合回归任务预测结果是连续价格不能用交叉熵。3.2 优化器与学习率adam不是万能的Keras默认adam优化器在大多数任务上表现稳定但股票序列的loss曲面很崎岖adam的默认学习率0.001不一定最优。一个更稳的做法是先用默认值跑几个epoch观察loss下降曲线如果震荡厉害把学习率降到0.0005或0.0001如果下降太慢可以适当上调。from keras.optimizers import Adam model.compile( optimizerAdam(learning_rate0.0005), lossmean_squared_error )这里把学习率降到0.0005适合50个units的双层LSTM。调整学习率时要连batch_size一起考虑batch越大梯度越平滑学习率可以稍高batch小则梯度噪声大学习率要更保守。我通常固定batch_size为32先粗调学习率再细调网络宽度。3.3 训练循环与Early Stopping训练LSTM最忌讳跑满全部epochs。前十几个epoch模型还在收敛后面往往就开始过拟合训练loss继续降、验证loss反弹。解决办法是设置EarlyStopping监控验证集loss连续patience个epoch没有改善就停止并恢复最优权重。from keras.callbacks import EarlyStopping, ModelCheckpoint early_stop EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) checkpoint ModelCheckpoint( best_lstm_model.h5, monitorval_loss, save_best_onlyTrue ) history model.fit( X_train, y_train, epochs200, batch_size32, validation_data(X_val, y_val), callbacks[early_stop, checkpoint], verbose1 )epochs200配合patience10如果验证集loss连续10个epoch不创新低训练就会提前结束。ModelCheckpoint把验证集loss最低的模型单独存盘这样即使后面跑坏也不影响最终产物。训练完成后用history字典画loss曲线看train和val的间距两者都低且接近说明拟合良好val远高于train说明过拟合。3.4 预测、逆归一化与可视化训练完成后要预测并还原成真实价格。预测时输入必须是最近lookback天的序列输出是归一化后的数值经scaler.inverse_transform转回价格单位。predicted model.predict(X_test) predicted_prices scaler.inverse_transform(predicted) actual_prices scaler.inverse_transform(y_test.reshape(-1, 1)) import matplotlib.pyplot as plt plt.figure(figsize(12, 5)) plt.plot(range(len(actual_prices)), actual_prices, labelActual) plt.plot(range(len(predicted_prices)), predicted_prices, labelPredicted) plt.legend() plt.show()这里inverse_transform用的是训练时那个scaler。值得注意的是对y_test也做一次reshape再还原保证形状对齐。画图时x轴默认是样本序号不是真实日期如果要按日期对齐需要把test_df的date字段切到对应的长度再传入plt的x轴。预测结果经常出现“滞后效应”即预测曲线比真实曲线晚一天——这是用前一天数据预测后一天的常见现象不一定是模型错了下文会细说。4. 参数调优与避坑指南四个让模型翻车的常见问题4.1 数据泄露模型“偷看”了未来现象训练loss和验证loss都很低模型评估时R²高达0.98但换到另一段时间的数据就彻底失灵。原因最常见的是在归一化时先fit全量数据再切分或者特征构造时不小心把未来信息混进历史窗口。比如用t1当天的涨跌幅作为t时刻的特征这就是直接泄露答案。解决把归一化、特征构造全部限制在训练集范围内完成测试集和验证集的数据变换只能继承训练集的参数。检查特征的时候确认每一行X里的数据都在y之前。4.2 预测曲线滞后看起来准其实没学到趋势现象预测值和真实值几乎平行但整条曲线向右平移了一天方向上好像“模型很聪明”。原因用t时刻及之前的数据预测t1时最优解往往退化成“复制最近一天的值”。因为回归模型最小化MSE时最简单的策略就是输出昨天的价格。这不是LSTM学坏了而是评价口径和任务设定导致的偏差。解决改用多步预测目标比如预测未来3天或5天的收盘价打破“复制昨天”的捷径。或者把特征从原始价格换成收益率、涨跌幅这类平稳序列模型更难走捷径。4.3 过拟合训练集上神仙实盘上翻车现象训练集上的误差几乎为零验证集和测试集误差却完全失控且验证loss曲线在训练后期持续上升。原因模型参数过多、训练轮数过长或者样本量太小。股票数据本身信噪比极低模型很容易把随机波动当作规律。解决先减小units比如从50降到32观察loss变化增加Dropout比例到0.3early stopping的patience设为8或10并始终保持save_best_only。还可以用滚动训练的方式用过去1000天训练、预测未来5天然后滑动窗口继续做这样能看到模型在不同市场状态下的稳定性。4.4 梯度消失与NaN训练loss直接变成nan现象训练进行到某个epoch后loss变成nan之后无法恢复。原因学习率过大、梯度爆炸或输入数据存在极端值。股票数据里偶发的复权跳空会产生巨大的价格差异归一化后虽然缩小了范围但极端值仍然存在。解决先把学习率降到0.0001再用ClipNorm梯度裁剪from keras.optimizers import Adam opt Adam(learning_rate0.0001, clipnorm1.0) model.compile(optimizeropt, lossmean_squared_error)提示clipnorm是梯度裁剪的兜底方案能单独使用但一般我会同时降低学习率双管齐下更稳。clipnorm1.0会把梯度的全局范数限制在1以内从根源上避免梯度爆炸。同时检查归一化后的数据是否有超过3倍标准差的异常值如果有就先做截断处理。Keras里设置clipnorm后训练过程一般会明显稳定但代价是收敛变慢属于“后悔药”式的兜底方案。5. 评估指标与滚动预测让模型从lab走向可用5.1 只看loss是不够的方向准确率更贴近交易回归模型用MSE评估很合理但做股票预测的最终目标是判断涨跌方向。两个模型一个MSE稍大、方向判断对80%另一个MSE小、方向判断对50%后者在实盘里几乎没有价值。因此我习惯在MSE之外额外计算方向准确率和真实价格下的RMSEimport numpy as np def direction_accuracy(y_true, y_pred): true_diff np.diff(y_true.flatten()) pred_diff np.diff(y_pred.flatten()) correct np.mean((true_diff * pred_diff) 0) return correct acc direction_accuracy(actual_prices, predicted_prices) rmse np.sqrt(np.mean((actual_prices - predicted_prices) ** 2)) print(fRMSE: {rmse:.2f}, Direction Accuracy: {acc:.2%})方向准确率只看相邻两天预测值的变化方向是否与真实一致比纯误差更能体现实战能力。RMSE的单位是价格本身能直观反映预测偏离多少个点。初看时如果RMSE低但方向准确率只有50%说明模型只是在“跟着走”没有真正预测能力。5.2 滚动验证一次测试集结果不能说明问题单次划分测试集的结果随机性太大赶上单边上涨行情模型很容易显得很准。更稳妥的方法是滚动验证从数据中间某一点开始每次用过去N天训练预测未来M天然后把窗口整体后移持续多轮。每轮记录RMSE和方向准确率最后一并统计。def rolling_validate(model_fn, data, train_days1000, pred_days5, step100): results [] start train_days while start pred_days len(data): train_data data[start - train_days:start] test_data data[start:start pred_days] err model_fn(train_data, test_data) results.append(err) start step return resultsmodel_fn是一个自定义包装函数负责用train_data训练模型并返回在test_data上的误差。train_days决定每次训练用多长的历史pred_days是连续预测的天数step是每次滑动的间隔。这种做法的价值在于能观察模型在不同市场阶段的表现是只在震荡行情有效还是趋势行情也稳定。如果多轮结果方差极大说明模型的泛化能力不可靠需要回炉调整特征或结构。这也是我在复盘时最常用的习惯——不信单次的漂亮曲线只信多次滚动的稳定结果。5.3 模型保存与后续迭代训练好的模型用model.save(lstm_stock.h5)保存下次预测时直接用load_model加载即可。后续迭代的方向集中在三块一是增加特征维度成交量、RSI、MACD、市场情绪指数二是尝试多步预测三是用小学习率加更大epoch做精细训练。LSTM不是黑匣子它的输出边界、时间窗口和特征输入都是可控的关键是你愿意在验证和迭代上花多少时间。说一个我自己的教训最早做这个LSTM模型时我只看训练loss看到0.001就很兴奋结果跑滚动验证发现方向准确率只有48%比抛硬币还低。后来强制自己每次训练完必须跑一遍方向准确率和滚动验证再决定要不要继续调参。从那以后我每次训练完都把这套验证流程走一遍再不敢只盯着loss曲线自嗨。希望这篇能帮你在LSTM股票预测这条路上少走几个弯路。本文还有配套的精品资源点击获取