简介:基于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_flag=hfq/qfq | 本地研究、课程设计 | 停牌日数据缺失需 forward fill |
| tushare 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_date='2015-01-01', end_date='2023-12-31', frequency="d", adjustflag="2" ) data_list = [] while (rs.error_code == '0') & rs.next(): data_list.append(rs.get_row_data()) df = pd.DataFrame(data_list, columns=rs.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(drop=True) print(df[['date', 'close']].head())逻辑说明:先用query_history_k_data_plus拉取后复权日线,adjustflag=2表示后复权;然后用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_size=20, test_ratio=0.2): diff = np.diff(close, prepend=close[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, prepend=close[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_sequences=True, 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(optimizer=keras.optimizers.Adam(learning_rate=lr), loss='mse') early_stop = keras.callbacks.EarlyStopping(patience=5, restore_best_weights=True) model.fit(X_train, y_train, epochs=30, batch_size=batch_size, validation_data=(X_val, y_val), callbacks=[early_stop], verbose=0) val_loss = model.evaluate(X_val, y_val, verbose=0) return val_loss逻辑说明:fitness函数接收一个粒子向量,把它拆成五个超参数,构造 LSTM 模型、训练、在验证集上评估,返回验证集 MSE 作为适应度值。PSO 的每一次迭代都要反复调用这个函数。参数说明:patience=5表示验证集 loss 连续 5 轮不下降就提前停止,这是给 PSO 省时间的关键;restore_best_weights=True保证返回的是验证集最优权重而不是最后一轮权重;verbose=0关掉训练日志,否则 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_size=8, max_iter=10): 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_size=8是权衡耗时和效果的临界值——把粒子数加到 20,收敛会更好,但每多一个粒子就要多训练 8 次 LSTM;max_iter=10意味着最多要训练 80 个 LSTM 模型,单机 CPU 上可能要跑两个小时。时间充裕可以把max_iter加到 15,但我很少超过这个数,因为到后期 PSO 收敛很慢,边际收益太低。
提示:PSO 是随机算法,每次运行结果不完全一致。正式实验要固定
np.random.seed,并记录群体最优的完整轨迹,方便复盘算法是否陷入了早熟收敛。
4. 模型训练与评估:先看方向正确率,再盯验证集 loss
4.1 训练脚本的组织方式:训练集、验证集、测试集三分离
很多初学者把测试集当验证集用,这是 PSO-LSTM 项目里最高频的翻车点。PSO 在迭代过程中会不断根据验证集误差调整超参数,如果这个“验证集”就是最终评估用的测试集,那测试集的信息已经通过粒子位置泄漏进模型了。最后画出来的预测曲线符合得再好,也不能说明模型具备泛化能力。我一般把整个数据集按时间顺序切成 70%、15%、15% 三块,每块内部不 shuffle,LSTM 训练时只允许接触训练集和验证集。
训练轮数方面,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), dim=5, 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]), epochs=80, batch_size=int(round(batch_size)), verbose=1) test_loss = final_model.evaluate(X_test, y_test, verbose=0) print(f"best 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.01,RMSE 很小,但你可能根本没抓住这波上涨。我做评估时会同时算三个指标: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(f"RMSE: {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, label='true close', linewidth=2) plt.plot(price_pred, label='pred close', linewidth=2, 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,让粒子在前期大步搜索、后期精细收敛。如果品牌类代码里已经是固定w=0.6,至少把c1、c2从对称的 1.5 改成前大后小,比如c1=1.8、c2=1.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降到 20,patience降到 3,swarm_size降到 5,max_iter降到 8。还有一招是参考常见做法——先用一组直觉超参数(学习率 0.001、32 个神经元)训练一轮,把数据量缩减到原来的 60% 做 PSO 搜索,拿到最优粒子后再用全量数据复训。PSO 找到的超参数对有噪声的金融数据来说,精度不需要太高,接近最优即可。
5.5 现象:模型输出恒定值或接近恒定值,pred 曲线是一条水平线
原因:差分序列中绝大多数值接近 0,LSTM 在梯度更新时偏向预测均值。如果训练集中上涨日和下跌日分布严重不均衡,模型学到的策略就是“预测 0 附近”,RMSE 看起来还行,方向正确率却只能随机。
解决:检查训练集差分值的均值与标准差。标准差过小说明行情平稳,这类数据不适合做价格预测;此时不能硬调 LSTM,应该引入额外特征,比如成交量变化、板块指数差分,给模型提供信息增量。
5.6 现象:明明代码一样,换了一只股票后测试集 RMSE 暴增 10 倍
原因:不同股票的价格量级和波动率差异巨大,直接用同一套归一化参数跑全市场会失真。银行股价格长期在 3 到 10 元之间波动,而高价股可能上下几倍,归一化区间被极端值拉宽后,正常波动被压缩到极窄区间。
解决:归一化参数(min_val、max_val)必须在每个标的内部单独计算,不允许跨标的共享。你可以编写一个 wrapper 函数,每次切换股票代码时自动重算归一化参数并清空 PSO 历史粒子,否则旧股票的粒子位置会污染新股票的搜索空间。
6. 滚动外推预测:从单步预测走向真正可用的最后一步
前文测试集的评估方式是“每个窗口独立输入模型得到下一步预测”,但生产环境里你要的是“用截至今天的数据,预测明天调整收盘价是涨是跌”。这两者的区别在于是否使用模型自己的预测值作为下一步输入。滚动外推的具体流程是:用历史窗口预测出第 T+1 步,把这个预测值拼进窗口末尾,丢弃最旧的值,再预测第 T+2 步。这段逻辑充其量 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), verbose=0)[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 风力发电功率预测模型,反而在企业项目里落地了。技术本身没有错,错的是把它用在信噪比太低的任务上。希望你跑通源码后,先别急着追求完美预测曲线,把方向正确率和滚动外推这两块基础功打扎实,再去想能不能换一个更合适的数据源或模型结构——希望帮到你。
本文还有配套的精品资源,点击获取