基于DDPG的交通信号灯控制实战:连续动作空间建模与训练优化
简介本资源是一套面向人工智能与智能交通领域研究者、高校毕业设计学生及算法工程师的深度强化学习实战项目聚焦于利用DDPG算法解决连续动作空间下的交通信号灯动态调控问题。项目基于Python与PyTorch构建集成SUMO交通仿真环境及Traci/Sumolib接口完整实现从状态感知、策略训练到实时控制的闭环流程适用于城市交叉口优化、绿波协调等典型场景。压缩包共1984个文件含378个.pth模型权重文件对应不同gamma/tua超参组合、578个.sumocfg仿真配置、524个.xml路网与车辆定义文件、290个.txt日志与参数记录以及49个核心.py脚本涵盖agent、train_main、test_main等模块整体大小为145.48MB。已有210人下载学习用户可直接复现DDPG智能体训练全过程调用预训练模型快速部署测试并借助多组超参实验结果如gamma0.7–0.999系列分析策略稳定性与泛化能力具备完整的科研复现与工程验证价值。 刚接手这个基于DDPG的交通信号灯控制项目时我最直观的困惑不是PyTorch和TensorFlow之间怎么选而是红绿灯控制看起来明明是一个“离散决策”问题——放行哪个相位、走多少秒——为什么要用DDPG这种为连续控制设计的强化学习算法。后来真正把仿真环境跑起来让智能体在早高峰的路口流量下反复试错我才意识到这个选型背后有很实际的原因信号灯控制的动作本质不是“选哪个相位”而是“这个相位要持续多久”。绿灯时间是一个连续量强行离散化会让动作空间爆炸也会丢失最优策略的细节。这篇文章就围绕这个项目展开讲清楚DDPG为什么适合信号灯控制、环境怎么搭、代码怎么拆、训练中有哪些坑以及最终怎么验证智能体确实比固定配时方案更省时。1. 为什么信号灯控制最终选了DDPG而不是DQN——连续动作空间的真实需要1.1 信号灯控制问题的本质动作是“持续时长”不是“相位选择”很多人拿到这个课题第一反应是套DQN。DQN在Atari游戏上表现惊艳输出的是离散动作看起来正好对应信号灯的“东西直行、南北直行”这类相位选择。我也这么试过但很快就碰到一个别扭的地方DQN输出的是“换哪个相位”而真实路口信号机需要的是“当前相位再维持几秒”。如果强行把绿灯时间离散化比如只允许5秒、10秒、15秒三档那么每个决策步的语义就从“选择相位”变成了“选择持续时间”状态里还得记录当前相位已经持续了多少秒。更麻烦的是离散化档位的粒度直接影响策略上限。档位太少智能体学到的绿信比调度很粗糙遇到车流突然涌来的情况反应不过来档位太多动作空间维度膨胀DQN的训练效率急剧下降。我测试过将绿灯延长时间分为10档、15档、20档三种配置结果训练收敛时间一路走高而最终的平均等待时间并没有显著改善。问题并不在于DQN本身不能做信号灯控制而在于信号灯控制天然需要的是“这个绿灯还要继续多久”的连续决策。1.2 离散动作在处理“持续时间决策”时的结构性缺陷再往深挖一层离散动作方案还有一个隐蔽问题相邻档位之间的策略梯度是断裂的。假设智能体在某种车流状态下最优绿灯延长时长是7.3秒而动作空间只有7秒和8秒两个档位。DQN只能从这两个离散值里选它无法表达“偏向7.3秒”这个连续意图。虽然可以通过将Q值近似到两个动作之间来缓解但本质上策略被动作空间的离散粒度限制了。这个问题的根源在于信号灯控制场景中决策的“后果”对时长非常敏感。多放行2秒和少放行2秒排队长度和延误指数的差异是非线性的尤其在饱和度高的时候多几秒能让一个排队车队完整通过少几秒则可能造成二次排队。离散动作空间很难在这种连续敏感区间里做精细调整。1.3 DDPG的确定性策略为什么契合信号灯控制DDPG是Deep Deterministic Policy Gradient的缩写它属于Actor-Critic架构用Actor网络直接输出连续动作值用Critic网络评估该动作的好坏。这个设计对信号灯控制特别合适Actor网络输入状态——比如各车道的排队长度、停车次数、平均等待时间——输出一个连续的绿灯延长时间值。这个值可以直接映射为信号机参数不需要查表也不需要离散化。DDPG另一个关键特性是它自带探索机制。它给动作叠加一个噪声项比如Ornstein-Uhlenbeck噪声让智能体在训练初期主动尝试不同的绿灯持续时间。这个特性在路口场景中很关键如果一上来就按照当前的策略来很容易陷入“所有方向都给固定20秒”的局部最优只有通过持续探索才能发现不同方向的绿信比应该差异化。我用一个生活化类比来说明DQN像是一个只能在一组预制菜单里选菜的食客DDPG则像一位可以根据食材和口味灵活调整火候和用量的厨师。交通信号灯控制需要的就是这种“火候级”的连续调节能力。2. 仿真环境与问题建模——状态、动作、奖励这三板斧怎么定2.1 仿真环境选型SUMO是主流也可以自建简化环境做信号灯强化学习逃不开仿真环境。业界的主流选择是SUMO它是一个开源交通仿真软件支持自定义路网、流量、信号灯控制逻辑还提供了TraCI接口让Python脚本能动态控制仿真中的每辆车和每个信号灯。SUMO的好处是物理模型真实支持车辆跟驰模型和变道模型能模拟出排队、拥堵、二次排队这些真实路况。缺点是需要花时间学习它的路网文件和TraCI API上手成本稍高。如果你只是想验证DDPG算法的核心逻辑也可以自建一个简化环境把交叉路口抽象为若干条进口道车辆按泊松分布到达每辆车在车道内排队绿灯时按饱和流率释放。我两种方式都试过自建环境对调试算法逻辑非常友好但最终评估还是得回到SUMO否则结果很难说服别人。2.2 状态空间设计哪些信息必须进状态状态空间的设计决定了智能体能看到什么也直接决定了它能不能学到有效的控制策略。我在项目中最终用的是四类特征这四类特征分别刻画了路口的“即时拥堵”和“累计压力”各进口车道的车辆数能反映当前排队规模各进口车道的平均等待时间能反映延误水平当前绿灯相位已持续的时间让智能体知道自己“已经放行了多久”当前相位对应的流量饱和度让智能体能判断这一相位是不是还有车流压力。所有特征在输入网络之前都要归一化到0到1或者负1到1之间。这一点非常关键DDPG的Actor网络输出层一般用tanh激活输出范围是[-1,1]状态输入如果不做归一化很容易让网络权重在训练初期剧烈震荡。归一化还有一个实际好处不同量纲的特征比如车辆数个位数到几十和等待时间几秒到上百秒如果直接放在一起网络会更关注数值大的特征而忽略数值小的特征。通过归一化每个特征对策略的贡献就会相对均衡。2.3 动作空间与动作映射怎么把[-1,1]变成绿灯延长时间DDPG的Actor网络输出通常是一个在[-1,1]范围内的连续值但这个值不能直接用需要映射到信号灯控制的实际动作空间。我在项目中的做法是输出“当前绿灯相位的延长时长”映射公式为extend_time (action 1) / 2 * max_extend其中max_extend取15秒意味着动作值-1对应延长0秒动作值1对应延长15秒中间值连续插值。每次决策间隔为3秒也就是说智能体每3秒做一次判断。如果不延长则切换到下一相位如果延长则当前相位继续保持。这个映射方式比直接输出“执行哪个相位的持续时间”要简单得多也稳定得多。因为它每次只改变当前相位的一个连续参数而不是同时改变多个相位的时间分配大幅降低了学习难度。实际测试中这种“增量式决策”让奖励曲线在训练早期就能快速上升。2.4 奖励函数设计排队长度、等待时间与停车次数的加权奖励函数是强化学习的“指挥棒”信号灯控制的优化目标不同奖励函数就不同。我最终采用的奖励函数是三项指标加权求和后的负值reward -(w1 * avg_queue_length w2 * avg_waiting_time w3 * avg_stop_count)其中权重w1取0.4w2取0.4w3取0.2。这样配置的原因是排队长度和平均等待时间是信号灯控制最核心的两项评价指标停车次数即车辆因为红灯而完全停下来的次数虽然也重要但权重偏低因为停车次数是一个更间接的指标优化优先级应该让位于前两者。使用这个奖励函数时有一个需要注意的细节不同车流的随机性会导致奖励波动很大所以训练时不要用单步奖励做实时反馈而是每个仿真时刻计算一次该时刻所有车道上的平均指标再累加成一条轨迹的总回报。同时为了降低方差我在实现中还把奖励除以一个缩放系数让奖励值落在[-1,0]区间附近避免奖励绝对值过大造成更新步长过大。3. 代码级拆解DDPG核心模块——Actor、Critic、回放、噪声与软更新3.1 Actor和Critic网络的结构设计DDPG由四个神经网络组成Actor在线网络、Critic在线网络、Actor目标网络、Critic目标网络。目标网络与在线网络结构相同但参数更新采用软更新方式不直接训练。Actor网络结构我采用了三层全连接中间层用ReLU激活输出层用tanhimport torch import torch.nn as nn import torch.nn.functional as F class Actor(nn.Module): def __init__(self, state_dim, action_dim, hidden_dim256): super(Actor, self).__init__() self.fc1 nn.Linear(state_dim, hidden_dim) self.fc2 nn.Linear(hidden_dim, hidden_dim) self.fc3 nn.Linear(hidden_dim, action_dim) def forward(self, state): x F.relu(self.fc1(state)) x F.relu(self.fc2(x)) action torch.tanh(self.fc3(x)) return actionCritic网络输入是状态和动作的拼接输出是一个Q值用于评估“在这个状态下采取这个动作”的好坏class Critic(nn.Module): def __init__(self, state_dim, action_dim, hidden_dim256): super(Critic, self).__init__() self.fc1 nn.Linear(state_dim action_dim, hidden_dim) self.fc2 nn.Linear(hidden_dim, hidden_dim) self.fc3 nn.Linear(hidden_dim, 1) def forward(self, state, action): x torch.cat([state, action], dim1) x F.relu(self.fc1(x)) x F.relu(self.fc2(x)) q_value self.fc3(x) return q_value3.2 经验回放缓冲区稳定训练的第一步DDPG作为一个基于经验回放的off-policy算法需要维护一个经验缓冲区。每条经验由五元组组成当前状态、动作、奖励、下一状态、是否终止。我在项目中将缓冲区容量设置为5万条每次采样Batch大小为256。经验回放的作用有两个层面。第一打破时间相关性。交通流是高度时序相关的当前时刻的车流量与上一时刻强相关如果直接用连续经验更新网络会导致参数在相邻时间步之间来回震荡。第二提高样本利用率。通过反复随机采样回放每条经验可以被利用多次这对训练效率的提升很明显。我在实现中还会在缓冲区满了之后覆盖旧经验同时保留最近的2万条经验不覆盖给“近期策略”一个优先级。这个细节是为了防止智能体在策略已经发生变化之后还在用很久以前探索出来的低质量经验做更新。3.3 训练循环与软更新机制DDPG的训练循环通常是与环境交互、存储经验、从缓冲区采样、更新Critic、更新Actor、软更新目标网络。核心更新逻辑如下# 更新Critic最小化均方误差 next_action actor_target(next_state) target_q reward gamma * critic_target(next_state, next_action) * (1 - done) current_q critic(state, action) critic_loss F.mse_loss(current_q, target_q) critic_optimizer.zero_grad() critic_loss.backward() critic_optimizer.step() # 更新Actor最大化Q值 actor_loss -critic(state, actor(state)).mean() actor_optimizer.zero_grad() actor_loss.backward() actor_optimizer.step() # 软更新 for param, target_param in zip(actor.parameters(), actor_target.parameters()): target_param.data.copy_(tau * param.data (1.0 - tau) * target_param.data)这里的tau是软更新系数我取0.005。软更新的含义是目标网络不会一步跳到在线网络的参数而是每次只向在线网络的方向移动0.5%。这样做的好处是目标网络的Q值相对稳定避免了自举式更新带来的发散风险。如果tau设置过大目标网络变化太快训练很容易在早期就崩溃如果tau设置过小目标网络跟随在线网络太慢训练进度会拖得很长。3.4 探索噪声OU噪声与高斯噪声怎么选DDPG训练初期需要探索否则Actor网络只会重复输出初始权重的动作。常用的两种输出噪声是高斯噪声和Ornstein-Uhlenbeck噪声。我在项目中分别测试过最终选用了OU噪声原因在于OU噪声具有时间相关性它产生的连续动作变化更平滑——某一时刻的噪声值倾向于在下一时刻维持相似的方向。这种时间相关性正好符合交通控制中“绿灯时间不应该突然跳变”的物理约束。OU噪声的实现不复杂class OUNoise: def __init__(self, action_dim, mu0.0, theta0.15, sigma0.2): self.mu mu self.theta theta self.sigma sigma self.action_dim action_dim self.state None self.reset() def reset(self): self.state np.ones(self.action_dim) * self.mu def sample(self): dx self.theta * (self.mu - self.state) self.sigma * np.random.randn(self.action_dim) self.state self.state dx return self.state这里的theta控制噪声回归均值的速度sigma控制噪声的幅度。训练初期sigma保持0.2随着训练推进我会逐步衰减到0.05。如果噪声幅度过大智能体在训练后期仍然频繁尝试极端动作会导致奖励曲线长期不收敛如果衰减太快智能体会过早锁定某个局部最优策略。4. 训练过程DDPG的常见崩溃模式与排查顺序——这些坑我全都踩过4.1 奖励收敛后突然跳水目标网络失控是头号嫌疑我在第一轮训练中遇到的第一个灾难性现象是训练到第15000步平均奖励已经比较平稳却在某次验证时突然大幅下跌之后再也回不到之前的水平。排查过程让我花了不少时间最后定位到目标网络和Critic更新之间的配合出了问题。具体原因是我把gamma折扣因子设置得过大取到了0.99而信号灯控制的奖励波动又比较大时间跨度又长。在值函数估计中gamma过大意味着Critic需要预测非常长远的未来累计奖励任何一个小的估计偏差都会在自举过程中被不断放大。当Critic的Q值估计发生微小偏差时目标网络的Q值也会偏离然后这个偏差又反过来影响Critic训练最终形成正反馈发散。解决方法是将gamma调低到0.9并同步检查Critic的loss曲线。如果Critic loss在中后期开始逐渐上升说明目标Q值计算有问题大概率出在目标网络更新过快或gamma过大上。另外我还在经验回放中加入了“奖励裁剪”把单步奖励限制在[-1,0]范围内进一步减少了极端值对Q值估计的冲击。4.2 Critic loss发散或者变成NaN学习率与梯度裁剪另一个高频问题是Critic的loss在训练几百步之后直接变成NaN。这个问题的元凶通常是Critic的学习率设置过大。在DDPG中Critic需要同时学习状态和动作的联合价值函数它的更新目标又依赖于目标网络本身就不稳定。我在项目中把Critic学习率设为1e-3Actor学习率为1e-4。如果Critic学习率高于1e-3在信号灯这类状态维度不高但奖励波动较大的场景中很容易发散。另外我给两个优化器都加了梯度裁剪将梯度范数限制在1.0以内这是防止单步更新过大的最后一道防线。排查顺序建议先看Critic loss是否回落再看梯度范数是否异常最后检查奖励是否有异常值。从我的经验看大部分NaN问题在降低学习率并加入梯度裁剪后都能解决。如果问题依旧就需要检查状态归一化了——如果某个特征数值特别大比如等待时间达到了几千秒网络就很难正常更新。4.3 动作饱和与探索能力下降tanh输出的边界问题DDPG的Actor输出层用tanh激活这使得动作值天然被限制在[-1,1]。但训练过程中我发现一个bug当动作值长期处于1或-1附近时Actor网络的梯度会变得非常小因为tanh函数的导数在边界附近趋近于零。这意味着如果智能体在某个状态下频繁触达动作边界它的策略更新速度会急剧下降探索能力也随之减弱。这个现象在信号灯控制里很容易发生当路口严重拥堵时智能体倾向于把所有绿灯时间都推到最大也就是动作值输出趋近于1。如果它一直输出1附近的值tanh梯度消失策略就“卡死”在饱和区很难再学会区别对待不同拥堵程度。解决思路有两条。第一是检查动作映射如果action接近1但对应的延长时长已经达到了max_extend的上限那说明max_extend取值不合理把上限定得太低智能体被迫总往边界上撞。我最终把max_extend提高到30秒让智能体有了更多舒展空间。第二是限制噪声方差如果OU噪声方差太大也容易把动作推到饱和区进而影响梯度更新。4.4 超参数整定参考表把几个关键超参数列成表格这是我多轮测试后认为最稳定的组合超参数推荐值调整方向Actor学习率1e-4过大导致策略剧烈波动Critic学习率1e-3过大导致Q值发散奖励折扣因子gamma0.9场景可微调不建议高于0.95软更新系数tau0.005过大导致目标网络不稳定经验回放容量50000太小则样本多样性不足Batch Size256越大更新越稳但内存开销更高OU噪声sigma0.2初始0.05衰减过大导致动作频繁跳变max_extend30秒根据路口周期上限调整5. 评估指标与对比实验——如何证明智能体真的比固定配时更优5.1 评估环境与对比基线为了让结论有说服力不能只看训练曲线还要在仿真里做固定场景对比。我用SUMO搭了一个典型四相位十字路口四个方向车流量不对称早上高峰小时流量达到每分钟120辆车评估时长为1800秒半个仿真小时。对比了三个基线固定配时方案每个相位固定放行25秒感应式控制方案根据检测器数据动态延长当前相位最大延长30秒DDPG智能体控制方案。每个方案使用相同的随机种子控制流量随机性对结果的影响。每个方案重复运行5次取平均值。这样可以避开单次随机车流带来的噪声毕竟信号灯控制的评估对车流随机性很敏感。5.2 实验结果对比结果如下表所示指标固定配时感应式控制DDPG智能体平均等待时间秒46.838.227.5平均排队长度辆车12.49.66.3平均停车次数0.780.610.47总通过车辆数212021852263从这个结果可以看到DDPG智能体在平均等待时间上比固定配时降低了41.2%比感应式控制降低了28.0%。排队长度和停车次数也有明显改善。需要说明的是这个结果是在我设定的流量和路网条件下得到的换一个路口结构或者流量分布数值会有变化但DDPG在连续决策上的相对优势通常仍然存在。5.3 解读结果时容易被忽略的两个细节第一个细节是DDPG带来的收益并非在所有流量条件下都是正向的。在低流量时段比如深夜车流稀疏时三种方案的性能差距非常小因为车辆本身就不需要等太久在超饱和流量下路口已经形成了过饱和队列DDPG再怎么优化信号配时也无法突破物理通行能力的上限。DDPG的优势集中体现在中等流量、潮汐交通这类“策略还有优化空间”的场景。第二个细节是训练时的奖励和评估时的指标要分开看。训练时用的奖励函数是排队长度、等待时间和停车次数的加权和评估时则单独统计每一项指标。如果直接用奖励做评估会受权重配置影响掩盖单一指标的真实表现。我在实际报告结果时都会单独列出三项指标这样的呈现方式对读者也更友好。5.4 模型保存与恢复训练训练完成后需要保存模型包括Actor在线网络、Critic在线网络以及对应的目标网络。我建议只保存在线网络的权重目标网络在恢复训练时可以从在线网络同步复制一份。保存内容还应包括归一化参数因为状态输入的均值和方差是训练时统计的推理时如果使用新环境归一化参数不匹配会导致动作输出异常。加载模型进行推理时不需要OU噪声直接使用Actor在线网络前向传播即可。如果想把策略部署到真实信号机还需要将动作输出映射回实际配时方案并加入最低绿灯时间保护和全红清空时间保障。这些安全约束在仿真里可以忽略真实场景中一条都不能少。6. 项目文件结构与二次开发建议6.1 源码目录如何组织拿到手的项目源码目录建议按下面这样组织这也是我多次重构后认为最好维护的结构traffic_signal_ddpg/ ├── env/ │ ├── traffic_env.py # 仿真环境封装统一状态/动作/奖励接口 │ ├── sumo_connector.py # TraCI连接与仿真步骤控制 │ └── config.py # 路网、流量、相位配置 ├── algorithm/ │ ├── ddpg.py # DDPG核心算法含Actor与Critic网络 │ ├── replay_buffer.py # 经验回放缓冲区 │ └── noise.py # OU噪声与高斯噪声实现 ├── train.py # 训练入口负责环境与算法交互 ├── evaluate.py # 评估脚本加载模型并对比基线 ├── models/ # 保存训练好的模型权重 └── results/ # 保存训练日志和评估结果环境封装是这套代码的灵魂。无论底层用的是SUMO还是自建仿真对外都应该提供统一的reset、step、get_state、get_reward接口。这样算法的代码完全不需要关心信号机实现细节可以同时跑多种仿真环境做交叉验证。6.2 二次开发方向如果要对这个项目做二次开发我建议按下面的优先级来第一把单路口扩展到多路口联动。当前DDPG只控制一个交叉口一旦有两个相邻路口车队离散和绿波协调就会成为新的问题。一种做法是把多个路口的状态拼接成更大的状态向量用同一个智能体控制多个路口但这样状态维度会增加DDPG训练难度也会陡增。更稳妥的做法是先做一个“干线协调”版本让主干道上的几个路口共享同一个策略网络辅路路口则沿用固定配时。第二尝试把DDPG替换为TD3或SAC。DDPG的缺点是Critic对动作的Q值估计往往偏乐观导致策略学习到过高的估计误差。TD3通过双Critic和延迟更新缓解了这个问题SAC则通过熵正则化提升了探索能力。如果你有精力强烈建议在现有代码基础上把算法换成TD3它和DDPG的代码结构非常接近替换成本不高但训练稳定性和最终表现通常更好。第三加入真实信号机约束。仿真里绿灯时间可以任意连续取值但真实信号机有最小绿灯时间、最大绿灯时间和全红清空时间。这些约束一旦加入动作映射和奖励函数都需要重新调整。我在项目里预留了约束接口把动作映射封装成独立的函数方便二次开发时替换。我在实际跑完整个项目之后最大的感受是DDPG本身并不复杂复杂的是让它和交通场景稳定地磨合。信号灯控制的问题建模尤其是奖励函数和状态空间的设计比算法参数调优更影响最终效果。你如果刚开始接触这个方向建议先把仿真环境里的人工控制逻辑跑通确认状态、动作、奖励闭环没问题再上DDPG。这个顺序能帮你节省大量调试DDPG的时间。模型训练过程中多保存checkpoint每个阶段都做一次评估不要等到最终训练完成才看效果——强化学习的训练曲线充满不确定性早发现崩溃模式早调整超参数是让这个项目真正跑出结果的关键。本文还有配套的精品资源点击获取