带约束的产品特性优化
Product feature optimization with constraints
我训练了一个 Lightgbm 模型来学习对数据集进行排序。该模型预测样本的相关性得分。所以预测越高越好。现在模型已经学习了,我想找到一些能给我最高预测分数的特征的最佳值。
所以,假设我有功能 u,v,w,x,y,z
,我想优化的功能是 x,y,z
。
maximize f(u,v,w,x,y,z) w.r.t features x,y,z where f is a lightgbm model
subject to constraints :
y = Ax + b
z = 4 if y < thresh_a else 4-0.5 if y >= thresh_b else 4-0.3
thresh_m < x <= thresh_n
数字是随机组成的,但约束是线性的。
Objective 关于 x
的函数如下所示:
所以这个函数非常尖锐,不流畅。我也没有梯度信息,因为 f
是一个 lightgbm 模型。
使用 我写下了以下 class :
class ProductOptimization:
def __init__(self, estimator, features_to_change, row_fixed_values,
bnds=None):
self.estimator = estimator
self.features_to_change = features_to_change
self.row_fixed_values = row_fixed_values
self.bounds = bnds
def get_sample(self, x):
new_values = {k:v for k,v in zip(self.features_to_change, x)}
return self.row_fixed_values.replace({k:{self.row_fixed_values[k].iloc[0]:v}
for k,v in new_values.items()})
def _call_model(self, x):
pred = self.estimator.predict(self.get_sample(x))
return pred[0]
def constraint1(self, vector):
x = vector[0]
y = vector[2]
return # some float value
def constraint2(self, vector):
x = vector[0]
y = vector[3]
return #some float value
def optimize_slsqp(self, initial_values):
con1 = {'type': 'eq', 'fun': self.constraint1}
con2 = {'type': 'eq', 'fun': self.constraint2}
cons = ([con1,con2])
result = minimize(fun=self._call_model,
x0=np.array(initial_values),
method='SLSQP',
bounds=self.bounds,
constraints=cons)
return result
我得到的结果总是围绕着最初的猜测。我认为这是因为函数的非平滑性以及缺少任何对 SLSQP 优化器很重要的梯度信息。有什么建议我应该如何处理这类问题?
自从我上次写了一些严肃的代码以来已经过去了很长时间,所以如果我不完全清楚每件事的作用,我深表歉意,请随时要求更多解释
进口:
from sklearn.ensemble import GradientBoostingRegressor
import numpy as np
from scipy.optimize import minimize
from copy import copy
首先,我定义了一个新的 class,它允许我轻松地重新定义值。这个 class 有 5 个输入:
- 值:这是 'base' 值。在你的等式
y=Ax + b
中,它是 b
部分
- 最小值:这是此类型将评估为的最小值
- 最大值:这是此类型将评估为
的最大值
- 乘数:第一个棘手的。它是其他 InputType 对象的列表。第一个是输入类型,第二个是乘法器。在你的例子中
y=Ax +b
你会得到 [[x, A]]
,如果等式是 y=Ax + Bz + Cd
它将是 [[x, A], [z, B], [d, C]]
- 关系:最棘手的一个。它也是其他 InputType 对象的列表,它有四个项目:第一个是输入类型,第二个定义它是否是您使用
min
的上边界,如果它是您使用 max
的下边界。列表中的第三项是边界的值,第四项是与之相连的输出值
当心 如果您将输入值定义得太奇怪,我确定会有奇怪的行为。
class InputType:
def __init__(self, value=0, minimum=-1e99, maximum=1e99, multipliers=[], relations=[]):
"""
:param float value: base value
:param float minimum: value can never be lower than x
:param float maximum: value can never be higher than y
:param multipliers: [[InputType, multiplier], [InputType, multiplier]]
:param relations: [[InputType, min, threshold, output_value], [InputType, max, threshold, output_value]]
"""
self.val = value
self.min = minimum
self.max = maximum
self.multipliers = multipliers
self.relations = relations
def reset_val(self, value):
self.val = value
def evaluate(self):
"""
- relations to other variables are done first if there are none then the rest is evaluated
- at most self.max
- at least self.min
- self.val + i_x * w_x
i_x is input i, w_x is multiplier (weight) of i
"""
for term, min_max, value, output_value in self.relations:
# check for each term if it falls outside of the expected terms
if min_max(term.evaluate(), value) != term.evaluate():
return self.return_value(output_value)
output_value = self.val + sum([i[0].evaluate() * i[1] for i in self.multipliers])
return self.return_value(output_value)
def return_value(self, output_value):
return min(self.max, max(self.min, output_value))
使用它,您可以修复从优化器发送的输入类型,如 _call_model
:
所示
class Example:
def __init__(self, lst_args):
self.lst_args = lst_args
self.X = np.random.random((10000, len(lst_args)))
self.y = self.get_y()
self.clf = GradientBoostingRegressor()
self.fit()
def get_y(self):
# sum of squares, is minimum at x = [0, 0, 0, 0, 0 ... ]
return np.array([[self._func(i)] for i in self.X])
def _func(self, i):
return sum(i * i)
def fit(self):
self.clf.fit(self.X, self.y)
def optimize(self):
x0 = [0.5 for i in self.lst_args]
initial_simplex = self._get_simplex(x0, 0.1)
result = minimize(fun=self._call_model,
x0=np.array(x0),
method='Nelder-Mead',
options={'xatol': 0.1,
'initial_simplex': np.array(initial_simplex)})
return result
def _get_simplex(self, x0, step):
simplex = []
for i in range(len(x0)):
point = copy(x0)
point[i] -= step
simplex.append(point)
point2 = copy(x0)
point2[-1] += step
simplex.append(point2)
return simplex
def _call_model(self, x):
print(x, type(x))
for i, value in enumerate(x):
self.lst_args[i].reset_val(value)
input_x = np.array([i.evaluate() for i in self.lst_args])
prediction = self.clf.predict([input_x])
return prediction[0]
我可以如下定义你的问题(一定要按照与最终列表相同的顺序定义输入,否则并不是所有的值都会在优化器中正确更新!):
A = 5
b = 2
thresh_a = 5
thresh_b = 10
thresh_c = 10.1
thresh_m = 4
thresh_n = 6
u = InputType()
v = InputType()
w = InputType()
x = InputType(minimum=thresh_m, maximum=thresh_n)
y = InputType(value = b, multipliers=([[x, A]]))
z = InputType(relations=[[y, max, thresh_a, 4], [y, min, thresh_b, 3.5], [y, max, thresh_c, 3.7]])
example = Example([u, v, w, x, y, z])
调用结果:
result = example.optimize()
for i, value in enumerate(result.x):
example.lst_args[i].reset_val(value)
print(f"final values are at: {[i.evaluate() for i in example.lst_args]}: {result.fun)}")
我训练了一个 Lightgbm 模型来学习对数据集进行排序。该模型预测样本的相关性得分。所以预测越高越好。现在模型已经学习了,我想找到一些能给我最高预测分数的特征的最佳值。
所以,假设我有功能 u,v,w,x,y,z
,我想优化的功能是 x,y,z
。
maximize f(u,v,w,x,y,z) w.r.t features x,y,z where f is a lightgbm model
subject to constraints :
y = Ax + b
z = 4 if y < thresh_a else 4-0.5 if y >= thresh_b else 4-0.3
thresh_m < x <= thresh_n
数字是随机组成的,但约束是线性的。
Objective 关于 x
的函数如下所示:
所以这个函数非常尖锐,不流畅。我也没有梯度信息,因为 f
是一个 lightgbm 模型。
使用
class ProductOptimization:
def __init__(self, estimator, features_to_change, row_fixed_values,
bnds=None):
self.estimator = estimator
self.features_to_change = features_to_change
self.row_fixed_values = row_fixed_values
self.bounds = bnds
def get_sample(self, x):
new_values = {k:v for k,v in zip(self.features_to_change, x)}
return self.row_fixed_values.replace({k:{self.row_fixed_values[k].iloc[0]:v}
for k,v in new_values.items()})
def _call_model(self, x):
pred = self.estimator.predict(self.get_sample(x))
return pred[0]
def constraint1(self, vector):
x = vector[0]
y = vector[2]
return # some float value
def constraint2(self, vector):
x = vector[0]
y = vector[3]
return #some float value
def optimize_slsqp(self, initial_values):
con1 = {'type': 'eq', 'fun': self.constraint1}
con2 = {'type': 'eq', 'fun': self.constraint2}
cons = ([con1,con2])
result = minimize(fun=self._call_model,
x0=np.array(initial_values),
method='SLSQP',
bounds=self.bounds,
constraints=cons)
return result
我得到的结果总是围绕着最初的猜测。我认为这是因为函数的非平滑性以及缺少任何对 SLSQP 优化器很重要的梯度信息。有什么建议我应该如何处理这类问题?
自从我上次写了一些严肃的代码以来已经过去了很长时间,所以如果我不完全清楚每件事的作用,我深表歉意,请随时要求更多解释
进口:
from sklearn.ensemble import GradientBoostingRegressor
import numpy as np
from scipy.optimize import minimize
from copy import copy
首先,我定义了一个新的 class,它允许我轻松地重新定义值。这个 class 有 5 个输入:
- 值:这是 'base' 值。在你的等式
y=Ax + b
中,它是b
部分 - 最小值:这是此类型将评估为的最小值
- 最大值:这是此类型将评估为 的最大值
- 乘数:第一个棘手的。它是其他 InputType 对象的列表。第一个是输入类型,第二个是乘法器。在你的例子中
y=Ax +b
你会得到[[x, A]]
,如果等式是y=Ax + Bz + Cd
它将是[[x, A], [z, B], [d, C]]
- 关系:最棘手的一个。它也是其他 InputType 对象的列表,它有四个项目:第一个是输入类型,第二个定义它是否是您使用
min
的上边界,如果它是您使用max
的下边界。列表中的第三项是边界的值,第四项是与之相连的输出值
当心 如果您将输入值定义得太奇怪,我确定会有奇怪的行为。
class InputType:
def __init__(self, value=0, minimum=-1e99, maximum=1e99, multipliers=[], relations=[]):
"""
:param float value: base value
:param float minimum: value can never be lower than x
:param float maximum: value can never be higher than y
:param multipliers: [[InputType, multiplier], [InputType, multiplier]]
:param relations: [[InputType, min, threshold, output_value], [InputType, max, threshold, output_value]]
"""
self.val = value
self.min = minimum
self.max = maximum
self.multipliers = multipliers
self.relations = relations
def reset_val(self, value):
self.val = value
def evaluate(self):
"""
- relations to other variables are done first if there are none then the rest is evaluated
- at most self.max
- at least self.min
- self.val + i_x * w_x
i_x is input i, w_x is multiplier (weight) of i
"""
for term, min_max, value, output_value in self.relations:
# check for each term if it falls outside of the expected terms
if min_max(term.evaluate(), value) != term.evaluate():
return self.return_value(output_value)
output_value = self.val + sum([i[0].evaluate() * i[1] for i in self.multipliers])
return self.return_value(output_value)
def return_value(self, output_value):
return min(self.max, max(self.min, output_value))
使用它,您可以修复从优化器发送的输入类型,如 _call_model
:
class Example:
def __init__(self, lst_args):
self.lst_args = lst_args
self.X = np.random.random((10000, len(lst_args)))
self.y = self.get_y()
self.clf = GradientBoostingRegressor()
self.fit()
def get_y(self):
# sum of squares, is minimum at x = [0, 0, 0, 0, 0 ... ]
return np.array([[self._func(i)] for i in self.X])
def _func(self, i):
return sum(i * i)
def fit(self):
self.clf.fit(self.X, self.y)
def optimize(self):
x0 = [0.5 for i in self.lst_args]
initial_simplex = self._get_simplex(x0, 0.1)
result = minimize(fun=self._call_model,
x0=np.array(x0),
method='Nelder-Mead',
options={'xatol': 0.1,
'initial_simplex': np.array(initial_simplex)})
return result
def _get_simplex(self, x0, step):
simplex = []
for i in range(len(x0)):
point = copy(x0)
point[i] -= step
simplex.append(point)
point2 = copy(x0)
point2[-1] += step
simplex.append(point2)
return simplex
def _call_model(self, x):
print(x, type(x))
for i, value in enumerate(x):
self.lst_args[i].reset_val(value)
input_x = np.array([i.evaluate() for i in self.lst_args])
prediction = self.clf.predict([input_x])
return prediction[0]
我可以如下定义你的问题(一定要按照与最终列表相同的顺序定义输入,否则并不是所有的值都会在优化器中正确更新!):
A = 5
b = 2
thresh_a = 5
thresh_b = 10
thresh_c = 10.1
thresh_m = 4
thresh_n = 6
u = InputType()
v = InputType()
w = InputType()
x = InputType(minimum=thresh_m, maximum=thresh_n)
y = InputType(value = b, multipliers=([[x, A]]))
z = InputType(relations=[[y, max, thresh_a, 4], [y, min, thresh_b, 3.5], [y, max, thresh_c, 3.7]])
example = Example([u, v, w, x, y, z])
调用结果:
result = example.optimize()
for i, value in enumerate(result.x):
example.lst_args[i].reset_val(value)
print(f"final values are at: {[i.evaluate() for i in example.lst_args]}: {result.fun)}")