我接过一个能源调度的项目,最初整条优化链路用的是确定性模型,风电出力、负荷全按预测点估计代入,结果连续阴天那几天,模型给出的方案在第一轮执行就被现实打脸:实时调整成本直接超了预算的40%。后来把目标函数从“最小化期望成本”改成“最小化最坏情况成本”,配合列与约束生成算法(C&CG)去解两阶段鲁棒优化模型,问题才算真正收住。这篇内容就是围绕这个方向做的一次系统复盘:两阶段鲁棒优化模型怎么建、C&CG算法怎么落地、数据处理机制在整个流程里到底扮演什么角色,以及四个典型场景下的建模实施细节。适合正在做电力调度、供应链网络设计、生产计划、资源预留这类问题的运筹优化工程师和研究生参考。
1. 两阶段鲁棒优化到底解决什么问题:从“确定值假设”说起
1.1 一个让我吃过大亏的确定性模型
先说那个调度项目。原始模型里,风电出力被当成固定参数,约束条件是“出力=预测值”,所有调度指令都围绕这个单一数值展开。预测值准的时候没什么问题,一旦天气系统变化,实际出力和预测值之间可能出现20%以上的偏差。更麻烦的是,这个偏差还会传导:风电少了,火电就要多出,但火电爬坡速率有限,现货市场电价又是波动的,最后实际成本远高于模型算出来的最优成本。
这类问题在教科书里叫“数据不确定性下的决策问题”。确定性模型本质上是把不确定参数当作确定值,然后用一套静态约束去求解。模型越精细,对确定值假设越敏感,一旦真实参数偏离假设,结果就崩得越厉害。这不是算法本身的问题,而是问题定义的问题:你在用一个没有不确定性信息的世界观,去解一个充满不确定性的世界。
1.2 鲁棒优化、随机规划与两阶段决策的本质差异
面对不确定性,业界常用的思路有三类。
第一类是随机规划。它假设不确定参数服从某个已知概率分布,优化目标是期望成本最小。这个方法理论上漂亮,但落地有两个硬伤:一是真实分布很难精确获得,分布假设一错,最优解也就跟着错;二是期望目标会把“极端但代价极高”的场景平均掉,而业务方往往最关心的恰恰是极端场景下方案不能崩。
第二类是鲁棒优化。它不假设概率分布,而是给不确定参数划定一个集合——叫不确定性集合——然后要求方案在这个集合内的所有实现下都可行,目标函数通常取最坏情况下的表现。这种方法保证了稳健性,但代价是保守:如果集合定太大,所有决策都按最坏情况来,经济性会很差。
第三类就是两阶段鲁棒优化。它把决策拆成两个阶段:第一阶段是“现在就必须拍板”的决策,比如建多少仓库、装多少机组、租多少容量;第二阶段是“等不确定性实现之后”的调整决策,比如按实际需求调度物资、按实际出力调整机组。模型写成外层最小化第一阶段成本,内层最大化最坏情况下的第二阶段补偿成本。
一句话总结三者的差别:随机规划在赌一个平均的未来,单阶段鲁棒优化在防一个最坏的未来,两阶段鲁棒优化在“先拍板、后补救”的结构里防最坏的未来。
1.3 两阶段鲁棒优化的一般形式与适用边界
两阶段鲁棒优化的一般形式可以写成:
min_{x∈X} ( c^T x + max_{u∈U} min_{y∈Y(x,u)} d^T y )
其中 x 是第一阶段决策变量,u 是不确定性参数,U 是不确定性集合,y 是第二阶段决策变量,Y(x,u) 是给定 x 和 u 后第二阶段可行域。目标函数里层是一个 max-min 结构:对于每个第一阶段方案 x,不确定性会选一个让后续成本最大的 u,然后我们在该 u 下做最优调整。
这个结构有两个核心边界要清楚。第一,只有第二阶段存在调整空间时,两阶段模型才比单阶段鲁棒优化有意义。如果第二阶段没有任何变量可以动,那 max-min 就退化成 max,本质上还是单阶段问题。第二,U 的形状直接决定模型可解性。线性约束构成的凸多面体集合最容易处理;椭球集合会引入二阶锥约束;离散场景集合则需要配合枚举或分支策略。选定 U 之前,务必确认手里的求解器和算法能接得住。
我把这个项目里遇到的实际问题先放在这里:两阶段鲁棒优化确实强大,但它真正难的点不在模型形式,而在“如何高效求解 max-min 结构”和“如何让 U 紧贴真实数据”。这两个问题,分别由 C&CG 算法和数据处理机制来解决。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 列与约束生成算法:把“max-min”层层拆开的迭代框架
2.1 C&CG 的总体思路
两阶段鲁棒优化的核心难点,在于内层的 max-min 不是单层优化问题,无法直接用常规求解器一步解出。如果 U 是连续集合,问题天然是双层结构;如果 U 是离散场景集合,场景数量稍微大一点,直接把所有场景写进模型也会让问题规模爆炸。列与约束生成算法(Column-and-Constraint Generation,简称 C&CG)就是为这个问题设计的迭代框架。
C&CG 的思路很直接:主问题里只维护少量“已经发现的最坏场景”,用这些场景去求一个当前最优的 x 和当前最低的鲁棒成本(下界);子问题则在固定 x 的前提下,去寻找真正的、让第二阶段成本最大的那个场景 u,把真实成本反馈出来(上界)。每迭代一轮,就把子问题发现的这个最坏场景作为新的“列”,连同第二阶段变量和约束一起加入主问题,形成更紧的近似。如此反复,直到上界和下界足够接近。
用砍价来类比最贴切。第一阶段决策者是买家,先按手里已有的几个卖家报价出方案;子问题是卖家,总能在你方案里找到一个让你最难受的价格;然后你把这个价格纳入下一轮谈判范围,重新出方案。双方迭代到卖家找不到更让你难受的价格为止。
2.2 主问题(MP)的构造细节
C&CG 主问题的标准形式是:
min_{x, η, y_k} c^T x + η
s.t. A x ≤ b
η ≥ d^T y_k, ∀k ∈ K
B_k x + C_k y_k ≥ h_k, ∀k ∈ K
x ∈ X, y_k ∈ Y, η ∈ R
这里 K 表示已经迭代发现的场景下标集合。每一个 k 对应一个特定的不确定性实现 u_k,以及一套独立的第二阶段决策变量 y_k。注意,每次迭代新加进主问题的不仅是一条约束,而是一组“变量块+约束块”。
主问题求解出来有两个关键产物:一个是第一阶段决策 x,一个是目标函数值。由于主问题只考虑了部分场景,它给出的目标值一定不高于完整两阶段问题的目标值,所以主问题提供的是原问题的下界(LB)。这也是后面收敛判据的基准。
有一点特别提醒:主问题规模会随迭代次数线性增长。如果迭代到 50 轮,主问题里就有 50 套第二阶段变量。对于大规模实际问题,这会让主问题的求解时间逐轮上升。因此,实际工程里要给 C&CG 设置最大迭代上限,还要在每轮 MP 求解时开启 warm start,用上一轮的 x 做初值。
2.3 子问题(SP)求解:把 max-min 降维成单层
固定第一阶段解 x^* 之后,子问题写作:
Q(x^) = max_{u∈U} min_{y∈Y(x^,u)} d^T y
这个 max-min 结构不能直接丢给求解器。常规做法是:内层的 min 问题如果是线性规划(或满足强对偶条件的凸规划),就用对偶变换把它转为对偶最大化问题,这样内层和外层都是 max,合并成一个单层最大化问题。
以第二阶段为线性规划为例,原始 SP 经过强对偶后目标函数变成:
max_{u, λ} (h_u - B_u x^*)^T λ
其中 λ 是对偶变量,u 仍然是原始不确定性变量。此时约束条件由对偶可行性、U 的定义、以及 u 与对偶变量之间的耦合关系组成。如果 U 是线性不等式集合,整个 SP 仍然是线性规划,可以用常规 LP 求解器处理。
子问题求解完成后,得到目标值 Q(x^),原问题的一个上界就是 UB = c^T x^ + Q(x^)。同时,SP 的解中会给出对应的最坏场景 u^。这个 u^* 就是下一轮要追加进主问题的新“列”。
如果第二阶段包含整数变量,对偶这条路就走不通。这时候常用的替代方案有:把 U 离散化为有限候选极点集合,枚举极点求解;或者对 u 做分支定界;又或者用启发式先生成场景,再在 SP 内做校验。我在实际项目中,如果第二阶段变量全连续,就放心用对偶;如果含整数,就优先考虑把 U 离散化,让 SP 变成枚举问题。
2.4 C&CG 与 Benders 分解的差异
很多刚接触 C&CG 的人会问:它和 Benders 分解(L-shaped method)到底什么关系?两者的迭代框架确实很像,都是主问题-子问题循环,但机制有本质区别。
Benders 分解在主问题里只添加割平面(约束),不添加新的决策变量。割平面是从子问题的对偶空间中提取的,本质上是在逐步逼近第二阶段价值函数的支撑面。C&CG 则在主问题里同时添加第二阶段变量和对应约束块,等于直接把一个“场景子问题”整体搬进主问题,逼近方式更直接。
实际体验下来,C&CG 对包含离散场景或预算约束集合的两阶段问题通常收敛更快,因为每轮新增的信息量更大。代价是主问题规模膨胀明显。Benders 虽然主问题增长慢,但面对 max-min 结构时,割平面逼近效率有时不高,往往需要更多轮次才能收敛。我的经验是:如果第二阶段是线性规划且 U 是预算型盒式集合,优先上 C&CG;如果主问题本身规模已经特别大,再考虑 Benders。
3. 数据处理机制:不确定性集合是怎么“喂”出来的
3.1 从原始数据到不确定性集合的完整链路
在落地两阶段鲁棒优化之前,最容易被低估、也最能决定项目成败的一步,是构造不确定性集合 U。U 定得太宽,模型输出一个永远按最坏情况准备的保守方案,成本高得没法向业务交代;U 定得太窄,模型认为的不确定性范围小于真实波动,所谓鲁棒解其实一推就倒。C&CG 算法本身并不关心 U 是怎么来的,它只负责在给定的 U 上求解,因此数据处理机制在整个流程里的优先级非常高。
我惯用的数据链路是:原始数据 → 清洗(异常值、缺失值、重复值)→ 按业务周期切分 → 统计不确定参数的均值、方差、协方差和分位数 → 综合这些统计量构造 U → 用样本外数据做覆盖校验。
以风电出力为例,原始数据来自 SCADA 系统,里面常见三类脏数据:停机检修时段出力恒为0、传感器故障造成的跳变、限电时段出力被人为压低。如果不把这些时段剔除,均值会被带偏,方差会被拉大,构造出的 U 会异常保守。清洗之后,还要区分天气类型和季节,因为晴天的风电预测误差分布和台风天的完全不是一回事。
3.2 盒式集合、预算集合、椭球集合的选择逻辑
构造 U 的常见方式有三种,这里给出对比和选型建议。
| 集合类型 | 数学形式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 盒式集合 | |z_i| ≤ Γ_i | 形式简单,求解快 | 假定所有参数同时达到最坏值,过于保守 | 参数相关性弱、极端同发概率很低 |
| 预算集合 | Σ|z_i| ≤ Γ,且 |z_i|≤1 | 控制同时偏离的维度数量,灵活性好 | 需要额外确定预算 Γ | 电力调度、库存、生产计划 |
| 椭球集合 | z^T Σ^{-1} z ≤ Γ | 内嵌协方差结构,能反映相关性 | 模型变为二阶锥,求解变慢 | 参数相关性显著、数据充分 |
选型时我会先问自己三个问题:数据里参数之间的相关性到底有多强?相关性强且样本充足,用椭球;相关性可以忽略,用预算集合;业务上确实存在“所有参数同时恶化”的可能性,且概率不能忽略,才用盒式。盒式集合看似方便,但往往是最容易制造“天价方案”的元凶。我在生产调度项目中就吃过这个亏:加工时间不确定参数相关性很强,直接用盒式集合,结果所有工位都按最坏加工时间同时准备产能,算出来的总成本比预算集合高出将近20%,并且业务上完全没有必要——现实中所有工位同时故障的概率极低。
3.3 场景削减与样本外验证的实操方法
有些问题里,不确定性不是连续集合,而是一堆历史场景或模拟场景。比如供应商供货中断的情景来自历史事件记录,或者蒙特卡洛仿真产生了几千个需求样本。把这些场景全部放进模型显然不可行,于是要做场景削减。
常用的削减方法有三种:K-means聚类,把相似场景合并成簇,取簇中心作为代表场景;快速前向选择(Fast Forward Selection),基于场景之间的概率距离迭代选出最重要场景;还有基于矩匹配的方法,直接筛选出能保留原样本均值、方差和协方差的少量场景。削减完成后,务必用未参与削减的原始样本做一次覆盖校验。
校验方法可以这样设计:把削减后的场景集合作为 U,随机抽取大量原始样本作为验证集,对每个样本检查第二阶段是否可行。如果验证集里某个样本找不到可行解,或者第二阶段成本明显高于鲁棒模型给出的最坏情况值,说明场景削减丢了关键信息,需要调大保留场景数量或改变削减算法。这一步不能省,因为场景削减本质上是在用信息损失换计算效率,而这种损失是不可见的,不校验就不知道损失在哪里。
4. 四个典型场景下的模型构建与 C&CG 实施
4.1 场景一:微电网日前-实时经济调度
第一个场景是微电网经济调度。第一阶段在日前决定机组启停、储能充放电计划和与主网的购电协议;第二阶段在实时根据风电、光伏实际出力和负荷波动,进行机组出力调整、储能调整,必要时候甩负荷。风力出力是主要的不确定参数。
构建模型时,第一阶段变量包括机组开停机状态、储能充放电状态;第二阶段变量包括实时出力增量、储能调整量、甩负荷量。目标函数是第一阶段发电成本加上最坏情况下的实时调整成本和惩罚成本。
这个场景的数据处理重点是构造动态预算集合。风电预测误差有明确的时效规律:距离实际时刻越近,误差越小,因此预算参数 Γ_t 会随时间 t 递减。我用历史风电预测数据和实际出力数据,按预测提前时间分桶统计误差分位数,取 90% 分位数的误差作为 Γ_t 的基准,再乘一个安全系数。这样的 U 比固定盒式集合精准得多。
C&CG 在这个场景里的实现,我记得很清楚:MP 求出机组启停计划后,SP 在固定启停计划下找最坏的风电出力组合。由于风电出力不确定性的维度是时段数量(比如96个时段),预算集合下 SP 是线性规划,对偶变换后求解很快。整个迭代大概到第 8 轮时 gap 就能降到 1% 以下,比直接用盒式集合的收敛性反而更好,因为预算集合限缩了极端场景的选择空间。
4.2 场景二:供应链网络设计与应急物资预储
第二个场景是两级供应链网络设计,也可以理解为应急物资预储选址问题。第一阶段决定仓库或储备库的选址和建设容量,第二阶段在需求实现后做物资分配和运输调度。不确定参数是各需求点的需求量。
这里的数据处理有个鲜明的特点:需求样本之间强相关。比如自然灾害发生时,周边多个需求点的需求量会同时暴增。所以我放弃盒式集合,改用预算多面体集合,并引入一个总需求上限约束。具体形式是:每个需求点的需求在估计值附近波动,但所有需求点的总偏离量不超过 Γ。这样既允许局部需求暴涨,又约束了总体需求不至于天马行空。
模型层面,第一阶段是0-1选址变量加容量变量,第二阶段是标准运输问题。第二阶段运输问题是线性规划,对偶条件完备,适合 C&CG 直接迭代。我在实施中发现,这类问题 SP 找到的最坏场景往往集中在少数几个需求点同时高位的情况,而不是所有点都最高。这就是预算集合比盒式集合合理的直接证据。
4.3 场景三:生产车间产能与班次规划
第三个场景来自制造车间。第一阶段决定设备数量、班次人员配置和外包产能;第二阶段在加工时间不确定下安排具体排产,若产能不足则产生加班成本或延期惩罚。不确定参数是各工位的加工时间。
这个场景的数据机制要强调分位数估计。加工时间历史数据往往右偏,用均值加减方差构造对称集合,最容易低估高加工时间侧的风险。我用的是左尾 5% 分位数和右尾 95% 分位数作为单变量波动区间,再用历史数据的秩相关系数做预算约束。这样得到的 U 非对称,更贴近真实。
模型里有一点要注意:第二阶段如果含排产顺序决策,可能引出整数变量,导致 SP 不再可直接对偶。我在实施时做了个简化——第二阶段只做产能分配和加班量决策,排产顺序问题拆出去单独用启发式处理。这样第二阶段保持线性,C&CG 流程顺畅很多。这也印证了之前说的:两阶段鲁棒优化建模时,第二阶段结构越“规整”,算法实施越省心。
4.4 场景四:数据中心容量预留与弹性资源调度
第四个场景是数据中心的容量预留。第一阶段与云服务商签订固定容量合同,第二阶段根据每小时实际负载波动,在池化资源不足时临时购买弹性容量。
这个场景我选择椭球集合来刻画负载不确定性,因为数据中心负载有强周期性和时段相关性:白天高峰期各业务线负载一起上涨,夜间一起回落。盒式集合会假设“所有时段同时达到峰值”,这在物理上不可能;椭球集合通过协方差矩阵把时段之间的关联性编码进去,让 SP 找出的最坏负载曲线更符合真实运行时序。
椭球集合让 SP 变成带二阶锥约束的优化问题,求解器需要用能处理二次约束的版本。C&CG 迭代逻辑不变,但每轮 SP 的求解时间显著增加。为控制总耗时,我给 SP 设置了时间上限,并在超时后回退到预算集合做近似校验。这种“混合不确定集合”的做法在实践中很实用:主迭代用快速近似的 U,最终一轮用精确椭球 U 做验证。
5. 实施中容易翻车的细节与调参经验
5.1 子问题对偶化时经常搞反的符号问题
C&CG 实施过程中,我见过最多的错误出在 SP 对偶变换环节。对偶问题写出来,目标函数符号反了、约束不等号方向反了、对偶变量符号定义错了,这些都非常隐蔽。线性规划对偶性本身有明确规律,但实际问题里约束多,稍不留神就会漏写某个约束的对偶项。
我的排查方法是:拿到对偶 SP 之后,不要急着跑全量数据,先构造一个只有 3-5 个不确定参数的小算例,用暴力枚举方法(把 U 的所有极点都枚举一遍,分别求内层 min 再取 max)算出精确的最坏情况值,再和目标值比对。一旦不等,几乎都是符号或约束遗漏问题,逐条核对就能快速定位。这个方法帮我避免了大量无效调试。
5.2 大M参数设置与数值稳定性
在 SP 对偶化、或处理含逻辑关系的约束时,大M法几乎是绕不开的工具。大M参数的选取却是双刃剑:取太大,模型数值条件数恶化,求解器出现大量数值警告,解的可信度下降;取太小,可能把本来可行的区域错误切除。
我的经验是:基于变量量纲估算一个可靠的M值上界,然后在满足约束逻辑的前提下尽量取小。比如容量变量上界是1000,某条逻辑约束需要的 M 取 1000 就够,没必要取 10000。而且,如果求解器支持指示变量(indicator constraint),优先用指示变量替代大M,Gurobi 对指示变量的数值处理通常更稳。
5.3 收敛判据和 gap 设置的尺度问题
主问题和子问题产生的 LB 与 UB 之间的收敛判据,直接决定模型在什么水平停下来。判定得太松,解明显不是最坏情况下的最优;判定得太紧,迭代轮数大增,主问题规模膨胀到不可接受。
我通常用相对 gap:gap = |UB - LB| / |LB|,阈值设 1e-3 或 1e-4,同时设置最大迭代次数作为保底。目标函数量级很大时,绝对 gap 没有意义;目标函数接近零时,相对 gap 又会因为分母太小而失真,这种情况可以切换成绝对 gap。另外,每一轮都要确认 SP 返回的 u 确实被完整传递到 MP,如果只回传了目标值而漏传了场景,循环就会陷入“同一个场景反复加入”的死循环。
5.4 数据切分中的泄漏与季节性问题
数据处理层面最常见的坑是数据泄漏。构造不确定性集合用的是全量历史数据,验证也用同一批历史数据,结果就是模型在验证集上表现极好,一到新数据就失灵。规范做法是:把历史数据按时间顺序切分成训练段和验证段,只用训练段构造 U,验证段用来测覆盖率。
第二个坑是季节性。风电出力、电力负荷、供应链需求都有明确季节周期。如果忽略季节,把一年数据合并做统计,冬天和夏天的统计量混在一起,不确定性会被系统性扭曲。我处理风电数据时,按春夏秋冬四季分别统计误差分位数,同时在每周内部区分工作日和周末,这样构造出的动态预算集合才真正贴合实际业务节奏。
6. 结果解释与算法加速的经验
两阶段鲁棒优化模型跑通了之后,容易忽视的另一件事是结果解释。面对业务方,不能只说“这个方案在模型意义下最优”,还需要解释“为什么我要用最坏情况来定方案”。我的做法是做一个前沿面分析:用不同宽度的不确定性集合分别跑模型,画出成本目标值随保护水平变化的曲线。曲线走势能直观说明,多保一档极端情况,需要付出多少成本代价。业务方看到这个曲线,才能真正理解鲁棒解的本质:不是求一个平均最优的方案,而是牺牲部分平均收益,换取极端情况下不崩盘。
算法加速方面,有三个技巧在实际项目中价值很大。第一,对 MP 开启 warm start,每轮用上一轮的 x 作为初始解,能明显压缩 MP 求解时间。第二,SP 在有多组候选场景时可以并行求解,特别是 U 是离散集合时,把场景拆分到多线程,收效显著。第三,提前计算 U 的极点候选集合,SP 检查时只枚举这些极点,能省去对偶变换的复杂度和求解器内迭代的耗时。
最后说一点个人体会。C&CG 算法本身学起来并不难,难的是搞清楚两阶段决策结构的业务含义,以及把不确定性集合校准到跟真实世界对齐。数据处理和集合构造在整个两阶段鲁棒优化项目里,往往要占掉 60% 以上的时间,算法构型反而只占一小部分。这个比例和很多初次接触鲁棒优化的同学直觉相反,但只要完整跑完一个真实项目,基本都会认同这个判断。如果你正在做类似问题,建议先别急着写算法,把数据打开、把不确定性集合的构造逻辑想清楚,后面每一步都会轻松很多。
