基于github上的一个贝叶斯优化开源项目,其用法在项目的说明中有详细英文记录,这里主要是整理简化,并参考了其他文献来记录一下项目中用的数学函数以及论文中一些单词的说明。
原理
这篇文章(参考一)从详细说明了该项目的核心思想和过程,包括该过程用到的先验函数和采集函数的介绍。博客和github项目里面都提到了exploration与exploitation这两个单词。可以解释为不确定策略的探索(Exploration)和当前策略的开采(Exploitation)之间进行权衡,也可以理解为explore是在未知领域进行探索,exploit是在已有范围进行开发利用;Exploration 是要在全局范围内搜索,避免陷入局部最优,Exploitation 是要在当前最优的附近搜索,找到更好的解。即:
探索(exploration):简单来说就是尽量选择远离已知点的点为下一次用于迭代的参考点,即尽量探索未知的区域,点的分布会尽可能的平均。
利用(exploitation):简单来说就是尽量选择靠近已知点的点为下一次用于迭代的参考点,即尽量挖掘已知点周围的点,点的分布会出现一个密集区域,容易进入局部最大
def BayesianOptimization(target,x,Y):
IF 初始化?
Yes:
xold为已知的所有点中目标函数取得最大值的自变量值
IF target(xold)>Y?
Yes: return xold,target(xold) #撞了大运,还没开始迭代就结束了
No: Pass
No: 随机初始化
While target(xnew)<Y:
利用PF(高斯过程回归)求解未知点(在事先定义的自变量范围内,有成千上万个未知点)的均值与方差
利用AC(EI或PI或UCB)找到贝叶斯优化器猜测的最大值的点xnew,一般是AC函数最大点
return xnew,target(new)
x:自变量取值范围,Y:可以接受的黑箱函数因变量取值。这其中的两个核心过程为先验函数(Prior Function,PF)与采集函数(Acquisition Function,AC),采集函数也可以叫效能函数(Utility Funtcion)。先验函数(高斯过程回归)是计算每一点处函数值的均值和方差,根据均值和方差来构造采集函数,用以决定你个本次迭代时在哪个点处进行采样。
其具体的高斯过程回归方程和采集函数可以参考二这里
引用上面提到的文章来概括其主要过程,方程的数学过程细节还是直接参考原作者吧。
算法的思路是首先生成一个初始候选解集合,然后根据这些点寻找下一个有可能是极值的点,将该点加入集合中,重复这一步骤,直至迭代终止。最后从这些点中找出极值点作为问题的解。
这里的关键问题是如何根据已经搜索的点确定下一个搜索点。贝叶斯优化根据已经搜索的点的函数值估计真实目标函数值的均值和方差(即波动范围),如图所示。上图中红色的曲线为估计出的目标函数值即在每一点出处的目标函数值的均值。现在有3个已经搜索的点,用黑色实心点表示。两条虚线所夹区域为在每一点处函数值的变动范围,在以均值即红色曲线为中心,与标准差成正比的区间内波动。在搜索点处,红色曲线经过搜索点,且方差最小,在远离搜索点处方差更大,这也符合我们的直观认识,远离采样点处的函数值估计的更不可靠。
根据均值和方差可以构造出采集函数(acquisition function),即对每一点是函数极值点的可能性的估计,反映了每一个点值得搜索的程度,该函数的极值点是下一个搜索点,如下图所示。下图中的矩形框所表示的点是采集函数的极大值点,也是下一个搜索点。
算法的核心由两部分构成:对目标函数进行建模即计算每一点处的函数值的均值和方差,通常用高斯过程回归实现;构造采集函数,用于决定本次迭代时在哪个点处进行采样。
理解到这里的概括后,再次看参考一文章里的step1-9的图片就比较好理解了。
使用方式
初级入门
接下来就是照搬github上的教程了。
想要使用贝叶斯优化只需要三个部分,优化参数,目标方程,优化器
目标方程:例子给的是一个两元函数,这里也可以是机器学习训练的一个项目。
from bayes_opt import BayesianOptimization
def black_box_function(x, y):
return -x ** 2 - (y - 1) ** 2 + 1
优化参数:机器学习的话,一般指的是超参数。这里是X,Y。并设定好参数的区间。
优化器的设置,直接使用BayesianOptimization进行初始化
pbounds = {'x': (2, 4), 'y': (-3, 3)}
optimizer = BayesianOptimization(
f=black_box_function,
pbounds=pbounds,
verbose=2, # verbose = 1 prints only when a maximum is observed, verbose = 0 is silent
random_state=1,
)
f=black_box_function 这里比较简单指的就是一个函数。如果是机器学习,这里定义的函数可以是完整的一个项目,调用这个函数来进行项目的启动,最后返回相应的结果就可以。
接下来就可以进行迭代寻优了
optimizer.maximize(
init_points=2,
n_iter=3,
)
这里面涉及到两个参数,init_point和n_iter
n_iter: 总共迭代的次数。
init_point:这个是随机点执行的次数。上面的基础理论部分提到,贝叶斯优化是建立在上一次优化的基础上进行优化。这是exploitation部分。但是想要exploration,扩大全局最优的可能,使用init_point来控制,直接使用随机点而不依赖上次计算的结果。
最终打印出整个迭代后的最优结果(最后的几次不一定是最优解)
print(optimizer.max)
想要打印出每次迭代的结果
for i, res in enumerate(optimizer.res):
print("Iteration {}: \n\t{}".format(i, res))
可以通过一下命令重新设置参数的边界
optimizer.set_bounds(new_bounds={"x": (-2, 3)})
optimizer.maximize( init_points=0, n_iter=5, )
还有一种情况就是引导优化,我们觉得参数的某个值的范围内可能存在最大值,可以使用这个命令设置:
optimizer.probe(
params={"x": 0.5, "y": 0.7},
lazy=True,
)
lazy表示这延迟写入该值。这个时候在下次调用max函数的时候将设置的参数值写入到优化器,并计算。
可以连续写入:
optimizer.probe( params=[-0.3, 0.1], lazy=True, )
调用maximize
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 | =================================================
运行过程数据的保存与恢复
保存:之前就之使用了个优化器类,通过设置该类的初始化参数verbose也可以看到运行中的状态。如果想要保存到本地文件内,可以使用如下流程:
from bayes_opt.logger import JSONLogger
from bayes_opt.event import Events
logger = JSONLogger(path="./logs.json")
optimizer.subscribe(Events.OPTIMIZATION_STEP, logger)
optimizer.maximize(
init_points=2,
n_iter=3,
)
应当注意的是,只会记录当前以及以后的数据。
加载:一方面可以加载已经优化的过程数据,另一方面可以直接作用在另外一个新的优化器上。
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))
load_logs(new_optimizer, logs=["./logs.json"]);
new_optimizer.maximize(
init_points=0,
n_iter=10,
)
高级进阶
其实通过上面初级入门的教程已经适用绝大部分的场景。高级进阶的部分主要是细化优化器执行的每一步的具体过程。比如说optimizer.maximize其实是执行了suggest
, probe以及
register方法。
通过前面文献一和文献二的理论基础我们知道优化器的一次迭代大概经历如下:定义目标,初始化变量(更新后变量值),采集函数确定变量更新位置,计算目标值。在初级入门教程中直接使用maximize一个方法将这些全部包括。其实可以拆分为如下步骤:
from bayes_opt import UtilityFunction
# 定义目标函数
def black_box_function(x, y):
return -x ** 2 - (y - 1) ** 2 + 1
optimizer = BayesianOptimization(
f=None, # 先定义为空
pbounds={'x': (-2, 2), 'y': (-3, 3)},#自定义变量范围
verbose=2,
random_state=1,
)
#采集函数的选取以及提到的探索exploration和利用exploitation的平衡参数的确定
utility = UtilityFunction(kind="ucb", kappa=2.5, xi=0.0)
# 确定变量的下个位置
next_point_to_probe = optimizer.suggest(utility)
print("Next point to probe is:", next_point_to_probe)
# 计算目标函数值
target = black_box_function(**next_point_to_probe)
print("Found the target value to be:", target)
# 写入优化器
optimizer.register(
params=next_point_to_probe,
target=target,
)
以上程序写好后,只会执行一次。循环执行的方式为:
for _ in range(5):
next_point = optimizer.suggest(utility)
target = black_box_function(**next_point)
optimizer.register(params=next_point, target=target)
print(target, next_point)
print(optimizer.max)