简介这份资源是遗传算法优化LSTM时间序列预测的Python实现代码面向具备一定深度学习基础、希望提升模型预测精度的研究者与开发者。它针对LSTM参数调优依赖经验、易陷入局部最优的问题用遗传算法对权重与偏置进行全局搜索适用于时序预测、序列建模等场景。压缩包共4个文件以2个py源码和2个pyc字节码为主源码分别承担遗传算法优化与LSTM模型构建、数据加载职责整体仅8KB轻量易读。目前已有6965人学习下载热度较高。读者可从中获得完整的GA-LSTM实现思路包括种群编码、交叉变异、选择策略与模型训练评估流程并可通过修改数据加载部分适配自有数据理解遗传算法如何迭代更新参数、对比不同代模型性能为复杂模型的参数配置优化提供可复用的代码参考。1. GA-LSTM 到底在优化什么从一组翻车的预测曲线说起拿 LSTM 做时序预测的人多半遇到过这种场面网络结构调了半天层数、隐藏单元、学习率、batch size 全试了一遍验证集 loss 就是卡在某个值下不去换一组随机种子结果又飘一大截。问题往往不在 LSTM 本身而在那些没人告诉你该怎么定的超参数上——窗口长度取 24 还是 48隐藏层堆一层还是两层学习率是 1e-3 还是 5e-4这些组合空间是连续的、耦合的网格搜索跑到天亮也搜不完。GA-LSTM 要解决的就是这件事用遗传算法Genetic AlgorithmGA去自动搜 LSTM 的超参数组合把「调参靠玄学」变成「调参靠进化」。它适合手里有明确时序预测任务、已经能跑通单模型 LSTM、但被超参数卡住的从业者也适合想把优化算法和深度学习串起来做一版完整 pipeline 的人。下面从原理、代码、参数、踩坑一路讲透代码可以直接抄去改。2. 遗传算法和 LSTM 怎么接编码方式决定成败2.1 为什么是 GA 而不是网格搜索或贝叶斯优化先把选型理由说清楚不然代码写了也不知道自己在干什么。超参数搜索常见三条路网格搜索、随机搜索、贝叶斯优化。网格搜索的复杂度是各维度取值数的乘积LSTM 有 4 到 5 个关键超参每个取 5 个值就是 3000 多次训练单次训练几分钟一天都跑不完。随机搜索比网格好但它是无记忆的不会利用已经试过的点。贝叶斯优化比如 Optuna、Hyperopt在低维连续空间里确实强但它依赖代理模型维度一高、参数里混着整数和类别变量时代理模型的拟合会变差而且它容易过早收敛到局部最优。GA 的优势在于它天然处理混合编码整数、类别、连续可以混在一条染色体里种群并行搜索不容易陷进局部最优而且实现门槛低不依赖额外库。代价是它需要评估的个体多、计算量大。所以结论是当你的单次训练成本可控几分钟以内、超参数维度在 3 到 6 之间、且参数类型混杂时GA 是性价比很高的选择。如果你的单次训练要半小时以上先考虑降采样或者用贝叶斯优化别硬上 GA。2.2 染色体编码把超参数打包成一条基因GA 的核心是把「一组超参数」编码成一条染色体每个基因对应一个超参数。LSTM 预测任务里我一般会优化这 5 个基因位超参数类型取值范围gene[0]时间窗口长度 look_back整数6 ~ 72gene[1]第一层隐藏单元数整数16 ~ 128gene[2]第二层隐藏单元数整数0 ~ 1280 表示单层gene[3]学习率连续对数刻度1e-4 ~ 1e-2gene[4]dropout 比率连续0.0 ~ 0.5编码时有个关键细节学习率必须用对数刻度采样因为 1e-4 和 1e-3 的差异远比 1e-3 和 2e-3 重要。如果直接在线性空间里交叉变异学习率会一直往大值漂训练直接发散。这是血泪经验第一次写 GA-LSTM 时我就在这翻过车。import numpy as np # 超参数边界每行是 (最小值, 最大值, 类型) # 类型 0整数, 1连续, 2对数连续 BOUNDS np.array([ [6, 72, 0], # look_back [16, 128, 0], # units_1 [0, 128, 0], # units_2, 0 表示单层 [1e-4, 1e-2, 2], # learning_rate, 对数刻度 [0.0, 0.5, 1], # dropout ]) def random_gene(): 随机生成一条染色体 gene [] for low, high, kind in BOUNDS: if kind 0: gene.append(np.random.randint(int(low), int(high) 1)) elif kind 1: gene.append(np.random.uniform(low, high)) else: # 对数连续 gene.append(np.exp(np.random.uniform(np.log(low), np.log(high)))) return np.array(gene, dtypefloat) def decode(gene): 把染色体解码成可用的超参数字典 return { look_back: int(round(gene[0])), units_1: int(round(gene[1])), units_2: int(round(gene[2])), learning_rate: float(gene[3]), dropout: float(np.clip(gene[4], 0.0, 0.5)), }这段代码里BOUNDS用第三列区分参数类型random_gene按类型分别采样decode负责把浮点基因还原成整数超参数。注意units_2取 0 时表示不建第二层这个判断要放到建模函数里别在解码阶段就砍掉否则交叉变异会丢失信息。np.clip是兜底防止交叉变异后 dropout 越界。2.3 适应度函数用验证集 loss 当进化压力适应度函数决定 GA 往哪个方向进化。做预测任务最直接的就是用验证集的 MSE 或 MAE取负值当适应度GA 默认求最大。这里有个必须注意的点适应度评估要固定随机种子、固定数据划分否则同一个染色体两次评估结果不一样GA 会以为自己在进化其实是在追噪声。import tensorflow as tf from tensorflow.keras import layers, models, callbacks def build_and_eval(gene, X_train, y_train, X_val, y_val, epochs30): 根据染色体训练 LSTM 并返回验证集 loss越小越好 cfg decode(gene) tf.random.set_seed(42) # 固定种子保证评估可复现 np.random.seed(42) model models.Sequential() model.add(layers.Input(shape(cfg[look_back], X_train.shape[2]))) model.add(layers.LSTM(cfg[units_1], return_sequences(cfg[units_2] 0))) if cfg[units_2] 0: model.add(layers.LSTM(cfg[units_2])) model.add(layers.Dropout(cfg[dropout])) model.add(layers.Dense(1)) model.compile( optimizertf.keras.optimizers.Adam(cfg[learning_rate]), lossmse ) early callbacks.EarlyStopping(monitorval_loss, patience5, restore_best_weightsTrue) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochsepochs, batch_size32, callbacks[early], verbose0 ) return min(history.history[val_loss]) def fitness(gene, *data): 适应度 负的验证集 lossGA 求最大 try: val_loss build_and_eval(gene, *data) return -val_loss except Exception: return -1e9 # 训练失败给极差适应度直接淘汰build_and_eval里return_sequences根据是否有第二层动态设置这是 LSTM 堆叠的硬性要求——第一层要输出序列才能接第二层。EarlyStopping的 patience 设 5是为了在 GA 里控制单次评估时间别让某个烂染色体训满 30 轮。fitness用 try-except 包住因为某些极端超参数组合比如学习率过大会导致 loss 变 NaN这种个体必须给极差适应度淘汰掉不能让异常中断整个进化流程。3. 把 GA 主循环写出来选择、交叉、变异三件套3.1 种群初始化与精英保留种群大小一般取 10 到 20。太小比如 5多样性不够几代就收敛到局部最优太大比如 50计算量翻几倍收益递减。我一般用 12 到 16。精英保留是必须的每一代把适应度最高的 1 到 2 个个体原样复制到下一代防止最优解在交叉变异中被破坏。没有精英保留的 GA经常出现「上一代最好的个体这一代找不到了」的尴尬。def init_population(size): return [random_gene() for _ in range(size)] def select(pop, fits, k3): 锦标赛选择随机抽 k 个取适应度最高的 idx np.random.choice(len(pop), k, replaceFalse) best idx[np.argmax([fits[i] for i in idx])] return pop[best].copy()锦标赛选择的k叫选择压力k 越大越偏向选强者收敛快但容易早熟k3 是比较稳的默认值。replaceFalse保证抽的是不同个体避免自己跟自己比。3.2 交叉与变异模拟二进制交叉加多项式变异连续和整数混合编码用模拟二进制交叉SBX加多项式变异比较稳。SBX 能让子代在父代附近生成同时保持探索能力多项式变异负责跳出局部。整数基因在交叉变异后要四舍五入并 clip 回边界。def crossover(p1, p2, eta15): 模拟二进制交叉 SBX child1, child2 p1.copy(), p2.copy() for i in range(len(p1)): if np.random.rand() 0.5: if abs(p1[i] - p2[i]) 1e-12: u np.random.rand() beta (2*u)**(1/(eta1)) if u 0.5 else (1/(2*(1-u)))**(1/(eta1)) child1[i] 0.5*((1beta)*p1[i] (1-beta)*p2[i]) child2[i] 0.5*((1-beta)*p1[i] (1beta)*p2[i]) return child1, child2 def mutate(gene, eta20): 多项式变异 for i, (low, high, kind) in enumerate(BOUNDS): if np.random.rand() 1/len(BOUNDS): u np.random.rand() delta (2*u)**(1/(eta1)) - 1 if u 0.5 else 1 - (2*(1-u))**(1/(eta1)) gene[i] gene[i] delta*(high - low) # 边界修复 for i, (low, high, kind) in enumerate(BOUNDS): gene[i] np.clip(gene[i], low, high) return geneeta是分布指数越大子代越靠近父代。交叉概率 0.5 是逐基因判断的变异概率用1/len(BOUNDS)保证平均每条染色体变一个基因。边界修复那一步不能省SBX 和多项式变异都可能把基因推出范围尤其是学习率这种对数刻度的越界后取 exp 会炸。3.3 主循环把上面拼起来def run_ga(data, pop_size14, generations20, elite2): pop init_population(pop_size) best_gene, best_fit None, -np.inf history [] for gen in range(generations): fits [fitness(g, *data) for g in pop] order np.argsort(fits)[::-1] pop [pop[i] for i in order] fits [fits[i] for i in order] if fits[0] best_fit: best_fit, best_gene fits[0], pop[0].copy() history.append(best_fit) print(fGen {gen}: best val_loss {-best_fit:.6f}) new_pop [pop[i].copy() for i in range(elite)] # 精英保留 while len(new_pop) pop_size: p1 select(pop, fits) p2 select(pop, fits) c1, c2 crossover(p1, p2) new_pop.append(mutate(c1)) if len(new_pop) pop_size: new_pop.append(mutate(c2)) pop new_pop return best_gene, best_fit, history主循环里每一代先评估、再排序、再记录最优、最后生成新种群。elite2表示前两名直接进下一代。history记录每代最优适应度用来判断是否收敛——如果连续 5 代最优值不动基本可以停了。整个流程跑完best_gene就是搜到的最优超参数组合decode一下就能拿去训练最终模型。4. 避坑与排查GA-LSTM 最容易翻车的 5 个地方4.1 现象适应度曲线一路涨但最终模型效果很差原因过拟合验证集。GA 在验证集上反复搜索验证集实际上变成了「训练集的一部分」搜出来的超参数只对这份验证集好。解决留一份独立的测试集全程不参与 GA 评估只在最后用最优超参数训练完模型后测一次。如果测试集和验证集差距很大说明搜索过拟合了把种群和代数降下来或者用 K 折交叉验证的平均 loss 当适应度。4.2 现象某一代开始 loss 全是 NaN整个种群适应度都是 -1e9原因学习率基因变异到了过大值或者 dropout 太小加上学习率大导致梯度爆炸。解决学习率用对数刻度前面 BOUNDS 里已经处理同时在build_and_eval里加梯度裁剪clipnorm1.0并在fitness里捕获异常返回极差适应度。这样烂个体自然被淘汰不会污染种群。4.3 现象GA 跑了 20 代最优值几乎没变原因种群多样性丢失所有个体长得差不多。解决检查变异概率是不是太低或者选择压力锦标赛 k是不是太大。把 k 从 3 降到 2变异概率适当提高或者每隔几代随机注入 1 到 2 个全新随机个体。另外确认random_gene的采样范围没写错如果所有个体初始化就挤在一个角落后面怎么进化都出不来。4.4 现象每次运行 GA 结果都不一样没法复现原因随机种子没固定。GA 本身有随机性初始化、选择、交叉、变异LSTM 训练也有随机性权重初始化、dropout。解决在run_ga开头固定np.random.seed和tf.random.set_seed并且在build_and_eval里也固定一次。注意 TensorFlow 在 GPU 上的某些算子仍有非确定性要完全复现得设tf.config.experimental.enable_op_determinism()但会拖慢训练按需取舍。4.5 现象单次 GA 跑了好几个小时效率太低原因每个个体都训满 epochs且种群大、代数多。解决三个手段叠加——用EarlyStopping砍掉没希望的个体把epochs从 100 降到 30 左右GA 阶段只需要相对排序不需要训到极致数据量大时先用子采样数据搜超参数搜完再用全量数据训最终模型。我一般还会把pop_size和generations做成可配置先小规模试跑确认流程通了再放大。5. 进阶技巧用 GA 的收敛曲线判断该不该继续投入跑完 GA 之后别急着拿最优超参数去训最终模型先看收敛曲线。把history画出来横轴代数、纵轴最优验证 loss。如果曲线在前 5 代快速下降然后走平说明搜索空间合理、GA 找到了好区域这时候把最优基因decode出来用全量数据训一个完整模型epochs 拉到 100 以上patience 设 10再在独立测试集上评估。如果曲线一直震荡不收敛说明种群太小或者变异太猛调参数重跑。如果曲线一开始就平说明初始化范围没覆盖到好区域检查 BOUNDS 是不是设窄了。还有一个实用技巧把 GA 搜出来的最优超参数和人工调参的结果做对比用同一份测试集、同一个评估指标MAE、RMSE、MAPE 都算一遍。如果 GA 只比人工好一点点那说明你的任务对超参数不敏感没必要上 GA把精力放到特征工程上更划算如果 GA 明显更好那这套 pipeline 就值得固化下来以后换数据集只改数据加载部分GA 主循环和建模函数基本不用动。import matplotlib.pyplot as plt # history 是 run_ga 返回的每代最优适应度负的 val_loss plt.plot([-h for h in history], markero) plt.xlabel(Generation) plt.ylabel(Best Val Loss) plt.title(GA Convergence Curve) plt.grid(True) plt.show()这张图是我每次跑完 GA 必看的它比任何指标都直观地告诉你「还有没有继续搜的必要」。如果曲线还在缓慢下降可以加代数接着跑如果已经平了 5 代以上直接停别浪费算力。最后说个我自己的习惯GA-LSTM 这套东西我从来不会一上来就用在正式项目里而是先拿一份小数据集几百到几千条跑通全流程确认编码、适应度、交叉变异、边界修复都没问题再换真实数据。因为 GA 的 bug 很隐蔽——它不会报错只会安静地给你一个看起来还行但其实没优化到位的结果。先小后大、先验证再投入这个顺序能帮你省下大量返工时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?