简介:基于深度学习的股票分析预测系统 Python 实现,是面向计算机相关专业高年级本科生的期末大作业级源码,曾获 98 分导师评分,适配课程设计、毕业设计与 AI 入门进阶。项目采用多层感知机与长短期记忆网络混合架构,覆盖技术指标提取、特征工程、时序建模和性能评估,代码结构完整且已在本地调试通过。资源包共 25 个文件,约 6.34 MB,其中 6 个 py 脚本承担数据下载、指标计算、策略回测与预测绘图等核心流程,csv 提供沪深300与个股历史行情样例,jpg 为预测效果图,另有 README 与 dependency 清单便于快速运行。已有 96 人学习下载。通过源码与文档可掌握数据预处理、模型训练策略、超参数优化及评价指标体系,并借助中文注释与模块化设计进行二次扩展,适合想要完整复现深度学习股票预测流程的学习者。
1. 基于深度学习的股票分析预测系统到底在做什么
“基于深度学习的股票分析预测系统Python实现(高分期末项目)”,这条标题几乎是很多金融、计算机、数据科学方向学生的期末刚需。你大概率已经见过不少“祖传代码”:把股价历史数据丢进LSTM,跑几十个epoch,画一张预测曲线,拿个还不错的分数交差。但这篇不会让你停留在“会跑通”的水平——我会把这个项目的完整落地路径拆开:从数据怎么取、特征怎么造,到模型怎么选、回测怎么做,再到那些老师不会在课堂上讲、但真正影响你成绩和模型可信度的坑。
这个系统本质上解决的是一个有监督回归问题:用过去N天的量价数据,预测未来M天的价格或涨跌方向,而不是像某些夸大宣传说的那样“预测明天的精确股价”。它适合正在准备课程设计、期末项目或毕业设计的从业者,也适合想系统了解深度学习在量化场景里真实边界的入门者。我默认你是用Python + PyTorch走通全线,这是目前课程项目中复用率最高、也最容易解释清楚的技术栈。
2. 数据是预测的地基:先用Python拿到干净可用的股票数据集
2.1 数据源选型:为什么我不建议直接爬网页数据
常见做法是优先选择结构化数据接口而不是自己去写爬虫。原因很直接:你用爬虫从财经网站抓日线数据,页面结构改一次你就得跟着改一次,而且抓下来的数据缺失、停牌、复权等问题处理起来非常被动。
我一般建议从两个方向获取数据:
- 使用免费且稳定的Python第三方库直接拉取,比如通过akshare这类开源数据接口获取A股行情,或用yfinance拉美股数据。这种方式适合期末项目快速起步。
- 使用TuShare Pro、聚宽等平台提供的量化数据接口。这类平台数据质量高,但部分字段需要积分或权限。
以akshare为例,一段获取A股日线数据的代码非常简洁:
import akshare as ak # 获取A股某只股票的历史行情数据 df = ak.stock_zh_a_hist(symbol="000001", period="daily", start_date="20200101", end_date="20241201", adjust="qfq") # qfq表示前复权 print(df.head())这段代码拉回来的是包含日期、开盘价、收盘价、最高价、最低价、成交量、成交额、振幅、涨跌幅、涨跌额、换手率的DataFrame。注意adjust="qfq"参数:前复权处理了分红送股造成的价格跳空,这是做量化分析必须的第一步。不复权数据在除权日会出现人为的价格暴跌假象,模型会把这种“假信号”学进去。
2.2 清洗与对齐:缺失值、停牌和重复日期的一个都不能放过
原始数据不能直接进模型。清洗时我会做四件事,顺序固定,照这个顺序来基本不会出问题:
- 检查重复日期。有的数据源在极端行情下会重复推送同一行。
- 填充或删除缺失值。如果某天停牌,行情数据会是NaN,需要决定是前向填充还是直接删除。我在这里建议用前向填充,因为停牌期间市场信息确实没有更新,用收盘价填充可以保持时间序列的连续性。
- 检查时间序列是否等间隔。交易日历要剔除周末和节假日,不能按自然日排列。
- 统一索引为日期格式且升序排列。
import pandas as pd df.columns = [c.strip() for c in df.columns] # 去除列名空格 df = df.drop_duplicates(subset=["日期"]) df = df.sort_values("日期").reset_index(drop=True) # 前向填充停牌日缺失值 df = df.fillna(method="ffill").dropna() # 把日期列设为索引 df["日期"] = pd.to_datetime(df["日期"]) df.set_index("日期", inplace=True) print(df.isnull().sum())这里最容易被新手忽略的是drop_duplicates必须在sort_values之前做。如果先排序再去重,重复行往往会留在数据集末尾或头部,影响后续窗口切分。另外,fillna方法在较新的pandas版本中已经改为df.ffill(),建议直接使用新写法。
2.3 训练集和测试集切分:随机切分是时间序列预测的大忌
分类项目里常用的train_test_split(random_state=42)在这里不能直接用。股票数据是强时间依赖的,如果测试集数据混在训练集之前,模型在时间上“窥探”了未来,测试指标会虚假地好看到离谱。正确做法是按时间顺序切分,用前80%作训练,后20%作测试,同时留出中间一段作为验证集。
# 假设总数据量为 N N = len(df) train_end = int(N * 0.7) val_end = int(N * 0.9) train = df.iloc[:train_end] val = df.iloc[train_end:val_end] test = df.iloc[val_end:]验证集的作用是调节超参数和早停,测试集只能在整个模型流程确定之后跑一次。如果你反复用测试集调参,测试集就失去了评估意义,这在答辩时非常容易被老师追问到无话可说。
3. 特征工程与数据窗口化:想明白模型到底在学什么
3.1 原始价格不能直接进模型:归一化和技术指标的取舍
直接拿开盘价、收盘价、成交量输入LSTM,模型训练会极其不稳定。原因是价格区间很大,比如几百元的股票和几元的股票,数值量级差异悬殊,梯度更新会被大数值特征主导。更严重的是,不同股票之间的量纲不统一,模型学到的权重无法迁移。
常用的做法是归一化。这里要区分两种归一化:全局归一化在时间序列中隐藏着未来信息泄漏的隐患。因为整个数据集的均值和最大值包含了测试段的信息,在训练时你其实已经“看过”了未来的分布。正确的做法是:只用训练集的数据计算均值和标准差,然后用这个标定过的参数去转换验证集和测试集。
from sklearn.preprocessing import StandardScaler feature_cols = ["开盘", "收盘", "最高", "最低", "成交量", "涨跌幅"] scaler = StandardScaler() # 关键:只在训练集上 fit,在验证集/测试集上 transform scaler.fit(train[feature_cols]) train_scaled = scaler.transform(train[feature_cols]) val_scaled = scaler.transform(val[feature_cols]) test_scaled = scaler.transform(test[feature_cols])特征方面,除了量价原始数据,我一般会加入几个对模型有明显帮助的技术指标,常见做法是计算收益率、对数收益率、5日均线偏离度和涨跌幅。但这里要控制特征数量在8到15个之间,太少了模型学不到规律,太多了在这个规模的项目里只会增加过拟合风险。
3.2 滑动窗口:用过去多少天预测未来多少天最合理
把一张张的截面数据变成LSTM可以吃的序列样本,靠的是滑动窗口切分。这里有一个经常被作业党忽视的问题:窗口大小直接决定样本的边界条件,窗口太大,样本数量骤减,模型可学习的数据不够用;窗口太小,模型看不到中期的趋势特征。
我做过的对比实验里,用5到20天的窗口比较常见。日线数据上,我个人偏好用10天作为默认窗口:能覆盖两周的交易信息,也能保留足够的样本数量。预测目标方面,做回归的话可以预测未来1天到5天的收盘价,做分类的话可以预测未来一天涨还是跌。
import numpy as np import torch from torch.utils.data import Dataset def create_sequences(features, target, window_size=10, pred_days=1): X, y = [], [] for i in range(window_size, len(features) - pred_days + 1): X.append(features[i - window_size:i]) y.append(target[i + pred_days - 1]) # 预测未来第 pred_days 天 return np.array(X), np.array(y) # 以预测未来第5天收盘价为例 target_col = train_scaled[:, feature_cols.index("收盘")] X_train, y_train = create_sequences(train_scaled, target_col, window_size=10, pred_days=5)这里要注意create_sequences的实现中,features[i - window_size:i]取的是第i-window_size到第i-1天,共10天数据,预测的是第i+pred_days-1天的收盘价。也就是说样本不会用当天数据预测当天,避免信息重叠。如果你想预测5天后的价格,这个逻辑是自洽的。
3.3 标签构造:对期末项目来说,回归和分类怎么选
这是个策略问题,直接决定你模型结构和答辩时的解释逻辑。回归任务输出一个连续价格值,评估用MAE或RMSE;分类任务输出涨/跌概率,评估用准确率、F1分数。我的建议是:如果你的目标是把项目做出亮点,就选择分类+回归的多任务输出。但在有限的期末时间下,我仍然建议先跑通单一回归主任务,再附加一个方向准确率的评估指标,这样既不过度增加模型复杂度,又能回答“模型是否有实际意义”的问题。
4. 模型核心:基于PyTorch搭建LSTM预测模型的完整步骤
4.1 模型结构设计:单层还是双层LSTM,隐藏层维度多大
LSTM是这个项目里最常见的深度学习模型,不是因为它在股票预测上是最先进的,而是因为它和序列数据天然契合,且每个答辩老师都了解它。对期末项目来说,结构太复杂的模型容易过拟合,还很难解释;太简单的模型又显得工作量不足。我一般推荐的结构是:
import torch.nn as nn class StockLSTM(nn.Module): def __init__(self, input_size, hidden_size=64, num_layers=2, dropout=0.2): super(StockLSTM, self).__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): lstm_out, (h_n, c_n) = self.lstm(x) last_hidden = h_n[-1] # 取最后一层LSTM的隐含状态 out = self.fc(last_hidden) return out几个参数的解释:
input_size对应特征数量,也就是滑动窗口切出来的每条样本的列数。hidden_size=64是在效果和训练开销之间的折中值,A股日线数据量不大,64足够了。如果你换到分钟级数据,可以适当调整到128。num_layers=2比1层效果好的原因是能够捕捉到更高阶的时间模式,但超过3层在几千条样本的小数据集上基本就是过拟合,训练时间还暴涨。batch_first=True意味着输入张量形状为(batch, seq_len, feature_size)。这是初学阶段最容易写错的点,PyTorch默认的LSTM输入是(seq_len, batch, feature_size),不设置这个参数就得在送入模型前做维度交换。
h_n[-1]取的是最后一层、最后一个时间步的隐含状态向量。为什么不是lstm_out[:, -1, :]?这两个其实等价,只是后者可读性稍差。
4.2 训练循环与早停:调好学习率和epoch数能少走一半弯路
训练环节是期末项目翻车最频繁的地方。常见的现象是loss不下降、loss直接变成NaN、或者训练到一半就发散。这些绝大多数是学习率的问题,而不是模型代码的问题。
我习惯用lr=0.001做默认学习率,优化器选用Adam。如果你发现loss下降非常缓慢,可以调到0.005;如果发现loss震荡剧烈或发散,果断调回0.0001。
import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset X_train_t = torch.tensor(X_train, dtype=torch.float32) y_train_t = torch.tensor(y_train, dtype=torch.float32).unsqueeze(-1) X_val_t = torch.tensor(X_val, dtype=torch.float32) y_val_t = torch.tensor(y_val, dtype=torch.float32).unsqueeze(-1) train_dataset = TensorDataset(X_train_t, y_train_t) val_dataset = TensorDataset(X_val_t, y_val_t) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False) model = StockLSTM(input_size=X_train.shape[2], hidden_size=64, num_layers=2) criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=0.001) scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.5, patience=5) best_val_loss = float("inf") patience, wait = 10, 0 for epoch in range(100): model.train() train_loss = 0.0 for batch_x, batch_y in train_loader: optimizer.zero_grad() outputs = model(batch_x) loss = criterion(outputs, batch_y) loss.backward() optimizer.step() train_loss += loss.item() * batch_x.size(0) train_loss /= len(train_dataset) # 验证 model.eval() val_loss = 0.0 with torch.no_grad(): for batch_x, batch_y in val_loader: outputs = model(batch_x) val_loss += criterion(outputs, batch_y).item() * batch_x.size(0) val_loss /= len(val_dataset) scheduler.step(val_loss) if val_loss < best_val_loss: best_val_loss = val_loss wait = 0 torch.save(model.state_dict(), "best_model.pt") else: wait += 1 if wait >= patience: print(f"Early stop at epoch {epoch+1}") break if (epoch + 1) % 10 == 0: print(f"Epoch {epoch+1}: train_loss={train_loss:.6f}, val_loss={val_loss:.6f}")这段代码包含了几个日后做任何深度学习项目都可能用得上的习惯:
ReduceLROnPlateau是当验证集loss连续5个epoch不降时,把学习率减半的学习率调度器。它能帮助模型在loss平台期继续收敛,而不是卡死。- 早停机制配合保存最优模型,避免因为最后的epoch过拟合导致你辛辛苦苦训练的结果反而变差。我调试模型时最常吃的亏就在这:训练到最后几个epoch训练loss还在降,但验证loss已经飙升,如果不用早停,你拿到的将是过拟合后的退化模型。
- 验证集必须在每个epoch都评估,而不是训练完再去评估。这样早停才有意义。
4.3 序列长度和批量大小的参数考量
处理股票日线这类中小规模数据时,批量大小设置值得单独讲一下。数据总量可能只有几千条样本,batch_size=64是比较通用的选择。如果你发现模型收敛速度偏慢,可以把batch_size减到32,增加每个批次内的梯度更新频率;如果训练震荡,可以调大到128,让梯度更平滑。
同时batch_first=True时,输入的维度顺序是[批次大小, 序列长度, 特征数],在用DataLoader打包时要注意,PyTorch会自动把列表数据转换成张量,但数据张量里的最后一个维度必须匹配模型输入层大小。
5. 股票预测系统的避坑指南:五条真实踩坑记录
5.1 归一化泄漏:预测结果烂得离谱的真正原因
- 现象:训练loss很低,验证loss也好看,但画出来的预测曲线几乎是一条平移的直线,预测值比实际走势延迟了很多。
- 原因:用了全局归一化或者全局反归一化。预测曲线延迟的本质是LSTM学到了“复制最近一天的价格作为未来预测”的捷径,这通常出现在目标序列高度平滑、模型的预测范围又很短的情况下。你做一个回归任务预测未来一天,这个现象会更明显。而如果在预处理时用了全量数据的均值和方差,测试集的信息会被反推出来,指标虚高掩盖了这个问题。
- 解决:严格只用训练集拟合scaler,然后transform验证集和测试集。反归一化时也只使用训练集的scaler参数。这一条在答辩时也是老师最爱问的,答清楚了能加不少印象分。
5.2 训练loss是NaN:梯度炸了还是数据有问题
- 现象:训练到中途,loss变成NaN,再训练也不恢复。
- 原因:最常见是特征里有NaN值没有清理干净;其次是学习率过大导致梯度爆炸;还有可能是数据里有极端异常值,比如某天涨幅异常导致数值很大,参加运算后直接溢出。
- 解决:第一步检查数据清洗结果,打印每个特征列的isnull数量;第二步把学习率降到0.0001;第三步在优化器上添加梯度裁剪。
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)5.3 预测曲线严重滞后:模型被上一个时间步“骗”了
- 现象:预测值比真实值恰好慢了一到两个交易日。
- 原因:这个问题本质上是LSTM对目标序列的“复制粘贴”偏置。尤其是当损失函数只用MSE、而目标序列又比较平滑时,模型发现最省力的方法是预测值贴近上一个时间步的真实值,因为这样MSE也不会太高。换句话说,模型在学“惯性”而不是学“趋势”。
- 解决:这是股票预测项目中最经典的一个问题,不要指望模型能精准预测变盘点。我的做法是:将预测目标从“未来1天收盘价”调整为“未来5天收盘价”,拉长预测步长能让模型被迫学习中期趋势,同时用方向准确率作为辅助评估指标。如果模型预测的方向准确率显著高于50%,才说明它学到了有效信息。有时间的话可以尝试用TransformerEncoder替换LSTM,但期末项目里这通常是加分项而非必需项。
5.4 验证集和测试集指标差异巨大:你的切分方式存在随机性
- 现象:验证集loss在0.01,测试集loss直接变成0.1。
- 原因:股票数据在不同时间段的市场状态差异非常大。如果你切分的验证集恰好处在一段温和上涨的行情里,模型预测偏差自然小;而测试集遇到急跌或暴涨行情,模型没见过这种波动,误差被拉大。
- 解决:训练多个不同时间段下的模型做对照,或者使用滚动窗口式的切分方式。期末项目里我会建议:把不同年份的行情都纳入测试集,而不是只取最后一段连续时间。不然你的测试集只能证明“模型对未来某一段特定行情有效”,这个结论非常脆弱。
5.5 加载模型后预测结果不一致:忘掉了训练状态切换
- 现象:训练完重新加载模型,预测出来的数值和训练时保存的结果不一致,甚至差很多。
- 原因:训练时模型处于train模式,Dropout层是激活的;而预测时模型应在eval模式,Dropout被关闭。如果保存的是整个模型对象而非state_dict,加载后默认处于train模式。另一种原因是测试数据没有做相同的特征缩放。
- 解决:加一行
model.eval(),再用with torch.no_grad()包住推理过程。模型保存只保存state_dict,不要为了省事直接用torch.save(model, ...)。
6. 回测与可视化:让项目从“能跑通”进化到“能答辩”
6.1 用matplotlib画出预测值和真实值的对照组
代码跑通只是起点,期末项目要拿到高分,展示层至少要包含三张图:训练过程的loss曲线、预测值vs真实值对比图、预测误差分布图。这里给出对比图和误差分布图的代码:
import matplotlib.pyplot as plt # 预测测试集 model.load_state_dict(torch.load("best_model.pt")) model.eval() with torch.no_grad(): X_test_t = torch.tensor(X_test, dtype=torch.float32) y_pred_scaled = model(X_test_t).numpy().flatten() # 反归一化,注意这里要用训练集的scaler y_pred = scaler.inverse_transform( np.concatenate([np.zeros((len(y_pred_scaled), len(feature_cols)-1)), y_pred_scaled.reshape(-1, 1)], axis=1) )[:, -1] y_true = scaler.inverse_transform( np.concatenate([np.zeros((len(y_test), len(feature_cols)-1)), y_test.reshape(-1, 1)], axis=1) )[:, -1] plt.figure(figsize=(12, 5)) plt.plot(y_true, label="真实值", color="steelblue") plt.plot(y_pred, label="预测值", color="crimson", linestyle="--") plt.legend() plt.title("股票收盘价预测:真实值 vs 预测值") plt.xlabel("测试集样本序号") plt.ylabel("收盘价(元)") plt.show()反归一化的写法有点绕,解释一下:inverse_transform要求输入特征的列数和拟合时一样,所以我把预测出的一维收盘价拼接在一堆0的前面,这样逆变换时只会使用scaler里对应收盘价列的均值和方法,其他列填0不影响这个结果。这个技巧看似取巧,但确实是实践中最常用的反归一化方式。
6.2 多模型对比:让深度学习项目和“玄学”拉开差距
如果要拿高分,最后一个亮点我建议做多模型对比实验,把深度学习模型和几个基线模型放在同一套测试集上比较。常见的对比组合是:线性回归、随机森林、单层LSTM、双层LSTM(也就是最终主模型)。
表格在答辩PPT里可以直接用:
| 模型 | RMSE | MAE | 方向准确率 | 训练耗时 |
|---|---|---|---|---|
| 线性回归 | 0.087 | 0.066 | 52.1% | 2秒 |
| 随机森林 | 0.092 | 0.071 | 49.8% | 5秒 |
| LSTM单层 | 0.064 | 0.049 | 55.3% | 1分30秒 |
| LSTM双层(早停) | 0.058 | 0.044 | 57.9% | 2分10秒 |
注意,上表中的数值是示意,不要照搬进你的报告,但对比项目的结构和分析思路可以复用。方向准确率在52%以上就已经不算随机了,57%左右在日线预测这个场景里是不错的水平。关键是你要能解释清楚:为什么深度学习虽然在一些指标上优于传统机器学习,但优势并不是“碾压性”的,原因是金融时序数据的信噪比极低、市场有效性等因素在起制约作用。这种客观分析反而比“我的准确率95%”更能打动老师。
6.3 最后的工程化建议:把训练和预测拆成两个可复用的模块
期末项目接近交付前,我会强烈建议你把代码重新组织一次,至少分成data_loader.py、model.py、train.py、predict.py四个模块。这样做有几个实际好处:答辩时老师要你现场修改窗口长度或预测天数时,你不会手忙脚乱地在一份几百行的大脚本里寻找参数;盲审(如果有)或查重时,模块化的代码结构逻辑清晰,一看就是自己写的,不会与网上的整段源码撞车。
我现在养成的习惯是:每一个模型的超参数、数据切分规则、随机种子都写在配置文件里或作为命令行参数传入。这样每次实验结果都能完整复现,项目做完几个月后回来还能回忆起当时每个数字是怎么来的。如果你的期末项目有时间余量,这个好习惯会很加分。希望这篇实战笔记能帮你在期末阶段少走几条弯路,把精力放在真正体现工作量和技术深度的地方。
本文还有配套的精品资源,点击获取