概述
贝叶斯优化算法是机器学习领域一种经典的优化算法,本文从一个实际的问题出发$$ y = x^2 + z^2 $$ 使用python代码, 一步步实现BO算法。
BO算法基本流程
一个经典的Bayesian Optimization流程如下。
高斯过程
我们知道,高斯过程是用来替代昂贵的目标函数的,也就是流程图里的代理模型。比如说,我们的机器性能很差,计算$$ y = x^2 + z^2 $$要花费1个小时(当然,这里只是做一个假设,因为实际工程上的目标函数十分复杂,计算一次要非常久),所以我们就要使用一个其它的万能函数来代替目标函数,这个过程就是高斯过程回归。
class GaussianProcess:
def __init__(self, kernel_variance=1.0, noise_variance=1e-10):
self.kernel_variance = kernel_variance
self.noise_variance = noise_variance
self.X_train = None
self.y_train = None
self.alpha = None
self.L = None
def kernel(self, X1, X2):
sqdist = np.sum(X1**2, 1).reshape(-1, 1) + np.sum(X2**2, 1) - 2 * np.dot(X1, X2.T)
return self.kernel_variance * np.exp(-0.5 * sqdist)
def fit(self, X_train, y_train):
self.X_train = X_train
self.y_train = y_train
K = self.kernel(X_train, X_train) + self.noise_variance * np.eye(len(X_train))
self.L = np.linalg.cholesky(K)
self.alpha = np.linalg.solve(self.L.T, np.linalg.solve(self.L, y_train))
def predict(self, X_s):
K_s = self.kernel(self.X_train, X_s)
K_ss = self.kernel(X_s, X_s) + 1e-8 * np.eye(len(X_s))
v = np.linalg.solve(self.L, K_s)
mu_s = K_s.T.dot(self.alpha)
cov_s = K_ss - v.T.dot(v)
return mu_s, np.diag(cov_s)
我们看上面这个高斯过程,init函数用来初始化所有的变量,高斯过程涉及到三个比较重要的函数
核函数
核函数(也称为协方差函数)用于衡量输入数据点之间的相似性,这里我们使用RBF核函数$$k(x_i, x_j) = \sigma^2 \exp\left(-\frac{|x_i - x_j|^2}{2\ell^2}\right)$$
拟合函数 fit
这一步骤也可以叫做回归过程,就像最小二乘法求一次函数系数也叫做回归一样,我们要使用高斯函数去预测未知点的均值$\mu_s $ 和$ \Sigma_s$,我们也必须要求出高斯过程中的一些“回归系数”,这里我们需要按照下面的式子计算未知点的值
\label{eq:mu}
\mu_s = \mathbf{K}_s^T \mathbf{K}^{-1} \mathbf{y}_{\text{train}}
\end{equation}
\label{eq:sigma}
\mathbf{\Sigma}_s = \mathbf{K}_{ss} - \mathbf{K}_s^T \mathbf{K}^{-1} \mathbf{K}_s
\end{equation}
对于上述公式中的 $ \mathbf{K}^{-1}\mathbf{y}_{\text{train}} $ ,我们使用 $ \alpha $ 表示, $ \alpha $可以从方程 $ \eqref{eq:alpha} $ 解出。
\label{eq:alpha}
\mathbf{K\alpha} = \mathbf{y}_{train}
\end{equation}
对应到代码里,我们可以看到除了 $ \mathbf{\alpha} $ ,我们还将 $ \mathbf{K} $ 分解成为了一个下三角矩阵 $ \mathbf{L} $ ,之所以这样做是因为矩阵 $\mathbf{K}$ 是拟合过程中所有点的协方差,衡量输入点的相似度,该矩阵中的某些数值可能会导致直接求逆出现数值不稳定,所以我们将其分解为下三角矩阵。同时由于该矩阵是正定对称矩阵,我们在代码中使用Cholesky分解可以更快得到结果。
def fit(self, X_train, y_train):
self.X_train = X_train
self.y_train = y_train
K = self.kernel(X_train, X_train) + self.noise_variance * np.eye(len(X_train))
self.L = np.linalg.cholesky(K)
self.alpha = np.linalg.solve(self.L.T, np.linalg.solve(self.L, y_train))
预测函数 predict
预测函数predict的作用是当输入一个不知道y值的x,我们根据高斯过程中已知的量和方程 $ \eqref{eq:mu} \eqref{eq:sigma} $ 去求出对应的预测值。这里的 $ \mu_s $ 和 $ \Sigma_s $ 分别代表预测出来的平均值和方差
Bayesian Optimization
有了这个高斯过程之后我们来理解BO过程,贝叶斯优化算法,说白了也就是生成一大堆数据点,然后找到数据点里值最小的点,但是一般我们目标函数计算开销很大,所以我们使用高斯模型计算这些点。
这里有一个问题,我们的高斯模型计算出来的值是平均值和方差,所以要有一个acquisition_function利用平均值和方差计算预测的值,当我们求最小值时,我们可以直接用平均值减去方差。
高斯过程给出的预测的值并不是实际值,我们拿到acquisition_function给出的预测值后,找到最小的点,然后求出该点的实际值,把该点加入GP过程的fit函数,对代理模型进行更新,重复这个过程直到达到最大迭代次数,或者值的精度达到了要求。
本文用的python代码
import numpy as np
def objective_function(x, z):
return x**2 + z**2
class GaussianProcess:
def __init__(self, kernel_variance=1.0, noise_variance=1e-10):
self.kernel_variance = kernel_variance
self.noise_variance = noise_variance
self.X_train = None
self.y_train = None
self.alpha = None
self.L = None
def kernel(self, X1, X2):
sqdist = np.sum(X1**2, 1).reshape(-1, 1) + np.sum(X2**2, 1) - 2 * np.dot(X1, X2.T)
return self.kernel_variance * np.exp(-0.5 * sqdist)
def fit(self, X_train, y_train):
self.X_train = X_train
self.y_train = y_train
K = self.kernel(X_train, X_train) + self.noise_variance * np.eye(len(X_train))
self.L = np.linalg.cholesky(K)
self.alpha = np.linalg.solve(self.L.T, np.linalg.solve(self.L, y_train))
def predict(self, X_s):
K_s = self.kernel(self.X_train, X_s)
K_ss = self.kernel(X_s, X_s) + 1e-8 * np.eye(len(X_s))
v = np.linalg.solve(self.L, K_s)
mu_s = K_s.T.dot(self.alpha)
cov_s = K_ss - v.T.dot(v)
return mu_s, np.diag(cov_s)
def acquisition_function(mu, sigma, kappa=2.0):
return mu - kappa * sigma
def bayesian_optimization(n_iter=500, bounds=(-10, 10)):
# 初始化
X_train = np.random.uniform(bounds[0], bounds[1], (5, 2))
y_train = np.array([objective_function(x[0], x[1]) for x in X_train])
gp = GaussianProcess()
gp.fit(X_train, y_train)
for _ in range(n_iter):
# 预测
X_sample = np.random.uniform(bounds[0], bounds[1], (100, 2))
mu, sigma = gp.predict(X_sample)
# 计算采集函数
acquisition_values = acquisition_function(mu, sigma)
# 选择下一个评估点
next_sample = X_sample[np.argmin(acquisition_values)]
# 评估目标函数
y_next = objective_function(next_sample[0], next_sample[1])
# 更新数据集
X_train = np.vstack((X_train, next_sample))
y_train = np.append(y_train, y_next)
# 重新拟合高斯过程
gp.fit(X_train, y_train)
return X_train[np.argmin(y_train)], np.min(y_train)
# 运行贝叶斯优化
best_point, best_value = bayesian_optimization()
print(f"Optimal value of x and z: {best_point}")
print(f"Minimum value of objective function: {best_value}")
程序输出结果如下:
Optimal value of x and z: [ 0.00952015 -0.0112046 ]
Minimum value of objective function: 0.0002161764355009679
Comments NOTHING