简介:这是面向时间序列预测场景的GA-LSTM遗传算法优化LSTM预测代码,适合具备Python与深度学习基础、希望借助遗传算法改善LSTM网络参数配置的开发者,可迁移到股价走势、电力负荷等序列预测任务中。压缩包共4个文件,以Ga.py与lstm.py两个Python脚本为主,并辅以对应的pyc编译文件与测试数据,整体约8KB,代码量精简、结构明晰,便于快速拆读和复用。
已有6964人学习下载。资源清晰划分了遗传算法与LSTM两大模块:Ga.py承载种群初始化、适应度评估、交叉及变异等迭代寻优逻辑,lstm.py则负责数据序列化与模型结构定义,test测试数据可用于检验优化后的泛化能力。对照代码可快速理解GA与LSTM结合的完整流程,掌握一种通用的参数优化思路,直接运用于其他序列预测任务。
1. 把 LSTM 超参数交给遗传算法:GA-LSTM 到底解决什么问题
调 LSTM 超参数,最怕手搓。时间步、神经元数、学习率、batch size、dropout,每个变量都是连续区间,彼此还耦合。你用网格搜索把这几个数扫一遍,一台 8 核机器可能跑一个通宵;靠经验拍脑袋,又常常在过拟合和欠拟合之间来回翻车。GA-LSTM 的思路是把超参数选择当成黑箱优化问题:底层仍然是 LSTM 做时间序列预测,顶层用遗传算法去迭代超参数组合,每一轮进化都用“验证集误差”回答哪个参数组合更好。
这个组合最常见的落地场景是单变量预测,比如电力负荷预测、股票收盘价预测、设备寿命预测里的回归任务。适合谁呢?已经会用 LSTM 出预测结果、但调参没有确定方向的人。纯新手也能照抄代码跑通,但至少得懂 Python 的循环、函数和 Keras 的 fit/predict。本文会从 LSTM 基座写到遗传算法算子,最后给出一份可复现的 Python 实现,以及我在实际调参中踩过的典型坑。
2. 先搭 LSTM 预测基座:窗口怎么建、验证集怎么切、误差怎么算
任何遗传算法优化都需要一个“评估闭环”:给一组超参数,训练一次 LSTM,返回一个分数。这个闭环的底层就是 LSTM 的时间序列预测子流程。我一般会先把固定超参数的 LSTM 跑通,确认数据格式、切分方式、误差计算都正确,再往上装遗传算法。否则 GA 跑完十几代,回头发现训练集和验证集重叠,所有优化成果都要推倒重来。
对于单变量序列,最常见做法是把序列改造成监督学习形式:用前lookback个时间点预测后horizon个点。先用窗口从原始序列里切出二维输入和一维目标,再按时间顺序拆分。这里有几个选择会直接决定 GA 优化空间,先花一点篇幅说清楚。
2.1 单步滚动预测为什么比多步输出更适合 GA 迭代
预测任务分两种:单步预测和多步预测。单步预测每次只预测下一个时间点,模型简单,训练开销小;多步预测要一次性输出未来 M 个点,或者用递归方式预测多步,模型复杂度和误差传递问题都会放大。GA-LSTM 在搜索阶段要训练大量模型,我强烈建议先锁定单步滚动预测。这样能显著缩短单次评估时间,让同样预算下进化更多代。
等到 GA 选出最优超参数,你再决定是不是要换成多步模型。常见做法是在最终阶段把输出层改为M个神经元,或保留单步模型做滚动预测。用单步评估只是为了让超参数选择更便宜,不代表业务上线时必须用单步。这一点理解之后,窗口构造就很好写了。
import numpy as np def make_windows(data, lookback=12, horizon=1): X, y = [], [] for i in range(len(data) - lookback - horizon + 1): X.append(data[i:i + lookback]) y.append(data[i + lookback:i + lookback + horizon]) return np.array(X), np.array(y)make_windows的时间复杂度是 O(n×lookback),对几千条数据完全够用。X的 shape 是(样本数, lookback, 特征数),Keras LSTM 需要这个三维输入;y是(样本数, horizon)。特征数这里取 1,因为例子是单变量序列。如果是多变量输入,把多个特征拼到最后一个维度就行,后面 GA 解码时input_shape需要同步修改。
2.2 数据清洗与滑动窗口构造:缩放必须在切分之前想清楚
时间序列预测里最常见的翻车点是 MinMaxScaler 到底什么时候 fit。很多人在整个数据上scaler.fit_transform,然后再切训练集、验证集。这在 LSTM 超参数搜索里是有泄漏风险的:验证集的数据分布已经被缩放器记住,验证误差会被低估。正确的顺序应该是先切分,再用训练集 fit 缩放器,用同一套缩放器 transform 验证集和测试集。
Python 实现上,我一般会先把原始序列拆成 train 和 val 两块,分别缩放后再各自构造窗口。下面这段是修正后的流程:
from sklearn.preprocessing import MinMaxScaler def scale_by_train(train_raw, val_raw): scaler = MinMaxScaler(feature_range=(0, 1)) train_scaled = scaler.fit_transform(train_raw.reshape(-1, 1)) val_scaled = scaler.transform(val_raw.reshape(-1, 1)) return train_scaled, val_scaled注意val_raw.reshape(-1, 1)是因为 scaler 要求二维输入。切分时还要小心:如果数据带有季节周期,建议在 train 和 val 之间留出一段 gap,比如把最后几十个点丢弃,避免验证集紧贴训练集尾部导致周期性被直接复制。GA 评估阶段我们关注的是相对排序,不是绝对最优,所以宁可多留 gap 也不要用信息重叠的数据。
2.3 训练集、验证集、测试集三分法:GA 选超参数时只用验证集
GA 的适应度函数不能拿训练集误差当分数,那只会选出记忆能力强的模型;也不能拿最终测试集当分数,否则遗传算法会在几十代迭代里逐渐把测试集“记住”。正确做法是保留一部分完全不参与遗传算法的测试集,只有等到进化结束、确定最终超参数后,才用它在最终模型上评估一次。
我习惯把数据按 7:2:1 切:70% 训练,20% 验证,10% 完全保留。GA 每一轮评估里,模型在训练集上训练,在验证集上算 RMSE,这个 RMSE 就是 GA 的适应度。最终超参数确定后,我会拿训练集加验证集重新训练最终模型,再用保留测试集看真实误差。
split_train = int(len(scaled_data) * 0.7) split_val = int(len(scaled_data) * 0.9) train_raw = scaled_data[:split_train] val_raw = scaled_data[split_train:split_val] test_raw = scaled_data[split_val:] train_scaled, val_scaled = scale_by_train(train_raw, val_raw) _, test_scaled = scale_by_train(train_raw, test_raw)这段代码里有个细节:scale_by_train(train_raw, test_raw)返回的第一个 train_scaled 会被扔掉,但第二个是有效的缩放器。要注意如果数据量特别小,10% 保留测试集可能只有几个点,RMSE 波动会很大。这时可以改成按数值点切分:比如保留最后 50 个点当测试集,不足再补。GA 的验证集本身也要足够长,否则单次验证误差根本不能反映泛化水平。
3. 遗传算法的原理与代码:个体编码、适应度、选择交叉和变异
遗传算法原理用一句话概括就是:模拟自然选择,让一组候选解通过选择、交叉、变异不断演化。落到 LSTM 调参场景,候选解就是一组超参数。很多刚接触 GA 的人会去找二进制编码的教材实现,实际工程里我更推荐实数编码。因为 LSTM 超参数里有整数也有浮点数,直接用 0 到 1 的染色体,再用解码函数映射到真实超参数,写起来直观也不容易越界。
这一章会给出可直接复用的 GA 核心代码。为了让你看懂每一步,我会把“个体表示”和“进化算子”分开讲。读者如果已经写过遗传算法 Python 代码,可以直接跳到 3.2 看适应度函数怎么和 Keras 配合。
3.1 个体编码的超参数表和解码函数
GA 种群里的每个个体是一条染色体。这里染色体是长度为 5 的实数向量,每个分量在 0 到 1 之间。五个基因分别对应lookback、units、learning_rate、batch_size、dropout。超参数范围如下表:
| 基因 | 超参数 | 搜索范围 | 编码方式 |
|---|---|---|---|
| g0 | lookback | 5~30 | 整数线性映射 |
| g1 | LSTM units | 16~128 | 整数线性映射 |
| g2 | learning_rate | 1e-4~1e-2 | 对数缩放 |
| g3 | batch_size | 16~128 | 2 的幂映射 |
| g4 | dropout | 0~0.4 | 浮点线性映射 |
学习率用对数缩放,而不是线性缩放。因为从 1e-2 到 1e-4 跨越两个数量级,线性映射会把 1e-3 和 1e-2 之间的差异压到很小,GA 很难区分。对数映射让染色体在 0 到 1 区间里移动时,学习率在数量级上均匀变化。batch_size 映射成 2 的幂,是为了满足多数深度学习框架对 batch_size 的偏好。
param_bounds = { 'lookback': (5, 30), 'units': (16, 128), 'lr': (1e-4, 1e-2), 'batch_size': (16, 128), 'dropout': (0, 0.4), } def decode(individual): # 前两个基因是整数,直接用 round 映射到边界 lookback = int(round(individual[0] * (param_bounds['lookback'][1] - param_bounds['lookback'][0]) + param_bounds['lookback'][0])) units = int(round(individual[1] * (param_bounds['units'][1] - param_bounds['units'][0]) + param_bounds['units'][0])) # 学习率用对数,基因从 0 到 1 对应 1e-4 到 1e-2 lr = 10 ** (-4 + individual[2] * 2) # batch_size 用 2 的幂,基因 0~1 对应 4~7,也就是 16 到 128 batch_exp = int(round(individual[3] * (7 - 4) + 4)) batch_size = 2 ** batch_exp dropout = individual[4] * param_bounds['dropout'][1] return { 'lookback': lookback, 'units': units, 'lr': lr, 'batch_size': batch_size, 'dropout': dropout, }decode是遗传算法和 LSTM 模型之间唯一的接口。你以后想加num_layers、activation、optimizer,只需要改这段映射。要特别注意int(round())后的结果是否会越界:chromosome 是 0 到 1 之间的浮点数,round 之后可能因为边界浮点误差得到 16 或 128,这没问题,但 dropout 记得 clip 到合法范围。
3.2 适应度函数:把验证集 RMSE 翻译成 GA 能用的分数
GA 里的适应度要数值化,还要保证比较方向一致。LSTM 回归任务最简单的是用验证集 RMSE,越小越好。为了和种群选择逻辑统一,我这里直接让适应度函数返回 RMSE,后面的锦标赛选择直接按小值选中。下面是最小可用的评估函数:
import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.optimizers import Adam def fitness_fn(individual, X_train, y_train, X_val, y_val): params = decode(individual) try: tf.random.set_seed(42) model = Sequential() model.add(LSTM(params['units'], activation='tanh', input_shape=(params['lookback'], X_train.shape[-1]))) model.add(Dropout(params['dropout'])) model.add(Dense(1, activation='linear')) model.compile(optimizer=Adam(learning_rate=params['lr']), loss='mse') model.fit(X_train, y_train, epochs=15, batch_size=params['batch_size'], validation_data=(X_val, y_val), verbose=0) pred = model.predict(X_val, verbose=0) rmse = float(np.sqrt(np.mean((y_val - pred) ** 2))) except Exception: rmse = 1e10 return rmsefitness_fn里有三个关键点。第一,每次建立独立模型,不共享权重,这是 GA 评估公平性的前提。第二,tf.random.set_seed(42)尽量降低初始化差异,但 GPU 上多个操作可能仍然有抖动,这个放到第五章细说。第三,except Exception是兜底:一旦某个超参数组合导致训练过程崩溃,比如 NaN、shape 不匹配,直接给一个惩罚性分数,避免 GA 选择到无效解。
需要注意X_train和X_val的 lookback 维度和解码后的params['lookback']必须一致。如果 GA 在迭代中改变了 lookback,那么每次训练前都要重新make_windows,否则 shape 会报错。这里为了简化,把窗口数据预先用最大 lookback 构造所有候选窗口,解码时再按params['lookback']截取。下面代码演示这个技巧。
3.3 选择、交叉、变异算子与种群初始化
遗传算法的三个核心算子分别是选择、交叉、变异。选择负责把验证集 RMSE 较小的个体保留下来,交叉在种群内交换基因片段,变异负责在局部探索。我这里不搞复杂概率模型,直接给出经过验证的最简实现。
def init_population(pop_size, gene_len=5): return np.random.rand(pop_size, gene_len) def tournament_select(pop, scores, k=3): candidates = np.random.choice(len(pop), k, replace=False) best = min(candidates, key=lambda i: scores[i]) return pop[best] def crossover(parent1, parent2, prob=0.8): if np.random.rand() < prob: point = np.random.randint(1, len(parent1) - 1) child = np.concatenate([parent1[:point], parent2[point:]]) return child return parent1.copy() def mutate(individual, prob=0.15, step=0.1): child = individual.copy() mask = np.random.rand(len(child)) < prob child[mask] = np.clip(child[mask] + np.random.normal(0, step, mask.sum()), 0, 1) return childtournament_select的k=3表示从种群中随机抽 3 个个体,选 RMSE 最小的一个当作父代。这个操作不依赖全局排序,方便并行。crossover是单点交叉:交换点之前来自 parent1,之后来自 parent2。之所以用np.random.randint(1, len(parent1)-1),是避免换到空片段。mutate用的是高斯扰动,步长 0.1,放在 0 到 1 区间里不会太猛。
有了这些算子,一个 GA 代际循环只有四步:算适应度、选父代、交叉生成子代、变异。伪代码逻辑在第四章完整脚本里会出现。这里想强调:GA 的有效性不在于算子有多复杂,而在于适应度函数是否准确测量了 LSTM 的泛化误差。算子标准就够用。
4. 端到端复现 GA-LSTM:完整跑通并判断结果是否值得信
前面零件已经拆完,这一章把它们拧成一台完整机器。你会在一个合成正弦波序列上看到 GA-LSTM 的完整流程。用正弦波替代真实数据的理由是它能低成本复现,不用额外下载数据集,把重点放在 GA 和 LSTM 的衔接上。实际业务时把 sine 数据换成 pandas 读入的 csv 列即可。
完整脚本长度大约一百多行,我会按数据准备、解码评估、进化主循环三块给出。建议先整体过一遍,再照着敲,不要直接复制就跑,否则改数据集时容易漏掉 shape。
4.1 把代码串成主流程:初始化、进化、解码、训练、记录
先准备数据。这里用带噪声的正弦波构造一个回归序列,然后按前文顺序切分。
np.random.seed(1) t = np.arange(0, 30, 0.02) raw = (np.sin(t) + 0.05 * np.random.randn(len(t))).reshape(-1, 1) split_train = int(len(raw) * 0.7) split_val = int(len(raw) * 0.9) scaler = MinMaxScaler() train_raw = raw[:split_train] val_raw = raw[split_train:split_val] test_raw = raw[split_val:] scaler.fit(train_raw) train_scaled = scaler.transform(train_raw) val_scaled = scaler.transform(val_raw) test_scaled = scaler.transform(test_raw)这里注意scaler.fit(train_raw)只 fit 训练部分。接着用最大 lookback 构造窗口。GA 解码时,每个个体需要的滑动窗口长度不同,所以我们在这一步造一个“超集窗口”,后续按个体截取。
MAX_LOOKBACK = 30 X_train_all, y_train = make_windows(train_scaled, MAX_LOOKBACK) X_val_all, y_val = make_windows(val_scaled, MAX_LOOKBACK) # 按个体提取指定 lookback def extract_windows(X_all, lookback): return X_all[:, -lookback:, :]extract_windows取每个样本的最后lookback个时间步。这里采用X_all[:, -lookback:, :],可以保证窗口的时间顺序不被打断。如果你从X_all[:, :lookback, :]截取,等于每次预测都丢掉最新数据,RMSE 会明显变差,这是新手最容易犯的错。
下面是进化主循环。种群大小设成 6,遗传代数 5,最少意味着 6×5=30 次 LSTM 训练。对合成数据大约几分钟内能跑完;真实数据上请一定先减小 epochs。
pop_size = 6 n_generations = 5 population = init_population(pop_size) for gen in range(n_generations): scores = [] for ind in population: params = decode(ind) lookback = params['lookback'] Xt = extract_windows(X_train_all, lookback) Xv = extract_windows(X_val_all, lookback) score = fitness_fn(ind, Xt, y_train, Xv, y_val) scores.append(score) best_idx = np.argmin(scores) print(f"gen {gen}: best rmse={scores[best_idx]:.5f}, " f"params={decode(population[best_idx])}") new_population = [population[best_idx]] while len(new_population) < pop_size: p1 = tournament_select(population, scores, k=3) p2 = tournament_select(population, scores, k=3) child = crossover(p1, p2) child = mutate(child) new_population.append(child) population = np.array(new_population)主循环里最容易被忽略的一行是new_population = [population[best_idx]]。这是精英保留策略:每一代最优个体直接进入下一代,防止交叉变异破坏已经找到的好参数。没有它,GA 可能在后期退化,RMSE 甚至比上一代还差。tournament_select的 k 越大选择压力越强,我这里用 k=3 比较温和。
4.2 进化日志与收敛判断:这些现象说明 GA 在工作
跑完上面的代码,控制台会输出类似下面这样的日志:
gen 0: best rmse=0.11738, params={'lookback': 23, 'units': 66, 'lr': 0.0032, 'batch_size': 32, 'dropout': 0.22} gen 1: best rmse=0.09821, params={'lookback': 27, 'units': 88, 'lr': 0.0018, 'batch_size': 64, 'dropout': 0.15} gen 2: best rmse=0.08776, params={'lookback': 29, 'units': 76, 'lr': 0.0014, 'batch_size': 64, 'dropout': 0.17} gen 3: best rmse=0.08653, params={'lookback': 28, 'units': 80, 'lr': 0.0011, 'batch_size': 64, 'dropout': 0.18} gen 4: best rmse=0.08594, params={'lookback': 29, 'units': 79, 'lr': 0.0012, 'batch_size': 64, 'dropout': 0.16}看到前几代 RMSE 快速下降,后几代下降变慢,说明 GA 在有效收敛。最理想的是最后一代的最优个体相对前一代变化不大,这代表搜索已经趋于稳定。如果第二十代比第十代还差,多半是训练随机性太大,或者种群多样性丢失,记得先回去固定随机种子。
这里有两点需要警惕。一是 RMSE 本身受序列分布影响,不同的数据用同一个 RMSE 值没有对标意义,只能看种群内相对排序。二是 GA 的最佳个体不一定在所有数据上都能复现,因为单个验证集只有一段序列。更稳的做法是跑三次 GA,把三次最优超参数取并集,再逐个验证。
4.3 先跑三次随机搜索再上 GA:用基线判断 GA 值不值得
GA 不是调参银弹。如果超参数空间很小,或者你的验证集非常稳定,也许随机搜索就能达到同样效果。我在项目中习惯的做法是先用随机搜索建立基线:同样评估 30 次,每次随机采一组超参数,记录最优 RMSE;然后再跑 GA。两条线对比,能告诉你 GA 的相对收益有多少。
下面这张表是我在不同项目里用的对比维度,不是固定结果,只是评估思路:
| 调参方式 | 30 次评估后是否逼近局部最优 | 实现复杂度 | 是否容易改并行 |
|---|---|---|---|
| 网格搜索 | 通常不会,维度多则稀疏 | 低 | 容易并行 |
| 随机搜索 | 会碰到较优点,但无方向 | 低 | 容易并行 |
| GA-LSTM | 有方向,能逼近更好组合 | 中 | 中等,需要同步父代 |
随机搜索代码非常简单:for i in range(30): ind = np.random.rand(5); score = fitness_fn(...)。如果随机搜索 30 次的最优 RMSE 已经接近 GA 结果,说明这个参数空间用不着 GA,别为用而用。如果 GA 在同样预算下能稳定低 10% 到 20% 的 RMSE,就值得把它固化到项目流程里。
5. GA-LSTM 避坑与排查:五个实测才懂的教训
GA-LSTM 的坑,一半在遗传算法,一半在时间序列。下面是五个我实际遇到过的典型问题。每一条都按现象、原因、解决三步写,你在复现时可以直接对照排查。
5.1 随机种子没固定,同一组超参数调出来的 RMSE 飘得离谱
现象:GA 里同一个个体在第二次评估时 RMSE 从 0.11 变成 0.09,导致下一代选择结果变化剧烈。
原因:LSTM 的权重初始化是随机的,验证集切分顺序不同也会改变 RMSE。虽然 Keras 有全局随机种子,但 TensorFlow 在 GPU 上的一些算子仍存在不确定性的黑匣子。
解决:在fitness_fn开头固定tf.random.set_seed(42),GA 主循环开始处固定np.random.seed(42),两者不要混用。如果你的机器是 CPU 推理,RMSE 基本能稳定复现;在 GPU 上想完全复现,需要设置环境变量TF_DETERMINISTIC_OPS=1,但会牺牲速度。折中方案是每个个体评估两次取均值,预算不够时至少保证一次评估的代码路径完全一致。
5.2 验证集被反复翻牌,GA 也在“过拟合验证集”
现象:GA 选出的超参数在验证集上 RMSE 很低,但在完全没见过的测试集上表现变差。
原因:遗传算法的每一代都基于同一段验证集做选择。优化进行到几十代之后,它不只是泛化,而是把验证集的噪声也拟合进去了。
解决:最终选出的超参数不要直接在验证集上做最终评价。先用训练集加验证集训练最终模型,再用保留测试集测一次。如果测试集误差比验证误差高出一大截,说明出现了选择偏差。实践中我会留出完全隔离的测试集,测试集只允许用一次。
5.3 数据缩放发生在切分之前,偷偷泄漏了未来信息
现象:GA 得到的验证 RMSE 只有 0.03,但特征可视化发现验证集和训练集分布完全一样。
原因:整个数据集先fit_transform再做切分,scaler 用到了全量数据。验证集和测试集的均值和方差被训练集信息污染,使验证误差偏小。
解决:遵循第 2 章的scale_by_train流程,只对训练集fit,验证集和测试集用同一个 scalertransform。另外,如果数据有成段的缺失值,最好用前一段数据做填充,不能用整列均值填充,那也是一种泄漏。
5.4 训练轮数和种群大小没有配合,预算烧光还看不到趋势
现象:pop_size=30,generations=20,每代还要训练 15 epochs,整体需要 600 次 LSTM 训练。机器跑了一整夜,只完成两代。
原因:在没有评估成本预估的情况下,把 GA 参数设置得过大。GA 的复杂度不是 O(pop_size × generations) 这么简单,每个个体还可能因为 lookback 不同而训练更慢。
解决:第一轮先跑pop_size=4, generations=3, epochs=5,摸清单次评估耗时。预算充足时再增加到pop_size=12, generations=10。训练轮数方面,可以用早停控制单个个体耗时,但要注意公平性,见下一条。
5.5 早停回调在 GA 评估中乱用,带来不公平竞争
现象:有的个体早停在 12 epochs,有的跑满 50 epochs,GA 会偏向早停产生的低损失模型。
原因:早停是根据验证损失动态停止的。GA 的每个个体接受不同训练强度,相当于在比“谁训练得更少更巧”,而不是比“谁的参数结构更好”。
解决:如果你给每个个体都加了 EarlyStopping,必须确保最大 epochs 一致,并且至少让每个模型跑到最低阈值,比如前 5 个 epochs 内不早停。更公平的做法是固定 epochs,不做自适应早停。只有在最终训练阶段,才对 GA 选出的最优超参数使用 EarlyStopping。
6. 进阶玩法:让每一次 GA-LSTM 评估都不白跑
遗传算法评估成本高,所以每一次训练都值得做点优化。第一个技巧是把适应度评估从 train from scratch 改成 warm start。种群内很多个体只差一两个基因,如果每次从头初始化权重,不仅慢,还增大了随机性。常见做法是先训练一个基础超参数的模型,保留权重,GA 评估新个体时用 LSTM 层权重做初始值继续微调几步。注意,这个技巧只在 GA 迭代到后半段、参数范围收窄时用,前半程还是要从头训练以保证公平。
第二个技巧是并行评估。每个个体互相独立,天然可并行:
from concurrent.futures import ProcessPoolExecutor def evaluate_with_extract(ind): params = decode(ind) Xt = extract_windows(X_train_all, params['lookback']) Xv = extract_windows(X_val_all, params['lookback']) return fitness_fn(ind, Xt, y_train, Xv, y_val) with ProcessPoolExecutor(max_workers=4) as pool: scores = list(pool.map(evaluate_with_extract, population))进程池需要注意:ProcessPoolExecutor会复制内存里的X_train到子进程,数据太大时用内存映射文件;每个子进程都要重新设置随机种子。
第三个技巧是保存评估日志。我最早跑 GA-LSTM 时只记住最后一行的最优参数,后来想分析收敛情况却找不到历史。现在我至少会在每一代末写入一行 CSV:gen, individual_id, lookback, units, lr, batch_size, dropout, rmse。这样即使进程崩溃也能断点续跑;某一代出现异常 RMSE 时,还能回溯到是哪组参数导致的。
我个人的习惯是:先跑三轮随机搜索作为基线,再让 GA 跑固定预算。每次拿到 GA 最优结果,都要先在保留测试集上验证。不少时候遗传算法看起来有效,实际只是验证集过拟合;有了保留测试集这道防线,踩坑的概率就会低很多。希望这些经验能帮到你。
本文还有配套的精品资源,点击获取