简介:《遗传算法与人工神经网络的应用》是一份面向机器学习、深度学习与数据建模学习者的PDF技术资料,由作者jiebing2020整理上传。文档围绕遗传算法与人工神经网络(ANN)两大核心技术展开,既介绍遗传算法的编码、适应度函数、选择交叉与变异机制,也讲解多层感知器结构、BP反向传播原理及深度神经网络的逐层特征学习过程,并结合文本检索优化场景,展示如何借助遗传算法的全局搜索能力与神经网络的模式识别能力提升检索效率。压缩包共1个文件,为PDF格式,整体大小1.36MB,篇幅精炼,适合需要快速理解两类算法原理及融合应用的读者。资源目前已有195人学习下载,可作为课程设计、论文写作或入门复习的参考资料,帮助读者在较短时间内建立算法框架、掌握关键公式与训练流程。
1. 遗传算法与人工神经网络的应用:把超参搜索从“试到哪算哪”变成进化流程
手调过神经网络超参的人都懂:学习率、隐层节点数、正则强度三者稍微动一下,验证集损失就像换了一个模型。遗传算法与人工神经网络的应用,听起来像论文标题,本质上解决的是这件事——把超参搜索从“试到哪算哪”变成一套能自动迭代的进化流程。GA不替代神经网络,也不替代反向传播,它只负责在超参空间里搜出更优的起跑位置和约束条件。适合正在做回归、时序预测或故障诊断,又被网格搜索折磨过的工程师阅读。这篇文章从原理、算子设计讲到可复现代码和避坑记录,按文中流程走一遍,你就能用几十行核心循环跑出自己的 GA 寻优。
2. 为什么偏偏是遗传算法来配合人工神经网络:反向传播精调不了的超参数
2.1 反向传播擅长精调,却决定不了自己往哪走
大多数人对反向传播的第一印象是“它能找到最优权重”,这个印象需要打个折扣。BP 本质上是一个局部搜索器,它沿着梯度方向修正权重,最终落在哪个局部极小值,高度依赖起点在哪、步长多大、正则怎么压。换到实际训练里,起点由初始化策略决定,步长由学习率决定,正则强度由 weight decay 决定——这些全是超参数。
也就是说,反向传播做得再好,也只在“给定超参数之后”的权重空间里干活。超参数选得离谱,比如学习率 1e-2 配上深层网络,训练曲线可能前几步就冲到 Nan;学习率 1e-6,训练几千步后 loss 还在原地。手调超参的本质问题在于:验证集 loss 对超参数的响应既不光滑也不可导,你没法用梯度告诉我“学习率该加 0.003 还是减一半”。这时候反而轮到遗传算法登场,它根本不关心目标函数长什么样,只要给每个候选超参组合打个分就行。
这也是为什么很多时候用 GA 训 ANN 的第一个工程动作不是写神经网络代码,而是先把“验证集上表现得好不好”封装成一个黑匣子打分函数。神经网络在这个框架里不是被 GA 训练,而是被 GA 按超参组合反复实例化、训练、评分、丢弃。
2.2 超参空间的四个特点,决定网格搜索与贝叶斯优化的边界
神经网络超参空间有四个特点:离散与连续混合、高度非凸、不可导、评估昂贵且带噪声。学习率是连续值但最好在对数域里搜;隐层节点数是正整数;激活函数是类别型变量。四种常用搜索方案放到这个空间里,边界立刻清晰起来。
| 方案 | 擅长 | 短板 | 在 ANN 超参场景的体验 |
|---|---|---|---|
| 网格搜索 | 低维、少量候选 | 维度一多组合爆炸 | 4 个超参各取 5 档就是 625 次完整训练 |
| 随机搜索 | 实现简单,适合做基线 | 没有记忆,不利用已评估点 | 300 次训练后可能还没摸到好区域 |
| 贝叶斯优化 | 连续、低维、评估便宜 | 对类别/整数变量处理别扭 | 架构参数一多,采集函数容易失效 |
| 遗传算法 | 混合编码、无梯度、群体并行 | 评估次数需求大 | 种群 12、迭代 15,180 次训练就能看趋势 |
贝叶斯优化在纯连续超参上很强,但一旦出现“隐层数量用 8 还是 12”这类整数变量,高斯过程核函数的距离度量就开始别扭。遗传算法没有这个心理负担:整数、实数、类别统统编码成向量或字符串,交叉和变异天然处理混合变量。代价是评估次数多,但 GA 的种群本身就是并行结构,多几台机器就能把时间压回来。
2.3 遗传算法原理落进这个场景:GA 和 ANN 结合的两种形态
遗传算法原理一旦脱离经典 TSP 例子,很多人会卡在“染色体到底代表什么”。在运输路径规划里,个体通常是一条完整配送路径;在 ANN 超参搜索里,个体是一组超参向量。换了个载体,选择、交叉、变异这三板斧还是同一套,这就是 GA 通用性的来源,也是新手最容易把概念搞混的地方。
GA 与 ANN 结合有两种落地形态。第一种叫超参进化:固定网络结构不变,只优化学习率、隐层节点数、正则系数这些训练超参,评估成本可控,工程上最常用。第二种叫结构进化:把网络层数、每层宽度、激活函数都编码进染色体,搜索空间更大,理论上限更高,但每个个体要完整训练一个结构差异很大的网络,评估成本成倍上涨。
这篇文章只展开第一种形态。理由是超参进化能把“GA 调参”和“网络训练”两个问题解耦,代码可复现,坑也可枚举。结构进化更适合在超参进化跑通之后再碰,否则出了问题根本分不清是算子在错还是网络结构表达方式在错。
3. 遗传算法核心设计:编码、适应度函数与三个算子的参数选择
3.1 编码方案选实数还是二进制:海明悬崖不是小问题
GA 第一步是决定个体长什么样。很多入门教程默认给二进制编码,因为经典的 0/1 字符串交叉、变异直观。但神经网络超参场景里,二进制编码会带来一个非常实际的问题:海明悬崖。
海明悬崖指两个相邻整数的二进制表示可能差距很远。11 的二进制是 01011,12 是 01100,有三位的差异。GA 做交叉后,子代很可能跳到完全不同的区域,搜索行为变得极不稳定。学习率从 1e-4 到 1e-3 这类跨越,在二进制下可能要翻转好几位才能完成,进化效率非常差。用格雷码能缓解这一问题,但格雷码在连续超参上的映射还是要额外写代码。
我一般直接用实数编码,并且把每个分量归一化到 [0, 1] 区间。这样交叉、变异算子不需要知道每个超参的真实物理范围,只需要在 [0, 1] 边界内操作,最后解码时再映射到真实值。比如学习率用对数映射:个体数值 0.5 对应 10^(-5 + 0.5 * 4),也就是 1e-3,保证低学习率和高等学习率在搜索空间里均匀分布。隐层节点数用线性缩放后四舍五入,既保留整数语义又不破坏交叉变异的连续性。
3.2 适应度函数怎么设:验证集 MSE 加惩罚项才不偏科
适应度函数是 GA 唯一的信息来源,它设错了,后面算子再精妙也没用。有两个原则必须坚持:第一,不能用训练集损失做适应度,否则进化就是过拟合大赛;第二,不能只用验证集 loss,还要加一个网络规模的惩罚项。
验证集 loss 作主项很好理解。训练误差小但泛化差的个体,在验证集上一测就露馅。惩罚项则针对另一种情况:两个个体验证集 loss 几乎一样,但一个网络有 15 层一个只有 3 层,工程上显然选小的。惩罚项简单地写成val_mse + alpha * (h1 + h2),alpha 取 0.001 量级,只用来打破平局,不应主导适应度。这个细节很多人忽略,导致 GA 总在结构越来越大的方向漂。
还有一个必须处理的工程细节:评估噪声。同样是给定一组超参,随机初始化不同、mini-batch 顺序不同,验证集 loss 会有波动。GA 的锦标赛选择不是对个体排序后精确淘汰,它只保证“更好的个体有更高概率留下”,所以小噪声可以容忍。但噪声如果大到掩盖真实差异,就必须在评估函数里固定随机种子。这个点在第五章展开,它是新手最容易翻车的隐蔽坑。
3.3 选择、交叉、变异算子搭配:锦标赛 + SBX + 多项式变异
算子的搭配直接影响收敛速度和多样性。选择我用锦标赛选择,k 取 2。每个个体从种群中随机抽 2 个,留下适应度更好的那个。锦标赛选择的优点是不需要把全部个体排序,选择压力通过 k 调节,k 越大淘汰越狠。k=2 是平衡点,k=3 会加快收敛但容易早熟。
交叉算子建议用 SBX(模拟二进制交叉)。SBX 的核心是一个 beta 参数,它控制子代和父代的相似程度。eta_c越大,beta 分布越集中在 1 附近,子代越像父代;eta_c越小,子代越容易离父母远。我一般取eta_c=15,这是 NSGA-II 里的常用值,在 ANN 超参搜索场景里表现稳定。SBX 的优点是它特别适合连续变量,能保证子代落在父母围成的区间附近,而不是像均匀交叉那样盲目跳跃。
变异算子用多项式变异,eta_m=20。它和 SBX 共享相似的分布思想:以小概率对一个维度做一个扰动,扰动幅度受边界约束。变异率0.1 ~ 0.2在这个场景里比较合理,低于 0.05 种群多样性基本靠交叉撑,很危险;高于 0.3 又会把好个体打成碎片。最后加上精英保留策略,每代把最优的 1~2 个个体原样复制到下一代,保证历史最优不会丢。
| 参数 | 推荐值 | 作用 | 调低影响 | 调高影响 |
|---|---|---|---|---|
| 锦标赛 k | 2 | 控制选择压力 | 收敛慢 | 早熟 |
| SBX eta_c | 15 | 子代贴近父代程度 | 子代发散 | 子代太像父代 |
| 变异率 | 0.15 | 维持多样性 | 容易早熟 | 好个体被破坏 |
| 多项式变异 eta_m | 20 | 变异步长分布 | 变异步长大 | 变异步长小 |
| 精英保留数 | 2 | 保证不丢历史最优 | 最优可能回丢 | 多样性被压缩 |
4. 遗传算法 Python 代码详解:用 GA 自动搜 ANN 超参的最小可跑通流程
4.1 固定网络结构,把适应度函数写成验证集 MSE
这段代码用 sklearn 的加州房价数据集做演示,只取前 6000 条样本,训练 5000 条、验证 1000 条。train_test_split 固定 random_state,是为了让 GA 评估的每一代都面对同一批数据,否则换数据本身就成了干扰变量。
import numpy as np import torch import torch.nn as nn from sklearn.datasets import fetch_california_housing from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler # ---------- 数据准备 ---------- X, y = fetch_california_housing(return_X_y=True) X_train, X_val, y_train, y_val = train_test_split( X[:6000], y[:6000], test_size=1000, random_state=42 ) scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_val = scaler.transform(X_val) X_train = torch.tensor(X_train, dtype=torch.float32) y_train = torch.tensor(y_train, dtype=torch.float32) X_val = torch.tensor(X_val, dtype=torch.float32) y_val = torch.tensor(y_val, dtype=torch.float32) # ---------- 个体解码:把 [0,1]^4 映射成真实超参 ---------- def decode(ind): # ind 是 4 维向量,每位都在 [0,1] 区间 lr = 10 ** (-5 + ind[0] * 4) # 1e-5 ~ 1e-1,对数均匀 h1 = int(round(5 + ind[1] * 10)) # 5 ~ 15 个节点 h2 = int(round(ind[2] * 8)) # 0 ~ 8,0 表示只有一个隐层 wd = 10 ** (-5 + ind[3] * 4) # 1e-5 ~ 1e-1 return lr, h1, h2, wd # ---------- 适应度:训练一个 MLP 并返回验证集 MSE ---------- def evaluate(ind): torch.manual_seed(123) # 固定种子,让同一基因型的评估可复现 lr, h1, h2, wd = decode(ind) if h2 > 0: model = nn.Sequential( nn.Linear(X_train.shape[1], h1), nn.ReLU(), nn.Linear(h1, h2), nn.ReLU(), nn.Linear(h2, 1) ) else: model = nn.Sequential( nn.Linear(X_train.shape[1], h1), nn.ReLU(), nn.Linear(h1, 1) ) opt = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=wd) loss_fn = nn.MSELoss() for epoch in range(20): model.train() perm = torch.randperm(len(X_train)) for i in range(0, len(X_train), 64): idx = perm[i:i+64] opt.zero_grad() pred = model(X_train[idx]).squeeze() loss = loss_fn(pred, y_train[idx]) loss.backward() opt.step() model.eval() with torch.no_grad(): val_pred = model(X_val).squeeze() return loss_fn(val_pred, y_val).item()decode 里的对数映射是关键。学习率搜索范围设为 1e-5 到 1e-1,如果直接线性搜索,1e-5 到 1e-4 那一整段在数值上几乎被压缩成一条线,而这段往往是有效区域。对数映射把数量级均匀铺开,GA 才有机会在小学习率区间做精细搜索。h2 的round(ind[2] * 8)允许出现 0,这时模型只用单隐层,等于 GA 自己在比较单隐层和双隐层的结构差异。
import random POP_SIZE = 12 # 种群规模,越小越快但搜索能力差 GENERATIONS = 15 # 迭代代数,演示用,正式场景至少 25 CROSS_PROB = 0.9 # 交叉率,信息交换主要靠它 MUT_PROB = 0.15 # 变异率,过低容易早熟 ETA_C = 15 # SBX 分布指数 ETA_M = 20 # 多项式变异分布指数 ELITISM = 2 # 每代保留的最优个体数 def tournament_select(pop, fitness, k=2): idx = random.sample(range(len(pop)), k) best = min(idx, key=lambda i: fitness[i]) return pop[best] def sbx_crossover(p1, p2): c1, c2 = p1.copy(), p2.copy() if random.random() > CROSS_PROB: return c1, c2 for i in range(len(p1)): if abs(p1[i] - p2[i]) < 1e-10: continue u = random.random() if u <= 0.5: beta = (2 * u) ** (1 / (ETA_C + 1)) else: beta = (1 / (2 * (1 - u))) ** (1 / (ETA_C + 1)) c1[i] = 0.5 * ((1 + beta) * p1[i] + (1 - beta) * p2[i]) c2[i] = 0.5 * ((1 - beta) * p1[i] + (1 + beta) * p2[i]) c1[i] = min(max(c1[i], 0.0), 1.0) c2[i] = min(max(c2[i], 0.0), 1.0) return c1, c2 def polynomial_mutation(ind): ind = ind.copy() for i in range(len(ind)): if random.random() > MUT_PROB: continue u = random.random() if u < 0.5: delta = (2 * u) ** (1 / (ETA_M + 1)) - 1 else: delta = 1 - (2 * (1 - u)) ** (1 / (ETA_M + 1)) ind[i] += delta ind[i] = min(max(ind[i], 0.0), 1.0) return ind pop = np.random.rand(POP_SIZE, 4) fitness = [evaluate(ind) for ind in pop] for gen in range(GENERATIONS): elite_idx = np.argsort(fitness)[:ELITISM] elites = [pop[i].copy() for i in elite_idx] new_pop = [ind.copy() for ind in elites] while len(new_pop) < POP_SIZE: p1 = tournament_select(pop, fitness) p2 = tournament_select(pop, fitness) c1, c2 = sbx_crossover(p1, p2) c1 = polynomial_mutation(c1) c2 = polynomial_mutation(c2) new_pop.append(c1) new_pop.append(c2) pop = np.array(new_pop[:POP_SIZE]) fitness = [evaluate(ind) for ind in pop] best_pos = int(np.argmin(fitness)) print(f"gen {gen+1:02d} | best val_mse {fitness[best_pos]:.5f} | " f"lr={10**(-5+pop[best_pos][0]*4):.2e} | " f"h1={5+round(pop[best_pos][1]*10)} | " f"h2={round(pop[best_pos][2]*8)} | " f"wd={10**(-5+pop[best_pos][3]*4):.2e}")这个主循环的套路是标准的两件事:先用精英保留保住历史最优,再用锦标赛选择从上一代里挑父母。注意elites被拷贝后直接放进新种群,没有被交叉和变异污染。之后循环生成子代直到填满POP_SIZE,每个子代都经历一次交叉加一次变异。最后用new_pop[:POP_SIZE]截断是为了防止 while 循环多生成一个个体导致种群规模膨胀。
SBX 的 beta 公式来源是模拟二进制交叉,它让两个子代关于父母对称分布,而且子代与父代的差的分布由 ETA_C 控制。这个分布的特性是:子代大概率落在父母之间,但也有一定概率远超父母范围,刚好承担了探索的职责。多项式变异里的 delta 在接近 0 或接近 1 时都有不错的扰动概率,这是它名字里“多项式”的原因,分布在边界处不会塌缩。
4.3 参数第一次怎么设:种群、代数、交叉率、变异率
第一次跑通的最保守参数就是代码块里的这组:POP_SIZE=12, GENERATIONS=15, CROSS_PROB=0.9, MUT_PROB=0.15, ELITISM=2。12 个个体评估一轮大约等于完整训练 12 个网络,15 轮总共 180 次训练,在 5000 条样本、20 个 epoch 的前提下,CPU 上大约二三十分钟能跑完。
真正值得花时间观察的是每一行 print 输出的 best val_mse。它应该在前几代明显下降,然后越来越慢。如果三代之后完全没有变化,先怀疑早熟而不是直接改算子。想更快,可以降 epoch 到 10,或者把训练样本裁到 3000 条,评估一次压到秒级后再放大种群。想搜得更细,把代数提上去比加种群更划算,因为代数每加 1 只多一次种群评估,而种群规模加 1 每代都要重新评估。
5. 常见问题与避坑:GA 训 ANN 的五个翻车现场
5.1 适应度评估太慢:先砍数据量和 epoch,再谈要不要并行
现象:跑了几个小时,GA 只进到四五代,大家开始怀疑人生。
原因:每个个体都要完整训练一遍神经网络,适应度评估是整个 GA 流程的绝对瓶颈,一个 epoch 里有十几万样本时,一次评估可能要几十秒到几分钟,12 个个体就是几个小时。
解决:先确认你的评估到底慢在哪。最直接的方案是把训练样本裁到可接受范围内,验证集保留 1000 条左右就行;epoch 减到 10 或 15,GA 要的是相对优劣,不是绝对精度。第二步才是并行化,用 multiprocessing 把同一代的个体分给多个 worker。注意并行粒度按个体切,不要按 epoch 切,因为网络本身没法轻易跨 worker 同步。血泪经验:并行之前先用单进程把单次评估时间压到 2 秒以内,否则并行只会把机器核心全占满然后等更久。
5.2 二进制编码的海明悬崖在超参搜索里更隐蔽
现象:GA 用二进制编码,学习率怎么搜都在几个固定数值附近打转,收敛曲线像台阶。
原因:相邻实数在二进制表示上差距大,交叉后子代直接跳到搜索空间另一端,小范围精细搜索根本做不到。
解决:换成实数编码,并且每个维度归一化到 [0, 1]。如果项目硬性要求二进制编码,那至少换成格雷码,它能保证相邻整数的编码只有一位不同。但对神经网络超参这种连续+整数混合场景,实数编码是更省事的正路。
5.3 交叉变异后越界:边界修复不是可选项
现象:某代出现学习率为负数的个体,训练直接报错,整个评估流程崩溃。
原因:SBX 和多项式变异的扰动公式在父母边界附近时,可能把子代推出上下界。如果你的 search space 是 [1e-5, 1e-1],变异算子并不知道这个物理含义。
解决:每次交叉和变异后,对每个维度做clip到 [0, 1] 或真实边界内。代码里的min(max(value, 0.0), 1.0)就是干这个的。别为了省一行代码去掉它,边界修复不是可选项,它同时保证了进化解永远落在合法区域。曾经有人因为没做 clip,让网络隐层节点数变成 0 以下,训练期间反复 NaN 还以为是学习率问题。
5.4 早熟收敛:多样性消失得比想象快
现象:第 5 代之后 best val_mse 几乎不动,打印出来的最优超参也完全一样。
原因:种群太小,锦标赛选择压力大,再加上变异率低,群体很快被少数强势个体占领。GA 的进化本质是搜索,如果群体像克隆,交叉无效,变异又太小,就没法产生新东西。
解决:先把种群从 12 提到 20 以上,把变异率从 0.1 提到 0.15 或 0.2。另一种更硬核的补救是每 5 代随机重新初始化 2~3 个个体加入种群,给进化注入外来基因。精英保留一般留 1~2 个就够了,留太多等于亲手加速早熟。
5.5 评估随机性在干扰选择:同一个体两次分数不一样
现象:GA 选了某个个体当精英,把它 reuse 到下一代,结果同一组超参重新训练后验证 MSE 比上一代打印的值差了一截。
原因:没固定随机种子。每次训练时权重初始化、mini-batch 抽样都在变,验证集 MSE 自然是随机变量。如果这个噪声比两个个体间的真实差距还大,锦标赛选择就是掷骰子。
解决:在 evaluate 函数开头加torch.manual_seed(...),让同一基因型多次评估得到同一个分数。进阶做法是按个体内容生成一个哈希值当种子,这样不同个体有不同的初始化,但同一个体可复现。要注意固定种子后,不同个体之间的比较是公平的,因为大家面对的是相同的数据顺序和初始权重来源。
6. 结果验证与进阶技巧:先用正交实验框范围,再看单代收敛曲线
6.1 一张正交实验表先框定超参范围,避免 GA 在无效区间白跑
GA 不是万能的搜索引擎,搜索范围如果框得离谱,进化解再好也是矮子里拔高个。我一般先把四个超参各取三个水平,跑一张 L9 正交实验表,用极差分析看哪个水平方向 MSE 更低,然后把这个方向收窄后交给 GA。
| 超参 | 低水平 | 中水平 | 高水平 |
|---|---|---|---|
| 学习率 | 1e-4 | 1e-3 | 1e-2 |
| 隐层1节点 | 8 | 16 | 32 |
| 隐层2节点 | 0 | 4 | 8 |
| weight decay | 0 | 1e-4 | 1e-2 |
比如发现学习率在 1e-3 附近明显更好,就把 GA 的搜索下界从 1e-5 提到 1e-4,上界从 1e-1 压到 5e-2,等于把 80% 的无效搜索空间剪掉。这一步花的时间远小于 GA 后期多跑几十代,属于高杠杆操作。
6.2 看单代最优适应度曲线,并用三次重复实验对抗运气
GA 是随机算法,单次跑出的历史最优值没有任何说服力。我判断一个 GA 配置是否有效的标准是:单代最优适应度曲线应该快速下降然后渐趋平缓,而不是上一代 0.31、下一代 0.29、再下一代又弹回 0.34 这样上下跳。
流程上,同一套参数重复跑 3 次,取 3 次最优值的中位数,再和随机搜索用相同的评估预算比较。随机搜索 180 次、GA 180 次,如果 GA 的中位数没有明显优势,那问题多半不出在算子而在于预处理、网络结构或数据量。
| 方法 | 总评估次数 | 最优验证 MSE(3 次中位数,示意) |
|---|---|---|
| 随机搜索 | 180 | 0.31 |
| GA 参数 A | 180 | 0.26 |
| GA 参数 B(仅调变异率) | 180 | 0.24 |
只看中位数还不够,记录每次 GA 跑完后的最优个体,看它是否落在同一片区域。如果三次进化解的学习率都在 1e-3 附近、隐层节点都在 12~16,说明搜索收敛到了可信的盆地;如果三次完全散开,说明适应度函数噪声仍然太大,回到 5.5 想办法压缩评估方差。我最早跑这套的时候,光顾着调算子,没先确认超参搜索范围,结果一半个体都在无效区间打转,后来老老实实先做正交实验才看到收敛曲线正常下降。先框范围再进化,这一步省下的时间比任何 GA 调参技巧都多。希望帮到你。
本文还有配套的精品资源,点击获取