ARTICLE DETAIL

资讯详情

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

LSTM股票预测实战:从数据预处理到模型评估的完整方案

LSTM股票预测实战:从数据预处理到模型评估的完整方案 简介本资源是一套面向高校本科生课程设计与量化投资初学者的LSTM股票价格预测实践方案聚焦金融时序建模核心难点解决股价非线性、长依赖特征提取与端到端预测落地问题。压缩包共107个文件9.44MB含24个Python源码文件含数据预处理、模型构建、训练与可视化主程序、19个编译缓存文件、17个备份文件、13个说明与配置文本以及HTML/CSS/JS前端展示页面和JPG图表等完整覆盖从环境搭建、数据清洗、滑动窗口构造、多层LSTM训练含Adam优化与早停机制到误差评估RMSE、MAPE与结果可视化的全流程。已有59人学习下载配套技术文档系统阐释LSTM在金融场景的适用原理并提供可直接运行的代码结构、参数调优指南及评估指标分析逻辑无需修改即可部署验证适合作为课程设计高分提交材料或量化研究入门实践范例。 做股票预测这事我一直把丑话说在前头没有人能用一段代码稳定预测明天的开盘价如果能他早就埋头闷声发财了不可能把源码开源出来。但“用LSTM预测股票价格”依然是时间序列领域里最经典、最能练手的项目之一不管你是想入门深度学习还是想理解循环神经网络在金融数据上的应用这个项目都能帮你把一整条技术链走通——从数据清洗、特征构造、模型训练到效果评估每一步都能得到直观反馈。这篇文章就把我实际跑过的一套完整方案拆开讲Python源码直接给关键步骤的取舍理由也一并说清楚。如果你有Python基础想把LSTM真正用在结构化数据上这篇内容值得你照着敲一遍。1. 项目整体设计与思路拆解1.1 这个项目到底解决什么问题用一句话概括这个项目的目标输入过去60个交易日的行情数据开盘价、最高价、最低价、收盘价、成交量让LSTM模型预测下一个交易日的收盘价。这是典型的多变量时间序列预测问题也是股票预测类项目最常见的任务框架。为什么不直接预测涨跌幅因为收盘价的回归目标更容易构造数据不需要做额外标注模型训练也稳定。预测涨跌幅虽然更接近实际交易决策但标签构造的细节比较多比如涨跌阈值怎么定、横盘算涨还是跌都会直接影响模型效果。作为一套完整系统先把收盘价回归做好再把输出加工成“趋势判断”其实是更稳妥的路线。我在做这个项目时把整个任务拆成四个子模块数据获取、数据预处理、模型构建与训练、结果评估与可视化。每个模块单独封装成函数方便以后替换数据源或调整网络结构。1.2 为什么选择LSTM而不是ARIMA或Transformer最开始我也纠结过到底用ARIMA还是LSTM。ARIMA在线性假设强、波动平稳的数据上确实有效但股票行情天然带有非线性和非平稳特征ARIMA处理这种数据需要反复做差分和定阶调参周期很长而且效果上限明显。LSTM的优势在于通过遗忘门、输入门、输出门三个门控机制可以有选择地记住长时间序列里的关键信息。这句话听起来很玄落到实际就是如果股价连续几天上涨后突然放量下跌LSTM能通过门控把“前几天的上涨趋势”和“今天的放量突变”结合起来判断而不是像简单循环网络那样容易把早期信息冲掉。至于Transformer这样更新的架构从时间序列预测的角度也有不错表现但想达到同样的效果对数据量的要求比LSTM高不少训练时间也长得多。对中小规模行情数据来说LSTM仍然是一个性价比极高的起点代码量少、训练稳健、解释起来也直观。等把LSTM跑通、理解到门控机制后再切换到其他时序模型也不迟。这也是我把这套源码用LSTM来实现的核心理由。1.3 系统整体架构整个流程如下数据采集 → 数据清洗 → 归一化 → 滑窗构造样本 → 按时间划分训练集/测试集 → 构建LSTM模型 → 训练 → 反归一化 → 计算评估指标 → 可视化。这里有一个非常容易被忽略的点就是“滑窗构造样本”和“划分训练集/测试集”的顺序。我一贯的做法是先按时间划分数据段再在每一段内部构造样本。这样能防止测试集的信息在构造样本时被模型提前看到。数据层面我使用某只股票近八年的日线数据前80%作为训练集后20%作为测试集。训练集负责让模型见过足够多的市场状态测试集用来模拟“模型面对未见过的行情”时的表现。如果直接把数据集随机打乱再切分看起来训练集和测试集都有涨有跌但实际上模型已经提前见过了测试段的价格特征这类数据泄漏在时间序列任务里是致命伤。2. 环境准备与数据获取2.1 Python环境与依赖这个项目的依赖并不复杂核心库五个就够pandas2.0.3 numpy1.24.3 yfinance0.2.28 scikit-learn1.3.0 tensorflow2.13.0 matplotlib3.7.2Python版本建议3.9到3.11之间。TensorFlow在3.12上偶尔有兼容问题没有必要为了追新给自己添堵。我实际使用的环境是Python 3.10加TensorFlow 2.13纯CPU训练也能跑只是每个epoch稍微慢一点如果机器有NVIDIA显卡装好CUDA后训练速度能提升不少。这个项目规模不大对硬件没有硬性要求。2.2 数据源选择与数据获取数据获取我习惯用yfinance它封装了Yahoo Finance的接口不需要申请API Key开箱即用。以苹果公司AAPL为例获取近八年日线数据只需下面几行import yfinance as yf df yf.download(AAPL, start2015-01-01, end2023-12-31) df df[[Open, High, Low, Close, Volume]].copy() df.dropna(inplaceTrue) print(df.head()) print(len(df))yfinance返回的数据结构比较规整列名是Open、High、Low、Close、Adj Close、Volume。需要注意股票拆股、分红会改变历史价格口径如果直接使用原始Close遇到拆股日期附近的数据会出现跳变。稳妥的做法是使用Adj Close作为收盘价的预测目标其他列继续用原始数据做特征。这个细节第一次跑的时候很容易忽略后面模型效果一旦出现莫名其妙的高误差可以回头检查一下这里。如果换成国内A股数据yfinance的数据可能不全可以换成akshare、tushare这类库它们也提供日线行情接口字段设计大同小异。切换数据源时只需要保证最终DataFrame里有Open、High、Low、Close、Volume这五列即可后面的代码基本不用改。2.3 数据清洗与归一化的正确姿势数据清洗的重点有两个一是去重二是处理缺失值。股票数据偶尔会出现同一天重复记录的情况处理方式是按下单日期去重df df[~df.index.duplicated(keepfirst)]至于停牌导致缺失的日期要分情况。如果只缺失个别日期用前向填充即可如果某段时间长期停牌建议直接把那段数据删掉因为填充出来的价格没有真实交易含义只会干扰模型。归一化我选用MinMaxScaler将数据压缩到0到1之间。这一步的目的是让不同量纲的特征价格从几元到上千元成交量可能到几千万在同一个尺度下参与计算避免模型被量纲更大的特征主导。这里有一个非常关键的细节归一化时只能对训练集单独fit测试集只做transform。很多人图省事在原始数据上直接fit_transform测试段的价格信息就混进了训练时的归一化参数里这同样属于数据泄漏。from sklearn.preprocessing import MinMaxScaler feature_cols [Open, High, Low, Close, Volume] scaler MinMaxScaler(feature_range(0, 1)) train_size int(len(df) * 0.8) train_df df.iloc[:train_size] test_df df.iloc[train_size:] scaled_train scaler.fit_transform(train_df[feature_cols]) scaled_test scaler.transform(test_df[feature_cols])这里80/20的划分是按时间顺序切分的不是随机抽样。训练段比例我试过0.85测试段只剩一年多但两段行情的风格差异可能过大所以后面还是回到0.8。训练集比例不是一个固定值如果你拿到的是十年以上的数据可以适当把训练比例降到0.75给测试集留下更长时间来验证。3. 核心代码实现与实操要点3.1 滑窗样本构造LSTM无法直接接收“从2015年到2022年的所有日线数据”作为一条样本它需要你手动构造出“特征窗口 预测目标”这样的监督学习样本。我用的是长度为60个交易日的滑窗也就是用过去大约三个月的交易数据预测下一天的收盘价。构造样本的核心代码def create_sequences(data, time_step60): X, y [], [] for i in range(len(data) - time_step): X.append(data[i:i time_step]) y.append(data[i time_step, 3]) # 第3列是Close return np.array(X), np.array(y) X_train, y_train create_sequences(scaled_train, time_step60) X_test, y_test create_sequences(scaled_test, time_step60) print(X_train shape:, X_train.shape) # (样本数, 60, 5) print(y_train shape:, y_train.shape)注意create_sequences里的y取的是data[i time_step, 3]这里第3列对应Close。如果你把列顺序改成了Open、High、Low、Volume、Close那索引就要改成4。这种硬编码索引最容易出bug更稳妥的写法是先用列表记录列名再根据列名找索引。生成的X_train形状是(样本数, 60, 5)这就是LSTM需要的三维输入格式样本数、时间步长、特征维度。很多初学者会在这里卡住以为LSTM输入是二维的其实缺少时间步维度时需要先用reshape补上第三维。3.2 模型结构设计我的网络结构是一个两层LSTM加上全连接输出层from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model Sequential([ LSTM(units50, return_sequencesTrue, input_shape(60, 5)), Dropout(0.2), LSTM(units50, return_sequencesFalse), Dropout(0.2), Dense(units25), Dense(units1) ]) model.compile(optimizeradam, lossmean_squared_error, metrics[mae]) model.summary()第一层LSTM的return_sequencesTrue意思是输出完整的序列到下一层LSTM这样第二层LSTM才能继续处理每个时间步的输出。最后一层LSTM的return_sequencesFalse只返回最后一个时间步的隐状态再接两层全连接最终输出一个预测的收盘价数值。units取50是经验值我试过32、64、128。32的错误率略高128的训练时间明显增加但效果提升有限50在数据量和训练成本之间比较平衡。Dropout设0.2目的是随机丢弃一部分神经元的输出降低过拟合风险。在股票数据这种噪声很大的场景过拟合几乎是注定会发生的事Dropout是成本最低的防御手段之一。3.3 训练策略与EarlyStopping训练时我加上了EarlyStopping监控验证集的损失当连续多个epoch没有下降就提前停止训练from tensorflow.keras.callbacks import EarlyStopping early_stop EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) history model.fit( X_train, y_train, validation_split0.1, epochs100, batch_size32, callbacks[early_stop] )validation_split0.1表示从训练数据中再留出10%作为验证集EarlyStopping用这个验证集的loss来决定是否继续训练。patience10表示连续10轮val_loss没有改善就停。restore_best_weightsTrue很关键它会把模型权重恢复到验证集loss最低的那个epoch而不是用最后被迫停止时那组已经过拟合的权重。batch_size32意味着每32个样本做一次参数更新。如果内存紧张可以调小到16如果数据量大且训练太慢可以调大到64。batch_size大小也会影响收敛稳定性batch太小时梯度噪声大太大又容易陷入局部最优32属于比较通用的折中值。3.4 反归一化与评估指标训练完成后模型输出的预测值是在0到1之间的归一化数值必须反归一化回真实价格再计算误差y_pred_scaled model.predict(X_test) # 构造与训练集相同列数的空数组再把预测值填回Close列然后逆变换 dummy np.zeros((len(y_pred_scaled), 5)) dummy[:, 3] y_pred_scaled[:, 0] y_pred scaler.inverse_transform(dummy)[:, 3] dummy_y np.zeros((len(y_test), 5)) dummy_y[:, 3] y_test y_true scaler.inverse_transform(dummy_y)[:, 3]这里用了一个小技巧MinMaxScaler的inverse_transform需要完整的特征矩阵所以先创建空矩阵把预测值填到Close对应位置逆变换后再取Close列。如果不这么做直接对单列做逆变换会得到错误结果因为Scaler在fit时记录了每列的最小值和最大值。评估指标我同时看RMSE、MAE和MAPEfrom sklearn.metrics import mean_absolute_error, mean_squared_error rmse np.sqrt(mean_squared_error(y_true, y_pred)) mae mean_absolute_error(y_true, y_pred) mape np.mean(np.abs((y_true - y_pred) / y_true)) * 100 print(fRMSE: {rmse:.4f}) print(fMAE: {mae:.4f}) print(fMAPE: {mape:.2f}%)RMSE对较大误差更敏感能反映预测中是否出现“离谱”的偏差MAE更直观单位就是价格MAPE则是百分比方便和不同价位的股票对比。这三个指标一起看能快速判断误差主要来自系统性偏移还是偶发大偏差。3.5 预测结果可视化最后把真实收盘价和预测收盘价画在同一张图上import matplotlib.pyplot as plt plt.figure(figsize(14, 6)) plt.plot(y_true, labelTrue Close, linewidth2) plt.plot(y_pred, labelPredicted Close, linewidth2, linestyle--) plt.legend() plt.title(LSTM Prediction vs True Close Price) plt.xlabel(Days (test set)) plt.ylabel(Close Price) plt.grid(alpha0.3) plt.show()画图最主要的目的是迅速发现问题。我第一次跑完测试集时发现预测曲线几乎等于前一天的真实价格往后平移了一天也就是典型的“滞后性”问题。曲线看起来拟合得很好RMSE也很低但仔细看预测值和前一天真实值高度重合这种模型实际上等于在预测“今天的价格和昨天一样”并没有学到真正的趋势规律。这个现象在第4部分会详细讲。4. 常见问题与排查技巧实录4.1 数据泄漏时间序列项目最容易犯的错数据泄漏是我见过最多的问题也是自己踩过最深的坑。泄漏的形式主要有三种。第一种是切分前先归一化。scikit-learn的fit_transform如果直接作用在整个df上会提前用测试段的最小值和最大值调整训练数据模型等于提前知道了未来价格的范围。虽然在实际测试集中看起来误差不大但一旦换到新的时间段性能会急剧下降。第二种是打乱数据次序。很多从图像分类项目转过来的朋友习惯用train_test_split的默认shuffleTrue切分股价数据这就破坏了时间序列的顺序依赖。股票数据里第100天的行情和第1天的行情本来没有因果顺序但LSTM学习的是时序依赖数据一旦被打乱时间步之间的连续性就没了。第三种是使用未来信息做特征。例如在预测第N天的收盘价时把第N天的成交量也放进X里训练虽然在构造样本时看起来合理但预测未来时根本没有当天的数据可用。我在这套代码里没有引入这类特征但换成其他特征工程时一定要检查。排查数据泄漏的方法很简单把训练和测试的归一化参数分别打印出来如果测试集单独fit后得到的Min和Max与训练集差异非常大说明两个集市的数值分布已经有差距。正常的处理是始终保持“只用训练集计算参数”。4.2 预测滞后与“看起来很像”的假象预测曲线比真实曲线滞后一天是最常见的假性成功。原因是单步预测模型在预测第N天时输入中包含了第N-1天的收盘价信息而第N-1天是已知的真实值模型发现“反正前一天的收盘价和目标值最接近”于是会倾向于直接输出前一天的收盘价。这个策略在损失函数上的得分不差但它没有任何预测能力。我自己的验证办法是做一步递推预测把模型输出的第N天预测值当作下次输入的一部分用预测价格替换真实价格再预测第N1天。如果递推几步后预测迅速偏移说明模型并没有学到长期规律只是在做“粘滞预测”。这种做法虽然会放大误差但更能真实反映多步预测下的模型能力。缓解滞后性的常用方向包括改用预测涨跌幅而不是绝对价格、在输入特征中把前一天的收盘价剔除、增加时间步长、加入更多技术指标特征。没有万能解药需要根据数据反复实验。4.3 归一化范围与极端值的影响股票的成交量分布往往有极端值比如某天突然放出天量成交。MinMaxScaler会对最大值和最小值极其敏感一个极端的成交高峰会把其他所有成交量数据压缩到很小的区间里导致模型在成交量特征上学不到太多有效信息。处理这个问题的办法有两个。第一个是使用RobustScaler代替MinMaxScalerRobustScaler基于四分位距做缩放对异常值没那么敏感。第二个是如果希望保留MinMaxScaler就先对成交量做log1p变换压缩长尾后再归一化。我建议在样本量不大时优先试RobustScaler改造起来只需要替换Scaler和对应的inverse_transform其他代码不用动。4.4 报错速查表下面是我在个人电脑和服务器上跑这套代码时遇到过的问题整理成表格方便对照报错信息原因解决办法Input 0 of layer lstm is incompatibleinput_shape设置错误维度不对检查input_shape(time_step, feature_num)确认数据三维形状Failed to find data adapter that can handle inputX_train不是numpy数组先用np.array转换数据类型y contains previously unseen labels分类任务里标签没有对齐回归任务一般不会出现检查是否误用了CategoricalCrossentropyLoss: nan学习率过大或数据中有NaN检查数据是否包含空值降低learning_rateCUDA/cuDNN initialization errorTensorFlow和CUDA版本不匹配换CPU版本跑或在环境变量中关闭GPUCUDA_VISIBLE_DEVICES还有一个比较隐蔽的问题当使用yfinance时偶尔会下载到包含NaN的日期段。我习惯在训练前加一行断言assert df[Close].notna().all(), Close列存在NaN请处理后再继续这类硬性检查能把错误提前暴露在数据源头避免模型训练到一半才爆出无法理解的报错。5. 这套方案的局限与扩展方向5.1 评估模型效果的正确姿势评估LSTM股票预测模型时低RMSE不代表模型能赚钱。我更建议把评估拆成两个层面回归层面和交易层面。回归层面看RMSE、MAE、MAPE交易层面则把预测结果转化成交易信号比如预测未来一天涨幅超过某个阈值就买入然后回测这个策略的收益率和最大回撤。只有交易层面的回测能赚钱才有实际意义。我特别建议做样本外验证。除了固定的测试集再预留最近三个月的数据完全不参与任何训练和调参等最后模型固定了才拿来做最终验证。如果最近三个月表现和测试集差异巨大说明模型对市场风格变化的泛化能力很弱需要重新设计特征或收集更多数据。5.2 为什么不要拿预测值直接去实盘这个项目作为学习项目价值极高但直接实盘交易是另一个量级的问题。股票价格不仅受历史行情影响还受财报、公告、宏观经济、市场情绪、流动性冲击等因素影响LSTM模型只能看到历史价格和成交量天然丢失了大量基本面信息和事件驱动信息。再加上市场是动态博弈的当很多人用同一个策略时策略本身也会失效。我的建议是把研究重心放在特征工程、样本外验证和风险控制上而不是追求更低的loss。5.3 可扩展的几个方向如果想把这套项目继续深挖我会推荐三个方向。第一个方向是加入更多特征。可以把技术指标MACD、RSI、布林带等作为额外特征列和原始行情一起输入LSTM。加入特征的代码改动很小只要在create_sequences之前把特征列concat到df中再更新feature_cols即可。第二个方向是把单步预测改成多步或Seq2Seq预测。先用Encoder LSTM读取过去60天的序列再用Decoder LSTM输出未来5天或10天的收盘价这样可以更好评估模型的中期趋势判断能力也更接近实际投资决策场景。第三个方向是换成Attention机制或Transformer。前面说过LSTM性价比高但当你对时序建模有了一定理解后可以尝试在LSTM层之上加入注意力层让模型自动重点关注最近几天的关键行情波动。这个改动也不是很大而且能明显提升对长序列关键信息的捕捉能力。我在实际使用中的体会是这类模型的价值更多在于逼你建立一套完整的数据处理与评估流程而不是真的给你一个印钞机。把“数据泄漏”这几个字牢牢刻在脑子里宁可代码慢一点、步骤多一点也不要让任何未来信息在无意中溜进训练集。这才是时间序列项目最核心的工程素养。本文还有配套的精品资源点击获取
返回列表