作业车间调度是制造系统里最经典的优化问题之一。这些年我一直在帮工厂做排产相关的算法落地,最深的体会是:调度算法从理论到能用,中间隔着一条叫"状态表达"的河。模型选得再花哨,超参调得再好,状态特征设计不到位,整个深度强化学习方案就是纸上谈兵。这篇我用实际项目的视角,系统梳理一下作业车间调度中深度强化学习的状态特征设计框架,把踩过的坑、验证过的方案和可以直接抄作业的代码结构都摊开讲。
先说清楚这篇文章适合谁:正在做车间调度、柔性产线排程、制造执行系统(MES)里智能决策模块的算法工程师;以及想入深度强化学习落地制造场景的研究生。如果你对作业车间调度问题(Job Shop Scheduling Problem, JSP)有基本概念,读起来会非常顺畅。如果完全没接触过,我建议先稍微了解一下工序、机器、完工时间这些基础概念再来看,不然有些细节会接不上。
1. 项目目标与问题边界
1.1 我们在解决什么问题
作业车间调度的核心难点,一句话说就是:给定一批工件,每个工件有固定的多道工序,每道工序必须按顺序在某台特定机器(或某组机器)上加工,怎么安排加工顺序和开工时间,才能让某个目标最优?最常见的目标是最小化最大完工时间(Makespan),也有工厂更关心延迟率、设备利用率或者能耗。
传统方法比如遗传算法、禁忌搜索、分支定界,在小规模问题上表现不错。但一旦订单数量上百个、工序上千道,这些方法的求解时间会指数级爆炸。深度强化学习(Deep Reinforcement Learning, DRL)天然适合这种场景:它把调度过程建模成马尔可夫决策过程(MDP),智能体每到一个决策点,看一眼当前车间的"快照",决定下一步做什么动作。只要状态特征设计得够好,训练出来的策略网络,一次前向推理就能给出非常好的调度方案,快、可扩展、还能针对不同实例做迁移。
1.2 为什么状态特征设计是成败的关键
我在做第一个调度DRL项目时,一开始把注意力全放在网络结构上,用了一堆Transformer、图注意力,效果却很一般。后来复盘发现:问题出在状态特征上。DRL的核心公式是状态-动作-奖励,状态是智能体做决策的唯一信息来源。如果特征本身含混、冗余、重复、缺失关键信号,再强的网络也学不出来。
打个比方:你让一个从没进过厨房的人做菜,他的感官接收信息的能力是有限的。如果他只看得到食材,看不到锅的温度,闻不到香味,听不到油爆的声音,那就算他有米其林大厨的头脑也白搭。状态特征就是智能体的感官,决定的不是它多聪明,而是它能感知到什么、感知得是否准确。
1.3 状态特征设计要满足的三个核心属性
我把多年的经验浓缩成三个词:信息完备性、结构规整性、训练稳定性。
信息完备性是指状态要包含足够的信息来支撑决策。对于作业车间调度,正在加工的机器状态、每个工件的工序进度、哪些设备空闲、哪些工件即将可用,这些不能漏。
结构规整性是指状态特征是固定维度的向量,或者说至少在一个batch内是规整的。因为深度神经网络的输入维度是固定的,你不能这次给100维、下次给120维。调研中发现,很多论文用图神经网络处理这类变长问题,但是工程落地时,如果你能通过精巧的固定维度特征设计,让网络在简单MLP或全连接网络上跑出效果,部署成本会低非常多。
训练稳定性是指特征的数值范围不能剧烈波动。状态特征里如果有一部分是几百、几千的大数值,另一部分是0到1的小数值,网络收敛会非常慢,甚至发散。特征归一化不是可选项,是必需项。
2. 状态特征体系设计方法论
2.1 第一层:工序级特征
工序级特征是所有特征中最贴近问题本质的一层,它回答的问题是"当前这些待排工序在什么状态下"。每个工件由多道工序构成,每道工序都有几个天然属性:
- 工序所属的工件编号(或者更准确地说,工件在当前排序中的序号)
- 这道工序是工件的第几道工序
- 该工序需要哪一台机器加工
- 在对应机器上的加工时间
- 该工序的剩余处理时间占总加工时间的比例
- 该工件本身的剩余工序数
在这一层,设计上最容易犯的毛病是:直接采用"工件是哪个"的ID作为特征,比如工件编号0到N-1。这种离散ID特征对神经网络来说是灾难,因为它没有任何语义上的顺序性或相似性。我见过有项目把工件ID one-hot编码后拼进状态,结果训练出的策略对训练集中的工件ID有严重的过拟合,换一个数量级的实例就彻底崩盘。
我更推荐的做法是:把工件的关键属性抽象成时间、比例这类连续量。比如"该工件剩余总加工时间"是一个好特征,"该工件当前工序序号占其总工序数的比例"是一个好特征,但"该工件是第7个工件"不是一个好特征。
2.2 第二层:机器级特征
机器级特征描述的是每台机器当前的工作负荷和忙碌情况。在决策时,我们需要知道:
- 每台机器上当前正在加工的工序剩余时间是多少
- 该机器的等待队列长度
- 该机器的总负载(所有被分配但未开工工序的加工时间之和)
- 该机器已累计完成的有效工作量
- 该机器未来一段时间内是否会被大量占用
这一层特征我经历过最深刻的教训是:只用"当前机器忙不忙"这种二值特征远远不够。调度中最关键的决策往往发生在机器从忙转闲的瞬间——这时候你要决定下一个放入机器的是哪个工件。如果你只知道机器"忙"或"闲",就无法评估"放入一个新工序会带来多大的挤占效应",决策质量会大幅度下降。
我最终采用的方案是:用一组滑动窗口的"未来负载曲线"作为机器特征。把时间轴分成若干等份,比如未来10个时间窗口,每个窗口内该机器的累计加工负荷就是一项特征。这样一个机器用10个数就能表达出它的忙碌节奏。
可以做一个小的对比,帮助读者直观理解:
| 设计思路 | 特征示例 | 表达能力 | 问题所在 |
|---|---|---|---|
| 基础版 | 是否空闲 | 弱 | 丢失了负载量信息 |
| 进阶版 | 队列长度+总负载 | 中 | 丢失了时间分布信息 |
| 推荐版 | 滑动窗口负载曲线 | 强 | 需要多调一个窗口数量参数 |
2.3 第三层:全局级特征
全局级特征描述的是整个车间的宏观状态,它的作用是让智能体"跳出局部,纵览全局"。这层特征包括:
- 当前系统时间(归一化后)
- 所有机器平均空闲率
- 所有工序的平均延迟程度
- 当前完成率(已完成工序占总工序的比例)
- 归一化剩余总工作量
为什么需要全局特征?因为纯粹的局部特征会让智能体陷入"短视"。比如两台机器,一台队列很长但是总负载很大,另一台队列短但是即将空闲,只看局部特征,算法可能倾向于清空短队列,但全局最优解可能是优先处理长队列里最关键的瓶颈工序。
我在部署时发现,全局特征加入后,策略网络的决策质量有一个非常明显的跃升,尤其是在实例规模扩大时。这个现象后来想明白了:全局特征提供了对当前调度阶段的一种"定位感",网络可以从中判断现在处于调度早期的宽松期还是晚期的紧张期,从而选择合适的策略模式。
3. 核心特征编码实操
3.1 维度规整的处理办法
作业车间调度问题的难点还在于:不同实例的工件数量、机器数量都不同。如果每台机器都生成一份特征,那么机器数是10时特征维度是100,机器数是50时特征维度是500,网络很难弹性地处理。
我比较常用的办法有两种:
第一种是最大规模对齐法:在数据集构建阶段,统计所有实例的最大机器数和最大工序长度,每个实例的特征向量都按最大维度生成,不足的部分用0填充,再用一个Mask向量标明哪些位是有效的。这种方法简单、粗暴、有效,但缺点是特征维度会被最大实例拉得很高,小实例上有大量0填充,浪费计算。
第二种是特征聚合法:不直接对每台机器单独建特征,而按排名聚合。比如把机器的负载从高到低排序,取Top-K台的负载值,再取Bottom-K台的负载值,配合平均值、中位数、方差。这样不管机器总数是10还是200,只要K固定,特征维度就固定了。
我测试下来,两种方法各有长短。如果模型使用注意力机制或卷积,最大规模对齐法是优选,因为保留了完整的空间关系。但如果模型只是全连接网络(在部署时经常这样做),特征聚合法更稳定、更轻量。我实际落地时两种方法用了不同项目:给车间规模波动特别大的集团级排产平台用特征聚合;给单一工厂固定机群的排程模块用最大对齐。
3.2 时间相关特征的归一化策略
调度问题里的时间特征(加工时间、剩余时间、等待时间)有一个特点:倍数差异极大。一道工序可能只需要30秒,另一道可能要用8小时。如果直接把这些原始值塞进网络,特征空间会被大数值主导,小数值特征几乎学不到东西。
推荐的归一化策略是最大完工时间预估归一化:在所有工件静态数据已知的前提下,我们可以用简单的启发式方法(比如最短加工时间优先规则跑一个快速仿真)得到一个参考最大完工时间T_ref,然后把所有时间类特征除以T_ref。这样让所有时间特征落在0到1区间附近,且保留了绝对时间的变化信息。
这一步我强烈建议在数据管道里做,而不要在损失函数里做。把归一化和特征提取绑定成一个模块,训练和推理共用同一套代码,可以避免最大的坑:推理和训练时用了不同的归一化基准,导致模型完全失效。
我踩过这个坑,具体情况是这样的:训练时用的归一化基准是所有训练实例的全局最大时间,上线测试时某个新实例恰好有更长的大工件,结果模型输出的调度方案在完工时间上莫名其妙翻了倍。后来把归一化基准改成每批实例的启发式预估T_ref,问题才彻底消失。
3.3 Mask特征与决策边界处理
在作业车间调度中,不是所有工件在任何时候都能被放上机器。一个工件必须等它的前道工序完成才能开下一道工序,一台机器同一时间只能加工一道工序。如果特征里没有Mask信息,智能体可能输出一个非法动作,把不该放的工序放上去,或者在她还没准备好时就强行开工。
模型设计时,我将状态特征拆成两段:全候选集特征和合法动作Mask。全候选集特征是全量的、不区分合法性的原始状态;合法动作Mask是一个变长二进制数组,标明哪些候选工序此刻是可以被选择进入对应机器的。训练时,损失函数里对非法动作的Q值直接做遮蔽处理,这样网络只需要在合法动作空间里做选择。
实际操作中有个小技巧:Mask不只是二值0/1,还可以加入"偏好引导"的半Mask。比如,如果某台机器当前空闲,且一个工件的下一道工序恰好可以在该机器上加工,那么这个动作的Mask值设为1.2(比常规1略大),相当于给网络一个轻微的倾向性先验。这样在不显著限制探索空间的前提下,加速了初始训练阶段的有效动作访问。
4. 网络结构衔接与训练细节
4.1 从特征向量到策略输出的完整链路
状态特征设计好了以后,还要考虑它和网络的衔接方式。我经常用的一套组合是这样的:
- 原始状态特征(工序级 + 机器级 + 全局级拼接成一维向量)
- 过一个两层全连接特征编码器,输出一个256维的隐向量
- 隐向量分别进入两个头:策略头(输出每个动作的概率或者Q值)和值函数头(输出当前状态的估计价值)
- 训练时用TD误差更新,决策时直接取Q值最大的合法动作
这套结构本质上是一个Dueling DQN的变体。选择Dueling结构的原因在于:作业车间调度的奖励很多时候非常稀疏,状态价值V(s)比动作优势A(s,a)的波动要平滑得多,把两者分开建模可以加速训练。
网络层数不需要很深,我测试过2层和6层的对比,效果差异不大,但推理速度差了将近3倍。在车间场景里,决策时延直接关系到产线等待时间,所以除非特殊必要,尽量不要堆深度。
4.2 奖励函数与状态特征的联动设计
很多人忽略了状态特征和奖励函数是唇亡齿寒的关系。我在设计奖励函数时,刻意让奖励信号和特征中包含的信息保持"对齐"。
举个具体例子:如果状态特征中包含"机器空闲时间占比",那么奖励函数里可以加一项惩罚,惩罚所有机器的平均空闲时间,这样智能体在优化奖励时,就会调整机器负载分布,而这个调整又会反过来体现为状态特征的变化,形成一个闭环的学习信号。
但如果状态特征里压根没有机器空闲信息,你却在奖励里要求最小化机器空闲,智能体就处于"盲飞"状态,它接收不到与奖励对应的信息,学习会非常缓慢且不鲁棒。
工业界常用的一种奖励组合:
r = -α × Makespan增量 - β × 平均机器空闲率增量 - γ × 超期工序惩罚
在实际项目中,α、β、γ的取值要边训练边调。我一般先从α主导开始,训练到策略基本收敛后,再逐步提高β和γ的权重,让模型从"只关注最终完工时间"过渡到"同时关注过程中间状态"。
4.3 实测经验:状态特征维度与训练收敛速度的关系
这块我做了不少对比实验,用同一份数据集(包含200个中等规模的作业车间实例)训练的观察结果可以整理成一个参考表:
| 特征维度 | 网络规模 | 收敛所需环境交互步数 | 最终评价Makespan偏差(vs. 最优解) |
|---|---|---|---|
| 32维 | 极小网络 | 150k | 12.4% |
| 128维 | 标准MLP | 100k | 6.7% |
| 512维 | 加深MLP | 80k | 5.8% |
| 1024维 | 大网络 | 140k | 6.2% |
有意思的地方在于,512维是表现最佳的,1024维反而出现了轻微的过拟合和训练不稳定。特征太多并不总是好事,尤其当部分特征是冗余或高度相关时,网络会花费额外的容量去"理解"这种相关性,而不是去学习真正的调度策略。
我的建议是:特征设计要像做减法一样。先用42个核心特征跑通链路,然后每增加一个特征,都要用A/B测试去验证它是否带来真实的决策质量提升。宁可特征少而精,不要大而全。
5. 实战踩坑与排查速查表
5.1 问题一:训练不收敛或震荡剧烈
表现:损失函数上下跳动,策略指标忽高忽低。
我排查过这个现象很多次,最可能的原因是状态特征中的数值范围没有处理好。车间调度的工序时间常常跨越几个数量级,一次归一化处理不到位,梯度更新就会出现"被大数值特征主导"的情况。
另一个原因经常出现在"最大规模对齐法"的特征填充上:大量0填充的Mask位如果没在损失函数里做遮蔽,模型的梯度会被无意义的0特征稀释。我刚开始就犯了这个错,后来把Mask乘到特征向量和损失上,问题立刻缓解了大半。
5.2 问题二:换一批实例就失效(泛化性差)
表现:训练集里效果很好,换一组新到的订单数据就明显退化。
泛化性差的根源往往在特征里带了"实例特有信息"。比如把工件ID、具体工件名称直接当成特征,或者把每一台机器的编号抽象成独立的槽位编号。正确的做法是只保留物理含义明确的信息:加工时间、所需机器类型、工序序号等。
我个人的一个补充经验是:训练数据集里故意混入不同规模、不同工序分布的实例,模拟现实中的订单波动。状态特征设计得当与否,在这种"乱序混合"的训练方式下会非常直观地暴露出来。如果换了数据分布就崩,先别急着加正则化和早停,回过头检查特征里有没有不该出现的实例特有变量。
5.3 问题三:调度动作"犹豫不决"
表现:网络输出的概率分布非常平坦,各动作Q值差距很小,调度决策像在随机猜测。
这种情况往往发生在训练早期,属于"模型还没找到规律"。但我发现一个很多人忽略的细节:状态特征里如果同时放了过多高度相关的特征(比如机器负载信息同时出现了三次,虽然形式略为不同),网络很容易陷在"伪相关性"里,迟迟无法找到真正的决策信号。
解决方式就是做特征筛选。计算各特征之间以及特征与最终最优动作标签(如果用最优解做监督辅助)的互信息,把互信息极低和特征间相关系数超过0.9的冗余特征剔除。
5.4 常见问题速查表
| 现象 | 主要原因 | 方向性解决建议 |
|---|---|---|
| 训练不收敛 | 特征数值范围未归一 | 用启发式预估T_ref做时间特征归一 |
| 换实例就崩 | 特征带实例ID信息 | 删除ID,保留物理量特征 |
| 收敛太慢 | 奖励与特征不对齐 | 检查奖励项是否都有对应特征支撑 |
| 策略平坦 | 特征冗余互信息低 | 做互信息筛选,减少相关特征 |
| 推理时延高 | 网络过深或特征过大 | 压层数到2-3层,特征压到512维以内 |
| 非法动作频出 | Mask未接入损失函数 | 确保动作Mask遮蔽到策略输出层 |
5.5 独家避坑:状态特征设计要"面向部署做冻结"
在很多项目里,状态特征设计在训练阶段是一套代码,到了部署阶段又因为要对接不同的MES系统而修改了一部分。这是非常危险的动作。我的建议是:特征提取函数的输入输出接口在项目启动初期就尽量固定,训练代码和推理解析代码共用同一个特征提取模块。
有一次我把特征提取函数做了接口重构,从"字典输入"改成"数据库查询输入",忘记同步更新训练端的构造方式,结果整个策略模型的决策逻辑完全错乱了,但报错信息非常隐蔽,排查了两天才发现是特征喂错了。从那以后,我对特征提取模块一律采用"单一函数、单一定义、双端共用"的铁律。
6. 落地效果评估与扩展方向思考
6.1 基准评估的方法与经验
状态特征设计得好不好,必须用数据说话。我通常分三层评估:
第一层是训练曲线的对比。观察不同特征设计方案下,训练回合逐步增加时累积奖励的变化曲线。正常情况应该是单调上升然后趋于平稳,如果在某个位置长期平台期,说明特征表达能力到了瓶颈。
第二层是离线指标对比。用标准测试实例集(比如作业车间调度公共基准里不同规模的数据),对比DRL方案的Makespan与最优解(或上界解)的差距。我要求自己的方案在不同规模下普遍保持在10%以内的差距,才算有实用价值。
第三层是仿真平滑度测试。用离散事件仿真器跑一遍调度方案,统计机器的平均利用率、工序平均等待时间等过程指标。这一步最容易暴露"看起来解好,实际产线跑不顺"的问题。比如生产的调度方案Makespan很低,但机器切换频繁,实际换模时间远超预估,总完工时间反而更高。
6.2 后续可以这样扩展
整套框架里最核心的可扩展点是面向大规模工件的图结构特征。如果工件数量非常大(比如超过500个),统一维度的全局特征表达会比较紧张,这时候可以考虑在状态特征中插入图神经网络的节点嵌入表达——每个工件是图上的一个节点,工序之间的先后关系就是图的边,用GNN层把节点信息做消息传递后,汇总成固定维度的特征向量输出给全连接网络。
另一种我非常看好的扩展方向是基于元学习的跨工厂迁移。状态特征设计得好的标准之一,是同一个特征提取模块可以适配不同工厂的差异。元学习则更进一步:让策略能够在看到几个示范实例后快速调整特征提取的权重,适应新产品结构和新机器配置。
不过,不管怎么扩展,我始终认为状态特征设计是DRL调度方案的根本。它决定了整个系统的信息天花板,网络结构只是在这个天花板下面做拟合。与其花大量精力去改造网络结构,不如踏踏实实把特征体系设计好。
我个人在实际项目中的体会是,状态特征设计这项工作,就像给一个资深调度员做岗前培训:你不能只告诉他"尽量用最短时间做完活",你得把他日常经验里判断优先级的所有信号都完整告诉他。机器在响、工件在排队、瓶颈在形成、紧急单在插队,这些信号汇总成他的判断力。DRL里的状态特征,就是把这种"判断力"从隐性经验变成显性数据的过程。只要特征框架搭对,后面的训练和部署都会顺利很多。