基于非合作博弈的居民负荷分层调度与双层鲸鱼算法求解
第一次把“双层鲸鱼算法”跑起来的时候我盯着迭代收敛曲线看了一整个晚上。原因是内层几十个居民用户每轮都要重新求解最优负荷响应外层鲸鱼算法又在不断更新电价策略任何一端抖动外层适应度曲线就像心电图一样上下乱跳。我花了大约三周才把这一整套模型理顺基于非合作博弈的居民负荷分层调度用 Matlab 写代码、用双层鲸鱼算法求解最终在算例里把峰谷差压了下去。这篇文章把项目从问题建模、博弈结构、算法设计到代码实现、收敛调参、结果分析全部拆开讲一遍适合两类人一类是刚接触需求响应、虚拟电厂、双层优化的学生另一类是拿到类似代码但不知道如何改参数、如何让结果变稳定的复现型玩家。看完你至少能少走两三个月的弯路也能对“双层算法”这件事有一个比论文摘要更踏实的判断。1. 从“命令式控制”到“自利优化”这个模型到底在解决什么1.1 集中式直接负荷控制为什么在居民场景失灵居民负荷调度的初衷很简单洗衣机、热水器、空调、电动汽车充电桩这些家用负荷不要全部挤在早晚用电高峰。传统做法是直接负荷控制也就是由电网侧下发指令空调降功率、充电桩暂停、热水器错峰启动用户被动执行。这种模式在工业和商业负荷上很管用因为工厂有专人负责设备管理商业楼宇也有物业统一运营。但放到居民楼里问题立刻暴露用户数量大、单个负荷容量小、用电偏好差异大如果每台设备都要上报状态并接收指令通信开销、用户隐私、设备改造都是很大的负担。更要紧的是居民用户的用电行为根本不是“指令型”的。人家晚上想开空调你直接给切了一次两次可以长期下去没有可行性。让用户真正响应调度的不是行政命令而是价格信号和自身利益。所以业内才会逐步转向“价格型需求响应”把用电决策权还给用户让电价去引导行为。1.2 为什么是“分层的”加“非合作博弈”价格型需求响应听起来容易真正建模时却有一个绕不开的矛盾上层和下层不是同一个优化目标。上层通常是售电公司、虚拟电厂或社区聚合商希望削峰填谷、降低系统峰值同时考虑购电成本和收益下层居民用户则想让电费尽量低、舒适度尽量高。这两个目标没法塞进同一个目标函数里需要分层描述上层先决定电价策略下层根据电价调整负荷下层的响应结果反过来又影响上层目标。这种“领导者—跟随者”关系就是典型的 Stackelberg 博弈结构。下层用户之间的关系则属于“非合作博弈”。居民之间没有强制契约没有统一协调人每个人都按自己的用电偏好做决策。如果几户人家的电动汽车都想在凌晨 2 点低谷电价时段充电大家都往那个时段挤变压器容量可能越限局部电压也可能出问题。他们没有合作机制只能各自调整用电行为最终形成一种谁都没有动力单方面改变的稳定状态也就是 Nash 均衡。把这个均衡作为分层调度模型的求解目标标题里的“基于非合作博弈的居民负荷分层调度”就是这么来的。1.3 这个模型能带来什么直接变化举一个最简单的例子。假设同一个台区里有几户典型住户一户有屋顶光伏和储能另一户只有空调和电动汽车。统一调度模式下程序直接告诉他们各自该在哪个时段做什么而在博弈框架里他们面对同一个分时电价光伏户可能选择午间给储能充电电动汽车户只能把充电安排到深夜甚至凌晨。从社区整体看峰值负荷降下来了从每户来看他们是在“自由选择”中完成了自我优化没有谁在强迫谁。模型输出的不是一张强制指令清单而是一套电价曲线以及与之对应的用户响应策略。这件事在工程上的意义是把大量异构用户的私有决策问题转变成一个只需要公开价格信号的公共问题。2. 上层定电价、下层求负荷分层模型中的变量、目标函数与约束2.1 上层决策分时电价与售电公司的目标上层需要一个明确的决策变量。实际项目里最常用的是把一天分成 24 个时段决策变量为每个时段的电价向量 p [p1, p2, ..., p24]有时也会细分到 48 个点。我的实现以 24 时段分时电价为主没有再叠加激励补贴这样模型边界清晰结果也容易解释。上层目标可以选择两种常见形式一是最小化系统净负荷峰谷差二是最大化售电公司收益。我最终选用峰谷差最小化因为结果直观而且不受平均电价数值量纲的干扰。目标函数可以写成最小化各时段总负荷与均值的偏差平方也就是min sum_{t1..T} (L_t - L_avg)^2其中 L_t sum_i x_{i,t}表示 t 时段所有用户的总用电负荷。约束条件包括电价上下限通常限制在 0.3 到 1.2 元/kWh 之间避免出现极端价格信号一天平均电价维持在基准水平附近防止整体电价水平漂移必要时还加一个相邻时段电价变化幅度限制避免价格跳变过大导致用户行为剧烈振荡。这里的商业逻辑是上层不是裁判而是参与者。它通过价格调节用户负荷最终优化自己的运营指标这一点和传统“一刀切补贴型”需求响应有本质区别。2.2 下层决策居民用户的效用函数与用电约束下层用户数量记为 n每个用户 i 的控制向量是 x_i [x_{i,1} ... x_{i,T}]。实际建模中我会把家用负荷分成三类不可调负荷照明、电视、冰箱、可平移负荷洗衣机、洗碗机、电动汽车充电、可削减负荷空调、热水器。分类直接决定建模复杂度。用户目标是最大化自身收益通俗说就是用电带来的舒适度减去电费支出。收益部分常用二次效用函数max sum_t [ alpha_i * U(x_{i,t}) - p_t * x_{i,t} ]其中 U 是凹函数典型形式是 U(x) ax - bx^2。这样求出来的最优响应满足“边际效用等于电价”的条件连续型设备可以直接用解析式表达。可平移负荷则要做离散建模比如洗衣机必须在某个时间窗口内完成全部用电量单时段功率是 0/1 变量。下层约束通常包括功率上下限x_{i,t} 在 [x_min, x_max] 范围内储能约束SOC(t1) SOC(t) 充电功率 - 自放电SOC 不能超过容量上下限可平移负荷总量约束一天内的总用电量等于设定值台区容量约束所有用户同时段总功率不能超过变压器容量。2.3 非合作博弈如何进入分层框架很多新手到这里会问每户人家明明是自己独立求解为什么还要扣一个“非合作博弈”的帽子关键在共享资源的耦合。用户之间不交换策略但他们会因为同一个台区容量、同一个供电网络而被耦合在一起。如果每户独立决策后的总负荷没有超过容量约束那博弈就体现在价格信号下的独立决策如果超过了就需要一个出清机制把容量稀缺性反馈成“虚拟价格”或者“容量信号”让用户继续调整。这个过程就等价于非合作博弈的迭代出清。在 Matlab 代码里我不需要真正让 n 个用户互相交换信息。实现方式是对每个用户面对同一组电价独立求解然后汇总负荷矩阵再校验共享容量约束不满足时修正价格或反馈容量信号重复迭代直到收敛。这种实现思路非常关键也是整个模型代码里最容易出 bug 的部分。3. 双层鲸鱼算法的求解机制外层搜价格、内层求用户响应3.1 为什么不用纯数学求解而要选启发式算法从数学形式看这是一个双层优化问题传统方法是把下层问题用 KKT 条件转换成上层约束再交给商业求解器一次求解。但这个模型的下层一旦引入 0/1 离散变量、非线性效用函数、多个用户异构响应KKT 转换会变得非常繁琐很多情况下甚至难以写出闭合表达式。同时下层用户数量变化时模型也要跟着改通用性很差。于是求解架构变成上下层各管一段外层用启发式搜索去枚举可能的电价向量内层调用一个“用户响应函数”求解负荷把结果返回给外层评估。这种嵌套式求解在学术上被叫作“双层智能算法”具体用鲸鱼算法实现就对应到了标题里的“双层鲸鱼算法”。它不是唯一选择但在一堆启发式算法里鲸鱼算法参数少、实现简单初始收敛速度也比较快适合作为第一版原型。3.2 鲸鱼算法做外层搜索的基本流程鲸鱼优化算法的灵感来自座头鲸的泡泡网捕猎核心机制包括三个阶段包围猎物、螺旋气泡网攻击、随机搜索。放在电价优化问题里每一只“鲸鱼”的位置就是一组 24 维电价向量。算法步骤大致如下初始化种群位置每个个体每一维在电价上下限之间随机生成对种群里的每一只鲸鱼将当前位置作为电价向量调用下层响应函数计算所有用户的用电计划汇总总负荷矩阵评估上层目标函数得到该鲸鱼的适应度根据迭代次数和随机概率选择收缩包围或螺旋更新位置达到最大迭代次数或适应度变化小于阈值输出最优电价和对应负荷。这个流程中最核心的性能指标是“一个种群更新需要调用多少次下层函数”。如果种群数设 20迭代 100 代那就要调用 2000 次内层响应函数假设下层有 30 个用户等于要解 6 万个用户子问题。所以内层函数的计算效率和缓存策略直接决定这个项目是晚饭前跑完还是第二天早上才能拿到结果。3.3 减轻内层计算负担的两个实践策略第一个策略是用户聚合。不需要真的建模 30 个完全独立的家庭可以按用电行为参数聚合成 3 到 5 类典型用户再把结果按每类人数加权汇总。这样做既保留了用户行为差异又把问题规模压缩了一个数量级。第二个策略是连续设备解析化、离散设备小规模枚举。空调和储能这类连续可调设备直接用“边际效用等于电价”的公式求解洗衣机和洗碗机这些离散设备由于可完成的启停时段组合数量有限用窗口枚举完全可行。在代码里把连续响应和离散枚举分开实现既能提速也让调试时更容易定位问题。% 双层鲸鱼主循环结构示意 for iter 1:maxIter for k 1:popSize priceVec whalePos(k, :); % 当前鲸鱼位置作为电价向量 loadMat bottom_response(priceVec, params); % 内层用户响应 fitness(k, 1) upper_objective(priceVec, loadMat); end [bestVal, bestIdx] min(fitness); whalePos updateWhalePositions(whalePos, bestIdx, iter, maxIter); end这段代码虽然短但它体现了双层算法的全部骨架外层更新的是电价向量内层计算的是用户负荷两者通过适应度函数连接。4. Matlab 实现的分层结构数据、主循环与函数划分4.1 数据准备把设备参数与负荷曲线拆出来我强烈建议把所有输入参数集中放在一个结构体里而不是散落在主脚本各处。我的做法是建立 params 结构体内容包括电价上下限通常取 0.3 到 1.2 元/kWh一天平均电价参考值设为 0.6 元/kWh每类典型用户的功率上下限、可平移负荷窗口、储能容量和初始 SOC不可调负荷的 24 小时预测曲线台区变压器容量限制。数据来源可以是 Excel 或 CSV 文件用一个load_case_data.m函数读取并打包成 params。这样之后做敏感性分析或者换算例时只需要改输入文件不需要翻主程序避免“改了一处漏掉另一处”的低级错误。4.2 主要的代码模块划分代码模块我分成三个核心部分upper_objective.m输入电价向量和用户负荷矩阵输出峰谷差或售电商收益bottom_response.m输入电价向量和 params输出所有用户的逐时用电矩阵whale_optimizer.m实现鲸鱼算法主循环调用前面两个函数输出最优电价和最优负荷。bottom_response.m内部还会继续拆分比如aircon_response、ev_response、detachable_load等子函数。有人会问为什么不用现成的fmincon做下层连续优化我的观点是fmincon 每次调用都有额外开销而调度问题结构规整连续设备用解析式求最优功率速度和稳定性都要更好。function loadMat bottom_response(priceVec, params) nUser params.nUser; T params.T; loadMat zeros(T, nUser); for u 1:nUser loadMat(:, u) solve_user_scheduling(priceVec, params.users(u)); end % 如果考虑台区容量约束在这里做容量出清修正 loadMat capacity_clearance(loadMat, params.capacityLimit); end如果用户数量真的很大建议把用户循环改成矩阵向量化计算或者用parfor并行加速。这里要注意一个性能陷阱bottom_response()会被外层反复调用上千次每次多一次全用户循环累积时间差异会非常明显。4.3 加速运行的三个关键手段双层优化的一个天生问题就是运行时间长。我总结出三个有效的加速手段第一下层响应向量化。24 个时段乘 n 个用户的数据结构尽量用矩阵运算一次性完成而不是在用户维度上写for循环。第二可平移负荷结果缓存。每一类典型用户的可平移设备只有几种初始化时就可以把可能的启停策略组合提前算好后续运行里只做查表直接跳过重复枚举。第三并行计算。如果种群数较大用并行计算工具箱把不同鲸鱼个体的下层响应计算并行化。种群是“尴尬并行”结构线程间不需要通信加速比很乐观唯一要注意的是随机数流控制确保每次运行结果可复现。4.4 主流程的伪代码全貌整个流程合成起来是这样的% 主脚本 params load_case_data(); whalePos initializeWhale(popSize, T, pmin, pmax); for it 1:maxIter for k 1:popSize price whalePos(k, :); loads bottom_response(price, params); fitness(k, 1) upper_objective(price, loads); end [bestFitness(it), bestIdx] min(fitness); whalePos updateWhalePositions(whalePos, bestIdx, it, maxIter); end plot(1:maxIter, bestFitness); xlabel(迭代次数); ylabel(上层目标函数值);这样的模块化设计最大的好处是后续想换算法比如把鲸鱼换成粒子群或差分进化只需要替换updateWhalePositions这一个函数。我在这个项目跑了两个月后用同一个框架扩展了不同目标函数和用户类型基本没有动主结构代码。5. 跑崩过、收敛慢、最优解可疑我在实现时踩过的四个坑5.1 内层不收敛导致外层适应度变成噪声第一版实现里我把连续设备和可平移设备放在同一个迭代求解器里内层容差设置得特别小结果外层每轮评估出来的适应度总是轻微抖动收敛曲线像锯齿一样根本无法判断算法到底有没有收敛。排查下来发现内层求解器每次返回给外层的负荷矩阵不完全一致外层目标函数自然就跟着抖动。后来把内层容差放宽到适合的相对值比如以 kW 为单位的负荷设置 1e-3 量级容差外层适应度才稳定下来。这里要提醒一点内层精度不是越高越好重点是保证每次外层评估的指标前后可比。5.2 鲸鱼算法的随机性让结果每次都不一样元启发式算法天然带随机性复现实验时一定要先设置随机数种子比如rng(2024)保证每次运行结果一致。但更本质的问题是电价向量有 24 个维度搜索空间很大传统 WOA 容易陷入局部最优。针对这个问题我的做法是加多起点重启在前 20 代里保留一部分概率随机重置部分鲸鱼个体或者干脆跑 5 次独立实验报告最好值、平均值和最差值而不是只挑一次运气好的结果。学术上这叫“多策略增强”工程上其实就是承认启发式算法的不确定性用重复实验和统计结果来对冲。5.3 电价上下限太宽结果出现“谷时新峰”这是我在算例阶段遇到的最奇怪的现象峰谷差不但没下降反而转移到了下午。原因是电价上下限设置得太宽用户把所有可平移负荷全部压到了最低电价时段结果造成新的用电尖峰。这个锅不完全在算法而在模型约束不足。解决办法有两个一是给可平移负荷加“同时率约束”限制同一时段内可平移负荷的总功率比例二是把峰谷电价差限制在合理范围我实际用下来 3 倍以内比较合适既能激励用户错峰又不会诱导所有负荷疯狂平移。5.4 用二维穷举验证算法不是可选项而是必选项当结果可疑或收敛不稳定时最有效的排错手段是降维验证。把模型的时间维度从 24 缩到 2 个时段电价变成二维向量然后用网格搜索把整个电价平面每个格点都算一遍得到一个完整的适应度地形。这时候再让鲸鱼算法去找最优对比是否落在网格搜索的最优区域附近。我靠这个方法半天内就发现了代码里一个数组索引错位的问题。很多人跳过这一步直接跑完整模型一旦出了问题面对 24 维空间完全没有排查头绪。所以我强烈建议所有复现这个模型的人先做二维或三维时段的玩具案例再升级到完整 24 时段。6. 怎么看算例效果从指标对比到稳定性验证6.1 结果表里应该包含哪些核心指标调度结果不能只看一张负荷曲线。我每次做算例分析都会整理一个结果对比如下指标无调度基准仅分时电价双层博弈调度最大负荷(kW)较高中等较低峰谷差(kW)最高中等最低平均电价(元/kWh)0.600.610.60用户侧总体效用低较高较高具体数值会因算例数据不同而变但趋势通常一致加入博弈调度后峰谷差明显下降用户总体效用不会恶化。如果跑出来峰谷差反而变大优先检查两个地方电价上下限是否过宽可平移负荷同时率约束是否缺失。这两处是最容易导致“假优化”的模型漏洞。6.2 如何验证结果确实是一个均衡点双博弈得到的结果应该是可解释的高电价时段用户自动削减高耗能设备低电价时段填补用电。但光看曲线还不够还要做“偏离检验”在收敛得到的电价策略下随机挑一个用户允许它单独改变用电方案看它的自身效用会不会下降。如果绝大多数随机偏离都会导致该用户效用下降说明当前结果至少是一个局部稳定点也就是 Nash 均衡的一个局部验证。这个方法无法证明全局唯一但作为项目验收手段已经足够。6.3 面对负荷不确定性时的处理思路居民负荷本质上是有随机性的今天和明天的不可调负荷曲线不一样。我的做法是先对不可调负荷做蒙特卡洛模拟生成 5 到 10 个典型场景然后在每一类场景下分别跑调度模型最后用期望值或最坏情况目标来评价上层策略。如果你直接把某一天的实测负荷拿来跑得到的调度方案只对那一天有效没有太大参考意义。加上多场景后上层电价策略的鲁棒性会更好复现结果也更值得写进报告。7. 项目复盘与复用建议代码层面值得保留的习惯有四个一是上下层函数严格分离二是全局参数集中设置三是运行前设置随机数种子保证可复现四是每次调度结束后把最优电价和负荷曲线保存成表格文件方便后续画图和横向比较。基于这个框架适合扩展的方向也很多。可以换掉上层目标从峰谷差改成售电公司利润模型模型的博弈结构基本不需要变可以改用实时激励补贴代替分时电价等于把决策变量从电价曲线变成补贴额度曲线可以把下层典型用户参数用正态分布随机采样生成大量虚拟用户让结果更贴近真实社区还可以把 WOA 换成 WOA 加局部搜索的混合算法收敛速度通常还能再快一些。我个人最大的体会是这个项目里最花时间的不是算法本身而是让模型假设和数据参数真正对得上。跑通主循环并不难难的是从一条异常收敛曲线里反推出是约束条件写错了还是内层容差设得不对还是电价边界太宽。所以最后再给一个建议动手复现之前先把可平移负荷窗口、储能初始 SOC、电价上下限这三个输入写在项目文档首页它们是最容易在调试中被反复改动的参数也直接决定整个调度结论的走向。建模与算法是骨架参数和数据才是血和肉把这两件事分开管理后面的工作会顺得多。