我先说个实在话:这类“多目标遗传算法做分布式电源选址定容”的项目,乍一看是个算法题,但真正跑过一遍的人都知道,难点根本不在遗传算法本身,而在“怎么把电力系统的约束和成本算清楚”以及“多目标怎么权衡”。我今年刚完整做完一个类似的课题,从IEEE 33节点系统入手,把代码从单目标一路改到多目标,踩了不少坑,也总结出一套能直接复用的套路,今天一次性说清楚。
1. 分布式电源选址定容:问题的本质是什么
要做代码之前,先把数学模型想明白。很多新手一上来就翻NSGA-II的源码,改两个函数就以为自己搞定了,结果跑出来的结果连潮流都不收敛,这就是典型的“重算法、轻模型”。
1.1 为什么选址定容这么关键
分布式电源(Distributed Generation,DG)指的通常是接入配电网末端的光伏、风电、储能、微型燃气轮机这些中小型发电单元。它们不像传统火电机组那样集中在远方,而是贴近负荷侧,所以“装在哪”“装多大”直接决定了配电网的运行状态。
这里有个很直观的类比:一个小区里要建充电站,如果都挤在同一个入口,高峰期必然拥堵;如果分散均匀,大家充电方便,电网压力也小。DG选址定容就是干这个事,只不过把“充电站”换成“分布式电源”,把“拥堵”换成“网损、电压越限、投资成本”这些电气指标。
从工程角度讲,选址定容考虑的核心问题有几个:
- 网损:DG如果放在负荷中心附近,能减少功率长距离输送,网损自然下降;但放错位置反而会增加损耗。
- 电压质量:配电网末端电压偏低是常见问题,DG能提供就地无功支撑,提升电压水平;可如果容量过大,又可能造成电压越上限。
- 投资与运行成本:不是装机容量越大越好,设备成本、运维成本都要算进总账。
- 环保效益:光伏、风电这类清洁能源有环境价值,这也是很多论文里第三个目标函数的来源。
你打开任何一篇相关论文,目标函数基本就是网损最小、电压偏差最小、成本最小这三个的组合。我的建议是:第一步先把这三个目标用数学公式写出来,再谈算法。
1.2 单目标 vs 多目标:为什么必须多目标
有些初版代码为了省事,把网损、电压、成本加权成一个综合指标,跑一次遗传算法就得出一组解。这种“加权法”不是不行,但有个致命问题:权重怎么定?网损重要还是成本重要?这本质上是决策者的偏好,不是算法能替你定的。
多目标优化则不同。它一次跑出一组“帕累托最优解集”(Pareto前沿),每个解都是“在某个目标上更好、同时另一个目标上不差”的折中方案。比如解A网损更低但成本高,解B成本低但电压偏差大,两个解没有绝对优劣,决策者可以结合实际需求选。
这就像买车:有人看重油耗,有人看重动力,有人看重空间。单目标优化等于提前说“油耗最重要”,而多目标优化是把“不同配置的备选车型都列出来”,让你自己挑。对于配电网规划这种多利益主体博弈的问题,后者的工程价值显然更大。
2. 多目标遗传算法选型解析:NSGA-II凭什么成为默认解
解决了“为什么多目标”之后,下一个问题是“用哪种多目标算法”。现在主流的选择是NSGA-II(Non-dominated Sorting Genetic Algorithm II),也就是带精英策略的非支配排序遗传算法。我项目里用的就是它,跑下来稳定性确实不错。
2.1 NSGA-II的核心机制与选型理由
NSGA-II在2002年由Deb等人提出,到现在二十多年了,依然是工程界应用最广的多目标进化算法。它最核心的三个机制,对应解决了三个痛点:
第一,快速非支配排序。每代种群里的个体,按照Pareto支配关系分层。比如个体A在所有目标上都优于个体B,那B就被A支配。排序后,第1层是最优解,第2层次之,依此类推。选择时优先保留层级靠前的个体,保证收敛方向正确。
第二,拥挤度距离。同一层里可能有很多个体,但不能全留下来。拥挤度距离衡量的是“这个个体周围有多少邻居”,距离越大说明这个区域解越稀疏,越值得保留。这保证了Pareto前沿的均匀分布,不会挤在一团。
第三,精英保留策略。父代和子代合并后再排序,那么上一代的最优解不会因为随机操作丢失。这保证了算法不会越跑越差。
这三板斧组合下来,NSGA-II在收敛性和分布性之间取得了不错的平衡。虽然这几年也出现了NSGA-III、MOEA/D这些新算法,但我选NSGA-II的原因很简单:matlab和python的现成库都成熟,社区案例多,而且对于节点数不超过100的配电网模型,NSGA-II的精度已经足够了。
2.2 关键参数设置与工程经验
参数设置是遗传算法里“玄学”感最强的地方,但也是有规律可循的。我以标准IEEE 33节点系统为例,给出我调试好的参数模板:
| 参数 | 推荐取值 | 调整经验 |
|---|---|---|
| 种群规模 | 100~200 | 节点多、变量多时取大值,但过大会拖慢计算速度 |
| 迭代代数 | 100~200 | 看适应度曲线是否收敛,没收敛就加代数 |
| 交叉概率 | 0.85~0.95 | 太小搜索速度慢,太大破坏优良个体 |
| 变异概率 | 0.01~0.1 | 这个值很敏感,我用0.05跑得最稳 |
| 编码方式 | 实数编码 | 每个基因位存DG接入节点编号,不要用二进制 |
这里我想特别强调变异概率。我一开始用默认的0.1,跑出来Pareto前沿倒是很分散,但很多个体的潮流不收敛,因为变异把DG容量改得太大,超出了节点承受能力。后来把变异概率降到0.03~0.05,并加了约束修复,效果明显改善。这印证了一个理念:非支配排序只管目标函数排序,不管解是否物理可行,所以约束处理必须单独设计。
3. 核心代码实现:从约束到Pareto前沿的完整链路
代码部分是这个项目的重头戏。我的实现思路很直接:MATLAB做主框架,潮流计算用Matpower,遗传算法逻辑自己写(不调工具箱,方便改)。这样每一步都在自己掌控中,调试起来不抓瞎。
3.1 编码方式与种群初始化
选址定容问题有两个决策变量:在哪些节点装DG和每个节点装多大容量。我用一个二维实数数组表示个体,行是染色体序号,列的一半是节点编号,另一半是对应容量。
% 种群初始化 - 以IEEE 33节点为例 n_pop = 200; % 种群规模 n_dg = 3; % 预设DG接入数量 n_node = 33; % 配电网节点数 % 每个个体结构: [接入节点1, 节点2, 节点3, 容量1, 容量2, 容量3] pop = zeros(n_pop, 2 * n_dg); for i = 1:n_pop % 随机选取3个不同节点(排除变电站节点1) nodes = randperm(n_node - 1) + 1; pop(i, 1:n_dg) = nodes(1:n_dg); % 容量在[0.1, 1.5]MW之间随机,避免过小或过大 pop(i, n_dg+1:end) = 0.1 + 1.4 * rand(1, n_dg); end注意两个容易踩坑的点。第一,同一染色体里不能出现重复节点,一个节点装两个DG在物理上可以,但对优化无意义,最好初始化时就去重。第二,容量范围要参考系统负荷水平,IEEE 33节点系统总负荷约3.7MW,单个DG取0.1~1.5MW比较合理,取太大后续潮流计算必炸。
3.2 潮流计算与适应度评估
适应度评估是整段代码的灵魂。每个个体要被解码成“DG接入方案”,然后代入潮流计算,得到网损、电压等指标。
%% 适应度函数伪代码 function [cost_net, v_dev, cost_invest] = evaluateIndividual(genes, systemData) % 1. 解码:提取接入节点和容量 % 2. 修改系统数据:在对应节点增加PQ注入功率 % 3. 调用Matpower潮流计算,得到节点电压和支路损耗 % 4. 计算目标函数值并返回 end我用Matpower的原因主要有两点:一是它经过了大量测试验证,比手写牛顿-拉夫逊法稳得多;二是它支持直接修改bus和gen矩阵,接口友好。改造时只需要新建一个case文件,把选定节点改成PQ节点,然后填入DG的出力值即可。
潮流计算完成后,三个目标函数这样计算:
- 网损Ploss:Matpower里直接读loss结果,或按下式统计:所有支路首末端功率差之和。
- 电压偏差ΔU:计算所有节点电压与额定值1.0p.u.之差的绝对值累加,这个指标比只看最低电压更全面。
- 综合成本C:等年值投资成本加运行维护成本,贴现率按8%、运行年限按20年折算,这个公式很多论文里有,我直接按标准做了。
3.3 非支配排序与拥挤度距离的实现
这一块是NSGA-II的机制核心。非支配排序的关键是判断两个个体之间的支配关系。用MATLAB实现时,我先把每个个体的目标值存成一个矩阵,然后两两比较。
function [fronts, rank] = nonDominatedSort(obj_values) % obj_values: n_pop x n_obj 的目标值矩阵 % 假设所有目标都是求最小 n_pop = size(obj_values, 1); dominated_count = zeros(n_pop, 1); % 被支配次数 dominate_set = cell(n_pop, 1); % 支配的个体集合 for i = 1:n_pop for j = 1:n_pop if i == j continue; end % 判断i是否支配j if all(obj_values(i, :) <= obj_values(j, :)) && ... any(obj_values(i, :) < obj_values(j, :)) dominate_set{i} = [dominate_set{i}, j]; dominated_count(j) = dominated_count(j) + 1; end end end % 分层:被支配次数为0的是第1层,依次递推 front = 1; fronts = {}; while sum(dominated_count == -1) < n_pop % 全部处理完 current_front = find(dominated_count == 0); if isempty(current_front) break; end fronts{front} = current_front; for k = current_front dominated_count(k) = -1; % 标记已处理 for m = dominate_set{k} dominated_count(m) = dominated_count(m) - 1; end end front = front + 1; end end拥挤度距离这部分,思路是“先按某一目标排序,再把边界的个体距离设为无穷大,中间个体算相邻个体目标值的差”。需要特别注意的是:每个目标都要先归一化再算距离,否则网损动辄几百千瓦、成本动辄几百万,数值上会把电压偏差这样的“小量”完全淹没。
3.4 完整算法主流程与选择策略整合
把前面各部分串起来的完整主流程,可以用这个套路套任何NSGA-II问题:
1. 初始化种群P0 2. 迭代 t = 1 to maxGen: a. 对种群Pt做二元锦标赛选择,得到父代池 b. 交叉(模拟二进制交叉SBX)和变异(多项式变异) c. 生成子代Qt d. 合并 Pt 和 Qt,得到2倍规模种群Rt e. 对Rt做非支配排序,按层级从低到高放入新种群Pt+1 f. 如果当前层放不下全部个体,计算拥挤度,选距离大的保留 3. 输出最终Pareto前沿解集我常用的选择操作是二元锦标赛:随机抽两个个体,优先取层级低(Pareto排序靠前)的,如果层级相同取拥挤度大的。这样写出来代码逻辑清晰,速度也快。
下面给出交叉和变异两个算子的关键代码片段。我用的是模拟二进制交叉(SBX),因为它在实数编码问题上表现稳定,而且参数η_c控制着子代与父代的相似程度。
% 模拟二进制交叉 function [c1, c2] = sbx_crossover(p1, p2, eta_c) u = rand(); if u <= 0.5 beta = (2*u)^(1/(eta_c+1)); else beta = (1/(2*(1-u)))^(1/(eta_c+1)); end c1 = 0.5 * ((1+beta)*p1 + (1-beta)*p2); c2 = 0.5 * ((1-beta)*p1 + (1+beta)*p2); % 边界检查 c1 = max(min(c1, 1.5), 0.1); c2 = max(min(c2, 1.5), 0.1); end多项式变异也类似,用一个分布指数η_m控制扰动的集中程度。我这里的η_c和η_m一般取15和20,这是Deb论文里的推荐值,实测效果挺好。如果你发现自己跑出来的Pareto前沿很窄,优先检查是不是交叉概率太小、或者选择压太强,而不是急着调变异。
3.5 约束处理的罚函数技巧
前面提到,遗传算法生成的解很可能违反约束。比如DG总容量过大导致某个节点电压超过1.05p.u.,或者DG容量为负(虽然初始化限定了,但交叉变异后可能越界)。我的做法是越限即罚:不直接丢弃该个体,而是给它的目标函数加上一个很大惩罚项。
% 约束违反检测示例 voltage_violation = sum(max(0, voltage - 1.05).^2) + ... sum(max(0, 0.95 - voltage).^2); if voltage_violation > 0 % 电压越限惩罚系数,经验值设为10000 cost_net = cost_net + 10000 * voltage_violation; v_dev = v_dev + 10000 * voltage_violation; end为什么不直接丢弃?因为遗传算法本质上是在“大范围试探”,超过边界一点点的新个体,其基因可能携带了某些优秀片段,直接丢弃会损失多样性。罚函数相当于告诉算法“这条路错得太离谱,但保留下来看看有没有可用的基因信息”。这一招看着简单,却是约束优化里最好用也最常用的工程手段,比各种花哨的可行性修复方法省心得多。
4. 调试经验与常见问题速查
代码写完之后,真正的工程挑战才开始:怎么确认Pareto前沿是对的?怎么加快收敛?怎么判断自己算错了?这一章把我这次项目里掉过的坑和最终的解决方案如实列出来,省得你再走一遍弯路。
4.1 收敛性判断与Pareto前沿的可靠性
很多同学跑完算法,一看Pareto前沿画出来挺漂亮,就觉得完了。我踩坑之后总结出一个关键校验方法:拿一个极端解跟单目标优化结果对比。
比如Pareto前沿里应该存在一个“网损最小”的极端解。你用单目标遗传算法只优化网损(把其他目标权重设成0),算出来的最优结果,应该和Pareto前沿里网损方向的最端点非常接近(理论上同等或更优)。如果两者差异很大,说明多目标代码里适应度评估或排序逻辑有bug。
第二个校验是观察算法的收敛曲线。多目标算法因为没有单一适应度值,我喜欢记录“第1层Pareto前沿的个体数量”和“网损目标的中位数随代数变化”。如果前沿数量从很多逐渐减少到稳定在一个区间,同时目标中位数单调下降后不再明显变化,说明算法基本收敛。如果前沿数量持续剧烈抖动,大概率是约束罚函数签数设得太小,或者变异概率过大。
第三个是直观判断法:合法的Pareto前沿在目标空间里应该是一条单调下凹的曲线(或曲面)。如果画出来的散点是杂乱无章的、甚至很多点明显被其他点支配,那就说明非支配排序有问题。
4.2 计算耗时问题与加速方案
IEEE 33节点系统的潮流计算非常快,单次要不到0.05秒,所以200个体跑200代,总体耗时也就在几十秒量级。但如果你换成IEEE 123节点系统,或者加了负荷不确定性要做多次抽样,计算量会呈指数增长。这时候要考虑三个加速手段:
- 并行计算:MATLAB的parfor是把种群中个体评价并行的最简单手段。适应度评估互相独立,天然适合并行。
- 稀疏存储:潮流计算中雅可比矩阵是稀疏的,确认Matpower用的是稀疏矩阵运算,不要自己写稠密求逆。
- 减少潮流次数:如果目标函数里需要用多次潮流结果(比如24小时负荷曲线),可以先用聚类把负荷曲线压缩成3~5个典型场景,加权计算目标值,精度损失不大,但速度提升非常可观。
4.3 常见错误速查表
这里整理一张速查表,都是我在帮别人检查代码时遇到的高频问题:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| Pareto前沿只有一两个点 | 种群多样性不足 | 增大种群规模,降低变异概率 |
| 前沿分布偏在某一侧 | 目标函数未归一化 | 按各自范围归一化后再排序 |
| 结果比单目标优化差 | 非支配排序方向写反 | 检查是min还是max,统一方向 |
| 潮流不收敛警告很多 | DG容量越界 | 检查编码边界,交叉变异后加边界修复 |
| 每次运行结果差异巨大 | 随机种子未固定 | 初调时固定随机种子,正式跑多组取统计 |
| 电压全部偏低 | DG接入容量过小 | 适当放宽容量上下限范围 |
其中“每次运行结果差异巨大”这点我想多说一句。遗传算法本质是随机算法,不同种子跑出不同结果完全正常。但如果你需要复现,就在代码开头写死随机种子;如果是为了验证算法稳定性,就固定几个种子跑5~10次,统计Pareto前沿的分布范围。别指望两次运行结果一模一样,那是没理解随机优化。
4.4 多目标结果的后处理与决策建议
跑出Pareto前沿只是第一步,工程上还得从多个解里挑一个实施。这个环节很多代码模板不给出来,但恰恰是实际项目里对方最关心的。
我常用两个方法:一个是模糊满意度法,把所有目标归一化后,取每个个体满意度最高的作为推荐方案;另一个更简单粗暴——直接看工程偏好。比如电网公司如果当前网损率超标严重,就选网损最小的解;如果项目预算有限,就选成本最低的解。给出手算结果Pareto前沿图,同时把两三个典型方案的指标列成表格,决策者能看明白,数值上又有说服力,这就是这个算法的交付价值。
5. 项目复盘与后续扩展方向
整个项目跑完,最深的体会是:不要迷信算法本身的高深程度,而要把精力花在“问题建模是否符合物理规律”和“代码逻辑是否可验证”上。NSGA-II只是优化引擎,真正决定结果上限的,是你把配电网的网架结构、负荷特性、DG运行约束考虑得有多完整。
如果有后续课题,我建议从三个方向扩展。
第一,把确定性优化改成鲁棒优化。实际运行中光伏出力和负荷都有很强的随机性,用典型日数据代替全年会使结果偏乐观。可以引入场景概率法,或者用信息间隙决策理论(IGDT)建模不确定性,但代码复杂度会上一个台阶。
第二,加入储能和电动汽车充电桩。很多项目现在都要求DG和储能协同规划,因为储能能平抑DG的波动性。这需要把决策变量扩展到“储能容量”“充放电策略”甚至“日内时序”,变量维度高了很多,但NSGA-II依然处理得动。
第三,考虑动态重构。网架重构和多时段运行是配电网智慧化的方向,规划层的选址定容结果如果能在运行层配合开关动作,预期收益会进一步提升。这类问题更适合用两阶段优化或双层优化架构,也是未来一段时间的热点。
最后再分享一个小技巧:拿本函数的非支配排序那一段,如果越看越绕,建议画一张三目标的散点图,用颜色深浅表示支配层级,一目了然。调试多目标算法的视觉化工具,远比硬看数字高效得多。这个习惯我保留到今天,遇到多目标优化问题,先画图、再调参,能省一半调试时间。