这个课题我前后做了两个版本。第一版只做经典经济调度,成本优化加功率平衡约束,遗传算法跑起来还算顺;第二版把排放目标和输电损耗都塞进目标函数,麻烦事一下子多了不少。今天就把完整的思路、数学建模和Python代码实现从头到尾拆一遍,重点说清楚二进制编码怎么做、多目标怎么融合、网损怎么算、哪些参数最值得调试,给正在做电力系统优化课题或者毕业设计的人一个能直接落地的参考。
这个内容解决的核心问题,是在满足负荷需求的前提下,协调多台发电机组的出力,让燃料成本尽量低、污染物排放尽量少、输电损耗尽量小。这三个目标常常互相冲突,传统解析方法很难处理,所以用基于二进制的遗传算法来搜索最优解。适合的读者包括电气工程相关专业的研究生、做调度算法的工程师,以及对智能优化算法感兴趣但还没上手写过完整代码的人。我会把建模、编码、Python实现和调参经验都摊开讲,尽量让一个刚接触遗传算法的人也能照着复制改参数就跑起来。
1. 二进制的遗传算法到底解决了一个什么问题
1.1 经济调度问题:从单目标到多目标的演化
传统经济调度的目标很纯粹:在满足系统负荷和机组出力上下限的前提下,求解各台发电机组的最优出力,使总燃料成本最低。燃料成本通常用二次函数近似描述:
$$ F_i(P_i) = a_i P_i^2 + b_i P_i + c_i $$
其中 $P_i$ 是第 $i$ 台机组的出力,$a_i$、$b_i$、$c_i$ 是机组成本系数。在不考虑网损时,约束条件就是功率平衡 $\sum P_i = P_D$($P_D$ 为总负荷需求)和各机组上下限 $P_{i,\min} \le P_i \le P_{i,\max}$。这类问题用等微增率准则或者拉格朗日乘数法就能解,教科书上讲得很清楚,工程上也用了很多年。
但现实情况比教科书复杂。一方面,电厂需要控制 NOx、SO2 等污染物的排放,排放可以近似写成机组的二次函数,于是出现了“含排放目标的经济调度”,它其实是一个多目标优化问题;另一方面,电能从发电端送到负荷中心,输电线路上的网损不可忽略,功率平衡约束就变成了 $\sum P_i = P_D + P_L$,其中 $P_L$ 是所有线路总网损,而且 $P_L$ 是机组出力的函数。这样一来,目标函数有了两个互相冲突的项,约束等式里又嵌了非线性项,拉格朗日法解起来非常别扭,λ迭代法也容易因为非线性陷入迭代震荡。这也是我第二版选择改用智能优化算法的直接原因。
加了排放目标和网损模型之后,问题的数学形式可以写成:
$$ \min \quad f = \sum_{i=1}^{N} \left[ w_c \cdot F_i(P_i) + w_e \cdot E_i(P_i) \right] $$
其中 $E_i(P_i) = \alpha_i P_i^2 + \beta_i P_i + \gamma_i$ 是排放函数,$w_c$ 和 $w_e$ 是成本和排放的权重系数。这个目标函数是非线性的、非凸的,再加上等式约束和不等式约束,传统方法要么需要大量求导和迭代,要么对初始点非常敏感。遗传算法不依赖梯度,不需要函数可导,只需要能算出每个解的目标值就能搜索,所以很适合这类问题。
1.2 为什么选二进制遗传算法,不选实数编码或粒子群
做智能优化的人都知道,同样是遗传算法,编码方式分二进制编码、实数编码、排列编码等好几类。经济调度里的决策变量是连续功率值,理论上用实数编码更直观,但我在这个项目里特意选了二进制编码,原因是:
第一,二进制编码天然把连续空间离散化了。机组出力有明确的上下限,二进制串解码后自动落在 $[P_{i,\min}, P_{i,\max}]$ 区间内,不需要额外处理变量越界问题。第二,二进制串的每一位代表一个基因位点,交叉和变异的语义清晰,单点交叉、位翻转变异都是最经典的遗传算子,调试起来容易判断问题出在哪个环节。第三,很多文献里的经典算例(包括 IEEE 标准测试系统)用的就是二进制遗传算法,结果对比起来方便。
不过二进制编码也有明显的代价:串长直接决定搜索空间的规模。如果一个机组用 10 位二进制表示,10 台机组就是 100 位,搜索空间大小是 $2^{100}$,这个数量级远超种群能覆盖的范围。所以二进制 GA 靠的不是穷举,而是通过选择、交叉、变异逐步逼近最优区域。为了缓解搜索空间过大的问题,编码位数只要满足精度需求即可,不需要追求过高的分辨率。
下表是我在方案选型时对二进制 GA 和实数 GA 的对比,供参考:
| 对比维度 | 二进制遗传算法 | 实数遗传算法 |
|---|---|---|
| 编码方式 | 决策变量映射为 0/1 串 | 决策变量直接用浮点数数组 |
| 搜索空间 | 离散,大小由编码位数决定 | 连续,理论上无穷 |
| 越界处理 | 解码后自动落在上下限内 | 需要额外设计边界约束逻辑 |
| 交叉算子 | 单点/多点/均匀交叉 | 模拟二进制交叉、算术交叉 |
| 变异算子 | 位翻转 | 高斯扰动、均匀扰动 |
| 实现复杂度 | 低,逻辑直观 | 中,需要写额外的算子 |
| 局部搜索精度 | 受编码位数限制 | 精度更高 |
| 可解释性 | 强,符合经典 GA 理论 | 相对弱一些 |
两种方案都用过之后,我的建议是:如果追求快速验证算法流程、重点在多目标或约束处理上,用二进制编码省心很多;如果在乎最终收敛精度、且决策变量维数不高,实数编码也完全可以,但边界处理和算子实现要小心。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 编码、目标函数和约束:模型设计的关键环节
2.1 二进制编码长度怎么定,精度怎么算
把连续功率映射成二进制串,第一步是确定每个机组的编码位数 $n_{bits}$。精度公式很简单:
$$ \Delta P = \frac{P_{\max} - P_{\min}}{2^{n_{bits}} - 1} $$
比如某机组出力范围 $[100, 500]$ MW,用 10 位二进制表示,$2^{10} - 1 = 1023$,精度 $\Delta P = 400 / 1023 \approx 0.391$ MW。对大多数经济调度场景,0.4 MW 左右的精度足够用了。如果希望精度提高到 0.1 MW,需要 $n_{bits} \ge \lceil \log_2(400/0.1 + 1) \rceil \approx 12$ 位,这时候每增加 1 位,单机的搜索空间就翻一倍,所以不要盲目追求高精度。
解码公式是:
$$ P_i = P_{i,\min} + \frac{(P_{i,\max} - P_{i,\min}) \cdot V_i}{2^{n_{bits}} - 1} $$
其中 $V_i$ 是该机组对应二进制片段转成的十进制整数。这里有个容易被忽略的细节:$V_i$ 可以取到 $2^{n_{bits}} - 1$,解码结果正好是 $P_{\max}$,不会越界,所以不需要额外做截断。
完整的个体编码是把所有机组的二进制片段首尾拼接。例如 3 台机组、每台 10 位,一个个体就是 30 位的 0/1 字符串。片段顺序要和机组编号一致,后面解码时按固定长度切分即可。
2.2 目标函数:燃料成本、排放成本如何加权融合
多目标处理最直接的方法是加权求和。在我的实现里,综合目标函数写成:
$$ f_{total} = w_c \cdot \sum F_i(P_i) + w_e \cdot \sum E_i(P_i) + \text{惩罚项} $$
在只研究排放目标对调度结果影响的场景下,通常固定 $w_c = 1$,然后扫描 $w_e$ 从 0 到 1 或者更大,观察成本和排放的权衡曲线。这样做的好处是代码改动小,一个权重循环就能跑出不同偏好下的调度方案;缺点是加权法只能找到凸帕累托前沿上的点,如果目标函数是非凸的,前沿中间的某些点会漏掉。不过对经济调度这个场景,成本和排放函数都是二次函数,凸性基本有保证,加权法的结果已经很有参考价值。
这里的权重系数不叫“价格”,因为排放函数计算出来的单位是吨/小时或 kg/h,和成本不一定在同一量纲。实际工程中可以用碳税、排污费把排放转换成经济成本,也可以让 $w_e$ 作为无量纲权重来调节解在帕累托前沿上的位置。我在实验中通常先跑 $w_e = 0$ 得到纯成本最优解,再跑 $w_e = 1$ 得到纯排放最优解,中间均匀取几个点,绘制的权衡曲线能直接看出“多花多少钱可以少排多少污染物”,这个信息比单纯一个最优解有价值得多。
还有一个细节:成本函数量级通常在 $10^4$,排放函数量级在 $10^2 \sim 10^3$,如果直接把排放项加进目标函数,不乘任何系数,排放权重的影响会被淹没。所以要么在排放项前乘一个缩放系数,要么用权重 $w_e$ 扫描时把范围拉宽。我在代码里会用 emission_scale = np.mean(cost_values) / np.mean(emission_values) 做一个粗略量级对齐,再在这个基础上乘 $w_e$,这样权重调整起来更直观。
2.3 输电损耗用B系数法,功率平衡约束怎么处理
输电损耗的精确计算需要跑潮流,但在经济调度里常用 B 系数法做近似。B 系数法的思想是把网损表示成机组出力的二次型:
$$ P_L = \sum_{i=1}^{N} \sum_{j=1}^{N} P_i B_{ij} P_j + \sum_{i=1}^{N} B_{i0} P_i + B_{00} $$
简化版可以忽略一次项和常数项,直接用 $P_L = \sum\sum P_i B_{ij} P_j$。B 系数矩阵通常由基准运行点的潮流结果推导得到,也可以从电力系统分析软件(比如 MATPOWER)导出。对测试系统来说,直接用文献中给出的 B 系数矩阵即可。
网损的加入让功率平衡约束变成 $\sum P_i = P_D + P_L$,这不再是一条直线约束,而是嵌入到目标函数里的非线性等式约束。处理方式有很多种,我在项目里用的是最通用的惩罚函数法:把约束违反量平方后乘以一个较大的惩罚系数加到目标函数中。
$$ f_{obj} = w_c \cdot \sum F_i + w_e \cdot \sum E_i + C_{penalty} \cdot \left( \sum P_i - P_D - P_L \right)^2 $$
机组出力上下限不需要额外惩罚,因为二进制解码已经保证了。这样做的优点是实现简单,缺点是需要小心调惩罚系数:太小则结果不满足功率平衡,太大则目标函数地形尖锐,搜索容易陷入局部最优。这个调参过程我会在第 4 章详细讲。
3. Python代码实现:从种群初始化到迭代收敛
3.1 数据准备:机组参数、排放系数、B系数矩阵
先把测试系统的数据准备好。我用一个 3 机系统作为示例,实际扩展到更多机组只需要把列表加长。
python复制import numpy as np
import random
# 燃料成本系数:a * P^2 + b * P + c
a = [0.0070, 0.0095, 0.0090]
b = [7.0, 6.2, 6.5]
c = [240.0, 200.0, 220.0]
# 排放系数:alpha * P^2 + beta * P + gamma
alpha = [0.00421, 0.00683, 0.00613]
beta = [0.32767, -0.54551, 0.39832]
gamma = [13.85932, 40.26690, 32.10982]
# 机组出力上下限
Pmin = [100, 60, 80]
Pmax = [500, 300, 350]
# B系数矩阵(二次型网损系数)
B = np.array([
[0.0017, 0.0012, 0.0007],
[0.0012, 0.0014, 0.0009],
[0.0007, 0.0009, 0.0030],
])
# 负荷需求 (MW)
PD = 700
n_unit = len(a)
这里要注意 B 系数矩阵必须是对称的,这是二次型表达的前提。矩阵元素单位是 1/MW,所以算出来的网损单位是 MW。如果你用的是包含一次项 $B_{i0}$ 和常数项 $B_{00}$ 的完整 B 系数法,只需要在网损函数里多加两项即可。
3.2 解码、适应度评估与惩罚函数
编码与解码是二进制遗传算法的核心桥接环节。我用每个机组 10 位二进制拼接成 30 位个体,解码时按固定长度切分。
python复制n_bits_per_unit = 10
chrom_length = n_unit * n_bits_per_unit
def decode_individual(ind):
"""将二进制个体解码为各机组出力值列表"""
P = []
for i in range(n_unit):
seg = ind[i * n_bits_per_unit : (i + 1) * n_bits_per_unit]
val = int(seg, 2)
Pi = Pmin[i] + (Pmax[i] - Pmin[i]) * val / (2**n_bits_per_unit - 1)
P.append(Pi)
return P
计算网损:
python复制def calc_loss(P):
"""B系数法计算输电损耗"""
P_arr = np.array(P)
PL = P_arr @ B @ P_arr
return PL
这里用 @ 做矩阵乘法,注意 P_arr 是一维数组,P_arr @ B @ P_arr 的结果就是二次型 $\sum\sum P_i B_{ij} P_j$。
目标函数:
python复制def objective_function(ind, w_e=0.5, penalty_coef=10000.0):
P = decode_individual(ind)
fuel_cost = 0.0
emission = 0.0
for i in range(n_unit):
fuel_cost += a[i] * P[i]**2 + b[i] * P[i] + c[i]
emission += alpha[i] * P[i]**2 + beta[i] * P[i] + gamma[i]
loss = calc_loss(P)
power_balance_violation = abs(sum(P) - PD - loss)
# 成本权重固定为1,排放权重由外部传入
obj = 1.0 * fuel_cost + w_e * emission + penalty_coef * power_balance_violation**2
return obj
我建议把 w_e 和 penalty_coef 作为参数传进函数,而不是写死在函数内部。这样调参时只需要改外层变量,不需要改函数本身,避免来回改代码引入低级错误。
适应度函数在遗传算法里习惯上“越大越好”,而这里的 objective_function 是“越小越好”。最简单的做法是直接取倒数:
python复制def fitness(ind, w_e=0.5, penalty_coef=10000.0):
obj = objective_function(ind, w_e, penalty_coef)
return 1.0 / (1.0 + obj)
不过我在实现里用的是锦标赛选择,它直接比较目标函数值大小,不需要把目标值转换成适应度,所以我实际写代码时通常不显式定义 fitness 函数,而是把 objective_function 的值传给选择算子。这样少一层转换,逻辑更清晰。
3.3 选择、交叉、变异三种算子怎么落地
锦标赛选择实现:
python复制def tournament_select(population, obj_values, tournament_size=3):
candidates = random.sample(range(len(population)), tournament_size)
best_idx = min(candidates, key=lambda i: obj_values[i])
return population[best_idx]
锦标赛选择的优点是不需要归一化目标值,也不需要处理“最小化问题倒转成最大化”的问题,直接从候选集中挑目标值最小的就行。tournament_size=3 是常用的取值,它控制选择压力:值越大,选择压力越大,收敛越快,但也更容易早熟。
单点交叉:
python复制def single_point_crossover(p1, p2, pc=0.9):
if random.random() > pc:
return p1, p2
point = random.randint(1, len(p1) - 1)
c1 = p1[:point] + p2[point:]
c2 = p2[:point] + p1[point:]
return c1, c2
交叉点是随机选的,注意取值范围要避开 0 和串尾,否则交叉等于没交叉。
位翻转变异:
python复制def bit_flip_mutation(ind, pm=0.03):
lst = list(ind)
for i in range(len(lst)):
if random.random() < pm:
lst[i] = '1' if lst[i] == '0' else '0'
return ''.join(lst)
变异率 $p_m$ 的常见取法是 $1 / \text{chrom_length}$,比如串长 30,$p_m \approx 0.033$。这个值可以保证平均每个个体每代大约翻转 1 个位点,既保持了一定的探索能力,又不会让算法退化成随机搜索。
3.4 精英保留与主循环框架
主循环的逻辑是:初始化种群,计算目标值,用锦标赛选择选出父代,交叉、变异生成子代,然后把精英(历史最优个体)直接保留下来,替换掉子代中的最差个体。这样能保证最优解不会在进化过程中丢失。
python复制def genetic_algorithm(pop_size=60, max_gen=200, pc=0.9, pm=0.03, w_e=0.5, penalty_coef=10000.0, elite_size=2, seed=42):
random.seed(seed)
np.random.seed(seed)
# 初始化种群
population = []
for _ in range(pop_size):
ind = ''.join(random.choice('01') for _ in range(chrom_length))
population.append(ind)
best_individual = None
best_obj = float('inf')
history = []
for gen in range(max_gen):
obj_values = [objective_function(ind, w_e, penalty_coef) for ind in population]
# 记录全局最优
current_best_idx = int(np.argmin(obj_values))
if obj_values[current_best_idx] < best_obj:
best_obj = obj_values[current_best_idx]
best_individual = population[current_best_idx]
history.append(best_obj)
# 精英保留
sorted_idx = np.argsort(obj_values)
elites = [population[i] for i in sorted_idx[:elite_size]]
# 生成新一代
new_population = []
while len(new_population) < pop_size:
p1 = tournament_select(population, obj_values)
p2 = tournament_select(population, obj_values)
c1, c2 = single_point_crossover(p1, p2, pc)
c1 = bit_flip_mutation(c1, pm)
c2 = bit_flip_mutation(c2, pm)
new_population.extend([c1, c2])
# 用精英替换最差的个体
new_population[:-elite_size] = new_population[:pop_size - elite_size]
new_population[pop_size - elite_size:] = elites
population = new_population[:pop_size]
# 返回最优解和收敛曲线
return best_individual, best_obj, history
这里有个细节:new_population.extend([c1, c2]) 之后,new_population 的长度可能刚好是 pop_size,也可能是 pop_size + 1(因为循环一次生成两个子代,而种群大小不一定是偶数)。稳妥的做法是循环结束后 population = new_population[:pop_size] 做截断,同时保证精英替换后总长度正好为 pop_size。
实际运行时,我把 w_e 放到外层循环里,用不同权重扫描多组实验。这样一份代码能同时得到纯成本方案、纯排放方案和折中方案,方便后续画帕累托曲线。
4. 参数怎么调,实验怎么做:实际调参实录
4.1 种群、迭代次数、编码位数如何搭配
最初我用的是“教科书默认参数”:种群 50、迭代 150、串长 30、交叉概率 0.7、变异概率 0.01。跑出来的结果能用,但有个问题:功率平衡约束的违反量偏大,大概在几 MW 到十几 MW 之间波动,必须靠惩罚函数硬拉回来。这说明目标函数的惩罚项虽然生效了,但搜索过程没有充分收敛到可行域内部。
后来我把种群调到 80、迭代调到 300,结果明显改善。核心原因很简单:30 位二进制串的搜索空间是 $2^{30} \approx 10^9$,50 个个体迭代 150 代,总共只评估了 7500 个解,这个采样密度对 $10^9$ 空间来说太稀疏了。经济调度问题虽然没有想象中那么复杂,但约束惩罚项会在可行域周围形成陡峭的“山谷”,种群太小很难稳定地沿着山谷找到最优。
经验公式可以参考:二进制串长每增加 10 位,种群规模尽量翻倍;迭代次数至少保证“种群 × 迭代数”覆盖搜索空间的 $10^{-6}$ 量级以上。当然这是经验值,不是严格推导,但对大多数经济调度算例够用。
4.2 交叉概率、变异概率的调试经验
交叉概率 $p_c$ 控制基因重组的频率。$p_c$ 太低(比如 0.4),种群很快失去多样性,个体之间趋于同质;$p_c$ 太高(比如 0.99),好的基因块容易被频繁拆散,收敛速度变慢。我试下来 0.85 ~ 0.95 之间比较合适,最后固定用 0.9。
变异概率 $p_m$ 是最敏感的开关。$p_m = 0.01$ 时,30 位串平均每代只有 0.3 个位点变异,对 80 个个体来说,整个种群每代只有 24 个位点发生变化,探索能力严重不足;$p_m = 0.1$ 时,每个个体平均有 3 个位点翻转,算法行为接近随机搜索,经常出现“好不容易找到一个好解,下代就被变异破坏了”的情况。我最后的取值在 0.03 附近,接近 $1/\text{chrom_length}$ 的理论推荐值。
还有一个实用技巧:可以在迭代中期把 $p_m$ 临时调大。比如前 100 代用 0.02,后 100 代提高到 0.05,这样前期保持稳定收敛,后期增加跳出局部最优的概率。我在代码里实现了一个简单版本,用 if gen == 100: pm = 0.05 直接改,效果比固定变异率好一些。
4.3 不同排放权重下,成本和排放的权衡结果怎么解读
我只说实测下来的趋势,具体数值取决于你的测试系统。
当 $w_e = 0$ 时,算法只最小化燃料成本,得到的调度方案成本最低,但排放最高。这是因为低成本的机组往往不是低排放机组,多烧便宜煤和少排污染物天然矛盾。当 $w_e = 0.5$ 时,排放开始明显下降,成本略有上升,这时候的调度方案会偏向经济性和环保性的折中。当 $w_e = 1.0$ 时,排放达到一个很低的水平,但成本上升幅度可能超过 10%。
这个权衡曲线特别适合放在论文或报告里展示,它直观地回答了“为了减少单位排放需要多花多少钱”这个决策者最关心的问题。要注意的是,单个权重下跑出来的结果有随机性,正式分析时每个权重至少跑 10 次,取最优值或均值,而不是跑一次就下结论。
另外,排放权重的上限不一定要停到 1。你可以先跑 $w_e = 0$ 和 $w_e = 5$,看看排放是否已经收敛到“再加大权重也不会明显下降”的饱和点,再据此确定扫描范围。这样画出来的帕累托曲线覆盖更完整。
5. 常见问题与排查技巧实录
5.1 功率平衡一直不收敛,大概率是惩罚系数没调好
这是我在第一版代码里踩得最狠的坑。当时 penalty_coef 取的是 100,目标函数里成本和排放的量级是上万,惩罚项的平方项量级也是上万,看起来差不多,但搜索结果总是出现 $\sum P_i$ 比 $P_D + P_L$ 小 20~30 MW 的情况。原因很简单:100 倍的惩罚对目标函数的“推回”力度不够,GA 觉得少发一点电省下来的燃料成本比惩罚还多,所以干脆不满足约束。
把 penalty_coef 从 100 一路调到 10000 之后,功率平衡才开始被严格满足。经验是:惩罚系数至少要比目标函数中燃料成本项的量级大 1~2 个数量级。但也不要无限大,penalty_coef = 10^6 会让目标函数的梯度变化极其剧烈,搜索过程像在悬崖边上走路,一个交叉操作就可能把个体推出可行域,导致大量个体被惩罚项主导,多样性骤减。
建议的调参方法是:固定其他参数,跑一个 $w_e = 0$ 的基准实验,观察 power_balance_violation 的均值。如果这个值超过 0.5 MW,就增大惩罚系数,直到满足为止。这个检查动作应该加在代码里,每次迭代都输出一下当前最优个体的违反量。
5.2 早熟现象:怎么判断,怎么缓解
早熟是遗传算法最经典的问题,表现是前几十代快速收敛,之后最优解基本不动,种群内所有个体几乎变成同一个或少数几个模式。判断方法很简单:打印每一代的最优目标值和种群内个体目标值的标准差。如果目标值标准差在 30 代内就趋近于 0,说明种群多样性严重不足。
缓解早熟的办法,按优先级排序:
- 提高变异概率,比如从 0.02 提到 0.05;
- 增大种群规模,让初始多样性更充足;
- 改用锦标赛选择,并适当增大锦标赛规模,但要小心选择压力过大会加剧早熟;
- 引入自适应变异率:如果连续 N 代最优解没有提升,临时把变异率提高一个档位。
实际项目中,我一般先调种群规模和变异概率,只有在两者都无效时才考虑自适应机制,因为自适应逻辑会增加代码复杂度,对新手不友好。
5.3 二进制串溢出与解码越界问题
解码公式本身不会越界,因为 $V_i = 2^{n_{bits}} - 1$ 时结果正好是 $P_{\max}$。但有一个容易被忽略的坑:如果直接用 int('1111111111', 2) 换成十进制是 1023,没问题;但如果把多个片段拼接后整体转 int,再按位切割,必须处理字符串切片的边界。我在第一版写过一个 bug,把片段切分下标写成了 i * n_bits_per_unit 和 (i + 1) * n_bits_per_unit - 1,导致最后一个片段的最后一位丢失,解码结果比预期偏小。
建议在解码函数里加一个断言:
python复制assert len(ind) == chrom_length, f"个体长度 {len(ind)} 不等于 {chrom_length}"
这样一旦串长不对,程序立刻报错,而不是悄悄给出错误结果。
5.4 随机种子与结果可复现性
遗传算法本质是随机搜索,每次运行结果都会有波动。如果你只是想看一个大致最优解,不固定随机种子也能用;但如果要对不同权重、不同参数做对比实验,不固定种子就会噪声太大,无法判断差异来自参数还是运气。
我的做法是每次实验开始前 random.seed(seed) 和 np.random.seed(seed) 同时设置,把 seed 作为实验参数之一。每个权重下跑多个种子(比如 10 个),最后报告最优值、平均值和标准差。这样写论文或报告时有说服力,后续排查问题时也能精确复现当时的结果。
5.5 网损计算的两个隐蔽细节
第一个细节:B 系数矩阵所代表的网损基准场景要和当前负荷水平匹配。B 系数法本质是在某个基准运行点附近做的近似,如果实际负荷离基准点太远,网损计算结果会有偏差,进而影响功率平衡约束的精度。工程上可以做分段 B 系数,不同负荷区间用不同的矩阵,但项目代码里通常取一组系数够用。
第二个细节:当机组台数较多时,网损二次型计算千万别手写双重循环,直接用 P_arr @ B @ P_arr 向量化运算,既简洁又不会下标出错。手写双重循环在 3 机系统里问题不大,但扩展到几十台机组时会又慢又容易错。
6. 一点个人体会与后续扩展思路
最后分享我的实际体会。经济调度加遗传算法,代码本身并不复杂,真正的难点在于目标函数怎么设、惩罚系数怎么调、编码位数和种群规模怎么匹配,这些工程细节决定了最终结果的可靠性。如果你从头开始做,我建议先只做成本加功率平衡,跑通之后再逐步加排放目标和网损,每一步都用基准算例验证过再往上叠,出问题时也容易定位。
这个项目后续的扩展方向很明确。加权法只是多目标处理的入门方案,想做得更严谨可以把加权和改成真正的多目标进化算法,用非支配排序的思路一次跑出完整的帕累托前沿。另一个方向是在经济调度的基础上加机组启停约束,把问题从 ED 升级为 UC-ED 联合优化,这时候二进制编码反而更有优势,因为机组启停 0/1 状态本来就可以用二进制位表示。爬坡约束、储能系统、新能源出力波动这些实际运行中的约束,也都可以在新的目标函数和约束框架里逐步加进去。改动起来都不算大,关键是弄清楚每一步修改对搜索空间和收敛行为的影响。
