作者:syuansheng
原文地址:https://yuanshengshe.github.io/posts/2adef9b1/
What is Colossus ?
Colossus是一个高性能、可针对特定问题定制和拓展的列生成求解框架。 它追求:
- 算法清晰化、数据结构透明化。
- 便捷的外部接口,保证算法的可拓展性。
目前,Colossus框架的主体模块已经测试完毕,子模块还在开发测试中,暂未开源~~ 这篇文档的主要目的是记录开发进展,对相关的接口进行说明,避免时间长了我自己都看不懂自不己写了啥,哈哈哈…
API reference
截止目前,Colossus框架的结构如下:
Colossus/
├── engine.py
├── utils.py
├── __init__.py
├── common_exact_method/
└── examples/
utils模块
ToolBox类
ToolBox类是自定义工具箱,核心方法就是addTool,用于为Engine类提供必要的可调用对象。:
addTool(self,name,tool,*args,**kwargs)
列生成算法与具体问题相关的部分实际就五件事儿:
- 如何构造初始列?
- 限制主问题是如何定义的?
- 定价子问题怎么求解?
- 如果我想把生成的列写入到文件里面,格式是什么?
- 如何管理列池?
这五件事儿是需要你自己根据你要解决的问题自行定义的,这部分工作框架没有办法替你完成,因为框架并不清楚你具体解决的是什么问题。
对于这五件事儿,框架能做的是让你更快捷、更有逻辑、更规范的去完成它们:
- 如何构造初始列?:通过Toolbox对象的addTool方法添加name=“generateInitialCols”的可调用对象。必须定义。
- 限制主问题是如何定义的?:通过Toolbox对象的addTool方法添加name=“buildRestrictedMP”的可调用对象。必须定义。
- 定价子问题怎么求解?:通过Toolbox对象的addTool方法添加name=“updateAndSolveSP_Exact”的可调用对象或name=“updateAndSolveSP_Heuristic”的可调用对象。必须定义至少一个。
- 如果我想把生成的列写入到文件里面,格式是什么?:通过Toolbox对象的addTool方法添加name=“coefficients_formatter”的可调用对象。可以空着不定义,如果你不需要把列写入文件的话。
- 如何管理列池?:通过Toolbox对象的addTool方法添加name=“modifyColumnPool”的可调用对象。可以空着不定义,如果你没有什么特殊的列池管理策略的话。
toolbox = ToolBox()
toolbox.addTool('generateInitialCols',generateInitialCols)
toolbox.addTool('buildRestrictedMP',buildRestrictedMP)
toolbox.addTool('updateAndSolveSP_Exact',updateAndSolveSP_Exact)
toolbox.addTool('updateAndSolveSP_Heuristic',updateAndSolveSP_Heuristic)
toolbox.addTool('coefficients_formatter',coefficients_formatter)
toolbox.addTool('modifyColumnPool',modifyColumnPool)
问题结构是灵活的,但是代码规则是写死的,所以你在定义generateInitialCols,buildRestrictedMP,updateAndSolveSP_Exact,updateAndSolveSP_Heuristic,coefficients_formatter,modifyColumnPool这些方法的时候必须遵循格式规范写,不然Engine类求解引擎对象读不懂。
原则上讲,这些可调用对象的参数必须与模版保持一致,这些参数将有Engine求解引擎在求解时自动传入。但为了灵活性起见,你也定义模板规定以外的其它参数,但是对于这类参数,你在使用
addTool方法把这些可调用对象加入工具箱的时候必须使用addTool的*args,**kwargs参数提前把这些模板规定以外的参数固定住。
def generateInitialCols():
1. 创建ColumPool列池对象
2. 构造Column列对象加入到ColumnPool中形成初始列池
3. 返回ColumnPool列池对象
def buildRestrictedMP(columnpool):
1. 根据输入的columnpool列池对象中包含的初始列column对象,构造Gurobi求解器的Model对象
2. 返回Gurobi求解器的Model类对象
def updateAndSolveSP_Exact(y,fingerprint,omega_next_col):
1. 创建一个用于存放改善列对象的列表improve_column_lst
2. 根据限制主问题的对偶值列表y和子问题表示fingerprint构造子问题并使用精确解算法求解
3. 根据reducedcost判断是否存在改善列
1. 构造改善列Column对象,其中改善列在列池columnpool对象中的方案编码omega应该是omega_next_col
2. 将得到的Column对象加入improve_column_lst中
4. 返回improve_column_lst
def updateAndSolveSP_Heuristic(y,fingerprint,omega_next_col):
1. 创建一个用于存放改善列对象的列表improve_column_lst
2. 根据限制主问题的对偶值列表y和子问题表示fingerprint构造子问题并使用(元)启发式算法求解
3. 根据reducedcost判断是否存在改善列
1. 构造改善列Column对象,其中改善列在列池columnpool对象中的方案编码omega应该是omega_next_col
2. 将得到的Column对象加入improve_column_lst中
4. 返回improve_column_lst
def coefficients_formatter(coefficients):
1. 传进来的是Column对象的coefficents属性,定义它格式化后的字符串
2. 返回格式化后的字符串
def modifyColumnPool(current_iter,columnpool):
1. 第一个参数是列生成当前的轮次,第二个参数是Columnpool列池对象
2. 通过ColumnPool对象的增删操作对列池就地进行修改
Environment类
Environment类是一个dataclass数据类,用于管理求解引擎Engine对象的相关参数,这些参数决定了Engine对象的行为:
- variable_name:一个字符串,是限制主问题决策变量(新列对应变量)的name属性
- relaxed_variables_name:一个字符串列表,用于指定限制主问题是通过线性松弛主问题中的哪些变量得到的,第一个字符串
对应的变量将被当做需要生成的变量(列)。Engine框架将在最后阶段将这些变量恢复为relaxed_variables_type中对应类型。建议这些字符串的首字母不同,可避免startswith方法失效 - relaxed_variables_type:一个字符串(INTEGER或BINARY)列表,用于指定限制主问题中被松弛掉的变量原本的类型
- fingerprint_lst:元组的列表,存放所有子问题的唯一标识fingerprint
- barrier_method_on:bool对象。设定限制主问题是否使用内点法求解,推荐使用默认值开启内点求解,内点法得到的对偶变量一般好于单纯性法得到的
- exact_solve_on:bool对象。设定是用精确解算法求解定价子问题,如果同时设置了heuristic_solve_on=True,则精确解算法将在最后被调用兜底
- heuristic_solve_on:bool对象。设定是否使用启发式算法求解定价子问题
- mini_batch_percent:float对象。设定列生成每轮迭代求解多少个能产生改善列的子问题后就终止。例如,我有10个子问题,设定mini_batch_percent=0.2,
如果第1个子问题可以产生改善列,第2个子问题不能,第3个子问题能。我就求解到第3个子问题就不继续求解了。记录断点为第4个子问题,列生成下一轮次迭代从子问题4开始。如果
只有一个子问题,设置这个参数没有意义 - lp_file_ouput_on:bool对象。设定是否输出限制主问题的Gurobi模型文件
- ilp_file_output_on:bool对象。设定是否Gurobi模型的不可行性文件
- col_file_output_on:bool对象。设定是否将经由updateAndSolveSP_Exact或updateAndSolveSP_Heuristic生成的Column对象写入.col后缀的列文件
- result_file_output_on:bool对象。设定是否将模型的求解结果写入.result后缀的文件
- dual_alpha:float对象。设定对偶平滑的系数,本轮次y=dual_alpha*上轮次的alpha+(1-dual_alpha)*本轮次的alpha,默认设置为0即不进行对偶平滑处理,注意
dual_alpha在迭代的过程中默认使用的几何衰减策略,即dual_alpha=0.87096359*dual_alpha,差不多列生成迭代100次dual_alpha就会从1衰减到0 - truncate_threshold:float对象。设定当限制主问题相邻两次最优值差的绝对值相差多大时认定没有发生改进
- max_not_bertter:int对象。设定当限制主问题相邻两次最优值连续max_not_bertter次没有发生改进就终止迭代
- max_time_limit:float对象。设定当求解多长时间,如果限制主问题还没有收敛就强制停止迭代
- rmp_log_to_console:bool对象。设定是否展示Gurobi求解限制主问题的过程
除了variable_name,relaxed_variables_name,relaxed_variables_type,fingerprint_lst四个参数是必须设置的,其它参数都是可选的。如果你不理解这些参数的设置,请使用默认配置。
environment = Environment('labda',['labda'],['INTEGER'],[(1,)])
以上设置的意识是主问题对应变量名叫labda,被松弛的变量名叫做labda,松弛之前他是Gurobi的GRB.INTEGER类型,而子问题只有一个,他的fingerprint是(1,)。其余环境参数使用默认配置。
Column类
在列生成中,一个列包含的信息由以下几部分构成:
- 这个列在方案集合中对应的索引?
- 这个列在目标函数中的系数?
- 这个列在约束中对应的系数?
Column类就是用来存储这些信息的,并且还提供了其它一些属性用于列池的管理:
- omega:这个列在限制主问题中对应的索引,必须设置为一个int对象。对应信息1
- reduced_cost:这个列对应的reduced_cost,一般情况下它应该是一个float对象或int对象。如果是初始列,即不是通过定价子问题产生的请将这个属性设置为None
- dummy_reduced_cost:虚拟的reduced_cost,默认None
- tau:这个列在限制主问题目标函数中的系数,需要是一个float对象或int对象。对应信息2
- coefficients:这个列在限制主问题约束中对应的系数,需要是一个list对象。对应信息3
- in_rmp_optimal_basis_when:一个list对象,存储当前这个列在列生成的哪些轮次进入了限制主问题的最优基
一般来说,你在创建一个Column对象的时候至少把信息1,2,3对应的参数设置好。其它参数设不设置取决于你自己的需求:
column = Column()
column.omega = ...
column.tau = ...
column.coefficeients = [...]
ColumnPool类
Columns类是用来用于管理列对象Column的,包含以下属性:
- omega_next_col:是一个整数,用于查询下一个添加进来的Column对象的omega应该设置为多少。它在列生成迭代的整个过程中单调不减,从而保证列的索引不会因为某些列的删除而发生错乱
- col_num:是一个整数,用于查询当前列池中包含多少列
- deleted_col_pool:是一个dict对象,存储从当前列池中被删去的列。有的时候你可能误删了某些列,然后又想把这些列拿回去,可以从deleted_col_pool中找
- _changed:是一个bool对象,用于检测列池是否发生修改,一旦检测到列池状态变化,Engine对象内应该自动对限制主问题模型做出相应的修改,然后重置状态
- _memory_change:是一个字典对象,用于记录发生的修改,值是column对象的omega属性构成的列表
- ColumnPool对方案集合的索引管理是单调不减的,所以建模的时候如果你确实需要对方案集做分组处理,请使用额外的indicator parameter,具体操作参考:https://yuanshengshe.github.io/posts/4e8c5c2/
- ColumnPool实施的是惰性更新的机制,通过_changed和_memory_change参数实现。这两个参数不建议你在外部做修改,除非你懂会有什么后果
- ColumnPool集成自字典对象,支持使用
[omega]直接查询omega对应的Column对象- ColumnPool的所有属性都只建议读取不建议修改,他们会随着ColumnPool提供的方法的调用实现自动更新
ColumnPool对象提供以下操作对其中的列做增加和删除:
def addCol(self,column):
# 原子级的添加新列
...
def delColByOmega(self,omega):
# 原子级的删除列
...
def addCols(self,column_lst):
# 批量添加新列
...
def delColByReducedCost(self,lb=-float('inf'),ub=float('inf')):
# 批量删除列
...
def _alreadyCommitChangeToRMP(self):
# 如果已经将所有变更提交到了RMP中,则重置列池的状态为未修改,这个方法不需要提供给用户,在Engine内部使用,用户不准调用这个
...
大部分时候你应该只会用到addCol方法,除非你有特定的列池管理需求
engine模块
Engine类
Engine类是列生成框架的接口,内部实现还是蛮复杂的,但这些你不用管,只要你把Environment对象和ToolBox对象构造好甩给他就行了。然后调用runEngine就可以跑起来的。
engine = Engine('csp_engine',environment,toolbox) # 第一个参数是引擎的名字,会作为相关文件输出的前缀
engine.runEngine()
默认情况下,运行Engine引擎之后,控制会打印出如下信息,其中列生成迭代表格的各列含义如下:
- iter:列生成迭代当前的轮次
- time:从Engine启动到当前轮次的时长
- cols_num:当前列的数量
- rmp_obj:RMP最优值
- no_better:连续未改进次数
- rmp_time:当前轮次中限制主问题的求解时长
- sps_method:当前轮次中子问题求解使用的定价方式
- sps_time:当前轮次子问题的求解时长
>>Engine配置信息
[1] Environment(variables_name='labda', relaxed_variables_name=['labda'], relaxed_variables_type=['INTEGER'], fingerprint_lst=[1], barrier_method_on=True, exact_solve_on=True, heuristic_solve_on=True, mini_batch_percent=1.0, lp_file_ouput_on=True, ilp_file_output_on=True, col_file_output_on=True, result_file_output_on=True, dual_alpha=0, truncate_threshold=1e-06, max_not_bertter=inf, max_time_limit=3600.0, rmp_log_to_console=False)
[2] ToolBox contains tools: buildRestrictedMP, coefficients_formatter, generateInitialCols, updateAndSolveSP_Exact, updateAndSolveSP_Heuristic
>>Note:在调用runEngine前请先检查配置是否满足你的要求
>>预备:初始列构造完毕
Set parameter Username
Set parameter LicenseID to value 2759492
Academic license - for non-commercial use only - expires 2026-12-27
Set parameter LogToConsole to value 0
>>限制主问题数学模型构造完毕,进入列生成迭代轮次
>>列生成迭代过程:
+-----+---------+---------+--------------+----------+------------+------------+------------+
|iter |time(s) |cols_num |rmp_obj |no_better |rmp_time(s) |sps_method |sps_time(s) |
+-----+---------+---------+--------------+----------+------------+------------+------------+
|1 |0.00 |34 |30.10 |0 |0.00 |heuristic |0.00 |
|2 |0.01 |35 |27.60 |0 |0.00 |heuristic |0.00 |
|3 |0.02 |36 |27.20 |0 |0.01 |heuristic |0.00 |
|4 |0.02 |37 |25.92 |0 |0.00 |heuristic |0.00 |
|5 |0.02 |38 |25.50 |0 |0.00 |heuristic |0.00 |
|6 |0.03 |39 |24.83 |0 |0.00 |heuristic |0.00 |
|7 |0.03 |40 |24.50 |0 |0.00 |heuristic |0.00 |
|8 |0.04 |41 |24.46 |0 |0.00 |heuristic |0.00 |
|9 |0.04 |42 |24.42 |0 |0.00 |heuristic |0.00 |
|10 |0.05 |43 |24.38 |0 |0.01 |heuristic |0.00 |
|11 |0.05 |44 |24.10 |0 |0.00 |heuristic |0.00 |
|12 |0.06 |45 |24.09 |0 |0.00 |heuristic |0.00 |
|13 |0.07 |46 |23.87 |0 |0.01 |heuristic |0.00 |
|14 |0.07 |47 |23.86 |0 |0.00 |heuristic |0.00 |
|15 |0.17 |47 |23.86 |1 |0.00 |exact |0.10 |
|16 |0.26 |48 |23.79 |0 |0.00 |exact |0.09 |
|17 |0.36 |49 |23.79 |1 |0.00 |exact |0.09 |
|18 |0.46 |50 |23.71 |0 |0.01 |exact |0.09 |
|19 |0.57 |51 |23.62 |0 |0.01 |exact |0.10 |
|20 |0.67 |52 |23.60 |0 |0.00 |exact |0.10 |
|21 |0.76 |53 |23.50 |0 |0.00 |exact |0.09 |
|22 |0.87 |54 |23.49 |0 |0.00 |exact |0.10 |
|23 |0.98 |55 |23.48 |0 |0.00 |exact |0.10 |
|24 |1.10 |56 |23.48 |0 |0.00 |exact |0.12 |
|25 |1.22 |57 |23.46 |0 |0.00 |exact |0.11 |
|26 |1.35 |58 |23.44 |0 |0.00 |exact |0.12 |
|27 |1.45 |59 |23.44 |0 |0.00 |exact |0.10 |
|28 |1.57 |60 |23.42 |0 |0.00 |exact |0.11 |
|29 |1.67 |61 |23.42 |0 |0.00 |exact |0.10 |
|30 |1.80 |62 |23.42 |0 |0.01 |exact |0.11 |
+-----+---------+---------+--------------+----------+------------+------------+------------+
>>列生成迭代轮次终止,下面将根据现有ColumnPool对主问题进行求解:
Gurobi Optimizer version 12.0.0 build v12.0.0rc1 (win64 - Windows 11+.0 (26200.2))
CPU model: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz, instruction set [SSE2|AVX|AVX2|AVX512]
Thread count: 4 physical cores, 8 logical processors, using up to 8 threads
Optimize a model with 15 rows, 62 columns and 105 nonzeros
Model fingerprint: 0x241be89a
Variable types: 28 continuous, 34 integer (0 binary)
Coefficient statistics:
Matrix range [1e+00, 1e+01]
Objective range [1e+00, 1e+00]
Bounds range [0e+00, 0e+00]
RHS range [2e+00, 8e+00]
Found heuristic solution: objective 28.0000000
Presolve removed 0 rows and 29 columns
Presolve time: 0.02s
Presolved: 15 rows, 33 columns, 76 nonzeros
Variable types: 28 continuous, 5 integer (2 binary)
Root relaxation: objective 2.341949e+01, 26 iterations, 0.00 seconds (0.00 work units)
Nodes | Current Node | Objective Bounds | Work
Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time
* 0 0 0 23.4194915 23.41949 0.00% - 0s
Explored 1 nodes (26 simplex iterations) in 0.05 seconds (0.00 work units)
Thread count was 8 (of 8 available processors)
Solution count 1: 23.4195
Optimal solution found (tolerance 1.00e-04)
Best objective 2.341949152542e+01, best bound 2.341949152542e+01, gap 0.0000%
>>已经将Engine=csp_engine的求解结果写入csp_engine.result文件中
[Finished in 5.0s]
Result类
我第一次写列生成算法的时候没有经验,写完之后想要获取求解结果以及迭代过程中的相关数据非常麻烦。
所以针对Colossus框架的设计,我专门搞了一个dataclass数据类Result来存放求解结果以及迭代过程中的相关数据。你可以通过它获取到以下信息:
- history_incumbent:一个list对象,存储限制主问题历史每个轮次最优目标函数值
- history_col_num:一个list对象,存储限制主问题历史每个轮次列的数量
- history_rmp_time:一个list对象,存储限制主问题历史每个轮次的求解时长
- history_sps_time:一个list对象,存储历史每个轮次求解一组子问题的用时
- history_optimal_basis_col_omega:一个嵌套list对象,存储历史每个轮次那些在限制主问题最优基中的Column对象对应的omega属性
- best_objective:float对象,求解主问题的最优值
- total_time:float对象,总求解时长
- master_problem_time:最后求解主问题的时长
关于这个类的用法,不需要你自己创建,在Engine运行完成之后访问result属性(Result对象)就行了。
common_exact_method子包
提供一些常用的子问题精确求解算法,目前包含的算法较少,还在开发测试当中…
label_setting_method函数
def label_setting_method(adjacency_lst,s_node,t_node,resource_ub):
# Args:
# adjacency_lst:邻接表,默认顶点node值0,1,2,...,len(adjacency_lst)-1。[[(1,1,2),(2,2,2)],[],[]]这个图有3个node分别是0,1,2。只有两条弧分别从0到1,0到2。cost分别是1和2,资源消耗都是2
# s_node:起点对应node值
# t_ndoe:重点对应node值
# resource_ub:资源上限值
# Returns:
# best_obj:最优目标值
# best_path:最优目标值对应路径,满足资源限制约束!
参考论文Multicommodity Network-Flow Problem with Side Constraints on Paths Solved by Column Generation复现的求解RCSSP问题的单向标签算法。仅适用于弧成本和资源消耗非负的图。
Future
这个框架算是对我第一年研究生生涯方法论层面的总结,当然还有很多待完善的地方。
希望我可以在毕业之前把这个框架完善好,下一步打算融合一些机器学习的东西进去。
就这样,over~~
如果你也在做列生成、分支定价、或者大规模整数规划,欢迎一起交流。这个框架虽然还远称不上成熟,但至少让我的代码不再是一锅粥了。
也欢迎催更——说不定你催一催,我就更有动力把坑填完了 😄。
项目名:Colossus,
对,就是那个“巨像”,希望它有一天真的能成为巨像吧~~
1973



被折叠的 条评论
为什么被折叠?



