BayesianOptimization 序列域缩减(Sequential Domain Reduction)完全指南:SequentialDomainReductionTransformer 原理、参数与实战
BayesianOptimization 序列域缩减(Sequential Domain Reduction)完全指南:SequentialDomainReductionTransformer 原理、参数与实战
序列域缩减(Sequential Domain Reduction, SDR)是 BayesianOptimization 仓库内置的一种搜索空间动态调整机制:在贝叶斯优化迭代过程中,每次迭代都会围绕当前最优解对参数的取值范围做"平移(pan)+ 缩放(zoom)",从而把高斯过程代理模型的搜索资源集中到最有希望的区域,显著加快收敛。本文以 docsrc/reference/domain_reduction.rst 为核心,结合 bayes_opt/domain_reduction.py 源码、examples/domain_reduction.ipynb 示例与 tests/test_seq_domain_red.py 测试,系统讲解该组件的设计动机、核心数学流程、三个可调参数、minimum_window 边界保护机制,以及完整的接入与复现步骤,读完即可在代价昂贵或振荡严重的优化任务中直接使用这一加速方案。
一、为什么需要序列域缩减
在常规贝叶斯优化流程中,整个优化过程始终在用户最初给定的全局边界(pbounds)内搜索。这在很多场景下没有问题,但在两类场景中会明显拖慢收敛:
- 代价函数计算昂贵:每一次函数求值成本很高(例如仿真模拟、复杂数值计算、需要调用外部服务的评估),我们希望把有限的迭代次数尽量花在最有希望的局部区域,而不是在整个全局空间内"撒网"。
- 优化过程振荡严重:当目标函数高度非线性、多峰时,优化器会在不同区域之间来回试探,难以稳定收敛到最优点附近。
SDR 的思路很直接:在每一轮真正的迭代(非初始化采样阶段)结束后,根据当前已找到的最优位置,把下一轮的搜索窗口重新定位并收缩。正如 examples/domain_reduction.ipynb 背景一节所述,该方法的优势"通常在代价函数特别昂贵,或优化过程振荡剧烈时最能体现"。
该实现建立在 Stander, N. 与 Craig, K. 于 2002 年发表的论文 "On the robustness of a simple domain reduction scheme for simulation-based optimization" 基础之上,仓库在 bayes_opt/domain_reduction.py 的模块文档字符串中明确标注了这一出处。因此,本节与后续公式均可在该论文与源码之间相互印证。
核心概念:pan 与 zoom
SDR 每轮迭代的核心操作只有两个,二者同时应用、逐轮更新搜索空间:
- Pan(平移):把感兴趣的区域(region of interest)重新居中到当前已找到的最优点附近。也就是说,窗口中心跟着当前最优解走。
- Zoom(缩放):在平移的同时收缩区域范围,把下一轮的搜索限定在更小的窗口内。
上图为仓库 docsrc/static/sdr.png 中随 notebook 提供的示意图:(a) 展示搜索子区域随采样点由 x^(0) 更新到 x^(1) 整体平移;(b) 展示采样点不变时搜索域以同一中心向内缩小;(c) 展示实际迭代中"平移 + 缩小"同时发生的窗口变化,其中 r1^(0)、r1^(1) 分别表示两轮迭代的搜索范围。
二、快速上手:在 BayesianOptimization 中启用 SDR
使用 SDR 只需三步:导入 SequentialDomainReductionTransformer、构造变换器实例、将其作为 bounds_transformer 参数传入 BayesianOptimization。完整可运行示例见 examples/domain_reduction.ipynb。
import numpy as np
from bayes_opt import BayesianOptimization
from bayes_opt import SequentialDomainReductionTransformer
def ackley(**kwargs):
x = np.fromiter(kwargs.values(), dtype=float)
arg1 = -0.2 * np.sqrt(0.5 * (x[0] ** 2 + x[1] ** 2))
arg2 = 0.5 * (np.cos(2. * np.pi * x[0]) + np.cos(2. * np.pi * x[1]))
return -1.0 * (-20. * np.exp(arg1) - np.exp(arg2) + 20. + np.e)
pbounds = {'x': (-5, 5), 'y': (-5, 5)}
# 1) 构造 SDR 变换器(可选参数见下文)
bounds_transformer = SequentialDomainReductionTransformer(minimum_window=0.5)
# 2) 开启 SDR 的优化器
mutating_optimizer = BayesianOptimization(
f=ackley,
pbounds=pbounds,
verbose=0,
random_state=1,
bounds_transformer=bounds_transformer,
)
# 3) 正常运行 maximize:初始化采样 + 迭代优化
mutating_optimizer.maximize(init_points=2, n_iter=50)
bounds_transformer 参数在 bayes_opt/bayesian_optimization.py 中定义为 DomainTransformer | None,默认值为 None(即不启用任何边界变换)。构造 BayesianOptimization 时,若传入该参数,会先经过类型检查(必须是 DomainTransformer 的实例),然后立即调用 initialize 完成内部状态初始化,相关代码见 bayes_opt/bayesian_optimization.py。
与标准优化器的对照实验
为了直观看到 SDR 的效果,官方 notebook 构造了两个完全相同的优化问题(相同的 f、pbounds、verbose、random_state),唯一区别是其中一个传入了 bounds_transformer:
# 对照组:不启用 SDR
standard_optimizer = BayesianOptimization(
f=ackley,
pbounds=pbounds,
verbose=0,
random_state=1,
)
standard_optimizer.maximize(init_points=2, n_iter=50)
运行后,将两者的目标函数历史画在同一张图上:
import matplotlib.pyplot as plt
plt.plot(mutating_optimizer.space.target, label='Mutated Optimizer')
plt.plot(standard_optimizer.space.target, label='Standard Optimizer')
plt.legend()
notebook 的结论是:SDR 很快就把搜索范围收缩到了最优点附近,目标值收敛速度明显快于不做缩减的对照组。注意两组都使用 random_state=1,保证初始采样与随机过程完全一致,因此收敛差异只能归因于搜索边界的变化。
观察窗口逐轮收缩
SequentialDomainReductionTransformer 会在内部记录每一次变换后的边界,保存在 bounds 属性中(初始全局边界为第一个元素,之后每一轮追加一个新窗口)。官方 notebook 用它画出了变量 x 上下界的逐轮变化:
# 注:变换器只在真正的迭代阶段生效,不在 init_points 初始采样阶段,
# 因此绘图时 x 轴要整体平移 init_points 的个数。
x_min_bound = [b[0][0] for b in bounds_transformer.bounds]
x_max_bound = [b[0][1] for b in bounds_transformer.bounds]
x = [x[0] for x in mutating_optimizer.space.params]
bounds_transformers_iteration = list(range(2, len(x)))
plt.plot(bounds_transformers_iteration, x_min_bound[1:], label='x lower bound')
plt.plot(bounds_transformers_iteration, x_max_bound[1:], label='x upper bound')
plt.plot(x[1:], label='x')
plt.legend()
三、参数详解:gamma_osc、gamma_pan、eta 与 minimum_window
SequentialDomainReductionTransformer 的构造签名如下(见 bayes_opt/domain_reduction.py):
SequentialDomainReductionTransformer(
parameters: Iterable[str] | None = None,
gamma_osc: float = 0.7,
gamma_pan: float = 1.0,
eta: float = 0.9,
minimum_window: float | Sequence[float] | Mapping[str, float] = 0.0,
)
gamma_osc:振荡抑制系数(默认 0.7)
- 作用:用于缩放(通常是抑制)振荡的参数。当优化过程在最优解附近来回摆动(前后两次最优位置方向相反)时,窗口的收缩会相对保守,避免被振荡误导。
- 典型取值:论文建议区间约为
[0.5, 0.7],仓库默认0.7。
gamma_pan:平移系数(默认 1.0)
- 作用:用于缩放平移量的参数,典型值为
1.0(即不做额外缩放,让窗口中心完全跟随当前最优解)。 - 默认值:
1.0。
eta:缩放(zoom)参数(默认 0.9)
- 作用:控制窗口收缩的基准速率。
eta越接近 1,窗口收缩越慢(每次只缩小一点点);eta越小,窗口收缩越快,但也更容易在未充分探索时过早陷入局部区域。 - 默认值:
0.9。上述三个参数的取值说明均可在 examples/domain_reduction.ipynb 的 Parameters 一节找到原文出处。
minimum_window:每个参数的最小窗口宽度(默认 0.0)
- 作用:为每个参数设置窗口宽度的下限,防止搜索域在连续收缩后被压缩到过小、无法继续精细搜索,甚至在数值上失去意义。
- 取值形式:
float:所有参数使用同一个最小值;np.ndarray/Sequence[float]:按参数顺序逐参数指定,长度必须与参数个数一致;Mapping<a href="https://link.gitcode.com/i/95ce5b7992c31f13677e56b4d78c8314" target="_blank">str, float](字典):按参数名指定,内部会依据target_space.keys的顺序自动重排(测试test_minimum_window_dict_ordering专门验证了字典顺序与pbounds一致,见 [tests/test_seq_domain_red.py)。
- 默认值:
0.0,即默认不强制限制窗口大小。
parameters 参数
- 可选地指定需要做域缩减的参数集合(字符串可迭代对象)。值得注意的是,源码在 bayes_opt/domain_reduction.py 处留有
TODO: Ensure that this is only applied to continuous parameters注释,提示"应只作用于连续参数"这一约束尚未强制实施;同时initialize中会检查所有参数都必须是FloatParameter,否则抛出ValueError("Domain reduction is only supported for all-FloatParameter optimization.")。换句话说,当前版本的 SDR 只支持全连续(Float)参数空间,混入整数等类型会直接报错,相关校验在 bayes_opt/domain_reduction.py,测试见 tests/test_seq_domain_red.py。
四、核心机制:窗口是怎么一步步收缩的
1. initialize:初始化内部状态
构造完成后,BayesianOptimization 会调用 initialize(target_space) 完成一次性的状态初始化(bayes_opt/domain_reduction.py),主要工作包括:
- 若
minimum_window传入的是字典,先按target_space.keys的顺序转成列表; - 校验所有参数均为
FloatParameter; - 保存原始全局边界
original_bounds(即pbounds),并初始化历史记录bounds = [original_bounds]; - 将
minimum_window展开成长度等于参数个数的数组(若传入标量则广播为等长列表); - 把当前最优位置初始化为每维边界的均值:
previous_optimal = current_optimal = mean(bounds, axis=1); - 计算初始窗口半径
r = bounds[:, 1] - bounds[:, 0](各维窗口宽度); - 按下方公式计算初始的
previous_d、current_d、c、c_hat、gamma、contraction_rate,并把初始r收缩一次; - 调用
_window_bounds_compatibility(original_bounds)校验全局边界宽度不小于对应minimum_window,否则抛出ValueError("Global bounds are not compatible with the minimum window size.")。
2. _update:每轮迭代更新收缩率与窗口中心
每一轮真正的迭代结束后,transform 会先调用 _update(bayes_opt/domain_reduction.py),核心更新逻辑如下:
- 把上一轮的
current_optimal、current_d归档为previous_*; - 用
target_space.max()["params"](当前已找到的最优参数)更新current_optimal; - 计算当前最优相对上一轮最优的归一化位移:
- 计算前后两轮位移的乘积
c = d_prev * d_cur,并取其带符号平方根c_hat = sign(c) * sqrt(|c|)。这里c_hat的符号很关键:当连续两轮朝同一方向移动(c > 0)时c_hat > 0,意味着趋势一致、可以更大胆地收缩;当方向反转(c < 0,即振荡)时c_hat < 0,收缩会变得保守; - 计算自适应系数:
即:趋势一致时系数偏向 gamma_pan,振荡时偏向 gamma_osc;
6. 计算收缩率并更新窗口半径:
从公式可见:位移越大,gamma 对收缩率的加权影响越大;当最优位置几乎不动(|d| ≈ 0)时,收缩率约等于 eta,此时窗口每轮按 eta 稳定收缩。
3. transform:生成并裁剪新边界
transform 是每轮迭代被 BayesianOptimization 调用的入口(bayes_opt/domain_reduction.py),逻辑如下:
- 调用
_update(target_space)更新内部状态; - 以当前最优为中心、当前半径
r构造新窗口:
new_bounds = np.array(
[self.current_optimal - 0.5 * self.r, self.current_optimal + 0.5 * self.r]
).T
- 调用
_trim(new_bounds, self.original_bounds)对窗口进行裁剪与合法化(见下一节); - 将新边界追加到
self.bounds历史记录中; - 用
_create_bounds(target_space.keys, new_bounds)把数组形式的边界转换为{参数名: [下界, 上界]}字典返回。
4. 调用时机:只在真正的迭代阶段生效
maximize 内部(bayes_opt/bayesian_optimization.py)有如下关键逻辑:
if self._bounds_transformer and iteration > 0:
# The bounds transformer should only modify the bounds after
# the init_points points (only for the true iterations)
self.set_bounds(self._bounds_transformer.transform(self._space))
即:初始采样阶段(init_points)不触发变换,只有从第 1 轮真正的迭代开始,每完成一次 probe 后才调用 transform 并更新边界。这也解释了 notebook 画边界收缩图时为什么要用 range(2, len(x)) 平移横轴。
5. 边界如何更新
BayesianOptimization.set_bounds(bayes_opt/bayesian_optimization.py)接收变换器返回的字典并写入搜索空间,后续的采样与高斯过程拟合都在新的窗口内进行。此外,序列化/反序列化(load)时也会记录并恢复 transformed_bounds,保证带 SDR 的优化器断点续跑后仍能沿用收缩后的边界(bayes_opt/bayesian_optimization.py)。
五、窗口安全网:_trim 与 minimum_window 的边界保护
窗口在平移 + 缩放过程中可能"跑出"全局边界,也可能被压缩到小于 minimum_window。_trim(bayes_opt/domain_reduction.py)负责把新窗口拉回合法范围,分两步:
第一步:裁剪到全局边界内
- 先对每个参数的窗口上下界排序(
np.sort),保证[下界, 上界]顺序正确; - 若下界小于全局下界,重置为全局下界;若上界大于全局上界,重置为全局上界(贴近全局边界时属于正常情况,不发警告);
- 若下界已大于全局上界(窗口整体跑到全局上界之外),则把下界重置为全局下界并发出
UserWarning("A parameter's lower bound is greater than the global upper bound...");上界小于全局下界时同理。这部分行为被 tests/test_seq_domain_red.py 中的test_trim_when_both_new_bounds_exceed_global_bounds用多组越界输入验证。
第二步:保证窗口不小于 minimum_window
若某维窗口宽度小于 minimum_window[i],则把差值(width_deficit)平均分摊到两侧扩展;如果某一侧空间不足(贴近全局边界),则把欠缺的量转移到空间更充足的另一侧。源码中用 expand_left、expand_right、expand_left_deficit、expand_right_deficit 以及最后的 adjust_left、adjust_right 完成这一"一侧不够、另一侧补齐"的再分配逻辑。
minimum_window 的合法性校验
initialize 阶段会调用 _window_bounds_compatibility(bayes_opt/domain_reduction.py):只要任一参数的全局边界宽度小于其 minimum_window,就抛出 ValueError。对应测试:
test_minimum_window_is_kept:验证所有窗口宽度最小值不小于minimum_window(tests/test_seq_domain_red.py);test_minimum_window_array_is_kept:验证逐参数数组形式的minimum_window被逐个保持(tests/test_seq_domain_red.py);test_exceeded_bounds:验证minimum_window大于全局边界时抛出ValueError(tests/test_seq_domain_red.py)。
六、完整调用链与约束小结
综合 bayes_opt/bayesian_optimization.py 与 bayes_opt/domain_reduction.py,SDR 在单轮迭代中的完整调用链为:
BayesianOptimization.__init__:类型检查bounds_transformer→initialize(target_space)(快照全局边界、初始化窗口与收缩状态);maximize:完成init_points个随机初始采样(不触发变换);- 每轮迭代:
probe评估新点 → 若iteration > 0,调用bounds_transformer.transform(self._space); transform:_update更新中心与收缩率 → 生成新窗口 →_trim裁剪到全局边界并保证最小宽度 → 返回{参数: 新边界};set_bounds(new_bounds):把新窗口写回搜索空间,供下一轮采样与 GP 拟合使用。
使用 SDR 前需要明确的约束与边界条件:
| 约束项 | 说明 | 依据 |
|---|---|---|
| 仅支持全 Float 参数 | 任一参数不是 FloatParameter 即抛 ValueError |
bayes_opt/domain_reduction.py、tests/test_seq_domain_red.py |
minimum_window 长度 |
数组/序列长度必须等于参数个数,否则抛 ValueError |
bayes_opt/domain_reduction.py |
minimum_window 与全局边界兼容 |
任一全局窗口宽度小于 minimum_window 即抛 ValueError |
bayes_opt/domain_reduction.py |
| 变换器类型 | 必须是 DomainTransformer 实例,否则抛 TypeError |
bayes_opt/bayesian_optimization.py |
| 变换时机 | 仅在真正的迭代阶段(iteration > 0)触发,初始采样阶段不变换 |
bayes_opt/bayesian_optimization.py |
七、实践建议与延伸阅读
- 何时该用 SDR:目标函数单次评估成本高、希望用更少迭代逼近最优点时;或优化过程在多个区域间振荡、难以收敛时。若目标函数便宜且全局探索更重要(例如存在多个等优峰,需要覆盖全部峰),则默认关闭 SDR 更稳妥。
- 如何调参:先从默认值(
gamma_osc=0.7、gamma_pan=1.0、eta=0.9)起步,观察窗口收缩速度;若感觉收敛过快、疑似过早陷入局部,可增大eta(如0.95)放慢收缩;若收敛过慢,可适度降低eta并配合更大的gamma_pan。minimum_window建议按业务可接受的最小搜索范围设置,避免窗口收缩到数值上失效。 - 如何验证效果:照抄上文"与标准优化器的对照实验"一节,保持
random_state一致、只切换bounds_transformer,对比space.target收敛曲线,并用bounds_transformer.bounds画出各维窗口的逐轮收缩轨迹。 - 进一步阅读:完整可运行示例见 examples/domain_reduction.ipynb;源码见 bayes_opt/domain_reduction.py;测试覆盖见 tests/test_seq_domain_red.py;
DomainTransformer抽象基类定义(initialize/transform等抽象方法)位于同一模块 bayes_opt/domain_reduction.py,如需自定义变换器(例如自研域缩减策略),只需继承该类并实现这三个抽象方法,再作为bounds_transformer传入即可。
