这是一个建立在贝叶斯推理和高斯过程基础上的受限全局优化包,它试图在尽可能少的迭代中找到一个未知函数的最大值。这种技术特别适用于高成本函数的优化,在这种情况下,探索和利用之间的平衡很重要。
贝叶斯优化的工作原理是构建一个函数的后验分布(高斯过程),它最能描述你要优化的函数。随着观测数据的增加,后验分布也在不断改进,算法也变得更加确定参数空间中哪些区域值得探索,哪些不值得探索,如下图所示。
当你一次又一次地迭代时,考虑到它对目标函数的了解,算法会平衡其探索和利用的需要。在每一步,高斯过程被拟合到已知的样本(以前探索过的点),后验分布,结合探索策略(如UCB(置信度上限),或EI(预期改进)),被用来决定下一个应该探索的点(见下图)。
这个过程旨在尽量减少找到接近最优组合的参数组合所需的步骤数。为此,该方法使用了一个代理优化问题(寻找获取函数的最大值),尽管仍然是一个困难的问题,但更便宜(在计算意义上),而且可以采用普通工具。因此,贝叶斯优化法最适合于对要优化的函数进行采样是一个非常昂贵的工作的情况。关于这种方法的正确讨论,请参见参考文献。
1. 指定要优化的函数
这是一个函数优化包,因此,第一个也是最重要的要素当然是要优化的函数。
免责声明: 我们清楚地知道下面这个函数的输出是如何取决于其参数的。显然,这只是一个例子,你不应该期望在真实的情况下知道它。然而,应该清楚的是,你不需要这样做。为了使用这个包(更广泛地说,这个技术),你所需要的只是一个函数f
,它接受一组已知的参数并输出一个实数。
def black_box_function(x, y):
"""我们希望最大限度地利用内部未知的函数。
这只是作为一个例子,对于所有的意图和目的来说
意思是把这个函数的内部结构,即:产生其输出值的过程
产生其输出值的过程是未知的。
"""
return -x ** 2 - (y - 1) ** 2 + 1
2. 开始工作
我们需要开始的是实例化一个BayesianOptimization
对象,指定一个要优化的函数f
,以及它的参数和相应的边界pbounds
。这是一个受限的优化技术,所以你必须指定每个参数的最小值和最大值,以便它能够工作。
from bayes_opt import BayesianOptimization
# 参数空间的有界区域
pbounds = {'x': (2, 4), 'y': (-3, 3)}
optimizer = BayesianOptimization(
f=black_box_function,
pbounds=pbounds,
verbose=2, # verbose = 1 只在观察到最大值时才打印,verbose = 0 是沉默的。
random_state=1,
)
BayesianOptimization
对象将开箱即用,不需要太多的调整。你应该注意的主要方法是 “最大化”,它所做的正是你认为的那样。
有许多参数可以传递给最大化,然而,最重要的参数是。
n_iter
。你想进行多少步的贝叶斯优化。步骤越多,越有可能找到一个好的最大值。init_points
: 你想执行多少步的随机探索。随机探索可以通过多样化的探索空间来帮助你。
optimizer.maximize(
init_points=2,
n_iter=3,
)
| iter | target | x | y |
-------------------------------------------------
| 1 | -7.135 | 2.834 | 1.322 |
| 2 | -7.78 | 2.0 | -1.186 |
| 3 | -7.11 | 2.218 | -0.7867 |
| 4 | -12.4 | 3.66 | 0.9608 |
| 5 | -6.999 | 2.23 | -0.7392 |
=================================================
找到的参数和目标值的最佳组合可以通过属性bo.max
访问。
print(optimizer.max)
{'target': -6.999472814518675, 'params': {'x': 2.2303920156083024, 'y': -0.7392021938893159}}
而所有探测到的参数及其相应的目标值的列表可以通过属性bo.res
获得。
for i, res in enumerate(optimizer.res):
print("Iteration {}: \n\t{}".format(i, res))
Iteration 0:
{'target': -7.135455292718879, 'params': {'x': 2.8340440094051482, 'y': 1.3219469606529488}}
Iteration 1:
{'target': -7.779531005607566, 'params': {'x': 2.0002287496346898, 'y': -1.1860045642089614}}
Iteration 2:
{'target': -7.109925819441113, 'params': {'x': 2.2175526295255183, 'y': -0.7867249801593896}}
Iteration 3:
{'target': -12.397162416009818, 'params': {'x': 3.660003815774634, 'y': 0.9608275029525108}}
Iteration 4:
{'target': -6.999472814518675, 'params': {'x': 2.2303920156083024, 'y': -0.7392021938893159}}
2.1 改变界限
在优化过程中,你可能会意识到为某些参数选择的边界是不合适的。对于这些情况,你可以调用方法set_bounds
来改变它们。你可以传递任何现有的参数的组合和它们相关的新界限。
optimizer.set_bounds(new_bounds={"x": (-2, 3)})
optimizer.maximize(
init_points=0,
n_iter=5,
)
| iter | target | x | y |
-------------------------------------------------
3. 引导优化
通常情况下,我们对参数空间的区域有一个概念,我们的函数的最大值可能在那里。对于这些情况,BayesianOptimization
对象允许用户指定要探测的特定点。默认情况下,这些点将被懒惰地探索(lazy=True
),这意味着这些点将在下次调用maximize
时才被评估。这个探测过程发生在高斯过程接管之前。
参数可以以字典的形式传递,比如下面。
optimizer.probe(
params={"x": 0.5, "y": 0.7},
lazy=True,
)
或者作为一个可迭代的对象。请注意,顺序必须是按字母顺序排列的。你可以使用optimizer.space.keys
来指导。
print(optimizer.space.keys)
['x', 'y']
optimizer.probe(
params=[-0.3, 0.1],
lazy=True,
)
optimizer.maximize(init_points=0, n_iter=0)
| iter | target | x | y |
-------------------------------------------------
| 11 | 0.66 | 0.5 | 0.7 |
| 12 | 0.1 | -0.3 | 0.1 |
=================================================
4. 保存、加载和重启
默认情况下,你可以通过在实例化BayesianOptimization
对象时设置verbose>0
来跟踪优化的进展。如果你需要对日志/警报有更多的控制,你将需要使用一个观察者。关于观察者的更多信息,请查看高级之旅的笔记本。在这里,我们将只看到如何使用本地的JSONLogger
对象来保存和加载文件中的进度。
4.1 保存进度
from bayes_opt.logger import JSONLogger
from bayes_opt.event import Events
观察者范式的工作原理是:
- 实例化一个观察者对象。
- 将观察者对象与优化器引发的特定事件联系起来。
BayesianOptimization
对象在优化过程中发射了一些内部事件,特别是,每次它探测函数并获得新的参数-目标组合时,它将发射一个Events.OPTIMIZATION_STEP
事件,我们的记录器将监听该事件。
注意事项:记录器不会回看以前探测的点。
logger = JSONLogger(path="./logs.json")
optimizer.subscribe(Events.OPTIMIZATION_STEP, logger)
optimizer.maximize(
init_points=2,
n_iter=3,
)
| iter | target | x | y |
-------------------------------------------------
| 13 | -12.48 | -1.266 | -2.446 |
| 14 | -3.854 | -1.069 | -0.9266 |
| 15 | -3.594 | 0.7709 | 3.0 |
| 16 | 0.8238 | 0.03433 | 1.418 |
| 17 | 0.9721 | -0.1051 | 0.8701 |
=================================================
4.2 加载进度
当然,如果你存储了进度,你将能够把它加载到BayesianOptimization
的新实例中。最简单的方法是通过调用util
子模块的load_logs
函数。
from bayes_opt.util import load_logs
new_optimizer = BayesianOptimization(
f=black_box_function,
pbounds={"x": (-2, 2), "y": (-2, 2)},
verbose=2,
random_state=7,
)
print(len(new_optimizer.space))
0
load_logs(new_optimizer, logs=["./logs.json"]);
print("New optimizer is now aware of {} points.".format(len(new_optimizer.space)))
New optimizer is now aware of 5 points.
new_optimizer.maximize(
init_points=0,
n_iter=10,
)
| iter | target | x | y |
-------------------------------------------------
| 1 | -3.548 | -2.0 | 1.74 |
| 2 | -3.041 | 1.914 | 0.3843 |
| 3 | -12.0 | 2.0 | -2.0 |
| 4 | -3.969 | 2.0 | 1.984 |
| 5 | -0.7795 | -1.238 | 0.5022 |
| 6 | 0.529 | 0.685 | 0.9576 |
| 7 | 0.2987 | 0.1242 | 0.1718 |
| 8 | 0.9544 | 0.2123 | 0.9767 |
| 9 | 0.7157 | -0.4369 | 1.306 |
| 10 | 0.982 | -0.07186 | 1.113 |
=================================================
接下来的步骤
这次参观应该足以涵盖这个包的大多数使用场景。然而,如果你觉得你需要了解更多,请查看 "advanced-tour.ipynb"笔记本。在那里你可以找到这个包的其他更高级的功能,这些功能可能是你正在寻找的。另外,请浏览实例文件夹,了解实施技巧和想法。