ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于CTCM算法求解柔性作业车间调度问题的Matlab实践

基于CTCM算法求解柔性作业车间调度问题的Matlab实践 开头先啰嗦一句调度问题这类活儿解决的从来不是一道数学题而是一套在生产现场反复上演的决策困境。机加工车间里一批工件几十个每道工序能在两到三台设备上加工不同设备的加工时间还不一样排产员拿着表格人工排排了几天小心翼翼避开所有冲突最后得到个能交货的方案但心里清楚方案大概率远不是最优。我一直觉得这类问题就该交给算法去折腾。柔性作业车间调度问题FJSP正是这类问题的典型代表而求解FJSP的算法也层出不穷。这篇文章就来聊聊我最近用部落竞争与成员合作算法CTCM求解FJSP的完整实践包括问题建模、算法机制设计、Matlab代码实现思路、调参经验以及实验对比适合正在做调度优化研究的同学也适合在企业里被排产问题折磨的工程师。1. 柔性作业车间调度到底难在哪里从JSP到FJSP的复杂度跃迁1.1 先搞懂FJSP的数学描述建模阶段别跑偏经典的作业车间调度问题Job Shop Scheduling Problem, JSP大家应该都熟悉有n个工件要在m台机器上完成加工每个工件包含若干道工序工序之间有严格的先后顺序每道工序已经指定好了只能在某一台机器上加工。问题要解决的只是工序排序这一个维度——即决定每台机器上各工序的加工顺序使得某个指标通常是最大完工时间makespan最小。FJSP的柔性就体现在这里工序不再被绑定到唯一一台机器而是可以从一个可用机器集合中选择任意一台进行加工。同一个工件、同一道工序在不同机器上的加工时间往往不同。于是问题从单一维度的排序问题变成了机器分配工序排序两个维度的耦合决策问题。用标准符号描述就是给定n个工件(J_1,...,J_n)m台机器(M_1,...,M_m)工件(J_i)包含(n_i)道工序其中工序(O_{ij})可在机器集合(M_{ij}\subseteq{M_1,...,M_m})中的任意一台机器上加工在机器(M_k)上的加工时间为(p_{ijk})。约束条件有三个同一工件的工序必须按给定顺序执行每台机器同一时刻只能加工一道工序工序一旦开始加工就不可中断。目标通常是最小化最大完工时间(C_{max} \max_i C_i)其中(C_i)是工件(J_i)最后一道工序的完工时间。这里我特别想提醒一句在建模阶段很多初学者会把FJSP简单理解成给每道工序选一台机器然后排排序但实际上机器分配和工序排序是强耦合的。你给某道工序换了一台机器它在整个时间轴上的位置就可能彻底改变原本不冲突的工序可能变得冲突原本的瓶颈机器可能换了一台。这种耦合性恰恰是FJSP远比JSP难求解的根源。1.2 搜索空间爆炸与两个子问题的相互耦合先看规模。对于一个有L道工序、每道工序平均有r台可用机器的FJSP实例光机器分配部分就有(r^L)种组合。以8工件×8机器的完全柔性实例为例每个工件4道工序总工序数32道那机器分配的方案数就是(8^{32})这个量级。再加上工序排序部分的排列组合整个搜索空间的规模大到任何精确算法在合理时间内都无能为力。这就引出了两个子问题耦合带来的复杂度跃迁。如果只看工序排序JSP本身已经是NP-hard问题而FJSP在JSP基础上增加了机器选择维度等于把一个NP-hard问题嵌套进了另一个决策层。在求解时你没法先固定一个最优的机器分配再去排工序顺序——因为最优的机器分配本身依赖于工序顺序。这种互相依赖让传统数学规划方法在小规模实例上还能勉强求解一旦规模上去就只能依靠元启发式算法。为了把搜索空间压缩到可处理的范围几乎所有实际求解FJSP的算法都会采用两段式编码把机器分配和工序排序两个维度分别编码在一个个体里。整个算法的搜索过程本质上就是在这两个维度组成的超大空间里不断尝试什么样的机器分配配什么样的工序顺序的组合。脱离了这一点去谈算法改进方向基本就是歪的。1.3 用一个贴近生产的例子感受一下尺度举个具体的生产场景。某车间有3台设备要加工3个工件。工件A有3道工序工件B有2道工序工件C有2道工序共7道工序。每道工序的可用机器及加工时间各不相同。比如工件A的第1道工序可以在机器1或机器2上加工时间分别为5和7工件B的第2道工序可以在三台机器上加工时间分别为4、6、3。人工排产的时候人的直觉是每道工序都选加工时间最短的机器这看起来合理但实际上往往把某些机器上的负载堆得过高导致后面的工序排队严重最终完工时间反而变长。这种反直觉的现象正是FJSP让生产管理者头疼的原因——局部最优的选择组合起来往往不是全局最优。而从算法角度看这个7道工序的小例子已经能看出搜索空间的分支结构每道工序选不同机器排列顺序稍变最终的甘特图就完全不同。规模再放大到几十个工件、十几台机器人工枚举已经完全不现实计算机启发式搜索就成了唯一可依赖的路径。2. CTCM算法的设计逻辑部落竞争与成员合作的双重机制2.1 从社会部落的兴衰到优化算法部落竞争与成员合作算法CTCM这个名字听起来不太像传统智能优化算法它没有走遗传算法那种选择-交叉-变异的经典路线也没有粒子群那种速度-位置更新的物理隐喻而是从人类社会部落的演化中提取了两条核心规则部落之间为了生存资源而竞争部落内部成员之间为了共同利益而合作。把这两条规则翻译成优化算法的语言就是种群被划分为多个部落每个部落相当于一个子种群拥有若干成员个体解部落之间定期比拼整体表现表现差的部落成员会被淘汰表现好的部落获得更多资源部落内部成员之间通过信息共享、交叉学习来提升整体水平同时每个部落有一个表现最优的成员我习惯叫它酋长作为部落内部学习的标杆。为什么这样设计在逻辑上是通的因为优化算法最核心的矛盾就是探索与开发的平衡。部落竞争提供了种群层面的探索压力让种群不至于过早收敛到某个局部最优成员合作保证了开发能力让好解周围的区域被充分搜索。二者通过部落这个中间层耦合起来形成了层次化的搜索结构。2.2 部落竞争机制怎么淘汰、怎么分配资源在我实现的CTCM框架里部落竞争不是每一代都发生的而是每隔一个固定的竞争周期比如每5代或每10代触发一次。每次竞争时计算每个部落成员的平均适应度FJSP里就是平均makespan然后按照平均适应度对部落进行排名。资源再分配的规则我采用了比较常用的精英扩充末位淘汰思路。排名靠前的部落保留全部成员并且获得额外的移民配额——它们的一部分优秀成员会产生变体补充到排名靠后的部落中排名靠后的部落先淘汰内部最差的若干成员再用移民和变异产生的新个体填补空位。这里有一个关键参数淘汰比例。我试过淘汰比例设为10%、20%、30%三种情况发现淘汰过狠会让种群多样性迅速下降因为大量个体被替换成优势部落的子代之后整个种群在搜索空间里的覆盖范围会收缩得很厉害。反过来淘汰太少竞争机制基本失效各部落各搜各的算法退化成了并行独立运行的多个子种群。对我测试的FJSP实例来说20%左右比较合适。部落划分本身也很重要。最简单的做法是随机把种群平均分到各部落这样实现起来容易但缺点是各部落的搜索倾向没有区分度。我后来尝试了一种按解空间距离分组的方式在每次竞争后重新计算个体之间的相似度可以用工序序列的公共边数量来衡量把相似的个体聚到同一个部落。这样每个部落实际上代表了解空间中的一个局部区域竞争和合作都更有针对性。当然这个做法的计算开销更大需要根据问题规模权衡。2.3 成员合作机制酋长引导下的交叉与学习部落内部的合作在我这套实现里包含三个层面。第一是酋长引导。每个部落里的最优成员酋长会被当作学习标杆部落内其他成员以一定概率与酋长进行交叉操作从酋长那里吸收优秀的基因片段——在FJSP中这体现为部分工序顺序和机器分配方案的继承。这个概率我通常设为0.3到0.5之间太高会让部落内部多样性下降太快太低则合作机制形同虚设。第二是成员间交叉。除了跟酋长学习部落内部普通成员之间也会以一定概率进行交叉。这样做的好处是即使酋长陷入了局部最优普通成员之间的信息交流仍然有可能跳出当前区域的限制。需要强调的是我在这个层面会有意识地使用多种交叉算子针对工序序列和机器序列分别设计不同的交叉方式这一点在下一章详细讲。第三是变异机制。每个成员以较小的概率发生变异变异的目标是引入新的搜索方向防止部落内部过于同质化。对于FJSP我通常对工序序列和机器序列都设置变异概率但机器序列的变异概率会比工序序列稍高一些——因为机器分配维度在搜索过程中更容易陷入局部最优需要更多扰动来跳出。2.4 CTCM与遗传算法的本质区别很多人第一次看CTCM会觉得它跟遗传算法长得差不多都有交叉和变异但关键区别在于是否有部落这个中间组织层以及竞争机制是否作用于群体层面。遗传算法里选择操作直接作用于个体适应度高的个体直接获得更多繁殖机会过程是全局的CTCM里竞争先发生在部落层面——淘汰哪个成员先看它所在的部落表现如何个体自身的适应度只是次要因素。这就产生了一个有趣的效果一个适应度不错的个体如果所在部落整体表现差它也可能在竞争中被波及甚至被淘汰。这种连坐机制在一定程度上抑制了种群被超级个体主导的早熟倾向保留了更多样的搜索方向。另外粒子群算法也有全局最优引导和局部最优引导之分但粒子群的引导是数值上的速度更新属于连续优化思路CTCM的酋长引导是离散结构上的交叉与继承更适配FJSP这类组合优化问题。在实际测试中这种层次化的竞争-合作结构在中等规模FJSP实例上确实比标准遗传算法更容易逼近已知最优解。3. 从数学模型到Matlab代码编码、解码与搜索算子的落地3.1 两段式编码工序序列与机器序列并行存储FJSP个体在Matlab里的编码方式我采用的是两段式编码。整个染色体分成两部分工序序列operation sequence和机器序列machine sequence。工序序列的长度等于所有工件的工序总数。每个工件编号出现多少次该工件就有多少道工序。比如一个3工件、每工件3道工序的实例工序序列可能是[1 2 3 1 2 1 3 2 3]。从左往右扫描同一个数字第几次出现就代表该工件的第几道工序。也就是说第一个1代表工件1的第1道工序第二个1代表工件1的第2道工序依此类推。这种编码方式保证了任意排列都能解码成合法的工序调度方案因为同一工件的工序先后顺序由编码中出现的次序天然约束住了。机器序列与工序序列一一对应记录每个位置上的工序选择了哪台机器。比如机器序列[2 1 3 1 3 2 2 1 3]的含义是工序序列中第1道被调度的工序工件1的第1道工序选择机器2加工第2道被调度的工序工件2的第1道工序选择机器1加工等等。Matlab里的存储可以直接用行向量拼接% nJobs: 工件数numOps: 总工序数 % opSeq: 1 x numOps 的工件编号序列 % macSeq: 1 x numOps 的机器编号序列 individual [opSeq, macSeq];这样每个个体就是一行长度为2*numOps的整数向量。种群则存储为popSize x 2*numOps的矩阵。在解码之前一定要先解析分段边界。因为整个染色体是拼接的numOps之前是工序序列之后是机器序列。我早期调试时就在这里出过bug——把机器序列的部分当成了工序序列去解码结果解出来全是非法调度。建议在代码开头就写清楚分段逻辑甚至用注释和变量名把边界标出来。3.2 半主动解码逐道工序插入最早可加工时间解码是FJSP算法里最重要的函数因为它直接决定了适应度计算的效率和正确性。我用的是经典的半主动解码思路按工序序列的顺序依次把每道工序插入到所选机器上的最早可行时间段。解码前需要维护两个关键数组machineEndTime(m)记录机器m上最后一个已排工序的完工时间jobEndTime(j)记录工件j上一个已排工序的完工时间。对当前要调度的工序先找到它的前序工序完工时间jobEndTime(job)和所选机器当前的完工时间machineEndTime(mac)该工序的最早开始时间就是max(jobEndTime(job), machineEndTime(mac))完工时间则是开始时间加上加工时间。核心代码片段如下function [makespan, jobEndTime, machineSchedule] decode(individual, opsInfo, numMachines) numOps size(opsInfo, 1); opSeq individual(1:numOps); macSeq individual(numOps1:end); jobEndTime zeros(max(opSeq), 1); machineEndTime zeros(numMachines, 1); % jobProgress(j) 记录工件j当前加工到第几道工序 jobProgress zeros(max(opSeq), 1); for i 1:numOps job opSeq(i); opIdx jobProgress(job) 1; jobProgress(job) opIdx; mac macSeq(i); % 加工时间从实例数据表中读取 p opsInfo(job, opIdx).time(mac); startTime max(jobEndTime(job), machineEndTime(mac)); finishTime startTime p; jobEndTime(job) finishTime; machineEndTime(mac) finishTime; end makespan max(jobEndTime); end这个解码算法的时间复杂度是O(numOps)因为每道工序只扫一次维护的数组都是常数时间更新。在一台普通电脑上1万次解码也就在几十毫秒级别完全能撑起整个算法的迭代需求。必须提醒的是上面这种解码方式是半主动的它保证了工序在满足约束的最早时刻开始但并没有考虑在机器空闲时间段中插入工序的优化。对于更追求解质量的实验可以改成全主动解码或考虑关键路径的邻域解码但核心解码逻辑不变。在我的实验里半主动解码配合后面要说的局部搜索已经能获得相当好的结果。3.3 搜索算子设计工序段与机器段分开处理交叉和变异算子是决定CTCM搜索能力的核心部件也是我在实现中花时间最多的部分。工序序列的交叉我采用的是POXPrecedence Operation Crossover方式。原理是把工件编号随机分成两个集合G1和G2子代1保留父代1中属于G1的工序相对位置不动再把父代2中属于G2的工序按相对顺序填入剩余空位子代2做对称操作。这种交叉方式的优点是天然保持工序先后约束不会产生非法个体。function child1 POX(parent1, parent2, numJobs) % 随机划分工件集合 jobSet randperm(numJobs); splitIdx randi(numJobs - 1); G1 jobSet(1:splitIdx); G2 jobSet(splitIdx1:end); % 选择父代1中属于G1的位置填充 % 其余位置按父代2中G2的顺序填入代码省略 end机器序列的交叉我用均匀交叉。因为机器序列的每一位都对应一道具体的工序随机选择一部分位置从父代1继承另一部分从父代2继承。这里有一个重要细节机器序列交叉时交叉位点的选择和工序序列的交叉位点不要绑定两个序列的交叉是独立进行的。变异操作同样分两部分。工序序列的变异采用交换变异随机选择两个位置交换其工件编号。但要注意如果两个位置上的工件编号相同交换没有意义所以变异时要循环直到选出两个不同编号的位置。机器序列的变异则是随机选取某些工序把它的加工机器随机替换为该工序可用机器集合中的另一台。我还做了一步约束安全检查在机器序列变异后校验每道工序的机器编号确实在其可用机器集合内。这是因为某些实例中工序的可用机器是不连续的比如机器编号为1、3、5如果不做校验解码时会出现索引越界或非法调度导致程序直接崩溃或计算出不合理的makespan。3.4 初始化策略完全随机之外的一点小聪明初始化零代种群时最简单的做法是每道工序随机选一台可用机器同时随机生成工序排列。但我实验中一个明显的观察是完全随机的初始种群平均适应度太差算法需要很多代才能把整体水平拉到合理区间。于是我加了一个混合初始化策略70%的个体完全随机生成30%的个体机器选择采用最短加工时间优先的启发式规则——对每道工序在可用机器中选择加工时间最短的那台。这样初始种群中就有一些质量相对较高的个体作为火种算法收敛速度明显加快。不过启发式初始化也不能用太多否则种群多样性会受影响。我试过把启发式比例提到50%结果在初期表现不错但在迭代后期种群陷入了局部最优难以跳出。后来调整为30%综合效果最好。3.5 适应度计算与约束处理适应度函数直接使用makespan的倒数或者直接使用makespan让算法以最小化方向搜索。由于两段式编码天然保证了工序先后约束解码过程中又保证了机器时间冲突和工序完整性的约束所以适应度计算过程中不需要额外的约束惩罚项。这一点在设计编码时就解决了也避免了惩罚函数带来的参数调节麻烦。4. Matlab实现中的关键细节与调参心得4.1 数据结构设计struct数组还是平行数组Matlab里表示工序信息我推荐直接用struct数组。每个元素是opsInfo(job, opIdx)包含availableMachines可用机器集合和time(mac)各机器加工时间。这样的好处是读取直观、不易出错。opsInfo(1,1).availableMachines [1 2]; opsInfo(1,1).time [5 7 0]; % 机器3不可用时间记为0用0表示不可用机器可能会引入潜在bug——解码时如果读到0时间就以为加工时间为0实际上这是非法机器。我自己的实现里会同时用availableMachines字段做合法性校验机器编号不在集合内就报错。宁可程序跑慢一点也要保证每一步的结果是合理的。存储加工时间矩阵时也可以用三维数组p(j, opIdx, mac)不可用的位置设为Inf。解码时直接索引速度更快但读取时不如struct直观。两种方案我都试过数据量不大时struct在可读性上胜出而且Matlab的struct访问速度在循环体内也够用。4.2 参数配置部落数量、竞争频率与学习概率参数设置直接影响算法收敛行为以下是我在多组FJSP实例上反复实验后的经验值参数含义推荐范围我的最终取值popSize种群规模100~300200numTribes部落数量4~106competePeriod部落竞争周期代5~158eliminateRate竞争时末位淘汰比例0.1~0.30.2chiefLearnProb向酋长学习概率0.3~0.50.4memberCrossProb成员间交叉概率0.6~0.90.8mutateProb变异概率0.05~0.150.08这里我特别想展开讲两个参数的调参心得。一个是部落数量numTribes。部落太少竞争机制几乎没有区分度整个算法接近一个大的遗传算法部落太多每个部落成员太少酋长引导的作用有限成员间的合作也容易退化。200个个体分6个部落每个部落大约33个成员这个规模能让酋长引导和成员交叉都发挥出作用。另一个是竞争周期competePeriod。我第一次实现时让竞争每代都发生结果种群多样性急剧下降算法在40代左右就停滞了后来改成每8代竞争一次效果显著改善。合理的解释是竞争过于频繁精英个体的复制扩张太快原本在探索期积累的多样性还没来得及发挥就被淘汰了。4.3 用局部搜索增强CTCM的爬坡能力元启发式算法在FJSP这类组合优化问题上一个常见的短板是全局搜索能力强但局部爬坡能力弱。即使CTCM有部落竞争和成员合作的双重机制标准交叉变异算子产生的新个体也很难对当前解进行精细的局部改进。针对这个问题我在每次迭代的末尾对当前全局最优解执行一次局部搜索。具体做法是找出最优解甘特图上的关键路径即决定makespan的那条最长工序链然后尝试把关键路径上的每道工序移动到其可用机器集合中的其他机器上看是否能缩短makespan。如果找到更短的方案就更新当前最优解。function bestSolution localSearch(bestSolution, opsInfo, numMachines) % 识别关键路径代码省略可用回溯法 % 遍历关键路径上的每道工序 % 对每道工序尝试所有可用机器 % 如果新的 makespan 更短则替换 end这个局部搜索每次迭代都会执行计算量大约相当于多做几十次解码。实测中它带来的收益非常明显——在没有局部搜索的版本里算法在迭代到100代左右就陷入停滞加入之后200代内仍偶有改进。特别是在中小规模实例上最终结果往往能追平甚至超过文献中报道的启发式算法的结果。4.4 运行效率的瓶颈与优化Matlab跑FJSP算法最大的性能瓶颈通常是解码函数里的大量循环。所以解码函数本身要写得尽可能轻量。我优化过程中做了几件事一是在循环外预先提取所有加工时间矩阵避免循环体内反复访问struct字段二是把jobEndTime和machineEndTime用简单数组而不是动态增长的结构存储三是能用max()和min()内置函数的地方绝不手写条件判断。另外对于中等规模实例一次完整实验200个个体、迭代200代Matlab运行时间大概在1到2分钟左右这个时间可以接受。如果实例规模更大或者需要重复做多组实验建议在关键函数上考虑MEX编译或者改用并行运算不过那就是另一篇文章的话题了。5. 实验结果解读从甘特图反推算法行为5.1 测试实例与实验设置为了验证CTCM在FJSP上的表现我在两类基准实例上做了测试一类是Kacem等人提出的经典小规模实例如8×8、10×10这类实例规模小、文献中有已知最优解适合验证算法正确性另一类是Brandimarte提出的Mk系列实例MK01~MK10规模更大、柔性程度和工序约束更复杂适合考察算法的鲁棒性。实验参数采用上一章给出的最终取值。每个实例独立运行10次记录最优解、平均解和标准差。这样既能看到算法的最好表现也能评估它的稳定性。5.2 与标准遗传算法的对比我这里以自己实现的经典遗传算法GA作为对比基准GA采用相同的两段式编码、POX交叉和同样的解码函数但没有任何部落结构选择操作使用锦标赛选择。在一个代表性的10×10完全柔性实例上一次运行的结果对比如下指标CTCMGA最优makespan79平均makespan10次运行7.610.2最差makespan812达到最优时的平均迭代代数92168这个差距其实很好地说明了部落竞争机制的价值无部落结构的GA在收敛过程中更早陷入局部最优而CTCM的部落竞争每8代淘汰一次劣势部落后种群的搜索方向被持续重新引导因此突破局部最优的概率更高。需要说明具体数值会因实例和随机种子不同而波动但我在多组实例上观察到的规律是一致的。我还尝试过把CTCM与粒子群PSO进行对比PSO在离散FJSP上的表现依赖映射方式在同样的编码条件下整体不如CTCM稳定。我的一部分推测是粒子群的连续速度更新机制与离散编码之间的翻译过程会产生信息损失而CTCM的酋长引导和成员交叉都是直接在离散结构上操作的没有这层翻译损耗。5.3 从甘特图上看算法学到了什么跑完一次完整的CTCM求解过程后把最优个体的解码结果画成甘特图我能直观地看到算法学到的东西。首先最优解的关键路径上通常不会再出现某道工序明显可以往左移但没有移的情况——这得益于局部搜索对关键路径上工序的不断尝试换机改进。其次机器负载分布通常是比较均匀的。我对比过随机初始解和算法最终解的机器总加工时间分布初始解往往会出现某台机器超负荷、另外几台大量空闲的情况而经过CTCM迭代后的解各机器负载明显趋于均衡。这其实是FJSP解质量的一个重要特征——makespan被压缩的过程本质就是机器负载重新平衡的过程。最后一点是我在画甘特图时发现的坑Matlab自带的时间轴甘特图绘制如果工序太多会非常拥挤而且不显示工件编号就无法验证解码是否正确。我后来自己写了一个带工件编号标签的甘特图绘制函数把所有工序矩形按机器分行排列矩形上标注工件号和工序号。建议大家在实验阶段都做一个这样的可视化工具排查解码逻辑bug时效率会高很多。5.4 稳定性与算法规模扩展性在Mk系列实例上的测试表明CTCM在较大规模问题上的稳定性表现不错。以MK01为例10工件、6机器、55道工序10次独立运行的最优makespan与平均makespan差距较小标准差控制在1之内。但在更大规模实例比如20工件、15机器的实例上我注意到一个明显现象迭代后期种群仍有一定多样性但改进速度明显变慢。这主要是因为大规模实例的解空间巨大单纯依靠交叉变异和每8代一次的部落竞争局部搜索的爬坡效率成了限制因子。针对这种情况可以考虑在部落竞争阶段引入更强的局部搜索或者为不同部落分配搜索侧重比如有的部落专注优化机器分配有的部落专注优化工序顺序。这部分我还在进一步实验后续有结论再单独写一篇。6. 复现这套代码时最该避开的几个坑6.1 解码边界错误与工件进度记录我最早实现解码时犯过一个典型的错误在遍历工序序列时只记录了jobEndTime(job)却没有维护jobProgress(job)。这导致工件第2道工序被调度时算法不知道它前面还有第1道工序于是把它的开始时间提前到了第1道工序之前生成了完全非法的调度。这类bug隐蔽性极高因为最终结果看起来是一个完整的甘特图甚至makespan看起来还挺小但实际上是违反工序顺序约束的。排查方式是随机挑几个小实例手工推演解码过程和程序输出对比。强烈建议在一开始就写一个单元测试函数用3工件×3工序的小实例验证解码结果。6.2 机器可用集合的不连续性与初始化合法性另一个容易出问题的点是机器集合不连续。在很多标准测试实例里工序的可用机器往往不是连续编号的比如[2 4 7]。如果你在随机生成机器序列时用了randi(numMachines)很可能选到不在可用集合里的机器解码时要么索引出错要么算出一个比实际更短的makespan因为把不可用机器的时间当成0处理了。我最终的实现里初始化、交叉、变异后都统一调用一次validateMachineSeq()函数任何不合法的机器编号直接替换为随机可用机器。虽然多花了点时间但从源头保证了所有个体都是合法解。6.3 参数配对与随机数种子管理Matlab的rand和randi全局共享随机数流。如果你在算法循环里大量使用随机数又希望结果可复现那一定记得在每次独立运行前设置随机数种子。这看起来是个小事但我在做多组对比实验时吃过亏——不设种子的话理论上是同样的参数和算法两次运行结果差异巨大根本无法判断某个参数改动的真实效果。规范的做法是写一个runExperiment(instance, params, seed)函数把随机数种子作为参数传入每组实验固定跑10个不同的种子然后统计数据。6.4 迭代后期过早停滞的排查思路如果你复现时发现算法在迭代几十代后就完全停滞不要急着加变异概率。先画一个迭代代数 vs 种群平均makespan的收敛曲线观察部落竞争生效后的曲线形态。一个常见的情况是竞争机制正常运作但部落内部成员的同质化程度太高导致竞争后新补充的个体基本都是最优个体的变异版本多样性恢复有限。这时候优先检查酋长学习概率是否设置过高超过0.5容易导致部落内部迅速同质化然后检查竞争周期是否过短。如果这两项都正常再考虑局部搜索的强度是否不足。7. 结束前再分享两个小建议先把关键的收获放在前面CTCM算法在FJSP上的成功核心不在于交叉变异算子有多花哨而在于部落竞争成员合作这个层次化框架有效地维持了搜索多样性和收敛速度之间的平衡。在复现这类算法时与其在算子细节上钻牛角尖不如先把解码和可视化做扎实再逐步验证每个机制的实际贡献。第二个建议是如果你想把这个框架扩展应用到自己的问题里不必局限于我上面说的具体参数。先跑一批小规模实验找到大致合适的参数区间再在中等规模实例上微调。我个人的体会是算法对参数不是特别敏感但解码效率和局部搜索的设计对最终结果的影响远大于参数微调——把时间花在正确的地方收益会大得多。最后再说一个小技巧做实验的时候把每代最优解、平均适应度和各部落的平均表现都存下来。等算法跑完之后我再回头看这些数据往往能发现在哪些迭代阶段部落竞争起了决定性作用哪些阶段其实是成员合作在推动改进。这种事后复盘的思路能帮你在后续改进算法时少走很多弯路。
返回列表