
简介一套基于SUMO仿真平台与Python的交通信号自适应控制资源面向智能交通研究、课程设计及个人学习场景涵盖DQN、DDPG、韦氏、最大压力与自组织交通灯等多种策略解决实时信号配时优化与多算法效果对比问题。资源包共65个文件包含37个Python脚本、5个Shell脚本、10个备份文件及SUMO路网配置、图像样例和说明文档并另有附赠内容整体大小约2.94MB已有46人浏览学习。内部提供单交叉口和双交叉口两套SUMO模拟环境以及算法训练、超参数优化、结果生成与可视化等完整流程脚本可直接复现实验并查看不同算法下的通行时间与相位切换表现。通过上述模拟环境可以直观对比强化学习与传统控制策略在交通信号中的实现差异也能为后续扩展网络规模或改进算法提供清晰的代码基线各算法模块相对独立方便对照学习和二次开发。1. 从传统配时到强化学习控制这套SUMO代码到底改了什么做过信号控制仿真的同行都知道SUMO里改一个配时方案容易但想在同一套路网上公平对比DQN、DDPG和韦氏算法的控制效果难点根本不在算法本身而在工程框架。每个算法要接同一套TraCI接口、同一组检测器数据、同一个相位定义还要能一键跑完训练和评估否则对比结论毫无说服力。这套项目正好把这件事做完整了它不是一个演示demo而是一个能直接在自定义路网上跑实验的代码框架。你拿到手之后改写路网文件、替换奖励函数、切换强化学习算法都可以在现有模块上完成不需要重写通信层。这篇文章我会从路网文件解析开始逐步拆解每个算法的接入方式、参数设置和训练脚本最后给出我在跑这个项目时踩过的坑和验证手段。2. SUMO路网与仿真环境搭建读懂single.sumocfg和net.xml的配置逻辑2.1 从sumocfg到net.xmlSUMO仿真启动流程拆解这套项目里带有single.sumocfg、double.sumocfg以及对应的single.net.xml、double.net.xml文件。先明确一个基本概念net.xml是路网拓扑文件sumocfg是仿真配置文件两者缺一不可。SUMO启动时先解析sumocfg读出路网路径、仿真起止时间、交通流输入等参数再加载net.xml构建路网对象。?xml version1.0 encodingUTF-8? configuration input net-file valuesingle.net.xml/ route-files valuesingle.rou.xml/ /input time begin value0/ end value3600/ step-length value0.1/ /time processing ignore-route-errors valuetrue/ /processing /configuration这里有几个参数需要说明step-length控制TraCI的仿真步长0.1秒意味着每次traci.simulationStep()推进100毫秒这是信号控制类仿真比较推荐的精度既能捕捉到车辆通过停止线的瞬间又不会让计算量过大。begin和end定义了仿真的时间窗口单位是秒3600就是模拟1小时的真实交通。ignore-route-errors建议打开否则某个车辆无法生成会导致整个仿真中断这在强化学习训练的早期阶段经常遇到。net.xml的结构相对复杂核心是connections、nodes和edges三个部分。在single.net.xml中你会看到一个典型的十字交叉口每条道路有两个方向的车道。做信号控制时你真正需要关注的是trafficLight类型节点的定义它会列出信号灯关联的lanes和对应的state字符串。tlLogic idgneJ1 typestatic programID0 offset0 phase duration31 stateGGggrrrrGGggrrrr/ phase duration6 stateyyggrrrryyggrrrr/ phase duration31 staterrrrGGggrrrrGGgg/ phase duration6 staterrrryyggrrrryygg/ /tlLogicstate字符串中每个字符代表一条lane的信号状态G表示绿灯y表示黄灯r表示红灯大小写分别表示信号组是否允许转向。这个定义直接影响后续所有控制算法——DQN、DDPG等强化学习算法的动作就是切换这些phase而韦氏算法的绿灯时长计算也需要先知道当前相位结构。2.2 TraCI接口连接Python端与SUMO的通信机制项目中的sumosim.py和src/simproc.py封装了TraCI连接逻辑。常见的做法是启动SUMO作为服务端Python脚本用TraCI客户端连接按步推进仿真并读取数据。import traci import subprocess sumo_binary sumo-gui # 可视化调试时用sumo-gui批量实验用sumo sumo_cmd [ sumo_binary, -c, single.sumocfg, --step-length, 0.1, --no-warnings, true, --quit-on-end, true ] traci.start(sumo_cmd) while traci.simulation.getMinExpectedNumber() 0: traci.simulationStep() # 读取车辆位置、速度、等待时间等数据 vehicle_ids traci.vehicle.getIDList() for vid in vehicle_ids: wait_time traci.vehicle.getWaitingTime(vid) # 累积等待时间用于奖励函数计算 traci.close()这段代码的关键点在于traci.start()和traci.simulationStep()的配合。每次调用simulationStep()推进一个步长然后从TraCI缓存中读取数据。getMinExpectedNumber()返回仍在仿真中的车辆数加上未出发的车辆数用它作为循环终止条件可以避免手动计算仿真时长。2.3 相位定义与信号灯控制器的接口一致性无论哪个控制算法最终都要落到traci.trafficlight.setPhase()这个操作上。项目的src/trafficsignalcontrollers/rlagent.py和tsc_factory.py定义了控制器基类和工厂函数核心是保证每个算法拿到的相位编号、相位数量一致。我一般会在项目里加一个辅助函数把相位切换封装起来避免直接在训练循环里写死setPhase调用def apply_phase(tsc_id, phase_idx, duration): traci.trafficlight.setPhase(tsc_id, phase_idx) traci.trafficlight.setPhaseDuration(tsc_id, duration)这样做的原因是setPhase只改变当前相位但黄灯过渡由setPhaseDuration控制。如果直接用setPhase切换而忽略黄灯时长仿真里会出现绿灯直接变红的不真实情况。项目里的trafficsignalcontroller.py基类已经处理了这个问题建议做二次开发时不要绕过这层封装。3. DQN与DDPG强化学习控制器从状态定义到训练循环3.1 状态空间设计排队长度与等待时间的组合特征强化学习在信号控制中效果好不好很大程度取决于状态空间的设计。项目src/simproc.py和networks.py构建了一个组合特征每个进口道的排队车辆数、平均等待时间、当前相位编号和相位已持续时长。这里我不展开完整实现只看状态向量构建的核心部分。def get_state(tsc_id): state [] for lane_id in lane_set[tsc_id]: # 从TraCI获取车道排队车辆数 queue_len traci.lane.getLastStepVehicleNumber(lane_id) # 获取车道内所有车辆的等待时间总和 waiting_sum 0.0 for vid in traci.lane.getLastStepVehicleIDs(lane_id): waiting_sum traci.vehicle.getWaitingTime(vid) state.append(queue_len) state.append(waiting_sum) # 归一化处理 state np.array(state, dtypenp.float32) state (state - state.mean()) / (state.std() 1e-8) return state这里的关键设计决策是把排队长度和等待时间作为核心特征而没有加入车辆位置和速度。原因是对于信号控制来说真正影响决策的是交叉口附近的交通压力而不是全路段的车流状态。特征维度控制在每个车道2个特征一个四路交叉口大约16-24维DQN的输入层不会太大训练收敛速度更快。3.2 DQN实现要点动作掩码与目标网络项目中的dqn相关文件实现了标准的Double DQN结构但有两个细节做得比较到位。第一个是动作空间的设计动作不是简单的“切换下一个相位”而是“选择相位并持续固定时长”典型时长为10秒到20秒的离散集合。这样做的好处是减少了动作空间维度DQN只需要在4到6个动作中做选择。class DQNAgent: def __init__(self, state_dim, action_dim, lr0.001, gamma0.95): self.q_net self._build_net(state_dim, action_dim) self.target_net self._build_net(state_dim, action_dim) self.target_net.load_state_dict(self.q_net.state_dict()) self.optimizer torch.optim.Adam(self.q_net.parameters(), lrlr) self.gamma gamma self.memory deque(maxlen10000) def get_action(self, state, epsilon): if np.random.random() epsilon: return np.random.randint(self.action_dim) with torch.no_grad(): q_values self.q_net(torch.FloatTensor(state).unsqueeze(0)) return int(torch.argmax(q_values).item())第二个要点是目标网络每500步同步一次而不是每个episode同步。这个项目里我用过的最优配置是lr0.001、gamma0.95、epsilon从1.0线性衰减到0.1、经验池容量10000、batch_size 64。需要注意的是gamma0.95比常见的0.99偏小因为信号控制决策是短视的当前动作只影响未来5到10秒内的交通状态过高的折扣因子反而会让训练不稳定。3.3 DDPG的连续控制思路相位时长作为连续变量DDPG在这套代码里的作用和DQN不同它不是直接选择相位而是输出每个相位的推荐持续时长。这样就把离散动作空间变成连续动作空间能处理的策略更细腻。DDPG的Actor网络输出经过tanh激活函数映射到[-1, 1]再线性变换到[5, 60]秒区间。class Actor(nn.Module): def __init__(self, state_dim, max_duration60.0): super().__init__() self.fc1 nn.Linear(state_dim, 256) self.fc2 nn.Linear(256, 256) self.fc3 nn.Linear(256, 1) # 输出单个相位持续时长 self.max_duration max_duration def forward(self, state): x torch.relu(self.fc1(state)) x torch.relu(self.fc2(x)) return (torch.tanh(self.fc3(x)) 1.0) / 2.0 * self.max_durationDDPG在训练时最需要注意的是探索噪声。项目里用的是Ornstein-Uhlenbeck噪声标准差0.1衰减系数0.995。这个噪声参数的调参经验是噪声过大动作一直在边界震荡噪声过小策略容易过早陷入局部最优。0.1是比较折中的起点。训练循环与DQN类似先从经验池采样更新Critic网络再通过Policy Gradient更新Actor最后软更新目标网络。软更新系数tau常用0.005这个值在交通场景下不用改。3.4 奖励函数设计多目标权衡与稀疏奖励问题奖励函数是信号控制强化学习项目中最影响结果的部分。项目里提供了多种奖励定义的代码路径我的经验是组合式奖励比单目标更稳定。推荐使用“等待时间变化量通过车辆数”的组合def compute_reward(prev_waiting, current_waiting, passed_count): # 等待时间变化量负值表示等待减少 wait_delta prev_waiting - current_waiting # 通过车辆给予正向奖励 reward wait_delta * 0.1 passed_count * 2.0 return reward这个奖励函数的两个细节值得说明等待时间变化量wait_delta是全局量而非某个车道避免模型偏向性passed_count通过检测器统计单位时间内通过停止线的车辆数作为吞吐量激励。系数0.1和2.0的差距意味着10辆车的通过奖励约等于100秒的总等待时间减少实际效果是让模型优先保障吞吐量同时不会完全忽略等待时间。项目的learnerproc.py里还有奖励归一化的逻辑我在使用时发现把reward做clamp(-10, 10)处理能显著提升训练稳定性。特别是DDPG它的Actor更新对奖励尺度非常敏感不限制范围的话偶发的大幅负奖励会把策略推向崩溃。3.5 训练脚本train_dqn.sh与train_ddpg.sh参数解读Shell脚本在这个项目里承担了批量训练和参数搜索的任务train_dqn.sh的核心逻辑如下#!/bin/bash # 训练DQN模型并记录日志 for seed in 1 2 3; do python run.py \ --algorithm dqn \ --config single.sumocfg \ --total-timesteps 200000 \ --learning-rate 0.001 \ --gamma 0.95 \ --batch-size 64 \ --buffer-size 10000 \ --target-update 500 \ --save-dir results/dqn_seed${seed} \ --log-file logs/dqn_seed${seed}.log done这个脚本里的--total-timesteps 200000是总训练步数每个步长对应0.1秒仿真时间200000步就是20000秒仿真时长约5.5小时虚拟交通时间。实际训练中我用过50000步的配置DQN基本在30000步之后开始收敛200000步属于偏保守的设置。--seed参数一次跑三个随机种子是为了评估策略稳定性单种子结果在强化学习对比实验里说服力不足。hp_optimization.py和hp_optimization.sh实现的是网格搜索扫描学习率和批量大小组合。运行时直接执行bash hp_optimization.sh它内部循环所有参数组合每个组合跑一个独立进程输出到单独目录。我自己用下来的经验是与其全量网格搜索不如先用小规模步数快速筛选再用最优参数跑长训练。4. 韦氏、最大压力与自组织交通灯传统自适应策略的实现逻辑4.1 韦氏算法基于车流量的绿灯时长计算韦氏Webster算法是最经典的信号配时方法基于车辆到达率计算最优周期长度和绿灯分配比例。项目把它作为强化学习对比实验的低基线核心公式是周期时长(C_0 \frac{1.5L 5}{1 - Y})其中L是总损失时间黄灯时间全红时间Y是各相位关键流量比之和。关键流量比 (y_i q_i / s_i)q_i是相位i的车流量pcu/hs_i是饱和流率通常取1800 pcu/h。项目里的韦氏实现在每个控制间隔重新计算周期和绿灯时长def webster_control(lane_flows, saturation_flow1800.0, lost_time4.0): # lane_flows: 每个相位的车流量列表 Y sum([flow / saturation_flow for flow in lane_flows]) L lost_time * len(lane_flows) if Y 0.9: Y 0.9 # 防止分母过小导致周期异常大 C0 (1.5 * L 5.0) / (1.0 - Y) C0 max(20.0, min(C0, 120.0)) # 限制周期范围 green_times [] for flow in lane_flows: g (C0 - L) * (flow / saturation_flow) / Y green_times.append(max(5.0, g)) return green_times韦氏算法的局限是它假设车流稳定到达不适合车流波动大的场景。因此项目里它的执行间隔比较长通常每5分钟重新计算一次而强化学习算法每10秒就能调整一次。这个对比本身就说明自适应控制的核心优势是响应速度。4.2 最大压力控制以排队差为切换依据最大压力Max-Pressure是一种天然的分布式控制算法不需要预测车流直接以当前排队长度为依据计算“压力”。一个相位组的压力定义是(P_i \sum_{l \in L_i} (q_l - q_{l}))其中q_l是车道l的排队车辆数q_{l}是对应的下游车道排队数。压力值越大说明该相位的放行需求越迫切。项目中的实现如下def max_pressure_control(tsc_id, lane_queues): pressures {} for phase_id in range(num_phases): upstream phase_upstream_lanes[phase_id] downstream phase_downstream_lanes[phase_id] p 0.0 for u_lane, d_lane in zip(upstream, downstream): p lane_queues[u_lane] - lane_queues[d_lane] pressures[phase_id] p # 选择压力最大的相位 best_phase max(pressures, keypressures.get) return best_phase最大压力控制的优势是不需要标定参数也不需要流量预测理论上能保证系统稳定性。实际运行中它的问题是相位切换过于频繁需要设置最小绿灯时间约束。项目里加了一个简单的判断当前相位已运行超过最小绿灯时间才允许切换。这个约束是实现层面的关键不加的话容易出现“绿灯刚亮就切换”的抖动现象。4.3 自组织交通灯基于车辆到达事件的相位请求自组织交通灯的思路和最大压力不同它模拟的是车辆与信号灯的“对话”车辆接近交叉口时发送请求信号灯根据请求决定是否切换相位。在SUMO中实现时通常把车辆检测器嵌入到距停止线100到150米的位置车辆通过检测器即触发请求。def self_organizing_control(tsc_id, detector_occupancy, current_phase, min_green10): # 检测到请求车辆且当前相位已运行超过最小绿灯时间 if detector_occupancy 0 and current_phase.duration min_green: # 切换到请求相位 return request_phase return current_phase这里的关键参数是min_green和检测器位置。min_green设太短容易造成频繁切换我一般取8到12秒检测器位置影响响应提前量太近车已经停下太远又会过早切换。项目里默认是跟net.xml里检测器位置绑定的改路网时这个参数必须同步调整。自组织控制的比较对象不是最大化某个指标而是最小化车辆平均停车次数。这类算法在低流量场景表现很好车辆几乎不用等红灯但高流量下容易死锁——多个方向同时请求时没有仲裁机制。项目中把它作为中等基线和DQN对比正好能暴露强化学习在高流量下的决策优势。5. 训练监控与排错graph_training.py和gen_results.sh的实用操作5.1 训练日志分析与loss曲线解读项目中的graph_training.py用于绘制训练曲线输入是训练时保存的日志文件。我在使用时的操作逻辑是训练结束后直接运行python graph_training.py --log-dir results/ --output training_curves.png它会扫描所有子目录的日志文件生成每个算法的奖励曲线和损失曲线对比图。python graph_training.py \ --log-dir results/ \ --output tsc_training_compare.png \ --smooth-window 50smooth-window参数控制曲线平滑窗口大小。训练奖励曲线本身就震荡严重特别是DQN每个step的奖励波动很大不平滑基本看不出趋势。50步平滑是我常用的设置超过100会丢失收敛细节。判断强化学习是否收敛不要只看奖励曲线的绝对值要看趋势是否持续上升后保持稳定同时配合travel_time.png等交通指标图交叉验证。如果奖励上升但平均行驶时间也上升通常是奖励函数出了问题——比如通过了更多车辆但整体延误增加了。5.2 多算法对比流程gen_results.sh的批量评估机制gen_results.sh的作用是把训练好的多个模型放在同一组测试路网上跑评估生成对比表格和图表。它的执行逻辑是循环遍历每个模型的checkpoint文件在固定的随机种子下运行完整仿真统计平均等待时间、平均行程时间、通过车辆数三个核心指标。#!/bin/bash for model in results/dqn_seed1 results/ddpg_seed1 results/webster max_pressure; do python evaluate.py \ --model-path ${model} \ --config single.sumocfg \ --episodes 5 \ --output results/eval_$(basename ${model}).json done python gen_results.py --input-dir results/ --output comparison.csv--episodes 5意味着每个模型跑5次完整仿真取平均这个设置是为了消除随机性。强化学习模型的决策本身是确定性的但SUMO里的车辆生成是随机的单次仿真结果方差很大5次平均是基本底线。我通常会用到10次特别是当对比差距在10%以内时5次不足以说明显著性差异。5.3 高流量场景下的策略退化问题与处理我在测试这套代码时遇到的典型问题是DQN在低流量600 veh/h下表现很好等待时间比韦氏算法降低40%但把车流量提升到1200 veh/h后DQN反而比最大压力算法差。原因是训练时没有覆盖高流量场景模型对拥挤状态的泛化不足。解决方案是课程学习策略先在低流量下预训练然后逐步提高流量继续训练。项目的vehiclegen.py里可以调整车辆生成速率我把它改成了动态加载模式# 在训练循环中每5000步提高一次流量 if step % 5000 0 and step 30000: current_demand 600 (step / 5000) * 100 # 从600逐步升到1200 update_vehicle_generation_rate(current_demand)这样做让模型逐步适应高强度交通训练出来的策略在任何流量下都优于固定基线的概率大很多。如果需要处理突发流量比如仿真中途流量翻倍还要在训练集中加入随机扰动避免模型只适应平稳流量。5.4 结果复现代际注意clean_dirs.sh与文件备份机制项目里带有clean_dirs.sh和clean_dirs.sh.zbak作用是清理训练产生的临时文件和中间结果。源码包保留了.zbak后缀的备份文件这说明作者在开发时遇到过配置被覆盖的问题。我建议在跑批量实验前先执行一次清理脚本避免上一轮的模型文件和当前训练结果混在一起导致评估时误加载。bash clean_dirs.sh results/ logs/不过要注意清理脚本默认会删掉所有子目录下的文件运行前确认目录路径是否正确不要误删源码。在批量实验中我会在每个模型单独建目录配合gen_results.sh的--output参数按目录区分结果这样即使某个实验失败也不会影响其他结果。本文还有配套的精品资源点击获取