资讯动态

基于DQN的交通信号相位调优:Python+SUMO仿真源码解析

发布时间:2026/10/26 17:34:46 来源:尧图企业网站定制
简介这是一份基于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 源码和 xlsxraw_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.osmnetconvert 转换用的原始 OSM 数据路网文件shixin_shanyin.net.xmlSUMO 实际加载的路网车流文件shixin_shanyin.rou.xml车辆发车时间、路径定义检测器文件shixin_shanyin.det.xml车道排队检测、数据采集配置入口shixin_shanyin.sumocfg仿真启动入口引用以上文件相位表raw_tsc.xlsx相位编号与持续时间定义车道方向表lane_direction.xlsx车道-相位映射关系核心代码DQN_main.py、RL_brain.pyDQN 训练与决策主程序路网文件里出现了多个版本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 rD:\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-porttraci 会自动挑选空闲端口实战里为了让排错更直观我习惯固定端口要不然排查时都不知道连的是哪一个进程。2.4 读取TLS与相位配置能拿到哪些真实数据连上仿真后第一件要确认的事是信号灯到底有几个、ID 是什么。Excel 里的名称是给人读的SUMO 生成的 TLS ID 却是按节点 ID 拼出来的两者常常对不上。用 getIDList() 把真实 ID 打印出来import traci traci.start([rD:\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, dtypenp.float32) return state注意traci.lane.getLastStepVehicleNumber 接收的是 lane IDdet.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, lr0.001, gamma0.9, memory_size2000, batch_size32): 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, replaceFalse ) 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, tau0.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 之后还需要连续调用 simulationStepSUMO 才会推进仿真并让车辆按新相位运行。如果 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 训练不收敛奖励信号、学习率与决策周期现象训练了几百 episodereward 曲线不仅没上升反而越来越低或者一直在一个负值区间震荡。原因常见是两个。一个是奖励给的绝对值太大或太小排队长度几十辆车直接当数值用了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 的 importfrom Priority_RL_brain import DQN优先经验回放会多一次重要性采样训练会慢一些但收敛稳定性和样本利用率通常都有提升。PDQN_main.py 则是把这套优先队列接到主训练循环的完整版本想对比普通 DQN 和优先 DQN 的差异跑两个脚本各训练几百 episode把 reward 曲线叠图就行。第三个方向是扩展多路口协同验证。项目里有多个 net.xml这是现成的实验素材。多路口场景下不要一上来就让一个智能体控制所有路口维度增加太快训练慢。先训练单个路口得到一套参数再复制一套结构相同的智能体部署到第二个路口共享部分权重最后再做联合调优。这一步需要把状态向量扩展成包含相邻路口排队信息的拼接形式代码改动集中在 get_state 函数。最后的验证流程是我自己的习惯训练完成后固定 epsilon 为 0.02跑一遍仿真每步记录排队长度同时跑同一车流文件的定周期配时基线。把两条曲线画在同一张图里观察 DQN 在哪些时段优于基线、哪些时段不如再回头调整奖励权重。这套动作我坚持了很久因为光看累计 reward 判断不了策略的真实表现只有落在一张对比图里才不会自我安慰。从那以后我每次跑新路网都强制自己走一遍对齐流程先打印 TLS ID再核对相位表最后跑一次只读脚本检查车道方向三个步骤全部确认过后才进训练循环。这套顺序帮我避开过不少无谓的翻车希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑