☰
DQN深度强化学习控制SUMO交通信号灯:高分毕设源码解析与避坑
2026/9/28 12:50:55 网站建设 项目流程

简介:基于Python与SUMO仿真平台,运用深度强化学习DQN算法实现交通信号灯相位时间动态调整的完整源码项目,专为计算机、通信、人工智能、自动化等专业学生打造,适合期末课程设计、大作业或毕业设计参考。项目答辩评审达98分,代码已调试运行通过,并附带交通路网文件、出行需求脚本与说明文档,可快速复现实验场景。资源包共32个文件,以xml路网配置、osm地图数据、py算法脚本、xlsx数据表及sumocfg仿真配置文件为主,整体大小536KB,结构清晰便于按需查阅。目前已有80人学习下载。从内容看,包含DQN主程序、优先级强化学习改进版本、辅助工具脚本以及多组路网与路由文件,便于对照不同信号控制策略效果;基础扎实的读者还可基于现有框架替换路网或调整奖励函数,拓展自适应信号控制研究。

1. DQN 控制 SUMO 交通信号灯:这套高分毕设项目拆开来看并不复杂

要说清楚这个项目,一句话就够:基于 Python 开发、以 SUMO 作为仿真平台,用强化学习里的 DQN 算法做交通信号灯相位时间调整的完整源码。它不是一个算法演示片段,而是一条能跑的闭环——SUMO 负责微观交通仿真,Python 通过 TraCI 读取路口状态、下发相位决策,DQN 在每个仿真步都在学习当前相位该延长还是切换。我拆这套源码的第一感觉是分层干净:RL_brain.py 只管学习和决策,DQN_main.py 只管仿真交互,路网、车流、配置各自独立成文件,项目整体完整可运行,答辩能拿 98 分不意外。

具体能做什么:在 SUMO 上复现一个交叉口的自适应信号控制,和固定配时方案对比平均等待时间;可以改状态定义、奖励函数做自己的实验;也可以顺着 Priority_RL_brain.py 和 PDQN_main.py 继续做优先经验回放的对比研究。适合计算机、自动化、交通工程方向学生做课程设计或毕业设计,也适合想快速上手 SUMO 加深度强化学习的从业者当作基线工程参考。

2. 项目文件结构:先把二十几个仿真文件按路网、车流、算法三层分清楚

2.1 文件清单:哪些是路网、哪些是车流、哪些是算法

拿到压缩包先别急着跑,.net.xml 和 .osm 混在一起很容易看花眼。按 SUMO 项目的通用组织方式,我把这批文件分成四层:路网、车流、配置、算法。这样分的好处是排查问题时能快速定位——仿真跑不起来先查路网层,车辆生成不对先查车流层,控制效果差才轮到算法层。

路网层里 shixin_shanyin.net.xml 是主路网,由 shixin_shanyin.osm 转换而来;1.net.xml、second.net.xml、third.net.xml、forth.net.xml 是实验过程中截取的局部路网或简化版本;hello.net.xml 是从 helloworld.osm 转出的最小演示路网,适合先跑通链路。output.net.xml、aim.net.xml 这类带 output 前缀的文件是转换过程的中间产物,一般不用动。

车流层对应 .rou.xml:shixin_shanyin_original_rou.xml 是原始车流,shixin_shanyin_high.rou.xml 是高流量场景,shixin_shanyin_all7.rou.xml 像是把多组出发需求合并后的完整车流。配置层就是 shixin_shanyin.sumocfg,它把路网、车流和仿真参数绑定在一起。另外还有 shixin_shanyin.det.xml,这是感应线圈检测器文件,用来统计通过某个断面的车流量,做数据报表时很常用。

算法层是核心:DQN_main.py 是训练主入口,RL_brain.py 是 DQN 实现,Priority_RL_brain.py 和 PDQN_main.py 是优先经验回放版本。辅助文件里 lane_direction.xlsx 存车道方向映射,raw_tsc_原.xlsx 是原始信号灯配时底表,这两个表格在构造状态和计算基线时会用到。README.md 里有环境依赖和启动顺序,我建议先读它再动手。

层次典型文件作用
路网层shixin_shanyin.net.xml、*.osm道路拓扑、信号灯位置、连接关系
车流层*.rou.xml车辆出发时间、行驶路线、流量大小
检测器层shixin_shanyin.det.xml断面车流量统计
配置层shixin_shanyin.sumocfg绑定路网车流、仿真时长和输出
算法层DQN_main.py、RL_brain.py训练主循环与 DQN 网络实现
辅助数据lane_direction.xlsx、raw_tsc_原.xlsx车道方向映射、原始配时基线

2.2 OSM 转 net.xml:路网拓扑怎么来的

路网不是手画的,.osm 是 OpenStreetMap 导出的真实地图数据,SUMO 不认识 .osm,必须先转成 .net.xml。常见做法是用 netconvert,命令行写法如下:

netconvert --osm-files shixin_shanyin.osm \ --output-file shixin_shanyin.net.xml \ --geometry.remove \ --roundabouts.guess \ --ramps.guess

三个选项的作用要说清楚:--geometry.remove 移除不影响通行能力的几何冗余点,路网更干净;--roundabouts.guess 自动识别环岛,避免环岛被拆成一堆普通交叉口;--ramps.guess 识别匝道连接关系。真实 OSM 数据里常有断头路、错连边,转换结果必须用 netedit 人工检查一遍,尤其信号灯所在交叉口的连接关系,这一步省不得。

如果你只是调试 DQN,不建议一上来用完整 OSM 路网。我的习惯是先用 hello.net.xml 或 1.net.xml 这样的简化路网把训练跑通,确认状态、动作、奖励定义没问题了,再换主路网 shixin_shanyin.net.xml 做正式实验。完整路网车辆多、交叉口多,每一步 TraCI 请求的耗时都更长,在简化路网上调试能省下大量等待时间。

2.3 sumocfg 配置:仿真时长、输出与 TraCI 开关

shixin_shanyin.sumocfg 是 SUMO 的启动入口,决定加载哪条路网、哪份车流、跑多久、输出什么。典型结构如下:

<?xml version="1.0" encoding="UTF-8"?> <configuration> <input> <net-file value="shixin_shanyin.net.xml"/> <route-files value="shixin_shanyin_original_rou.xml"/> </input> <time> <begin value="0"/> <end value="3600"/> <step-length value="0.1"/> </time> <output> <tripinfo-output value="tripinfo.xml"/> </output> </configuration>

input 段指定路网和车流,time 段里 end 是仿真结束时刻,step-length 是仿真步长,信号灯场景我一般用 0.1 秒,太小仿真极慢,太大相位切换精度受影响。output 段的 tripinfo-output 会记录每辆车完整行程,后面算平均等待时间、旅行时间都靠它。

有一点要注意:接 TraCI 时配置文件的 end 不会按传统方式严格生效,因为每一步推进都由 Python 端的 simulationStep 控制,配置里的 end 更多是兜底上限。训练模式我一般不加 GUI,用命令行 sumo 跑,并把 warn 级日志关掉,训练速度可以快好几倍。

提示:压缩包里多个 net.xml 对应不同实验阶段,跑之前一定确认 sumocfg 里配的 net-file 和 route-files 是同一套,这是最常见的翻车点。

3. DQN 核心模块:RL_brain.py 里的网络、回放与决策是这套系统的引擎

3.1 网络结构:三层全连接加 target network,不要贪深

RL_brain.py 里第一块是 Q 网络。标准 DQN 用一个全连接网络把状态向量映射到每个动作的 Q 值,深度不需要超过三层,因为交通状态的特征维度有限,层数多了反而过拟合、训练也更慢。为什么不用表格型 Q-learning?因为状态向量里包含多个车道的排队长度和等待时间,组合起来的状态空间是天文数字,表格根本存不下,必须靠神经网络做函数逼近。

import torch import torch.nn as nn class DQNNet(nn.Module): def __init__(self, n_state, n_action): super(DQNNet, self).__init__() self.fc1 = nn.Linear(n_state, 128) self.fc2 = nn.Linear(128, 128) self.fc3 = nn.Linear(128, n_action) def forward(self, x): x = torch.relu(self.fc1(x)) x = torch.relu(self.fc2(x)) return self.fc3(x)

n_state 是状态向量维度,n_action 是动作数量。隐层 128 是常用宽度,状态里包含大量车道排队信息时可以加到 256,但没必要继续加深。输出层不加激活函数,因为 DQN 的 loss 是对 Q 值的回归,不是分类,原始输出直接进 MSE loss。

target network 是 DQN 稳定的关键。自举更新最大的问题是目标值跟着当前网络跑,训练过程抖动。常见做法是维护一份参数冻结的 target net,每隔 C 步把 eval net 的参数复制过去:

def sync_target(self): self.target_net.load_state_dict(self.eval_net.state_dict())

C 在 200 到 1000 之间都常见,对应仿真里的几百个步长。C 太小 target 失去平滑意义,C 太大整个训练收敛变慢。我一般先用 500,观察 loss 波动再调。这里提醒一句:同步用 load_state_dict,别用赋值,否则两个网络共享同一份参数,target 机制就失效了。

3.2 状态、动作、奖励三要素在信号灯场景的落地

DQN 落地,关键是三要素定义。状态向量我现在还在用的组合是:固定时间段内各进口车道的排队车辆数、车道平均等待时间、当前相位编号、当前相位已持续时长。这些都能从 TraCI 直接读到,拼成一维向量就是 n_state。不要贪心把几十个原始指标全塞进去,特征之间高度相关反而干扰 Q 值拟合。

动作空间要小。信号灯控制不是连续控制问题,常见做法是定义两个动作——保持当前相位继续放行 N 秒,或立即切换到下一相位;也可以定义成「在当前相位基础上延长 5、10、15 秒」这样的候选集。动作数量一多,DQN 收敛难度直线上升,我从两动作起步,效果不够再加动作。

奖励函数决定智能体学到的目标。单看某一辆车的等待时间,噪声太大,训练不稳定。我一般用全部受控车道平均等待时间的负值,奖励越接近 0 说明路口越通畅:

def get_reward(): total_wait = 0.0 for lane_id in controlled_lane_ids: total_wait += traci.lane.getWaitingTime(lane_id) return -total_wait / max(len(controlled_lane_ids), 1)

这里对受控车道等待时间累加、平均、取负,含义是让智能体学会压缩全路口的总等待。如果训练后期发现智能体倾向于把绿灯拉得极长不放——这是很典型的现象,说明奖励里缺相位切换惩罚,可以在切换动作上额外扣一个常数,抑制信号灯僵化。这个常数我一般取 0.5 到 1.0,太大智能体不敢切换,太小拦不住长时间绿灯。

3.3 经验回放与 epsilon-greedy:稳定采样的两个细节

第三块是经验回放缓冲区。没有回放的话,同一轨迹里相邻状态高度相关,梯度方向很偏,训练抖动大。用定长 deque 存经验、随机采样打乱相关性,是 DQN 能稳定的基础。

from collections import deque import random class ReplayBuffer: def __init__(self, capacity=20000): self.buffer = deque(maxlen=capacity) def push(self, s, a, r, s_, done): self.buffer.append((s, a, r, s_, done)) def sample(self, batch_size): batch = random.sample(self.buffer, batch_size) return [torch.FloatTensor(x) for x in zip(*batch)]

容量 20000 是常见配置,容量太小容易反复学旧经验,太大占用内存且新经验占比低。采样用 zip(*batch) 把状态、动作、奖励、下一状态、终止标志分别聚合成张量,直接喂网络。注意 next_state 在终止态时不该有折扣回报,更新时靠 done 掩码把对应的 next Q 置零,否则终端状态被高估,训练后期会出现莫名其妙的震荡。

choose_action 用 epsilon-greedy:随机数小于 epsilon 就探索,否则取 Q 值最大的动作。epsilon 从 1 线性衰减到 0.05 左右,衰减总步数和仿真总步数挂钩。评估阶段 epsilon 置 0,只做贪心决策,这样才能公平对比不同策略的表现。训练和评估用同一套函数、不同 epsilon 值,是我一直保留的习惯。

4. 训练主循环:从 TraCI 连接到 DQN_main.py 的逐段拆解

4.1 TraCI 连接:Python 和 SUMO 之间的通信桥

TraCI 是 SUMO 提供的客户端接口,Python 通过它读取仿真状态、下发控制指令。DQN_main.py 启动时的第一步就是拉起 sumo 进程并建立连接:

import traci sumo_cmd = [ "sumo", "-c", "shixin_shanyin.sumocfg", "--remote-port", "8813", "--no-warnings" ] traci.start(sumo_cmd)

训练阶段用命令行列模式的 sumo,不要用 sumo-gui,窗口渲染会拖慢训练。--remote-port 8813 是 TraCI 默认端口,被占用时换成随机高位端口。--no-warnings 屏蔽 warn 日志,日志一多,训练进度信息很容易被淹没。

连接完成后,仿真推进权完全在 Python 手里:每次 traci.simulationStep() 才往前走一步,没有调用就停在原地。这正是训练循环能插入决策的位置——在两次 simulationStep 之间读状态、选动作、下指令、算奖励。理解了这个机制,就理解了整条链路的数据流方向。

4.2 DQN_main.py 训练主循环:一个 episode 里发生了什么

一个 episode 代表一次完整仿真。训练逻辑是标准的交互式强化学习流程,可以概括为:读状态、选动作、执行、推进仿真、算奖励、存经验、学一次。

def run_one_episode(agent, max_steps=500): traci.simulationStep() step = 0 episode_reward = 0.0 s = get_state() while step < max_steps: a = agent.choose_action(s) apply_action(a) traci.simulationStep() r = get_reward() s_ = get_state() done = (traci.simulation.getMinExpectedNumber() == 0) agent.store_and_learn(s, a, r, s_, done) s = s_ episode_reward += r step += 1 if done: break return episode_reward

get_state 组装状态向量,get_reward 计算这一时刻的奖励,apply_action 把动作转成信号灯指令。store_and_learn 内部先往回放缓冲区 push 一条经验,再判断 buffer 里是否攒够一个 batch,够就采样并更新一次网络权重。done 判断用的是 traci.simulation.getMinExpectedNumber——返回 0 表示路网里不再有未出发和未到达的车辆,仿真自然结束。max_steps 是兜底,防止车流文件里有车辆卡死导致 episode 无限拉长。

重置 episode 我推荐 traci.load 而不是关闭重开进程,反复 traci.start 的进程开销在长训练里非常明显。traci.load 重新加载同一个 sumocfg 配置即可,状态干净且速度快。

4.3 关键超参:学习率、batch、epsilon、gamma 怎么配

这套系统的超参数直接影响能不能收敛,逐个说:

超参数建议区间说明与踩坑
learning rate1e-4 ~ 1e-3偏大必震荡,0.01 直接不收敛
batch_size32 ~ 64状态维度高时取 64
gamma0.9 ~ 0.99信号灯场景要看到车辆的长期代价
buffer 容量10000 ~ 50000太小反复学旧经验,太大新经验占比低
epsilon 衰减覆盖 30%~50% 总步数衰减过快提前锁定次优动作
target 同步周期 C200 ~ 1000太小失去平滑,太大收敛慢

学习率我踩过 0.01 的坑,loss 直接震荡发散,后来固定 1e-3 起步,稳定后再往下降。gamma 值代表智能体的远见程度,信号灯控制里一辆车的延迟会传递到后续车辆,gamma 小于 0.9 时智能体只看得到眼前几步,容易做短视决策。

训练过程中真正要盯的不是 loss,而是每个 episode 的平均等待时间。loss 下降只能说明 Q 函数在拟合,不代表控制策略变好。我建议每次 episode 结束都输出平均等待时间,和固定配时基线比一比,这个数字有下降趋势才是真的在学。把每个 episode 的等待时间和奖励都追加写进日志文件,画曲线用,比盯着控制台输出靠谱得多。

5. 避坑排查:SUMO+DQN 组合最常见的 5 个翻车现场

5.1 traci 模块找不到:全是 PYTHONPATH 的锅

现象:运行 DQN_main.py 直接报 ImportError: No module named traci,sumolib 也导入失败,换 Python 版本后这个问题尤其频繁。

原因:SUMO 的 traci 和 sumolib 包放在 $SUMO_HOME/tools 目录里,Python 默认搜索路径不含这个目录,不主动加就必然报错。

解决:先确认环境变量再启动:

export SUMO_HOME=/path/to/sumo export PYTHONPATH=$SUMO_HOME/tools:$PYTHONPATH

再稳一点的做法是在脚本头部加兜底逻辑,运行时检查 SUMO_HOME 并把 tools 目录动态加进 sys.path:

import os, sys if "SUMO_HOME" in os.environ: sys.path.append(os.path.join(os.environ["SUMO_HOME"], "tools")) else: raise RuntimeError("请先设置 SUMO_HOME 环境变量")

这段代码放在 import traci 之前,能省掉大多数环境问题的时间。Windows 上还要注意 Python 位数和 SUMO 版本匹配,64 位 Python 配 64 位 SUMO,别混。

5.2 车辆原地不动:net.xml 和 rou.xml 不是同一套路网

现象:仿真能启动,但日志报 lane not found 或 edge not found,画面里车辆全部卡在生成点,一辆都动不了。

原因:rou.xml 里的边 ID 是参照旧路网写的,当前 net.xml 是从 OSM 重新转出来的,拓扑重建后边 ID 对不上。压缩包里多个 net.xml 并存,加载时最容易搞混。

解决:训练前先不带 TraCI 干跑一遍完整校验:

sumo -c shixin_shanyin.sumocfg

SUMO 会先把路网和车流校验完,有问题会在启动阶段直接报出来。确认没问题再启动训练。换路网时重新生成对应车流,不要沿用旧文件的 ID。也可以打开 netedit,用 find 功能对照一下两个文件的 edge 列表,十秒钟能定位问题。

5.3 TraCI 连不上或端口被占用:残留进程没清理干净

现象:第一次训练正常,第二次运行报 Could not connect to TraCI on port 8813,或者连接建立后无响应、仿真不推进。

原因:上一次训练异常退出,sumo 子进程没被回收,8813 端口被占;或者端口被系统里其他程序占用。在 Jupyter 里反复执行训练代码块,这个问题频发。

解决:先杀残留进程再启动。更稳妥的是每次训练用随机高位端口,从根上避开冲突:

pkill -f sumo
import random port = random.randint(8000, 9000) sumo_cmd += ["--remote-port", str(port)] traci.start(sumo_cmd)

用一个变量把端口贯穿整个训练脚本,记录日志时顺手把端口打出来,排查问题时能快速对上是哪一次运行。端口冲突用 netstat 查也很快,但随机端口直接省掉这一步。

5.4 DQN 不收敛:先怀疑奖励和探索,再怀疑网络

现象:上千步训练后 loss 不降、平均等待时间没改善,奖励在一个固定负值附近振荡,换了学习率也没用。

原因:最常见就两个。一是奖励太稀疏,只在 episode 结束才给一次,智能体拿不到即时反馈;二是 epsilon 衰减太快,智能体过早进入纯贪心,反复执行同一个次优动作,经验的多样性没了。

解决:奖励改成每个仿真步都算,用排队长度或等待时间的变化量当即时信号。epsilon 衰减总步数拉长到总训练步数的 30% 到 50%。再加一招:每次相位切换动作时给一个小的负奖励,抑制频繁切换,这个改动对稳定性的效果很明显。

还有一个我后来才学到的习惯:每个 episode 结束都存一份模型 checkpoint,这就是后悔药。训到第 500 个 episode 发现曲线崩了,能从第 300 个的存档接着调参,不用从头再来。没有 checkpoint 的话,一次失败可能要重跑几小时。

5.5 完整路网训练慢成幻灯片:别开 GUI,别每步都决策

现象:换到 shixin_shanyin.net.xml 完整路网后,一个 simulationStep 要几百毫秒,sumo-gui 下更慢,一个 episode 要好几分钟,完全没法迭代调参。

原因:完整路网车辆多、交叉口多,SUMO 微观仿真是逐车逐路段模拟的,计算量本身就大;GUI 渲染和频繁的 TraCI 往返请求进一步放大开销。

解决:训练阶段用命令行 sumo 加 --no-warnings,不开 GUI。先用简化路网调通算法链路,再上完整路网。还有一个省时的通用技巧:把决策间隔从每个仿真步改成每 5 秒或每 10 秒一次——信号灯相位本来就是按秒级变化的,不需要每 0.1 秒都决策:

if step % (int(5.0 / 0.1)) == 0: a = agent.choose_action(s) apply_action(a)

这个判断把 0.1 秒步长下每 5 秒做一次决策,训练时间能缩短接近一个数量级,控制效果几乎不损失。

6. 进阶方向:把均匀采样换成优先经验回放,对比 DQN 与 PDQN 的收敛差距

项目里自带的 Priority_RL_brain.py 和 PDQN_main.py,就是标准的优先经验回放实现。和普通 DQN 的关键差别只在采样:普通回放均匀抽经验,优先回放按照 TD-error 的大小给每条经验定优先级,误差大的经验被抽中的概率更高,训练效率明显提升。priority 要加一个小常数,避免零优先级的经验永远不被采样,同时要用重要性采样权重修正更新偏差。

实现优先回放一般借助 SumTree 结构,把叶子节点按优先级组织成树,采样时用总优先级随机切段再逐层定位:

class SumTree: def __init__(self, capacity): self.capacity = capacity self.tree = [0.0] * (2 * capacity) self.data = [None] * capacity def add(self, priority, data): idx = self.write self.data[idx] = data self._update(idx, priority) self.write = (self.write + 1) % self.capacity def get(self, segment): parent = 1 while parent < self.capacity: left = parent * 2 if segment <= self.tree[left]: parent = left else: segment -= self.tree[left] parent = left + 1 leaf = parent - self.capacity return leaf, self.data[leaf]

这段代码是 SumTree 的核心:add 把新经验挂到叶子节点并向上更新优先级累计值,get 按随机段定位叶子。树形结构保证采样复杂度是 O(log n),数据量大时不拖后腿。PDQN_main.py 里还会多算一步 importance weight,更新 Q 网络时用它修正采样偏差,这个不要漏。

验证 PDQN 是否真的有效,我的做法是固定随机种子,用同一份路网和车流分别跑 DQN 与 PDQN,记录每个 episode 的平均等待时间曲线,对比到达同一等待时间水平所需的 episode 数。混合交通场景下 PDQN 往往用更少样本逼近最优控制效果,这也是这个项目作为高分毕设的亮点之一。

从那以后,我每做一次信号灯强化学习实验,都强制走一遍「固定随机种子、双策略对照、等待时间曲线」三件套才敢下结论。这个项目最值得带走的不是某个模型权重,而是一条能反复验证的完整链路——把 DQN、PDQN、SUMO、TraCI 拼接起来,换上自己的路网和交通需求就能开始跑实验。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询