基于DQN的交通信号灯相位时间优化:从SUMO仿真到实战调参
发布时间:2026/9/9 1:14:50来源:尧图网络
简介面向毕业设计与课程设计场景资源提供了一套基于开源SUMO交通仿真平台和深度强化学习DQN算法的信号灯相位时间优化项目。整套代码采用Python编写覆盖路网构建、仿真交互、模型训练与结果分析等关键环节适合对智能交通和强化学习感兴趣的开发者快速上手。项目共32个文件包含路网配置、地图数据、算法脚本、实验结果表格及说明文档其中XML和OSM文件用于构建仿真环境PY文件为核心控制逻辑压缩包仅533KB部署轻量。目前已有545人学习使用。通过阅读源码可以理解状态特征选取、动作输出方式和奖励函数设计并掌握将DQN应用于交通信号控制的完整流程理解从路网搭建、仿真交互到模型训练与收敛评估的整个链路便于在此基础上扩展优先级决策或迁移至其他路网场景。 大概半年前我准备一个交通仿真的课程项目时盯着SUMO仿真界面里的红绿灯发呆东西方向已经堵成一片南北方向却一辆车都没有可信号灯依然按着固定配时在傻傻地放行。那一刻我意识到交通信号灯控制远比看起来复杂它本质上是一个动态环境下的序列决策问题而这类问题恰好是强化学习的看家本领。于是我基于Python SUMO仿真平台用DQN做了一套能根据实时车流调整交通信号灯相位时间的方案整套源码也整理成了一个开源项目。这篇文章就把这套方案从环境搭建、算法设计到训练调参的完整过程拆给你看适合正在做毕设、参加竞赛或者想入门强化学习在交通领域应用的同学参考。1. 信号灯控制的痛点为什么固定配时总是差一口气1.1 交通流本身就是非平稳的传统路口信号机用的都是固定配时方案也就是预先算好每个相位的红绿灯时长然后按周期循环执行。这种方式在车流比较稳定的时候勉强够用但一旦遇到早晚高峰、学校放假、突发事故、旁边路口修路车流特征就会完全变化。固定配时没有任何感知能力只能按部就班地放行结果就是绿灯方向没车红灯方向排队几百米整个路口通行效率大幅下降。很多同学可能会觉得那我用多时段配时方案不就行了高峰一套、平峰一套。这确实比固定配时强但本质上还是开环控制交通流一旦出现计划之外的波动多时段方案照样失灵。交通仿真里我们经常说一句话交通流本身是非平稳的每分钟的车流量都在变所以真正有效的策略必须是闭环的、能根据实时状态做出反应的。1.2 DQN在信号灯场景里到底做什么用DQN解决信号灯问题其实是把信号灯控制器当成一个智能体让它通过与环境不断试错来学习一套控制策略。每个决策时刻智能体观察当前路口的状态比如各个方向的车队有多长、车辆等了多久然后决定当前绿灯相位应该继续延长还是切换到下一个相位。切换之后环境发生改变智能体会得到一个奖励信号比如等待车辆减少了多少。经过成千上万次试探DQN会逐渐学会什么情况下该延长、什么情况下该切换。用一个生活化类比固定配时相当于一台自动售货机投币后永远出同一瓶饮料DQN相当于一个有经验的老交警他会看一眼各个方向的车流再决定先放谁、放多久而且随着经验积累判断越来越准。1.3 这个项目适合谁、能学到什么如果你正在做智慧交通方向的毕设或竞赛项目这个题目几乎是标配如果你想入门强化学习但不想跑那种玩具环境交通信号灯也是一个很合适的实战场景。跑通这个项目你至少能收获四样东西SUMO这个专业交通仿真器的基本使用包括路网生成、车流配置、TraCI接口调用强化学习三要素——状态、动作、奖励函数——如何映射到真实工程问题里DQN的完整训练链路包括经验回放、目标网络、epsilon-greedy探索一套可以扩展的多路口信号灯控制源码框架后续换算法比如DQN换PPO只需要改agent部分。2. SUMO环境搭建与TraCI通信先说环境再谈算法2.1 SUMO安装与环境变量配置SUMO全称Simulation of Urban MObility是一款开源的微观交通仿真平台。所谓微观就是每辆车都有独立的加速度、最大速度、换道行为能够比较真实地模拟交通流。安装方式取决于操作系统。Windows用户可以直接去官网下载安装包也可以用包管理器安装Ubuntu等Linux系统推荐命令安装macOS用brew也能装。安装完成后需要确认SUMO_HOME环境变量指向安装目录并且把sumo和netconvert这些可执行文件所在目录加入PATH否则后续Python调用会找不到命令。验证安装可以打开终端执行一句命令sumo --version如果能看到版本号说明安装成功。要注意的是SUMO更新迭代很快不同大版本的TraCI接口函数会有差异我这边用的是1.15.0版本如果你用的版本比较新个别接口名可能需要微调。2.2 搭建一个十字路口仿真场景训练信号灯控制先得有一个能反复实验的路口场景。最经典的就是单十字路口两个方向交叉每条进口道双向各两车道中间路口设置信号灯。SUMO里搭建场景有两种方式一是用自带的netedit图形化编辑器画二是用XML文件定义节点和边然后通过netconvert命令生成路网。图形化适合微调命令行适合自动化生成我项目中用的是后者的思路。路网的基本结构是nodes.xml里定义节点坐标edges.xml里定义连接的边。一个简单十字路口可以这样描述nodes node idN x0 y300/ node idS x0 y0/ node idE x300 y150/ node idW x0 y150/ /nodes定义好节点后用netconvert生成路网时给中间节点加一个typetraffic_light属性系统就会自动为该路口分配一套两相位信号灯方案。车流文件rou.xml则定义了车辆什么时候出现、从哪个进口道到哪个出口道可以通过flow标签灵活设置每小时车流量。最后用sumo.sumocfg把这些文件汇总起来。训练时只需要在Python里用一行命令启动仿真import traci traci.start([sumo, -c, config.sumocfg])训练阶段我强烈建议不用sumo-gui因为图形界面会拖慢仿真速度。要看可视化效果时再换回sumo-gui即可。2.3 TraCI接口让Python成为交通指挥官TraCITraffic Control Interface是SUMO提供的通信接口基于TCP协议。Python通过import traci连接上仿真进程后就能实时读取路网状态、控制车辆和信号灯相当于给仿真环境开了一个后门。日常用得最多的TraCI方法大概是这几个功能方法说明获取当前仿真时间traci.simulation.getTime()单位秒获取车道排队车辆数traci.lane.getLastStepVehicleNumber(laneID)通过具体车道ID查询获取路段等待时间traci.edge.getWaitingTime(edgeID)返回所有车辆等待总秒数获取当前信号灯相位traci.trafficlight.getPhase(tlsID)返回相位序号设置当前相位剩余时间traci.trafficlight.setPhaseDuration(tlsID, dur)控制绿灯延长/缩短切换相位序号traci.trafficlight.setPhase(tlsID, idx)直接跳转到指定相位项目里为了统一获取当前路口的综合状态我会封装一个StateExtractor类把所有TraCI查询集中在一起这样训练主程序看起来更干净后面加特征也好维护。3. DQN建模三件套状态、动作、奖励函数的设计逻辑3.1 状态空间给智能体一双能看路况的眼睛状态空间的设计直接决定智能体能不能学会策略。如果只给一个当前时间DQN什么都学不会如果给全路网几百辆车的坐标输入维度太大训练难度又会爆炸。需要找到一组既能描述路况、又足够精简的特征。我最终使用的状态向量由一个路口的关键信息拼装而成四个进口道方向的排队车辆数单位辆四个方向的平均等待时间单位秒当前相位编号和当前相位已经持续的秒数路口总等待时间单位秒。之所以要把当前相位编号和相位已持续时长放进去是因为DQN的动作决策跟当前信号灯在哪个状态密切相关。同时排队长度和等待时间这两类特征一个体现空间拥堵一个体现时间延误组合起来能帮助智能体平衡放行效率和公平性。有一点必须提醒喂给网络的输入一定要做归一化。排队长度可能到几十辆等待时间可能到几百秒这些数值直接放在一起会让神经网络初期的梯度被大数值维度主导导致训练很不稳定。我的做法比较简单排队车辆数除以路口最大车道容量等待时间除以一个经验上限比如180秒把大部分特征压到0到1范围内。3.2 动作空间把相位时间调整变成DQN的输出标题里的相位时间调整在代码上其实可以抽象成两类动作设计方式。第一种是把绿灯时间离散成几个固定档位比如动作0表示延长5秒动作1表示延长10秒动作2表示延长15秒动作3表示立即切换相位。这种方式动作空间更细但会让训练收敛变慢因为几个延长时间选项产生的状态差异很小Q值难分高下。第二种是把动作简化为二分类继续延长当前绿灯相位或者结束当前相位、进入下一相位。我最终选择的是这个方案。原因很简单单路口信号灯控制的本质决策就是切换还是不切换至于延长多久可以通过决策频率来间接控制。我的决策间隔设为10秒也就是说每10秒智能体评估一次如果选择保持绿灯自动加10秒如果选择切换信号灯就会在下一个仿真步进入黄灯过渡再进入下一相位。这个设计还有一个好处它完全符合真实信号机的工作逻辑。现实中信号灯并不可能每秒钟都在调整决策频率过低或过高都不合理。决策间隔太短会导致频繁切换形成绿灯刚亮就灭的抖动现象间隔太长又会让智能体反应迟钝。10秒是我在试验中觉得平衡性最好的值。3.3 奖励函数排队长度变化量怎么量化奖励函数是强化学习最容易被忽略、但也最决定成败的部分。信号灯控制最常见的优化目标是减小车辆平均等待时间、减少排队、提高通行量但这些目标直接作为奖励并不好优化因为它们都是长期累积量反馈稀疏且延迟严重。我采用的奖励公式是R - (L_t - L_{t-1})其中L_t是当前时刻路口所有进口道的排队车辆总数L_{t-1}是上一决策时刻的排队车辆总数。这个公式的含义非常直观如果这次决策让排队车辆减少了奖励为正让排队增加了奖励为负。智能体最大化累积奖励本质上就是在最小化整个仿真时段内的排队增量。为什么不用平均等待时间作为直接奖励因为等待时间的变化到决策之间有时序滞后而且受偶发车流波动影响大方差很高。排队长度变化量则是一个相对平滑、立竿见影的指标SRStability也更好。如果你想进一步优化可以在奖励里加一个切换惩罚项比如每次切换相位时额外减一个固定值防止智能体频繁抖动。我试验后发现加了切换惩罚反而会导致智能体过于保守该切换时不切换所以最终版本里没加。3.4 三件套的整体联动状态、动作、奖励这三者不是孤立的它们共同定义了马尔可夫决策过程。具体到运行流程每个决策时刻智能体根据状态选择动作动作改变信号灯相位时间相位时间影响车流运行车流运行产生新的状态和奖励然后进入下一个决策循环。只有三者都合理DQN才能真正学到东西。我见过不少同学跑不出效果第一反应是改网络结构和学习率其实问题往往出在状态特征不够或奖励函数设计不当上。4. 训练流程与源码拆解从经验回放到目标网络4.1 网络结构与超参数设置DQN的核心是用深度神经网络来逼近Q函数也就是在当前状态下每个动作能带来的未来累计奖励期望。我的网络结构非常简单三层全连接import torch.nn as nn import torch.nn.functional as F class DQN(nn.Module): def __init__(self, state_dim, action_dim): super().__init__() self.fc1 nn.Linear(state_dim, 128) self.fc2 nn.Linear(128, 128) self.fc3 nn.Linear(128, action_dim) def forward(self, x): x F.relu(self.fc1(x)) x F.relu(self.fc2(x)) return self.fc3(x)信号灯控制的状态维度本就不高一两百维以内没必要上CNN、Transformer这类复杂的结构。全连接网络加两到三层隐藏层就完全够用隐藏层宽度设为128或256即可参数太多反而容易过拟合。经验里比较关键的超参数我整理成了表格方便直接抄作业参数推荐值说明学习率1e-4调大容易出现Q值发散折扣因子gamma0.95兼顾短期排队和长期效果经验池容量20000太大旧样本占比过高batch size64常用值32偏慢128偏抖目标网络同步步数500太频繁等于没有目标网络epsilon初始值1.0早期充分探索epsilon最小值0.05保留一定随机性epsilon衰减步数5000线性衰减4.2 训练主循环的流程拆解训练主循环的代码骨架并不复杂关键是理解每个步骤为什么存在。最核心的逻辑如下for episode in range(EPISODES): traci.start(sumoCmd) state get_state() total_reward 0 while traci.simulation.getMinExpectedNumber() 0: action epsilon_greedy(state, epsilon) reward, next_state execute_action(action) replay_buffer.push((state, action, reward, next_state, False)) if len(replay_buffer) BATCH_SIZE: train_step() state next_state total_reward reward traci.close()每个episode相当于一次完整的仿真比如模拟3600秒的早高峰交通。仿真开始后循环不断读取状态、选动作、执行动作、攒经验、更新网络直到所有车辆都离开路网一个episode结束。execute_action这个函数是关键。当智能体选择保持当前相位时我会调用traci.trafficlight.setPhaseDuration(tlsID, currentRemain 10)让当前绿灯相位继续延长10秒当选择切换时就把当前相位的剩余时间设为1秒让SUMO自然进入黄灯过渡相位再切换到下一相位。这样做的目的是把相位切换的过渡处理交给SUMO内部机制避免手动跳相位导致车辆冲突。4.3 经验回放和目标网络到底解决了什么DQN相比传统Q-learning最大的改进就是引入了经验回放和目标网络这两个机制都是在解决训练稳定性的问题。经验回放是把智能体探索过的所有(state, action, reward, next_state)存进一个缓冲区训练时按batch随机采样。为什么要随机采样因为强化学习的样本之间存在高度时间相关性——车辆排队、放行这个过程中相邻几步的状态几乎差不多如果直接用连续样本训练网络会在这段局部模式上来回震荡学不到全局规律。随机采样打破了这个相关性让每次梯度更新尽量来自于多样的、独立分布的经验。目标网络是另一个稳定器。DQN的损失函数是Loss (r gamma * max_a Q_target(s, a) - Q_online(s, a))^2注意这里计算目标值时用的是Q_target网络而不是正在更新的Q_online网络。因为如果用同一个网络同时计算预测值和目标值每一轮更新都会让目标和预测一起动优化过程就会变成追一个不断移动的靶子很容易震荡甚至发散。目标网络的做法是定期比如每500步把在线网络的参数复制过来固定一段时间让目标值相对稳定训练才能收敛。4.4 源码目录结构项目源码的组织方式尽可能清晰核心目录如下project/ ├── env/ │ ├── config.sumocfg # SUMO仿真配置 │ ├── net.net.xml # 路网文件 │ └── rou.xml # 车流文件 ├── agent/ │ ├── dqn.py # DQN网络定义 │ ├── replay_buffer.py # 经验回放缓冲区 │ └── train.py # 训练主循环 ├── utils/ │ ├── state_extractor.py # 从TraCI获取状态与奖励 │ └── config.py # 超参数配置 └── evaluate.py # 训练后评测脚本源码本身并不复杂但把它拆成环境、算法、工具三个模块会让你后续扩展起来非常舒服。比如想换成PPO算法只需要替换agent目录里的文件想换成多路口场景只需要在utils里增加一个状态聚合器。5. 实验结果对比与参数调优DQN比固定配时强在哪5.1 评价指标与固定配时基线训练完成后需要用一套客观指标来评估DQN到底有没有用。我做了两轮实验第一轮把DQN和固定60秒周期配时做了对比第二轮尝试了不同的车流强度。评测用的核心指标有三个平均等待时间所有车辆在路口前停车的平均总时长平均排队长度每个决策时刻各进口道排队的车辆数均值路网吞吐量仿真时段内通过路口的车辆总数。固定配时的基线直接用SUMO默认信号机方案不做任何优化。评测时把DQN的信号灯控制程序接到同一套路网和车流文件上其他条件完全一致只让信号灯决策逻辑不同。5.2 训练曲线怎么解读DQN训练初期通常会有一段表现很差的阶段。我的经验里前20个episode奖励值甚至明显低于固定配时的对照值因为epsilon很大智能体在疯狂探索经常做出不合常理的切换动作导致路口频繁出现放行空车道、堵住车流量大的方向这种尴尬局面。这其实是正常现象不要慌。随着训练进行epsilon逐渐衰减经验池里累积了足够的有效样本网络开始学到排队长的方向优先放行这类规律奖励曲线会逐步上升并超过基线。大概训练到150个episode左右奖励曲线变得平稳这时再跑评测DQN的每个指标都会有可观提升。我在一次典型的对比实验里得到的数字大概是在中等流量下DQN比固定配时降低了约18%的平均等待时间和22%的平均排队长度在轻流量场景下两者差别不大因为车本来就不堵在重流量或流量突变场景下DQN的优势会进一步拉开有时等待时间能降低30%以上。这说明DQN强项在于应对不均衡、不稳定的车流而不是替代所有配时方案。5.3 影响收敛的关键参数训练过程中有几处参数是真正决定成败的我单独拿出来说。第一个是奖励的尺度。如果奖励数值波动太大比如排队长度变化从-30到30网络会对梯度方向非常敏感必须把奖励除一个缩放因子我这里除以了最大排队长度让奖励落在[-1,1]区间训练稳定很多。第二个是决策间隔。10秒是我最后选定的值但建议你做一次敏感性分析。间隔太短信号灯频繁变动车流根本来不及响应间隔太长智能体在两次决策之间会错失很多优化机会。用不同流量脚本做几组对照你会找到最适合自己场景的数字。第三个是目标网络同步步数。设成太小比如每100步同步一次目标网络几乎等于在线网络失去意义设成太大比如每5000步目标值和当前预测偏离过大训练早期容易不稳定。500到1000步是一个经验合理区间。6. 踩坑记录与个人心得十几个小时仿真换来的经验6.1 相位切换导致的处处红灯问题我第一次把切换动作设为直接调用traci.trafficlight.setPhase跳转到下一个绿灯相位训练出来的效果惨不忍睹路口经常出现四个方向全是红灯的状态车辆全部停摆。原因是SUMO信号灯内部是有相位顺序的直接跳转跳过了黄灯过渡阶段破坏了信号灯状态机的内部一致性。后来我把切换逻辑改成设置当前相位剩余时长为极短值1秒让信号灯按SUMO自己的规则进入黄灯过渡相位再进入下一个相位问题就消失了。这里的一个经验是不要跟仿真器内置的状态机对抗尽量顺着它的机制去做控制否则你会在很多莫名其妙的仿真异常上浪费时间。6.2 奖励函数方差过大导致训练崩溃还有一个坑出现在换用平均等待时间变化量作为奖励时。等待时间受单辆车极端值影响很大偶尔一辆车等了3分钟这个变量的变动会让奖励瞬间产生很大波动DQN的损失函数跟着震荡训练曲线一路发散。后来我把奖励改成排队长度变化量再乘一个缩放系数训练曲线立刻稳定下来了。如果你的任务必须用等待时间做奖励我建议对单值进行截断处理比如最大等待时间封顶180秒或者对奖励做clip到[-1,1]总之不要让极端值主导梯度方向。6.3 如果打算扩展到多路口该怎么做单路口跑通之后很多人会想扩展到多路口协调控制。我的建议是不要简单粗暴地把每个路口都放一个独立DQN那样多个智能体在共同环境中各自优化容易出现震荡。更稳妥的方式是先做一个多路口共享参数的单智能体方案把所有路口的状态拼接成一个大向量输入同一个网络输出所有路口的动作这样训练稳定且代码改动不大。再往上的图神经网络或多智能体强化学习属于进阶方向建议先把单路口基本功打扎实再碰。最后再分享一个小技巧训练时每隔几个episode就把当前模型保存一份同时跑一次固定配时基线作为对照。这样你能在训练过程中实时观察智能体是否真的超过了基线也能在崩溃时回滚到之前效果最好的模型。我在这个项目里前前后后跑了十几个小时的仿真大部分时间都花在调参和修bug上只有把环境、建模、训练链路都理顺了强化学习在信号灯上的效果才真正凸显出来。本文还有配套的精品资源点击获取
网站建设高端定制企业官网