做电力系统优化的人应该都遇到过这种尴尬局面:模型在论文里看起来很完美,但一到实际数据就各种不收敛,要么峰谷差没削下来,要么用户电费反而涨了。这个“基于非合作博弈的居民负荷分层调度模型”其实就是在解决需求响应里最棘手的两个问题——用户之间的利益冲突,以及多层决策主体之间“你定电价、我调负荷”的互相制约关系。标题里的“双层鲸鱼算法”不是某个官方名词,而是把两个鲸鱼优化算法(WOA)嵌套起来用:外层优化电价策略,内层求解用户最优响应,最终逼近主从博弈的均衡解。
这块内容适合谁参考?我觉得主要是三类人:一是做电力系统优化调度方向的研究生,想找一个能落地复现的博弈调度框架;二是电网公司或负荷聚合商里做需求响应策略的工程师,需要一套不依赖商业求解器的求解方法;三是刚接触WOA算法、想在具体场景里练手的同学。下面我会把模型为什么这么设计、每一层数学上在做什么、Matlab代码怎么拆,以及我实际调试中踩过的坑完整过一遍。
1. 整体思路:为什么居民负荷调度要分两层
1.1 单层削峰为什么不够用
先说一个很多初做需求响应的人容易掉进去的误区:把居民负荷当成一个大水池,直接让优化算法统一调配所有用户的空调、热水器和电动汽车。这么做在数学上很简单,目标函数写成系统削峰效果最优,约束加上设备功率上下限,一套线性规划或者粒子群就解决了。但落到实际运行中,这种“集中式指挥”有两个硬伤。
第一个硬伤是用户意愿被无视了。你让某个用户把空调在下午两点关掉一小时,他觉得热;你让他把电动汽车充电挪到凌晨三点,他第二天早高峰要用车。集中优化只看全局指标,忽略了每个用户自己的用电效用,结果就是计算出来的调度方案根本没人愿意执行。
第二个硬伤是反弹效应。如果所有用户都收到同一个“晚8点后电价便宜”的信号,大家都把负荷挪到同一段低价时段,原本的高峰确实削掉了,但新的负荷高峰又被制造出来,这就是典型的“峰谷倒置”。这个问题在单层模型里无解,因为你没有把用户之间的竞争关系建模进去。
我自己的体会是,居民负荷调度必须放弃“一个指挥官指挥所有人”的思路,改成“运营商定价、用户自己决策”的分层机制。用户保留用电自主权,但通过电价信号被引导;运营商不直接下指令,只调整杠杆。这样既尊重了用户差异,又从结构上避免了所有用户同时涌向同一时段的问题。
1.2 非合作博弈在调度模型里解决什么矛盾
非合作博弈这个词听着吓人,其实本质一句话就能讲透:参与博弈的各方各自追求自身利益最大化,不达成任何具有约束力的合作协议。放到居民负荷调度场景里,就是运营商和居民用户之间、居民与居民之间,都在为自己的利益做决策。
居民与运营商之间是典型的Stackelberg主从博弈。运营商先发布电价信号,用户观察到电价之后再决定自己怎么用电器,用户后续的用电行为又反过来影响运营商的收益和系统负荷曲线。这种“你先走、我跟着走、我的反应又影响你的收益”的结构,是天然的双层优化问题。
居民与居民之间则是纳什博弈。每个用户的用电决策会影响总负荷曲线,进而影响系统对下一轮电价的调整,所以用户之间存在间接竞争关系。比如大家都在抢低谷时段的便宜电,充电桩和热水器一起往凌晨堆,边际效用就会下降。非合作博弈模型会把这种竞争关系显式表达出来,最终收敛到一个纳什均衡点——在这个点上,任何一个用户单方面改变自己的用电策略,都不可能让自己更舒服,于是大家都不动了。
我常用一个非常生活化的类比帮朋友理解这套逻辑:几个学生共享一个充电桩,每个人都想在自己最方便的时间充电,但如果大家都选同一个时间,要么排队、要么被配电容量限制。每个学生只能根据“什么时间充电最划算”来做自己的选择,没人能强迫别人换时间。最后形成的那个充电分布,就是博弈均衡。
1.3 为什么偏偏是双层鲸鱼算法
双层优化模型最正统的数学求解方法是把下层问题用KKT条件替换,然后把整个模型转化成带互补松弛约束的单层优化问题,再用商业求解器求解。这个方法在理论上是漂亮的,但我实际使用中有三个非常头疼的阻碍。
第一,下层用户模型往往不是标准凸问题。用户的效用函数、舒适度惩罚项、各种设备状态变量混在一起,目标函数可能有多个局部极值,KKT条件转换后求解器经常找不到全局解,甚至直接报错。第二,即使能转化,问题规模也不小,电网侧动辄上百个用户、24个时段,加上各种约束,求解时间长得让人怀疑人生。第三,很多需求响应场景需要程序嵌入到实际调度系统里,商业求解器的授权和部署也不是都能解决的。
所以我把目光转向了启发式算法。鲸鱼优化算法(WOA)是Mirjalili在2016年提出的,模拟座头鲸的泡泡网捕食行为,结构上有三个角色:包围猎物、气泡网螺旋攻击、随机搜索。它的好处很贴合这个场景:参数少(主要是收敛因子a和螺旋常数b)、不要求目标函数可导、天然适合处理非凸非线性问题,而且代码逻辑清晰,嵌入Matlab之后调试非常方便。
“双层鲸鱼算法”的设计是我把两个WOA嵌套起来用。外层WOA的每个个体代表一组电价策略,内层WOA在给定电价下求解用户的最优负荷响应,外层把内层返回的用户响应代入运营商目标函数,作为该个体的适应度。这样做的好处是上下层都用同一套启发式框架,代码复用率高,而且不依赖任何外部求解器,纯Matlab就能跑通。
2. 模型这样建:上层定价、下层响应、均衡判定
2.1 上层运营商模型
上层模型的决策者是电力零售商或负荷聚合商,核心决策变量是分时电价向量,我习惯用24维的向量表示,对应一天24个时段的价格。每个时段电价约束在政府规定的上下限之间,比如上限1.2元/千瓦时、下限0.3元/千瓦时,这个范围可以根据实际场景调整。
运营商的优化目标不是单一的。我常用的目标函数是把“收益最大化”和“削峰填谷指标”做加权组合。收益部分等于售电收入减去批发购电成本,再减去对用户参与响应的激励支出;削峰填谷部分则用调度后的峰谷差相对于原始峰谷差的削减比例来衡量。两个目标用权重系数平衡,权重怎么选取决于你是更看重经济性还是更看重系统运行安全。
这个目标函数有一个很容易被忽略的细节:它依赖于下层的用户响应。也就是说,运营商在做决策时不能凭空想象“我把晚8点的电价定到0.5元,用户就会挪走多少负荷”,必须在优化过程中先调用内层模型,算出在当前电价下用户会怎么反应,再回来评估这组电价好不好。这正是上层模型必须嵌套下层模型的根本原因。
上层约束除了电价上下限之外,还有几个实操中很常见的约束。比如运营商利润率不能低于某个下限,否则零售商没有动力参与;再比如电价调整幅度不能太大,避免对用户造成电价冲击,我在代码里用的是相邻两天同一时段的电价变化不超过20%的约束形式。
2.2 下层居民负荷模型
下层模型的决策者是单个居民用户。用户手里有什么牌呢?典型可调负荷有三种:空调(可以提前预冷、短时提高设定温度)、热水器(功率可调、储能水箱有热惯性)、电动汽车(充电时段和充电功率可平移)。每种负荷都有功率上下限、可调度时段范围,以及每天总用电量的约束。
用户的优化目标是“用电效用最大化减去用电成本”。效用函数我推荐用二次型:U(x) = a·x - b·x²。这个形式的好处是边际效用递减,前一度电带来巨大的舒适度提升,后面每增加一度电的爽感越来越弱,最后趋于饱和。这在经济学上是符合直觉的,而且二次函数在数学上性质好,不容易给内层算法制造额外的非线性困难。
用电成本就是用户在各时段用电量乘以对应电价的总和,再减去参与调度的激励补偿。如果电价在某个时段特别低,用户把负荷挪过去,电价成本下降;但如果把空调关了导致温度超过舒适区间,对应的罚函数项会上升。用户就在“省钱”和“舒适”之间做权衡,这就是博弈论里说的策略空间:用户选择什么时段用什么负荷。
从行业论文和实测经验看,不同用户的效用函数参数差异很大,所以下层模型通常不是单个用户,而是一组用户各自优化。实操中如果用户数量特别多(比如上千户),我会先用K-means把用户聚成三类或五类典型用户,每类用户用一组代表性的负荷数据和效用参数参与博弈,既降低计算量,又保留了用户异质性。
2.3 均衡怎么判定
博弈模型建立了之后,最核心的问题就是怎么判断“算完了”。在Stackelberg博弈里,均衡状态是:给定运营商的电价,用户已经做出了自己最优的负荷响应;同时在这个用户响应下,运营商也找不到更好的电价策略来提升自己的目标函数。两个层面都“动不了”了,才算达到均衡。
理论层面,纳什均衡的存在性依赖于用户效用函数是拟凹的、策略空间是非空紧集。这些条件在我用的二次效用函数和线性约束下通常能保证,我不在代码里显式证明,而是用数值方法验证。实操层面,我用的判定方式是双层的:外层看电价向量的变化,内层看负荷响应向量的变化,连续若干轮迭代的变化量都小于一个阈值,比如相对误差0.001,就认为均衡达成了。
在实际编写代码时,还有一个细节特别容易翻车:电价初始值给得太离谱,会导致下层用户在电价极高时段完全不投入负荷,拉回目标函数之后运营商又不敢把电价降下来,算法陷入局部解。我的处理办法是多次初始化。随机生成三组不同的初始电价和初始负荷,各自跑完全程,对比最终均衡点的目标函数值是否一致。如果有差异,就要检查是算法早熟还是模型本身存在多重均衡。
3. Matlab代码实现:三层循环结构与关键细节
3.1 程序整体框架与文件划分
拿到一个博弈调度模型,先别急着写循环。我习惯先把Matlab工程拆成六个脚本,避免所有逻辑堆在一个main文件里改起来想死。
- main.m:参数设置、数据加载、调用双层WOA、结果绘图。
- outer_woa.m:外层WOA主循环,负责更新电价。
- inner_woa.m:内层WOA主循环,负责给定电价下的用户负荷优化。
- obj_operator.m:运营商目标函数,输入电价和用户响应,输出适应度。
- obj_user.m:单用户目标函数,输入电价和该用户的负荷策略,输出该用户的收益值。
- update_whale.m:WOA位置更新子函数,包含包围、螺旋、随机搜索三个阶段。
先定义核心结构体。电价策略我存成一个矩阵,行是鲸鱼个体,列是24个时段的电价;用户负荷响应存成三维数组,第一维是用户编号,第二维是时段,第三维是鲸鱼个体编号。这样在Matlab里做向量化运算比较方便。
外层优化循环的伪代码如下:
% main.m 中调用双层WOA的核心逻辑 for t = 1:maxIterOuter for i = 1:popSize price = whale_position(i, :); % 当前电价策略 % 内层WOA:给定price,求用户最优响应 [load_resp, user_best] = inner_woa(price, userData, param); % 计算运营商适应度 fitness(i) = obj_operator(price, load_resp, userData, param); end % 按WOA机制更新新一代电价策略 whale_position = update_whale(whale_position, fitness, ...); end这段结构是双层优化的骨架。外层不管生成什么电价,都先塞给内层去求用户响应,再把响应结果代回上层算适应度。整个过程的计算量主要在调用内层这一块,所以内层参数要控制好,不要贪多。
3.2 外层鲸鱼算法在做什么
外层WOA优化的对象是24维电价向量,所以每只鲸鱼的位置本质上就是一组可能的分时电价。初始化时我用上下限之间的均匀分布随机生成,确保电价在可行域内。
外层每次位置更新分三条路。第一条是包围猎物:当前记录的全局最优电价是猎物,其他鲸鱼向它靠拢;第二条是气泡网攻击,用螺旋公式更新位置,核心是让鲸鱼在向猎物靠近的同时保持螺旋形态的搜索,保证局部开发能力;第三条是随机搜索,随机选一个个体作为目标,保证全局探索,避免所有鲸鱼全部挤到同一个局部最优电价上。
我习惯把外层迭代次数设为80到100代。太少电价策略还没来得及充分探索,太多则总计算时间成倍上涨,性价比很低。种群规模20到30个通常就够用了,因为电价维度虽然高,但相邻时段电价天然有相关性,搜索空间的实际有效维度没有24这么大。
外层WOA还有一个可以大幅提速的小技巧:每一轮迭代后,把历史最优电价对应的内层用户响应保存下来,作为下一轮内层WOA的初始解。这种热启动方式能让内层算法少走弯路,我实测能够节省大概20%到30%的总计算时间,非常划算。
3.3 内层鲸鱼算法的处理要点
内层WOA的用户数量如果很多,逐用户循环会很慢。我的做法是先把用户聚类到几个典型用户,再对每个典型用户独立调用内层WOA。因为用户在电价确定后彼此之间的优化是独立的,这个阶段天然适合并行。在Matlab里用parfor把不同用户的优化任务分到多个worker上,CPU多核利用率能明显提升。
内层决策变量的维度是24维负荷向量,对应24个时段该用户的用电量。每个时段的负荷功率要卡在0和最大可调功率之间,总用电量要满足日总需求约束。这些约束我在目标函数里用罚函数处理。罚函数系数设置很关键,太小约束形同虚设,太大则会让目标函数曲面过于陡峭,算法很难收敛。经验值是让罚项量级和原始目标函数量级相当,初算时可以先跑一次不带约束的松弛优化,观察目标量级再定。
内层迭代次数不需要太多,30到40代足够。这个判断基于一个事实:给定电价后,用户优化问题比上层电价优化问题要简单得多,收敛速度快。我一开始把内层也设成80代,结果总计算时间直接爆炸,而且用户响应曲线在30代以后基本就不动了,白白浪费算力。
内层WOA的输出除了最优负荷向量之外,我还会额外返回一个标志字段,记录这个用户的优化过程是否正常收敛。外层拿到的如果是一堆不收敛的结果,那适应度评估就没有意义。这个标志在后续排查博弈均衡问题时非常有用。
3.4 参数设置与热启动建议
我把自己常用的参数列在下面,这些值针对24时段、5类典型用户、一天调度周期的场景,供参考:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 外层种群规模 | 30 | 电价策略搜索空间较大,不宜太小 |
| 外层迭代次数 | 80 | 覆盖探索和开发两个阶段 |
| 内层种群规模 | 20 | 单用户维度相对低,20只鲸鱼够用 |
| 内层迭代次数 | 35 | 再增加对结果改善很小 |
| 电价下限 | 0.3元/kWh | 依据场景或政策调整 |
| 电价上限 | 1.2元/kWh | 避免电价冲击 |
| 负荷转移比例上限 | 15% | 防止反弹效应,也体现用户舒适约束 |
| 罚函数系数 | 1e3 | 需根据目标量级微调 |
还有两个小细节我必须提醒。第一,电价向量初始值不要全部一样,否则WOA在初始阶段的多样性不足,容易出现早熟收敛。第二,每一轮外层评估结束后,把本轮最优电价对应的用户负荷响应存到工作区变量里,方便后面画负荷曲线和电价曲线的对应关系。调试时如果发现曲线对不上,多半是数据索引串了,这个问题排查起来很费时间,所以从一开始就把数据结构定义清楚。
4. 常踩的坑与排查实录
4.1 典型问题速查表
我把自己在调试这套双层博弈调度模型时遇到过的问题整理成了一张表,很多问题都不是个例,如果你也跑类似代码,大概率会在某个环节撞上。
| 现象 | 直接原因 | 解决方案与排查方法 |
|---|---|---|
| 内层负荷振荡不收敛 | 目标函数过于平坦,WOA无法准确判断方向 | 增大内层种群或迭代次数;对负荷变量做移动平均平滑;检查罚函数系数是否过小 |
| 内层结果总落在边界 | 约束太紧,可行域退化成一条线 | 检查负荷转移比例上限、总用电量等约束是否合理;适当放宽设备功率上下限 |
| 电价和负荷曲线来回震荡 | 博弈迭代过程缺少平滑机制 | 引入价格平滑因子,新电价 = 旧电价 + 学习率×(推荐电价 - 旧电价),限制单步变化幅度 |
| 总耗时难以接受 | 外层调用内层次数太多 | 用parfor并行优化不同用户;内层迭代数降到35;增加热启动机制 |
| 目标函数出现NaN | 数值稳定性问题 | 检查是否有log(0)、0除以0等操作;在计算适应度前加isfinite判断,异常值置为一个很大的惩罚数 |
| 多次运行结果差距大 | 算法收敛到不同局部解 | 增加种群规模和迭代次数;用多种随机初始化策略对比最终目标值,判断是否有多重均衡 |
这里面最隐蔽的是“内层总落在边界”的问题。有一段时间我设置了比较严的“用户日总用电量不变”约束,导致用户必须在24个时段的总负荷和保持不变。但可调负荷比例本来就小,内层WOA每次尝试调整都要严格控制总和,探索空间被严重压缩。后来我把“总量不变”改成“总量在±10%内浮动”,同时用罚函数控制偏离程度,内层的搜索空间一下子打开了,收敛速度和结果质量都明显提升。
4.2 我调试这套代码的顺序
先说结论:不要一上来就跑完整24时段、5类用户、80代外层的全规模模型。我踩过这个坑,跑一次要将近十分钟,调试过程中只改一个参数又要重跑,效率太低。
我的做法是从最小可运行案例开始。先设3个用户、4个时段(峰平谷平)、外层10代、内层10代,只求逻辑链路通。这个规模下就算卡死或者报错,定位问题也很快。确认能跑通之后,逐步增加时段数、用户数、迭代次数,每增加一个维度就跑一遍并观察结果是否合理。
还有一个我特别推荐的检查手段:先固定电价不变,只跑一次内层WOA,看看用户负荷响应是否符合直觉。比如电价高峰时段负荷明显下降、低谷时段负荷上升,那说明内层逻辑没问题。接着固定用户响应不变,只跑外层WOA,看电价是否趋于削峰填谷的方向。两个环节单独验证都通过之后,再合起来跑双层嵌套。这种“分而治之”的调试方式可以帮你把问题定位到具体的某一层,而不是在双层耦合的混沌状态里瞎猜。
另外,保存中间结果这个习惯真的很重要。每轮外层迭代的电价、用户响应、目标函数值都存到mat文件里。这样即使某次跑出来的结果不理想,也不用全部重跑,可以复盘到底是哪一轮开始失稳的。我第一次调试时没存中间结果,出了问题只能靠记忆猜,后来改成每10轮保存一次快照,排查效率高了一个量级。
4.3 结果分析看什么
模型跑通之后,很多人只盯着最终收敛的那条负荷曲线看,我觉得这不够。至少要同时看四样东西:调度前后的系统总负荷曲线、电价随时间变化的曲线、每个典型用户的负荷转移率、运营商收益和用户电费的对比。
调度前后的负荷曲线是验证模型有没有意义的直接证据。我见过一些模型跑完,总负荷曲线削峰是削了,但削峰后的曲线出现了新的小高峰,或者在谷段又堆起来一个平台,这些都是反弹效应的体现,说明用户之间的竞争关系没有被充分约束,需要检查下层模型的舒适度惩罚项权重是不是太低了。
电价曲线要注意看时段间的价格差是否足够引导用户。如果博弈收敛后电价在24个时段里都差不多,用户根本没有改变用电行为的动机,调度效果肯定差。我通常会在目标函数加一个“电价峰谷比不低于1.8”的约束,保证价格信号有足够的区分度。
运营商收益和用户电费的对比也很重要。很多模型只优化运营商收益,结果用户电费反而涨了,这种方案显然不具备可落地性。我最终采用的判据是帕累托改进:调度后运营商收益提高的同时,至少大部分用户电费不高于调度前,否则这个“均衡”在现实中没人买单。这也是为什么我把用户电费变化放进结果分析里,而不是只看系统层面的削峰指标。
最后再分享一个小经验:双层博弈模型的调试周期普遍比单层优化模型长,最花时间的往往不是写算法本身,而是让整个迭代过程稳定收敛。我的建议是老老实实按“最小案例→逐步扩规模→分离验证上下层→全模型联调”的路径走,别跳步骤。如果你正准备复现类似模型,可以从3个用户、4个时段、20次迭代起步,先把主从博弈的迭代逻辑跑通,再逐步逼近真实规模。