ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

循环神经网络股票预测源码复现:从LSTM原理到工程避坑

循环神经网络股票预测源码复现:从LSTM原理到工程避坑 简介本资源是一套基于Python实现的循环神经网络RNN股票价格预测完整项目源码面向计算机、金融工程或人工智能方向的本科生及入门级深度学习学习者适用于毕业设计、课程大作业与时间序列建模实践。项目采用LSTM/GRU等典型RNN结构集成数据预处理、模型训练、超参调优、结果可视化与多步预测功能代码经过本地环境含TensorFlow/Keras完整编译验证评审得分达95分以上难度适中且经助教审定具备教学示范性与工程可复现性。压缩包共125个文件含75个Python核心脚本涵盖模型定义、训练循环、评估逻辑、26个CSV历史股价与污染数据用于多变量特征融合实验、15个文本配置与日志文件以及检查点、H5模型权重等运行必需文件整体体积仅5.46MB轻量易部署。目前已有237人下载学习配套结构清晰、模块解耦合理便于理解RNN时序建模全流程并快速迁移至其他金融时间序列任务。 做量化交易或者深度学习时间序列的同行一定遇到过这种场景拿到一份“预测模型源码包”解压一看数据在哪儿脚本从哪儿跑起代码里的 LSTM 参数凭什么这么设跑出来的图线到底是“预测”还是“照着历史描了一遍”这篇博文不是把源码解压出来给你罗列注释而是按工程复现的角度把“基于Python的循环神经网络股票价格预测源码.zip”这套项目从设计思路、数据构造、模型原理到调参避坑完整捻一遍。适合三类人刚入门想找个完整深度学习案例练手的 Python 开发者想用 RNN 试试金融时间序列的量化爱好者以及拿到源码但不知道怎么改造成自己股票池的人。读完你会发现预测准不准是另一回事但这一整套流水线思维才是压缩包里最值钱的东西。1. 拿到源码后的第一件事先搞懂整套预测链路怎么串起来的很多初学者解压源码包之后的习惯是打开 train.py 直接跑报错再一个坑一个坑填。我的习惯相反先把整个项目的函数调用关系和目录结构理一遍。因为预测类项目跟 Web 项目最大区别是数据流方向非常固定数据清洗和特征工程的代码量往往超过模型本身你如果不清楚每条数据是“怎么从 csv 变成 tensor 再变成预测结果的”后面调参根本无从下手。1.1 源码目录结构与核心模块我手里这套源码.zip 解压后目录大致长这样不同版本可能有简化但骨架类似stock_rnn_prediction/ ├── data/ │ ├── raw/ # 原始股票日线数据通常是CSV │ └── processed/ # 清洗后用于训练和预测的数据 ├── src/ │ ├── data_loader.py # 数据读取与预处理 │ ├── dataset.py # 滑窗采样构造训练样本 │ ├── model.py # RNN/LSTM模型定义 │ ├── train.py # 训练主脚本 │ ├── predict.py # 预测与可视化 │ └── config.py # 全局参数配置窗口、学习率、epoch等 ├── checkpoints/ # 模型权重保存位置 ├── results/ # 预测结果图与指标 └── requirements.txt模块间依赖关系非常明确config 提供参数data_loader 读原始数据并清洗dataset 按滑窗切成样本model 定义网络结构train 调 dataset 和 model 完成训练并保存权重predict 加载权重对最新一段行情做预测并画图。提示拿到任何源码包第一步永远是先看 config.py或类似的配置文件。不是看代码实现而是先把所有参数抄一遍到笔记本上挨个搞清楚它控制哪一段流水线。这个习惯能在后续排查“为什么预测曲线这么怪”时省下大量时间。1.2 为什么要用“滑窗采样”而不是全量序列直接训练股票价格是一长串时间序列。假设我们有 3000 个交易日的收盘价能不能把这 3000 个数一口气塞进 LSTM 训练理论上网络能接收长序列但实践中梯度传播会非常不稳定而且计算量巨大。更关键的是预测任务本身具有固定的时间依赖关系——我们要的不是“记住 3000 天的走势”而是“根据最近 N 天推断下一天”。所以源码里几乎清一色采用滑窗rolling window策略设窗口长度为 W把第 1 到 W 天的数据作为输入第 W1 天的数据作为标签然后窗口后移一天第 2 到 W1 天作为输入第 W2 天作为标签……依次类推。举个例子窗口 W60样本1X [t1, t2, ..., t60] y t61 样本2X [t2, t3, ..., t61] y t62 ...这样 3000 天的序列能切出约 2940 个样本。窗口长度本身是个超参数源码里常见默认值是 30、60、90对应一个月、一季度、半年的交易日数量。窗口太短模型看不到中期趋势窗口太长历史信息里噪音过多反而干扰当前判断。我个人在股票日线上常用的起步值是 60后续对比 30 和 90 再做决定。1.3 数据划分绝非随手切 7:3分类任务里随机划分训练集和测试集没问题因为样本独立。但时间序列数据存在顺序依赖如果随机打乱等于让模型“偷看未来”——训练集里混杂着测试集末尾几天的数据验证时指标自然虚高。源码里通常的做法是严格按照时间先后切分比如前 80% 交易日作为训练集后 20% 作为测试集。更讲究一点的做法是设定“验证集”从训练集中再按时间切出最后一段用于早停和选参。很多人跑完源码发现测试集精度很高换到实时预测就拉胯多半是数据划分阶段埋下的隐患。后面章节我会专门展开这个点。2. 数据预处理这段代码决定了预测上限模型只是逼近这个上限模型再花哨数据不行都是白搭。股票价格序列最典型的几个问题缺失值、极端值、量纲差异大。源码里 data_loader.py 主要就干这三件事。2.1 缺失值与复权问题处理股票数据经常出现停牌导致当天无记录或者复牌后价格跳空。很多源码包默认直接 dropna() 一行删掉。这种做法会切断时间连续性导致滑窗样本数量减少。更稳妥的方式是前向填充即用上一个交易日的价格填充缺失日并额外构造一个“是否停牌”的特征列输入模型。另外要注意的是复权。如果你从数据源拉的是不复权价格遇到分红送股时价格会突然跳空模型会认为这是剧烈的涨跌信号产生严重误导。源码若没做复权处理你需要自己在数据源端选择“前复权”或“后复权”再导出。这是非常常见的一个坑我在不少教程源码里看到过跑出来的曲线一遇到除权日就出现尖峰往往就是这个原因。2.2 归一化必须用训练集的统计量价格序列的量级差异很大比如贵州茅台股价上千元而一些低价股几块钱如果不归一化模型权重更新会被大数值特征主导。源码一般用 MinMaxScaler 把价格压缩到 [0,1] 区间from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) # 只对训练数据 fit再对训练和测试数据分别 transform train_scaled scaler.fit_transform(train_close.reshape(-1, 1)) test_scaled scaler.transform(test_close.reshape(-1, 1))这里最关键的是fit 只能调用一次而且只能用在训练集上。一旦你用全量数据去 fit测试集的统计信息就“泄漏”进训练阶段评估结果会过度乐观。真正的预测场景下你只有历史数据未来数据是未知的所以要用训练集的 min max 去缩放未来的数据才是对未知数据的诚实模拟。2.3 特征工程成交量、技术指标能否加进模型源码如果只喂收盘价一个特征模型本质上是在学“历史价格的形状”。这当然能学出一些东西但信息量有限。常见扩展是把成交量、最高价、最低价、开盘价都加进输入向量甚至计算 RSI、MACD、布林带等技术指标作为额外通道。多特征的输入维度会从 (batch, window, 1) 变成 (batch, window, feature_num)。LSTM 的 input_size 参数也要改为 feature_num。实践中加入成交量的提升是明显的因为量价配合本身是市场行为的核心信息。技术指标则要谨慎部分指标基于历史价格的同函数变换相当于给模型加了个固定的非线性映射能帮模型“一眼看到”特征但同时也增加了无用信息的风险。我的建议是先跑一版“纯收盘价”作为基线再逐步加特征每加一个就能看出它的边际贡献。3. 从原理到代码RNN/LSTM 模型部分到底做了什么进入 model.py 之前先把循环神经网络本身说透。为什么预测股票要选循环神经网络而不是用普通的前馈网络因为价格数据是时间序列今天的价格依赖过去一段时间的价格。普通的全连接网络强行把 60 天的价格拼成一个 60 维向量输入完全丢弃了时间顺序结构而循环神经网络天生带“记忆”能按时间步逐一处理输入并在隐藏状态中保留历史信息。3.1 RNN 的数学直觉与“记性”来源一个简单 RNN 在某时间步 t 的隐藏状态可以写成h_t tanh(W_ih * x_t W_hh * h_{t-1} b_h)其中 x_t 是第 t 天的输入如归一化后的价格h_{t-1} 是前一天的隐藏状态。你会看到h_t 既跟当前输入有关也跟前一时刻的隐藏状态有关。把这个公式按时间展开它其实把过去所有输入的信息都“编码”进了最后一个隐藏状态 h_T再接一个线性层输出预测值import torch.nn as nn class SimpleRNN(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2): super().__init__() self.rnn nn.RNN(input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue) self.fc nn.Linear(hidden_size, 1) def forward(self, x): # x shape: (batch, seq_len, input_size) out, _ self.rnn(x) # 取最后一个时间步的输出 out out[:, -1, :] return self.fc(out)3.2 为什么源码里普遍用 LSTM 而不是原生 RNN原生 RNN 有一个致命问题梯度消失。当序列超过几十步时反向传播经过时间维度逐层相乘梯度会指数级缩小模型几乎学不到长距离依赖。打个比方你让一个人回忆 30 天前的某个信息他记忆模糊很正常但如果连续 30 天每天都重复“昨天吃了什么”最后第十天你把第一天的信息完全忘了这就是长距离依赖丢失。LSTM长短期记忆网络通过三个门控单元——遗忘门、输入门、输出门——来控制信息的写入、保留和读出让梯度可以沿着“细胞状态”这条高速公路更稳定地回传。import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM(input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout) self.fc nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Linear(32, 1) ) def forward(self, x): out, (h_n, c_n) self.lstm(x) out out[:, -1, :] return self.fc(out)源码里用 LSTM 几乎是标配隐藏层大小常见 32、64、128层数 1 到 3。hidden_size 越大模型容量越大但也更容易过拟合——金融数据本身信噪比低大模型很容易把噪音当信号记住。我在实践中默认从 hidden_size64、num_layers2 起步如果训练集 loss 降得很快但验证集 loss 不降立即减小容量而不是先砸更多 epoch。3.3 多步预测与单步预测在设计上的本质区别源码里的预测目标通常有两种设计要么预测明天收盘价单步回归要么预测未来 5 天、10 天的价格走势多步预测。单步回归实现简单用最后一天的隐藏状态过一个线性层即可。多步预测则麻烦得多常见做法有两种一种是直接修改输出维度让最后一层输出未来 N 个价格另一种是把预测值不断反馈回输入端迭代地往后预测。第二种方法看起来很聪明但坑极大——误差会随着逐步递归不断累积预测到第三步可能已经严重偏离真实走势。源码里如果跑出来的多步预测曲线“前几步还行后几步崩掉”多半就是这个原因。我个人的建议如果业务上只需要判断“明天是涨是跌”老老实实做单步回归把多步预测问题抛给更复杂的模型架构比如后面的章节会提一嘴 Transformer 类方案。4. 训练、评估和可视化如何评判一个预测模型真正可用模型定义好了样本构造好了接下来是训练循环。这一部分源码不会花太多篇幅但恰恰是最容易“跑出幻觉结果”的地方。4.1 训练超参数与早停策略源码中常见的超参数配置参数常见默认值说明batch_size64每次迭代样本数适中即可learning_rate0.001Adam 优化的默认学习率epochs50~200需要配合早停使用window_size60输入序列长度hidden_size64LSTM 隐藏状态维度num_layers2LSTM 层数train_ratio0.8训练数据占比训练循环本身很标准前向传播计算预测值用 MSELoss 衡量预测与真实价格的差异反向传播更新参数。唯一值得强调的就是早停early stopping。训练集 loss 会一路下降但验证集 loss 通常先降后升那个“最低点”就是模型泛化能力最强的时候。如果不早停模型在训练集上越来越准在验证集上越来越差这就是过拟合的完美写照。best_val_loss float(inf) patience 10 trigger_times 0 for epoch in range(epochs): train_loss train_one_epoch(model, train_loader, optimizer, criterion) val_loss validate(model, val_loader, criterion) if val_loss best_val_loss: best_val_loss val_loss trigger_times 0 torch.save(model.state_dict(), checkpoints/best_model.pth) else: trigger_times 1 if trigger_times patience: print(fEarly stop at epoch {epoch}) break4.2 评估指标别只看“预测曲线和真实曲线长得像”肉眼观察预测曲线几乎贴在真实曲线上这种视觉反馈很有迷惑性。价格的高度自相关性会让“昨日价格”成为极强基线——就算模型只输出前一天的价格那条线也跟真实曲线非常接近。评估时必须引入量化指标源码里常见的是MSE均方误差预测值与真实值差值的平方均值反向传播用它做损失。MAE平均绝对误差对异常值更鲁棒适合金融数据因为金融数据本身有大量并非信息性的大幅波动。MAPE平均绝对百分比误差把误差归一化为百分比能直观反映“预测价格偏离真实价格的百分比”。更关键的对比实验是设置一个基线模型——直接预测“明天的价格等于今天的价格”。如果 LSTM 的 MSE 连这个朴素的基线都打不过那这模型加入任何特征都只是自嗨。很多源码演示里没有这一条基线对比导致初学者误以为那些好看的曲线意味着模型“学到了规律”。实际上它可能只是学会了“复制粘贴最近一天的值”这是非常常见的隐形失败模式。4.3 预测结果可视化与未来行情外推可视化一般画两张图第一张是训练过程中的 loss 曲线确认模型收敛、没有震荡第二张是测试集上真实价格与预测价格的对比曲线。跑通源码后我强烈建议你把 predict.py 的输出扩展一下画一张“最近 60 天历史 未来 20 天预测”的图才能直观判断模型对未来走势的判断。注意未来 20 天预测如果是多步外推方式实现的误差会随时间步累积图中会看到预测线逐渐“钝化”——慢慢趋近于一条直线这是因为模型不敢预测剧烈波动这是正常现象不代表代码有 bug。5. 实战踩坑我复现这类源码时遇到的五个典型问题与排查思路这一段是本文最实用的部分。拿到一份压缩包源码连环境配置、数据下载都顺利通过但跑出来的结果让人怀疑人生问题通常集中在这五类。5.1 数据泄漏导致测试集指标虚高我曾有一次复现类似项目测试集上的 MAPE 只有 1.2%当时觉得“这模型无敌了”。后来我检查代码发现数据缩放器的 fit 是在全量数据上进行的。前面说过这等于把测试集上未来的统计信息提前泄漏给了训练阶段。修正成只对训练集 fitMAPE 立刻变成了 3.5%。虽然仍然不差但这才是模型真实水平的保守估计。重要拿到源码后第一件事全局搜索fit_transform和MinMaxScaler看它作用的数组是全量数据还是仅训练集。如果是全量这基本属于“自欺欺人型”代码必须改。5.2 训练集随机打乱毁掉时间依赖有些源码为了所谓“提高模型泛化”在构造 DataLoader 时开了shuffleTrue。对图像分类这没错对时间序列就是灾难。打乱后样本之间没有时间顺序关系LSTM 学到的“记忆”结构被彻底破坏训练期间 loss 可以降到一个很低的值但本质上是在背书。正确的做法是shuffleFalse如果确实需要随机性可以在每个 epoch 内对“batch 内部的排列”做轻微打乱保持时间顺序不跨样本断裂。5.3 直接预测原始价格 vs 预测差分序列股票价格非平稳长期看有上涨趋势这让模型很容易学到“下一个价格约等于当前价格 小扰动”。你可以做一个简单改进不去预测价格本身而是预测价格的一阶差分今日收盘价 - 昨日收盘价。差分序列近似平稳模型需要真正学习涨跌的模式而不是“照抄上一天”。源码若没有这个选项你可以自己加一个预处理开关很快就能体会到效果差异。5.4 特征归一化时误把类别型变量当连续变量有些源码会加入星期几0~6作为特征用来捕捉星期效应。这本身是合理的但如果直接把这列扔进 MinMaxScaler等于把“周一”和“周二”之间的差值当成数值大小来理解毫无语义。正确的做法是 One-Hot 编码或者至少作为嵌入特征处理。类似的还有“是否涨停”“是否停牌”这些二值标记直接用 0/1 是没问题的。5.5 CPU 训练太慢如何快速验证代码能跑通LSTM 训练在 CPU 上很慢尤其 window_size 大、候选股票多的时候。源码本地验证阶段建议把 epoch 临时改成 3验证集比例调大一点用一小段数据先快速验证数据流和模型 forward 没问题再决定要不要上 GPU 或云主机。这不算偷懒而是工程上常规的最小可行性验证。我曾见有朋友直接在云 GPU 实例上跑完整训练结果第一轮结束才发现数据加载逻辑有 bug白白烧掉好几个小时机时。6. 源码使用指南与二次开发怎么把它改造成自己的选股辅助工具最后这部分写给想把源码真正用起来的人。压缩包里给的通常是一个演示用的股票比如某知名蓝筹股的日线数据你需要把它换成自己关心标的。6.1 环境安装与快速启动步骤项目若基于 Python 3.8 和 PyTorch安装命令很简单pip install torch numpy pandas matplotlib scikit-learn如果你本地有 GPU 且装了 CUDA 版 PyTorch请自行以官方安装命令为准。安装完成后把 config.py 里的data_path指向你自己的 CSV 文件运行python src/train.py python src/predict.pypredict.py 会输出一张预测图并打印测试集上的评估指标。如果一切顺利你就能看到前文描述的那套输出。如果中途报维度不匹配的错十有八九是 CSV 的列名和源码里读的列名不一致比如源码读close你的 CSV 里叫收盘统一改列名即可。6.2 自定义股票数据与参数调整思路从行情软件或数据接口导出历史日线数据时注意保证足够长的历史跨度建议至少 1000 个交易日约 4 年太少的话滑窗采样后样本量不足以训练。导出后放进data/raw/目录保持date, open, high, low, close, volume的表头结构。模型参数不必盲目照搬针对不同股票波动特性可以做针对性调整波动剧烈的题材股减小窗口长度到 30让模型更关注近期信息隐藏层大小降到 32 防过拟合。大盘蓝筹股窗口 60 或 90 更合适hidden_size 可以保留 64。训练轮数建议设置 100并开启早停不要傻傻跑满 200 个 epoch。6.3 预测与策略结合从“预测价格”到“辅助决策”预测出明天的价格后怎么用直接全仓买入是不现实的预测误差远大于交易成本。更合理的用法是把它当成一个信号过滤器比如“模型预测明日涨跌幅超过 1% 且趋势连续 3 天向上抬升时才触发关注”或者与传统的均线策略结合只有当价格站上 20 日均线且 LSTM 预测也为正向时才生成买入信号。另一个可以考虑的方向是预测波动率而不是价格。价格预测误差大但波动率比如 GARCH 类模型与 LSTM 结合的可预测性相对高对仓位管理、止损设置都更有实用价值。源码包的 LSTM 部分天然可以复用它做波动率回归任务只要把标签从“收盘价”换成“已实现波动率”即可。个人经验二次开发时先保证原有 demo 完整跑通一次形成基准再每次只改一个变量。不要同时改数据源、特征、模型结构、超参数——否则一旦结果恶化你根本不知道是哪个环节导致的。版本记录与“单因子实验”的思路在调模型时同样适用。最后再分享一个我在复现这类项目时反复用的技巧每跑完一版把模型权重和训练指标一起存档文件名带上前缀描述例如lstm_win60_h64_feat5_mae0.023.pth。这样回看实验记录时一眼就能知道这个模型的输入窗口、容量、特征数和误差水平。项目跑多了你会意识到源码本身是起点而你怎么在上面做受控实验、积累经验才决定了这套循环神经网络预测代码能走多远。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进