☰
循环神经网络股票预测源码复现:从LSTM原理到工程避坑
2026/10/23 13:18:53 网站建设 项目流程

简介:本资源是一套基于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 天的数据作为输入,第 W+1 天的数据作为标签;然后窗口后移一天,第 2 到 W+1 天作为输入,第 W+2 天作为标签……依次类推。

举个例子,窗口 W=60:

样本1:X = [t1, t2, ..., t60] y = t61 样本2:X = [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_size=1, hidden_size=64, num_layers=2): super().__init__() self.rnn = nn.RNN(input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True) 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_size=1, hidden_size=64, num_layers=2, dropout=0.2): super().__init__() self.lstm = nn.LSTM(input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout) 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_size=64、num_layers=2 起步,如果训练集 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(f'Early stop at epoch {epoch}') break

4.2 评估指标:别只看“预测曲线和真实曲线长得像”

肉眼观察预测曲线几乎贴在真实曲线上,这种视觉反馈很有迷惑性。价格的高度自相关性会让“昨日价格”成为极强基线——就算模型只输出前一天的价格,那条线也跟真实曲线非常接近。评估时必须引入量化指标,源码里常见的是:

  • MSE(均方误差):预测值与真实值差值的平方均值,反向传播用它做损失。
  • MAE(平均绝对误差):对异常值更鲁棒,适合金融数据,因为金融数据本身有大量并非信息性的大幅波动。
  • MAPE(平均绝对百分比误差):把误差归一化为百分比,能直观反映“预测价格偏离真实价格的百分比”。

更关键的对比实验是设置一个基线模型——直接预测“明天的价格等于今天的价格”。如果 LSTM 的 MSE 连这个朴素的基线都打不过,那这模型加入任何特征都只是自嗨。很多源码演示里没有这一条基线对比,导致初学者误以为那些好看的曲线意味着模型“学到了规律”。实际上,它可能只是学会了“复制粘贴最近一天的值”,这是非常常见的隐形失败模式。

4.3 预测结果可视化与未来行情外推

可视化一般画两张图:第一张是训练过程中的 loss 曲线,确认模型收敛、没有震荡;第二张是测试集上真实价格与预测价格的对比曲线。跑通源码后,我强烈建议你把 predict.py 的输出扩展一下:画一张“最近 60 天历史 + 未来 20 天预测”的图,才能直观判断模型对未来走势的判断。注意,未来 20 天预测如果是多步外推方式实现的,误差会随时间步累积,图中会看到预测线逐渐“钝化”——慢慢趋近于一条直线,这是因为模型不敢预测剧烈波动,这是正常现象,不代表代码有 bug。

5. 实战踩坑:我复现这类源码时遇到的五个典型问题与排查思路

这一段是本文最实用的部分。拿到一份压缩包源码,连环境配置、数据下载都顺利通过,但跑出来的结果让人怀疑人生,问题通常集中在这五类。

5.1 数据泄漏导致测试集指标虚高

我曾有一次复现类似项目,测试集上的 MAPE 只有 1.2%,当时觉得“这模型无敌了”。后来我检查代码,发现数据缩放器的 fit 是在全量数据上进行的。前面说过,这等于把测试集上未来的统计信息提前泄漏给了训练阶段。修正成只对训练集 fit,MAPE 立刻变成了 3.5%。虽然仍然不差,但这才是模型真实水平的保守估计。

重要:拿到源码后第一件事,全局搜索fit_transform和MinMaxScaler,看它作用的数组是全量数据还是仅训练集。如果是全量,这基本属于“自欺欺人型”代码,必须改。

5.2 训练集随机打乱毁掉时间依赖

有些源码为了所谓“提高模型泛化”,在构造 DataLoader 时开了shuffle=True。对图像分类这没错,对时间序列就是灾难。打乱后,样本之间没有时间顺序关系,LSTM 学到的“记忆”结构被彻底破坏,训练期间 loss 可以降到一个很低的值,但本质上是在背书。正确的做法是shuffle=False,如果确实需要随机性,可以在每个 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.py

predict.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)。这样回看实验记录时,一眼就能知道这个模型的输入窗口、容量、特征数和误差水平。项目跑多了,你会意识到,源码本身是起点,而你怎么在上面做受控实验、积累经验,才决定了这套循环神经网络预测代码能走多远。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询