简介:面向车间排产调度研究者和工程实践者,这份Python代码以深度强化学习中的DQN算法求解生产排序问题,并在gym风格模拟环境中训练与评估调度策略,适合具备强化学习基础、希望将算法落地制造场景的中级读者。压缩包共3个Python文件,整体约5KB,分别覆盖DQN网络结构与参数更新、排产环境的状态动作奖励建模、训练评估运行流程,体量轻便但模块划分清晰。已有848人学习。读者可获得可运行的DQN核心实现,包括经验回放与目标网络等关键机制,也能基于可扩展的环境定义替换奖励、约束和动作空间,复现实验并对比不同排产策略,作为工业调度优化的算法原型与二次开发起点。
1. 别急着 pip install:这套 DQN 排产环境翻车的从来不是算法
某开发者拿到这份「深度强化学习 DQN 车间排产调度优化算法 + gym 环境」的 Python 代码,习惯性pip install torch gym numpy一把梭。import 不报错,他以为成了;训练脚本一跑,reward 是一条直线,episode 长度固定不变,第二天早上看还是那张图。
翻日志才发现 gym 的 step 返回值早就变了,done永远不置真,智能体在一个状态上打转。深度强化学习的 DQN 本体其实是整套代码里最好复现的部分,难的是它脚下那层 gym 环境:Python 3.9 和 3.11 装出来的 gym 不是一套 API,torch 的 CPU 轮子和 CUDA 轮子混装会让.cuda()直接抛断言,numpy 2.x 又会把老代码里的类型别名清掉。
这套东西适合两类人:一类是想把 DQN 从倒立摆、CartPole 这类玩具环境挪到真实业务、拿车间排产练手的;另一类是手上已经有一版 SPT、EDD 调度规则,想验证学习型方法在 makespan 上到底能不能更省工期的人。装之前先把版本这件事定下来,比装完再回滚省事得多。
2. 环境定版:Python 3.9 + PyTorch 2.1 + gym 0.26 怎么搭才不打架
车间排产这类环境不像纯算法题,它对运行时很敏感:观测是个矩阵,动作是离散的工单编号,reward 由完工时间、延期惩罚、机器利用率几项加权拼出来。任何一层依赖版本对不上,最先崩的不是训练速度,而是你根本判断不了「学不会」到底是算法问题还是环境问题。所以第二步不是装,是定版。
2.1 先看对照表,把「能一起活」的组合挑出来
下面这套组合我反复用过,装完能直接跑到收敛,不用中途换轮子:
| 组件 | 推荐版本 | 可用区间 | 取舍说明 |
|---|---|---|---|
| Python | 3.9.x | 3.8 – 3.10 | 3.8 部分新语法不支持,3.11+ 有些科学计算轮子还没预编译 |
| PyTorch | 2.1.x | 1.13 – 2.3 | 2.x 的torch.load默认weights_only变了,老代码要注意 |
| gym | 0.26.2 | 0.21 / 0.26 | 0.21 和 0.26 的 reset/step 签名完全不同,二者只能选一 |
| numpy | 1.24.x | 1.21 – 1.26 | 2.x 删掉了一批类型别名,老代码容易踩 |
| pandas | 2.0.x | 1.3+ | 读工单表、工时表用,纯 numpy 也能替代 |
| matplotlib | 3.7.x | 3.5+ | 画训练曲线和甘特图,无显示环境要切 Agg 后端 |
几个选择我当时是这么权衡的:Python 钉 3.9 是因为它同时满足 gym 0.26 和 PyTorch 2.1 的轮子覆盖,向上到 3.10 也行,但 3.11 之后 numpy、torch 的预编译包对齐得没那么整齐。gym 选 0.26.2 而不是更新的 gymnasium,是因为这份 DQN 代码里的环境注册、reset(seed=...)写法是按 0.26 铺的;gymnasium 是 0.26 改名后的延续,API 基本一致,只是 import 名不同,你完全可以二选一,但不要两个都装——同时存在时import gym和import gymnasium会分别指向不同包,环境类注册不上就是这么来的。
PyTorch 我一般锁 2.1.x,不用最新版。原因很实际:DQN 的网络只有两三层全连接,跑车间排产这种几千步一轮的小环境,CPU 和 GPU 的差别远小于版本折腾的成本,2.1 的 API 稳定、报错信息清楚,够用。
2.2 conda 建独立环境:Python 版本要钉死,别用系统自带的
系统自带的 Python 别动,那不是给你做实验用的。用 conda 拉一个独立环境,把解释器版本写死:
# 创建独立环境,解释器锁 3.9 conda create -n drl-sched python=3.9 -y # 激活 conda activate drl-sched # 确认当前解释器确实是新环境里的那个 python -V # 期望输出 Python 3.9.x which python # 路径应包含 envs/drl-sched,而不是 /usr/bin/python这几行的作用分别是:conda create会在envs/下建一个独立目录,装多少包都不会污染系统;-y跳过交互确认,方便写进脚本批量执行;which python这一步别省,我见过太多「装完还是旧版本」的情况,本质是终端没激活成功或者 shell 初始化脚本把 PATH 覆盖回去了。如果which python指向的还不是envs/drl-sched,先执行conda init bash再重开终端,比手动改 PATH 稳妥。
环境名我习惯用drl-sched这种带业务含义的,同一台机器上还会并存drl-cartpole、rl-test,名字含糊的话两个月后你自己都分不清哪个能跑。
2.3 依赖写进 requirements.txt,别一条条 pip
一条条 pip 的问题是没法复现。第二个人照着你的聊天记录装,装出来的版本大概率不一样。落成文件,一次装完:
# 先把依赖清单写下来,版本号用 == 锁死 cat > requirements.txt <<'EOF' numpy==1.24.4 pandas==2.0.3 matplotlib==3.7.3 gym==0.26.2 torch==2.1.2 tqdm==4.66.1 EOF # 一次性安装,失败就整单回滚,不会装一半 pip install -r requirements.txt -i <你的内网或镜像源地址>==和>=的区别在这里很关键:DQN 代码对 numpy 的依赖主要集中在数组形状和 dtype 上,>=一旦跨到 2.x 就会出现np.float、np.bool找不到的报错,而这类报错往往藏在第三方包里,翻起来很烦。-i后面写你所在环境能通的源地址,内网机器就换成内网源,别硬连公网。
装完之后用pip list | grep -E "torch|gym|numpy"核一遍,三个版本号和你写的一致再往下走。
2.4 内网或离线机器:把轮子先下到本地
不少车间现场的机器是不通外网的,这种情况下pip install会卡在解析阶段。做法是在一台能联网的同架构机器上把包下载下来,再拷过去:
# 在联网机器上,把依赖和它们的依赖全部下载到 wheels 目录 pip download -r requirements.txt -d ./wheels --python-version 39 --platform manylinux2014_x86_64 --only-binary=:all: # 拷贝到离线机器后,从本地目录安装,不走网络 pip install --no-index --find-links=./wheels -r requirements.txt--python-version和--platform必须和你目标机器一致,否则下到的轮子型号不对,装的时候会提示is not a supported wheel on this platform。--only-binary=:all:是为了避免下到源码包——源码包在离线机上编译,缺编译器就直接失败了。这一步是血泪经验:我第一次做内网部署时没加这两个参数,拷过去 40 多个包,一半是 tar.gz,当场返工。
3. 装依赖与跑通 gym 车间环境:从 import 到 env.reset() 返回什么
依赖装完只是把材料备齐,能不能跑通取决于两件事:torch 有没有装对轮子,以及 gym 环境的 reset/step 签名有没有跟你手上的代码对齐。这一章把这两件事拆开做。
3.1 装 PyTorch:先确认有没有 CUDA,再决定装哪个轮子
别上来就装 CUDA 版。先看机器:
# 有 NVIDIA 卡且驱动正常,这条命令才会有输出 nvidia-smi有输出、且右上角显示的 CUDA Version 不低于 11.8,才可以考虑 GPU 轮子;命令不存在或者报错,就直接装 CPU 版,别折腾。CPU 版对这套排产环境完全够用:
# CPU 版:适合没有独显的机器,安装体积小,不会因为驱动问题报错 pip install torch==2.1.2 --index-url <你的源地址> # 或者 CUDA 12.1 版:确认过 nvidia-smi 之后再装 # pip install torch==2.1.2 --index-url <cu121 的源地址>装完立刻验证,这一步别跳:
import torch print(torch.__version__) # 期望 2.1.2 print(torch.cuda.is_available()) # CPU 机器上应为 False,属正常 x = torch.randn(2, 3) print(x.device) # 应为 cputorch.cuda.is_available()返回 False 不代表装坏了,只代表没有可用 GPU 或者装的是 CPU 轮子。真正要警惕的是反过来:装了 CUDA 轮子但驱动版本偏低,import 阶段就会抛The NVIDIA driver on your system is too old,这种时候不用怀疑代码,降级装 CPU 版最快。
3.2 gym 0.26 还是 gymnasium:这个选择决定你后面要改多少行
这一点值得单独说。0.21 时代env.step(a)返回四元组(obs, reward, done, info),env.reset()只返回一个 obs;0.26 之后变成五元组(obs, reward, terminated, truncated, info),reset()返回(obs, info)。这份 DQN 代码如果按 0.26 写,你装 0.21 就会在第一次 step 时炸掉,报too many values to unpack。
# 二选一,不要都装 pip install gym==0.26.2 # 如果你想用 gymnasium(API 与 0.26 基本一致,只是包名不同) # pip install gymnasium==0.29.1判断标准很简单:打开资源包里的环境文件,搜def step,看它return了几个值。五个就装 0.26 或 gymnasium,四个就装 0.21。改代码和改环境哪个成本低选哪个——我的习惯是统一往 0.26 靠,因为后面的评估、渲染、wrapper 生态都在这个方向上。
选 gymnasium 的话,环境文件里的import gym要全部换成import gymnasium as gym,注册函数也换成gymnasium.envs.registration.register,其他基本不用动。
3.3 把车间排产环境注册进 gym:id、reset、step 三处对齐
自定义环境不是gym.make("CartPole-v1")那种内置的,得先注册。注册代码一般放在包入口或者训练脚本顶部:
import gym from gym.envs.registration import register register( id="JobShopScheduling-v0", # 训练脚本里 gym.make 用这个 id,必须完全一致 entry_point="envs.job_shop:JobShopEnv", # 指向资源包里环境类的模块路径:类名 max_episode_steps=200, # 兜底上限,防止策略卡死时无限循环 kwargs={"n_jobs": 10, "n_machines": 5}, # 车间规模,按实际工单量改 )三个参数各有讲究。id建议带版本后缀-v0,以后改了状态定义就升到-v1,避免旧模型加载到新环境上得出莫名其妙的评估结果。entry_point的冒号前面是模块路径、后面是类名,路径是相对你运行脚本的目录算的,所以训练脚本要在项目根目录下执行,否则会报ModuleNotFoundError。max_episode_steps一定要给,排产环境里如果智能体反复选同一个已经完成的工单,环境自己不判结束,这轮 episode 就永远不终止,表现出来就是训练卡在第 1 轮不动。
kwargs里的n_jobs、n_machines决定了观测维度和动作维度。10 工单 5 机器的规模,动作空间一般是Discrete(10)或者Discrete(50)(工单×机器的组合),改这两个数之后,DQN 的输出层也得跟着改,后面第 4 章会讲。
3.4 一次 smoke test:五行代码确认环境没装歪
跑训练之前先做冒烟测试,这一步能挡掉八成的低級问题:
import numpy as np import gym import envs.job_shop # 触发环境模块导入,确保 register 已执行 env = gym.make("JobShopScheduling-v0") obs, info = env.reset(seed=42) # 0.26 起 reset 返回二元组 print("obs shape :", np.shape(obs)) # 期望一维数组,长度与状态编码一致 print("action sp :", env.action_space) # 期望 Discrete(n) print("obs dtype :", np.asarray(obs).dtype) # 期望 float32,int 会导致后面网络报错 total = 0.0 for _ in range(10): a = env.action_space.sample() # 随机策略,只验证通路不验证效果 obs, r, terminated, truncated, info = env.step(a) total += r if terminated or truncated: break print("cum reward:", round(total, 3)) print("info keys :", list(info.keys())) # 期望能看到 makespan / tardiness 之类的键 env.close()这段代码验证四件事:观测的形状和 dtype 对不对、动作空间是不是离散的、step 能不能正常返回五元组、info 里有没有排产相关的统计字段。obs dtype如果是 int64,说明环境的状态编码没做归一化,DQN 的线性层吃 int 会报类型错,得在环境里astype(np.float32)。info里没有makespan这类字段的话,第 6 章的评估就没法做,趁早补上。
冒烟测试通过,环境这层就算落地了。
4. 配置 DQN 训练入口:状态维度、超参和模型落盘
环境通了之后,卡人的地方从「装不上」变成「装上了但学不会」。这一章按配置顺序过:网络维度、超参取值、启动参数、模型保存。每一处都是改一个数就可能让训练白跑几万步的位置。
4.1 状态和动作维度对齐:网络输出层别写死
DQN 的网络结构本身很简单,坑全在维度上:
import torch import torch.nn as nn class QNetwork(nn.Module): def __init__(self, state_dim: int, action_dim: int, hidden: int = 128): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, action_dim), # 输出维度必须等于可选动作数 ) def forward(self, x: torch.Tensor) -> torch.Tensor: return self.net(x) # 维度从环境里读,绝对不要手写 64、8 这种数字 import gym env = gym.make("JobShopScheduling-v0") state_dim = env.observation_space.shape[0] action_dim = env.action_space.n print(state_dim, action_dim)state_dim从observation_space.shape[0]取,action_dim从action_space.n取,这两行是防止维度错位的唯一可靠做法。我见过的最典型的翻车是:改了n_jobs之后忘了改输出层,网络照常训练,loss 也在降,但 argmax 出来的动作编号有一半超出动作空间,env.step直接抛IndexError——而且是在训练了半小时之后才抛。
hidden=128对 10 工单 5 机器的规模是够的。状态维度在几十到一两百之间时,128 或 256 差别不大;如果你的排产问题工单量上到几百,观测维度会迅速膨胀,这时候优先做状态特征压缩(比如只编码剩余工序数和机器负载),而不是把 hidden 加到 1024。
另外注意forward的输入 dtype 必须是 float32,从 numpy 数组转过来的时候显式指定:
s = torch.as_tensor(obs, dtype=torch.float32, device=device).unsqueeze(0)unsqueeze(0)是补 batch 维,因为网络按 batch 输入设计,单个观测进去要凑成(1, state_dim)。
4.2 超参表:gamma、buffer、target 更新、epsilon 怎么定
超参没有标准答案,但排产场景有几条经验值可以照抄:
| 参数 | 建议值 | 调整方向 | 说明 |
|---|---|---|---|
| gamma | 0.95 | 0.90 – 0.99 | 一轮排产几十步,0.99 会让远处回报权重过大,容易发散 |
| lr | 1e-3 | 3e-4 – 1e-3 | 配合 Adam,高于 1e-3 常见 loss 抖动 |
| buffer_size | 50000 | 2e4 – 1e5 | 小于 1e4 时样本相关性太强,学不动 |
| batch_size | 64 | 32 – 128 | CPU 上 64 是速度和质量的分界点 |
| target_update | 500 | 200 – 1000 | 步为单位,太频繁等于没有目标网络 |
| eps_start / end | 1.0 / 0.05 | — | 探索率,低于 0.05 后基本靠已有策略 |
| eps_decay_steps | 20000 | 1e4 – 5e4 | 占总步数的 10% 左右比较合适 |
gamma 这里单独提一句:很多教程默认 0.99,那是给 Atari 那种上千步一局的环境用的。车间排产一轮通常几十步就结束,折扣因子拉太高,早期随机探索产生的噪声回报会被放大,表现出来就是 Q 值一路上冲然后 nan。0.95 是我在排产场景里比较稳的起点。
target_update用步数而不是 episode 计数,因为 episode 长度会随策略变化,按局更新节奏不稳。
4.3 训练脚本的启动参数:seed、device、save-dir
参数别硬编码在脚本里,用 argparse 抽出来,方便对比实验:
import argparse def parse_args(): p = argparse.ArgumentParser() p.add_argument("--env-id", default="JobShopScheduling-v0") p.add_argument("--seed", type=int, default=42) p.add_argument("--device", default="cpu", choices=["cpu", "cuda"]) p.add_argument("--total-steps", type=int, default=200_000) p.add_argument("--gamma", type=float, default=0.95) p.add_argument("--lr", type=float, default=1e-3) p.add_argument("--buffer-size", type=int, default=50_000) p.add_argument("--batch-size", type=int, default=64) p.add_argument("--target-update", type=int, default=500) p.add_argument("--eps-decay", type=int, default=20_000) p.add_argument("--save-dir", default="./checkpoints/run-001") return p.parse_args()启动就一行:
python train_dqn.py --env-id JobShopScheduling-v0 --device cpu --seed 42 \ --total-steps 200000 --save-dir ./checkpoints/run-001--save-dir一定要带实验编号,别都往./checkpoints里写。同一个目录覆盖三次之后,你根本分不清哪个模型对应哪组超参,这是最没必要的重复劳动。--device显式传,不要在代码里if torch.cuda.is_available()自动判断——自动判断在多人共用的机器上会给你惊喜,别人占着卡的时候你这次跑的是 CPU,速度差十倍,但日志里看不出来。
4.4 断点续训与模型落盘:别把三小时的训练赌在一次运行上
保存的时候把 online 网络、target 网络、优化器状态、当前步数一起存:
import os import torch def save_ckpt(path, net, target_net, optim, step): os.makedirs(os.path.dirname(path), exist_ok=True) torch.save({ "step": step, "online": net.state_dict(), "target": target_net.state_dict(), "optim": optim.state_dict(), # 少了它续训后学习率状态会重置 }, path) def load_ckpt(path, net, target_net, optim, device="cpu"): ckpt = torch.load(path, map_location=device) net.load_state_dict(ckpt["online"]) target_net.load_state_dict(ckpt["target"]) optim.load_state_dict(ckpt["optim"]) return ckpt["step"] # 续训从这一步接着算 epsilon 衰减三个state_dict缺一不可。只存 online 网络的话,续训时 target 网络是随机初始化的,前几百步的 TD 目标全是噪声,曲线会突然掉下去再爬回来,很容易被误判成「模型崩了」。step也必须存,因为 epsilon 是按全局步数衰减的,不恢复步数就等于重新探索一遍。
保存频率建议每 5000 步一次,同时保留最近三个,磁盘占用可控,也不至于一次异常退出全丢。
5. 避坑与排查:五个让训练曲线一直躺平的常见问题
这一章是我自己和身边人实际踩过的,按「现象 → 原因 → 解决」记,遇到对得上的直接照做。
5.1 现象:第一次 step 就报too many values to unpack
原因:环境按 0.26 写,返回五元组,但训练循环还是 0.21 的四元组写法obs, r, done, _ = env.step(a)。
解决:全局搜env.step,统一改成obs, r, terminated, truncated, info = env.step(a),并且把原来判断done的地方改成done = terminated or truncated。这两件事必须一起改,只改解包不改判断,episode 会在超时的时候不结束,训练步数虚高但有效样本很少。顺手把env.reset()也改成obs, info = env.reset(seed=...),0.26 下少接一个返回值同样是解包错误。
5.2 现象:reward 一直不涨,argmax 永远是同一个动作
原因:状态没做归一化。车间排产的原始观测里,工单剩余工时可能是 0–480 这种量级,机器负载率是 0–1,两者差几百倍,进网络之后大数值特征直接主导梯度,小特征等于没输入。
解决:在环境的_get_obs里做缩放,工时除以最大工时,负载率保持 0–1,交期转成「剩余时间/总工期」的比例。归一化之后再跑,同样的超参下 Q 值分布会明显散开。如果不想改环境,加一层gym.ObservationWrapper做 min-max 归一也行,但要注意统计量得从训练集算,别用测试集的数据。
5.3 现象:loss 跑到几千步突然变成 nan,之后再也不恢复
原因:reward 量纲太大加上 gamma 偏高,Q 值正反馈放大。排产环境里 makespan 动辄几百,如果直接当 reward 用(或者取负),TD 目标会迅速膨胀。
解决:三件事一起做——reward 缩放(比如除以基准 makespan 或者乘 0.01)、损失换成nn.SmoothL1Loss()(Huber,对大误差不敏感)、梯度裁剪torch.nn.utils.clip_grad_norm_(net.parameters(), 10)。我一般是先上 Huber,还压不住再动 reward 量纲,最后才考虑降 gamma。另外 nan 出现后不要试图从那个 checkpoint 续训,权重已经废了,回退到上一个正常存档重来。
5.4 现象:每次重跑结果差很多,没法对比两个超参哪个好
原因:只设了random.seed(),numpy、torch、环境内部的随机源都没管。排产环境里工单到达顺序、加工时间扰动往往各自用了独立的随机数生成器。
解决:写一个统一的种子函数,在创建环境和创建网络之前各调一次:
import random import numpy as np import torch def seed_everything(seed: int = 42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) # 无 GPU 时这行无副作用 torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False # 两个位置都要调:env.reset(seed=seed) 之前,以及网络初始化之前环境那边还要显式传env.reset(seed=seed),光靠全局种子管不到环境内部的生成器。做完这步,同参数两次运行的曲线应该基本重合,重合不了说明环境里还有没受控的随机源,得去翻环境代码。
5.5 现象:matplotlib 保存图片时报RuntimeError: Invalid DISPLAY variable
原因:在没有图形界面的机器(远程终端、容器)上,matplotlib 默认想连显示服务。
解决:在 import pyplot 之前切后端:
import matplotlib matplotlib.use("Agg") # 必须在 import pyplot 之前 import matplotlib.pyplot as plt顺序错了不生效,这是最容易忽略的细节。另外中文标签会显示成方框,补一行plt.rcParams["font.sans-serif"] = ["DejaVu Sans"]并避免在图上写中文,比折腾字体文件省事。
6. 装完怎么验证跑通:三个指标确认 DQN 真在学排产
训练日志里 loss 在降,不代表模型在学排产。DQN 完全可能把 Q 值整体平移而 argmax 不变,loss 看着很漂亮,策略跟随机没区别。所以装完之后我固定跑一套评估,三个指标一起看,缺一个都不下结论。
评估时关掉探索,走纯贪心:
import numpy as np import torch import gym import envs.job_shop @torch.no_grad() def evaluate(net, env_id, episodes=20, device="cpu"): env = gym.make(env_id) net.eval() # 切评估模式 rets, makespans = [], [] for ep in range(episodes): obs, _ = env.reset(seed=1000 + ep) # 固定评估种子,保证每次可比 done, ret = False, 0.0 while not done: s = torch.as_tensor(obs, dtype=torch.float32, device=device).unsqueeze(0) a = int(net(s).argmax(dim=1).item()) # 贪心,不加 epsilon 噪声 obs, r, term, trunc, info = env.step(a) ret += r done = term or trunc rets.append(ret) makespans.append(info.get("makespan", np.nan)) env.close() return np.mean(rets), np.std(rets), float(np.nanmean(makespans))episodes=20是为了让均值有意义,单局的随机性太大;评估种子固定成1000+ep,保证昨天和今天的模型是在同一批工单上比,不然涨了跌了都说不清。net.eval()一定要有,虽然这份网络没有 Dropout 和 BatchNorm,但养成习惯,以后换结构不会漏。
三个指标这样看:
| 指标 | 看什么 | 合格线(参考) |
|---|---|---|
| 评估平均回报 | 训练中途每 5000 步跑一次,看趋势 | 后 1/3 训练明显高于前 1/3,且方差收窄 |
| 平均 makespan | 和 SPT、EDD 这类规则基线对比 | 不要求全面胜出,但至少接近,不能差出 50% |
| 动作分布 | 统计贪心策略下各工单被选中的频次 | 不能集中在单一动作上,分布应随状态变化 |
第二个指标是最容易被忽略也最有说服力的。规则基线不需要写多复杂,SPT 就是每步选剩余加工时间最短的工单,十几行代码。如果 DQN 训完还不如 SPT,那大概率不是训练不够,而是 reward 设计和目标不一致——比如 reward 里延期惩罚权重给得太高,模型学会了「优先处理急单」而牺牲整体 makespan,这时候该调的是 reward 权重,不是继续加训练步数。
第三个指标查起来很快,跑一遍评估,把每次的a记下来做个np.bincount,如果 90% 都落在同一个编号上,说明策略退化了,多半是 5.2 里的归一化问题或者探索衰减太快。
我第一次跑这套环境时,训了六万步,曲线一路向上,评估出来却比 SPT 差一截。查了半天发现是评估时忘了固定种子,每次都在不同工单上比,涨跌全是噪声。从那以后我装完任何 DQN 环境,都会先跑 20 局固定种子的贪心评估,再决定要不要开长训练——省下来的时间比省下来的电费实在得多。这份代码和 gym 环境我整理在同一个包里,按第 2 章的版本表装一遍,第 3 章的冒烟测试过了再动训练脚本,能少走不少弯路。希望帮到你。
本文还有配套的精品资源,点击获取