资讯动态

多目标鲸鱼算法求解置换流水车间调度问题及MATLAB实现

发布时间:2026/10/9 6:58:52 来源:尧图企业网站定制
1. 生产调度问题的核心矛盾先搞清楚在解什么题先说个我身边的真实场景。上周帮一个做机加工的朋友看他们的排产表发现车间计划员还在用Excel手动拖订单每来一个急单就要重新排一遍几十个工件在几台机床之间的顺序怎么安排全靠老师傅拍脑袋。结果就是机器利用率忽高忽低交货期经常压线。我问他为什么不用排产软件他说试过几套商用APS要么太贵要么模型跟车间实际对不上最后还是回到Excel。这其实就是生产调度排程问题的典型困境理论上这是个组合优化问题实际上车间天天在用人肉启发式算法解决。当工件数量超过15个、机器数量超过5台的时候穷举所有加工顺序已经是不可能完成的任务——15个工件在流水线上排列光是顺序就有15!种可能也就是大约1.3万亿种就算一秒钟验一种也需要四万多年。这就是为什么需要智能优化算法而多目标鲸鱼算法正是解决这类离散组合优化问题的有效工具之一。1.1 置换流水车间调度问题从约束到建模流水车间调度Flow Shop Scheduling有一个非常明确的定义n个工件按照相同的工艺路线依次经过m台机器加工。注意这个相同工艺路线是核心特征——工件1和工件2都要先上铣床、再上磨床、最后上钻床顺序不能颠倒。而置换流水车间调度Permutation Flow Shop Scheduling Problem, PFSP更严格不仅工艺路线相同而且所有工件在每台机器上的加工顺序也必须一致。也就是说你定了工件在铣床上的加工顺序之后在磨床和钻床上也得按这个顺序来不能中途换序。这个限制听起来有点死板但实际工厂里很常见——生产线的物理布局决定了工件只能沿着一个方向流动中间如果有倒序物流成本和换线时间会暴涨。用数学语言来定义这个问题的约束条件不复杂但每一步都有物理含义参数n表示工件数量m表示机器数量p(i,j)表示工件i在机器j上的加工时间这是个n×m的矩阵也是问题输入的核心数据决策变量一个长度为n的工件排列序列排列π表示第k个位置上加工哪个工件硬约束一每个工件在同一时刻只能在一台机器上加工也就是每个工件在每个机器上只有一个加工时间窗硬约束二每台机器在同一时刻只能加工一个工件这对应机器资源独占性硬约束三工件在机器间的转移时间忽略不计或按固定传输时间处理这一步看实际情况但大多数基准算例都假设为零硬约束四工序一旦开始就不能中断这排除了抢占式调度。计算完工时间时最常用的递推公式如下假设C(i,j)表示工件i在机器j上的完工时间那么C(1,1) p(1,1); for j 2:m C(1,j) C(1,j-1) p(1,j); % 第一个工件在后续机器上只能等前一台完成 end for k 2:n C(k,1) C(k-1,1) p(k,1); % 后续工件在第一台机器上要排队 for j 2:m C(k,j) max(C(k,j-1), C(k-1,j)) p(k,j); % 核心递推取前序工序完成和本机前一个工件完成的较大值 end end makespan C(n,m);这个递推公式是整个调度计算的地基。max(C(k,j-1), C(k-1,j))这一步很多人看不懂我解释一下工件k要开始在机器j上加工必须同时满足两个条件一是它自己在机器j-1上的工序已经完成C(k,j-1)二是机器j已经把前一个工件k-1加工完C(k-1,j)。两个条件都满足才能开工所以取max就是取两者中晚的那个时刻。这个逻辑就是甘特图画出来的理论基础也是后面适应度函数计算的核心。1.2 调度目标不止一个完工时间、拖期与机器均衡单目标调度问题通常只优化一个指标最常见的是最大完工时间Makespan也就是最后一个工件完成加工的时刻。这个指标对应的是整批订单什么时候能全部交付直接决定产能。但工厂实际运行中排产的人脑子里同时装着好几件事最大完工时间Cmax要小订单整体交付快加权拖期要小特别是重点客户的订单不能逾期逾期要罚款甚至丢客户机器负载要均衡不能让一台机床累死、另一台闲着否则设备折旧和保养周期都会出问题在制品库存要低工件堆在车间等待的时间越短流动资金的占用越少。这些问题之间存在矛盾。比如你为了让最大完工时间最小可能把所有工件都往最快的机器上塞结果这台机器过载、其他机器闲置设备利用率严重失衡再比如为了赶某个大客户的交期把它的工件全部提前结果其他订单全部逾期总拖期反而更大。这种互相冲突的目标就是多目标优化的用武之地。所以多目标鲸鱼算法面前的问题严格来说是在满足上述四条硬约束的前提下同时优化若干个互相冲突的目标函数最后输出一组帕累托最优的调度方案而不是一个唯一的答案。这组方案让计划员可以根据当天的实际情况比如哪台设备该检修了、哪个客户又打电话催了在多个方案里灵活选。1.3 为什么要用群智能算法而不是穷举或线性规划经典运筹学解决小规模调度问题是用分支定界法能保证找到全局最优但计算时间随问题规模指数增长。15个工件的PFSP分支定界在普通电脑上跑几小时都有可能40个工件以上基本上就别想了。线性规划建模调度问题确实可以做0-1整数规划模型能精确描述约束但解一个稍微复杂的实例商业求解器也可能跑一晚上。群智能算法的思路完全不一样——不追求每次都找到全局最优而是以很高的概率在可以接受的时间内找到近优解。遗传算法是这一块的常客粒子群算法也常被用来做调度鲸鱼算法是近年来表现相当亮眼的一个新成员。它的位置更新机制在连续优化问题上的勘探能力很强而经过合理的离散化改造后在调度这种组合优化问题上也能表现得很有竞争力。后面我会详细讲它和遗传算法、粒子群算法在求解调度问题时侧重点上的差异。2. 鲸鱼算法的搜索机制与调度问题的适配逻辑鲸鱼优化算法Whale Optimization Algorithm, WOA是Mirjalili在2016年提出的模拟的是座头鲸的捕食行为。我在第一次接触这个算法时觉得它的机制很有意思它不是简单的跟着最优个体走而是有三种完全不同的移动策略轮流施加在种群身上这就让搜索过程既有集中的开发又有随机的勘探。2.1 三种位置更新策略的生物学直觉座头鲸捕食有一个标志性动作吐出一串气泡形成一张气泡网把磷虾群困在网里然后从网底螺旋上升一口吞掉。算法把这一行为抽象成了三种位置更新模式第一种包围猎物。这是标准操作。鲸鱼发现猎物位置后其他鲸鱼会向这个位置收缩靠拢。数学上表示为当前个体向着当前最优解的位置移动移动幅度由随机向量A控制。当|A| 1时群体呈现向最优解聚集的趋势。公式是D abs(C .* X_best - X); X_new X_best - A .* D;其中A和C是系数向量。A的取值很关键A 2*a.*rand - a这里的a从2线性递减到0。a大时A的波动范围大搜索步幅也大a逐渐变小后步幅收紧群体收敛。这个递减策略跟粒子群算法的惯性权重递减是同一个思想都是前期全局勘探、后期局部开发。第二种气泡网螺旋更新。这条路径模拟的是鲸鱼螺旋上升的过程。公式是D_spiral abs(X_best - X); X_new D_spiral .* exp(b .* l) .* cos(2*pi*l) X_best;这里b是对数螺旋的形状常数l是[-1,1]之间的随机数。注意这个更新是直接在当前最优解和当前个体之间画一条对数螺旋线个体沿着螺旋线向最优解靠拢。这种策略保证了对最优解邻域的精细搜索有点像是局部搜索操作。第三种随机搜索猎物。当|A| ≥ 1时算法不再参考当前最优解而是随机挑一个个体作为临时猎物来更新位置。这是鲸鱼算法取巧的地方——用随机个体的位置替代全局最优强行让部分个体脱离聚集区去探索未知区域。这个机制对避免局部最优至关重要因为调度问题的搜索空间存在大量局部最优解如果没有这种强随机扰动种群很容易早熟收敛。在标准WOA中每轮迭代以50%的概率在收缩包围和螺旋更新之间选择而是否走随机搜索路线则完全由A的取值决定。这个设计思路简单粗暴效果却很好。2.2 从连续位置到离散工序序列SPV排序规则是怎么工作的这里有个必须说清楚的问题。调度问题里的决策变量是工件的排列顺序它是一个离散的、整数类型的序列而鲸鱼算法天生是为连续空间设计的它更新出来的位置向量X [x1, x2, ..., xn]是一堆连续实数。这两个世界的变量对不上必须做映射也就是离散化处理。最常用的映射方法是SPV方法全称是Smallest Position Value。思路一句话就能说清把位置向量里每个维度的数值从小到大排序排序后得到的序号序列就是工件的加工顺序。举一个具体例子假设有5个工件某一头鲸鱼的位置向量是X [0.83, 0.12, 0.55, 0.97, 0.31]对这5个数从小到大排序最小的是0.12第2维其次是0.31第5维然后是0.55第3维、0.83第1维、0.97第4维所以得到的调度序列是[2, 5, 3, 1, 4]即最先加工工件2最后加工工件4。这个映射的关键在于位置向量里各维度的相对大小关系决定了顺序但绝对值大小不影响。这意味着鲸鱼算法在连续空间里的任何移动都会转化为排列顺序的变化而且编码永远合法——不会出现某个工件被重复分配或漏掉的情况。另一种常见的映射思路是LOP方法Largest Order Value跟SPV相反取数值从大到小排序。两者的效果差别不大我用下来感觉SPV更直观代码更容易和别人解释所以下面实现都基于SPV。2.3 和遗传算法、粒子群算法相比鲸鱼算法的优势在哪很多人问过我调度问题不是遗传算法GA的经典阵地吗为什么要用鲸鱼算法这几类算法我都做过对比测试说下个人感受。遗传算法的核心操作是交叉和变异。交叉操作能很好地保留父代优秀基因片段——这在调度问题里体现为保序性两个好的父序列交叉后子代往往继承了一部分块顺序这是GA在置换问题上表现稳定的原因。但GA的瓶颈在于参数多交叉概率、变异概率、锦标赛规模、精英保留数量随便一个调不好收敛速度就大打折扣。粒子群算法PSO的优点是收敛快因为它的速度-位置更新模型让粒子快速朝历史最优和全局最优飞行。但PSO在置换流水车间调度里有个老毛病——容易早熟。粒子一旦聚集到某个局部最优附近速度更新公式很难让它们挣脱出来除非你把惯性权重设得很激进但那样又笨重不收敛。我做30个工件的测试算例时PSO经常在100代前就锁死在一个次优解后续迭代全部浪费。鲸鱼算法恰好在这两者之间找到了平衡螺旋更新提供了类似GA局部微调的效果而|A|≥1时的随机搜索提供了类似变异算子跳出局部最优的能力。更重要的是它的参数极少——核心参数只有一个收敛因子a比GA和PSO都省心。实际测试中在处理50×20规模的Talliard基准算例时鲸鱼算法的收敛速度略慢于PSO但最终解质量平均高出3%到5%且多次独立运行的方差更小。这个稳字对生产调度来说比单次跑出最优解更重要。3. 从单目标到多目标非支配排序与帕累托存档如果你只是想要一个最短完工时间的方案跑单目标优化就够了。但前面我强调过生产现场真正的诉求一定是多重的。这里就要引入多目标优化的核心概念——帕累托支配关系。3.1 为什么单目标的最优解在生产现场不好用单一目标优化的结果是把所有资源都集中向这个目标倾斜。我做过的案例里有个非常典型的现象一个3台机器、20个工件的算例只优化完工时间时得到的最优方案最大完工时间确实很短但机器负载极不均衡——第一台机器的总加工时间是380分钟第二台是410分钟第三台只有160分钟。车间主任看到这种方案直接摇头第三台机器的操作工在那待一天生产报表怎么填反过来如果你把机器负载均衡也列为目标单目标加权求和比如0.6×完工时间0.4×负载方差确实也能得到一个折中解但问题在于权重怎么定没有哪个车间主任能明确告诉你完工时间的重要程度是机器负载的1.5倍。加权法本质上是在优化之前就把你的偏好固化下来而排除出来的解无法覆盖整个帕累托前沿的全貌。多目标优化的思路不一样。它不问你权重而是直接给你一整套互不支配的备选方案。方案A说我愿意多等20分钟完工换取三台机器负载基本均衡方案B说能不能先把完工时间压到最短哪怕一台机器闲着两个方案都是数学意义上的最优解只是偏好不同。计划员看方案时可以根据当天的急单情况、设备状态做选择这才是调度的实际场景。3.2 帕累托前沿的提取逻辑支配、非支配与拥挤度距离帕累托支配的定义一句话就能说清楚如果解X在所有目标函数上都不差于解Y且至少在一个目标上严格优于Y那么X支配Y。举个例子方案X的完工时间是310分钟、机器负载方差是2200方案Y的完工时间是340分钟、负载方差是1800。在完工时间上XY但在负载方差上XY那么X和Y互不支配——它们各占一个目标上的优势。集合中所有不被其他解支配的解构成第一层帕累托前沿Rank 1然后去掉它们之后继续分层就得到Rank 2、Rank 3等后续层。提取非支配解集的实现方法不复杂但代码上有几个细节要注意。假设每头鲸鱼有k个目标函数值种群规模为N那么进行一次完整的分层排序的伪逻辑如下% 初始化每个个体的被支配数量 for i 1:N for j 1:N if i j, continue; end % 逐个比较目标函数 dominate_i false; dominate_j false; for t 1:k if f(i,t) f(j,t), dominate_i true; end if f(i,t) f(j,t), dominate_j true; end end if dominate_i ~dominate_j % i支配jj的被支配数1 elseif dominate_j ~dominate_i % j支配ii的被支配数1 end end end这个双重循环的复杂度是O(N²)对100以内的种群规模完全可以接受。但还有一个关键点是拥挤度距离的计算。拥挤度距离衡量的是某个解在目标空间中周围解的密集程度——距离越大说明这个解所在的区域越空旷越值得保留因为它能维持整个帕累托前沿的均匀分布。计算拥挤度的方法是按每个目标函数分别排序然后取相邻两个解的目标函数值之差做归一化累加。边界点的拥挤度赋为无穷大保证边界解一定能进入下一代。这一步在遗传算法NSGA-II中是被反复验证过的机制搬到鲸鱼算法里一样成立。3.3 外部存档更新多目标鲸鱼算法的完整流程拼图单目标鲸鱼算法在每一轮迭代中只需要记住一个全局最优解多目标就没这么简单了——非支配解不止一个而且每轮迭代都会产生新的候选解。所以多目标鲸鱼算法MOWOA的做法是引入一个外部存档External Archive也就是一个专门存放当前最优帕累托前沿解的容器。整个算法的迭代流程可以概括成八个步骤初始化随机生成N头鲸鱼的位置向量计算各自的目标函数值提取当前非支配解集存入外部存档从外部存档中随机选一个解作为当前引领解相当于单目标里的全局最优对每个个体执行鲸鱼算法的三种位置更新策略包围/螺旋/随机搜索得到新位置将新位置的连续向量通过SPV规则映射为调度序列计算目标函数值合并原来的种群和新生成的种群做非支配排序拥挤度排序选出前N个作为下一代种群更新外部存档把新种群里的Rank 1解加入存档然后剔除被支配的旧存档解如果存档容量超过预设上限按拥挤度距离删除最拥挤位置的解然后回到步骤3继续迭代。步骤3这里有个值得注意的细节引领解是从存档里随机选的不是固定选第一个解。如果每次都选同一个解作为引领所有鲸鱼都会朝那个方向聚集帕累托前沿的多样性会被破坏。随机选择则能让群体不定期分散到前沿的不同区域这个操作成本极低但对最终解的分布均匀性帮助很大。外部存档容量设多少合理我常用100也就是最后给用户输出不超过100个备选调度方案。容量太大时拥挤度距离的计算开销会明显增长且方案太多对实际选型是负担容量太小比如10个前沿覆盖又不够完整。折中下来100是个在效果和效率上都比较平衡的数字。4. MATLAB实现从初始化到帕累托前沿输出的关键代码下面进入到真正能落地的环节。我按自己实验时的习惯把代码分成几个模块来讲。每个模块都是可以单独测试的函数最后统一在主脚本里串联。完整代码我会展示核心部分其余按注释补全。4.1 数据结构与参数配置我的做法是工件加工时间矩阵用标准的二维数组p(n,m)存储种群用一个结构体数组来表示每个个体包含三个字段——位置向量pos连续值、调度序列seq离散值通过SPV映射得到、目标函数值obj一个行向量。主脚本的配置段如下%% 问题数据加载与参数配置 % p_matrix: n个工件 x m台机器的加工时间矩阵由基准算例或实际数据生成 n 30; % 工件数量 m 10; % 机器数量 load(p_matrix.mat); % 假设已有加工时间矩阵 % 算法参数 N 80; % 种群规模 MaxIter 500; % 最大迭代次数 ArchiveSize 100; % 外部存档容量 b 1; % 螺旋形状常数 ObjNum 2; % 目标函数个数完工时间、总拖期或机器负载方差 % 初始化种群 pop struct(pos, [], seq, [], obj, []); for i 1:N pop(i).pos rand(1, n); % 连续位置向量取值范围(0,1) pop(i).seq SPV_mapping(pop(i).pos); % 映射为工件排列 pop(i).obj evaluate(pop(i).seq, p_matrix); % 计算目标函数 end加工时间矩阵怎么来如果你没有实际数据可以用标准随机生成函数做仿真测试比如randi([5, 50], n, m)生成一个加工时间在5到50之间的实例。但正式做实验时建议直接下载Taillard基准算例集这是调度领域公认的测试集网上有现成的.mat文件可以加载这样你的结果可以和文献里的算法做横向对比。4.2 调度时间推进的核心函数甘特图计算逻辑的代码化这是整个程序里最重要的一段必须写对。evaluate函数接收一个调度序列和加工时间矩阵输出各项目标值。function [makespan, total_tardiness] evaluate(seq, p_matrix, due_dates) n length(seq); m size(p_matrix, 2); C zeros(n, m); % 第一台机器的第一个工件处理 C(1,1) p_matrix(seq(1), 1); % 第一台机器的剩余工件处理只需等前一个工件完成 for k 2:n C(k,1) C(k-1,1) p_matrix(seq(k), 1); end % 第一工件的剩余机器处理只需等自己前一台机器完成 for j 2:m C(1,j) C(1,j-1) p_matrix(seq(1), j); end % 双循环推进关键递推 for k 2:n for j 2:m C(k,j) max(C(k,j-1), C(k-1,j)) p_matrix(seq(k), j); end end makespan C(n,m); total_tardiness sum(max(C(:,m) - due_dates, 0)); end这里有个新手很容易忽略的细节C(:,m)是所有工件在最后一台机器上的完工时间要算拖期必须有每个工件的交期due_dates作为输入。如果没有交期数据第二个目标可以改成机器总负载方差计算公式是先算每台机器的总加工时间再求这些总加工时间的方差。这个目标不需要额外数据对生产均衡性非常敏感。实际使用时两个目标是哪两个完全看你车间的痛点。我建议第一目标固定为最大完工时间第二个目标根据场景在总拖期和机器负载方差之间选。下面演示的案例里我选的是完工时间机器负载方差避免交期数据不准确带来的干扰。4.3 SPV映射工具函数这个函数短小精悍但每一个调度类鲸鱼算法都离不开它。function seq SPV_mapping(pos) [~, idx] sort(pos, ascend); % 按位置值升序排序输出原始索引 seq idx; % 序号即为工件加工顺序 end没别的话说就是这么简单。sort函数返回两个值第一个值是排序后的数组本身我们用不上第二个值idx是每个排序位对应的原始坐标。那个~符号就是用来丢弃第一个返回值。有人可能会问为什么不直接对调度序列做交叉变异非要绕道连续空间我的理解是绕道连续空间的好处在于鲸鱼算法的三种位置更新机制——特别是螺旋更新和随机搜索——可以直接无缝套用不需要重新设计离散算子。调度序列的交叉算子需要小心翼翼地处理重复元素复杂度高效果还不一定好。SPV映射让这个问题彻底消失这就是为什么鲸鱼算法的离散化改造比遗传算法更省心。4.4 位置更新三策略的代码分支实现位置更新模块是算法的核心三种策略对应三个代码分支。function pop_new whale_update(pop, leader_pos, iter, MaxIter, b) N length(pop); n length(pop(1).pos); a 2 - 2 * iter / MaxIter; % 收敛因子线性递减 pop_new pop; for i 1:N r1 rand; r2 rand; r3 rand; A 2*a*r1 - a; C 2*r2; p rand; if p 0.5 if abs(A) 1 % 包围猎物朝引领解收缩 A_vec A .* ones(1, n); C_vec C .* ones(1, n); D abs(C_vec .* leader_pos - pop(i).pos); pop_new(i).pos leader_pos - A_vec .* D; else % 随机搜索随机挑一个个体当临时猎物 rand_idx randi(N); target pop(rand_idx).pos; D abs(C .* target - pop(i).pos); pop_new(i).pos target - A .* D; end else % 气泡网螺旋更新 l -1 2*rand; D_spiral abs(leader_pos - pop(i).pos); pop_new(i).pos D_spiral .* exp(b*l) .* cos(2*pi*l) leader_pos; end % 越界处理位置向量的元素保持在(0,1)区间内 pop_new(i).pos max(0, min(1, pop_new(i).pos)); pop_new(i).seq SPV_mapping(pop_new(i).pos); pop_new(i).obj evaluate(pop_new(i).seq, ...); end end这段代码里有几个只有实际跑过才会注意到的点。首先是A和C的处理——在原始论文里A和C就是普通的标量随机值但如果你直接用标量和向量做乘法MATLAB的隐式扩展对于较新版本没问题老版本会直接报错。我这里用了ones(1,n)显式广播兼容性最好。其次是p0.5这个50%概率的分支选择。有文献建议把p的阈值调成0.7——螺旋更新比例更高局部开发更强——但我在调度问题上测试过不同值发现0.5就挺好因为调度问题需要保持足够的勘探能力螺旋更新占比过高反而容易陷入局部最优。这个结论可能不适用于连续优化问题但在PFSP上0.5是可信的经验值。还有个细节是越界处理。位置向量超出[0,1]区间时直接截断到边界。曾经见过有人用反射策略超出部分按边界对称反弹效果反而不好因为反射会把位置向量的相对大小关系打乱SPV映射出的顺序会偏离原来的搜索方向。简单截断反而是最优解。4.5 非支配排序与存档更新的实现思路非支配排序的完整代码很长这里我只说核心逻辑框架因为完整实现网上有很多现成的NSGA-II代码可以改。function [rank, crowd_dist] non_dominated_sort(pop) N length(pop); % 第一步计算所有个体之间的支配关系得到被支配次数和支配列表 for i 1:N for j 1:N if dominates(pop(i).obj, pop(j).obj) % i支配j end end end % 第二步分层被支配次数为0的个体进第一层 % 第三步删除第一层后更新剩余个体的被支配次数继续分层 % 第四步对同一层的个体按拥挤度距离排序 end外部存档更新的逻辑要和分层配合但实现上有个更轻量的替代方案——合并存档和当前种群一起做非支配排序只保留Rank 1解。这样每轮迭代的存档里全是当前最优的非支配解没有历史遗留被支配的旧解。如果Rank 1解超过存档容量用拥挤度排序删除最拥挤的个体。这种策略的优点是实现简单、逻辑清晰缺点是存档的质量波动大——如果某一轮突然冒出一个极端解可能把原来的好解区域挤掉一半。但评测下来对调度问题的影响不大因为我最后输出的是整个Rank 1集合而不是单独某几个解。4.6 数据输出与甘特图可视化的经验算法跑完后把存档里的调度序列逐条解析画出甘特图这是给车间领导演示时的重要环节。MATLAB甘特图的基本画法是用barh函数水平堆叠每个工件的加工时间段不同机器分行显示。figure; hold on; for k 1:n for j 1:m start_time C_before_defined(k,j); rectangle(Position, [start_time, j-0.4, p_matrix(seq(k),j), 0.8], ... FaceColor, color_map(k)); text(start_time p_matrix(seq(k),j)/2, j, num2str(seq(k)), ... HorizontalAlignment, center); end end画甘特图要注意的是起始时间的提取——我的习惯是把递推公式里每一步的C(k,j-1)和C(k-1,j)比较结果存下来分别保存每个工序的开工时间比事后反向推测要省力得多。甘特图上的颜色按工件编号区分每个工件在一条水平线上保持同色这样一眼就能看出工件在机器间的流转路径。5. 实测与调参基准算例上的收敛表现和典型坑所有代码写完不能只满足于能跑必须放到基准算例上检验效果。我用两组经典算例做了测试一组是Carlier系列的小规模问题比如Car1到Car8工件数从7到11另一组是Taillard系列的50×20中等规模算例。5.1 基准算例验证对照已知最优解判断代码正确性测试流程很简单把已知最优解的目标值从文献或公开数据库拿到和我的算法结果做差距对比。跑Car17个工件、5台机器时算法在第50代左右就找到了已知最优解703说明编码和递推公式没问题。这个验证步骤特别重要——很多人在编码阶段就写错了递推公式结果算法再怎么调优都是在一个错误的适应度函数上瞎忙活。先用小规模算例验证适应度函数正确再放大规模测算法性能这个顺序不能颠倒。在Taillard 50×20实例上种群规模80、迭代500次的配置下单次运行耗时大约35秒MATLAB R2021b普通i5笔记本。最终得到的完工时间平均比已知最优解高出约4.5%。这个数字对于多目标算法是可以接受的因为单目标算法通常也只能做到2%至3%的偏差而且我们同时还在优化第二个目标本质上是在用目标冲突换解决方案的多样性。对照实验我也做了同样条件下把算法改成单目标只优化完工时间最终偏差约3.1%。多目标版比单目标版多出1.4个百分点的完工时间偏差但换来了整个帕累托前沿的多目标选择空间这笔账在生产决策里是划算的。5.2 参数敏感性与收敛性分析的实测数据我分别测了以下几组参数对结果的影响结论都以最大完工时间尽可能接近已知最优解为评判标准参数测试值结论种群规模40 / 80 / 12080和120的最终结果差距在1%以内40则明显变差建议不低于60最大迭代次数200 / 500 / 1000200代时还在收敛500代基本稳定1000代几乎没有额外改善螺旋形状系数b0.5 / 1 / 1.5b1时结果最好过小螺旋过紧容易重复探索过大则跳跃太远位置向量范围[0,1] / [0,10]对SPV排序无影响因为只有相对大小有意义范围不影响结果这个实验结果印证了一件事多目标鲸鱼算法的参数敏感性远低于遗传算法。遗传算法的交叉概率和变异概率需要根据问题规模反复调整鲸鱼算法只需要保证种群够大、迭代够多参数怎么设都差不多。这对工程实践是很大的优势少了一个需要玄学调参的环节。收敛性分析除了看最终结果还要关注每代存档Rank 1解的数量变化。我观察到前50代存档数量会从个位数迅速增长到80到100这说明种群在快速发现新的非支配区域后期存档数量基本稳定但解的分布会越来越均匀。如果存档数量长期不增长甚至下降大概率是收敛因子a递减过快可以改成非线性递减比如余弦递减来缓解。5.3 我踩过的几个典型坑每个都能让结果跑偏第一个坑是递推公式的行列顺序写反。刚开始实现时我把C(k,j-1)和C(k-1,j)写成了C(k-1,j)和C(k,j-1)看似对称关系没变但循环的索引顺序出了偏差导致计算出的完工时间在部分算例上偏小。这个坑特别隐蔽因为对小规模算例误差很小只有放到40工件以上才明显。排查方法是把生成的甘特图用手工时间线逐项核对用最短的事件链条验证。第二个坑是目标函数值数量级不一致导致支配关系判断失效。假设完工时间的量级在1000分钟机器负载方差的量级在10000两个目标对比时方差大数值永远主导支配判断——完工时间优化得再好在支配关系里也激不起水花。这个必须做归一化处理。我的做法是对每个目标函数分别做min-max归一化后再参与支配判断当然最后输出还是用原始值。第三个坑是关于存档容量设置的误区。我一开始把存档设成15觉得方案少一点好选结果发现Rank 1解被拥挤度删除时经常把前沿两端的极端解挤掉只剩中间一堆相似方案。后来把存档提到100前沿覆盖全面多了。这个经验让我明白帕累托前沿的价值在于多样性容量必须够大否则等于把多目标优化又变回单目标。第四个坑可能只在新版MATLAB会遇到——隐式扩展和循环内的变量重载。位置更新循环里如果你不小心把A同时用作标量和向量变量某些时候会触发隐式扩展的正确行为某些时候维度不匹配直接报错。我的建议是所有系数统一用标量乘以ones(1,n)扩展后再参与向量运算这样不同版本之间都稳定。6. 多目标鲸鱼算法的扩展思路从流水车间到更多生产场景流动性车间Flow Shop只是生产调度家族里最规整的一个分支实际工厂里的约束远比它复杂。这个算法框架本身有很强的迁移能力下面这几个方向是我自己尝试过或看别人做过的都能在现有代码上直接扩展。6.1 混合流水车间增加并行机与层级间的等待约束混合流水车间Hybrid Flow Shop比标准流水车间多了一个特征每一道工序有多台并行机可选。也就是说工件经过第一道工序时可以选择机器A或机器B选择哪台机器会影响排队时间和总完工时间。这个问题的核心挑战是既要决定所有工件在每道工序上的先后顺序又要决定每个工件具体在哪台并行机上加工。在这个场景下鲸鱼算法的连续位置向量可以拆成两个块前n维用SPV映射决定工序顺序后n维用另一套映射规则比如把连续空间划分成K个区间对应K台并行机决定机器分配。两个块在每次位置更新时同时被修改自然保证了顺序变量和分配变量是联合搜索的不会出现顺序优化了但机器分配没跟上导致的性能瓶颈。我用这个方法试过一条模拟的半导体晶圆测试车间约束条件里还有各工序间的等待时间上限——半成品不能放置太久否则氧化。多目标里除了完工时间还加了一个超等待时间总惩罚算法输出的一组方案里能清晰地看到完工时间和等待惩罚之间的此消彼长关系。6.2 换产时间与资源消耗第三个目标的引入实际车间里换产调整时间是绕不开的。工件A加工完换工件B模具、刀具、参数都得重新调整这段停机时间随着工件顺序的不同可长可短。这种问题叫SDSTSequence Dependent Setup Time顺序依赖换产时间调度比标准PFSP复杂在任何两个工件的先后顺序都会附加上一个换产成本。多目标鲸鱼算法处理这个问题几乎不用改框架只需要改目标函数——在最大完工时间里加入换产时间或者在总拖期里加入换产导致的延迟影响。我在一个模拟钣金折弯车间的案例里增加了换产时间数据算法依然能正常收敛只是收敛速度稍慢。当时把目标定为最小化完工时间、最小化总换产时间、最小化最大机器负载三个目标同时优化最终输出了一套方案在三个目标之间达到了比人工排产明显更优的权衡。6.3 动态扰动下的重调度紧急插单与机器故障调度领域有个残酷的现实排产优化的再好生产现场一个急单插进来、一台机床突然报警停机所有计划都要推翻重来。动态重调度是一个倍受关注的研究方向而多目标鲸鱼算法天然适合做重调度因为它的多目标特性可以同时考虑新计划的完工时间和相对原计划的偏差程度。后者是工厂非常看重的指标——扰动越小车间乱的程度越低。具体做法是当扰动发生时用当前时刻的车间状态哪些工件已经完成、哪些机器正在加工什么作为新问题的初始条件重新初始化种群并运行算法把方案与初始顺序的差异程度作为第二个目标函数参与优化。鲸鱼算法的快速收敛能力在这里就体现出了优势——重调度要求快速给出新方案不能像初始排产那样跑几百代所以我把迭代次数压缩到100代。实测下来紧急插单场景下算法在10秒内就能给出一个完工时间增量不大、且和原计划差异尽可能小的新方案这个速度完全满足车间级的响应需求。当然动态重调度是一个非常大的话题涉及不确定性与鲁棒优化远不是一段代码能覆盖的。但从工具角度说多目标框架给了调度员从多个备选重调度方案中挑选的余地这个价值比单一挂着最优两个字的方案要高得多。7. 写在最后算法能不能用取决于车间里的人怎么看有一次给工厂做项目汇报我放了一大屏帕累托前沿图十几种颜色的散点台下的车间主任沉默了一会儿问了一句我就想知道明天早上8点第一台机床先加工哪个工件这个问题让我想了很久。科研人员关心的是收敛性、分布性、超体积指标但车间要的是可执行的方案。多目标算法的最大优势在于给出选择但如果使用者面对100个方案不知所措这个优势反而变成了负担。我的解决方案是程序最终只输出三到五个差异最大的方案分别标注它们的性格——这个方案最快这个方案负载最均衡这个方案拖期风险最小。计划员不需要理解帕累托前沿只需要看到差异化的选择。用MATLAB做生产调度优化的全套代码包括SPV映射、递推计算、非支配排序、外部存档维护和甘特图绘制我按模块化思路组织在几个函数文件里方便按需替换和扩展。你在跑通这些代码之后第一个要做的不是急着调参数而是拿你们车间真实的历史订单数据试一遍输入加工时间矩阵和各工件的交期跑出来看看方案和老师傅的排产表相比好在哪、差在哪。只有拿实际数据验证过的算法才是真正解决你车间问题的算法。最后多说一句动手方面的建议不要直接下载别人的完整代码就跑。你把适应度函数的递推公式自己推一遍把SPV映射自己写一遍把存档更新的逻辑自己理一遍整个过程下来你不仅会算这道题还会理解每一行代码背后的决策逻辑。这样当车间里的真实约束加进来时你才能第一时间知道该改哪一处、不能动哪一处。调度问题的乐趣和挑战从来不只是跑出一个漂亮的前沿图而是让这张图真正变成车间里按部就班运转的一天。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑