ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

深度强化学习DDPG实战:连续动作空间下的交通信号灯控制

深度强化学习DDPG实战:连续动作空间下的交通信号灯控制 简介交通信号灯控制项目 Traffic-Signal-Control-master 是一份基于 DDPG 深度强化学习算法的 Python 源码面向智能交通和强化学习领域的研究者与开发者旨在解决信号灯连续控制与配时优化问题。压缩包共 23 个文件核心为 9 个 py 模块覆盖训练脚本、网络结构定义、经验回放与可视化等环节另有 xml、iml 工程配置、pyc 缓存以及 2 张训练过程中的损失函数图像整体大小仅 103KB轻量紧凑。目前已有 1207 人学习/下载具备不错的参考热度。资源附带论文与完整可运行代码通过损失函数图像可直观观察模型收敛情况读者在阅读源码和动手调参过程中还能借助项目中的可视化模块对比不同超参数对 DDPG 表现的影响。这一项目以图像为输入、以信号灯配时为输出是理解深度强化学习在连续动作空间中完成端到端决策的典型范例。1. 从DQN到DDPG连续动作空间下的信号灯控制交通信号灯控制看起来是个典型离散决策问题——红黄绿相位按固定配时轮转就够了。但真实路口的拥堵点往往出现在绿灯时长分配上排队长度、上游来流都在连续变化固定配时表很难覆盖所有场景。把绿灯时长当作连续动作来优化比在离散相位集合里查表更贴合物理世界。DDPGDeep Deterministic Policy Gradient正是为连续动作空间设计的深度强化学习算法它用Actor网络直接产出控制量、Critic网络评估动作质量同时借用DQN的经验回放和目标网络来稳定训练。这个Python项目把DDPG完整落地到交通信号灯控制上代码里既有训练主循环pg_control.py、train_vis.py也保留了Q值和损失函数曲线适合想用深度强化学习做实际控制任务、又不想只停留在理论推导的开发者。2. 代码走读networks.py 中的 Actor-Critic 结构与策略梯度实现2.1 目录结构暴露的算法成分解压后能看到几个关键文件networks.py是网络骨架pg.py是策略梯度实现dqn_nature.py和dqn_nips.py分别是两个经典DQN版本replay_memory.py是经验回放pg_control.py是控制入口。DDPG本身是 DPG确定性策略梯度与 DQN 的混合体这套目录恰好把两个源头都独立出来了——pg.py对应策略梯度dqn_nature.py对应值函数学习两者组装起来就是完整的 Actor-Critic 结构。2.2 networks.pyActor 与 Critic 的搭建方式networks.py里定义了策略网络和价值网络。Actor 网络的输入是状态向量排队长度、平均车速、当前相位等输出是连续动作值即绿灯延长时间。与 DQN 输出离散 Q 值不同Actor 最后一层通常用tanh激活把动作限制在[-1, 1]区间方便后续映射到实际配时。class ActorNetwork: def __init__(self, state_dim, action_dim, lr, tau): self.state_dim state_dim self.action_dim action_dim self.lr lr self.tau tau def build(self): # 状态输入排队长度、车流密度、当前相位等特征 self.state_input tf.placeholder(tf.float32, [None, self.state_dim]) # 两层全连接隐层维度按场景复杂度调整 layer1 tf.layers.dense(self.state_input, 64, activationtf.nn.relu) layer2 tf.layers.dense(layer1, 64, activationtf.nn.relu) # tanh 将动作压到 [-1, 1]便于逆归一化为绿灯时长 self.action_output tf.layers.dense(layer2, self.action_dim, activationtf.nn.tanh) # Actor 的损失不是均方误差而是负的 Q 值 self.Q_input tf.placeholder(tf.float32, [None, 1]) self.actor_loss -tf.reduce_mean(self.Q_input) self.optimizer tf.train.AdamOptimizer(self.lr).minimize(self.actor_loss)这段代码的关键是actor_loss -tf.reduce_mean(self.Q_input)。Actor 的目标是让 Critic 打分更高所以梯度方向是最大化 Q 值等价于最小化负 Q。tau是软更新系数控制目标网络参数向在线网络靠近的步长后面调参时会反复提到它。Critic 网络的输入是两个状态和动作需要拼接后一起进入网络否则 Critic 对动作的梯度无法正确传播class CriticNetwork: def build(self): self.state_input tf.placeholder(tf.float32, [None, self.state_dim]) self.action_input tf.placeholder(tf.float32, [None, self.action_dim]) # 状态和动作拼接后共同预测 Q 值 concat tf.concat([self.state_input, self.action_input], axis1) layer1 tf.layers.dense(concat, 64, activationtf.nn.relu) layer2 tf.layers.dense(layer1, 64, activationtf.nn.relu) # Q 值是标量输出层不用加激活函数 self.q_value tf.layers.dense(layer2, 1, activationNone) # 目标 Q 值来自目标网络这里只做监督回归 self.target_Q tf.placeholder(tf.float32, [None, 1]) self.loss tf.reduce_mean(tf.square(self.target_Q - self.q_value))注意Critic 的 loss 是 TD 误差的平方不是策略梯度。Critic 负责评估Actor 负责决策两者更新频率可以不同常见做法是 Critic 更新两步、Actor 更新一步。2.3 pg.py 与 dqn_nature.pyDDPG 的两个亲属pg.py实现的是经典策略梯度REINFORCE它用整个回合的累计回报来更新策略方差大、样本效率低。DDPG 把其中的回报估计换成了 Critic 网络用 Critic 的单步 Q 值代替蒙特卡洛回报方差大幅下降这也是 DDPG 能在交通仿真这种长时序任务上跑起来的原因。dqn_nature.py对应 Nature 2015 版 DQN核心贡献是经验回放和目标网络。DDPG 的 Critic 部分直接继承了这套机制经验回放打破交通流数据的时序相关性目标网络让 TD 目标不至于随在线网络剧烈抖动。想快速定位训练问题优先看pg_control.py里三者的调用顺序——先采样再存 buffer最后更新网络顺序错了整个训练都会失真。3. 训练链路pg_control.py 到 train_vis.py 的完整闭环3.1 pg_control.py 中的训练主循环pg_control.py是项目的中枢它串起了环境交互、经验存储和网络更新。核心逻辑可以收敛成一个标准 DDPG 循环def run_training(env, agent, replay_buffer, episodes500, batch_size64): for episode in range(episodes): obs env.reset() episode_reward 0 step 0 while True: # Actor 根据当前状态输出连续动作训练初期叠加噪声探索 action agent.select_action(obs) next_obs, reward, done, info env.step(action) # 四元组存入经验池状态转移被打散 replay_buffer.push(obs, action, reward, next_obs, done) # 经验池攒够数量才开始学习避免初期样本分布过窄 if len(replay_buffer) batch_size: agent.update(replay_buffer.sample(batch_size)) obs next_obs episode_reward reward step 1 if done: break print(fepisode {episode}, step {step}, reward {episode_reward:.2f})这里有一个容易被忽略的设计select_action在训练阶段会叠加 OU 噪声或高斯噪声评估阶段则关掉噪声直接取 Actor 输出。交通信号灯控制的动作空间是绿灯延长时间噪声强度过大会导致路口频繁切换相位实际部署时建议在后期把噪声方差线性衰减到零。3.2 train_vis.py损失记录与可视化train_vis.py除了调用训练循环还承担了指标记录工作。每次网络更新后把actor_loss、critic_loss和当前Q值追加到日志列表周期性写入 CSV 或者直接绘制曲线。def log_training(critic_losses, actor_losses, q_values, episode, log_dirlogs): # 每个 episode 结束后聚合本回合均值防止单步噪声干扰判断 avg_critic_loss np.mean(critic_losses[-100:]) avg_actor_loss np.mean(actor_losses[-100:]) avg_q np.mean(q_values[-100:]) # 追加写入方便训练结束后用 matplotlib 或 tensorboard 回放 with open(f{log_dir}/train_log.csv, a) as f: f.write(f{episode},{avg_critic_loss:.4f},{avg_actor_loss:.4f},{avg_q:.4f}\n)取最近 100 步均值而不是单步值能滤掉单次更新造成的毛刺。Q 值曲线比 loss 更能反映训练状态——如果 Q 值持续上升后突然跳水多半是目标网络更新太快或经验池里混入了异常转移数据。3.3 从 Q_Mix Q.png 与 T_Mix Q.png 读训练状态项目 images 目录里的Q_Mix Q.png和T_Mix Q.png是两类 Q 值的混合曲线。前者是当前 Critic 网络对状态动作对的估计值后者是目标网络计算的 TD 目标值。两张图叠在一起看能判断训练是否健康。曲线形态Q_Mix Q.png 含义T_Mix Q.png 含义同步平稳上升Critic 评估趋于准确TD 目标逐步稳定策略在改善Q 上升但 T 停滞当前网络过乐观估计目标网络滞后可能出现过度估计两者剧烈震荡Actor 探索噪声过大软更新系数 tau 过大目标跟随太快常见做法是两个网络软更新系数取 0.001 到 0.01 之间T 曲线应当比 Q 曲线平滑。如果 T 曲线也跟着剧烈抖动先降 tau再考虑是否减小噪声强度不要一上来就调学习率。4. 环境与配置vis_env.py 状态设计及 dqn.inp 参数调优4.1 vis_env.py把仿真状态压缩成观测向量vis_env.py负责封装仿真环境对外提供标准的强化学习接口reset()、step(action)、render()。交通环境的难点在于原始状态维度太高——每条车道的排队长度、速度、占有率直接拼接会导致状态空间爆炸。项目里的做法是聚合成紧凑的特征向量一般包含当前相位剩余时间、进口道平均排队长度、出口道饱和度、最近 5 个时间步的平均车流量。状态设计直接影响训练难度。排队长度只反映瞬间拥堵建议叠加短时间窗内的流量均值当前相位信息必须包含否则 Actor 无法感知信号灯处于哪个阶段输出动作会缺乏时间上下文。vis_env.py里step()的奖励函数同样关键回合奖励可以直接用负的累计排队长度或负的车均延误但要注意量纲——延误以秒计数值通常在几十到几百不归一化的话 Critic 的梯度会很不稳定。4.2 dqn.inp 配置参数逐项拆解dqn.inp这类配置文件在交通仿真里用来声明交叉口结构、车流到达率、相位间隔等静态参数。DDPG 训练涉及的核心参数集中在另一组配置里常见做法是把它们集中到一个字典或配置类中参数作用建议取值范围调参倾向gamma折扣因子控制长期回报权重0.9 ~ 0.99交通流是长时序任务偏向 0.95 以上actor_lrActor 网络学习率1e-4 ~ 3e-4过大会导致动作输出震荡critic_lrCritic 网络学习率1e-3 ~ 3e-3可比 Actor 大但过大引发 Q 值发散tau软更新系数0.001 ~ 0.01越小目标网络越稳定收敛慢buffer_size经验回放容量50000 ~ 200000交通场景样本丰富尽量取大batch_size每次采样训练样本数32 ~ 128过小梯度噪声大过慢noise_sigma探索噪声标准差0.1 ~ 0.3后期衰减否则策略无法收敛gamma的取值需要注意。信号灯控制是持续数小时的长任务如果gamma取 0.9未来 10 步以外的回报几乎被忽略模型会变得短视只优化当前路口的瞬时排队而不顾下游拥堵。我一般从 0.95 起步观察延误曲线是否异常波动再微调。4.3 replay_memory.py经验回放的数据组织经验回放的实现并不复杂难点在于采样效率和数据结构选择。from collections import deque import random import numpy as np class ReplayMemory: def __init__(self, capacity): # 超过容量自动弹出最旧样本控制内存上限 self.buffer deque(maxlencapacity) def push(self, state, action, reward, next_state, done): state np.array(state, dtypenp.float32) next_state np.array(next_state, dtypenp.float32) self.buffer.append((state, action, reward, next_state, done)) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) # zip(*batch) 将样本按字段拆列然后分别堆叠成 batch states, actions, rewards, next_states, dones map(np.stack, zip(*batch)) return states, actions, rewards.reshape(-1, 1), next_states, dones.reshape(-1, 1)容量太小会导致样本相关性高训练不稳定容量太大则老样本占比过高策略更新缓慢。交通信号灯控制中done通常表示单个回合结束如仿真 1 小时careless 地把doneTrue的样本与普通样本混在一起训练会让 Critic 高估终止状态附近的 Q 值。# 错误示范直接覆盖 next_state 计算 TD 目标时不处理 done td_target reward gamma * target_Q # 正确做法done 时未来回报为 0 td_target reward gamma * target_Q * (1 - done)5. 三个易错点与收敛性检查5.1 连续动作值到离散相位的映射DDPG 输出的动作在[-1, 1]连续区间但交通信号灯最终要落到具体的绿灯延长时间或相位切换动作上。常见的映射方案是把动作值线性变换到最小和最大绿灯时长之间取整后再做边界裁剪。def action_to_green_time(action, min_time5.0, max_time60.0): # action 在 [-1, 1] 内归一化到 [0, 1] 再映射到绿灯时长区间 normalized (action 1.0) / 2.0 green_time min_time normalized * (max_time - min_time) return int(np.clip(green_time, min_time, max_time))边界裁剪必须放在取整之后否则clip拿到的可能是浮点值边界实际执行时出现非法配时。另一个细节动作输出接近正负 1 时梯度接近零Actor 容易陷入饱和训练阶段应该限制噪声范围避免频繁把动作推到边界上。5.2 软更新系数与目标网络滞后性软更新公式是target_weights tau * online_weights (1 - tau) * target_weights。tau越小目标网络更新越慢TD 目标越稳定但 Critic 对策略变化的反馈也越迟钝。交通信号灯场景里我发现 0.005 是个不错的起点——既能让目标网络跟上环境变化又不会因为跟随太快导致 Q 值发散。如果训练中期发现 Q 值和 loss 的曲线呈正弦状起伏优先怀疑tau偏大其次是 Critic 学习率过高。这两个方向比调 Actor 学习率更有效。5.3 损失不降时先检查什么很多人在训练初期看到critic_loss不下降就急着调学习率实际上先要做三件事确认 reward 尺度是否在合理范围延误值过大则先做归一化确认状态特征有没有做标准化排队长度和车流量的量纲差异会拖垮 Critic确认经验池里的done标记是否干净。这三项都正常但 loss 仍然震荡再回到tau和噪声方差上排查。本文还有配套的精品资源点击获取
返回列表