首页 / 资讯中心 / 文章详情

LSTM股票预测大作业高分代码拆解:数据预处理、模型训练与评估回测

LSTM股票预测大作业高分代码拆解:数据预处理、模型训练与评估回测 ★ FEATURED ARTICLE
简介这是一份Python期末大型作业主题为深度学习在股票分析预测中的应用主要面向计算机相关专业学生与需要项目实战的自学者。项目在导师指导下完成并获评98分源代码已完成本地编译与调试可直接运行。包内共20个文件包含6个Python脚本如data_downloader、stock_indicator、prediction、backtest、strategy、6张预测结果图片、3个CSV数据集以及README、requirements等说明文件压缩包大小4.24MB。核心内容覆盖数据清洗、特征提取、模型构建、训练验证与回测评估等完整流程使用LSTM等深度学习模型处理时间序列并借助NumPy、Pandas、TensorFlow/Keras等工具实现分析预测。已有76人学习下载适合希望系统掌握深度学习金融应用、提升Python实战能力的学习者参考。1. 深度学习股票分析预测大作业这份高分代码到底是怎么组织的期末周拿到“深度学习应用于股票分析预测”这道题大多数人的第一反应不是不会写而是怕写完跑不起来。框架装了一下午、数据下载失败、Loss 不收敛、画出来的预测曲线和真实曲线错位一个周期——这些都是期末季最常见的翻车现场。我今天拆的这份高分作业代码核心是用 LSTM 对股票收盘价做时间序列预测完整走了一遍“数据获取 → 特征工程 → 建模训练 → 评估可视化”的主线流程最后再用方向准确率做一轮回测验证。换句话说它不是一个花架子 Demo而是一个从数据到结论都能自洽的完整项目。适合三类人第一次做深度学习大作业、想抄一个能交差框架的在校生想快速把 LSTM 落地到行情数据上看看效果的从业者以及需要一个 baseline 再做改进的量化方向初学者。2. 数据源与特征工程把 K 线转换成模型能吃的张量2.1 复权价格与数据获取选什么数据源为什么复权做股票预测第一步是取数。期末作业场景下我见过三种常见来源yfinance 免费接口、tushare pro需要 token、akshare。这份代码默认用的是 yfinance因为不需要注册、不用填 tokenpip 装好就能跑最适合作为课程作业的默认方案。数据粒度上代码默认取日线interval1d个股选的是 AAPL如果你想换成 A 股标的只需要改 ticker 符号比如600519.SS贵州茅台或000001.SS平安银行yfinance 对美股和部分 A 股代码都支持。import yfinance as yf import pandas as pd df yf.download(AAPL, start2020-01-01, end2024-06-30, progressFalse) df df[[Open, High, Low, Close, Volume]].copy() # 用 close 生成复权因子简单做一次后向复权 df[Adj Close] df[Close] / df[Close].iloc[0] * 100这里有个关键点很多同学直接拿原始收盘价建模遇到除权除息日价格会出现一个向下的跳空缺口模型会把这个缺口当做一个真实的下跌信号来学导致预测结果在分红季前后明显失真。常见做法是使用复权价格前复权或后复权yfinance 返回的Adj Close就是复权收盘价但这份代码的默认逻辑更简单——用一个基准日做归一化处理用相对收益替代绝对价格。对于期末作业来说只要你的论文里写清楚“本研究使用后复权收盘价”这个处理就足够严谨了。参数progressFalse是为了关掉下载进度条避免在 Jupyter 里刷屏。2.2 技术指标特征MA、RSI、MACD 怎么算进矩阵光有原始 OHLCV开盘、最高、最低、收盘、成交量五列数据LSTM 也能训练但预测效果通常比较“钝”。原因在于原始价格序列是非平稳的直接输入会让模型花大量容量去拟合价格水平而不是拟合价格变化。所以这份代码在特征工程上做了三件事计算移动平均线 MA、相对强弱指标 RSI、MACD 指标然后把它们和收益率拼成一个多维特征矩阵。def add_technical_features(df): df df.copy() # 简单移动平均5 日和 20 日 df[MA5] df[Close].rolling(window5).mean() df[MA20] df[Close].rolling(window20).mean() # 对数收益率消除量纲影响 df[Return] df[Close].pct_change() * 100 # RSI(14)上涨均幅 / 下跌均幅 的衍生 delta df[Close].diff() gain delta.clip(lower0).rolling(window14).mean() loss (-delta.clip(upper0)).rolling(window14).mean() df[RSI] 100 - 100 / (1 gain / loss) # MACD 快慢线差离值 ema12 df[Close].ewm(span12, adjustFalse).mean() ema26 df[Close].ewm(span26, adjustFalse).mean() df[MACD] ema12 - ema26 return df.dropna()为什么用这些特征简单解释一句就行MA 刻画趋势方向RSI 刻画超买超卖状态MACD 刻画动量变化这三个是传统技术分析里最常用的因子拿到这里作为神经网络输入本质上是用先验知识帮模型缩小搜索空间。注意dropna()会把前 20 行左右的数据丢空因为 MA20 需要 20 个交易日才能算出第一个值。如果你的数据集比较短可以把 MA20 换成 MA10或者用min_periods参数放宽窗口限制。2.3 归一化为什么 MinMaxScaler 必须分两次 fit这是全代码里最容易被忽略、但又最致命的环节。LSTM 默认使用 tanh 和 sigmoid 激活函数输入范围最好在 0 到 1 附近否则梯度很容易饱和。这份代码用了MinMaxScaler做归一化如果你写的是下面这种“常规操作”期末答辩时老师一眼就能看出问题from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() # 错误写法全量数据 fit 后再 transform scaled scaler.fit_transform(feature_df[[Close, MA5, MA20, RSI, MACD]])错在哪fit_transform是在整个数据集包括测试集上计算 min 和 max这等于把未来的价格区间信息泄漏给了训练过程。正确做法是对训练集单独fit再用同一个训练集上得到的 min 和 max 去transform验证集和测试集from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() # 先按时间顺序切分再在训练段上 fit train_size int(len(feature_df) * 0.8) train_df feature_df.iloc[:train_size] test_df feature_df.iloc[train_size:] scaler.fit(train_df[feature_cols]) # 只用训练段求 min/max train_scaled scaler.transform(train_df[feature_cols]) test_scaled scaler.transform(test_df[feature_cols]) # 复用训练段的参数这正是这份代码里做得规范的地方。train_size默认取前 80% 作为训练段剩下的 20% 作为测试段。值得注意的细节是这个 split 是按时间顺序硬切的不能用train_test_split的默认随机模式否则序列的上下文关系全部被打乱模型就失去了时间序列的意义。3. LSTM 模型构建与训练时间步长、隐藏层与早停3.1 为什么选 LSTM 而不是普通神经网络或 RNN股票数据是典型的时间序列前后的价格存在强相关性。如果用普通的前馈神经网络MLP去拟合输入的每个时刻之间是相互独立的模型只能看到一个窗口里的离散特征抓不到时间维度上的依赖关系。而传统 RNN 理论上能处理序列但存在梯度消失问题——当序列长度超过 20 步左右反向传播的梯度就会指数级衰减模型学不到“20 天前发生了什么”。LSTM 通过三个门控机制遗忘门、输入门、输出门和一个记忆单元cell state来解决这个问题。遗忘门决定丢弃哪些历史信息输入门决定记住哪些新信息输出门决定输出什么给下一层。在课程作业的维度上你不需要把门的数学公式完整写进代码但要在论文里说明LSTM 能选择性地记住长期趋势比如 20 日前的支撑位同时遗忘短期的噪声波动比如一天的随机涨跌这是它适合股票预测的主要原因。3.2 滑动窗口切片把连续序列切出“样本-标签”对拿到归一化后的矩阵后不能直接丢进 LSTM。LSTM 的输入 shape 是(batch_size, timesteps, features)其中timesteps是看多长的历史窗口features是特征维度。这份代码用滑动窗口的方式构造样本例如设定lookback20就是用第 1~20 天预测第 21 天用第 2~21 天预测第 22 天依次类推。import numpy as np def create_sequences(data, lookback20): X, y [], [] for i in range(lookback, len(data)): X.append(data[i - lookback:i, :]) # 过去 lookback 天的全部特征 y.append(data[i, 0]) # 当天收盘价作为预测目标 return np.array(X), np.array(y) lookback 20 X_train, y_train create_sequences(train_scaled, lookback) X_test, y_test create_sequences(test_scaled, lookback)lookback是这份代码里最重要的超参数默认 20对应大约一个月的交易日。这个值的设定有讲究太短比如 5模型看不到中期趋势对拐点的判断很迟钝太长比如 60模型容易过拟合噪声而且训练量变大期末作业的实验时间会拉长。我一般会建议你用 20 和 30 各跑一组对比把结果截图放进论文里这就是一个很好的“参数敏感性分析”章节素材。注意切片后X_train.shape是(样本数, 20, 6)6 对应 Close、MA5、MA20、RSI、MACD、Return 这六列特征。3.3 模型结构与训练参数层数、神经元数、Dropout 与 EarlyStopping这份代码的模型结构是两层 LSTM 加一层全连接输出。第二层 LSTM 设置了return_sequencesFalse意思是只返回最后一个时间步的输出而不是每个时间步都输出这样才能接上Dense(1)做单值回归预测。模型编译用的损失函数是mse均方误差优化器是Adam学习率设为0.001——这是回归任务的默认安全组合。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping model Sequential() model.add(LSTM(units64, return_sequencesTrue, input_shape(X_train.shape[1], X_train.shape[2]))) model.add(Dropout(0.2)) model.add(LSTM(units64, return_sequencesFalse)) model.add(Dropout(0.2)) model.add(Dense(units1)) model.compile(optimizeradam, lossmse) model.summary() early_stop EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) history model.fit(X_train, y_train, epochs50, batch_size32, validation_data(X_test, y_test), callbacks[early_stop], verbose1)第一层LSTM(units64, return_sequencesTrue)返回每个时间步的隐藏状态这样第二层 LSTM 才能继续序列建模。Dropout 层加在两轮 LSTM 之间作用是随机丢掉 20% 的神经元输出防止模型把训练集的噪声背下来。EarlyStopping监控验证集损失如果连续 10 个 epoch 都没有下降就提前终止训练并且用restore_best_weightsTrue回滚到验证集最优的权重。batch_size 设置 32epochs 设 50配合早停通常二十来个 epoch 就能收敛CPU 上跑十几分钟也能出结果。4. 评估与可视化RMSE 只是及格线方向准确率才是分水岭4.1 回归指标RMSE、MAE 的计算与解释LSTM 的原始输出是归一化后的数值不能直接用来算误差。要还原成真实价格必须先做inverse_transform的反归一化操作。这里最容易出的问题我已经在上面强调过scaler.inverse_transform期望输入的列数必须和当初fit时的列数一致。这份代码把全部特征列一起缩放所以反归一化时要切片取回收盘价那一列。from sklearn.metrics import mean_squared_error, mean_absolute_error # 预测结果反归一化 y_pred_scaled model.predict(X_test) y_test_inv scaler.inverse_transform(np.concatenate([y_test.reshape(-1, 1), np.zeros((len(y_test), 5))], axis1))[:, 0] y_pred_inv scaler.inverse_transform(np.concatenate([y_pred_scaled, np.zeros((len(y_pred_scaled), 5))], axis1))[:, 0] rmse np.sqrt(mean_squared_error(y_test_inv, y_pred_inv)) mae mean_absolute_error(y_test_inv, y_pred_inv) print(fRMSE: {rmse:.2f}, MAE: {mae:.2f})上面的代码块用了一个小技巧np.concatenate把预测值拼上五列 0凑成和原始特征矩阵一致的结构再做反归一化最后取第一列收盘价。y_test 的逆变换会用到同一个 scaler但顺序上要注意inverse_transform得到的矩阵第一列才是收盘价对应的缩放第二列是 MA5 等特征对应的缩放不能直接取整个矩阵。RMSE 的量纲是价格单位比如美元或分如果你想做一个与价格无关的评估可以除以测试期间的平均价格得到百分比误差。期末答辩时老师更关心的是你知不知道这两个指标各自的局限——RMSE 对大误差敏感MAE 更鲁棒所以这份代码两个都算了这是加分项。4.2 预测曲线可视化一张图把论文的“实验结果”撑起来期末大作业的评分很大程度取决于结果图好不好看、能不能讲出故事。这份代码画了两条曲线真实收盘价和 LSTM 预测收盘价横轴是时间纵轴是价格。从视觉上最常见的预测曲线形态是“滞后一拍”——预测线和真实线几乎平行但总是晚一天。这在股票预测领域几乎是必然的因为 LSTM 在最小化 MSE 时的最优策略是“预测值 ≈ 昨天的真实值”因为股价变化接近随机游走昨天的价格就是今天价格的无偏估计。import matplotlib.pyplot as plt plt.figure(figsize(12, 6)) plt.plot(test_df.index[-len(y_test_inv):], y_test_inv, labelReal Close, colorblack) plt.plot(test_df.index[-len(y_pred_inv):], y_pred_inv, labelLSTM Predict, colorsteelblue, linestyle--) plt.xlabel(Date) plt.ylabel(Close Price (USD)) plt.legend() plt.title(LSTM Prediction on Test Set) plt.show()画图的参数里值得说一句的是横坐标索引由于滑动窗口切掉了前lookback个样本预测结果对应的时间点应该是test_df.index[lookback:]而不是test_df.index[:]否则曲线会整体向左偏移一个窗口这在答辩时很容易被老师追问。如果你发现预测线整体比真实线低很多另一个常见错误是反归一化时用错了 scaler或者训练集和测试集的价格区间相差过大比如测试区间发生了暴涨暴跌导致缩放后的预测值在反向变换时发生偏移。4.3 方向准确率比 RMSE 更能说明问题的指标回归误差只说明“预测价格偏离了多少”但股票预测实际更关心方向判断得对不对——明天是涨还是跌。一个模型如果 RMSE 偏大但方向准确率超过 55%在应用层面就比特单纯预测更准的马模型更有价值。所以这份代码额外计算了“方向命中率”比较sign(真实收益)和sign(预测收益)是否一致。# 真实下一日涨跌方向与预测下一日涨跌方向 true_next np.diff(y_test_inv) # 真实明天减今天 pred_next np.diff(y_pred_inv) # 预测明天减今天 true_direction np.sign(true_next) pred_direction np.sign(pred_next) accuracy np.mean(true_direction pred_direction) print(fDirection Accuracy: {accuracy:.2%})此处有个细节要注意np.diff会让结果长度比原始数组少 1所以计算准确率时两个方向序列的长度要确保一致。方向准确率的合理基线是 50%——相当于抛硬币如果模型低于 55%说明它的预测基本没有实际参考价值但作为大作业能跑到 52%~58% 已经属于正常水平。你可以把这组数字和 RMSE 一起写进论文结论比单纯展示一张“拟合得很漂亮”的图更能说明问题。5. 避坑与常见问题排查期末周最常见的五个翻车现场5.1 现象训练集 Loss 很低测试集 Loss 高得离谱原因标准化时在全量数据上 fit 了 MinMaxScaler或者滑窗切分时没有按时间顺序严格切分导致测试集的分布信息提前泄漏进训练过程。模型的“高分”只是记住了价格区间而不是学到了规律。解决先把数据按时间顺序切成 train 和 test 两段只对训练段fit标准化器再分别transform。每次跑新实验前检查scaler.min_和scaler.scale_是否只基于训练段计算。5.2 现象Loss 能降到很低但预测曲线是一条近似水平的直线原因股价序列本身接近随机游走模型在 MSE 优化下的最优解就是预测“等于上一个时刻的真实值”。如果你看到预测线几乎贴着真实线但滞后一天这其实是正常现象不用惊慌。但如果预测线完全水平且偏离真实值说明 LSTM 的输入特征失效了可能原因是特征列里混入了太多一律性数值比如常数列或者学习率太大导致模型没有收敛。解决把lookback调小到 10~15 天并检查特征列是否存在 NaN 填充错误的异常值。5.3 现象yfinance 下载 A 股数据时部分股票返回空 DataFrame原因yfinance 对 A 股代码的格式要求是600519.SS上交所或000001.SZ深交所如果写成600519或600519.SZ接口会查不到数据。另一个隐蔽问题是连续停牌的股票比如 ST 股在区间内没有交易记录下载结果可能只有一行或为空。解决用df.dropna(axis0, howany)提前清洗并打印df.head()确认数据区间是否正确换标的时建议先用 yfinance 的Ticker.history(period1mo)做一次连通性测试。5.4 现象LSTM 训练十几个 epoch 后 val_loss 震荡不降原因最常见的是学习率偏大Adam 在接近局部最优时出现震荡其次是滑窗序列样本之间存在高度重合训练集和验证集的分布几乎相同导致验证集 Loss 无法真实反映泛化能力。解决先把学习率降到0.0005或0.0003重跑如果依然震荡增加 Dropout 比例到 0.3并把patience从 10 降到 5让 EarlyStopping 更早触发。注意每次改超参都要记录别靠手感瞎调——期末答辩时老师说“你这个参数为什么取 64”你得答得上来。5.5 现象预测结果整体比真实价格低或高一个固定比例原因反归一化时张量结构对不上。比如当初对 6 列特征做了标准化但inverse_transform时只传入了 1 列收盘价sklearn 会报维度错误于是很多人改成“先用 scaler 处理 1 列的预测值”结果缩放参数与训练时不一致。解决不管预测几列都用np.concatenate拼接成和原始特征维度一致的矩阵再切片取收盘价那一列。这个坑在这份代码里已经处理好了但如果你改成只预测Close一列要记得重写整个标准化流程不能混用。6. 验证的进阶玩法从“跑通代码”到“能解释结果”6.1 把方向准确率扩展成一次最简回测方向准确率只是一个统计值期末作业想拿高分最好把它翻译成“策略收益”。你可以做这样的最简回测从测试集开始每天按 LSTM 预测方向做多或做空或者只做多头计算累计收益。代码逻辑很简单不需要引入回测框架capital 1.0 positions [] for i in range(len(pred_direction)): daily_ret true_next[i] / y_test_inv[i] # 当日真实收益率 if pred_direction[i] 0: capital * (1 daily_ret) # 预测涨持有 elif pred_direction[i] 0: capital * (1 - daily_ret) # 预测跌空仓或反向 positions.append(capital) buy_hold np.cumprod(1 true_next / y_test_inv[:-1]) # 基准一直持有这个回测没有考虑交易费逻辑上只是把“预测方向”与“真实收益”相乘得到一个策略净值曲线再和“一直持有”做对比。答辩时你能讲清楚“模型方向准确率 54%回测曲线跑赢基准”就已经超过九成只贴 MSE 的同学了。6.2 滚动向前验证把“一次性切分”升级成更可信的评估方式静态切分最大的问题是结果对“在哪个日期切分”非常敏感切在牛转熊的节点和切在熊转牛的节点效果是天壤之别。滚动向前验证的做法是固定训练窗口长度比如 300 天每次向前滚动 20 天重新训练一次模型并预测未来 20 天然后把多次预测结果拼起来。整个过程会跑很多轮模型每个模型只重训一两次训练开销大但对期末作业来说你只需要跑 3 次滚动3 折就能画出一条比静态切分更有说服力的预测曲线。6.3 多说一句从那以后我每次拿到时间序列大作业都会先检查三个地方数据切刀有没有按时间排序、Scaler 是不是只在训练集上 fit、反归一化有没有补齐维度。这套流程走一遍项目基本不会出大问题。这份代码把这三件事都处理干净了你拿到手先跑通再改参很快就能看到一条能解释的预测曲线。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站