
晚上七点小区里的负荷曲线开始陡峭爬升空调、热水器、洗衣机、电动车充电桩几乎同时运转。每一户人家都觉得自己在合理用电——下班了当然要开空调吃完饭当然要洗衣服电价白天晚上差不了太多何必委屈自己。可电网侧看到的是另一番景象峰谷差越拉越大变压器接近满载备用容量被逐步吃干。这种每个家庭都理性、整体却低效的局面正是非合作博弈最典型的应用场景。我做了一套基于非合作博弈的居民负荷分层调度模型用双层鲸鱼算法在Matlab里完成了求解把电网定电价、用户响应用电这个动态博弈过程完整仿真了出来。这篇东西会把模型的数学化过程、双层算法的求解逻辑、Matlab代码的骨架和关键片段都摊开讲也会把我在实际调参过程中踩过的坑一并交代。适合正在做需求响应、家庭能量管理或者智能用电方向课题的同学参考尤其是那些想复现博弈启发式算法路线又不知道从哪下手的。先说结论这套框架跑通之后模拟场景下的峰谷差能压掉将近一半用户的平均电费也有小幅下降而且整个过程不需要集中控制器去命令任何一台家电全靠电价信号引导。我认为这个思路的价值不在于某个具体参数有多好看而在于它把一个带冲突的多主体决策问题用博弈模型和双层优化结构拆得比较干净。1. 为什么居民负荷调度会变成一场博弈1.1 个体理性与全局最优的天然矛盾随便找一个小区晚高峰的负荷构成来看照明和冰箱这类刚性负荷占比不大真正把曲线推上去的是空调、热水器、洗衣机和电动车充电。这几类负荷有个共同特点——时间上存在弹性。空调可以提前半小时开热水器可以错峰加热洗衣机能延后到夜间电动车更不用说充六个小时和充两个小时在用车体验上几乎没有差别。既然有弹性就存在调度空间。问题在于谁来调如果电网侧有一个中央大脑直接控制每户的每台设备理论上可以把所有弹性负荷平移到低谷时段全局最优方案很容易算。但现实中没有任何一个用户会接受自家的洗衣机半夜被远方某个系统远程启动这涉及用户自主权、隐私和契约边界。更现实的做法是电网定一个随时间变化的电价用户看到电价之后自己决定什么时候用电。这其实就是博弈——电网希望削峰填谷用户希望省钱双方都在追求自己的利益。这种各有各的小算盘、谁也不听谁的的局面就是非合作博弈。注意非合作不等于对抗而是指参与方之间不存在有约束力的合作协议每个玩家只对自己的目标负责。电价就是玩家之间的交互媒介电网改变电价用户改变用电行为用户的用电行为汇总之后又反过来影响电网的调度效果。1.2 分层结构从哪里来为什么这个问题必须用分层模型而不是一个单层优化我一开始也犯过懒想直接把所有用户的负荷决策和一个全局目标塞进一个大优化问题里。后来发现两个问题。第一物理结构本身就是分层的。电网公司/聚合商在上层面对整个区域的总负荷负责用户在下层面对自己家里的电费负责。上层看不到每台微波炉的开关状态下层也左右不了电网的容量规划。这种信息结构和决策权力天然是Leader-Follower关系也就是Stackelberg博弈上层先出牌定电价下层看到牌后再行动排负荷上层还要预判下层的反应来优化自己的策略。第二单层集中优化在数学上可以算但在语义上站不住。它要求所有用户的目标统一成一个目标这等于假设用户愿意牺牲自己的利益去成全电网的削峰。现实中用户只想省钱如果削峰要求我大白天开空调、晚上忍着热我凭什么配合所以模型必须把用户自己的成本函数放在下层让用户在给定的电价下自行决策电网只能通过价格间接引导。1.3 非合作博弈建模的三要素把这个问题形式化为非合作博弈需要明确三个基本要素。玩家集合这里有两层玩家——上层是电网调度端或者区域聚合商下层是N个居民用户。下层用户之间也构成博弈关系因为大家共享同一个电价信号和同一个总负荷约束如果存在配变容量上限某户多用电就会挤压其他用户的可用空间。不过在大多数居民负荷场景里用户之间的耦合主要靠电价间接实现不一定需要显式的容量约束。策略空间电网的策略是24小时的电价曲线或者峰平谷三段电价每个用户的策略是自己各类柔性负荷的启停时间与功率曲线。策略空间的维度差别很大电网有一条24维的价格向量需要决策每个用户则是一条更长的负荷调度向量。收益函数电网的收益或者说目标通常是削峰填谷——最小化总负荷的方差或者峰谷差用户的收益是负的电费支出即尽量少花钱同时还要兼顾舒适度所以收益函数里通常要加一个舒适度惩罚项。这三要素一旦明确整个模型的骨架就定了。后面要做的就是把这套博弈关系写成可求解的数学优化问题并且找到合适的算法去逼近均衡解。2. 分层调度模型的数学化过程2.1 上层目标电网侧的削峰填谷函数上层模型的决策变量是电价向量 p [p₁, p₂, ..., p_T]T 取24的话就是24个时段的实时电价。实际工程里不会让电价随意乱跳所以一般加两个约束每个时段的电价在合理区间内比如 0.3 元/kWh 到 0.8 元/kWh 之间全天平均电价保持在一个基准值附近比如 0.55 元/kWh这是为了保证电网没有通过涨价变相多收钱让博弈在公平尺度上进行。上层目标写成一个典型的最小化负荷方差形式min f(p) (1/T) Σ_t (P_total(t) - P_avg)²其中 P_total(t) Σ_k P_k(t)即所有用户在第 t 个时段的总负荷P_avg 是全天平均负荷。最小化方差等价于让负荷曲线尽量平既能降低峰谷差也能提高设备利用率。也有人直接用 max(P_total) - min(P_total) 作为目标两种写法差别不大方差形式更平滑对优化算法更友好。这里有个关键点P_total(t) 不是上层直接能决定的它是下层所有用户在上层给定电价 p 之后各自优化出来的总结果。所以上层目标函数的每一次评估都必须先解决一次下层问题。这就是双层优化的嵌套本质。2.2 下层模型用户侧的电费最小化问题下层模型里每个用户 k 在给定电价向量 p 之后求解自己的最优用电策略 x_k。用户的目标是最小化自己的电费支出加上舒适度损失惩罚min C_k(x_k, p) Σ_t p(t)·P_k(t) λ·ComfortPenalty_k其中 P_k(t) 是用户 k 在第 t 时段的总用电功率它由刚性负荷、可转移负荷、可中断负荷空调和储能设备的功率叠加而成。ComfortPenalty_k 用来约束空调这类设备——如果为了省钱把空调关太久室内温度偏离设定值就需要付出惩罚。λ 是用户对舒适度的敏感系数不同用户可以设定不同的 λ体现用户的差异化偏好。这个目标函数看起来简单真正的复杂度全在约束条件里。居民负荷不是任意功率都行每类设备都有自己的物理限制具体见下表。负荷类型典型案例调度自由度核心约束刚性负荷照明、电视、冰箱不可调度必须实时满足可转移负荷洗衣机、洗碗机运行时段可平移总耗电量固定、连续运行时长固定、有允许运行窗口可中断负荷空调、电热水器功率可调、可短时关停室内温度保持在舒适区间储能型负荷电动车、家庭电池充放电方向与功率可调SOC上下限、充放电功率限制这些约束加在一起下层问题是一个非线性、非凸、混合整数型的优化问题。尤其是空调的室内温度动态方程会引入状态变量和连续时间的耦合这决定了下层问题不能简单地用线性规划直接处理也为我选择鲸鱼算法提供了充足理由。2.3 用户之间的耦合要不要显式建模我在初版模型里没有加入用户之间的直接耦合每个用户在给定电价下的优化是完全独立的。这样做的好处是下层问题被拆成 N 个互不干涉的子问题可以逐个求解也可以并行求解。代价是非合作博弈的味道淡了一些——用户之间没有真正的策略交互。如果想让博弈更实可以考虑加一个共享的配电容量约束Σ_k P_k(t) ≤ P_cap(t)。这样一来某用户把负荷移到晚高峰就会挤占其他用户的可用容量用户之间就形成了真正的策略依赖。这时候下层不能再逐个独立求解需要采用迭代最优响应Iterative Best Response的思路每个用户基于其他用户上一轮的负荷曲线求解自己的最优策略然后反复迭代直到所有用户都不再改变策略即达到纳什均衡。考虑到标题里强调的是分层调度模型而不是用户间竞争细节我最终采用的是上层定电价、下层独立响应的Stackelberg博弈框架但在代码里预留了容量约束的接口。如果你需要把用户间竞争也装进去可以在下层加一个循环迭代改动量并不大。3. 双层鲸鱼算法是如何一步步逼近均衡解的3.1 标准鲸鱼算法的三个搜索机制在讲什么先回顾一下标准鲸鱼优化算法WOA的核心。这个算法模拟座头鲸的捕食行为主要包含三个机制包围猎物、气泡网攻击、随机搜索猎物。包围猎物阶段的更新公式是X(t1) X*(t) - A·|C·X*(t) - X(t)|其中 X* 表示当前最优解A 和 C 是由收敛因子 a 生成的系数向量A 2a·r₁ - aC 2r₂a 在迭代过程中从2线性递减到0。这个机制让种群向当前最优个体收缩相当于局部搜索。气泡网攻击则体现了鲸鱼绕着猎物做螺旋上升的轨迹更新公式是X(t1) D·e^(bl)·cos(2πl) X*(t)其中 D |X*(t) - X(t)|b 是螺旋形状常数l 是 [-1,1] 之间的随机数。算法用一个随机概率 p 来决定当前个体走收缩包围还是螺旋更新两者都以当前最优解为靶心属于开发Exploitation阶段。随机搜索则在 |A| ≥ 1 时触发此时个体不再参考最优解而是随机选一个种群中的个体作为中心进行更新用于保持全局探索能力避免陷入局部最优。WOA相对粒子群和遗传算法的优势在于参数少主要就是种群规模和迭代次数不需要调惯性权重、交叉变异概率这些额外超参数实现起来很简单螺旋更新机制让它在处理多峰问题上表现不错。对于我这种需要反复嵌套调用的场景参数越少越好。3.2 双层求解框架外层搜电价内层搜负荷把WOA装进双层模型里思路非常直接。外层WOA的每个个体是一条电价曲线内层WOA的每个个体是一个用户的负荷调度方案。整体流程可以概括为外层种群初始化随机生成 M 条满足电价上下限和均值约束的价格曲线。外层评估对每条价格曲线依次调用内层求解——对每个用户分别运行一次内层WOA得到该用户在当前电价下的最优负荷曲线汇总成总负荷。上层适应度计算用总负荷计算负荷方差作为外层个体的适应度。外层更新根据WOA的包围、螺旋、随机搜索机制更新电价种群进入下一轮。结束判断达到最大迭代次数或者最优适应度连续若干轮变化小于阈值。这个过程的内层其实包含了 N 次独立的WOA求解N为用户数。如果用户数量大这个嵌套计算量会非常可观。我在代码里把内层WOA写成了一个独立函数每次调用针对一个用户这保证了结构清晰也方便后续用parfor并行化。3.3 收敛性判断与Stackelberg均衡的关系双层迭代结束后我们得到的解对应的博弈含义是什么外层最优电价 p* 加上所有用户在 p* 下的最优反应 x*(p*)构成了一个Stackelberg均衡在 p* 下任何用户单独改变自己的负荷策略都不可能降低自己的电费同时电网方面在预见到用户会按最优反应行事的前提下任何其他电价策略都不可能比 p* 产生更小的负荷方差。严格来说启发式算法不能像数学规划那样证明收敛到一个全局均衡点但可以通过三个手段提高可信度多次独立运行取最好结果、观察收敛曲线是否趋于平稳、以及用随机生成的多个初值做交叉验证。我在项目里就是这么处理的不追求理论上的全局最优证明而是用工程手段保证结果稳定可复现。4. Matlab代码实现从主流程到核心函数4.1 代码文件结构与主程序骨架整个Matlab工程的模块划分如下main.m主程序负责参数设置、数据生成、双层求解流程控制、结果绘图。WOA_upper.m外层鲸鱼算法求解最优电价曲线。WOA_lower.m内层鲸鱼算法求解单个用户在给定电价下的最优负荷。obj_upper.m上层目标函数输入电价输出负荷方差。obj_lower.m下层目标函数输入用户负荷方案输出电费与舒适度惩罚。load_data.m生成模拟的居民负荷参数刚性负荷曲线、设备工作参数等。repair.m对越界的负荷方案做修复保证约束可行。主程序的骨架是这样的%% 主程序基于非合作博弈的居民负荷分层调度 clc; clear; close all; rng(42); % 固定随机种子保证结果可复现 % 基础参数 T 24; % 调度时段数按小时 N_user 10; % 居民用户数量 max_iter_outer 100; % 外层WOA迭代次数 pop_outer 30; % 外层种群规模 max_iter_inner 80; % 内层WOA迭代次数 pop_inner 20; % 内层种群规模 % 电价约束 price_min 0.3; % 电价下限元/kWh price_max 0.8; % 电价上限 price_ref 0.55; % 全天平均电价约束值 % 生成居民负荷数据 users load_data(N_user, T); % 外层WOA求解 [best_price, best_fit] WOA_upper(users, T, price_min, price_max, ... price_ref, max_iter_outer, pop_outer, ... max_iter_inner, pop_inner); % 结果输出与绘图 plot_price(best_price); plot_load_curve(users, best_price);主程序的逻辑非常直白设参数、生成数据、调用双层算法、绘图。实际调试时建议把随机种子固定下来否则每次运行结果不同很难判断是算法问题还是随机性造成的波动。4.2 外层鲸鱼算法的Matlab实现片段外层WOA负责搜索电价曲线。每条鲸鱼的位置是一个 T×1 的向量初始化时保证均值约束满足——我用的方法是先随机生成在上下限内的向量再整体平移使得均值等于 price_ref。核心更新部分如下function [best_price, best_fit] WOA_upper(users, T, price_min, price_max, ... price_ref, max_iter, pop, ... inner_iter, inner_pop) % 初始化种群每条鲸鱼是一条电价曲线 X price_min (price_max - price_min) * rand(pop, T); % 修正均值约束整体平移到基准电价 for i 1:pop X(i,:) X(i,:) - mean(X(i,:)) price_ref; X(i,:) max(price_min, min(price_max, X(i,:))); end % 初始评估 fitness zeros(pop, 1); for i 1:pop fitness(i) obj_upper(X(i,:), users, inner_iter, inner_pop); end [best_fit, idx] min(fitness); best_price X(idx,:); % 主迭代 for t 1:max_iter a 2 - t * (2 / max_iter); % 线性递减收敛因子 for i 1:pop r1 rand(); r2 rand(); A 2 * a * r1 - a; C 2 * r2; p_rand rand(); % 螺旋/包围选择概率 if p_rand 0.5 if abs(A) 1 % 收缩包围向当前最优电价靠近 D abs(C .* best_price - X(i,:)); new_price best_price - A .* D; else % 随机搜索以随机个体为中心 rand_idx randi([1, pop]); D abs(C .* X(rand_idx,:) - X(i,:)); new_price X(rand_idx,:) - A .* D; end else % 螺旋气泡网更新 D abs(best_price - X(i,:)); l 2 * rand() - 1; b 1; new_price D .* exp(b * l) .* cos(2 * pi * l) best_price; end % 边界与均值约束修复 new_price max(price_min, min(price_max, new_price)); new_price new_price - mean(new_price) price_ref; new_price max(price_min, min(price_max, new_price)); % 评估新位置 new_fit obj_upper(new_price, users, inner_iter, inner_pop); if new_fit fitness(i) X(i,:) new_price; fitness(i) new_fit; end % 更新全局最优 if new_fit best_fit best_fit new_fit; best_price new_price; end end end end两个细节值得说。第一均值约束的修复是先平移再截断截断之后均值会偏所以又做了一次平移这个顺序不能反否则容易出现大量个体被截断在边界上导致种群多样性丢失。第二我只在新位置适应度更好的情况下才接受更新相当于一个贪心门控实际效果比无条件接收更稳定。4.3 下层用户响应的求解与数据传递下层目标函数 obj_upper 里调用内层WOA关键代码是这样的function fval obj_upper(price, users, inner_iter, inner_pop) T length(price); N_user length(users); total_load zeros(T, 1); % 对每个用户独立求解最优负荷 for k 1:N_user schedule_k WOA_lower(price, users(k), inner_iter, inner_pop); total_load total_load schedule_k; end % 上层目标负荷方差最小化 avg_load mean(total_load); fval sum((total_load - avg_load).^2) / T; end内层WOA_lower返回的是用户k在各种设备上的功率叠加曲线。设备的功率建模放在load_data.m里每个用户结构体包含刚性负荷基准曲线、洗衣机/洗碗机的额定功率和可用时间窗、空调的热动态参数、电动车的充电需求等。内层WOA的编码方式我把所有可调度设备的逐时功率拼接成一个长向量每一条鲸鱼就是这个向量。内层目标函数如下function cost obj_lower(schedule, price, user) % 电费支出 elec_cost price(:) * schedule(:); % 舒适度惩罚空调温度偏离设定值的惩罚 comfort user.lambda * sum(abs(user.temp_room - user.temp_set)); cost elec_cost comfort; end这里有个常见的建模陷阱直接把空调功率作为决策变量室内温度是功率的积分结果需要通过热动态方程逐时递推算出然后才能计算舒适度惩罚。如果省略温度递推只看功率本身空调的舒适度约束就完全失效了优化算法会把空调压到零功率用户舒适度也没法评估。我初版代码就栽在这个地方后来老老实实把一阶热动态方程写进去才算对了。4.4 参数配置与随机种子的工程细节双层嵌套算法的计算开销需要认真估算。外层 pop_outer30 个个体、迭代100轮内层 N_user10 个用户、每个用户跑 pop_inner20 个个体、迭代80轮总的目标函数评估次数大约是 100 × 30 × 10 × 20 × 80 4800万次。这个量级在Matlab里跑起来非常酸爽单线程可能要几个小时。我的应对策略有三个。第一内层迭代次数不是全程固定前期用60次快速估算、后期用100次精确评估类似粗算细算结合。第二所有目标函数和负荷递推都用向量化写法避免在循环里逐时内层嵌套尽量把24个时段的运算写成矩阵运算。第三如果机器有多核把内层用户循环改成parfor这一步在我这从三个多小时缩短到四十多分钟效果立竿见影。随机种子同样重要。调试阶段必须用rng(42)这类固定种子否则每次跑完数字都不一样没法判断一个改动到底是变好了还是随机噪音。等算法调通了再放开随机性做多次独立实验统计结果。5. 仿真结果与调参避坑实录5.1 收敛曲线异常下降缓慢的排查第一次跑通全流程时我盯着外层收敛曲线看了很久——最优适应度在迭代到一半的时候就开始平台期下降得非常慢。我最初怀疑是外层WOA的搜索能力不够把种群规模和迭代次数都加大结果变化微乎其微。后来排查发现问题出在内层求解精度上。外层迭代前期电价还在大范围跳动内层WOA只给80次迭代解的精度也不高导致上层评估同一个电价时前后两次算出来的总负荷可能差不少。这种带噪声的适应度对任何启发式算法都是致命伤——种群分不清到底是自己飞得更好还是噪声造成的假象。解决方法是内层迭代次数随着外层进程逐步增加。前期内层跑50次后期提到100次让上层在临近收敛时评估得越来越准。另外每次外层评估同一个个体时我用固定随机种子保证同一电价被评估多次的结果完全一致消除了随机性带来的评估抖动。5.2 电价初值对最终方案的敏感性双层WOA对电价初值有一定敏感性至少在我的模拟场景里是这样。如果电价初始种群全都集中在平段附近最终收敛结果往往是一个变化比较温和的TOU方案削峰效果中等如果初始种群包含了比较极端的峰谷电价组合就容易搜出价格差更大、削峰更狠的方案。这不是坏事但也提醒我不要指望算法从一个随随便便的初值就能稳定找到全局最优。我的做法是先用峰谷平三段固定电价跑一轮把结果作为外层初始种群的一个个体塞进去再混合随机个体。这样既保留了启发式搜索的全局性又给种群提供了一个有经验的老兵作为引导收敛速度和最终质量都有改善。5.3 约束处理的取舍惩罚还是修复对刚性约束的处理我在项目里混合使用了惩罚和修复两种手段。对于空调温度这类软约束用惩罚项效果好——温度稍微越界一点不是灾难用λ系数调节既可。但对于洗衣机的连续运行时长、电动车的最终SOC这类硬约束光靠惩罚不够因为惩罚项权重设大了会把目标函数变得很崎岖设小了又会出现实际不可行的解。硬约束我用的是修复策略内层WOA每次生成新个体后调用repair.m把不满足连续运行约束的设备序列做局部调整把SOC末端值修正到目标区间。修复之后再评估适应度。这样做的收敛速度比纯惩罚快很多最后得到的调度方案也基本不存在看起来省电但根本没法执行的问题。5.4 模拟场景下的削峰填谷效果在10个用户、24时段、含空调/洗衣机/洗碗机/电动车的模拟场景下我统计了调度前后的典型指标。注意这些是模拟数据只用于说明模型表现不代表任何真实小区。指标调度前调度后变化幅度峰值负荷/kW286.4231.7-19.1%谷值负荷/kW118.2152.629.1%峰谷差/kW168.279.1-53.0%负荷率0.580.7224.1%用户平均电费/元32.529.8-8.3%负荷曲线从晚高峰陡峭尖塔变成双峰变矮、谷底抬升的形态电动车充电基本都挪到了凌晨谷段洗衣机绝大多数安排在上午和深夜空调在保证舒适度的前提下做了预热和间歇运行。用户平均电费下降了8.3%说明博弈不是零和——电价优化让电网削了峰用户也省了钱这是一个帕累托改进的方向。每个用户的电费变化并不均匀敏感系数λ小的用户更在乎钱节省更多λ大的用户更在乎舒适几乎没省多少。这个差异恰恰说明非合作博弈模型的合理性不同用户按自己的偏好做出不同选择系统统一调度无法体现这种个性化。最后再说一个我实际体会最深的事情这种双层博弈模型最花时间的不是写外层算法而是把下层设备的物理约束建模准确。空调的热动态、电动车的SOC递推、洗衣机的连续运行窗口任何一处建模粗糙都会让上下层的迭代结果看起来漂亮、实际执行时一塌糊涂。先把下层模型掰扯清楚再谈算法改进顺序千万别反。这样一套框架跑下来后面如果再遇到类似的多主体分层决策问题比如园区微网里运营商与用户的互动定价、电动汽车聚合商与车主的充电博弈基本上换一套约束和目标函数就能复用我觉得这才是这套工作最有价值的地方。