非合作博弈分层调度模型与双层鲸鱼算法在居民需求响应中的应用
这些年做负荷调度和需求响应相关的仿真项目我见过不少让人头疼的场景居民侧负荷随意用、峰谷差越拉越大、电网侧被迫频繁调节机组出力而简单的固定峰谷电价又很难真正改变用户行为。后来在某个模拟项目里试了基于非合作博弈的分层调度思路配合双层鲸鱼算法在Matlab里求解效果比我想象中扎实。这里把整套模型的拆解思路、算法实现细节和调试经验整理出来给正在做类似方向的朋友一份可以直接参考的实践记录。这个方向适合谁看如果你是做电力系统优化调度、能源互联网策略研究或者在写居民需求响应方向论文的开发者这篇内容应该能帮你省下不少绕路时间。文章会按“为什么用博弈论—分层模型怎么搭—双层鲸鱼算法怎么落地—仿真中出现过的坑”这条线索展开代码逻辑部分我尽量讲得能直接照搬。1. 居民负荷调度的现实困境从何下手比怎么优化更难1.1 负荷不是铁板一块先要把类型分明白居民家里的用电设备五花八门但从调度角度看它们响应能力完全不一样。我在建模时习惯把所有负荷分成三类这样后面定决策变量和约束条件才有抓手。可转移负荷洗衣机、洗碗机、电动汽车充电桩这类设备工作时段可以平移但启动后一般不能中断。它们的特点是“总用电量固定、用电时段灵活”这是调度优化的主力对象。可削减负荷空调、热水器、部分供暖设备运行功率可以在一定范围内连续调节短时间内降低功率不会造成灾难性影响但削减太多会影响舒适度。这类负荷适合做功率调节不适合做时段平移。刚性负荷照明、电视、冰箱这类设备用电时段和功率基本不可控在模型里一般作为固定基线处理。三类负荷对电价信号的敏感度差异非常大。可转移负荷的响应速度最快——只要峰谷电价差足够大用户愿意把充电时间挪到夜间可削减负荷需要配合温度舒适度区间来约束刚性负荷基本不参与调度。如果把三类负荷混在一起建一个黑箱模型上层优化器根本不知道应该向谁发力结果往往会失真。1.2 供电商和用户的目标天然冲突在传统“电网定电价、用户被动接受”的模式下双方目标其实是对着干的。供电商希望削峰填谷、降低系统峰谷差、减少购电成本最好用户都在谷段用电但用户在乎的是电费便宜、生活舒适不会因为你电网需要削峰就牺牲自己的用电习惯。我见过不少只优化供给侧的策略结果并不理想——电价定得太激进用户直接无视响应率低得可怜电价定得太柔和峰谷差又压不下去。问题就出在把用户当成“被动执行者”而没有考虑用户会在给定电价下做出对自己最有利的选择。非合作博弈在这个场景里的价值恰好就是正视这种冲突上层的供电商制定电价策略下层的居民用户根据电价优化自己的用电计划双方都在追求自身利益最大化最终通过迭代收敛到一个互相接受的均衡状态。这不是一个“压制用户”的过程而是把双方博弈关系显式建模让电价信号本身去引导行为。1.3 非合作博弈的基础认知每个人都为自己选最优第一次接触非合作博弈的开发者可能会被“博弈论”三个字吓住但实际在负荷调度里用到的核心概念并不复杂。先说纳什均衡。简单理解就是给定其他参与者的策略任何单个参与者都找不到更好的单方面改进行为。把这个映射到居民负荷调度里——给定供电商公布的一组分时电价每个用户都会找到自己的最优用电计划所有用户都按最优计划执行后供电商再基于这些用电行为评估电价策略是否合适如果某一方觉得自己吃亏且有能力调整策略就继续迭代。最终达到的状态就是没有任何一个参与者愿意在别人不动的情况下独自改变策略。这里容易被误解的点是“非合作”。非合作不等于对抗和扯皮而是说每个决策主体只考虑自己的目标和约束不指望通过行政命令或强制手段让其他主体配合。在居民负荷场景里供电商和用户之间、用户和用户之间都天然是非合作的——“别人家怎么用电是你控制不了的你能做的只是在给定信息下选好自己的策略”。这种假设比合作博弈更贴近现实。2. 分层调度模型怎么搭上层定电价下层调负荷2.1 上层模型供电商的定价策略上层决策者设定分时电价策略目标是兼顾自己的运营收益和系统运行效益。实际建模时我习惯用“调度周期内总负荷峰谷差最小化 供电收益不低于基准”作为双目标再做加权处理。上层的决策变量是24个时段的电价向量或者简化成峰、平、谷三档电价。如果走三档电价路线决策变量会少很多但精度有限。我做过对比直接以24时段电价为变量虽然求解压力更大但收敛后电价曲线更平滑削峰效果也更明显。约束条件一般包括电价上下限避免电价过高或过低引发公平性问题平均电价保持在一定水平防止运营商通过整体抬高电价获利相邻时段电价变化幅度不能过大防止价格突变给用户带来不可预期的账单冲击这里要特别提一下“用户响应度”的问题。如果上层模型假设用户100%严格按照电价优化用电计划就偏离现实了。比较稳的办法是引入一个响应系数表示实际参与负荷调整的比例。比如某时段电价激励很强时理论上可转移负荷应该全部移到谷段但实际上只有80%的用户会响应。这个系数可以让模型更贴近真实场景后面调参时也方便做灵敏度分析。2.2 下层模型居民用户的经济用电计划下层每个用户或每类典型用户在接收到电价信号后构建自己的用电成本最小化问题。决策变量包括可转移负荷的启停时间段、可削减负荷在各时段的功率。目标函数是电费支出最小同时要兼顾舒适度惩罚项——如果为了让电费便宜而把空调全关掉模型得给出一个惩罚项来阻止这种不合理的极端行为。我在实际模型中用到的用户目标函数形式大致是电费支出各时段电价乘以对应时段的用电功率之和舒适度惩罚可削减负荷实际功率与期望功率的偏差超过一定阈值后产生惩罚成本可转移负荷的启停约束连续运行特性不能被破坏这个模型看起来思路清晰但落地时有个细节很关键所有用户的决策彼此独立互不通信。每个用户只知道电价和自己家的情况不知道自己邻居怎么用电。这正是非合作博弈的体现——没有中央协调单元指导每个用户必须怎么做而是靠价格信号实现间接协调。2.3 上下层的耦合电价到负荷负荷又回到电价上下层不是孤立的而是通过一条循环链路耦合在一起供电商发布一组初始电价每个用户针对这个电价独立求解自己的负荷计划所有用户的计划汇总起来形成系统总负荷曲线供电商观察总负荷曲线评估削峰填谷效果和收益调整电价进入下一轮迭代直到电价和负荷不再发生显著变化这个过程中上层电价影响下层负荷下层负荷反过来又影响上层电价的调整方向。如果不显式分层把运营商和所有用户的决策变量放进一个大优化问题里求解那么每个用户的具体隐私信息都必须暴露给上层。在真实场景里没有几个用户愿意做这种事。分层模型的最大工程价值就是通过“迭代协调”替代“集中统一求解”保护了用户侧的决策隐私同时把问题规模控制在可求解范围内。2.4 为什么不行做一个整体大优化算力和隐私是两座山这是我被问过很多次的问题“既然目标函数都有约束条件也不复杂为什么不能把所有用户的决策变量放到一个大优化器里直接求全局最优”技术上确实可以但有两个现实问题。第一个是计算复杂度。假设有50个用户每个用户有20个可转移负荷的时段决策再加上功率调节决策联合决策变量就有上千维。上层再加24个电价变量这个混合整数非线性问题用常规求解器跑一次时间成本很高。如果把用户规模扩大到几百户问题规模会直接失控。第二个是信息和隐私壁垒。集中式求解要求上层拿到所有用户的用电偏好、负荷参数和内部约束这在现实里几乎无法实现。居民不会愿意为了“系统最优”而交出这些信息。分层模型通过迭代的方式让每一步只暴露“在当前电价下你打算怎么用电”而不是把所有偏好参数全交出去工程上更容易被接受。3. 双层鲸鱼算法的核心原理与Matlab代码实现3.1 鲸鱼优化算法到底在模仿什么鲸鱼优化算法Whale Optimization Algorithm, WOA是模拟座头鲸捕猎行为的一种群智能优化算法。座头鲸捕食时有一种非常特别的行为——气泡网攻击先潜入水下围绕猎物喷出螺旋状的气泡网把猎物困住然后从下方张大口向上冲一次性吞掉聚集的猎物。算法把这种行为抽象成几个核心的数学更新机制包围猎物种群中的鲸鱼个体根据当前最优位置来更新自己的位置对应算法里的“包围”阶段。螺旋气泡网攻击以螺旋轨迹逼近最优位置对应算法的局部搜索阶段保证收敛精度。随机搜索当参数满足一定条件时个体放弃跟随当前最优位置转而随机在搜索空间探索对应全局搜索阶段防止陷入局部最优。这几条机制的数学公式在很多基础资料里都有篇幅原因我不逐条推导直接给出Matlab核心更新逻辑的写法。function [newPos, fitness] whaleUpdate(pos, bestPos, lb, ub, iter, maxIter, b, fitnessFunc) % pos : 当前个体位置向量 % bestPos : 当前全局最优位置向量 % lb, ub : 变量下界和上界向量 % b : 螺旋形状常数通常取1 % fitnessFunc : 适应度函数句柄 dim length(pos); newPos zeros(1, dim); % 收敛因子a随迭代线性递减 a 2 - iter * (2 / maxIter); % 随机系数 r1 rand(); r2 rand(); A 2 * a * r1 - a; C 2 * r2; p rand(); if p 0.5 % 收缩包围机制 if abs(A) 1 D abs(C * bestPos - pos); newPos bestPos - A * D; else % 随机搜索 randIdx randi([1, size(pos, 1)]); randPos lb (ub - lb) .* rand(1, dim); D abs(C * randPos - pos); newPos randPos - A * D; end else % 螺旋更新 l -1 2 * rand(); D abs(bestPos - pos); newPos D * exp(b * l) .* cos(2 * pi * l) bestPos; end % 边界约束 newPos max(newPos, lb); newPos min(newPos, ub); fitness fitnessFunc(newPos); end这段代码是鲸鱼算法最常用的基础更新框架。注意最关键的参数A它的绝对值反映个体当前是“开发”还是“探索”。a从2线性下降到0意味着前期搜索空间大、后期收窄精度高这是算法能在“全局搜索”和“局部开发”之间取得平衡的核心。3.2 为什么选鲸鱼算法而不是粒子群或遗传算法做负荷调度很多人第一反应是粒子群算法PSO或者遗传算法GA我在早期项目里也主要用这两种。后来切换到鲸鱼算法主要看中三点。第一参数少调参成本低。粒子群要调惯性权重、个体学习因子、社会学习因子遗传算法要调交叉概率、变异概率、种群规模、选择策略——每个参数都直接影响收敛质量调起来非常费时间。鲸鱼算法核心参数只有种群规模、迭代次数和螺旋常数bb通常固定取1真正需要人工干预的实际只有前两个。第二探索与开发的平衡做得好。鲸鱼算法用p这个随机值在螺旋开发和包围收缩之间切换再用A的绝对值决定是包围还是随机搜索。这种多层随机机制使得算法前期不容易过早收敛后期又具备较强的局部精细搜索能力。我在多个测试函数上做过对比鲸鱼算法的收敛曲线通常比PSO更平滑震荡更少。第三双层嵌套时结构非常自然。后面马上要讲的双层模型外层要反复调用内层优化结果来评估适应度这就意味着外层算法每评估一次适应度都要完整跑一遍内层优化。如果内外层都选择参数少、收敛快的算法总计算时间才可控。鲸鱼算法在这方面比遗传算法有明显优势——遗传算法的交叉变异操作在嵌套调用时流程太重了。3.3 双层嵌套怎么组织和实现内外层各干什么这个模型的双层结构需要理解成“外层套内层”的关系外层鲸鱼算法负责优化24时段电价。每个鲸鱼个体就是一组电价向量它的适应度评估方式是——把这一组电价传给内层等内层用户算完自己的最优负荷计划返回各时段总负荷然后外层根据总负荷计算目标函数峰谷差、供电商收益加上电价约束的罚函数得到最终适应度值。内层鲸鱼算法负责优化单个用户在给定电价下的用电计划。把用户的负荷决策变量编码成一个位置向量目标函数就是电费支出加舒适度惩罚。每个内层个体跑完自己的迭代后返回最优用电计划给外层作为“用户对电价的响应”。内外层为什么必须用循环而不是一次性求解因为这里存在一个“逆向递归”的关系——外层的适应度函数内部就要调用一次完整的内层优化如果不把内层计算封装成函数外层每次迭代都要手动写一遍全流程代码会乱到没法维护。在Matlab里实现时我建议用几个独立的函数文件来组织outer_woa.m—— 外层主程序负责电价种群初始化和外层迭代inner_woa.m—— 内层主程序负责用户在给定电价下的负荷计划求解get_load_response.m—— 将电价传给内层并返回总负荷曲线的接口函数objective_outer.m—— 外层适应度计算objective_inner.m—— 内层适应度计算这个分层结构有个明显的工程优势如果你想换一种内层求解算法比如把内层鲸鱼算法换成二次规划求解器只需要修改inner_woa.m的return格式保持统一其他函数完全不用动。我在后续实验中就试过内层直接用商业求解器整体框架没有做任何重构。3.4 适应度函数与约束处理罚函数比硬限制靠谱约束处理是建模最容易翻车的地方。有的开发者倾向于在位置更新后直接强制截断变量范围比如电价不在允许范围内就直接拉回边界值。这在简单约束下问题不大但在这个双层模型里容易出问题。我推荐在内外层适应度函数中都采用罚函数机制。比如电价约束可以这样处理如果某个时段的电价超出允许范围不直接截断而是把超出的量乘上一个罚系数加到目标函数里让算法在搜索过程中“意识到”这个解不可接受并且能根据超出的程度平滑地调整搜索方向。这种做法的好处在于鲸鱼算法的个体更新依赖适应度值的相对大小。如果你把所有违反约束的个体适应度一律设成同一个很大的常数算法就丢失了“哪些个体违反得更轻”的信息搜索效率会大幅下降。而罚函数保留了这种梯度信息算法能知道往哪个方向调整能更快回到可行域。function fitness objective_outer(price, lambda1, lambda2, priceBase, loadResult) % price : 当前电价向量 % lambda1 : 峰谷差目标权重 % lambda2 : 收益惩罚权重 % priceBase : 基准电价用于收益约束 % loadResult : 传入内层计算得到的各时段总负荷向量 peakValleyDiff max(loadResult) - min(loadResult); revenue sum(price .* loadResult); revenuePenalty max(0, priceBase - revenue) * 1000; priceBoundPenalty sum(max(0, price - priceMax).^2) sum(max(0, priceMin - price).^2); fitness lambda1 * peakValleyDiff lambda2 * revenuePenalty priceBoundPenalty; end罚系数取多少合适这个没有统一答案但我的经验是先让罚项的量级和目标项量级接近再逐步增大罚系数。如果罚系数太小算法会钻空子找违反约束的解如果太大罚项会淹没目标项优化变成单纯找可行解。我通常用10倍步长去试找到一个在目标值和约束违反度之间平衡的值。4. 仿真实验设计与参数调优的实战经验4.1 基础场景怎么设定用典型日数据跑通闭环仿真场景我建议先不要铺得太大。最开始我拿某小区的典型日负荷数据做过一版实验后来发现用公开的、标准化的算例数据跑通逻辑闭环比用复杂但来源不明的数据靠谱得多。我常用的基础参数设定如下表所示供第一次搭模型的朋友参考参数项设定值说明调度周期24小时1小时为1个时段数据粒度适中计算量可接受用户类别数3类每类代表一种典型用电模式可转移负荷每类2~3个设备洗衣机、洗碗机、电动汽车可削减负荷每类1~2个设备空调、热水器电价范围0.3 ~ 1.2 元/kWh参照常见居民阶梯电价上下限外层鲸鱼种群20电价维度不高20足够外层迭代数50先跑通再提高内层鲸鱼种群15单用户决策变量规模不大内层迭代数30调参阶段从低开始这个规模下Matlab跑一次完整双层优化大约几分钟到十几分钟。如果第一次仿真就跑上百迭代、大规模用户调试成本和等待时间会让你怀疑模型写错了。4.2 平衡点收敛怎么看不能只看目标函数下降双层优化的收敛判断是新手最容易误解的地方。外层目标函数下降了不代表上下层已经达到均衡——因为目标函数和用户响应行为是两回事。我在跑仿真时判断收敛有三个标准缺一不可第一外层目标函数值稳定。连续若干代之间适应度变化幅度小于一个很小的阈值。第二电价曲线稳定。看两个连续迭代轮次之间24个时段电价的最大变化量。如果电价还在明显波动说明上层策略没有定下来。第三负荷响应曲线稳定。电价定了用户响应结果也要稳定。如果电价差不多但用户侧负荷曲线还在跳来跳去说明内层算法没有收敛外层基于这个不稳定结果做的调整自然不可信。有一个非常典型的现象值得注意外层算法已经收敛到了一个不错的解但内层因为迭代次数不够实际上返回的是一个次优响应导致外层目标函数“看起来”更好或更差从而误导了后续搜索。解决思路是先确保内层在任意给定电价下都能给出一致、稳定的响应结果再做外层收敛。简单说就是先调内层参数后调外层参数。4.3 双层优化跑不出理想结果时的排查顺序这事我踩过不少坑总结出一套排查顺序基本能涵盖绝大多数情况。第一步查内层单点优化是否正确。拿一组固定的电价单独跑内层算法手动检查最优负荷计划是否符合直觉。比如峰时电价高可转移负荷应该尽量避开谷时电价低负荷应该往那边靠。如果这一步结果不符合常识说明内层建模或代码有bug不要急着去调外层。第二步查内外层接口的数据传递。外层传给内层的电价向量有没有被正确地转换单位内层返回的负荷曲线时段顺序有没有对齐这听起来低级但我实际遇到过两次这种问题——一次是索引偏移了一位另一次是单位从MW写成了kWh导致负荷量级差了一千倍外层目标函数被一个错误的负荷数据带偏。第三步查约束是否有可行域为空的情况。有时候电价范围设得太窄加上收益约束太紧可行解根本不存在。算法在不可行区域里转圈适应度曲线一直是平的或者乱跳。这时候放宽约束条件重新跑再逐步收紧回来。第四步查权重系数间的量级匹配。峰谷差本身可能是几千kW的量级收益惩罚可能是几十万元的量级如果不做归一化处理小目标会被大目标彻底淹没。务必将不同目标项归一化到同一个量级再配权重否则表面上在调权重实际上是在做量纲换算。4.4 收敛震荡的处理双层博弈的一个顽固难点双层优化的震荡问题在某些参数组合下特别明显——外层调整电价内层跟着改负荷负荷又反过来推动电价调整结果就形成持续振荡数值始终无法稳定。我在几次实验里总结出三个有效对策。第一限制每轮电价的调整步长。不要让外层算法在相邻两代之间大幅跳变电价。给电价更新加一个最大变化幅度就像给PID控制加限幅一样可以显著缓解响应链路的振荡。第二引入历史解的记忆机制。简单说就是两次迭代之间做松弛——例如取当前解和历史解的加权平均作为下一轮使用的解让系统对变化更平滑。这在处理博弈迭代收敛问题时是很经典的手法。第三检查负荷模型的连续性。很多负荷参数如果设置不当会让内层目标函数出现强烈的非凸性比如可转移负荷的启动时间选择在相邻时段之间跳来跳去导致负荷曲线剧烈波动。这种情况下可以考虑把时间粒度细化或者增加负荷启停的约束平滑项。5. 从实验台到真实场景的限制与扩展方向5.1 模型理想化假设带来的边界基于非合作博弈的分层调度模型在仿真里表现很好但真要走向实际部署有几个假设必须重新审视。第一个是响应度的确定性假设。模型默认用户会严格按照优化结果行动现实里用户可能看了账单也不愿意动或者因为意外情况临时改变用电计划。想让模型更落地可以把用户响应建模成概率性的——在优化目标里加入响应不确定性项或者跑多个随机场景做期望值优化。第二个是信息交互的现实约束。分层模型保护了用户隐私但上层仍然需要知道每个用户的响应结果才能迭代。真实场景里这些数据从哪里来、多久更新一次、可能存在哪些误差都会影响最终的调度精度。工程化的做法是用历史响应数据训练一个用户行为预测器替代直接调用优化器获取响应结果。第三个是电价公平性的公众感受。模型里的电价可以为一个目标函数而自由波动但现实中的电价调整要面对用户接受度和社会舆情。价格波动幅度如果过大即使模型计算出来是“最优”的也难以落地执行。5.2 可以继续深挖的几个方向这套模型框架的价值在于“分层博弈”的思路可以复用到不少邻近问题上我自己已经在尝试的几个方向供你参考。电动汽车聚合商博弈把若干电动汽车车主聚合起来用类似的分层模型和电网互动内层目标从电费最小改充放电计划的最优。价格型与激励型需求响应的融合在电价引导的基础上再叠加需求响应补贴策略用户的决策空间更丰富博弈行为也更复杂。多时间尺度协同目前是24小时尺度实际运行中还需要日内滚动修正用超短期预测更新负荷基线外层电价也需要分阶段固定避免用户无所适从。5.3 给初次上手者的最后一个提醒关于代码调试速度我个人的体会是先把一切都调小、调少、调简单。用3个时段、2个设备、1个用户跑通整个链路确认博弈逻辑闭环无误再逐步扩展时段和设备数量。很多人一上来就建24时段、50个用户、十几类设备的大模型结果一个接口错误都看不出来最后被变量传递问题白白耗掉好几天——这种事我经历过太多次了。如果这套方法对你的项目有帮助我建议重点啃透两个地方一是内外层数据传递的一致性二是约束罚函数与目标函数之间的量级匹配。这两个地方做扎实了整个仿真一般不会出现太大问题。后面如果你做了电动汽车聚合或者多用户博弈的扩展版本也欢迎多交流参数调优的心得。