干这个方向的人应该都有体会,每次看到"数据驱动""两阶段""分布鲁棒"这几个词叠在一起,第一反应都是"这又能水一篇论文"——但如果你真的动手用过Matlab去复现,会发现里面有不少坑是论文里根本不写的。这篇博文,我准备把这个课题从建模到求解再到代码实现,完整拆开讲一遍,围绕电热综合能源系统这个场景,说明白1-范数和∞-范数约束在数据驱动分布鲁棒里面到底扮演什么角色,以及你在Matlab里应当怎么落地。无论你是刚入门的研究生,还是已经调过几版CCG算法的老手,我相信这里都有你可以直接用的东西。
数据驱动的两阶段分布鲁棒(1-范数和∞-范数约束)的电热综合能源系统研究(Matlab代码实现)
1. 问题从哪来:为什么电热综合能源系统需要"分布鲁棒"
1.1 电热系统里的不确定性到底有多棘手
先问一个问题:一个电热综合能源系统(Integrated Electricity and Heat System, IEHS)里面,最让调度员头疼的随机量是什么?答案基本集中在两个——风电出力和热负荷。风电出力就不用细说了,天然具有间歇性;但热负荷的随机性容易被忽略,它受天气、建筑保温、用户行为影响,波动幅度甚至比电力负荷还大。而偏偏电和热在系统中是强耦合的,联结二者的核心设备是热电联产机组(CHP),它的电出力和热出力在运行区间内互为约束,不能单独调节。这意味着电源侧的风电不确定性、负荷侧的热需求波动,会通过CHP的运行域传导到整个系统,导致"一步错、步步错"。
传统的处理手段有两类。一类是随机规划(Stochastic Programming),假设不确定量的概率分布是完全已知的;另一类是鲁棒优化(Robust Optimization),只用一个不确定集合来描述所有可能取值,完全放弃概率信息。前者的问题是:真实系统中你根本拿不到精确分布,顶多是从历史数据里估计一个经验分布。后者的问题是:过于保守,为了覆盖极端场景,调度结果的经济性会变得很差,而且这种保守是"盲目"的——它不区分"这个场景概率很高"和"这个场景十年一遇"。
那有没有一种方法,既利用数据里的概率信息,又不完全依赖某个假设的分布?这就是分布鲁棒优化(Distributionally Robust Optimization, DRO)登场的理由。它假设不确定量的真实分布属于一个"模糊集"(Ambiguity Set),这个集合由历史数据构造,覆盖所有可能的分布。我们优化的目标是:在最坏情况分布下,让总成本最小。换句话说,DRO站在随机规划和鲁棒优化中间——用数据构造模糊集,在模糊集内寻找最坏分布,从而兼顾鲁棒性和经济性。
1.2 为什么偏偏选"两阶段"
再来看"两阶段"三个字。在电热系统的日前调度中,决策天然分两个阶段:
- 第一阶段(日前决策):在不确定性实现之前,决定机组启停状态、各机组的基础出力基准点、储热罐的充放热计划。这些决策一旦做出,当天之内基本无法调整。
- 第二阶段(实时调整):风电出力、热负荷的实际值逐渐明确后,系统通过调整机组出力偏差、切负荷、弃风等手段,保证供需平衡。这个阶段的成本是"情景依赖"的,不同场景下的调整代价不同。
用一个直白类比:第一阶段的决策好比你提前订好了餐厅和菜品(不能退了),第二阶段是你落座后根据当天胃口好坏决定加菜还是打包(可以灵活应对)。好的调度策略,就是让第一阶段决策尽量"平衡",使得第二阶段无论出现什么情况,总代价都可控。两阶段分布鲁棒模型,就是在第一阶段就考虑第二阶段的最坏情况期望成本,从而导出在不确定性下长期表现最优的日前计划。
这两个阶段合在一起,正好对应了一个整数变量加连续变量的混合整数线性规划(MILP)问题——而这也是为什么这个项目能在Matlab中用YALMIP配合求解器来解决的原因。
2. 数学模型拆解:1-范数和∞-范数约束的真实含义
2.1 模糊集怎么构造才叫"数据驱动"
正式写模型之前,必须把一个概念搞清楚:分布鲁棒优化里的"数据驱动",本质上是说模糊集不是拍脑袋定的,而是从历史数据中生成的。第一步,把风电出力和热负荷的历史数据整理成N个离散场景,每个场景对应一组不确定参数取值。假设每个场景的权重(即概率值)为(p_k),初始经验分布为(p_0),其中(p_{0,k} = 1/N)。
然后关键来了:真实分布不一定等于经验分布,但我们相信它不会离经验分布太远。怎么度量"太远"?论文里常用的是概率距离或范数距离。这个课题选用的是1-范数和∞-范数同时约束,模糊集可以写成:
[ D = \left{ p \in \mathbb{R}^{N} \ \middle| \ | p - p_0 |1 \le \theta_1, \ | p - p_0 |{\infty} \le \theta_{\infty}, \ \sum_{k=1}^{N} p_k = 1, \ p_k \ge 0 \right} ]
其中:
- (| p - p_0 |1 = \sum{k=1}^{N} |p_k - p_{0,k}|),约束所有场景概率偏差的绝对值之和;
- (| p - p_0 |{\infty} = \max{k} |p_k - p_{0,k}|),约束单个场景概率偏差的最大值;
- (\theta_1) 和 (\theta_{\infty}) 分别是两个范数约束的半径(允许偏差上界)。
这个构造方式不是随便选的,它有两个非常实际的好处。第一,相比Wasserstein距离模糊集或KL散度模糊集,范数约束模糊集的可行域是一个多面体,线性规划求解方便;第二,1-范数和∞-范数是互补的——1-范数约束限制整体偏移总量,∞-范数约束防止某个点的概率被单独拉得太极端。两者结合,模糊集既不宽得离谱,又不会出现某个场景概率被调成0或者过高的病态情况。
2.2 两阶段模型的完整表达式
有了模糊集,两阶段分布鲁棒模型可以写成如下形式:
[ \min_{x \in X} \ c^T x + \max_{p \in D} \sum_{k=1}^{N} p_k \cdot Q(x, \xi_k) ]
其中:
- (x) 表示第一阶段决策变量,(X) 是满足机组启停、爬坡、储热罐容量等约束的可行域;
- (c^T x) 是第一阶段的燃料成本、启停成本和购电成本;
- (Q(x, \xi_k)) 是在第一阶段决策固定为x、不确定量取第k个场景值(\xi_k)时,第二阶段的最优运行调整成本。
这个式子的核心逻辑是:外层先选第一阶段计划,内层在最坏分布下评估期望调整成本。特别要注意,(\max_{p \in D})把分布概率作为决策变量,因此这里的"期望"不是对单一固定分布取期望,而是在一系列可能分布中,选那个让成本最大的分布来计算期望——这正是DRO保守性的来源,也是它相比随机规划能给出"稳妥方案"的原因。
再展开第二阶段问题(Q(x, \xi_k)),它是一个线性规划,目标函数为:
[ Q(x, \xi_k) = \min_y \ d^T y ]
约束条件包括:
- 电力平衡约束:(\sum_{\text{unit}} P_{\text{unit}} + P_{\text{wind},k} - P_{\text{curtail},k} = P_{\text{load},k} + P_{\text{HP}} + P_{\text{EB}})
- 热力平衡约束:(\sum_{\text{CHP}} H_{\text{CHP}} + H_{\text{GB}} + H_{\text{discharge},k} - H_{\text{charge},k} = H_{\text{load},k})
- 机组出力调整上下限、CHP电热运行可行域、储能约束等
其中(y)是第二阶段的调整决策变量(各机组出力偏差、弃风量、切负荷量、储热罐充放热功率),(d)是相应的惩罚系数(比如弃风惩罚、切负荷惩罚,通常设置得远高于正常发电成本,以保证调度机制不乱切)。
这里还有值得一提的细节:第一阶段和第二阶段通过"机组出力基准点"和"储热罐充放热计划"耦合。如果第一阶段定的储热罐放热计划不合理,第二阶段热负荷一波动,要么切负荷,要么启停燃气锅炉,成本瞬间飙升。DRO的价值恰恰在这里——它会逼着第一阶段在模糊集内把所有场景都"想一遍",得到一个"无论哪个场景真实发生,都不至于太狼狈"的储热计划和出力计划。
2.3 为什么是1-范数和∞-范数而非其它
很多人第一次看文献会问:为什么不用Wasserstein距离?为什么不用矩模糊集?选择1-范数和∞-范数有几个实际原因:
- 线性性:这两个范数约束在多面体上定义,线性规划或MILP求解器可以直接处理,不需要引入半定规划或二次约束。对于系统规模较大的电热综合能源系统,这关系到能不能在可接受时间内求得解。
- 对偶友好:在CCG算法框架下,内层求最坏分布是一个线性规划,其对偶问题的结构非常清晰,可以用对偶变量把max-min问题化成单层可解形式。
- 直观可解释:(\theta_1)直接告诉你"所有场景真实概率相对经验概率的总偏差上界",(\theta_\infty)告诉你在单个场景上最大偏差的容忍度。这比纯抽象的距离度量更容易面向工程人员解释和调参。
当然,它的代价是模糊集的形状比较"方",不如Wasserstein球那样在连续概率空间里有漂亮的度量性质。但如果你做的是电热系统日前调度这类工程落地型课题,实用性和可求解性往往比纯粹的理论优雅更重要——这也是我在复现这个方向时,觉得范数约束非常适合作为入门又足够支撑一篇论文的原因。
3. 求解算法核心:CCG迭代与Matlab实现细节
3.1 先把max-min问题重塑成可以解的形态
模型写出来是一回事,能求解是另一回事。直接面对上面的两阶段模型,内层"(\max_{p \in D} \sum p_k Q(x, \xi_k))"本质上是个嵌套优化问题,不能直接丢给Gurobi或CPLEX解。标准解法是列与约束生成(Column-and-Constraint Generation, CCG),它的思路和Benders分解有亲缘关系,但收敛速度通常更快。
CCG的核心思想是:把原问题拆成一个主问题(MP)和一个子问题(SP)。
- 主问题:包含第一阶段决策变量和一个有限的"最坏场景候选集合",每次迭代后把SP找出的最坏场景作为一个新的约束(或列)加入主问题;
- 子问题:给定第一阶段解,寻找使第二阶段成本最大的场景概率分布,并把该分布情形反馈给主问题。
迭代收敛判据是:主问题的目标函数值(下界)与子问题得到的真实第二阶段最坏期望成本之和(上界)之差小于给定间隙(\epsilon)。
具体到本项目,子问题给定x后为:
[ \max_{p \in D} \sum_{k=1}^N p_k \cdot Q(x, \xi_k) ]
其中(Q(x, \xi_k))本身是线性规划的最优值函数。通过线性规划强对偶,可以把每个(Q(x, \xi_k))改写为最大化(或最小化)的对偶形式,然后与外层的(\max_{p \in D})合并成一个更大的线性规划。这样SP就变成一个标准LP,可以直接用求解器解出最坏分布(p^)。随后MP在下一轮迭代中加入与(p^)对应的约束:
[ c^T x + \sum_{k=1}^{N} p_k^* \cdot Q(x, \xi_k) \le \eta ]
并继续求解,直到满足收敛判据。这个"主问题加一列约束、子问题解一个LP"的结构,在Matlab中用YALMIP建模非常自然。
3.2 CCG算法的逐步流程与Matlab代码骨架
下面给出一个贴近实际工程的CCG迭代流程,并配上Matlab核心代码骨架。
Step 1:初始化
- 生成历史数据场景集({\xi_k}_{k=1}^N);
- 设置模糊集半径(\theta_1, \theta_\infty);
- 初始化上界(UB=+\infty)、下界(LB=-\infty)、迭代次数(iter=1);
- 设置收敛间隙(\epsilon)(比如1e-4)。
Step 2:求解主问题(MP)
主问题是在当前最坏场景集合下求最低成本,并把辅助变量(\eta)加入目标函数:
[ \min_{x, y, \eta} \ c^T x + \eta ]
主问题的YALMIP建模如下:
% 主问题变量 x = sdpvar(ng, 1); % 第一阶段机组基准出力 u = binvar(ng, 1); % 机组启停状态 eta = sdpvar(1, 1); % 第二阶段成本辅助变量 % 目标函数 obj_MP = c' * x + sum(StartCost .* u) + eta; % 基本约束 Constraints_MP = [A_eq * x == b_eq, A_ineq * x <= b_ineq]; % CCG迭代中添加的最坏场景约束 % 每轮CCG迭代会追加一组约束(下详) ... ops = sdpsettings('solver', 'cplex', 'verbose', 1, 'display', 'iter'); optimize(Constraints_MP, obj_MP, ops);Step 3:求解子问题(SP)
子问题解出最坏概率分布p。这里为了求解方便,把第二阶段问题的对偶式加入,将max-min转成LP:
% 子问题:给定第一阶段解x_hat,求最坏分布 p = sdpvar(N, 1); % 场景概率变量 lambda = sdpvar(N, 1); % 对偶变量(每个场景对应一个) % 第二阶段成本Q_k用对偶表示:Q_k = max over dual variables % 这里示意性地将Q_k表示成对偶目标值的线性形式 Q_k = dual_expr; % 由具体模型推导 % 内层 worst-case 期望 obj_SP = - sum(p .* Q_k); % 若Q_k是对偶最大化,则这里是凸的 % 模糊集约束(1-范数 && ∞-范数) Constraints_SP = [sum(p) == 1, p >= 0, ...]; Constraints_SP = [Constraints_SP, norm(p - p0, 1) <= theta1]; Constraints_SP = [Constraints_SP, norm(p - p0, inf) <= theta_inf]; optimize(Constraints_SP, obj_SP, ops); p_star = value(p);Step 4:更新上界
用当前第一阶段解(x^*)直接计算所有场景下第二阶段真实成本,结合第一阶段成本构成上界:
[ UB = \min(UB, \ c^T x^* + \sum_{k=1}^N p_k^* \cdot Q(x^*, \xi_k)) ]
Step 5:更新下界与收敛判断
主问题最优值就是当前下界:(LB = \text{obj_MP})。如果(UB - LB \le \epsilon)则停止;否则把子问题找到的最坏分布对应的约束加入主问题(就是给(p^*)固定后,把第二阶段成本表达式作为约束加入MP),迭代次数加1,回到Step 2。
3.3 代码里最容易踩的坑
这个算法流程看起来不算复杂,但真正在Matlab里把代码调通,有几个坑我踩过很多次,值得专门提醒。
坑一:第二阶段问题不可行。求解子问题时会发现某些场景下,第一阶段给定的调度计划根本满足不了平衡约束。这通常是因为第一阶段忘记预留足够的调节空间,或者机组出力区间设置得不合理。解决办法是在第二阶段目标中加入松弛变量(切负荷量、弃风量),并设一个很大的惩罚系数。这不仅是数值技巧,在工程上也有实际意义——紧急情况下切一点负荷总比全系统崩溃要好。
坑二:YALMIP的norm函数在大规模场景下会生成很多额外变量。1-范数和∞-范数可以用线性约束等价替换,不一定要用norm函数。比如1-范数约束等价于引入辅助变量(t_k\ge0),加上(p_k - p_{0,k} \le t_k)和(p_{0,k} - p_k \le t_k),以及(\sum t_k \le \theta_1)。用显式线性不等式实现,求解速度有时比直接调norm快不少,尤其当N达到几百上千的时候。
坑三:CCG初始迭代阶段上下界振荡,迟迟不收敛。常见的原因是子问题求解精度不够,或者主问题加入的约束写成了严格不等式。另一个原因是模糊集半径设置偏大,导致子问题反复找极端场景,主问题不断加约束,迭代步数暴涨。实践中我会先检查半径是否合理,再检查约束是否添加正确。
坑四:YALMIP + CPLEX/Gurobi的版本兼容问题。尤其是2023版之后,一些老代码里调用的sdpsettings参数名可能失效。建议在代码开头统一设置求解器为'cplex'或'gurobi',并把输出信息重定向到文件,方便回溯调试。
4. 系统建模:电热综合能源系统的约束与参数怎么定
4.1 系统组成与设备运行域
算例系统不宜太复杂,适合复现和迭代调试。我通常采用改进的IEEE 6节点电力系统,耦合一个4节点热力系统。主要设备包括:
- 两台常规燃煤机组(G1、G2),作为第一阶段主力调节电源;
- 一台抽凝式CHP机组,电热联合运行,可行域是典型的梯形区域;
- 一台燃气锅炉(GB),作为热力备用;
- 一个储热罐(HS),承担热负荷转移功能;
- 风电场(WF),输出功率为不确定量;
- 电力负荷和热负荷,其中热负荷也带有一定随机性。
关键设备运行约束要写对,尤其是CHP的运行域。抽凝式CHP的可行电出力与热出力关系可以近似表示为:
[ P_{\text{CHP}} \in [\underline{P}{\text{CHP}}(H{\text{CHP}}),\ \overline{P}{\text{CHP}}(H{\text{CHP}})] ]
即电出力上下界都是热出力的线性函数,构成一个梯形。这组约束决定了系统的电热耦合强度——CHP在满足热负荷的同时,实际上"绑架"了部分电出力,这是电热综合能源系统调度难度的根源。
4.2 不确定性数据生成与场景缩减
要体现"数据驱动",首先要有历史数据场景集。实际项目里我们可能只有预测曲线和误差统计信息,因此常通过蒙特卡洛或拉丁超立方采样生成N个等概率样本。
例如,风电出力可表示为预测值加误差:
[ P_{\text{wind},k} = P_{\text{wind,forecast}} + \varepsilon_k,\quad \varepsilon_k \sim \mathcal{N}(0, \sigma_w^2) ]
热负荷同样处理。要注意:样本数量N不能太小——范数约束模糊集的经验分布是以N个场景为支撑的,N太小则经验分布可信度低,模糊集半径也难以合理化。我一般取N=200~500。如果N太大,第二阶段LP的求解规模会上升,不过现在CPLEX解几百个场景的LP是没问题的,影响不大。
场景生成好后,对每个场景做归一化,确保数值稳定性。这是个大坑——不归一化的话,热负荷数值可能上千,风电出力数值却只有几十,两者一起进入模糊集的范数距离计算时,风电的偏差会被热负荷的绝对值掩盖,导致模糊集形同虚设。
4.3 存储与参数标定
给出一个实用的参数配置表,可以直接用于复现。这个表是我在调参中积累的默认档位,适合做一个基准算例,后续再根据你的研究需要调整。
| 参数名称 | 数值 | 说明 |
|---|---|---|
| 场景数 N | 200 | 历史数据采样数 |
| θ1 (1-范数半径) | 0.6 | 总概率偏差上限 |
| θ∞ (∞-范数半径) | 0.05 | 单场景概率偏差上限 |
| 收敛间隙 ε | 1e-4 | CCG迭代停止条件 |
| 弃风惩罚系数 | 50 $/MWh | 第二阶段目标惩罚 |
| 切负荷惩罚系数 | 500 $/MWh | 远高于正常成本,防止随意切负荷 |
| 储热罐容量 | 200 MWh | 储热罐可存储热量上限 |
| CHP最大电出力 | 200 MW | — |
| 燃气锅炉最大热出力 | 150 MWth | — |
注意,θ1和θ∞不是拍脑袋定的,需要通过置信水平反推。常用的经验公式是:在置信水平α下,θ1满足(P{|p-p_0|_1 \le \theta_1} \ge \alpha)。这需要用到概率不等式估算。工程上最简单的方式是:换几组θ值跑一遍,观察结果成本的灵敏度。θ越大越保守,成本越高;选在成本急剧增加之前的"拐点"附近,通常是一个合理选择。这一点我会在后面详细展开。
5. 算例结果解读:怎么证明"分布鲁棒比随机规划更好"
5.1 对比实验设计
想让论文或报告有说服力,就要做对比。通常设置三组方案:
- 随机规划(SP):直接用经验分布(p_0)作为唯一分布,不走模糊集;
- 传统鲁棒优化(RO):不确定量落在区间内,不区分概率,最坏情况下取极端场景;
- 两阶段分布鲁棒(DRO):本文所用模型,模糊集由1-范数和∞-范数约束构造。
每组方案跑完后,对比三个指标:总成本、第一阶段计划、第二阶段最坏期望成本。
实际算例跑出来的典型结果是:
- SP的总成本最低,但它的风险也最大——一旦真实分布偏离经验分布,实际运行成本会急剧上升;
- RO最保守,总成本明显偏高,第一阶段几乎为所有可能场景都预留了大量备用,导致正常日子浪费;
- DRO介于两者之间,成本只比SP高几个百分点,却能保证在模糊集范围内不出现成本失控。
这个结果其实是符合直觉的:DRO用数据的概率信息换取了鲁棒性,而且通过调节θ1和θ∞,可以连续地在SP和RO之间移动——θ越小,方案越接近SP;θ越大,方案越接近RO。这种"可调性"在工程决策中非常宝贵。
5.2 置信度分析:为什么不真实分布怎么应对
再做一个更有说服力的验证:通过蒙特卡洛模拟,随机生成大量"真实分布"(不一定等于经验分布),然后分别把SP、RO、DRO三套方案放到这些真实分布下计算实际运行成本,统计成本分布。
结果一般会显示:SP方案的成本分布尾部很长——偶尔出现极端高成本;RO方案的成本分布集中但均值很高;DRO方案的成本分布尾部截断明显优于SP,均值又显著低于RO。这种"尾部风险控制"正是分布鲁棒真正的卖点:不是压最低平均成本,而是在不严重牺牲经济性的前提下,控制最坏情况下的实际损失。如果你的算例结果呈现这个趋势,说明你的模型和代码写对了。
5.3 参数灵敏度分析:θ怎么选才合理
θ1和θ∞的选取没有绝对标准,但做好灵敏度分析是论文必备内容。我也是在反复实验中发现,θ1对结果的影响通常比θ∞更明显,因为1-范数约束控制整体概率偏移的"总量",而∞-范数主要限制单个场景的极端偏移。
实际操作时,可以固定θ∞不变,把θ1从0.2、0.4、0.6、0.8、1.0依次增大,观察总成本和迭代次数的变化。典型趋势是:θ1增大到某个阈值之前,总成本缓慢上升;超过某个阈值后,成本可能大幅跳增,此时模糊集过大、方案过度保守。工程上选择阈值之前的"拐点"作为推荐值,比如0.6。同时,θ1增大会让CCG迭代次数增加,因为子问题更容易找出不同的极端场景。这个"成本-鲁棒性-计算量"的综合权衡,需要在报告里用图表呈现,结论才扎实。
6. Matlab代码调试中的常见问题与排查技巧
6.1 问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 子问题LP求解失败或不可行 | 模糊集为空(θ1太小或θ∞太小导致p无法满足所有约束) | 增大θ1或θ∞;检查sum(p)=1与范数约束是否冲突 |
| CCG迭代次数过多(>50) | 模糊集半径过大;场景数量过多;收敛间隙太小 | 适当减小θ1;对场景数做缩减;放宽ε到1e-3 |
| 上界持续不下降 | 主问题没有加入正确的割约束;第二阶段成本计算与子问题不一致 | 核对主问题新增约束是否使用了子问题返回的p_star;检查Q_k表达式是否一致 |
| 求解结果出现负成本或异常大数 | 目标函数符号写反;惩罚系数设置过大导致数值问题 | 逐项检查目标函数;调整惩罚系数到合理范围 |
| YALMIP报"Unable to instantiate solver" | 求解器路径未配置或版本不兼容 | 在yalmip/operators中检查求解器是否支持LP/MILP;重新添加CPLEX/Gurobi路径 |
6.2 我调试了几个小时才发现的细节
有几个非常不起眼的细节,浪费过我大量时间,这里专门写出来。
第一个是关于YALMIP中repmat的使用。当第二阶段约束里涉及多个场景的表达式时,很多人习惯用循环逐个写约束。场景数少还行,场景数一多,模型构建时间成倍增长。更好的做法是向量化:用repmat或直接构造二维变量矩阵,一次性写入所有场景的约束。这么做,模型构建时间可以从几十秒降到一两秒。
第二个是通过value()函数断言检查结果。每次迭代后,手动校验一下场景概率p_star是否满足sum(p)=1,是否都在[0,1]区间内。这个习惯帮我发现过两次模糊集约束写错的问题——一次是因为缺少sum(p)=1,另一次是因为∞-范数约束误写成了逐元素约束的平方和。
第三个是关于求解器日志。CPLEX在求解MILP时日志非常长,但关键信息是"Best bound"和"Best integer"两个值。建议写一个简单的日志解析脚本,自动提取这两个数并绘制收敛曲线,能直观判断CCG和MILP的收敛趋势。没有这条曲线,你很难判断迭代是"在收敛"还是"卡住了"。
第四个是一个容易混淆的点:1-范数约束中的θ1与∞-范数中的θ∞,它们与场景数的关系需要单独校准。当N改变时,经验分布p0的各分量p0_k=1/N也会变,所以同样的θ1和θ∞在不同N下对应不同的"模糊程度"。做灵敏度分析时,务必固定N,否则结果不可比。
6.3 提升代码运行效率的三个小技巧
- 场景缩减:如果初始采样场景数达到1000+,可以先做k-means聚类或同步回代缩减(SBR),把场景数降到200~300,再进入模糊集模型。这样CCG的迭代次数和子问题规模都会显著降低。
- 热启动(Warm Start):CCG每次迭代求解主问题时,可以以上一轮的解作为初始点,YALMIP的
assign和optimize的'usex0'参数可以配合使用。在MILP问题中热启动效果尤其明显。 - 缓存子问题的对偶解:在CCG迭代过程中,相同场景在相邻迭代中可能被反复求解,如果能够缓存每个场景的对偶变量初始值,可以加速LP求解。虽然CPLEX自带了一些记忆机制,但在自定义实现里显式复用初始点仍然能带来可感知的提升。
7. 我个人的几点实操体会
这个项目做下来,我最大的体会是:数据驱动两阶段分布鲁棒优化并不像有些论文写得那么玄乎,它本质上就是"用数据约束概率分布的不确定性范围,然后在这个范围内做最坏情况决策"。它的难点反而在细节——模糊集怎么表达、对偶怎么转、CCG的上下界怎么算、YALMIP的向量化怎么写,每一步都有不止一个坑。但把这些坑踩平之后,你会发现这套框架在电热综合能源系统里特别好用,因为CHP的强耦合特性和储热装置的时间转移特性,恰恰能让两阶段结构发挥出最大价值。
最后再分享一个小技巧:当你面对自己的算例结果不知道如何解释时,先不要急着调参数,而是用蒙特卡洛法把SP、RO、DRO三套方案放到"真实分布"下做一次批量模拟,画一张成本分布箱线图。这张图基本能一眼看出DRO的价值,也是论文中最容易打动审稿人的一张图。很多时候,方案好坏的差异并不在第一阶段的计划表上,而在于它面对真实不确定性时的"耐受力"——这个道理,做调度的人应该都懂。