简介:交通信号灯控制项目 Traffic-Signal-Control-master 是一份基于 DDPG 深度强化学习算法的 Python 源码,面向智能交通和强化学习领域的研究者与开发者,旨在解决信号灯连续控制与配时优化问题。压缩包共 23 个文件,核心为 9 个 py 模块,覆盖训练脚本、网络结构定义、经验回放与可视化等环节;另有 xml、iml 工程配置、pyc 缓存以及 2 张训练过程中的损失函数图像,整体大小仅 103KB,轻量紧凑。目前已有 1207 人学习/下载,具备不错的参考热度。资源附带论文与完整可运行代码,通过损失函数图像可直观观察模型收敛情况;读者在阅读源码和动手调参过程中,还能借助项目中的可视化模块,对比不同超参数对 DDPG 表现的影响。这一项目以图像为输入、以信号灯配时为输出,是理解深度强化学习在连续动作空间中完成端到端决策的典型范例。
1. 从DQN到DDPG:连续动作空间下的信号灯控制
交通信号灯控制看起来是个典型离散决策问题——红黄绿相位按固定配时轮转就够了。但真实路口的拥堵点往往出现在绿灯时长分配上,排队长度、上游来流都在连续变化,固定配时表很难覆盖所有场景。把绿灯时长当作连续动作来优化,比在离散相位集合里查表更贴合物理世界。DDPG(Deep 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.py:Actor 与 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, activation=tf.nn.relu) layer2 = tf.layers.dense(layer1, 64, activation=tf.nn.relu) # tanh 将动作压到 [-1, 1],便于逆归一化为绿灯时长 self.action_output = tf.layers.dense(layer2, self.action_dim, activation=tf.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], axis=1) layer1 = tf.layers.dense(concat, 64, activation=tf.nn.relu) layer2 = tf.layers.dense(layer1, 64, activation=tf.nn.relu) # Q 值是标量,输出层不用加激活函数 self.q_value = tf.layers.dense(layer2, 1, activation=None) # 目标 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.py:DDPG 的两个"亲属"
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, episodes=500, batch_size=64): 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(f"episode {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_dir="logs"): # 每个 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_lr | Actor 网络学习率 | 1e-4 ~ 3e-4 | 过大会导致动作输出震荡 |
critic_lr | Critic 网络学习率 | 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(maxlen=capacity) def push(self, state, action, reward, next_state, done): state = np.array(state, dtype=np.float32) next_state = np.array(next_state, dtype=np.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 地把done=True的样本与普通样本混在一起训练,会让 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_time=5.0, max_time=60.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和噪声方差上排查。
本文还有配套的精品资源,点击获取