ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

双种群进化算法优化模糊柔性作业车间调度:完工时间与能耗协同

双种群进化算法优化模糊柔性作业车间调度:完工时间与能耗协同 简介针对能源效率模糊柔性作业车间调度问题EFFJSP这份PDF文档实现了带反馈机制的双种群进化算法FBEA面向科研人员与智能调度系统开发者。文档完整复现了论文《A Bi-Population Evolutionary Algorithm With Feedback for Energy-Efficient Fuzzy Flexible Job Shop Scheduling》算法框架包含模糊数运算、约束处理、多目标优化等关键组件的Python代码与逐段解释详细展示了模糊能耗计算、四种启发式初始种群生成、基于质量的种群反馈调整以及新繁殖、交叉、变异和增强局部搜索等过程。可视化工具能够直观呈现调度方案分析结果方便对比与扩展。资源共1个PDF文件大小881KB已有59人浏览学习可作为学术研究复现基线或工业调度优化参考。1. 为什么压住完工时间和能耗需要双种群进化算法车间里有句老话排产排得好电费少一半交期还不恼。模糊柔性作业车间调度FFJSP难在哪工序可以选多台机器每台机器的加工时间又是个模糊区间——操作工手法快慢、材料批次波动都会让工时不是定数。这种系统里只盯完工时间电费会涨只盯能耗交货期就崩。双种群进化算法的做法是让一个种群顶着模糊完工时间搜另一个种群压能耗再定期把两边精英互换让两个互相打架的指标在同一个框架里同时往前走。本文从问题建模写到 Python 主循环把参数和踩坑一条条讲透适合正在做排产系统设计、想少走弯路的工程师。2. 模糊柔性作业车间调度的数学模型三角模糊数与综合能耗目标写代码前先把模型立住。这一章把一个 FFJSP 实例的输入、目标和约束都用数学形式固定下来后面所有解码器、适应度函数都要回到这里。很多第一次做这个方向的人一上来就写进化算子结果目标函数定义得稀里糊涂后面换算例就翻车。2.1 模糊加工时间用三角模糊数表示三个参数从哪里来柔性作业车间里工序 o_ij 可以在候选机器集合 M_ij 中的任意一台机器上加工这是“柔性”。加工时间不是一个确定值而是“大概 4 小时、快则 3 小时、慢则 6 小时”这就是“模糊”。工程上最常用的表示是三角模糊数 (a, b, c)a 是乐观时间b 是最可能时间c 是悲观时间。这三个参数不是拍脑袋填的。如果车间有 MES 或手工记录的历史工时我会按同一工序在同一台机器上的历史数据取分位数a 取 10% 分位数b 取 50% 分位数c 取 90% 分位数。数据少的时候就找班组长填“最快/最可能/最慢”三列这个办法粗但能用。注意 b 不一定是 (ac)/2工人说“多数时候偏慢”那 b 就更靠近 c这很正常。三角模糊数的加法是逐项相加两个三角模糊数相加仍然是三角模糊数这是调度推演能往下走的基础。比较和去模糊化我用加权平均公式defuzz (a 2b c) / 4也就是把最可能时间 b 的权重加倍。这个公式比直接取 (abc)/3 更贴近车间里“多数工时的落点偏 b”的事实而且代码里排序时就靠它做标量比较。后面第 5 章会专门说这个公式选错了会产生什么后果。2.2 模糊完工时间的传播从工序结束时间到系统最大完工时间调度解出来以后每个工序有一个模糊开工时间 S_ij 和模糊完工时间 C_ij。工序一旦在机器上开始就不能被打断所以 C_ij S_ij p_ij其中 p_ij 是在选中机器上对应的三角模糊加工时间。开工时间要同时满足两个约束同一台机器上一个工序干完才能干下一个同一个工件的工序要按工艺顺序一个接一个。于是S_ij max(该机器上一道工序完工时间, 该工件上一道工序完工时间)这两个时间都是三角模糊数max 怎么取我用的办法是去模糊化后取较大者再用它的原始模糊值往上累加。严格来说这丢掉了模糊数的部分形状信息但工程上快、稳、不容易出怪结果。如果你的排序要求更严格可以用两两比较的模糊数排序规则但每步解码的耗时会上一个量级。系统最大完工时间 C_max 等于所有工件最后一道工序完工时间中的最大值它仍然是一个三角模糊数。最终上报给业务系统时再用 defuzz (a2bc)/4 换算成单个数这个数就是调度方案在“按时交付”维度上的得分。2.3 能耗目标加工、待机与机器启停三部分怎么算能耗模型是双种群算法里的另一个主角。一个完整到能拿来谈系统落地的能耗目标至少包含三部分加工能耗工序在机器上实际切削/成型时消耗的能量等于加工功率 × 模糊加工时间去模糊化后的时长按每个工序累加待机能耗机器空转等待消耗的能量等于待机功率 × 空转时长启停能耗如果机器频繁开机、停机设备说明书上的启动电流冲击和热量损失不能忽略这部分通常折算成一次固定能耗。一个常见的系统设计误区是只算加工能耗。如果只算加工能耗进化算法会倾向于把工序集中到功率高但速度快的机器上表面看总加工能耗很小实际上其他机器空转等料待机电费哗哗涨。所以我在目标里一定要加待机能耗待机总时长用“每台机器的调度跨度减去该机器加工占用总时长”来估计。启停能耗可以在建模阶段先放一个常数等后面第 6 章再谈怎么细化。这样两个目标函数就固定下来目标 1 是模糊最大完工时间去模糊化后的值 F1目标 2 是总能耗 F2。F1 的分母单位是小时F2 的单位是千瓦时两者量纲不同、数值范围可能差几十倍这为后面双种群的权重设计埋了一个大坑第 3 章和第 5 章都会再提到。3. 双种群进化算法设计双链编码、遗传算子与精英迁移机制模型定好了接下来是算法结构。双种群进化算法不是把两个普通遗传算法并行跑那么简单它的核心在“分工”和“交换”这两个动作上。这一章先把选型理由讲透再给出编码、算子和迁移机制的完整设计让第 4 章的代码有依据可循。3.1 为什么拆成两个种群带权目标在单一种群里怎么打起来很多第一篇论文的做法是把两个目标加权成一个fitness w × F1 (1-w) × F2w 取 0.5然后跑一个标准遗传算法。这个做法不是不能用只是工程上有两个毛病。第一w 是固定值算法的搜索方向被焊死。你不能让同一个种群在“赶工”和“省电”之间反复横跳只能得到一条固定折中方向的解。第二w 对目标数值范围极度敏感F1 是几十小时级别F2 是几千千瓦时级别不归一化的话 w 根本没有意义。双种群的做法是让两个种群各自持有一组权重种群 A 用 w0.8 主要压模糊完工时间种群 B 用 w0.2 主要压能耗。两个种群各自朝不同的方向演化每过若干代把对方的好解拿过来。这样在一代一代搜索中不是一群人同时照顾两个目标而是两个专才队伍各自深挖然后互相借脑。等到跑完把两个种群合并起来做非支配排序就能得到一个覆盖面更宽的 Pareto 前沿。这个“分工交换”的框架比一个大种群带一个动态权重要稳定得多。3.2 染色体编码与解码方向工序顺序串 机器选择串FFJSP 的个体必须同时表达两件事工序谁先谁后、每道工序用哪台机器。我用双链编码这也是 FJSP 系列问题最经典的编码方式。第一条链是工序顺序串。假设有 3 个工件工件 0 有 2 道工序工件 1 有 3 道工件 2 有 2 道那么顺序串长度为 7内容是 0、0、1、1、1、2、2 的一个排列。解码时从左往右扫第几次出现某个工件号就代表这个工件的第几道工序。比如串是 [1,0,2,1,0,2,1]先解工件 1 的第 1 道工序再解工件 0 的第 1 道工序然后工件 2 的第 1 道工序解到第五位是 0 时解的是工件 0 的第 2 道工序。这种编码天然保证一个工件的工序顺序不会解出“第 2 道工序干完才允许干第 1 道”这种非法解。第二条链是机器选择串。长度和顺序串一样第 k 个基因对应第 k 个位置上的工序所使用的机器编号。这里有个必须守住的约束机器必须在工序的候选机器集合里。生成初代个体时如果随机指派了不可用机器要做一次合法性修复最简单的方法就是随机换一台候选机器。第 4 章代码里我会放一个更快的兜底逻辑。解码时按顺序串扫描对每个工序找到它的机器计算这台机器的完工时间和该工件上一道工序的完工时间取较大者作为模糊开工时间加上加工时间得到模糊完工时间更新时间表。整个过程沿着时间轴推进不重新排列已调度工序是标准贪心解码。贪心解码不是最优的比如没有主动去填机器上的空闲缝隙但它快、稳定、容易调试适合作为系统基线。进阶的插入式解码在第 6 章提一句即可。3.3 交叉、变异和精英迁移参数从跑通到收敛遗传算子也要跟着双链编码改。顺序串用顺序交叉OX做法是取父本 A 的一段连续基因作为模板再从父本 B 中把不属于这段基因的工序号按顺序填进子代的空位里。机器选择串用单点交叉就是从某个随机位置把两父本的机器基因切开来交换尾部。变异算子我分两条链处理顺序串随机交换两个位置的工序号保证不能把同一工件的工序数量改掉机器链以一定概率把某个工序换到另一台可用机器上。变异概率一般取 0.1 到 0.2太高会把已经收敛的好解打散。两个种群规模在 40 到 100 之间都可以小算例 40 足够。精英迁移是关键参数。迁移间隔太长两个种群各干各的等于白做了交换机制迁移间隔太短比如每 2 代就迁两个种群会迅速同质化Pareto 前沿反而变窄。我一般用迁移间隔 10 到 20 代每代从每个种群中挑出按自身适应度排名前 2 到 5 的个体复制一份给对方种群替换对方种群中最差的几个。迁移数控制在种群规模的 5% 到 10% 以内超过这个量基本等于在合并两个种群。整个算法的骨架如下第 4 章会把每一步补成可运行的 Python 代码# 算法骨架伪代码 # 初始化种群Aw_a偏完工时间和种群Bw_b偏能耗 # 对每个种群独立执行 # 锦标赛选择 - 顺序交叉 机器单点交叉 - 变异 - 生成新一代 # 每 migrate_interval 代 # 种群A的精英复制给种群B种群B的精英复制给种群A # 各自替换最差的 migrate_num 个个体 # 迭代结束后合并两个种群的 Pareto 非支配解4. 系统设计落地可运行的 Python 双种群调度核心代码这一章把前面三章的模型和算法落成可以直接跑的 Python 代码。代码分成四段模糊数类、问题生成器、解码器、双种群进化主循环。每一段后面跟着参数说明和设计原因。我没有用任何第三方库标准库就能跑通方便你直接放进自己的系统里做原型验证。4.1 模糊数类与测试算例生成器import random class TriFuzzy: 三角模糊数 (a, b, c)约束 a b c __slots__ (a, b, c) def __init__(self, a, b, c): if not (a b c): raise ValueError(三角模糊数必须满足 a b c) self.a, self.b, self.c float(a), float(b), float(c) def __add__(self, other): return TriFuzzy(self.a other.a, self.b other.b, self.c other.c) def defuzz(self): # 加权平均中间值 b 权重加倍更贴近车间现实 return (self.a 2 * self.b self.c) / 4.0 def __repr__(self): return f({self.a}, {self.b}, {self.c}) def defuzz_key(t): return t.defuzz() def gen_instance(n_jobs, n_machines, seed1): 生成一个随机 FFJSP 算例 rng random.Random(seed) instance [] for _ in range(n_jobs): ops [] for _ in range(rng.randint(2, 4)): cand [] for m in range(n_machines): if rng.random() 0.6: base rng.randint(3, 9) cand.append((m, TriFuzzy(base - 2, base, base 3))) if not cand: # 保证每道工序至少有一台机器能加工 m rng.randrange(n_machines) cand.append((m, TriFuzzy(1, 3, 5))) ops.append(cand) instance.append(ops) return instance machine_power [5.0, 2.0, 8.0] # 各机器加工功率单位 kW idle_power [0.5, 0.2, 0.8] # 各机器待机功率单位 kW第一个类 TriFuzzy 是整个程序的地基。注意__slots__不是为了耍酷是防止在做几十代进化的时候创建大量无用属性字典拖慢内存。加号运算符被重载所以解码器里可以直接写end start ptime。defuzz方法就是参数说明里那个 (a2bc)/4 公式所有需要比较和累加能耗的地方都用它。gen_instance生成一个两层嵌套结构的算例外层是工件列表内层是每个工件的工序列表每个工序是一个候选机器列表候选机器列表的元素是“机器编号 三角模糊数”。0.6 这个概率控制机器柔性程度概率越小机器可选性越低问题越硬。如果你手上有真实的工艺路线数据把这段换成从数据库读入即可后面所有代码都不受影响。4.2 解码器从双链基因到模糊完工时间和能耗def flat_ops(instance): 把嵌套的 instance 展平成全局工序索引表 flat [] key {} for j, ops in enumerate(instance): for i, cand in enumerate(ops): key[(j, i)] len(flat) flat.append((j, i, cand)) return flat, key def decode(instance, order, machine_gene): 输入算例、工序顺序串、机器选择串 输出去模糊化后的完工时间和总能耗 flat, key flat_ops(instance) n_mach len(machine_power) next_op [0] * len(instance) # 每个工件下一次要解的工序下标 job_end [TriFuzzy(0, 0, 0)] * len(instance) # 每个工件最后一道工序的模糊完工时间 mach_end [TriFuzzy(0, 0, 0)] * n_mach # 每台机器最后一道工序的模糊完工时间 mach_busy [0.0] * n_mach # 每台机器的加工占用时长去模糊化 for jid in order: op_idx next_op[jid] next_op[jid] 1 k key[(jid, op_idx)] cand flat[k][2] m machine_gene[k] # 如果机器基因非法回退到耗时最短的候选机器 if m not in [x[0] for x in cand]: m min(cand, keylambda x: x[1].defuzz())[0] ptime [x[1] for x in cand if x[0] m][0] start max(mach_end[m], job_end[jid], keydefuzz_key) end start ptime mach_end[m] end job_end[jid] end mach_busy[m] ptime.defuzz() makespan max(job_end, keydefuzz_key).defuzz() energy 0.0 # 加工能耗加工功率 x 加工时长 for k, (j, i, cand) in enumerate(flat): m machine_gene[k] if m not in [x[0] for x in cand]: m min(cand, keylambda x: x[1].defuzz())[0] ptime [x[1] for x in cand if x[0] m][0] energy ptime.defuzz() * machine_power[m] # 待机能耗机器跨度减去加工占用时间 for m in range(n_mach): span mach_end[m].defuzz() energy max(0.0, span - mach_busy[m]) * idle_power[m] return makespan, energy解码器核心逻辑在for jid in order这个循环里。对顺序串的每一个基因先取对应工件下一次要解的工序编号再找到这台工序在全局工序索引表里的位置读取候选机器集合。机器号的合法性兜底放在循环内如果进化算子产生了非法机器直接退回到该工序候选机器里“去模糊化时间最短”的那一台保证任何一个个体都至少能被解码。start max(mach_end[m], job_end[jid], keydefuzz_key)这一行同时兑现了 2.2 节里的两个约束机器忙就等机器工件上一道工序没干完就等工序。模糊数没有原生的 max这里用去模糊化之后的值比较大小然后取原始模糊值。待机能耗的计算是一个务实近似假设机器从 0 时刻开始统计每台机器的“跨度”用最后完工时间表示减去实际加工占用时间剩下的就是待机时长再乘待机功率。这个模型在只有两台机器、订单排队严重时会有偏差因为机器的空闲时间可能分散在加工区间里但对于算法对比和参数调优来说已经足够稳定。如果想精确到每个空闲缝隙需要在解码器里额外维护每台机器的区间列表代码复杂度会上一个台阶性能也会明显下降。4.3 双种群进化主循环选择、交叉、变异与精英迁移def make_individual(instance, rng): 随机生成一个合法个体 flat, _ flat_ops(instance) order [] for j, ops in enumerate(instance): order [j] * len(ops) rng.shuffle(order) machine_gene [] for _, _, cand in flat: machine_gene.append(rng.choice([m for m, _ in cand])) return order, machine_gene def crossover(pa, pb, rng): 顺序串 OX 交叉机器串单点交叉 order_a, mac_a pa order_b, mac_b pb n len(order_a) if n 2: p1, p2 sorted(rng.sample(range(n), 2)) seg_a order_a[p1:p2] tmp_b [x for x in order_b if x not in seg_a] child_a tmp_b[:p1] seg_a tmp_b[p1:] seg_b order_b[p1:p2] tmp_a [x for x in order_a if x not in seg_b] child_b tmp_a[:p1] seg_b tmp_a[p1:] else: child_a, child_b list(order_a), list(order_b) cut rng.randrange(n) mac_c_a mac_a[:cut] mac_b[cut:] mac_c_b mac_b[:cut] mac_a[cut:] return (child_a, mac_c_a), (child_b, mac_c_b) def mutate(ind, instance, rng, p_mut0.15): 顺序串交换两个位置机器串换一台可用机器 order list(ind[0]) if rng.random() p_mut and len(order) 1: i, j rng.sample(range(len(order)), 2) order[i], order[j] order[j], order[i] mac list(ind[1]) flat, _ flat_ops(instance) for k in range(len(mac)): if rng.random() p_mut: cand flat[k][2] available [m for m, _ in cand] if len(available) 1: mac[k] rng.choice([m for m in available if m ! mac[k]]) return (order, mac) def fitness(ind, weight, scaler, instance): 权重适应度目标值先做 min-max 归一化 o1, o2 decode(instance, ind[0], ind[1]) n1 (o1 - scaler[0]) / (scaler[1] - scaler[0] 1e-9) n2 (o2 - scaler[2]) / (scaler[3] - scaler[2] 1e-9) return weight * n1 (1 - weight) * n2make_individual里用rng.shuffle保证每个工件的工序数量在顺序串中不丢失机器基因逐个工序从候选机器集合里随机选一台保证初始群体 100% 合法。交叉函数里[x for x in order_b if x not in seg_a]这一句是 OX 交叉的核心它保留父本 B 中除模板片段之外的所有工序顺序维持每个工件出现的次数不变。机器串的单点交叉没有合法性校验因为合法性兜底已经被放在了解码器里。适应度函数里有个关键细节两个目标不直接乘权重而是先做 min-max 归一化。scaler 是一个四元组格式是“完工时间下限、完工时间上限、能耗下限、能耗上限”由初始种群估算得到。这一步不做的话能耗数值几千、完工时间才几十w0.8 也救不回来这是 3.1 节里埋下的坑。def tournament(pop, fit_func, rng, k2): 锦标赛选择 best None best_fit float(inf) for _ in range(k): ind rng.choice(pop) f fit_func(ind) if f best_fit: best, best_fit ind, f return best def run_dual_pop(instance, gen80, pop_size40, migrate_interval10, migrate_num2, w_a0.8, w_b0.2, seed1): rng random.Random(seed) pop_a [make_individual(instance, rng) for _ in range(pop_size)] pop_b [make_individual(instance, rng) for _ in range(pop_size)] # 用两个初始种群一起估算目标值范围 raw [decode(instance, ind[0], ind[1]) for ind in pop_a pop_b] scaler ( min(x[0] for x in raw), max(x[0] for x in raw), min(x[1] for x in raw), max(x[1] for x in raw), ) def fit_a(ind): return fitness(ind, w_a, scaler, instance) def fit_b(ind): return fitness(ind, w_b, scaler, instance) for g in range(gen): new_a, new_b [], [] while len(new_a) pop_size: p1 tournament(pop_a, fit_a, rng) p2 tournament(pop_a, fit_a, rng) c1, c2 crossover(p1, p2, rng) new_a.append(mutate(c1, instance, rng)) new_a.append(mutate(c2, instance, rng)) while len(new_b) pop_size: p1 tournament(pop_b, fit_b, rng) p2 tournament(pop_b, fit_b, rng) c1, c2 crossover(p1, p2, rng) new_b.append(mutate(c1, instance, rng)) new_b.append(mutate(c2, instance, rng)) pop_a new_a[:pop_size] pop_b new_b[:pop_size] if (g 1) % migrate_interval 0: pop_a.sort(keyfit_a) pop_b.sort(keyfit_b) best_a pop_a[:migrate_num] best_b pop_b[:migrate_num] pop_a[-migrate_num:] best_b pop_b[-migrate_num:] best_a # 合并两个种群的个体 total pop_a pop_b front [] for ind in total: o1, o2 decode(instance, ind[0], ind[1]) dominated False for other in total: oo1, oo2 decode(instance, other[0], other[1]) if (oo1, oo2) ! (o1, o2) and oo1 o1 and oo2 o2: dominated True break if not dominated: front.append((o1, o2, ind[0], ind[1])) return front instance gen_instance(3, 3, seed7) front run_dual_pop(instance, gen80, pop_size40, migrate_interval10, migrate_num2, w_a0.8, w_b0.2, seed7) for o1, o2, _, _ in sorted(front): print(fmakespan{o1:.2f} energy{o2:.2f})主循环的关键参数在函数签名里gen 是总迭代代数pop_size 是每个种群的个体数migrate_interval 是迁移间隔migrate_num 是迁移数量w_a 和 w_b 是两个种群各自对完工时间的权重。注意 migrate_num 取 2 相当于迁入个体只占单种群 40 个体的 5%这是前面说过的安全区间。run_dual_pop末尾的 Pareto 前沿筛选用了最直接的双重遍历复杂度是 O(N²) 量级原型阶段完全够用。如果算例到几百道工序个体总数上千这里会明显变慢届时要改成先把所有个体按目标 1 排序再线性扫描一遍这个优化留给读者自己实现。跑完代码你会看到打印出来的若干行 (makespan, energy)。这些就是双种群算法在当前算例上找到的折中解集合有的解完工时间短但能耗高有的能耗低但工期长。把这些点画到二维坐标轴上就是一条 Pareto 前沿。5. 避坑指南模糊数、能耗权重与迁移参数的 5 个现场翻车案例这一章全部来自我实际调试这类算法时的血泪教训。每个案例按“现象 → 原因 → 解决”来写方便你遇到类似情况时直接对照。5.1 模糊数比较直接取 (abc)/3排序结果偏向中间值现象两个方案比较明明一个方案的工期区间是 (2, 10, 10)另一个是 (4, 5, 12)直觉上前者风险极大但两者算出来的平均值都是 7算法认为它们一样好导致评价结果一片糊收敛很慢。原因三角模糊数的算术平均把所有可能值等权处理没有体现“最可能时间”在车间里的主导地位。两个分布完全不同的模糊数可能算出同一个均值这是模糊调度里最常见的错误之一。解决统一用 (a2bc)/4 做去模糊化或者用基于面积比较的模糊数排序法。后者更严谨但代码开销大我的项目中绝大多数场景用前者就够了。关键是全系统只用一个公式不要在解码器里用一种、在适应度里又换另一种。5.2 只算加工能耗不算待机能耗机器越选越快、电费反而涨现象能耗优化种群跑出来的方案把所有工序都往高功率机器上塞单个方案的“加工能耗”很小但放到真实车间里其他机器空转待机总电费比优化前还高。原因目标函数里只有加工功率乘以加工时间没有待机能耗项算法自然发现“把工序集中到少数机器”是省能量的捷径但这不真实。解决把待机能耗纳入目标 2。最简单的做法就是第 4 章代码里那种用机器跨度减去加工占用时间作为待机时长乘上待机功率。如果车间里有自动启停功能还要加启停能耗否则算法会设计出频繁启停机器的方案来“省待机费”。5.3 迁移间隔设成 2 代双种群退化成了单种群现象跑了 60 代最后合并出来的 Pareto 前沿只有四五个点两个种群各自的最优解几乎一样多样性比单种群还差。原因迁移太频繁种群 A 的精英很快充满种群 B种群 B 的精英也充满种群 A两个种群的选择压力趋同等于把两个种群捏成了一个。解决迁移间隔至少要覆盖一个完整的选择-交叉-变异周期我一般设 10 到 20 代。迁移数控制在种群规模的 5% 到 10%。另外迁移时只复制精英个体的副本用它们替换对方种群里的最差个体不要拿对方精英把本地中等偏下的个体也挤掉否则本地搜索方向会被破坏。5.4 解码器里双重循环找插入位置算例一大就超时现象换到有 20 台机器、30 个工件的算例后单次评估耗时从几毫秒涨到几十毫秒跑 100 代双种群 80 个个体一个小时出不来结果。原因解码器里每解一道工序都要扫一遍候选机器集合而且能耗计算里又扫了一次全部工序Pareto 筛选阶段还反复解码同一个个体。三重浪费叠加在一起复杂度直接爆炸。解决第一把 flat 表和候选机器集合在生成个体前全部预计算好不要在 decode 里重复生成第二Pareto 筛选时先缓存每个个体的 (o1, o2)不要对同一个个体解码两次第三机器合法性检查可以合并到能耗计算的那一次循环里减少一次完整遍历。还有一招是固定随机种子之后用 profile 工具看热点通常瓶颈都在 decode 函数里的列表推导式。5.5 权重拍脑袋定不先做目标归一化换算例全崩现象w_a0.8, w_b0.2 在 3 机小算例上效果不错换到 15 机算例后种群 A 完全不在乎能耗种群 B 完全不在乎工期搜索成了一边倒。原因两个目标的量纲和数量级不一样。小算例里完工时间 20 小时、能耗 2000 千瓦时直接加权后能耗天然占据主导换算例成了完工时间 200 小时、能耗 3000 千瓦时权重关系又反过来。解决像第 4 章代码那样先用初始群体样本估算两个目标各自的最小值和最大值做 min-max 归一化再乘权重。归一化之后w_a0.8 意味着“这个种群 80% 的注意力放在完工时间上”可解释性也强多了。每隔二三十代用当前群体的实际范围重新校准一次 scaler效果会更好代价是每次校准要多算一次所有个体的目标值。6. 双种群系统跑通后的验证方法与三个进阶改进方向代码跑通了别急着写验收报告。先做三件事第一固定随机种子跑 5 次检查 Pareto 前沿的稳定性第二把前沿解可视化看是不是均匀覆盖两个目标的两端第三把最好方案和车间现行排产对比确认能耗降低不是靠牺牲过度工期换来的。稳定性验证可以直接用代码里的 seed 参数。把 seed 分别取 1 到 5各跑一遍记录每轮前沿解的完工时间最小值、能耗最小值计算中位数和极差。如果极差超过中位数的 20%说明算法稳定性有问题优先检查迁移参数再检查变异概率是不是太大。可视化方面把每个解画成 (makespan, energy) 散点图好的前沿应该像一条从左上到右下的弧线中间没有明显缺口。随机种子最短完工时间最低能耗前沿解个数112.486.37212.189.58312.884.96412.288.17512.585.78这张表只是示意格式具体数字以你的算例为准但判断逻辑是通用的几个种子的最短完工时间差异要小前沿解数量要相对稳定。三个进阶改进方向按性价比排序。第一个是改解码器把贪心解码升级成插入式解码让新工序可以塞进机器空闲时间段的缝隙里完工时间通常会再降 3% 到 8%代价是解码耗时翻倍。第二个是把能耗模型细化加入峰谷平电价和机器启停能耗在电价高时段主动安排待机这个对真实账单的影响比任何参数调优都大。第三个是把双种群框架和 NSGA-II 的拥挤度距离结合两个种群各自跑完一定代数后合并做一轮非支配排序再按拥挤度分成新种群继续进化Pareto 前沿的均匀性会明显改善。我自己做这类调度系统时有个习惯每一次调整算法都先用三组固定随机种子跑完对照再下结论绝不拿单次实验当成调参成果。这个习惯救过我很多次因为进化算法本身就是随机过程单次结果好可能只是运气。希望帮到你。本文还有配套的精品资源点击获取
返回列表