ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

SUMO交通信号自适应控制:五套算法仿真对比框架详解

SUMO交通信号自适应控制:五套算法仿真对比框架详解

简介:一套基于SUMO仿真平台的多算法交通信号自适应控制项目,面向智能交通方向的研究者、工程师及进阶学习者,用于对比DQN、DDPG、韦氏算法、最大压力控制与自组织交通灯在交叉口信号优化中的实际表现。压缩包共65个文件,核心为37个Python脚本(模型训练、神经网络、信号控制、指标统计),另有Shell训练脚本、SUMO路网配置(sumocfg/xml)、可视化输出图与说明文档,整体仅2.94MB,便于直接开展实验。已有46人学习/下载。资料内含单/双交叉口仿真场景、完整的强化学习环境封装、调参优化脚本及结果图表,可支撑从算法建模到效果对比的完整流程;其中DQN与DDPG侧重深度强化学习策略,韦氏、最大压力与自组织方法提供了传统及分布式控制参照,适合用于课程设计、毕业课题或个人研究,能帮助快速理解各类控制策略的建模思路与运行机制。

1. SUMO交通信号自适应控制:为什么我把五套算法放进同一个仿真框架里对比

一个十字路口,车流早晚高峰差异大,固定配时很容易出现一边空放、一边排长队的局面。把DQN、DDPG、最大压力、自组织交通灯和韦氏算法放到同一套SUMO路网里跑,是验证“学习型控制是否真的比启发式控制强”最直接的做法。这份资源给的不是训练好的成品,而是一整套可跑的实验框架:net.xml定义路网,sumocfg定义仿真参数,src下的Python模块负责训练与指标统计,根目录的Shell脚本负责启动训练和批量出图。适合两类人:一类是交通工程方向的研究生,需要快速拿强化学习和传统控制方法做对比实验;另一类是刚接触强化学习落地的工程师,想找一个能跑DQN与DDPG的仿真环境。我拆完之后的建议是:先不要动任何算法文件,把single.sumocfg跑通,再往里面加自己的状态和奖励。注意这套资源按个人学习用途开放,实验归实验,别直接把里面的代码封装到商业项目里。

2. 项目结构与算法边界:从run.py到五套控制器的选型取舍

2.1 目录与文件分工:训练入口、路网与仿真模块各管一段

拿到压缩包先不要急着运行,先看目录。根目录的run.py是总入口,负责解析算法类型、加载路网配置、拉起SUMO并启动训练或评估;train_dqn.sh和train_ddpg.sh是Shell启动脚本,封装了DQN和DDPG两种强化学习算法的训练命令;hp_optimization.py做超参搜索,gen_results.sh批量跑算法对比,graph_results.py把结果画成图片,graph_training.py画训练过程曲线。这个拆分思路很典型:入口、训练、测评、画图分开,方便在服务器上批量跑实验而不必每次都手工改参数。

networks目录下是核心仿真配置:single.sumocfg对应单交叉口场景,double.sumocfg对应双交叉口场景,single.net.xml和double.net.xml分别是两种场景的路网文件。拿到手先跑single,单交叉口状态空间小,训练一轮很快,能确认环境通不通;跑通了再切double,看算法在多交叉口协同下是否还稳得住。

src目录是真正的代码主体。sumosim.py控制仿真主循环,learnerproc.py负责learner进程,distprocs.py做分布式进程通信,这是典型的“仿真与训练分离”设计。trafficmetrics.py统计平均等待时间、排队长度等指标,trafficsignalcontrollers里放五种信号控制器,rlagents里放强化学习训练器。用下面的命令快速核对一下你的解压目录跟我描述的是否一致:

cd <项目根目录> find . -maxdepth 2 -type f | sort

-maxdepth 2限制递归深度避免刷屏,-type f只看文件不看目录。如果发现缺了networks、src、run.py中的任何一个,先补完整再往下走。我把主要文件的职责整理成表,改代码前先对照它找位置:

文件/目录职责改动时看什么
run.py命令行入口,选择算法与仿真配置--algo、--cfg参数名
networks/single.sumocfg单交叉口仿真参数SUMO版本兼容性
src/sumosim.py仿真主循环与traci交互步长、仿真时长、状态采集频率
src/trafficmetrics.py等待时间、排队长度统计reward的数据来源
src/neuralnets.pyQ网络、Actor与Critic网络定义网络层数、激活函数
src/trafficsignalcontrollers五种控制器实现与工厂注册新增算法时扩展的位置

我自己看这类代码的习惯是:先画一条数据流——车流从sumocfg生成,sumosim.py每步读取路网状态,交给trafficsignalcontrollers里的控制器决策信号灯,trafficmetrics.py统计效果,强化学习算法跨进程拿状态、给动作、收reward。把这条链路理清楚,后面改任何一环都知道会影响什么。

2.2 DQN与DDPG:离散相位与连续动作的两条技术路线

DQN处理的是离散动作空间。单交叉口场景里,信号相位通常被离散成固定的几组:南北直行、南北左转、东西直行、东西左转。DQN要做的是在某个状态下从这些相位中选择一个最优动作,本质上是学一个Q值函数,输出每个动作的期望累计收益,再按epsilon-greedy策略做探索。项目里这一块放在neuralnets.py和nn_factory.py中:nn_factory.py负责根据配置创建不同结构的网络,neuralnets.py定义Q网络的层结构。如果你发现DQN训练不收敛,我一般会先检查这里的状态输入是不是覆盖了排队长度、等待时间、车道占有率这几项核心信息,而不是急着加网络层数。快速定位网络定义位置可以执行:

grep -n "class.*Network\|def forward" src/neuralnets.py | head -30

grep把类名和forward方法行号打出来,head -30限制输出条数。看到网络结构之后,下一步要确认的是动作空间定义在哪个文件。通常动作数量决定Q网络输出维度,如果动作定义和网络输出对不上,训练直接报shape mismatch。

DDPG面对的场景不一样。交通信号并不一定是只能离散切换,也可以把绿灯时长当成连续输出动作。DDPG用Actor网络输出连续动作,Critic网络评估当前状态下这个动作的Q值,配合经验回放和目标网络来稳定训练过程。连续动作下信号灯时长可以平滑调整,比如南北绿灯从20秒到25.3秒,这对缓解信号切换带来的交通流冲击有帮助。项目里对应实现集中在rlagents目录,learnerproc.py负责更新网络参数,distprocs.py负责在仿真进程和训练进程之间传递状态、动作与奖励。

需要注意,DQN和DDPG虽然都叫强化学习,踩坑点完全不同。DQN的典型问题是离散动作下的探索不足,epsilon衰减太快就会过早锁定到一个次优相位;DDPG的典型问题是连续输出容易震荡,奖励尺度设计不好经常在训练中期出现动作突变。后面避坑章节会展开讲这两个问题在实际运行中怎么识别。

2.3 韦氏、最大压力与自组织:不依赖学习的启发式控制策略

很多做RL交通控制的人容易忽视一个事实:在没有与学习型控制器对比的情况下,训练曲线再好看也没有说服力。max pressure、self-organizing和项目中的韦氏算法,就是几个强启发式控制器,用来当公平的baseline。

最大压力控制是分布式控制里很经典的一族方法,核心思路是计算交叉口各相位对应的“压力”,压力可以近似为排队长度之差或者占用率之差,每次选择压力最大的相位放行。它不学习、不调参,只看当前局部状态,在理论上对网络交通流有保障。在single和double场景下跑这个算法,能得到一个相当有竞争力的参考线——如果DQN训练完连最大压力都打不过,大概率不是代码bug,是状态或reward设计出了问题。

自组织交通灯的思路更简单:每个信号灯根据本地检测到的车流情况独立决策,车辆到达率达到某个阈值才切换相位,否则保持当前相位。这样避免了全局优化带来的控制延迟,在低流量场景下表现很好,缺点是高流量下容易某个方向长时间不放行。

韦氏这个命名容易让人联想到Webster配时公式,但项目里的实现是按车辆轨迹分析来做的,关注的是车辆在交叉口的转向、排队和通过轨迹,从轨迹数据推断应该延长还是切换相位。它介于“固定配时”和“真正自学习”之间,属于一种利用短期轨迹信息做动态绿灯延长的控制器。

这三种控制器实现都在trafficsignalcontrollers目录下,trafficsignalcontroller.py定义统一接口,tsc_factory.py按名字注册和创建控制器。想确认目前注册了哪些控制器,可以执行:

grep -n "register\|__init__" src/trafficsignalcontrollers/tsc_factory.py | head -20

如果后面想加自己的改进算法,照着这个文件的注册方式加一个类即可,不需要动sumosim.py主循环,这一点对扩展实验非常友好。

3. 环境准备与单交叉口首跑:让SUMO先动起来再谈训练

3.1 SUMO安装与Python绑定:SUMO_HOME、PYTHONPATH和traci

在Ubuntu/Debian系系统上安装SUMO最省事的方式是apt直接装:

sudo apt update sudo apt install sumo sumo-tools echo 'export SUMO_HOME=/usr/share/sumo' >> ~/.bashrc echo 'export PATH=$PATH:$SUMO_HOME/bin' >> ~/.bashrc echo 'export PYTHONPATH=$SUMO_HOME/tools:$PYTHONPATH' >> ~/.bashrc source ~/.bashrc sumo --version

这段脚本做四件事:安装SUMO主程序和工具集;把SUMO_HOME指向程序目录;把bin目录加入PATH方便命令行直接调用sumo和sumo-gui;最关键的是把tools目录加入PYTHONPATH,因为项目import traci依赖这里的Python库。如果你用Windows或者macOS,路径会不一样,但思路相同——只要能配置好这些环境变量,sumo和Python这侧都能启动。配置结束后用sumo --version确认SUMO能运行,再在Python侧验证traci。

python -c "import traci"

这条命令不打印任何信息才是好事,import成功就说明traci绑定完成。如果这里报ModuleNotFoundError,多半是PYTHONPATH没生效或者Shell环境没重开,我一般会直接在当前终端source一遍.bashrc再测。如果你是拿vscode开发,建议直接在vscode终端里配好环境再启动,免得跑训练脚本时一会儿找不到sumo、一会儿找不到traci。

项目对Python版本没有写死,但强化学习训练涉及numpy和pytorch,建议Python 3.8以上。requirements.txt里列了依赖,安装时用pip install -r requirements.txt即可;如果网络源慢,换成国内pip镜像源会快很多,这是Python安装依赖的常规步骤。

3.2 跑通single.sumocfg:从sumo-gui启动到traci接管

环境准备好之后,先用图形界面确认路网没有问题:

cd <项目根目录> sumo-gui -c networks/single.sumocfg

执行后SUMO窗口里应该出现一个十字路口,车辆按设定好的路线生成并行驶。这一步的意义是验证net.xml路网与sumocfg配置在当前SUMO版本下能被正常解析。如果连这一步都跑不出车,后面所有训练都无从谈起。常见的不通过原因集中在版本差异和路径问题,版本差异详见避坑章节。

图形界面只是第一步,项目真正运行是准备用run.py拉起SUMO,通过traci做实时控制。模拟一下这个过程,最小可用的traci连接代码如下:

import traci sumo_cmd = ["sumo-gui", "-c", "networks/single.sumocfg"] traci.start(sumo_cmd) # 启动SUMO,建立本地连接 for step in range(1000): traci.simulationStep() # 推进一个仿真步 # 在这一步读取状态、修改信号灯、收集指标 traci.close()

traci.start()启动SUMO子进程,simulationStep()每次推进一帧,默认步长是100ms。在仿真步之间可以读取车辆位置、速度、排队长度,也可以直接改信号灯相位。对强化学习项目来说,这个循环就是agent与环境交互的接口:sumosim.py本质上做的事情就是把这段循环拆到多进程里。

跑通之后的判断标准不是“SUMO窗口打开”而是“车辆在路口正常排队、绿灯放行、没有车辆卡死不动的现象”。你可以在窗口里观察到车辆积压时排队长度增长、绿灯放行时车队消散,说明仿真环境是活的状态。再进一步,直接执行:

python run.py --algo maxpressure --cfg networks/single.sumocfg

如果命令行能输出每个episode的平均等待时间,说明项目自带的数据链路已经完全贯通。到这里,这份SUMO交通信号自适应控制项目才算真正在你的机器上落地。

3.3 用net.xml读懂路网参数:edge、lane与junction之间的关系

跑通之后,建议打开single.net.xml认真读一遍。SUMO的路网核心是edge、lane、junction三类元素:edge是一条道路,lane是edge上的具体车道,junction是道路之间的连接点。红绿灯逻辑在tlLogic节点里定义,下面挂的是phase列表,每个phase包含时长和灯色状态。

元素作用看什么参数
edge道路段id、function(normal/source/sink)
lane具体车道speed限制、允许的车辆类型
junction交叉口或路口连接点type=traffic_light表示有信号灯
tlLogic信号灯方案phase列表、周期时长、红灯/绿灯状态
connection车道间的转向连接左转、直行、右转关系

理解这个结构对后面自定义场景很重要。比如你想把单交叉口改成三相位,需要动tlLogic里的phase定义;想加一条车道,需要动edge和lane节点。用netedit可视化编辑比手改xml方便,但手改能让人更清楚SUMO内部的数据组织方式。我一般会先用netedit打开single.net.xml,再把对应节点在源码里对照着看一遍,这样后续改路网时出错率明显下降。

4. DQN与DDPG的复现路径:训练脚本、超参搜索与结果绘图

4.1 train_dqn.sh与train_ddpg.sh:Shell入口打开训练流程

根目录的train_dqn.sh是把DQN训练命令固化成脚本,方便反复执行。项目同时提供了.zbak后缀的备份文件,注意不要直接运行带.zbak的文件,双击运行前先看一眼文件是否具备可执行权限。典型训练启动脚本长这样(不同机器上你大概率需要改环境名和路径):

#!/bin/bash # 训练DQN的Shell启动脚本(常见写法示例) source ~/anaconda3/etc/profile.d/conda.sh conda activate trafficrl python run.py \ --algo dqn \ --cfg networks/single.sumocfg \ --epochs 200 \ --steps 3600 \ --save-dir output/dqn

作用是把Python入口、算法名、仿真配置、训练轮数集中放到一个可重复执行的命令里。trafficrl是虚拟环境名,每个人机器上不一样,我习惯先conda env list确认。--epochs控制训练轮数,--steps控制每轮仿真步数,步长100ms时3600步对应6分钟仿真时间。--save-dir指定模型和日志保存位置。DDPG对应的脚本结构一致,把--algo换成ddpg,最多再加几个DDPG特有的参数。

如果执行过程中报“command not found”,通常是bash脚本第一行的#!/bin/bash前面多了空格,或者脚本没有执行权限,chmod +x train_dqn.sh就能解决。如果报Python模块缺失,就是requirements.txt没装完。Shell脚本作为工程入口最大的价值不是炫技,是把一组容易记错的参数固定下来,每次训练跑同一套配置,实验结果才可复现。

4.2 hp_optimization.py:网格搜索里的参数联动问题

DQN这类算法的效果对学习率、折扣因子、batch size非常敏感,手动调一轮动辄几小时。hp_optimization.py就是为了解决这个问题,把一组候选参数组合依次跑一遍,把每组参数对应的最终指标记录下来。常见实现逻辑可以抽象成下面这个简化版:

import itertools search_space = { "learning_rate": [1e-4, 3e-4, 1e-3], "gamma": [0.90, 0.95, 0.99], "batch_size": [32, 64, 128], } keys = list(search_space.keys()) for values in itertools.product(*search_space.values()): params = dict(zip(keys, values)) # 用 params 覆盖默认参数,启动一轮训练 # 把这轮最终平均等待时间追加到结果文件 print(params)

itertools.product会做笛卡尔积,把三个超参的所有组合都枚举出来。实际项目里hp_optimization.sh会串行或并行调用这些组合,结果写入pickle或文本文件供后续分析。参数组合数量增长很快:上面三个参数三档取值就是27组训练,单组训练就要几十分钟,全部跑完要很久。所以我个人建议先跑小范围粗搜,锁定learning_rate的数量级,再缩小范围精调。gamma在交通信号控制场景里通常选0.9到0.99之间,因为车流状态有一定持续性但又不至于需要特别长远地规划。超参这东西本身有几分玄学,固定其余变量、一次只动一个参数,比交替修改多个参数容易定位问题。

import pickle data = pickle.load(open("results.pkl", "rb")) for entry in data: print(entry["params"], entry["final_wait_time"])

这是读取已保存结果的示意代码,项目里picklefuncs.py处理了这部分序列化逻辑。最终指标可以看final_wait_time这类字段,对比不同超参组合时,注意每个组合是否用了相同的训练轮数。轮数不一样,指标之间没有可比性。

hp_optimization.py产出的结果最终通过graph_results.py画成tsc_hp.png这类图,你会看到不同超参组合的训练曲线或结果对比。这里容易犯的错是把所有组合的结果混在一起画,曲线互相重叠看不出差异。我一般在代码里给每组参数加一个label前缀,画图时按label分色,问题就清楚多了。

4.3 gen_results.sh与graph_results.py:批量测评与对比图输出

多算法对比实验最繁琐的是“重复跑同一套流程”,gen_results.sh的作用就是把“跑所有算法”这个动作自动化。脚本内部大致是这样一个循环:

#!/bin/bash # 批量运行多种交通信号控制器 for algo in dqn ddpg maxpressure selforg webster; do python run.py --algo "$algo" --cfg networks/single.sumocfg done

这里algo名字要和tsc_factory.py里注册的类名对应,项目在命名上统一了算法名,所以循环才能通用。跑完后所有的指标数据被写入本地文件,再由graph_results.py读取。samples目录下能看到travel_time.png、intersection_moe.png等输出图:travel_time.png重点关注,它给出平均旅行时间随时间或训练轮数的变化;intersection_moe.png对应多个评价指标的对比。看这些图的时候,务必注意纵轴单位的可比性——旅行时间用秒、等待时间用秒、排队长度用辆,横轴对齐后才不会把不同尺度的曲线强行比较。

如果gen_results.sh里某个算法跑到一半崩了,整个脚本默认会中断。我在实操中会在每个算法命令后加|| echo "FAILED: $algo",让脚本跳过失败的算法继续跑后面的,最后统一看哪些失败。不要小看这个习惯,批量跑实验时它能帮你省一整轮返工时间。

5. 避坑与排查:SUMO版本、路径与训练过程的五个常见问题

5.1 打不开single.net.xml:相对路径和工作目录不一致

现象是运行sumo或run.py时报错,提示Can not load network或Could not open network file。这个错我见得太多了,绝大多数情况不是文件损坏,而是当前Shell工作目录不在项目根目录,sumocfg里用相对路径引用net.xml,shell没有按预期目录运行。解决方式很直接:进到项目根目录再执行命令,或者把所有命令里的相对路径改成绝对路径。排查时先用pwd确认当前位置,再用ls确认networks目录是否真的存在。我现在的习惯是所有Shell脚本第一行加cd "$(dirname "$0")",让脚本不管从哪里调用都固定到自身所在目录,这个习惯从那次折腾一小时路径问题之后就再没改过。

5.2 DQN训练不收敛:奖励稀疏与epsilon衰减过快

现象是训练几十轮之后平均等待时间不降反升,或者一直围绕一个较高值抖动。先排除代码bug,用baseline控制器(最大压力或固定周期)跑同一路网,看等待时间的合理区间在哪里。如果baseline没问题,问题就出在reward设计或者epsilon衰减过快:奖励函数稀疏,agent长期拿不到有效反馈;或者epsilon从1.0衰减到0.05的速度太快,探索不充分就早收敛了。解决思路是在trafficmetrics.py统计指标基础上,把reward改成负等待时间或负排队长度,并观察训练每个epoch的reward曲线,确认有没有逐步上升。调参时也不要只看最后几个epoch,要结合graph_training.py输出的训练过程曲线判断。

5.3 .zbak文件不能直接跑:备份与可执行权限的区分

根目录里train_ddpg.sh.zbak、hp_optimization.sh.zbak这些文件是原始文件的备份。Linux下文件不是按后缀决定能否运行,但脚本内里的命令路径和当前项目结构不一致时就会出错。明确一点:要运行的是去掉.zbak的版本,不是把.zbak当脚本用。如果项目里没有不带.zbak的同名文件,就用cp train_dqn.sh.zbak train_dqn.sh恢复备份再改。查看文件类型可以用file train_dqn.sh.zbak确认内容,再用head -n 5预览前几行是不是Shell脚本。保留.zbak文件是很多作者留下来的习惯性操作,不要删,它往往是最原始的可用版本。

5.4 double.sumocfg不出车:route定义和edge id对不上

现象是double.sumocfg能启动,路口长时间没有车辆生成。多数原因是sumocfg里车辆流定义与net.xml的edge id对不上,或者车辆生成参数太稀疏,前几百步看不到车。排查顺序:先用sumo-gui加载double.sumocfg,看GUI报错区域指示的是哪个文件;然后打开sumocfg检查routes文件路径;最后看vehiclegen.py里车辆生成相关的代码,确认生成间隔是多少秒、每条route上分配多少辆车。项目用vehiclegen.py动态生成车流的好处是不同算法可以共用同一套车流输入,但前提是edge id必须和路网里实际存在的id一致。

grep -n "route" networks/double.sumocfg grep -n "edge" networks/double.net.xml | head -20

第一条命令看sumocfg引用的route定义,第二条命令列出net.xml里的edge id。对照net.xml里的edge id改掉route定义,车辆马上就能恢复正常生成。

5.5 DDPG输出NaN:奖励尺度与学习率问题

现象是DDPG训练中期动作输出变成NaN,loss也跟着爆炸。DDPG对奖励尺度和学习率都非常敏感。当reward数值在几百上千量级,Critic输出的Q值梯度会直接爆炸;如果奖励绝对值过大,训练快速发散的症状之一就是网络权重出现NaN。建议第一件事先在rlagent.py里把reward打印出来看取值范围,如果波动过大,用clip操作把reward限制在[-1,1]或先除以一个常数做归一化。第二件事把Actor和Critic的学习率降一个数量级,DDPG稳定性比DQN差,1e-3是危险区,1e-4起步是常见做法。做完这两步再重启训练,比反复调整网络结构有效得多。拿着一套打印了NaN的权重去分析任何训练过程都没有意义,先让数值稳定下来再谈效果。

6. 进阶:改奖励函数与自定义车流,把算法对比做出可信度

真正拿这套SUMO项目做实验时,值得改的就是三处:reward、车流、评价口径。reward直接影响DQN和DDPG的学习方向。在rlagent.py里,reward通常由trafficmetrics.py输出的指标组合而成,常见形式是负的加权和:

reward = -metrics["avg_waiting_time"] - 0.5 * metrics["avg_queue_length"]

负等待时间让agent学会减少延误,负排队长度是防止它只照顾绿灯方向而放任其他方向积压。0.5是惩罚权重,取决于你更看重通行效率还是公平性。调权重时每次都固定其他变量,只动一个权重,否则多个变量一起改,谁起的作用都说不清。

车流方面我建议改vehiclegen.py:把随机种子固定下来,不然每轮训练的车流完全随机,算法之间的差异会混入大量噪声。到达率、起始时间、不同进口道的流量比例都在这个文件里调整。固定种子之后,跑一次固定周期信号作为车流基准值,再跑最大压力,最后跑DQN/DDPG,这样对比才有意义。

验证方法上,我个人的一个习惯是每换一个算法版本,就记录三组数字:平均等待时间、平均旅行时间、每个交叉口的最大排队长度。graph_results.py的图可以看整体趋势,但这三组数字要记在实验笔记里,它们能快速告诉我一个算法是不是真的在进步,而不是只在某个指标上刷得好。

这条路走下来,我对交通信号强化学习项目最大的感受是:真正让你翻车的往往不是算法写不对,而是环境没跑通、奖励没对齐、对比基座没立好。从那以后,我每次拿到类似的交通控制工程项目,都会强制先跑一轮固定周期信号作为baseline,再跑最大压力,最后才上DQN或DDPG。这套流程既可以复现别人项目里的结论,也可以把自己新写的算法放到同一把尺子上量一量。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表