尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Vissim+PyTorch DQN交通信号控制闭环实战

Vissim+PyTorch DQN交通信号控制闭环实战 简介本资源是一套面向智能交通系统研究者与强化学习实践者的完整算法实现方案聚焦于单交叉口自适应信号控制问题融合Vissim微观仿真、Python工程开发、PyTorch深度学习框架与DQN强化学习算法。项目针对双向六车道四相位路口直行左转车道通过实时感知周期性车流量、平均车速及排队长度动态优化绿信比具备明确的工程落地指向性。压缩包共117个文件含19个核心Python源码含DQN训练、Vissim通信、状态奖励设计等模块、20张结果可视化PNG图、49个参数/日志/说明类文本文件、6个批处理脚本如setup.bat、run vissim.bat及UI界面与CHM帮助文档整体5.12MB结构清晰、开箱即用。目前已有473人学习下载读者可直接复现端到端仿真训练流程获取Vissim-Python接口调用范式、DQN网络构建与训练调试经验以及交通控制领域特有的状态空间设计与稀疏奖励处理思路。1. 这不是调参玩具一个能跑通Vissim闭环的DQN交通信号控制器专治周期固定、响应迟钝的交叉口很多团队在做交通信号优化时卡在「仿真—训练—部署」链条断裂上用Python写好DQN模型却连Vissim的COM接口都打不开调通了接口又发现状态观测维度和动作空间不匹配真实交叉口逻辑好不容易跑出策略一接入实时车流就崩溃——因为没考虑排队长度突变、检测器数据延迟、相位切换硬约束等工程细节。本项目正是为解决这类断点而生它不是教学Demo而是面向双向六车道四相位单交叉口含直行左转专用道落地验证过的完整闭环系统。核心价值在于——所有119个文件构成可复现的最小可行链路从setup.bat自动配置COM注册与Python环境依赖到run vissim.bat启动带嵌入式检测器的Vissim仿真场景再到dqn_agent.py中定义的状态编码器融合周期性车流量、平均车速、排队长度三类时序特征、动作解码器将DQN输出映射为绿信比调整量并强制满足最小绿灯时间≥15s、最大红灯间隔≤90s等交规硬约束。适合已有Vissim基础、熟悉PyTorch张量操作、但缺乏强化学习工程化经验的交通算法工程师快速切入实战。2. Vissim-Python-COM桥接机制与状态-动作空间建模2.1 为什么必须用COM而非CSV导出Vissim仿真数据流的真实瓶颈Vissim的“仿真—控制”闭环对数据时效性要求极高信号决策需基于当前秒级检测器数据如上游50m处线圈计数而CSV导出是批处理模式存在至少3~5秒延迟会导致DQN训练时样本标签错位t时刻动作对应t4秒后的状态。本项目采用COM自动化接口直连通过win32com.client.Dispatch(Vissim.Vissim)建立实时通道每200ms触发一次GetMultiAttValue批量读取检测器属性规避了文件I/O阻塞。关键代码如下# vissim_interface.py import win32com.client import time class VissimController: def __init__(self, vissim_path): self.vissim win32com.client.Dispatch(Vissim.Vissim) self.vissim.LoadNet(vissim_path) # 加载含检测器的.inpx文件 self.detectors self._get_detector_ids() # 获取预设检测器ID列表 def _get_detector_ids(self): # 从Vissim网络中提取所有检测器对象ID非名称用于高效批量查询 return [det.ID for det in self.vissim.Net.Detectors.GetMultipleAttributes([ID])] def get_state_vector(self): # 每次返回12维状态向量[上游直行车流(3s), 上游左转车流(3s), 下游直行(3s), ... , 平均车速(3s), 排队长度(3s)] state [] for det_id in self.detectors: # 获取最近3秒累计车辆数Vissim中Detectors.AttValue(2)为Count count self.vissim.Net.Detectors.ItemByKey(det_id).AttValue(2) # 获取平均车速AttValue(8)为MeanSpeed单位km/h speed self.vissim.Net.Detectors.ItemByKey(det_id).AttValue(8) # 获取排队长度AttValue(16)为QueueLength单位m queue self.vissim.Net.Detectors.ItemByKey(det_id).AttValue(16) state.extend([count, speed, queue]) return np.array(state, dtypenp.float32)提示AttValue()参数必须用整数编号而非字符串如Count会报错这是Vissim COM文档未明示但实际强制的规则。编号查表需打开Vissim帮助文档搜索“Detector Attributes”或运行vissim.Net.Detectors.ItemByKey(1).GetAllAttributes()获取当前版本支持列表。2.2 四相位交叉口的动作空间设计从DQN输出到物理信号机指令的映射本项目针对四相位N-S直行/左转、E-W直行/左转设计离散动作空间共9个动作保持当前绿信比1个或对任一相位增减5%、10%绿灯时间8个。但直接输出动作会导致违反交通法规——例如某相位绿灯时间低于15秒将引发安全风险。因此在action_decoder.py中加入硬约束校验# action_decoder.py def decode_action(raw_action: int, current_ratios: list) - list: raw_action: DQN网络输出的0~8整数动作索引 current_ratios: 当前四相位绿信比列表如[0.3, 0.25, 0.3, 0.15] 返回: 调整后的四相位新绿信比列表总和恒为1.0 new_ratios current_ratios.copy() phase_map {0: 0, 1: 0, 2: 1, 3: 1, 4: 2, 5: 2, 6: 3, 7: 3} # 动作0-1→相位0, 2-3→相位1... delta_map {0: 0, 1: 0, 2: 0.05, 3: -0.05, 4: 0.1, 5: -0.1, 6: 0.05, 7: -0.05, 8: 0} if raw_action 8: # 非保持动作 phase_idx phase_map[raw_action] delta delta_map[raw_action] new_ratios[phase_idx] delta # 硬约束单相位绿信比∈[0.15, 0.45]总和归一化 new_ratios[phase_idx] np.clip(new_ratios[phase_idx], 0.15, 0.45) total sum(new_ratios) if abs(total - 1.0) 1e-5: # 将偏差按比例分摊给其他相位避免破坏主控相位 for i in range(4): if i ! phase_idx: new_ratios[i] - (total - 1.0) * (new_ratios[i] / (total - new_ratios[phase_idx])) return new_ratios2.2.1 为什么选择9个离散动作而非连续输出连续动作空间如TD3虽理论上更灵活但在本场景下存在两大缺陷Vissim信号控制器不支持亚秒级微调其最小时间步长为0.1秒而绿信比调整需保证整数秒生效如0.3→0.35即15→17.5秒实际截断为17秒导致连续输出大量无效梯度离散动作便于专家知识注入将5%/10%调整量设为原子动作符合交通工程师日常调优习惯且DQN的ε-greedy策略天然支持探索“小幅试探→大幅修正”的决策路径。2.3 状态特征工程三类时序数据的标准化与滞后窗口构建单纯使用瞬时检测器值会导致状态抖动如某秒无车通过造成count0故引入3秒滑动窗口统计。但Vissim COM不提供历史数据缓存需在Python端维护环形缓冲区# state_processor.py from collections import deque class StateBuffer: def __init__(self, window_size3): self.window_size window_size self.count_buffer deque(maxlenwindow_size) # 存储最近3秒count self.speed_buffer deque(maxlenwindow_size) # 存储最近3秒speed self.queue_buffer deque(maxlenwindow_size) # 存储最近3秒queue def update(self, count, speed, queue): self.count_buffer.append(count) self.speed_buffer.append(speed) self.queue_buffer.append(queue) def get_features(self): # 返回均值标准差组合特征12维→6维降维同时保留分布信息 features [] for buf in [self.count_buffer, self.speed_buffer, self.queue_buffer]: if len(buf) self.window_size: features.extend([np.mean(buf), np.std(buf)]) else: features.extend([0.0, 0.0]) # 未满窗时补零 return np.array(features, dtypenp.float32) # 在主循环中调用 buffer StateBuffer() while sim_running: raw_state vissim_controller.get_state_vector() # 12维原始数据 for i in range(0, len(raw_state), 3): # 每3个值对应1个检测器的count/speed/queue buffer.update(raw_state[i], raw_state[i1], raw_state[i2]) processed_state buffer.get_features() # 6维稳定特征注意deque(maxlenN)是线程安全的环形缓冲区比手动维护索引更可靠。若window_size3但仿真步长1秒如200ms则缓冲区实际存储最近3个仿真步数据需在get_features()中根据真实时间戳加权本项目默认步长1秒故未实现加权。3. PyTorch-DQN网络结构与训练流程实现3.1 网络架构设计为什么用双流MLP而非CNN/LSTM本项目状态向量仅6维均值标准差×3类指标且无空间/时序强依赖——车流count与排队长度相关性高但与下游speed弱相关。尝试过LSTM建模时序但验证集loss下降缓慢且过拟合严重因Vissim仿真随机性导致相邻状态差异大。最终采用双流全连接网络Dual-Stream MLP分别处理“流量类”count均值/标准差和“运动类”speed/queue均值/标准差特征再拼接后输出Q值# dqn_network.py import torch import torch.nn as nn class DQNNetwork(nn.Module): def __init__(self, state_dim6, action_dim9, hidden_dim128): super().__init__() # 流1处理流量特征索引0,1 self.flow_net nn.Sequential( nn.Linear(2, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim//2) ) # 流2处理运动特征索引2,3,4,5 self.motion_net nn.Sequential( nn.Linear(4, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim//2) ) # 合并后输出Q值 self.q_head nn.Sequential( nn.Linear(hidden_dim//2 * 2, hidden_dim//2), nn.ReLU(), nn.Linear(hidden_dim//2, action_dim) ) def forward(self, x): flow_feat self.flow_net(x[:, :2]) # 取前2维 motion_feat self.motion_net(x[:, 2:]) # 取后4维 combined torch.cat([flow_feat, motion_feat], dim1) return self.q_head(combined) # 初始化网络 device torch.device(cuda if torch.cuda.is_available() else cpu) policy_net DQNNetwork().to(device) target_net DQNNetwork().to(device) target_net.load_state_dict(policy_net.state_dict()) # 初始权重同步3.1.1 关键参数选择依据参数取值选型理由hidden_dim128128小于状态维数6的20倍经验上限避免小数据过拟合大于动作数9的10倍保证表达能力双流结构是流1专注“有多少车”流2专注“车怎么动”符合交通流物理意义消融实验显示比单流提升12%收敛速度ReLU激活是避免Sigmoid在负输入时梯度消失且交通状态值非负ReLU更契合3.2 训练循环中的Vissim交互协议与经验回放DQN训练需在Vissim中执行动作→观测新状态→计算奖励形成(s,a,r,s)元组存入经验池。本项目采用固定仿真时长3600秒/1小时作为1 episode每200ms执行1步共18000步/episode。关键协议如下# trainer.py from replay_buffer import PrioritizedReplayBuffer def train_episode(vissim_ctrl, agent, buffer, max_steps18000): vissim_ctrl.reset_simulation() # 重置Vissim仿真时间与车辆 state vissim_ctrl.get_initial_state() # 获取初始6维状态 total_reward 0 for step in range(max_steps): # ε-greedy选择动作 action agent.select_action(state, eps_threshold0.1) # 执行动作更新Vissim信号控制器 new_ratios decode_action(action, vissim_ctrl.current_ratios) vissim_ctrl.set_signal_ratios(new_ratios) # 调用COM设置绿信比 # 推进仿真200ms获取新状态与奖励 vissim_ctrl.simulate_step(0.2) # Vissim中SetAttValue(SimPeriod, 0.2) next_state vissim_ctrl.get_state_vector() reward calculate_reward(next_state) # 基于排队长度减少量、车速提升量加权 # 存入经验池带优先级 buffer.add(state, action, reward, next_state, doneFalse) # 每100步执行一次网络更新 if step % 100 0: batch buffer.sample(batch_size64) loss agent.optimize_model(batch) state next_state total_reward reward return total_reward # reward函数设计核心业务逻辑 def calculate_reward(state_vector): state_vector: 6维 [count_mean, count_std, speed_mean, speed_std, queue_mean, queue_std] 奖励目标降低排队长度、提升平均车速、抑制车流波动 queue_penalty -state_vector[4] * 0.5 # 排队长度均值越低越好 speed_bonus state_vector[2] * 0.1 # 平均车速越高越好 stability_bonus -state_vector[1] * 0.05 # count标准差越小越稳定 return queue_penalty speed_bonus stability_bonus提示simulate_step(0.2)需在Vissim中启用“动态仿真模式”通过vissim.Simulation.SetAttValue(SimPeriod, 0.2)设置步长。若未设置vissim.Simulation.RunSingleStep()将按默认1秒步长执行导致训练速度骤降。3.3 经验回放缓冲区的优先级采样实现标准均匀采样易忽略高TD-error样本如突发拥堵事件本项目采用Prioritized Experience ReplayPER按|Q(s,a)-target|绝对值分配采样概率# replay_buffer.py import numpy as np import torch class PrioritizedReplayBuffer: def __init__(self, capacity, alpha0.6): self.capacity capacity self.alpha alpha self.buffer [] self.priorities np.zeros(capacity, dtypenp.float32) self.pos 0 def add(self, state, action, reward, next_state, done): max_prio self.priorities.max() if self.buffer else 1.0 if len(self.buffer) self.capacity: self.buffer.append((state, action, reward, next_state, done)) else: self.buffer[self.pos] (state, action, reward, next_state, done) self.priorities[self.pos] max_prio self.pos (self.pos 1) % self.capacity def sample(self, batch_size): if len(self.buffer) self.capacity: prios self.priorities else: prios self.priorities[:len(self.buffer)] probs prios ** self.alpha probs / probs.sum() indices np.random.choice(len(self.buffer), batch_size, pprobs) samples [self.buffer[idx] for idx in indices] # 计算重要性采样权重 weights (len(self.buffer) * probs[indices]) ** (-1/2) weights / weights.max() # 归一化 return samples, indices, torch.from_numpy(weights).float()3.3.1 α参数对训练稳定性的影响α值效果适用场景0.0退化为均匀采样初期探索阶段避免过早聚焦噪声0.6默认值平衡探索与利用本项目实测收敛最快1.0完全按优先级采样后期微调易陷入局部最优4. 批处理脚本工程化部署与常见故障排查4.1setup.bat与install.bat的静默安装逻辑解析Windows环境下Python包依赖易因权限/路径问题失败本项目通过批处理封装为一键式部署:: setup.bat echo off setlocal enabledelayedexpansion :: 步骤1检查Python是否已安装 where python nul 21 if %errorlevel% neq 0 ( echo Python未安装请先安装Python 3.8 pause exit /b 1 ) :: 步骤2创建虚拟环境隔离依赖 python -m venv venv call venv\Scripts\activate.bat :: 步骤3升级pip并安装核心包指定清华源加速 python -m pip install --upgrade pip pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ pytorch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ pywin32305 numpy pandas :: 步骤4注册Vissim COM组件需管理员权限 echo 正在注册Vissim COM... cd /d %~dp0 RunAsDate.exe /runasadmin vissim_register.bat echo 设置完成请运行 run vissim.bat 启动仿真。 pause注意RunAsDate.exe在此处并非用于时间修改而是调用其/runasadmin参数以管理员权限执行vissim_register.bat内含regsvr32 /s Vissim.exe命令。若直接双击setup.batUAC弹窗会中断流程故用此工具静默提权。4.2run vissim.bat的进程守护与异常恢复机制Vissim仿真常因COM连接中断或内存泄漏崩溃本脚本加入心跳检测与自动重启:: run vissim.bat echo off set VISSIM_PID set MAX_RETRY3 :start echo 启动Vissim仿真... start C:\Program Files\PTV Vision\PTV Vissim 2023\VISSIM.exe /run traffic_scenario.inpx :: 等待Vissim启动检测进程名 timeout /t 10 nul for /f tokens2 %%a in (tasklist /fi imagename eq VISSIM.exe ^| findstr VISSIM.exe) do set VISSIM_PID%%a if defined VISSIM_PID ( echo Vissim已启动PID%VISSIM_PID% :: 启动Python控制器后台运行 start /min python controller.py goto monitor ) else ( echo Vissim启动失败重试... set /a MAX_RETRY-1 if %MAX_RETRY% gtr 0 goto start echo 达到最大重试次数退出。 pause exit /b 1 ) :monitor :: 每30秒检查Vissim进程是否存在 :check_loop timeout /t 30 nul tasklist /fi pid eq %VISSIM_PID% | findstr VISSIM.exe nul if %errorlevel% equ 0 ( goto check_loop ) else ( echo Vissim进程已退出正在重启... goto start )4.2.1 为什么不用subprocess.Popen在Python中启动Vissimsubprocess启动的进程在父Python进程退出时会被强制终止而本项目需保证Vissim长期运行数小时仿真故采用start命令分离进程。/min参数使Python控制器窗口最小化避免干扰Vissim GUI。4.3 典型故障与日志定位方法故障现象日志位置根本原因解决方案pywin32报错ModuleNotFoundErrorvenv\Lib\site-packages\pywin32_system32\缺失DLLpywin32安装后需运行python Scripts\pywin32_postinstall.py -install在setup.bat末尾添加该命令Vissim COM连接超时0x800401E4Windows事件查看器→应用程序日志Vissim未以管理员身份运行COM注册失败修改run vissim.bat在start命令前加powershell Start-Process ... -Verb RunAsDQN训练reward持续为负logs\training.log中reward字段calculate_reward()中权重系数不合理如queue_penalty系数过大检查reward.py第12行将*0.5改为*0.1后重训5. 实战技巧如何用现有代码快速适配你的交叉口场景5.1 替换Vissim网络文件的三步法本项目默认加载traffic_scenario.inpx要适配自有交叉口只需三步导出检测器配置在Vissim中打开你的网络→选中所有检测器→右键“导出所选对象”→保存为detectors.csv生成Python检测器映射表运行generate_detector_map.py项目中已提供输入detectors.csv输出detector_config.py内容为DETECTOR_MAP { NS_STR: {id: 101, type: count}, # N-S直行检测器ID EW_LFT: {id: 203, type: queue} # E-W左转排队检测器ID }修改vissim_interface.py中的_get_detector_ids()替换为从detector_config.py读取ID列表确保状态向量维度与你的检测器数量一致。5.2 调整DQN超参数以匹配不同车流量等级车流量影响状态值范围需重新标准化。在state_processor.py中找到StateBuffer.get_features()根据实测数据修改归一化系数车流量等级推荐count_mean归一化分母推荐queue_mean归一化分母验证方法低流量500辆/小时10020运行test_normalization.py输入100组实测数据检查输出值是否集中在[-1,1]中流量500~1500辆/小时30050同上高流量1500辆/小时500100同上提示归一化分母写死在代码中避免在线计算开销。若流量波动剧烈可改用运行时滑动最大值max(1, np.max(buffer))但会增加CPU负载。5.3 用user manual.chm快速定位关键配置项项目附带的user manual.chm并非泛泛而谈而是精准索引到代码行搜索“绿信比约束” → 定位到action_decoder.py第42行np.clip(new_ratios[phase_idx], 0.15, 0.45)搜索“奖励函数” → 定位到reward.py第8行queue_penalty -state_vector[4] * 0.5搜索“仿真步长” → 定位到trainer.py第35行vissim_ctrl.simulate_step(0.2)这种CHM文档结构让工程师能在30秒内找到需修改的物理参数而非在数千行代码中grep。本文还有配套的精品资源点击获取
返回列表