ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

数据驱动进化优化:代理模型加速昂贵黑盒优化实战

数据驱动进化优化:代理模型加速昂贵黑盒优化实战

简介:《数据驱动进化优化》是Springer《计算智能研究》系列第975卷的正式出版物,由金耀初、王晗丁、孙朝丽合著,面向进化计算、机器学习与数据科学方向的研究人员及工业实践者,帮助读者应对复杂优化问题中实验成本高、目标冲突多、数据稀缺等挑战。资源为1个PDF文件,压缩包约24.33MB,内容完整涵盖代理模型辅助优化、多目标优化、知识迁移与深度神经架构搜索等前沿主题,并配有算法源码与真实案例。全书从进化计算、机器学习与数据科学三者的融合出发,系统讲解如何借助少量试验数据预测复杂系统行为、平衡相互竞争的多目标、跨领域复用已有知识,以及用进化算法自动搜索高效神经网络结构。目前已有1717人学习下载,适合希望将最新进化优化算法落地于理论研究或工业应用的中高级读者,兼具学术参考与工程实践价值。

1. 数据驱动进化优化:当种群不再盲目试错,收敛速度能快多少

如果你做过连续参数优化,大概率经历过这种场面:目标函数评估一次要跑几十秒仿真,遗传算法种群规模 100、迭代 500 代,算了一整夜还在原地打转。问题不在算法本身,而在于每一代都在从零开始试错,前面积累的评估数据被白白丢掉。数据驱动进化优化要解决的就是这件事——用已经采样到的 (x, f(x)) 去训练一个代理模型,让算法在真正调用昂贵评估之前,先在模型上把候选解筛一遍。它适合三类人:手里有昂贵黑盒函数(仿真、实验、A/B 测试)的工程师,想把评估次数从万级压到千级的算法同学,以及正在做超参搜索但嫌贝叶斯优化太慢的调参党。核心思路一句话:代理模型负责“猜”,进化算法负责“探”,两者交替推进。

2. 代理模型怎么选:从 Kriging 到轻量集成

2.1 为什么默认选高斯过程而不是神经网络

在样本量小于 500 的场景下,高斯过程(GP,也叫 Kriging)几乎是默认答案。原因不是它精度最高,而是它自带方差估计——每预测一个点,除了给出均值 μ(x),还给出不确定度 σ(x)。这个 σ 直接决定了下一代该往“已知好区域”挖,还是往“没探过的地方”探,也就是后面要讲的采集函数。神经网络要拿到同等可靠的不确定度,得额外做 MC Dropout 或深度集成,工程成本高出一截。

代价是 GP 的训练复杂度是 O(n³),样本超过两三千就明显吃力。所以常见做法是:前期样本少用 GP,样本积累到一定量后切换到随机森林或梯度提升树做代理,牺牲不确定度换速度。我一般会在 300 个样本处设一个切换阈值,实测下来这个位置比较稳。

2.2 一个能直接跑的最小代理模型封装

下面这段用 sklearn 的 GaussianProcessRegressor 包一个代理模型,重点是同时返回均值和标准差,供采集函数使用。

import numpy as np from sklearn.gaussian_process import GaussianProcessRegressor from sklearn.gaussian_process.kernels import Matern, ConstantKernel class Surrogate: def __init__(self, n_restarts=5): # Matern 5/2 对连续优化问题比 RBF 更稳,不易过平滑 kernel = ConstantKernel(1.0, (1e-3, 1e3)) * Matern( length_scale=1.0, length_scale_bounds=(1e-2, 1e2), nu=2.5) self.gp = GaussianProcessRegressor( kernel=kernel, n_restarts_optimizer=n_restarts, normalize_y=True, alpha=1e-6) # alpha 是观测噪声,仿真确定性强就设小 def fit(self, X, y): self.gp.fit(X, y) def predict(self, X): mu, sigma = self.gp.predict(X, return_std=True) return mu, sigma

逻辑说明:normalize_y=True让 GP 内部对输出做标准化,避免目标函数量纲差异导致核参数拟合失败,这是很多人第一次用 GP 时结果离谱的元凶。alpha是噪声项,如果你的评估是确定性仿真(同样输入永远同样输出),设 1e-6 到 1e-10;如果评估带随机性(比如蒙特卡洛仿真),要设成实际观测噪声的方差量级。n_restarts_optimizer控制核超参的随机重启次数,5 次是精度和耗时的折中,样本少时可以加到 10。

2.3 采集函数:EI 和 UCB 该用哪个

代理模型给出 μ 和 σ 后,需要一个标量指标来决定下一个评估点。最常用两个:

采集函数公式核心适用场景关键参数
EI(期望改进)对改进量的期望积分目标是最小化且想要稳定收敛当前最优值 y_best
UCB(置信上界)μ + κ·σ想显式控制探索强度κ 通常取 2~3

EI 的优点是无需调参,缺点是当 σ 普遍很小时会过度保守。UCB 的 κ 是探索旋钮:κ 大偏探索,κ 小偏利用。我的经验是前期用 κ=3 快速铺开,后期降到 κ=0.5 精修。下面给一个 EI 的实现,注意它假设是最小化问题。

from scipy.stats import norm def expected_improvement(mu, sigma, y_best, xi=0.01): # xi 是探索余量,防止 sigma 趋零时 EI 全为零 imp = y_best - mu - xi Z = imp / (sigma + 1e-12) ei = imp * norm.cdf(Z) + sigma * norm.pdf(Z) ei[sigma < 1e-12] = 0.0 return ei

参数说明:y_best是当前已评估样本里的最小值;xi一般取 0.01 倍的目标函数量级,太小会导致早熟收敛,太大则退化成随机搜索。sigma + 1e-12是防止除零,别省这个保护。

3. 把代理模型接进进化循环:三种主流架构

3.1 代理辅助进化:只对候选做预筛选

最省事、改动最小的架构。每一代遗传算法照常做交叉变异生成子代,但在真正评估前,先用代理模型预测所有子代的 μ,只挑预测最好的前 k 个去调用真实评估,其余直接丢弃或用 μ 填充。这样每代真实评估次数从种群规模降到 k。

关键参数是 k。k 太小(比如 5)会导致种群多样性崩塌,几代之后全是近亲;k 太大就失去省评估的意义。我一般设 k = max(5, 种群规模 × 0.1)。另外要注意:被丢弃的个体不能简单扔掉,最好把它们的 (x, μ) 也存进代理模型的训练集,相当于用模型预测做数据增强,但要标记为“伪样本”,训练时给更低权重。

3.2 代理驱动进化:模型直接当适应度函数

当真实评估极其昂贵(一次几十分钟),可以整个进化过程都在代理模型上跑,只在最后把最优解拿去真实评估。这种做法风险高,因为模型一旦在某区域外推失真,算法会兴高采烈地收敛到一个虚假最优点。常见补救是每隔若干代做一次“模型校验”:挑几个模型最自信的点去真实评估,如果预测和真实值偏差超过阈值,就把这些点加回训练集并重新拟合。

def surrogate_driven_loop(surrogate, real_eval, bounds, n_gen=50, check_every=10): X = latin_hypercube_sample(bounds, 20) # 初始采样 y = np.array([real_eval(x) for x in X]) surrogate.fit(X, y) for gen in range(n_gen): # 在代理模型上跑完整 GA pop = run_ga_on_surrogate(surrogate, bounds) if gen % check_every == 0: # 校验:取代理预测最优的 3 个点做真实评估 top3 = pop[np.argsort([surrogate.predict(p[None])[0][0] for p in pop])[:3]] for x in top3: X = np.vstack([X, x]) y = np.append(y, real_eval(x)) surrogate.fit(X, y) # 用真实数据纠正模型 return X[np.argmin(y)], y.min()

逻辑说明:latin_hypercube_sample做初始空间填充,20 个点是 GP 能稳定拟合的下限。check_every=10意味着每 10 代花 3 次真实评估做纠偏,整体真实评估预算 = 20 + 3×(n_gen/10),比纯进化省一个数量级。注意run_ga_on_surrogate里适应度直接用 μ,不涉及真实调用,所以可以放心把种群和代数开大。

3.3 多保真度:低保真数据先铺路

如果你的问题有“粗网格仿真”和“细网格仿真”两档,那就用多保真度代理。低保真评估便宜,先大量采样训练一个低保真代理;再用少量高保真样本学一个“低保真到高保真”的差值模型。最终预测 = 低保真预测 + 差值修正。这种做法在流体、结构仿真里很常见,能把高保真评估次数再压一半。代价是你得有两套评估流程,工程上多一层维护。

4. 避坑与排查:五个让方案翻车的细节

4.1 现象:代理模型 R² 很高,但优化结果很差

原因:训练集和测试集来自同一批采样,模型只是在插值,没有外推能力。进化算法偏偏喜欢往没探过的边界跑,那里模型全是幻觉。

解决:每次拟合后留 20% 样本做留出验证,并且专门检查边界区域的预测方差。如果边界 σ 异常小(说明模型过度自信),把核的 length_scale 上界调大,强制模型更“谦虚”。

4.2 现象:种群多样性几代内归零

原因:预筛选 k 太小,或者采集函数过度利用(EI 的 xi 太小、UCB 的 κ 太小),所有子代都挤在同一个峰附近。

解决:给采集函数加一个最小探索量,EI 的 xi 不低于 0.01×目标量级;同时每代强制保留 10% 随机个体不做筛选,直接真实评估。这个“随机注入”是血泪经验,能救命。

4.3 现象:GP 拟合报错或预测全为常数

原因:输入维度太高(超过 20 维)或样本里有重复点导致协方差矩阵奇异。

解决:高维先做降维(PCA 或直接砍掉不敏感维度);重复点加 alpha 噪声项,或者去重。维度超过 30 时别硬上 GP,换随机森林。

4.4 现象:真实评估次数没省下来,反而更多了

原因:模型校验太频繁,或者初始采样点太多。有人一上来就采 200 个初始点,光初始化就把预算花完了。

解决:初始采样点控制在 10×维度 以内,且不超过 50。校验频率按预算倒推:总预算 500 次,校验占 10% 即 50 次,分摊到 50 代就是每代 1 次。

4.5 现象:结果每次跑都不一样,无法复现

原因:GP 的核超参优化有随机重启,GA 有随机种子,两处随机叠加。

解决:固定 numpy 和 random 的种子,并且把 GP 的random_state也固定。注意固定种子后 GP 的n_restarts_optimizer仍会带来微小差异,追求严格复现就设成 0,代价是核参数可能不是最优。

5. 进阶技巧:用采集函数的历史轨迹判断该不该停

跑了这么多轮,我最想分享的不是某个参数,而是一个判断“什么时候该停”的习惯。数据驱动进化优化最大的诱惑是——你总觉得再跑几代会更好,但真实评估预算在燃烧。我的做法是记录每一代采集函数的最大值(EI 的峰值或 UCB 的峰值),画一条曲线。当这条曲线连续 5 代下降且幅度小于初始值的 1%,说明代理模型已经对当前区域“吃透”了,再跑就是浪费真实评估。

def should_stop(ei_history, patience=5, tol=0.01): if len(ei_history) < patience + 1: return False recent = ei_history[-patience:] base = ei_history[0] # 连续 patience 代峰值下降且总降幅小于 tol declining = all(recent[i] >= recent[i+1] for i in range(len(recent)-1)) small_drop = (max(recent) - min(recent)) < tol * abs(base) return declining and small_drop

这个判据比“固定代数”靠谱得多,因为它直接反映了模型还能不能提供有效信息。另一个技巧是把每代真实评估的最优点单独存一份,最后对比代理模型预测的最优和真实最优的差距——如果差距稳定在 5% 以内,说明代理可信;如果忽大忽小,说明模型在某些区域不可靠,得回去检查采样覆盖。

我自己的习惯是:每次开新问题,先用 30 个初始点拟合 GP,手动看一遍预测均值和真实值的散点图,确认没有系统性偏移再让循环自动跑。这个 5 分钟的手动检查,帮我省过至少三次整夜的无效计算。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进