这个系列是记录笔者在使用CPLEX过程中遇到的一些小问题和相应的解决方案。对于不同的求解器未必有相同的功能,仅供参考。
在使用求解器求解混合整数规划问题时,我们有时候想要的并不是最优的解,而是目标函数值在一定gap内的一组解。
在这种情景下,我们可以使用CPLEX的solution pool功能。
CPLEX中有两种产生solution pool的方式:
- 在默认模式下,进行branch-and-cut的过程中,求解器如果遇到符合要求的解,会把它加入solution pool中,当然在过程中由于并没有找到最优解,这里加入solution pool的解有一些可能不符合要求,需要在后续进行删除;
- 用
populate
来生成solution pool:在这种模式下,求解器首先找到最优解,然后在已知最优解的基础上,通过一些启发式算法来寻找符合要求的解来加入solution pool中。
通过设置下面几个参数,我们就可以得到一个solution pool:
-
model.parameters.mip.pool.intensity
,这个参数可以取1-4的值,代表筛选solution pool时的枚举强度(其实也就是在搜索过程中搜索的深度)。设置为1时,不会影响求解器的速度也不用存储额外信息,求解器在求解过程中“顺便”将遇到的一些符合要求的解加入解池;设置为2时,在branch-and-cut中生成树结构时额外存储一些信息,以便在求解之后快速找回一些符合要求的解,这个模式会额外使用一定内存,但是不会对明显影响优化求解速度;设置为3时,搜索深度会更深,也会额外存储一些信息,因此优化求解速度会变慢,同时会额外利用空间;设置为4时,全枚举所有符合要求的解,这种模式下求解速度会变得很慢。 -
model.parameters.mip.pool.relgap
,这个参数控制solution pool中的解相对于最优解的gap。例如说我们的最优解目标函数值为10,设置relgap为0.2,那么在枚举解时,如果遇到目标函数值大于等于8的解,这个解就会被放入solution pool中。 -
model.parameters.mip.pool.capacity
,这个参数控制solution pool的大小(也就是解池能容纳的最大解个数)。当搜索到的符合要求的解超过解池大小时,会执行replacement strategy去替换解池中的解。 -
model.parameters.mip.pool.replace
,这个参数可以取1-3的整数值。1是默认的替换策略,在这个模式下会用新搜索到的符合要求的解替换解池中最早搜索到的解;在模式2下,会替换掉解池中目标函数值最差的解;在模式3下,会根据解池的diversity,替换掉解池中对diversity贡献最小的解。 -
parameters.mip.limits.populate
,这个参数控制使用populate进行解池构建时,每次调用populate能获得的最大解池规模。
我们可以以一个简单的背包问题为例,来看一下如何使用solution pool功能。
给定问题如下:
物品编号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
价值 | 12 | 3 | 10 | 3 | 6 | 2 | 3 |
重量 | 5 | 4 | 7 | 2 | 6 | 1 | 2 |
我们现在有一个最大负载为10的背包,求能够装入背包的物品的最大价值。
在求最值的情况下,无论使用求解器建模求解还是用动态规划,都比较容易能获得最大价值。但是如果我们想要知道所有价值落在区间 [80%*maxValue, maxValue]的解呢?
我们可以使用如下代码让CPLEX帮我们生成一个solution pool:
from docplex.mp.model import Model
# --------------------------------
# 参数
# --------------------------------
values: list = [12, 3, 10, 3, 6, 2, 3] # 物品的价值
weights: list = [5, 4, 7, 2, 6, 1, 2] # 物品的重量
capacity: int = 10 # 背包的最大重量
lower_bound: float = 0.8 # 需要所有 0.8*maxObj ~ Obj的解
# --------------------------------
# 建立模型
# --------------------------------
# 实例化模型
mdl = Model("Knapsack Prob")
# 添加决策变量: x[i] - 是否选择第i个物品
idx: list = list(range(1, len(values) + 1))
x = mdl.binary_var_dict(idx, name='x')
# 添加约束 -- 选择的物品重量不能超过背包能承受的最大重量
mdl.add_constraint(mdl.sum(x[i] * weights[i - 1] for i in idx) <= capacity)
# 目标函数 -- 最大化背包内物品的价值
mdl.maximize(mdl.sum(x[i] * values[i - 1] for i in idx))
# --------------------------------
# 设置solution pool参数
# --------------------------------
mdl.parameters.mip.pool.intensity = 4
mdl.parameters.mip.pool.relgap = 1 - lower_bound
mdl.parameters.mip.limits.populate = len(weights) ** 3
mdl.parameters.timelimit = 30 # 设置一下最大求解时间,防止跑的时间太久
solution_pool = mdl.populate(log_output=True)
# --------------------------------
# 输出结果
# --------------------------------
if not solution_pool:
raise Exception("No solution found for the problem")
else:
n_feasible_sols: int = solution_pool.size
print("{} solutions in solution pool".format(n_feasible_sols))
for i in range(n_feasible_sols):
print("----Solution {:d}".format(i))
active_var_index = [idx + 1 for idx, var in enumerate(solution_pool._solutions[i].get_all_values()) if
var > 0.9]
print("Selected Assignment Units: {}".format(active_var_index))
print("Objective value of the solution: {:.2f}".format(solution_pool._solutions[i].objective_value))
在运行之后,得到结果:
5 solutions in solution pool
----Solution 0
Selected Assignment Units: [1, 4, 6, 7]
Objective value of the solution: 20.00
----Solution 1
Selected Assignment Units: [1, 6, 7]
Objective value of the solution: 17.00
----Solution 2
Selected Assignment Units: [1, 4, 7]
Objective value of the solution: 18.00
----Solution 3
Selected Assignment Units: [1, 4, 6]
Objective value of the solution: 17.00
----Solution 4
Selected Assignment Units: [1, 2, 6]
Objective value of the solution: 17.00
可以看到除了最优解[1, 4, 6, 7]之外,解池中还包含所有目标函数值 > 20 * 0.8的解。