news 2026/10/9 17:11:57

遗传算法优化LSTM超参数,股市预测自动化调参实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
遗传算法优化LSTM超参数,股市预测自动化调参实战

简介:这份资源是Python基于遗传算法优化LSTM模型的股市预测完整项目,适合需要完成毕业设计、期末大作业或课程设计的Python学习者,也适合对量化交易与时间序列预测感兴趣的开发者。资源内含源代码、训练好的模型和配套数据集,代码附有详细注释,从LSTM模型搭建到遗传算法参数寻优均有清晰说明,便于新手理解与二次开发。压缩包共89个文件,以Python脚本(24个py)、编译缓存(19个pyc)、文本说明(13个txt)及Web前端资源(html/css/js)为主,另含少量Excel数据文件、sqlite3数据库和项目配置文件,整体大小仅8.88MB,结构紧凑且易于部署。项目经过严格调试确保可运行,界面简洁、操作流程完整,已被导师认可为高分项目,目前已有74人学习使用,可直接作为毕设或课程设计的核心成果,省去从零搭建的繁琐过程。

1. 遗传算法优化LSTM的股市预测:超参调不动,换个思路搜

同样是拿日线数据预测下一交易日收盘价,有人跑通三层LSTM依然亏损,有人用单层结构配合合适的超参数做出像样的回测曲线。真正拉开差距的往往不是网络层数,而是timestep、学习率、隐藏层神经元这些不起眼的超参数。遗传算法优化LSTM的股市预测方案,本质就是把调参从手工试错变成有方向的进化搜索:用GA自动寻找LSTM的超参数组合,再训练模型做价格预测。下面按这套方案的完整链路展开,覆盖数据集准备、GA与LSTM结合的代码实现、常见翻车点与验证方法,适合准备做金融时序实验、想摆脱手动试参的开发者和研究者。预测结果只作技术验证,不构成投资建议。

2. LSTM超参数为什么难调:从选型逻辑到搜索策略的选择

股价日线数据是小样本、强噪声、非平稳的时间序列,LSTM能不能从里面学到可迁移的模式,很大程度不是网络结构决定的,而是超参数决定的。同一个模型结构,timestep从5改到20,验证集损失可能差一个数量级。先搞清楚哪些参数最敏感,再谈用什么策略去搜。

2.1 决定预测效果的五个核心超参数

我一般把LSTM在日线数据上的超参数分成五类,它们对训练行为和最终预测的影响各不相同。下面这张表是常用的起始取值范围,具体项目里可以按数据量缩放。

参数参考范围对模型的影响
timestep5~30记忆窗口长度,决定模型看多少天历史
hidden116~128第一层隐藏神经元数,决定记忆容量
hidden20~64第二层神经元数,0表示不堆层
learning_rate1e-4~1e-2梯度步长,直接影响收敛质量
batch_size16~64梯度估计稳定性,影响训练方差
dropout0~0.4随机失活比例,抑制过拟合

timestep是最容易出问题的参数。股市日线数据的有效记忆窗口通常在5到30个交易日之间,对应一周到一个多月的价格走势。设得太短,模型只看得到最近几天的随机波动,学不到趋势;设得太长,输入里混入大量与当前走势无关的旧信息,梯度在时间维上反复传播,容易消失。hidden1则直接决定LSTM的记忆容量,日线数据本身信息量有限,128个神经元通常已经足够,再往上加只会让训练变慢并加重过拟合。

learning_rate和batch_size这对参数要放在一起看。LSTM在长序列上训练时梯度范数波动很大,学习率稍高就会震荡,稍低又收敛太慢;batch_size太小梯度噪声大,太大则每个epoch迭代次数少,模型没看够数据就进入下一轮。dropout是一个容易被忽略的变量,股市数据的信噪比极低,不加dropout的LSTM很容易把噪声当成规律记下来。

2.2 搜索策略选型:网格搜索、随机搜索与GA的边界

很多人第一反应是网格搜索,把每个参数设几个档位,排列组合挨个跑。问题是组合数增长太快。假设只搜四个参数,每个参数给三个档,就有81个组合:

import itertools grid = { "timestep": [5, 10, 20], "hidden": [32, 64, 128], "lr": [1e-4, 1e-3, 1e-2], "batch": [16, 32, 64], } combos = list(itertools.product(*grid.values())) print(f"组合数: {len(combos)}") # 输出: 组合数: 81

81次训练看起来不多,但注意每次LSTM训练都要跑几十个epoch,单次耗时几十秒到几分钟。真实项目里再加大dropout、hidden2、激活函数这些维度,组合数立刻突破几千。网格搜索的致命问题不是慢,而是它完全不利用已经跑过的组合的结果:第50个组合和第3个组合的信息是孤立的。

随机搜索比网格搜索聪明一些,它能在同样的预算下覆盖更广的参数空间,但依然不利用历史评价结果。遗传算法补上的正是这一点:每一代根据种群适应度决定下一轮搜索方向,优胜劣汰,把「跑过的好参数」保留下来继续进化。GA还有两个和LSTM天然匹配的特性,一是它对目标函数是否可导没有要求,LSTM训练本身不可导,但评估结果可以直接当适应度;二是它能同时处理连续参数(学习率)和离散参数(timestep、batch_size),不用像贝叶斯优化那样做复杂的核函数设计。

2.3 GA-LSTM的完整工作流程:谁负责搜索,谁负责训练

GA和LSTM的分工很清楚:GA搜超参数,LSTM做训练和预测,两者通过「评估函数」连接。整个流程是这样的:

  1. 定义每个超参数的取值范围,生成一个「基因」,即一组完整的超参数组合。
  2. 初始化一个种群,比如12个个体,每个个体是一组随机超参数。
  3. 对每个个体构建LSTM,用训练集训练,用验证集算适应度。
  4. 根据适应度排序,保留最优的若干个精英个体。
  5. 用锦标赛选择挑出父代,做交叉和变异,生成新一代种群。
  6. 重复第3到第5步,直到达到预设进化代数。
  7. 取最后一代适应度最高的个体,作为最终超参数,在全量数据上重新训练并测试。

这里有一个成本约束必须提前想清楚:每评估一个个体就要完整训练一次LSTM,种群12个个体、进化10代,意味着最多要训练上百个LSTM。所以种群规模不建议超过20,进化代数控制在10代左右,优先保证每代个体的质量,而不是堆数量。

3. 数据集准备与特征构造:复权、滚动窗口与时序切分

模型输入的数据质量决定了GA能搜到什么水平的参数。股市预测实验里最常见的失败不是算法写错,而是数据集里混入了未来信息,或者切分方式让验证集失真。这一章讲清楚怎么把原始行情数据处理成LSTM的标准监督样本。

3.1 行情数据读取与清洗:复权处理别偷懒

先说数据来源。实际项目里可以直接用开源的财经数据接口拉日线数据,但为了读者方便复现,我用本地CSV作为输入,字段结构统一为date、open、high、low、close、volume。真实项目替换成接口拉取即可,后续处理逻辑完全一致。

import pandas as pd # 读取本地行情CSV,接入数据接口时替换这个读取函数即可 df = pd.read_csv("stock_daily.csv", parse_dates=["date"]) df = df.sort_values("date").drop_duplicates(subset="date") df = df.dropna(subset=["close"]) # 只保留建模需要的列,统一列名 df = df[["date", "open", "high", "low", "close", "volume"]].reset_index(drop=True) print(df.head()) print(df["date"].min(), "->", df["date"].max(), "共", len(df), "条")

这段代码做了三件基础但重要的事:按时间排序、按日期去重、丢弃收盘价为空的行。排序和去重必须做,因为很多数据接口在停牌日会重复或乱序返回。时间索引统一之后,后面所有rolling窗口和shift操作才有意义。

这里有一个不能省的处理是复权。如果直接用不复权的原始价格,遇到除权除息日,股价会凭空跳空,LSTM会把这种人为的价格断裂当成真实的行情波动去学习,预测结果自然失真。常见的做法是使用前复权数据,以最新交易日为基准,把历史价格按分红送股比例折算。数据接口一般提供复权因子参数,拉数据时直接指定前复权即可。

3.2 特征构造:价格、成交量与技术指标的窗口化

LSTM的输入是二维矩阵,形状为(样本数,timestep,特征数)。所以不能直接拿单列收盘价喂进去,而是要把每一行数据扩展成多个特征,再用滑动窗口切成样本。我常用的特征组合是价格、成交量和技术指标三类混搭。

# 技术指标特征 df["ma5"] = df["close"].rolling(5).mean() df["ma10"] = df["close"].rolling(10).mean() df["ret"] = df["close"].pct_change() # 当日收益率 df["vol_ma5"] = df["volume"].rolling(5).mean() # 成交量5日均值 df["rsi"] = compute_rsi(df["close"], 14) # RSI指标,自行实现 # 标签:预测下一交易日收盘价 df["label"] = df["close"].shift(-1) df = df.dropna().reset_index(drop=True) features = ["open", "high", "low", "close", "volume", "ma5", "ma10", "ret", "vol_ma5", "rsi"] label_idx = features.index("close") if "close" in features else 0

特征列的选择有一个铁律:t时刻的样本里,任何一列都不能包含t+1及之后的信息。MA5、MA10、RSI这些指标都是用历史数据算出来的,没问题。但有些技术指标在做「未来N日均线」时会把未来数据带进来,这种特征必须排除。rolling窗口产生的NaN在dropna之后会吃掉最前面的若干行,这是正常的,因为那些位置没有足够的历史数据来计算指标。

3.3 时序切分与归一化:先切分再缩放是铁律

数据切分必须按时间顺序进行,绝对不能随机打乱。股市数据是强时序相关的,随机打乱等于把未来样本混进训练集,模型会在验证集上表现出虚高的精度。我习惯按7:1.5:1.5的比例切训练集、验证集和测试集,测试集留到最后一段,模拟「对未来一段未知行情做预测」。

train_cut = int(len(df) * 0.70) val_cut = int(len(df) * 0.85) train_df = df.iloc[:train_cut] val_df = df.iloc[train_cut:val_cut] test_df = df.iloc[val_cut:] from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler() train_scaled = scaler.fit_transform(train_df[features]) val_scaled = scaler.transform(val_df[features]) test_scaled = scaler.transform(test_df[features])

注意fit_transform和transform的使用位置。MinMaxScaler只能对训练集调用fit_transform,让统计量只来自训练集;验证集和测试集一律transform。如果对全量数据先fit再切分,验证集和测试集的最大值、最小值信息会混进缩放公式,这是一种非常隐蔽的数据泄漏,后面避坑章节还会展开讲。

切分完成后,再用滑动窗口把连续序列切成LSTM的监督样本:

def make_windows(data, label_idx, timestep): X, y = [], [] for i in range(len(data) - timestep): X.append(data[i:i + timestep]) y.append(data[i + timestep, label_idx]) return np.array(X), np.array(y) X_train, y_train = make_windows(train_scaled, label_idx, timestep=10) X_val, y_val = make_windows(val_scaled, label_idx, timestep=10) X_test, y_test = make_windows(test_scaled, label_idx, timestep=10)

窗口生成必须在切分之后做,不能在切分之前对整个数据集生成窗口再切,否则相邻样本会跨阶段重叠,训练集和验证集共享大量时间片段,验证指标失真。这是整个数据准备环节最容易被忽略的坑。

4. GA-LSTM的Python实现:基因编码、进化循环与最小可跑示例

这一章给出可以直接改的代码骨架。GA只负责搜索超参数,LSTM负责具体训练,两者通过适应度函数对接。代码按四个模块拆开讲:基因编码、适应度计算、进化算子、主循环。

4.1 基因编码与种群初始化:混合参数空间的映射方案

基因编码就是把一组超参数表示成一个对象。Python里最自然的表示是dict,键是参数名,值是这个个体的基因取值。遗传算法处理连续和离散混合的参数空间,关键是给每个参数选择合适的采样方式。

import random GENE_RANGE = { "timestep": (5, 30), # 整数,记忆窗口 "hidden1": (16, 128), # 整数,第一层神经元数 "hidden2": (0, 64), # 整数,0表示不用第二层 "lr": (1e-4, 1e-2), # 连续,学习率 "batch_size": (16, 64), # 整数,batch大小 "dropout": (0.0, 0.4), # 连续,dropout比例 } def init_individual(): return { "timestep": random.randint(5, 30), "hidden1": random.randint(16, 128), "hidden2": random.randint(0, 64), "lr": 10 ** random.uniform(-4, -2), # 对数均匀采样 "batch_size": random.choice([16, 32, 64]), "dropout": random.uniform(0.0, 0.4), } # 生成初始种群 population = [init_individual() for _ in range(12)]

学习率的采样方式值得单独说明。学习率在1e-4到1e-2之间,如果用均匀采样,绝大多数随机数会落在数量级较大的区域,导致小学习率几乎采不到。我习惯用对数均匀采样,先在对数空间均匀取数,再取10的幂,这样1e-4、1e-3、1e-2三个数量级被采到的概率相同。batch_size不要用randint连续采样,直接从[16, 32, 64]里选一个更符合实际训练习惯。

4.2 适应度函数:MSE与方向准确率的加权组合

适应度是整个GA的指挥棒。如果只用均方误差MSE,模型会倾向于拟合价格水平,预测曲线贴着真实值走,但涨跌方向可能完全不对。股市预测里真正影响决策的是方向,所以我把适应度定义为验证集MSE减去方向准确率的加权值,数值越小代表个体越优。

from sklearn.metrics import mean_squared_error import numpy as np def direction_accuracy(y_true, y_pred): # 比较相邻两日预测的涨跌方向是否与真实一致 diff_true = np.diff(y_true.flatten()) diff_pred = np.diff(y_pred.flatten()) direction_true = np.sign(diff_true) direction_pred = np.sign(diff_pred) # 真实涨跌幅为0的情况按预测正确处理,避免分母失真 correct = (direction_true == direction_pred) | (direction_true == 0) return np.mean(correct) def evaluate_individual(ind, X_train, y_train, X_val, y_val, epochs=30): model = build_lstm(ind) # 按基因构建LSTM模型,函数见4.4 history = model.fit( X_train, y_train, validation_data=(X_val, y_val), epochs=epochs, batch_size=ind["batch_size"], verbose=0, callbacks=[EarlyStopping(patience=5, restore_best_weights=True)], ) y_pred = model.predict(X_val, verbose=0) mse = mean_squared_error(y_val, y_pred) acc = direction_accuracy(y_val, y_pred) return mse - 0.3 * acc # 加权组合,数值越小越好

MSE和方向准确率的量纲不同,直接相加其实不严谨,这个写法只是为了先跑通流程。习惯上我会再把两者分别做归一化后加权,或者在MSE相近的个体里用方向准确率做二次筛选。真实项目里建议以MSE为主排序,方向准确率作为淘汰条件,比如方向准确率低于52%的个体直接淘汰,这样更稳。

4.3 选择、交叉、变异与精英保留的实现

进化算子我习惯用三个固定套路:锦标赛选择、单点交叉、均匀变异,再加精英保留。这三个算子实现简单,参数语义清楚,是GA-LSTM场景下最不容易出问题的组合。

def tournament_select(population, k=3): # 随机抽k个个体,返回适应度最好的 candidates = random.sample(population, k) return min(candidates, key=lambda ind: ind["fitness"]) def crossover(p1, p2): # 单点交叉:随机选一个基因位,交换之后的所有基因 keys = list(GENE_RANGE.keys()) point = random.randint(1, len(keys) - 1) child1 = {k: p1[k] for k in keys[:point]} | {k: p2[k] for k in keys[point:]} child2 = {k: p2[k] for k in keys[:point]} | {k: p1[k] for k in keys[point:]} return child1, child2 def mutate(ind, rate=0.15): # 每个基因以rate概率重新随机初始化 for key in GENE_RANGE: if random.random() < rate: ind[key] = init_individual()[key] return ind def retain_elites(population, num=2): # 按适应度排序,返回最优的前num个个体 return sorted(population, key=lambda ind: ind["fitness"])[:num]

锦标赛选择的k=3是比较常用的力度,k越大选择压力越大,种群会更快收敛,但也更容易早熟。交叉用单点交叉,对6个基因位的编码来说足够。变异率0.15起步是比较稳的,太高会退化成随机搜索,太低则容易进化崩溃,后面避坑章节细说。

4.4 主循环:种群进化与LSTM训练如何串起来

下面这个主循环可以把前面所有模块串起来。为了控制篇幅,build_lstm函数直接用Keras的Sequential接口实现,按照基因里的超参数构建模型骨架。

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping def build_lstm(ind): model = Sequential() model.add(LSTM(ind["hidden1"], return_sequences=ind["hidden2"] > 0, input_shape=(ind["timestep"], X_train.shape[2]))) if ind["hidden2"] > 0: model.add(LSTM(ind["hidden2"], return_sequences=False)) model.add(Dropout(ind["dropout"])) model.add(Dense(1)) model.compile(optimizer="adam", loss="mse", lr=ind["lr"]) return model POP_SIZE = 12 GENERATIONS = 10 ELITE_NUM = 2 population = [] for i in range(POP_SIZE): ind = init_individual() ind["fitness"] = evaluate_individual(ind, X_train, y_train, X_val, y_val) population.append(ind) for gen in range(GENERATIONS): elites = retain_elites(population, ELITE_NUM) next_pop = [dict(e) for e in elites] while len(next_pop) < POP_SIZE: p1 = tournament_select(population) p2 = tournament_select(population) c1, c2 = crossover(p1, p2) next_pop.append(mutate(c1)) if len(next_pop) < POP_SIZE: next_pop.append(mutate(c2)) for ind in next_pop[ELITE_NUM:]: ind["fitness"] = evaluate_individual(ind, X_train, y_train, X_val, y_val) population = next_pop best = population[0] print(f"第{gen+1}代 best fitness={best['fitness']:.4f} " f"lr={best['lr']:.2e} hidden={best['hidden1']} timestep={best['timestep']}")

每代进化要做的工作量大约等于(POP_SIZE - ELITE_NUM)次完整的LSTM训练,12个个体的种群每代约10次训练,10代就是100次。如果单次训练要跑40个epoch、耗时1分钟,整个GA流程就是100分钟量级。第一次跑通时建议把epochs降到10、GENERATIONS降到3,确认流程没跑错再放大。还要注意evaluate_individual里每一次都是新建模型、重新初始化权重,GA搜的是超参数而不是权重,这点不要搞混。

5. 股市预测模型避坑手册:数据泄漏、进化崩溃与过拟合伪装

GA-LSTM这套流程的坑主要集中在数据切分和进化设置两个方向。以下五条都是真实会发生的问题,按「现象、原因、解决」梳理,每条都值得在执行前先对照检查。

5.1 数据侧三个坑:归一化泄漏、标签错位、样本重叠

坑一:归一化泄漏。现象是验证集loss低得离谱,但测试集回测曲线整体偏移严重。原因是有人在切分之前对全量数据做了fit_transform,MinMaxScaler的min和max把未来信息带进了训练阶段的缩放公式。解决方法是严格按3.3节的代码,只对训练集fit,验证集和测试集只transform。更隐蔽的变体是:用全量数据的均值和标准差做Z-Score标准化,同样属于泄漏,必须在切分之后分别计算。

# 错误示范:先全局fit再切分,验证集信息泄漏进训练集 scaler = MinMaxScaler() scaled_all = scaler.fit_transform(df[features]) train_scaled = scaled_all[:train_cut] # 正确做法:只fit训练集 scaler = MinMaxScaler() train_scaled = scaler.fit_transform(train_df[features]) val_scaled = scaler.transform(val_df[features]) test_scaled = scaler.transform(test_df[features])

坑二:标签错位。现象是训练时loss收敛得很好,但预测结果总是比真实行情慢一天,看起来像「用今天的信号预测了今天的价格」。原因是构造标签时shift方向用的不对,或者refresh了数据后没有重新生成标签。预测下一交易日收盘价,标签必须是close.shift(-1),也就是t+1时刻的真实收盘价;如果写成close本身,模型学的就是「用今天预测今天」的自回归复制。

坑三:样本重叠。现象是验证集上的预测曲线贴得异常平滑,几乎完美拟合,但换到一段全新的行情上预测精度断崖式下跌。原因是滑动窗口切样本时相邻窗口重叠了绝大部分时间步,训练集和验证集在时间上高度重合,验证指标虚高。解决方法是切分时在训练集和验证集之间留出若干天的gap,或者用不重叠的验证块。更彻底的办法是第6章的walk-forward滚动验证。

5.2 进化与训练侧两个坑:进化崩溃与过拟合伪装

坑四:进化崩溃。现象是种群适应度从第4代开始完全不再变化,搜出来的timestep永远等于初始随机值,GA形同虚设。原因是变异率太低加上精英保留过多,最优个体在几代之内复制充满整个种群,多样性归零,交叉算子无新基因可用。解决办法是三个方向同时调:变异率从0.15提到0.25,精英数量从2降到1,每代随机注入1到2个全新随机个体作为「移民」。还有一个容易被忽略的细节:LSTM每次训练权重初始化是随机的,同一个体两次评估的适应度会有抖动,如果不对每个个体固定随机种子,选择压力会被评估噪声干扰,建议在build_lstm时设置tf.random.set_seed和np.random.seed。

坑五:过拟合伪装。现象是训练集loss一路降到很低,验证集loss也不错,但方向准确率始终在52%左右徘徊,回调测就亏钱。原因是LSTM容量过大、技术指标特征过多、dropout太小,模型把训练集的噪声当成规律记住了。解决方法是压缩搜索空间:hidden1上限从128收到64,hidden2直接去掉,dropout下限从0改为0.1,特征列删掉冗余的MA指标只保留价格和成交量。同时把适应度函数改成我们前面说的「MSE排序 + 方向准确率过滤」,方向准确率低于阈值的个体直接淘汰,避免GA朝着纯拟合的方向进化。

# 方向准确率阈值过滤:在evaluate_individual末尾追加 if acc < 0.52: return 1e9 # 直接给一个非常大的惩罚值,让该个体失去竞争力

6. 从实验到滚动验证:walk-forward、集成预测与可视化

GA搜出的最优超参数不能直接宣布胜利,还需要用接近真实交易的方式做验证。这一章的三个技巧,是我认为这套方案从「能跑」到「可信」最关键的收尾动作。

6.1 walk-forward:用「只用过去」的方式做滚动预测

常规的切分验证是静态的,训练集、验证集、测试集各占一段,模型只预测测试集这一段。实盘里模型要不断地用最新数据重新训练、预测下一天,两种场景差异很大。walk-forward的做法是:从某个时间点开始,每次只用该点之前的数据训练,预测之后若干天,然后把时间点向前推进,重复这个过程。这是防止数据泄漏最有效的手段。

def walk_forward(df_scaled, timestep, horizon=5): preds, truths = [], [] for i in range(timestep + horizon, len(df_scaled)): train_data = df_scaled[:i] test_data = df_scaled[i - timestep:i] model = build_lstm(best_individual) model.fit(train_data, ..., epochs=20, verbose=0) pred = model.predict(test_data.reshape(1, timestep, -1), verbose=0) preds.append(pred[0, 0]) truths.append(df_scaled[i, label_idx]) return preds, truths

这段代码刻意写得简洁,重点是「每次重新训练」这个动作。它能暴露那些只在固定测试集上看起来完美的参数:如果某个超参数组合在walk-forward里表现不稳定,说明它过拟合了历史段落,而不是学到了可迁移的规律。计算成本高是正常的,我一般只对GA最后选出的前3个个体做walk-forward,而不是对每个个体都跑。

6.2 用GA种群做集成:前五名个体取中位数

GA运行结束时,种群最后一代的前几名个体适应度相近,但基因不同,代表不同的搜索方向。与其只挑第一名,不如把前五名都拿出来做集成预测。LSTM权重初始化随机,单个模型的预测有方差,取中位数比对极端预测更稳健。

from statistics import median # top5是最后一代适应度最好的5个个体 ensemble_preds = [] for ind in top5: model = build_lstm(ind) model.fit(X_train_all, y_train_all, epochs=30, verbose=0) pred = model.predict(X_test, verbose=0) ensemble_preds.append(pred.flatten()) final_pred = [median(values) for values in zip(*ensemble_preds)] direction_acc = direction_accuracy(y_test, np.array(final_pred).reshape(-1, 1)) print("集成方向准确率:", direction_acc)

这个技巧几乎没有额外成本,GA反正已经把这些个体训练过了,只需要重新用更多数据训练一次就能用于集成。GA种群的多样性在这里变成了一种白捡的稳定性。

6.3 可视化验证:把预测曲线平移一天看是否滞后

最后做一次可视化检查。把真实收盘价和预测收盘价画在同一张图里,然后刻意把预测曲线整体右移一个交易日,观察两边的贴合程度。如果右移后依然贴合得很好,说明模型实际上在预测「昨天的值」,只是把输入里的收盘价延迟复制了一遍,这是LSTM做金融预测最常见的失败形态。真正的预测应该是曲线整体右移后出现明显错位,才是「用过去推未来」的正常表现。

import matplotlib.pyplot as plt plt.figure(figsize=(10, 4)) plt.plot(test_dates, y_test, label="真实收盘价", linewidth=1) plt.plot(test_dates, final_pred, label="预测收盘价", linewidth=1, alpha=0.8) plt.plot(test_dates, np.roll(final_pred, 1), label="预测值右移一天", linewidth=1, linestyle="--") plt.title(f"方向准确率: {direction_acc:.2%}") plt.legend() plt.show()

有次我在某只个股上把方向准确率调到了58%,以为终于找到了稳健的参数组合,结果walk-forward一跑立刻打回原形。原因是静态验证集和训练集的时间窗口重叠,适应度评估被污染了。从那以后我把滚动验证当成模型上线前的最后一关,GA搜出的参数再漂亮,过不了这一关我也不会用它。这个方向最值得做的不是追求预测精度本身,而是把一套带约束的搜索和验证流程跑通,形成不用肉眼调参的实验体系,以后换数据集、换标的都能复用。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 17:09:19

漫画角色跳出分镜,就算剧情战斗成立了吗?实测6个分镜状态节点

很多“一句话把图片或故事生成游戏”的演示&#xff0c;都会出现一个很有吸引力的镜头&#xff1a;漫画书打开&#xff0c;角色从分镜中跳出来&#xff0c;随后与怪物交战。这个画面可以证明题材方向和视觉转场已经形成&#xff0c;却不能直接证明它是一段能够重复游玩的剧情战…

作者头像 李华
网站建设 2026/10/9 17:06:29

pstack-claude 工作栈搭建指南:Claude Code 跨平台安装与报错排查

1. 从"pstack-claude"这个名字说起&#xff1a;它到底想解决什么问题第一次看到pstack-claude这个项目名&#xff0c;很多人会愣一下——pstack 是什么&#xff1f;和 Claude 又是什么关系&#xff1f;我最初的反应也是这样。拆开来看&#xff0c;pstack通常指代&quo…

作者头像 李华
网站建设 2026/10/9 17:06:22

量化投资alpha策略实证研究:从理论到实践

1. 从"alpha"这个词说起&#xff1a;它到底指什么很多人第一次接触量化投资&#xff0c;看到"alpha策略"这四个字&#xff0c;第一反应是懵的。alpha在Python里是某个库的名字&#xff0c;在数学里是希腊字母&#xff0c;在投资圈里又是另一回事。我刚开始…

作者头像 李华
网站建设 2026/10/9 17:05:09

Scanopy 服务定义完全手册:轻松添加200+服务类型

Scanopy 服务定义完全手册&#xff1a;轻松添加200服务类型 【免费下载链接】scanopy Network diagrams that update themselves 项目地址: https://gitcode.com/gh_mirrors/ne/scanopy Scanopy 是一款"能自己更新的"开源网络拓扑工具&#xff0c;而让它真正聪…

作者头像 李华
网站建设 2026/10/9 17:04:39

热成像人物检测数据集:面向边缘部署的夜间鲁棒目标检测基准

简介&#xff1a;本资源是面向计算机视觉开发者与AI研究者的热成像人物检测专用数据集&#xff0c;聚焦低光、夜间及复杂环境下的目标检测与实例分割任务&#xff0c;特别适配YOLO系列模型训练与红外安防系统研发。数据集共606张热成像与对比视觉图像&#xff0c;配套606个YOLO…

作者头像 李华