基于NSGA-II的柔性作业车间调度Matlab实现与调参心得
发布时间:2026/9/8 8:26:27来源:尧图网络
我做了快十年的车间调度相关研究从最早的流水车间、置换流水车间到后来真正让我头疼的柔性作业车间调度问题FJSP前后写过遗传算法、粒子群、模拟退火甚至一些改进的混合算法但要说工程和学术上最均衡、最容易被审稿人和企业接受的一套方案我还是首推非支配排序遗传算法NSGA-II。这次就把我基于Matlab写的这套FJSP求解代码完整拆开讲讲建模思路、算法设计、代码结构以及我在实际运行中踩过的坑和调参心得。不管你是刚接触调度的研究生还是准备把智能优化算法落地到生产排程的工程师这篇文章应该都能给你一些可以“抄作业”的参考。柔性作业车间调度这个问题的核心难点说直白一点就是“机器选择”和“工序排序”两个决策同时做。再加上现实工厂里经常要同时考虑生产效率、设备负载、能耗成本这些互相打架的指标单目标优化根本不够用。NSGA-II这类多目标进化算法之所以受欢迎就是因为它能一次性给出一组非支配解让决策者在完工时间和设备负载之间自己权衡而不是被迫接受一个“唯一最优”。我在这里会从问题建模开始一直讲到Matlab代码实现的每个关键环节顺便把柔性作业车间常用的国际标准算例、评价指标、参数配置都整理出来希望能帮你少走几个月弯路。1. 问题描述与数学建模1.1 柔性作业车间调度问题到底“柔性”在哪在普通作业车间调度里每个工件的每个工序都只能在一台固定机器上加工唯一要决策的就是工序的排列顺序。而柔性作业车间调度打破了这一限制每个工序可以选择多台机器中的任意一台去加工机器不同加工时间也不同。这个“多了一个选择”的设计让模型的可行解空间一下子扩大了好几个数量级。举个例子。一个工件有3道工序每道工序都对应3台可选机器那光这个工件的机器组合方案就有3的3次方等于27种。如果车间里有10个工件、每件平均5道工序、每道工序平均4台可选机器机器组合方案的数量就是4的50次方量级这还没算上工序排序的排列组合。这种规模的组合爆炸意味着精确算法只有在极小规模下才能用大规模场景基本都靠启发式算法和智能优化算法。从决策角度拆解FJSP其实就是两个子问题耦合在一起机器选择子问题为每一道工序从可选机器集中挑一台。工序排序子问题在满足同一工件工序先后约束的前提下安排所有工序在时间轴上的加工顺序。这两个子问题不是独立的。你选了一台快的机器可能全局排下来这台机器排队最长结果完工时间反而不理想你把一个关键工序放前面可能后面另一台设备空转严重。所以任何只优化其中一个子问题、忽略另一个的做法在理论上都是不完整的。1.2 数学符号与约束条件我在建模时通常用下面这套符号体系参考了Kacem算例和Brandimarte算例的通用表述方便后续做实验对比。工件集合(J {J_1, J_2, ..., J_n})共n个工件。机器集合(M {M_1, M_2, ..., M_m})共m台机器。工件(J_i)包含(n_i)道工序第(j)道工序记为(O_{ij})。工序(O_{ij})的可选机器集合为(M_{ij} \subseteq M)在机器(M_k)上的加工时间为(p_{ijk})。核心约束包括同一工件的工序必须按工艺路线顺序加工前一道工序结束才能开始下一道工序这是工序优先级约束。一台机器同一时刻只能加工一个工序这是资源唯一性约束。工序一旦开始加工就不允许中断即不可抢占约束。所有工件和机器在零时刻都可用的假设也可以扩展加入释放时间和准备时间但基础版本通常不考虑。目标函数方面我考虑最多的是三个最大完工时间Makespan(C_{max})机器总负载(W_T)以及关键机器上的最大负载(W_M)。经典文献里大多用两个或三个目标组合比如优化(C_{max})和(W_T)或者同时优化这三个指标。1.3 为什么我会把目标函数设计成三个写论文和做实际项目不一样。如果只是给企业做一个排产工具老板可能只看按时交货率和设备利用率但做算法研究时目标函数选几个、选哪些直接决定算法的搜素空间和结果评价方式。我设计三目标的原因是“信息量更足”。(C_{max})反映的是生产效率(W_T)反映能耗和生产成本(W_M)则直接影响瓶颈设备利用率。两个解可能在(C_{max})上完全一样但负载均衡程度差很多单看二维帕累托图根本看不出来。有了(W_M)这个第三维可以更完整地评价一个调度方案的工程优劣。当然三维目标的代价是帕累托前沿的可视化变难收敛性和多样性评价也要用IGD、HV这类综合指标。如果你是第一次做FJSP我建议先跑二维目标“完工时间总负载”把代码跑通、结果稳定之后再升级成三目标版本。这也是我最初做这个项目时切换过来的路径。2. 为什么偏偏选了NSGA-II而非其他多目标算法2.1 多目标进化算法的选型逻辑现在多目标优化算法其实不少NSGA-II、MOEA/D、SPEA2、NSGA-III以及近年的RVEA等各有适用场景。我在FJSP选型时就考虑了几个现实问题。第一FJSP的解空间不连续、不可导目标函数不能写成简单的数学表达式所以基于梯度的方法直接出局。第二FJSP是离散组合优化问题要求算法自带离散编码和对应算子很多连续优化算法虽然效果漂亮但改造成本很高。第三我手头的算例规模不算特别大但也不小算法收敛速度不能太慢最好能在几百代内给出可用前沿。NSGA-II在这几个维度上的综合表现是最稳的。它的快速非支配排序机制可以把种群中解按照支配关系分成多个层级越靠前的层级代表解质量越好这种机制和FJSP这种多个目标相互冲突的场景天然匹配。拥挤度距离则保证了同一层级里解之间的间距不让算法把搜索资源全浪费在帕累托前沿的某一个局部区域。再加上精英保留策略父代和子代合并后取前N个最优解不会因为随机扰动而丢失稳定性特别好。2.2 NSGA-II的三个核心机制拆解我结合FJSP的特点把NSGA-II的三个核心模块讲清楚你在看代码或者复现时会更有方向。快速非支配排序。这一步要给所有个体打上层级标签。如果个体A在所有目标上都不差于个体B并且至少在一个目标上严格优于B那么A支配B。遍历整个种群把不被任何个体支配的放入第一层帕累托前沿然后把它们暂时移除再找第二层依此类推。这个过程的时间复杂度是(O(MN^2))M是目标个数N是种群规模对FJSP来说这个量级可以接受。拥挤度距离计算。同一层级的解谁更“值得保留”看它和左右邻居的解距离。对每个目标排序后把边界个体的距离设成无穷大中间个体的距离用相邻个体目标值的归一化差累加。距离大的解周围更空旷多样性更好因此优先级更高。精英保留策略。每一代做完交叉变异后父代和子代合并成一个2N规模的临时种群然后对这2N个个体重新做非支配排序和拥挤度计算依次填充进新的N规模种群直到填满。这意味着哪怕子代全都变异崩了父代的优秀个体依然保留着算法不会出现“一代不如一代”的大幅回退。从实现角度看这三个机制加在一起不过两三百行Matlab代码逻辑非常清晰。而且这些机制和数据问题无关你今天拿它跑FJSP明天改成流水车间或者车辆路径问题只需要换编码解码和目标函数模块算法框架不用动。这也是我推荐你在项目的早期把算法框架和问题模型分开写的原因后期维护会轻松得多。2.3 NSGA-II在FJSP上有哪些常见变体NSGA-II本身已经很成熟但在FJSP上直接套用朴素版本效果只能算中规中矩。我试过几种改进挑两个值得说的。一个是用混合初始化策略替代完全随机初始化。随机生成初始种群的好处是多样性好但收敛速度慢用启发式规则生成初始种群比如优先把关键工序分配到完成时间最早的机器或者用最短加工时间优先规则生成高质量的初始个体可以让算法起点就很高。我通常的做法是60%的个体用启发式规则生成40%随机生成兼顾收敛和多样。另一个是在变异算子中嵌入邻域搜索。当生成一个新个体后把工序序列里的关键工序也就是制约makespan的那些工序重新分配到一台更合适的机器上如果makespan降低了就保留否则回退。这种“带局部搜索的变异”我实测下来能让最终前沿质量提升明显尤其在机器规模较大的算例上效果相当显著。3. 编码解码与初始解生成3.1 编码方式选型从单层到双层编码是FJSP里最艺术的部分。你选的数据结构决定了交叉变异好不好写、解码容不容易出现不可行解。我第一次做FJSP时用的是单层编码就是工序序列上用工件号表示机器选择用一个固定长度数组表示但一遇到不同工件工序数不同的情况就需要填充边界和掩码交叉的时候很容易产生长度错位的子代特别烦。后来我固定使用双层编码。第一层是机器选择串长度等于所有工件的总工序数每个位置上的元素是某道工序选择的机器号第二层是工序排序串长度同样等于总工序数每个位置上的元素是工件号。一个元素出现几次就代表这个工件的第几道工序。比如排序串[2,2,1,3,1]表示先加工工件2的第一道工序再加工工件2的第二道工序然后工件1的第一道工序以此类推。这种编码方式下只要排序串里每个工件出现的次数等于它的工序数解码出来的一定是可行调度大大降低了非法解出现的概率。3.2 解码策略为什么我坚持插入式贪婪解码编码是静态的解码才真正把它变成可执行的调度方案。解码方式对最终makespan影响极大。最基本的解码是半主动解码按排序串从头到尾每道工序找对应机器的最早可加工时刻安排上去就可以。但这种方法往往浪费机器上的空闲时间段。举个例子某个高温工序本来可以在早上8点到10点加工结果被排到了下午2点因为前面有一个长工序把机器从早到晚全占了。可是实际上早上这台机器可能完全空着只是排序串上前面的工序还没安排完。插入式贪婪解码解决的就是这个“缝隙利用”问题。实现细节是这样的拿到排序串当前这道工序先根据机器选择串知道它要用哪台机器然后在它前道工序完工时刻之后去这台机器的已排工序区间里找空闲时间段。如果存在一段空闲区间长度够加工这道工序就直接插入更新机器占用表如果找不到就追加到机器当前最后一道工序的结束时刻之后。这个策略虽然每次都要扫描机器时间表时间复杂度略高但能显著压缩makespan。我在Brandimarte MK01算例上实测过同样的编码和进化代数插入式贪婪解码比半主动解码的最终最优makespan能小8%到12%。3.3 初始种群的生成规则初始种群的质量影响收敛速度前面已经提到过但具体用哪些规则我需要展开说说。我用的规则都是在工程上有明确意义的机器选择部分第一类是最短加工时间优先每个工序只看自身的可选机器里哪台加工时间最短直接选它。这个规则生成速度快单机指标好缺点是容易把所有工序都压到一两台快机器上导致负载失衡。第二类是全局最小负载优先每次选择当前累计负载最低的机器可以均衡所有设备。第三类是随机选择保持多样性。我会按一定比例混合这些规则。工序排序部分最常用的规则包括SPT最短加工时间优先按当前可安排工序中最短加工时间者先排、MOR最多剩余工序优先、随机规则。同样的混合使用。初始化时我一般这样分配比例40%完全随机保证探索性40%用启发式组合规则让起点有质量20%用随机混合规则介于两者之间。这样既不会让种群过早收敛到局部最优也不会像纯随机初始化那样前面几十代基本在瞎逛。4. 遗传算子设计与参数配置4.1 选择算子锦标赛选择为主NSGA-II的框架里选择这一步是在做完非支配排序和拥挤度计算后进行的。我的选择操作是标准的二元锦标赛每次随机挑两个个体如果它们的帕累托层级不同保留层级更小的如果层级相同保留拥挤度距离更大的。重复这个操作直到选满一个临时父代种群。这种做法的好处是并不需要把每个个体的适应度转成一个数字只需要两两比较非常契合多目标优化的场景。如果你看到有人把多个目标加权求和变成单目标然后跑遗传算法那本质上已经不是NSGA-II了是把它退化成加权和算法那样做虽然也能得到一个解但会丢失帕累托前沿的全貌。4.2 交叉算子工序串与机器串要分开设计交叉算子是遗传算法的发动机但FJSP里如果对工序串和机器串用同一个算子往往会互相破坏。我的做法是拆开来分别设计。工序排序串用的是IPOX基于工件优先顺序的交叉。具体做法是把工件集合随机分成两个子集Group1和Group2子代1先继承父代1中所有属于Group1的工件位置再把父代2中属于Group2的工件依次填入空白位置。子代2反过来对调。这个算子保证了同一个工件在子代中的相对顺序不会被打乱因此解码后依然满足工序优先约束合法率非常高。机器选择串用的是MPX多点交叉。做法是先生成一个随机0-1掩码掩码为1的位置上子代1继承父代1的机器子代2继承父代2的机器掩码为0的位置则互换。就这么简单但因为机器选择串每个位置都对应一道工序交换后可能产生机器和工序不匹配的问题所以我在交叉后有个修复函数逐一检查新机器是否在工序的可选机器集中不在就随机重置。交叉概率我通常取0.8到0.9。数值太大容易把好个体拆散太小则搜索速度不够。0.85是我在大量实验后比较稳的一个值各种规格算例下都没有出现过明显早熟问题。4.3 变异算子两道防线防止过早收敛变异算子的作用有两个层面维持种群多样性以及在小范围邻域内做局部寻优。对于工序排序串我采用的是两点交换变异随机选两个位置交换元素。对于机器选择串我采用单点变异随机选一个工序把它当前的机器换成另一台可用机器。变异概率通常取0.1左右如果种群规模大可以适当下调到0.05防止高频变异把优化方向全部打乱。我最近在代码里加了一个小技巧当检测到种群连续10代的最优makespan没有变化时自动触发一次“强化变异”——把种群中10%的个体重新初始化并在机器选择串上做一次基于最小负载规则的局部修正。这种做法能有效跳出局部最优而且对整体收敛过程干扰极小。唯一要注意的是如果触发次数太频繁收敛曲线会来回震荡所以我设置了两代重置周期的冷却时间。4.4 参数配置速查表这里把我常用的参数整理成一个表方便你直接抄去用。参数项我的取值说明种群规模100算例不超过10个工件15台机器时100足够最大进化代数200-500Kacem小算例用200Brandimarte大算例用500交叉概率0.85工序串和机器串分开用同一个概率变异概率0.1可随代数增大而缓慢衰减锦标赛规模2标准二元锦标赛初始启发式个体比例0.6过高易早熟过低收敛慢精英保留比例100%NSGA-II天然精英保留5. Matlab核心代码实现导读5.1 整体代码架构我写Matlab代码有一个习惯每个功能模块单独一个函数文件主脚本只负责数据装配和循环调用。这样不仅调试方便替换解码器、换测试算例都很快。整个FJSP项目我分了下面这些文件main.m主脚本负责读数据、初始化、进化循环、结果输出。initializePopulation.m生成初始种群返回结构体数组。decodeSchedule.m插入式贪婪解码输入个体输出调度甘特图和目标值。nonDominatedSort.m快速非支配排序返回每个个体的层级和前沿集合。crowdingDistance.m计算同一前沿个体的拥挤度距离。tournamentSelection.m锦标赛选择。crossoverIPOX.m和crossoverMPX.m两个序列各自的交叉算子。mutationOP.m和mutationMachine.m两个序列各自的变异算子。evaluateObjective.m计算makespan和总负载。plotGantt.m画出甘特图。plotPareto.m画出帕累托前沿。这个结构非常适合扩展。你想把二维目标变成三维只用改evaluateObjective.m和绘图函数要换测试算例只动数据读取即可。5.2 解码函数的核心逻辑解码是整个实现里最关键的部分我把它写成一个伪代码形式的逻辑放在下面你照着移植到任何语言都不难function [Cmax, WT, schedule] decodeSchedule(chromo, data) % chromo.ops : 工序排序串 % chromo.mac : 机器选择串 % schedule : 保存每个工序分配的机器、开始时间、结束时间 numJobs data.numJobs; numMach data.numMach; jobStep zeros(1, numJobs); % 每个工件当前处理到第几道工序 machEnd zeros(1, numMach); % 每台机器的当前最后结束时间 machSegs cell(1, numMach); % 每台机器上的已排工序段 [start, end] for idx 1:length(chromo.ops) jobID chromo.ops(idx); stepNow jobStep(jobID) 1; % 当前工序号 jobStep(jobID) stepNow; machine chromo.mac(idx); % 该工序所用机器 pTime data.processTime{jobID}(stepNow, machine); % 前道工序结束时间 prevEnd 0; if stepNow 1 prevEnd schedule(jobID, stepNow-1).endTime; end % 在当前机器的空闲集合中寻找可插入间隙 startTime machEnd(machine); for seg machSegs{machine} segStart seg(1); segEnd seg(2); if segStart - max(prevEnd, machEnd(machine)) pTime % 更准确是找prevEnd之后且在该段之前的空隙 end end % 实际更稳妥的双循环写法遍历机器时间轴找可插入空隙 [startTime, endTime] findInsertionGap(machSegs{machine}, prevEnd, pTime); % 更新机器占用段 machSegs{machine} [machSegs{machine}; startTime, endTime]; machEnd(machine) max(machEnd(machine), endTime); % 记录结果 schedule(jobID, stepNow).jobID jobID; schedule(jobID, stepNow).step stepNow; schedule(jobID, stepNow).machine machine; schedule(jobID, stepNow).startTime startTime; schedule(jobID, stepNow).endTime endTime; end Cmax max(machEnd); WT sum(machEnd); end这一段里最容易出错的地方是findInsertionGap的边界条件。注意一台机器的空闲区间必须同时满足两个条件起始时间大于等于前道工序的结束时间且区间长度大于等于加工时间。很多人写到这里只检查了机器上有没有空闲时间忘了检查前驱约束结果排出来的调度根本不符合工艺顺序。5.3 主循环的骨架主循环的逻辑其实不复杂就是“生成子代 - 合并 - 排序 - 截断”四步反复迭代% 初始化种群 population initializePopulation(data, popSize); for gen 1:maxGen % 先对当前种群做非支配排序 population nonDominatedSort(population); population assignCrowdingDistance(population); % 生成下一代用锦标赛选择父代再做交叉和变异 offspring []; while length(offspring) popSize p1 tournamentSelection(population); p2 tournamentSelection(population); [c1, c2] crossover(p1, p2, data); c1 mutation(c1, data, pm); c2 mutation(c2, data, pm); offspring [offspring, c1, c2]; end offspring offspring(1:popSize); % 防止越界 % 合并 环境选择 combined [population, offspring]; combined nonDominatedSort(combined); combined assignCrowdingDistance(combined); population selectByRankAndDistance(combined, popSize); % 记录每一代最佳个体 bestRecord(gen) getBestMakespan(population); end % 最终结果输出 plotPareto(population); plotGantt(population(1)); % 展示非支配前沿中makespan最小的解这段代码基本是NSGA-II的经典模板你如果之前跑过其他多目标问题会感觉很熟悉。区别就在于交叉变异的实现、解码函数的数据结构、以及目标函数的计算方式被替换成了FJSP的版本。6. 实验设计与结果分析6.1 测试算例怎么选我做实验时主要用两类标准算例。一类是Kacem算例规模小比如8工件8机器的Kacem 8x8适合快速验证算法是否正确单次运行几十秒就能跑完。另一类是Brandimarte系列从MK01到MK10规模更大、机器柔性更高是论文里最常用的对比基准比如MK01是10工件6机器MK10是20工件15机器。这两类算例的数据格式网上都找得到原始文本我自己把它整理成了Matlab的.mat文件直接load就能用省去了解析的麻烦。如果你的场景不是标准算例而是企业实际数据建议先把工艺数据整理成同样的格式每个工件对应一个工序列表每个工序对应一个可用机器数组和加工时间数组。这步数据清洗往往比算法本身还要耗时但做好了代码一跑就通。6.2 评价指标不只是看最优makespan很多初学者跑完NSGA-II只看帕累托前沿图里那个最优makespan这是不够的。多目标算法的评价需要同时看收敛性和多样性我常用三个指标。第一个是最小makespan就是从最终前沿里挑makespan最小的解这是工程上最直观的指标。第二个是IGD反转世代距离通过计算真实帕累托前沿和算法获得前沿之间的距离值越小说明前沿越贴近真实最优不过小算例才能拿到真实前沿大算例通常用已知最好解替换。第三个是HV超体积指标计算算法获得的非支配解集在目标空间里覆盖的体积HV越大说明解的收敛性和多样性越好。HV不需要知道真实前沿只用给每个目标定一个参考点就行我用的是种群中所有个体每个目标最大值的1.2倍。还有C-metric这个指标是拿两个算法的解集做比较的A和B两算法谁的解被另一个支配的比例更低谁就在收敛性上占优。论文写对比实验时这个指标是标配。6.3 帕累托前沿的分析方式当你的代码跑出了帕累托前沿不要只看一张散点图就结束。我习惯做两个层面的分析。第一层是前沿的分布形态。如果一个算例的帕累托前沿形状是平滑的、凹向原点的说明两个目标之间权衡关系明显算法的多样性也不错如果前沿上有很多空洞说明算法在某些区域搜索不足需要提高变异概率或者种群规模。如果前沿上的点集中在一个很小的区域贴在一起说明算法过早收敛多样性出了问题。第二层是选取几个代表性解去画甘特图。帕累托前沿上不同位置解对应的调度方案差异很大比如makespan最小的解可能把所有工序都压到少数快机器上总负载不低但完成快总负载最小的解则把工序均匀分配给所有机器但makespan明显变大。你把这两个解都画成甘特图贴在报告里比单纯摆数据更有说服力评审人或者领导一眼就能看出算法真的理解了问题而不是在跑黑盒。6.4 我在MK01算例上的典型结果拿MK01算例做例子10个工件、6台机器、总工序数55我用种群100、迭代300代跑一次大约40秒左右能完成。最终帕累托前沿上大概能获得40到60个非支配解makespan最小能到42左右总负载最小能到160附近单目标优化的已知最优makespan是40左右说明NSGA-II得到的结果距离单目标最优已经非常接近而它同时还给你留了一堆其他负载更低的备选方案。这和传统单目标遗传算法的优势对比一目了然。单目标遗传算法吭哧吭哧跑半天最后只能告诉你“最优makespan是42”但NSGA-II会告诉你“如果你愿意让makespan多两天总负载可以降低18%”这种决策支持信息在实际排产里才是真正有价值的东西。7. 常见问题与调试经验7.1 解码结果不符合预期大概率是边界条件写错了我调试FJSP代码时遇到的第一个大坑就是findInsertionGap里前道工序结束时间没有正确传递。问题的表现是甘特图里出现了机器冲突一台机器上两个工序重叠时间加工这在实际生产中是不可能发生的。排查思路很简单写一个合法性校验函数解码完每一个个体后自动遍历所有工序检查每个工序是否满足前驱约束、是否满足机器唯一性约束一旦发现非法调度把它标记出来然后单独调试这个个体。这个校验函数我一直保留在代码里后期改任何解码逻辑都会先跑一遍校验确认合法率100%再继续。另一个常见毛病是机器选择串里存了机器号但机器号不在当前工序的可选机器集中。这个主要是在交叉或者变异后产生的不合法机器号导致的。我在初始化函数里就预先存了一个availableMachines结构每次生成或修复机器时都查一遍这个结构非法值直接重新生成。7.2 帕累托前沿多样性差检查你的拥挤度距离如果你发现跑完几百代前沿上的点还是挤在一个角落几乎都聚集在极小makespan和极小负载附近那最可能的原因有两个。第一个是拥挤度距离计算时目标值没有做归一化。FJSP的三个目标量纲差异不大但跨度不同如果直接拿原始值算距离跨度过大的目标会主导距离计算另一个目标上的多样性会被忽略。解决办法很直接对每个目标在当前种群的最大值和最小值做归一化把距离计算全部放到0到1的区间里。第二个是选择压力太大。当精英保留比例过高时种群会快速收敛到少数几个超级个体的周围多样性就被牺牲掉了。这时候我会把交叉概率调低一点变异概率调高一点或者在选择时引入一个很小的随机因素比如5%的概率随机选个体而不是严格按照排序结果选保证探索不会完全停止。7.3 运行速度太慢性能热点在这里Matlab本身是解释型语言循环写多了容易卡。我的代码经过剖析后发现性能热点几乎全部集中在解码函数上一个个体在300代进化中会被解码几百次每次都扫描机器时间表找插入空隙累积下来耗时巨大。优化的手段有三个。第一用向量化操作重写机器时间表的扫描逻辑能不用for循环就不用。当然Matlab的cellfun不一定比for快要实测为准。第二给解码函数加缓存如果同一个个体在进化过程中被重复解码直接返回上次的结果。在精英保留策略里父代和子代合并后确实有大量重复个体这个优化效果立竿见影。第三把最大进化代数调低一点用更多初始启发式解来弥补代数减少带来的搜索不足。7.4 免费工具和资源推荐最后分享一个资源层面的经验。标准算例的数据在网上的公开研究页面就能拿到Kacem和Brandimarte算例都已经整理成多钟格式我自己整理的.mat版本可以分享给一起做这个方向的朋友。算法框架也可以自己写NSGA-II本身实现并不复杂我上面给的代码骨架基本够用不过如果你一开始不想自己从零写也可以参考Matlab官方提供的多目标优化工具箱示例但要注意官方示例是通用框架连到FJSP的解码函数上还需要自己动手改。调试阶段建议先用Kacem小算例跑通因为小算例的已知最优解是公开的你可以快速确认自己的结果是不是在合理范围内。不要一上来就跑MK10这种大算例出错了根本不知道是算法问题还是数据问题排查成本太高。从我这些年的实践来看FJSP配上NSGA-II这套组合是科研和工程两端都站得住脚的方案。算法框架清晰、扩展性强、结果可视化方便而且代码一旦写好换数据、换目标函数、换约束条件改动量都不大。我现在的日常做法是把它作为一个基础工具需要临时给某个排产问题快速出几个候选方案时直接套用这套代码改改数据和目标函数就能在几分钟内出结果实际使用体验很好。最后一个建议一定要把你调试过程中遇到的每一个“啊原来是这里出了问题”记下来。调度问题里的坑非常一致你今天被坑的点和明年那个师弟师妹被坑的点大概率一模一样。我当时把“机器时间表插入空隙”“前道工序约束传递”“归一化拥挤度距离”这三个问题专门写成了调试笔记后来在带新人时省了不知道多少口舌。希望这篇博客也能帮你省下那段最痛苦的入门时间。
网站建设高端定制企业官网