简介针对能耗调度问题资源包提供NSGA-III多目标优化算法的MATLAB实现适合研究多目标优化、能源管理或云计算/数据中心任务调度的开发者和学生使用。NSGA-III是在NSGA-II基础上改进的经典算法能够更好地平衡收敛性与种群多样性适用于能耗最小化、效率最大化等多目标权衡场景。压缩包共21个文件以16个.m源代码为主另有3个txt说明文件和2个url参考链接整体仅19KB便于快速查阅与复用。目前已有293人学习浏览。代码源自经典的YPEA126工具包结构清晰、注释完整读者可直接运行示例还可基于此修改目标函数与约束条件开展自定义能耗调度实验是入门和进阶NSGA-III算法的轻量级参考。1. 能耗调度为什么离不开 NSGA-III以及它到底解决什么问题做过楼宇能源调度或车间排程的人都有这种体验电价分时、设备功率、光伏出力、用户偏好全部堆在一张时间轴上你要给每个可平移负荷定启动时刻再给储能定每小时充放电功率。“当天买电最省”“全天总耗电量最低”“用户希望的那个点别偏差太多”——这三个指标互相拉锯压一个就翘两个。NSGA-III 这类多目标进化算法专门解决这种能耗优化问题它不是给你一个所谓最优解而是给你一整条 Pareto 前沿把“省电费、降能耗、保舒适”之间的取舍摆到桌面上由人去拍板。这篇笔记就围绕 NSGA-III 在能耗调度上的建模、编码、参数和避坑来写适合想快速落地的算法工程师、能源行业从业者和正在做毕设的研究生。2. 能耗调度建模先于算法为什么 NSGA-III 适合多目标能耗优化2.1 从 NSGA-II 到 NSGA-III参考点机制到底改了什么先回答一个常见疑问NSGA-II 已经很经典为什么能耗调度这种问题要用 NSGA-III二者的非支配排序骨架完全一样区别在维护多样性的方式。NSGA-II 用拥挤距离crowding distance保证前端个体均匀分布这在二维目标空间里表现不错一旦目标数增加到三个以上拥挤距离对超平面角落和中心区域的区分能力迅速下降跑出来的解常常堆在中间区域或者某些极端点附近Pareto 前沿看起来“一团糊”。NSGA-III 把这个机制整个换成参考点机制在目标空间里预先铺一层均匀分布的参考点每个解按坐标归一化后关联到离它最近的参考点选择阶段优先保留那些关联了较少个体的参考点所在的解。这样解的分布会被参考点“拉”着铺开三维和四维场景下依然均匀。能耗调度恰好是典型的三目标起步问题。单目标加权只会给你一个折中答案可实际决策中往往要回答“如果我要把电费再压 5%舒适度要牺牲多少小时”这类问题。这个时候 NSGA-III 的参考点分布能力就显出价值它可以把前端解均匀铺满整个目标平面供人挑选。理解了这个差异再看建模部分会更清楚为什么目标函数之间不能太相关——如果三个目标是线性相关的Pareto 前沿塌缩成一维参考点机制的优势就完全发挥不出来。2.2 能耗调度问题的三个目标函数与约束怎么写能耗调度不是一个标准数学问题每家的约束和设备都不一样。这里用最常见的“楼宇微电网日前能耗调度”作为示例框架时间粒度取一小时时间窗 24 小时决策对象是三个可平移负荷的启动时刻和储能设备每个小时的充放电功率。三个目标函数设计成下面这样f1 购电成本最小这是企业最关心的账。把每个小时的电网取电量乘以该小时电价再求和。f2 总购电量最小对应碳排放和能耗总量因为电网电力基本来自火电购电越少碳排放越低。f3 用户不舒适度最小用可平移负荷实际启动时刻与偏好启动时刻之差的绝对值之和表示。这里有一点要特别注意f1 和 f2 在分时电价下并不完全同步比如光伏发得多的中午时段电价往往也低此时“花最少的钱”和“用最少的电”并不总是指向同一个调度方案。f3 又和前两者呈强对抗三个目标互相拉扯Pareto 前沿才撑得开。约束条件包括每个可平移负荷必须在自己的允许时间窗内启动储能电池的 SOC 全程不能越界日终 SOC 要回到初始值附近否则调度方案只是“透支了明天的电”电网购电不允许为负值也就是不卖电给电网。约束的处理方式不是把所有违反量写进目标函数做惩罚而是单独定义为不等式约束交给算法去执行约束支配这个区别后面在避坑章节会展开。2.3 决策变量编码整数启动时刻与储能充放电功率的连续化映射能耗调度问题的决策变量天然混合负荷启动时刻是整数储能功率是连续值。许多 NSGA-III 代码库支持混合变量编码但配置和算子参数都更繁琐工程上常省事的做法是全部变量按 [0,1] 区间实数编码在适应度评估函数里把实数映射回真实的决策变量。比如某负荷允许在 0 点到 6 点之间启动窗口终点减时长后的最晚启动时刻那么决策变量 x 映射到 start 0 x * 6取整后就是实际启动时点。储能功率映射也更直接bat_power (x * 2 - 1) * pmax其中 pmax 是储能最大充放电功率得到 -3kW 到 3kW 之间的连续值负值表示充电正值表示放电。连续化映射的好处不止是省去混合变量的配置成本还让 NSGA-III 的 SBX 交叉和高斯变异等算子可以直接作用在全部变量上搜索行为更平滑。代价是原本就是整数的启动时刻在取整时可能引入微小的重复解但能耗调度的目标函数对启动时刻的小幅偏差不敏感实际工程中这点精度损失完全可以接受。核心是映射必须在解码阶段统一保证同一个决策变量始终映射到同一个物理含义。3. 用 pymoo 跑通 NSGA-III 能耗调度完整代码与参数解析3.1 准备数据分时电价、光伏出力、固定负荷与储能参数先定义问题数据。这里给出的是演示用数据实际项目中这些数组要替换成当地电网的分时电价、光伏预测曲线和历史负荷数据。import numpy as np # 分时电价单位 元/kWh24 个点对应 0~23 时 price np.array([0.52, 0.52, 0.52, 0.52, 0.52, 0.55, 0.62, 0.78, 0.95, 0.98, 0.90, 0.82, 0.72, 0.71, 0.75, 0.85, 0.92, 0.88, 0.80, 0.70, 0.60, 0.52, 0.52, 0.52]) # 光伏出力单位 kW演示数据白天有出力夜间为 0 pv np.array([0, 0, 0, 0, 0, 0.1, 0.6, 1.8, 3.5, 5.2, 6.6, 7.1, 7.0, 5.8, 3.9, 1.6, 0.5, 0.1, 0, 0, 0, 0, 0, 0]) # 固定负荷单位 kW照明和基础设备用电 fixed_load np.array([1.8, 1.6, 1.6, 1.5, 1.5, 1.8, 2.5, 3.2, 3.5, 3.4, 3.0, 2.7, 2.7, 2.8, 2.9, 3.0, 3.2, 3.5, 4.2, 4.5, 4.0, 3.2, 2.4, 2.0]) # 可平移负荷定义名称、功率kW、持续时长h、允许窗口(开始,终止)、偏好启动时刻 appliances [ {name: EV充电, power: 6.0, duration: 2, window: (0, 8), pref: 1}, {name: 洗衣机, power: 1.8, duration: 2, window: (9, 20), pref: 14}, {name: 洗碗机, power: 1.5, duration: 2, window: (19, 23), pref: 21}, ] # 储能参数 battery { capacity: 10.0, # kWh pmax: 3.0, # 最大充放电功率 kW eff_charge: 0.95, # 充电效率 eff_discharge: 0.95, # 放电效率 soc_init: 0.5, # 初始 SOC }这段数据里需要注意两个细节。第一个是电价曲线的峰谷形态早高峰和晚高峰各有一个尖峰这会让 NSGA-III 倾向于把可平移负荷移到谷段但光伏出力集中在中午储能系统会把午间光伏存下来留到晚间用三者互相竞争形成了前文说的 Pareto 拉锯。第二个是负荷窗口的写法window的第二个值是“最晚必须启动的时刻”不是最晚结束时刻。洗碗机窗口写 (19, 23)意思是它可以在 19、20、21、22 点启动但 23 点启动就超过 24 点边界了这个数据边界非常容易踩坑。3.2 用 pymoo 定义能耗调度优化问题这一步把上面的数据塞进 pymoo 的Problem子类核心是定义变量个数、目标个数、约束个数以及在_evaluate里完成“解码决策变量 → 模拟一天能耗 → 计算目标与约束”的完整链路。from pymoo.core.problem import Problem class EnergySchedulingProblem(Problem): def __init__(self): self.appliances appliances self.battery battery self.n_load len(appliances) # 可平移负荷个数 self.n_hour 24 # 调度时段数 n_var self.n_load self.n_hour # 3个启动时刻 24个储能功率 super().__init__( n_varn_var, n_obj3, n_ieq_constr1, xlnp.zeros(n_var), xunp.ones(n_var), ) def _decode(self, z): 把[0,1]区间的决策变量解码为真实调度动作 starts [] for i, app in enumerate(self.appliances): lo app[window][0] hi app[window][1] - app[duration] # 最晚可行启动时刻 starts.append(int(lo z[i] * (hi - lo))) # 储能功率-pmax 表示充电pmax 表示放电 bat (z[self.n_load:] * 2 - 1) * self.battery[pmax] return starts, bat def _evaluate(self, X, out, *args, **kwargs): F np.zeros((X.shape[0], 3)) G np.zeros((X.shape[0], 1)) for k in range(X.shape[0]): starts, bat self._decode(X[k]) f_cost 0.0 f_energy 0.0 f_comfort 0.0 soc self.battery[soc_init] soc_violation 0.0 for t in range(self.n_hour): # 统计当前时段总负荷固定负荷 正在运行的平移负荷 load self.fixed_load[t] for i, app in enumerate(self.appliances): if starts[i] t starts[i] app[duration]: load app[power] # 电网取电 负荷 - 光伏 - 储能放电不允许卖电取负则归零 p_grid load - self.pv[t] - bat[t] if p_grid 0: p_grid 0.0 f_cost p_grid * self.price[t] f_energy p_grid # 更新 SOC充放电效率分开处理 if bat[t] 0: soc - bat[t] / (self.battery[capacity] * self.battery[eff_discharge]) else: soc (-bat[t]) * self.battery[eff_charge] / self.battery[capacity] # 统计SOC越界量作为约束违反 if soc 1.0: soc_violation soc - 1.0 soc 1.0 elif soc 0.0: soc_violation -soc soc 0.0 for i, app in enumerate(self.appliances): f_comfort abs(starts[i] - app[pref]) F[k, 0] f_cost F[k, 1] f_energy F[k, 2] f_comfort # 约束日终SOC回到初始值附近加上全程越界量 G[k, 0] abs(soc - self.battery[soc_init]) - 0.01 soc_violation out[F] F out[G] G逐个说逻辑。_decode里启动时刻用int()截断而不是round()因为 round 在边界值上可能把 23 取到 24导致启动时刻落在窗口外截断永远落在窗口内的安全区间代价是精度损失不到一个小时对目标函数影响可忽略。功率平衡的计算顺序是先算负荷再算电源电网取电被 clamp 到非负等于做了“不卖电”的隐性处理这种做法比把负值也纳进目标函数更贴近真实电表的计量方式。SOC 更新严格区分充放电效率放电时电池内部实际消耗比交流侧功率大除以效率充电时存入电池的能量比交流侧小乘以效率这一步不做精细处理后面调度出来的储能策略会失真得很厉害。约束函数把“日终 SOC 回位偏差”和“全天越界量”累加在一起允许 0.01 的容差否则 NSGA-III 很难搜索到严格满足 SOC 回位的解。3.3 主流程NSGA-III 算法配置与跑一个 80 代的最小例子实例化问题对象生成参考方向配置 NSGA3 算法调用minimize收敛。这是整个工程里最少改动就能换数据复用的部分。from pymoo.algorithms.moo.nsga3 import NSGA3 from pymoo.optimize import minimize from pymoo.util.ref_dirs import get_reference_directions from pymoo.operators.sampling.rnd import FloatRandomSampling from pymoo.operators.crossover.sbx import SBX from pymoo.operators.mutation.pm import PM # 把数据挂到问题实例上 problem EnergySchedulingProblem() problem.price price problem.pv pv problem.fixed_load fixed_load # 3目标、12等分参考方向生成 91 个参考点 ref_dirs get_reference_directions(das-dennis, 3, n_partitions12) print(参考点数量:, len(ref_dirs)) algorithm NSGA3( pop_size100, samplingFloatRandomSampling(), crossoverSBX(prob0.9, eta15), mutationPM(prob1.0 / problem.n_var, eta20), eliminate_duplicatesTrue, ref_dirsref_dirs, ) res minimize(problem, algorithm, termination(n_gen, 80), seed42, verboseFalse) print(Pareto 前沿前5个解的目标值:) print(res.F[:5])这段代码有四个参数需要解释。第一个是get_reference_directions(das-dennis, 3, n_partitions12)三目标 12 划分生成 C(123-1, 3-1) 91 个参考方向参考点数量直接影响种群下限后面参数章节会专门讲。第二个是pop_size100要大于参考点数量 91略大一点给算法留出冗余。第三个是SBX(prob0.9, eta15)模拟二进制交叉交叉概率 0.9 意味着绝大多数配对的父代都会产生子代eta 控制子代离父代的远近15 是中等偏聚集的取值。第四个是PM(prob1.0 / n_var, eta20)多项式变异变异概率取变量数的倒数保证每个个体平均变异一个基因。运行结束后的res.F就是 Pareto 前沿上的目标值矩阵每一行一个解三列分别是电费、总购电量、舒适度偏差。4. NSGA-III 参数调优参考点、种群、代数和算子怎么配合4.1 参考点数量决定种群下限这是第一个要定的参数很多刚上手 NSGA-III 的人会把种群大小的习惯从 NSGA-II 带过来随便设个 50、80。但 NSGA-III 的种群数量不能低于参考点数量否则部分参考方向永远没有关联个体前端解会出现大片空白扇区。本例中 n_partitions12 对应 91 个参考点pop_size 设 100 是合理的。如果想要更致密的分布可以调大划分粒度n_partitions15 时参考点数为 C(17,2)136此时 pop_size 至少要到 140。代价是每代评估工作量线性上涨而能耗调度的适应度评估涉及 24 步的逐时段功率模拟种群翻倍计算时间也接近翻倍。这里有个工程上的权衡参考方向的均匀性由 das-dennis 产生它要求目标数与划分次数配合三目标用 das-dennis 效果最好四目标以上推荐用 Riesz energy 或 sphere 方法生成参考方向因为等距划分在更高维会产生过多参考点。如果项目实际目标数大于 3建议先跑一次小种群实验观察非支配解数量与参考点数量的比例再回头定 pop_size。4.2 SBX 与 PM 参数能耗问题为什么交叉概率要高一点SBX 的两个参数 prob 和 eta 作用不同prob 决定两个父代是否产生子代eta 决定子代离父代的散布程度。能耗调度的决策变量之间存在强耦合——负荷启动时刻和储能功率共同决定每小时电网取电两个变量一旦被拆散到不同个体子代很可能出现“负荷移到光伏高峰但储能没在充电”这类不协调组合。交叉概率偏低会让这类关联结构难以被重新组装所以工程上一般把 prob 提高到 0.9 而不是教科书里常见的 0.8。eta 取 15 保持子代与父代相近避免大范围跳跃产生太多高能耗的废解。变异概率按 1/n_var 已经足够本例 27 个变量对应约 3.7%每代每个个体平均只变异一个基因。变异强度太大储能功率曲线会被打碎成高频抖动的形状物理上不可行而且难以通过约束筛选。4.3 代数、随机种子与玄学NSGA-III 的收敛代数没有普适答案取决于问题规模和目标函数计算成本。这个示例问题 80 代已经能跑出形态完整的前沿把代数提高到 200 代前沿会更光滑但收益边际递减。真正值得做的不是盲目加码而是用上第五节说的 HV 指标曲线判断收敛点如果最近 30 代 HV 涨幅不到 1%基本可以停了。随机种子对结果的影响经常被忽略NSGA-III 是随机算法同一个问题换一个 seed 跑出的前沿可能有可见差异。我一般每个参数组合跑 3 个 seed看最优前沿的包络而不是单次结果否则调参时很容易对着一次随机波动做无效调整。这个习惯能避免很多“明明改好了却更差”的灵异现象。5. NSGA-III 能耗优化避坑5 个亲测踩过的坑5.1 把约束违反写进目标函数Pareto 前沿混入大量不可行解现象跑出来的许多解“能耗很低”但仔细看负荷启动时间全在窗口之外储能功率超出设备上限明显是物理上不可行的方案。原因最早贪图省事把 SOC 越界量、时间窗违反量加权加到某个目标函数里做惩罚。约束值量级通常是几十而目标函数值量级是几惩罚项直接把目标值淹没了算法把大量搜索资源浪费在“违反约束但目标好看”的区域。解决改用 pymoo 的 n_ieq_constr 参数单独声明不等式约束让 NSGA-III 自带的约束支配机制处理可行性。约束支配会优先保留可行解只有可行解之间才比较目标函数值不可行解之间比较约束违反量。代码里约束G[k,0] abs(soc - soc_init) - 0.01 soc_violation就是干这个事的。5.2 参考点数量大于种群规模前端解出现大片空洞现象跑完后 Pareto 前沿上只有零星几个点分布极不均匀甚至某些参考方向附近一个解都没有。原因pop_size 设成 60参考点却有 91 个NSGA-III 在环境选择阶段根本不够人填充所有参考方向的 niche。这个问题在目标数增加时更严重6 目标问题用 das-dennis 划分参考点数量会膨胀到上千种群规模稍小就穿帮。解决先把参考点数量算清楚再定 pop_size。三目标 n_partitions12 时 pop_size 不应小于 91稳妥起见取 100 到 130。另外可以用len(ref_dirs)打印出来核对不要凭感觉估。5.3 用 round 取整启动时刻出现“第 24 点启动”这种幽灵解现象解码结果里出现 start24对应时间是次日 0 点已经超出 24 小时调度周期这类解直接违反时间窗约束但约束函数因为只检查了最终时刻没检查解码边界所以没有被拦截。原因窗口上界映射时用了 round()。例如某负荷窗口 (19, 23)时长 2h最晚启动时刻 22当连续变量映射到 22.7 时 round 得到 23而正确行为是“在 23 点启动一个 2 小时的负荷”会越过调度周期终点。解决解码统一用int()向下截断让所有映射落在[lo, hi]闭区间内并把窗口终点设置为“最晚启动时刻”而不是“最晚结束时刻”。这个边界问题在光伏窗口、储能时段加边界时很容易复现建议所有边界参数都用“能用 int 直接落在区间内”的方式定义。5.4 储能效率设为 1调度结果变成“疯狂搬电”现象Pareto 前沿上出现大量把电能从中午搬到晚上的解电池充放电循环频繁但总能耗并没有比不搬明显更好。原因效率为 1 意味着充放电零损耗算法发现“反正不亏就把光伏电搬到晚高峰用”这种策略确实能省钱但现实中每度电充放一次损耗约 10%搬电策略的收益被损耗吃掉大半调度结果失真。解决充电效率和放电效率分别建模放电按/eff_discharge消耗电池容量充电按*eff_charge存入电池。效率参数来自电池手册或现场实测别用默认值。这个小改动会让算法自动权衡“搬电收益”和“效率损耗”调度出来的储能曲线会理性很多。5.5 只画 Pareto 图不验证收敛80 代和 200 代结果差很远现象某次实验 80 代跑完前沿看起来已经很均匀同样的参数换一个 seed或者把代数加到 200发现前端明显向外推进之前认为“收敛”的结果其实只是局部前沿。原因NSGA-III 的停止条件默认是固定代数而每个问题的收敛速度差很多。能耗调度如果有细致的仿真模型每代评估很慢固定代数很容易在未收敛时就停了。解决每 N 代记录一次种群的非支配前沿计算 Hypervolume 指标画收敛曲线曲线进入平台期再加跑 30 代验证。不要在没看曲线的状态下直接汇报“已收敛”这是这类项目最有说服力的验证方法下章给具体实现。6. 进阶用 HV 验证 NSGA-III 收敛并在 Pareto 前沿上选最终方案6.1 用 Hypervolume 曲线判断收敛点Hypervolume 衡量非支配解集在目标空间占据的体积数值越大说明解集越靠近理想点且分布越广。计算时需要指定一个参考点通常取所有目标最差值再放宽一点from pymoo.indicators.hv import HV # res.F 是最终代非支配解目标值这里列出近似用法 ref_point np.array([res.F[:, 0].max() * 1.1, res.F[:, 1].max() * 1.1, res.F[:, 2].max() * 1.1]) hv HV(ref_pointref_point) print(HV:, hv(res.F))在跑的过程中每隔 10 代算一次 HV把结果画成横轴为代数、纵轴为 HV 的曲线曲线进入平缓段就说明算法基本收敛。这个做法比盯着代数参数凭空调可靠得多是我做这类多目标项目时最后一道把关。曲线持续上升说明还有改进空间再加代数曲线平了之后继续跑就是纯烧电费。6.2 从 Pareto 前沿挑一个方案TOPSIS 排序Pareto 前沿上几十上百个解最终落地方案仍然要选一个。常见做法是给三个目标定权重后用 TOPSIS 排序。比如企业最看重电费其次碳排放舒适度相对次要就把权重设成 0.5、0.3、0.2对res.F做归一化后计算理想解距离取距离最近的解。注意三个目标量纲差异大电费是几十元舒适度偏差是个位数小时不归一化直接算距离会完全被电费主导。选完解还要回到决策变量层看重排出来的负荷启动时间和储能充放电曲线确认它是一个人类管理员会接受的方案。这条习惯救过我很多次——有的解目标值很漂亮但储能功率曲线像锯齿一样剧烈抖动工程上一眼就看出设备受不了遇到这种解正确的做法不是强行接受而是回到模型里给储能功率变化率加约束重新跑一轮。做这类项目越往后越会意识到NSGA-III 只是把“取舍”这件事变得可视、可复现最后拍板的那一下永远是人。希望这篇关于 NSGA-III 能耗调度的实战笔记能帮你在自己的能耗优化项目上少走几步弯路。本文还有配套的精品资源点击获取
