Source code for smt_optim.acquisition_functions.expected_improvement
import numpy as np
import scipy.stats as stats
from scipy.special import erfcx
import copy
[docs]
def expected_improvement(mu: float, s2: float, f_min: float) -> float:
"""
Expected Improvement acquisition function.
Parameters
----------
mu: float
Mean prediction.
s2: float
Variance prediction.
f_min: float
Best minimum objective value in training data.
Returns
-------
float
Expected Improvement value.
"""
if s2 > 0:
s = np.sqrt(s2)
z = (f_min - mu)/s
return (f_min - mu) * stats.norm.cdf(z) + s * stats.norm.pdf(z)
else:
return 0
[docs]
def vec_expected_improvement(mu: np.ndarray, s2: np.ndarray, f_min: float) -> np.ndarray:
"""
Vectorized Expected Improvement acquisition function.
Parameters
----------
mu: np.ndarray
Mean prediction of shape (num_points, 1).
s2: np.ndarray
Variance prediction of shape (num_points, 1).
f_min: float
Best minimum objective value in training data.
Returns
-------
np.ndarray
Expected Improvement values of shape (num_points, 1).
"""
mask_s = s2 > 0
ei = np.full_like(mu, 0)
if not np.any(mask_s):
return np.full_like(mu, 0)
else:
s = np.sqrt(s2[mask_s])
z = (f_min - mu[mask_s])/s
ei[mask_s] = (f_min - mu[mask_s])*stats.norm.cdf(z) + s*stats.norm.pdf(z)
return ei
# ------- TODO: CLEAN LOG EXPECTED IMPROVEMENT -------
# ------- LOG EXPECTED IMPROVEMENT -------
[docs]
def log_ei(mu: np.ndarray, s2: np.ndarray, f_min: float) -> np.ndarray:
"""
Vectorized Log Expected Improvement acquisition function.
LogEI is more numerically stable that the EI acquisition function especially when the GP's variance is small.
From: https://arxiv.org/abs/2310.20708.
Parameters
----------
mu: np.ndarray
Mean prediction of shape (num_points, 1).
s2: np.ndarray
Variance prediction of shape (num_points, 1).
f_min: float
Best minimum objective value in training data.
Returns
-------
np.ndarray
"""
s = np.sqrt(s2)
c1 = np.log(2*np.pi)/2
c2 = np.log(np.pi/2)/2
epsilon = np.finfo(np.float64).eps
z = np.zeros_like(mu)
# create mask where std is equal or less than 0
mask_s = (s > 0).ravel()
not_mask_s = ~mask_s
z[mask_s] = (f_min - mu[mask_s]) / s[mask_s]
z[not_mask_s] = np.nan
mask1 = (z > -1).ravel()
mask2 = ((-1/np.sqrt(epsilon) < z) & (z <= -1)).ravel()
mask3 = (z <= -1/np.sqrt(epsilon)).ravel()
log_h = np.empty_like(z)
log_h[mask1] = np.log(stats.norm.pdf(z[mask1]) + z[mask1] * stats.norm.cdf(z[mask1]))
log_h[mask2] = -z[mask2]**2/2 - c1 + log1mexp(np.log(erfcx(-z[mask2]/np.sqrt(2))*np.abs(z[mask2])) + c2)
log_h[mask3] = -z[mask3]**2/2 - c1 - 2*np.log(np.abs(z[mask3]))
log_ei = copy.deepcopy(log_h)
log_ei[mask_s] = log_h[mask_s] + np.log(s[mask_s])
# impose infinite value if std <= 0
log_ei[not_mask_s] = -np.inf
return log_ei
[docs]
def log1mexp(z: np.ndarray) -> np.ndarray:
mask1 = (-2*np.log(2) < z).ravel()
mask2 = ~mask1
log1mexp_arr = np.empty_like(z)
log1mexp_arr[mask1] = np.log(-(np.exp(z[mask1]) - 1))
log1mexp_arr[mask2] = np.log1p(-np.exp(z[mask2]))
return log1mexp_arr
[docs]
def fidelity_correlation(covariance: np.ndarray, li_var: np.ndarray, lj_var: np.ndarray) -> np.ndarray:
"""
GP posterior fidelity correlation between 2 fidelity levels. The correlation is clipped between 0 and 1.
:param covariance: Posterior covariance prediction between fidelity levels i and j.
:type covariance: np.ndarray
:param li_var: Variance prediction of fidelity level i.
:type li_var: np.ndarray
:param lj_var: Variance prediction of fidelity level j.
:type lj_var: np.ndarray
:return: The fidelity correlation value
:rtype: np.ndarray
"""
return np.clip(np.abs(covariance/np.sqrt(li_var * lj_var)), 0, 1)
[docs]
def probability_of_improvement(mu: np.ndarray, s2: np.ndarray, f_min: float) -> np.ndarray:
"""
Probability of Improvement acquisition function.
:param mu: Mean prediction.
:type mu: np.ndarray
:param s2: Variance prediction.
:type s2: np.ndarray
:param f_min: Minimum predicted objective value.
:type f_min: np.ndarray
:return: The PI acquisition function value.
:rtype: np.ndarray
"""
pi = np.zeros_like(mu)
mask_s = (s2 > 0)
pi[mask_s] = stats.norm.cdf((f_min - mu[mask_s])/np.sqrt(s2[mask_s]))
return pi