简介:基于多智能体算法的城市交通信号控制仿真项目,是一份面向智能交通研究者、控制系统开发人员及高校相关专业学生的完整源码包,旨在应对传统信号控制在复杂交通流下的管理瓶颈,并为信号优化策略提供可重复、低成本的实验环境。系统将每个交通信号灯建模为独立智能体,通过多智能体之间的信息交换与协调,动态调整信号相位和时长,覆盖交通流建模、智能体设计、信号控制策略、评价指标和实时仿真等关键技术环节。压缩包共173个文件,包体约49.64MB,主要内容包括C++/Python编写的核心算法与仿真引擎源码、JSON配置数据、CMake构建脚本以及Markdown/PDF项目文档,目录组织规范,便于按模块查阅和二次开发。目前已有241人学习浏览,适合具备一定编程基础并希望深入多智能体交通控制领域的读者。借助这份材料,可还原典型场景下的信号配时优化实验,理解多智能体协作机制,并可基于源码扩展自己的控制策略与评估体系,获得从理论到落地的完整实践经验。
1. 多智能体算法做城市交通信号控制:这套仿真系统到底解决什么问题
先说结论:基于多智能体算法的城市交通信号控制仿真系统,本质上是把每个路口信号灯当成一个能独立做决策的智能体,让它们通过与周围车辆、上下游路口的交互来实时调整配时,而不是傻傻跑固定红绿灯周期。很多团队复现这类项目失败,并不是算法代码写错了,而是栽在路网搭建、相位定义和仿真器连接这些“非算法”环节上。这套仿真系统适合两类人:一类是做智能交通方向毕业设计或课题的研究生,另一类是刚接触车路协同、想验证信号控制策略是否有效的工程师。它不承诺真实路口上线,但能让你在几分钟内跑通一个单路口到多路口控制闭环,把“自适应控制”从口头概念变成可量化延误的曲线图。读完这篇文章,你会清楚这条路线的可行边界、最小实现代码,以及那些让人反复翻车的坑位。
2. 为什么城市交通信号控制仿真方案要用SUMO:选型先于写代码
2.1 多智能体算法落地的前提:仿真器必须支持外部实时控制
多智能体算法与传统定时控制最大的差别,在于决策是动态的、分布式完成的。每个智能体需要持续读取当前路口排队长度、上下游占有率,再根据策略算出下一时刻相位切换动作。这就对仿真器提出了硬性要求:必须支持外部程序在线控制仿真步骤,并且能在运行中修改信号灯状态。
如果仿真器只能离线跑完再分析数据,那多智能体算法基本没法落地。常见的做法是用SUMO的TraCI接口,它允许Python脚本按步长驱动仿真,像拧水龙头一样实时下发信号灯命令。MATLAB/Simulink也能做类似的交通流仿真,但在接口灵活性和开源可复现性上不如SUMO。这里没有贬低MATLAB的意思,只是对于“我要跑多智能体信号控制”这个具体场景,SUMO的TraCI生态最成熟,网上能查到的开源案例也最多。
2.2 SUMO、CityFlow、MATLAB三选一:一张参数表说清
很多从项目源码入手的人,第一步会纠结仿真器选型。这里把三套常见方案的核心差异列成参数表,你按自己的场景对号入座即可。
| 对比维度 | SUMO | CityFlow | MATLAB/Simulink |
|---|---|---|---|
| 外部实时控制接口 | TraCI,Python/C++均可 | 自带Python API,但不支持逐秒级复杂干预 | 需要Simulink Real-Time,配置成本高 |
| 路网编辑 | netedit图形化工具 | 写JSON路网定义 | 手动建模,路网复杂时效率低 |
| 多路口扩展 | 原生支持路网级仿真,智能体间通信可自行通过TraCI实现 | 支持,但社区资料偏少 | 适合单路口控制策略验证 |
| 车流随机性 | 随机车流、概率转向、行程文件均可配置 | 支持基本随机车流 | 需要额外工具箱 |
| 开源可复现 | GPL开源,部署简单 | 开源,但版本迭代较慢 | 商业授权,交付不便 |
我一般会直接选SUMO,因为源码包里多数时候也默认带了SUMO相关配置。如果你下载的zip里出现的是CityFlow或MATLAB的.mat文件,那就先读README再决定要不要换环境。版本上建议SUMO 1.18或更高版本,老版本TraCI的traci.trafficlight接口行为有差异,容易遇到“代码没变但结果对不上”的玄学问题。
2.3 解压zip包后先找这三个文件,少走半天弯路
拿到一个“优质项目实战.zip”后,第一反应别急着跑,先把目录结构打开按优先级找三个文件。第一个是README.md或环境要求.txt,里面一般写了Python版本、SUMO版本和依赖库安装顺序。第二个是.sumocfg结尾的配置文件,它是整个仿真入口,指向路网文件、车流文件和仿真参数。第三个是主控制脚本,通常叫main.py或run_experiment.py。
我遇到过很多次这样的情况:代码本身没毛病,但zip解压后多了一层嵌套目录,导致脚本相对路径找不到.net.xml文件。所以拿到源码包先把目录整理成“项目根目录下直接有主脚本和配置”的结构,再开始改参数。这一步虽然枯燥,但能避开之后所有“文件找不到”的报错。另外注意zip包里的路网文件可能是用绝对路径引用的,这在别人机器上跑不通,需要统一改成相对路径。
3. 先把路网和车流搭对:复现城市交通信号控制仿真最容易翻车的前置条件
3.1 路网生成:用netedit画一个十字路口作为最小可运行样例
复现多智能体信号控制前,最好先有一个能跑的“最小路网”,不然算法再漂亮也调不出口碑来。常见做法是直接用SUMO自带的netedit工具手工画一个四向十字路口,保存后会自动生成.net.xml文件。画路网时最核心的步骤是:先创建四条双向道路,每条道路在与路口衔接的地方分成两个方向的车道,然后用“TL连接”把路口信号灯和对应车道绑定。
如果你是第一次用netedit,不要急着导入真实地图。真实地图的坐标偏移和车道数往往不适合初期验证。先在空白画布上画一个每条进口道都是“左转+直行+右转”三车道配置的十字路口,然后让netedit自动生成内部连接。画完后关闭netedit,用netconvert检查一遍路网连通性,命令如下:
netconvert --sumo-net-file cross.net.xml --output-file cross_checked.net.xml --geometry.remove这个命令的作用是重新解析一次路网文件,去掉冗余几何坐标,并检查车道之间的连接是否符合SUMO规则。如果内部连接有缺失,它会在终端输出警告,这种警告在后续仿真里会表现为车辆到路口突然消失或绕路。检查通过后再用sumo-gui打开这个路网,肉眼确认四条进道方向正确,信号灯位置落在路口中心附近。
3.2 车流定义:用随机车流验证信号算法时的三个关键参数
路网就绪后,就该配置车流了。绝大多数城市交通信号控制仿真的车流来源有两种:固定OD行程文件(.trips.xml)和随机车流。初次复现建议用随机车流,因为参数简单、生成快,能让算法在流量波动下展示自适应能力。在SUMO中生成随机车流通常用randomTrips.py脚本:
python "$SUMO_HOME/tools/randomTrips.py" -n cross.net.xml -r trips.xml -e 3600 -p 2 --random-flows参数含义:-n指定路网文件,-r指定输出车流文件,-e 3600表示模拟3600秒,-p 2表示平均每2秒产生一辆车,--random-flows表示按随机流方式生成更自然的车流分布。这里有一个关键点:-p值的大小直接决定路网拥挤程度。当p=2时是轻交通流,每进口道排队很少;p=0.5时就是高负载,能明显看出固定配时和智能体控制的延误差距。
除此之外还需要调trip的终点分布。SUMO默认将车辆随机分配到路网中的随机位置,但如果在单路口场景下,最好让车辆从四个进口道均匀产生并驶向出口道。否则全部车辆挤在一个方向,信号控制再智能也看不出效果。实际操作中可以用--seed固定随机种子,保证两次仿真结果可对比。
3.3 坐标、比例尺与车道数量的隐藏关联
新手经常忽略的一个问题是路网坐标系的缩放。netedit画路网时,如果拖动节点导致道路被拉伸得特别长,车辆行驶时间会异常大,延误计算完全失真。SUMO内部以米为单位,路网中一条车道的长度必须和真实物理尺度大致对应,否则仿真出来的平均延误数值没有工程意义。
车道数量则和流量设定强相关。四进口道都用单车道,车流稍微一高就会形成排队溢到上游路口,这时候智能体算法再怎么调都难以缓解,因为瓶颈在路网容量而不是信号策略。所以让算法“有表现力”的一个实用技巧是:进口道设为双车道(一条左转、一条直右),出口道设两车道。这样既保留多智能体协调的需求,又不会让路网容量成为压倒性瓶颈。车道数写进.net.xml里面是numLanes属性,用netedit选中边后在右侧属性栏修改,改完务必重新生成连接关系。
4. 多智能体信号控制的最小实现:从TraCI固定配时到排队压力决策
4.1 智能体拆解:交叉口agent、信号agent与协调通道
在编码之前,先把智能体结构理顺。一个路口智能体(intersection agent)负责收集本路口的数据,比如各进口道排队长度、上下游溢出情况;一个信号智能体(signal agent)负责执行相位切换决策。两者可以合并成一个进程或一个类,但在架构设计上要分开,因为多路口扩展时,信号智能体需要和相邻路口的交叉口智能体交换信息,去达成绿波或均衡排队这样的全局目标。
代码层面推荐把每个路口的智能体封装为一个独立的SignalController类,每个类内部拥有phase_id、min_green、max_green、queue_history这些属性。这样后续做分布式通信时,只需在类之间增加消息传递机制,不需要改动仿真主循环。对于这个项目里的多智能体算法,初期实现不必上复杂的强化学习,用排队长度压力平衡策略就能看到显著效果。先跑通后,再考虑换成DQN等深度强化学习方案。
4.2 首个可运行的TraCI控制循环:固定配时版
无论信号控制策略多高级,代码骨架都是同一个TraCI控制循环。下面给出一段最小可运行的固定配时控制代码,作为后续智能体决策的“脚手架”:
import traci import time sumo_binary = "sumo-gui" sumo_cmd = [sumo_binary, "-c", "cross.sumocfg", "--start", "--quit-on-end"] traci.start(sumo_cmd) step = 0 PHASE_DURATION = 20 # 每相位时长(秒) YELLOW_TIME = 3 # 黄灯时间(秒) try: while traci.simulation.getMinExpectedNumber() > 0: traci.simulationStep() if step % PHASE_DURATION == 0: current_phase = traci.trafficlight.getPhase("gneJ0") traci.trafficlight.setPhase("gneJ0", (current_phase + 1) % 4) step += 1 finally: traci.close()这段代码的核心逻辑是:每20秒切换一次相位,切换时直接让SUMO进入下一个相位编号。getMinExpectedNumber用于判断仿真是否还有未到达终点的车辆,它为0时仿真即将结束,循环安全退出。代码中相位编号0、1、2、3对应网路文件里定义的信号灯Program,通常是“南北直行、南北左转、东西直行、东西左转”的顺序。
值得注意的细节是:黄灯相位在SUMO里通常也是独立相位。直接硬切相位会导致当前绿灯车辆没有安全清空时间,模拟结果会偏乐观。我习惯把相位设计成“南北直行+黄灯+东西直行+黄灯”这种编号结构,切换时先切到黄灯,等3秒再切到下一相位。这一步是新手最容易忽略的,却直接影响仿真延误数据的可信度。
4.3 压力平衡算法:用排队长度做实时决策的核心代码
固定配时跑通后,核心任务就是把它升级为基于多智能体算法的自适应控制。这里选择一种工程上最稳、可解释性最强的方案:排队压力平衡(Max Pressure)算法。它不需要任何训练过程,每15秒读取一次路口各相位对应车道的排队长度,然后选择“排队压力最大”的相位给绿灯。相位压力定义为该相位对应所有进口道的车辆等待时间总和。
下面给出基于TraCI的参考实现:
import traci import traci.constants as tc sumo_cmd = ["sumo-gui", "-c", "cross.sumocfg", "--start", "--quit-on-end"] traci.start(sumo_cmd) # 相位编号与对应车道组,需要按自己的路网修改 PHASE_LANES = { 0: ["N2TL_0", "S2TL_0"], # 南北直行 1: ["N2TL_1", "S2TL_1"], # 南北左转 2: ["E2TL_0", "W2TL_0"], # 东西直行 3: ["E2TL_1", "W2TL_1"], # 东西左转 } MIN_GREEN = 15 # 最小绿灯时间,防止相位频繁切换 MAX_GREEN = 60 # 最大绿灯时间,防止某方向被饿死 YELLOW_TIME = 3 # 黄灯时间 def phase_pressure(phase_id): total_wait = 0.0 for lane_id in PHASE_LANES[phase_id]: total_wait += traci.lane.getWaitingTime(lane_id) return total_wait step = 0 try: while traci.simulation.getMinExpectedNumber() > 0: traci.simulationStep() if step % 15 == 0: # 每15秒决策一次 phase = traci.trafficlight.getPhase("gneJ0") # 如果当前相位的绿灯时间还没到最小值,继续维持 if step - last_switch_time < MIN_GREEN: step += 1 continue if step - last_switch_time > MAX_GREEN: traci.trafficlight.setPhase("gneJ0", (phase + 1) % len(PHASE_LANES)) last_switch_time = step continue # 计算所有相位的压力,选出最大压力的相位 pressures = {pid: phase_pressure(pid) for pid in PHASE_LANES} best_phase = max(pressures, key=pressures.get) if best_phase != phase: # 先切到黄灯,黄灯结束后切换 traci.trafficlight.setPhase("gneJ0", (phase + 1) % len(PHASE_LANES)) last_switch_time = step step += 1 finally: traci.close()逻辑说明:每15秒统计一次各相位的车道等待时间,等待时间最长的相位优先获得绿灯。如果当前相位已经超过最大绿灯时间,即使不是压力最大的相位也会强制切换,避免某个方向长时间被饿死。决策频率15秒不是一个拍脑袋的数字,它和车辆通过路口的平均清空时间有关,太短容易抖动,太长则反应迟钝。
这个简化版压力平衡算法是量化多智能体信号控制效果的最低门槛。它虽然名字里有“平衡”二字,但本质上每个路口之间还没有协同通信,仍属于“单点自适应”。要让系统呈现出真正的多智能体特征,一个容易实现且有效的扩展方法是:将下游路口的排队长度也加入当前路口的压力计算,让上游路口在放行前先感知下游是否拥堵。实现方式是在phase_pressure函数中额外读取traci.lane.getLastStepVehicleNumber(downstream Lane)并乘以一个惩罚系数。系数设置为0.5左右比较稳,太高会导致所有路口都不敢放行。
4.4 参数说明:相位切换、最小绿灯与黄灯时间的设置逻辑
上面代码里出现的MIN_GREEN、MAX_GREEN、YELLOW_TIME这三个参数,是整个信号控制系统的“安全阀”。MIN_GREEN设为15秒是为了防止相位切换过于频繁。若两次决策间隔只有3秒,排队压力稍微波动,绿灯就会在南北和东西之间来回跳。实践中最小绿灯不能小于行人过街时间,一般取15到20秒。
MAX_GREEN设置为60秒,是为了限制单个相位的最长放行时间。如果某个方向持续有压力,算法理论上会一直给它绿灯,但这样会产生严重的公平性问题:另一条路彻底堵死。所以必须设置最大绿灯时间,到了时间强制切换,哪怕当前方向的压力仍然最大。黄灯时间则根据路口限速计算,限速60km/h的路口,黄灯时间设在3到5秒之间,低于3秒会造成清空不净,高于5秒则会降低路口吞吐量。
这四个参数在源码包里多数情况下已经配置好一组默认值,但不同路网上限速、车道数不同,默认值往往不是最优的。我一般会在仿真出结果后,先调MIN_GREEN和MAX_GREEN,用同一车流跑三组对比,再固定下来。参数定下来之前,不要动算法本身的代码,否则算法和参数搅在一起很难定位翻车原因。
5. 仿真系统运行常见问题排查:五个让我翻过车的坑位
5.1 现象:SUMO版本不一致导致TraCI连接不断断开
刚拿到人家的源码zip,最容易遇到的就是这种报错:traci.exceptions.FatalTraCIError: connection closed by SUMO,或者卡在“Loading configuration”后毫无反应。最常见的元凶是SUMO版本与你本地的TraCI接口不匹配。SUMO的TraCI协议不是完全向后兼容的,旧版本脚本在新版本下调用traci.trafficlight.getPhase可能返回异常。解决步骤是先执行sumo --version确认版本,再查看源码里requirements.txt声明的版本号,二者必须严格一致。升级或降级SUMO后,把之前生成的.net.xml重跑一次路网连通性检查,防止旧文件结构被新版本误解析。
5.2 现象:车辆在路口绕圈找不到出口,延误数据乱跳
仿真跑起来后,如果发现部分车辆在路口附近反复变道甚至原地打转,那不是多智能体算法的锅,而是路网连接不完整。SUMO的路网文件里,每条车道通过connection元素定义转向关系。netedit自动生成连接通常没问题,但如果你手动改了车道数或加宽路口后没有重新生成连接,就会留下“有些进口道只能右转”的隐患。排查方法是让仿真跑一小段,在sumo-gui里点击出问题的车辆,查看它当前所在车道和下一段期待车道,再回到netedit里补齐缺失的connection。这类问题导致的延误偏差非常大,经常能差出30%以上。
5.3 现象:绿灯刚亮几秒就被切走,相位频繁抖动
多智能体控制器最常见的“假智能”表现就是信号抖动。当你把决策间隔设得很短,比如每3秒算一次排队压力,而当前绿灯方向排队车辆恰好清空,压力瞬间变为0,算法立刻认为要切换相位,结果绿灯仅仅亮了5秒就跳走。这看起来是算法在自适应,实际上是系统不稳定。根本原因是缺少最小绿灯时间约束,以及压力函数中使用了瞬时排队而非平滑排队。解决方法是给MIN_GREEN赋15秒以上的值,用历史平均等待时间替代瞬时秒级读数。压力平滑可以用queue_history列表存最近五次的数据取均值,能让相位间隔变得稳定得多。
5.4 现象:zip解压后路径错乱,主脚本运行报找不到文件
这类问题在源码包复现中占到三成以上。解压后的目录结构往往是“仿真-基于多智能体算法的城市交通信号控制仿真系统实现-附项目源码-优质项目实战”套一层,里面再来一个“代码”文件夹,再把路网文件放在data/network里。脚本用相对路径cross.sumocfg启动时,工作目录不对自然找不到。处理方式不是去改代码里的硬编码路径,而是先把整个工程重新组织成固定的目录结构:根目录放主脚本、network/放路网文件、route/放车流文件、output/放结果。然后统一用os.path.join拼接路径,不要写死字符串。这样以后再发给别人,就不会被路径问题困扰。
5.5 现象:仿真结果每次跑都不一样,同一份代码两种结论
有时候代码没改,前后两次跑出来的平均延误数字差很多。两种常见原因:一是随机车流的随机种子里没有固定,导致每次生成的车流量和分布不一样;二是多路口协调场景下,仿真微小的时序差异会被下游路口放大。对这类随机系统做决策评估,必须固定随机种子。SUMO生成车流时用--seed参数指定种子值,仿真器启动时也可加--seed 2024固定所有内部随机数。这样每次跑的实验数据才具有可比性,往报告里写结论也理直气壮。另一个习惯是修改算法后先跑同一场景三次,结果波动在2%以内再用该场景做对比。
6. 进阶验证:把多智能体控制器与定时控制做一个让别人信服的对比
6.1 对比实验的最小口径:同路网、同车流、同随机种子
要让仿真结果有说服力,不能只展示多智能体控制器的效果,必须和定时控制做对照实验。对照的口径统一为三件事:同一个.net.xml路网文件、同一份.trips.xml车流文件、同一个随机种子。唯一变量是信号灯控制逻辑。定时控制用固定配时跑,多智能体用上一章的压力平衡跑,各跑三次取均值作为最终结果。
评估指标至少看三个维度。平均等待时间是最直观的,SUMO中用traci.lane.getWaitingTime按时间累加后除以通过车辆数即可算出。最大排队长度反映路口是否发生溢出。吞吐量则是单位时间内通过路口的车辆总数,代表通行能力。如果多智能体方案只降低了平均等待时间,但吞吐量下降,那这个方案极不划算。遇到这种情况先别急着否定算法,去查下班高峰车流是否已经饱和,路网容量不足时任何信号控制都回天乏术。
6.2 把对比脚本写进交付包里,让别人信任你的结论
一个高完成度的项目交付,不只要给出控制代码,还要给出数据对比脚本。我一般会单独写一个run_comparison.sh,让使用者一条命令跑完全部实验。脚本内部顺序为:第一步生成固定车流文件,第二步运行定时控制仿真并导出延误指标,第三步运行多智能体控制仿真并导出指标,第四步用Python读取两个输出文件,打印对比表格。这个脚本省去了手工跑实验的重复劳动,也让源码包的读者能快速复现你的结论,整体质量感和一条条命令行指导完全不同。
import json with open("output/fixed_waiting.json") as f: fixed = json.load(f) with open("output/mp_waiting.json") as f: mp = json.load(f) print(f"固定配时平均等待: {fixed['avg_wait']:.1f} s") print(f"多智能体平均等待: {mp['avg_wait']:.1f} s") print(f"提升幅度: {(1 - mp['avg_wait'] / fixed['avg_wait']) * 100:.1f}%")这段脚本本身的逻辑就是读取两个JSON文件中的平均等待时间并算出提升比例。写交付包时我有个深刻教训:JSON字段名和数据单位必须写在README里,否则半个月后自己看都容易忘记是秒还是分钟。另外,数据导出部分和主仿真代码尽量分开,仿真跑完单独导出,不要混在一个循环里反复开关文件,否则会拖慢仿真速度,延时数据也会混入IO噪声。
对比做完后,你会发现多智能体方案在流量波动场景下通常能提升10%到25%的通行效率,但在流量极低或极饱和状态下,也可能只提升不到5%,甚至略微更差。这时候如实把边界条件写进README,比隐瞒问题更能让使用者信任。回头再看这套系统,最值钱的部分其实不是算法本身,而是你能把路网、车流、实验口径、参数边界完整地讲明白。希望帮到你。
本文还有配套的精品资源,点击获取