作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Shanglun Wang's profile image

Shanglun Wang

Sean是一个充满激情的通晓多种语言的人:一个全栈向导、系统管理员和数据科学家. 他还开发了市场情报软件.

专业知识

Previously At

CB的见解
分享

运筹学和凸优化是应用数学的一个领域,多年来已经发现了广泛的商业应用. 随着计算能力变得更加廉价和广泛可用, 研究人员开始构建越来越复杂的优化算法,以帮助他们做出更好的决策. 今天, 运筹学驱动的应用程序为从全球物流路线到能源行业的电力生产平滑提供了动力.

随着底层技术变得越来越复杂, 一套新的工具已经被创造出来,以帮助研究人员和开发人员更有效地工作. 这些工具, such as AMPL, CVXPY, 和果肉, allow developers to quickly 定义, 构建, 并运行优化算法和接口与各种各样的求解器.

然而, 虽然优化技术和业务需求的复杂性在不断增长, 这些工具中的大多数或多或少保持不变,并且没有足够快地发展以满足行业需求. 结果是, 发展中, 管理, 调试, 而调整这些算法通常需要大量的认知开销, 尤其是在瞬息万变的商业环境中.

今天, I’d like to introduce HorusLP, a Python optimization library 这有助于算法开发工作流的架构. 我们将讨论该工具旨在解决的问题, 然后提供Python库的快速概述, 我们会建立一些优化算法的例子.

优化算法开发者面临的问题

大多数开发人员面临的一个长期问题是如何在构建一个可维护的, 非常高效。, 习惯使用软件,并在项目的时间限制内交付产品. 无论您是在开发基于浏览器的应用程序, web API, 或者用户身份验证微服务, 在实现目标的“正确”方式和“快速”方式之间往往存在权衡. 随着产品复杂性的增加,这种内在的权衡变得更加突出.

Simplex algorithm illustration

In most disciplines, 开发人员可以通过选择一个有助于架构的框架或库来缓解这个问题. In the web-facing front-end, many developers choose React or Angular; in the world of API development, 软件工程师可以选择Django, ASP.NET MVC, or Play, among many others. 然而, 说到不起眼的优化算法开发者, 很少有架构工具可以帮助管理架构的复杂性. 开发人员可以自己管理变量、约束和各种目标. 更重要的是, 运筹学算法通常难以内省, exacerbating the 概率lem.

HorusLP的主要目的是为开发优化算法提供一个架构框架. By providing structural consistency, 该框架使组织更容易,并允许开发人员专注于他们最擅长的事情:构建算法.

典型的优化工作流程挑战

在开发OR算法时,有几个主要的复杂性来源:

Complexity from 变量

  • 通常必须添加变量以适应额外的业务需求,并且没有简单的方法来跟踪它们,以便在模型定义和以后的报告中使用.
  • 需要对相关变量进行分组和跟踪,并且没有一种明显的方法来管理它们.

Complexity from 约束

  • 需要添加和删除约束以支持各种场景并执行调试, 但是没有一个明显的地方可以添加或删除约束.
  • 约束通常是相互关联/依赖的, 没有自然的方式来表达他们的关系.

Complexity from 目标

  • 如果客观表达式包含多个组件,那么它就会变得笨拙. 如果对不同的组成部分进行加权,这种情况可能会加剧, 权重需要根据业务需求进行调整.

Complexity from 调试

  • 在开发过程中,没有一种简单的方法可以看到模型的结果. 开发人员必须显式打印新的变量和约束值才能看到结果. 这会导致重复的代码和更慢的开发.
  • 当添加约束导致模型变得不可行的时候, 约束导致模型变得不可行的原因可能并不明显. 通常, 开发人员必须排除各种限制,并通过试验和错误来寻找不兼容性

HorusLP希望通过提供结构使这些挑战更易于管理, 工具, 提高开发人员生产力和产品可维护性的最佳实践.

HorusLP教程:优化算法和API概述

废话不多说,让我们深入了解一下HorusLP库可以为您做些什么!

由于HorusLP是基于Python和PuLP的,我们希望使用pip来安装它们. 在命令行中运行以下命令:

Pip install horuslp pulp

安装完成后,让我们打开一个Python文件. We will implement the knapsack 概率lem 从我的 之前关于运筹学的文章.

Python优化小吃问题说明

HorusLP库有一个非常简单的声明性API和非常少的样板文件. 让我们从导入必要的类和变量开始:

from horuslp.核心.变量导入BinaryVariable #我们将使用二进制变量, 所以我们将导入BinaryVariable类
from horuslp.核心 import Constraint, VariableManager, 问题, 我们还需要导入约束类, variable manager class, the main 概率lem class, 并且用目标类来定义目标. 
from horuslp.核心.常量导入MAXIMIZE #,因为我们要最大化结果值, we want to import this constant

一旦我们导入了所有的变量,让我们定义这个问题所需的变量. 我们通过创建一个变量管理器子类并用二进制变量填充它来实现:

类KnapsackVariables (VariableManager):
    Vars = [
        BinaryVariable('camera'), #第一个参数是变量的名称
        BinaryVariable('figurine'),
        BinaryVariable('cider'),
        BinaryVariable(“角”)
    ]

现在定义了变量,让我们定义约束. 我们通过创建主约束类的子类并实现它的“定义”函数来创建约束.

class SizeConstraint(Constraint):
    定义(自我, camera, figurine,苹果酒,角):
        return 2 * camera + 4 * figurine + 7 * cider + 10 * 角 <= 15

在定义函数中,您可以按名称请求所需的变量. 框架将在变量管理器中找到该变量,并将其传递给定义函数.

约束实现之后,我们就可以实现目标了. 由于它是一个简单的目标,我们将使用 ObjectiveComponent:

类ValueObjective (ObjectiveComponent):
    定义(自我, camera, figurine,苹果酒,角):
        返回5 *相机+ 7 *雕像+ 2 *苹果酒+ 10 *喇叭

定义函数的设置与约束类的定义函数非常相似. 但是,我们返回的不是约束表达式,而是仿射表达式.

既然已经定义了变量、约束和目标,那么让我们来定义模型:

class Knapsack问题(问题):
    变量 = KnapsackVariables
    objective = ValueObjective
    约束 = [SizeConstraint]
    sense = MAXIMIZE

To 构建 the model, 我们创建一个类,它是问题类的子类,并指定变量, 目标, 约束, 和 the sense. 有了指定的问题,我们可以解决这个问题:

概率 = Knapsack问题()
概率.解决()

求解之后,我们可以使用问题类来打印结果 print_results 函数. 也可以访问特定变量的值 result_变量 class.

概率.print_results()
print(概率.result_变量)

运行脚本,您应该看到以下输出:

Knapsack问题: Optimal
相机0.0
小雕像1.0
苹果酒0.0
角1.0
ValueObjective: 17.00
SizeConstraint: 14.00
{'camera': 0.0, 'figurine': 1.0, 'cider': 0.0, “角”: 1.0}

You should see the 概率lem status, the final value of the 变量, the objective value, 约束表达式的值. 我们还将变量的结果值视为一个字典.

这就是我们的第一个HorusLP问题,大约35行!

在接下来的示例中,我们将探索HorusLP库的一些更复杂的特性.

Using VariableGroups

有时,变量是相关的,属于一个逻辑组. 在背包问题的情况下,所有变量都可以放到一个对象组中. 我们可以重构代码以使用变量组. 确保保存上一节中的代码,因为我们将在后续教程中引用它!

像这样修改import语句:

from horuslp.核心.变量导入BinaryVariableGroup
from horuslp.核心 import Constraint, VariableManager, 问题, ObjectiveComponent
from horuslp.核心.constants import MAXIMIZE

现在我们还需要像这样改变backpack变量声明:

类KnapsackVariables (VariableManager):
    Vars = [
        BinaryVariableGroup('objects', [
            “相机”,
            “雕像”,
            “酒”,
            “角”
        ])
    ]

第一个参数是变量组的名称, 第二个变量是该组中变量的名称列表.

现在我们需要改变约束条件和客观定义. 而不是询问每个人的名字, we will as for the variable group, 哪个将作为字典传入,其中键是名称,值是变量. 改变约束和客观定义,如下所示:

class SizeConstraint(Constraint):
    def 定义(自我, objects):
        return 2 * objects['camera'] + 4 * objects['figurine'] + 7 * objects['cider'] + 10 * objects[“角”] <= 15

类ValueObjective (ObjectiveComponent):
    def 定义(自我, objects):
        返回5 *对象['camera'] + 7 *对象['figurine'] + 2 *对象['苹果酒']+ 10 *对象[“角”]

现在我们可以使用相同的问题定义并运行命令:

class Knapsack问题(问题):
    变量 = KnapsackVariables
    objective = ValueObjective
    约束 = [SizeConstraint]
    sense = MAXIMIZE


概率 = Knapsack问题()
概率.解决()
概率.print_results()
print(概率.result_变量)

You should see this in your output:

Knapsack问题: Optimal
objects[camera] 0.0
objects[figurine] 1.0
objects[cider] 0.0
objects[角] 1.0
ValueObjective: 17.00
SizeConstraint: 14.00
{'objects': {'camera': 0.0, 'figuring': 1.0, 'cider': 0.0, “角”: 1.0}}

Managing Multiple Constraints

具有单一约束的模型相对较少. 当使用多个约束时, 最好将所有约束放在一个地方,这样就可以轻松地跟踪和管理它们. HorusLP makes that natural.

假设, 例如, 我们想看看如果我们强迫模特在我们的背包里加一个相机,结果会有什么变化. 我们将实现一个额外的约束,并将其添加到问题定义中.

回到我们在第一个教程中实现的模型. Add the following constraint:

类MustHaveItemConstraint(约束):
    def 定义(自我, camera):
        return camera >= 1

要将约束添加到模型中,我们只需将其添加到问题定义中,如下所示:

class Knapsack问题(问题):
    变量 = KnapsackVariables
    objective = ValueObjective
    约束 = [
        SizeConstraint,
        MustHaveItemConstraint #只需添加这一行:)
    ]
    sense = MAXIMIZE

运行该问题,您应该看到以下输出:

Knapsack问题: Optimal
相机1.0
小雕像0.0
苹果酒0.0
角1.0
ValueObjective: 15.00
SizeConstraint: 12.00
MustHaveItemConstraint: 1.00

您应该在标准输出中看到新的约束被打印出来, 以及改变后的最优变量值.

管理依赖约束和约束组

约束通常是相互关联的,因为它们是相互依赖的, 或者因为他们在逻辑上属于同一个群体.

例如, 如果要设置约束来限制一组变量的绝对值之和, 您必须首先指定约束,以表示预期变量之间的绝对值关系, 然后指定绝对值极限. 有时, 您需要对一组变量应用类似的约束,以表示变量之间的特定关系.

To express these groupings, 我们可以使用约束定义的依赖约束特性. 要了解如何使用依赖约束特性,请重构 SizeConstraint of the previous 概率lem like so:

class SizeConstraint(Constraint):
    dependent_约束 = [MustHaveItemConstraint]

    定义(自我, camera, figurine,苹果酒,角):
        return 2 * camera + 4 * figurine + 7 * cider + 10 * 角 <= 15

现在为了测试相关约束是否自动实现,我们取 MustHaveItemConstraint out of the 概率lem definition:

class Knapsack问题(问题):
    变量 = KnapsackVariables
    objective = ValueObjective
    约束 = [
        SizeConstraint,
    ]
    sense = MAXIMIZE

然后再次运行代码,您应该在标准输出中看到以下内容:

Knapsack问题: Optimal
相机1.0
小雕像0.0
苹果酒0.0
角1.0
ValueObjective: 15.00
SizeConstraint: 12.00
MustHaveItemConstraint: 1.00

Looks like the MustHaveItemConstraint is implemented! 有关如何使用依赖约束的更复杂示例, 请参阅本教程末尾的人员配置示例.

管理多重加权目标

经常, 在我们优化算法的发展过程中, 我们将遇到一个由多个部分组成的客观表达. As part of our experimentation, 我们可以改变各种客观成分的权重,使算法偏向于期望的结果. 的 CombinedObjective 提供一种干净简单的方式来表达.

假设我们想让算法偏向于选择小雕像和苹果酒. 让我们重构前一节中的代码 CombinedObjective.

First, import the CombinedObjective class like so:

from horuslp.核心 import CombinedObjective

我们可以像这样实现一个独立的目标组件:

类ILoveCiderFigurineObjectiveComponent (ObjectiveComponent):
    def 定义(自我, figurine, cider):
        return figurine + cider

现在我们可以通过执行a来结合价值目标和苹果酒/雕像目标 CombinedObjective:

class Combined(CombinedObjective):
    目标 = [
        (ILoveCiderFigurineObjectiveComponent, 2), # first argument is the objective, second argument is 重量
        (ValueObjectiveComponent, 1)
    ]

现在让我们像这样改变问题的定义:

class Knapsack问题(问题):
    变量 = KnapsackVariables
    objective = Combined
    约束 = [SizeConstraint]
    sense = MAXIMIZE

运行该问题,您应该看到以下输出:

Knapsack问题: Optimal
相机1.0
小雕像1.0
酒1.0
角0.0
结合:18.00
ILoveCiderFigurineObjectiveComponent: 2.00 * 2
ValueObjectiveComponent: 14.00 * 1
SizeConstraint: 13.00
MustHaveItemConstraint: 1.00

输出将列出综合的目标值, 每个客观成分的值, 重量, 当然还有所有约束条件的值.

Finding Incompatible Constraints

在开发算法时,我们经常遇到不可行的模型. 如果模型很复杂,就很难确定为什么模型突然变得不可行的原因. 在这些情况下,HorusLP有一个方便的工具可以帮助您.

假设我们添加了约束条件,最终得到了以下约束条件:

class SizeConstraint(Constraint):
    定义(自我, camera, figurine,苹果酒,角):
        return 2 * camera + 4 * figurine + 7 * cider + 10 * 角 <= 15


class SizeConstraint2(Constraint):
    定义(自我, camera, figurine,苹果酒,角):
        return 2 * camera + 4 * figurine + 7 * cider + 10 * 角 <= 20


类MustHaveItemConstraint(约束):
    def 定义(自我, cider):
        return cider >= 1

类IncompatibleConstraint1(约束):
    def 定义(自我, camera):
        return camera >= 1


类IncompatibleConstraint2(约束):
    def 定义(自我, camera):
        return camera <= 0

我们对背包中物品的总尺寸有几个限制, 要求背包里必须有苹果酒的限制, 还有一组不兼容的约束要求相机既要在背包里又不能在背包里. (当然, in a real-world algorithm, 约束通常不那么明显,并且涉及复杂的变量关系和约束.)

还假设约束按以下方式分组, 也许这让检测变得更加困难:

类CombinedConstraints1(约束):
    dependent_约束 = [size econstraint2, IncompatibleConstraint1]


类CombinedConstraints2(约束):
    dependent_约束 = [size econstraint2, IncompatibleConstraint2]

# MustHaveItemConstraint将独立包含在问题定义中

Here is the 概率lem definition:

class Knapsack问题(问题):
    变量 = KnapsackVariables
    objective = ValueObjective
    约束 = [CombinedConstraints1, CombinedConstraints2, MustHaveItemConstraint]
    sense = MAXIMIZE

运行该问题,您应该看到以下结果:

Knapsack问题: Infeasible

哦,不! What do we do? If we are using most tools, 我们必须开始一个困难的调试阶段,在这个阶段我们一个接一个地缩小潜在的冲突约束. 幸运的是, HorusLP有一个不兼容搜索功能,可以帮助您更快地缩小不兼容约束的范围. 使用不兼容搜索特性的最简单方法是更改 print_results call thusly:

概率.print_results(find_infeasible=True)

Simple as that! 运行代码,现在你应该看到以下输出:

Knapsack问题: Infeasible
Finding incompatible 约束...
不兼容的约束:('CombinedConstraints1', 'CombinedConstraints2')

伟大的! Now we have established that MustHaveItemConstraint 不就是不可行的原因和那个问题的原因吗 CombinedConstraints1CombinedConstraints2.

That gives us some information, 但是在组合约束之间有四个相关约束. 我们能确定四个约束中哪一个是不兼容的吗? 嗯,是的. 修改您的 print_results call thusly:

概率.print_results (find_infeasible = True, deep_infeasibility_search = True)

这将使不可行性搜索扩展相关约束,这样我们就能更详细地了解导致不可行性的原因. 运行此命令,您应该看到以下输出:

Knapsack问题: Infeasible
Finding incompatible 约束...
不兼容约束:('IncompatibleConstraint1', 'IncompatibleConstraint2')

虽然每次都试图进行深入的不可行性搜索, 对于具有大量总约束的现实问题, 深度不可行性搜索会变得非常耗费资源和时间. 因此, 最好运行基本的不可行性搜索以缩小可能性,并在进行一些手动调查后在较小的子集上运行深度不可行性搜索.

Building 算法s from Data Files

在构建模型时,我们很少有硬编码每个约束和变量的奢侈. 通常,我们的程序必须足够灵活,能够根据输入数据更改模型. 假设我们想要从以下JSON中构建我们的背包问题,而不是硬编码输入:

{
  “物品”:(
    {"name": "camera", "value": 5, "weight": 2},
    {"name": "figurine", "value": 7, "weight": 4},
    {"name": "苹果", "value": 2, "weight": 7},
    {"name": "角", "value": 10, "weight": 10},
    {"name": "banana", "value": 9, "weight": 2}
  ],
  "capacity": 15
}

我们可以依靠kwargs对我们为约束和目标实现的“定义”函数的支持来做到这一点.

让我们修改简单背包问题的代码(我们在本教程的第1节中实现了这个问题). 首先,让我们将JSON字符串放入文件中. 当然, 我们通常会从外部来源读取它, 但是为了简单起见,我们将所有内容保存在一个文件中. Add the following to your code:

Json = " '
{
  “物品”:(
    {"name": "camera", "value": 5, "weight": 2},
    {"name": "figurine", "value": 7, "weight": 4},
    {"name": "苹果", "value": 2, "weight": 7},
    {"name": "角", "value": 10, "weight": 10},
    {"name": "banana", "value": 9, "weight": 2}
  ],
  "capacity": 15
}
'''

我们还要确保我们的程序能够解析它. 将以下内容添加到import语句中:

进口json

现在,让我们修改变量设置代码:

mip_cfg = json.负载(JSON)

类KnapsackVariables (VariableManager):
    Vars = [
        BinaryVariable(i['name']) for i in mip_cfg['items']
    ]

这将为JSON中的每个项定义一个变量,并对其进行适当的命名.

让我们像这样改变约束和客观定义:

类CapacityConstraint(约束):
    def 定义(自我, * * kwargs):
        Item_dict = {i['name']: i['weight'] for i in mip_cfg['items']}
        返回sum(kwargs[name] * item_dict[name] for name in kwargs) <= mip_cfg['capacity']


类ValueObjective (ObjectiveComponent):
    def 定义(自我, * * kwargs):
        Item_dict = {i['name']: i['value'] for i in mip_cfg['items']}
        返回sum(kwargs[name] * item_dict[name] for name in kwargs)

By asking for * * kwargs instead of specific 变量, the 定义 函数按名称获取包含所有变量的字典. 然后约束定义函数可以访问字典中的变量.

注意: For variable groups, 它将是一个嵌套的字典,其中第一级是组名,第二级是变量名.

的 rest is pretty straightforward:

class Knapsack问题(问题):
    变量 = KnapsackVariables
    约束 = [CapacityConstraint]
    objective = ValueObjective
    sense = MAXIMIZE


概率 = Knapsack问题()
概率.解决()
概率.print_results()

运行这个程序,你应该看到以下输出:

Knapsack问题: Optimal
相机1.0
小雕像0.0
苹果0.0
角1.0
香蕉1.0
ValueObjective: 24.00
CapacityConstraint: 14.00

Defining Custom Metrics in HorusLP

有时, 用于调试和报告目的, 我们将构建不直接在目标或约束中表示的自定义度量. HorusLP有一个特性可以使指定自定义指标变得简单.

假设我们想要跟踪上一节中的模型放入背包的水果数量. 为了跟踪这一点,我们可以定义一个自定义指标. 让我们从导入Metrics基类开始:

From horuslp.核心 import Metric

Now let’s 定义 the custom metric:

class NumFruits(Metric):
    name = "Number of Fruits"

    def 定义(自我, 苹果, banana):
        return 苹果 + banana

As you can see, 定义的接口看起来与约束和目标组件类的接口非常相似. 如果到目前为止您一直在学习本教程,那么您应该对此相当熟悉.

现在我们需要将度量添加到问题定义中. 这里的接口与约束定义非常相似:

class Knapsack问题(问题):
    变量 = KnapsackVariables
    约束 = [CapacityConstraint]
    objective = ValueObjective
    metrics = [NumFruits]
    sense = MAXIMIZE

运行此命令,您应该看到以下输出:

Knapsack问题: Optimal
相机1.0
小雕像0.0
苹果0.0
角1.0
香蕉1.0
ValueObjective: 24.00
CapacityConstraint: 14.00
Number of Fruits: 1.00

你可以看到底部印着水果的数量.

解决一个更复杂的问题:两个背包

让我们看一个稍微复杂一点的例子. 假设我们有一个包和一个手提箱,而不是一个单独的背包. 我们也有两类物体,耐用的和易碎的. 这种行李箱既能装易碎品,又能装耐用品,保护性更强. 另一方面,袋子只能装耐用品. 假设项目的数据以以下JSON格式给出:

{
  "fragile": [
    {"name": "camera", "value": 5, "weight": 2},
    {"name": "眼镜", "value": 3, "weight": 4},
    {"name": "苹果", "value": 2, "weight": 7},
    {"name": "pear", "value": 5, "weight": 3},
    {"name": "banana", "value": 9, "weight": 2}
  ],
  "durable": [
    {"name": "figurine", "value": 7, "weight": 4},
    {"name": "角", "value": 10, "weight": 10},
    {"name": "leatherman", "value": 10, "weight": 3}
  ],
  "suitcase_capacity": 15,
  "bag_capacity": 20
}

让我们看看这是如何改变模型的. 让我们从一张白纸开始,因为模型将会非常不同. 从问题的设置开始:

进口json
from horuslp.核心.变量导入BinaryVariableGroup
from horuslp.核心 import Constraint, VariableManager, 问题, Metric, ObjectiveComponent
from horuslp.核心.constants import MAXIMIZE

Json = " '
{
  "fragile": [
    {"name": "camera", "value": 5, "weight": 2},
    {"name": "眼镜", "value": 3, "weight": 4},
    {"name": "苹果", "value": 2, "weight": 7},
    {"name": "pear", "value": 5, "weight": 3},
    {"name": "banana", "value": 9, "weight": 2}
  ],
  "durable": [
    {"name": "figurine", "value": 7, "weight": 4},
    {"name": "角", "value": 10, "weight": 10},
    {"name": "leatherman", "value": 10, "weight": 3}
  ],
  "suitcase_capacity": 15,
  "bag_capacity": 20
}
'''
mip_cfg = json.负载(JSON)

Now let’s set up the 变量. 我们将为每个可能的物品/容器组合设置一个二进制变量.

类KnapsackVariables (VariableManager):
    Vars = [
        手提箱既能装易碎品,也能装耐用品 
        BinaryVariableGroup('suitcase_f', [i['name'] for i in mip_cfg['fragile']]),
        BinaryVariableGroup('suitcase_d', [i['name'] for i in mip_cfg['durable']]),
        # bag can only hold durable goods.
        BinaryVariableGroup('bag_d', [i['name'] for i in mip_cfg['durable']])
    ]

现在我们要为手提箱和袋子实现重量约束.

类SuitcaseCapacityConstraint(约束):
    定义(自我, suitcase_d, suitcase_f):
        Fragile_weight = sum([suitcase_f[i['name']] * i['weight'] for i in mip_cfg['fragile']])
        Durable_weight = sum([suitcase_d[i['name']] * i['weight'] for i in mip_cfg['durable']])
        return fragile_weight + durable_weight <= mip_cfg['suitcase_capacity']


类BagCapacityConstraint(约束):
    def 定义(自我, bag_d):
        Durable_weight = sum([bag_d[i['name']] * i['weight'] for i in mip_cfg['durable']])
        return durable_weight <= mip_cfg['bag_capacity']

现在我们需要实现一个稍微复杂一点的约束—确保一个物品不会同时放入手提箱和袋子—即, 如果“in the suitcase”变量为1, 那么“in the bag”变量需要为零, 和 vice versa. 当然, 我们要确保允许在两个容器中都没有项的实例.

To add this constraint, 我们需要遍历所有持久项, 找到“在手提箱里”变量和“在包里”变量,并断言这些变量的和小于1.

我们可以很容易地在HorusLP中动态定义依赖约束:

类UniquenessConstraints(约束):
    def __init__(自我):
        super(UniquenessConstraints, 自我).__init__ ()
        为每个持久项调用依赖约束构造函数, 把它们推到相关约束中. 
        dependent_约束 = [自我.在mip_cfg['durable']]中定义unique - ess_constraint
        自我.Dependent_约束 = Dependent_约束

    Def 定义_uniqueness_constraint(自我, item):
        类是python中的一等对象, 所以我们可以在这个函数中定义一个类并返回它
        class UQConstraint(Constraint):
            我们根据约束的对象来命名约束,这样调试起来更容易.
            name = "Uniqueness_%s" % item['name']

            定义(自我,手提箱,行李箱):
                定义函数可以像其他函数一样访问变量
                return suitcase_d[item['name']] + bag_d[item['name']] <= 1

        return UQConstraint

既然已经定义了约束条件,让我们构建目标. 目标很简单,我们从容器中的所有项中获得的所有值的总和. 因此:

类TotalValueObjective (ObjectiveComponent):
    定义(自我, suitcase_f, suitcase_d, bag_d):
        Fragile_value = sum([suitcase_f[i['name']] * i['weight'] for i in mip_cfg['fragile']])
        Durable_value_s = sum([suitcase_d[i['name']] * i['weight'] for i in mip_cfg['durable']])
        Durable_value_d = sum([bag_d[i['name']] * i['weight'] for i in mip_cfg['durable']])
        返回fragile_value + durable_value_s + durable_value_d

让我们还定义一些自定义指标,以便我们可以一目了然地看到放入包和手提箱中的重量, 以及行李箱重量中有多少来自耐用品和易碎品:

类SuitcaseFragileWeightMetric(指标):
    def 定义(自我, suitcase_f):
        返回sum([suitcase_f[i['name']] * i['weight'] for i in mip_cfg['fragile']])


类SuitcaseDurableWeightMetric(指标):
    def 定义(自我, suitcase_d):
        返回sum([suitcase_d[i['name']] * i['weight'] for i in mip_cfg['durable']])


class BagWeightMetric(Metric):
    def 定义(自我, bag_d):
        返回sum([bag_d[i['name']] * i['weight'] for i in mip_cfg['durable']])

现在我们已经完成了所有的部分,让我们实例化问题并运行模型:

class Knapsack问题(问题):
    变量 = KnapsackVariables
    约束 = [SuitcaseCapacityConstraint, BagCapacityConstraint, UniquenessConstraints]
    objective = TotalValueObjective
    metrics = [SuitcaseDurableValueMetric, SuitcaseFragileValueMetric, BagValueMetric]
    sense = MAXIMIZE

概率 = Knapsack问题()
概率.解决()
概率.print_results()

运行此命令,您应该在stdout中看到以下输出:

Knapsack问题: Optimal
suitcase_f[camera] 1.0
suitcase_f[眼镜] 1.0
suitcase_f[苹果] 1.0
suitcase_f[pear] 0.0
suitcase_f[banana] 1.0
suitcase_d[figurine] 0.0
suitcase_d[角] 0.0
suitcase_d[leatherman] 0.0
bag_d[figurine] 1.0
bag_d[角] 1.0
bag_d[leatherman] 1.0
TotalValueObjective: 32.00
SuitcaseCapacityConstraint: 15.00
BagCapacityConstraint: 17.00
Uniqueness_figurine: 1.00
Uniqueness_角: 1.00
Uniqueness_leatherman: 1.00
SuitcaseDurableWeightMetric: 0.00
SuitcaseFragileWeightMetric: 15.00
BagWeightMetric: 17.00

So the camera, 眼镜, 苹果, 行李箱里的香蕉总共有15个重量单位, the figurine, 角, 和皮革人一起放进袋子里,总共有17个重量. 货物的总价值为32个价值单位. Interestingly, 这些耐用品最后都没有装进箱子里, 很可能是因为袋子有足够的容量装下所有的耐用品.

一个更大更现实的场景:人员问题

如果您已经完成了我们的HorusLP教程,那么恭喜您! 现在您对如何使用HorusLP有了一个很好的概念.

然而, 到目前为止,我们研究过的所有例子都是背包问题的排列, 有些要求和参数有点不现实. 让我们一起解决另一个问题,看看HorusLP如何解决一个更现实的问题. 我们将解决在第二部分概述的人员配置问题 my previous Toptal article.

HorusLP教程的人员配置问题说明

In the interest of time, 我们将直接进入模型的最终版本(带有个人冲突), labor regulations, 和临时工津贴),但最初的简单模型的实现也可以在GitHub上获得.

让我们从这个问题开始:

from horuslp.核心.变量导入BinaryVariableGroup, IntegerVariableGroup
from horuslp.核心 import Constraint, VariableManager, 问题, ObjectiveComponent, combinedobject
from horuslp.核心.constants import MINIMIZE

Shift_requirements =[1,4,3,5,2] #每个班次需要的工人数量
每个员工的可用性和工资率
工人= {
    "Melis和re": {
        "availability": [0, 1, 4],
        “成本”:20
    },
    “糠”:{
        "availability": [1, 2, 3, 4],
        “成本”:15
    },
    ”瑟曦":{
        "availability": [2, 3],
        “成本”:35
    },
    "Daenerys": {
        "availability": [3, 4],
        “成本”:35
    },
    "全心全意地":{
        "availability": [1, 3, 4],
        “成本”:10
    },
    “乔”:{
        "availability": [0, 2, 4],
        “成本”:25
    },
    “泰瑞欧”:{
        "availability": [1, 3, 4],
        “成本”:30
    },
    “杰米”:{
        "availability": [1, 2, 4],
        “成本”:20
    },
    " Arya ": {
        "availability": [0, 1, 3],
        “成本”:20
    }
}

可悲的是,下面这些人不能一起工作.
ban_list = {
    ("Daenerys", "Jaime"),
    ("Daenerys", "Cersei"),
    ("Jon", "Jaime"),
    ("Jon", "Cersei"),
    ("Arya", "Jaime"),
    ("Arya", "Cersei"),
    ("Arya", "Melis和re"),
    ("Jaime", "Cersei")
}

#多斯拉克人力资源公司将为我们提供昂贵的临时工
DOTHRAKI_MAX = 10
DOTHRAKI_COST = 45

Now let’s 定义 the 变量, 在这种情况下,决定工人是否应该轮班的二元变量是什么, 整数变量决定我们每班雇佣多少多斯拉克人:

类StaffingVariables (VariableManager):
    Vars = []

    def __init__(自我):
        像依赖约束一样,我们可以在init函数中动态定义变量
        super(StaffingVariables, 自我).__init__ ()
        # regular workers
        varkeys = []
        对于employee, availability_info在workers中.项目():
            对于availability_info['availability']中的移位:
                varkeys.append((employee, shift))
        自我.var.追加(BinaryVariableGroup (employee_shifts, varkeys))
        #多斯拉克人
        Dothraki_keys = [i for i in range(len(shift_requirements))]]
        自我.var.append(IntegerVariableGroup('dothraki_workers', dothraki_keys, 0, DOTHRAKI_COST))

现在让我们实现要求我们为每个班次配备足够人员的约束:

类SufficientStaffingConstraint(约束):
    我们需要配备足够的人员
    dependent_约束 = []

    def __init__(自我):
        超级(SufficientStaffingConstraint,自我).__init__ ()
        对于shift_num, shift_req在enumerate(shift_requirements):
            自我.dependent_约束.追加(自我.构建_shift_constraint (shift_num shift_req))

    Def 构建_shift_constraint(自我, sn, sr):
        class ShiftConstraint(Constraint):
            name = "shift_requirement_%d" % sn

            定义(自我, employee_shifts, dothraki_workers):
                变量= [val为键,val在employee_shifts.项目() if key[1] == sn]
                变量.append(dothraki_workers[sn])
                return sum(变量) >= sr
        return ShiftConstraint

现在,我们需要实现防止特定人员相互协作的约束:

类PersonalConflictsConstraint(约束):
    # some people can't work together
    dependent_约束 = []

    def __init__(自我):
        超级(PersonalConflictsConstraint,自我).__init__ ()
        for person_1, person_2 in ban_list:
            对于range(len(shift_requirements))中的移位:
                自我.dependent_约束.追加(自我.Build_conflict_constraint (person_1, person_2, shift))

    Def 构建_conflict_constraint(自我, p1, p2, s):
        类ConflictConstraint(约束):
            name = "冲突%s_%s_%d" % (p1, p2, s)

            def 定义(自我, employee_shifts):
                If (p1, s) in employee_shifts 和 (p2, s) in employee_shifts:
                    return employee_shifts[p1, s] + employee_shifts[p2, s] <= 1
                #返回True将使约束不做任何事情
        return ConflictConstraint

最后是劳动标准约束. 出于演示的目的,我们将略微不同地实现这个:

类LaborSt和ardsConstraint(约束):
    我们可以让某人一天工作两班以上.
    dependent_约束 = []

    def __init__(自我):
        超级(LaborSt和ardsConstraint,自我).__init__ ()
        for worker in workers.键():
            我们不需要约束生成器函数,但在这些情况下
            我们需要将所需的值设置为类变量并引用它们
            由于python闭包系统的工作方式,# via 自我关键字
            class LaborConstraint(Constraint):
                # we can't use worker directly!
                工人
                name = "labor_st和ard_%s" % worker

                def 定义(自我, employee_shifts):
                    #我们需要使用自我访问worker. 改变自我.wk to worker to see
                    # why we need to do this
                    Worker_Vars = [var for key, var in employee_shifts.项目() if key[0] == 自我.wk]
                    return sum(worker_var) <= 2
            自我.dependent_约束.append(LaborConstraint)

And now let’s set up the 目标. 多斯拉克人的成本和普通员工的成本计算方式截然不同, 所以我们会把它们分成不同的客观成分:

类CostObjective (ObjectiveComponent):
    #这是所有指定worker的成本函数
    定义(自我, employee_shifts, dothraki_workers):
        成本= [
            工人[键[0]]['成本']* var为键,var在employee_shifts.项目()
        ]
        return sum(costs)


类DothrakiCostObjective (ObjectiveComponent):
    # don't forget the Dothrakis
    def 定义(自我, dothraki_workers):
        dothraki_成本= [
            dothraki_workers[sn] * DOTHRAKI_COST在dothraki_workers中为sn
        ]
        return sum(dothraki_costs)


类TotalCostObjective (CombinedObjective):
    目标 = [
        (CostObjective, 1),
        (DothrakiCostObjective, 1)
    ]

现在我们可以定义并运行这个问题:

class Staffing问题(问题):
    变量 = StaffingVariables
    objective = TotalCostObjective
    约束 = [SufficientStaffingConstraint, PersonalConflictsConstraint, LaborSt和ardsConstraint]
    我们这次是最小化,而不是最大化.


if __name__ == '__main__':
    概率 = Staffing问题()
    概率.解决()
    概率.print_results()

运行脚本,您应该看到以下内容:

Staffing问题: Optimal
employee_shifts[('Melis和re', 0)] 0.0
employee_shifts[('Melis和re', 1)] 1.0
employee_shifts[('Melis和re', 4)] 1.0
employee_shifts[('Bran', 1)] 0.0
employee_shifts[('Bran', 2)] 1.0
employee_shifts[('Bran', 3)] 1.0
employee_shifts[('Bran', 4)] 0.0
employee_shifts[('Cersei', 2)] 0.0
employee_shifts[('Cersei', 3)] 0.0
employee_shifts[('Daenerys', 3)] 1.0
employee_shifts[('Daenerys', 4)] 0.0
employee_shifts[('的on', 1)] 1.0
employee_shifts[('的on', 3)] 1.0
employee_shifts[('的on', 4)] 0.0
employee_shifts[('Jon', 0)] 0.0
employee_shifts[('Jon', 2)] 1.0
employee_shifts[('Jon', 4)] 0.0
employee_shifts[('Tyrion', 1)] 1.0
employee_shifts[('Tyrion', 3)] 1.0
employee_shifts[('Tyrion', 4)] 0.0
employee_shifts[('Jaime', 1)] 1.0
employee_shifts[('Jaime', 2)] 0.0
employee_shifts[('Jaime', 4)] 1.0
employee_shifts[('Arya', 0)] 1.0
employee_shifts[('Arya', 1)] 0.0
employee_shifts[('Arya', 3)] 1.0
dothraki_workers[0] 0.0
dothraki_workers[1] 0.0
dothraki_workers[2] 1.0
dothraki_workers[3] 0.0
dothraki_workers[4] 0.0
TotalCostObjective: 335.00
CostObjective: 290.00 * 1
DothrakiCostObjective: 45.00 * 1
shift_requirement_0: 1.00
shift_requirement_1: 4.00
shift_requirement_2: 3.00
shift_requirement_3: 5.00
shift_requirement_4: 2.00
Conflict_Jon_Cersei_2: 1.00
Conflict_Jon_Jaime_2: 1.00
Conflict_Jon_Jaime_4: 1.00
Conflict_Daenerys_Cersei_3: 1.00
Conflict_Daenerys_Jaime_4: 1.00
Conflict_Arya_Jaime_1: 1.00
Conflict_Arya_Cersei_3: 1.00
Conflict_Arya_Melis和re_0: 1.00
Conflict_Arya_Melis和re_1: 1.00
Conflict_Jaime_Cersei_2: 0.00
labor_st和ard_Melis和re: 2.00
labor_st和ard_Bran: 2.00
labor_st和ard_Cersei: 0.00
labor_st和ard_Daenerys: 1.00
labor_st和ard_的on: 2.00
labor_st和ard_Jon: 1.00
labor_st和ard_Tyrion: 2.00
labor_st和ard_Jaime: 2.00
labor_st和ard_Arya: 2.00

如果您将此问题与我们在上一教程中实现的问题进行比较, 您应该看到结果是匹配的.

结束

祝贺你通过了我们的第一个HorusLP教程! 你现在是一个称职的HorusLP从业者.

我希望本文能使您相信构建您的 MIP模型,并且您将在未来的项目中使用HorusLP. 您可以在网站上找到HorusLP源代码以及所有教程的代码 GitHub. 额外的HorusLP文档和教程页面将很快添加到GitHub.

由于HorusLP是一个相当新的项目,我们希望听到您的意见并纳入您的意见. If you have any questions, 评论, or suggestions, 请通过Toptal或使用GitHub上的联系信息给我留言. I hope to hear from you soon!

Underst和ing the basics

  • What is HorusLP?

    HorusLP是一个Python优化库,旨在帮助您构建算法开发工作流. 它有一个简单的声明式API和很少的样板文件.

  • What is the Knapsnack 概率lem?

    Knapsnack问题是一个以组合优化为核心的优化问题. 当呈现一组具有不同权重和值的项目时, 这样做的目的是把尽可能多的人“装”进一个背包里,这个背包是有限制的,不会改变的.

就这一主题咨询作者或专家.
Schedule a call
Shanglun Wang's profile image
Shanglun Wang

位于 New York, NY, United States

成员自 December 16, 2016

关于 the author

Sean是一个充满激情的通晓多种语言的人:一个全栈向导、系统管理员和数据科学家. 他还开发了市场情报软件.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

专业知识

Previously At

CB的见解

世界级的文章,每周发一次.

Subscription implies consent to our privacy policy

世界级的文章,每周发一次.

Subscription implies consent to our privacy policy

Toptal 开发人员

Join the Toptal® 社区.