GA-LSTM实践:用遗传算法自动搜索LSTM最优超参数
发布时间:2026/9/28 15:57:15来源:尧图网络
简介这份代码基于遗传算法优化长短时记忆网络面向时间序列预测任务通过遗传算法全局搜索网络权重提升模型泛化能力。标签虽标注为某一数值计算软件但实际实现采用一种主流编程语言适合具备该语言基础的机器学习学习者也适合希望了解进化算法与深度学习结合的研究者。压缩包内共五个文件包括两个源代码文件、两个编译后文件以及一个文本说明文档整体仅九千字节结构精简。目前已有三百九十人学习下载。代码流程涵盖数据预处理、遗传算法核心逻辑、网络构建与训练预测等环节利用适应度函数评估个体优劣并通过选择、交叉、变异等操作迭代寻优帮助读者快速掌握遗传算法与长短时记忆网络组合模型的实现细节便于在此基础上开展二次开发或对比实验。1. 遗传算法和LSTM放在一起到底解决什么问题做时间序列预测的人大概率都经历过这种场景LSTM模型的精度明明还有提升空间但面对timesteps、units、learning_rate、batch_size这一堆超参数手动试了几天也不见明显起色。网格搜索把参数组合枚举一遍训练一次深度学习模型就要几分钟甚至更久等全部跑完项目的交付节点已经快到了。GA-LSTM 这个名字看起来很学术其实就是把遗传算法当作一个自动调参器替你去搜索 LSTM 的最优超参数组合。它不改变 LSTM 本身的结构而是用“选择、交叉、变异”这三板斧在几十个参数组合里快速逼近一组好用的配置适合手里有预测任务、又不想在调参上无限投入的 Python 从业者。遗传算法在运输调度这类组合优化问题里常常被提起它的原理放到 LSTM 超参搜索上也同样成立把每一组超参数编码成一条染色体用验证集的误差当适应度让好的参数组合一代一代繁殖下去。这套思路代码量不大最朴素的 numpy 实现也就两三百行能稳定收敛后面所有时间序列预测项目都能直接复用。2. 从原理到选型GA-LSTM 为什么值得自己写一套2.1 遗传算法和 LSTM 是怎么拼在一起的GA-LSTM 不改变 LSTM 内部的门控结构它是一个两层嵌套的系统。外层是遗传算法负责维护一个种群种群里的每个个体是一组 LSTM 超参数内层是 LSTM 训练与验证用某组超参数真实地训练一次网络得到验证集误差把这个误差反馈给外层作为该个体的适应度分数。LSTM的结构很好理解就是那套输入门、遗忘门、输出门的循环网络处理序列数据时能记住长距离依赖。但真正让它难用的地方在于同一份数据units设为 32 和设为 128learning_rate设为 0.001 和设为 0.01最终预测精度可能差出一大截。遗传算法做的事就是拿一个随机初始化的超参数种群不停算适应度、挑选精英、杂交产生下一代。每迭代一轮种群整体就朝着验证集误差更低的方向移动一步。# 遗传算法与 LSTM 结合的伪代码框架帮助理解两者边界 population init_population(size20) # 随机生成 20 组超参数 for generation in range(10): fitness_list [] for individual in population: # 每一条个体都包含units, learning_rate, batch_size, timesteps units, lr, batch, timesteps decode(individual) val_loss train_and_evaluate_lstm(units, lr, batch, timesteps) fitness_list.append(1.0 / (val_loss 1e-6)) # 损失越小适应度越高 parents select_top_k(population, fitness_list, k4) # 精英保留 population crossover_and_mutate(parents, population_size20)这段伪代码把两层系统的分工画得很清楚遗传算法只负责生成超参数组合和评价结果LSTM 只负责给定参数下的训练与预测。1.0 / (val_loss 1e-6)这个写法是让损失越小适应度越高加1e-6是为了防止损失为 0 时除零。实际项目里我不会真的用随机初始化而是先用一组手工经验值作为个体之一混进初始种群保证至少不差于手工调参。2.2 和网格搜索、贝叶斯优化相比GA 赢在哪里网格搜索最让人难受的是“维度爆炸”。假设你要调 4 个超参数每个给 5 个候选值那就是5 x 5 x 5 x 5 625次完整的训练。就算每个模型训 30 秒也要 5 个多小时。遗传算法初始随机 20 个个体迭代 10 代每代里优秀个体继续繁殖虽然每代也涉及一部分重新训练但实际总训练次数通常在 60 到 120 次之间不到网格搜索的五分之一。贝叶斯优化在这两年也很流行但它在超参维度升高之后拟合代理模型的开销会变大而且对初始化点位的质量有要求。与之相比遗传算法的优势在于实现透明、容易并行、对参数类型没有太多限制。units是整数learning_rate是浮点数optimizer是离散的字符串选项GA 可以混合编码一次搞定贝叶斯优化处理离散和连续混合空间时要小心地设计核函数和采集函数稍不留神就会在离散维度上表现不佳。我一般会用这张表来决定项目里要不要上 GA-LSTM方案适用场景不适用场景调参次数估算手工经验调参基线模型、排期紧张数据量大、精度要求高5~15 次网格搜索超参数少2超参数多、单次训练慢指数级增长贝叶斯优化中低维度连续参数混合类型参数多30~80 次GA-LSTM混合类型超参数、可并行训练单次训练极慢且无法并行60~150 次注意最后一个边界条件如果单次 LSTM 训练要 20 分钟以上且你没有多张显卡或 CPU 多核并行那 GA-LSTM 也会等得人发慌。这种情况我建议先砍数据量做小样本验证别上来就全量训练。2.3 三个关键设计决定 GA-LSTM 的收敛速度第一个关键设计是编码方案。units这类整数参数可以直接用二进制基因表示但基因位数要算好。比如 units 范围是 [16, 256]用 4 位二进制可以表示 0~15映射公式为16 基因值 * (256 - 16) / 15这样 4 位基因就能覆盖整个范围。learning_rate 这种跨越多个数量级的浮点数盲目用二进制编码会有精度问题更稳妥的做法是直接对浮点数做均匀量化或者用实数编码。第二个关键设计是适应度函数。常见做法是在验证集上取 MSE 或者 MAE我强烈建议再加一个惩罚项如果某组参数训练出来的模型在验证集上震荡剧烈即使平均损失不高也应该给一个惩罚。序列预测模型最怕的就是预测曲线滞后或大幅震荡这种模型上线后根本不能用。第三个关键设计是搜索空间边界。遗传算法的交叉、变异看似能探索任意区域但如果初始边界设得离谱比如 learning_rate 上限给到 1.0那算法很大概率在浅层乱跳。我一般会把 bounds 设置成search_space { units: [16, 256], # LSTM 隐藏层神经元数量 learning_rate: [0.0001, 0.01], # 超过 0.01 的训练基本发散 batch_size: [16, 128], timesteps: [3, 30], # 回看窗口长度 }timesteps这个参数常常被忽略但它对预测效果的影响有时候比units还大。窗口太短学不到周期性太长则引入噪声GA 的价值恰恰在于能同时帮你确定这个值。3. 从数据到基因序列GA-LSTM 落地前的三个准备步骤3.1 用滑动窗口把时间序列变成 LSTM 能吃的样本LSTM 的输入形状是(样本数, timesteps, 特征数)而原始时间序列是一维或二维的(时间步, 特征数)。天底下做序列预测的模型第一件事都是切窗口。假设原始数据有 1000 个时间步timesteps 10那能生成 990 个样本每个样本用前 10 个时间步预测第 11 个时间步。import numpy as np def create_sequences(data, timesteps): X, y [], [] for i in range(len(data) - timesteps): X.append(data[i:i timesteps]) y.append(data[i timesteps]) return np.array(X), np.array(y) # 单特征序列示例 raw_data np.sin(np.linspace(0, 20, 500)) 0.1 * np.random.randn(500) X, y create_sequences(raw_data, timesteps10) print(fX shape: {X.shape}, y shape: {y.shape})这里有个容易犯的错误range(len(data) - timesteps)生成的样本数比len(data) - timesteps少一个因为最后一个时间步没有对应的未来值。做多步预测时y可以改成未来 N 步的值序列但标签构造方式略有不同。create_sequences函数里data[i:itimesteps]是左闭右开区间最后一个索引是itimesteps-1恰好避开标签时刻本身。3.2 时间顺序切分预测任务严禁乱打乱样本分类任务可以把样本随机打乱后切训练集和测试集但时间序列预测绝对不行。用未来的数据训练模型去预测过去这叫信息泄漏测试集上的漂亮精度全是假的。正确做法是按时间顺序切分前 80% 训练后 20% 验证。train_ratio 0.8 split_idx int(len(X) * train_ratio) X_train, X_test X[:split_idx], X[split_idx:] y_train, y_test y[:split_idx], y[split_idx:] # 归一化必须先 fit 在训练集上再 transform 验证集 mean, std X_train.mean(), X_train.std() X_train (X_train - mean) / std X_test (X_test - mean) / std归一化放在切分之后、GA 循环之前。mean和std只能从训练集计算测试集用同一组参数转换。我在项目里遇到过这种情况有人把整份数据统一归一化验证集误差居然低到 0.001后来发现是因为测试集的分布信息已经通过全局均值和标准差泄漏进训练过程了。这也是 GA-LSTM 预测任务最容易翻车的一个点。3.3 基因与超参数的映射关系遗传算法的核心操作都发生在基因空间里但 LSTM 只认识真实的超参数值。所以需要一个解码器完成两者转换。gene_bits 8 # 每个超参数用 8 位二进制基因表示 def decode_individual(gene_array, search_space): 把二进制基因数组解码为超参数字典 params {} keys list(search_space.keys()) for idx, key in enumerate(keys): low, high search_space[key] # 取基因片段映射到 [low, high] gene_value int(.join(map(str, gene_array[idx * gene_bits:(idx 1) * gene_bits])), 2) params[key] low (high - low) * gene_value / (2 ** gene_bits - 1) # units 和 batch_size 必须是整数 params[units] int(params[units]) params[batch_size] int(params[batch_size]) return paramsgene_bits决定搜索分辨率8 位基因能产生 256 个离散档位对超参搜索来说已经足够了。二进制编码的好处是后续的交叉和变异操作可以直接在基因位上进行不需要额外处理连续值边界越界的问题。2 ** gene_bits - 1是最大基因值用来做归一化分母。解码后units和batch_size转成整数learning_rate 保持浮点数这一个函数就是遗传算法和 Keras 之间的桥梁。4. 从零实现一套 GA-LSTM进化循环与完整训练流程4.1 先跑通一个普通 LSTM 作为对照基线GA 再怎么优化前提是普通 LSTM 能正常收敛。先建立一个简单的单层 LSTM 模型确认数据管道和训练流程没毛病再叠加遗传算法。不要一上来就 GA-LSTM 全家桶出问题都分不清是 LSTM 的问题还是 GA 的问题。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense from tensorflow.keras.optimizers import Adam def build_lstm(units, timesteps, feature_dim, learning_rate): model Sequential([ LSTM(units, activationtanh, input_shape(timesteps, feature_dim)), Dense(1) # 单步预测输出 ]) model.compile(optimizerAdam(learning_ratelearning_rate), lossmse, metrics[mae]) return model # 先用一组经验参数验证数据管道没问题 model build_lstm(units64, timesteps10, feature_dim1, learning_rate0.001) model.fit(X_train, y_train, epochs20, batch_size32, validation_split0.1, verbose1)Keras 的Sequential模型组织清晰LSTM层第一个参数是神经元数量input_shape里的timesteps必须和训练数据第二维一致。Dense(1)是输出层做单步回归。跑完这 20 个 epoch记录一下验证集 loss 是多少这就是 GA 后续要打破的分数。4.2 GA 主循环选择、交叉、变异的可搬运代码基础 LSTM 跑通之后把它的训练包装成一个函数输入超参数字典输出验证集损失。遗传算法在 numpy 层面做进化操作不动 LSTM 内部逻辑。import numpy as np from tensorflow.keras.callbacks import EarlyStopping POP_SIZE 16 # 种群大小16 个个体并行评估 GENE_LENGTH 4 * 8 # 4 个超参数每个 8 位基因 N_GENERATIONS 8 # 进化代数 MUTATION_RATE 0.05 ELITE_SIZE 4 # 精英保留个体数 def evaluate_individual(gene_array): params decode_individual(gene_array, search_space) model build_lstm(params[units], params[timesteps], 1, params[learning_rate]) early_stop EarlyStopping(monitorval_loss, patience5, restore_best_weightsTrue) history model.fit( X_train, y_train, epochs30, batch_sizeparams[batch_size], validation_data(X_test, y_test), callbacks[early_stop], verbose0 ) return history.history[val_loss][-1] def initialize_population(size): return np.random.randint(0, 2, (size, GENE_LENGTH)) def tournament_select(population, fitness, k3): # 锦标赛选择随机挑 k 个取适应度最好的 idx np.random.choice(len(population), k, replaceFalse) best_idx idx[np.argmin(fitness[idx])] return population[best_idx] def uniform_crossover(p1, p2): mask np.random.randint(0, 2, GENE_LENGTH).astype(bool) child1 np.where(mask, p1, p2) child2 np.where(mask, p2, p1) return child1, child2 def mutate(gene_array, rate): mutation_mask np.random.random(GENE_LENGTH) rate gene_array[mutation_mask] 1 - gene_array[mutation_mask] # 0 变 11 变 0 return gene_array # 进化主循环 population initialize_population(POP_SIZE) best_fitness_history [] for gen in range(N_GENERATIONS): fitness np.array([evaluate_individual(ind) for ind in population]) best_idx np.argmin(fitness) best_fitness_history.append(fitness[best_idx]) print(fGeneration {gen1}: best val_loss {fitness[best_idx]:.6f}) # 精英直接进入下一代 elite_idx np.argsort(fitness)[:ELITE_SIZE] new_population [population[i].copy() for i in elite_idx] # 填充剩余个体 while len(new_population) POP_SIZE: p1 tournament_select(population, fitness) p2 tournament_select(population, fitness) c1, c2 uniform_crossover(p1, p2) new_population.append(mutate(c1, MUTATION_RATE)) if len(new_population) POP_SIZE: new_population.append(mutate(c2, MUTATION_RATE)) population np.array(new_population)ELITE_SIZE是精英保留数量保证每一代最好的个体不会在交叉变异中丢失。锦标赛选择里的k3是经典参数太小容易早熟收敛太大则选择压力过强。变异率0.05算保守一般取 0.01 到 0.1 之间太高会让进化退化成随机搜索。每代打印的best val_loss应该单调下降至少前几代应该有明显变化如果完全不动大概率是编码或适应度函数出了问题。4.3 并行评估把种群训练分发到多个 CPU 核心每代 16 个个体的 LSTM 训练是纯串行的如果是单机多核 CPU可以用multiprocessing或concurrent.futures并行提速这个优化在 GA-LSTM 里几乎必做。from concurrent.futures import ProcessPoolExecutor def evaluate_population_parallel(population): with ProcessPoolExecutor(max_workers4) as executor: fitness list(executor.map(evaluate_individual, population)) return np.array(fitness)ProcessPoolExecutor会把种群里的每个个体发送到独立进程训练4 个 worker 同时进行总耗时大约能缩减到原来的四分之一。注意evaluate_individual必须是模块级函数不能是嵌套在某个函数内部闭包否则会报PicklingError。GPU 并行是另一种思路但 TensorFlow 默认占用显存16 个模型同时训练显存不够分CPU 多进程是更稳妥的选择。4.4 最终代码GA 找到最优超参数后的训练与预测进化结束后把最优基因解码用全量训练数据重新训练一次最终模型然后在测试集上预测。best_gene population[np.argmin(fitness)] best_params decode_individual(best_gene, search_space) print(Best params:, best_params) # 用最优超参数在全部训练数据上重新训练 final_units best_params[units] final_timesteps best_params[timesteps] final_batch best_params[batch_size] final_lr best_params[learning_rate] model build_lstm(final_units, final_timesteps, 1, final_lr) model.fit(X_train, y_train, epochs50, batch_sizefinal_batch, validation_split0.1, verbose1) # 预测与反归一化 y_pred model.predict(X_test) # 若 y_test 做过归一化需用之前的 mean/std 还原 y_pred_original y_pred * std mean进化过程中用EarlyStopping(patience5)防止浪费算力最终训练时我给到epochs50让模型充分收敛。反归一化这一步容易漏掉预测值是在标准化空间里的要还原成原始数据的量纲才能算业务口径的误差。5. 避坑指南GA-LSTM 高频踩坑的 4 条真实现场5.1 验证集误差每代都在降但测试集预测一团糟现象GA 进化过程看起来很顺利最优验证损失从 0.05 降到 0.01但把模型放到测试集上一画预测曲线明显滞后或失真。原因遗传算法把搜索空间里的配置都试了一遍其中某些超参数组合恰好过拟合了验证集。比如timesteps过大、units过多模型就把验证集的噪声也学进去了。解决把验证集再切一块出来当“进化验证集”GA 在 A 块上选超参数全部进化结束后再用 B 块做最终评估如果 B 块误差和 A 块差距巨大说明选出来的超参数过拟合了验证集。另外EarlyStopping的 patience 可以适当调小到 3让训练更早停住。5.2 某组超参数字典传进去模型直接报 shape 错误现象GA 跑到第 3 代程序突然崩了报错信息是Input 0 of layer lstm is incompatible with the layer。原因基因解码后timesteps变了但训练数据X_train是在外部切好的input_shape和数据维度对不上。解决GA 的搜索空间里包含timesteps时数据切窗口必须放在进化循环内部或者把timesteps固定住只调其他三个参数。更通用的做法是evaluate_individual内部根据params[timesteps]重新切窗口和归一化代价是每代重复切数据但换来的是代码不会莫名崩掉。5.3 种群过早统一多样性下降后收敛不动现象前几代 loss 下降很快到第 4 代开始几乎不变化打印出来的种群基因几乎全部相同。原因锦标赛选择压力过大、变异率太低。ELITE_SIZE如果设得太大新个体很难竞争过精英基因库萎缩。解决把ELITE_SIZE从 4 减到 2变异率从 0.05 提到 0.1。另一个偏方是每代随机注入 2 个完全随机的个体让算法有机会跳出局部最优。5.4 多进程并行在某些机器上直接死锁现象加了ProcessPoolExecutor之后程序在executor.map处卡住CPU 占用飙高但就是不返回结果。原因evaluate_individual内部创建 TensorFlow 模型某些版本下 TensorFlow 与多进程 fork 机制冲突进程在初始化图时互相等待。解决把evaluate_individual里模型创建和训练的逻辑封装到一个独立函数里并在主进程外层加if __name__ __main__保护。如果还卡死换用multiprocessing.get_context(spawn)替代默认 fork 方式。这一条也算是 GA-LSTM 里最玄学的一个坑进程模型换一下可能就好了。6. 验证 GA-LSTM 是否真的有效一份可操作的对比实验流程GA-LSTM 的代码写完后还差最后一步证明它比随便挑的一组参数强。没有对比就谈不上优化效果。我一般会固定同样的数据切分、同样的模型结构、同样的随机种子只改超参数来源跑三组对比。对比组超参数来源预期结果基线 A手工经验参数如 64/0.001/32/10中规中矩基线 B随机搜索取 10 组里最优的略好于 AGA-LSTM遗传算法进化 8 代的输出应优于 A 和 B 的中位水平验证指标不要只看 MSE。序列预测的曲线形态很重要漏峰、滞后、过度平滑这些毛病是 MSE 看不出来的。我会额外计算一个方向准确率预测值比上一时刻高/低的方向是否与真实值一致这个指标在库存预测和电量预测里比 MSE 更贴近业务价值。def direction_accuracy(y_true, y_pred): diff_true np.diff(y_true.flatten()) diff_pred np.diff(y_pred.flatten()) correct np.sign(diff_true) np.sign(diff_pred) return np.mean(correct)np.diff计算相邻时间步的差值np.sign把差值压缩成 1、-1、0 三个方向方向一致的比例就是方向准确率。一般预测模型能做到 60% 到 70% 就不错了低于 50% 说明连随机猜都不如这时候要怀疑数据本身是否存在强噪声。同一组 GA 参数在不同随机种子下可能得到不同结果不能跑一次就下结论。我的习惯是每组实验跑 3 次取中位数作报告最好把每次的验证损失曲线也保留下来。跑完这组对比实验如果 GA-LSTM 确实没有明显赢过随机搜索那大概率是搜索空间边界设得不对或者适应度函数对验证集噪声太敏感回到第 2 章重新审视三个关键设计即可。这套方法说到底是个调参效率工具不是魔法数据质量差了谁来了也救不回。希望这些流程和踩坑记录能帮你在自己的预测任务里走得更顺一点。本文还有配套的精品资源点击获取
网站建设高端定制企业官网