ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

梯级水电与火电联合调度中的NSGA-III多目标优化及Matlab实现

梯级水电与火电联合调度中的NSGA-III多目标优化及Matlab实现 做电力调度优化这几年有一个很典型的场景水电来水不确定火电又必须顶上两边还不能只盯一个指标。把梯级水电和火电机组放进同一个优化框架里用NSGA-III去搜帕累托前沿是很多课题组和工程师都在做的方向。这个标题里其实有两个关键点一是“梯级水电和火电的联合调度”二是“NSGA-III的Matlab实现”。前者是问题建模后者是求解工具两者缺一不可。很多初学者拿到这类代码后最常见的反应是能跑通但不知道非支配排序、参考点关联这些模块为什么要放在一起或者反过来自己从头写NSGA-III却不清楚水电火电的约束该在哪里处理。这篇文章会把问题建模、算法原理、Matlab实现三个层面串起来讲把我在实际复现中踩过的坑也一并写上适合正在做电力系统调度、多目标优化方向的研究生以及想用Matlab快速上手NSGA-III的工程师。1. 课题拆解这个调度问题到底在算什么1.1 三个目标的冲突关系先解释为什么需要多目标算法。梯级水电指的是多个水库沿同一条河流串联布置上游水库的发电流量会流入下游水库所以各个电站不能独立运行下游水库的入库流量很大程度取决于上游的调度方式。火电机组则是传统煤电机组负责在负荷高峰或来水不足时补齐缺口。联合调度的目标通常不止一个最常规的设计是三个系统总煤耗成本最低、梯级水电的弃水量最小、火电总排放最低。这三个目标互相制约。水电多发火电就能少发煤耗自然下降但如果水库为了发电把水用得太多后续时段可能面临无水可用甚至为了防洪安全不得不强制泄水造成弃水。排放目标和煤耗目标方向相近但又不完全等同比如机组在极低负荷工况下运行单位电量的排放率会明显上升。这种互相制衡的组合就是典型的多目标优化问题不存在一个解让三个目标同时达到最优只存在一组互不支配的帕累托解集。1.2 为什么选择NSGA-III而不选传统加权法传统做法是把多目标加权成单目标求解但权重需要人为设定而且权重的小幅变化可能导致完全不同的调度方案。另一种做法是每次只优化一个目标把其他目标转成约束反复求解效率很低。NSGA-III的价值在于一次性得到一整条分布均匀的帕累托前沿让调度人员根据来水、负荷、环保要求等实际情况在解集里做选择。NSGA-III和NSGA-II最大的区别是把保持种群多样性的机制从拥挤度距离换成了参考点引导。这个改动让算法在三个及以上目标的问题上表现稳定得多。下面用一张表说明几种常见处理方式的特点。方法适用目标数主要问题是否适合本课题线性加权法2-3权重难定一次只能求一个解不推荐约束转化法2-3需要多次求解效率低不推荐NSGA-II2三目标以上拥挤度失效勉强可用NSGA-III3及以上实现复杂度较高推荐如果课题里只有两个目标比如只考虑煤耗和弃水用NSGA-II就够了。但本课题包含三个目标NSGA-III的参考点机制能更好地维持前沿的分布均匀性这也是标题里明确写NSGA-III的原因。2. 数学建模目标函数、约束条件和决策变量该怎样设计2.1 目标函数设计在Matlab里写代码之前必须先把数学模型写清楚。假设调度周期T为24小时火电机组数量为N_th梯级水电站数量为N_h三个目标函数可以写成如下形式。煤耗目标[ \min C \sum_{t1}^{T}\sum_{i1}^{N_{th}} f_i(P_{i,t}) ]其中P_{i,t}是第i台火电机组在t时段的出力f_i是煤耗特性曲线工程上通常用二次函数拟合[ f_i(P_{i,t}) a_i P_{i,t}^2 b_i P_{i,t} c_i ]弃水目标[ \min W \sum_{t1}^{T}\sum_{j1}^{N_h} S_{j,t} ]S_{j,t}是第j个水电站在t时段的弃水流量。弃水意味着水资源没有被用来发电属于一种浪费。排放目标[ \min E \sum_{t1}^{T}\sum_{i1}^{N_{th}} g_i(P_{i,t}) ]g_i通常是排放系数乘以煤耗量或者用单独的二次函数拟合。实际项目中如果拿不到排放数据可以直接用总煤耗的某个比例估算但学术研究中最好使用真实机组参数。2.2 约束条件清单约束条件需要从系统、火电、水电三个层面分开整理。下面这张表可以当作写代码时的检查清单每条约束对应Matlab程序里的一个判断模块。约束类型数学表达式作用系统功率平衡ΣP_th ΣP_hydro P_load每时段发电与负荷必须相等火电出力上下限P_min ≤ P_i,t ≤ P_max机组物理运行边界火电爬坡约束-R_down ≤ P_i,t - P_i,t-1 ≤ R_up机组出力变化速率限制水电发电流量范围Q_min ≤ Q_j,t ≤ Q_max发电流量上下限水库库容范围V_min ≤ V_j,t ≤ V_max水库水位安全区间水量平衡V_j,t V_j,t-1 I_j,t Q_prev - Q_j,t - S_j,t上下游水库间的水量传递出库流量约束Q_out_min ≤ Q_j,t S_j,t ≤ Q_out_max下游生态与防洪要求这里面最需要理解的是水量平衡。上游水库下泄的水经过一段水流延迟后进入下游水库所以下游水库的入库流量不是天然来水而是上游出库流量加区间来水。如果上游把水拦住了下游可能缺水如果上游集中泄水下游可能被迫弃水这就是梯级调度“牵一发而动全身”的难点。水电出力与发电流量、水头的关系在程序中也要单独建模常用公式[ P_{j,t} 9.81 \times \eta_j \times Q_{j,t} \times H_{j,t} ]其中η_j是机组综合效率H_{j,t}是发电水头。严格说水头又取决于水库水位水位又取决于库容库容又取决于水量平衡形成一条完整的耦合链。很多新手代码里只把水电出力当成一个变量直接约束上下限忽略了水头和库容的联动结果算出来的方案在真实水库里根本无法执行。2.3 决策变量编码与解空间规模决策变量的选择直接影响NSGA-III的搜索难度。一种常见的编码方式是把每台火电机组每时段的出力、每个水电站每时段的出库流量拼成一个向量。假设有10台火电机组、4个水电站、24个时段决策变量维度就是[ (10 4) \times 24 336 ]336维的解空间已经非常大。更麻烦的是这种编码方式产生不可行解的概率极高因为几乎是随机填的变量值很难同时满足功率平衡、库容范围、爬坡约束。所以代码里必须配套约束处理方法要么惩罚要么修复后面会专门展开。我个人的建议是如果条件允许尽量把水电站的决策变量设计成“出库流量”而不是“出力”。因为出力需要由流量和水头推算直接编码出力很难保证水量平衡反过来确定出库流量后水量平衡和库容变化可以按时间递推计算很多不一致的解在评价函数里就能被自然识别出来。火电侧则直接编码出力区间内的数值让功率平衡约束通过惩罚处理。3. NSGA-III算法核心原理从拥挤度到参考点3.1 NSGA-II的拥挤度为什么在多目标上失效NSGA-II通过非支配排序把种群分成不同的前沿层级然后在同一层里用拥挤度距离来保证多样性。拥挤度距离的思路是某个个体前后两个邻居在各目标方向上的距离之和越大说明它周围越稀疏应该优先保留。问题在于拥挤度距离本质上是在一维方向上衡量稀疏程度当目标数从两个增加到三个甚至更多时种群中非支配个体的比例会急剧上升同层个体的数量非常大拥挤度距离无法准确反映高维空间里的真实分布。结果就是进化后期种群容易聚集在某些目标轴的局部区域丢失前沿的其他部分。NSGA-III改用的参考点方式是专门为应对这种“维度灾难”设计的。3.2 参考点生成与个体关联NSGA-III的核心步骤可以拆成三块生成参考点、归一化目标值、关联与选择。参考点生成通常在算法开始前完成。三维目标空间里参考点落在归一化超平面上数量由分割数H决定。对于M个目标参考点数量通过组合数计算[ N_{ref} \binom{HM-1}{M-1} ]如果M3且H12参考点数量就是C(14,2)91。这些参考点在理想情况下均匀分布是算法“期望”得到的前沿位置。归一化是很多自写代码容易忽略的环节。三个目标的量纲差别巨大煤耗可能是几万吨弃水可能是几十万立方米排放可能是几百吨。如果直接用原始目标值计算个体到参考点的距离量纲大的目标会彻底主导距离计算量纲小的目标形同虚设。正规做法是先找理想点和极值点求出每个目标方向上的截距把所有目标值归一化到[0,1]区间。关联机制的关键不是普通欧氏距离。每个参考点从原点出发形成一条参考方向计算个体到这条参考方向的垂直距离距离最近的参考点就是该个体所属的小生境。之后统计每个参考点关联了多少个体优先从关联个体数少的小生境里选个体这样能保证前沿在参考点附近都有人分布不会扎堆。3.3 主循环流程与关键算子NSGA-III的每一代主循环流程可以这样理解父代种群通过交叉变异生成子代父子两代合并然后做非支配排序分层。前面的分层直接进入下一代只有到达临界分层时才动用参考点机制挑选分层中的个体直到种群数目恢复N。这个“先分层、后小生境”的设计非常巧妙。非支配排序保证收敛性让解不断向前沿逼近参考点小生境保证多样性让前沿铺满整个空间。两者缺一不可。实数编码的调度问题推荐使用模拟二进制交叉和多项式变异这两种算子专为连续变量设计在Matlab中实现也简单。核心循环的Matlab思路如下for gen 1:maxGen % 父代选择锦标赛选择 parentIdx tournamentSelection(population); % 交叉变异生成子代 offspring sbxCrossover(population(parentIdx,:), crossoverProb); offspring polynomialMutation(offspring, mutationProb); % 评价子代 offspringFitness evaluateObjective(offspring); % 合并父子种群 combinedPop [population; offspring]; combinedFit [fitness; offspringFitness]; % 非支配排序 环境选择 [population, fitness] environmentalSelection(combinedPop, combinedFit, N, refPoints); end代码里的environmentalSelection就是NSGA-III最复杂的地方先分前沿再归一化再做参考点关联最后用小生境计数组装下一代。4. Matlab代码实现的框架与关键函数写法4.1 程序模块与数据结构设计拿到课题后不要直接开写主函数先把程序模块拆出来。一个结构清晰的项目至少应该包含以下文件。文件名功能nsga3_main.m主程序入口负责参数设置和循环problem_data.m定义火电、水电、负荷等基础数据evaluate_objective.m解码个体并计算三个目标值nondominated_sort.m非支配排序返回前沿层编号reference_point_generation.m生成参考点集合normalize_objective.m目标值归一化求截距associate_to_reference.m计算个体与参考点的关联关系niching_select.m小生境选择从临界层中补齐个体sbx_crossover.m模拟二进制交叉polynomial_mutation.m多项式变异数据结构建议全部用struct封装。火电机组参数可以定义成struct数组比如th(i).Pmin、th(i).Pmax、th(i).RampUp水电站参数类似但要额外加一个上游关系矩阵用于描述梯级拓扑。把数据从代码逻辑里分离出来后续换机组数据或者增加风电场时只需要改动problem_data文件。4.2 核心函数代码逐段拆解先看非支配排序。这一步的目的是给每个个体分配一个前沿层编号。简单但直观的Matlab实现可以是function front nondominated_sort(objVals) % objVals: N行M列, 每行是一个个体的各目标值 N size(objVals, 1); dominatedCount zeros(N, 1); dominatedSet cell(N, 1); front zeros(N, 1); currentFront []; for p 1:N for q 1:N if p q, continue; end if all(objVals(p,:) objVals(q,:)) any(objVals(p,:) objVals(q,:)) dominatedSet{p} [dominatedSet{p}, q]; elseif all(objVals(q,:) objVals(p,:)) any(objVals(q,:) objVals(p,:)) dominatedCount(p) dominatedCount(p) 1; end end if dominatedCount(p) 0 front(p) 1; currentFront [currentFront, p]; end end rank 1; while ~isempty(currentFront) nextFront []; for p currentFront for q dominatedSet{p} dominatedCount(q) dominatedCount(q) - 1; if dominatedCount(q) 0 front(q) rank 1; nextFront [nextFront, q]; end end end rank rank 1; currentFront nextFront; end end这段代码的复杂度是O(N^2)种群不大时完全够用。但注意这里只做了排序没有保留每层个体的索引实际项目中建议返回cell数组fronts每一层存一个索引列表方便后续环境选择直接按层操作。再看参考点关联的简化版。真正的NSGA-III要求沿参考方向的垂直距离而不是直接算欧氏距离下面这段代码体现的是核心逻辑。function [assoc, dist] associateToRef(normalizedObj, refPoints) % normalizedObj: N x M, refPoints: R x M N size(normalizedObj, 1); R size(refPoints, 1); dist zeros(N, R); for i 1:N for j 1:R refVec refPoints(j,:) / norm(refPoints(j,:)); projection dot(normalizedObj(i,:), refVec) * refVec; perpendicular normalizedObj(i,:) - projection; dist(i,j) norm(perpendicular); end end [dist, assoc] min(dist, [], 2); % assoc: 每个个体最近参考点编号 end这里的核心是投影计算。个体到参考线的距离越近说明它越接近这条参考方向。要注意如果参考点生成时没有归一化这里需要先处理。4.3 Matlab环境里的实际坑位我在实际跑这类代码时遇到过不少环境问题这里挑几个典型的说。新版Matlab对并行计算的处理方式经常让人头疼。很多人在主循环里写了一句parfor准备加速适应度评价结果一运行就提示“no parallel pool”。这通常是并行池没有启动或者并行计算工具箱配置有问题。解决方法是先手动执行parpool(local)或者把parfor改成普通for先确保逻辑正确。并行池不是越大越好种群只有一两百时开8个worker带来的通信开销可能比串行还慢建议先用串行调试再针对评价函数做性能分析。另一个高频问题是函数命名冲突。自写代码里尽量不要把函数命名为fitness、evaluate这种通用词容易和工具箱内置函数或脚本变量冲突。我见过有人定义了一个evaluate.m结果调用了半天都是内置的另一个函数数据全错但程序不报错。版本差异同样要留意。在比较新的Matlab版本中有些老接口会被移除或者行为改变。比如处理license激活异常时出现的“license manager error -8”基本是许可证文件或环境变量的问题和算法代码无关。遇到这类报错先检查系统环境不要怀疑自己的算法逻辑。性能优化上建议把24时段、10台火电这种循环尽量向量化。比如计算煤耗时不要一个机组一个时段地算而是用矩阵一次性算完全部时段再sum起来速度提升会很明显。进化算法要跑几十万次评价慢一倍就意味着多等几个小时。5. 约束处理与参数调优让代码真正跑出可用结果5.1 惩罚函数先归一化再罚而不是罚系数直接拉到10000约束处理是整个课题里最影响结果质量的部分。最常见的做法是惩罚函数法把约束违反量加到目标函数里[ F_i F_i \lambda \sum \max(0, g_j(x)) \mu \sum |h_k(x)| ]其中g_j是不等式约束h_k是等式约束。很多初学者喜欢把λ设成1e8认为罚得越狠越好。实际上罚系数过大种群会被强行压到可行域边界搜索空间极其有限最终得到的帕累托前沿会非常窄。罚系数太小不可行解大量混入前沿可能整体偏离真实可行域。我给一个实用经验先把所有目标归一化到同一个量级再让惩罚项也以相对比例形式出现。比如功率不平衡量的惩罚可以写成“不平衡功率占系统总负荷的比例”乘上当前代的平均目标值。这样惩罚系数只需要在0.1到10之间调整不用反复试1e3还是1e8。还有一个容易被忽略的细节不要把等式约束和不等式约束混在一起用同一个罚系数。功率平衡是等式约束应该用平方项惩罚爬坡约束是区间不等式用线性过度罚就行。混合使用容易让算法把精力花在不该花的地方。5.2 参数设置表与调参经验NSGA-III的参数设置直接影响算法性能。下面是我常用的初始参数范围可以直接抄。参数建议取值说明种群大小N200-400与参考点数量保持同量级最大迭代次数300-1000个体维度越大代数越多交叉概率0.8-0.95太高破坏优秀个体太低收敛慢变异概率1/DD为决策变量维数每个变量平均变异一次参考点分割数H12-16三维目标对应91-153个参考点锦标赛规模2常见选择保持选择压力适中调参的一个通用方法是先跑小规模问题比如把24时段换成6时段把10台火电换成3台在这个规模上验证算法逻辑和约束处理是否正确。逻辑没问题之后再放大到完整规模这时候调参才有意义。否则你根本分不清是参数不对还是bug没除完。我评估算法表现时除了看帕累托前沿图还会计算超体积指标HV。HV值反映了前沿在目标空间覆盖的体积能综合衡量收敛性和多样性。不同参数组合跑完后比较HV值比肉眼看图可靠得多。5.3 调参中容易踩的坑第一个坑是决策变量范围设计不合理。如果火电出力的上下限范围设得太宽而负荷曲线离开边界很远那初始种群大量个体都会落在功率不平衡的惩罚区搜索效率极低。一个有效的技巧是用“启发式初始化”先按负荷比例分配火电和水电出力生成一小部分可行或接近可行的个体其余个体再随机生成。这样初始种群的质量明显更高。第二个坑是参考点数量与种群大小的搭配。参考点太多而种群太小大量小生境只有一个甚至没有个体选择压力分散参考点太少而种群太大多样性又不够。一般建议参考点数量略小于种群大小留出少量弹性。我自己做这个课题时的经验是即使NSGA-III算法实现完全正确如果约束处理不合理最后得到的帕累托前沿也可能非常难看。先修约束处理再调参数这个顺序不能反。6. 结果分析与扩展帕累托前沿怎么用代码还能怎么改6.1 结果可视化与折中解选取Matlab里画三维帕累托前沿非常方便用scatter3即可。三个轴分别对应煤耗、弃水、排放。正常情况下前沿应该是一条在三维空间里展开的曲面越靠近原点说明解越好。只有一条前沿还不够调度中往往需要挑一个折中解。最简单的方法是找出“距离理想点最近的解”。理想点就是每个目标独立最优时组成的坐标点计算每个解与理想点的归一化欧氏距离距离最小者就是折中解。这个方法实现简单适合作为Matlab代码里的默认选解逻辑。如果做学术研究通常还需要对比算法性能。比如把NSGA-II和NSGA-III跑同样次数对比两者的IGD指标或者超体积指标。IGD需要知道真实前沿实际工程问题里没有可以用所有算法最终结果的并集近似当作真实前沿。6.2 从静态调度走向动态调度的扩展思路这套代码后续可以往两个方向扩展。一是加入新能源把风电或光伏的出力预测序列作为负的负荷叠加到功率平衡约束里目标函数中增加弃风弃光的惩罚项。这个扩展在建模层面很简单难点在于新能源不确定性的刻画。二是做滚动优化把24小时静态调度改成每个小时滚动求解一次用当前时刻的真实来水更新预测重新跑NSGA-III这就从静态优化变成了带反馈的动态调度。另外如果研究需要求解更大规模的问题比如上百台机组那NSGA-III的O(N^2)非支配排序会成为瓶颈可以考虑引入分布式评价或者改用更高效的排序实现。但对绝大多数课题来说Matlab加上合理的向量化已经足够。最后再分享一个小技巧在代码里固定随机种子比如rng(42)这样每次运行结果完全可复现。做参数对比实验时不同参数组合都从同一组初始种群出发比较才公平。我在最初调试时常常因为随机性把不同版本的结果拿来对比得出错误结论固定种子之后这个问题彻底消失了。
返回列表