OptiMUS: Scalable optimization modeling with (MI)LP solvers and large language models
标题翻译:OptiMUS:基于混合整数线性规划求解器与大语言模型的可扩展优化建模
1. 内容简介
文章结合了求解器和LLM,实现了一种可扩展优化建模工具——OptiMUS,可以对混合整数线性规划(MILP)问题建立数学模型、编写和调试求解器代码、自主评估生成的解决方案,并根据评估结果改进其建模和代码。对比目前的别的方法,该方法在面对长描述或复杂问题时表现更佳。
对比2023.10( OptiMUS: Optimization Modeling Using MIP Solvers and large language models)一版的改进变动:
-
OptiMUS的系统结构更加模块化,引入了多个LLM代理(agents)的合作机制(不是强化学习中的多智能体,此处 LLM agents的不同在于提示词设计模板的不同);
-
利用连接图(connection graph),能够独立处理每个约束和目标,将数据与问题描述分开,这样OptiMUS就可以解决具有长描述和大数据文件的问题,而无需过长的提示;
-
数据集更新:
- NL4OPT: 1101个简单线性规划LP问题的集合;
- NLP4LP:67个长描述和更复杂优化问题;
- ComplexOR:一个包含复杂运筹学问题的数据集,测试了系统处理不同行业应用中复杂问题的能力。
2. 研究背景
数学优化在飞机机组人员调度、智能电网运行、投资组合优化等现实决策问题中有着广泛的应用。这些应用大多可以表示为一个**线性规划(LP)或者混合整数线性规划(MILP)**问题。
混合整数线性规划问题(MILP):在给定一组线性约束的条件下,最小化目标函数。
2.1. 研究现状
- 解决混合整数线性规划(MILP)问题一般要用求解器 —— 求解器属于一种专家系统,需要很多知识,不适合普通用户;
- 求解器也很难用,界面复杂,操作困难;
- 目前很多研究用LLM直接求解,而不是把LLM与其他工具集成在一起,或者是直接研究是否存在更优的求解器;
- 有研究从结构化的语言或者自然语言中检测约束,但对于新出现的问题泛化能力不强;
- 没什么研究直接从自然语言中来对问题建模
===> 于是作者就来研究用LLM进行优化建模联合求解器一起解决问题。
3. 研究方法
3.1. OptiMUS概览
总流程简述:首先对自然语言描述的问题进行预处理,提取参数、约束、目标函数和背景信息,变成一种结构化的问题。然后OptiMUS使用多代理框架来处理和解决这个结构化问题。
具体算法流程:
- P(0):初始化的结构化问题,它由预处理模块将自然语言描述的问题转换而来;
- msg:消息,最初为空,某一轮中LLM代理对任务的反馈信息,用于即时汇报执行状态;
- conversation:对话,初始化为空列表,所有msg的累计记录,用于保持任务完整的上下文和历史,确保代理在解决复杂问题时能有一个整体的参考;
for循环处理:
- 选择代理:由manager通过检查到目前为止的对话历史,决定选择下一个代理(formulator、programmer 或 evaluator)来执行任务
- 执行任务:被选择的代理处理任务P(t),并返回处理后的问题P(t+1)和msg,这一步可能是生成新的公式、编写求解器代码或者调试代码等
- 更新对话记录:代理的处理结果(msg)会被记录到对话历史conversation中,用于后续参考
检查是否完成:如果代理返回msg = "Done",说明问题已经被成功解决
3.1.1. 预处理过程
对应代码中的(tagert_extractor.py)
这一步把用户自然语言描述的问题输出为结构化的问题,包含以下几个部分:
- 参数(符号表示,形状,文字描述);
- 约束:问题中的限制;
- 背景:现实世界中问题的解释,提高常识推理;
- 目标:该问题最终的优化目标是什么;
预处理这一块用了三个提示词分别来完成(提示词均在原文附录中可找到):
- 提取问题描述中的参数
- 将问题分割成目标和约束(其中有隐性约束,比如产品生产量是一个正数)
- 消除多余的(例如:重申多次)不必要的(例如:事实性约束:产品生产量是一个正数)和不正确的约束(例如:生产数量必须完全等于需求)
3.1.2. LLM代理团队
可以说这一部分是这篇论文的核心设计亮点。代理团队一共有4个代理:
-
Manager(管理代理):负责协调系统中的其他代理,根据当前的对话内容决定下一步应该调用哪个代理,以及这个代理应该执行什么任务;
-
Formulator(公式生成代理):负责从自然语言描述(结构化后的问题)中生成数学公式(即对问题建模),识别出问题的目标函数和约束条件,更新连接图中的连接,未来迭代过程中可能会对这些公式进行必要的修正;
-
Programmer(编程代理):用python语言,将建模生成的数学公式转化为求解器代码(如Gurobi的代码),未来迭代过程中可能会调试代码中的错误;
-
Evaluator(评估代理):执行生成的代码,检查代码的运行结果是否正确。如果运行中遇到错误,它会标记问题并返回给管理代理,管理代理稍后会让别的代理进行修正(涉及到是否调用公式生成代理重新建模或者让编程代理重新写求解器代码)。
公式生成代理的处理过程:(单一约束的例子)
左图显示了处理约束所需的输入信息,包括约束的自然语言描述(每种原材料的使用量不应超过库存上限)、涉及的参数(MaterialReq表示原材料需求,MaterialCap表示原材料的可用容量)。
公式生成代理会读取当前约束的自然语言描述,从中提取需要自己定义的变量(Production)和参数(MaterialReq、MaterialCap)。
然后把约束转化为数学公式,生成LaTeX表达式,记录公式的内容并更新连接图中的边(将 MaterialReq、MaterialCap等节点与当前约束节点建立连接)。
3.1.3. 连接图(Connection Graph)
连接图一直被提到,这到底是个什么东西呢?这可以说是这篇论文的第二个亮点。
前面已经提到了有4个代理,那么这么多代理,要怎么保证公式之间的一致性? ===> 这就产生了连接图的概念。
连接图:是一种三层的图结构,分别包含参数(Parameters)、变量(Variables)和 约束(Constraints)三个节点层级。(记录了约束、目标、参数和变量之间的关联)。图的边表示这些节点之间的关联。(例如,某个约束涉及的变量和参数会与该约束建立边连接)。
OptiMUS使用连接图来检索每个提示词的相关上下文,以便提示词保持简短。这个连接图还用于生成和调试代码,以及纠正错误的公式。(文中这么说的,但看代码后感觉这个图不太算常规意义上的图,也不一定只记录了这三层东西。)
连接图示例:
根据图4很抽象的理解一下连接图:
图4就是想表达,运行时检测到了一个索引超出范围的错误,编程代理收到错误信息后,并不会直接检查整个代码,而是通过连接图提取到当前执行代码中可能会出错的约束相关的变量和参数(变量Production和参数MaterialReq、MaterialCap)。就是只检查这三行代码,而不去检查别的,减少了无关的检查。(本质上是为了减少LLM的上下文)
至于为什么只检查这三个,是根据task具体的描述来的,task的上下文中提到的约束就是这三个。
配合附录B勉强理解一下连接图
代码片段里的state就是所谓的连接图
对连接图的个人理解和思考(很抽象,看了源代码后感觉不是图,也不好评价什么是边,什么是层)
# 节选自run.py
state = {
"background": state["background"],
"problem_type": "LP",
"parameters": state["parameters"],
"constraint": state["constraints"],
"variables": [],
"objective": state["objective"],
"solution_status": None,
"solver_output_status": None,
"error_message": None,
"obj_val": None,
"log_folder": log_dir,
"data_json_path": f"data/{dataset}/{problem}/data.json",
}
state本身是一个字典,字典中的每个字段可以是不同的数据结构,具体取决于该字段所存储的数据类型和用途,例如:
-
state[“variables”]、state[“parameters”]中每个元素又是一个字典:表示每一个变量、参数又包含definition、symbol和shape等属性。
-
state[“constraint”] 存储了模型中所有的约束条件。每个约束包含公式定义、涉及的变量和参数等信息。
# 节选自formulator.py
prompt = prompt_template.format(
background=state["background"],
targetType=target_type,
targetDescription=target["description"],
variables=json.dumps(
[
{
"definition": v["definition"],
"symbol": v["symbol"],
"shape": v["shape"],
}
for v in state["variables"]
],
indent=4,
),
parameters=json.dumps(
[
{
"definition": p["definition"],
"symbol": p["symbol"],
"shape": p["shape"],
}
for p in state["parameters"]
],
indent=4,
),
)
...
# Extract related variables and parameters from the formulation
related_stuff = self.get_related_stuff(
state, formulation, update["new_variables"]
)
update["variables_mentioned"] = related_stuff["variables_mentioned"]
update["parameters_mentioned"] = related_stuff["parameters_mentioned"]
这样,如果某个参数/变量和当前我要调试的约束有关,我可以遍历存在于连接图中的变量和参数,并把相关的参数/变量提取加入到提示词当中,保证了公式之间的一致性。
4. 实验
4.1. 实验设置
-
数据集
- NL4OPT: 1101个简单线性规划LP问题的集合,包含每个问题的自然语言描述,以及列出参数、变量和人工专家制定的注解。
- ComplexOR:37个涉及不同领域的复杂运筹问题。
- NLP4LP: 包含54个LP问题和13个MILP问题。
-
对照组
- standard prompting: 直接把问题给LLM,让LLM生成求解器代码。
- Reflexion:反思来自编译和运行时错误,允许通过迭代改进先前步骤。
- CoE:专家思维链。
-
评估指标
- accuracy准确率:正确解决的实例数(代码成功运行且最优值正确时,实例才被视为正确解决)
- compilation error rate(CE):编译错误率
- runtime error rate(RE):执行错误率
4.2. 实验结果
OptiMUS的准确率最高。
在每个数据集上有多少生成的求解器代码是可以运行的,有多少达到了最优解。(可运行不代表解决了问题(不是最优解))。
4.3. 消融实验:代码调试和LLM的选择对OptiMUS性能的影响
- 完整的 OptiMUS(使用 GPT-4)
- 去掉调试功能的OptiMUS
- 混合模型(GPT-3.5 管理代理 + GPT-4 其他代理)
- 全使用GPT-3.5
- 使用Mixtral-8x7B这种小一点的语言模型
结果:
- 调试功能的重要性:在去掉调试功能后,OptiMUS 在较为复杂的数据集(ComplexOR和NLP4LP)上准确率明显下降。
- 管理代理的重要性:在使用 GPT-3.5作为管理代理的配置下,系统在较复杂的数据集上(尤其是NLP4LP)表现不如全GPT-4 配置,因为复杂数据集中代理之间的交互更多更复杂。
- 模型规模的影响:使用较小的GPT-3.5 或 Mixtral-8x7B模型会导致系统性能显著下降。
4.3.1. 敏感性分析
图5:代码出错了,管理代理要多叫其他代理来调试解决,管理代理被允许去选择别的代理的最大次数如何影响准确性。
图6:解决的问题中每个代理平均被叫的次数。
发现:管理代理优先考虑修复代码,而不是考虑公式建模中的错误。只有当编程代理声称代码正确时,公式生成代理才会被选择进行调试。
OptiMUS和CoE在不同数据集上的平均提示词长度(消耗token数)
对于OptiMUS来说,不同数据集上的提示长度几乎没有变化,因为模块化方法,使得OptiMUS能够仅提取和处理每个LLM调用的相关上下文。
导致OptiMUS解决问题失败的常见原因:
- 错误的建模
- 错误的约束或缺失约束
- 编码错误(可能混淆了参数和变量,原文附录C详述)
5. 总结
文章开发了OptiMUS,一个基于LLM代理的模块化工具,旨在根据自然语言描述与传统求解器结合来自动化制定和解决优化问题。
5.1. 限制与未来方向
- 大模型很昂贵,能不能根据不同情况来微调所使用的LLM模型,比如GPT3.5建模不行,但可能别的方面能持平GPT4。
- 能不能加入和用户的互动反馈,根据用户偏好、运行时间、问题规模等找到最佳公式。
- 根据对准确性和运行时间需求的全面评估自动选择最佳的求解器。
- 文中的管理代理能不能用强化学习来改进,让它更会选择下一个要叫哪个代理。