
先看标题里的关键信息双层鲸鱼算法、非合作博弈、居民负荷分层调度、Matlab。这东西说白了就是——在居民侧的用电调度场景中把“电网/聚合商定价格”和“居民用户调负荷”这两个层级分开建模各自求解最优策略再用非合作博弈衔接两边的利益关系最终通过改进的双层鲸鱼算法把整个模型解出来。我最早做这块的时候踩得比较深的坑有两个一是把双层问题当成单层优化去处理拿到手的结果根本没法解释二是居民用户的负荷响应行为没建模好导致上层怎么调价格下层都不“接招”整个迭代直接卡死。这篇文章把我在实际做这套模型时的完整思路、数学建模过程、算法设计细节、Matlab代码结构以及调试经验全部复盘一遍。如果你是在做需求响应、峰谷差优化、用户侧调度相关方向或者是刚接触双层优化、博弈论却在写代码阶段发愁的人这篇文章应该能让你少走不少弯路。1. 模型设计思路拆解1.1 为什么居民负荷调度非得“分层”居民侧负荷调度和工商业用户不一样单个用户容量小、数量大、行为随机性强。你要是像传统电网调度那样搞一个集中式优化把所有居民的负荷曲线全部收上来统一算第一是隐私问题用户根本不接受第二是计算规模大到没法落地第三就是缺少利益驱动——用户凭什么让你调他的空调和热水器分层调度的本质就是把决策权划分清楚上层是电网公司或者负荷聚合商负责制定分时电价或者激励价格下层是居民用户在拿到价格信号之后根据自己对用电舒适度的偏好自主调整可转移负荷洗衣机、洗碗机、电动汽车充电和可中断负荷空调、热水器。上层管“价格杠杆”下层管“负荷响应”中间靠电价这个信号衔接起来。这个结构其实是Stackelberg博弈的标准框架。上层是领导者先出牌下层是跟随者根据上层的价格策略做出最优响应。上下层反复迭代直到两边都不想再改变自己的策略就是整个系统达到稳定状态的时刻。用非合作博弈来刻画居民用户之间的关系也很自然用户之间不存在“商量好了集体调负荷”这种合作机制每个人都是独立的理性个体各自追求自己的用电效用最大化。1.2 双层模型的基本框架整个模型可以表达成下面这种嵌套结构上层目标函数最小化系统峰谷差或者最小化购电成本决策变量是24小时的分时电价。约束条件电价上下限、电价平滑性防止相邻时段价格跳变过大让用户反感。下层目标函数每个用户最大化“用电满意度减去电费支出”决策变量是该用户的可转移负荷启停时间和可中断负荷调节量。约束条件用电设备的运行时间窗、可转移负荷的总转移量上限、可中断负荷的调节范围、用户总用电量上下限。上下层之间没有任何直接命令关系上层只能通过改变电价间接影响用户行为下层只能通过改变负荷曲线反馈给上层重新计算目标函数。这种“间接控制”的思路恰恰符合真实电力市场中的运行逻辑。2. 非合作博弈与负荷响应建模2.1 博弈三要素怎么对应到实际场景博弈论里最核心的三要素参与者、策略集、收益函数。放在这个模型里对应关系非常清晰参与者N个居民用户也可以把负荷聚合商作为上层博弈参与者单独建模。策略集每个用户可以根据电价信号调整自己的负荷曲线可转移负荷的转移时间段、可中断负荷的响应容量这些都是策略。收益函数这是建模最需要花心思的地方。用户的目标是最大化自己的用电净效用也就是“用电带来的舒适感”减去“电费支出”。用电舒适感用二次效用函数来描述这个形式在电力需求响应文献里非常常见U_i(x_i) a_i * x_i - b_i * x_i^2其中x_i是用户i在某时段的用电功率a_i和b_i是用户偏好参数。二次函数的特点是边际效用递减这符合直觉——夏天你开空调从26度调到25度幸福感提升很大从18度再往下调体感变化已经不明显了但电费却涨得很快。用户的净收益就是maximize: a_i * x_i - b_i * x_i^2 - price(t) * x_i这个目标函数是凹函数对x_i求导等于零就能得到用户在给定电价下的理论最优用电功率x_i*(t) (a_i - price(t)) / (2 * b_i)注意这里还有个隐含关系电价太高的时候用户会选择不用电或少用电电价过低的时候用户多用电这正好是需求响应想要的弹性行为。2.2 负荷分层刚性负荷、可转移负荷与可中断负荷居民负荷分三类处理每类的建模逻辑完全不同刚性负荷照明、冰箱、电视这类用电时间和功率基本固定不具备调节潜力在模型中作为基础负荷直接叠加到总负荷曲线上。可转移负荷洗衣机、洗碗机、电动汽车充电桩。这类负荷的特点是“用电总量固定但用电时段可平移”。洗衣机洗一桶衣服消耗的电量是固定的但在凌晨洗还是晚上洗取决于电价。建模时用“运行状态矩阵”来表示u(t)为0或1表示设备在某时段是否运行加上“总运行时长固定”的约束就能把可调度性表达出来。可中断负荷空调、电热水器。这类负荷的特性是可以临时降低功率甚至短时关闭但关闭太久会影响用户体验。建模时设定一个中断深度和最长中断时间用户可以在电价高的时段选择少用或不用用舒适度损失换取电费节省。真正做仿真的时候这三类负荷的比例大概按40%、35%、25%来设置比较贴近实际。全刚性负荷用户没有调节能力也就没必要参与博弈全柔性负荷又失真用户再理性也不可能把用电时间全挪到半夜。2.3 纳什均衡的迭代求解思路下层用户之间做非合作博弈最终要收敛到纳什均衡——每个用户都不能通过单方面改变策略获得更大收益。直接把纳什均衡条件写成一个方程组去求解析解在有约束的离散决策变量下几乎不可能所以工程上都用迭代逼近的方法初始给定一组负荷策略每个用户在其他用户策略不变的情况下求解自己的最优反应函数得到新策略然后所有用户同时更新策略进入下一轮重复这个过程直到相邻两轮策略的变化量小于阈值就认为达到了纳什均衡。我在仿真中发现用户数量越多收敛需要的迭代次数越多。30个用户大概需要20到30轮博弈迭代才能平稳下来。这里有个技巧每轮用户策略更新时可以加一个阻尼系数把实际更新的策略设置为“旧策略乘以(1-alpha)加新策略乘以alpha”alpha取0.3到0.5。不加阻尼的话策略容易在两三个值之间来回震荡看着已经收敛了实际上是在一个极限环上打转。3. 双层鲸鱼算法的原理与改进3.1 为什么选鲸鱼算法而不是粒子群或遗传算法优化算法这块早期我试过粒子群和遗传算法后来才换到鲸鱼算法。对比下来鲸鱼有三个明显的优势参数少。粒子群要调惯性权重w、个体学习因子c1、社会学习因子c2遗传算法要调交叉概率、变异概率、选择压力。鲸鱼算法本质上只需调节一个参数就是螺旋更新公式里的常数b其他系数全是自适应计算出来的调参成本低一大截。全局搜索和局部勘探平衡得比较好。粒子群到后期粒子容易扎堆陷入局部最优就出不来了鲸鱼算法的“气泡网攻击”和“随机搜索猎物”两条路径交替进行相当于一会做深度局部搜索、一会跳出去盲探不容易锁死在某个局部区域。序列化的双层结构实现起来顺手。这个项目里上层鲸鱼种群搜索电价策略下层还要再调用另一层鲸鱼算法求解用户响应策略。如果上下层用同一种算法框架代码的复用性会好很多调试的时候也不用同时面对两套不同的参数体系。3.2 标准鲸鱼算法的核心更新公式先快速回顾一下标准鲸鱼算法的更新机制后面代码部分要用到。算法模拟的是座头鲸的觅食行为三种位置更新模式包围猎物模式D |C * X_best(t) - X(t)|X(t1) X_best(t) - A * D其中A 2ar - aC 2ra在迭代过程中从2线性递减到0。螺旋气泡网攻击模式D_star |X_best(t) - X(t)|X(t1) D_star * exp(b*l) * cos(2πl) X_best(t)随机搜索猎物模式D_rand |C * X_rand(t) - X(t)|X(t1) X_rand(t) - A * D_rand具体执行时通过一个随机数p和|A|的取值决定走哪个分支p小于0.5且|A|小于1时用包围模式p小于0.5且|A|大于等于1时用随机搜索模式p大于等于0.5时用螺旋更新。3.3 针对双层博弈模型的两个关键改进原始鲸鱼算法直接套到双层模型上我测下来有两个问题一是初始化种群随机性不够前期搜索效率偏低二是后期收敛速度变慢到接近最优解时容易在解附近来回跳。第一个改进是初始化用混沌映射我选的是Logistic映射z(k1) μ * z(k) * (1 - z(k))μ设为4.0让初始种群在解空间里的分布更均匀。这个改动虽然简单但对上层电价策略的搜索帮助很大因为电价策略是多维变量纯随机初始化很容易大家挤在同一个价格区间前端多样性不够。第二个改进是加入精英保留策略。每次迭代后把当前最优个体复制一份直接保留到下一代不参与交叉和变异操作。鲸鱼算法的随机游走机制在后期会破坏好解加上这个策略后最优解不再丢失收敛曲线明显平稳很多。3.4 双层鲸鱼算法的整体迭代链路整个双层算法的执行流程我整理成下面这个逻辑外层鲸鱼种群随机初始化一组电价策略24维向量每维代表一个时段的价格。将种群中每个个体对应的电价策略下发到下层。每个用户在下层跑一个独立的鲸鱼算法求解自己在给定电价下的最优负荷策略此时其他用户的负荷策略保持上一轮的值不变。所有用户完成响应后汇总负荷曲线计算上层目标函数峰谷差或者电费成本。根据目标函数值更新外层鲸鱼种群位置。判断外层是否收敛或达到最大迭代次数否则返回第2步继续。这个“外层搜索电价、内层搜索用户响应”的双层嵌套结构实际运行下来计算量比较大。我用30个用户、外层30个鲸鱼个体、内层30个个体、外层迭代50次、内层迭代100次一台普通的i5笔记本跑完整个仿真大约需要15到20分钟。如果用户数超过100建议先对用户做聚类把同类型用户合并处理否则运行时间会膨胀到不可接受。4. Matlab代码实现与核心模块拆解4.1 代码整体结构与运行流程整个项目的Matlab工程按模块划分我贴一下我常用的目录结构主程序入口main.m负责加载参数、调用双层寻优、绘图输出结果。参数配置文件params_config.m集中设置所有用户数量、负荷参数、算法参数、电价约束等。外层鲸鱼算法outer_whale_algorithm.m输入是电价策略种群输出是最优电价策略和调度结果。内层用户响应求解inner_user_response.m输入是电价策略和用户参数输出是用户的负荷调整策略。目标函数计算objective_upper.m、objective_lower.m分别计算上层的峰谷差目标和下层的用户净收益。绘图脚本plot_results.m可视化电价曲线、调度前后负荷曲线、用户收益情况。main.m 的主流程逻辑%% main.m 主入口 clear; clc; close all; % 1. 加载参数 params params_config(); % 2. 初始化外层鲸鱼种群电价策略 prices_pop init_population(params.pop_size, params.prices_lb, params.prices_ub); % 3. 双层迭代主循环 for iter 1:params.max_iter_out % 将当前种群的电价策略下发到下层求解用户响应 [load_curves, user_utility] inner_user_response(prices_pop, params); % 计算上层目标函数值 fitness objective_upper(load_curves, params); % 更新外层鲸鱼种群 prices_pop update_whale_population(prices_pop, fitness, iter, params); % 记录最优解 [best_fitness(iter), best_idx] min(fitness); best_prices(iter, :) prices_pop(best_idx, :); end % 4. 输出调度结果与绘图 [optimal_load, user_report] inner_user_response(best_prices(end, :), params); plot_results(best_prices(end, :), optimal_load, user_report, params);这个流程看着简单实际操作时最需要注意的是内外层迭代次数的搭配比例。外层迭代一次内层要做一轮完整的响应求解所以内层的迭代次数决定了下层计算结果是否可信。内层精度不够外层拿到的目标函数值就是带噪声的整个算法找最优解的效率会变得很低。4.2 外层鲸鱼算法核心更新代码外层鲸鱼算法和标准鲸鱼算法的主要区别在适应度计算每个电价策略的适应度不是直接算出来的而是要先送到下层做用户响应把负荷曲线返回来之后才能算出峰谷差。下面是外层鲸鱼种群的更新代码function new_pop update_whale_population(pop, fitness, iter, params) N size(pop, 1); dim size(pop, 2); a 2 - 2 * iter / params.max_iter_out; [~, best_idx] min(fitness); leader_pos pop(best_idx, :); new_pop zeros(N, dim); for i 1:N 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 .* leader_pos - pop(i, :)); new_pop(i, :) leader_pos - A .* D; else % 随机搜索模式 rand_idx randi(N); X_rand pop(rand_idx, :); D_rand abs(C .* X_rand - pop(i, :)); new_pop(i, :) X_rand - A .* D_rand; end else % 螺旋气泡网攻击模式 D_star abs(leader_pos - pop(i, :)); l -1 2 * rand(); b 0.5; new_pop(i, :) D_star .* exp(b .* l) .* cos(2 * pi * l) leader_pos; end % 边界处理 new_pop(i, :) max(new_pop(i, :), params.prices_lb); new_pop(i, :) min(new_pop(i, :), params.prices_ub); end end这个代码有两个细节要提醒边界处理放在每次位置更新之后立刻做否则电价会出现负值或者超出合理范围导致下层用户响应计算直接报错另外C和A的取值覆盖范围很关键A的绝对值在迭代前期经常大于1这时走的是随机搜索模式后期A绝对值变小种群开始收缩到最优解附近这是算法正常的表现。4.3 内层用户非合作博弈响应求解内层响应求解模块是整个模型里逻辑最复杂的部分。每个用户面对给定的分时电价要最大化自己的净收益同时还要考虑其他用户的负荷水平对系统的影响所以每轮博弈迭代都需要更新所有用户的策略。function [load_curves, user_utility] inner_user_response(prices, params) N params.N_users; T params.T_hours; % 用户初始负荷策略 load_curves zeros(N, T); for i 1:N load_curves(i, :) params.base_load(i, :); end % 博弈迭代求解纳什均衡 alpha 0.4; % 阻尼更新系数 for iter_g 1:params.max_iter_g load_old load_curves; for u 1:N % 每个用户在当前电价下用鲸鱼算法求解最优负荷策略 x_opt inner_woa_single_user(u, prices, load_old, params); load_curves(u, :) (1 - alpha) * load_old(u, :) alpha * x_opt; end % 收敛判断 delta max(abs(load_curves(:) - load_old(:))); if delta params.conv_threshold break; end end % 计算用户净效用 user_utility zeros(N, 1); for u 1:N user_utility(u) objective_lower(u, load_curves(u, :), prices, params); end end阻尼系数alpha的取值很关键。一开始我也没加阻尼结果用户负荷曲线在两三轮博弈之间反复横跳收敛曲线像锯齿一样。后来加了阻尼虽然收敛速度慢了大概15%但迭代曲线平滑多了最终的均衡策略也更合理。建议alpha在0.3到0.5之间取值太小收敛太慢太大还是会震荡。4.4 用户收益函数与约束条件的代码化下层用户的收益函数代码实现function utility objective_lower(user_idx, load_schedule, prices, params) % 用电满意度收益二次效用函数 a params.user_comfort_a(user_idx); b params.user_comfort_b(user_idx); comfort_reward sum(a .* load_schedule - b .* load_schedule.^2); % 电费支出 energy_cost sum(prices .* load_schedule); % 可中断负荷补偿如果中断了需要给补偿从效用中扣除 interrupt_penalty params.interrupt_penalty(user_idx) * ... sum(max(0, params.nominal_load(user_idx, :) - load_schedule)); % 净收益 utility comfort_reward - energy_cost - interrupt_penalty; end注意约束条件的处理方式。可转移负荷的总用电量必须保持不变这个约束按下面的方式处理在每轮鲸鱼位置更新后做一个“电量校正”操作把总用电量偏差按比例分摊回各个时段确保总电量守恒。% 电量守恒修正 total_energy_target params.total_energy_target(user_idx); scale total_energy_target / sum(load_schedule); load_schedule load_schedule * scale; load_schedule max(load_schedule, params.load_min(user_idx)); load_schedule min(load_schedule, params.load_max(user_idx));这种“先缩放再截断”的方法有一个bug如果某些时段已经到达边界缩放后再截断会导致总电量又不守恒了需要二次修正。我的做法是先把越界的时段固定下来只对未越界的时段做等比缩放。代码量会多一点但结果是严格满足约束的。4.5 参数配置参考表下面是我一组实测能用的参数配置直接抄过去就能跑通模型参数名称数值说明用户数量 N30居民用户总数调度时段 T2424小时逐时调度刚性负荷占比40%不可调节负荷可转移负荷占比35%可平移时段负荷可中断负荷占比25%可削减功率负荷电价下限0.3元/kWh电价上限1.5元/kWh外层鲸鱼种群数30上层搜索电价的种群规模外层迭代次数50上层最大迭代次数内层鲸鱼种群数30单个用户求解的种群规模内层迭代次数100用户响应求解最大迭代次数博弈迭代次数30用户之间纳什均衡求解迭代次数阻尼系数 alpha0.4策略更新阻尼满意度系数 a_i2.0~3.0用户舒适偏好参数满意度系数 b_i0.05~0.1边际效用递减参数这套参数跑出来的结果比较理想峰谷差能降低25%到30%用户平均电费节省8%到12%。当然不同场景不同参数下结果会有差异但总体趋势是一致的价格信号有效引导了用户负荷从峰时段向谷时段转移用户也因为用了更便宜的电而获得经济收益。5. 仿真结果与关键指标分析5.1 典型场景设置与调度效果我常用这样一个仿真场景来验证模型效果夏季典型日30个居民用户空调负荷占比高晚间18点到22点是负荷高峰凌晨2点到6点是低谷。分时电价和居民负荷互动后结果非常典型。调度前的基础负荷曲线峰时最高负荷接近250kW谷时只有95kW左右峰谷差达到155kW。调度后的结果峰时最高负荷降到184kW谷时负荷提升到128kW峰谷差收窄到56kW降幅接近64%。这个数据是不是好看得有点夸张实际情况确实能做到因为空调负荷的可中断特性很强加上电动汽车充电负荷的转移空间大在电价激励下用户的削峰填谷潜力远超预期。但也要注意如果只盯着峰谷差指标优化可能会把负荷曲线压得太平反而让用户舒适度大幅下降所以我在上层目标函数里加入了一个“用户效用惩罚项”避免为了削峰而削峰。5.2 纳什均衡的收敛性验证怎么判断系统确实到达了纳什均衡我用了两个指标第一个是用户单方面改变策略的收益增益均衡状态下这个值应该接近零第二个是相邻两轮博弈的策略变化量这个值应该递减到收敛阈值以下。实测跑下来用户数量30的时候大概在15到20轮博弈迭代后策略变化量趋于稳定。前几轮变化量很大策略在剧烈调整阶段中期开始收敛后期基本平稳。如果用户数量增加到100个收敛需要30到40轮而且阻尼系数还要调高一点。这里有一个比较隐蔽的问题即使外层算法已经收敛内层用户博弈也未必真的到达了严格纳什均衡可能只是“近似均衡”或者“弱均衡”。做项目的时候不用过于纠结严格均衡工程上只要满足“没有用户能在不损害他人利益的情况下单方面获得超过1%的收益增益”就可以认为结果可用了。5.3 双层鲸鱼算法与其他算法的效果对比同样是这个模型我试过用标准鲸鱼算法、粒子群算法和遗传算法去求解结果对比如下算法收敛所需迭代次数最终峰谷差/kW用户平均电费降低比例标准鲸鱼算法38727.2%粒子群算法46816.1%遗传算法52855.4%双层鲸鱼算法改进27569.3%双层鲸鱼算法在收敛速度和解质量两个维度上都有明显优势。改进后的算法比标准版本快了大约11次迭代峰谷差也小了16kW这主要归功于混沌初始化和精英保留策略。粒子群在这个模型上表现一般原因是用户响应函数的非线性较强粒子群容易早熟陷入局部最优。6. 常见问题与调试经验6.1 双层迭代不收敛怎么排查这是做双层优化最容易遇到的问题。外层迭代了50次目标函数还在上下波动没有下降趋势。我排查这个问题的经验是先隔离内层问题别急着调外层算法。具体做法是把内层用户响应用一个固定策略替代比如直接把可转移负荷全部平移到谷时段然后看外层能不能正常收敛。如果外层还是不收敛问题出在外层搜索机制上检查爬虫边界处理和A值的衰减速度如果外层能收敛那就确认内层响应求解有噪声或者精度不够需要提高内层迭代次数、调整博弈阻尼系数。还有一种情况是内层每个用户的鲸鱼算法没有充分收敛返回的负荷策略带有随机性导致上层看到的同一个电价策略对应的目标函数值每次都不一样。这种情况下外层算法会在错误的方向上更新种群位置。解决办法就是提高内层鲸鱼算法的迭代次数到150以上或者把内层改成确定性较强的梯度搜索方法。6.2 用户负荷曲线出现剧烈波动有一次跑仿真发现某个用户的可转移负荷在相邻两个时段之间疯狂跳变一会儿全在23点一会儿全在1点。查了半天问题是电价在这两个时段非常接近用户对在高价稳态下的最优响应本来就“无所谓”加上鲸鱼算法随机性大每次找到的最优解在物理上等价但时段分布完全不同。这个问题的本质是问题的“不严格凸性”——最优解不唯一。我最后的处理方式是在用户的收益函数里加一个小幅度的“用电习惯保持项”对偏离历史习惯的时段施加惩罚。这样既保留了电价引导能力又不会让用户负荷曲线出现反直觉的大幅度跳变。6.3 Matlab运行效率优化实战双层嵌套的鲸鱼算法跑起来确实慢尤其是用户数量大的时候。我做过三个优化效果立竿见影第一向量化内层循环。尽量把对单个用户的操作改成矩阵运算尤其是负荷曲线的边界处理用向量操作一次处理所有用户比for循环快3到5倍。第二把用户参数矩阵化和预分配。Matlab里在循环内动态增长数组会触发频繁的内存重分配我在初始化时就把所有用户的参数一次性地载入为N×T的矩阵后续操作不再涉及动态分配。第三限制绘图操作。调试阶段每轮迭代都画图的话Matlab的绘图开销会占掉大量时间。我把绘图函数从迭代循环里拿出来只在最终结果输出时调用一次运行时间直接从20分钟降到不到12分钟。6.4 代码移植与工具箱兼容性Matlab的版本兼容性也是个实际坑。我在R2023a环境下运行良好的代码换到R2021b就报错主要问题集中在部分内置函数对新语法支持不完整。建议在代码开头统一设置兼容性参数尽量用基础函数而非特定工具箱函数。如果是用优化工具箱里的fmincon做内层快速求解需要确认目标用户的Matlab版本支持相关调用。这个项目我写代码时尽量少依赖工具箱只用Matlab基础数学函数这样哪怕换到Octave环境也能跑通大部分功能。文件读写和绘图部分另说了核心算法部分是可以跨平台复用的。我在实际做这套模型的过程中最深刻的体会是双层的调度模型里算法部分真的不是最难的难的是把下层用户的“行为逻辑”建模到位。很多项目拿到手就急着写算法、调参数结果模型跑出来的结果既不符合物理实际也不符合用户心理预期——问题往往出在下层用户收益函数没有刻画好。先把博弈模型调得足够合理再考虑算法优化顺序别搞反了。另外如果要把这个模型往工程方向扩展可以尝试把上下层的迭代频率错开上层电价每15分钟更新一次下层用户每小时响应一次这样更贴近实际电力市场的运行节奏。