简介强化学习是一种通过智能体与环境交互试错来学习最优策略的机器学习范式DDPG算法则能够在连续动作空间中高效决策常用于机器人控制与自动驾驶等领域。在智能交通中交通信号灯控制被建模为序列决策问题传统定时控制难以应对随机波动的车流。本文以DDPG为核心结合SUMO交通仿真环境搭建十字路口仿真场景设计包含排队长度与等待时间的10维状态空间、绿灯延长动作空间以及归一化奖励函数并给出完整的Python实现。经过训练的智能体在随机车流场景下相比定时控制可降低约30%的平均等待时间。文中还总结了奖励函数设计、超参数调整等工程实践中的典型踩坑经验为深度强化学习入门者以及智能交通控制开发者提供可复用的参考。 信号灯控制这个领域我前前后后折腾了快两年。最近把手头这套基于DDPGDeep Deterministic Policy Gradient的交通信号灯控制智能体完整整理了一下顺手把源码、模型和训练日志打包成了zip。这个项目用Python实现配合SUMO交通仿真环境目标是让一个强化学习智能体自己学会怎么控制十字路口的红绿灯配时。最终模型在随机车流场景下把车辆平均等待时间相比传统定时控制降低了30%左右而且泛化能力还不错。这篇文章就把整个项目的核心思路、算法细节、代码实现和踩坑记录完整复盘一遍适合正在入门深度强化学习、或者想用RL做交通控制但不知道从哪下手的同学参考。1. 为什么用强化学习做信号灯控制1.1 传统信号灯控制的困境先说清楚我们到底在解决什么问题。城市路口的信号灯控制主流方案无非三种定时控制、感应控制和自适应控制。定时控制就是固定配时比如南北向绿灯40秒、东西向绿灯30秒按固定周期轮换。感应控制会在路口埋线圈或装摄像头检测到有车等待就延长绿灯没车就提前切换本质上是个阈值规则。自适应控制则是基于实时交通数据做优化计算但很多实际落地的方案依赖精确的交通流预测模型路况一复杂就力不从心。这三种方案的共同问题在于它们都是基于人工预设规则或简化模型的没有办法真正“适应”千变万化的交通流。早高峰、晚高峰、节假日、突发事件车流特征完全不一样固定规则很难同时兼顾所有场景。就算你用一套复杂的自适应算法本质上还是在用数学模型去拟合交通规律而交通系统恰恰是一个高度随机、非线性、时变的系统你很难用精确的数学公式描述它。1.2 强化学习解决这个问题的思路强化学习的思路完全是另一条路。我们不试图建模交通流的精确规律而是让一个智能体信号控制器直接和环境交通路口交互通过试错的方式学到最优策略。智能体在每个决策时刻观察当前交通状态比如各方向排队长度、等待时间然后决定信号灯该怎么变化环境给出一个奖励信号比如负的平均等待时间智能体根据奖励调整自己的策略目的是让长期累积奖励最大化。这个思路跟交通控制问题的适配度非常高。首先交通流天然满足马尔可夫性也就是说下一时刻的状态很大程度上取决于当前状态和当前采取的动作这正好是强化学习的标准假设。其次信号灯控制是一个典型的序列决策问题现在的配时选择不仅影响当前车辆通行还会影响未来一段时间内的交通状态强化学习恰恰擅长处理这种需要考虑长期回报的决策。第三强化学习不需要构建精确的交通流模型只要有仿真环境就够了这让它在复杂路况下比传统方法有天然优势。这个项目选的是十字单路口场景先把单路口做透验证算法有效性之后再扩展到多路口协调这是比较稳妥的路线。2. DDPG算法选型它凭什么能胜任2.1 先看DDPG解决什么问题在强化学习的算法谱系里DDPG属于Actor-Critic家族同时结合了深度Q网络DQN和经验回放的思想。简单理解它有两个角色Actor是“做决策的”给定当前状态输出一个动作Critic是“打分的”给定当前状态和Actor输出的动作评估这个动作有多好。两个角色在训练中互相促进Actor通过Critic的反馈改进策略Critic通过经验数据更新评分标准。DDPG最核心的特点是它能在连续动作空间上做决策这是它区别于DQN的最大优势。DQN通过输出每个离散动作的Q值来选择最优动作非常适合像游戏AI这种动作集合有限的场景。但一旦动作空间变成连续量比如“绿灯延长多少秒”这件事理论上可以是0到60秒之间的任意实数DQN就力不从心了你不可能为每0.1秒都输出一个Q值。2.2 信号灯控制如何转化为连续决策问题这里有个关键点需要单独拿出来说清楚。信号灯控制表面上是一个离散决策问题——切换哪个相位、切换或不切换——但DDPG要处理的是连续动作这中间需要一个巧妙的设计转换。我的做法是把动作定义为“当前绿灯相位的延长时长”。智能体在每个决策时刻不看别的就看当前正在放行的相位有没有必要继续绿灯输出一个0到最大绿灯时长之间的连续值代表“还可以再绿多少秒”。这个连续延时时长超过某个阈值就继续保持绿灯否则切换相位。这样既保留了DDPG处理连续动作的优势又和信号灯控制的实际操作习惯完美对齐。具体映射关系是这样DDPG的Actor网络输出一个经过tanh激活的值范围在[-1, 1]之间然后线性映射到[0, max_green_extension]比如[0, 30]秒。当输出值对应的延时时长小于3秒这个阈值可以调就说明当前相位通行效率已经很低了直接切换相位大于等于3秒就按输出值延长绿灯。这个3秒阈值是经验值太小会导致频繁切换产生绿灯损失时间太大又失去了连续延长的意义。2.3 与其他强化学习算法的对比项目初期我也对比过DQN、PPO等算法最终选DDPG不是拍脑袋决定的这里把对比结果整理成表格方便大家理解算法动作空间样本效率训练稳定性实现复杂度信号灯场景适配度DQN离散中中低中需要人为离散化动作DDPG连续高中对超参敏感中高天然匹配时长控制PPO连续/离散中高中高中高但实现复杂PPO的优势是稳定性好但DDPG的样本效率更高在仿真环境里训练速度更快而且它天然能应对“绿灯延长”这类连续控制需求。从最终效果来看DDPG在这个任务上收敛效果也足够好所以我最终选择了DDPG作为核心算法。3. 仿真环境与问题建模3.1 SUMO环境搭建重度强化学习项目离不开仿真环境。交通控制领域最主流的开源仿真器是SUMOSimulation of Urban MObility它是德国宇航中心开发的支持微观交通仿真能精确到每辆车、每条车道的行驶状态而且提供了TraCI这个Python接口可以实时读取仿真状态、控制信号灯、发送车辆。项目实践中最常用的方案就是“SUMO Python TraCI”这套组合在交通强化学习领域基本是标配。安装SUMO之后需要准备路网文件。我用的是一个标准的十字路口路网东南西北四个方向各有一条进车道和一条出车道进车道又分左转、直行、右转三条车道。路网文件可以用SUMO自带的netedit工具手工画也可以用xml直接写项目zip包里已经包含了完整的cross.sumocfg和cross.net.xml直接运行就能加载仿真环境。TraCI接口的用法很直接示例代码如下import traci # 启动SUMO仿真器加载路网 traci.start([sumo, -c, cross.sumocfg, --no-window]) # 获取某个车道的排队长度 queue_length traci.lane.getLastStepHaltingNumber(WE_0) # 获取所有车辆的平均等待时间 total_waiting 0.0 for veh_id in traci.vehicle.getIDList(): total_waiting traci.vehicle.getWaitingTime(veh_id) # 设置信号灯相位 traci.trafficlight.setPhase(gneJ1, phase_index)有个小技巧值得说下训练时用--no-window参数关闭SUMO的图形界面能节省大量渲染开销训练速度快很多。但调试阶段一定要打开窗口看可视化亲眼看智能体学出来的策略是什么样的这对理解算法行为、定位问题非常有帮助。3.2 状态空间、动作空间与奖励函数设计状态空间的设计决定了智能体能看到什么信息。我用的是442结构总共10维四个方向进口道的车辆排队长度单位辆从TraCI的getLastStepHaltingNumber获取四个方向进口道的平均等待时间单位秒从getVehicleWaitingTime计算平均当前相位编号离散值映射到0~1之间当前相位已持续时长单位秒归一化到0~1这里有一个关键细节所有状态分量必须做归一化处理。排队长度除以最大排队容量比如30辆等待时间除以最大等待上限比如120秒这样不同量纲的数据才能顺利进入神经网络。如果不做归一化训练基本必炸。动作空间上面已经提过1维连续变量当前相位的绿灯延长时长范围[0, 30]秒。奖励函数我前前后后改了很多版最终稳定下来的是这个公式def compute_reward(): # 所有车辆的平均等待时间秒 avg_waiting total_waiting_time / max(len(vehicles), 1) # 四个进口道排队长度之和 total_queue sum(lane_queues) # 归一化后的奖励负值希望智能体最小化这两项 reward -(avg_waiting / 120.0 0.5 * total_queue / 30.0) return reward奖励设计是这个项目的灵魂后面会单独讲我踩过的几个大坑。总之最终方案的核心思路就是让智能体同时优化“平均等待时间”和“总排队长度”两个指标通过归一化系数把不同量纲拉到一个尺度上再乘以权重平衡两者的重要性。4. 代码实现细节与训练过程4.1 项目文件结构zip包解压后项目结构是这样的ddpg_traffic_control/ ├── cross.sumocfg # SUMO仿真配置文件 ├── cross.net.xml # 十字路口路网文件 ├── train.py # 训练主脚本 ├── ddpg_agent.py # DDPG智能体实现Actor/Critic网络经验回放 ├── env_traffic.py # SUMO环境封装给智能体提供状态和奖励 ├── evaluate.py # 模型评估脚本对比DDPG vs 定时控制 ├── models/ # 训练好的模型权重 │ ├── actor.pth │ └── critic.pth └── logs/ # TensorBoard训练日志 └── events.out.tfevents.*env_traffic.py是项目的核心枢纽它把SUMO的TraCI接口封装成标准的强化学习环境接口对外暴露三个方法reset()重置仿真状态step(action)执行动作并返回新的状态、奖励、是否结束get_state()采集当前交通状态。这样设计的好处是环境接口和算法完全解耦以后想换PPO或SAC算法只需要替换ddpg_agent.py环境完全不用动。4.2 Actor与Critic网络结构网络结构我也做了多次迭代最终版本是三层全连接网络。Actor和Critic都用了256个神经元的隐藏层这个层宽在10维状态输入下表现最稳定太窄64学不到复杂映射关系太宽512容易过拟合且训练慢。Actor网络的代码实现import torch import torch.nn as nn import torch.optim as optim import numpy as np class Actor(nn.Module): def __init__(self, state_dim10, action_dim1, max_action30.0): super(Actor, self).__init__() self.fc1 nn.Linear(state_dim, 256) self.fc2 nn.Linear(256, 256) self.out nn.Linear(256, action_dim) self.max_action max_action def forward(self, state): x torch.relu(self.fc1(state)) x torch.relu(self.fc2(x)) # tanh输出范围[-1,1]映射到[0, max_action] return (torch.tanh(self.out(x)) 1.0) / 2.0 * self.max_actionCritic网络接收状态和动作的拼接作为输入class Critic(nn.Module): def __init__(self, state_dim10, action_dim1): super(Critic, self).__init__() self.fc1 nn.Linear(state_dim action_dim, 256) self.fc2 nn.Linear(256, 256) self.out nn.Linear(256, 1) def forward(self, state, action): x torch.cat([state, action], dim1) x torch.relu(self.fc1(x)) x torch.relu(self.fc2(x)) return self.out(x)这里有几个从实践中学到的网络设计细节。一是Actor的输出层直接使用tanh激活而不是sigmoidtanh的梯度在0附近比较陡峭训练初期的动作探索效率更高。二是Critic把动作拼接到第一层输入而不是在中间层拼接这是因为中间层特征已经高度抽象动作信息很难通过反向传播有效影响特征提取。三是网络权重初始化用默认的PyTorch初始化就行我自己试过Xavier初始化和正交初始化在这个任务上效果区别不大反而默认初始化最省事。4.3 经验回放、软更新与噪声探索DDPG的稳定性依赖于三个关键机制经验回放、目标网络软更新、动作噪声探索。经验回放池我设置了10万条记录的容量。每次从环境中拿到一条经验状态、动作、奖励、下一状态、是否结束存进回放池。训练时从中随机采样64条记录打破样本之间的时间相关性让神经网络的训练数据满足独立同分布假设。目标网络软更新是DDPG的另一个核心设计。Actor和Critic各有一个目标网络它们的参数不是直接复制而是通过微小步长逐步逼近当前网络参数def soft_update(target, source, tau0.005): for target_param, source_param in zip(target.parameters(), source.parameters()): target_param.data.copy_( tau * source_param.data (1.0 - tau) * target_param.data )tau取0.005意味着目标网络的参数每步只向当前网络移动0.5%这保证了训练目标的稳定性。如果更新太快目标值随之剧烈变化训练会发散太慢则学习速度受影响。探索策略上DDPG常用的是OU噪声Ornstein-Uhlenbeck process它和纯高斯噪声的区别是OU噪声在时间上有相关性不会每步都随机跳变产生的动作更平滑更适合像交通控制这类需要动作连续性的场景。我用的参数是theta0.15、sigma0.2训练过程中噪声幅度会从1.0线性衰减到0.1让智能体前期充分探索后期趋向确定性策略。4.4 训练主循环完整训练流程是环境交互和网络更新的交替循环。每个episode模拟1小时的交通流也就是3600个仿真秒智能体每10秒做一次决策所以一个episode有约360个决策点。车流生成参数到达率、转向比例在每次episode开始时随机设置让智能体在训练中见过各种不同的交通状况。核心训练循环的代码逻辑如下# 每10秒决策一次进行一轮训练 for episode in range(max_episodes): state env.reset() episode_reward 0.0 for step in range(360): # 3600秒 / 10秒 360个决策点 # 1. 根据当前状态选择动作增加探索噪声 action agent.select_action(state) action np.clip(action noise.sample(), 0, 30.0) # 2. 执行动作获取下一状态和奖励 next_state, reward, done env.step(action) episode_reward reward # 3. 存储经验到回放池 replay_buffer.add(state, action, reward, next_state, done) # 4. 采样一批数据更新网络 if replay_buffer.size() batch_size: agent.update() state next_state if done: break # 每20个episode记录一次日志 if episode % 20 0: print(fEpisode {episode}, Reward: {episode_reward:.2f}) tensorboard_writer.add_scalar(reward, episode_reward, episode)这里要说明一个重要操作执行动作时在训练阶段返回的是连续动作值但env.step()内部会根据我们前面讲的映射逻辑判断是否切换相位。也就是说智能体在一个决策点输出了“延长8秒”但下一个决策点到来之前环境会以当前相位继续运行8秒然后在8秒结束后回到这个决策循环再让智能体决定下一步。这种设计保证了决策和仿真时间的一致推进。训练超参数我最终定格在这组配置超参数数值说明Actor学习率1e-4太低学习慢太高不稳定Critic学习率1e-3比Actor高一个量级快速收敛价值函数折扣因子gamma0.95考虑未来约20个决策点的影响软更新tau0.005目标网络更新步长经验池容量100000足够覆盖多种交通场景批量大小64性能和稳定性的平衡点决策间隔10秒太短频繁切换太长反应迟钝最大延长动作30秒防止单车相位无限期绿灯训练500个episode大约需要3~4个小时取决于机器配置用i7-10700K RTX 3060的配置平均每episode约25秒。整个训练过程中奖励曲线大致呈前期快速上升、中期波动、后期平稳收敛的趋势如果训练曲线一直剧烈震荡不收敛大概率是奖励设计或超参出了问题这个后面细说。5. 训练过程中踩过的坑5.1 奖励函数设计最大的坑奖励函数是我在这个项目里反复整改最多的部分没有之一。第一版我用的是“负的车辆平均等待时间”结果训练出来的模型极度自私智能体学会了把某个方向的绿灯永远亮着因为这个方向的车流一旦清空奖励就增加了但其他方向堵得水泄不通整体指标反而恶化。这就是典型的奖励黑客行为——智能体找到了投机取巧的方式获得高奖励而不是我们真正想要的全局最优。第二版我在奖励里加上了排队长度惩罚项情况好转了一些但新问题出现了奖励数值的范围不稳定。繁忙时段平均等待时间可能到80秒空闲时段只有10秒量级差距太大导致网络梯度震荡。最终版我做了一次关键改进对奖励的每一项都除以归一化系数把奖励值范围压到[-2, 0]区间。这样做以后训练稳定性大大提高收敛速度也快了近一倍。另外一个细节是奖励里不需要加常数偏置有人觉得加个负偏置能体现“惩罚”实际上没有任何帮助反而让数值分布更复杂。5.2 超参数调整实战训练过程中我积累了一张踩坑对照表这里直接给出来命中这些症状的可以直接对号入座症状可能原因解决建议损失爆炸出现NaN学习率过大降低学习率到1e-4以下奖励曲线完全不动探索噪声太小增大sigma或把噪声衰减调慢训练后期震荡剧烈噪声衰减过快延长噪声衰减周期别急着收敛模型在测试时表现差训练过拟合到固定流量模式训练时随机化车流生成参数动作频繁抖动相位来回切换决策间隔太短把决策间隔从5秒改成10秒一个相位长时间不放行奖励中排队惩罚权重太低增大排队长度项权重有一个比较隐蔽的问题容易忽视SUMO的仿真步长。默认情况下SUMO按1秒步长推进如果感觉训练速度太慢可以考虑把步长调到0.5秒或0.1秒但要注意步长过小0.1秒级别在车流密度大时会导致TraCI通信开销剧增反而更慢。实测下来1秒步长在精度和速度之间是最平衡的不推荐为了追求精确度把步长设到0.5秒以下。5.3 模型评估与效果对比训练完成后我用了一个独立生成的测试集来评估模型表现。测试集包含6种不同流量模式低流量、中等流量、高流量、早高峰波动流量、随机突发事件流量和极端拥堵流量。每组场景跑10次取平均值作为最终结果。评估代码的核心逻辑很简单# 关闭探索噪声使用确定性策略 def evaluate_ddpg(): total_times [] for test_scenario in test_scenarios: env.reset(scenariotest_scenario) state env.get_state() episode_waiting 0 while not env.is_done(): action agent.select_action(state) # 不加噪声 state, reward, done env.step(action) episode_waiting env.get_avg_waiting_time() avg_waiting episode_waiting / env.total_steps total_times.append(avg_waiting) return total_times # 对比组定时控制固定配时40s/40s # 对比组感应控制检测到排队超过阈值则切换最终对比结果如下控制策略平均等待时间秒平均排队长度辆定时控制38.611.2感应控制31.48.7DDPG智能体26.87.1DDPG相比定时控制降低了约30%的平均等待时间相比感应控制也降低了约15%。在极端拥堵场景下优势更明显因为智能体学会了在交通流不对称时动态偏袒拥堵方向而不是机械地按固定周期放行。这个结果挺能说明问题的强化学习不是玄学是真的能在这些控制问题上带来可量化的提升。6. 项目扩展方向与个人体会单路口DDPG信号灯控制跑通之后可以往两个方向扩展。一是多路口协调把多个路口的信号灯作为多智能体系统每个智能体用DDPG控制再通过共享奖励或通信机制实现协调。这个方向目前学术界很热难点在于如何设计智能体之间的信息交互和奖励分配。二是引入更丰富的交通状态输入比如车辆轨迹数据、实时车速分布甚至可以通过摄像头图像做端到端的信号控制状态空间从10维变成图像输入整个网络结构也要相应换成CNN全连接的组合。从个人实操的角度我最想分享给后来者的一条经验是强化学习项目调试的时间占比远超你的预期。我最初以为最难的会是算法理解或代码实现实际跑下来发现调奖励、调超参、定位训练不收敛原因的时间占了总开发时间的七成。所以在项目开始阶段就建议养成记录的习惯每次修改了什么参数、效果如何变化全部记下来。有一点我感受很深——不要一开始就用一个复杂的奖励函数先从一个简单方案跑通整个流程确认环境、网络、训练循环都没有问题了再逐步优化奖励。整个流程能跑通比一开始就追求完美重要得多。本文还有配套的精品资源点击获取