双种群进化算法求解能源效率模糊柔性作业车间调度复现解析
2026/9/24 12:34:55 网站建设 项目流程

简介:针对能源效率模糊柔性作业车间调度问题,这份PDF完整给出了带反馈机制的双种群进化算法的设计思路和Python实现。该算法以最小化模糊完工时间和模糊总能耗、最大化最小一致性指标为目标,利用模糊数刻画处理时间的不确定性,并详细说明模糊能耗计算方式;通过四种启发式方法生成初始种群,借助基于种群质量的反馈机制动态调整两个子种群大小,同时设计了新的繁殖、交叉和变异算子,再配合增强局部搜索,使算法兼顾探索与开发能力。PDF内除算法原理外,还提供了可运行的代码和逐步解释,覆盖模糊数运算、约束处理、多目标优化框架及可视化分析工具,能够帮助读者复现文献中的实验结果,也便于扩展到其他元启发式方法或不确定性处理场景。整个资源包共一个PDF文件,大小约八百八十一KB,已有五十九人学习,适合从事模糊调度、进化计算与智能优化研究的科研人员和工程师参考。

1. 能源效率模糊柔性作业车间调度:一份可运行的双种群进化算法复现代码

拿到一份论文复现代码,第一步要做的不是跑,而是先搞清楚它到底要解决什么问题。这篇笔记拆的代码,来自论文《A Bi-Population Evolutionary Algorithm With Feedback for Energy-Efficient Fuzzy Flexible Job Shop Scheduling》,解决的是能源效率模糊柔性作业车间调度问题(EFFJSP):工件工序多、每道工序能在多台机器上做,加工时间只能用三角模糊数估,完工时间和能耗两个目标同时要压。这个场景在机械加工车间里很常见——排产时没人能给你一个精确的加工时长,老师傅只会说"大概3到4分钟"。代码本身不是开箱即用的完整工程,里面有占位实现、缺函数、甚至有语法错误,但骨架是完整的。这篇笔记会把双种群进化算法的核心逻辑、需要补的代码和踩过的坑一次讲清,适合要做调度论文复现对比、或者想用进化算法做车间排产的工程师。

2. 从FJSP到EFFJSP:模糊数建模与三个目标的度量方式

2.1 柔性作业车间调度凭什么难:问题要素拆解

柔性作业车间调度(FJSP)和传统作业车间调度(JSP)的区别,在于"柔性"两个字。JSP里每道工序指定在哪台机器上做,调度只排顺序;FJSP里每道工序有一组候选机器,机器选哪台、工序怎么排,两个决策耦合在一起,解空间直接爆炸。EFFJSP在FJSP基础上加了两个东西:一是处理时间不确定,用模糊数建模;二是目标函数加上了总能耗,而不只是完工时间。

建模时最核心的数据结构是工序表和机器表。工序表长这样:每个工件有多道工序,每道工序是一个候选机器列表,列表里每个元组是(机器编号, 模糊处理时间)。这里有个常见误解,以为模糊处理时间是"大概值随便填",实际上三角模糊数(a, b, c)里b是最可能值,a和c是上下界,后面的比较、加法、去模糊化全部依赖这三个参数的定义。

处理时间不确定是有现实来源的:刀具磨损、材料批次差异、操作工熟练度不同,都会让同样一道工序在不同时间完工。用三角模糊数不是玄学,而是因为它运算简单——两个三角模糊数相加仍是三角模糊数,解析性质好,适合嵌套进遗传算法的适应度计算里。

2.2 三角模糊数:先写一个能算的模糊数类

原代码第一行是from fuzzy import FuzzyNumber,但这个第三方库不一定装得上,而且它的接口和自己的代码不一定兼容。我通常会自己维护一个最小可用的三角模糊数类,几十行搞定,不引入额外依赖:

class TriFuzzyNumber: """三角模糊数 (a, b, c), 满足 a <= b <= c""" def __init__(self, a, b, c): assert a <= b <= c, f"invalid triangular fuzzy number: {a}, {b}, {c}" self.a = float(a) self.b = float(b) self.c = float(c) def __add__(self, other): """模糊加法: 两个三角模糊数相加""" return TriFuzzyNumber(self.a + other.a, self.b + other.b, self.c + other.c) def __mul__(self, scalar): """标量乘法: 功率(确定值) * 时间(模糊数) 仍是三角模糊数""" if isinstance(scalar, (int, float)): return TriFuzzyNumber(self.a * scalar, self.b * scalar, self.c * scalar) raise TypeError("only scalar multiplication supported") def __radd__(self, other): """支持 sum() 的起始值 0 与之相加""" return self if other == 0 else self.__add__(other) def defuzzify(self): """去模糊化: 加权期望值 (a + 2b + c) / 4, b 权重最高""" return (self.a + 2.0 * self.b + self.c) / 4.0 def alpha_cut(self, alpha=0.5): """alpha 截集, 返回区间 [lower, upper]""" lower = self.a + alpha * (self.b - self.a) upper = self.c - alpha * (self.c - self.b) return lower, upper def __repr__(self): return f"TriFN({self.a:.2f}, {self.b:.2f}, {self.c:.2f})"

逻辑说明:加法和标量乘法是模糊数运算的两个基本操作,完工时间是多道工序模糊时间相加,能耗是功率乘模糊时间再累加,靠这两个操作就能算整条调度线的模糊makespan和模糊能耗。alpha_cut把模糊数转成区间,用于后面的一致性指标比较和不确定性处理。

参数说明:defuzzify用 (a+2b+c)/4 而不是简单的 (a+b+c)/3,因为b是最可能值,权重翻倍更贴近实际完工预期。alpha_cut取0.5是常见默认,区间收窄一半,等于取了模糊数的"中间水平"。

2.3 三个目标的度量:完工时间、能耗和一致性指标

EFFJSP的三个目标分别是模糊完工时间、模糊总能耗、最小一致性指标。前两个好理解,第三个容易卡住。一致性指标解决的是模糊数怎么比较的问题——两个三角模糊数A和B比较大小,不能说A一定大于B,只能说"A小于B的可能性有多大"。最小一致性指标就是所有工序顺序约束里,模糊比较成立程度的最小值,越接近1说明排序越可信。

原代码里calculate_agreement_index没有具体实现,我在复现时用的是基于alpha截集重叠率的简化方法:

def agreement_index(a: TriFuzzyNumber, b: TriFuzzyNumber) -> float: """a 不超过 b 的一致性程度, 基于 0.5 截集区间比较 返回 1.0 表示完全一致, 0.0 表示完全冲突 """ a_low, a_up = a.alpha_cut(0.5) b_low, b_up = b.alpha_cut(0.5) if a_up <= b_low: return 1.0 if a_low >= b_up: return 0.0 overlap = max(0.0, min(a_up, b_up) - max(a_low, b_low)) span = (a_up - a_low) + (b_up - b_low) return 1.0 - overlap / span if span > 0 else 1.0

逻辑说明:先取两个模糊数的0.5截集区间,区间完全不交叠时判断明确,交叠越多一致性越低。这个实现的精度比论文里的可能性理论模型粗,但在复现框架里够用,换更严格的necessity模型不影响算法骨架。

参数说明:alpha取0.5和前面的不确定性处理器保持一致。注意这个指标输出范围是[0,1],而完工时间和能耗是量纲完全不同的数值,后面算加权适应度时必须分开归一化,否则会出现能耗压过一切的问题,这个坑在第5章会专门讲。

EFFJSP类的能耗计算在原代码里是energy = processing_time * machine.power_consumption,用我们自己的TriFuzzyNumber就能算:

def calculate_fuzzy_energy(self, schedule): total_energy = TriFuzzyNumber(0, 0, 0) for op in schedule: machine = op['machine'] processing_time = op['processing_time'] total_energy += processing_time * machine.power_consumption return total_energy

这里的隐含假设是能耗与加工时间线性相关,机器功率恒定。真实车间里存在空载、待机、启停等状态,功率不是常数,原论文为了可解性做了线性简化。如果要在实际产线上用,至少要把空载功率加进去,在能耗模型里多维护一个状态项。

3. 双种群框架与初始化:四种启发式生成与动态反馈调整

3.1 双种群怎么分工:探索与开发的资源博弈

单种群遗传算法最大的问题是选择压力一上来,种群多样性快速流失,容易陷进局部最优。FBEA的做法是拆两个种群:种群1负责探索(Exploration),保持多样性,满世界找有潜力的区域;种群2负责开发(Exploitation),在优质解附近精细搜索,配合局部搜索算子深度挖掘。两个种群独立选择、交叉、变异,共享一个全局最优解档案。

这里值得注意的一个设计点是,两个种群不是固定大小从头到尾的。算法每代会分别评估两个种群的质量,质量高的种群下一轮分到更多人,质量低的分到更少人。这个"质量反馈"机制是FBEA区别于普通双种群算法的核心,相当于在迭代过程中动态调整探索和开发之间的资源配比。

对应到原代码里,FBEA初始化时pop1_size = pop_size // 2pop2_size = pop_size // 2,但后面run循环里每次都要调用adjust_population_sizes重新分配。分配依据是各自的平均适应度,适应度越高(在最小化问题里是值越小)的种群拿到的名额越少,因为说明它已经收敛得差不多了,把资源让给更需要探索的种群。

3.2 四种启发式初始化:随机、SPT、LPT与能耗优先

原代码的initialize_population用了四种启发式:随机生成、最短处理时间优先(SPT)、最长处理时间优先(LPT)、能耗优先。比例为随机占一半,SPT和LPT各占四分之一,种群2全部用能耗优先。

启发式生成逻辑倾向适用场景
随机生成每个工序随机选机器无偏好,保多样性初始探索
SPT每道工序选模糊时间最短的机器压缩完工时间时间敏感排产
LPT每道工序选模糊时间最长的机器反向扰动,增加多样性防止初始解扎堆
能耗优先选功率×时间乘积最小的机器压能耗能耗敏感排产

SPT个体的生成逻辑在原代码里只有函数名没有实现,补全时注意比较的是模糊数而不是确定值:

def generate_spt_individual(self): individual = [] for job_idx, job_ops in enumerate(self.problem.operations): for op_idx, alternatives in enumerate(job_ops): # 对候选机器按去模糊化后的时间排序,取最小 best_machine, best_time = min( alternatives, key=lambda alt: alt[1].defuzzify() ) individual.append({ 'job': job_idx, 'op_idx': op_idx, 'machine': best_machine, 'processing_time': best_time }) return individual

逻辑说明:每个工序独立选机器,完全不考虑机器负载均衡和工序顺序约束,所以SPT初始解质量不一定高,但它的价值在于给种群提供"时间导向"的基因片段,方便后续交叉把好机器选择组合到一起去。

参数说明:min的key函数用的defuzzify值,也就是(a+2b+c)/4,省略了对a和c的考量。如果想更保守,可以把key换成b值,或者用alpha_cut区间的上界,对不确定更敏感。这是个值得调的参数,在数据波动大的算例上效果差异明显。

能耗优先个体和SPT的区别在于目标函数换成processing_time * machine.power_consumption。实现时机器需要带power_consumption属性,原代码里这个属性是在__init__之外虚引用的,跑起来必报AttributeError,我在第5章的避坑部分会给出完整补法。

3.3 基于质量的反馈机制:两个种群怎么动态切分

反馈机制的核心就一段代码,但它决定了整套算法的资源分配逻辑:

def evaluate_population_quality(self, population): """种群质量 = 平均适应度的倒数, 适应度越小质量越高""" fitness_list = [self.calculate_fitness(ind) for ind in population] avg_fitness = sum(fitness_list) / len(fitness_list) if avg_fitness == 0: return float('inf') return 1.0 / avg_fitness def adjust_population_sizes(self, q1, q2): """按质量比例重新分配两个种群的大小, 总和保持 pop_size 不变""" total_quality = q1 + q2 new_pop1_size = int(self.pop_size * (q1 / total_quality)) new_pop2_size = self.pop_size - new_pop1_size # 种群1: 超员则按适应度截断, 缺员则随机补充 if len(self.pop1) > new_pop1_size: self.pop1.sort(key=lambda x: self.calculate_fitness(x)) self.pop1 = self.pop1[:new_pop1_size] else: for _ in range(new_pop1_size - len(self.pop1)): self.pop1.append(self.generate_random_individual()) # 种群2: 同样的收缩/扩张逻辑 if len(self.pop2) > new_pop2_size: self.pop2.sort(key=lambda x: self.calculate_fitness(x)) self.pop2 = self.pop2[:new_pop2_size] else: for _ in range(new_pop2_size - len(self.pop2)): self.pop2.append(self.generate_energy_aware_individual())

逻辑说明:质量评估用的是平均适应度的倒数,适应度越小质量越高。这样当一个种群的平均适应度变差时,质量值变小,下一轮分到的名额就变少,资源被挤到表现更好的种群。截断时用排序后切片保留最优秀的个体,缺员时补的是该种群的偏好启发式个体,保证补充进去的新个体不破坏种群原有的搜索方向。

参数说明:这里有一个容易被忽略的细节,new_pop1_size = int(...)向下取整,余数全部归到pop2。normal跑的时候pop_size是偶数没问题,如果pop_size设成奇数或100以下,pop1和pop2的规模差会累积,建议改为round或者把余数轮流分配给两个种群。

4. 进化算子与增强局部搜索:交叉变异的细节实现

4.1 个体编码:从扁平列表到OS+MS双段编码

原代码里的generate_random_individual生成的是一个扁平化工序列表,每个元素是一道工序的机器选择。这种编码有个问题:它隐含了工序按工件内顺序执行的假设,丢失了跨工件的工序先后关系,算makespan时会出错。复现时我建议改造成柔性作业车间调度标准的双段编码:第一段是OS(Operation Sequence)工序序列,第二段是MS(Machine Selection)机器选择。

OS编码是一个工件号重复序列,每个工件号出现次数等于它的工序数,第k次出现代表该工件的第k道工序。MS编码和OS等长,每个位置存放OS对应工序所选机器。解码时从左到右扫OS,把每道工序放进对应机器的可用时间窗口,更新该机器和该工件的完工时间,最终所有机器完工时间的最大值就是makespan。

改造后的随机个体生成如下:

def generate_random_individual(self): os_sequence = [] for job_idx, job_ops in enumerate(self.problem.operations): for _ in job_ops: os_sequence.append(job_idx) # 工件号出现次数 = 工序数 random.shuffle(os_sequence) # 打乱工序顺序 ms_sequence = [] for job_idx in os_sequence: op_pos = os_sequence[:].index(job_idx) # 简化示意, 实际应统计出现次数 # 正确做法: 遍历时维护每个工件已出现的次数, 取对应工序 pass

这里用了一个错误的示意写法,实际实现要维护一个计数器,记录每个工件在当前OS序列里已经出现几次,映射到对应的工序下标,然后从该工序的候选机器列表里随机选一台。细节虽然多但值得一次写对,因为后面POX交叉和局部搜索全都建立在这套编码之上。

4.2 POX交叉:保序继承与随机分组

FBEA的繁殖过程用POX(Precedence Operation Crossover)交叉,比传统的单点/两点交叉更适合工序序列编码。POX的核心思想是把工件随机分成两组,一组工件的工序位置完全继承自父代1,另一组工件的工序位置继承自父代2,保持各自相对顺序不变:

def pox_crossover(self, parent1, parent2): """POX交叉: 适用于OS工序序列 parent1, parent2 是 (os, ms) 双段编码的个体 """ os1, ms1 = parent1 os2, ms2 = parent2 jobs = range(self.problem.jobs) # 随机把工件分成两组 jobs1 = set(random.sample(list(jobs), random.randint(1, self.problem.jobs - 1))) jobs2 = set(jobs) - jobs1 child1_os, child2_os = [], [] # 子代1: 属于jobs1的工序从父代1按序取, 属于jobs2的从父代2按序取 pos2 = 0 for job in os1: if job in jobs1: child1_os.append(job) else: while os2[pos2] not in jobs2: pos2 += 1 child1_os.append(os2[pos2]) pos2 += 1 # 子代2: 相反 pos1 = 0 for job in os2: if job in jobs2: child2_os.append(job) else: while os1[pos1] not in jobs1: pos1 += 1 child2_os.append(os1[pos1]) pos1 += 1 return (child1_os, ms1), (child2_os, ms2)

逻辑说明:POX的关键性质是子代的工序序列天然满足工序先后约束,不需要额外修复。比如父代1里工件3的出现顺序是"工序1→工序2→工序3",子代1里属于jobs1的所有工件继承这个相对顺序,属于jobs2的工件顺序则来自父代2,两个来源拼在一起依然满足各自工件的工序约束。

参数说明:random.randint(1, n_jobs - 1)保证两组都不为空,交叉才有意义。机器选择段ms在示例里直接跟父代走,更精细的做法是对MS段做均匀交叉,按0.5概率逐位交换两台机器选择。

4.3 变异与局部搜索:交换和插入的互补组合

FBEA的变异不是简单随机扰动,而是有方向性的搜索。交换变异随机选OS序列两个位置互换,扰动大;插入变异把一个工序抽出来插到另一个位置,扰动小且更容易改善拥堵段。增强局部搜索就基于这两个变异构造邻域,以0.5的概率随机选一种,接受改进解:

def local_search(self, population): improved = [] for ind in population: os_seq, ms_seq = ind if random.random() < 0.5: # 交换变异: 随机交换两个位置 new_os = os_seq[:] i, j = random.sample(range(len(new_os)), 2) new_os[i], new_os[j] = new_os[j], new_os[i] new_ind = (new_os, ms_seq) else: # 插入变异: 抽出一个元素插入到随机位置 new_os = os_seq[:] i, j = random.sample(range(len(new_os)), 2) val = new_os.pop(i) new_os.insert(j, val) new_ind = (new_os, ms_seq) # 只有当新解适应度更优时才接受 if self.calculate_fitness(new_ind) < self.calculate_fitness(ind): improved.append(new_ind) else: improved.append(ind) return improved

逻辑说明:注意这里只对pop2做局部搜索,符合"pop2负责开发"的分工设计。局部搜索只看一步邻域,贪心接受,计算量可控。如果对pop2全体每代都做深度邻域搜索,计算量会大好几倍,收益对比下来不划算。

参数说明:交换和插入各占50%是个简单均衡。实际调参时可以按种群收敛状态动态调整——收敛快时多用插入变异做精细化搜索,多样性下降明显时提高交换变异比例。我在MK01算例上的血泪经验是,横竖各跑20次取均值再定比例,不要靠一两次实验拍板。

5. 复现避坑:代码跑到报错停下来的五处关键点

5.1 初始化函数末尾多了一个右括号

现象:把代码复制进PyCharm,SyntaxError直接标在initialize_population最后一行,提示unmatched ')'。这个报错在代码俯视时完全不显眼,因为那行有for表达式又有方括号,右括号一多一少很容易混过去。

原因:self.pop2 = [self.generate_energy_aware_individual() for _ in range(self.pop2_size)])末尾多了一个右括号,和前面的方括号不匹配。属于整理代码时的笔误。

解决:删除末尾多余的右括号,让语句变成self.pop2 = [self.generate_energy_aware_individual() for _ in range(self.pop2_size)]。顺手检查文件里所有类似的长列表推导式,统一用Black格式化工具跑一遍,这类低级语法错误之后基本不再出现。

5.2 引用了未定义的能耗优先个体生成函数

现象:语法错误修完再跑,ModuleNotFoundError消失了,但紧接着抛NameError: name 'generate_energy_aware_individual' is not defined。

原因:原代码在initialize_population里调用了generate_energy_aware_individual()生成pop2,但整个类里只有注释提到能耗优先,没有实际函数体。这是论文代码最常见的毛病——写了个名字,没写实现。

解决:补上函数定义:

def generate_energy_aware_individual(self): individual = [] for job_idx, job_ops in enumerate(self.problem.operations): for op_idx, alternatives in enumerate(job_ops): # 能耗 = 功率 * 时间, 直接对候选机器的能耗去模糊化排序 best_machine, best_time = min( alternatives, key=lambda alt: alt[1].defuzzify() * self.machine_power[alt[0]] ) individual.append({ 'job': job_idx, 'op_idx': op_idx, 'machine': best_machine, 'processing_time': best_time }) return individual

注意这里访问了self.machine_power字典,需要在FBEA的__init__里预定义,否则会报AttributeError。机器功率表是EFFJSP问题的输入参数,不属于算法本身,建议从外部传入。

5.3 模糊数模块没有随代码分发

现象:from fuzzy import FuzzyNumber这一行直接ModuleNotFoundError。查pip包管理器会发现确实有个叫fuzzy的库,但它的接口和自己的代码预期完全不一样,还得额外装依赖。

原因:论文代码默认读者环境里已经有模糊数处理工具,实际交付时没有把模块文件打包进去。第三方库接口不可控,不如自己实现。

解决:直接用第2章的TriFuzzyNumber类替换全部FuzzyNumber调用。替换时注意两点:第一,FuzzyNumber(0, 0, 0)对应TriFuzzyNumber(0,0,0),第二,sum()函数累加模糊数时,PyCharm可能因为起始值0的类型警告,TriFuzzyNumber里实现了__radd__专门处理这个情况。从那以后我处理这类依赖,会先写一个依赖清单,明确哪些是第三方库、哪些需要自己补实现。

5.4 去模糊化加权量纲失衡:能耗数值压制完工时间

现象:代码能跑,但观察种群进化过程,最优解永远在往"低能耗"方向偏移,完工时间几乎没改善。把best_solution打印出来一看,makespan比随机解还差,能耗降得非常狠。

原因:calculate_fitnessmakespan.defuzzify() * 0.5 + energy.defuzzify() * 0.3 - agreement * 0.2,完工时间量纲是几十到几百,能耗是几千到几万,0.3的权重乘以能耗数值后远超完工时间项,本质上是单目标优化了能耗。

解决:加权前先做归一化,各目标除以当前种群内最大值的估计值。初版实现可以简单用归档集里各目标的最大值做分母,运行稳定后再换成动态归一化:

def calculate_fitness(self, individual, max_energy=5000.0, max_makespan=200.0): makespan, energy, agreement = self.problem.evaluate(individual) norm_makespan = makespan.defuzzify() / max_makespan norm_energy = energy.defuzzify() / max_energy return 0.5 * norm_makespan + 0.3 * norm_energy - 0.2 * agreement

参数说明:分母的初始估计值来自随机初始化种群的统计。跑完第一轮后用实际最大值更新,后面每50代刷新一次,这个动态归一化的做法比固定阈值更稳妥。

5.5 能耗优先初始种群同质化,反馈机制反而帮倒忙

现象:pop2初始化时全部用能耗优先启发式生成,种群里的个体几乎一样。跑几十代后,吞吐机制让pop2越来越小,pop1被挤到很小的容量,整体搜索能力反而下降。

原因:pop2本身同质化严重,平均适应度稳定在一个值附近,质量评分不再有区分度;反馈机制按比例分配时,pop2拿到的名额忽高忽低,种群结构不稳定。启发式初始化是好的,但比例设计失衡。

解决:pop2的初始化比例改成混合模式:能耗优先占50%,随机生成占30%,LPT占20%。这样保留能耗导向的同时,给pop2足够的基因多样性。调整后跑MK01算例,完工时间和能耗的Pareto前沿明显更完整。

6. 把复现代码改造成对比实验工具:算例接入与结果验证

6.1 标准算例的接入方式:从确定时间到模糊时间

复现代码跑通只是第一步,论文里说"实验表明FBEA能有效解决EFFJSP问题",那实验用的算例从哪来?常见做法是拿柔性作业车间调度的标准算例做转换。Brandimarte的MK系列和Kacem系列是FJSP领域最常用的两组公开算例,数据格式都是"每道工序对应的候选机器及确定加工时间"。

接入时把确定时间t转成三角模糊数即可,一条规则走天下:下界取t×0.9向下取整,最可能值取t,上界取t×1.1向上取整:

def convert_to_fuzzy(instance_file, fuzzy_ratio=0.1): """读标准FJSP算例, 把确定加工时间转成三角模糊数""" with open(instance_file, 'r') as f: lines = f.readlines() jobs_data = [] # 解析格式根据具体算例略有不同, 核心是拿到每道工序的候选机器和时间 for line in lines[1:]: # 跳过首行工件数/机器数说明 nums = list(map(int, line.strip().split())) op_k = nums[0] # 本行有几个候选机器 alternatives = [] idx = 1 for _ in range(op_k): machine_id = nums[idx] t = nums[idx + 1] idx += 2 delta = max(1, int(t * fuzzy_ratio)) alternatives.append( (machine_id, TriFuzzyNumber(max(1, t - delta), t, t + delta)) ) jobs_data.append(alternatives) return jobs_data

逻辑说明:模糊区间不设为对称的,下界至少为1,避免出现0或负数加工时间。delta取t的10%,数据波动大的算例可以调到20%,这相当于给问题预设了不确定性水平。

参数说明:fuzzy_ratio是复现实验里一个可调的实验变量。想验证算法在不确定环境下的鲁棒性,就对比fuzzy_ratio在0.05、0.1、0.2三档下的表现差异。原论文的模糊能耗方法和这个转换规则是兼容的,因为能耗模型只依赖模糊时间的期望值。

6.2 对比实验设置:种群规模、迭代次数与统计口径

要和论文结论对齐,或者和NSGA-II等经典算法对比,实验设置必须可复现。建议固定三组参数:总种群大小pop_size=100,最大迭代max_gen=500,每个算例独立运行30次取均值和标准差。原代码里NSGA2Scheduler只有骨架,但对比实验的框架可以直接复用。

对比时有个容易翻车的点:两个算法的终止条件要对齐评价次数而不是迭代次数。NSGA-II如果每代只评估一次种群,总评价次数是pop_size × max_gen;FBEA因为有两个种群,每代要评估的个体总数可能不同,直接比max_gen不公平。

def run_experiment(algorithm_class, problem, runs=30, pop_size=100, max_gen=200): records = [] for seed in range(runs): random.seed(seed) np.random.seed(seed) algo = algorithm_class(problem, pop_size=pop_size, max_gen=max_gen) algo.run() m, e, ag = problem.evaluate(algo.best_solution) records.append({ 'makespan': m.defuzzify(), 'energy': e.defuzzify(), 'agreement': ag, 'seed': seed }) return records

逻辑说明:固定随机种子是论文复现的底线,不固定种子跑出来的结果每次都不一样,任何人都无法复核你的实验。30次独立运行的均值反映算法平均性能,标准差反映稳定性,两者都要写进论文或实验报告。

参数说明:对比算法(比如NSGA-II)只要传入同样的problem实例就保证目标函数一致。如果想做消融实验分析反馈机制的作用,直接把adjust_population_sizes调用注释掉,让pop1和pop2固定各占50%,就是一组干净的对照组。

6.3 结果怎么看:收敛曲线与有效前沿

最后一步是验证算法真的有效。我一般会同时看两样东西:收敛曲线和Pareto前沿。收敛曲线画的是每代best_fitness,如果曲线在100代以内就平滑到一条直线,说明早熟;如果到400代还在明显下降,说明终止代设小了。Pareto前沿的画法是把30次运行的所有非支配解收集起来,画出以makespan为横轴、能耗为纵轴的散点图,看分布是不是一条向右下凸的曲线。

对EFFJSP来说,还有个更直接的验算法:用第2章的一致性指标算最优解的模糊完工时间,和确定值版本对比。如果确定时间算出来的makespan落在模糊makespan的截集区间内,说明模糊模型没有偏离物理实际——这一步是很多复现笔记里缺失的,但它恰恰是审稿人和工程师最关注的部分。

从那以后,我每次拿到一份调度相关的复现代码,都会先列一个待补清单,把第三方依赖、未实现函数、缺失的编码解码逻辑全部标出来,再上标准算例跑一轮10次的统计确认算法不是在随机数上碰运气。这套流程帮我避开过不少看似能跑、实则结论站不住脚的代码,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询