news 2026/9/15 20:30:53

深度强化学习驱动的交通信号灯控制:DDPG建模与仿真实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深度强化学习驱动的交通信号灯控制:DDPG建模与仿真实践

简介:面向智能交通与强化学习领域初学者及进阶开发者,提供一套基于深度确定性策略梯度(DDPG)的交通信号灯控制完整源码包。项目包括详细论文与Python实现,涵盖环境模拟、Agent交互、神经网络构建等核心环节,可用于毕业设计、课题研究或算法对比实验。压缩包共有23个文件,其中9个Python脚本承担主要逻辑,如DQN变体、策略梯度、经验回放与网络结构定义;另有4个XML配置/IDE文件、4个pyc缓存文件、2张训练损失函数PNG图和1份README说明,整体仅103KB,便于快速下载与部署。已有1207人加入学习/下载,说明其实用性被较多开发者认可。通过阅读论文并运行代码,可直观理解Actor与Critic网络的协作机制,掌握连续动作空间策略调优方法;利用loss曲线可判断模型收敛趋势,为改进信号配时方案提供实验依据。

1. 交通信号灯控制为什么需要深度强化学习来接手

路口信号灯控制没有表面上那么简单。固定配时方案在城市车流波动大时会迅速失效,早高峰和晚高峰的流量特征完全不同,更不要说偶发拥堵和事故。传统自适应控制依赖预定义的流量模型,而深度强化学习不假设流量分布,它通过不断和仿真环境交互,把“当前路口排了多少车、等待多久、下一秒该放哪个方向”这件事学出来。标题里的“交通信号灯识别”在这个场景下不是识别红绿灯颜色,而是让智能体从传感器或仿真数据里读出路口的真实状态。通常我们用 Python 做仿真和训练,模型用 DDPG 处理连续控制问题。无论你是在做毕设、研究方向还是真实路口的原型验证,这套流程都能直接参考。

2. 先建模,再训练:交通信号灯 MDP 里状态、动作与奖励怎么定

DDPG 属于基于 Actor-Critic 的深度强化学习算法,适合连续动作空间。交通信号灯控制恰恰是一个离散相位选择问题,很多资料会草率地说“用 DDPG 取代 DQN 即可”,但两者并不等价。想把 DDPG 用在信号灯上,必须先承认动作是离散的,再想办法把它连续化,否则 critic 输出的 Q 值没有稳定意义。这一节先解决建模问题。

2.1 交通信号灯识别在强化学习里的意思是让智能体看懂路口

如果你把摄像头画面直接丢给神经网络,那是在做“信号灯状态识别”,但强化学习要的是控制决策,不是单纯的图像分类。我一般会先把路口信息转成结构化状态,比如每个进口道的排队长度、平均车速、当前相位剩余时间。这样做的原因是:深度强化学习对状态表示的连续性非常敏感,原始图像经过 CNN 编码后虽然可行,但训练数据量和计算成本都会增加,而排队长度和等待时间已经能表达控制所需的大部分信息。

state_dict = { "lane_queue": [12, 3, 8, 15], # 四个进口道的排队车辆数 "lane_speed": [6.2, 11.5, 7.8, 4.1], # 平均车速 km/h "current_phase": 0, # 当前相位编号 "phase_remaining": 10 # 当前相位剩余时间 }

这里的lane_queue是直接和奖励挂钩的观测,current_phase让智能体知道自己的上一动作是否还在生效,phase_remaining能避免它频繁切换相位。若只输入原始像素,网络必须自己推断“哪里堵”,学习难度会大很多。注意状态向量要归一化,排队长度除以最大容量,速度除以道路限速,否则 critic 的梯度会被大数值特征主导。

2.2 四相位路口的状态怎么表示才不丢失关键信息

典型十字路口有四个相位:南北直行、南北左转、东西直行、东西左转。每个相位对应一组允许通行的车流方向。状态向量可以把四个相位的排队长度拼进去,或者把每个进口道单独拆成直行和左转两个方向。我更倾向后者,因为直行和左转的排队长度变化规律不同。

state_size = 4 * 2 + 2 # 4个进口道 * (直行/左转) + 当前相位 + 剩余时间 state = np.zeros(state_size) state[0] = queue_north_straight / 20.0 state[1] = queue_north_left / 15.0 state[2] = queue_east_straight / 20.0 # 其他方向同理 state[-2] = current_phase / 3.0 # 假设0-3 state[-1] = phase_remaining / max_phase_time

归一化后的状态量纲统一,训练时 critic 和 actor 的损失更平稳。不要漏掉phase_remaining,没有它,智能体可能每隔一秒就换相位,导致路口“抖动”,车辆永远走不了。

2.3 离散相位与 DDPG 连续动作的映射

DDPG 的 actor 输出是一个连续动作值,而信号灯动作只允许从四个相位中选一个。常见做法有两种:

  • 把 actor 输出看作一个连续量,按区间映射到相位。比如输出值在 [-1, 1] 时,四个区间对应四个相位。
  • 用 Gumbel-Softmax 让动作变成可微的离散采样,但训练复杂度更高。

第一种更稳。我在训练时会让 actor 输出一个标量,然后通过phase_id = int((action + 1) / 2 * num_phases)转成整数动作。但这样离散操作无法回传梯度。DDPG 本身只要求动作连续即可,因此不能直接套用。一个更好的做法是让 critic 对每个相位打分,再加噪声后取最大值。

phase_scores = critic(state, phase_onehot) # 对4个候选相位分别打分 phase_action = torch.argmax(phase_scores)

这样批评家网络学会了每个相位的 Q 值,actor 网络却可以不参与动作选择。严格说这已经不是标准 DDPG,而是 DDPG 与 DQN 的混合策略。如果你坚持标准的 actor-critic 流程,还有一个近似做法:让 actor 输出 4 维向量,经过 softmax 变成相位选择概率,再用概率加权求和得到“伪动作”输入 critic。但这个方法会让训练震荡,不推荐作为第一版。

2.4 奖励函数决定训练方向:用总等待时间还是平均排队长度

奖励是信号灯问题最容易出错的地方。只用排队长度做奖励,智能体会偏向放行排队多的车道,结果导致其他车道饿死。只用等待时间,当所有车都等很久时,奖励空间平滑,信号梯度不明显。我会组合多个指标:

reward = - (total_queue + 0.5 * total_waiting_time + 0.2 * phase_change_penalty)

total_queue是四个方向排队车辆数之和,total_waiting_time是所有车辆累计等待秒数,phase_change_penalty是相位切换惩罚项。如果不加切换惩罚,智能体学会找一个最“轻松”的相位永远不动,因为切换的动作会让某个方向排队数瞬时上升。

奖励要归一化,我一般按路口总容量做除法,让奖励保持在 -1 到 0 之间。当车辆平均等待时间超过某个阈值时,可以额外给一个大的负奖励,加速学习“避免死锁”的行为。

3. 用 Python 和 PyTorch 实现最小可运行的 DDPG 交通信号灯训练脚本

不装 SUMO 也能验证算法。为了把重点放在深度强化学习逻辑上,我会先用一个紧凑的单路口仿真环境模拟车流到达。这个环境虽然简单,但保留了强化学习的关键特征:随机到达、延迟反馈、动作切换代价。

3.1 没有 SUMO 也能跑:写一个紧凑的路口仿真环境

仿真环境只需要维护每个车道的排队计数器,按时间步释放车辆。为了实现“排队累积”的效果,每个时间步有一定概率生成新车,绿灯相位下排队车辆按固定速率消散。

import numpy as np class SimpleIntersectionEnv: def __init__(self, lane_dirs=4, max_queue=30): self.lane_dirs = lane_dirs self.queues = np.zeros(lane_dirs, dtype=np.float32) self.max_queue = max_queue self.current_phase = 0 self.phase_remaining = 5 def reset(self): self.queues = np.zeros(self.lane_dirs, dtype=np.float32) self.current_phase = 0 self.phase_remaining = 5 return self._get_state() def _get_state(self): state = np.concatenate([self.queues / self.max_queue, [self.current_phase / 3.0, self.phase_remaining / 10.0]]) return state def step(self, action): # 相位切换成本 phase_change_penalty = 0.5 if action != self.current_phase else 0.0 self.current_phase = action self.phase_remaining = 5 # 车辆到达,每个方向到达概率不同 arrivals = np.random.poisson(0.8, size=self.lane_dirs) self.queues = np.minimum(self.queues + arrivals, self.max_queue) # 放行当前相位对应进口道 release_count = 3 self.queues[action] = max(0, self.queues[action] - release_count) self.queues[action] = max(0, self.queues[action] - release_count) waiting = np.sum(self.queues) reward = - (waiting + phase_change_penalty) done = False return self._get_state(), reward, done, {}

release_count表示一个时间步内能通过路口的车辆数,取值越大代表通行能力越强,模型收敛速度会变快。这里故意让相位改变时时间步仍然固定,形成一个简单的“切换犹豫”机制。如果要更接近真实,可以把phase_remaining做成动态递减,但要记得phase_remaining与信号周期长度相关,这会影响网络输入分布。

3.2 Actor-Critic 与回放池代码骨架

DDPG 的核心是四个网络:actor、target_actor、critic、target_critic,加上一个经验回放池。网络不一定要很深,两层 256 即可。

import torch import torch.nn as nn import torch.optim as optim from collections import deque import random class Actor(nn.Module): def __init__(self, state_size, action_size, hidden=256): super().__init__() self.fc1 = nn.Linear(state_size, hidden) self.fc2 = nn.Linear(hidden, hidden) self.out = nn.Linear(hidden, action_size) def forward(self, x): x = torch.relu(self.fc1(x)) x = torch.relu(self.fc2(x)) return torch.tanh(self.out(x))

actor 输出范围被tanh限制在 -1 到 1。action_size 设为 1,后续通过int((action + 1) / 2 * 4)转为相位编号。critic 需要把状态和动作拼接起来,输出 Q 值。

class Critic(nn.Module): def __init__(self, state_size, action_size, hidden=256): super().__init__() self.fc1 = nn.Linear(state_size + action_size, hidden) self.fc2 = nn.Linear(hidden, hidden) self.out = nn.Linear(hidden, 1) def forward(self, state, action): x = torch.cat([state, action], dim=1) x = torch.relu(self.fc1(x)) x = torch.relu(self.fc2(x)) return self.out(x)

经验池用一个deque固定容量,避免相关性太强的样本连续更新。采样时随机抽取一个 batch,打破时间相关性。训练循环中,rewardsdones都被转换成张量,注意dones需要和 reward 一起计算target_q。这里有一个常见错误:很多人会忽略done标记,导致目标 Q 值包含终端状态后的假想奖励。

3.3 完整训练循环和参数设置

训练循环的关键在于软更新和探索噪声。每次更新 critic 时,从经验池取 batch,计算 TD target:

def update(model, optimizer, loss): optimizer.zero_grad() loss.backward() optimizer.step()

主循环中,actor 的目标策略使用贪心探索。探索策略用 Ornstein-Uhlenbeck 噪声或简单的高斯噪声。我会用高斯噪声,在动作上叠加标准差逐渐衰减的随机值。

def ddpg_train(env, actor, critic, target_actor, target_critic, replay_buffer, epochs=500, batch_size=64): optimizer_actor = optim.Adam(actor.parameters(), lr=1e-4) optimizer_critic = optim.Adam(critic.parameters(), lr=1e-3) tau = 0.005 gamma = 0.99 for epoch in range(epochs): state = env.reset() total_reward = 0 for t in range(100): state_tensor = torch.FloatTensor(state).unsqueeze(0) action = actor(state_tensor).detach().numpy().squeeze() noise = np.random.normal(0, 0.2, size=action.shape) action = np.clip(action + noise, -1, 1) action_id = int((action + 1) / 2 * 4) action_id = min(max(action_id, 0), 3) next_state, reward, done, _ = env.step(action_id) replay_buffer.append((state, action_id, reward, next_state, done)) state = next_state total_reward += reward if len(replay_buffer) > batch_size: batch = random.sample(replay_buffer, batch_size) # 把离散动作转换为连续值参与 critic 计算 states, actions, rewards, next_states, dones = zip(*batch) states = torch.FloatTensor(np.array(states)) actions = torch.FloatTensor(np.array(actions)).unsqueeze(1) rewards = torch.FloatTensor(np.array(rewards)).unsqueeze(1) next_states = torch.FloatTensor(np.array(next_states)) dones = torch.FloatTensor(np.array(dones)).unsqueeze(1) with torch.no_grad(): target_actions = target_actor(next_states) target_q = target_critic(next_states, target_actions) target_q = rewards + gamma * target_q * (1 - dones) current_q = critic(states, actions) critic_loss = nn.MSELoss()(current_q, target_q) update(critic, optimizer_critic, critic_loss) new_actions = actor(states) actor_loss = -critic(states, new_actions).mean() update(actor, optimizer_actor, actor_loss) with torch.no_grad(): for target_param, param in zip(target_critic.parameters(), critic.parameters()): target_param.data.mul_(1 - tau).add_(param.data, alpha=tau) for target_param, param in zip(target_actor.parameters(), actor.parameters()): target_param.data.mul_(1 - tau).add_(param.data, alpha=tau) return total_reward

要注意target_q里的target_actions使用 target_actor 网络,和暂时的噪声无关。critic 的输入动作必须是连续值,因此使用动作对应的相位编号时需要做一次 one-hot 或直接转成 float。更简单的方法是让环境返回action_id,再转回[-1, 1]区间,否则维度不匹配。

3.4 参数设置的默认值和调试起点

我训练这类问题很少用大学习率。actor 的学习率通常比 critic 小一个数量级,因为 actor 的更新梯度来自 critic 的近似值,过大的更新容易把策略推入死区。batch size 至少 64,经验池容量 20000 起步。太小会让训练不稳定,太大则旧策略数据拖慢新策略学习。

学习率 actor: 1e-4 学习率 critic: 1e-3 gamma: 0.99 tau: 0.005 到 0.01 noise std: 0.2 开始,逐步降到 0.05 batch size: 64 replay buffer: 20000

如果你是在跑免费 python 源码大全里别人写好的交通信号灯项目,通常最需要改的就是这几个参数。源码里其他部分可能写得很完整,但这里的噪声和软更新频率直接决定稳定性。

4. DDPG 训练不收敛?关键参数表和三个必备诊断方法

训练一个中等复杂度的 DDPG 交通灯模型,最常见的现象是奖励曲线来回震荡、永远学不到一个稳定的相位切换策略。这不是算法不适合,而是信号灯问题在动作切换上具有瞬时跳变特点,普通探索噪声会让智能体频繁切换,影响时序数据稳定性。

4.1 训练参数推荐表

下表是三个实验阶段的参考配置,可以根据你的路口大小调整。

参数快速原型标准训练精细调优
网络隐藏层128x128256x256512x256
actor 学习率1e-41e-45e-5
critic 学习率1e-31e-31e-3
噪声标准差0.30.20.1
奖励折扣 gamma0.90.990.99
每回合步数50100200
经验池大小50002000050000

快速原型阶段用较小经验池,方便迭代 reward 函数;标准训练阶段可以观察是否出现预期趋势;精细调优阶段就要引入课程学习。注意 gamma 越大,智能体越重视长远收益,但信号灯控制中过长的时间尺度反而会让单步奖励和总延误之间的关联变弱。对于单个路口,0.99 就足够。

4.2 哪几个超参数最影响稳定性

第一是 actor 的学习率。交通信号灯控制的问题中,状态变化比较平缓,但相位切换是硬边界,actor 更新幅度过大会导致策略从“一直放行某个方向”跳到“永远不放行任何方向”。降低 actor 学习率能减少这种剧烈摆动。第二是噪声衰减速度。如果噪声一直维持在 0.3,即使接近收敛也会造成大量无效切换。最好每 100 回合衰减一次,衰减系数 0.995。第三是 reward 的尺度。奖励数值过大,比如直接使用排队车辆数不加归一化,会导致 critic 的 target Q 值波动超过网络拟合能力。我一般将奖励除以最大排队总数,让单步奖励范围保持在 -1 到 0 之间。

还有人容易忽略tau软更新系数。tau太大,target 网络跟得太快,DDPG 失去稳定目标,训练容易发散;tau太小,target 网络更新过慢,训练初期 critic 收敛很慢。0.005 左右的默认值在多数仿真下够用。

4.3 从奖励曲线和固定配时基线判断是否学到东西

只盯着训练平均奖励是不够的。DDPG 的 critic 是自己估计目标 Q 值,会产生乐观偏移,所以训练奖励上升可能只说明 critic 和 actor 一起“自欺欺人”。我会固定一个简单的定时策略作为基线:每个相位固定放行 15 秒循环。然后每训练 50 轮做一次评估,评估时不加噪声。

python evaluate.py --model checkpoint.pth --baseline timed

评估脚本用同一个环境跑 10 个回合,输出平均等待时间。如果 DDPG 策略的平均等待时间低于定时策略且方差不大,说明确实学到了负奖励的最小化策略。另外一个更直接的诊断是看相位切换日志。如果你发现模型输出集中在某一个相位,说明被局部最优困住,优先修改奖励的相位切换惩罚或噪声初始值。

4.4 常见误用:把 DDPG 当黑盒、回了太多重复样本

很多失败案例来自对动作映射的理解偏差。在一个公开的交通信号灯源码里,原本动作是离散的,有人直接改成 DDPG 后,把 actor 的连续输出当作相位编号使用,不做区间映射也不处理维度,导致 critic 的输入混乱。正确的做法是:环境始终只走离散动作,actor 的输出只是用来做选择的一部分。另一种常见误用是经验回放中不区分状态是否相似。交通路口高峰和低峰的状态差异很大,如果随机均匀采样,大多数时间在回放“低峰低排队”的样本,模型对高峰场景的适应不够。建议参考优先经验回放,或者按当前时刻的排队数占比分段采样。

5. 把信号灯 DDPG 系统做扎实:从仿真验证到落地前要补的功夫

最后一步不是封装模型,而是确保实验可复现,并给真实场景留空间。我每次都会做一个固定随机种子复现实验,否则同一个脚本跑两次结果差别很大,很难判断改参数是否有效。

import random import numpy as np import torch seed = 42 random.seed(seed) np.random.seed(seed) torch.manual_seed(seed)

训练脚本开头加上这段,并且在环境初始化里也传入同样种子,这样复现实验时奖励曲线能完全一致。注意到 Python 的随机库、NumPy 和 PyTorch 各自独立,必须全部设置。

对于更复杂的场景,我建议使用课程学习:先在高到达率固定的环境下训练,让智能体学会基本绿灯时序;再让到达率随机变化,增加鲁棒性;最后加入突发流量事件。reward shaping 也是关键技巧,如果只给累计等待时间做负奖励,智能体在低流量时没有动力优化绿灯时长,因为等待时间本身很小。可以额外给一个“通行效率”的短时奖励,比如每个时间步成功通过路口的车辆数。

真实路口的 DDPG 交通信号灯系统还需要一个保险层,被称为安全壳。这个安全壳负责监控模型输出的相位切换建议,当发现某个方向排队长度超过上限,或者连续切换间隔小于最小绿灯时间时,拒绝执行该动作并回退到预设应急预案。这个逻辑不能靠强化学习训练,因为它属于硬约束。在每次实验前,先测试动态环境下极端流量对模型的影响。常见做法是设置一个简单的规则:排队数超过 80% 容量的车道所对应的相位,最少保持 5 个时间步,防止模型来回切换导致路口锁死。训练时把这个规则也写进环境,让智能体的探索空间被限制在安全范围内,这样强化学习和安全约束可以共存。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 20:30:50

美容整形网站建设:搞定域名服务器,用免费工具优化SEO

美容整形网站建设:搞定域名服务器,用免费工具优化SEO 域名服务器搞不懂,是很多医美机构老板建站时的第一道坎。选错服务器,网站打开慢如蜗牛,客户看两眼就走了;域名没备案,直接打不开,钱白花。别慌,今天这篇干货,专门拆解美容整形网站建设里的坑,教你用 免费工具 把SEO做扎实,让网站真正带来客流。…

作者头像 李华
网站建设 2026/9/15 20:29:43

基于ASP.NET MVC的物业报修系统源码设计与部署实战

简介:一套基于ASP与MVC框架的小区物业报修管理系统源码,面向从新手到有一定经验的开发人员,可解决物业报修工单管理中的提交、派单、处理与状态跟踪等实际问题。项目采用Visual Studio作为开发工具,SQL Server作为后台数据库&…

作者头像 李华
网站建设 2026/9/15 20:27:49

变分模态分解VMD实战:MATLAB脚本参数详解与信号处理应用

简介:围绕变分模态分解(VMD)的MATLAB实现资源包,面向需要分析非线性、非平稳信号的科研与工程人员。压缩包内为单个VMD.m脚本,体积仅2KB,轻量易用。该算法由Dietz和Steiner于2011年提出,能够将实…

作者头像 李华
网站建设 2026/9/15 20:25:35

2026年高性价比显卡牌子推荐与选购攻略

我注意到你的消息中还没有填入具体的项目信息(项目标题、项目正文、关键词、摘要描述)。你提供的内容似乎只有模板和要求,并没有实际的项目数据。如果你是想让我根据示例标题"2026年显卡推荐什么牌子好性价比高?(…

作者头像 李华
网站建设 2026/9/15 20:25:34

拒绝模板丑站:3个实战案例拆解美容整形网站建设

拒绝模板丑站:3个实战案例拆解美容整形网站建设 还在用那种满屏闪烁GIF、配色像迪厅一样的模板网站吗?在医美行业,这种“非主流”风格不仅劝退高净值客户,更会被搜索引擎判定为低质垃圾页面。很多机构老板找上门,第一句话就是:“这站太丑,不够用,客户一看就划走。”…

作者头像 李华
网站建设 2026/9/15 20:25:25

JavaScript内存优化实战:从垃圾回收到Detached DOM泄漏治理

1. 这不是“理论课”,是前端工程师每天都在面对的内存战场JavaScript 内存问题,从来不是浏览器控制台里一闪而过的heap size数字,而是你改完一行代码后,用户反馈“点按钮卡顿三秒”、产品经理追问“为什么新功能上线后页面白屏率涨…

作者头像 李华