Python+SUMO+DQN实现交通信号灯相位优化实战
发布时间:2026/10/2 3:23:02来源:尧图网络
简介基于Python与SUMO平台的深度Q网络交通信号灯相位优化系统是一份面向本科毕业设计、课程设计及交通工程研究的完整源码实现。系统通过SUMO微观交通仿真获取实时车流数据以车道车辆密度、排队长度等构建多维状态空间利用DQN算法动态调控信号相位时长并采用经验回放与目标网络提升收敛稳定性。包体共39个文件、压缩后626KB涵盖Python算法脚本、XML路网与仿真配置、OSM地图数据、Excel流量统计及说明文档核心代码含DQN主程序、强化学习决策模块、优先级采样变体和辅助工具配套参数配置、训练可视化及评估体系路网覆盖多条交通场景便于直接仿真与二次开发。目前已有76人学习浏览适合正在开展智能交通、强化学习或SUMO仿真研究的高校学生和研发人员参考复用。1. 拿到 PythonSUMODQN 信号灯优化源码先看它替你把哪三层难题解决了Python 写强化学习控制 SUMO 红绿灯劝退大多数人的从来不是 DQN 算法本身而是你照着教程写完了 agent却发现车流不会自己出来、traci 连不上仿真、reward 全是噪声。我拆这套基于 Python 与 SUMO 平台的 DQN 强化学习交通信号灯相位优化系统源码时最直接的感受是它把“强化学习怎么落地到交通仿真”这条路完整走通了一遍——不光是 DQN 那几层网络还包括从路网文件、随机车流、traci 数据采集到相位切换的每一个环节。这套系统的核心价值在于三层第一层SUMO 仿真环境的搭建和车流注入逻辑让训练数据不是凭空编的第二层状态、动作、奖励函数的建模方式直接决定 DQN 能不能学到“减少等待”而不是瞎切相位第三层经验池、目标网络、epsilon 衰减这些 DQN 工程细节保证训练曲线真的能收敛。适合两类人正在做交通控制方向毕设的学生以及想用强化学习做一个完整闭环练手项目的 Python 开发者。如果你是零基础建议把第 2、3 章读透再开始跑代码后面会少走很多弯路。2. 交通相位里的强化学习建模State、Action、Reward 为什么这么设计建模决定训练上限。相位建模错了后面全是白调。这一章我把这套源码里最关键的三块拆开讲相位体系是什么样的、状态到底从 SUMO 里抓哪些数据、奖励函数为什么要写成“变化量惩罚项”的组合。2.1 先搞懂 SUMO 里的相位、信号组和绿灯时长信号灯相位本质上就是一组信号光色组合。以最常见的十字路口为例一个完整周期至少要有四个主相位东西直行、东西左转、南北直行、南北左转。SUMO 的 traffic light 系统里有一个信号组的概念一个信号组对应一条或一组车道上的红绿灯而相位的作用对象是这些信号组的集合。你在 netedit 里画完交叉口后修改一个相位的状态等于同时改变某个方向的通行权。绿灯时长就是这个相位在信号方案里持续的时间比如 duration 20表示这个相位亮 20 秒绿灯。这套源码里绿灯时长并不是固定 20 秒就切而是由 DQN 在每个决策点决定“继续当前相位”还是“切到下一个相位”。这样做的意义在于相位的先后顺序是固定的符合交通规范不允许从东西直行直接跳到南北左转这类跳相行为但每个相位停留多久由 agent 根据当前车流状态实时调整。这才是“相位优化”在 SUMO 语境下的真正含义——不是去优化绿灯的秒数配比表而是优化“什么时候切换相位”这个决策本身。为了让后面代码不迷路先给一个最简交叉口的相位表相位编号放行方向相位内动作含义0东西直行东西方向直行车道绿灯1东西左转东西方向左转车道绿灯2南北直行南北方向直行车道绿灯3南北左转南北方向左转车道绿灯注意实际 netedit 生成的相位表里还包含黄灯和全红过渡相位这种过渡相位不需要 DQN 来决策后面第 4 章会专门说怎么处理。2.2 State 设计从 traci 里抓哪些数据怎么拼成向量State 的设计决定了 DQN 能“看到”什么。常见做法是从每条进口车道抓三个指标排队车辆数、累计等待时间、平均速度。排队车辆数反映拥堵空间范围等待时间反映延误严重程度平均速度反映车流是否接近饱和。三者组合起来才能区分“车很多但跑得动”和“车不多但全堵死了”这两种完全不同的场景。import traci import numpy as np # 以四进口双车道为例N/S/E/W 各两条车道 LANE_IDS [ N2T_0, N2T_1, S2T_0, S2T_1, E2T_0, E2T_1, W2T_0, W2T_1 ] def get_state(current_phase): vec [float(current_phase)] for lane in LANE_IDS: halting traci.lane.getLastStepHaltingNumber(lane) waiting traci.lane.getWaitingTime(lane) speed traci.lane.getLastStepMeanSpeed(lane) vec [halting, waiting, speed] return np.array(vec, dtypenp.float32)这段代码的逻辑是先把当前相位编号作为第一个特征让 agent 知道“我现在放行的是哪个方向”然后对每条车道追加三个特征。current_phase 必须是整数转成 float 是为了拼进张量时不报类型错误。等待时间的单位是秒from traci.lane.getWaitingTime 返回的是该车道所有车辆累计等待秒数之和速度单位是 m/s如果你的路网限速是 60km/h跑满时均值大约是 16.7别把这个数当 km/h 用后面奖励函数会吃大亏。这套源码选择用 8 条车道的组合特征而不是全路网所有车道是因为决策只需要覆盖交叉口上下游的进口道。如果路网更大建议按“离交叉口最近 200 米”来筛选车道否则状态维度过大DQN 收敛速度会明显下降。状态向量长度为 1 8 * 3 25对应后面 DQN 网络输入层的节点数。2.3 Reward 设计为什么用“等待量变化排队惩罚”而不是直接减等待秒数Reward 是这套源码里最值得抄的地方。很多人第一次写交通信号灯强化学习reward 直接写-total_waiting_time结果训练曲线疯狂震荡因为不同 episode 的车流量随机性太大绝对等待时间在不同车流密度下方差极高学习信号被噪声淹没。这套源码用的是变化量加惩罚项的写法prev_waiting 0.0 def get_reward(): global prev_waiting total_waiting sum(traci.lane.getWaitingTime(l) for l in LANE_IDS) total_halting sum(traci.lane.getLastStepHaltingNumber(l) for l in LANE_IDS) r (prev_waiting - total_waiting) * 0.6 - total_halting * 0.4 prev_waiting total_waiting return r核心逻辑是先算当前时刻的总等待秒数与上一时刻做差。如果等待时间比上一时刻减少了reward 为正说明刚才的相位切换缓解了延误如果增加了reward 为负说明当前相位放行方向选错了。total_halting * 0.4是排队惩罚项用来避免 agent 学会“切换频繁但不解决实质拥堵”的投机行为。0.6和0.4是权重系数可以理解成“更看重总延误缓解同时适当约束排队长度”。举个例子上一轮总等待 100 秒当前总等待 80 秒排队车辆数没有变化那 reward (100-80)*0.6 - 0 12。如果等待增加了 20 秒且排队多了 5 辆reward -12 - 2 -14。这个设计的好处是 reward 的数值量级基本控制在正负几十之间不会像绝对等待时间那样出现上千的极端值DQN 的训练稳定性会好很多。2.4 Action 空间离散相位编号 vs 连续配时为什么选离散DQN 本身是离散动作算法输出的是每个动作的 Q 值然后取最大 Q 值对应的动作。所以这里动作空间必须处理成离散的。常见做法有两种一种是把四个主相位编号成 {0, 1, 2, 3}让 DQN 直接决定下一个要切换到的相位另一种是定义为二值动作 {保持当前相位, 切换到下一相位}。这套源码采用四个主相位编号的离散动作空间。选离散而不是连续动作有两个实际原因第一SUMO 的信号灯控制接口本质上就是 setPhase 和 setRedYellowGreenState输入是相位索引或光色字符串不是连续数值连控制接口本身都是离散的第二信号灯相位优化问题天然是组合决策问题连续动作空间需要额外加一层“连续值到相位方案”的映射这层映射如果处理不好会出现动作边界抖动导致相位频繁切换反而违反交通工程常识。下面这张表可以直观看到两种做法的差别动作空间含义优点缺点离散 4 相位编号直接指定下一相位与 SUMO 接口天然匹配需要处理黄灯过渡连续动作值输出绿灯延长秒数或切换概率更接近真实配时映射复杂训练不稳定在实际实验里离散动作的 DQN 在 100 个 episode 内就能看到平均等待时间下降的趋势连续动作版本经常在 200 个 episode 后还在原地打转。所以如果你只是想要一个能跑通、能写进毕设的交通信号灯强化学习方案离散动作是性价比最高的选择。3. 跑通环境SUMO 与 Python 的路径配置、车流生成和第一个 traci 脚本很多人在这一步卡死。代码写完了但 Python 里 import traci 失败或者 SUMO 启动后没有车流。这一章把环境从零配到能跑每一步都是可复现的命令。3.1 版本选择与工具链为什么我建议 SUMO 1.15Python 3.10我拆这套源码的经验是版本匹配比什么都重要。SUMO 的 traci 接口在 1.15 之后变化不大但在 Python 3.11、3.12 上偶尔会出现 numpy 兼容和编码问题。所以我一般建议用 Python 3.10 配合 SUMO 1.15 或 1.18这两个组合我验证过最稳。安装完 SUMO 后最关键的一步是配置路径。很多新手以为把 SUMO 装好就行结果在 Python 里 import traci 报 ModuleNotFoundError。原因很简单traci 不是独立 pip 包它是 SUMO 安装目录 tools 文件夹下的一个子目录你必须把这个目录加进 Python 的模块搜索路径。import sys import os SUMO_HOME rD:\Program Files\sumo-1.18.0 SUMO_TOOLS os.path.join(SUMO_HOME, tools) os.environ[SUMO_HOME] SUMO_HOME sys.path.append(SUMO_TOOLS) import traci print(traci version:, traci.getVersion())这段代码把 SUMO_HOME 写入环境变量再把 tools 目录追加到 sys.path。为什么不用系统环境变量因为很多学校机房或远程环境的 PATH 是不可写的直接在脚本里设置最省事也方便不同项目切换不同版本的 SUMO。在 PyCharm 里配置 Python 环境时你甚至不需要额外修改 Run Configuration只要在代码文件开头加上这段就能保证 traci 找到。版本差异还有一个坑老版本 SUMO 里traci.lane.getWaitingTime的返回单位或精度与新版本有细微差别。如果训练出来的 reward 曲线特别毛糙先确认 SUMO 版本是不是 1.15 以上别在版本问题上浪费时间调参。3.2 生成路网和随机车流从 netedit 到 randomTrips.pySUMO 仿真需要三个文件路网文件 .net.xml、车流文件 .rou.xml、配置文件 .sumocfg。手写这三个文件不现实常见做法是用 netedit 画一个十字路口然后导出路网车流用 SUMO 自带的 randomTrips.py 生成。netedit 的操作流程是打开 netedit选择“新建网络”用道路工具画一条横向道路和一条纵向道路交叉选中交叉点创建信号灯然后保存为 cross.net.xml。这个交叉口的信号灯 ID 可以在 netedit 里查看通常是类似 “gneJ1” 或 “J2” 的命名后面 traci 操作信号灯时要用这个 ID。车流生成的命令如下python D:\Program Files\sumo-1.18.0\tools\randomTrips.py \ -n cross.net.xml \ -r cross.rou.xml \ -e 1200 \ -p 2 \ --random-flow \ --min-distance 100参数含义-n指定路网文件-r指定输出车流文件-e 1200表示仿真时间到 1200 秒为止也就是 20 分钟-p 2表示平均每 2 秒发一辆车--random-flow让起终点随机分布避免所有车都从同一条路进来--min-distance 100保证起终点之间至少间隔 100 米减少短路程车辆的异常行为。生成完车流文件后还需要手写一个 cross.sumocfg 把路网和车流关联起来。?xml version1.0 encodingUTF-8? configuration input net-file valuecross.net.xml/ route-files valuecross.rou.xml/ /input time begin value0/ end value1200/ step-length value0.1/ /time /configurationstep-length是仿真步长单位秒0.1 表示每 0.1 秒仿真内部推进一步。这个值不需要改除非你要做高精度排放仿真。到这里环境文件齐了下一步就是验证 traci 能不能拉通整个仿真。3.3 跑通第一个 traci 脚本只读数据验证闭环第一次连 SUMO不要直接上 DQN先写一个只读脚本确认三件事SUMO 能启动、车流在跑、能读到车道数据。这个调试步骤能节省后面至少两小时的排查时间。import sys import os import traci SUMO_HOME rD:\Program Files\sumo-1.18.0 sys.path.append(os.path.join(SUMO_HOME, tools)) LANE_IDS [N2T_0, N2T_1, S2T_0, S2T_1, E2T_0, E2T_1, W2T_0, W2T_1] sumo_bin os.path.join(SUMO_HOME, bin, sumo-gui.exe) sumo_cmd [sumo_bin, -c, cross.sumocfg, --start, --quit-on-end] traci.start(sumo_cmd) step 0 while step 500: traci.simulationStep() if step % 50 0: halted_lanes sum( traci.lane.getLastStepHaltingNumber(l) 0 for l in LANE_IDS ) print(fstep{step}, 有排队车辆的车道数{halted_lanes}) step 1 traci.close()这段脚本的逻辑是启动 SUMO-GUI 并加载配置文件然后循环推进 500 个仿真步。每 50 步打印一次“有排队车辆的车道数”如果这个数字一直在变说明车流生成正常且 traci 数据接口联通。--start让 SUMO 启动后自动进入仿真不需要手动点击运行--quit-on-end让仿真结束后自动退出避免脚本挂死。注意打印频率每 50 步打印一次500 步总共打印 10 次信息量足够判断数据是否稳定。如果出现某条车道一直为 0可能是该方向没有车辆路径回 netedit 检查道路连接如果报 Connection refused 之类的错误多半是上一个 traci 进程没关干净杀掉所有 sumo 和 python 进程再重试。4. DQN 核心代码走读经验池、目标网络与相位切换闭环全流程环境通了接下来就是这套源码的核心DQN 训练闭环。这一章我把代码分成三段走读——网络与经验池怎么定义、动作怎么映射到相位、训练主循环里每一步在干什么。你看完应该能自己改参数重跑。4.1 经验池与 Q 网络先选好三件套的核心参数DQN 的三个核心组件Q 网络、目标网络、经验回放池。Q 网络负责输出每个动作的 Q 值目标网络用于计算 TD 目标经验池打破样本间的时间相关性。参数设置直接影响收敛快慢。import random from collections import deque import torch import torch.nn as nn class DQN(nn.Module): def __init__(self, state_dim, action_dim, hidden128): super().__init__() self.net nn.Sequential( nn.Linear(state_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, action_dim) ) def forward(self, x): return self.net(x) state_dim 25 action_dim 4 q_net DQN(state_dim, action_dim) target_net DQN(state_dim, action_dim) target_net.load_state_dict(q_net.state_dict()) memory deque(maxlen20000)这里 state_dim25 对应第 2 章的状态向量长度action_dim4 对应四个主相位。hidden128 是中间层节点数这个数值在交通信号灯这种状态量不大25 维的问题上够用调到 256 对结果提升不大但训练时间翻倍。经验池用 deque 实现maxlen20000 表示最多存两万条经验超出自动丢弃最旧记录。target_net 初始直接拷贝 q_net 的权重之后每训练一定步数再同步一次。关键参数我习惯这样定batch_size64gamma0.9learning_rate1e-3target_update500epsilon 从 1.0 衰减到 0.05。gamma 为什么不设 0.99因为相位优化是近乎实时的决策当前动作只影响未来十几秒的交通状态远期回报占比太高反而让模型学得迟钝。0.9 的折扣因子在这个场景里更敏感。4.2 动作映射与黄灯过渡从“跳相位”改成“压相位”DQN 输出一个动作编号后不能直接把这个编号塞给 setPhase因为真实信号灯从绿灯切到另一个方向的绿灯中间必须有黄灯过渡否则仿真里会出现车辆对穿交叉口的危险行为。这是一个新手几乎必踩的坑。TL_ID J2 # 改为你 netedit 里实际的信号灯 ID # 假设 netedit 相位表里0~3 为主相位4 为黄灯过渡相位 YELLOW_INDEX 4 current_phase 0 def apply_action(new_phase): global current_phase if new_phase current_phase: return # 先切到黄灯相位保持 4 秒 traci.trafficlight.setPhase(TL_ID, YELLOW_INDEX) for _ in range(4): traci.simulationStep() # 再切到目标主相位 traci.trafficlight.setPhase(TL_ID, new_phase) current_phase new_phase这段代码的核心是“黄灯过渡强制插入”。无论 DQN 输出的动作变化有多大执行顺序一定是当前相位 - 黄灯相位 - 目标相位。4 步黄灯的时长取决于你设定的仿真步长如果步长是 0.1 秒那么 4 步只有 0.4 秒太短实际应该根据黄灯相位 duration 来定常见做法是设置黄灯时间为 3 到 5 秒。上面代码的range(4)只是示意正式跑的时候请改成range(int(yellow_duration / step_length))。为什么用 setPhase 而不是 setRedYellowGreenStatesetPhase 只需要传相位索引简单可靠setRedYellowGreenState 需要你手写光色状态字符串比如 rrrrGGgrrrr 这种非常容易因为字符顺序和 link 数量不匹配而报错。我建议普通用户只用 setPhase把相位方案在 netedit 里设计好让索引对应关系固定下来。4.3 训练主循环从 observation 抓取到 learn 调度的一段完整流程训练主循环是整套代码的骨架。它要解决两件事一是按固定决策间隔收集样本二是周期性执行一次梯度下降。这里不贴全部代码只贴关键结构你能直接对应到源码相应函数。BATCH_SIZE 64 DECISION_INTERVAL 5 GAMMA 0.9 EPSILON_DECAY 0.9995 min_epsilon 0.05 optimizer torch.optim.Adam(q_net.parameters(), lr1e-3) loss_fn nn.MSELoss() def train_step(): if len(memory) BATCH_SIZE: return batch random.sample(memory, BATCH_SIZE) states, actions, rewards, next_states zip(*batch) states torch.tensor(np.array(states), dtypetorch.float32) next_states torch.tensor(np.array(next_states), dtypetorch.float32) rewards torch.tensor(np.array(rewards), dtypetorch.float32) actions torch.tensor(np.array(actions), dtypetorch.int64) q_values q_net(states).gather(1, actions.unsqueeze(1)).squeeze(1) with torch.no_grad(): next_q target_net(next_states).max(1)[0] target rewards GAMMA * next_q loss loss_fn(q_values, target) optimizer.zero_grad() loss.backward() optimizer.step()训练循环主体for episode in range(200): traci.start(sumo_cmd) state get_state(traci.trafficlight.getPhase(TL_ID)) step 0 episode_reward 0 while traci.simulation.getMinExpectedNumber() 0: traci.simulationStep() if step % DECISION_INTERVAL 0: action epsilon_greedy(state) apply_action(action) next_state get_state(traci.trafficlight.getPhase(TL_ID)) reward get_reward() memory.append((state, action, reward, next_state)) state next_state episode_reward reward if step % 500 0: target_net.load_state_dict(q_net.state_dict()) train_step() step 1 epsilon max(min_epsilon, epsilon * EPSILON_DECAY) traci.close() print(fepisode{episode}, total_reward{episode_reward:.2f})这段代码的调度逻辑每 5 个仿真步做一次决策和样本收集每 500 步同步一次目标网络每收集够 batch_size 条经验就做一次梯度下降。DECISION_INTERVAL5 很关键如果每 1 步就决策一次相位切换过于频繁车流还没对当前相位作出反应就变了经验池里全是无效样本如果设成 15 秒一次又来不及对突发拥堵作出响应。5 秒是我在这个场景里最常用的经验值。还要注意getMinExpectedNumber() 0这个循环结束条件——它表示路网里还有未到达终点的车辆。如果车流文件生成的车辆总数太少仿真会提前结束导致每个 episode 的数据量不够所以第 3 章的 randomTrips.py 参数里-e 1200一定不能设太小。5. 避坑指南SUMOPython 跑 DQN 最常翻车的 6 个现场与排查这一章是我实际跑这套代码的踩坑记录。每一条都是“现象 - 原因 - 解决”一条线讲清楚你看完至少能少折腾一晚上。5.1 import traci 找不到模块现象是红波浪线原因往往是 PATH现象PyCharm 里import traci标红运行直接 ModuleNotFoundError。原因traci 不是 pip 安装的包它只在 SUMO 安装目录的 tools 子目录里存在。你装了 SUMO 不等于 Python 能找到它。解决用第 3.1 节的脚本在 import traci 之前把sys.path.append加上。还有个大坑如果你在系统环境变量里配了 SUMO_HOME 但指向的是旧版本而你在脚本里又手动 append 了新版本路径Python 会先找到旧版本导致接口报错。我一般会在脚本开头打印traci.getVersion()确认版本号防止两个版本混用。5.2 setPhase 后车辆直接对穿相位索引和信号灯组索引没对上现象仿真一开始东西向和南北向车辆在交叉口中央撞成一团看着像没设信号灯。原因你在 netedit 里看到的“相位”和 setPhase 接受的索引不是一回事。netedit 的相位列表里一个相位可能是“东西直行右转”另一个相位是“全红过渡”索引顺序和你想的不一样。解决启动仿真后用 traci 打印当前信号灯的完整相位表核对每个索引对应的光色组合再写动作映射。这一步千万不能靠猜否则会出现绿灯放行冲突方向的低级事故。加上第 4.2 节的黄灯过渡逻辑后至少还要确认黄灯相位索引在相位表里的位置。5.3 reward 震荡得像随机游走先检查单位与量级再检查 seed现象训练 50 个 episodereward 曲线在正负几百之间来回跳完全看不出收敛趋势。原因大概率是状态和奖励里的单位不一致。比如 getWaitingTime 返回秒getLastStepMeanSpeed 返回 m/s如果你把速度当成 km/h 拼进 state等于给网络输入了一个量级偏差 3.6 倍的噪声特征reward 里如果直接用了绝对值等待时间不同车流密度下方差极大。解决先把 reward 改成第 2.3 节的变化量形式然后固定随机种子做对照实验。SUMO 的车流随机性很大固定 seed 的常见做法是生成车流时用 randomTrips.py 加上--seed 42同时在 Python 里random.seed(42)、np.random.seed(42)、torch.manual_seed(42)全部固定。seed 不固定你根本分不清 reward 下降是算法学的还是运气好。5.4 决策间隔 DT1 秒训练极其慢且 DQN 学不动现象训练一个 episode 要跑好久而且动作切换频率非常频繁看仿真画面像红绿灯在抽风。原因DT 太短agent 在车流还没对上一个相位变化作出反应之前就又要切换经验对之间的因果关系几乎不存在。解决把 DECISION_INTERVAL 调到 5 或 8 秒。注意DT 不是越小越聪明对于信号灯这种响应时滞明显的系统5 秒是一个不容易出错的取值。如果你发现某个方向排队特别长可以试试 8 秒给拥堵方向更多清空时间。5.5 开着 SUMO-GUI 跑 40 个 episode一晚上出不来一版结果现象训练速度极慢GPU 利用率上不去CPU 被 SUMO 图形渲染占满。原因SUMO-GUI 实时渲染画面消耗了大量本可以用于仿真的计算资源。解决训练阶段改用 sumo 而不是 sumo-gui通过--no-window参数跑无头模式只在需要观察车辆行为时用 GUI 回放。在 Linux 环境下无头模式是默认的在 Windows 上注意命令行工具要用 sumo.exe 而不是 sumo-gui.exe。5.6 没有基线对照你分不清 DQN 是“学了”还是“碰巧”现象训练完看总 reward 是正的觉得效果很好但横向一对比发现固定配时方案表现几乎一样甚至更好。原因没有设置基线对照单看 reward 绝对值的涨跌没有意义因为车流密度变化本身就影响 reward 量级。解决训练之前先跑一个固定配时比如每个相位固定 20 秒轮换的对照组记录平均等待时间和平均排队长度训练完用完全相同的车流文件再测一次 DQN两个结果对比才能说明优化有效。这一条应该成为你毕设实验设计里的默认动作不用是学霸只要做了对比结果就站得住。6. 验证 DQN 有没有真正学会控制两小时实验与相位扩展玩法前面全是准备工作这一章给你一套能在两小时内验证“DQN 到底学没学会”的实验方案以及三种让效果更进一步的扩展玩法。6.1 用固定配时基线做对照看三个信号验证实验的步骤很简单生成一组固定车流文件先跑固定配时每相位 20 秒轮换再跑 DQN最后对比三个指标。第一个指标是平均等待时间直接在训练循环里累加getWaitingTime求均值第二个是平均排队长度累加getLastStepHaltingNumber求均值第三个是平均通行速度少数几条关键车道求速度均值。按我前面配的参数40 个 episode 的 DQN 大约需要两小时实验安排正好。你不需要盯着训练曲线看每集的 reward那是调试时用的。验证时只看最终对比表如果 DQN 比固定配时的平均等待时间低 15% 以上排队长度有明显下降说明相位切换策略确实学到了车流规律如果两者持平先检查 DT 和 reward 权重别急着加网络复杂度。等你有了一组能稳定复现的结果再谈“优化成功”这四个字。6.2 扩展玩法相位扩展到 4 相位State 换成车流分布矩阵相位优化做顺手之后有三件事值得试。第一把动作空间从四相位扩展成包含黄灯等过渡相位在内的完整切换策略这需要重写 apply_action但状态和奖励可以不动。第二把 state 从“逐车道特征向量”改成“交叉口车辆分布矩阵”把路网网格化后统计每个网格的车辆数配一个卷积层或图注意力层这条路触到了 Colight 那类强化学习信号灯控制论文的思路适合做毕设的创新点。第三在 reward 里加一条“单车道最大等待时间惩罚”专门治“一个方向畅通、另一个方向等了三轮绿灯”的不公平现象这也是实际路口最容易被投诉的场景。整套源码的结构并不复杂——SUMO 提供仿真环境和数据源traci 提供 Python 到 SUMO 的桥梁DQN 负责从经验池里学习“什么时刻该放行哪个方向”。你能走到这一章说明前面第 3 章的环境已经通了。这里给你一个实操建议把自己的路网文件、车流文件、第 4 章代码在项目里各建一个文件夹固定好 seed 和 SUMO 版本从此以后我每次跑实验都强制自己先跑一遍固定配时基线再让 DQN 上场并且随时把 SUMO_HOME 的版本号写进实验笔记避免某天换了环境结果全变。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网