憋了好几个月的课题,总算把“基于粒子群优化算法的三方三层主从博弈能源系统优化模型”在 Matlab 里跑通了。这题看着高大上,拆开其实就是两层东西:外层用 PSO 把“售电价格/激励系数”定价解出来,内层再让用户去响应这个价格、优化自己的用电策略,来回迭代之后形成一个稳定的定价和用电方案。整个过程最折腾的不是公式推导,而是怎么把“三方三层的主从博弈结构”建模成可求解的程序,以及怎么让双层嵌套优化在 Matlab 里不卡死、不震荡、不给出离谱结果。
这篇文章我尽量把模型结构、粒子群参数设置、代码框架、调试心得和避坑经一次讲透。适合正在做综合能源系统/园区微网优化、电力市场多主体博弈、或者把 Stackelberg 博弈和智能算法结合做课题的同学参考。
1. 模型整体设计与思路拆解
1.1 为什么是“三方三层”,而不是传统单层优化
先说清楚这个题目的应用背景。传统的能源系统优化,通常只站在一个“总调度中心”的角度,把全系统的发电、储能、负荷组合在一起求最小成本。这在数学上很好解,但现实中根本不成立——因为园区里的各个主体都有自己的利益,谁也不会无条件听从“上帝视角”的调度。
“三方三层”就是对这个现实结构的抽象。以我做的园区综合能源系统为例:
- 第一方:上游能源供应方,包括电网公司和天然气公司。它们决定分时购电价、天然气价格、最大购电限额。
- 第二方:能源运营商,可以理解成园区微网的投资运营方。它从上游买电、买气,运营光伏、储能和燃气轮机,然后制定销售电价卖给用户。它要最大化自己的净收益。
- 第三方:终端用户,包括工商业用户和居民用户。用户会根据运营商给出的电价,决定什么时候用能、用多少能,目标是让自己的用能成本最小、舒适度损失最小。
三层结构里,最上面是“决策层”,也就是运营商定价;中间是“运行层”,负责功率平衡、储能充放电、购电购气安排;最下面是“响应层”,就是用户的需求侧响应。三个层级之间的信息是逐层传递的:上层定价影响下层用电,下层用电反过来影响上层收益,所以必须用博弈而不是单目标优化来建模。
这个结构的精妙之处在于,它比“电网-用户”两方博弈多了一个“能源运营商”作为中间缓冲,更贴近真实售电公司的业务逻辑。如果题目只有两层,那模型的博弈关系相对简单,但三方结构引入之后,上游的价格信号、运营商的定价策略和用户的负荷响应就形成了链条式耦合,难度和复杂度立刻上来了。
1.2 主从博弈(Stackelberg Game)的建模逻辑
主从博弈也叫 Stackelberg 博弈,核心思想是“领导者先走,跟随者再走,但领导者做决策时必须预判跟随者的反应”。
这个场景特别适合运营商和用户之间的互动:
- 运营商(领导者)先制定一天 24 小时的分时售电价格,以及可削减负荷的激励补偿系数。
- 用户(跟随者)看到价格后,在满足自己用电需求的前提下,调整柔性负荷的时间分布,追求用电成本最小。
- 运营商拿到用户的负荷响应结果后,核算自己的营收和成本,然后继续调整电价,直到用户的负荷响应不再变化,或者运营商的收益不再提高。
这个均衡状态就是主从博弈的 Stackelberg 均衡。用大白话说:大家都接受了当前的价格,用户不觉得换一种用电方式能更省,运营商也不觉得改个价格能多赚多少——双方都没有单方面改变的动机。
普通纳什均衡是两个主体同时决策,而 Stackelberg 的特点是强者拥有“先动优势”。在代码里,这种先后关系就变成了嵌套结构:外层是运营商的优化循环,内层是用户的优化子问题,外层每评估一个候选电价,都需要调用一次内层优化来得到用户对应的最优负荷响应。
1.3 为什么要用粒子群优化算法解这个模型
主从博弈模型可以用两种思路来求解:
第一种思路是解析法。把用户侧优化问题写成一阶最优性条件(KKT 条件),然后把下层问题转化为上层问题的约束,最终变成一个带互补松弛条件的单层优化问题,用商业求解器求解。听起来很完美,但这个思路有几个致命问题:如果用户侧模型不是严格的凸规划,KKT 条件就不完备;即使全是线性约束,转化后的互补约束也让求解器非常痛苦,经常出现数值病态;同时变量规模扩大,代码实现复杂度极高。
第二种思路就是智能算法嵌套求解。外层用 PSO 在电价解空间里搜索,内层针对每个候选电价直接调用线性规划或二次规划求解用户优化问题,把用户的最优负荷响应带回来计算运营商的适应度。
我选第二种思路,理由很实际:
- PSO 不要求目标函数光滑可导,运营商的收益函数里包含 max/min 逻辑、分段函数、储能状态转移时,智能算法可以无损处理。
- 内层用户问题用标准求解器(linprog 或 quadprog)求解,效率和可靠性都有保障。
- 代码可扩展性强。后面想在运营商侧加入碳成本、储能老化成本、风险约束,我只要改上层适应度函数,不需要重启整个模型。
- 发论文、写报告时,“PSO 优化出的结果 vs 固定电价方案”能形成清晰对比,直观展示博弈优化的优势。
1.4 PSO 在这个模型里到底在优化什么变量
很多初学者没搞清楚这一点,导致 PSO 的维度和含义设置得乱七八糟。
在这类“运营商-用户”主从博弈模型中,我实际用 PSO 优化的决策变量有两组:
- 24 时段的分时销售电价 p_sell(t),t=1...24
- 可削减负荷的单位补偿价格 comp(t),t=1...24(也可以只设一个固定补偿价格)
把这两组变量拼接成一个向量,粒子的维度就是 48。当然,如果你不想细化补偿,只优化 24 个电价变量也可以。我最后做的是 24 个电价 + 24 个补偿,总共 48 维变量。
运营商的收益目标函数是:
max Profit = 售电收入 - 上网购电成本 - 天然气购气成本 - 设备运维成本 - 碳排放成本 - 需求响应补偿支出
约束条件包括:
- 每个时段的功率平衡约束(购电 + 光伏出力 + 储能放电 + 燃气轮机出力 = 用户负荷 + 储能充电功率)
- 储能电池的 SOC 范围约束、充放电功率限值约束、始末 SOC 一致性约束
- 上网购电功率不超过变压器容量
- 售电价格在政府指导价区间内,不能超过用户心理价位上限
- 燃气轮机出力范围和爬坡约束
- 用户侧可转移负荷不能超过总可转移比例
这些约束一部分由运营商自己遵守,另一部分则需要在用户优化子问题中体现。我在 PSO 里处理运营商侧约束用的思路是:硬约束尽量通过粒子生成和边界处理保证,耦合约束用罚函数处理。具体怎么处理,后面实操部分详细说。
2. 核心细节解析与实操要点
2.1 决策变量编码与边界处理
PSO 的粒子编码方式决定了搜索效率。我的建议是:不要做二进制编码,直接把连续变量用实数向量表示,每一维的取值范围对应电价的可行区间。
假设允许售电价格在 0.3 元/kWh 到 1.2 元/kWh 之间浮动,那粒子第 1 到第 24 维的上下界就是 [0.3, 1.2]。补偿价格的第 25 到第 48 维上下界可以设置成 [0, 0.5]。
边界处理这里一定要留心。我见过很多代码在粒子越界时用最简单的“截断到边界”,这样会导致大量粒子堆积在边界上,种群多样性很快消失。我用的方法是:
- 对速度越界,做幅值钳制;
- 对位置越界,采用随机反射策略:超出上界的部分按一定概率反射回内部,而不是直接压到上界;
- 另外以 5% 的概率对越界粒子做一次随机重启,让粒子重新在解空间内搜索。
实际跑下来,这种处理方式在求解精度上比单纯截断高不少,尤其是价格上限附近本身就可能存在最优解时,粒子不至于过早扎堆。
2.2 用户侧负荷响应模型怎么建
用户侧优化是整个内层子问题的核心,需要构建一个“价格→负荷响应”的量化关系,这也是“三层”中需求响应的核心环节。
我把用户负荷分成三类:
- 固定负荷:照明、电梯、基本办公设备,这些完全刚性,不参与响应。
- 可转移负荷:洗衣机、热水器、工业流水线、充电桩等,可以把工作时段从一个时间窗平移到另一个时间窗。
- 可削减负荷:空调、暖气等温控负荷,可以适当降低设定温度或设备功率,但要给用户一定的补偿,且削减量有上限。
用户侧的目标函数是:
min 用户总成本 = 向运营商购电的电费 + 削减负荷带来的舒适度损失 - 削减补偿收入
其中电费 = sum(p_sell(t) * (固定负荷 + 可转移负荷运行功率 + 削减后剩余的可削减负荷))
用户侧的优化变量是各个时段可转移负荷的运行状态(0/1 或者 0 到 1 之间的连续变量)和可削减负荷的削减量。这是一个带约束的线性规划问题,用 Matlab 的linprog可以直接求解。
可转移负荷的时间窗限制是这个模型里最关键的约束。比如工业流水线必须在 8:00-12:00 内完成总共 4 小时的工作,但允许拆分或整体平移。这个约束翻译成线性表达式,就是要保证每个设备在其允许时间窗内,启动状态的累计小时数等于其所需总时长。
有一点你得特别注意:为了让用户侧保持一定“惰性”,不能让用户把所有负荷都转到某一个电价最低的时段,否则会出现负荷过于集中,反而不符合实际情况,而且会加剧博弈迭代的振荡。解决办法是给可转移负荷加一个“相邻时段转移成本”或“最大转移比例”限制,相当于用户虽然想省钱,但也不能完全牺牲用电习惯。
2.3 PSO 核心参数怎么定
粒子群优化算法的参数设计直接影响博弈能否收敛到稳定的 Stackelberg 均衡。我经过多组对比实验,总结出一套适合该模型的参数组合:
| 参数 | 推荐设置 | 备注 |
|---|---|---|
| 粒子数 N | 40 | 48 维变量搜索空间较大,粒子太少容易早熟 |
| 迭代次数 T | 150 | 在精度和耗时之间取平衡,后期收益增幅很小 |
| 惯性权重 w | 0.9 线性递减到 0.4 | 前期加强全局搜索,后期加强局部精细搜索 |
| 个体学习因子 c1 | 1.5 | 不能设太大,否则粒子容易“自嗨”而脱离群体 |
| 全局学习因子 c2 | 1.5 | 与 c1 保持相同,避免过分向历史最优靠拢 |
| 速度上限 Vmax | 0.2 倍变量区间宽度 | 防止粒子飞得太猛导致振荡 |
| 收敛判定 ε | 1e-4 | 连续 10 代种群最优适应度变化小于该值时提前终止 |
这套参数不是说在所有系统上都适用,但在我做过的园区规模(几十个节点、几百个用户的等值模型)上表现稳定。如果你的模型特别复杂,建议先把粒子数和迭代次数适当加大,找到稳定解之后再把参数调回去,效率和精度就能兼顾。
2.4 主从博弈迭代收敛条件的设置
这里有个很容易踩的误区:很多人把 PSO 本身的收敛当成博弈收敛,其实两者不是一个概念。
在双层嵌套结构里,博弈收敛指的是“领导者给出的定价策略和跟随者的最佳响应不再变化”,也就是达到了稳定均衡。我实际用来判断收敛的标准是:
- 记录每一轮外层 PSO 得到的最优电价向量 p_best;
- 比较相邻两轮“最优电价向量”的最大绝对偏差;
- 只要偏差小于 0.001 元/kWh,同时运营商的收益变化小于 0.1%,就认为已经达到主从博弈均衡。
注意,每次外层 PSO 结束都会得到一个新的“当前最优电价”。你可能以为跑完 PSO 之后博弈就结束了,其实不是这样。严谨的主从博弈求解是指:把 PSO 迭代若干代之后,让用户重新响应,再根据响应结果重新启动 PSO,重复这个过程直到双层循环都稳定。我称之为“外循环 + 内循环”的双层迭代。虽然这会显著增加计算量,但如果不做这一步,得到的解可能只是“未达到均衡的中间解”,论文审稿人一问就露馅了。
在代码实践中,我通常先跑一次 150 代的 PSO,拿到初步电价方案;然后把它作为初值,重新初始化 PSO 再跑 30 代作为修正。重复 3-4 次,电价基本稳定。这个技巧让代码消耗的计算时间可控,同时也让结果的“均衡质量”有了保证。
2.5 工具与运行环境配置
Matlab 方面我这边用的是 2021b 版本,后面换到 2023a 也没出现兼容问题。整个模型主要依赖优化工具箱(Optimization Toolbox)里的linprog。如果你的 Matlab 版本比较老,建议提前确认linprog的支持语法;新版和老版默认求解算法不同,老版本在单纯形法上的表现和大规模问题上会弱一些。
我强烈建议在计算资源允许的情况下,给 PSO 的适应度评价加上并行计算。方法很简单:把粒子群循环里的for i = 1:N改成parfor i = 1:N,并提前用parpool开启并行池。注意内层优化函数里不要有复杂的全局变量依赖,否则并行化会失效或者带来奇怪错误。
3. 实操过程与核心环节实现
3.1 代码整体架构设计
我写代码时遵循“模块化 + 数据驱动”的思路,一共拆了 6 个文件:
| 文件 | 功能 |
|---|---|
| main.m | 主脚本,负责数据初始化、调用 PSO、输出结果 |
| pso_outer.m | 上层 PSO 优化循环 |
| fitness_operator.m | 运营商适应度计算函数 |
| user_opt.m | 用户侧线性规划求解函数 |
| system_data.m | 系统参数结构体生成函数 |
| plot_results.m | 结果可视化函数 |
这种拆法最大的好处是:改系统参数完全不需要动求解逻辑;改目标函数只需要动对应的 fitness 函数。我在做灵敏度分析时,只需要循环改system_data.m里的参数,然后反复调用main.m就行。
3.2 上层 PSO 主循环的代码骨架
下面是pso_outer.m的核心代码骨架(已做了简化,突出博弈嵌套结构):
function [best_price, best_profit] = pso_outer(data) % 参数初始化 N = 40; % 粒子数 T = 150; % 迭代次数 n_dim = 48; % 24个电价 + 24个补偿系数 lb = [0.3*ones(1,24), 0.0*ones(1,24)]; ub = [1.2*ones(1,24), 0.5*ones(1,24)]; w = 0.9; w_end = 0.4; c1 = 1.5; c2 = 1.5; % 初始化粒子位置和速度 X = repmat(lb, N, 1) + rand(N, n_dim) .* (ub - lb); V = zeros(N, n_dim); pbest_X = X; pbest_fit = -inf(N, 1); [gbest_fit, idx] = -inf(1, 1); gbest_X = X(1, :); for iter = 1:T % 线性递减惯性权重 w_cur = w - (w - w_end) * (iter / T); % 计算每个粒子的适应度 fitness = zeros(N, 1); parfor i = 1:N % 内层用户优化响应 [user_load, user_cost] = user_opt(X(i, 1:24), X(i, 25:48), data); % 运营商收益 profit = fitness_operator(X(i, 1:24), X(i, 25:48), user_load, data); % 罚函数处理越界 fitness(i) = profit - penalty(X(i), lb, ub, data); end % 更新 pbest improve = fitness > pbest_fit; pbest_X(improve, :) = X(improve, :); pbest_fit(improve) = fitness(improve); % 更新 gbest [cur_best, cur_idx] = max(pbest_fit); if cur_best > gbest_fit gbest_fit = cur_best; gbest_X = pbest_X(cur_idx, :); end % 更新速度和位置 r1 = rand(N, n_dim); r2 = rand(N, n_dim); V = w_cur * V + c1*r1.*(pbest_X - X) + c2*r2.*(gbest_X - X); V = max(min(V, 0.2*(ub-lb)), -0.2*(ub-lb)); X = X + V; % 边界反射处理 for i = 1:N over_high = X(i,:) > ub; over_low = X(i,:) < lb; X(i, over_high) = ub(over_high) - abs(X(i, over_high) - ub(over_high)); X(i, over_low) = lb(over_low) + abs(lb(over_low) - X(i, over_low)); end end best_price = gbest_X; best_profit = gbest_fit; end3.3 内层用户优化函数怎么写
内层user_opt.m是整个模型的核心环节,它的任务是:给定运营商电价和补偿系数,求出用户的最优用电计划。
我构造的线性规划形式是:
min x = c_energy' * u_load - comp_incentive + comfort_loss s.t. A·x ≤ b Aeq·x = beq lb ≤ x ≤ ub决策变量包括固定负荷在各时段的用电功率(实际上是常数)、可转移负荷在各时段的启动指示变量、可削减负荷在各时段的削减量。目标函数里,电费按“剩余负荷 × 电价”计算,补偿收入按“削减量 × 补偿单价”计算。可转移负荷的启动状态用连续变量近似(0 到 1 之间),要转成真正的 0/1 规划则用intlinprog,但连续松弛在用能成本优化里精度已经足够,所以我保留了连续线性规划形式,求解速度非常快。
代码骨架如下:
function [user_load, user_cost] = user_opt(price, comp, data) % 变量顺序: % 1:24 可转移负荷各时段状态 % 25:48 可削减负荷各时段削减量 n = 48; f = zeros(n, 1); % 电费成本来自总负荷购电 % 这里用近似处理:总负荷 = 固定负荷 + 转移负荷的状态*额定功率 - 削减量 % 边际目标系数需要组合 % 约束:功率平衡、削减上限、时间窗约束... A = []; b = []; Aeq = []; beq = []; lb = [zeros(1,24), zeros(1,24)]; ub = [ones(1,24), ones(1,24) .* data.max_curtail(1,:)]; % 具体约束构建省略 [x_opt, cost_min] = linprog(f, A, b, Aeq, beq, lb, ub); % 计算用户实际负荷曲线 user_load = data.fixed_load + x_opt(1:24).*data.transfer_power - x_opt(25:48).*data.max_curtail; user_cost = cost_min; end实际代码里的约束矩阵会比这个骨架大很多,尤其可转移负荷的时间窗约束,需要仔细构建指示矩阵。建议先把所有约束在纸上写成标准形式,再逐行填入矩阵,不要边写边想。
3.4 系统参数与典型日数据准备
我使用的典型日仿真场景参数如下:
| 参数 | 数值 | 说明 |
|---|---|---|
| 调度周期 | 24h,单位时段 1h | 经典调度粒度 |
| 光伏装机 | 800 kW | 出力曲线取夏季典型日 |
| 储能容量 | 600 kWh | SOC 范围 0.1-0.9 |
| 储能最大充放电功率 | 200 kW | 充放电效率 0.95 |
| 燃气轮机容量 | 500 kW | 爬坡率 100 kW/h |
| 变电站购电上限 | 1000 kW | 从上级电网购电容量 |
| 固定负荷峰值 | 1200 kW | 双峰曲线 |
| 可转移负荷比例 | 20% | 约 240 kW |
| 可削减负荷比例 | 5% | 约 60 kW |
| 电网购电电价 | 峰 1.0 谷 0.35 元/kWh | 分时电价 |
| 售电价范围 | 0.3-1.2 元/kWh | PSO 决策变量边界 |
这些初始参数不同,最后的均衡电价形态会完全不一样。我建议你第一次跑模型时用一套典型的参数,先验证代码逻辑,再做大数据分析。
3.5 结果分析与对比实验设计
主程序运行完,我的输出包括:
- 24 小时最优售电价曲线;
- 24 小时用户总负荷响应曲线,以及原始负荷(无博弈)和响应后负荷的对比;
- 运营商总收益、用户总成本;
- 储能 SOC 变化曲线。
多组实验跑下来,最典型的规律是:运营商定的分时电价在“谷时段”会比购电价高出一些,在“峰时段”大幅提高,但会低于用户的心理承受上限;而用户会主动把可转移负荷挪到下午和夜间谷段,峰段负荷削减明显。运营商收益相比“固定电价方案”能提升大约 12%-18%,用户成本相比“完全不响应”能下降 8%-15%。这就是主从博弈带来的双赢效果,强烈建议在项目汇报里把这组对比做出来。
除了基础场景,我还会做以下灵敏度测试:
- 改变储能容量,观察电价峰谷差变化;
- 改变可转移负荷比例,观察用户响应能力和运营商收益的变化;
- 改变购电峰谷价差,观察博弈是否仍然收敛;
- 改变粒子数和迭代次数,观察最优解的稳定性。
3.6 如何判断程序收敛到了合理的均衡
运行完之后,不要只看收益数值,一定要画图观察曲线形态。
一个合理的主从博弈均衡电价曲线,通常是“跟随购电价趋势但幅度调整”的光滑曲线,不应该出现相邻时段价格剧烈跳变(除非有极端约束)。用户负荷曲线应该是平缓的,不应该在某一个时段出现负荷尖峰或断崖式下降。储能 SOC 曲线应该是连续变化的,从初始值出发,经过一个完整的充放循环后回到接近初始值的位置。
如果负荷曲线出现明显不合理突变,首先去查用户侧约束和电价边界,大概率是价格边界设得太宽,或者可转移负荷时间窗约束写错了。
4. 常见问题与排查技巧实录
4.1 结果出现 NaN 或 Inf
这是双层嵌套优化里最常见的崩溃现象,通常原因有两个:
一个是用户侧线性规划的初始约束不可行,linprog返回空解,然后上层适应度函数在空数组上做运算,自然就 NaN 了。排查方法很简单:在user_opt函数里检查exitflag,如果不是 1,直接打印一条错误信息,并把导致不可行的电价和补偿返回到命令行,方便定位是哪一组粒子出了问题。
另一个原因是粒子越界后出现了极端数值,比如电价变成负数,或者补偿系数变成很大的正数,导致适应度函数里出现除零或者对数无定义。处理办法就是更严格地做边界钳制和罚函数。
我觉得最实用的操作是:在fitness_operator函数入口处加入防御性判断,如果输入电价或补偿中含有任何 NaN、非正值、超出边界,就直接返回一个很大的负值。这样即使某个粒子出了问题,也不会导致整个程序崩溃。
4.2 PSO 早熟收敛,解的质量差
初版代码跑出来,我发现不管怎么初始化粒子,PSO 最后都把电价压到了可行域上限,整个搜索结果毫无区分度。后来定位到原因:适应度函数受某个约束影响太大,导致只要电价一高,收益就被罚函数压下去,粒子全都往低价区跑,失去了搜索意义。
解决办法有两个:
- 把罚函数系数调得合理一点,不能让惩罚项在适应度中占据绝对主导。一个好的经验值:惩罚项的数值量级控制在收益目标值的 10%-30% 之间。
- 引入“精英保留”和“随机扰动”。每代保留局部最优的 5 个粒子直接进入下一代,其余粒子按正常 PSO 更新;每隔 20 代,随机挑 3 个粒子做随机重启。
做了这两步之后,解的质量明显提升,而且多次随机试验的结果不再天差地别,稳定性好了很多。
4.3 博弈迭代振荡,不收敛
这是主从博弈模型特有的大坑。现象是:外层求出某个电价方案,用户响应之后,外层再算收益,发现收益变化巨大,下一轮电价又大幅跳回之前的方案,来回振荡。
根本原因在于用户对电价的响应过于敏感。如果可转移负荷占比太高,或者用户成本函数里没有舒适度损失等因素,用户会把大量负荷集中搬到某一两个时段,这种剧烈的负荷波动会让运营商收益函数变得非常不平稳,PSO 在这种不平稳的适应度面上自然很难收敛。
解决方法我总结了三个:
- 给用户目标函数增加平滑项,让负荷转移产生一定的“摩擦成本”,比如转移每千瓦时负荷产生 0.02 元的操作成本。
- 对可转移负荷设更严格的最大比例限制,例如每时段可转移量不超过该时段固定负荷的 30%。
- 外层的价格更新加上阻尼因子:每一轮新的价格不是直接赋值给下一轮,而是按
p_new = 0.7 * p_new + 0.3 * p_old做平滑,实测对抑制振荡非常有效。
4.4 计算时间太长,怎么优化
我最初版本的运行时间是 40 个粒子 × 150 代 × 每次调用linprog约 80 毫秒,总时间接近 80 分钟。做参数灵敏度分析时根本没法忍受。
优化方案按收益大小排序:
- 第一优先:开启
parfor并行计算。40 个粒子的适应度计算天然独立,直接并行化后,时间能缩短到 15-20 分钟。 - 第二优先:减少迭代次数。我测试发现,当迭代次数从 150 增加到 300 时,最终收益只提升 0.5% 左右,但时间翻倍。所以大多数场景 100 代就够了。
- 第三优先:内层用户优化问题可以提前推导出解析解或近似解,对于纯线性规划的小规模问题,手写求解器比
linprog快好几倍。如果不需要严格最优,可以用启发式调度规则代替内层线性规划,这样内层计算开销几乎可以忽略。
我最后采用的方案是:外层 PSO 用 60 代,开启并行池,内层用linprog,单次场景求解控制在 10 分钟以内。要发成论文的话,再用 150 代精修一次,得到最终结果。
4.5 常见问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 程序报 NaN / Inf | 内层 linprog 无可行解 | 检查价格边界和用户约束,加防御性判断 |
| 电价结果全贴边界 | 罚函数权重太大或边界不合理 | 调整罚函数系数,放宽边界 |
| 负荷曲线出现尖峰 | 转移负荷时间窗约束缺失 | 检查可转移负荷的状态约束矩阵 |
| 博弈迭代振荡 | 用户响应过于敏感 | 增加平滑项、阻尼因子或限制负荷转移比例 |
| 计算时间过长 | 粒子多、迭代多、未并行 | 开 parfor、降迭代次数、简化内层求解 |
| 多次运行结果差异大 | PSO 早熟或随机性太强 | 增加精英保留、固定随机种子做平均统计 |
| 储能 SOC 不归位 | 末时刻约束缺失 | 增加 SOC 初始终值等式约束 |
5. 我想补充的几点经验
代码从“能跑”到“结果可信”之间,隔着大量的验证工作。我最开始跑通程序时兴奋得不行,结果发现产出的一组电价方案完全不符合经济逻辑——低谷时段电价居然高于高峰时段,仔细一查是目标函数里的符号写错了。这类低级错误如果不通过逐步打印中间变量来排查,让你在里面找上一天都是正常的。
我的调试经验是:先把内层用户优化单独拿出来测试,给定一个固定电价,看用户的负荷响应是否符合直觉;再测单个粒子的适应度,手动构造一组合理电价和一组离谱电价,看看哪个收益高;最后再跑完整的 PSO。每一步都验证之后再拼装,节约的时间远超你多写的那些调试代码。
另外一个小技巧:跑 PSO 的时候把每次迭代的全局最优收益画出来,如果曲线是平滑上升并逐渐平缓,说明搜索过程健康;如果曲线频繁上蹿下跳,大概率是罚函数系数设置不当或边界处理有问题,赶紧停下来排查。
如果后续想让模型更像学术论文的成果,可以考虑在“三方”结构中引入多微网场景,或者把收益目标改为包含风险和碳交易的多目标 PSO。扩展思路有很多,但前提是把当前的主从博弈双层结构真正吃透。希望这篇分享能帮你少走几步弯路,跑出让人信服的结果。