以下为该学习地址的学习笔记
学习地址:Basic tour of the Bayesian Optimization package — Bayesian Optimization documentation
贝叶斯优化简介
贝叶斯优化是一种基于贝叶斯推断和高斯过程的全局优化方法,它试图在尽可能少的迭代次数内找到一个未知函数的最大值。这种技术特别适合用于高成本函数的优化,以及需要在探索(exploration)和利用(exploitation)之间找到平衡的情况。
贝叶斯优化的基本原理
-
后验分布构建:贝叶斯优化通过构建一个描述你要优化的函数的后验分布(通常是高斯过程)来工作。随着观察数据的增加,后验分布会不断改进,算法会逐渐确定参数空间中的哪些区域值得探索,哪些不值得探索。
-
探索与利用的平衡:在每一步迭代中,算法都会根据已知样本(先前探索过的点)拟合一个高斯过程。然后,后验分布结合探索策略(如UCB(上置信界)或EI(期望改进))来确定下一个应该探索的点。
优化过程
贝叶斯优化的过程旨在最小化找到接近最优参数组合所需的步骤数。为此,这种方法使用了一个代理优化问题(寻找采集函数的最大值),虽然这仍然是一个困难的问题,但计算成本较低,并且可以使用常见的工具。因此,贝叶斯优化特别适用于采样目标函数非常昂贵的情况。
关键概念
- 高斯过程:一种用于构建后验分布的非参数贝叶斯模型,它通过已知样本来推断未知函数的分布。
- 采集函数:一种策略函数,用于在每次迭代中决定下一个采样点。常见的采集函数包括上置信界(UCB)和期望改进(EI)。
- 探索(Exploration)与利用(Exploitation):探索是指寻找参数空间中未知或不确定的区域,而利用是指在已知的高潜力区域进行优化。贝叶斯优化通过平衡这两者来提高优化效率。
应用场景
贝叶斯优化适用于以下情况:
- 目标函数的计算代价高昂,例如机器学习模型的超参数调优。
- 需要在尽量少的迭代中找到接近最优的参数组合。
- 需要在探索新的参数区域和利用已有信息之间找到平衡。
通过贝叶斯优化,可以在高成本的函数优化问题中高效地找到最优解,减少不必要的计算开销。详细的讨论和更多的理论基础可以参考相关文献和资料。
1. 确定要优化的函数
这是一个函数优化软件包,因此最重要的第一要素当然是要优化的函数。
免责声明:我们很清楚下面函数的输出如何取决于其参数。显然,这只是一个示例,你不应该指望在实际场景中知道这一点。不过,你应该清楚,你并不需要知道。要使用这个软件包(更广泛地说,要使用这种技术),你所需要的只是一个接收已知参数集并输出实数的函数 f。
贝叶斯优化的核心是对函数进行优化。首先,需要定义一个目标函数(即需要优化的函数)。
def black_box_function(x, y):"""定义一个我们希望优化的未知内部函数。这里仅作为示例,在实际场景中,你不应该知道该函数的具体内部实现。只需要知道这个函数接受一组参数并输出一个实数即可。"""return -x ** 2 - (y - 1) ** 2 + 1
此函数返回一个值,该值基于输入参数 x和 y 计算得到。优化的目标是找到使得该函数值最大的参数组合。
2. 开始贝叶斯优化
我们需要实例化一个 BayesianOptimization
对象,指定要优化的函数 f
及其参数和对应的边界 pbounds
。贝叶斯优化是一种约束优化技术,因此必须指定每个参数的最小和最大值。
from bayes_opt import BayesianOptimization# 参数空间的有界区域
pbounds = {'x': (2, 4), 'y': (-3, 3)} # 指定参数 x 和 y 的边界optimizer = BayesianOptimization(f=black_box_function, # 需要优化的函数pbounds=pbounds, # 参数边界verbose=2, # verbose=1 时仅在观察到最大值时打印,verbose=0 时不打印random_state=1, # 随机种子,保证结果可重复
)
BayesianOptimization
对象可以直接使用,无需进行大量调优。主要需要关注的方法是 maximize
,它用于执行贝叶斯优化过程。
maximize
方法接受多个参数,最重要的有:
n_iter
:执行贝叶斯优化的步数,步数越多,找到好最大值的可能性越大。init_points
:执行随机探索的步数,随机探索有助于多样化探索空间。
optimizer.maximize(init_points=2, # 初始随机探索步数n_iter=3, # 贝叶斯优化步数
)
执行后,会显示每次迭代的参数组合及其对应的目标值(即函数值)。
markdown复制代码
| 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 |
=================================================
最佳参数组合及其对应的目标值可以通过 optimizer.max
属性访问。
print(optimizer.max)
# 输出最优参数及其对应的目标值
# {'target': -6.999472814518675, 'params': {'x': 2.2303920156083024, 'y': -0.7392021938893159}}
所有被探索的参数及其目标值可以通过 optimizer.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)}) # 修改参数 x 的边界为 [-2, 3]optimizer.maximize( # 开始优化过程init_points=0, # 初始随机探索步数设置为 0n_iter=5, # 进行 5 次贝叶斯优化迭代
)
执行结果会显示每次迭代的参数组合及其对应的目标值(即函数值)。
- iter: 迭代次数,即当前是第几次迭代。
- target: 目标值,即目标函数在该次迭代的参数组合下计算得到的值。
- x: 参数 x 的值。
- y: 参数 y 的值。
| iter | target | x | y |
-------------------------------------------------
| 6 | -2.942 | 1.98 | 0.8567 |
| 7 | -0.4597 | 1.096 | 1.508 |
| 8 | 0.5304 | -0.6807 | 1.079 |
| 9 | -5.33 | -1.526 | 3.0 |
| 10 | -5.419 | -2.0 | -0.5552 |
=================================================
3. 指导优化过程
在优化过程中,我们通常对参数空间的某些区域有一定的了解,认为这些区域可能包含函数的最大值。对于这种情况,BayesianOptimization
对象允许用户指定特定的点进行探测。默认情况下,这些点会被懒惰地(lazy=True
)探索,即这些点将在下次调用 maximize
时才会被评估。这个探测过程发生在高斯过程接管之前。
示例代码
可以将参数以字典形式传递,如下所示:
optimizer.probe( # 使用 probe 方法指定要探测的点params={"x": 0.5, "y": 0.7}, # 指定 x 和 y 的值lazy=True, # 懒惰探索,等到下次调用 maximize 时才评估这些点
)
也可以将参数作为可迭代对象传递。注意顺序必须是按字母顺序排列的。你可以使用 optimizer.space.keys
来查看顺序。
print(optimizer.space.keys) # 打印参数的键,以确认顺序
# 输出: ['x', 'y']optimizer.probe( # 使用 probe 方法指定要探测的点params=[-0.3, 0.1], # 参数按字母顺序排列,即 x=-0.3, y=0.1lazy=True, # 懒惰探索,等到下次调用 maximize 时才评估这些点
)
接下来调用 maximize
方法执行优化:
optimizer.maximize(init_points=0, n_iter=0) # 进行优化,此处 init_points 和 n_iter 均为 0 表示只评估探测点
- init_points:
- 表示在正式开始贝叶斯优化之前,进行随机探索的步数。
- 随机探索可以帮助多样化探索空间,但并不利用贝叶斯优化的优点。
- n_iter:
- 表示实际进行贝叶斯优化的步数。
- 贝叶斯优化利用已有数据和高斯过程模型来选择下一个探索点,以最大化目标函数。
- lazy=True:
- 表示指定的探测点将在下次调用
maximize
时被评估。 - 如果
lazy=False
,探测点会立即被评估。
- 表示指定的探测点将在下次调用
执行结果会显示每次迭代的参数组合及其对应的目标值(即函数值)。
| iter | target | x | y |
-------------------------------------------------
| 11 | 0.66 | 0.5 | 0.7 |
| 12 | 0.1 | -0.3 | 0.1 |
=================================================
- 指定探测点:
- 通过
optimizer.probe
方法指定一些参数组合(探测点)用于评估。 - 设置
lazy=True
表示这些探测点不会立即被评估,而是在下次调用maximize
时才会被评估。
- 通过
- 调用
maximize
方法:- 通过
optimizer.maximize(init_points=0, n_iter=0)
,告诉优化器这次优化不进行任何额外的随机探索(init_points=0
)和贝叶斯优化步骤(n_iter=0
)。 - 由于设置了
lazy=True
,因此这些探测点在这次maximize
调用时被评估。
- 通过
这样做的意义在于,你可以提前指定一些感兴趣的点,让优化器在正式开始贝叶斯优化之前先评估这些点。这对于你有某些先验知识或猜测可能有用的参数区域时非常有效。
3.1 补充
1. 探测点(Probing Points)
探测点是你认为可能重要或有趣的参数组合,提前指定这些点用于目标函数的计算。你可以使用 optimizer.probe
方法来指定这些点。
python复制代码
optimizer.probe(params={"x": 0.5, "y": 0.7},lazy=True, # 表示这些探测点在下次调用 maximize 时才会被评估
)
2. 评估(Evaluation)
评估是指对指定的探测点进行目标函数的计算。也就是说,把这些探测点作为参数代入目标函数,计算得到对应的目标值。
3. maximize
方法的调用
optimizer.maximize(init_points=0, n_iter=0)
表示这次优化过程中不进行任何额外的随机探索(init_points=0
)和贝叶斯优化步骤(n_iter=0
),只对之前通过 optimizer.probe
指定的探测点进行评估。
如下这个代码示例,执行这段代码时,maximize
方法会评估之前指定的探测点 (0.5, 0.7) 和 (-0.3, 0.1),并将评估结果记录下来。这些结果将用于更新贝叶斯优化模型,从而帮助优化算法更好地寻找目标函数的最大值。
#示例
from bayes_opt import BayesianOptimization# 目标函数
def black_box_function(x, y):return -x ** 2 - (y - 1) ** 2 + 1# 定义参数空间的边界
pbounds = {'x': (2, 4), 'y': (-3, 3)}# 创建贝叶斯优化对象
optimizer = BayesianOptimization(f=black_box_function,pbounds=pbounds,verbose=2, # 设置输出级别random_state=1,
)# 指定探测点
optimizer.probe(params={"x": 0.5, "y": 0.7}, # 参数 x=0.5, y=0.7lazy=True, # 懒惰评估,这些点会在下次调用 maximize 时进行评估
)# 指定另一个探测点
optimizer.probe(params=[-0.3, 0.1], # 参数按字母顺序排列,即 x=-0.3, y=0.1lazy=True, # 懒惰评估,这些点会在下次调用 maximize 时进行评估
)# 进行优化,此次 maximize 调用只评估指定的探测点
optimizer.maximize(init_points=0, n_iter=0)
4. 保存、加载和重启
默认情况下,通过设置 verbose > 0
可以跟踪优化的进度。如果需要更高级的日志记录或警报控制,可以使用观察者模式。这里我们将看到如何使用内置的 JSONLogger
对象来保存和加载进度。
4.1 保存进度
首先,导入所需的库:
from bayes_opt.logger import JSONLogger # 导入 JSONLogger 用于记录日志
from bayes_opt.event import Events # 导入 Events 用于触发事件
观察者模式的工作原理如下:
- 实例化观察者对象。
- 将观察者对象与优化器触发的特定事件绑定。
贝叶斯优化对象在优化过程中会触发多个内部事件,特别是每次探测函数并获得新的参数-目标组合时,会触发 Events.OPTIMIZATION_STEP
事件,日志记录器将监听该事件。
logger = JSONLogger(path="./logs.log") # 创建 JSONLogger 对象,指定日志文件路径
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.03434 | 1.418 |
17 | 0.9721 | -0.1051 | 0.87 |
4.2 加载进度
如果保存了进度,可以将其加载到一个新的贝叶斯优化实例中。最简单的方法是调用 load_logs
函数。
- 创建一个新的贝叶斯优化实例:用于优化一个目标函数
black_box_function
。 - 加载先前保存的优化进度:从日志文件
logs.log
中恢复已经探测过的点。 - 继续优化过程:在加载了之前的进度后,进行更多的贝叶斯优化迭代。
- 通过这段代码,你可以在不同的会话或运行中保存和加载贝叶斯优化的进度。这对于长时间运行的优化任务特别有用,因为你可以中途暂停,然后在加载进度后继续优化。这段代码展示了如何使用贝叶斯优化包的日志功能来保存和加载优化进度,并在新的优化器实例中继续优化过程。
from bayes_opt.util import load_logs # 导入 load_logs 函数new_optimizer = BayesianOptimization( # 创建新的贝叶斯优化对象f=black_box_function, # 目标函数pbounds={"x": (-2, 2), "y": (-2, 2)}, # 参数空间的边界verbose=2, # 输出级别random_state=7, # 随机种子
)
#这一步创建了一个新的 BayesianOptimization 对象,并指定了要优化的函数、参数边界、输出级别和随机种子。print(len(new_optimizer.space)) # 输出当前优化器空间中的点数
# 输出: 0 # 初始时点数为0
# 在没有加载日志之前,优化器空间中的点数为0。load_logs(new_optimizer, logs=["./logs.log"]) # 加载之前保存的日志文件
# 这一步从指定的日志文件 logs.log 中加载先前的优化进度,包括已经探测过的参数点和对应的目标值。print("New optimizer is now aware of {} points.".format(len(new_optimizer.space))) # 输出当前优化器空间中的点数
# 输出: New optimizer is now aware of 5 points.
# 加载日志后,优化器现在知道5个点(假设日志中保存了5个探测点)。new_optimizer.maximize( # 继续优化init_points=0, # 不进行初始随机探测n_iter=10, # 贝叶斯优化的迭代次数
)
# 这一步在加载了之前的进度后,进行10次贝叶斯优化迭代,以进一步探索和优化目标函数。
iter | target | x | y |
---|---|---|---|
1 | -3.548 | -2.0 | 1.74 |
2 | -3.041 | 1.914 | 0.3844 |
3 | -12.0 | 2.0 | -2.0 |
4 | -3.969 | 2.0 | 1.984 |
5 | -0.7794 | -1.238 | 0.5022 |
6 | 0.529 | 0.685 | 0.9576 |
7 | 0.2987 | 0.1242 | 0.1718 |
8 | 0.9544 | 0.2123 | 0.9766 |
9 | 0.7157 | -0.437 | 1.305 |
10 | 0.983 | -0.06785 | 1.111 |
下一步
这部分内容应该足以覆盖此包的大多数使用场景。如果需要了解更多,请查阅高级教程笔记本。在那里,你可以找到此包的其他高级功能,这些功能可能正是你所寻找的。此外,还可以浏览示例文件夹,获取实现技巧和灵感。