☰
GA-LSTM实践:用遗传算法自动搜索LSTM最优超参数
2026/9/28 15:57:11 网站建设 项目流程

简介:这份代码基于遗传算法优化长短时记忆网络,面向时间序列预测任务,通过遗传算法全局搜索网络权重,提升模型泛化能力。标签虽标注为某一数值计算软件,但实际实现采用一种主流编程语言,适合具备该语言基础的机器学习学习者,也适合希望了解进化算法与深度学习结合的研究者。压缩包内共五个文件,包括两个源代码文件、两个编译后文件以及一个文本说明文档,整体仅九千字节,结构精简。目前已有三百九十人学习下载。代码流程涵盖数据预处理、遗传算法核心逻辑、网络构建与训练预测等环节,利用适应度函数评估个体优劣,并通过选择、交叉、变异等操作迭代寻优,帮助读者快速掌握遗传算法与长短时记忆网络组合模型的实现细节,便于在此基础上开展二次开发或对比实验。

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 和设为 128,learning_rate设为 0.001 和设为 0.01,最终预测精度可能差出一大截。遗传算法做的事就是拿一个随机初始化的超参数种群,不停算适应度、挑选精英、杂交产生下一代。每迭代一轮,种群整体就朝着验证集误差更低的方向移动一步。

# 遗传算法与 LSTM 结合的伪代码框架,帮助理解两者边界 population = init_population(size=20) # 随机生成 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, k=4) # 精英保留 population = crossover_and_mutate(parents, population_size=20)

这段伪代码把两层系统的分工画得很清楚:遗传算法只负责生成超参数组合和评价结果,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, timesteps=10) print(f"X shape: {X.shape}, y shape: {y.shape}")

这里有个容易犯的错误:range(len(data) - timesteps)生成的样本数比len(data) - timesteps少一个,因为最后一个时间步没有对应的未来值。做多步预测时,y可以改成未来 N 步的值序列,但标签构造方式略有不同。create_sequences函数里data[i:i+timesteps]是左闭右开区间,最后一个索引是i+timesteps-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 params

gene_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, activation='tanh', input_shape=(timesteps, feature_dim)), Dense(1) # 单步预测输出 ]) model.compile(optimizer=Adam(learning_rate=learning_rate), loss='mse', metrics=['mae']) return model # 先用一组经验参数验证数据管道没问题 model = build_lstm(units=64, timesteps=10, feature_dim=1, learning_rate=0.001) model.fit(X_train, y_train, epochs=20, batch_size=32, validation_split=0.1, verbose=1)

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(monitor='val_loss', patience=5, restore_best_weights=True) history = model.fit( X_train, y_train, epochs=30, batch_size=params["batch_size"], validation_data=(X_test, y_test), callbacks=[early_stop], verbose=0 ) return history.history['val_loss'][-1] def initialize_population(size): return np.random.randint(0, 2, (size, GENE_LENGTH)) def tournament_select(population, fitness, k=3): # 锦标赛选择:随机挑 k 个,取适应度最好的 idx = np.random.choice(len(population), k, replace=False) 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 变 1,1 变 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(f"Generation {gen+1}: 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是精英保留数量,保证每一代最好的个体不会在交叉变异中丢失。锦标赛选择里的k=3是经典参数,太小容易早熟收敛,太大则选择压力过强。变异率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_workers=4) 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, epochs=50, batch_size=final_batch, validation_split=0.1, verbose=1) # 预测与反归一化 y_pred = model.predict(X_test) # 若 y_test 做过归一化,需用之前的 mean/std 还原 y_pred_original = y_pred * std + mean

进化过程中用EarlyStopping(patience=5)防止浪费算力,最终训练时我给到epochs=50,让模型充分收敛。反归一化这一步容易漏掉:预测值是在标准化空间里的,要还原成原始数据的量纲才能算业务口径的误差。

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 组里最优的略好于 A
GA-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 章重新审视三个关键设计即可。这套方法说到底是个调参效率工具,不是魔法,数据质量差了谁来了也救不回。希望这些流程和踩坑记录能帮你在自己的预测任务里走得更顺一点。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询