LSTM股票收盘价预测Python源码:从数据切分到滚动验证的完整实战
简介这是一份基于深度学习LSTM算法实现股票收盘价格预测的Python源码项目面向计算机相关专业正在做课程设计、期末大作业或毕业设计的学生以及需要项目实战练习的学习者。项目经导师指导并认可通过评审分达98分可作为高分参考范例帮助读者理解如何用LSTM对股票历史收盘价进行建模与预测。资源包共8个文件包含1个核心Python脚本、2个xml配置文件、2个txt数据与依赖说明、1个md说明文档、1个png预测效果图及gitignore等辅助文件压缩包约122KB结构精简、便于快速上手。目前已有406人学习下载。通过该源码读者可掌握LSTM网络搭建、数据预处理、模型训练与预测结果可视化的完整流程并借鉴项目目录组织与依赖管理方式为自身课题提供可复用的实现思路与排错参考。1. 用 LSTM 预测股票收盘价一份 Python 源码能跑通的最小闭环很多人第一次拿到「基于深度学习算法 LSTM 来进行股票收盘价格预测 Python 源码」这类项目卡住的地方往往不是 LSTM 本身而是数据怎么对齐、标签怎么切、预测结果为什么永远滞后一天。我见过太多人把 LSTM 当成黑匣子喂进去一段收盘价指望它吐出明天的价格结果训练 loss 降得很漂亮画出来的曲线却像一条平移过的影子。这个方向真正能落地的价值不在于预测得多准而在于把「时间序列建模」这套流程跑通数据清洗、窗口切分、特征构造、模型搭建、训练验证、结果回测。适合有 Python 基础、想拿一个完整项目练手的人也适合需要做课程设计或毕设的从业者。下面我按自己复现过多次的路径把这份源码背后的东西拆开讲清楚。2. LSTM 做收盘价预测先搞清楚它到底在学什么2.1 收盘价序列的三种建模思路与 LSTM 的定位股票收盘价是一串按交易日排列的标量最朴素的建模是把它当成回归问题用前 N 天的价格预测第 N1 天。传统机器学习模型比如线性回归、SVR 也能做但它们对时序依赖的捕捉很弱本质上把每个样本当成独立同分布的点。LSTM 的不同在于它内部有门控机制能选择性地记住或遗忘历史信息适合处理这种「当前值跟过去一段时间相关」的序列。常见做法有三种第一种是单变量单步预测输入过去 60 天收盘价输出下一天收盘价第二种是多变量单步预测除了收盘价还加入开盘价、最高价、最低价、成交量第三种是多步预测一次输出未来 5 天或 10 天。这份源码通常走的是第一种或第二种因为结构简单、容易复现。我一般会先跑通单变量版本确认数据管道没问题再往上加特征。需要明确一点LSTM 学的是序列中的统计规律不是因果规律。它看到的是「过去这样接下来大概率也这样」一旦市场结构变化模型就会翻车。所以别指望它直接拿来实盘把它当成一个时间序列预测的工程模板更合适。2.2 数据窗口切分用滑动窗口把一维序列变成监督学习样本原始数据是一列收盘价LSTM 需要的是「输入序列 标签」的样本对。滑动窗口是最常用的切分方式设定窗口长度 lookback比如 60表示用过去 60 个交易日的数据预测第 61 天。代码里通常用一个循环或 numpy 的 stride 技巧来生成。import numpy as np import pandas as pd def create_sequences(data, lookback60): data: 一维 numpy 数组已归一化的收盘价 lookback: 用过去多少天预测下一天 返回: X shape(样本数, lookback, 1), y shape(样本数,) X, y [], [] for i in range(lookback, len(data)): X.append(data[i - lookback:i]) # 过去 lookback 天 y.append(data[i]) # 第 i 天作为标签 X np.array(X) y np.array(y) # LSTM 输入要求三维: (样本, 时间步, 特征数) X np.reshape(X, (X.shape[0], X.shape[1], 1)) return X, y这段逻辑的关键在range(lookback, len(data))它保证每个样本的输入和标签在时间上不重叠错位。lookback是核心参数太小模型看不到足够的历史太大样本数减少训练变慢而且远期信息对当前预测的贡献本来就弱。我一般从 30 或 60 起步用验证集 loss 来调。注意归一化必须在切分之前做但归一化参数只能从训练集计算否则会引入未来信息这是血泪经验。2.3 归一化与反归一化别让未来数据污染训练集收盘价数值通常在几十到几百之间直接喂给 LSTM 会导致梯度不稳定。常见做法是用 MinMaxScaler 缩放到 [0,1]。但很多人会犯一个错误用全量数据 fit scaler然后再切训练测试。这等于让模型在训练时「看到」了测试集的极值属于数据泄露。正确顺序是先按时间切分训练集和测试集比如前 80% 训练后 20% 测试然后用训练集 fit scaler分别 transform 训练集和测试集预测出来的结果再用同一个 scaler 反归一化。代码里通常把 scaler 对象保存下来预测时复用。from sklearn.preprocessing import MinMaxScaler split int(len(prices) * 0.8) train_raw prices[:split] test_raw prices[split:] scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_raw.reshape(-1, 1)).flatten() test_scaled scaler.transform(test_raw.reshape(-1, 1)).flatten() # 测试集切窗口时需要把训练集最后 lookback 天拼进来保证第一个测试样本有完整历史 full_test np.concatenate([train_scaled[-lookback:], test_scaled]) X_test, y_test create_sequences(full_test, lookback)这里full_test的拼接容易被忽略。如果直接对test_scaled切窗口前 lookback 个点会被丢掉导致测试集预测从第 61 天开始跟训练集末端脱节。拼上训练集尾部预测才是连续的。反归一化时用scaler.inverse_transform注意输入形状要是二维。3. 搭一个能收敛的 LSTM 模型层数、单元数和 Dropout 怎么定3.1 Keras 搭建 LSTM 回归模型的最小代码用 TensorFlow/Keras 搭 LSTM 是最省事的路径几行就能定义一个可训练的模型。下面是一个我常用的基线结构一层 LSTM后面接 Dropout 和全连接输出。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.optimizers import Adam def build_lstm_model(lookback, units50, dropout0.2, lr0.001): model Sequential() # 第一层 LSTMreturn_sequencesFalse 表示只取最后一个时间步的输出 model.add(LSTM(units, input_shape(lookback, 1), return_sequencesFalse)) model.add(Dropout(dropout)) # 全连接层输出一个标量即预测的收盘价 model.add(Dense(1)) optimizer Adam(learning_ratelr) model.compile(optimizeroptimizer, lossmean_squared_error) return model model build_lstm_model(lookback60, units50, dropout0.2) model.summary()units50表示 LSTM 内部隐藏状态维度太小欠拟合太大容易过拟合且训练慢。return_sequencesFalse是因为我们只做单步预测只需要最后一个时间步的输出如果堆叠两层 LSTM第一层要设成 True。Dropout(0.2)在 LSTM 输出后随机丢弃 20% 的神经元缓解过拟合。损失函数用 MSE因为收盘价预测是回归任务。优化器 Adam 的默认学习率 0.001 通常够用如果 loss 震荡可以降到 0.0005。3.2 训练参数epoch、batch_size 和 EarlyStopping 的配合训练时最容易翻车的是过拟合训练 loss 一直降验证 loss 先降后升。我一般会加 EarlyStopping监控验证集 loss连续若干轮不下降就停止并恢复最优权重。from tensorflow.keras.callbacks import EarlyStopping early_stop EarlyStopping( monitorval_loss, patience10, # 连续 10 轮验证 loss 不降就停 restore_best_weightsTrue ) history model.fit( X_train, y_train, epochs100, batch_size32, validation_data(X_val, y_val), callbacks[early_stop], verbose1 )batch_size32是常见起点样本少可以降到 16 或 8。epochs100配合 EarlyStopping实际训练轮数由验证集决定。validation_data要从训练集里再切一段出来不能直接用测试集调参否则测试集就不再客观。我习惯按 训练:验证:测试 7:1.5:1.5 来分或者从训练集尾部取 10% 做验证。3.3 预测与反归一化把输出还原成真实价格训练完之后用测试集预测再反归一化才能跟真实收盘价对比。# 预测 pred_scaled model.predict(X_test) # 反归一化 pred_price scaler.inverse_transform(pred_scaled) true_price scaler.inverse_transform(y_test.reshape(-1, 1)) # 计算误差 from sklearn.metrics import mean_absolute_error, mean_squared_error mae mean_absolute_error(true_price, pred_price) rmse np.sqrt(mean_squared_error(true_price, pred_price)) print(fMAE: {mae:.2f}, RMSE: {rmse:.2f})pred_scaled形状是 (样本数, 1)inverse_transform要求二维所以y_test要 reshape。MAE 和 RMSE 是最直观的指标但别只看这两个数。我一般还会画一张对比图看预测曲线是不是整体滞后。如果预测曲线像是真实曲线向右平移了一天说明模型学到的是「昨天约等于今天」这时候要么加特征要么换建模方式。4. 避坑与排查收盘价预测里最常见的 5 个翻车现场4.1 预测曲线整体滞后一天现象画出来的预测线和真实线形状很像但总是慢半拍今天的预测值接近昨天的真实值。原因单变量 LSTM 在收盘价这种强自相关序列上最容易学到的就是「用最近的值当预测」。因为相邻交易日收盘价差异很小模型输出前一天的值就能把 MSE 降得很低。解决加入更多特征比如开盘价、最高价、最低价、成交量、涨跌幅或者把标签改成「下一日涨跌方向」做分类也可以预测收益率而不是绝对价格。我一般会先加成交量和技术指标观察滞后是否缓解。4.2 训练 loss 下降但验证 loss 上升现象训练集 MSE 一路降到很小验证集 MSE 先降后升模型在测试集上表现差。原因模型参数太多训练样本相对不足LSTM 把训练集的噪声也记住了。解决减小 units比如从 100 降到 32增大 Dropout 到 0.3 或 0.4加 L2 正则化减少训练轮数用 EarlyStopping 恢复最优权重。如果数据量实在少可以改用更简单的模型做基线对比。4.3 归一化用错导致结果完全不对现象反归一化后的预测价格数量级离谱或者全是同一个值。原因scaler 在全量数据上 fit或者训练集和测试集用了不同的 scaler或者反归一化时输入形状不对。解决严格按「先切分、再 fit 训练集、transform 测试集」的顺序保存 scaler 对象反归一化前检查数组形状是 (n, 1)。这个坑我踩过不止一次后来养成习惯每次都在切分后打印 scaler 的 data_min_ 和 data_max_ 确认。4.4 窗口切分时测试集和训练集重叠现象测试集第一个样本的输入窗口包含了训练集末尾的数据导致评估偏乐观。原因为了让测试集第一个样本有完整历史把训练集尾部拼进来了但没意识到这部分数据模型训练时见过。解决这是时间序列预测的固有难点。常见做法是留一段 gap或者接受这个重叠但在报告里说明。更严谨的做法是用 walk-forward 验证每次只用当前时间点之前的数据训练和预测。4.5 用随机划分代替时间顺序划分现象用 train_test_split 随机打乱后划分测试集里混入了未来数据指标虚高。原因时间序列不能随机打乱否则模型在训练时看到了未来的价格。解决永远按时间顺序切分前段训练后段测试。交叉验证也要用 TimeSeriesSplit不能用 KFold。5. 让预测更有参考价值多步预测与滚动验证的实操技巧单步预测只能告诉你「明天大概多少」实际参考价值有限。我后来更常用的是多步预测加滚动验证。多步预测有两种做法一种是直接多输出把 Dense 层改成输出 5 个值另一种是递归预测用预测出的明天去预测后天。直接多输出训练更稳定递归预测误差会累积但实现简单。# 直接多输出一次预测未来 5 天 def build_multi_step_model(lookback, horizon5, units64): model Sequential() model.add(LSTM(units, input_shape(lookback, 1))) model.add(Dropout(0.2)) model.add(Dense(horizon)) # 输出 horizon 个值 model.compile(optimizerAdam(0.001), lossmse) return model # 标签构造每个样本对应未来 horizon 天的价格 def create_multi_step_sequences(data, lookback60, horizon5): X, y [], [] for i in range(lookback, len(data) - horizon 1): X.append(data[i - lookback:i]) y.append(data[i:i horizon]) return np.array(X)[..., np.newaxis], np.array(y)horizon5表示预测未来 5 个交易日。标签从data[i:ihorizon]取注意循环上界要减去 horizon否则最后几个样本标签不完整。这种结构下模型输出的 5 个值共享同一个 LSTM 编码训练时梯度会同时优化 5 个预测比递归方式更稳。滚动验证是我现在必做的一步。思路是从某个时间点开始每次用之前的所有数据训练预测下一天然后窗口向前滑动一天重新训练或增量更新。这样得到的预测序列才是真正「当时能拿到」的结果。def walk_forward_validation(prices, lookback60, train_size500, step1): predictions [] for i in range(train_size, len(prices) - 1, step): train_data prices[:i] # 每次重新归一化和切窗口实际可优化为增量 scaler MinMaxScaler() scaled scaler.fit_transform(train_data.reshape(-1, 1)).flatten() X, y create_sequences(scaled, lookback) model build_lstm_model(lookback) model.fit(X, y, epochs20, batch_size32, verbose0) # 预测下一天 last_window scaled[-lookback:].reshape(1, lookback, 1) pred model.predict(last_window, verbose0) predictions.append(scaler.inverse_transform(pred)[0, 0]) return predictions这段代码每次重新训练计算量大实际可以改成固定模型、只更新输入窗口或者用增量学习。但滚动验证的思想很重要它模拟了真实使用场景避免用未来数据评估。我一般会对比滚动验证的 MAE 和一次性划分的 MAE如果差距很大说明模型对时间敏感稳定性不够。最后说一个我自己的习惯不管指标多好看我都会把预测曲线和真实曲线叠在一起看再算一下预测值的日变化率分布。如果预测值的波动明显小于真实值说明模型在「偷懒」只输出平滑的均值。这时候加特征、改损失函数或者换模型结构都比调参有用。希望帮到你。本文还有配套的精品资源点击获取