资讯动态

DQN车间排产实战:马尔可夫建模、奖励塑形与训练排查

发布时间:2026/10/26 2:07:27 来源:尧图企业网站定制
简介这套代码面向车间排产调度优化聚焦DQN算法在动态生产环境中的策略学习与调度决策适合强化学习学习者、运筹优化研究人员及工业工程师参考复现。资源包共3个Python文件整体约5KB体量轻巧却结构完整分别对应算法主体、gym风格调度环境建模和训练评估运行入口便于按模块研读与二次改造。已有848人学习具备一定关注度。代码围绕深度神经网络近似Q值、经验回放与目标网络等关键技术展开并通过状态空间、动作空间和奖励函数搭建排产模拟环境读者可据此理解如何将生产顺序优化转化为强化学习问题观察训练、评估与可视化流程借鉴环境封装和参数更新思路用于课程实验、课题原型或调度策略对比测试。1. 排产不是排序为什么 DQN 能把车间调度当成一场马尔可夫决策车间排产的教科书印象是「把一堆工序排个序」真上手做过的人都知道远不止于此。机床不能同时干两件活、同一工件的工序有硬性先后、换型要额外时间这些约束凑在一起解空间随工序数指数级膨胀是标准的 NP-hard 问题。更麻烦的是求解方式本身派工规则毫秒级出结果却只看眼前一步元启发式能出好解但每来一个新订单就得整锅重跑。深度强化学习里的 DQN 切进来的角度很反直觉——它不追求一次算出全局最优而是把「某台机床空出来的那一刻该选哪个工序」当成一个马尔可夫决策过程训练一个能在毫秒级给出决策的策略网络。这套建模约定怎么落到一个 gym 环境里、贝尔曼更新为什么能稳定下来、奖励怎么和最大完工时间对齐才是这份代码真正值得逐行读的地方。2. 把工序表塞进 gym状态、动作与转移的概率化建模强化学习的第一步几乎从不写在算法论文里而是写在一个env.py里怎么把一个静态的工艺路线表变成能一步步推进、能返回奖励、能被采样成批量的环境对象。这一步错了后面调任何超参都是在给错误的地基刷漆。2.1 状态编码从工艺路线表到定长张量原始数据长这样若干个工件每个工件有一串有序工序每道工序绑定一台机床和一个加工时长。它是变长的、带结构的而神经网络的输入必须是定长数值张量。常见的三种编码思路取舍差别很大。第一种是把整个车间压成一个全局向量比如每台机床的空闲时间、每个工件的剩余工作量维度是机床数 工件数。这种编码维度小、训练快缺点是网络看不到单道工序粒度的信息只能学出「大概往哪个方向偏」的粗策略遇到同型机床多、工序粒度细的车间就明显吃亏。第二种是析取图表示把每道工序当节点工件内的先后关系画成实线弧同一机床上待定先后关系的画成虚线弧整体交给图神经网络。表达力最强能把约束结构完整喂进模型代价是实现复杂度上一个量级训练也更吃数据。这份代码走的是第三条路每道工序一行特征构成(工序总数 N, 特征数 F)的矩阵再拼一个全局向量。每行通常这么填特征位含义为什么这么填剩余加工时长本工序的 duration让网络判断「先干短的还是先干长的」工序进度当前是工件的第几道 / 总道数靠后的工序拖尾会直接推高完工时间工件剩余工作量该工件后续所有工序时长之和近似关键路径的粗估所属机床相对负载该机床待处理总时长 / 全局平均给网络一个拥堵信号可开工标记工件前序是否已完成与动作掩码配合使用已完工标记是否为已完成工序防止网络把已完成工序当候选全局向量一般放当前仿真时刻、已完工工件数占比、机床平均利用率、剩余工序总数这几个标量。它们不针对任何具体工序但对判断「现在处于排产的早段还是收尾段」很关键——同一道长工序放在开工初期和收尾阶段代价完全不同。提示F一旦定死换一个特征就要重训。把特征抽取单独写成一个feature_fn(env) - np.ndarray函数改编码时只动这一处比散落在step里到处赋值省事得多。2.2 动作空间与合法性掩码排产里一半动作是废的动作空间有两种主流设计选错方向后面全是弯路。派工规则型动作是选一条规则比如最短加工时间优先、最长加工时间优先、最早完工优先、临界比优先。动作维度只有 4 到 6网络小、训练快但表达力被规则集锁死——网络最多只能学出「在某状态下该用哪条规则」学不出规则集合之外的决策。工序选择型动作是「从当前所有可开工工序里挑一个」动作维度等于工序总数。表达力完整代价是动作维度随问题规模线性增长而且绝大多数动作在任意时刻都是非法的——某道工序的前序没完成、或者它所在的机床正在加工别的活这个动作就不能选。这份代码用的是后者于是动作掩码成了不可绕开的一环。它的作用是在网络输出 Q 值之后、做 argmax 之前把所有非法动作的 Q 值强行压到负无穷。缺了这一步网络会很快学到一个「偷懒解」反复选择已完成的工序因为这些动作在多数实现里不改变状态网络误以为拿到了零成本的稳定回报。这个坑非常隐蔽因为损失函数照样在下降只是策略学歪了。import numpy as np import torch # 每个状态都要重新算一遍哪些工序此刻真的可以开工 def build_mask(env): mask np.zeros(env.n_ops, dtypenp.int8) for idx, (job_id, op_idx) in enumerate(env.op_index): # 已完工的工序不再作为候选 if env.op_done[idx]: continue # 工件的上一道工序必须已经结束本工序才具备开工资格 if op_idx 0 and not env.op_done[env.op_index.index((job_id, op_idx - 1))]: continue mask[idx] 1 return mask def masked_argmax(q_values: torch.Tensor, mask: torch.Tensor) - int: # 非法动作置为 -1e9 而不是 -inf避免 softmax/梯度里出现 NaN masked_q q_values.masked_fill(mask 0, -1e9) return int(masked_q.argmax().item())build_mask里判断的是「工件前序是否完成」注意这里刻意不检查机床是否空闲。原因是排产决策的时机点应当定在「工件层面有工序解锁」而不是「机床空出来」前者在一段仿真里产生的决策次数是固定的N次后者会让决策次数依赖于策略本身的表现episode 长度浮动批量采样和回报计算都会变得别扭。masked_argmax里用-1e9而不是-inf是血泪经验-inf一旦参与后续的gather或差分运算很容易冒出nan而nan在损失里会静默污染整个 batch 的参数。2.3 step 的契约一次决策如何推进整个车间step(action)是整个环境的心脏它要做四件事找到动作对应工序的最早可开工时刻、更新机床和工件的状态、算出这一跳的奖励、判断 episode 是否结束。def step(self, action): assert self.mask[action] 1, f收到非法动作 {action} job_id, op_idx self.op_index[action] mac, dur self.jobs[job_id][op_idx] # 开工时刻 max(机床空出来的时刻, 工件上一道工序完工的时刻) start max(self.mac_free[mac], self.job_ready[job_id]) end start dur # 推进状态机床下一次空闲、工件下一道可开工时刻 self.mac_free[mac] end self.job_ready[job_id] end self.op_done[action] True # 工件全部工序完成时记录它的完工时间 if op_idx len(self.jobs[job_id]) - 1: self.job_finish[job_id] end self.n_done_jobs 1 # 势函数奖励已完工工件完工时间均值的负值越接近终点越接近 0 phi_new -float(np.sum(self.job_finish)) / self.n_jobs reward (phi_new - self.phi_prev) * self.reward_scale self.phi_prev phi_new done self.n_done_jobs self.n_jobs if done: # 收尾时一次性把真实的 -C_max 补进奖励保证回报与目标同向 reward -float(max(self.job_finish)) * self.reward_scale self.mask build_mask(self) return self._obs(), reward, done, {makespan: max(self.job_finish) if done else None}start那一行是整个环境最本质的一行代码它同时表达了两种等待机床等工件、工件等机床。self.mac_free[mac]记的是这台机床处理完上一道活的时间self.job_ready[job_id]记的是这个工件上一道工序完工的时间取较晚者就是这道工序真正能开始的时刻。排产的「插入式」和「非插入式」区别很多时候就体现在这里要不要额外检查机床空档——这份代码是非插入式的机床一旦开工会走完整段时长实现简单、也更贴合多数离散制造现场的实际约束。reward_scale这个参数不是为了好看它决定了 Q 值的量级。如果让奖励直接等于「负的完工时间增量」一个 500 道工序的算例里单步奖励可能到 -50折扣累积几十步后 Q 值上到几百配合较大的学习率非常容易发散。把奖励缩放到[-1, 0]附近网络输出的 Q 值天然落在合理区间学习率、梯度裁剪阈值都能用一套通用配置。收尾时补的那笔-C_max也值得留意势函数奖励在同策略下不改变最优解但它对「最后一台机床拖了多久」这件事的反应是迟钝的末尾显式加一次真实目标值能让网络对尾部决策更敏感。3. DQN 的贝尔曼骨架目标网络与经验回放究竟在压制什么环境搭好之后算法这一层看起来全是熟面孔Q 网络、目标网络、经验回放、ε-greedy。熟面孔不等于理解尤其在排产这种「episode 长、单步回报小、状态高度结构化」的场景下每个组件压制的具体问题都不一样。3.1 Q 值与 TD 误差一次决策的收益怎么算Q 函数回答的问题很具体在状态 s 下选动作 a从这个选择开始、按当前策略一直走到 episode 结束能拿到的折扣累计奖励期望是多少。写成式子就是贝尔曼最优方程Q*(s,a) r γ·max_a Q*(s,a)。问题在于等式右边也含未知的 Q*所以只能迭代去逼近。DQN 的做法是拿「一步真实奖励 下一状态的 bootstrap 估值」当作监督信号让网络去拟合它q_sa q_net(obs).gather(1, act.unsqueeze(1)).squeeze(1) with torch.no_grad(): q_next target_net(obs_next).squeeze(-1) # (B, N) q_next q_next.masked_fill(next_mask 0, -1e9) # 下一状态的非法动作不参与 max a_star q_next.argmax(dim1) # Double DQN用在线网选动作 q_bootstrap q_next.gather(1, a_star.unsqueeze(1)).squeeze(1) q_target reward gamma * (1.0 - done) * q_bootstrap loss F.smooth_l1_loss(q_sa, q_target)这里有两处和排产强相关的细节。一处是masked_fill出现在目标值的计算里而不仅仅在动作选择时。如果下一状态里存在非法动作而它们的 Q 值恰好被高估max就会优先选中这些不存在的动作把虚高的估值一路回传整个价值函数会被系统性抬高。在动作空间里非法比例越高的场景这个错误的放大倍数越大排产恰好就是这种场景。另一处是a_star用在线网络选出、再用目标网络取值这是 Double DQN 的写法。原始 DQN 用同一个网络既选动作又估价值max操作天然偏向噪声大的动作过估计是结构性的而非偶然的。换成 Double 之后选动作和估值分离过估计大幅缓解。在奖励信号本来就不算密集的排产任务里过估计的后果不是「收敛慢一点」而是网络长期偏好少数几个看起来「运气好」的动作策略多样性直接塌掉。F.smooth_l1_loss而非 MSE 也不是随手选的。排产里不同算例的完工时间量级差别很大落进回放池的样本里难免混着量级悬殊的 TD 误差Huber 损失在误差较大时退化成线性相当于自动做了一次梯度限幅这比手工调clip_grad_norm_更平滑。3.2 目标网络给追捕者一个慢半拍的靶子如果去掉目标网络让q_target也用在线网络算会立刻遇到一个问题监督信号的标签本身随参数更新而移动。每更新一次权重右侧目标值就跟着变网络在追一个永远在跑的目标这在函数逼近下极易发散尤其在特征之间存在强相关时。目标网络的做法是维护一份参数的旧副本隔若干个梯度步整体同步一次或者每一步只做一次小幅度软更新θ⁻ ← τθ (1-τ)θ⁻。这样在两次同步之间的窗口里目标值相对静止回归问题恢复成普通的监督学习形态。class QNet(nn.Module): 每个工序独立打分输出维度恒为 1天然支持变工位数 def __init__(self, feat_dim, glob_dim, hidden128): super().__init__() self.op_enc nn.Sequential( nn.Linear(feat_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), ) self.head nn.Sequential( nn.Linear(hidden glob_dim, hidden), nn.ReLU(), nn.Linear(hidden, 1), ) def forward(self, ops, glob): h self.op_enc(ops) # (B, N, H) g glob.unsqueeze(1).expand(-1, h.size(1), -1) # (B, N, G) return self.head(torch.cat([h, g], dim-1)) # (B, N, 1) def soft_update(online: nn.Module, target: nn.Module, tau: float 0.005): for p_o, p_t in zip(online.parameters(), target.parameters()): p_t.data.mul_(1.0 - tau).add_(p_o.data, alphatau)这个网络结构本身就是一处设计取舍。它不对「动作」做 one-hot 输入而是每道工序独立算一个分数输出维度恒为 1。好处是动作维度不再被写进网络结构——同一个网络只要喂不同的N就能处理 20 道工序和 200 道工序的算例。坏处是工序之间的横向比较完全交给最后的argmax网络本身不做任何 pair-wise 比较如果你的状态特征里缺少「相对优先级」信息它会表现得很平庸。相比之下把整个状态摊平成一维向量、直接输出N维 Q 值的写法在网络表达能力上略强但一旦换规模就得换网络。tau取值要结合 episode 长度看。排产 episode 动辄几百步梯度步数多、目标变化本来就慢软更新系数取0.005到0.01之间比较合适如果取 0.1目标网络几乎等于在线网络防发散的作用就被稀释了。3.3 经验回放打破样本之间天然的时序相关排产环境有个很讨厌的特性相邻状态高度相似。一次决策只推进一道工序前后两个状态在特征矩阵上可能只差几个数位而 DQN 用的是随机梯度下降连续喂这种强相关样本会让梯度方向在几步内高度一致网络表现为「朝一个方向猛冲然后被下一个完全不同的算例拉回来」。回放池解决的就是这个。把转移(s, a, r, s, done, mask, next_mask)存起来训练时均匀采样一个批量。这样一来同一个 batch 里既有 episode 开头机床空闲、加工时段和收尾阶段的样本梯度方向被平均掉稳定性明显好转。class ReplayBuffer: def __init__(self, capacity: int): self.capacity capacity self.buf [] self.ptr 0 def push(self, transition): if len(self.buf) self.capacity: self.buf.append(transition) # 未满时追加 else: self.buf[self.ptr] transition # 满了之后环形覆盖最老的样本 self.ptr (self.ptr 1) % self.capacity def sample(self, batch_size: int): idx np.random.randint(0, len(self.buf), sizebatch_size) batch [self.buf[i] for i in idx] return collate(batch) # 把变长的 mask 张量堆成定长见下文说明掩码必须跟状态一起存。这一点很容易漏训练时要用「当前动作的掩码」去核对q_sa与q_target的一致性用「下一状态的掩码」去算 bootstrap两个掩码都不是从状态张量里能反推出来的只能显式存储。漏存的后果是训练用的掩码和采样时的掩码对不上损失函数在非法动作上给出虚假的强梯度。collate这一步也不是免费的。如果不同算例的工序数不同把多个样本堆成 batch 需要 padding 到最大长度同时把 padding 位置的掩码置零否则网络会把这些位置当作合法候选去打分。工程上更省事的做法是固定一个 episode 只用单一算例批量内N相同跨算例的泛化交给「同一个网络跑不同算例」来验证而不是塞进同一个 batch。两种做法各有代价前者要求你在采样时按N分组后者要求网络对变长输入真正鲁棒。4. 奖励与完工时间的对齐稀疏目标怎么不把人饿死强化学习项目里最容易翻车的地方八成不在算法在奖励。排产的目标是让最大完工时间C_max尽可能小而这个值只有 episode 结束时才真正确定。如果严格按目标给奖励前面几百步全是 0只有最后一步有信号网络基本学不动——这就是稀疏奖励的经典困境。4.1 奖励塑形三种能用的做法做法一终点型。每步奖励 0结束时给-C_max。最忠实于目标但样本效率极低除非算例特别小否则不建议直接用。做法二增量型。每完成一道工序奖励-(end - start)或者-dur。信号变密了但它引导的是「让每道工序尽快完工」不等于「让整体尽早完工」。一个反例某道长工序提前做完了但它占用的机床恰好是另一条关键路径上等着的那台整体完工时间反而被拖长。增量型奖励会给出正向反馈把策略往错的方向带。做法三势函数型。定义Φ(s)为衡量「离结束还有多远」的标量令r Φ(s) - Φ(s)。这样构造的塑形奖励在理论上不改变最优策略——你给了网络一个更密的梯度但没让它有机会去「刷分」。这是这份代码选的路子。Φ的常见取法有两种一种是「已完成工序总加工时长 / 全局总加工时长」直观但忽略了关键路径另一种是「已完工工件的完工时间均值的负值」也就是上面step里那段。后者的好处是它对尾部工件尤其敏感——均值不会被少数几个早完工的工件拉低太多拖尾一旦发生Φ立刻变差。def compute_potential(env) - float: # 只有全部工序完成的工件才计入未完成工件按已完成部分折算 # 避免网络学会抢着把简单工件做完刷势函数 total, weight 0.0, 0.0 for j in range(env.n_jobs): if env.job_finish[j] is not None: total env.job_finish[j] * 1.0 weight 1.0 else: done_ratio env.job_progress[j] / len(env.jobs[j]) total env.job_ready[j] * done_ratio weight done_ratio return -(total / max(weight, 1e-6))如果只用「已完成工件」势函数在一个工件刚完工的瞬间会跳变网络会学出一种投机行为集中资源把一个工件从头做到尾迅速把势函数推上去然后再处理别的。加上未完成工件的折算项相当于把「部分完成」的价值也计进势函数投机空间明显收窄。这是个典型的「理论上是势函数工程上还得再拧一下」的例子。4.2 折扣因子与完工时间之间的隐藏耦合γ在小算例里常被当作可有可无的超参在排产里它其实是个业务参数。排产 episode 长度等于工序总数N。如果γ 0.99那么第N步的奖励传回第 1 步时权重是0.99^N。N 200时这个数约等于 0.134也就是说尾部决策对开头的影响力只剩一成多。网络会偏向「先把手头这些活干得漂亮点」对远期后果不敏感。γ 1.0可以避免这个衰减但会带来另一个问题Q 值量级随 episode 长度线性增长网络输出层的数值范围被拉大配合塑形奖励容易不稳。折中的常见做法是γ取0.98到0.995同时把奖励缩放到[-1, 0]让 Q 值总量落在几百以内。还有一个容易被忽略的细节γ与塑形奖励组合时Φ的尺度必须和原始奖励同量级。如果Φ输出的值域是[-5000, 0]而原始奖励在[-1, 0]那么塑形项会完全主导梯度网络实际上在拟合势函数而不是在优化完工时间。上面代码里乘的reward_scale本职就是干这个的。4.3 评估口径别拿训练曲线当效果训练奖励在涨不等价于排产效果在变好。至少有三个地方会让这两件事脱钩。第一训练时走的是 ε-greedy带随机探索评估必须关掉探索、纯贪心。用同一个带噪声的策略去算 makespan数字会天然偏差且不可复现。第二训练算例和评估算例要分开。训练集用固定的一批工序表评估用另一批同分布但不同随机种子的表才看得出泛化。如果两边是同一批模型很可能在记忆而不是在学习。第三必须和派工规则对比才有意义。任何排产算法的最低门槛是打赢「最短加工时间优先」这类朴素规则如果 DQN 的C_max只是打平甚至更差那它唯一的价值就是推理快而这个价值在工序数很少时又不足以抵消训练成本。评估表至少要有三行SPT 基线、DQN 贪心结果、一个近优解参考小规模算例可用精确求解器跑出来大规模可以用长时间运行的元启发式近似。指标为什么必须报mean makespan主指标但只看均值会被个别极端算例带偏std makespan波动大说明策略不稳上线就是定时炸弹gap vs SPT相对基线的百分比跨算例可比单次决策耗时DQN 的真正卖点通常在毫秒级训练 / 推理的工序数上限网络能跑到多大规模决定适用范围5. 训练排查DQN 在排产环境里翻车的五个高频现场下面这五条基本覆盖了从环境写完到模型勉强能用这段时间里八成会撞上的问题。每条按「看到什么」「为什么」「怎么改」组织。5.1 现象奖励曲线纹丝不动Q 值却一路疯涨原因非法动作没有在目标值计算里被屏蔽。下一状态里那些本不该被选的动作 Q 值偏高max每次都挑它们虚高的估值被反复回传价值函数整体被抬起来和真实回报脱钩。因为损失函数是在拟合这个虚高目标loss 反而看起来在下降极其误导。解决检查q_target的计算路径确保masked_fill作用在q_next上、且在argmax和gather之前。同时确认存进回放池的next_mask和训练时用的next_mask是同一份不是重新算出来的。5.2 现象训练很早就不动了策略看起来像某条固定派工规则原因探索不足。ε 衰减太快或者 ε 的下限设得太低网络在还没见过足够多状态的时候就锁死了输出。另一种情况是回放池容量小于单轮训练样本量早期样本被反复重采样网络在几十个状态上过拟合。解决ε 从 1.0 线性衰减到 0.05衰减跨度覆盖至少十万步回放池容量设为单 episode 样本数的十倍以上。判断标准是看策略的动作分布熵如果熵在训练早期就掉到接近 0基本可以确定是探索出了问题。5.3 现象loss 突然跳到 nan之后再也恢复不了原因三个常见来源。一是masked_fill用了-inf参与gather后产生 nan二是奖励没缩放q_target数值过大Huber 损失的梯度在极端误差下依然能推爆参数三是学习率配得偏大加上软更新系数tau也偏大两处同时放松导致正反馈。解决掩码统一用-1e9奖励乘reward_scale缩到[-1, 0]加clip_grad_norm_(params, 10)兜底tau先降到0.005再逐步试。排查顺序建议从掩码开始因为它的表现最像「随机出现」的 nan。5.4 现象训练环境跑得很好换一批算例就崩原因状态特征里的某个量用了绝对刻度比如直接用「加工时长」的原始值而不同算例的时长分布差别很大。网络学到的是「时长超过 40 的工序优先」而不是「相对这台机床平均时长偏长的工序优先」一换分布就失效。解决所有时间类特征做相对化除以该算例的总加工时长或该机床的平均工序时长。工序数不同的算例还要注意掩码的 padding 位置不能参与打分。评估时固定随机种子跑至少 30 个算例取均值单个算例的结果没有参考价值。5.5 现象训练集上 makespan 比 SPT 还差原因塑形奖励和目标错位或者势函数尺度远大于原始奖励网络实际在优化势函数而不是完工时间。还有一种可能评估时忘了切到纯贪心ε 还开着随机动作直接毁掉 makespan。解决先打印一批 episode 里势函数项和终点项各自的累计贡献看哪一项占了绝对多数。如果塑形项占九成以上说明reward_scale没起作用或者Φ尺度失控。然后确认评估函数里没有random.random() eps这条分支。最后对比训练集上的贪心 makespan 与训练过程中的平均回报两者如果一个降一个不降说明奖励设计的引导方向确实偏了。6. 设计取舍DQN 到底该出现在排产系统的哪一层把 DQN 用在排产上最需要想清楚的不是网络结构是它在这个系统里的位置。DQN 属于 value-based 方法天然只处理离散动作而排产动作本来就是离散的这一点对得上。但它的 Q 值是对每个动作单独估的动作数随工序数线性增长一旦工序数上千输出层的计算量和每个动作分到的样本量都会成问题。这也是为什么代码里把网络写成「每道工序独立打分、输出恒为 1」——它绕开了固定动作维度的限制但没绕开「动作越多、每个动作的样本越稀」这个根本矛盾。对比一下另外两条路。策略梯度方法直接输出动作分布天然支持变长动作空间配合指针网络能处理工序数不固定的算例代价是方差大、样本效率低于 DQN。派工规则零训练成本、完全可解释、在稳定生产环境下表现其实相当能打它的弱点是短视——每一步只看当前最紧的那台机床看不到三步之后的连锁反应。DQN 真正的价值区间在中间环境有扰动插单、设备故障、返工规则调不过来元启发式又来不及重算的时候一个毫秒级出决策、且能感知远期后果的策略就有意义了。比较务实的落地形态通常不是「DQN 全权决策」而是两级结构底层保留规则调度做安全兜底DQN 只在它的候选集上做重排序或者用规则生成的示范轨迹先做模仿学习预热再上强化学习微调。这样即使网络训崩了产线也不会停。验证方法我习惯固定成一套同一批算例训练集之外、同一个随机种子、每个方法跑 20 次取均值和标准差横向比 SPT、LPT、ECT 三条规则纵向比一个跑了很久的元启发式当作参考上界。判断一个 DQN 模型值不值得留看的不是它比规则好多少而是「好出来的那部分能不能覆盖训练和调参的成本」以及「换一批分布略有差异的算例之后这个优势还在不在」。后者掉得越快说明模型学到的越是某个特定分布的特征而不是排产这件事本身的规律。注意跨规模泛化一定要单独测。用 50 工序训出来的网络直接跑 200 工序即使网络结构支持变长输入表现也经常断崖式下跌这不是 bug是状态特征的相对化没做到位或者训练分布压根没覆盖这个规模。从那以后我每次动奖励函数或者状态特征都会强制先跑一遍「规则基线 小算例精确解」的对照确认改动之后 gap 是缩小而不是被势函数项掩盖了。这套动作比看训练曲线啰嗦但它能把「模型在学」和「模型在自欺」这两件事分开。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑