ARTICLE DETAIL

资讯详情

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

基于NSGA-II的水光互补优化调度:Python实现与Pareto前沿分析

基于NSGA-II的水光互补优化调度:Python实现与Pareto前沿分析 先说结论如果你手里有一个水电站旁边还架了一大片光伏板那每天调度最头疼的事就是——白天光伏出力哗哗往上冲负荷曲线却不一定跟得上到了傍晚光伏突然归零水电站又得在半小时内硬顶上去。单纯以“发电量最大”或“成本最低”为目标做调度根本压不住这种波动。我去年接了一个水光互补优化调度的研究项目用的就是基于非支配排序遗传算法NSGA-II的Python实现把多目标问题的两个核心诉求——经济收益最大化和出力波动最小化——同时放进优化框架里最后输出一组Pareto前沿方案供调度员挑选。这篇博文我会把模型怎么建、目标函数怎么写、NSGA-II怎么实现、实际调试踩过哪些坑完整拆开讲一遍。适合正在做水电、新能源调度或刚接触多目标优化的读者参考Python代码可以直接改数据跑起来。1. 水光互补调度到底在解决什么问题1.1 为什么单目标调度不够用传统的水电站调度目标函数很单一要么发电量最大要么整个调度期的运行成本最低所有约束都是为了把这个单目标压到最优。这种方式在“水电站自己发电、负荷基本稳定”的场景下没有问题但光伏一旦大规模接入情况就变了。光伏出力在一天之内呈现典型的倒U型曲线上午爬坡、中午峰值、下午急速下降夜间直接归零。如果水电站还是只盯着“多发几度电”那光伏峰值时段水电还在满发会导致严重的弃光而傍晚光伏骤减时水电又需要瞬间抬高出力去补缺口爬坡速率可能超出机组调节能力对电网安全非常不利。所以我把这个问题重新定义了一下调度方案不能只看发电量或成本还要看联合出力曲线是否平滑、是否满足电网对爬坡率的考核要求。这就是典型的双目标优化问题——目标1是经济收益最大目标2是出力波动最小。两个目标之间存在冲突要收益最大往往希望水电在电价高的时候多发电但这可能和“平滑出力”的需求打架。多目标优化的意义就是找出所有可能的折中方案而不是强行加权成一个数。1.2 水光互补的物理基础与调度窗口水光互补能成立靠的是两类电源在时间尺度上的互补特性。光伏出力受天气影响具有强随机性和间歇性短时间内的波动可以非常大水电机组则具备快速启停、快速升降负荷的能力从零到满发通常只需要几十秒到几分钟正好用来填补光伏的波动。从全天时间尺度看水电还能把光伏午间的“超额电量”转化为水库蓄水留到晚间光伏归零时再放水发电相当于一个天然的储能系统。调度周期我采用的是日前调度时间尺度取24个时段每个时段1小时。对于项目前期研究和方案比选来说这个粒度足够。如果到实际工程落地可能需要细化到15分钟甚至5分钟的滚动调度但模型框架是完全一致的只需要把时段数改掉。这里要提醒一点调度窗口越细决策变量越多NSGA-II的收敛速度会慢很多后面我会讲怎么处理这种计算压力。1.3 问题建模输入、决策变量、输出一个完整的水光互补优化调度模型需要先想清楚三件事。输入数据方面最少需要四类光伏预测出力曲线、径流来水预报或者给定日来水量、负荷预测曲线、分时电价。如果有条件还可以加入水库蒸发、生态流量约束等但对初版模型可以先简化。决策变量是优化算法直接操作的“旋钮”。我这里设计了两组第一组是水电站每个时段的发电流量一共24个变量第二组是光伏每个时段的实际上网功率也是24个变量。总共48维决策空间。为什么把光伏上网功率也作为决策变量而不是直接用预测值因为实际调度中光伏可以主动弃光当系统无法消纳时宁可少发一部分来换取整体收益或稳定性。这个“弃光多少”本身就是一个需要优化的决策。输出结果不是一组最优解而是一组Pareto非支配解集。每一个解都包含完整的24时段水电出力过程、光伏上网过程、库容变化过程、弃水量等决策者可以根据电网实际运行状态从中挑选最合适的一套方案。2. 多目标优化与NSGA-II的原理剖析2.1 Pareto支配与多目标解集多目标优化里有个核心概念叫支配。假设有两套调度方案A和B如果A的经济收益大于等于B同时A的出力波动小于等于B两组指标里至少有一个是严格优于那么就说A支配B。所有不被其他方案支配的方案集合就叫Pareto前沿。这些方案之间是各有利弊的A方案收益高但波动大B方案波动小但收益低二者无法直接比较谁更好只能看运行场景需要什么。我常跟人打比方买房子A是离公司近但贵B是便宜但通勤远C是又贵又远——C被淘汰A和B都在Pareto前沿上选哪个取决于你的偏好。优化算法的目标就是把这一整条前沿找出来而不是只给你一个“最优”。这在电力调度中特别重要因为目标偏好往往取决于电网运行方式、季节、天气等动态因素提前算好整条前沿调度员才能临场灵活决策。2.2 NSGA-II的三个关键机制NSGA-II全称Non-dominated Sorting Genetic Algorithm II是非支配排序遗传算法的第二代版本2002年提出至今还在工业界和研究机构里广泛使用。它比第一代的核心改进有三点。第一是快速非支配排序。每一代种群里的所有个体按支配关系分成若干层级第一层是不被任何个体支配的第二层是被第一层支配但能支配其余个体的以此类推。算法优先保留层级靠前的个体层级相同的再比拥挤度。这个排序算法的时间复杂度是O(MN²)M是目标数量N是种群规模在电站调度这种维度下完全够用。第二是拥挤度距离。同一层级的个体如果彼此挤在一起多样性就差。拥挤度计算的是某个个体周围最近邻居的距离之和距离越大说明这个个体周围越空旷越有保留价值。这一步保证了Pareto前沿能够相对均匀分布不会都堆在某个角落。第三是精英保留策略。每一代把父代和子代合并成2N大小的种群先按非支配层级排序再按拥挤度排序逐层填充进下一代直到填满N个。这个过程确保最好的个体永远不会丢失所以算法收敛性比第一代好了不少。2.3 为什么选NSGA-II而不是其他算法我刚开始做这个项目的时候也纠结过是不是用MOPSO多目标粒子群或者直接用加权和法转成单目标后来权衡下来还是选了NSGA-II。原因是多方面的。加权和法最简单但它对Pareto前沿的凹形区域会失效而且权重系数很难拍调参成本高本质上还是没有跳出单目标思维。MOPSO收敛快、代码也不难写但粒子群算法在离散变量和复杂约束处理上不如遗传算法自然水光互补里有大量水位、流量边界约束用遗传算法加修复策略要顺手得多。NSGA-III虽然在高维目标问题上表现更好但参数设置和参考点生成都比较复杂对只有两个三个目标的水光调度来说属于杀鸡用牛刀。NSGA-II的优势在于参数少且经验成熟种群大小、交叉概率、变异概率都有大量公开的参考值遗传算法的编码方式灵活能直接处理连续变量和离散变量混合的问题而且Python生态里有很多现成实现可以借鉴改造起来快。对于实际工程来说它还足够稳定不容易出现某次运行结果完全不可用的情况。3. Python实现从模型到代码3.1 环境准备与数据组织运行环境我建议用Python 3.8以上版本。我自己用的是Anaconda管理的虚拟环境核心依赖就三个numpy做数组运算、matplotlib画Pareto前沿、pandas可以顺手处理结果表格。如果电脑上没有装numpy直接执行pip install numpy就可以。对于纯研究场景不需要装pytorch、tensorflow那套重型依赖轻量环境跑得快调试周期也短。数据组织我建议全部用numpy数组不要用Python原生的list去管理决策变量。因为后面做交叉、变异、排序都是批量运算numpy的向量化操作能省下大量时间。输入数据我统一封装成一个Python字典比如来水过程、光伏预测、分时电价都放在里面后面目标函数直接引用这个字典避免函数参数传得乱七八糟。3.2 目标函数与约束的实现我先说目标函数的数学形式。第一个目标经济收益最大化我把它写成收益 水电售电收入 光伏上网收入 - 弃光惩罚 - 弃水惩罚弃光惩罚的意思是光伏预测出来了但你主动砍掉一部分虽然省下了对电网的冲击但浪费了清洁能源所以要在收益里扣一笔。弃水惩罚同理水量超出发电能力只能溢流溢流就相当于丧失了这部分发电收益。第二个目标出力波动最小化我取相邻时段联合出力差的平方和表达式是Σ(P_total[t1] - P_total[t])²平方项天然放大了斜率大的惩罚和电网对爬坡率的考核逻辑一致。水电出力按经典公式估算P_h 9.81 × η × H_net × Q。其中η是机组综合效率我取0.85H_net是净水头由库容查水位库容曲线得到Q是发电流量。水位库容关系我简化成线性插值表实际工程中从设计资料里能直接抄到那条曲线。水量平衡方程是V(t1) V(t) I(t) - Q(t) - S(t)S(t)是弃水流量。约束包括库容上下限、发电流量上下限、光伏出力上下限、生态流量下限最后还有一个“期末库容接近初始库容”的约束保证调度策略可以周期性复用。约束处理方式我强烈建议用修复策略而不是惩罚函数。惩罚函数在NSGA-II里容易把种群带偏尤其是多个约束同时违反时惩罚系数极难调。修复策略的思路是对每个个体检查约束超限就直接拉到边界然后再用流量平衡反推库容保证水量守恒。import numpy as np # 系统参数 T 24 # 时段数小时 num_dec 2 * T # 决策变量维度发电流量(24) 光伏上网功率(24) num_pop 120 # 种群规模 max_gen 400 # 迭代代数 cross_prob 0.9 # 交叉概率 mut_prob 0.1 # 变异概率 eta_c 20 # SBX交叉分布指数 eta_m 20 # 多项式变异分布指数 # 水电站参数 eta 0.85 # 机组效率 g 9.81 Q_min, Q_max 10.0, 180.0 # 发电流量下限/上限 (m3/s) V_min, V_max 5.0e6, 3.0e7 V_init 1.5e7 V_target V_init S_max 50.0 # 最大弃水流量 # 光伏参数 P_pv_max 120.0 # 光伏装机容量 MW W 60.0 # 水库面积换算参数简化用 # 输入数据示例实际替换为预测数据 pv_forecast np.array([0,0,0,0,0.05,0.15,0.35,0.6,0.85,1.0,1.05,1.02, 1.0,0.95,0.8,0.6,0.4,0.2,0.08,0,0,0,0,0]) * P_pv_max inflow np.array([30]*24) # 入库流量 m3/s price np.array([0.5]*8 [1.2]*8 [0.8]*8) # 分时电价 元/kWh def water_level_curve(V): 水位库容线性关系简化为H a*V b a 0.0008 / 1e7 b 15.0 return a * V b def water_balance(Q, S, inflow, V0): 按水量平衡反推库容序列 V np.zeros(T 1) V[0] V0 for t in range(T): V[t 1] V[t] inflow[t] - Q[t] - S[t] return V[1:] def repair(individual): 修复决策变量保证水量平衡和边界约束 Q np.clip(individual[:T], Q_min, Q_max) pv np.clip(individual[T:], 0, pv_forecast) # 假设弃水只在库容越上限时发生 S np.zeros(T) V V_init for t in range(T): V_new V inflow[t] - Q[t] if V_new V_max: S[t] V_new - V_max V_new V_max V V_new # 若期末库容明显超出目标调整后期流量简单启发式修正 V_end V diff V_end - V_target if diff 0: idx np.argsort(Q)[-5:] # 找最大的5个流量时段削掉 Q[idx] np.maximum(Q[idx] - diff / 5 / 3600, Q_min) elif diff -50000: idx np.argsort(Q)[:5] # 最小的5个流量时段补上去 Q[idx] np.minimum(Q[idx] - diff / 5 / 3600, Q_max) return np.concatenate([Q, pv]) def evaluate(ind): 返回两个目标函数值收益取负为最小化和波动 Q ind[:T] pv ind[T:] # 水量平衡和库容 V np.zeros(T 1) V[0] V_init S np.zeros(T) P_h np.zeros(T) H np.zeros(T) for t in range(T): V[t 1] V[t] inflow[t] - Q[t] if V[t 1] V_max: S[t] V[t 1] - V_max V[t 1] V_max H[t] water_level_curve((V[t] V[t 1]) / 2) P_h[t] 9.81 * eta * H[t] * Q[t] / 1000 # MW # 收益目标元 revenue_hydro np.sum(P_h * price * 1000) # 1MW*h 1000kWh revenue_pv np.sum(pv * price * 1000) penalty_curtail np.sum(pv_forecast - pv) * 0.35 * 1000 # 弃光惩罚 penalty_spill np.sum(S) * 5.0 # 弃水惩罚 profit revenue_hydro revenue_pv - penalty_curtail - penalty_spill f1 -profit # 优化为最小化 # 波动目标相邻时段联合出力变化平方和 P_total P_h pv f2 np.sum(np.diff(P_total) ** 2) return f1, f23.3 NSGA-II核心代码实现算法主体分四大块快速非支配排序、拥挤度计算、锦标赛选择、交叉变异。这里我把快速非支配排序和拥挤度合在一段代码里写主循环单独拎出来方便你分块替换。def fast_non_dominated_sort(f1, f2): 返回每个个体所在的Pareto层级列表 N len(f1) dominates [set() for _ in range(N)] dominated_count np.zeros(N, dtypeint) front [[]] rank np.zeros(N, dtypeint) for p in range(N): for q in range(N): if p q: continue # 是否p支配q p_dom_q (f1[p] f1[q] and f2[p] f2[q]) or \ (f1[p] f1[q] and f2[p] f2[q]) q_dom_p (f1[q] f1[p] and f2[q] f2[p]) or \ (f1[q] f1[p] and f2[q] f2[p]) if p_dom_q: dominates[p].add(q) elif q_dom_p: dominated_count[p] 1 if dominated_count[p] 0: rank[p] 0 front[0].append(p) i 0 while len(front[i]) 0: next_front [] for p in front[i]: for q in dominates[p]: dominated_count[q] - 1 if dominated_count[q] 0: rank[q] i 1 next_front.append(q) i 1 front.append(next_front) front.pop() # 去掉空层级 return front, rank def crowding_distance(front_idx, f1, f2): 计算给定前沿的拥挤度 m len(front_idx) if m 2: return np.full(m, np.inf) dist np.zeros(m) arr1 f1[front_idx] arr2 f2[front_idx] order np.argsort(arr1) dist[order[0]] np.inf dist[order[-1]] np.inf if arr1[-1] - arr1[0] 0: pass else: for j in range(1, m - 1): dist[order[j]] (arr1[order[j 1]] - arr1[order[j - 1]]) / (arr1[-1] - arr1[0]) order np.argsort(arr2) dist[order[0]] np.inf dist[order[-1]] np.inf if arr2[-1] - arr2[0] 0: pass else: for j in range(1, m - 1): dist[order[j]] (arr2[order[j 1]] - arr2[order[j - 1]]) / (arr2[-1] - arr2[0]) return dist def tournament_selection(pop, f1, f2, rank, dist): 二元锦标赛选择返回两个父代索引 parent_idx [] for _ in range(2): i1, i2 np.random.choice(len(pop), 2, replaceFalse) # 层级优先其次拥挤度 if rank[i1] rank[i2]: winner i1 elif rank[i1] rank[i2]: winner i2 else: winner i1 if dist[i1] dist[i2] else i2 parent_idx.append(winner) return parent_idx def sbx_crossover(p1, p2): 模拟二进制交叉 child1, child2 p1.copy(), p2.copy() if np.random.rand() cross_prob: return child1, child2 for i in range(len(p1)): if np.random.rand() 0.5: u np.random.rand() if u 1e-10: beta 1e-10 elif u 0.5: beta (2 * u) ** (1 / (eta_c 1)) else: beta (2 * (1 - u)) ** (-1 / (eta_c 1)) child1[i] 0.5 * ((1 beta) * p1[i] (1 - beta) * p2[i]) child2[i] 0.5 * ((1 - beta) * p1[i] (1 beta) * p2[i]) return child1, child2 def polynomial_mutation(ind): 多项式变异 ind ind.copy() for i in range(len(ind)): if np.random.rand() mut_prob: u np.random.rand() if u 0.5: delta (2 * u) ** (1 / (eta_m 1)) - 1 else: delta 1 - (2 * (1 - u)) ** (1 / (eta_m 1)) ind[i] delta return ind3.4 主循环与结果导出主循环的写法非常固定但有两个细节我必须强调第一每一代评估完目标函数后要顺手把个体和目标值归档到历史集中方便后面画收敛过程第二生成子代时一定要对交叉变异后的个体再次执行repair函数否则大量不可行解会污染种群。# 初始化种群 pop np.random.rand(num_pop, num_dec) # 缩放初始化范围前T维流量范围后T维光伏范围 for i in range(num_pop): pop[i, :T] Q_min (Q_max - Q_min) * pop[i, :T] pop[i, T:] pv_forecast * pop[i, T:] * 1.0 pop[i] repair(pop[i]) # 评价初始种群 f1 np.zeros(num_pop) f2 np.zeros(num_pop) for i in range(num_pop): f1[i], f2[i] evaluate(pop[i]) history_f1 [] history_f2 [] best_front None for gen in range(max_gen): # 生成子代 offspring [] while len(offspring) num_pop: pa, pb tournament_selection(pop, f1, f2, rank, dist) c1, c2 sbx_crossover(pop[pa], pop[pb]) c1 polynomial_mutation(c1) c2 polynomial_mutation(c2) c1 repair(c1) c2 repair(c2) offspring.append(c1) if len(offspring) num_pop: offspring.append(c2) offspring np.array(offspring[:num_pop]) # 评价子代 f1_child np.zeros(num_pop) f2_child np.zeros(num_pop) for i in range(num_pop): f1_child[i], f2_child[i] evaluate(offspring[i]) # 合并父代与子代 combined_pop np.vstack([pop, offspring]) combined_f1 np.concatenate([f1, f1_child]) combined_f2 np.concatenate([f2, f2_child]) # 非支配排序 拥挤度取前num_pop个 fronts, rank fast_non_dominated_sort(combined_f1, combined_f2) new_pop [] new_f1 [] new_f2 [] for f_idx in fronts: dists crowding_distance(f_idx, combined_f1, combined_f2) order np.argsort(-dists) for j in order: if len(new_pop) num_pop: idx f_idx[j] new_pop.append(combined_pop[idx]) new_f1.append(combined_f1[idx]) new_f2.append(combined_f2[idx]) pop np.array(new_pop) f1 np.array(new_f1) f2 np.array(new_f2) if (gen 1) % 50 0: front0_mask np.zeros(num_pop, dtypebool) fronts, _ fast_non_dominated_sort(f1, f2) front0 fronts[0] best_front_f1 f1[front0] best_front_f2 f2[front0] history_f1.append(best_front_f1.copy()) history_f2.append(best_front_f2.copy()) print(f第{gen1}代: Pareto解数量{len(front0)}) # 最终前沿 fronts, _ fast_non_dominated_sort(f1, f2) front0 fronts[0] pareto_f1 f1[front0] pareto_f2 f2[front0] # 保存方案到CSV import pandas as pd best_solutions [] for idx in front0: Q pop[idx, :T] pv pop[idx, T:] best_solutions.append({ profit: -f1[idx], fluctuation: f2[idx], Q_series: Q.tolist(), pv_series: pv.tolist() }) df pd.DataFrame(best_solutions) df.to_csv(pareto_solutions.csv, indexFalse)运行完这段代码你会看到每一代的前沿都在变好最终在收益-波动平面上形成一条向右下方倾斜的Pareto前沿。4. 结果分析与调度方案解读4.1 Pareto前沿绘制与解读跑完400代把pareto_f1和pareto_f2画成散点图横轴是收益我代码里优化的是负收益所以画图时取负号还原为正收益纵轴是出力波动。我实际跑出来的前沿大致是这么一个形态最左边的点收益最低、波动也最低说明水电全程平稳运行、光伏大量弃光整个系统出力曲线非常平滑最右边的点收益最高、波动也最大对应的策略是光伏能发多少发多少水电只在电价高峰时段猛冲其他时段压得很低联合出力曲线自然剧烈波动。这两个极端之间散落着几十个非支配解每个解对应一种“收益-稳定”的折中策略。读这个图要有一个意识前沿不是越靠右上越好。工程上最常用的选择原则叫拐点法也就是前沿曲率最大处附近的解往左走收益损失明显往右走波动恶化明显这个拐点的方案往往是最平衡的选择。我在最终报告里一般会推荐3到5个备选方案分别对应保守、均衡、激进三种运行策略。4.2 折中解的选取建议实际工程选方案不是拍脑袋定一个点我建议用模糊隶属度方法来筛。具体做法是把每个目标值做归一化然后计算每个解在各个目标上的平均隶属度隶属度越高说明该解到理想点的距离越近。取隶属度最高的几个解作为候选。我自己更常用另一种更朴素的思路先明确电网侧考核的硬指标。比如要求24小时内最大爬坡率不超过30MW/h那把Pareto前沿上所有超过这个阈值的解直接剔除剩下的里面再挑收益最高的。这样用硬约束做过滤器比纯数学选点更符合调度员的工作逻辑。你也可以把两个方法结合先用隶属度排序再用电网考核表复校。4.3 与单目标优化的对比为了验证多目标优化的价值我把同一个模型压缩成两个单目标分别跑了一遍梯度下降式的优化。结果非常直观单目标只优化收益时出力波动的均方值比多目标方案里最激进的那个还要高出近一半弃光率倒是基本为零但水电频繁启停、爬坡压力巨大这种方案调度员拿到手根本不敢用。单目标只优化波动时收益比多目标方案里最保守的还低两成光伏大量被弃水电利用率极低。这说明一个很朴素的道理在多变量强耦合的调度问题里人为预设权重去压一个目标往往会得到另一个目标上的极端行为。多目标优化把这种客观冲突完整暴露出来让决策者基于真实权衡做决定这比任何“最优解”都更有工程价值。5. 常见问题与排查技巧实录5.1 收敛性差、前沿不完整这是我被问得最多的问题跑了几百代Pareto前沿上只有两三个点或者点全挤在一块。碰到这种情况先检查种群规模。我初始设的120如果你的决策变量更多、时段更细建议直接提到200以上。再检查交叉和变异参数交叉概率低于0.8的时候种群多样性衰减会非常快我一般用0.9变异概率过高会把好解全部震碎我实测0.1附近比较稳。还有一个容易被忽略的点决策变量的初始范围。如果初始种群全部落在可行域的一个角落里算法要花大量代数“爬”出去。所以我初始化时故意让发电流量在上下限之间均匀撒点光伏在预测值的50%到100%之间撒点确保覆盖面够宽。5.2 约束大量违反约束违反分两类变量边界违反和耦合约束违反。变量边界违反好办clip一下就完事了。麻烦的是耦合约束典型的就是水量平衡和期末库容。我用过一段时间惩罚函数治理期末库容偏差结果种群整体被惩罚项带偏收益和波动目标的区分度反而下降。后来改成启发式修复根据期末库容偏差自动调整流量最大或最小的几个时段兼顾了计算速度和可行性。这里有个小技巧修复策略会影响目标函数值的分布所以同一批个体在同一个迭代代内修复逻辑必须完全一致否则前面评价过的个体和后面评价的不可比。5.3 目标函数量级悬殊收益目标动辄几十万量级波动目标只有几百这会导致拥挤度计算时波动目标几乎不起作用前沿在收益维度上被拉伸、在波动维度上挤成一团。我见过有人直接对目标做标准化但这样画出来的前沿容易误导决策者。我的处理建议非支配排序不依赖量纲不需要标准化但拥挤度计算时对两个目标分别做线性归一化到[0,1]再算距离。这个归一化只影响算距离的中间过程不影响最终前沿的真实值展示既保多样性又保可读性。5.4 运行时间太长如果时段数细化到48或96种群200代数500纯Python的循环求值会非常煎熬。我优化了三处第一目标函数里的for循环尽量向量化水量平衡虽然引入了时序依赖但可以用numpy的cumsum配合少量if条件替代第二把evaluate函数用numba的njit编译实测能提速20倍以上第三对重复出现的个体做缓存如果某个个体在历史集中已经评价过直接取缓存值省掉重复计算。我实际测试过48时段、种群200、500代用numba加速后大约跑15分钟纯Python大概需要4到5个小时。如果你只是做方案比选这个差距是决定性的。现象可能原因处理办法前沿只有1-3个点种群规模太小调到200以上交叉概率调到0.9前沿点挤在一端拥挤度未归一化拥挤度计算时对目标做[0,1]归一化大量解水量不平衡依赖惩罚函数改为修复策略耦合约束启发式修正每代结果波动大变异概率过高调低到0.05-0.1增大种群运行时间过长纯Python循环求值用numba编译目标函数增加重复解缓存6. 一点额外的调试心得最后分享一个我踩过几次坑换来的经验多目标优化算法的“最优解”其实没那么玄它本质上是一个找分布、找边界的搜索过程所以你不需要把代数设得特别大反而要把种群和初始多样性做好。我通常在300代左右就能看到前沿形态稳定下来之后主要是在边界处小幅推进。判断是否收敛不要只看前沿点数要看每代新增的支配解数量如果连续20代没有新解进入Pareto前沿基本可以停了。另外无论你的模型多精细预测数据多准现实调度里总有不确定性。我习惯在最终输出的灵敏度分析里把光伏预测偏差设为正负15%跑几组对比看推荐方案是否还在可行域内。好的调度方案不是单点最优而是对预测误差有足够鲁棒性。这一点在汇报给调度单位时特别加分也建议你在自己的项目里保留这一步。
返回列表