尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

PSO-LSTM神经网络调整收盘价预测:超参数优化与时序建模实战

PSO-LSTM神经网络调整收盘价预测:超参数优化与时序建模实战 简介基于PSO-LSTM神经网络的股票调整收盘价预测源码包面向需要完成期末大作业或课程设计的高校学生也适合刚接触深度学习时序预测的开发者。项目使用粒子群算法自动搜索LSTM最优超参数包含数据预处理、模型搭建、训练与评估等核心环节可对多只股票调整收盘价进行单步预测代码注释详尽且数据与主程序分离新手也能看懂并快速部署。压缩包共10个文件包含7个CSV行情数据集、1个Python主程序、README运行说明及辅助文本数据集覆盖美股、A股、黄金等标的整体仅490KB轻量易用。目前已有171人学习下载既可作为金融数据分析的实践项目也可延伸到参数调优和毕业设计拓展。下载后能获得完整可运行的预测脚本、多品种测试数据及优化思路便于对照预测结果并迁移到其他时间序列任务。1. 拿到这份 PSO-LSTM 源码包先别急着跑代码如果你搜到“基于 PSO-LSTM 神经网络的股票调整收盘价预测 Python 源码”大概率是两种情况一是课程设计/毕业论文需要交一份能跑的深度学习预测代码二是想拿它做量化交易的选股信号。先说结论这个方案在学术框架里很成熟但股票价格预测的信噪比极低不要指望它当印钞机。它的真正价值在于让你完整走通“数据清洗 → 特征构造 → 神经网络建模 → 启发式算法调参”这条流水线尤其是粒子群优化PSO怎么自动替 LSTM 找超参数这个思路可以直接迁移到其他时序任务上。这篇笔记就按我自己复现这类项目时的顺序来讲先从数据源头把调整收盘价搞明白再做差分、归一化和滑动窗口接着实现 PSO 与 LSTM 的耦合最后谈评估指标和那些容易翻车的细节。2. 先搞懂调整收盘价从三个数据源拿到干净训练数据的实操对比2.1 为什么模型吃的是调整收盘价而不是原始收盘价量化回测里有个经典教训用不复权的原始收盘价训练模型遇到除权除息日价格曲线会突然跳出一个大坑或者一根长阳LSTM 会把这当成真实的趋势信号去学习结果模型在分红季节前后频繁误判。要把缺口切掉最常用的做法是前复权或后复权后复权以最早上市日为基准把后续所有价格按分红送转比例向上抬升前复权以最新价格为基准把历史价格向下调整。两者对预测任务的影响不大但前复权有个边界坑——新股上市初期因子调整接近负值个别数据源会给出负价格训练前必须清洗。我一般直接用后复权的调整收盘价做序列预测。后复权价格反映的是“如果从上市那天一直持有并不断分红再投资今天的持仓成本应该对应多少市值”它的数值更接近真实收益曲线而且不会因为最新价格变动让历史窗口整段重算。训练时如果把前复权价格存到本地每过一段时间最新价变了历史数据要重新拉一遍麻烦。2.2 数据源选择baostock、tushare 与 akshare 的字段对比国内能稳定拿到调整收盘价的数据源主要有三个baostock、tushare pro 和 akshare。排序上我最常用 baostock因为它不需要 token日线前复权/后复权因子是免费开放的字段直接叫adjustFlag传参qfq或hfq就能取。tushare pro 的数据质量好但调整因子在积分门槛里新注册用户调adj_factor接口可能受限。akshare 本质是爬虫聚合偶尔会因网页改版断供不适合做成定时任务。数据源是否需要 token调整收盘价字段适合场景主要坑baostock否adjust_flaghfq/qfq本地研究、课程设计停牌日数据缺失需 forward filltushare pro是需积分adj_factor 需自行计算策略回测、生产链路积分门槛限制高频调取akshare否接口名随版本变化快速验证接口维护不稳需锁版本取数代码本身不复杂关键在两个细节一是把adjustflag参数显式写成2代表后复权避免依赖默认行为二是停牌日会出现整行 NaN很多新手直接把 NaN 行 drop 掉导致时间轴断裂后续滑动窗口会产生跨周拼接的假序列。正确的做法是用ffill()填充停牌价格再把当天成交量为 0 的样本打上标记。import baostock as bs import pandas as pd lg bs.login() # 取平安银行 2015-01-01 到 2023-12-31 的后复权日线 rs bs.query_history_k_data_plus( sz.000001, date,code,open,high,low,close,preclose,volume,amount,adjustflag, start_date2015-01-01, end_date2023-12-31, frequencyd, adjustflag2 ) data_list [] while (rs.error_code 0) rs.next(): data_list.append(rs.get_row_data()) df pd.DataFrame(data_list, columnsrs.fields) bs.logout() df[close] pd.to_numeric(df[close]) df[volume] pd.to_numeric(df[volume]) # 停牌日补位而不是删行 df df.replace(, pd.NA).ffill() df df.dropna(subset[close]).reset_index(dropTrue) print(df[[date, close]].head())逻辑说明先用query_history_k_data_plus拉取后复权日线adjustflag2表示后复权然后用ffill()填充停牌造成的空价格。参数说明baostock 的frequency支持d、w、m三档预测调整收盘价用日线即可adjustflag的取值1是前复权、2是后复权、3是不复权这里必须显式指定我吃过默认值的亏——早期版本不传这个参数默认不复权模型在一个除权缺口上疯狂报错。2.3 窗口样本怎么构造时间顺序、差分与合法划分拿到价格序列后不能直接塞进 LSTM。股票价格本身非平稳直接用原始价格训练模型学到的往往是“昨天的价格近似等于今天”看曲线拟合得很好一换样本就崩。常见做法是先做一阶差分把价格序列转成收益率或价格变化量再归一化。我习惯做一阶差分后接最大最小值归一化把差分值压到 0 到 1 之间避免 LSTM 的 tanh 激活函数过早饱和。滑动窗口的构造有一个必须遵守的纪律窗口内部必须按时间连续训练集和测试集的划分不能打乱。有种翻车操作是直接调用train_test_split默认 shuffle 会把 2020 年的样本和 2022 年的样本混进同一个训练集测试集里出现了“未来的邻居”评估结果虚高。import numpy as np def make_dataset(close, window_size20, test_ratio0.2): diff np.diff(close, prependclose[0]) # 一阶差分 # 最大最小值归一化 min_val, max_val diff.min(), diff.max() diff_norm (diff - min_val) / (max_val - min_val 1e-9) X, y [], [] for i in range(len(diff_norm) - window_size): X.append(diff_norm[i:i window_size]) y.append(diff_norm[i window_size]) X np.array(X).reshape(-1, window_size, 1) y np.array(y) # 按时间顺序切分不做 shuffle split_idx int(len(X) * (1 - test_ratio)) return X[:split_idx], y[:split_idx], X[split_idx:], y[split_idx:], min_val, max_val逻辑说明先把收盘价转成一阶差分再做归一化然后用滑动窗口生成(样本数, 窗口长度, 特征维度)的三维数组最后按时间顺序切分。参数说明window_size我一般取 20 或 30对应约一个月交易日的价格变化取太大比如 60会让模型感知到过多的旧信息在风格切换的市场里反应迟钝test_ratio取 0.2 是折中方案如果样本量少可以降到 0.1但要保证测试集至少包含 200 个以上样本否则评估指标的方差会大到没意义。数据这一层最容易踩的另一个坑是差分泄露。np.diff(close, prependclose[0])保持了数组长度不变但第一个差分值是 0这不是真实变化量。虽然 LSTM 对单个异常点不太敏感但如果你严格追求训练/测试分布一致应该把prepend改为不填充直接丢弃第一个无效样本。两种写法都能跑我倾向于后者因为测试集的第一个预测时刻对应的是“真实的前一交易日差值”不是人造的 0。3. PSO 与 LSTM 的耦合粒子编码、适应度函数与迭代策略3.1 为什么调参选 PSO 而不是网格搜索LSTM 的超参数空间是连续的学习率、隐藏层神经元数、层数、dropout、batch size、训练轮数排列组合起来是个天文数字。网格搜索在这个空间里完全是碰运气随机搜索稍微好点但同样没有反馈。PSO 的核心逻辑是初始化一群“粒子”每个粒子代表一组超参数组合每轮迭代根据粒子自身历史最优位置和群体历史最优位置更新速度与位置不断向误差更低的方向逼近。相比遗传算法PSO 没有交叉变异参数少、收敛快实现起来不到一百行代码非常适合给 LSTM 这种训练一次就要几分钟的模型做黑盒优化。但要注意PSO 是连续优化算法而 LSTM 的隐藏层神经元数、层数这类参数是离散整数粒子位置更新后需要用四舍五入或强制取整来适配。速度更新时还要做边界约束避免粒子飞到负学习率这种非法区间。3.2 粒子如何编码一个粒子代表一组 LSTM 超参数我常用的粒子编码是五维向量学习率、LSTM 第一层神经元数、LSTM 第二层神经元数可选为 0 表示单层、dropout、batch size。训练轮数不放进粒子固定给 30 或 50 轮因为轮数对模型容量的影响和早期停止重合放进 PSO 会显著拉长整体耗时。适应度函数是整个耦合的关键。PSO 每评估一个粒子就要完整训练一次 LSTM 并在验证集上算误差。验证集不能复用测试集否则 PSO 会“看着测试集调参”最终评估结果明显偏乐观。我一般把数据切成三份训练集 70%、验证集 15%、测试集 15%PSO 只看验证集误差测试集只用于最后一轮评估。import numpy as np from tensorflow import keras from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.models import Sequential def fitness(particle, X_train, y_train, X_val, y_val): lr, units1, units2, dropout, batch_size particle units1 max(4, int(round(units1))) units2 max(0, int(round(units2))) dropout np.clip(dropout, 0.0, 0.5) batch_size max(8, int(round(batch_size))) model Sequential() if units2 0: model.add(LSTM(units1, return_sequencesTrue, input_shape(X_train.shape[1], 1))) model.add(Dropout(dropout)) model.add(LSTM(units2)) else: model.add(LSTM(units1, input_shape(X_train.shape[1], 1))) model.add(Dropout(dropout)) model.add(Dense(1)) model.compile(optimizerkeras.optimizers.Adam(learning_ratelr), lossmse) early_stop keras.callbacks.EarlyStopping(patience5, restore_best_weightsTrue) model.fit(X_train, y_train, epochs30, batch_sizebatch_size, validation_data(X_val, y_val), callbacks[early_stop], verbose0) val_loss model.evaluate(X_val, y_val, verbose0) return val_loss逻辑说明fitness函数接收一个粒子向量把它拆成五个超参数构造 LSTM 模型、训练、在验证集上评估返回验证集 MSE 作为适应度值。PSO 的每一次迭代都要反复调用这个函数。参数说明patience5表示验证集 loss 连续 5 轮不下降就提前停止这是给 PSO 省时间的关键restore_best_weightsTrue保证返回的是验证集最优权重而不是最后一轮权重verbose0关掉训练日志否则 PSO 每评估一个粒子就刷半屏输出根本没法看。3.3 PSO 主循环惯性权重、个体学习因子与社会学习因子class Particle: def __init__(self, dim, bounds): self.position np.array([np.random.uniform(b[0], b[1]) for b in bounds]) self.velocity np.zeros(dim) self.best_position self.position.copy() self.best_score float(inf) def pso_optimize(fitness_func, dim, bounds, swarm_size8, max_iter10): swarm [Particle(dim, bounds) for _ in range(swarm_size)] global_best_position swarm[0].position.copy() global_best_score float(inf) w, c1, c2 0.6, 1.5, 1.5 for t in range(max_iter): for p in swarm: score fitness_func(p.position) if score p.best_score: p.best_score score p.best_position p.position.copy() if score global_best_score: global_best_score score global_best_position p.position.copy() for p in swarm: r1, r2 np.random.rand(dim), np.random.rand(dim) p.velocity (w * p.velocity c1 * r1 * (p.best_position - p.position) c2 * r2 * (global_best_position - p.position)) p.position p.position p.velocity p.position np.clip(p.position, [b[0] for b in bounds], [b[1] for b in bounds]) return global_best_position, global_best_score逻辑说明每个粒子先随机初始化位置和速度然后循环执行“评估适应度 → 更新个体最优和群体最优 → 更新速度与位置”。w是惯性权重控制粒子保持前一时刻速度的程度c1是个体学习因子让粒子朝自己找到过的最好位置靠拢c2是社会学习因子让粒子朝整个群体找到过的最好位置靠拢。参数说明swarm_size8是权衡耗时和效果的临界值——把粒子数加到 20收敛会更好但每多一个粒子就要多训练 8 次 LSTMmax_iter10意味着最多要训练 80 个 LSTM 模型单机 CPU 上可能要跑两个小时。时间充裕可以把max_iter加到 15但我很少超过这个数因为到后期 PSO 收敛很慢边际收益太低。提示PSO 是随机算法每次运行结果不完全一致。正式实验要固定np.random.seed并记录群体最优的完整轨迹方便复盘算法是否陷入了早熟收敛。4. 模型训练与评估先看方向正确率再盯验证集 loss4.1 训练脚本的组织方式训练集、验证集、测试集三分离很多初学者把测试集当验证集用这是 PSO-LSTM 项目里最高频的翻车点。PSO 在迭代过程中会不断根据验证集误差调整超参数如果这个“验证集”就是最终评估用的测试集那测试集的信息已经通过粒子位置泄漏进模型了。最后画出来的预测曲线符合得再好也不能说明模型具备泛化能力。我一般把整个数据集按时间顺序切成 70%、15%、15% 三块每块内部不 shuffleLSTM 训练时只允许接触训练集和验证集。训练轮数方面PSO 内层 LSTM 用 30 个 epoch 加早停外层拿到最佳粒子后我会用完整的训练加验证集重新训练一次轮数放大到 80再评估测试集。这样做是因为 PSO 评估下的模型只用了 70% 的训练数据而最终部署应该让模型看到尽可能多的历史信息。best_pos, best_score pso_optimize( lambda pos: fitness(pos, X_train, y_train, X_val, y_val), dim5, bounds[(1e-4, 1e-2), (16, 128), (0, 64), (0.0, 0.4), (16, 128)] ) # 用最佳粒子重新训练最终模型 lr, units1, units2, dropout, batch_size best_pos final_model build_lstm(lr, units1, units2, dropout) final_model.fit(np.vstack([X_train, X_val]), np.concatenate([y_train, y_val]), epochs80, batch_sizeint(round(batch_size)), verbose1) test_loss final_model.evaluate(X_test, y_test, verbose0) print(fbest particle: {best_pos}, test loss: {test_loss})逻辑说明先跑 PSO 拿到最优粒子再用该粒子的超参数在训练集加验证集上重新训练最后在测试集上评估。参数说明PSO 的边界bounds要和fitness内部的取整逻辑保持一致。学习率下限1e-4、上限1e-2超过这个范围梯度要么消失要么震荡LSTM 神经元数上限 128 对单变量序列足够128 以上参数量陡增但收益很小dropout 上限 0.4太高会让模型欠拟合。4.2 评估指标RMSE、MAPE 与方向正确率回归任务最常用的 RMSE 在股票预测里有个大问题它只衡量数值偏离程度不衡量方向是否猜对。预测值从 10.00 变成 10.01RMSE 很小但你可能根本没抓住这波上涨。我做评估时会同时算三个指标RMSE 看整体误差量级MAPE 看相对误差方向正确率看模型涨跌判断的命中率。对交易决策来说方向正确率才是最贴近实际收益的指标。from sklearn.metrics import mean_squared_error, mean_absolute_percentage_error pred final_model.predict(X_test).flatten() truth y_test.flatten() rmse np.sqrt(mean_squared_error(truth, pred)) mape mean_absolute_percentage_error(truth, pred) direction_acc np.mean((np.diff(pred) 0) (np.diff(truth) 0)) print(fRMSE: {rmse:.6f}, MAPE: {mape:.4f}, Direction Acc: {direction_acc:.2%})逻辑说明对测试集的连续预测序列计算数值误差和方向命中率。方向正确率的计算方式是对预测值和真实值各自做一阶差分判断相邻时刻的增减方向是否一致。参数说明这里用的是差分后的一阶序列方向正确率反映的是“下一时刻涨跌方向是否猜对”而不是“价格水平是否贴近”。如果只报告 RMSE模型在平稳行情里看起来很好一旦出现急涨急跌就现原形——这也是网上很多 PSO-LSTM 股票预测项目展示图曲线完美贴合的根本原因它们拿原始收盘价做数值拟合但没算方向命中率。注意测试集预测时LSTM 的每个样本窗口是独立的、不共享状态因此pred序列不是真正的滚动外推预测。真正的滚动预测是从测试集第一个窗口出发用预测值不断拼接成新窗口再预测下一步这部分在第 6 章单独讲。4.3 预测结果可视化画什么图才能看出模型好坏最常见的翻车展示图是“收盘价预测曲线 vs 真实收盘价曲线”两条线几乎重合。问题在于模型预测的是差分序列可视化时要把差分累加成价格做累加还原# 差分化预测还原成价格序列 diff_pred pred * (max_val - min_val) min_val price_pred np.cumsum(diff_pred) close_test[0] # 画图比较 import matplotlib.pyplot as plt plt.figure(figsize(12, 5)) plt.plot(close_test, labeltrue close, linewidth2) plt.plot(price_pred, labelpred close, linewidth2, linestyle--) plt.legend() plt.title(adjusted close price prediction on test set) plt.show()逻辑说明先把归一化预测值反变换回差分空间再做cumsum累加还原成价格序列。参数说明close_test[0]是测试集第一天的真实收盘价作为累加起点差分累加会把单步预测误差不断累积所以后段曲线会逐渐偏离这是差分方法的固有特性不是代码 bug。看到预测曲线和真实曲线在测试集后半段拉开距离不必急着调参要先确认是不是误差累积造成的。5. PSO-LSTM 的常见问题与排查六条血泪经验5.1 现象PSO 迭代前期 loss 下降很快后期纹丝不动原因PSO 粒子群在迭代后期聚集到局部最优附近速度趋于 0惯性权重固定导致粒子无法跳出当前区域。这是标准 PSO 的早熟收敛特征。解决把惯性权重w改成线性递减从 0.9 降到 0.4让粒子在前期大步搜索、后期精细收敛。如果品牌类代码里已经是固定w0.6至少把c1、c2从对称的 1.5 改成前大后小比如c11.8、c21.2加强前期个体探索。5.2 现象训练集的 loss 一直降测试集的方向正确率只有 50%原因模型过拟合历史噪声。股票价格差分序列的白噪声成分很高LSTM 层数多、神经元数大时模型有能力“背下”训练集的每个随机波动。解决优先检查 PSO 找到的 dropout 是不是接近 0如果是手动把 dropout 下限从 0 改成 0.1 再跑一轮同时把 LSTM 层数限制在 2 层以内对单变量价格序列3 层以上的 LSTM 几乎不会带来收益。5.3 现象预测曲线和真实曲线在测试集开头重合越往后越离谱原因差分累加导致单步误差逐日累积后段偏离不代表模型失效而是误差累积的结果。还有另一种可能测试集窗口依赖训练集最后一段数据如果训练集和测试集之间存在停牌日补位的ffill假数据前几步预测会被带偏。解决把方向正确率的计算窗口从全测试集改为“最近 60 个交易日”滑动窗口看的是近端表现同时检查停牌日填充策略是否在切分边界制造了连续假样本。5.4 现象PSO 评估一个粒子要几分钟整体跑一个通宵还没结束原因swarm_size和max_iter乘积太大加上每个 LSTM 都固定训练 30 个 epoch没有早停机制。解决把epochs降到 20patience降到 3swarm_size降到 5max_iter降到 8。还有一招是参考常见做法——先用一组直觉超参数学习率 0.001、32 个神经元训练一轮把数据量缩减到原来的 60% 做 PSO 搜索拿到最优粒子后再用全量数据复训。PSO 找到的超参数对有噪声的金融数据来说精度不需要太高接近最优即可。5.5 现象模型输出恒定值或接近恒定值pred 曲线是一条水平线原因差分序列中绝大多数值接近 0LSTM 在梯度更新时偏向预测均值。如果训练集中上涨日和下跌日分布严重不均衡模型学到的策略就是“预测 0 附近”RMSE 看起来还行方向正确率却只能随机。解决检查训练集差分值的均值与标准差。标准差过小说明行情平稳这类数据不适合做价格预测此时不能硬调 LSTM应该引入额外特征比如成交量变化、板块指数差分给模型提供信息增量。5.6 现象明明代码一样换了一只股票后测试集 RMSE 暴增 10 倍原因不同股票的价格量级和波动率差异巨大直接用同一套归一化参数跑全市场会失真。银行股价格长期在 3 到 10 元之间波动而高价股可能上下几倍归一化区间被极端值拉宽后正常波动被压缩到极窄区间。解决归一化参数min_val、max_val必须在每个标的内部单独计算不允许跨标的共享。你可以编写一个 wrapper 函数每次切换股票代码时自动重算归一化参数并清空 PSO 历史粒子否则旧股票的粒子位置会污染新股票的搜索空间。6. 滚动外推预测从单步预测走向真正可用的最后一步前文测试集的评估方式是“每个窗口独立输入模型得到下一步预测”但生产环境里你要的是“用截至今天的数据预测明天调整收盘价是涨是跌”。这两者的区别在于是否使用模型自己的预测值作为下一步输入。滚动外推的具体流程是用历史窗口预测出第 T1 步把这个预测值拼进窗口末尾丢弃最旧的值再预测第 T2 步。这段逻辑充其量 40 行def rolling_forecast(model, last_window, steps, min_val, max_val): window last_window.copy() preds [] for _ in range(steps): # 输入模型预测单步差分值 y_pred model.predict(window.reshape(1, -1, 1), verbose0)[0, 0] preds.append(y_pred) # 反归一化累加回价格 # 窗口内保持归一化空间直接拼接差分预测值 new_val np.clip(y_pred, 0, 1) window np.roll(window, -1) window[-1] new_val pred_diff np.array(preds) * (max_val - min_val) min_val return np.cumsum(pred_diff)逻辑说明rolling_forecast接收一个已经在归一化空间的窗口数组循环预测下一步并更新窗口内容。np.roll(window, -1)把所有值左移一位再把新预测值放到最后一位。参数说明steps是外推步数我强烈建议不超过 10 步。差分序列模型本身没有长记忆每一步都在用上一步的误差作为输入步数越长曲线越偏向随机游走。我自己做过最多 30 步的滚动外推结果稳定地收敛到一条直线——模型最终预测的差分值趋于 0累积后价格不再变化。最后的进阶建议是把 PSO 的适应度函数从“最小化验证集 RMSE”改成“最大化验证集方向正确率”。这个改动会显著影响粒子收敛方向模型不再追求数值贴近而是在意涨跌方向命中。代价是方向正确率指标非光滑PSO 收敛会变慢需要把max_iter上调到 15 左右。我试过的最优组合是RMSE 作为早期筛选、方向正确率在最后 3 轮迭代中作为排序依据两阶段选择比单一指标更稳。这套方案的真正用途不是直接实盘而是帮你建立一套可复用的“启发式搜索 深度时序模型”框架。我第一次跑通 PSO-LSTM 时也以为找到了印钞机直到拿方向正确率一算发现和抛硬币没差多少。后来我把它改造成 PSO 自动调参的 LSTM 风力发电功率预测模型反而在企业项目里落地了。技术本身没有错错的是把它用在信噪比太低的任务上。希望你跑通源码后先别急着追求完美预测曲线把方向正确率和滚动外推这两块基础功打扎实再去想能不能换一个更合适的数据源或模型结构——希望帮到你。本文还有配套的精品资源点击获取
返回列表