居民负荷调度这几年越来越热,原因很现实:空调、电动汽车、户用储能大面积普及,家里一天的电量曲线越来越陡,电网侧削峰的难度也水涨船高。我最近做的一个项目,就是用双层鲸鱼算法求解基于非合作博弈的居民负荷分层调度模型,全部流程在Matlab里搭完并且成功收敛。这篇文章不整虚的,从为什么用博弈模型、什么时候用双层结构,到上下层的目标函数怎么写、WOA怎么嵌套、代码怎么组织、调参怎么避坑,一条线讲清楚。适合正在做需求响应、负荷管理、电力市场仿真的研究生和工程师,也适合对智能优化算法怎么落地的同学参考。
1. 先把问题说清楚:居民负荷调度里面藏着怎样的博弈
1.1 负荷不是铁板一块,先给居民用电设备分分类
做调度模型的第一步,不是急着写目标函数,而是把居民家里那些五花八门的设备按“可调度属性”归类。我一般分成四类。
第一类是刚性负荷,比如照明、电视、电脑,它们的用电时段基本固定,功率也改不了,这类负荷是硬约束,模型里直接当成基线负荷曲线叠加进去。
第二类是可转移负荷,典型的是洗衣机、洗碗机、电动汽车充电,它们的特点是总用电量(或者叫任务量)固定,但在一天哪些时段完成是可以挪动的。比如电动汽车回家后充3度电,凌晨充还是晚上10点充,体验差别不大,但电价差别很大。
第三类是可中断或可调节负荷,比如空调、电热水器,功率在一定范围内连续可调,启动和停机有柔性,但和人的舒适度强相关。调度这类负荷时,不能只算电费,还得把舒适度损失折算进去,否则优化结果虽然便宜,但用户根本受不了。
第四类是储能设备,户用电池。它的独特之处在于既能充电又能放电,相当于给用户一个“时间搬移”的手段,电价高的时候放,电价低的时候充,可以把很多矛盾对冲掉。
| 设备类型 | 典型设备 | 是否可平移时段 | 是否可调节功率 | 是否涉及舒适度 |
|---|---|---|---|---|
| 刚性负荷 | 照明、电视 | 否 | 否 | 弱 |
| 可转移负荷 | 洗衣机、EV、洗碗机 | 是 | 否 | 一般 |
| 可调节负荷 | 空调、热水器 | 部分 | 是 | 强 |
| 储能设备 | 户用电池 | 双向 | 是 | 无 |
把这四类分开建模,后面写约束条件和目标函数的时候才会清楚。很多刚接触这个方向的人上来就想把整个家庭建模成一个统一优化问题,结果设备特性互相打架,约束矩阵特别丑,求解也不稳定。我建议还是老老实实分层分设备建模。
1.2 各打各的算盘:售电公司与居民构成非合作博弈
为什么居民负荷调度要用非合作博弈?因为现实里做决策的并不是同一个人,而且大家的利益不完全一致。
上层是售电公司或者配电网运营商,它手里握着定价权,希望通过制定分时电价获得售电利润,同时又不希望负荷过于集中导致峰谷差失控。下层是居民用户,看到电价之后,在“省钱”和“舒适”之间权衡,给出自己的用电计划。两边都不会主动牺牲自己的利益去成全局队。
这种决策有先后顺序、利益不一致、互相不信任的结构,就是典型的主从递阶博弈,学术上叫Stackelberg博弈。上层先出价格,下层把价格当作已知条件做最优响应,上层再根据下层的响应调整价格,循环往复。
我经常用一个生活类比来理解这个结构:商场搞促销先定折扣,顾客看折扣决定买什么,商场发现顾客反应不够热烈就再调折扣,最后大家都稳定在一个“谁都不愿意单方面改变策略”的状态。这个状态就是纳什均衡。在电力模型里,均衡点就是一组电价和一组用电计划,售电公司改电价会降低自己利润,居民改用电计划会增加自己电费,两边都被锁住。
1.3 为什么一定要“分层”调度,能不能一步到位
有人会问:既然目标是削峰填谷、降低电费,为什么不把电价和用户用电计划放在同一个优化问题里全局求解?答案是,这种做法相当于强行让上下层变成合作关系,与真实机制不符。
从机制上看,电价是上层的手段,用电是下层的自由选择,上下层天然就是递阶关系。从数学上看,如果直接合并,就是一个包含混合整数变量和主从约束的复杂问题,用KKT条件把下层问题替换到上层约束里虽然是一个方向,但下层含有设备启停的0/1变量,KKT推导非常繁琐,非凸情况下还容易出现伪解。
或者说去掉KKT转换这个弯子,直接用双层迭代逼近均衡,在工程上的可维护性和可解释性都更强。这也是我选择“双层鲸鱼算法”来求解的核心原因:上层一个优化问题,下层一个优化问题,各算各的,通过数据流反复交互直到收敛。
2. 双层鲸鱼算法的设计思路与核心机制
2.1 WOA为什么值得选
智能优化算法里,粒子群、遗传算法、差分进化都很常见,我最后选鲸鱼算法(WOA)不是因为它名字萌,而是因为三个原因。
一是参数少。WOA的核心控制参数就几个:包围系数A、随机系数C、螺旋常数b,以及概率p。哪怕你之前没写过这个算法,照着标准公式半小时就能把主循环写出来,调参的压力比粒子群和遗传算法小不少。
二是搜索机制有层次。WOA模拟座头鲸的泡泡网捕食行为,包含三种位置更新机制:随机搜索、包围猎物、螺旋上升。随机搜索负责全局探索,包围和螺旋负责局部开发,三者通过p值和|A|的阈值切换。这个结构天然适合双层嵌套,因为内层需要较强的局部收敛能力,外层需要一定的探索能力,WOA正好两头都占。
三是实现成本低。不超过一百行的标准WOA函数,既可以用在内层设备调度,也可以用于外层电价搜索,代码复用率很高。后面调试问题的时候,只需要维护一套基础算法逻辑,不用为每个模块单独写一套不同风格的优化器。
2.2 “双层”不是两个WOA简单串起来,而是上下层决策的循环映射
有人可能以为双层WOA就是“跑完外层WOA然后再跑内层WOA”,这是误解。它的本质是两层优化问题通过决策变量互相耦合,形成循环迭代关系。
我项目里的具体结构是这样的:上层决策变量是24时段的分时电价向量,维度是24。下层决策变量是各设备的功率计划,如果是6个设备、每个设备24时段,解向量维度就是6×24=144。上层每给出一组电价,下层都要在这个电价下重新优化一次居民用电计划。
整个循环用伪代码看是这样的:
外层WOA(电价层) while 未达到最大迭代 对于每个鲸鱼个体:解向量 = 24时段电价 对每个电价个体: 调用内层WOA求解居民最优用电计划 返回对应总负荷曲线 计算上层目标函数(售电利润或峰谷差) 根据目标函数更新外层鲸鱼位置 end 输出最优电价 + 该电价下的居民负荷计划这个过程里,内层WOA的每次求解都不是随便跑一跑,而是给定电价后居民侧的最优响应。外层根据这个响应评估自己的收益,再去更新价格,实际上就是在逼近主从博弈的纳什均衡。
值得注意的是,外层每评估一个个体,内层就要完整优化一次,所以计算开销非常大。如果你把外层种群设成50、迭代100次,内层种群也设成50、迭代100次,总评价次数就是外层5000次乘以内层5000次,最后等于是2500万次目标函数评估,Matlab跑起来会非常酸爽。后面我会具体说怎么控制这个开销。
2.3 用双层WOA逼近纳什均衡的合理性
这个方法能不能找到精确的纳什均衡?严格来说很难证明。但工程应用里,我们并不需要一个解析的均衡点,只需要一个稳定的可接受解。
从实际操作角度看,分时电价本身就是离散化的,一天24小时已经是粒度;居民负荷预测也不可能绝对精确。在这种情况下,用双层WOA迭代出来的近似均衡,精度已经足够支撑方案对比和机制设计。
还有一个更实际的原因:下层问题是混合整数优化,包含设备启停的0/1变量和储能SOC这种连续性状态量。用KKT一次性消元需要很强的凸性假设,而居民负荷模型的约束里既有线性也有非线性,写KKT条件不仅繁琐,还容易出错。相比之下,上下层各用一个WOA,直接在数据层面交互,代码改动小,逻辑透明,出了问题也知道该查哪一层。
3. 模型搭建:目标函数、约束与参数设置
3.1 上层决策模型:售电公司的目标怎么写
上层的核心是电价。我设的决策变量是24个时段各自的电价p1到p24,每个时段电价有上下限,比如最低0.2元每度、最高1.5元每度。
上层目标函数我做了两个版本。第一个是纯售电利润最大化,表达式是:
售电利润 = 分时电价点乘总用电负荷 - 购电成本点乘总用电负荷
其中购电成本可以设为常数值,或者按另一个分时曲线,看你研究的是独立售电公司还是电网统一购售电模式。
但纯利润目标有一个问题:如果电价设置得过高,居民会大幅削减可调负荷,总售电量下降,利润反而不一定大。这虽然能体现博弈关系,但输出结果不太适合直接展示“削峰填谷”的效果。所以我用了第二个版本,在利润后面再加一个峰谷差惩罚项:
上层目标 = 售电利润 - lambda * 总负荷曲线的标准差这个lambda就是上层对削峰填谷的重视程度。设成0,上层就是纯粹的利己商家;设得大,上层就变成“既想赚钱又要保障系统安全”的调度者。不同lambda会得到完全不同的均衡结果,这一块有兴趣的同学可以自己做敏感性分析,会发现很有意思的现象。
上层的约束相对简单,除了电价上下限,我一般还限制相邻时段电价不能波动太剧烈,不然用户会觉得价格离谱,也不符合实际电价政策。
3.2 下层决策模型:居民的电费与舒适度权衡
下层是居民侧。决策变量是所有设备在24时段内的功率矩阵。对于洗衣机、洗碗机这类离散设备,功率只有0和1两种状态;对于空调、电热水器,功率可以在上下限之间连续调节;对于储能电池,充放电功率可以是负到正之间的连续值。
居民的目标函数是:
min 总电费 + omega * 舒适度损失总电费就是电价向量点乘总负荷曲线。舒适度损失怎么写是建模的关键。我用的方式是:对空调,用实际功率和偏好功率的偏差平方来度量;对可转移负荷,用任务完成时段和偏好完成时段的偏差来度量。omega是舒适度权重,如果你设成0,优化结果会非常激进,居民把所有负荷全挪到谷时,但洗衣机深夜开、空调不开,生活体验很差;如果omega设太大,居民几乎不响应电价,负荷曲线和优化前没区别,上层博弈也就没意义。
下层的约束比较多,我列一下:
- 设备功率上下限约束;
- 可转移负荷的任务量约束:一天的总用电量是固定的;
- 储能电池SOC递推约束:SOC(t+1) = SOC(t) + 充电效率乘以充电功率 - 放电功率除以放电效率;
- 电池SOC范围约束,通常在0.1到0.9之间,不能过充过放;
- 电网互联功率上限,这个视研究需要,可加可不加。
3.3 仿真参数怎么定,才既有代表性又不难收敛
参数设置直接决定你的模型能不能在可接受的时间内跑出稳定结果。我调过一轮之后,给出下面这套比较稳妥的初始配置。
| 参数项 | 推荐值 | 说明 |
|---|---|---|
| 调度周期 | 24时段,1小时粒度 | 基础配置 |
| 居民户数 | 先用1户,再扩展N户 | 验证逻辑不要贪多 |
| 可调度设备 | 洗衣机、洗碗机、EV、空调、热水器、电池 | 覆盖四类负荷 |
| 电价初始值 | 谷0.3、平0.5、峰0.8元/kWh | 先给合理初值 |
| 电价边界 | 0.2 ~ 1.5元/kWh | 防止算法搜索到离谱价格 |
| 外层WOA种群 | 20 | 比较稳妥 |
| 外层WOA迭代 | 30 | 能保证收敛趋势 |
| 内层WOA种群 | 20 | 不需要太大 |
| 内层WOA迭代 | 50 | 保证居民响应充分 |
| 电池容量 | 10 kWh | 常见户用规格 |
| 电池最大功率 | 2 kW | 和家庭线路匹配 |
这个配置跑下来,单户模型的耗时大约在几十秒到几分钟级别,取决于你的电脑性能。如果是多户模型,内层要按户循环,耗时线性增加,所以先用单户调通逻辑是最高效的策略。
4. Matlab实现关键环节拆解
4.1 工程结构怎么组织
很多初学者拿到这种双层模型,习惯把所有代码写完在一个main脚本里,后面一改约束就头疼。我强烈建议按函数模块拆分,至少包含下面这几个文件:
main_bilevel_woa.m % 主入口 load_base_data.m % 设备参数和基线负荷数据 obj_upper.m % 上层目标函数 obj_lower.m % 下层目标函数 WOA_standard.m % 标准WOA优化器 plot_result.m % 结果绘图这样设计的好处是,内层和外层共用同一个WOA_standard函数,只是在调用时传入不同的目标函数句柄和解空间维度。如果你后面想把内层换成粒子群或者遗传算法,只需要改一行调用,不需要重构整个工程。
主入口代码大概长这样:
% main_bilevel_woa.m clear; clc; close all; rng(2024); Data = load_base_data(); % 外层WOA参数 OutPop = 20; OutIter = 30; lb_price = 0.2 * ones(24, 1); ub_price = 1.5 * ones(24, 1); % 运行双层优化 [best_price, best_load, profit_history] = ... run_bilevel_woa(OutPop, OutIter, lb_price, ub_price, Data); % 绘图及输出 plot_result(best_price, best_load, Data, profit_history);注意变量命名要统一用列向量,这里我踩过不小的坑:Matlab里24×1和1×24在点乘时结果不一样甚至直接报维度错误。我建议项目一开始就规定所有曲线和电价向量一律使用列向量,目标函数内部再统一转换。
4.2 内层WOA的目标函数与约束处理
内层是整个模型的核心瓶颈,目标函数质量直接决定居民侧的响应是否合理。
内层解向量我按下述方式组织:假设6个设备、24时段,解向量长度是144。前72维对应离散设备的0/1状态,后72维对应连续功率设备。WOA本身只能搜索实数连续空间,所以对离散设备要做一次舍入映射,使用round(x),把实数映射到0和1;对连续设备则直接裁剪到功率上下限。
内层目标函数的框架代码:
function cost = obj_lower(x, price, Data) % x: 设备功率向量,长度 = ndev * 24 X = reshape(x, Data.ndev, 24); % 离散设备:round到0/1 X(Data.bin_idx, :) = round(X(Data.bin_idx, :)); % 功率上下限约束:直接裁剪 X = min(max(X, Data.Pmin), Data.Pmax); % 总负荷 total_load = sum(X, 1); % 电费 bill = price(:)' * total_load(:); % 舒适度损失,以空调为例 discomfort = Data.omega * sum((X(Data.ac_idx, :) - Data.Tref).^2, 'all'); cost = bill + discomfort; end这里面有几个细节值得展开。
第一个是约束处理方式。我采用的是“裁剪+罚函数”的组合。设备功率上下限可以直接裁剪;电池SOC这种状态量没办法裁剪,必须通过罚函数处理,惩罚系数不能太大也不能太小,太大的话目标函数梯度被罚款项主导,算法搜出来的设备计划虽然不越界但很不自然;太小的话电池会拼命越界。我调试下来,惩罚系数从出力1e3到1e5之间做一个粗略调节,一般就能找到稳当的区间。
第二个是舒适度权重。Data.omega的取值我建议先固定在0.1到0.5之间。太大会让居民无视电价,太小则忽略生活体验。可以先跑一版纯电费目标(omega=0),看看设备计划到底有多激进,再逐步增大omega,观察负荷曲线的变化趋势,这个过程能帮你理解自己模型的性质。
第三个是热启动问题。内层WOA每次求解后,都有一个最优解向量。这个最优解千万不要丢,下一轮外层用新电价调用内层时,把上一轮的最优解作为初始种群中的一个个体,通常放在第一个位置,其他个体随机生成。我实测下来,加了热启动之后外层收敛曲线的锯齿明显减少,效果立竿见影。
4.3 外层WOA如何组织迭代
外层WOA相对清爽,因为解向量就是24维电价向量。但要注意,外层WOA的适应度函数里必须调用内层WOA,这是整个双层嵌套的核心。
外层循环的框架:
function [best_price, best_load, profit_history] = ... run_bilevel_woa(OutPop, OutIter, lb_price, ub_price, Data) % 初始化外层种群 Position = rand(OutPop, 24) .* (ub_price' - lb_price') + lb_price'; % 缓存内层最优解用于热启动 last_inner_x = []; for t = 1:OutIter a = 2 - 2 * t / OutIter; for i = 1:OutPop price = Position(i, :)'; % 调用内层WOA:居民最优响应 [load_opt, inner_best_x, ~] = ... WOA_inner(price, last_inner_x, Data); last_inner_x = inner_best_x; % 热启动更新 % 计算上层收益 profit = obj_upper(price, load_opt, Data); fitness = -profit; % WOA默认求最小 % 更新全局最优... % 标准WOA位置更新公式... end profit_history(t) = -Best_fitness; end end这里的窍门是把上一层的最优解存入last_inner_x,传给下一层做热启动。但千万注意:当外层个体距离较远时,热启动的作用有限,这时内层还是要有足够的迭代次数自己探索。
还有一个非常实用的加速技巧:外层种群中往往会有很多接近的个体,它们的电价向量差不了太多。你可以增加一个简单判断——如果当前个体和上一轮已经评估过的最优个体电价差小于某个阈值,就直接复用缓存的内层响应结果,不再重复调用内层WOA。这个缓存机制在种群较大时能节省三分之一以上的计算时间。
4.4 收敛曲线和负荷曲线怎么看
跑完程序之后,不要只盯着最后一组数字,一定要画出收敛曲线和负荷曲线对比图。
收敛曲线看两个东西:一是外层利润是否随迭代上升并趋于平稳;二是曲线有没有剧烈锯齿。如果锯齿特别大,大概率是内层每次求解精度不够或者没有热启动。如果曲线整体还在上升但没平,说明迭代次数不够,可以适当增加外层迭代。
负荷曲线对比图是我最看重的调试工具。把优化后的总负荷曲线和原始基线负荷曲线叠在一起,一眼就能看出调度效果:晚高峰的负荷是不是削下来了,凌晨低谷是不是填上去了,峰谷差缩小的幅度是不是符合预期。
电价曲线也值得对比。你会发现上层搜出来的最优电价不会是简单的高峰高价,而是一种“带着锯齿的阶梯曲线”,某些时段电价明显抬高,某些时段压低,整体和用户用电禀赋形成某种默契。这就是博弈均衡在价格信号上的直观体现。我第一次看到这个图的时候,对博弈模型的理解比读十篇论文都深。
5. 实战踩坑与排查技巧实录
5.1 常见坑位速查表
这部分是我实际调试过程中整理出来的速查表,几乎每个都能对应到具体现象,排查时可以直接对照。
| 现象 | 大概率原因 | 解决方案 |
|---|---|---|
| 外层利润曲线严重振荡,像锯齿一样 | 内层每次从随机初始解开始,响应精度不稳定 | 热启动:把上一轮内层最优解带入下一轮 |
| 优化后负荷曲线基本没变化 | 舒适度权重omega过大,或电价边界太窄 | 调小omega到0.1~0.5,放宽电价上限 |
| 优化后电费降了但舒适度崩坏 | omega过小,居民尽量不用电 | 调大omega,看设备运行时段是否符合生活常识 |
| 电池SOC一直越界或充放电异常 | 罚函数系数太小,或SOC递推方向写反 | 检查充放电符号,加大惩罚系数并单独测试电池模块 |
| 内层WOA大量时间被占用 | 内层种群和迭代次数设置过高 | 内层迭代控制在30~50,种群20左右即可 |
| 报错“矩阵维度不一致” | 行向量和列向量混用 | 全局统一使用列向量,目标函数内部做reshape |
| 主从均衡结果不符合直觉 | 上层lambda(峰谷差惩罚)过大或过小 | 调节上层目标里标准差惩罚的权重做敏感性分析 |
5.2 从“跑不通”到“稳定收敛”的调试顺序
我最推荐的调试顺序是“先内后外、先固定后联动”。
第一步,固定电价,只调内层。写死一组峰平谷三段式电价,先单独跑内层WOA,观察居民侧的反应:峰时段的可调负荷是否减少,谷时段负荷是否上升,电池是否在谷时充电、峰时放电。如果这些直觉反应都对,内层才算是通了。这一步通不过,后面全是白搭。
第二步,固定最近一次居民响应曲线,只调外层。用内层输出的一组固定负荷曲线,单独跑外层WOA,观察售电公司利润随电价的变化趋势。你会发现当电价值过高时,利润下降;当电价过低时,利润也下降,中间存在一个峰——这就是内层响应在起作用。
第三步,两层联调。联调前先把内层最优解初始化,并且打开热启动开关。第一次联调时可以把双方迭代次数都设得小一点(比如外层20、内层30),确认链路没报错、变量传递正确,再逐步加大迭代次数。
第四步,看收敛结果是否稳定。同一组参数下跑三到五次,如果每次都收敛到差异很大的价格和负荷曲线,说明模型稳定性差,优先怀疑内层搜索不充分或者罚函数系数不合适,而不是急着加大种群。盲目加迭代确实能改善一点,但不解决本质问题。
5.3 代码复现的几个版本与参数敏感性建议
最后给一点代码复现层面的建议。
第一,先用单户模型验证。所谓单户,就是只有一个居民,但他家里有洗衣机、电动汽车、空调等多种设备。单户模型已经能体现完整的主从博弈结构,而且跑得快,适合反复调参。
第二,多户扩展不要合并优化。如果你想做多个居民之间的非合作博弈,千万别把所有居民的决策变量合并到一个大优化问题里,那等于假设用户之间在合作。正确做法是内层循环里写成:
for n = 1:N [load_opt_n] = WOA_inner(price, Data(n)); end total_load = sum(load_opt_all, 2);每户独立响应同一电价,再把负荷逐时相加,继续向上层返回。这样用户之间就存在竞争关系了,因为大家都在抢一段便宜电价时段的线路容量。
第三,参数敏感性一定要做。WOA的种群和迭代、舒适度权重omega、上层峰谷差惩罚lambda,每一个参数单独做一组对照试验。比如omega分别取0.1、0.3、0.5,看看均衡条件下的负荷曲线和居民电费会有什么规律。这一段实验数据写论文或者做项目汇报时特别有说服力,也最能帮助你理解模型对参数的依赖程度。
Mahua最后一个建议:Matlab版本用R2020b及以上的常规版本就够,不需要额外安装专业工具箱,基础平台加优化工具箱就能跑完整个工程。注意把随机种子固定下来,比如用rng(2024),否则每次运行结果不同,你还以为自己代码出了问题,其实只是初始化不一样。
这个项目做完之后,我个人最大的体会是:算法本身不难,难的是把博弈关系拆清楚再落成代码。双层WOA本质上只是把主从博弈的上下层决策问题各用一个启发式搜索来逼近,它并不神秘,真正决定模型质量的,还是你对负荷特性、约束条件和目标函数的设计是否合理。先稳住单户、固定电价的内层逻辑,再加多户、动态迭代,这一步一个脚印的调试路径,是走下来最顺的一条路。