SUMO与DQN实战:交通信号灯相位时间的强化学习调整
发布时间:2026/10/2 14:28:49来源:尧图网络
简介该源码项目以SUMO为交通仿真平台基于Python语言实现深度强化学习中的DQN算法对路口交通信号灯的相位时间进行动态调整与优化主要面向计算机、通信、人工智能、自动化等专业的学生和从业者可用于毕业设计、课程设计及期末大作业。项目代码已经过调试测试答辩评审达到九十八分具备较强的完整性与可运行性。压缩包内共三十二个文件涵盖路网与仿真配置的xml/sumocfg文件、osm地图数据、py核心算法脚本、xlsx对比数据表以及md说明文档资源整体约五百三十六KB目录结构便于按模块查阅。目前已有八十五人学习。读者可借助DQN主程序、强化学习模型、辅助脚本及原始与优化后的信号灯相位数据快速复现智能交通控制实验理解状态、动作、奖励函数的设计思路并在此基础上扩展实现不同策略或路网场景整体代码包含清晰的模块划分与注释适合作为高年级学生和研究者入门强化学习与交通仿真的实践样例。1. 为什么用 SUMO DQN 调整信号灯相位先跑通再谈原理每天通勤经过的十字路口高峰期车辆排成长龙信号灯却还是那套固定相位时间——无论路面有没有车绿灯该亮多久还是多久。这个“基于 Python 实现的利用 SUMO 作为仿真平台采用强化学习中的 DQN 进行交通信号灯相位时间的调整”项目就是让智能体自己观察路口状态学习什么时候切换相位、绿信比该持续多久。它解决的是自动配时问题尤其适合做交通仿真的学生、刚接触强化学习的工程师以及想验证深度强化学习算法在离散动作场景下落地的研究者。项目核心有三块SUMO 提供仿真环境和车辆行为模型Python 通过 TraCI 接口与仿真器交互DQN 负责信号相位决策。下面我会从概念讲起给出一套能跑通的最小代码最后重点拆解参数和踩坑点。2. SUMO 与 DQN 的关键概念先搞清楚“相位”和“状态”再写代码很多人卡在第一关SUMO 里的 phase 到底和强化学习的 action 怎么对应实际上只要你把相位定义、状态特征、动作空间和奖励函数想清楚代码就是按着这几个概念填空。2.1 SUMO 的信号灯控制模型Phase 是动作绿信比是结果SUMO 中每个交叉口的信号灯由tlLogic描述里面包含多个phase。一个 phase 由duration持续时间和state灯色状态字符串组成例如GrGr表示南北直行绿灯、东西直行红灯。传统定时控制把这些 phase 写死成固定循环而智能控制要做的是动态决定当前 phase 持续多久、下一步切到哪个 phase。典型的路网文件里信号灯定义长这样tlLogic idJ1 typestatic programID0 offset0 phase duration31 stateGrGr/ phase duration6 stateyryr/ phase duration31 staterGrG/ phase duration6 stateryry/ /tlLogic这里state的每个字符对应信号灯组的一个方向G是绿灯r是红灯y是黄灯。typestatic表示该信号灯按固定配时运行这是我们后面用 TraCI 动态控制时要绕过的默认模式。在 Python 中通过 TraCI 读写信号灯import traci # 获取当前相位索引和剩余时长 phase_index traci.trafficlight.getPhase(J1) remaining traci.trafficlight.getPhaseDuration(J1) # 强行切换相位并指定下一相位持续 15 秒 traci.trafficlight.setPhase(J1, 2) traci.trafficlight.setPhaseDuration(J1, 15.0)getPhase返回的是 phase 在tlLogic里的索引0、1、2、3不是灯色字符串。setPhaseDuration只修改当前相位的剩余时长不会改变 phase 的顺序。如果你想实现“跳到某个固定相位并保持一段时间”一定要先用setPhase切换再调用setPhaseDuration设置新时长的起点。这两个函数是后面所有动作封装的基础我的习惯是用一个字典把 phase 索引和实际灯色对应起来方便调试和观察。2.2 DQN 怎么在交通信号任务上落地状态、动作、奖励的三个设计选项DQN 是深度值函数方法通过 Q 网络估算Q(s,a)然后选择 Q 值最大的动作。它在交通信号控制中非常合适因为信号灯动作天然是离散的。交通信号控制里常见的动作空间有三种第一二值动作保持当前相位或者切换到下一个相位。实现最简单但表达力有限。第二持续时间动作相位保持不变只输出一个持续时间增量比如 5~30 秒。第三显式相位选择输出 0、1、2、3 对应四个相位。但直接跳相位会让黄灯过渡变难所以实际项目中更常用“保持或切换”的动作空间。状态特征至少需要包含当前相位索引、相位已持续时长、各进口道排队长度、平均等待时间。排队长度用 Lane 上的减速车辆数表示等待时间可以直接从 TraCI 的getLastStepHaltingNumber和getLastStepMeanSpeed推导。这些特征必须在量级上做归一化否则 Q 网络很容易梯度爆炸。奖励函数的设计是这整个项目的“玄学”点。常见三种奖励方案公式优点缺点总等待时间变化量r -(wait_time_now - wait_time_before)即时反映控制收益信号灵敏单步噪声大需要平滑排出车辆数r completed_vehicles_now直观鼓励吞吐量容易只放行排队短的相造成饥饿排队长度负值r -total_queue稳定好计算对溢流不敏感容易饱和我通常选“总等待时间变化量”作为主奖励因为它能直接说明相位切换后是否改善了路口的通行状况。配合经验回放和目标网络DQN 能学到比较稳定的策略。下面会从零到一搭建这套系统。3. 从零到一搭建 SUMO Python DQN 的最小可运行代码这一章是整篇博文里最值得“抄作业”的部分。我会先搭环境再用 TraCI 写一个可交互的仿真循环最后嵌入 DQN 训练框架。3.1 环境准备SUMO 安装、Python 依赖和网络生成SUMO 是独立的仿真程序不能通过 pip 直接安装。安装完后再配置 Python 环境。我的经验是使用 conda 建一个虚拟环境避免和系统 Python 平台包冲突# Ubuntu/Debian 可以用 apt 安装Mac 用 homebrew sudo apt update sudo apt install sumo sumo-tools sumo-doc # Windows 用户直接下载安装包并把 sumo/bin 加入 PATH sumo --versionPython 侧需要torch、numpyTraCI 库本身在 SUMO 安装目录的tools下不需要单独 pip install。如果import traci报错把SUMO_HOME/tools手动加入PYTHONPATHexport SUMO_HOME/usr/share/sumo export PYTHONPATH$SUMO_HOME/tools:$PYTHONPATH python -c import traci; print(traci)网络生成我推荐最省事的osmWebWizard.py它会打开浏览器让你选一块地图区域自动下载路网、生成随机车流并输出osm.sumocfgpython $SUMO_HOME/tools/osmWebWizard.py如果你不想用真实地图也可以用netgenerate快速生成一个带信号灯的交叉口但拼接rou.xml和net.xml会稍麻烦。初学阶段直接用osmWebWizard生成的路网和车流比手搓配置文件快得多。3.2 基于 TraCI 的仿真交互接口获取车辆信息、控制信号灯先用一个最简脚本验证 TraCI 能连上 SUMO并让信号灯每 10 秒切换一次相位import traci sumo_cmd [ sumo-gui, # 或者用 sumo 纯命令行 -c, osm.sumocfg, --start, # 启动后自动开始仿真 ] traci.start(sumo_cmd) while traci.simulation.getMinExpectedNumber() 0: traci.simulationStep() if traci.simulation.getTime() % 10 0: tl_id J1 current_phase traci.trafficlight.getPhase(tl_id) next_phase (current_phase 1) % 4 traci.trafficlight.setPhase(tl_id, next_phase) traci.trafficlight.setPhaseDuration(tl_id, 10.0) traci.close()这段代码解决了“控制信号灯”这个问题。traci.simulationStep()每次推进一个默认仿真步长通常 1 秒getMinExpectedNumber()返回还在路上的车辆数等于 0 时仿真结束。getPhase和setPhase是我们之前讲过的核心接口。注意决策间隔和仿真步长的区别。默认仿真步长是 1 秒但信号灯不需要每秒都决策。上面代码用getTime() % 10 0实现每 10 秒切换一次相位也就是说动作间隔是 10 秒。你要把setPhaseDuration的时长和决策间隔保持一致否则相位会被下一个决策覆盖。3.3 搭建 DQN 训练框架神经网络定义、经验回放、训练循环现在把上面的交互循环包装成一个强化学习环境。神经网络和训练循环可以用下面这套代码骨架import random import numpy as np import torch import torch.nn as nn import torch.optim as optim class DQN(nn.Module): def __init__(self, state_dim, n_actions): super().__init__() self.net nn.Sequential( nn.Linear(state_dim, 64), nn.ReLU(), nn.Linear(64, 64), nn.ReLU(), nn.Linear(64, n_actions) ) def forward(self, x): return self.net(x) class ReplayBuffer: def __init__(self, capacity10000): self.capacity capacity self.buffer [] self.pos 0 def push(self, s, a, r, s_, done): if len(self.buffer) self.capacity: self.buffer.append((s, a, r, s_, done)) else: self.buffer[self.pos] (s, a, r, s_, done) self.pos (self.pos 1) % self.capacity def sample(self, batch_size): return random.sample(self.buffer, batch_size)训练循环的关键片段state_dim 8 n_actions 2 # 0 表示保持当前相位1 表示切换下一个相位 policy_net DQN(state_dim, n_actions) target_net DQN(state_dim, n_actions) target_net.load_state_dict(policy_net.state_dict()) optimizer optim.Adam(policy_net.parameters(), lr1e-3) replay ReplayBuffer(capacity20000) batch_size 32 gamma 0.99 for step in range(2000): # epsilon-greedy 选择动作 if random.random() epsilon: action random.randrange(n_actions) else: with torch.no_grad(): q_vals policy_net(torch.tensor(state, dtypetorch.float32)) action q_vals.argmax().item() next_state, reward, done env.step(action) replay.push(state, action, reward, next_state, done) state next_state if len(replay.buffer) batch_size: batch replay.sample(batch_size) states torch.tensor([b[0] for b in batch], dtypetorch.float32) actions torch.tensor([b[1] for b in batch], dtypetorch.long) rewards torch.tensor([b[2] for b in batch], dtypetorch.float32) next_states torch.tensor([b[3] for b in batch], dtypetorch.float32) dones torch.tensor([b[4] for b in batch], dtypetorch.bool) q_target rewards gamma * target_net(next_states).max(1)[0].detach() * (~dones) q_pred policy_net(states).gather(1, actions.unsqueeze(1)).squeeze(1) loss nn.MSELoss()(q_pred, q_target) optimizer.zero_grad() loss.backward() optimizer.step() if step % 500 0: target_net.load_state_dict(policy_net.state_dict())env.step(action)是前面 TraCI 交互的封装。这里需要注意执行一个和强化学习 step 不对齐的 TraCI 循环训练步数和仿真秒数不是一回事。我的做法是每次step里让 SUMO 向前跑 10 秒正好一个决策间隔。action0时维持当前相位不动action1时调用setPhase切换。奖励函数采用reward -(当前总等待时间 - 上一决策时刻总等待时间)并用一个小数缩放比如除以 100避免数值过大。4. 让训练收敛的关键参数状态归一化、奖励平滑和 Epsilon 衰减DQN 跑交通信号最常遇到的问题是训练很久不收敛、Q 值发散或者模型学到“永远切换”的偏置。这些问题大部分不是网络结构的问题而是输入和奖励没处理好。4.1 状态与奖励设计为什么直接使用等待时间容易发散直接拿原始等待时间作为特征数值范围会从几十秒到几千秒。Q 网络中间的 ReLU 对输入尺度很敏感输入量级相差 100 倍时梯度方向会被大尺度特征带跑。所以状态必须归一化。假设一个路口有四个进口道每个进口道取一条直行车道状态向量可以这样构造def build_state(traci, tl_id, lanes): state [] for lane in lanes: queue traci.lane.getLastStepHaltingNumber(lane) # 用 50 作为车道最大排队长度估计值 state.append(queue / 50.0) wait traci.lane.getWaitingTime(lane) # 用 60 秒作为最大等待估计值 state.append(min(wait / 60.0, 1.0)) current_phase traci.trafficlight.getPhase(tl_id) phase_duration traci.trafficlight.getPhaseDuration(tl_id) state.append(current_phase / 4.0) state.append(phase_duration / 30.0) return state奖励函数建议使用“等待时间变化量”并做平滑。比如reward -(wait_now - wait_before) / 100除以 100 后奖励幅度落在 ±5 以内Q 值不会爆炸。另一个细节是等待时间变化量本身带噪声尤其车辆进入或离开路口时会有突变。我习惯在训练时对奖励做一次指数滑动平均reward_ema reward_ema * 0.9 reward * 0.1用这个平滑后的值作为实际训练奖励能明显抑制 loss 抖动。4.2 四个必调参数学习率、经验池容量、目标网络更新频率、迷你批大小参数我的建议范围影响学习率 lr1e-4 到 1e-3太高 Q 值震荡甚至 NaN太低收敛极慢经验池容量20000 到 50000太小则样本相关性高太大则学习滞后目标网络更新频率500 步到 2000 步太频繁会拜拜地追逐不频繁则训练不稳定batch_size32 到 128越大梯度越稳但内存和计算代价高这四个参数里最容易被忽略的是目标网络更新频率。如果每次训练都同步目标网络Q 目标和预测一起变训练容易发散。我最初用step % 500 0同步一次效果尚可。但要注意这里的步数是指强化学习的 step不是仿真秒数。一个仿真场景跑 1000 秒决策间隔 10 秒只产生 100 个强化学习 step所以500步更新一次目标网络对应的其实是超过 8000 个仿真周期。Epsilon 衰减也是强制要求。默认epsilon0.9不衰减智能体会一直随机乱转。我的经验是设置起点 1.0、终点 0.01、衰减系数 0.995epsilon max(0.01, epsilon * 0.995)每训练一个 step 衰减一次大约 500 步后 epsilon 就降到 0.08 左右。如果你发现模型训练了很久还在频繁切换相位先检查 epsilon 是不是真的降下来了而不是急着改网络架构。5. 高频问题排查SUMO 版本冲突、traci 连接卡死、DQN 不收敛这个项目涉及的坑很多来自工具链版本、仿真与训练步长错配、以及 DQN 超参数设定。我把最常见的四类问题列在这里每条都是现象、原因、解决三步走。5.1 SUMO 版本差异导致 TraCI API 失效现象代码在别人的电脑上能跑换到自己电脑上traci.trafficlight.setPhaseDuration直接报AttributeError或者加载osm.sumocfg时提示某个 XML 属性不存在。原因SUMO 的版本迭代很快部分 TraCI 函数在 1.8 前后改了命名或行为net.xml的 schema 也可能出现兼容性差异。比如老版本用stateGGgr新版可能要求显式定义连接关系否则校验失败。解决先执行sumo --version确认版本再查阅当前版本的 TraCI 文档。如果项目代码是旧版可以尝试安装对应的 SUMO 版本。我一般用 conda 单独建一个环境直接把 SUMO 固定到 1.18 或 1.20 之类的稳定版本避免升级影响训练流程。5.2 traci 连接卡死现象traci.start()之后程序就卡住没有报错sumo-gui 窗口也没出现。原因常见是 SUMO 的二进制不在 PATH 里或者启动命令拼错了-c指向的配置文件路径。TraCI 默认通过端口 8813 通信如果端口被占用也会导致连接失败。解决先手动在终端跑sumo -c osm.sumocfg --start确认配置文件能正常启动。然后在 Python 里显式设置sumo_cmd里的绝对路径并增加重试traci.start(sumo_cmd, numRetries10)如果还是卡住杀掉残留的 sumo-gui 进程再试。记得在循环结束处调用traci.close()否则下一个训练进程会因端口未释放而连不上。5.3 状态没归一化Q 值溢出成 NaN现象训练到第几百步打印的 loss 变成nanQ 值也全是nan。原因状态特征里如果有waiting_time这种几百上千的数输入到网络后中间层加权求和可能直接溢出。另一个原因是奖励尺度太大造成 Q 目标爆炸。这个项目里网络只有三层很浅所以“梯度爆炸”不如说是“目标 q 值爆炸”。解决对状态和奖励都做缩放我在 4.1 已经给出了归一化和奖励平滑的做法。如果已经出现 NaN补上梯度裁剪torch.nn.utils.clip_grad_norm_(policy_net.parameters(), max_norm10.0)clip_grad_norm_能防止梯度过大通常训练一两步后 loss 会恢复正常。5.4 Epsilon 不衰减导致策略永远随机现象训练结束拿模型去评估它还是像随机策略一样乱切相位平均等待时间比固定配时还差。原因如果 epsilon 一直固定在 1.0智能体就不会去用学到的 Q 值相当于一直在随机探索。我见过不少项目里忘记写epsilon * 0.995或者衰减系数设成 0.9999 导致下降太慢。解决把 epsilon 衰减作为训练循环的显式步骤每步都执行并且在代码里打印当前 epsilon 值。比如if step % 100 0: print(fstep {step}, epsilon{epsilon:.3f})如果你已经用了经验回放还可以在训练后期把 epsilon 强制设成 0.02 以下跑一段确定性评估看策略是否真实有效。6. 进阶对比基线评估 DQN 效果以及让模型更稳的三个技巧训练出一个能跑通的模型只是开始。真正要判断这个项目有没有价值必须回到交通工程视角去评估。6.1 用固定配时基线做对比别只看平均等待时间我建议用同一个osm.sumocfg只改信号灯控制方式分别跑固定配时、随机动作和 DQN 控制。统计维度包括总等待时间、平均每车延误、最大排队长度和通过车辆数。固定配时可以从 SUMO 自带 static 程序里读出来直接作为基线。每次评估至少跑 10 个不同随机种子取平均值因为单车流的随机波动可能会掩盖真实差异。统计脚本里最直观的指标是traci.vehicle.getWaitingTime对所有车辆求和除以完成出行车辆数。这个指标比“平均排队长度”更贴近驾驶员感知也更容易向项目老师或评审解释。6.2 三个让模型更稳的技巧Double DQN、优先回放、奖励塑造Double DQN 解决 Q 值过估计。标准 DQN 用同一个网络选择并评估最大动作容易高估价值。改成从target_net中取最大 Q 对应的动作再用target_net计算该动作的价值训练会稳很多。优先经验回放让重要样本被更频繁地学习。交通场景里“突发拥堵”的样本很少但对策略影响极大随机均匀采样容易忽略它们。给每个经验按TD error排序抽样概率正比于优先级能加速收敛。奖励塑造成本最低。除了等待时间变化量额外减去当前总排队长度的一小部分能引导智能体不要长时间让某一车道排队。例如reward -(wait_change / 100) - 0.01 * total_queue这一项不需要额外传感器TraCI 里直接就能读取。6.3 我的习惯训练前先跑固定配时给 Q 网络一个“锚点”我最近一次做类似项目有个很实用的习惯在训练开始前用固定配时跑 20 个完整仿真周期把平均等待时间记录为 baseline。然后在训练过程中每 1000 步保存一次当前模型并用小规模评估集对比只有 mAP、平均等待时间低于 baseline 的模型才保留。这么做的好处是能直观看到 DQN 相比固定配时到底提升了多少而不是“训练完再说”。另外我习惯冻结随机种子包括 Python 的random.seed、NumPy 的np.random.seed和 PyTorch 的torch.manual_seed。SUMO 车流生成也尽量使用固定随机种子这样才能保证每个对比实验之间只改变控制策略这一个变量。以上就是我基于 SUMO DQN 做信号灯相位时间调整的完整路径。从最基础的概念搭到可训练的代码再到参数调优和排坑每一步都是动手踩出来的。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网