ARTICLE DETAIL

资讯详情

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

深度强化学习DDPG实现交通信号灯自适应控制实战指南

深度强化学习DDPG实现交通信号灯自适应控制实战指南 简介面向智能交通与强化学习研究者的深度强化学习源码工程聚焦交通信号灯控制场景提供基于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_extend20.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 rewarddelay_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_action20.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_actionActor 网络就是状态到动作的确定性策略。两个隐藏层各 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, capacity100000): self.buffer deque(maxlencapacity) 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], dtypenp.float32) a np.array([x[1] for x in batch], dtypenp.float32).reshape(-1, 1) r np.array([x[2] for x in batch], dtypenp.float32).reshape(-1, 1) ns np.array([x[3] for x in batch], dtypenp.float32) done np.array([x[4] for x in batch], dtypenp.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(), lr1e-4) critic_opt torch.optim.Adam(critic.parameters(), lr1e-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-5critic 学习率1e-3critic loss 发散时降到 5e-4tau软更新系数0.005Q 值抖动大时降到 0.001gamma折扣因子0.99决策周期较长时降到 0.95tau 控制目标网络跟随当前网络的速度。tau 太大目标网络一直在追当前网络训练不稳定tau 太小目标网络更新太慢学习速度明显下降。交通信号灯场景一个决策步是现实中的 5 秒gamma0.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_QUEUE20 的归一化在五车道场景下每车道排队 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 定义第二件事跑一个固定配时基线做对照两件事做完项目能不能成心里就有数了。希望帮到你。本文还有配套的精品资源点击获取
返回列表