
简介基于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.xmlshixin_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 version1.0 encodingUTF-8? configuration input net-file valueshixin_shanyin.net.xml/ route-files valueshixin_shanyin_original_rou.xml/ /input time begin value0/ end value3600/ step-length value0.1/ /time output tripinfo-output valuetripinfo.xml/ /output /configurationinput 段指定路网和车流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, capacity20000): self.buffer deque(maxlencapacity) 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_steps500): 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_rewardget_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状态维度高时取 64gamma0.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. 避坑排查SUMODQN 组合最常见的 5 个翻车现场5.1 traci 模块找不到全是 PYTHONPATH 的锅现象运行 DQN_main.py 直接报 ImportError: No module named tracisumolib 也导入失败换 Python 版本后这个问题尤其频繁。原因SUMO 的 traci 和 sumolib 包放在 $SUMO_HOME/tools 目录里Python 默认搜索路径不含这个目录不主动加就必然报错。解决先确认环境变量再启动export SUMO_HOME/path/to/sumo export PYTHONPATH$SUMO_HOME/tools:$PYTHONPATH再稳一点的做法是在脚本头部加兜底逻辑运行时检查 SUMO_HOME 并把 tools 目录动态加进 sys.pathimport 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.sumocfgSUMO 会先把路网和车流校验完有问题会在启动阶段直接报出来。确认没问题再启动训练。换路网时重新生成对应车流不要沿用旧文件的 ID。也可以打开 netedit用 find 功能对照一下两个文件的 edge 列表十秒钟能定位问题。5.3 TraCI 连不上或端口被占用残留进程没清理干净现象第一次训练正常第二次运行报 Could not connect to TraCI on port 8813或者连接建立后无响应、仿真不推进。原因上一次训练异常退出sumo 子进程没被回收8813 端口被占或者端口被系统里其他程序占用。在 Jupyter 里反复执行训练代码块这个问题频发。解决先杀残留进程再启动。更稳妥的是每次训练用随机高位端口从根上避开冲突pkill -f sumoimport 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 拼接起来换上自己的路网和交通需求就能开始跑实验。希望帮到你。本文还有配套的精品资源点击获取