☰
深度强化学习DDPG实现交通信号灯自适应控制实战指南
2026/10/5 5:30:17 网站建设 项目流程

简介:面向智能交通与强化学习研究者的深度强化学习源码工程,聚焦交通信号灯控制场景,提供基于Python的完整实现。压缩包共23个文件,主体为9个Python脚本,涵盖训练入口、环境交互、经验回放与网络构建;辅以XML配置、训练超参数及两张损失函数变化图,整体仅103KB,结构紧凑,便于本地运行调试。已有1207人学习下载。项目除核心DDPG思路外,还包含DQN多种变体与策略梯度算法,配合README与论文资料,可对比不同强化学习算法在信号控制中的表现。通过损失曲线观察收敛趋势,能帮助入门者快速掌握从环境搭建到模型调参的完整流程,也为后续扩展多路口协调控制提供了参考基线。

1. 交通信号灯控制为什么值得用深度强化学习重做一遍

固定配时信号灯在高峰时排队溢出、平峰时绿灯空放,感应控制又只盯着单个方向的来车,多相位协调时经常顾此失彼。Traffic-Signal-Control-master 这类工程做的事情,就是把深度强化学习里的 DDPG 算法接到交通信号灯识别与控制场景上:让智能体根据当前排队长度、车道等待时间和车流密度,动态决定当前相位绿灯再延长几秒。DDPG 是一种适合连续动作空间的深度强化学习算法,而“绿灯延长多少秒”恰好是连续量,所以这个组合在信号控制里很自然。适合谁来看:被固定配时方案磨过的算法工程师,或者想在仿真环境里验证 DRL 控制策略的研究生。这篇笔记从 MDP 建模、代码骨架、调参到踩坑,给出一条能照着复现的落地路径。

2. 交通信号灯 MDP 建模:状态、动作、奖励怎么设计才不翻车

很多复现失败,第一反应都是怪 DDPG 不收敛,但大部分问题出在更前面:你根本没把信号灯控制定义成一个清晰的 MDP。DDPG 不是黑匣子魔法,它只是在一个已经定义好的状态、动作、奖励结构里做函数逼近。交通信号灯控制的 MDP 建模一旦变形,再好的算法也救不回来。

2.1 状态空间:排队长度、等待时间和车流密度,交给智能体的是什么

标准十字路口通常按四相位管理:东西直行、东西左转、南北直行、南北左转。每个相位下,智能体在每个决策时刻看到的应该是四个进口道、多条车道的排队长度、累计等待时间和本周期通过车辆数。除此之外,当前相位编号和当前相位已经执行的时间也必须进状态,否则 DDPG 的 Actor 是“无记忆”的,它根本不知道绿灯已经亮了多久,也就没法判断延长多少秒是合理的。

我一般会这样组织状态向量:

# 状态向量拼接:训练和评估必须保持完全相同的顺序 def build_state(lane_queue, lane_wait, lane_flow, phase_id, phase_left): # lane_queue: 每个车道当前排队车辆数 # lane_wait: 每个车道累计等待时间(秒) # lane_flow: 最近一个决策周期内通过的车辆数 # phase_id: 当前相位编号 # phase_left: 当前相位已经执行的秒数 queue_norm = [q / MAX_QUEUE for q in lane_queue] # 排队数除以车道容量上限 wait_norm = [w / MAX_WAIT for w in lane_wait] # 等待时间除以一个合理上界 flow_norm = [f / MAX_FLOW for f in lane_flow] # 通过车辆数除以饱和流率 phase = [0.0] * NUM_PHASE phase[phase_id] = 1.0 return queue_norm + wait_norm + flow_norm + phase + [phase_left / MAX_GREEN]

这段代码的关键在于把所有量纲不同的原始值统一压到 0 到 1 附近。排队长度可能是 5 辆也可能 30 辆,等待时间可能几秒也可能上百秒,如果直接用原始数值拼成一个向量,数值大的维度会主导 Actor 的输出,Critic 的 Q 值估计也会被带偏。MAX_QUEUE 我一般按每条车道 20 到 30 辆取,MAX_WAIT 取 120 秒,MAX_FLOW 按进口道饱和流率估算,MAX_GREEN 取你政策允许的最大绿灯时长,比如 60 秒。

一个典型四进口、每进口两条车道的状态维度是:排队 8 维、等待 8 维、流量 8 维、相位 one-hot 4 维,加当前绿灯已执行时间 1 维,总共 29 维。这个规模对 DDPG 来说非常轻松,不需要额外做 CNN 或注意力。真实路口如果有更多车道,按同样规律扩展即可。

2.2 动作空间:为什么用 DDPG 输出连续绿灯延长量,而不是 DQN

信号灯控制最常见的做法有两种:一种是离散动作,比如“切换到下一个相位”“保持当前相位 10 秒”“保持当前相位 20 秒”,用 DQN 就能做;另一种是连续动作,直接输出一个绿灯延长的秒数。离散做的缺点是动作粒度太粗,现实中绿灯延长 7 秒和 8 秒的差别可能直接决定一个周期能不能清空排队。DDPG 的价值就在这里:确定性策略网络直接输出连续值,再映射到绿灯延长秒数。

动作映射代码通常长这样:

def map_action(raw_action, max_extend=20.0): # 网络输出是 tanh 激活,值域 [-1, 1] # 这里映射到 [0, max_extend] 秒,作为当前相位的绿灯延长量 extend = (raw_action + 1.0) / 2.0 * max_extend return extend

为什么用 tanh 再接线性映射,而不是让网络直接输出任意实数?因为 DDPG 的 Actor 更新依赖 Critic 对动作的梯度,动作输出范围不受约束时,训练初期容易出现极端值,导致 Critic 在从未见过的动作区域做出荒谬估计。tanh 把动作限制在有限区间,映射到绿灯延长量后,还要在控制器侧加一层保护:如果延长量小于某个阈值,比如 2 秒,就认为智能体想切换相位,直接结束当前相位。

这里有个常见误区:动作空间不等于相位切换指令。DDPG 只负责回答“当前相位还值不值得再放行 X 秒”,相位切换的合法性检查、黄灯时长、全红清空时间,都应该由底层的信号控制器负责。把交通规则写死在网络输出里,是很多复现项目后期被各种异常状态打爆的原因。

2.3 奖励函数:平均等待时间、吞吐量与公平性怎么同时顾

奖励函数是交通信号灯 DDPG 里最容易“翻车”的地方。最直接的版本是用平均等待时间的变化量:如果这个决策让所有车道的排队等待总和减少了,就给正奖励。但只有这一项,智能体很快就会学会“刷奖励”——它发现只要一直保持当前相位绿灯,通过车辆数就会增加,等待时间变化量也会暂时变好,结果是其他方向被饿死。

我常用的奖励结构是三项叠加:

def compute_reward(prev_wait, cur_wait, queue_len, throughput): # prev_wait / cur_wait: 前一决策点和当前决策点的总等待时间 # queue_len: 每个车道当前排队数 # throughput: 本决策周期内通过路口的车辆总数 # 1. 等待时间减小量:负值说明排队在加剧,正值说明缓解了 delay_reduce = prev_wait - cur_wait # 2. 溢出惩罚:任意车道排队超过容量 80%,给予额外惩罚,防止单方向饿死 overflow_penalty = sum(max(0, q - MAX_QUEUE * 0.8) for q in queue_len) # 3. 通行奖励:鼓励真正放行车辆,而不是空放绿灯 flow_reward = throughput * 0.1 reward = delay_reduce - overflow_penalty + flow_reward return reward

delay_reduce 这一项通常是负的,因为只要有车不断到达,总等待时间每一步都在增长。这正是我们想要的信号:智能体要努力让等待时间增长得慢一点,而不是维持在一个静态值。overflow_penalty 的系数很关键,我一般把它放大到 2 到 5 倍,因为一旦某条车道排队溢出,影响是跨周期的,会拖累后续十几个决策步的回报。flow_reward 的权重不要给太大,0.1 倍即可,否则智能体会走极端去放行那些本来排队就不长的方向,以刷高吞吐量。

如果你想做更严格的建模,可以把“平均最大等待时间”也放进状态,然后在奖励里对超过阈值的情况做线性惩罚。原因是信号控制本身是天然多目标问题,单纯优化平均等待时间,会牺牲少数方向上的长等待车辆,这在真实路口是不可接受的。

3. 用 Python 把 DDPG 接到交通信号灯环境:最小可复现代码骨架

建模完成之后,立刻就会面对一个工程问题:DDPG 的 Python 代码本身不难写,难点是让智能体和交通仿真器顺畅地对话。很多 Traffic-Signal-Control-master 这类工程拿到手,第一件事不是读网络结构,而是先确认环境接口是 gym 风格还是自定义风格,再决定训练循环怎么写。

3.1 仿真器选择与接口约定:先把环境 step 写成统一格式

最常见的仿真环境是 SUMO,通过 TraCI 接口从 Python 侧读取车道排队和等待时间,并下发灯色控制指令。CityFlow 也很多人用,特点是跑得比 SUMO 快,适合大规模路网实验。但不管底层是哪个仿真器,我强烈建议把环境封装成统一的 reset 和 step 接口。后续换仿真器、换路网拓扑,只改环境内部,训练代码一行都不用动。

class TrafficSignalEnv: def __init__(self, sumo_cmd, phase_config): self.sumo_cmd = sumo_cmd self.phase_config = phase_config def reset(self): # 关闭上一次残留的仿真连接,重新拉起 SUMO 实例 if hasattr(self, "traci_conn"): self.traci_conn.close() self.traci_conn = TraCI(self.sumo_cmd, ...) self.phase_id = 0 self.phase_left = 0 state, _ = self._read_state() return state def step(self, action): # action 是一个 float,表示当前相位绿灯延长秒数 extend = map_action(action) if extend < 2.0: self._switch_to_next_phase() else: self._extend_green(extend) # 推进仿真:每个决策点固定推进一个 decision_interval for _ in range(DECISION_INTERVAL // SIM_STEP): self.traci_conn.simulation_step() self.phase_left += SIM_STEP next_state, wait_info = self._read_state() reward = compute_reward(self.prev_wait, wait_info['total_wait'], wait_info['queue_len'], wait_info['throughput']) self.prev_wait = wait_info['total_wait'] done = self.traci_conn.is_end_of_simulation() return next_state, reward, done, {}

这里两个参数是信号灯 DDPG 的“隐形开关”:DECISION_INTERVAL 是智能体的决策周期,我一般设 5 秒,太短会让一个周期内样本高度相关,太长又会让控制反应迟钝;SIM_STEP 是仿真步长,SUMO 里常用 0.1 秒,负责平滑推进车辆运动。注意黄灯和全红相位必须在 _switch_to_next_phase 里显式模拟,否则智能体在密集车流下会频繁切换相位,仿真里会出现车辆冲突,表现成各种奇怪的奖励尖刺。

3.2 Actor-Critic 与经验回放的 PyTorch 实现:核心代码

环境就绪后,DDPG 本体就很标准了。Python 侧依赖只用到 torch 和 numpy,环境版本 Python 3.8 以上即可。

import torch import torch.nn as nn import torch.nn.functional as F import numpy as np class Actor(nn.Module): def __init__(self, state_dim, action_dim, max_action=20.0): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, action_dim), nn.Tanh() # 限制输出到 [-1, 1] ) self.max_action = max_action def forward(self, s): # 映射到最大绿灯延长秒数 return self.net(s) * self.max_action

Actor 网络就是状态到动作的确定性策略。两个隐藏层各 256 个神经元足以处理 29 维状态和 1 维动作;加深到 512 层在这个问题上收益很小,只会拖慢训练。如果是车流量很大的双口路网,可以改成 300 维,但不建议一上来就堆大网络,先跑通再调结构。

class Critic(nn.Module): def __init__(self, state_dim, action_dim): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim + action_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, 1) ) def forward(self, s, a): # 状态和动作拼接后一起输入 return self.net(torch.cat([s, a], dim=-1))

Critic 的输入是状态加动作的拼接。很多新手把动作省略掉,只用状态预测 Q 值,那就变成了 V 函数,DDPG 的动作梯度就没法算了。经验回放缓冲区可以直接用 deque 实现:

from collections import deque import random class ReplayBuffer: def __init__(self, capacity=100000): self.buffer = deque(maxlen=capacity) def add(self, s, a, r, ns, done): # 每个决策步产生一条样本:状态、动作、奖励、下一状态、终止标志 self.buffer.append((s, a, r, ns, done)) def sample(self, batch_size): batch = random.sample(self.buffer, batch_size) s = np.array([x[0] for x in batch], dtype=np.float32) a = np.array([x[1] for x in batch], dtype=np.float32).reshape(-1, 1) r = np.array([x[2] for x in batch], dtype=np.float32).reshape(-1, 1) ns = np.array([x[3] for x in batch], dtype=np.float32) done = np.array([x[4] for x in batch], dtype=np.float32).reshape(-1, 1) return s, a, r, ns, done

采样时把动作 reshape 成 (batch, 1),是因为 Critic 拼接状态和动作时,要求动作是二维张量。这个细节经常让人调试半天,其实就是维度没对齐。

3.3 训练主循环:交通流推进与梯度更新的节奏

训练主循环要把环境推进和深度强化学习的策略更新耦合成一个正确的节奏。我一般会先让仿真跑一段预热,收集足够样本,再开始梯度更新,避免策略刚开始还在乱探索时就被用来更新目标。

def train(env, actor, critic, target_actor, target_critic, buffer): actor_opt = torch.optim.Adam(actor.parameters(), lr=1e-4) critic_opt = torch.optim.Adam(critic.parameters(), lr=1e-3) tau, gamma, batch_size = 0.005, 0.99, 64 noise_std = 0.2 for episode in range(500): state = env.reset() state = torch.FloatTensor(state).unsqueeze(0) done = False while not done: # 训练时在动作上叠加高斯噪声做探索 action = actor(state).detach().item() action = np.clip(action + np.random.normal(0, noise_std), 0, 20.0) next_state, reward, done, _ = env.step(action) next_state = torch.FloatTensor(next_state).unsqueeze(0) buffer.add(state.numpy()[0], action, reward, next_state.numpy()[0], float(done)) state = next_state if len(buffer.buffer) > batch_size * 10: s, a, r, ns, d = buffer.sample(batch_size) s_t = torch.FloatTensor(s) a_t = torch.FloatTensor(a) r_t = torch.FloatTensor(r) ns_t = torch.FloatTensor(ns) d_t = torch.FloatTensor(d) # 1. 更新 Critic:预测 Q 值逼近目标 Q 值 with torch.no_grad(): target_q = r_t + gamma * target_critic(ns_t, target_actor(ns_t)) * (1 - d_t) q = critic(s_t, a_t) critic_loss = F.mse_loss(q, target_q) critic_opt.zero_grad() critic_loss.backward() torch.nn.utils.clip_grad_norm_(critic.parameters(), 10) critic_opt.step() # 2. 更新 Actor:让动作在当前状态下的 Q 值尽量大 actor_loss = -critic(s_t, actor(s_t)).mean() actor_opt.zero_grad() actor_loss.backward() torch.nn.utils.clip_grad_norm_(actor.parameters(), 5) actor_opt.step() # 3. 软更新目标网络 for tp, sp in zip(target_actor.parameters(), actor.parameters()): tp.data.copy_(tau * sp.data + (1 - tau) * tp.data) for tp, sp in zip(target_critic.parameters(), critic.parameters()): tp.data.copy_(tau * sp.data + (1 - tau) * tp.data) noise_std = max(0.05, noise_std * 0.995) # 每个 episode 后衰减

每一步的节奏是:环境先走一个决策周期,产生一条样本,然后从缓冲区里采样更新。注意 target_q 计算时必须用 target_actor 和 target_critic,并且要加 (1 - d) 的掩码,否则终止状态会把 Q 值错误地传到下一轮。梯度裁剪放在 backward 之后、step 之前,是防止奖励尺度异常时 Critic 梯度爆炸的兜底措施,强烈建议保留。

4. DDPG 在交通信号灯场景的调参清单:4 个决定成败的参数

网络结构和训练循环跑通之后,真正的血泪经验全在参数上。交通信号灯场景与经典的 MuJoCo 控制环境有很大差异:状态是周期性强、动作边界受交通安全约束、奖励天然带噪声。下面 4 个参数是我每次复现都最先检查的。

4.1 学习率与软更新 tau:让 Critic 先稳下来

DDPG 的 Actor 和 Critic 学习率绝对不能相等。Critic 要先学会相对准确的 Q 值,Actor 才知道往哪个方向改动作。如果 Actor 学得快,它会跑向 Critic 还没学会评估的区域,训练曲线就会出现“奖励突然暴涨又立刻崩掉”的典型曲线。

参数常见初始值什么情况需要调
actor 学习率1e-4策略震荡大时降到 5e-5
critic 学习率1e-3critic loss 发散时降到 5e-4
tau(软更新系数)0.005Q 值抖动大时降到 0.001
gamma(折扣因子)0.99决策周期较长时降到 0.95

tau 控制目标网络跟随当前网络的速度。tau 太大,目标网络一直在追当前网络,训练不稳定;tau 太小,目标网络更新太慢,学习速度明显下降。交通信号灯场景一个决策步是现实中的 5 秒,gamma=0.99 相当于考虑未来 500 秒内的累计回报,基本覆盖一个完整信号周期。如果你的仿真周期特别长,比如一个 episode 模拟 3600 秒,可以考虑适当降低 gamma,否则远期回报的贡献会淹没近期决策的影响。

4.2 奖励尺度与状态归一化:等待时间除以多少才合适

交通场景的奖励天然是大数值,一个决策周期内总等待时间变化可能是几百上千秒,如果不除一个常数,Critic 的 Q 值动辄成千上万,梯度更新量完全失控。我的习惯是把所有奖励单位的量级压到个位数或十位数:总等待时间除以 300,吞吐量乘 0.1,排队惩罚按条数乘 2。这个尺度不是玄学,目的是让 critic loss 在训练前几百步内落在 10 到 100 的范围,方便观察收敛趋势。

状态归一化比奖励归一化更容易被忽视。排队长度除以车道容量上限后,所有特征都在 0 到 1 区间;但相位 one-hot 和绿灯已执行时间天然是 0 到 1,混在一起没有问题。真正的问题是很多工程只归一化了排队,等待时间直接塞原始秒数进去,结果等待时间维度权重被放大,智能体变得过度敏感于长等待车辆,频繁切换相位。简单说:状态里所有特征,要么在 0 到 1,要么在 -1 到 1,不存在几十上百的原始数值。

4.3 经验回放容量与 batch size:样本时效性和相关性怎么平衡

交通流数据高度时序相关:同一时段进入路口的车辆会产生一串连续决策样本,这些样本之间不是独立的。经验回放的作用就是打散这种相关性。buffer 容量太小,采样时大概率抽到同一波车流里的相邻样本,训练就会震荡。我一般设 10 万到 50 万之间的容量。按一个 episode 模拟 3000 秒、决策间隔 5 秒算,一个 episode 产生 600 条样本,100 个 episode 才 6 万条,10 万容量足够存大约 150 个回合的数据。

batch size 在 64 到 256 之间都合理。batch 太大,梯度更稳定但更新慢;batch 太小,单次更新受噪声影响大。有一个信号灯场景特有的问题:不同时段的样本质量差异很大。平峰期样本里动作几乎不影响等待时间,高峰期样本又混合着溢出惩罚,如果 buffer 里平峰样本过多,策略会偏向保守,不敢延长绿灯。解决办法是在采样时多做一步,按奖励绝对值加权采样,但这个做法会引入额外复杂度,第一次复现不建议加。

4.4 动作噪声:训练后期必须衰减,否则策略永远带着抖动

DDPG 是确定性策略,训练时必须靠外设噪声做探索。常见做法是给 Actor 输出的动作加高斯噪声,标准差从 0.2 到 0.3 起步,随训练衰减到 0.05 左右。噪声的作用是让智能体尝试一些次优动作,避免策略固化在局部解。但如果噪声一直在,训练后期 Critic 的估计会被反复引入的动作波动干扰,导致“训练曲线看着在涨,关了噪声一测就垮”。

在信号灯场景,噪声衰减还有一个特殊含义:动作映射到绿灯延长量之后,0.3 的噪声可能让延长量变化 6 秒,这在交通上已经是很大的控制变化了。所以我除了衰减噪声,还会在 action 侧加一个低通处理,比如把当前动作和前一步动作做指数滑动平均,让绿灯时长变化更平滑。评估时必须把噪声归零,这是不用商量的。

5. 交通信号灯 DDPG 避坑指南:5 个高频翻车点与排查路径

这一节写的每一条,都是我实际跑过的代价。DDPG 在交通信号灯上的问题往往不是算法本身,而是环境、回报、归一化这些细节互相纠缠。下面按“现象 → 原因 → 解决”的方式给出一组可对照的排查记录。

5.1 奖励曲线在掉头,但路口平均等待时间没有下降

现象:critic loss 忽高忽低,奖励数值整体在上升,可你统计所有车辆从进入路口到离开的平均延误,发现并没有明显下降,甚至变差。

原因:奖励函数和你要优化的指标不一致。比如你用“排队长度差值”做奖励,但排队长度只反映某个瞬时的空间占用,不反映车辆已经等了多久。智能体发现只要让信号灯保持绿灯,排队自然减少,但它放行的可能是空空如也的车道,真正的延误车辆还在另一个方向压着。

解决:先把奖励换成“平均单车间隔时间变化量”,具体做法是每个决策步计算所有车辆累计等待时间总和,用前后两步的差做核心奖励项。然后单独打点记录平均等待时间,和 reward 画在同一张图里,看两者的相关趋势。如果 reward 在涨、等待时间在涨,直接判定奖励函数设计错误,不要继续调 DDPG 参数。

5.2 策略退化成“永远放行绿灯”,问题出在奖励塑形

现象:训练中期开始,某一相位绿灯一直延长,单方向车流被清空,其他相位车辆排队越长越多,整体奖励却依旧稳定偏高。

原因:这是典型的 reward hacking。少了公平性惩罚,智能体把动作永远滑向“延长当前相位”,因为持续放行会给 throughput 项源源不断带来正奖励,而其他方向的排队惩罚没有被设计进去。

解决:在奖励里强制加入单车道最大等待惩罚,或更直接地,给动作映射层加硬约束:绿灯持续超过 40 秒必须强制切换。示例:

def safe_mapping(raw_action, phase_left): extend = map_action(raw_action) # 硬约束:即使智能体想继续延长,也不允许超过最大绿灯时长 remain = 40.0 - phase_left extend = min(extend, remain) return extend

这类约束不影响 DDPG 的梯度传播,因为它是动作后处理层。你会发现加了硬约束后,智能体被迫在长排队方向之间做权衡,策略才会真正学到切换的时机。

5.3 换一个路口拓扑后性能崩盘:状态归一化没做好

现象:在单路口四相位的仿真里训练得很漂亮,换到一个五车道进口、自带左转专用道的路网,同样一套代码训练完全跑不动,奖励从一开始就是负数。

原因:状态里的排队长度、等待时间用的是绝对数值,不同路网的车道容量完全不一样。原来 MAX_QUEUE=20 的归一化,在五车道场景下每车道排队 15 辆属于正常平峰,却被归一化到 0.75,智能体以为这条路已经快堵死了,于是策略全面偏向这一个方向。

解决:把路网配置暴露给环境,状态归一化参数按每条车道单独计算:车道长度不同,排队容量上限就不同。等待时间上限也按仿真场景动态算,比如先跑一段固定配时的基线,取等待时间的 95 分位数作为 MAX_WAIT。这样换拓扑时,只需要改配置,不需要改网络和训练代码。

5.4 训练正常,评估时却差一大截:噪声没有关

现象:训练最后几百个 episode 奖励已经稳定在高位,但评估模式把噪声去掉之后,平均等待时间明显反弹,有时还不如固定配时基线。

原因:一是评估时忘了把 action 的噪声置零;二是训练时噪声衰减得太慢,策略已经适应了带噪声的动作分布,一旦噪声消失,它输出的动作落在和训练分布不同的区域,Critic 的估计和 Actor 的行为都开始失真。

解决:评估函数里显式把 noise_std 设为 0,并且设置多个随机种子跑评估取平均。判断标准是:训练采用噪声策略的奖励,和评估采用确定性策略的奖励,差距应该随着训练收敛逐步缩小。如果一直差很大,说明噪声衰减系数太慢,把 0.995 改成 0.99,让噪声更早降到 0.05 附近。

5.5 训练一整晚不收敛,loss 直接变成 NaN

现象:训练到几百个 episode 后,critic loss 突然出现 inf 或 NaN,然后所有梯度更新全部失效,奖励曲线瞬间跌到无效值。

原因:最常见是 critic 梯度爆炸,而梯度爆炸的根因又通常是奖励尺度异常。比如某一次仿真里出现极端排队,累计等待时间算出来上万秒,reward 出现一个超大异常点,这个点在 buffer 里被采样后,Q 值目标直接爆炸。其次是状态特征偶尔出现负数除以零的情况,比如某个车道流量为零时除以 MAX_FLOW 本身也是零。

解决:在 compute_reward 里对 reward 做截断,比如限定在 [-50, 50];状态归一化时对分母加 epsilon 防除零;训练循环里保留梯度裁剪。最后一步是保险丝:每次采样后检查 batch 里有没有 NaN 值,发现有就直接丢弃这一批样本,不让它进入 backward。这个方法不优雅但非常有效,血泪经验。

6. 仿真模型能发论文,但能不能落地:先过这三个验证门槛

仿真里 DDPG 策略再漂亮,离真实路口还有距离。我一般会拿三个门槛去判断一个模型到底能不能投到实际场景。

第一个门槛是输入噪声鲁棒性。SUMO 给的状态是理想精确的,真实路口的车辆检测器有遮挡、漏检和延迟。验证方法是训练完成后,在评估阶段给状态加 5% 到 10% 的高斯噪声,看平均等待时间会不会剧烈恶化。如果不行,回到训练,在状态输入上加同样的噪声做正则化,让 Actor 不要对精确数值过度敏感。

第二个门槛是流量模式泛化。只在固定流量矩阵下训练的模型,换到早高峰突发车流往往崩盘。做法是把训练流量拆成三种:平峰、高峰、随机脉冲,做多个流量种子。评估时用训练里没见过的那一组随机种子,单独记录指标。能通过这个门槛,模型才有跨时段迁移的潜力。

第三个门槛是交通安全约束。DDPG 只优化“效率”,不保证“安全”。最小绿灯时长要锁死,防止行人等待过久;最大绿灯时长要封顶,避免单方向持续放行;相位切换的黄灯和全红时间必须在控制器侧强制,不能依赖网络输出。你可以写一个这样的验证小函数:

def evaluate_with_safety(env, actor, seeds=[1, 2, 3]): results = [] for seed in seeds: env.set_seed(seed) state = env.reset() total_reward = 0 done = False while not done: raw = actor(torch.FloatTensor(state).unsqueeze(0)).item() phase_left = env.get_current_phase_duration() # 安全层:动作先过约束再进环境 safe_action = min(raw, 40.0 - phase_left) state, reward, done, _ = env.step(safe_action) total_reward += reward results.append(total_reward) return results

这三个门槛的顺序是固定的:先验证鲁棒性,再验证泛化性,最后验证安全性。前两个过不了,说明模型只能在仿真里自嗨;最后一个过不了,说明这个方案根本进不了真实信号机。

我早期做过一次复现,把大量时间花在改进网络结构上,后来发现瓶颈根本不是网络,而是状态归一化没做干净。从那以后我每次拿到 Traffic-Signal-Control-master 这类工程,第一件事永远是检查 MDP 定义,第二件事跑一个固定配时基线做对照,两件事做完,项目能不能成心里就有数了。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询