☰
基于非合作博弈的居民负荷分层调度与双层鲸鱼算法求解
2026/10/13 11:09:42 网站建设 项目流程

第一次把“双层鲸鱼算法”跑起来的时候,我盯着迭代收敛曲线看了一整个晚上。原因是内层几十个居民用户每轮都要重新求解最优负荷响应,外层鲸鱼算法又在不断更新电价策略,任何一端抖动,外层适应度曲线就像心电图一样上下乱跳。我花了大约三周才把这一整套模型理顺:基于非合作博弈的居民负荷分层调度,用 Matlab 写代码、用双层鲸鱼算法求解,最终在算例里把峰谷差压了下去。这篇文章把项目从问题建模、博弈结构、算法设计到代码实现、收敛调参、结果分析全部拆开讲一遍,适合两类人:一类是刚接触需求响应、虚拟电厂、双层优化的学生,另一类是拿到类似代码但不知道如何改参数、如何让结果变稳定的复现型玩家。看完你至少能少走两三个月的弯路,也能对“双层算法”这件事有一个比论文摘要更踏实的判断。

1. 从“命令式控制”到“自利优化”:这个模型到底在解决什么?

1.1 集中式直接负荷控制为什么在居民场景失灵

居民负荷调度的初衷很简单:洗衣机、热水器、空调、电动汽车充电桩这些家用负荷,不要全部挤在早晚用电高峰。传统做法是直接负荷控制,也就是由电网侧下发指令,空调降功率、充电桩暂停、热水器错峰启动,用户被动执行。这种模式在工业和商业负荷上很管用,因为工厂有专人负责设备管理,商业楼宇也有物业统一运营。但放到居民楼里,问题立刻暴露:用户数量大、单个负荷容量小、用电偏好差异大,如果每台设备都要上报状态并接收指令,通信开销、用户隐私、设备改造都是很大的负担。

更要紧的是,居民用户的用电行为根本不是“指令型”的。人家晚上想开空调,你直接给切了,一次两次可以,长期下去没有可行性。让用户真正响应调度的,不是行政命令,而是价格信号和自身利益。所以业内才会逐步转向“价格型需求响应”:把用电决策权还给用户,让电价去引导行为。

1.2 为什么是“分层的”加“非合作博弈”

价格型需求响应听起来容易,真正建模时却有一个绕不开的矛盾:上层和下层不是同一个优化目标。上层通常是售电公司、虚拟电厂或社区聚合商,希望削峰填谷、降低系统峰值,同时考虑购电成本和收益;下层居民用户则想让电费尽量低、舒适度尽量高。这两个目标没法塞进同一个目标函数里,需要分层描述:上层先决定电价策略,下层根据电价调整负荷,下层的响应结果反过来又影响上层目标。这种“领导者—跟随者”关系,就是典型的 Stackelberg 博弈结构。

下层用户之间的关系则属于“非合作博弈”。居民之间没有强制契约,没有统一协调人,每个人都按自己的用电偏好做决策。如果几户人家的电动汽车都想在凌晨 2 点低谷电价时段充电,大家都往那个时段挤,变压器容量可能越限,局部电压也可能出问题。他们没有合作机制,只能各自调整用电行为,最终形成一种谁都没有动力单方面改变的稳定状态,也就是 Nash 均衡。把这个均衡作为分层调度模型的求解目标,标题里的“基于非合作博弈的居民负荷分层调度”就是这么来的。

1.3 这个模型能带来什么直接变化

举一个最简单的例子。假设同一个台区里有几户典型住户,一户有屋顶光伏和储能,另一户只有空调和电动汽车。统一调度模式下,程序直接告诉他们各自该在哪个时段做什么;而在博弈框架里,他们面对同一个分时电价,光伏户可能选择午间给储能充电,电动汽车户只能把充电安排到深夜甚至凌晨。从社区整体看,峰值负荷降下来了;从每户来看,他们是在“自由选择”中完成了自我优化,没有谁在强迫谁。模型输出的不是一张强制指令清单,而是一套电价曲线以及与之对应的用户响应策略。这件事在工程上的意义,是把大量异构用户的私有决策问题,转变成一个只需要公开价格信号的公共问题。

2. 上层定电价、下层求负荷:分层模型中的变量、目标函数与约束

2.1 上层决策:分时电价与售电公司的目标

上层需要一个明确的决策变量。实际项目里最常用的是把一天分成 24 个时段,决策变量为每个时段的电价向量 p = [p1, p2, ..., p24],有时也会细分到 48 个点。我的实现以 24 时段分时电价为主,没有再叠加激励补贴,这样模型边界清晰,结果也容易解释。

上层目标可以选择两种常见形式:一是最小化系统净负荷峰谷差,二是最大化售电公司收益。我最终选用峰谷差最小化,因为结果直观,而且不受平均电价数值量纲的干扰。目标函数可以写成最小化各时段总负荷与均值的偏差平方,也就是:

min sum_{t=1..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(t+1) = SOC(t) + 充电功率 - 自放电,SOC 不能超过容量上下限;
  • 可平移负荷总量约束:一天内的总用电量等于设定值;
  • 台区容量约束:所有用户同时段总功率不能超过变压器容量。

2.3 非合作博弈如何进入分层框架

很多新手到这里会问:每户人家明明是自己独立求解,为什么还要扣一个“非合作博弈”的帽子?关键在共享资源的耦合。用户之间不交换策略,但他们会因为同一个台区容量、同一个供电网络而被耦合在一起。如果每户独立决策后的总负荷没有超过容量约束,那博弈就体现在价格信号下的独立决策;如果超过了,就需要一个出清机制,把容量稀缺性反馈成“虚拟价格”或者“容量信号”,让用户继续调整。这个过程就等价于非合作博弈的迭代出清。

在 Matlab 代码里,我不需要真正让 n 个用户互相交换信息。实现方式是对每个用户面对同一组电价独立求解,然后汇总负荷矩阵,再校验共享容量约束;不满足时修正价格或反馈容量信号,重复迭代直到收敛。这种实现思路非常关键,也是整个模型代码里最容易出 bug 的部分。

3. 双层鲸鱼算法的求解机制:外层搜价格、内层求用户响应

3.1 为什么不用纯数学求解,而要选启发式算法

从数学形式看,这是一个双层优化问题,传统方法是把下层问题用 KKT 条件转换成上层约束,再交给商业求解器一次求解。但这个模型的下层一旦引入 0/1 离散变量、非线性效用函数、多个用户异构响应,KKT 转换会变得非常繁琐,很多情况下甚至难以写出闭合表达式。同时,下层用户数量变化时,模型也要跟着改,通用性很差。

于是求解架构变成上下层各管一段:外层用启发式搜索去枚举可能的电价向量,内层调用一个“用户响应函数”求解负荷,把结果返回给外层评估。这种嵌套式求解在学术上被叫作“双层智能算法”,具体用鲸鱼算法实现,就对应到了标题里的“双层鲸鱼算法”。它不是唯一选择,但在一堆启发式算法里,鲸鱼算法参数少、实现简单,初始收敛速度也比较快,适合作为第一版原型。

3.2 鲸鱼算法做外层搜索的基本流程

鲸鱼优化算法的灵感来自座头鲸的泡泡网捕猎,核心机制包括三个阶段:包围猎物、螺旋气泡网攻击、随机搜索。放在电价优化问题里,每一只“鲸鱼”的位置就是一组 24 维电价向量。算法步骤大致如下:

  1. 初始化种群位置,每个个体每一维在电价上下限之间随机生成;
  2. 对种群里的每一只鲸鱼,将当前位置作为电价向量,调用下层响应函数,计算所有用户的用电计划;
  3. 汇总总负荷矩阵,评估上层目标函数,得到该鲸鱼的适应度;
  4. 根据迭代次数和随机概率,选择收缩包围或螺旋更新位置;
  5. 达到最大迭代次数或适应度变化小于阈值,输出最优电价和对应负荷。

这个流程中最核心的性能指标是“一个种群更新需要调用多少次下层函数”。如果种群数设 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、电价上下限这三个输入写在项目文档首页,它们是最容易在调试中被反复改动的参数,也直接决定整个调度结论的走向。建模与算法是骨架,参数和数据才是血和肉,把这两件事分开管理,后面的工作会顺得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询