简介:这是一份基于Python与SUMO仿真平台、采用DQN强化学习算法动态调整交通信号灯相位时间的完整源码项目。项目源自高分毕业设计,答辩评审达98分,代码经调试测试可运行,适合计算机、人工智能、自动化及交通相关专业学生作为课程设计、期末大作业或毕业设计的参考,具备良好的学习与拓展价值。资源包为zip压缩格式,共32个文件,容量约536KB,主要包括16个XML路网/路由/检测配置、6个OSM地图数据、5个Python核心脚本、3个Excel辅助表格、1个sumocfg仿真配置及README说明文档;其中XML定义路网与车流,OSM提供地图底图,Python脚本承载DQN及训练主流程,Excel用于数据记录,README便于快速上手。目前已有81人学习下载,项目从地图导入、路网构建到DQN训练与信号灯控制形成了完整闭环,读者可借此掌握强化学习在智能交通信号优化中的应用思路,在此基础上还能按需替换模型、扩展场景,进行二次开发与算法改进。
1. 从定周期配时到DQN在线决策:能跑的交通信号相位调优源码
交通路口信号灯控制说穿了是个序列决策问题:每个时刻,都要决定下一个相位换成哪个、这个相位要持续多久。定周期配时在平峰期够用,一旦出现潮汐流量或偶发拥堵,固定配时就会一边空放、一边积压。这套基于 Python 开发、用 SUMO 做仿真平台、用深度强化学习中的 DQN 做相位决策的源码,恰好解决这个问题。它把路口排队信息实时喂给智能体,由 DQN 输出下一段相位选择与持续时间,整个流程在 SUMO 仿真器里闭环验证。项目是个人毕设,答辩评审分 98 分,代码做过调试与实测,理论链路完整,适合计算机、自动化、人工智能专业的学生拿来做课程设计或毕业设计,也适合想快速跑通一次深度强化学习控制的从业者当参考底子。
2. SUMO仿真环境搭建:路网、车流与信号配时的数据链路
这个项目拿到手第一件事不是看代码,而是先看清文件之间的关系。整套仿真由 SUMO 兜底,Python 通过 TraCI 遥控仿真,而路网、车流、检测器三个文件直接影响 DQN 能观察到什么状态。先梳理清楚数据流向,后面排错会省很多力气。
2.1 文件清单:从OSM到net.xml再到xlsx的数据流动
解压下来,文件分成四组。第一组是 OSM 原始地图,shixin_shanyin.osm、hello.osm、first.osm 这类文件是 OpenStreetMap 导出的地图数据,坐标系是 WGS84 经纬度,SUMO 不能直接拿来跑仿真。第二组是 net.xml 路网文件,shixin_shanyin.net.xml、second.net.xml、third.net.xml 等,由 netconvert 从 OSM 转换而来,包含节点、边、车道和信号灯信息。第三组是 rou.xml 车流文件和 det.xml 检测器文件,定义车辆路径与检测器位置。第四组是 Python 源码和 xlsx,raw_tsc.xlsx 里是信号灯相位配时表,lane_direction.xlsx 里是车道方向映射,DQN 在训练时要读取这两份表。
这些文件彼此有清晰的引用关系。sumocfg 是 SUMO 的配置文件,shixin_shanyin.sumocfg 里通过 net-file、route-files、additional-files 三个标签分别指定路网、车流、检测器位置。Python 不直接读取 net.xml,而是通过 TraCI 连接到运行中的 SUMO 进程,再调用接口取得路网上的实时信息。xlsx 文件则由 Python 直接读取,用于映射相位编号与车道方向。把文件与各自角色对齐,就得到下面这张表:
| 文件类别 | 代表文件 | 在项目里的作用 |
|---|---|---|
| 地图源文件 | shixin_shanyin.osm | netconvert 转换用的原始 OSM 数据 |
| 路网文件 | shixin_shanyin.net.xml | SUMO 实际加载的路网 |
| 车流文件 | shixin_shanyin.rou.xml | 车辆发车时间、路径定义 |
| 检测器文件 | shixin_shanyin.det.xml | 车道排队检测、数据采集 |
| 配置入口 | shixin_shanyin.sumocfg | 仿真启动入口,引用以上文件 |
| 相位表 | raw_tsc.xlsx | 相位编号与持续时间定义 |
| 车道方向表 | lane_direction.xlsx | 车道-相位映射关系 |
| 核心代码 | DQN_main.py、RL_brain.py | DQN 训练与决策主程序 |
路网文件里出现了多个版本,first、second、third、forth 对应的就是调试过程中对路网反复调整留下的版本,不影响运行,但复盘时能看出来作者在哪里动过路网。如果要给别人复现,只留 shixin_shanyin 里配套的那一套即可。
2.2 netconvert路网生成与信号灯识别:复现一条自己的路网
大部分拿这套源码的人,最后都要换成自己的路网。从 OSM 生成 net.xml 的常见做法是:
netconvert --osm-files shixin_shanyin.osm -o shixin_shanyin.net.xml --tls-guess--osm-files 指定输入的 OSM 文件,-o 指定输出路网文件。--tls-guess 是关键参数:netconvert 会扫描 OSM 里带 traffic_signals 标签的节点,猜测其为信号灯节点并生成 TLS。不加这个参数,很多来自 OSM 的信号灯节点会被当成普通交叉口,后续 DQN 就找不到可控制的信号灯。
如果你手头的 OSM 是从在线地图导出的,还需要注意坐标系。SUMO 的 netconvert 默认会做投影转换,但有时需要显式指定投影方式。常见做法是加 --proj 参数,比如用 UTM 投影。proj 的完整字符串取决于你所在城市的 UTM 分带,写起来繁琐,所以我更建议直接把 OSM 交给 netconvert 默认处理,生成的 net.xml 如果坐标明显偏移,再用 netedit 打开手动修正。这个项目里多个 net.xml 版本并存,多半就是修路网时另存出来的。
等你有了自定义路网,别忘了检查信号灯是否真的落在 net.xml 里。简单的方法是用 sumo-gui 预览:
sumo-gui -c shixin_shanyin.sumocfg --start窗口打开后放一辆车进去,看信号灯能不能正常变相位。如果车走到路口直接穿过去,说明 TLS 没有生成,回 netconvert 步骤查 --tls-guess 是否生效。
2.3 用TraCI把仿真进程拉起来:连通性验证脚本
环境问题集中在两处:Python 能否找到 traci 模块,traci 能否连上 SUMO 进程。traci 不是 PyPI 上随便装的库,它是 SUMO 安装目录里的 python 子目录。常见做法是把 SUMO 的 tools 目录加进 PYTHONPATH,或者干脆把 sumo 的 python 目录设为 site-packages。
验证连通性时,不要一上来就跑完整训练,先写一个最小脚本:
import traci sumo_binary = r"D:\Program Files\SUMO\bin\sumo.exe" # 换成你自己的路径 sumo_cmd = [ sumo_binary, "-c", "shixin_shanyin.sumocfg", "--no-gui", "--remote-port", "8813", "--start", ] traci.start(sumo_cmd) print("仿真时刻:", traci.simulation.getCurrentTime()) traci.close()traci.start 会拉起一个 SUMO 子进程,并通过 TCP 端口维持控制连接。--remote-port 8813 可以显式指定端口,当同一台机器跑多个仿真时,这个端口必须唯一。--start 让 SUMO 跳过启动画面直接进入仿真。traci.simulation.getCurrentTime() 返回的是仿真时刻,单位毫秒。如果这段脚本能打印出仿真时刻,说明 Python 与 SUMO 链路是通的。很多新手在这里翻车,不是代码问题,而是 sumo 的 bin 目录没写进 PATH,或者 Python 调用的是别处的 traci.py。
这里要特别分清两个东西:sumo 二进制负责实际仿真,traci 是 Python 客户端,二者通过 TCP 端口对话。traci.start 里传的命令列表,第一个元素必须是 sumo 或 sumo-gui 的完整路径。如果不显式传 --remote-port,traci 会自动挑选空闲端口,实战里为了让排错更直观,我习惯固定端口,要不然排查时都不知道连的是哪一个进程。
2.4 读取TLS与相位配置:能拿到哪些真实数据
连上仿真后,第一件要确认的事是信号灯到底有几个、ID 是什么。Excel 里的名称是给人读的,SUMO 生成的 TLS ID 却是按节点 ID 拼出来的,两者常常对不上。用 getIDList() 把真实 ID 打印出来:
import traci traci.start([r"D:\Program Files\SUMO\bin\sumo.exe", "-c", "shixin_shanyin.sumocfg", "--no-gui", "--start"]) for tls in traci.trafficlight.getIDList(): print(tls) logic = traci.trafficlight.getAllProgramLogics(tls) for program in logic: for phase in program.getPhases(): print(" 相位时长:", phase.duration, "状态串:", phase.state) traci.close()shixin_shanyin.net.xml 里的 TLS ID 通常长成 cluster_12345678_12345678 这种形式,是周边若干节点合并成一个控制器的结果。getAllProgramLogics 返回的是信号灯程序,里面每个 phase 包含持续时间与状态串。状态串由 r、g、y 字符组成,r 表示红灯,g 表示绿灯,y 表示黄灯,每个字符对应一条车道。
拿到这些数据后,再对照 raw_tsc.xlsx 里的相位表,就能确定 Excel 里的“相位1”“相位2”在 SUMO 里对应的是哪个 TLS 程序里的第几个 phase。这一步对不齐,后面 DQN 的 setPhase 就是把动作发给了错误的相位。实际操作中,把 lane_direction.xlsx 里的车道方向整理成字典,按 lane id 映射方向,对应到状态串的字符索引,是最稳妥的做法。
3. DQN信号控制实现拆解:状态、动作、奖励与训练循环
文件结构清楚了,接下来进入 DQN 本体。这个项目的核心代码集中在 DQN_main.py 和 RL_brain.py 两个文件,额外的 Priority_RL_brain.py 与 PDQN_main.py 提供变体。读代码前,先把强化学习三件套和交通信号控制的对应关系捋顺,再逐个文件看实现。
3.1 MDP建模:把信号控制写成强化学习问题
DQN 解决的是马尔可夫决策过程,要建模先定义状态、动作、奖励。
状态 State 在信号控制里,最基本的描述是当前路口处于什么相位、这个相位已经持续多久、每个进口方向的车道排队有多少车。这份源码里,状态向量大致由三块拼成:相位索引、相位已持续时间、各关键车道排队长度。排队长度可以从 det.xml 定义好的检测器读取,traci.lane.getLastStepVehicleNumber() 返回的是指定 lane 上最近一个仿真步的车辆数,累加后除以车道条数,就得到平均排队。
动作 Action 的定义方式决定了控制粒度。一种是离散相位集:把南北直行、南北左转、东西直行、东西左转四类相位编码成 0、1、2、3 四个动作。另一种是持续时间调整:每个相位有若干个时长档位供选择。这份源码把两种思想结合,动作既包含选相位,也包含选持续时间,输出维度等于相位数量乘以时长档位数量。这种设计的优点是决策周期大,不会一个仿真步就切一次相位。
奖励 Reward 是影响训练效果的直接因素。常见做法是用决策前后排队长度变化量:切完相位后,如果路口总排队比刚才短,奖励为正;比刚才长,奖励为负。把奖励写成差值,而不是排队长度本身的负数,梯度更稳定,因为差值消除了不同场景之间的绝对水平差异。信号控制领域更精细的度量是用总延误,也就是所有车辆在路网里多绕出的时间,但计算总延误需要额外记录每辆车的进入与离开时刻,工程成本高。作为毕设项目,排队长度的差值已经够支撑训练。
状态读取函数通常这么组织:
def get_state(tls_id): phase_idx = traci.trafficlight.getPhase(tls_id) remain = traci.trafficlight.getPhaseDuration(tls_id) queue_lanes = [] for lane_id in key_lane_ids: queue_lanes.append( traci.lane.getLastStepVehicleNumber(lane_id) ) state = np.array([phase_idx, remain] + queue_lanes, dtype=np.float32) return state注意:traci.lane.getLastStepVehicleNumber 接收的是 lane ID,det.xml 里的检测器需要用 traci.inductionloop.getLastStepVehicleNumber() 读取。两者数据来源不同,状态向量一旦混用,前后维度一致但口径不一致,模型会学到错误规律。
3.2 RL_brain.py里的Double DQN与经验回放
RL_brain.py 承担网络定义、经验存储、训练更新的全部工作。网络结构是两层全连接,输入维度等于状态长度,第一层 256 个神经元,第二层 128 个,输出层是动作数量。对路口这种低维状态,这个容量足够,不需要上更深的网络。源码里用 forward 手动拼了 relu,没有直接用 Sequential 堆层,好处是后面想换三层结构,只需要改 forward 内部。
DQN 最关键的技巧是双网络。eval_net 每一步都更新,target_net 每隔一定步数同步一次。如果只有一个网络,Q 值估计会一边追目标一边移动目标,训练曲线容易剧烈震荡。RL_brain.py 里这块的写法大致如下:
class DQN: def __init__(self, n_features, n_actions, lr=0.001, gamma=0.9, memory_size=2000, batch_size=32): self.eval_net = MLP(n_features, n_actions) self.target_net = MLP(n_features, n_actions) self.memory = np.zeros( (memory_size, n_features * 2 + 2) ) self.batch_size = batch_size self.gamma = gamma def store_transition(self, s, a, r, s_): transition = np.hstack((s, [a, r], s_)) idx = self.memory_counter % self.memory_size self.memory[idx, :] = transition self.memory_counter += 1 def sample_memory(self): indices = np.random.choice( min(self.memory_counter, self.memory_size), self.batch_size, replace=False ) return self.memory[indices, :]self.memory 是一个固定大小的环形缓冲,存的是 state、action、reward、next_state 拼接起来的一条经验。memory_counter 超过 memory_size 时,新经验会覆盖最老的记录。2000 的 memory_size 在交通仿真场景下偏小,实际训练时样本多样性不够,我一般会调大到 10000 左右,排长度长的路口经验池太小容易反复学到同一批数据。
学习率可以参考:0.001 在大部分路网下能正常收敛,调高到 0.01 后前几百个 episode 可能会看到奖励曲线毛刺明显变多。gamma 取 0.9 意味着智能体更看重近期回报,交通信号控制场景对延迟敏感,gamma 不要取到 0.99 那么极端,不然优化起来很慢。
target_net 的同步方式也值得关注。每隔 N 步强制拷贝,或者用 soft update 来平滑替换。soft update 的写法是:
def replace_target_params(self, tau=0.01): for tp, ep in zip(self.target_net.parameters(), self.eval_net.parameters()): tp.data.copy_(tau * ep.data + (1 - tau) * tp.data)tau 是 0.01 时,每次同步只把 eval 网络参数往 target 方向移动 1%,target 网络变化缓慢,训练过程稳定。但 tau 太小会让 target 更新变慢,导致早期 Q 值偏差长期得不到修正。我建议在 0.005 到 0.02 之间试,每次调整记录一次训练曲线,别凭感觉改。
3.3 DQN_main.py主循环:决策周期与相位执行
主训练循环的骨架是:启动 SUMO,读取状态,选动作,执行相位,统计奖励,存经验,学习。区别在于决策不是每个仿真步都做,而是隔一个决策周期才做一次。这里要分清两个时间粒度:仿真步长是 SUMO 推进一次仿真的时间,通常 1 秒;决策周期是信号灯保持同一相位不变的时间,由 PHASE_DURATION 定义,项目里常见的取值是 15 到 30 秒。
主循环的核心结构如下:
for episode in range(EPISODES): traci.start(sumo_cmd) state = get_state(tls_id) while traci.simulation.getMinExpectedNumber() > 0: action = dqn.choose_action(state, epsilon) traci.trafficlight.setPhase(tls_id, action) for _ in range(PHASE_DURATION): traci.simulationStep() if traci.simulation.getMinExpectedNumber() == 0: break next_state = get_state(tls_id) reward = queue_before - queue_after dqn.store_transition(state, action, reward, next_state) dqn.learn() state = next_state traci.close() epsilon = max(0.05, epsilon * 0.995)getMinExpectedNumber() 返回仿真里尚未完成行程的车辆数,等于 0 时说明所有车都到目的地了,一个 episode 结束。traci.trafficlight.setPhase 把 TLS 切到指定相位编号。setPhase 之后,还需要连续调用 simulationStep,SUMO 才会推进仿真并让车辆按新相位运行。如果 setPhase 后只调一次 simulationStep 就读取状态,下一个决策周期的状态停留在绿灯刚亮的第 1 秒,信号特征不够完整。
choose_action 内部用随机数和 epsilon 比较,小于 epsilon 就随机选动作,否则取 eval_net 输出 Q 值最大的索引。调试时可以把 Q 值打印出来,观察网络对不同相位的偏好差异,这比只看 reward 曲线更接近模型内部。
epsilon 从 0.9 左右开始,每回合乘以一个小于 1 的系数逼向 0.05。epsilon 大时智能体疯狂探索,相位乱切;epsilon 减小后倾向利用学到的策略。这个衰减速度依赖于回合数,通常几百回合后就能降到 0.1 以下。
动作与相位持续时间的绑定藏在 raw_tsc.xlsx 里。setPhase 只负责切到哪个相位,每个相位持续多少秒由 SUMO 里的信号灯程序决定,要动态调整持续时间,需要调用 traci.trafficlight.setPhaseDuration(tls_id, new_duration)。如果只是切换相位不动时长,那调整的其实是相位序列,不是相位时间。
3.4 收敛观测与性能对比:reward曲线和基线怎么画
训练过程中的每个 episode 累计 reward 建议记录下来,画出平滑曲线。常见做法是每 100 个 episode 求一次移动平均,再画出来。如果曲线整体趋势向上且在尾部稳定在某个区间,说明策略在变好。震荡剧烈不一定是坏事,但持续发散就说明奖励设计或超参有问题。
还要跟定周期配时做一轮基线对比。在同一个 sumocfg 下,用固定相位序列跑完全程,记录平均排队长度或平均延误。再把训练好的 DQN 策略接到同一个车流文件上,跑同一随机种子,得到 DQN 版本的指标。两组数据放在一张图里,答辩时才有说服力。这个项目里 rou.xml 可以换用 shixin_shanyin_high.rou.xml 和 shixin_shanyin_all7.rou.xml,对应不同流量强度,正好用来验证 DQN 在高峰流量下是否仍然优于定周期配时。
4. 运行避坑与排查:五个让新手翻车的真实场景
这一章把我在拆这个项目时实际遇到并排查过的坑整理成五条,每条按现象、原因、解决给出,方便照着对照。
4.1 TraCI连接失败:先检查端口和进程
现象:traci.start 调用后抛异常,报“Could not connect to 127.0.0.1”或者提示连接被拒绝。
原因:SUMO 子进程没有成功启动 TraCI 服务,或者端口被别的进程占用了。还有一种隐蔽情况:系统 PATH 里有多个 Python 环境,导入的 traci 和当前启动的 SUMO 版本不匹配。
解决:先把 sumo 路径写死在命令列表里,再确认端口唯一。排查命令如下:
netstat -ano | findstr 8813 tasklist | findstr sumo第一条命令查看 8813 端口是否被占用,第二条命令确认 SUMO 进程是否真的起来了。如果端口被占用,换一个,比如 8814。如果 netstat 看不到 8813 监听,八成是 SUMO 二进制路径不对,或者 traci.start 没有把 --remote-port 传进去。
4.2 TLS的ID和Excel里的相位表对不上
现象:打印 getIDList() 拿到的 ID 是一串 cluster_ 开头的节点拼接名,而 raw_tsc.xlsx 里面写的是“相位1”“南北直行”这种名字,用 Excel 里的名字去调用 setPhase 直接报“traffic light not found”。
原因:SUMO 为减少信号控制冲突,会把相邻交叉口合并成一个信号灯组,生成 ID 时用节点编号拼接;xlsx 是人工整理的逻辑相位表,两套命名体系天然不兼容。
解决:不要手工翻译 ID,而是用脚本把 TLS ID、相位编号、车道方向一次打印出来,和 xlsx 对齐后再写进配置。对齐时建议生成一个中间字典文件:
tls_map = { "cluster_123_456": { "phase0": "ns_straight", "phase1": "ns_left", "phase2": "ew_straight", "phase3": "ew_left", } }这样 DQN_main.py 里所有动作索引都通过字典翻译,不再直接依赖 SUMO 的原始 ID,换路网时只需要改字典。
4.3 训练不收敛:奖励信号、学习率与决策周期
现象:训练了几百 episode,reward 曲线不仅没上升,反而越来越低,或者一直在一个负值区间震荡。
原因:常见是两个。一个是奖励给的绝对值太大或太小,排队长度几十辆车直接当数值用了,Q 值量级随之波动;另一个是决策周期太短,相位刚切换完还没看出效果就结束了一次决策,网络学不到因果。
解决:先把 reward 改成排队差并做一次缩放。常见做法是除以最大排队数,让奖励保持在个位数量级。决策周期太短就把 PHASE_DURATION 调大,20 秒起步。学习率也可以从 0.001 往 0.0005 降一档试试。超参修改后每次只动一个变量,否则出问题时无法定位是哪个改动起的作用。
4.4 路网连通性不足:车辆死锁与跑不完
现象:仿真跑到一半,车流量大的路口出现车辆长时间原地不动,拥堵向后蔓延,一个 episode 久久结束不了。
原因:OSM 导入后路网的连接不够完善,上游的车找不到下游车道,只能在路口附近反复变换车道,拖慢整体通行效率。另外 rou.xml 里如果出现了没有连接的路段,车辆会卡在路网边上。
解决:先用 sumo-gui 打开 net.xml,选中问题路口看相连 lane 的转向关系。如果确实缺连接,用 netedit 手动补连接。也有一个临时办法:把决策阶段卡住的路口判定为死锁,强制给那一条相位延长绿灯,让车先走。前者是彻底修复路网,后者是训练时的应急处理,严格来说后者对结果有污染,能用前一种就不用后一种。
4.5 车道-相位映射错位:直行绿灯放走了左转车队
现象:相位切换正常,但某个方向排队一直很长,其它方向空放,DQN 怎么调都学不好。进一步检查发现是 lane_direction.xlsx 里某条左转车道在校对时记成了直行。
原因:车道方向表是与 Excel 逐行对齐的,OSM 导入的车道顺序和字段顺序存在错位,一旦人工整理漏了某条 lane,后面的映射整体错位。
解决:在训练前跑一个只读脚本,逐车道输出 lane id 和该车道最近的车辆数,和相位表对照。地图上看到的方向,跟 SUMO 里 lane 的几何方向要自己核对一遍。
for lane_id in traci.lane.getIDList(): shape = traci.lane.getShape(lane_id) # 返回shape坐标 vehicles = traci.lane.getLastStepVehicleNumber(lane_id) print(lane_id, shape[-1], vehicles)shape[-1] 是车道终点的坐标,可以用来判断朝向。把朝向和相位状态串的 r/g/y 字符逐位对齐,确认哪个字符控制的是左转车道,再决定要不要修正 lane_direction.xlsx。这一步虽然看着繁琐,但它是整套源码能否跑出合理结果的基础。
5. 进阶玩法:给这套DQN加奖励权重、优先回放与多路口验证
这个项目自带的 DQN 已有完整闭环,但想让结果更漂亮,可以从三个方向改。
第一个方向是奖励精化。排队长度差值只反映空间累积,不反映通行时间。可以把排队差再乘以一个系数,并叠加一个小的延误惩罚。延误的近似计算可以从车辆平均速度反推,速度越慢说明路口越堵。实践里我用得最多的是:奖励 = 前一周期排队长度 - 当前周期排队长度 - 0.1 * 周期内平均延误。系数 0.1 要调,延误数值比排队长度大一个量级,不配系数的话奖励会被延误项淹没。
第二个方向是启用 Priority_RL_brain.py 的优先经验回放。优先回放的思想是:TD 误差大的样本蕴含更多信息,采样时给它更高权重。替换方式直接改 DQN_main.py 的 import:
from Priority_RL_brain import DQN优先经验回放会多一次重要性采样,训练会慢一些,但收敛稳定性和样本利用率通常都有提升。PDQN_main.py 则是把这套优先队列接到主训练循环的完整版本,想对比普通 DQN 和优先 DQN 的差异,跑两个脚本各训练几百 episode,把 reward 曲线叠图就行。
第三个方向是扩展多路口协同验证。项目里有多个 net.xml,这是现成的实验素材。多路口场景下不要一上来就让一个智能体控制所有路口,维度增加太快,训练慢。先训练单个路口,得到一套参数;再复制一套结构相同的智能体部署到第二个路口,共享部分权重;最后再做联合调优。这一步需要把状态向量扩展成包含相邻路口排队信息的拼接形式,代码改动集中在 get_state 函数。
最后的验证流程是我自己的习惯:训练完成后,固定 epsilon 为 0.02,跑一遍仿真,每步记录排队长度,同时跑同一车流文件的定周期配时基线。把两条曲线画在同一张图里,观察 DQN 在哪些时段优于基线、哪些时段不如,再回头调整奖励权重。这套动作我坚持了很久,因为光看累计 reward 判断不了策略的真实表现,只有落在一张对比图里才不会自我安慰。
从那以后,我每次跑新路网都强制自己走一遍对齐流程:先打印 TLS ID,再核对相位表,最后跑一次只读脚本检查车道方向,三个步骤全部确认过后才进训练循环。这套顺序帮我避开过不少无谓的翻车,希望帮到你。
本文还有配套的精品资源,点击获取