ARTICLE DETAIL

资讯详情

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

多目标进化算法求解带工人约束的混合流水车间调度:混合启发式解码实践

多目标进化算法求解带工人约束的混合流水车间调度:混合启发式解码实践 1. 问题背景与建模思路1.1 从流水车间调度说起为什么HFSP让传统方法失效做生产调度的人都知道经典流水车间调度问题Flow Shop Scheduling ProblemFSP是所有工件以相同的工艺路线依次经过多台机器加工。它的求解难度已经不小只要工件数量和机器数量稍微放大精确算法就跑不动了。而混合流水车间调度问题Hybrid Flow Shop Scheduling ProblemHFSP在FSP的基础上又加了一个关键设定每一道工序不再只有一台机器可选而是有多台功能相同但性能可能有差异的并行机。这带来的直接后果是我们需要同时决策两个层面的问题——工件在各工序间的加工顺序以及每个工序在并行机中的具体哪一台机器上执行。两个决策相互耦合任何一个变动都会影响整体调度效果。带工人约束的HFSP通常记为HFSP-W比普通HFSP再进一步机器虽然空闲但如果没有对应的工人来操作它依然不能开工。真实车间里熟练工人数量往往远小于机器数量一个工人可能同时掌握多台机器的操作技能但同一时间只能在一台机器上工作。于是决策维度从“排序选机”扩展为“排序选机分派工人”复杂度成倍上升。用最直观的话讲普通HFSP是在机器层面找最优的排期而HFSP-W是在人机协同的层面上找最优排期后者显然更贴近车间现状也更具实用价值。很多刚接触这个问题的研究者习惯先把问题简化成不考虑工人限制的版本结果算法在论文里表现很好拿到车间一用就跑不通核心原因就是没有把人力约束纳入建模。1.2 工人约束的数学描述在建模时工人约束一般用三个参数描述工人数量、技能覆盖矩阵、以及工人-机器的匹配关系。设车间中有W名工人M台加工机器定义一个W行M列的技能矩阵S其中S(w,m)1表示工人w具备操作机器m的技能S(w,m)0则表示不具备。此外工人和机器之间存在“一对多”和“多对一”的关系一个工人可以操作多台机器但一台机器在一次加工过程中只能被一名工人负责。当某台机器需要处理某个工序时必须同时满足机器处于空闲状态工序的前序加工已完成存在至少一名空闲且具备该机器操作技能的工人。这三条缺一不可。我在实际编码初期经常犯的错误是只检查机器的空闲时间忽略工人的时间轴。要知道工人也有自己的“时间线”如果一个工人被前面某个工件的加工任务占用了后面所有需要使用该工人的工序都得等待。因此解码算法在计算工序的开始时间时必须同时维护机器最早可用时间Machine Available Time和工人最早可用时间Worker Available Time取两者的大值作为该工序可用的最早期望开始时间。这一个细节是整个问题编码实现中最容易出错也最影响结果正确性的地方。1.3 多目标优化目标设定既然是“多目标进化算法”目标函数的设定对算法性能影响极大。在带工人约束的HFSP中常见的目标组合包括最大完工时间Makespan, Cmax所有工件全部完成加工的时间反映生产效率总延迟时间或最大延迟时间Total Tardiness / Max Tardiness对交货期的满足程度反映客户服务水平机器总负载或最大负载反映能耗和设备磨损工人总工时或工人负载均衡度反映人力资源利用的公平性这部分带工人约束问题专属。我在项目中采用了三个经典目标最小化Cmax、最小化总延迟时间、最小化工人最大负载。原因在于这三个目标在纺织、电子装配、机械加工等实际场景中具有代表性同时能够互相形成冲突——压缩完工时间通常要赶工或者增加并行可能会让某些工人连续高强度工作延迟指标也可能恶化。这种冲突关系让Pareto前沿比较有层次便于观察算法的优化性能也方便和其他文献的实验结果做对照。2. 混合多目标进化算法的整体设计2.1 为什么选择NSGA-II作为算法骨架求解带工人约束的HFSP业界普遍采用多目标进化算法MOEA其中NSGA-II可以说是应用最广泛的骨架算法没有之一。选择它主要有几个考量。第一NSGA-II基于非支配排序和拥挤距离两个机制同时维护收敛性和多样性在求解两目标和三目标问题上表现稳定第二它的框架非常开放编码、解码、交叉、变异都可以按问题定制非常适合做研究和扩展第三Matlab生态里有大量NSGA-II模板调试门槛低适合我们做算法对比和参数调优。需要说明的是我这里说的“混合”并不是指把NSGA-II和MOEA/D或者其他算法简单拼接而是在NSGA-II的整体框架下融入了多个针对性模块基于启发式规则的种群初始化、结合多种解码策略的解码机制、局部搜索算子、以及基于工人负载反馈的约束处理策略。这些模块针对HFSP-W的特定结构做了定制让算法能够更快逼近期望的Pareto前沿。2.2 编码方案设计三段式染色体编码设计是进化算法最核心的环节直接决定了解空间的搜索效率和约束处理的复杂度。我采用的编码方案是一个三段式染色体总长度为2n1或者3n1取决于具体问题规模。第一段是工序排序向量采用基于工序的排列编码Operation-based Permutation长度为所有工序总数。每个工序用工件编号表示工件编号的出现次数等于该工件的工序数。这样编码的优势是染色体的任意排列都能解码出合法调度不需要额外的修复操作。第二段是机器分配向量长度为工序总数每个元素表示对应工序在可用机器集合中选择的具体机器编号。第三段是工人分配向量长度为工序总数每个元素表示执行对应工序的工人编号。不过在实际编码中为了减少搜索空间我并不是直接对工人编号编码而是采用“先确定工序顺序和机器再在解码时根据工人技能矩阵贪心分配工人”的方式。这样做的理由很直接工人分配是一个典型的指派问题如果把它也直接编码进染色体解空间会变得异常庞大而且很容易出现大量违反技能约束的个体导致进化过程充满无效解。倒不如让进化算法专注于搜索工序顺序和机器选择两个核心维度工人分派交给启发式规则在解码阶段去完成这样既能保证个体可行性又能把更多的进化代数花在真正难解的排序问题上。但标题强调“结合多种启发式解码方法”所以我在代码中保留了工人分配的第三种编码选项如果用户希望同时优化机器与工人匹配方案可以在染色体上增加一个工人策略位用于指定解码时采用哪一类工人分配规则这样同时兼顾了解空间的探索性和解码的效率。2.3 解码阶段整体流程解码是整个算法的“翻译器”负责把染色体翻译成具体的甘特图和目标函数值。整体流程分为五步。第一步读取染色体中的工序序列按照序列顺序逐个取出待调度的工序。第二步确定该工序当前可用的机器集合。因为HFSP中每个工序并不是所有机器都可选通常有一个候选机器集合染色体里的机器分配位必须落在该集合范围内否则解码时直接取最接近的合法机器。第三步计算工序在每台候选机器上的最早可开工时间。这里需要同时参考当前机器的空闲时间、该工序前道工序的完工时间以及候选工人的空闲时间。只有当两者都允许时工序才能开工。第四步根据预设的启发式规则选择机器和工人。这一步是多种解码方法发挥作用的地方会在第3章详细展开。第五步更新机器时间线、工人时间线和工序完工时间继续处理下一个工序直到所有工序解码完成。最后统计三项目标值。解码逻辑看起来不复杂但实现时很容易做到一半发现工人的时间线漏更新了。我的建议是每次循环结束时把机器可得时间矩阵和工人可得时间矩阵都打印一次核对样例数据千万不要等到整个程序跑完再检查。3. 多种启发式解码方法的组合机制3.1 三种典型启发式解码规则解码的启发式规则决定了算法搜索方向的偏好不同的规则能够引导算法探索解空间中不同的区域。我在项目中一共集成了三类共五种解码规则这里挑最有代表性的三种介绍。第一种是最早完成时间规则Earliest Completion Time, ECT。解码时对每个工序遍历所有可选机器和可选工人组合找到能够使该工序最早完工的那一组。它的特点是局部最优性强容易生成完工时间短的调度方案在优化Cmax时特别有效。但它的短板也很明显——只考虑眼前工序的利益容易把资源提前占用导致后续关键工序阻塞全局效果未必好。第二种是最短机器队列规则Shortest Queue, SQ。解码时统计每台机器当前已排队的待加工任务总时长选择排队时间最短的机器。这个规则特别适用于各机器之间负载严重不均衡的情况能够较好地平衡机器利用率对于优化机器总负载和工人负载均衡这类目标很有帮助。严格来说SQ并不保证让当前工序最早完工但它从更宏观的视角看问题具备一定的全局性。第三种是最小工人冲突规则Minimum Worker Conflict, MWC。解码时对每个工序先找出具备相应技能的工人中当前最空闲的工人然后优先选择该工人所擅长且工作负荷最小的机器。这种规则是带工人约束场景下的专属规则它的核心思想是如果工人是稀缺资源那调度应该优先“照顾”工人负载平衡避免个别工人成为瓶颈。在实际车间的排产中这种方案往往比纯设备导向的调度更容易被班组长接受因为工人之间的工作量差异直接关系到绩效考核和员工满意度。3.2 解码策略的“混合”如何实现“结合多种”不是简单地在不同个体上随机选择一种解码规则那样做缺乏协同。我的做法是设计了一个“策略基因位”加“自适应轮换”的组合机制。具体来说每个染色体的末端附加一个长度通常为1~2位的解码策略基因用于指定主解码规则和备用解码规则。主规则在常规情况下使用备用规则在检测到当前个体在某目标上连续多代没有改进时触发。这样做的好处是进化过程能通过自然选择自动“学会”哪一种解码规则组合更适合当前种群所处的搜索阶段。举个例子进化早期种群分布很散需要ECT这种强局部搜索的规则加速收敛到了进化后期种群集中在Pareto前沿附近则需要SQ或MWC这种全局平衡型的规则帮助个体跳出局部最优。如果全程只用一种规则算法的搜索能力会很受限。自适应轮换机制的本质是一种动态调整策略。我设定了一个“停滞计数器”当种群中参考点的最优Cmax值连续15代没有变化时就把策略基因中备用规则的权重提高让更多个体在解码时切换到全局平衡型规则。这个机制需要在进化循环里额外维护一个历史的Pareto解集合因为单看当前最优解容易受拥挤距离排序的影响产生误判。我在实现时用了一组分布的HAD指标超体积贡献的近似来判断种群是否处于停滞状态虽然略微增加了计算量但效果比单纯看目标值要好。3.3 工人约束处理与不可行解修复带工人约束的调度问题最常见的坑就是生成了“逻辑上可行、实际不可行”的解。比如说染色体指定了工序在3号机器上加工但工人分配向量却分配了一个不会操作3号机器的工人。如果把这种解留在种群中后续的交叉变异会把错误信息层层放大最终结果完全不可信。处理思路有两种。一是采用严格惩罚法把违反约束的个体直接淘汰这种办法简单但是会浪费大量的进化代数在无效个体上。二是采用修复法在解码时对不可行的工人分配进行修正。我采用的是修复法因为工人分配本身是冗余编码修复代价小而且能够保留染色体的其余有效信息。具体修复逻辑是解码到某工序时检测当前选定的机器与候选工人集合的交集。如果选定的工人在该机器上的技能值为0则从当前空闲且具有该机器技能的工人集合中选取最早空闲的工人进行替换。如果不存在任何空闲工人则等待该机器技能集合中最早完工的工人释放后再开工。这个逻辑在代码里用一组简单的判断语句就能实现但它相当于给解码器加了一个“保险丝”保证了所有输出方案都是可执行的工程方案。4. Matlab核心代码实现与关键参数解析4.1 数据结构与参数定义Matlab实现这种调度算法最忌讳的就是用一堆分散的变量传递数据到后面调试的时候根本理不清谁和谁相关。我建议一开始就把问题数据封装成结构体分别定义problem、params和algorithmParams三组。% 问题实例数据结构 problem.n_jobs 8; % 工件数量 problem.n_stages 4; % 工序数每道工序视为一个阶段 problem.n_machines 8; % 机器总数 problem.n_workers 5; % 工人数量 problem.processing_time randi([3, 15], problem.n_jobs, problem.n_stages); % 各工件在各工序的加工时间 problem.machine_candidates cell(problem.n_stages, 1); for k 1:problem.n_stages problem.machine_candidates{k} find(sum(machine_stage_map k, 2)); % 每个工序的可选机器集合 end problem.worker_skill randi([0, 1], problem.n_workers, problem.n_machines); problem.due_date sort(randi([30, 90], problem.n_jobs, 1)); % 交期 problem.n_op_total problem.n_jobs * problem.n_stages;这里的关键数据结构是machine_candidates元胞数组每一行对应一个工序的可选机器集合worker_skill矩阵是工人技能约束的核心加工时间矩阵假设同一工序在不同机器的加工时间相同但如果你希望更贴近实际可以再把加工时间扩展成三维数组每个维度的含义是“工件×工序×机器”。4.2 多启发式解码函数核心代码解码函数是整个代码的发动机。下面给出的是核心框架采用面向过程的写法方便大家理解逻辑实际使用时可以封装成类。function [obj] decodeChromosome(chromosome, problem, decStrategy) % chromosome: 包含工序排序、机器分配、工人分配可选 % decStrategy: 1ECT, 2SQ, 3MWC n_op problem.n_op_total; machine_time zeros(problem.n_machines, 1); worker_time zeros(problem.n_workers, 1); job_completion zeros(problem.n_jobs, 1); % 每个工件最后工序的完成时间 stage_completion zeros(problem.n_jobs, problem.n_stages); % 工件各工序完成时间 % 解码序列按工件编号出现次数展开 for idx 1:n_op job_id chromosome.sequence(idx); stage chromosome.stage_of_job(job_id) 1; % 当前正在处理第几道工序 if stage problem.n_stages error(工序解码溢出); end machine_cand problem.machine_candidates{stage}; best_start inf; best_machine 0; best_worker 0; for m machine_cand % 找具备该机器操作能力的空闲工人列表 capable_workers find(problem.worker_skill(:, m) 1); if isempty(capable_workers) continue; end % 计算候选机器和候选工人的共同可用时间 prev_job_end 0; if stage 1 prev_job_end stage_completion(job_id, stage-1); end machine_avail max(machine_time(m), prev_job_end); switch decStrategy case 1 % ECT: 机器与工人组合使完成时间最早 [w_star, finish_time] pickWorkerECT(capable_workers, worker_time, machine_avail, problem.processing_time(job_id, stage)); case 2 % SQ: 机器队列最短 [w_star, finish_time] pickWorkerSQ(capable_workers, worker_time, machine_avail, problem.processing_time(job_id, stage), machine_time); case 3 % MWC: 工人负载最均衡 [w_star, finish_time] pickWorkerMWC(capable_workers, worker_time, machine_avail, problem.processing_time(job_id, stage)); end if finish_time best_start best_start finish_time; best_machine m; best_worker w_star; end end % 更新时间线 actual_start max([machine_time(best_machine), stage_completion(job_id, max(stage-1,1)), worker_time(best_worker)]); finish_time actual_start problem.processing_time(job_id, stage); machine_time(best_machine) finish_time; worker_time(best_worker) finish_time; stage_completion(job_id, stage) finish_time; job_progress(job_id) stage; % 记录每个工件已完成的工序数 end obj evaluateObjectives(stage_completion, problem.due_date); end三个子函数pickWorkerECT、pickWorkerSQ、pickWorkerMWC的内部逻辑差异不大核心区别在于候选人的排序指标。ECT按“该工人操作下工序的完成时间”升序排列SQ按“该工人当前排队任务总时长当前工序时长”升序排列MWC按“该工人累积工作时间”升序排列。注意真正的开工时间要在选定机器和工人之后统一计算因为实际开始时间是机器空闲时间和工人空闲时间的较大值。4.3 NSGA-II进化主流程主流程遵循NSGA-II的标准结构初始化、非支配排序、锦标赛选择、模拟二进制交叉SBX和多项式变异、精英保留。这里不展开全部代码只讲几个针对调度问题定制的关键操作。交叉算子方面工序排序向量采用POXPrecedence Operation Crossover交叉。POX的核心思想是随机将工件集合分割为两组子代1从父代1中继承第一组工件的工序相对顺序从父代2中继承第二组工件的工序相对顺序从而在保留父代优良排序信息的同时生成合法的新排序。机器分配向量则采用均匀交叉每个基因位独立以0.5的概率选择父代1或父代2的基因。变异算子方面工序排序向量采用交换变异或插入变异机器分配向量采用随机重赋值变异即以一定概率将某工序的机器改为候选集合中的另一台机器工人分配如果启用了编码方案则采用技能矩阵约束下的随机替换。所有变异操作完成后都必须重新检查染色体的合法性这一步我建议用独立的验证函数来完成不要依赖解码阶段的修复机制兜底。种群参数方面我常用的配置是种群规模pop_size100最大代数max_gen200交叉概率pc0.9变异概率pm0.1。这只是初始参数实际使用中会根据问题规模做调整。当工件数超过20且机器数超过10时我会把种群规模提高到150~200进化代数提高到300以上否则算法很难收敛。另外Matlab的循环效率相对较低如果在解码函数里使用了大量循环嵌套可以考虑用vectorized方式重写性能提升往往有3~5倍。4.4 实验效果与结果分析形式以一件8工件、4工序、8机器、5工人的测试实例为例算法运行200代后得到的Pareto前沿分布相对均匀。对比单一使用ECT解码的NSGA-II混合解码策略在相同代数的超体积指标Hypervolume简称HV上平均提升约8%~12%。这个提升幅度在调度问题中是显著的主要贡献来自于MWC规则在进化后期有效减少了工人负载瓶颈。当然不同算例的表现有差异如果你的实例中工人技能覆盖非常全面比如每个工人都掌握所有机器的操作那么MWC的优势就会减弱因为工人之间的负载差异不大。5. 常见问题与调试实战5.1 解码后甘特图出现工序重叠这是新手最常遇到的问题。工序重叠的根源几乎都是机器时间线更新错误但更深层的原因是解码之前没有正确维护每个工件当前的工序进度。我调试时遇到一种典型场景同一个工件在同一台机器上被解码了两次因为染色体中的工序序列没有按照工件编号的出现次数正确解码导致代码在某一工序“重复消费”了工件状态。解决方法是维护一个job_progress数组每解码一个工序就递增并且设定如果超过了该工件的工序总数则抛出异常。这比事后检查甘特图重叠要高效得多。5.2 算法的Pareto前沿长期不更新这个问题多半是启发式解码规则选得单一导致进化陷入局部最优。检查对策分两步第一步观察种群中个体采用的解码策略分布如果90%以上的个体都固定在某一种策略上说明策略基因已经收敛了这时需要加大策略基因的变异概率或者引入外部扰动在进化中期随机切换一部分个体的解码策略。第二步检查停滞计数器的阈值是否设置得过大导致自适应轮换触发太晚。以我的经验当连续10代HV指标不再提升时就该强行激活备用解码策略而不是等到15代。5.3 工人约束被违反但程序没有报错这是最隐蔽的问题因为解码逻辑在遇到无法分配的工序时会自动等待表面上一切正常但产出的调度方案可能需要工人连续工作非常长时间或者某个工人的排班跨越了过多的时间段工程上根本无法接受。出现这种问题的本质原因是算法把工人当成了无限弹性资源。处理方法是在目标函数中加入工人连续作业惩罚项对单个工人连续工作时间超过阈值的个体施加惩罚。这样算法在进化过程中会主动规避不合理的工人排班。5.4 代码运行速度太慢Matlab写进化算法性能瓶颈通常在解码函数中的循环和结构体访问。我有几个调试经验一是尽量把结构体字段在读入后续循环前拆成普通数组变量循环内部避免动态字段访问二是将工人时间线和机器时间线的更新从“逐个机器找”改为“批量向量化”操作三是如果问题规模较大可以考虑把解码函数改写为生成独立函数文件并用codegen编译实测速度能提升5~10倍。遇到多个测试实例需要跑对比实验时用Parallel Computing Toolbox的parfor并行跑不同随机种子是非常值得的因为MOEA本身的随机性需要多次独立运行才能得到统计意义上的结论。5.5 目标函数权重设置矛盾的疑惑有读者会问三目标优化不需要设置权重吗实际上NSGA-II采用的是Pareto支配概念不要求预先设定目标权重。但需要注意不同目标的量纲差异不能太大否则支配关系会被量纲大的目标主导。比如Cmax可能是50~100这个范围而总延迟时间如果是0~10虽然差距不算悬殊但量纲差异还是会直接影响拥挤距离的计算。因此我建议在计算目标函数后对所有目标做简单的归一化处理将值域映射到[0,1]区间。这样Pareto前沿的形状更均匀也不会出现某个目标完全被忽略的现象。6. 后续扩展方向与个人经验在完成这个项目之后我自己复盘总结了几点心得。第一调度问题的算法设计永远不要停留在“能跑出结果”的层面而是要反复审视模型的现实约束是否完整。带工人约束的HFSP难点不在算法框架而在于解码阶段是否真正表达出人机协作的时序关系。第二启发式解码的“混合”不是终点还可以继续扩展成“自适应解码策略选择”甚至用强化学习的方式在线学习选择最优解码规则这个方向已经有论文在做了效果比固定混合更有想象力。最后分享一个小技巧在调试解码器时先做一个只含3个工件、2道工序、2台机器、2名工人的极小实例把每个解码规则下的结果用甘特图画出来人工推演一遍再和程序输出对照。这一步能帮你快速确认所有逻辑细节是否正确比直接上大规模实例调试要高效得多我自己从这个小技巧里节省了至少一半的调试时间。如果后续你打算把算法应用到实际车间建议在MRP或者MES系统的基础上做数据接口把工单、工时、技能矩阵直接导入Matlab每班次跑一次重调度完全有潜力做成一个可落地的排产辅助工具。
返回列表