做虚拟电厂调度的人,大概都经历过这种循环:上层给用户报一个内部电价,用户拿到电价之后开始算计自己的储能、空调、充电桩怎么响应;用户的响应结果汇总回来,上层发现利润不是自己想要的,于是再改一层电价。两边来回咬合,上层每试探一次,下层就要重新把几十个分布式资源从零开始优化一遍。96个时段跑完,再叠加外层迭代,求解器能把你晾在座位上喝掉一整杯咖啡。这种“一上一下彼此较劲”的问题,就是典型的主从博弈优化调度。
这篇我重点聊“基于元模型优化的虚拟电厂主从博弈调度”:模型怎么建、元模型怎么选、MATLAB代码怎么写、收敛了怎么判断、有哪些坑。无论你是刚接触电气优化的研究生,还是做园区级虚拟电厂落地项目的工程师,都可以按这条思路往下搭。文章是完全个人视角的经验梳理,没有教科书腔,照着能救急。
1. 主从博弈调度到底在算什么:先把这个问题的每一步掰开
1.1 博弈双方是谁:上层定价格、下层做响应
虚拟电厂里的主从博弈,核心是两个决策主体的互动关系。上层是虚拟电厂运营商,手里有三件武器:内部购电价、内部售电价、对分布式资源的激励系数。下层是各类分布式资源,包括光伏+储能、电动汽车充电桩、温控负荷、可中断负荷等,它们作为价格接受者,根据上层给出的电价信号安排自己的运行方式。
这两层并不是简单的“下命令-执行”关系。上层定的电价会直接改变下层资源的行为,下层综合响应之后的总功率曲线又决定了上层在外部电力市场上的购售电成本与收益。也就是说,上层必须在下层最优响应的前提下寻找自己的最优决策,这就构成了典型的Stackelberg主从博弈结构。
我实际处理过的某园区虚拟电厂项目里,光伏装机、储能容量、充电桩数量都不算大,但一旦引入内部峰谷电价机制,运营商的决策空间和用户侧的响应行为就立刻纠缠在一起。上层把峰时电价抬高到某个值,储能可能集中放电,对上层来说是缓解了对外购电压力;但抬高幅度过大,用户侧可能干脆压低负荷甚至反送电,上层在内部结算上的支出又变多。这种反直觉的联动,正是博弈模型必须显式建模的原因。
1.2 上下层的数学描述长什么样
上层问题本质上是一个收益最大化问题。一般写法是最大化运营商的综合净收益:对外售电收入减去从外部电网的购电成本,减去对下层资源的调用补偿,再减去设备折旧和运维成本。决策变量是内部电价序列和相关激励参数,约束包括内部电价上下限、与外部电网的交互功率限额、内部功率平衡约束等。
下层每个分布式资源是一个独立的费用最小化问题。以储能为例,决策变量是各时段的充放电功率,约束包括充放电功率限制、SOC递推关系、SOC上下限等,目标是在电价序列下让运行成本最小。电动汽车充电桩则可能带有“离网时SOC达到设定值”的约束;温控负荷要考虑用户舒适度范围;可中断负荷则涉及中断补偿价格和中断次数上限。
这两层模型合起来,数学上是一个嵌套优化问题:
上层:max F(π, p*(π)) 下层:p*(π) = argmin f(π, p),给定上层电价π
注意下层使用的是“最优响应函数”p*(π),而不是某个固定的出力值。这个隐函数通常是非线性、非光滑甚至不连续的,因为下层混合整数变量(比如机组启停、充电桩开关)会让出力曲线出现跳变。直接把p*(π)代入上层目标,上层问题就变成一个高度非凸、不可导的优化问题,这也是为什么这类问题不能简单套用常规凸优化求解器。
1.3 嵌套求解的成本账:为什么跑一次这么慢
很多人第一次上手主从博弈调度,第一反应是“直接写个双层循环不就行了”:外层不断枚举或优化电价,内层针对每个电价调用一次下层求解器。听起来很简单,但算一笔账就明白问题在哪。
假设虚拟电厂里有20个分布式资源节点,每个节点是包含整数变量的混合整数优化问题,单次求解大约需要0.5秒到1秒。上层优化采用启发式或差分进化算法,一轮迭代需要评估50个候选电价方案。每个候选方案都要把20个下层节点全部求解一遍,那么单轮迭代就需要50×20=1000次下层求解,耗时约500秒到1000秒。而外层迭代往往需要几十轮才能接近均衡,总耗时轻松超过几个小时。
更麻烦的是,下层问题是随调度周期动态变化的。比如储能从初始SOC 20%和初始SOC 90%开始,最优响应完全不同;光伏出力的预测曲线变了,下层响应也跟着变。所以每次调度都要重新跑整套嵌套优化,没法简单复用昨天的结果。我在实际项目里第一版就是这个双层循环,跑到第三天实在受不了,才开始认真考虑用元模型做降本。
2. 元模型的直觉与选型:把最贵的部分预判成一张映射表
2.1 元模型在替谁打工:替代的是隐函数而非物理方程
很多同行听到“元模型”就以为是要把储能、光伏这些物理模型换成数据驱动模型,这其实是误解。物理模型本身很清晰,该保留必须保留。元模型真正要替代的,是从“上层电价π”到“下层最优响应p*(π)”这段嵌套求解过程,也就是那个昂贵隐函数的评估环节。
打个比方:你要知道餐厅每道菜的价格,最笨的办法是每次想点菜都跑一趟餐厅看菜单;聪明的做法是第一次去把菜单拍照存下来,之后想点菜直接查照片。元模型就是那张“菜单照片”——先用一组有代表性的电价方案,让真实下层模型逐个算出最优响应,再用回归模型学习“电价→最优响应”的映射关系。上层之后再来试探新的电价,直接从元模型几毫秒出结果,不用再触发任何下层求解器。
这个思路在数学上是有依据的:虽然p*(π)没有解析表达式,但它通常具备一定的连续性和平滑性——电价稍微变一点,电量的最优响应大概率也只变一点。既然是连续映射,就可以用平滑函数逼近它。真正的难点不在“能不能拟合”,而在“用什么采样策略、在多大范围训练、如何判断拟合误差不伤害均衡解”。
2.2 采样设计:拉丁超立方打底,加点迭代补精度
元模型的训练质量从采样那一刻就决定了。采样空间就是上层电价可能取值的区间,每个维度对应一个调度时段的某个价格参数。最不建议的做法是纯随机抽样,因为随机样本在高维空间里很容易在某些区间扎堆、在另一些区间留白。
我用的是拉丁超立方采样,MATLAB里一行命令就能实现:
% 各维度下限、上限分别放在 lb 和 ub 中,dim 是采样维度 samples = lhsdesign(nInit, dim); % 生成 [0,1]^dim 的拉丁超立方 samples = samples .* (ub - lb) + lb; % 映射到真实参数区间初始样本数量我一般按下述经验估:不少于输入维度的5到8倍,同时至少30个。维度低时可以稍微多取些,比如输入维度是8,取40到50个样本很稳;维度冲到20以上时,初始样本先控制在100到150个,跑完看精度再决定是否补充。
真正提精度的环节是“加点”。初始模型建好后,计算当前博弈迭代路径附近的预测方差,在方差大或迭代最可能出现的位置补充真实下层求解结果,更新元模型。这个过程类似自适应采样,只要保证每次补充的样本落在真实场景关心的区域,精度提升比单纯增加初始样本量划算得多。
2.3 Kriging、RBF、神经网络怎么选
元模型不是只有一种。我在MATLAB环境里主要比较过三种:Kriging(也叫高斯过程回归)、径向基函数RBF、前馈神经网络。它们各有各的脾气,选错了后面会有很多弯路。
| 维度 | Kriging / 高斯过程 | RBF | 神经网络 |
|---|---|---|---|
| 输入维度 | 中低维(15以下)表现好 | 中低维稳定 | 高维场景潜力大 |
| 是否提供预测方差 | 自带 | 不提供 | 一般不提供 |
| 训练成本 | 样本数百时中等 | 低 | 高,依赖调参 |
| 外推能力 | 超出训练区间会失真 | 同样失真 | 同样失真 |
| MATLAB实现 | fitrgp自带 | 需第三方或手写 | fitnet自带 |
| 适用阶段 | 初版首选 | 快速原型验证 | 数据量大后精修 |
我个人的建议是:第一版先选Kriging。原因不只是精度,而是它除了给出预测值,还能额外给出预测区间。这个预测方差是非常宝贵的调试信息——元模型在哪片区域不自信,方差就会明显变大,你可以精准地往那里补样本。RBF的好处是训练快、内存占用小,适合一开始不知道模型能不能收敛时快速跑通全流程。神经网络则更适合输入维度已经高到Kriging难以招架、且你有大量历史运行数据可以预训练的情况,达到这个阶段前不建议一上来就用。
关于Kriging的原理,直观理解就是“根据已知点的距离和平滑程度猜测未知点”。离已知样本近的位置,预测值高度依赖附近几个点的真实响应;距离拉远,预测方差变大、预测值回归到总体均值。这其实是一套很符合直觉的插值逻辑,只是高斯过程把它数学化了。
3. MATLAB代码实现:主从迭代与元模型训练的关键模块
3.1 代码框架:主循环、数据结构、模块划分
搭这种项目最忌讳的是把所有东西搓成一个巨型脚本。我推荐按四个模块分割:数据采样与数据集管理、真实下层求解器、元模型训练与预测、上层博弈迭代主循环。这样每一块都能单独调试,而且后面换元模型类型时只动一个模块。
数据集的组织方式是核心。我建议用一个MATLAB矩阵存所有训练样本,行是一个电价样本,前半段放电价向量,后半段放下层最优响应向量。实际编码时,不同分布式资源的响应变量最好分开存,方便单独训练多个元模型:
% 数据集:samplePi 是 N x dim 的矩阵 % sampleResp 是 N x (T*numRes) 的矩阵 % 每一行: 一个完整的电价方案对应的所有资源出力曲线 D = [samplePi, sampleResp];主循环的骨架可以抽象为几个关键步骤:初始化和生成初始样本、训练初始元模型、进入上层优化迭代、每次迭代先判断走“真实求解”还是“元模型预测”、周期性补充真实样本并重训、判断收敛并输出。
3.2 真实下层求解函数怎么封装
真实下层求解器是元模型的“老师”,必须保证它输出的响应是真正最优的,否则元模型学到的映射本身就是错的。我通常把每个分布式资源封装成一个独立函数:
function pOpt = solveLowerLevel(piVec, resourceIdx) % 输入:piVec 是 T x 1 的电价序列 % resourceIdx 是资源编号 % 输出:pOpt 是 T x 1 的最优出力/响应 % 内部调用该资源的真实优化模型,可以是linprog/intlinprog/fmincon ... end遇到储能这类有时间耦合约束的资源,要特别注意初始SOC、终止SOC、效率曲线这些参数必须在函数内部绑定好,避免在训练样本之间不一致。我踩过的坑是:训练样本用的储能初始SOC是50%,等到实际调度时初始SOC变成20%,元模型预测出来完全不准。解决方法是把SOC状态也作为元模型的输入维度,或者直接把训练和预测时的场景参数统一掉。
3.3 元模型训练与预测的MATLAB实现
MATLAB里最省事的是用统计与机器学习工具箱里的fitrgp。需要注意:fitrgp默认解决单输出回归问题,而我们的响应向量动辄几百维。正确做法是每列响应单独建模,而不是试图让一个模型输出全部变量:
% 假设训练输入 Xtrain (N x dim),第一列响应 Ytrain (N x 1) mdl = fitrgp(Xtrain, Ytrain, ... 'KernelFunction', 'ardsquaredexponential', ... 'Standardize', true, ... 'HyperparameterOptimization', 'none'); [ypred, ysd] = predict(mdl, Xquery); % ypred预测值,ysd预测标准差“ardsquaredexponential”这个核函数很推荐,它可以在训练时自动学习每个输入维度的相关长度。如果某个维度对响应几乎没影响,对应长度尺度会变得很大,模型就自动忽略它,这相当于内置了一层特征筛选。
多输出建模的循环我会写成这样,每个资源、每个时段单独一个模型。训练速度可以接受,因为每个模型只用N个样本做矩阵操作。如果样本量上了1000、响应维度又很高,我会先做主成分分析压缩响应维度,再针对主成分建元模型。
3.4 上层优化器与迭代终止逻辑
上层优化器我常用fmincon,因为电价上下限约束天然存在,可以写成边界约束。每次优化迭代内部调用的目标函数,必须改成基于元模型的快速评估,否则外层优化器每次梯度估计又会让计算量爆炸:
function cost = upperObj(piVec, mdlSet) % 用元模型快速预测每个资源的最优响应 for k = 1:numRes pPredict(:,k) = predict(mdlSet{k}, piVec); end % 根据预测响应计算上层收益,返回负值供最小化 cost = -calcUpperProfit(piVec, pPredict); end注意这里有个细节:上层优化器的内部循环调用的是元模型,而不是真实下层求解器。这正是计算量大幅下降的原因。整体主循环里再额外保留“周期性真实校验”的机制,比如每5轮或10轮用真实下层求解器算几个关键点,把预测误差反馈回来,误差超限就补充样本重训。
收敛终止条件我习惯设两条同时满足:电价向量变化量的无穷范数小于1e-4,且上层目标函数相对变化小于1e-3。如果只靠目标函数变化判断,很容易在平台期误判收敛;只靠电价变化,又可能在振荡尚未平息时就提前停掉。
3.5 如果不做元模型,KKT合并法的取舍
和元模型路线并列的还有一种经典解法:把下层优化问题的KKT条件作为约束并入上层,形成单层大优化。这个做法在理论上很漂亮,但有个硬前提——下层模型必须连续可微、凸性良好,且你能手动推导出完整的KKT系统。
实际虚拟电厂下层里,充电桩启停、储能禁止同时充放这些0/1变量,会把KKT推导变得非常痛苦;如果你还使用了第三方黑箱仿真器作为下层模型,KKT根本无从推导。元模型路线不挑下层内部结构,只要你能给出一组输入对应的真实最优响应,就具备训练条件。这也是我后来坚定选元模型路线的核心理由:适用于真实工程里各种“不能用纯数学规整”的场景。
4. 收敛性观察与均衡解的可靠性校验
4.1 三种“伪收敛”现象:振荡、漂移、假精确
主从博弈迭代不是天然会收敛到均衡的。我见过太多次“跑起来了但结果根本不敢用”的情况,典型有三类。第一类是电价序列在两个或多个值之间来回振荡,比如峰时电价一会儿1.2元一会儿0.9元,对应下层在充电和放电之间反复横跳,这种情况通常是上层目标函数在电价方向上有多个局部极大值,迭代法只是在不同极值之间来回切换。
第二类是漂移。电价每一次迭代只变化一点,方向却是慢慢朝边界推,一直推到上限或下限才停下。这时候要警惕:元模型在边界附近的预测可能严重失真,真实下层响应已经和预测不一致了,但外层优化器不知道。
第三类最隐蔽,叫“假精确”。迭代已停止,判据也满足了,但最后用真实下层复验时发现预测误差达到几个百分点。这种情况通常因为整个训练区间没有覆盖均衡点所在区域,元模型在均衡点附近是在做外推,表面光滑,实际偏差全藏在模型自由度里了。
4.2 收敛判据的设置与多起始点策略
针对振荡问题,我的做法是设置“连续稳定轮数”。即使电价变化已经很小,也要求连续3轮以上维持同一方向、同一量级的变化才宣布收敛。这个设计成本很低,但能过滤掉不少临界振荡。
针对多峰问题,最实用的策略是多起始点。随机生成8到10个不同的初始电价策略,分别跑完整套迭代,最终比较各起始点收敛到的上层收益,取收益最高的解作为输出。这一步本质上是在非凸问题里做全局搜索的工程近似。我在某项目里测试,单起始点得到的“均衡”在收益上比多起始点最优解低了将近12%,所以多起始点不是可选项,而应该直接写进主循环里。
4.3 元模型精度与均衡可信度的平衡线
元模型预测精度不是越高越好,因为精度追求有一个收益递减曲线。我常用的精度指标是归一化均方根误差NRMSE:真实响应与预测响应的均方根误差除以真实响应的变化范围。工程经验阈值大致如下:
- NRMSE小于1%,元模型对均衡解的影响基本可以忽略。
- NRMSE在1%到3%之间,均衡趋势可信,具体出力幅值需要用真实模型复验后修正。
- NRMSE大于5%,别继续往下跑了,先补样本或查训练区间覆盖问题。
这个阈值和上层收益函数的“光滑程度”有关。如果上层目标对下层出力非常敏感,比如外部电价波动剧烈时收益曲线很陡,那么哪怕2%的响应误差也会导致收益偏差放大到不可接受的程度。这种情况下我通常把阈值收到0.5%再启用元模型结果。
4.4 复验协议:用真实模型给答案盖章
无论元模型训练得多漂亮,对最终电价方案做一次真实下层求解是必须的。这个复验的成本只有一次,但价值巨大:
pTrue = solveLowerLevel(piFinal); % 用真实下层模型算一遍 pApprox = predictMetaModelAll(piFinal); % 元模型预测结果 nrmse = sqrt(mean((pTrue - pApprox).^2, 'all')) / (max(pTrue(:)) - min(pTrue(:)));如果这步NRMSE通过了,我才会把结果写入调度指令;通不过就回到“加点-重训”的循环。这套协议本质上是给元模型套上最后一道安全边际,避免数据驱动方法在关键决策点上“自信地犯错”。
5. 实操期间的坑与可以偷懒的地方
5.1 维度爆炸与时段耦合是两座大山
元模型最怕高维输入。电价值如果按“每个时段一个维度”展开,一天96个时段就是96维,再加多个资源参数,Kriging很快就不行了。我的经验是在建模阶段就把维度降下来,而不是等到训练失败才处理。
常用的降维手段有:把96个时段按负荷形态聚合成峰、平、谷三类,电价维度缩减到3到6个,再用调价系数还原到全时段;或者对每个分布式资源独立建映射,储能的输入电价维度只取其充放电相关的时段窗口,而不是全序列。我见过项目直接把96维电价输入一个高斯过程模型,训练时间成倍增长且预测方差大得离谱,根子就在于没有做维度控制。
5.2 边界外探:训练区间必须盖住均衡点
元模型的外推能力非常有限,这条几乎适用于所有代理模型。训练时电价区间设定在[0.3, 1.0]元/kWh,均衡电价很可能跑到1.1,那你在均衡点附近用的就是纯外推,预测结果基本靠猜。
我常用的办法是“两阶段训练”。第一阶段先用宽区间粗采样,让元模型大致找到均衡可能落在哪个子区域;第二阶段把采样区间收窄到均衡点附近再补一批样本,重训模型。这样既不浪费初始样本,又能把宝贵的采样预算集中在关键区域。实际操作中,第二阶段补50到80个样本往往就能让NRMSE明显改善。
5.3 多输出元模型的解耦训练
很多人第一次把“电价→所有资源出力”建模时,总想用一个模型输全部变量。这个思路听着统一,但实际训练时会遇到规模灾难和耦合噪声问题:单个资源建模误差会影响其他资源的预测训练。我建议坚决按资源解耦:储能一个模型、充电桩一个模型、温控负荷一个模型,每个模型只学自己的输入输出关系。资源之间即使存在物理耦合(比如共享变压器容量),这个约束放在上层校验环节去处理,不要试图让数据模型自己“悟”出来。
5.4 先用小规模版本跑通逻辑再上全维度
我每次搭新场景都会先砍规模:时段数从96缩到24甚至12,资源数量从几十个缩到三五个,先用这个小规模版本验证元模型逻辑是否正确、收敛判据是否合理。跑通之后再逐步把时段和资源数量拉满。别嫌这一步烦,它能在几分钟内暴露90%的逻辑错误,而全维度跑一次可能已经过去一小时了。
另外要注意,元模型的“学习对象”是真实下层求解器在特定参数下的最优响应,所以调度场景一变(比如储能SOC初值变了、EV数量变了),旧模型不能继续沿用。工程化做法是记录场景参数哈希,场景参数变化就触发重新采样和重新训练,自动避免旧模型带病上岗。
最后说一点我个人的体会。在这个项目里,真正让计算时间从“小时级”降到“分钟级”的,并不是某个特别新的算法,而是把“哪些东西该放进元模型、哪些东西必须保留真实物理约束”这件事想清楚了。下层物理模型必须真实,上层收益计算必须真实,中间那段嵌套迭代才能放心交给元模型去加速。每一步留个复验的口子,数据驱动的方法才敢在调度决策里落地。
再分享一个调试小技巧:每次更新完元模型,别急着继续迭代,先用10个随机新样本跑一遍真实下层求解器做交叉校验,把最大预测偏差打印出来。如果某个点的偏差突然比其他点大一个量级,十有八九是训练区间没盖住那个区域,直接在那个点附近补几个样本重训,比换模型、调参数都管用。这套流程多跑几个项目之后,你会慢慢形成对“哪里该信任元模型、哪里必须回到真实模型”的直觉,这种直觉才是这类方法实际项目的真正护城河。