GAMS场景缩减实战:用SCENRED解决随机优化场景爆炸问题

发布时间:2026/10/17 1:05:31
GAMS场景缩减实战:用SCENRED解决随机优化场景爆炸问题
做随机优化的人多半都经历过这种尴尬模型推导逻辑完美约束条件一个不少但一跑起来就是十几个小时死活算不完。最后定位瓶颈十个里有八个出在场景数量上。场景一多问题规模指数膨胀个人电脑直接歇菜即便是高性能服务器也要排队。这一期GAMS系列分享我专门聊聊场景缩减Scenario Reduction这个高级应用它到底怎么做到用更少的场景去保留原始不确定性里真正影响决策的核心信息。这个主题适合正在做随机规划、两阶段优化、能源系统容量规划或金融资产配置的建模人。尤其是你手里已经有一堆历史场景或者蒙特卡洛采样样本但模型算不动的时候场景缩减就是你最该掌握的预处理手段。它解决的不是“怎么优化”的问题而是“拿什么数据去优化”的问题。1. 为什么随机优化模型总在场景数量上栽跟头1.1 场景爆炸随机优化的真实痛点随机优化的本质是用一堆离散场景去近似连续分布然后用确定性的等价模型去求解。理论上场景数量越多近似越精确优化结果也越稳健。但代价是模型规模呈爆炸式增长。我举一个很常见的例子。某电力系统的日前调度模型里有三个相互独立的随机源风电出力、光伏出力、负荷水平。假设每个随机源取50个离散状态那么全场景组合就是50的三次方也就是12.5万个场景。如果机组组合问题是混合整数规划每个机组每个时段都有二进制变量12.5万个场景展开之后模型规模直接碾压任何一台个人电脑。这就是常说的“维度灾难”。很多人在这一步卡住后第一反应是减少采样数量比如每随机源只取10个状态组合后也有1000个场景勉强能跑但结果出来之后自己都不信。要么极端场景丢了要么概率分布失真优化出来的决策在实际场景下表现很差。1.2 场景缩减到底在干嘛删场景不是删信息场景缩减的核心思想是从原始场景集中有选择性地删除一些场景同时调整剩余场景的概率让缩减后的场景集在“概率距离”意义下尽量接近原始场景集。一句话总结就是用更少的数据点守住原始分布的关键形状。需要特别强调场景缩减不是随便抽几个场景也不是简单地把相似的合并。它是一个基于最优化思想的预处理过程。被删除的场景并没有完全消失它的概率质量会重新分配给某个保留场景。这样一来缩减后的场景集总概率仍然为1但是场景数量大幅下降求解难度随之下降。我在实际项目里经常把场景缩减比作压缩照片你不能随便删像素而是要用算法判断哪些像素对整体画面影响最小然后合并相似区域让压缩后的照片看起来仍然接近原图。1.3 SCENRED在GAMS生态里的位置GAMS自带了场景缩减工具SCENRED这意味着你不需要额外安装第三方求解器只需要在模型里定义好场景树的数据结构然后调用内置工具就能完成缩减。这个工具在随机规划建模流程中处于“建模前预处理”的位置正好卡在场景生成和优化求解之间。在我用过的各种工具里好用的场景缩减库并不多。GAMS内置的SCENRED仍然是两个主流方向之一另一类是想办法自己写启发式缩减算法。自己写的好处是灵活坏处是调试成本高、数学性质没保证。SCENRED把概率距离最小化的核心算法封装好我们能做的是把数据结构组织对、把参数配好它就能输出一套概率重新分配后的新场景集。2. 场景缩减的核心机制与数学直觉2.1 概率距离两个场景集怎么算“像不像”场景缩减的理论基础是概率距离最常用的是Wasserstein距离也叫推土机距离。这个距离描述的是把一个概率分布“搬运”成另一个概率分布平均下的最小消耗是多少。我用一个生活化的例子解释。想象面前有两堆沙子一堆在左边一堆在右边。要把左沙堆变成右沙堆的形状需要用铲子把沙子一铲一铲运过去。每一铲沙子的量乘以搬运距离加起来就是总消耗。Wasserstein距离就是让这个总消耗最小的可能值。两个场景集之间的距离本质上就是场景之间取值差异的加权和。每个场景有自己的概率权重和一组取值。GAMS的SCENRED在计算时会先定义一个场景间的距离函数通常取欧氏距离的p次方。p1时就是普通欧氏距离p2时相当于对差值加了平方权重惩罚大的偏差。2.2 最优缩减与快速向后缩减两种思路SCENRED中绕不开的两个概念是最优缩减和快速缩减。最优缩减的思路是在给定保留场景数量的前提下寻找一个全局最优的保留集合和被删场景的概率分配方案使得缩减后的分布与原始分布的概率距离最小。这个思路数学上很漂亮但计算代价高因为本质上它要比较海量的“保留集合”组合。当原始场景有几千个时全局搜索不现实。快速缩减的思路则是贪心式的最典型的就是后向消去法。它从完整场景集出发每次删除一个对整体概率距离贡献最小的场景同时把被删场景的概率累加到离它最近的保留场景上然后重复这个操作直到达到目标场景数。我自己的使用感受是初始场景在500个以内、保留场景不多时最优缩减值得一试初始场景上千保留场景数要求又不高时快速缩减跑起来明显更稳结果跟最优差距也不大。实际工程里优先用快速缩减完成第一版模型验证再视情况升级到最优缩减。2.3 概率再分配被删掉的场景去哪了很多人初学场景缩减时会有一个疑问被删掉的场景它的概率是扔掉了吗答案是没有。概率不能丢场景树要保证所有场景概率之和为1。在快速向后消去策略里被删除场景的概率会转移给距离它最近的保留场景。这个过程可以理解为被删场景代表的不确定性信息由那个最接近的保留场景“继承”了。所以最终缩减后的场景集里每个场景的概率不再均匀有的场景可能从原来的0.005涨到0.05因为它的周围合并了多个被删场景的概率。理解这一点很重要因为缩减后的新场景集不能照搬原来的均匀概率必须使用工具输出的新概率。我在项目里遇到过有人手动挑场景但忘了重新分配概率结果期望值计算偏差很大整个优化结果失真这完全是操作失误。3. GAMS中落地场景缩减数据结构、调用框架与关键参数3.1 场景树的数据组织用SCENRED做缩减之前第一件事是把场景数据整理成工具认识的结构。GAMS的SCENRED要求你提供一棵场景树核心是两个部分场景概率和场景取值。场景概率是一个一维参数表示每个场景发生的可能性。场景取值通常是一张二维表行是场景列是随机变量的维度。比如说你有200个场景、3个随机参数那就是一个200乘3的矩阵。如果你的随机优化模型还带多阶段结构需要额外定义阶段节点让工具知道哪些随机变量在哪个阶段显现。这里有个实操要点原始场景集的生成最好在GAMS外部完成比如Python、MATLAB或者历史数据库然后通过GDX文件导入GAMS。因为GAMS本身不擅长做大规模的统计采样和数据处理用外部工具生成、GAMS负责优化这个分工最清晰。3.2 SCENRED调用框架下面是一段经过简化的示意框架覆盖了从场景定义到调用SCENRED的主体逻辑。不同GAMS版本的关键字和具体接口会有些差别照着敲之前先查本机帮助文档确认。* 定义场景索引和随机维度 Sets sc 场景集合 / sc1*sc200 / rdim 随机参数维度 / r1*r3 /; * 定义场景概率与场景值 Parameters prob(sc) 场景概率 value(sc, rdim) 场景取值; * 初始化概率保证总和为1 prob(sc) 1 / card(sc); * 导入外部场景值示意通常来自GDX * execute_unload original_scenarios.gdx, value, prob; * execute_load original_scenarios.gdx, value, prob; * ---------- SCENRED 缩减框架 ---------- * 声明场景树对象并写入缩减配置 * 以下接口为示意不同版本可能有差异 SCENRED /树的定义/; * 设置缩减参数 SCENRED保留数量 10; SCENRED距离范数 2; SCENRED缩减策略 快速向后消去; * 执行缩减 $libinclude scenred * 读取缩减后的场景概率和新场景集 * 缩减完成后工具会把新的概率和场景索引返回这段代码的思路是把问题切成两步第一步准备原始数据和概率第二步让SCENRED做缩减。实际操作中我不会直接在GAMS里生成随机场景而是在外部采样好存成CSV或GDX再load进来。这样每个环节都能单独验证排查问题方便很多。3.3 关键参数的选择逻辑SCENRED参数里最影响结果的是三样保留场景数、距离范数、缩减策略。保留场景数是用户指定的它直接决定缩减后模型的规模。场景数越少模型跑得越快但概率距离会变大分布偏差增大。这个数字怎么选没有标准答案要看你的问题对尾部风险的敏感程度。对电力系统调度这类要保证极端场景不切负荷的问题保留场景数太少了很容易把极值丢掉。距离范数p的选择也讲究。p1时对场景间距离的惩罚是线性的缩减结果更关注“整体分布形状”p2时距离大的场景对惩罚的贡献呈平方增长缩减算法会更倾向于保留那些距离其他场景远、取值极端的场景。换句话说如果你的模型要处理极端风险p2通常更合适如果只是代表一般的波动p1就够了。缩减策略的选择则是在计算效率和全局最优之间的权衡。我的经验是先用快速向后消去跑通流程再用最优缩减做敏感性对比如果两者结果很接近说明缩减方案稳定可以直接用快速法上生产。3.4 跟采样数据的无缝配合场景缩减不是孤立的一步它通常排在蒙特卡洛采样或历史场景筛选的后面。我习惯的完整流程是历史数据清洗 → 随机采样生成大量场景比如1000个→ 用SCENRED缩减到目标数比如10到20个→ 重新优化求解。这套流程还有个好处就是采样阶段可以放心大胆地把场景数做多把分布的形状刻画细反正后面有缩减兜底。不用再为了求解能力而压缩采样数量导致原始分布失真。要注意的是缩减输出的是一个新场景集概率也已经重新分配过了后续模型里所有涉及期望值的表达式都要使用新概率而不是原概率。4. 完整实操案例风电出力场景缩减与机组组合4.1 算例设定我前一阵做了个某园区微网的机组组合优化随机源是两个风机出力和负荷需求。原始场景从历史数据补全后生成200个场景每个场景包含24小时的风电出力和24小时的负荷曲线。这个规模跑确定性等价模型已经很吃力因为机组组合本身有二进制变量200个场景展开就是200个平行机组组合问题耦合在一起。为了上生产环境我需要把场景数压到10个左右还要尽量保住原始分布的统计特征。4.2 场景生成代码示意场景生成在Python里完成用历史数据加蒙特卡洛噪声的方式扩充。生成后用GDX桥接传入GAMS。import numpy as np import pandas as pd # 读取历史风电和负荷数据这里用简化示意 wind_hist np.loadtxt(wind_history.csv, delimiter,) load_hist np.loadtxt(load_history.csv, delimiter,) n_scenario 200 rng np.random.default_rng(42) wind_scn [] load_scn [] for _ in range(n_scenario): # 随机选一天作为基础场景加噪声扰动 idx rng.integers(0, len(wind_hist)) wind_noise rng.normal(0, 0.08, size24) load_noise rng.normal(0, 0.05, size24) wind_scn.append(wind_hist[idx] * (1 wind_noise)) load_scn.append(load_hist[idx] * (1 load_noise)) wind_scn np.array(wind_scn) load_scn np.array(load_scn) # 导出GDX或者CSV再在GAMS中读取 np.save(wind_scenario.npy, wind_scn) np.save(load_scenario.npy, load_scn)采样时注意加一个随机种子的固定保证场景可复现。这一点很多人忽略结果做完一轮优化下次重新采样换了一批场景结果对不上排查问题相当痛苦。4.3 执行场景缩减把数据导入GAMS之后按照第3章的调用框架配置SCENRED保留场景数设为10距离范数选p2策略选快速向后消去。整个缩减过程很快200个场景降到10个耗时不到一分钟。缩减完成后我做了两件事验证结果第一检查新概率之和是否为1第二画缩减前后风电出力均值和95%分位数的对比曲线。两个指标匹配度都在可接受范围内说明10个场景基本抓住了原始分布的关键特征。4.4 缩减前后对比这里放一个我记录下来的对比表格数值做了脱敏处理但趋势很典型。指标原始场景缩减后10场景说明场景数量20010规模下降95%概率距离相对值00.038缩减后与原始集接近风电出力均值偏差基准0.6%期望值保住风电出力5%分位偏差基准2.1%尾部略受影响机组组合求解时间原方案不可行12分钟可落地求解负荷高峰场景覆盖全部保留1个极值场景需人工确认从表格能看出来场景缩减最大的收益是求解可行性。原方案200个场景的组合模型直接溢出缩减到10个之后可以稳定求解。代价是分位数估计有损耗但2%左右的分位偏差在工程上通常可以接受部分极端场景甚至更偏需要结合业务判断。4.5 这次实操踩过的坑这个算例里我踩过一个很典型的坑第一次缩减完直接拿新场景去跑优化结果发现目标函数值比预期差了很多。排查下来发现问题出在场景概率没有重新导入。SCENRED输出新概率之后我在后续模型里还惯性使用了原始均匀概率导致期望值计算偏差。还有一次我保留了8个场景结果缩减算法把95%分位数附近的尾部场景全删了优化结果在极端场景下出现切负荷。后来我把距离范数从p1改成p2极端场景保住了后续风险指标明显改善。这两个问题都不是代码错误而是对缩减机制理解不够深造成的写出来给大家避坑。5. 调参与效果验证别让缩减结果蒙混过关5.1 缩减质量怎么量化缩减完不能只看场景数量降下来了就收工必须做质量验证。最直接的方法是比较缩减前后的关键统计量均值、方差、分位数、相关性。如果你的优化模型里有风险约束比如CVaR还要专门比较尾部区域的偏差。另外一个很实用的手段是稳定性实验。固定随机种子重复做几次缩减对比每次优化出来的目标函数值波动范围。如果波动很小说明缩减方案稳定可以信任如果波动大说明场景数给少了或者缩减策略不合适需要调整。我在实际项目里推荐做一个“缩减数量-目标值”曲线分别缩减到5、8、10、15、20个场景记录优化目标值。通常你会看到场景数越少目标值偏差越大随着场景数增加目标值逐渐稳定到一个平台。选择平台期开始出现的最小场景数就同时兼顾了精度和计算效率。5.2 场景数到底选几个合适很多初学者会问到底保留多少个场景才合适。说实话没有万能的答案它取决于三个维度随机源的数量、模型对尾部风险的敏感度、可接受的计算时长。随机源越多需要的场景数通常也越多因为要在高维空间里覆盖分布。如果你的问题只关心期望值比如长期平均投资收益那么10到20个场景就够。如果问题关心极端风险比如电网在尖峰负荷下的可靠性那宁可少求几个场景也要手动确认极端场景被保留下来。我建议用肘部法则帮助决策画一条“场景数 vs 概率距离”的曲线看距离下降速度从哪个点开始明显放缓那个转折点就是性价比最高的场景数。这个方法直观、好解释适合跟不懂模型的业务方沟通。5.3 距离范数p的取舍距离范数p1和p2的区别前面提过这里展开聊一下为什么它这么重要。p1时距离的惩罚是线性增长算法倾向于平衡地保留各个区域的代表性场景整体分布形状保持得比较好但极端点的代表性会被稀释。这在大多数常规问题里够用计算稳定输出结果比较平滑。p2时距离大的场景对整体距离的贡献被放大算法会刻意保留那些与大多数场景都远的外点。这对关注极端风险的模型是好事但也可能带来一个问题缩减结果里出现概率极低、取值极端的场景这些场景权重可能很小但存在性对模型约束产生了不成比例的影响。所以我对你的建议是默认先跑p2看极端场景是否被保留如果结果表现出过度保守再试试p1对比。两种范数各跑一遍差个20%以内说明模型对缩减参数不敏感放心用。5.4 减完还需要做的工作重要的是缩减不是终点它只是把你的输入数据从“原始的大场景集”变成了“紧凑的新场景集”。后续优化做完别忘了对决策变量做一次在原始场景集上的回测验证。也就是用缩减后得到的决策放到原始200个场景里重新评估目标函数和风险指标看看偏差有多大。这一步在实际业务里很关键也是很多理论教程不会强调的部分。缩减后的决策在原始分布下到底靠谱不靠谱只有回测能回答。我见过不少项目缩减做得热闹最后一步回测没做等业务方拿真实数据考核时才发现问题。先用缩减场景优化再用原始场景评估这个闭环才算完整。6. 常见问题与排查技巧实录6.1 数据结构与语法问题SCENRED最常遇到的报错基本都是场景树数据结构不对。比如概率参数缺失、场景值矩阵维度不匹配、场景索引不一致。这些问题通过逐项检查数据维度就能定位我习惯在GAMS里加几个abort$语句在调用SCENRED之前校验概率之和是否在1附近、场景值是否存在缺失值。* 检查概率和是否为1偏差过大时中止 abort$(abs(sum(sc, prob(sc)) - 1) 1e-6) 场景概率之和不为1;另外不同GAMS版本的SCENRED接口细节有差异。如果你从老项目拷贝代码很可能出现关键字对不上、报错找不到对象的情况。遇到这种问题优先查本机GAMS安装目录下自带的SCENRED示例对照着改比自己硬猜快得多。6.2 概率相关陷阱概率问题是最隐蔽的坑。场景缩减完成后新概率不会自动覆盖到模型里其他数据集你需要手动把新概率重新赋值。还有如果你在缩减前对概率做了归一化或者加权处理缩减后这些操作要重新审视一遍。我遇到过一个案例原始场景概率不是均匀分布而是按历史频数加权。缩减完以后我忘了重新归一化导致后续期望值始终偏大。最后排查到是概率归一化的问题重算之后结果正常。6.3 缩减结果异常怎么办缩减后如果出现目标函数值剧烈变化优先检查三件事是否保留了极端场景、概率是否重新赋值、求解器是否收敛在次优解。前两个是数据问题第三个是求解问题三种情况的排查路径完全不同不要混在一起试。如果排除了以上三个因素目标值仍然不对那多数是场景数量太少。这时候别急着调算法参数先增加保留场景数看目标值是否恢复。目标值对场景数敏感是正常的尤其是有CVaR或鲁棒约束的问题尾部信息丢失的影响会直接传导到目标函数。6.4 性能优化建议场景缩减虽然本身很快但和采样、求解合在一起时也要注意整体流程的性能。我建议把采样、缩减、求解、回测四个环节写成独立的脚本或批处理文件每个环节输出中间结果。这样每次需要调整参数时不需要从头跑一遍采样直接在上一步结果上做增量修改。还有一个性能技巧在大规模问题上先缩减到一个比较少的场景数快速试算确认模型逻辑没问题后再逐步增加场景数跑正式结果。这比一上来就全量场景跑能省出大量调试时间。我在项目里用这个策略通常能把模型调通时间缩短一半以上。最后再分享一点实操体会做了这么多轮场景缩减相关的项目我个人最深的体会是场景缩减的价值不在于“让模型算得快”而在于“让你敢把不确定性放回模型里”。很多团队为了求快直接把不确定性简化成确定性期望模型爽了但优化结果在真实环境里一测就露馅。有了缩减你可以在计算资源和分布保真度之间做平衡而且这个平衡是量化可控的。如果你的项目还在用几百个场景硬扛求解我建议你下个项目开始前把这套流程加到建模链路里。先说结论、再做数据、然后缩减、最后优化加回测。熟练之后整套流程跑下来不到半天但模型的质量和稳定性提升是肉眼可见的。希望这期分享能帮你少踩几个坑少加几天班。