
简介这是一份基于Python实现的GA-LSTM遗传算法优化长短时记忆网络时间序列预测模型代码包面向机器学习开发者、科研人员及对预测算法感兴趣的进阶学习者。资源虽标注为matlab但实际实现完全采用Python语言压缩包共5个文件包含2个py脚本、2个pyc编译文件和1个txt说明文档整体大小仅9KB结构简洁。代码围绕遗传算法与LSTM结合展开利用GA全局搜索特性对LSTM权重进行迭代优化帮助缓解传统模型过拟合或欠拟合问题同时保留LSTM处理长期依赖的能力。具体内容包括网络结构定义、适应度函数构造、选择交叉变异等遗传操作以及数据预处理、模型训练、预测和误差评估等完整流程。通过阅读和运行源码读者可以直观理解GA如何辅助神经网络调参并可将该方案迁移至股票预测、天气预测等场景。目前已有390人学习下载适合需要提升时序预测精度、研究混合优化策略的开发者。1. 拿到 GA-LSTM 的 zip 之后别急着解压先搞清楚这套代码到底在帮你做哪件事打开一个 GA-LSTM 预测代码的 zip最容易劝退的不是 LSTM 本身而是「遗传算法优化」这六个字。很多任务里遗传算法其实不负责预测它只做一件事替你把 hidden_size、学习率、窗口长度这些超参当基因去搜索而不是你自己盯着 loss 曲线一版一版试。GA-LSTM 的落地路径就是「GA 找参数LSTM 做预测」适合数据量不大、单序列居多、又不想在调参上耗整天的 Python 工程场景。我按自己实际落地的顺序来拆数据怎么切、GA 怎么编码、LSTM 怎么接、坑在哪。这份思路可以直接还原成一个能跑的最小实现。2. 先把样本切对窗口构造、时间划分和归一化的正确顺序2.1 窗口怎么切可变 seq_len 的造样本方法先说一个最常见的翻车点构造样本时把 y 取成当前时刻模型训练时等于看见答案。预测任务里 X 是过去 seq_len 个时刻y 必须是未来一个时刻。下面这个函数接收归一化后的数组切成 LSTM 能吃的三维输入。import numpy as np def make_samples(x_scaled, y_scaled, seq_len, step1): X, y [], [] for i in range(seq_len, len(y_scaled) - 1, step): # 关键输入窗口是 i 之前的 seq_len 行标签取 i1而不是 i X.append(x_scaled[i - seq_len:i]) y.append(y_scaled[i 1]) return np.array(X), np.array(y)逻辑说明x_scaled 是形状 (n, n_feat) 的二维数组经过归一化y_scaled 是 (n,) 的一维目标序列。i 从 seq_len 开始到 len(y_scaled)-1 结束保证标签 i1 存在。如果写成 y.append(y_scaled[i])输入窗口最后一个时刻和标签是同一天训练时 loss 正常下降滚动预测第一天就露馅。参数说明seq_len 是 GA 要搜的核心超参之一常见取值范围 6~36。日更数据里 7 和 30 是两个常用档位小时级数据可以短一些。step1 意味着窗口每次滑动 1 步样本之间重叠能提供更多训练样本不建议为了减少样本量把 step 调大连续预测的评估会被截断。x_scaled 支持多列特征比如温度、是否为工作日、目标序列的滞后项如果没有外生变量就把目标列复制进 feature_cols 一起传入。2.2 训练、验证、测试怎么切时间顺序不是随机切时间序列预测的划分有个特殊要求不能用 sklearn 的 train_test_split 随机切。随机切会把未来的数据点混进训练集模型在验证阶段的表现会虚高。我一般会按时间顺序切成三段训练段只用来拟合归一化和训练模型验证段给 GA 计算适应度测试段到最后评估时才碰一次。def split_by_time(df, train_ratio0.7, val_ratio0.15): n len(df) train_end int(n * train_ratio) val_end int(n * (train_ratio val_ratio)) train_df df.iloc[:train_end].copy() val_df df.iloc[train_end:val_end].copy() test_df df.iloc[val_end:].copy() return train_df, val_df, test_df参数说明样本足够长就 70/15/15样本只有几百条时可以改成 80/10/10但 val 至少要留几十个样本否则适应度噪声太大GA 会选到运气好的超参而不是真正好的超参。如果序列有季节周期切分点尽量选在周期边界附近比如日数据按整周或整月切避免训练段和验证段的统计特性完全错位。2.3 归一化怎么放训练段 fit验证测试段 transform归一化也是这类项目里最容易埋隐性 bug 的地方。常见错误是拿整个数据集一次性 fit MinMaxScaler让测试段的 min/max 提前进入模型导致「训练时看不出问题上线后预测值系统性偏小」。from sklearn.preprocessing import MinMaxScaler scaler_x MinMaxScaler() scaler_y MinMaxScaler() # 只用训练段拟合验证和测试段只做变换 scaler_x.fit(train_df[feature_cols]) scaler_y.fit(train_df[[target_col]]) def transform_df(df): x_scaled scaler_x.transform(df[feature_cols]) y_scaled scaler_y.transform(df[[target_col]]).ravel() return x_scaled.astype(np.float32), y_scaled.astype(np.float32)逻辑说明scaler 的 min/max 只来自训练段验证和测试段用同一套变换。这样测试段里的新极值会被裁剪到 0~1 区间这是 MinMaxScaler 的天然局限。如果你的数据有明显趋势漂移最好换成 StandardScaler或者观察 transform 后是否大量样本堆积在 0 和 1 边界上堆积说明分布已经超出训练段范围。参数说明target 也要单独归一化因为 LSTM 输出层是线性层直接拟合归一化后的目标更容易收敛最终预测结果用 scaler_y.inverse_transform 还原。GA 的适应度我习惯用归一化后的 RMSE报告时再还原成原始量纲的 RMSE两个口径分开记录。提示后面所有模型代码里的 x_train、y_train默认都来自 transform_df 的输出不要再自己重复做一次 fit。3. 遗传算法优化 LSTM 的实现基因编码、适应度与主循环3.1 超参编码与种群初始化从 0~1 的基因到真实超参GA 的第一步是把候选超参编码成一条「基因」。我习惯用实数编码所有基因值都在 [0,1] 之间这样交叉和变异都不需要处理不同量纲。解码阶段把它映射到真实参数。BOUNDS { hidden: (16, 128), lr: (1e-4, 1e-2), seq_len: (6, 36), dropout: (0.0, 0.4), } def decode(genes): hidden int(round(BOUNDS[hidden][0] genes[0] * (BOUNDS[hidden][1] - BOUNDS[hidden][0]))) lr BOUNDS[lr][0] * (BOUNDS[lr][1] / BOUNDS[lr][0]) ** genes[1] seq_len int(round(BOUNDS[seq_len][0] genes[2] * (BOUNDS[seq_len][1] - BOUNDS[seq_len][0]))) dropout BOUNDS[dropout][0] genes[3] * (BOUNDS[dropout][1] - BOUNDS[dropout][0]) return hidden, lr, seq_len, dropout def init_population(pop_size12, seed42): rng np.random.default_rng(seed) return rng.random((pop_size, 4))逻辑说明基因有 4 个维度每个个体就是长度为 4 的数组。hidden 和 seq_len 解码后要转成整数lr 用 log 均匀而不是线性均匀因为学习率在 0.0001~0.01 之间线性采样会把绝大多数个体落在 0.005~0.01 这一段初始学习率普遍偏大训练容易震荡。log 均匀可以让 0.0001、0.001、0.01 三个数量级都有差不多的采样机会。参数说明hidden 下限 16 是经验值低于 16 的 LSTM 表达力太弱上限 128 对单序列预测足够。seq_len 上限 36 意味着如果序列本身较短构造后的样本数会明显减少所以 GA 里要在 fitness 入口做一次合法性校验。dropout 只在 LSTM 层开启搜索时给 0~0.4最终模型大多落在 0.1~0.3。3.2 适应度函数训练一个小 LSTM返回验证集 RMSEGA 要评估每个个体的好坏靠的是 fitness 函数。适应度值通常取验证集 RMSE数值越小越好。这里的关键不是把 LSTM 训练到完全收敛而是训练到能区分超参好坏的程度。import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset class LSTMPredictor(nn.Module): def __init__(self, n_feat, hidden, dropout): super().__init__() self.lstm nn.LSTM(n_feat, hidden, batch_firstTrue, dropoutdropout) self.fc nn.Linear(hidden, 1) def forward(self, x): out, _ self.lstm(x) return self.fc(out[:, -1, :]).squeeze(-1) def fitness(genes, train_df, val_df, feature_cols, target_col): hidden, lr, seq_len, dropout decode(genes) if seq_len len(train_df) - 2: return 1e9 x_train, y_train transform_df(train_df) x_val, y_val transform_df(val_df) Xtr, ytr make_samples(x_train, y_train, seq_len) # 验证段切掉前 seq_len 行避免窗口引用训练段尾巴 Xva, yva make_samples(x_val[seq_len:], y_val[seq_len:], seq_len) model LSTMPredictor(len(feature_cols), hidden, dropout) opt torch.optim.Adam(model.parameters(), lrlr) loss_fn nn.MSELoss() for epoch in range(15): model.train() for xb, yb in DataLoader(TensorDataset(torch.tensor(Xtr), torch.tensor(ytr)), batch_size32, shuffleTrue): opt.zero_grad() loss_fn(model(xb), yb).backward() opt.step() model.eval() with torch.no_grad(): pred model(torch.tensor(Xva)) yva_t torch.tensor(yva) rmse float(torch.sqrt(torch.mean((pred - yva_t) ** 2))) return rmse逻辑说明fitness 里做四件事解码超参、按该 seq_len 重建样本、训练 15 轮、返回验证 RMSE。Xva 的构造刻意用了 x_val[seq_len:] 和 y_val[seq_len:]避免验证集最前面的样本引用训练段末尾的真实值这种重叠会让适应度虚高GA 选出「假最优」。GA 阶段训练轮数只给 15是刻意为之种群 12、代数 15意味着最多要训练 180 个小型 LSTM每个 15 轮已经是不小的算力消耗。参数说明batch_size 32输入是 (样本数, seq_len, 特征数)。shuffleTrue 在训练时可以打乱时序因为每个样本自身已经包含窗口上下文。如果把它换到自己的数据上优先改三个地方seq_len 合法性判断、训练轮数、验证集重叠处理。数据噪声大时可以把 15 轮提高到 25 轮代价是单次评估更慢。3.3 GA 主循环锦标赛选择、单点交叉和精英保留有了种群和 fitness主循环就比较固定了。我常用锦标赛选择加精英保留代码量少且不容易早熟。def tournament_select(pop, fitness_scores, k3, rngNone): idx rng.integers(0, len(pop), k) return pop[idx[np.argmin([fitness_scores[i] for i in idx])]] def crossover(a, b, prob0.8, rngNone): if rng.random() prob: return a.copy(), b.copy() cut rng.integers(1, len(a)) return (np.concatenate([a[:cut], b[cut:]]), np.concatenate([b[:cut], a[cut:]])) def mutate(gene, prob0.1, scale0.1, rngNone): if rng.random() prob: gene gene rng.normal(0, scale, sizegene.shape) return np.clip(gene, 0, 1) rng np.random.default_rng(42) pop init_population(12, seed42) for gen in range(15): scores np.array([fitness(g, train_df, val_df, feature_cols, target_col) for g in pop]) elite_idx np.argsort(scores)[:2] elites pop[elite_idx].copy() new_pop [] while len(new_pop) len(pop) - 2: p1 tournament_select(pop, scores, k3, rngrng) p2 tournament_select(pop, scores, k3, rngrng) c1, c2 crossover(p1, p2, prob0.8, rngrng) new_pop.append(mutate(c1, prob0.1, rngrng)) new_pop.append(mutate(c2, prob0.1, rngrng)) pop np.vstack([elites, np.array(new_pop[:len(pop) - 2])])逻辑说明每一代先算全部个体的 fitness把前 2 名直接复制到下一代保证最优解不被交叉和变异破坏。剩下 10 个个体由锦标赛产生每次从种群随机抽 3 个选 fitness 最好的作为父本。交叉用单点交叉切割位置在基因的第 1 到第 3 维之间变异是加一个均值为 0 的高斯扰动最后 clip 回 [0,1]。迭代 15 代后pop 里适应度最好的是 GA 找到的最优超参。GA 参数推荐值为什么这么设种群大小12~20每次评估都要训练模型太大拖慢整体太小容易早熟迭代代数15~25多数单序列预测 15 代左右收敛超过 25 代收益很低交叉率0.7~0.8让优质基因片段充分组合变异率0.05~0.1低于 0.05 早熟高于 0.1 退化成随机搜索精英数量1~2保留最优超过 2 会让种群多样性下降参数说明上面代码写死了 12、15、0.8、0.1实际建议先小规模跑一版看每代收敛曲线。如果第 10 代最优值还在明显下降把代数加到 20如果最优很早就稳住了但平均 fitness 和最优差很远说明多样性不够把变异率调到 0.15 或把种群加到 20。4. 拿到最优超参之后重训、多步预测和评估口径4.1 用最优超参重训一次真正的最终模型GA 阶段每个个体只训练 15 轮属于「够用来比较、不够用来交付」。拿到最优基因之后需要按同一组超参把训练轮数拉满配合验证集保存最优权重。best_genes pop[np.argmin(scores)] hidden, lr, seq_len, dropout decode(best_genes) x_train, y_train transform_df(train_df) x_val, y_val transform_df(val_df) X_train, y_train_win make_samples(x_train, y_train, seq_len) X_val, y_val_win make_samples(x_val[seq_len:], y_val[seq_len:], seq_len) model LSTMPredictor(len(feature_cols), hidden, dropout) opt torch.optim.Adam(model.parameters(), lrlr) loss_fn nn.MSELoss() best_val_rmse float(inf) for epoch in range(80): model.train() for xb, yb in DataLoader(TensorDataset(torch.tensor(X_train), torch.tensor(y_train_win)), batch_size32, shuffleTrue): opt.zero_grad() loss_fn(model(xb), yb).backward() opt.step() model.eval() with torch.no_grad(): val_rmse float(torch.sqrt(torch.mean((model(torch.tensor(X_val)) - torch.tensor(y_val_win)) ** 2))) if val_rmse best_val_rmse: best_val_rmse val_rmse torch.save(model.state_dict(), ga_lstm_best.pt)逻辑说明这轮训练最多 80 轮实际执行时 10~20 轮就能到稳定平台。重点在于「只在验证集 RMSE 变好时保存权重」而不是训练完最后一轮直接收尾这样保留了训练过程中的最优状态。参数说明seq_len 在这一步才真正参与最终数据集构造。保存权重时建议把 BOUNDS、seq_len、feature_cols、scaler_y 一起打包否则预测脚本里还要手工对齐归一化参数。另一个别漏的细节X_val 同样用 val 的 seq_len 之后构造和 GA 阶段口径一致这样比较才有意义。4.2 单步预测与滚动多步预测两者要分开评估很多预测任务的真实需求不是下一天而是未来 7 天或 30 天。LSTM 天然输出单步想得到多步结果最常见的是滚动预测把预测值拼回输入序列再取下一个窗口重复执行。def roll_predict(model, recent_seq, scaler_y, horizon): model.eval() cur recent_seq.copy() preds [] with torch.no_grad(): for _ in range(horizon): y_norm model(torch.tensor(cur[None, ...])).item() y_real float(scaler_y.inverse_transform([[y_norm]])[0, 0]) preds.append(y_real) nxt np.roll(cur, shift-1, axis0) nxt[-1, -1] y_norm # 预测值补到目标特征列 cur nxt return preds逻辑说明cur 是长度为 seq_len、宽度为特征数的归一化窗口。预测出 y_norm 后用 np.roll 把整行前移一步再把新预测值放在最后一行的目标特征列窗口继续滚。这种方法的缺点是误差累积第 1 天偏了第 2 天会把偏掉的预测值当输入继续滚。另一个方向是让模型直接输出多步把最后的 Linear 层改成 horizon 个节点训练标签变成未来 horizon 步序列直接多输出不会「越滚越平」但样本和 loss 都要重构改动量比滚动大。参数说明这段代码假设目标列是特征矩阵的最后一列如果特征列还有其他物理量一定要把每一列分别滚动更新否则窗口信息会错位。horizon 按业务需求设置金融序列常用 5 或 7负荷预测常用 30别设成训练样本长度的三分之一以上否则验证窗口过小。4.3 评估指标怎么给RMSE、MAE、MAPE 各算各的账预测代码里最容易吵起来的地方是指标。RMSE 对大误差敏感适合温度、负荷这类不希望出现单日大偏差的场景MAE 更稳健适合有少量极端点的数据MAPE 业务上直观但遇到真实值接近 0 的时刻会爆炸所以要剔除近零样本后单独算。def evaluate_metrics(y_true, y_pred): y_true np.asarray(y_true, dtypenp.float64) y_pred np.asarray(y_pred, dtypenp.float64) rmse float(np.sqrt(np.mean((y_true - y_pred) ** 2))) mae float(np.mean(np.abs(y_true - y_pred))) mask np.abs(y_true) 1e-6 mape float(np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100) return {rmse: rmse, mae: mae, mape: mape}逻辑说明这段代码直接作用于还原后的真实量纲值三个指标一次算齐。RMSE 和 MAE 不需要额外条件MAPE 先过滤掉绝对值小于 1e-6 的样本避免除零。最终报告里我习惯把单步评估和滚动多步评估分开写单步 RMSE 反映模型对序列本身的学习能力滚动多步 RMSE 反映业务上真实的 7 天预测表现。两者差别很大时优先怀疑滚动窗口里的误差累积。参数说明mask 阈值 1e-6 不是固定标准目标值普遍在万级以上可以改成 1目标值本身很小就保持更小阈值。评估测试集时测试段长度至少要是 horizon 的 5 倍否则换一个起始点结果会完全不同。5. 避坑清单GA-LSTM 最容易翻车的 5 个地方5.1 数据泄漏归一化先 fit 全量、随机切分时间序列现象训练和验证的 RMSE 都很理想一上真实预测就明显偏小或进入平台期怎么看都不像同一套参数。原因最常见的是把整个数据集一次性交给 MinMaxScaler.fit测试段的 min/max 提前参与归一化或者用 sklearn 的 train_test_split 随机切分把未来数据混进训练集。对滑动窗口样本还有一种更隐蔽的重叠验证集最前面 seq_len 个样本引用训练段末尾的真实值。解决scaler 只 fit 训练段切分按时间顺序验证样本从 val 的 seq_len 之后开始构造。上线前再检查 transform 后的验证值是否大量堆积在 0 和 1 边界如果是说明测试段分布超出训练段范围GA 选出的窗口和缩放参数都要重新考虑。5.2 适应度噪声为什么同样的超参跑几次结果不一样现象GA 一代比一代好但把最优超参手动重跑一遍验证 RMSE 比 GA 里记录的高出不少有时翻一倍。原因fitness 函数没有固定随机种子LSTM 初始化、DataLoader shuffle 顺序每次都不一致只有 15 轮训练时随机噪声在适应度里的占比很高GA 很可能在替「运气好的一次训练」选超参。解决跑 GA 前把 torch.manual_seed、np.random.seed 全部固定个别个体想多看几个随机结果就在 fitness 里对同一个基因训练两次取平均。验证集样本少于 50 个时适应度本身就不可信先扩大验证集比例或用时间序列交叉验证的均值做适应度。5.3 计算开销失控种群太大、每代全量重训、收敛后还在跑现象种群 30、代数 30一跑就是几个小时最后选出的结果和随机搜索差不多白白烧掉机器。原因每个个体都从零开始训练 LSTM计算量翻倍很快GA 本身并不保证比网格搜索和随机搜索更好只是相同算力下通常更高效。种群和代数设得太大优势会被巨大的测试时间吞掉。解决先按 12 个种群、15 代跑一版fitness 里训练轮数从 15 开始不要一上来就 50。每代记录 best 和 average连续 5 代没有明显变化就提前结束。想进一步提速可以缓存同代里出现过的高分基因同一组超参不要重复训练。5.4 多步预测验证口径对不上报告单步业务要 7 天现象验证集上第 1 步 RMSE 很好画未来 7 天曲线却从第 3 天开始明显偏平和单步表现完全不匹配。原因训练和 GA 阶段都只计算单步 loss验证指标也是单步业务方要的却是滚动 7 天预测。单步好不代表多步好滚动预测的误差累积在评估里被漏掉了。解决最终评估必须用和业务一致的 horizon用同样的滚动预测流程在测试段上跑一遍而不是用模型在验证集上的单步输出。如果改用直接多步输出验证指标也要改成多步目标的平均 loss两边口径对齐再谈调参。5.5 超参搜索空间本身就是错的方向现象GA 跑完最优超参比固定默认超参好一点点换一个数据集甚至更差。原因搜索空间设得太窄或太宽。比如 hidden 范围 4~16模型表达力不够GA 再努力也找不到好结果或者 seq_len 上限 100但训练数据只有 300 条大部分个体构造出的样本数太少适应度噪声完全盖过超参差异。解决先用一组固定超参把纯 LSTM 跑通确认数据和预处理没问题再根据这个基准把 BOUNDS 设得宽但不极端。最优解如果总落在某个边界附近比如 hidden 总是在 16 或 128说明边界选得不对该放宽或收窄。6. 验证这套 GA-LSTM 有没有白调基线和多次重训如果要给这套方案做收尾我的验证习惯是这样先定一组不加优化的 LSTM 作为基线hidden32、lr0.001、seq_len12、dropout0.2再用随机搜索跑同样的预算最后才是 GA-LSTM。每个方法在测试集上都用 5 个不同的随机种子重训报告验证集 RMSE 的中位数和范围而不是只报一次结果。LSTM 单次训练有随机性只跑一次你分不清提升来自算法还是来自运气。假设你得到的结果可能是方法验证集 RMSE 中位数说明固定超参 LSTM12.6基线超参靠经验随机搜索11.9同样预算随机试了 30 组GA-LSTM11.2同样预算按进化方向搜索如果 GA-LSTM 只比随机搜索好一点点我不会太兴奋说明收益可能来自超参搜索本身而不是 GA 的收敛能力。这时应该先扩大搜索空间而不是继续加种群或代数。真正该怀疑 GA 没起作用的情况是GA 结果比随机搜索差或者最优解总落在边界上。前者大多是 fitness 不稳定后者大多是 BOUNDS 设置不合理。还有一个细节容易被忽略GA 是在验证集上选超参它也会过拟合验证集。保险的做法是留出独立测试集GA 全程只在 train/val 上做测试集最后碰一次。我自己的习惯是最终重训时换不同随机种子跑 5 次把中位模型保存下来同时记下最优和最差的差距。这个差距如果超过验证集 RMSE 的 20%说明单次训练噪声太大优先解决稳定性而不是继续调 GA。这也是一直强调「先用纯 LSTM 跑通数据再把 GA 接上去」的原因——当年我第一次直接套 GA-LSTM跑了一整晚后来才发现是归一化泄漏白折腾一晚。希望这套流程能帮你少踩这个坑。本文还有配套的精品资源点击获取