
去年年底我接到一个居民用电调度的小项目甲方给的数据乱七八糟但要求却很明确在保证住户舒适度的前提下尽量削峰填谷还得让不同用户之间“公平”地分摊调峰压力。那时候我手头正好在研究鲸鱼算法顺手就把“双层鲸鱼算法 非合作博弈”这套组合套了上去搞出了一个居民负荷分层调度模型。这篇博文就把这个模型的思路、Matlab实现和踩坑过程完整记录下来适合正在做电力系统优化调度、智能算法应用或者研究博弈论与需求侧响应的朋友参考尤其是那些想把智能算法真正跑通、而不是停留在理论推导阶段的人。先说结论这套模型解决的核心问题是“多个居民用户各自追求电费最小化但同时又受制于电网总负荷约束”的矛盾而双层鲸鱼算法恰好能处理这种“上下层决策互相影响”的嵌套优化结构。上层负责制定分时电价或激励系数下层各用户根据电价调整自己的用电计划整个迭代过程就是一个非合作博弈的寻优过程。最终的效果是总负荷峰值下降了约17%用户平均电费节省了12%左右最关键的是各用户之间的“满意度损失”差距被控制在很小的范围内避免了“牺牲少数人成全所有人”的尴尬。1. 整体设计思路为什么非合作博弈和分层调度天生一对1.1 居民负荷调度为什么需要“分层”居民用电负荷跟工厂、商业楼宇完全不同它的特点是单体容量小、数量庞大、随机性强。如果直接让电网调度中心去控制每个住户的空调、热水器、电动汽车充电桩那计算量是天文数字而且涉及用户隐私和舒适度问题现实中根本行不通。所以必须采用“分层”的思想上层是电网或售电公司下层是千家万户。上层不直接命令某个用户“你必须在晚上8点关掉空调”而是通过价格信号或激励补贴引导用户自己调整用电行为。用户呢也不是被动听话而是基于自己的用电需求和舒适度偏好在电价刺激下做出对自己最有利的选择。这种架构天然就是一个Stackelberg博弈上层是领导者下层是跟随者。领导者先出价跟随者根据出价优化自己的用电计划领导者再根据所有跟随者的反应调整出价直到收敛。注意这和“主从优化”是同一个逻辑只不过用了博弈论的框架来解释。1.2 非合作博弈在这个模型里扮演什么角色很多初学者一听“博弈”就以为是合作协商其实这里用的是非合作博弈意思是每个用户只关心自己的成本最小化不跟别人拉帮结派也不会主动牺牲自己的利益去成全整体。但妙就妙在虽然每个用户都是“自私”的但通过价格这个公共信号他们之间隐含着竞争关系用电高峰时段电价高谁把负荷挪到低谷谁就省钱于是大家会自发地错开用电高峰。这种“自私行为的宏观收敛”就是纳什均衡。在我的模型里下层每户人家都是一个独立的决策者他们在收到电价信号后用优化算法求解自己的最优用电计划。所有用户的计划汇总后形成新的总负荷曲线电网再据此调整下一轮的分时电价。来回迭代直到相邻两轮的价格变化小于某个阈值我们就说博弈达到纳什均衡了。这时候没有任何一个用户愿意单独改变自己的用电计划因为改了反而会增加自己的电费或者降低舒适度。1.3 双层鲸鱼算法的定位谁的优化问题用谁去解这里有个容易混淆的地方双层优化问题中有两个优化问题——上层的电价优化和下层的用电量优化它们不是上下级关系吗为什么叫“双层鲸鱼算法”我的做法是外层用鲸鱼算法搜索电价策略内层也用鲸鱼算法求解每个用户的负荷分配。也就是说鲸鱼算法同时在上下层工作只不过作用对象不同。当然内层也可以换成粒子群、遗传算法或者直接用二次规划求解器但为了保持算法风格统一、方便后续对比我全用鲸鱼算法。上层鲸鱼种群代表一组候选电价向量下层鲸鱼种群代表某个用户在所有时段的用电功率向量。每一次外层鲸鱼迭代都需要调用下层优化来评估该电价下用户的最佳反应从而算出上层的适应度值。这种嵌套结构计算量很大但逻辑上非常清晰。2. 核心模型与环节拆解从数学公式到代码映射2.1 居民负荷的类别划分与可调特性在建模之前我必须先把手里的负荷数据分好类。居民负荷大致分三类刚性负荷、可平移负荷、可削减负荷。刚性负荷就是照明、冰箱这类人在家就必须得开不可调度可平移负荷包括洗衣机、洗碗机、电动汽车充电它们可以在一定时间窗口内整体挪动比如洗碗机从晚上8点挪到深夜12点体验损失不大可削减负荷主要是空调、热水器这类负荷可以在某个时段降低功率运行比如空调本来开2kW高峰期降到1.5kW代价是温度略高一点用户舒适度有轻微损失。我的模型里每户配置是这样的日报刚性负荷曲线24个点可平移负荷2个设备洗衣机和电动汽车可削减负荷1台空调。可平移负荷有开始时间区间、持续时长、额定功率可削减负荷有最大最小功率限制以及舒适度惩罚系数。所有负荷数据都来自公开的居民用电数据集做了一定归一化处理。2.2 用户的成本函数电费、舒适度、转移意愿单个用户i在给定分时电价p(t)后他的优化目标是让总成本最小化。总成本包括三块用电费用、可平移负荷的舒适度惩罚、可削减负荷的舒适度惩罚。用电费用就是每个时刻的购电功率乘以该时刻电价再求和。可平移负荷的舒适度惩罚怎么算呢用户原本希望洗衣机在晚上8点开始你给挪到凌晨3点虽然电费便宜了但用户觉得别扭这个“别扭”要量化为成本。我用的函数是偏离期望开始时间的绝对值乘上一个惩罚系数α。可削减负荷的惩罚则是功率削减量的二次函数因为削减越多温度变化越大用户的难受程度是加速上升的。数学形式不展开但代码里就是把这些项加一起作为内层优化的目标函数。2.3 上层电网的目标削峰填谷与总负荷追踪上层售电公司制定分时电价目标有两个一是让总负荷曲线尽量平坦也就是峰值最小化、峰谷差最小化二是保证总用电量上下浮动不能太大免得发电侧安排不过来。我把上层目标函数设计为总负荷的方差最小化加上一个电价波动惩罚项防止电价忽高忽低导致用户无所适从。这个目标函数在外层鲸鱼算法中作为适应度值。需要注意的是上层不能直接命令下层它只能改变电价向量。每个电价向量都会导致下层用户重新优化一次用电计划从而得到一个新的总负荷曲线。所以外层鲸鱼每一次位置更新都要把所有下层用户的内层优化跑一遍相当于一个嵌套仿真。2.4 约束条件功率平衡、设备运行、价格边界约束条件是最容易翻车的地方。我整理了四大类约束。第一类是功率平衡约束每户每个时刻的用电功率等于刚性、可平移、可削减负荷之和总负荷是各户之和。第二类是设备运行约束可平移负荷一旦开始就必须连续运行到结束且只能在其允许开始时间区间内启动可削减负荷的功率必须在其最小和最大功率之间。第三类是用户侧约束各用户的总用电量在调度前后基本不变因为我们只是移峰填谷不是让用户省电。第四类是价格约束分时电价不能超过电网给定的上限和下限否则用户可能极端响应。在代码实现时这些约束有的写进目标函数作惩罚项有的通过编码方式内置到解的结构中目的就是让鲸鱼算法产生的解尽可能满足约束减少不可行解带来的无效计算。3. Matlab代码实现结构、关键函数与运行方式3.1 代码总览模块划分与文件组织我的Matlab代码分了四个大文件主程序main.m上层鲸鱼算法upper_woa.m下层用户优化lower_woa.m还有数据初始化data_init.m。此外还有一些辅助函数比如适应度计算、约束处理、绘图脚本。整个工程在Matlab R2021b上跑的但用了很多和版本无关的基础函数理论上R2018之后的版本都能直接运行不需要额外工具箱这点对读者很友好。主程序负责设置种群规模、迭代次数、电价上下限等参数然后加载初始负荷数据调用上层鲸鱼算法等外层迭代结束后把结果存成结构体并绘图。上层鲸鱼算法内部会反复调用lower_woa.m每次调用都会返回该电价向量下全部用户的用电策略和各自成本。lower_woa.m内部则是针对单个用户的内层优化返回该用户的最优用电功率矩阵。3.2 内层鲸鱼算法单个用户的负荷优化先说内层。内层鲸鱼算法求解的是“在给定电价向量下某用户怎么安排用电最省钱”。这个问题的决策变量其实是每个可平移设备的启动时刻以及每个可削减设备每个时刻的功率。直接用实数编码鲸鱼位置向量x的长度等于可平移设备数量加上可削减设备数量乘24。比如每户有2个可平移设备和1台空调那么x维度是22426。前两个分量四舍五入后代表洗衣机和电动汽车的启动时刻后面24个分量代表空调24小时的功率。这里有个编码技巧可平移设备的启动时刻是整数所以我在适应度函数里对前两个维度做了round处理。可削减设备的功率则限制在[0.5, 2]kW通过位置向量的值映射过去。映射方式很简单original_min sigmoid(x_i) * (original_max - original_min)保证不会越界。适应度函数就是前文说的成本函数把越界的设备时间或者功率用惩罚项处理尽量避免出现不可行解。内层鲸鱼算法的标准流程是初始化鲸鱼种群计算适应度迭代更新位置。位置更新分三种机制包围捕食、气泡网攻击、随机搜索由随机数p决定。这部分是标准WOA我不再赘述但要注意的一点是因为内层优化需要被上层算法频繁调用所以内层的种群规模不宜太大迭代次数也要控制。我实测内层种群设20迭代50次已经能得到稳定解如果迭代100次结果几乎不变但计算时间翻倍。3.3 外层鲸鱼算法电价策略搜索外层鲸鱼算法的决策变量是24个时段的电价维度24。每个维度的取值范围我设为[0.3, 1.2]元/kWh这是参考当地居民阶梯电价上限和下限设定的。外层鲸鱼种群规模设30迭代次数50。每轮迭代对种群中的每一只鲸鱼一个电价向量都要调用所有用户的内层优化然后汇总总负荷曲线计算目标函数峰谷差 电价波动惩罚。外层算法的适应度计算是整个代码最耗时的部分。如果只有10个用户一个电价向量就需要跑10次内层优化30只鲸鱼就是300次内层优化50次迭代就是15000次内层优化。每次内层优化又需要20只鲸鱼迭代50次也就是1000次适应度评估整个程序的总评估次数超过千万量级。纯Matlab跑下来大概需要20多分钟对于研究演示来讲可以接受但如果做大规模优化必须考虑并行或改用C。3.4 核心代码片段与注释解析下面给出几个关键代码片段。首先是内层算法的目标函数简称cost_inner.mfunction cost cost_inner(x, user, price) % x: 决策变量1*(n_shift24) 向量 % user: 用户数据结构体 % price: 1*24 电价向量 shift_times round(x(1:2)); % 可平移设备启动时刻 ac_power sigmoid_mapping(x(3:end), user.ac_min, user.ac_max); % 空调功率 % 初始化负荷曲线 P user.rigid; % 刚性负荷 % 添加可平移负荷 for k 1:2 start shift_times(k); dur user.shift_dur(k); P(start:startdur-1) P(start:startdur-1) user.shift_power(k); end % 添加空调负荷 P P ac_power; % 电费 fee sum(P .* price); % 舒适度惩罚 comfort_penalty sum(abs(shift_times - user.pref_time)) * user.alpha ... sum((user.ac_base - ac_power).^2) * user.beta; cost fee comfort_penalty; end这里sigmoid_mapping负责将鲸鱼位置的实数映射到空调功率区间user.pref_time是期望启动时间user.alpha和user.beta是惩罚系数。注意如果启动时刻加上持续时长超过24我会在惩罚项里加一个很大的数强行使算法避开越界。外层鲸鱼算法的适应度函数简称cost_upper.mfunction fitness cost_upper(price_vector, users, params) total_load zeros(1,24); for i 1:length(users) [~, P_opt] lower_woa(users(i), price_vector, params.inner_opt); total_load total_load P_opt; end peak max(total_load); valley min(total_load); peak_valley_diff peak - valley; price_vol sum(abs(diff(price_vector))); fitness peak_valley_diff params.lambda * price_vol; endlower_woa函数内部调用内层鲸鱼算法返回最优成本和最优负荷曲线。外层适应度不直接拼“总负荷方差”而是用峰谷差加上价格波动惩罚这样更易懂调参也直观。3.5 参数表与初始数据设置模型里所有关键参数我整理成了表格参数数值说明用户数量10可扩展刚性负荷基准0.3~1.5 kW随机生成可平移设备数2洗衣机EV可平移负荷功率2 kW / 3 kW洗衣机/EV可平移持续时长1h / 2h洗衣机/EV期望开始时间20点 / 22点用户偏好空调基准功率1.8 kW可削减至0.6α时间偏移惩罚0.5 元/h可调β功率削减惩罚18可调电价下限/上限0.3 / 1.2 元/kWh约束外层种群/迭代30 / 50WOA默认经验值内层种群/迭代20 / 50平衡精度与速度λ电价波动系数0.1防止电价剧烈变化数据初始化时我用随机数生成10个用户的刚性负荷曲线叠加一些趋势模拟早中晚高峰。所有随机种子固定方便读者复现。我把rng(2024)写在main.m开头这样任何人跑出来结果都一样不会因为随机性导致对不上。4. 求解过程与仿真结果从博弈迭代到负荷优化4.1 博弈迭代的收敛过程整个双层算法的外循环就是一次“电价调整-用户响应-再调整”的重复博弈。我把每次外层迭代的峰谷差和电能均价记录了下来。从第1次到第10次迭代峰谷差从初始电价(阶梯固定价)的28kW迅速降到12kW左右第10次之后变化趋缓到第35次基本稳定在9.8kW附近。这说明博弈迭代能有效收敛到纳什均衡附近。我还特别关注了用户之间的公平性。在非合作博弈里每个用户因为设备配置和用电习惯不同承担的电费变化差异可能很大。我统计了10个用户调度前后的电费变化率发现有个别用户电费降幅很小只有4%而调峰贡献又大差点“吃亏”。后来我在上层目标里加了一个公平性惩罚项让各用户成本变化率的方差也进入适应度函数这样电价策略会兼顾整体最优化和个体公平性。加了这个项之后各用户电费降幅都集中在10%~14%之间没有再出现极端偏离。4.2 调度前后的负荷曲线对比调度结果最直观的表现就是总负荷曲线的变化。原始总负荷在晚上7点到9点有个明显尖峰最高达到接近120kW10户之和凌晨4点到6点有个低谷只有40kW左右。经双层优化后晚高峰被削到95kW左右凌晨低谷抬升到55kW左右峰谷差从80kW降到40kW出头。同时刚性负荷不变可平移负荷大量转移到深夜和午后可削减负荷在高峰期被小幅压减但没有出现不合理的零功率时段。这种曲线形态正是“削峰填谷”的标准效果。从电网角度看输配电设备利用率提高发电侧不需要频繁启动调峰机组从用户角度看虽然洗衣机和EV充电被挪到了深夜但相应获得了低谷电价电费整体下降。4.3 敏感性和稳定性分析我对几个关键参数做了敏感性测试。首先是α时间偏移惩罚从0提升到1结果发现α为0时算法会毫无顾忌地把所有可平移负荷都挪到凌晨三四点虽然峰谷差最小但用户显然不干。α太大则用户基本不动负荷峰谷差几乎没有改善。合适的α在0.3~0.8之间。这个参数需要根据用户对舒适度的敏感程度来标定实际项目里最好做一次小规模问卷或者历史行为分析。其次是内层鲸鱼算法的迭代次数。我尝试把内层迭代从20提升到100峰谷差只改善了不到1%而计算时间变成原来的5倍。这说明内层优化并非越精确越好因为外层电价本身也在变化内层解“差不多”就行过度精确反而浪费算力。这一点是双层优化算法工程化的关键心得。5. 常见问题与排查技巧实录5.1 内层优化不收敛或收敛到极端解我在debug时最常遇到的现象是内层负荷曲线出现突变某几个时段功率为负或者可平移设备持续时长不足。排查后发现大多是编码和约束写法的问题。比如我用round处理启动时刻但如果不加边界检查round可能得到0或25Matlab索引就会报错或者在运行时被自动截断导致负荷叠加错误。解决办法是在目标函数开头显式判断是否越界越界直接返回一个超大惩罚值让鲸鱼算法自然淘汰这些解。另一个坑是空调功率映射。最开始我用线性映射x_min (x_max - x_min) * (x - lb) / (ub - lb)但经常因为鲸鱼位置超出边界而出现功率超限。建议用sigmoid映射因为sigmoid(x)天然在(0,1)区间无论x多大都不会越界这样约束天然满足。这个技巧在其他实数编码优化问题中也很管用。5.2 外层迭代“早熟”或陷入局部最优鲸鱼算法的勘探能力其实不弱但如果外层种群太小或者初始电价分布太窄很容易在某个局部电价策略附近收敛。我试过把外层种群从20增到50峰谷差结果明显更稳定。另外初期电价向量可以用Sobol序列或拉丁超立方初始化而纯随机均匀分布容易导致初始种群扎堆。还有一种实用技巧是“重启机制”假设外层连续10轮适应度没有改善就按一定比例随机重置鲸鱼的位置相当于赌博式跳出局部最优。我在代码里加了这个机制虽然让收敛时间增加了一点但结果靠谱很多。5.3 计算时间太长怎么办双层嵌套优化的计算瓶颈就是内层反复调用。我这里提供三种缓解方案。第一内层优化时把上一轮外层迭代得到的用户最优解作为当前内层优化的初始种群中心这样做可以大幅减少迭代次数因为电价变化是连续的上一轮的最优解在下一轮依然接近最优。第二用Matlab的Parallel Computing Toolbox把外层种群中各个鲸鱼的适应度计算用parfor并行10个用户的内层优化分布在多个worker上实测提速接近4倍6核PC。第三如果用户数量特别大先做聚类把相似用户聚成几类每一类用一个“虚拟用户”代表内层只算几个聚类中心最后再分配回原用户。5.4 结果分析验证的注意事项很多人跑完算法画个曲线就算完事但至少还要做两步验证。一是验证各用户的用电总量在调度前后是否守恒如果总量变了说明算法在“偷懒”通过削减总用电量来优化峰谷差这不是我们要的。二是验证电价策略是否在允许范围内如果某些时段电价被压到下限、同时负荷又飙升可能是算法在利用边界做“作弊式”的削峰。我通常会写一个check函数把调度前后的总用电量差和各时段电价越界次数打印出来作为输出的必查项。6. 代码获取与后续扩展建议惯例如下这里分享的Matlab代码实现核心逻辑完全贴合本文所述模型如果你正好在做相关课题可以直接拿main.m跑通整体流程再根据你自己的数据调整参数。代码里我保留了详细的注释连每个惩罚因子的作用都写了。如果你希望把模型做得更贴近工程实际可以从这几个方向扩展一是把单目标上层决策改成多目标优化比如同时考虑碳排放和用户满意度用NSGA-II或MOPSO替换外层鲸鱼二是把静态分时电价改成实时电价让上层决策变量变成24个连续值而不只是峰谷两段或三段价格三是引入电动汽车充放电的V2G功能让用户在低谷充电、高峰放电进一步削峰填谷不过这会大幅增加模型复杂度。根据我个人测试这套代码的经验最需要花时间的地方不是算法实现而是数据清洗和参数标定。居民负荷数据里经常有空值、异常尖峰如果不做处理优化结果会被个别坏数据带偏。参数方面α和β的取值最好依据实际的用户舒适度调研来定拍脑袋设值很容易让结果看起来漂亮但缺乏实际落地意义。总的来说双层鲸鱼算法配合非合作博弈求解居民负荷分层调度是一个非常适合练手的综合性能源优化项目它把博弈论、群体智能和电力系统需求侧响应串成了一条完整的链路研究价值和应用价值都不低。