简介:本资源是一套面向智能交通系统研究者与强化学习实践者的完整算法实现方案,聚焦于单交叉口自适应信号控制问题,融合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时刻动作对应t+4秒后的状态)。本项目采用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, dtype=np.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 状态特征工程:三类时序数据的标准化与滞后窗口构建
单纯使用瞬时检测器值会导致状态抖动(如某秒无车通过造成count=0),故引入3秒滑动窗口统计。但Vissim COM不提供历史数据缓存,需在Python端维护环形缓冲区:
# state_processor.py from collections import deque class StateBuffer: def __init__(self, window_size=3): self.window_size = window_size self.count_buffer = deque(maxlen=window_size) # 存储最近3秒count self.speed_buffer = deque(maxlen=window_size) # 存储最近3秒speed self.queue_buffer = deque(maxlen=window_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, dtype=np.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[i+1], raw_state[i+2]) processed_state = buffer.get_features() # 6维稳定特征注意:
deque(maxlen=N)是线程安全的环形缓冲区,比手动维护索引更可靠。若window_size=3但仿真步长<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_dim=6, action_dim=9, hidden_dim=128): 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], dim=1) 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_dim=128 | 128 | 小于状态维数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_steps=18000): 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_threshold=0.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, done=False) # 每100步执行一次网络更新 if step % 100 == 0: batch = buffer.sample(batch_size=64) 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 Replay(PER),按|Q(s,a)-target|绝对值分配采样概率:
# replay_buffer.py import numpy as np import torch class PrioritizedReplayBuffer: def __init__(self, capacity, alpha=0.6): self.capacity = capacity self.alpha = alpha self.buffer = [] self.priorities = np.zeros(capacity, dtype=np.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, p=probs) 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 2>&1 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/ pywin32==305 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.bat,UAC弹窗会中断流程,故用此工具静默提权。
4.2run vissim.bat的进程守护与异常恢复机制
Vissim仿真常因COM连接中断或内存泄漏崩溃,本脚本加入心跳检测与自动重启:
:: run vissim.bat @echo off set VISSIM_PID= set MAX_RETRY=3 :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 "tokens=2" %%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中启动Vissim?
subprocess启动的进程在父Python进程退出时会被强制终止,而本项目需保证Vissim长期运行(数小时仿真),故采用start命令分离进程。/min参数使Python控制器窗口最小化,避免干扰Vissim GUI。
4.3 典型故障与日志定位方法
| 故障现象 | 日志位置 | 根本原因 | 解决方案 |
|---|---|---|---|
pywin32报错ModuleNotFoundError | venv\Lib\site-packages\pywin32_system32\缺失DLL | pywin32安装后需运行python Scripts\pywin32_postinstall.py -install | 在setup.bat末尾添加该命令 |
Vissim COM连接超时(0x800401E4) | Windows事件查看器→应用程序日志 | Vissim未以管理员身份运行,COM注册失败 | 修改run vissim.bat,在start命令前加powershell Start-Process ... -Verb RunAs |
| DQN训练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辆/小时) | 100 | 20 | 运行test_normalization.py,输入100组实测数据,检查输出值是否集中在[-1,1] |
| 中流量(500~1500辆/小时) | 300 | 50 | 同上 |
| 高流量(>1500辆/小时) | 500 | 100 | 同上 |
提示:归一化分母写死在代码中,避免在线计算开销。若流量波动剧烈,可改用运行时滑动最大值(
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。
本文还有配套的精品资源,点击获取