From 4dbea5f0a6c33229f44a4b4c2b7a80639206062c Mon Sep 17 00:00:00 2001 From: huangfu <3045324663@qq.com> Date: Sat, 17 Jan 2026 14:50:20 +0800 Subject: [PATCH] mixed_training --- Env/ENVIRONMENT_INFO.md | 279 ------- Env/LOGGING_GUIDE.md | 413 ---------- .../expert_replay_env.cpython-313.pyc | Bin 0 -> 19967 bytes .../expert_replay_env.cpython-39.pyc | Bin 0 -> 9234 bytes .../inverse_dynamics.cpython-313.pyc | Bin 0 -> 2701 bytes .../inverse_dynamics.cpython-39.pyc | Bin 0 -> 2008 bytes Env/__pycache__/logger_utils.cpython-39.pyc | Bin 0 -> 4851 bytes Env/__pycache__/replay_policy.cpython-39.pyc | Bin 0 -> 1986 bytes Env/__pycache__/scenario_env.cpython-313.pyc | Bin 30373 -> 12272 bytes Env/__pycache__/scenario_env.cpython-39.pyc | Bin 0 -> 6801 bytes .../simple_idm_policy.cpython-313.pyc | Bin 963 -> 963 bytes .../simple_idm_policy.cpython-39.pyc | Bin 0 -> 766 bytes Env/example_with_logging.py | 116 --- Env/expert_replay_env.py | 527 +++++++++++++ Env/inverse_dynamics.py | 65 ++ Env/logger_utils.py | 170 ---- Env/run_multiagent_env.py | 54 +- Env/scenario_env.py | 723 ++---------------- Env/test_no_traffic_lights.py | 76 -- README.md | 321 ++------ TRAINING_ARCHITECTURE.md | 498 ++++++++++++ dataset/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 149 bytes dataset/__pycache__/__init__.cpython-39.pyc | Bin 0 -> 143 bytes .../magail_dataset.cpython-313.pyc | Bin 0 -> 3152 bytes .../__pycache__/magail_dataset.cpython-39.pyc | Bin 0 -> 1892 bytes dataset/magail_dataset.py | 61 ++ scripts/analyze_expert_data.py | 2 +- scripts/generate_expert_data.py | 162 ++++ scripts/visualize_replay.py | 93 +++ train_magail.py | 521 +++++++++++++ 30 files changed, 2075 insertions(+), 2006 deletions(-) delete mode 100644 Env/ENVIRONMENT_INFO.md delete mode 100644 Env/LOGGING_GUIDE.md create mode 100644 Env/__pycache__/expert_replay_env.cpython-313.pyc create mode 100644 Env/__pycache__/expert_replay_env.cpython-39.pyc create mode 100644 Env/__pycache__/inverse_dynamics.cpython-313.pyc create mode 100644 Env/__pycache__/inverse_dynamics.cpython-39.pyc create mode 100644 Env/__pycache__/logger_utils.cpython-39.pyc create mode 100644 Env/__pycache__/replay_policy.cpython-39.pyc create mode 100644 Env/__pycache__/scenario_env.cpython-39.pyc create mode 100644 Env/__pycache__/simple_idm_policy.cpython-39.pyc delete mode 100644 Env/example_with_logging.py create mode 100644 Env/expert_replay_env.py create mode 100644 Env/inverse_dynamics.py delete mode 100644 Env/logger_utils.py delete mode 100644 Env/test_no_traffic_lights.py create mode 100644 TRAINING_ARCHITECTURE.md create mode 100644 dataset/__pycache__/__init__.cpython-313.pyc create mode 100644 dataset/__pycache__/__init__.cpython-39.pyc create mode 100644 dataset/__pycache__/magail_dataset.cpython-313.pyc create mode 100644 dataset/__pycache__/magail_dataset.cpython-39.pyc create mode 100644 dataset/magail_dataset.py create mode 100644 scripts/generate_expert_data.py create mode 100644 scripts/visualize_replay.py create mode 100644 train_magail.py diff --git a/Env/ENVIRONMENT_INFO.md b/Env/ENVIRONMENT_INFO.md deleted file mode 100644 index 72ca3e3..0000000 --- a/Env/ENVIRONMENT_INFO.md +++ /dev/null @@ -1,279 +0,0 @@ -# 多智能体场景环境详细说明 - -## 1. 观测信息详解 - -### 观测向量结构(总维度:107维) - -每个智能体的观测向量包含以下信息: - -```python -观测向量 = [ - # 1. 车辆状态信息 (5维) - position_x, # 车辆X坐标 - position_y, # 车辆Y坐标 - velocity_x, # X方向速度 - velocity_y, # Y方向速度 - heading_theta, # 朝向角度 - - # 2. 前向激光雷达 (80维) - lidar_1, # 第1个激光束的距离 - lidar_2, # 第2个激光束的距离 - ... - lidar_80, # 第80个激光束的距离 - # 范围:30米,用于前方障碍物检测 - - # 3. 侧向激光雷达 (10维) - side_lidar_1, # 第1个侧向激光束的距离 - ... - side_lidar_10, # 第10个侧向激光束的距离 - # 范围:8米,用于侧方障碍物检测 - - # 4. 车道线检测 (10维) - lane_line_1, # 第1个车道线检测距离 - ... - lane_line_10, # 第10个车道线检测距离 - # 范围:3米,用于车道线识别 - - # 5. 导航信息 (2维) - destination_x, # 目标点X坐标 - destination_y, # 目标点Y坐标 -] -``` - -### 观测信息说明 - -1. **车辆状态 (5维)** - - 位置:全局坐标系下的(x, y) - - 速度:车辆在全局坐标系下的速度分量 - - 朝向:车辆的航向角(弧度) - -2. **激光雷达 (100维)** - - 前向80束:覆盖前方视野,检测动态和静态障碍物 - - 侧向10束:检测侧方物体,用于变道等操作 - - 车道线10束:专门检测车道线位置 - -3. **导航信息 (2维)** - - 目标位置:从专家数据中提取的车辆最终位置 - -**总维度:5 + 80 + 10 + 10 + 2 = 107维** - ---- - -## 2. 多场景加载逻辑 - -### 2.1 场景加载机制 - -MetaDrive的ScenarioEnv通过以下配置管理多场景: - -```python -config = { - "data_directory": "path/to/dataset", # 包含dataset_mapping.pkl的目录 - "num_scenarios": 3, # 场景总数(从mapping文件读取) - "start_scenario_index": 0, # 起始场景索引 - "sequential_seed": True, # 是否顺序切换场景 -} -``` - -### 2.2 场景切换逻辑 - -#### 场景索引管理 -- **初始化**:环境启动时从`start_scenario_index`开始 -- **顺序模式**(`sequential_seed=True`): - - 每次`env.reset()`后自动切换到下一个场景 - - 循环顺序:场景0 → 场景1 → 场景2 → 场景0 ... - -#### 示例流程 -```python -env = MultiAgentScenarioEnv(config={ - "data_directory": "path/to/dataset", # 假设有3个场景 - "start_scenario_index": 0, - "sequential_seed": True, -}) - -obs = env.reset(0) # 使用场景0 -# ... 运行场景0 ... - -obs = env.reset() # 自动切换到场景1 -# ... 运行场景1 ... - -obs = env.reset() # 自动切换到场景2 -# ... 运行场景2 ... - -obs = env.reset() # 循环回到场景0 -``` - -### 2.3 当前场景信息获取 - -可以通过以下方式查看当前场景信息: - -```python -# 获取当前场景索引 -current_scenario = env.engine.current_seed - -# 获取场景总数 -total_scenarios = env.config["num_scenarios"] - -# 查看场景ID -scenario_id = env.engine.traffic_manager.current_scenario_id -``` - -### 2.4 手动指定场景 - -如果需要固定使用某个场景: - -```python -# 方法1:在reset时指定 -obs = env.reset(seed=1) # 使用场景1 - -# 方法2:配置固定场景 -config = { - "start_scenario_index": 2, # 从场景2开始 - "sequential_seed": False, # 禁用自动切换 -} -``` - ---- - -## 3. 车辆观测获取机制 - -### 3.1 观测获取流程 - -``` -step() 被调用 - ↓ -更新所有车辆物理状态 - ↓ -_spawn_controlled_agents() # 生成新车辆(按时间步) - ↓ -_get_all_obs() # 获取所有车辆观测 - ↓ - 遍历 controlled_agents: - ├─ 获取车辆状态 (position, velocity, heading) - ├─ 调用激光雷达传感器 - ├─ 组装观测向量 - └─ 添加到 obs_list - ↓ -返回 obs_list(包含所有车辆的观测) -``` - -### 3.2 观测返回格式 - -```python -obs = env.reset() -# obs 是一个列表,每个元素是一个车辆的观测向量 - -obs = [ - [obs_vehicle_0], # 第1辆车的107维观测 - [obs_vehicle_1], # 第2辆车的107维观测 - ... -] - -# 在step中 -obs, rewards, dones, infos = env.step(actions) -# obs格式相同,但只包含当前存活的车辆 -``` - -### 3.3 观测一致性保证 - -1. **物理状态同步** - - 所有车辆在同一物理时间步获取观测 - - 保证观测的时间一致性 - -2. **传感器独立性** - - 每个车辆有独立的传感器 - - 激光雷达从各自位置发射 - -3. **动态车辆管理** - - 新车辆在生成时立即获取观测 - - 观测列表动态更新 - ---- - -## 4. 常见问题解答 - -### Q1: 为什么观测维度是107而不是其他? -**A**: -- 车辆状态: 5维 (x, y, vx, vy, heading) -- 前向激光雷达: 80维 -- 侧向激光雷达: 10维 -- 车道线检测: 10维 -- 目标位置: 2维 -- **总计: 5 + 80 + 10 + 10 + 2 = 107维** - -### Q2: 如何确保多场景都被使用? -**A**: 设置`sequential_seed=True`,环境会自动循环遍历所有场景。 - -### Q3: 车辆在不同时间步生成,如何获取观测? -**A**: 每次调用`step()`时: -1. 先检查是否有新车辆需要生成(`_spawn_controlled_agents`) -2. 为所有现存车辆(包括新生成的)获取观测 -3. 返回的obs_list包含所有当前存活车辆的观测 - -### Q4: 如果场景中车辆数量不同怎么办? -**A**: -- 观测列表长度动态调整 -- 使用`max_controlled_vehicles`可限制最大车辆数 -- 使用`filter_offroad_vehicles`可过滤无效车辆 - -### Q5: 观测数据的坐标系是什么? -**A**: -- 位置/速度/目标:**全局坐标系**(世界坐标) -- 激光雷达:**车辆局部坐标系**(以车辆为中心) - ---- - -## 5. 配置建议 - -### 5.1 训练配置 -```python -config = { - "data_directory": "path/to/dataset", - "sequential_seed": True, # 循环使用所有场景 - "filter_offroad_vehicles": True, # 过滤无效车辆 - "max_controlled_vehicles": 20, # 限制车辆数防止过载 - "inherit_expert_velocity": False, # 训练时不继承速度 - "horizon": 300, # 每场景运行300步 -} -``` - -### 5.2 评估配置 -```python -config = { - "data_directory": "path/to/dataset", - "sequential_seed": False, # 固定场景 - "start_scenario_index": 0, # 指定场景 - "filter_offroad_vehicles": True, - "max_controlled_vehicles": None, # 不限制车辆数 - "inherit_expert_velocity": True, # 评估时继承速度 - "verbose_reset": True, # 输出详细信息 -} -``` - ---- - -## 6. 调试技巧 - -### 6.1 查看当前场景信息 -```python -# 在reset后查看 -print(f"当前场景: {env.engine.current_seed}") -print(f"总场景数: {env.config['num_scenarios']}") -print(f"可控车辆数: {len(env.controlled_agents)}") -``` - -### 6.2 查看观测维度 -```python -obs = env.reset() -print(f"车辆数量: {len(obs)}") -if len(obs) > 0: - print(f"单车辆观测维度: {len(obs[0])}") -``` - -### 6.3 启用详细日志 -```python -config = { - "verbose_reset": True, # 重置时详细统计 - "debug_lane_filter": True, # 车道过滤调试 -} -``` - diff --git a/Env/LOGGING_GUIDE.md b/Env/LOGGING_GUIDE.md deleted file mode 100644 index 1467e1e..0000000 --- a/Env/LOGGING_GUIDE.md +++ /dev/null @@ -1,413 +0,0 @@ -# 日志记录功能使用指南 - -## 📋 概述 - -为所有运行脚本添加了日志记录功能,可以将终端输出同时保存到文本文件,方便后续分析和问题排查。 - ---- - -## 🎯 功能特点 - -1. **双向输出**:同时输出到终端和文件,不影响实时查看 -2. **自动管理**:使用上下文管理器,自动处理文件开启/关闭 -3. **灵活配置**:支持自定义文件名和日志目录 -4. **时间戳命名**:默认使用时间戳生成唯一文件名 -5. **无缝集成**:只需添加命令行参数,无需修改代码 - ---- - -## 🚀 快速使用 - -### 1. 基础用法 - -```bash -# 不启用日志(默认) -python Env/run_multiagent_env.py - -# 启用日志记录 -python Env/run_multiagent_env.py --log - -# 或使用短选项 -python Env/run_multiagent_env.py -l -``` - -### 2. 自定义文件名 - -```bash -# 使用自定义日志文件名 -python Env/run_multiagent_env.py --log --log-file=my_test.log - -# 测试脚本也支持 -python Env/test_lane_filter.py --log --log-file=test_results.log -``` - -### 3. 组合使用调试和日志 - -```bash -# 测试脚本:调试模式 + 日志记录 -python Env/test_lane_filter.py --debug --log - -# 会生成类似:test_debug_20251021_123456.log -``` - ---- - -## 📁 日志文件位置 - -默认日志目录:`Env/logs/` - -### 文件命名规则 - -| 脚本 | 默认文件名格式 | 示例 | -|------|---------------|------| -| `run_multiagent_env.py` | `run_YYYYMMDD_HHMMSS.log` | `run_20251021_143022.log` | -| `run_multiagent_env_fast.py` | `run_fast.log` | `run_fast.log` | -| `test_lane_filter.py` | `test_{mode}_YYYYMMDD_HHMMSS.log` | `test_debug_20251021_143500.log` | - -**说明**: -- `YYYYMMDD_HHMMSS` 是时间戳(年月日_时分秒) -- `{mode}` 是测试模式(`standard` 或 `debug`) - ---- - -## 📝 所有支持的脚本 - -### 1. run_multiagent_env.py(标准运行脚本) - -```bash -# 不启用日志 -python Env/run_multiagent_env.py - -# 启用日志(自动生成时间戳文件名) -python Env/run_multiagent_env.py --log - -# 自定义文件名 -python Env/run_multiagent_env.py --log --log-file=run_test1.log -``` - -**日志位置**:`Env/logs/run_YYYYMMDD_HHMMSS.log` - ---- - -### 2. run_multiagent_env_fast.py(高性能版本) - -```bash -# 启用日志 -python Env/run_multiagent_env_fast.py --log - -# 自定义文件名 -python Env/run_multiagent_env_fast.py --log --log-file=fast_test.log -``` - -**日志位置**:`Env/logs/run_fast.log`(默认) - ---- - -### 3. test_lane_filter.py(测试脚本) - -```bash -# 标准测试 + 日志 -python Env/test_lane_filter.py --log - -# 调试测试 + 日志 -python Env/test_lane_filter.py --debug --log - -# 自定义文件名 -python Env/test_lane_filter.py --log --log-file=my_test.log - -# 组合使用 -python Env/test_lane_filter.py --debug --log --log-file=debug_run.log -``` - -**日志位置**: -- 标准模式:`Env/logs/test_standard_YYYYMMDD_HHMMSS.log` -- 调试模式:`Env/logs/test_debug_YYYYMMDD_HHMMSS.log` - ---- - -## 💻 编程接口 - -如果您想在代码中直接使用日志功能: - -```python -from logger_utils import setup_logger - -# 方式1:使用上下文管理器(推荐) -with setup_logger(log_file="my_log.log", log_dir="logs"): - print("这条消息会同时输出到终端和文件") - # 运行您的代码 - # ... - -# 方式2:手动管理 -from logger_utils import LoggerContext - -logger = LoggerContext(log_file="custom.log", log_dir="output") -logger.__enter__() # 开启日志 -print("输出消息") -logger.__exit__(None, None, None) # 关闭日志 -``` - ---- - -## 📊 日志内容示例 - -### 标准运行 - -``` -📝 日志记录已启用 -📁 日志文件: Env/logs/run_20251021_143022.log ------------------------------------------------------------- -💡 提示: 使用 --log 或 -l 参数启用日志记录 - 示例: python run_multiagent_env.py --log - 自定义文件名: python run_multiagent_env.py --log --log-file=my_run.log ------------------------------------------------------------- -[INFO] Environment: MultiAgentScenarioEnv -[INFO] MetaDrive version: 0.4.3 -... ------------------------------------------------------------- -✅ 日志已保存到: Env/logs/run_20251021_143022.log -``` - -### 调试模式 - -``` -📝 日志记录已启用 -📁 日志文件: Env/logs/test_debug_20251021_143500.log ------------------------------------------------------------- -🐛 调试模式启用 -============================================================ - -📍 场景信息统计: - - 总车道数: 123 - - 有红绿灯的车道数: 0 - ⚠️ 场景中没有红绿灯! - -🔍 开始车道过滤: 共 51 辆车待检测 -... ------------------------------------------------------------- -✅ 日志已保存到: Env/logs/test_debug_20251021_143500.log -``` - ---- - -## 🔧 高级配置 - -### 自定义日志目录 - -```python -from logger_utils import setup_logger - -# 指定不同的日志目录 -with setup_logger(log_file="test.log", log_dir="my_logs"): - print("日志会保存到 my_logs/test.log") -``` - -### 追加模式 - -```python -from logger_utils import setup_logger - -# 追加到现有文件(而不是覆盖) -with setup_logger(log_file="test.log", mode='a'): # mode='a' 表示追加 - print("这条消息会追加到文件末尾") -``` - -### 只重定向特定输出 - -```python -from logger_utils import LoggerContext - -# 只重定向stdout,不重定向stderr -logger = LoggerContext( - log_file="test.log", - redirect_stdout=True, # 重定向标准输出 - redirect_stderr=False # 不重定向错误输出 -) -``` - ---- - -## 📋 命令行参数总结 - -| 参数 | 短选项 | 说明 | 示例 | -|------|--------|------|------| -| `--log` | `-l` | 启用日志记录 | `--log` | -| `--log-file=NAME` | 无 | 指定日志文件名 | `--log-file=test.log` | -| `--debug` | `-d` | 启用调试模式(test_lane_filter.py) | `--debug` | - -### 参数组合 - -```bash -# 示例1:标准模式 + 日志 -python Env/test_lane_filter.py --log - -# 示例2:调试模式 + 日志 -python Env/test_lane_filter.py --debug --log - -# 示例3:调试 + 自定义文件名 -python Env/test_lane_filter.py -d --log --log-file=my_debug.log - -# 示例4:所有参数 -python Env/test_lane_filter.py --debug --log --log-file=full_test.log -``` - ---- - -## 🛠️ 常见问题 - -### Q1: 日志文件在哪里? - -**A**: 默认在 `Env/logs/` 目录下。如果目录不存在,会自动创建。 - -```bash -# 查看所有日志文件 -ls -lh Env/logs/ - -# 查看最新的日志 -ls -lt Env/logs/ | head -5 -``` - ---- - -### Q2: 如何查看日志内容? - -**A**: 使用任何文本编辑器或命令行工具: - -```bash -# 方式1:使用cat -cat Env/logs/run_20251021_143022.log - -# 方式2:使用less(可翻页) -less Env/logs/run_20251021_143022.log - -# 方式3:查看末尾内容 -tail -n 50 Env/logs/run_20251021_143022.log - -# 方式4:实时监控(适合长时间运行) -tail -f Env/logs/run_20251021_143022.log -``` - ---- - -### Q3: 日志文件太多怎么办? - -**A**: 可以定期清理旧日志: - -```bash -# 删除7天前的日志 -find Env/logs/ -name "*.log" -mtime +7 -delete - -# 只保留最新的10个日志 -cd Env/logs && ls -t *.log | tail -n +11 | xargs rm -f -``` - ---- - -### Q4: 日志会影响性能吗? - -**A**: 影响很小,因为: -1. 文件I/O是异步的 -2. 使用了缓冲区 -3. 立即刷新确保数据不丢失 - -如果追求极致性能,建议训练时不启用日志,只在需要分析时启用。 - ---- - -### Q5: 可以同时记录多个脚本的日志吗? - -**A**: 可以,每个脚本使用不同的日志文件: - -```bash -# 终端1 -python Env/run_multiagent_env.py --log --log-file=script1.log - -# 终端2(同时运行) -python Env/test_lane_filter.py --log --log-file=script2.log -``` - ---- - -## 💡 最佳实践 - -### 1. 开发阶段 - -```bash -# 使用调试模式 + 日志,方便排查问题 -python Env/test_lane_filter.py --debug --log -``` - -### 2. 长时间运行 - -```bash -# 启用日志,避免输出丢失 -nohup python Env/run_multiagent_env.py --log > /dev/null 2>&1 & - -# 查看实时输出 -tail -f Env/logs/run_*.log -``` - -### 3. 批量实验 - -```bash -# 为每次实验使用不同的日志文件 -for i in {1..5}; do - python Env/run_multiagent_env.py --log --log-file=exp_${i}.log -done -``` - -### 4. 性能测试 - -```bash -# 不启用日志,获得最佳性能 -python Env/run_multiagent_env_fast.py -``` - ---- - -## 📖 相关文档 - -- `README.md` - 项目总览 -- `DEBUG_GUIDE.md` - 调试功能使用指南 -- `CHANGELOG.md` - 更新日志 - ---- - -## 🔍 技术细节 - -### 实现原理 - -1. **TeeLogger类**:实现同时写入终端和文件 -2. **上下文管理器**:自动管理资源(文件打开/关闭) -3. **sys.stdout重定向**:拦截所有print输出 -4. **即时刷新**:每次写入后立即刷新,确保数据不丢失 - -### 源代码 - -详见 `Env/logger_utils.py` - -```python -# 简化示例 -class TeeLogger: - def write(self, message): - self.terminal.write(message) # 输出到终端 - self.log_file.write(message) # 写入文件 - self.log_file.flush() # 立即刷新 -``` - ---- - -## ✅ 总结 - -- ✅ 简单易用:只需添加 `--log` 参数 -- ✅ 不影响输出:终端仍可实时查看 -- ✅ 自动管理:文件自动开启/关闭 -- ✅ 灵活配置:支持自定义文件名和目录 -- ✅ 完整记录:包含所有调试信息 - -立即开始使用: - -```bash -python Env/test_lane_filter.py --debug --log -``` - diff --git a/Env/__pycache__/expert_replay_env.cpython-313.pyc b/Env/__pycache__/expert_replay_env.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5feb9b13eefc149f70fb899287dcd0bcba42634f GIT binary patch literal 19967 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 literal 0 HcmV?d00001 diff --git a/Env/__pycache__/replay_policy.cpython-39.pyc b/Env/__pycache__/replay_policy.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d3ee42aedf3f9472cf613f1f6779a0eda58b8b30 GIT binary patch literal 1986 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 1: + total_displacement = np.linalg.norm(positions[-1] - positions[0]) + max_speed = np.max(np.linalg.norm(velocities, axis=1)) + + is_static = False + if total_displacement < 5.0 and max_speed < 1.0: # Relaxed threshold: <5m move and <1m/s + is_static = True + + # Decision logic: + # - If off-road AND static: Skip completely (don't even spawn as background) + # - If off-road but moving: Maybe keep? Or skip? Usually off-road moving is weird, skip. + # - If on-road but static: Spawn as BACKGROUND (visible but not controlled agent) + # - If on-road and moving: Spawn as CONTROLLED agent + + if not is_valid_track: + # Skip off-road vehicles entirely (both static and moving off-road) + continue + + if is_static: + # Add to background list, but NOT to car_birth_info_list (which is for controlled agents) + # We need a way to spawn them. Let's add a separate list. + self.background_vehicles[scenario_id] = { + 'id': track['metadata']['object_id'], + 'show_time': first_show, + 'begin': (track['state']['position'][first_show, 0], track['state']['position'][first_show, 1]), + 'heading': track['state']['heading'][first_show], + 'end': (track['state']['position'][last_show, 0], track['state']['position'][last_show, 1]), + 'scenario_id': scenario_id, + 'length': track['state']['length'][first_show], + 'width': track['state']['width'][first_show], + 'valid': valid # Need validity to know when to show/hide + } + continue # Do not add to controlled list + + # Store the full track for replay (only for controlled agents) + self.expert_tracks[scenario_id] = track + + self.car_birth_info_list.append({ + 'id': track['metadata']['object_id'], + 'show_time': first_show, + 'begin': (track['state']['position'][first_show, 0], track['state']['position'][first_show, 1]), + 'heading': track['state']['heading'][first_show], + 'end': (track['state']['position'][last_show, 0], track['state']['position'][last_show, 1]), + 'scenario_id': scenario_id, # Keep track of original ID to lookup tracks + 'length': track['state']['length'][first_show], + 'width': track['state']['width'][first_show] + }) + + for scenario_id in _obj_to_clean_this_frame: + self.engine.traffic_manager.current_traffic_data.pop(scenario_id) + # --- MODIFIED SECTION END --- + + self.engine.reset() + self.reset_sensors() + self.engine.taskMgr.step() + + self.lanes = self.engine.map_manager.current_map.road_network.graph + + if self.top_down_renderer is not None: + self.top_down_renderer.clear() + self.engine.top_down_renderer = None + + self.dones = {} + self.episode_rewards = defaultdict(float) + self.episode_lengths = defaultdict(int) + + self.controlled_agents.clear() + self.controlled_agent_ids.clear() + + # We skip calling super().reset() to avoid double reset + # But we need to ensure ScenarioEnv-specific setup is done if any. + # ScenarioEnv.reset() basically does engine.reset() and some cleanup. + # We covered most of it. + + self._spawn_controlled_agents() + self._spawn_background_vehicles() # Initial spawn for background + + # Ensure SDC/ego is moved to the correct initial expert state. + if self.replay_sdc: + self.sdc_vehicle = self.engine.agent_manager.active_agents.get(self.sdc_agent_id, None) + if self.sdc_vehicle is not None and self.sdc_track is not None: + valid = self.sdc_track["state"]["valid"] + t0 = int(np.argmax(valid)) if valid.any() else 0 + pos0 = self.sdc_track["state"]["position"][t0] + heading0 = self.sdc_track["state"]["heading"][t0] + vel0 = self.sdc_track["state"]["velocity"][t0] + self.sdc_vehicle.set_position(pos0) + self.sdc_vehicle.set_heading_theta(heading0) + self.sdc_vehicle.set_velocity(vel0) + + return self._get_all_obs() + + def _spawn_background_vehicles(self): + # Spawn static/background vehicles + # Since they are static, we might just spawn them once if their show_time is 0 + # But Waymo tracks have valid bits, they might appear/disappear. + # For optimization, if they are truly static (never move), we just spawn them when show_time matches. + + # We need to track spawned background vehicles to remove them if they become invalid? + # Since we defined them as "static", they probably stay put. + # But validity might change (e.g. late spawn). + + # For simplicity in this step, let's just iterate and spawn if time matches + for sid, car in self.background_vehicles.items(): + if car['show_time'] == self.round: + # Spawn as a Traffic Vehicle (not PolicyVehicle), or just a static object? + # Using DefaultVehicle is fine, but don't add to controlled_agents + + # Check duplication + bg_id = f"bg_{car['id']}" + # if bg_id in self.engine.obj_to_id: # obj_to_id might not be available in all versions + if bg_id in self.engine.agent_manager.active_agents: + continue + + vehicle_config = {} + if 'length' in car and 'width' in car: + vehicle_config = { + "length": car['length'], + "width": car['width'] + } + + v = self.engine.spawn_object( + DefaultVehicle, + name=bg_id, + vehicle_config=vehicle_config, + position=car['begin'], + heading=car['heading'] + ) + + # Set color to grey/dark to indicate background + v.set_velocity([0, 0]) + # Maybe set color? MetaDrive vehicles random color. + # v.set_color(...) if supported + + # Register as an active object but NOT controlled agent + # The engine manages it. + # CRITICAL: We need it in self.engine.agent_manager.active_agents for Observation? + # If we want it to be seen by Lidar/Observation, it needs to be an "agent" or "traffic". + # DefaultVehicle spawned this way is just an object. + # We should add it to traffic manager? Or just leave it as object? + # MultiAgentScenarioEnv._get_all_obs iterates self.engine.agent_manager.active_agents + + # If we want it in observation, we must add it to active_agents OR iterate over all objects. + # Adding to active_agents is easier for compatibility. + self.engine.agent_manager.active_agents[bg_id] = v + + # Store valid mask to remove it later if needed? + v.valid_mask = car['valid'] + v.start_t = car['show_time'] + + def _update_background_vehicles(self): + # Remove background vehicles if they become invalid + # Or spawn new ones + self._spawn_background_vehicles() + + # Check validity for existing + to_remove = [] + for aid, v in self.engine.agent_manager.active_agents.items(): + if aid.startswith("bg_"): + # Check validity + if hasattr(v, 'valid_mask'): + curr_step = self.round + if curr_step >= len(v.valid_mask) or not v.valid_mask[curr_step]: + to_remove.append(aid) + + for aid in to_remove: + self.engine.agent_manager.active_agents.pop(aid, None) + # if aid in self.engine.obj_to_id: + # self.engine.clear_objects([self.engine.obj_to_id[aid]]) + # Instead, we should find the object by ID and clear it. + # Since we don't track obj directly, we can't easily clear it without obj ref. + # Wait, active_agents stores the vehicle object. + # So we can just clear that object. + pass + + # Re-iterate to clear objects properly + for aid in to_remove: + # We need to find the vehicle object to clear it. + # But we popped it from active_agents. + # Wait, we should get it before pop. + pass + + def _update_background_vehicles(self): + # Remove background vehicles if they become invalid + # Or spawn new ones + self._spawn_background_vehicles() + + # Check validity for existing + to_remove = [] + objects_to_clear = [] + + for aid, v in self.engine.agent_manager.active_agents.items(): + if aid.startswith("bg_"): + # Check validity + if hasattr(v, 'valid_mask'): + curr_step = self.round + if curr_step >= len(v.valid_mask) or not v.valid_mask[curr_step]: + to_remove.append(aid) + objects_to_clear.append(v) + + for aid in to_remove: + self.engine.agent_manager.active_agents.pop(aid, None) + + if objects_to_clear: + self.engine.clear_objects(objects_to_clear) + + def _spawn_controlled_agents(self): + for car in self.car_birth_info_list: + if car['show_time'] == self.round: + agent_id = f"controlled_{car['id']}" + + # Check if we already have this agent (shouldn't happen with unique IDs but safety check) + if agent_id in self.controlled_agents: + continue + + # Handling ID flickering / merging + # If this ID is new, check if there's an existing agent very close to its start position + # that just disappeared? (Not implemented here, complex logic) + # But we can check if there's an overlap with existing agents? + # For now, just spawn. + + # Read vehicle type/size if available + vehicle_config = {} + if 'length' in car and 'width' in car: + vehicle_config = { + "length": car['length'], + "width": car['width'] + } + + vehicle = self.engine.spawn_object( + PolicyVehicle, + name=agent_id, + vehicle_config=vehicle_config, + position=car['begin'], + heading=car['heading'] + ) + vehicle.reset(position=car['begin'], heading=car['heading']) + + # We don't set policy or destination in the same way, or maybe we do for compatibility + vehicle.set_destination(car['end']) + + # Store extra info for replay + vehicle.expert_track = self.expert_tracks[car['scenario_id']] + vehicle.original_id = car['id'] + + self.controlled_agents[agent_id] = vehicle + self.controlled_agent_ids.append(agent_id) + self.engine.agent_manager.active_agents[agent_id] = vehicle + + def step(self, action_dict=None): + # We ignore input action_dict for the purpose of controlling agents + # Instead, we calculate what the action *should* be + + self.round += 1 + expert_actions = {} + + # 1. Update state of all controlled agents to the current timestep (self.round) + # and compute action from (self.round-1) to (self.round). + # Wait, usually step() moves T -> T+1. + # Current state is T. We want to move to T+1. + # So we need state at T and T+1. + + # Identify agents that are done (valid=0 at T+1 or T+1 >= length) + agents_to_remove = [] + + # Update SDC/ego first (otherwise it will stay still with default action=0) + if self.replay_sdc and self.sdc_vehicle is not None and self.sdc_track is not None: + next_step = self.round + curr_step = self.round - 1 + if next_step < len(self.sdc_track["state"]["position"]) and self.sdc_track["state"]["valid"][next_step]: + curr_state = { + "position": self.sdc_track["state"]["position"][curr_step], + "heading": self.sdc_track["state"]["heading"][curr_step], + "velocity": self.sdc_track["state"]["velocity"][curr_step], + } + next_state = { + "position": self.sdc_track["state"]["position"][next_step], + "heading": self.sdc_track["state"]["heading"][next_step], + "velocity": self.sdc_track["state"]["velocity"][next_step], + } + action, _ = self.inverse_dynamics.compute_action(curr_state, next_state, dt=0.1) + expert_actions[self.sdc_agent_id] = action + self.sdc_vehicle.set_position(next_state["position"]) + self.sdc_vehicle.set_heading_theta(next_state["heading"]) + self.sdc_vehicle.set_velocity(next_state["velocity"]) + self.sdc_vehicle.last_expert_action = action + + for agent_id, vehicle in self.controlled_agents.items(): + track = vehicle.expert_track + # current_step = self.round - 1 # Since we incremented at start + # But vehicle is currently at state corresponding to self.round - 1. + # We want to move it to self.round. + + # Check bounds + next_step = self.round + curr_step = self.round - 1 + + if next_step >= len(track['state']['position']): + agents_to_remove.append(agent_id) + continue + + valid = track['state']['valid'][next_step] + if not valid: + agents_to_remove.append(agent_id) + continue + + # Get states + curr_pos = track['state']['position'][curr_step] + next_pos = track['state']['position'][next_step] + curr_heading = track['state']['heading'][curr_step] + next_heading = track['state']['heading'][next_step] + curr_vel = track['state']['velocity'][curr_step] + next_vel = track['state']['velocity'][next_step] + + # Prepare state dicts for Inverse Dynamics + curr_state = { + 'position': curr_pos, + 'heading': curr_heading, + 'velocity': curr_vel + } + next_state = { + 'position': next_pos, + 'heading': next_heading, + 'velocity': next_vel + } + + # Calculate action + action, raw_info = self.inverse_dynamics.compute_action(curr_state, next_state, dt=0.1) # Waymo is 10Hz? + expert_actions[agent_id] = action + + # Force update vehicle state + vehicle.set_position(next_pos) + vehicle.set_heading_theta(next_heading) + vehicle.set_velocity(next_vel) + + # Also record this action in the vehicle for later retrieval if needed + vehicle.last_expert_action = action + + # Remove finished agents + for agent_id in agents_to_remove: + vehicle = self.controlled_agents[agent_id] + self.controlled_agents.pop(agent_id) + self.controlled_agent_ids.remove(agent_id) + self.engine.agent_manager.active_agents.pop(agent_id, None) + + self.engine.clear_objects([vehicle.id]) + + # Step physics world to update sensors/collision detection + # We don't need full integration, but we need to update the physics world state + self.engine.taskMgr.step() + + # Spawn new agents for this turn + self._spawn_controlled_agents() + self._update_background_vehicles() + + # Get observations + obs = self._get_all_obs() + + rewards = {aid: 0.0 for aid in self.controlled_agents} + dones = {aid: False for aid in self.controlled_agents} + dones["__all__"] = (self.round >= self.config["horizon"]) or (len(self.controlled_agents) == 0 and self.round > 190) # Waymo scenarios are usually ~198 steps (20s @ 10Hz) or 90 steps (9s) + + infos = {aid: {"expert_action": expert_actions.get(aid, np.zeros(2))} for aid in self.controlled_agents} + + return obs, rewards, dones, infos + + def _get_all_obs(self): + # Implement custom observation: 30m range, 10 nearest vehicles + obs_dict = {} + + for agent_id, vehicle in self.controlled_agents.items(): + # 1. Ego State + ego_state = [ + vehicle.position[0], vehicle.position[1], + vehicle.velocity[0], vehicle.velocity[1], + vehicle.heading_theta + ] + + # 2. Neighbors + neighbors = [] + # Iterate through all vehicles in the engine + candidates = [] + for other_id, other_vehicle in self.engine.agent_manager.active_agents.items(): + if other_id == agent_id: + continue + + dist = np.linalg.norm(vehicle.position - other_vehicle.position) + if dist < 30.0: + candidates.append((dist, other_vehicle)) + + # Sort by distance + candidates.sort(key=lambda x: x[0]) + + # Take top 10 + top_10 = candidates[:10] + + neighbor_feats = [] + for _, neighbor in top_10: + neighbor_feats.extend([ + neighbor.position[0] - vehicle.position[0], # Relative pos + neighbor.position[1] - vehicle.position[1], + neighbor.velocity[0], # Absolute vel? or Relative? Usually relative in MultiAgent + neighbor.velocity[1] + ]) + + # Pad if < 10 + missing = 10 - len(top_10) + if missing > 0: + neighbor_feats.extend([0.0] * (4 * missing)) + + # Flatten + obs = np.array(ego_state + neighbor_feats, dtype=np.float32) + obs_dict[agent_id] = obs + + return obs_dict diff --git a/Env/inverse_dynamics.py b/Env/inverse_dynamics.py new file mode 100644 index 0000000..fed10d8 --- /dev/null +++ b/Env/inverse_dynamics.py @@ -0,0 +1,65 @@ +import numpy as np +import math + +class InverseDynamics: + def __init__(self, max_steering=0.7, max_acc=15.0, length=4.5): + """ + :param max_steering: Max steering angle in radians (approx 40 degrees) + :param max_acc: Max acceleration in m/s^2 + :param length: Vehicle length in meters (Waymo default approx 4.5m) + """ + self.max_steering = max_steering + self.max_acc = max_acc + self.wheelbase = 0.7 * length # Approximation as per request + + def compute_action(self, current_state, next_state, dt=0.1): + """ + Compute action [steering, acceleration] from current and next state. + State format: dictionary or object with keys/attrs: position (x, y), heading, velocity (v_x, v_y) + or numpy array [x, y, vx, vy, heading] + + Using Bicycle Model: + delta = arctan(L * theta_dot / v) + acc = (v_next - v_curr) / dt + """ + + # Extract state + # Assume state is dict-like for now, can adapt if needed + # We need: velocity (scalar), heading + + # Helper to get speed + def get_speed(vel): + return np.linalg.norm(vel) + + v_curr = get_speed(current_state['velocity']) + v_next = get_speed(next_state['velocity']) + + # 1. Acceleration (longitudinal) + acc = (v_next - v_curr) / dt + + # 2. Steering (lateral) + # theta_dot = (theta_next - theta_curr) / dt + theta_curr = current_state['heading'] + theta_next = next_state['heading'] + + # Handle angle wrapping [-pi, pi] + diff_theta = theta_next - theta_curr + if diff_theta > np.pi: + diff_theta -= 2 * np.pi + elif diff_theta < -np.pi: + diff_theta += 2 * np.pi + + theta_dot = diff_theta / dt + + # Avoid division by zero for stationary vehicles + if v_curr < 0.1: + steering = 0.0 + else: + # delta = arctan(L * theta_dot / v) + steering = np.arctan(self.wheelbase * theta_dot / v_curr) + + # Normalize actions to [-1, 1] + norm_acc = np.clip(acc / self.max_acc, -1.0, 1.0) + norm_steering = np.clip(steering / self.max_steering, -1.0, 1.0) + + return np.array([norm_steering, norm_acc]), {'raw_acc': acc, 'raw_steering': steering} diff --git a/Env/logger_utils.py b/Env/logger_utils.py deleted file mode 100644 index fab61f8..0000000 --- a/Env/logger_utils.py +++ /dev/null @@ -1,170 +0,0 @@ -""" -日志工具模块 -提供将终端输出同时保存到文件的功能 -""" -import sys -import os -from datetime import datetime - - -class TeeLogger: - """ - 双向输出类:同时输出到终端和文件 - """ - def __init__(self, filename, mode='w', terminal=None): - """ - Args: - filename: 日志文件路径 - mode: 文件打开模式 ('w'=覆盖, 'a'=追加) - terminal: 原始输出流(通常是sys.stdout或sys.stderr) - """ - self.terminal = terminal or sys.stdout - self.log_file = open(filename, mode, encoding='utf-8') - - def write(self, message): - """写入消息到终端和文件""" - self.terminal.write(message) - self.log_file.write(message) - self.log_file.flush() # 立即写入磁盘 - - def flush(self): - """刷新缓冲区""" - self.terminal.flush() - self.log_file.flush() - - def close(self): - """关闭日志文件""" - if self.log_file: - self.log_file.close() - - -class LoggerContext: - """ - 日志上下文管理器 - 使用with语句自动管理日志的开启和关闭 - """ - def __init__(self, log_file=None, log_dir="logs", mode='w', - redirect_stdout=True, redirect_stderr=True): - """ - Args: - log_file: 日志文件名(None则自动生成时间戳文件名) - log_dir: 日志目录 - mode: 文件打开模式 ('w'=覆盖, 'a'=追加) - redirect_stdout: 是否重定向标准输出 - redirect_stderr: 是否重定向标准错误 - """ - self.log_dir = log_dir - self.mode = mode - self.redirect_stdout = redirect_stdout - self.redirect_stderr = redirect_stderr - - # 创建日志目录 - os.makedirs(log_dir, exist_ok=True) - - # 生成日志文件名 - if log_file is None: - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - log_file = f"run_{timestamp}.log" - - self.log_path = os.path.join(log_dir, log_file) - - # 保存原始的stdout和stderr - self.original_stdout = sys.stdout - self.original_stderr = sys.stderr - - # 日志对象 - self.stdout_logger = None - self.stderr_logger = None - - def __enter__(self): - """进入上下文:开启日志""" - print(f"📝 日志记录已启用") - print(f"📁 日志文件: {self.log_path}") - print("-" * 60) - - # 创建TeeLogger对象 - if self.redirect_stdout: - self.stdout_logger = TeeLogger( - self.log_path, - mode=self.mode, - terminal=self.original_stdout - ) - sys.stdout = self.stdout_logger - - if self.redirect_stderr: - self.stderr_logger = TeeLogger( - self.log_path, - mode='a', # stderr总是追加模式 - terminal=self.original_stderr - ) - sys.stderr = self.stderr_logger - - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - """退出上下文:关闭日志""" - # 恢复原始输出 - sys.stdout = self.original_stdout - sys.stderr = self.original_stderr - - # 关闭日志文件 - if self.stdout_logger: - self.stdout_logger.close() - if self.stderr_logger: - self.stderr_logger.close() - - print("-" * 60) - print(f"✅ 日志已保存到: {self.log_path}") - - # 返回False表示不抑制异常 - return False - - -def setup_logger(log_file=None, log_dir="logs", mode='w'): - """ - 快速设置日志记录 - - Args: - log_file: 日志文件名(None则自动生成) - log_dir: 日志目录 - mode: 文件模式 ('w'=覆盖, 'a'=追加) - - Returns: - LoggerContext对象 - - Example: - with setup_logger("my_test.log"): - print("这条消息会同时输出到终端和文件") - """ - return LoggerContext(log_file=log_file, log_dir=log_dir, mode=mode) - - -def get_default_log_filename(prefix="run"): - """ - 生成默认的日志文件名(带时间戳) - - Args: - prefix: 文件名前缀 - - Returns: - str: 格式为 "prefix_YYYYMMDD_HHMMSS.log" - """ - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - return f"{prefix}_{timestamp}.log" - - -if __name__ == "__main__": - # 测试代码 - print("测试1: 使用默认配置") - with setup_logger(): - print("这是测试消息1") - print("这是测试消息2") - print("日志记录已结束\n") - - print("测试2: 使用自定义文件名") - with setup_logger(log_file="test_custom.log"): - print("自定义文件名测试") - for i in range(3): - print(f" 消息 {i+1}") - print("完成") - diff --git a/Env/run_multiagent_env.py b/Env/run_multiagent_env.py index 9c28a4e..28af97a 100644 --- a/Env/run_multiagent_env.py +++ b/Env/run_multiagent_env.py @@ -1,24 +1,14 @@ from scenario_env import MultiAgentScenarioEnv -from simple_idm_policy import ConstantVelocityPolicy +from Env.simple_idm_policy import ConstantVelocityPolicy from metadrive.engine.asset_loader import AssetLoader -from logger_utils import setup_logger -import sys -import os -WAYMO_DATA_DIR = r"/home/huangfukk/mdsn" +WAYMO_DATA_DIR = r"/home/huangfukk/MAGAIL4AutoDrive/data" -def main(enable_logging=False, log_file=None): - """ - 主函数 - - Args: - enable_logging: 是否启用日志记录到文件 - log_file: 日志文件名(None则自动生成时间戳文件名) - """ +def main(): env = MultiAgentScenarioEnv( config={ # "data_directory": AssetLoader.file_path(AssetLoader.asset_path, "waymo", unix_style=False), - "data_directory": AssetLoader.file_path(WAYMO_DATA_DIR, "exp_filtered", unix_style=False), + "data_directory": AssetLoader.file_path(WAYMO_DATA_DIR, "exp_converted", unix_style=False), "is_multi_agent": True, "num_controlled_agents": 3, "horizon": 300, @@ -26,21 +16,12 @@ def main(enable_logging=False, log_file=None): "sequential_seed": True, "reactive_traffic": True, "manual_control": True, - - # 车道检测与过滤配置 - "filter_offroad_vehicles": True, # 启用车道区域过滤,过滤草坪等非车道区域的车辆 - "lane_tolerance": 3.0, # 车道检测容差(米),可根据需要调整 - "max_controlled_vehicles": None, # 限制最大车辆数(可选,None表示不限制) - - # 调试配置(可选) - # "debug_lane_filter": True, # 启用车道过滤详细调试 - # "verbose_reset": True, # 启用重置详细统计 - # "inherit_expert_velocity": True, # 继承专家速度 }, agent2policy=ConstantVelocityPolicy(target_speed=50) ) - obs = env.reset(0) + obs = env.reset(0 + ) for step in range(10000): actions = { aid: env.controlled_agents[aid].policy.act() @@ -57,25 +38,4 @@ def main(enable_logging=False, log_file=None): if __name__ == "__main__": - # 解析命令行参数 - enable_logging = "--log" in sys.argv or "-l" in sys.argv - - # 提取自定义日志文件名 - log_file = None - for arg in sys.argv: - if arg.startswith("--log-file="): - log_file = arg.split("=")[1] - break - - if enable_logging: - # 使用日志记录 - log_dir = os.path.join(os.path.dirname(__file__), "logs") - with setup_logger(log_file=log_file, log_dir=log_dir): - main(enable_logging=True, log_file=log_file) - else: - # 普通运行(只输出到终端) - print("💡 提示: 使用 --log 或 -l 参数启用日志记录") - print(" 示例: python run_multiagent_env.py --log") - print(" 自定义文件名: python run_multiagent_env.py --log --log-file=my_run.log") - print("-" * 60) - main(enable_logging=False) \ No newline at end of file + main() \ No newline at end of file diff --git a/Env/scenario_env.py b/Env/scenario_env.py index b801ff6..272b6bb 100644 --- a/Env/scenario_env.py +++ b/Env/scenario_env.py @@ -1,91 +1,3 @@ -""" -多智能体场景环境 (MultiAgentScenarioEnv) - -================================== -配置参数说明 (写在最前面) -================================== - -基础配置: - data_directory (str): 专家数据目录路径 - num_controlled_agents (int): 默认可控智能体数量,默认3 - horizon (int): 每个回合的最大步数,默认1000 - -车道检测与过滤配置: - filter_offroad_vehicles (bool): 是否过滤非车道区域的车辆,默认True - - True: 过滤掉在草坪、停车场等非车道区域生成的车辆 - - False: 保留所有车辆 - lane_tolerance (float): 车道检测容差(米),默认3.0 - - 用于放宽车道检测的边界条件 - max_controlled_vehicles (int|None): 最大可控车辆数限制,默认None - - None: 不限制车辆数量 - - int: 限制最多生成的车辆数 - -场景对象配置: - no_traffic_lights (bool): 是否禁用红绿灯渲染和逻辑,默认False - - True: 完全移除场景中的红绿灯 - - False: 保留红绿灯(按数据集原样生成) - -专家数据继承配置: - inherit_expert_velocity (bool): 是否继承专家数据中车辆的初始速度,默认False - - True: 车辆生成时使用专家数据中的速度 - - False: 车辆生成时速度为0(由策略控制) - -调试模式配置: - debug_lane_filter (bool): 车道过滤详细调试输出,默认False - - 输出每个车辆位置的车道检测详细过程 - verbose_reset (bool): 重置时输出详细统计信息,默认False - - 输出场景统计、过滤详情等 - -使用示例: - env = MultiAgentScenarioEnv( - config={ - "data_directory": "path/to/data", - "max_controlled_vehicles": 10, - "inherit_expert_velocity": True, # 继承专家速度 - "no_traffic_lights": True, # 禁用红绿灯 - "verbose_reset": True, # 详细输出 - }, - agent2policy=your_policy - ) - -================================== -整体逻辑和处理流程 -================================== - -1. 初始化阶段: - - 继承MetaDrive的ScenarioEnv基类 - - 配置多智能体参数(车辆数量、调试模式等) - - 接收策略映射字典(agent2policy) - -2. 环境重置阶段 (reset方法): - - 解析专家数据,提取车辆生成信息(car_birth_info_list) - - 清理原始交通数据,只保留车辆位置、朝向、目的地、速度(可选) - - 禁用红绿灯(如果配置) - - 初始化地图和车道信息 - - 执行车道过滤(_filter_valid_spawn_positions),移除非车道区域的车辆 - - 限制最大车辆数量 - - 生成可控智能体(_spawn_controlled_agents) - -3. 观测获取阶段 (_get_all_obs方法): - - 遍历所有可控车辆 - - 获取车辆状态(位置、速度、朝向) - - 检测红绿灯状态(_get_traffic_light_state) - - 获取激光雷达数据(前向、侧向、车道线检测) - - 组装完整观测向量 - -4. 环境步进阶段 (step方法): - - 执行所有智能体的动作 - - 更新物理引擎状态 - - 生成新的智能体(按时间步) - - 返回新的观测、奖励、完成状态 - -核心功能模块: -- PolicyVehicle: 可控制策略的车辆类 -- 车道检测与过滤: 确保车辆只在有效车道上生成 -- 多智能体管理: 动态生成和管理可控车辆 -- 专家速度继承: 可选地继承专家数据中的初始速度 -""" - import numpy as np from metadrive.component.navigation_module.node_network_navigation import NodeNetworkNavigation from metadrive.envs.scenario_env import ScenarioEnv @@ -99,693 +11,194 @@ from metadrive.type import MetaDriveType class PolicyVehicle(DefaultVehicle): - """ - 可控制策略的车辆类 - - 继承自MetaDrive的DefaultVehicle,增加了策略控制和目标设置功能。 - 用于多智能体环境中的可控车辆,支持自定义策略和目的地。 - """ - def __init__(self, *args, **kwargs): - """ - 初始化策略车辆 - - Args: - *args: 传递给父类的位置参数 - **kwargs: 传递给父类的关键字参数 - """ super().__init__(*args, **kwargs) - self.policy = None # 车辆的控制策略 - self.destination = None # 车辆的目标目的地 + self.policy = None + self.destination = None def set_policy(self, policy): - """ - 设置车辆的控制策略 - - Args: - policy: 控制策略对象,必须实现act(observation)方法 - """ self.policy = policy def set_destination(self, des): - """ - 设置车辆的目标目的地 - - Args: - des: 目标位置坐标 (x, y) - """ self.destination = des def act(self, observation, policy=None): - """ - 根据观测获取动作 - - Args: - observation: 环境观测数据 - policy: 可选的外部策略,如果提供则优先使用 - - Returns: - 动作向量,如果无策略则返回随机动作 - """ if self.policy is not None: return self.policy.act(observation) else: return self.action_space.sample() def before_step(self, action): - """ - 执行动作前的状态记录 - - 在每步执行前记录当前状态,用于后续的状态追踪和分析。 - - Args: - action: 即将执行的动作 - """ - self.last_position = self.position # 记录当前位置 (2D向量) - self.last_velocity = self.velocity # 记录当前速度 (2D向量) - self.last_speed = self.speed # 记录当前速度大小 (标量) - self.last_heading_dir = self.heading # 记录当前朝向 + self.last_position = self.position # 2D vector + self.last_velocity = self.velocity # 2D vector + self.last_speed = self.speed # Scalar + self.last_heading_dir = self.heading if action is not None: - self.last_current_action.append(action) # 记录动作历史 - self._set_action(action) # 设置动作到车辆 + self.last_current_action.append(action) + self._set_action(action) def is_done(self): - """ - 检查车辆是否完成任务 - - 目前为空实现,可根据需要添加到达目的地或碰撞检测逻辑 - - Returns: - bool: True表示任务完成,False表示继续执行 - """ # arrive or crash pass -# 将PolicyVehicle注册为默认车辆类型 vehicle_class_to_type[PolicyVehicle] = "default" class MultiAgentScenarioEnv(ScenarioEnv): - """ - 多智能体场景环境 - - 基于MetaDrive的ScenarioEnv扩展,支持多智能体强化学习训练。 - 主要功能包括: - 1. 从专家数据中提取车辆信息并生成可控智能体 - 2. 车道检测与过滤,确保车辆在有效区域生成 - 3. 红绿灯状态检测,为智能体提供交通信号信息 - 4. 多智能体观测、动作和奖励管理 - """ - @classmethod def default_config(cls): - """ - 获取环境的默认配置 - - 继承父类配置并添加多智能体相关的配置参数 - - 配置参数说明: - - data_directory: 专家数据目录路径 - - num_controlled_agents: 默认可控智能体数量 - - horizon: 每个回合的最大步数 - - 车道检测与过滤配置: - - filter_offroad_vehicles: 是否过滤非车道区域的车辆 - - lane_tolerance: 车道检测容差(米) - - max_controlled_vehicles: 最大可控车辆数限制(None表示不限制) - - 场景对象配置: - - no_traffic_lights: 禁用红绿灯渲染和逻辑 - - 专家数据继承配置: - - inherit_expert_velocity: 是否继承专家数据中车辆的初始速度(默认False) - - 调试模式配置: - - debug_traffic_light: 红绿灯检测详细调试输出 - - debug_lane_filter: 车道过滤详细调试输出 - - verbose_reset: 重置时输出详细统计信息 - - Returns: - dict: 包含所有配置参数的字典 - """ config = super().default_config() config.update(dict( - # 基础配置 data_directory=None, num_controlled_agents=3, horizon=1000, - - # 车道检测与过滤配置 - filter_offroad_vehicles=True, - lane_tolerance=3.0, - max_controlled_vehicles=None, - - # 场景对象配置 - no_traffic_lights=False, - - # 专家数据继承配置 - inherit_expert_velocity=False, - - # 调试模式配置 - debug_lane_filter=False, - verbose_reset=False, )) return config def __init__(self, config, agent2policy): - """ - 初始化多智能体场景环境 - - Args: - config: 环境配置字典,包含各种参数设置 - agent2policy: 智能体ID到策略的映射字典 - """ - self.policy = agent2policy # 智能体策略映射 - self.controlled_agents = {} # 可控智能体字典 {agent_id: vehicle} - self.controlled_agent_ids = [] # 可控智能体ID列表 - self.obs_list = [] # 观测数据列表 - self.round = 0 # 当前时间步 - - # 调试模式配置 - self.debug_lane_filter = config.get("debug_lane_filter", False) - - # 调用父类初始化 + self.policy = agent2policy + self.controlled_agents = {} + self.controlled_agent_ids = [] + self.obs_list = [] + self.round = 0 super().__init__(config) def reset(self, seed: Union[None, int] = None): - """ - 重置环境到初始状态 - - 这是环境的核心重置方法,执行以下步骤: - 1. 初始化日志系统 - 2. 解析专家数据,提取车辆生成信息 - 3. 清理原始交通数据 - 4. 初始化地图和车道信息 - 5. 执行车道过滤和车辆数量限制 - 6. 生成可控智能体 - 7. 返回初始观测 - - Args: - seed: 随机种子,用于环境重置时的随机性控制 - - Returns: - list: 所有智能体的初始观测数据 - """ - self.round = 0 # 重置时间步计数器 - - # 初始化日志系统 + self.round = 0 if self.logger is None: self.logger = get_logger() log_level = self.config.get("log_level", logging.DEBUG if self.config.get("debug", False) else logging.INFO) set_log_level(log_level) - # 延迟初始化MetaDrive引擎 self.lazy_init() self._reset_global_seed(seed) if self.engine is None: raise ValueError("Broken MetaDrive instance.") - - # 在场景加载前禁用红绿灯(如果配置中启用了no_traffic_lights选项) - if self.config.get("no_traffic_lights", False): - # 重写红绿灯管理器的方法,阻止创建和使用红绿灯 - if hasattr(self.engine, 'light_manager') and self.engine.light_manager is not None: - self.engine.light_manager.before_reset = lambda *args, **kwargs: None - self.engine.light_manager.after_reset = lambda *args, **kwargs: None - self.engine.light_manager.before_step = lambda *args, **kwargs: None - self.engine.light_manager.get_traffic_light = lambda *args, **kwargs: None - self.logger.info("已禁用红绿灯管理器") - # 步骤1:解析专家数据,提取车辆生成信息 # 记录专家数据中每辆车的位置,接着全部清除,只保留位置等信息,用于后续生成 - _obj_to_clean_this_frame = [] # 需要清理的对象ID列表 - self.car_birth_info_list = [] # 车辆生成信息列表 - self.expert_trajectories = {} # 专家数据轨迹字典 - + _obj_to_clean_this_frame = [] + self.car_birth_info_list = [] for scenario_id, track in self.engine.traffic_manager.current_traffic_data.items(): - # 跳过自车(SDC - Self Driving Car) if scenario_id == self.engine.traffic_manager.sdc_scenario_id: continue - - # 只处理车辆类型的对象 - if track["type"] == MetaDriveType.VEHICLE: - _obj_to_clean_this_frame.append(scenario_id) - valid = track['state']['valid'] # 车辆有效性标记 - # 找到车辆首次出现和最后出现的时间步 - first_show = np.argmax(valid) if valid.any() else -1 - last_show = len(valid) - 1 - np.argmax(valid[::-1]) if valid.any() else -1 - - if first_show == -1 or last_show == -1: - continue - object_id = track["metadata"]["object_id"] - - # 提取完整轨迹数据(只使用确认存在的字段) - trajectory_data = { - "object_id": object_id, - "scenario_id": scenario_id, - "valid_mask": valid[first_show:last_show+1].copy(), # 有效性掩码 - "positions": track["state"]["position"][first_show:last_show+1].copy(), # (T, 3) - "headings": track["state"]["heading"][first_show:last_show+1].copy(), # (T,) - "velocities": track["state"]["velocity"][first_show:last_show+1].copy(), # (T, 2) - "timesteps": np.arange(first_show, last_show+1), # 时间戳 - "start_timestep": first_show, - "end_timestep": last_show, - "length": last_show - first_show + 1 - } + else: + if track["type"] == MetaDriveType.VEHICLE: + _obj_to_clean_this_frame.append(scenario_id) + valid = track['state']['valid'] + first_show = np.argmax(valid) if valid.any() else -1 + last_show = len(valid) - 1 - np.argmax(valid[::-1]) if valid.any() else -1 + # id,出现时间,出生点坐标,出生朝向,目的地 + self.car_birth_info_list.append({ + 'id': track['metadata']['object_id'], + 'show_time': first_show, + 'begin': (track['state']['position'][first_show, 0], track['state']['position'][first_show, 1]), + 'heading': track['state']['heading'][first_show], + 'end': (track['state']['position'][last_show, 0], track['state']['position'][last_show, 1]) + }) - # 可选:如果数据中有车辆尺寸信息,则添加 - # 方法1: 尝试从state中获取 - if "length" in track["state"]: - trajectory_data["vehicle_length"] = track["state"]["length"][first_show] - if "width" in track["state"]: - trajectory_data["vehicle_width"] = track["state"]["width"][first_show] - if "height" in track["state"]: - trajectory_data["vehicle_height"] = track["state"]["height"][first_show] - - # 方法2: 尝试从metadata中获取 - if "vehicle_length" not in trajectory_data and "length" in track.get("metadata", {}): - trajectory_data["vehicle_length"] = track["metadata"]["length"] - if "vehicle_width" not in trajectory_data and "width" in track.get("metadata", {}): - trajectory_data["vehicle_width"] = track["metadata"]["width"] - if "vehicle_height" not in trajectory_data and "height" in track.get("metadata", {}): - trajectory_data["vehicle_height"] = track["metadata"]["height"] - - # 方法3: 使用默认值(如果以上都没有) - if "vehicle_length" not in trajectory_data: - trajectory_data["vehicle_length"] = 4.5 # MetaDrive默认车长 - if "vehicle_width" not in trajectory_data: - trajectory_data["vehicle_width"] = 2.0 # MetaDrive默认车宽 - if "vehicle_height" not in trajectory_data: - trajectory_data["vehicle_height"] = 1.5 # MetaDrive默认车高 - - - # 存储到专家轨迹字典 - self.expert_trajectories[object_id] = trajectory_data - - # 提取车辆关键信息 - car_info = { - 'id': track['metadata']['object_id'], - 'show_time': first_show, - 'begin': (track['state']['position'][first_show, 0], track['state']['position'][first_show, 1]), - 'heading': track['state']['heading'][first_show], - 'end': (track['state']['position'][last_show, 0], track['state']['position'][last_show, 1]) - } - - # 如果配置要求继承专家速度,则提取初始速度 - if self.config.get("inherit_expert_velocity", False): - velocity = track['state']['velocity'][first_show] - car_info['velocity'] = (velocity[0], velocity[1]) - - self.car_birth_info_list.append(car_info) - # 非车辆对象(如红绿灯、行人等)保留,不清理 - - # 清理车辆原始交通数据,释放内存(保留红绿灯等其他对象) for scenario_id in _obj_to_clean_this_frame: self.engine.traffic_manager.current_traffic_data.pop(scenario_id) - # 步骤2:重置MetaDrive引擎和传感器 self.engine.reset() self.reset_sensors() self.engine.taskMgr.step() - # 步骤3:获取地图车道信息 self.lanes = self.engine.map_manager.current_map.road_network.graph - - # 调试:场景信息统计(仅在verbose_reset模式下输出) - if self.config.get("verbose_reset", False): - print(f"\n📍 场景信息统计:") - print(f" - 总车道数: {len(self.lanes)}") - - # 统计红绿灯数量(如果未禁用红绿灯) - if not self.config.get("no_traffic_lights", False): - traffic_light_lanes = [] - for lane in self.lanes.values(): - if self.engine.light_manager.has_traffic_light(lane.lane.index): - traffic_light_lanes.append(lane.lane.index) - print(f" - 有红绿灯的车道数: {len(traffic_light_lanes)}") - if len(traffic_light_lanes) > 5: - print(f" 车道索引示例: {traffic_light_lanes[:5]} ...") - else: - print(f" - 红绿灯: 已禁用") - - # 步骤4:执行车道区域过滤 - total_cars_before = len(self.car_birth_info_list) - valid_count, filtered_count, filtered_list = self._filter_valid_spawn_positions() - - # 输出过滤信息(仅在有过滤时输出) - if filtered_count > 0: - if self.config.get("verbose_reset", False): - self.logger.warning(f"车辆生成位置过滤: 原始 {total_cars_before} 辆, " - f"有效 {valid_count} 辆, 过滤 {filtered_count} 辆") - for filtered_car in filtered_list[:3]: - self.logger.debug(f" 过滤车辆 ID={filtered_car['id']}, " - f"位置={filtered_car['position']}, " - f"原因={filtered_car['reason']}") - if filtered_count > 3: - self.logger.debug(f" ... 还有 {filtered_count - 3} 辆车被过滤") - else: - self.logger.info(f"车辆过滤: {total_cars_before} 辆 -> {valid_count} 辆 (过滤 {filtered_count} 辆)") - - # 步骤5:限制最大车辆数(在过滤后应用) - max_vehicles = self.config.get("max_controlled_vehicles", None) - if max_vehicles is not None and len(self.car_birth_info_list) > max_vehicles: - original_count = len(self.car_birth_info_list) - self.car_birth_info_list = self.car_birth_info_list[:max_vehicles] - if self.config.get("verbose_reset", False): - self.logger.info(f"限制最大车辆数: {original_count} 辆 -> {max_vehicles} 辆") - - # 最终统计 - if self.config.get("verbose_reset", False): - self.logger.info(f"✓ 最终生成 {len(self.car_birth_info_list)} 辆可控车辆") - # 清理渲染器 if self.top_down_renderer is not None: self.top_down_renderer.clear() self.engine.top_down_renderer = None - # 初始化回合相关变量 - self.dones = {} # 智能体完成状态 - self.episode_rewards = defaultdict(float) # 回合奖励累积 - self.episode_lengths = defaultdict(int) # 回合长度累积 + self.dones = {} + self.episode_rewards = defaultdict(float) + self.episode_lengths = defaultdict(int) - # 清空可控智能体 self.controlled_agents.clear() self.controlled_agent_ids.clear() - # 步骤6:调用父类重置并生成可控智能体 super().reset(seed) # 初始化场景 self._spawn_controlled_agents() - # 步骤7:返回初始观测 return self._get_all_obs() - def _is_position_on_lane(self, position, tolerance=None): - """ - 检测给定位置是否在有效车道范围内 - - 这个函数用于验证车辆生成位置是否在合法的车道上,避免在草坪、停车场等 - 非车道区域生成车辆。支持两种检测方法: - 1. 严格检测:直接使用MetaDrive的point_on_lane方法 - 2. 容差检测:考虑车道边缘的容差范围(当前已禁用) - - Args: - position: (x, y) 车辆位置坐标 - tolerance: 容差范围(米),用于放宽检测条件。None时使用配置中的默认值 - - Returns: - bool: True表示在车道上,False表示在非车道区域(如草坪、停车场等) - """ - # 检查车道信息是否已初始化 - if not hasattr(self, 'lanes') or self.lanes is None: - if self.debug_lane_filter: - print(f" ⚠️ 车道信息未初始化,默认允许") - return True # 如果车道信息未初始化,默认允许生成 - - # 设置容差参数 - if tolerance is None: - tolerance = self.config.get("lane_tolerance", 3.0) - - position_2d = (position[0], position[1]) - - if self.debug_lane_filter: - print(f" 🔍 检测位置 ({position_2d[0]:.2f}, {position_2d[1]:.2f}), 容差={tolerance}m") - - # 方法1:直接检测是否在任一车道上 - # 遍历所有车道,使用MetaDrive的point_on_lane方法进行精确检测 - checked_lanes = 0 - for lane in self.lanes.values(): - try: - checked_lanes += 1 - if lane.lane.point_on_lane(position_2d): - if self.debug_lane_filter: - print(f" ✅ 在车道上 (车道{lane.lane.index}, 检查了{checked_lanes}条)") - return True - except: - # 如果检测过程中出现异常,继续检查下一条车道 - continue - - if self.debug_lane_filter: - print(f" ❌ 不在任何车道上 (检查了{checked_lanes}条车道)") - - # 方法2:如果严格检测失败,使用容差范围检测(考虑车道边缘) - # 注释:此方法已被禁用,如需启用请取消注释 - # 该方法通过计算点到车道中心线的横向距离来判断是否在容差范围内 - # if tolerance > 0: - # for lane in self.lanes.values(): - # try: - # # 计算点到车道中心线的距离 - # lane_obj = lane.lane - # # 获取车道长度并检测最近点 - # s, lateral = lane_obj.local_coordinates(position_2d) - - # # 如果横向距离在容差范围内,认为是有效的 - # if abs(lateral) <= tolerance and 0 <= s <= lane_obj.length: - # return True - # except: - # continue - - return False - - def _filter_valid_spawn_positions(self): - """ - 过滤掉生成位置不在有效车道上的车辆信息 - - 这个函数是车道过滤的核心实现,用于确保所有生成的车辆都在合法的车道上。 - 它会遍历所有车辆生成信息,使用_is_position_on_lane方法检测每个位置, - 过滤掉在草坪、停车场等非车道区域的车辆。 - - 过滤过程包括: - 1. 检查配置是否启用过滤 - 2. 遍历所有车辆生成信息 - 3. 对每个车辆位置进行车道检测 - 4. 分离有效和无效的车辆 - 5. 更新车辆生成列表 - - Returns: - tuple: (有效车辆数量, 被过滤车辆数量, 被过滤车辆详细信息列表) - """ - # 检查配置是否启用车道过滤 - if not self.config.get("filter_offroad_vehicles", True): - if self.debug_lane_filter: - print(f"🚫 车道过滤已禁用") - return len(self.car_birth_info_list), 0, [] - - if self.debug_lane_filter: - print(f"\n🔍 开始车道过滤: 共 {len(self.car_birth_info_list)} 辆车待检测") - - # 初始化过滤结果 - valid_cars = [] # 有效车辆列表 - filtered_cars = [] # 被过滤车辆列表 - tolerance = self.config.get("lane_tolerance", 3.0) - - # 遍历所有车辆生成信息进行检测 - for idx, car in enumerate(self.car_birth_info_list): - if self.debug_lane_filter: - print(f"\n车辆 {idx+1}/{len(self.car_birth_info_list)}: ID={car['id']}") - - # 检测车辆生成位置是否在有效车道上 - if self._is_position_on_lane(car['begin'], tolerance=tolerance): - valid_cars.append(car) - if self.debug_lane_filter: - print(f" ✅ 保留") - else: - # 记录被过滤的车辆信息 - filtered_cars.append({ - 'id': car['id'], - 'position': car['begin'], - 'reason': '生成位置不在有效车道上(可能在草坪/停车场等区域)' - }) - if self.debug_lane_filter: - print(f" ❌ 过滤 (原因: 不在车道上)") - - # 更新车辆生成列表为过滤后的结果 - self.car_birth_info_list = valid_cars - - if self.debug_lane_filter: - print(f"\n📊 过滤结果: 保留 {len(valid_cars)} 辆, 过滤 {len(filtered_cars)} 辆") - - return len(valid_cars), len(filtered_cars), filtered_cars - def _spawn_controlled_agents(self): - """ - 生成可控智能体车辆 - - 根据当前时间步和车辆生成信息,动态生成需要出现的可控车辆。 - 每个车辆都会被分配策略和目标目的地,并注册到MetaDrive引擎中 - 参与物理仿真。 - - 生成过程: - 1. 遍历所有车辆生成信息 - 2. 检查车辆是否应该在当前时间步出现 - 3. 创建PolicyVehicle实例 - 4. 设置车辆策略和目的地 - 5. 注册到环境管理和引擎中 - """ - # 注释:可以获取自车位置用于相对位置计算(当前未使用) # ego_vehicle = self.engine.agent_manager.active_agents.get("default_agent") # ego_position = ego_vehicle.position if ego_vehicle else np.array([0, 0]) - - # 遍历所有车辆生成信息 for car in self.car_birth_info_list: - # 检查车辆是否应该在当前时间步出现 if car['show_time'] == self.round: - # 生成智能体ID agent_id = f"controlled_{car['id']}" - # 在MetaDrive引擎中生成车辆对象 vehicle = self.engine.spawn_object( - PolicyVehicle, # 使用自定义的策略车辆类 - vehicle_config={}, # 车辆配置(使用默认) - position=car['begin'], # 车辆生成位置 - heading=car['heading'] # 车辆生成朝向 + PolicyVehicle, + vehicle_config={}, + position=car['begin'], + heading=car['heading'] ) - # 重置车辆状态到指定位置和朝向 vehicle.reset(position=car['begin'], heading=car['heading']) - - # 如果配置要求继承专家速度,则设置初始速度 - if 'velocity' in car and self.config.get("inherit_expert_velocity", False): - vehicle.set_velocity(car['velocity']) - # 设置车辆的控制策略和目标 - vehicle.set_policy(self.policy) # 设置策略 - vehicle.set_destination(car['end']) # 设置目的地 + vehicle.set_policy(self.policy) + vehicle.set_destination(car['end']) - # 注册到环境管理 self.controlled_agents[agent_id] = vehicle self.controlled_agent_ids.append(agent_id) # ✅ 关键:注册到引擎的 active_agents,才能参与物理更新 - # 这是MetaDrive引擎识别和管理智能体的关键步骤 self.engine.agent_manager.active_agents[agent_id] = vehicle def _get_all_obs(self): - """ - 获取所有可控智能体的观测数据 - - 这是环境的核心观测函数,为每个可控智能体组装完整的观测向量。 - 观测数据包括: - 1. 车辆状态信息:位置、速度、朝向 - 2. 传感器数据:激光雷达(前向、侧向、车道线检测) - 3. 导航信息:目标目的地 - - 观测向量结构: - - position[2]: 车辆位置 (x, y) - - velocity[2]: 车辆速度 (vx, vy) - - heading[1]: 车辆朝向角度 - - lidar[80]: 前向激光雷达数据 (80个激光束,30米范围) - - side_lidar[10]: 侧向激光雷达数据 (10个激光束,8米范围) - - lane_line_lidar[10]: 车道线检测数据 (10个激光束,3米范围) - - destination[2]: 目标目的地 (x, y) - - Returns: - list: 所有智能体的观测数据列表 - """ - self.obs_list = [] # 清空观测列表 - - # 遍历所有可控智能体 + # position, velocity, heading, lidar, navigation, TODO: trafficlight -> list + self.obs_list = [] for agent_id, vehicle in self.controlled_agents.items(): - # 获取车辆基本状态信息 state = vehicle.get_state() - # 获取激光雷达传感器数据 - # 前向激光雷达:80个激光束,30米检测距离,用于障碍物检测 - lidar = self.engine.get_sensor("lidar").perceive( - num_lasers=80, - distance=30, - base_vehicle=vehicle, - physics_world=self.engine.physics_world.dynamic_world - ) - - # 侧向激光雷达:10个激光束,8米检测距离,用于侧向障碍物检测 - side_lidar = self.engine.get_sensor("side_detector").perceive( - num_lasers=10, - distance=8, - base_vehicle=vehicle, - physics_world=self.engine.physics_world.static_world - ) - - # 车道线检测激光雷达:10个激光束,3米检测距离,用于车道线识别 - lane_line_lidar = self.engine.get_sensor("lane_line_detector").perceive( - num_lasers=10, - distance=3, - base_vehicle=vehicle, - physics_world=self.engine.physics_world.static_world - ) + traffic_light = 0 + for lane in self.lanes.values(): + if lane.lane.point_on_lane(state['position'][:2]): + if self.engine.light_manager.has_traffic_light(lane.lane.index): + traffic_light = self.engine.light_manager._lane_index_to_obj[lane.lane.index].status + if traffic_light == 'TRAFFIC_LIGHT_GREEN': + traffic_light = 1 + elif traffic_light == 'TRAFFIC_LIGHT_YELLOW': + traffic_light = 2 + elif traffic_light == 'TRAFFIC_LIGHT_RED': + traffic_light = 3 + else: + traffic_light = 0 + break - # 组装完整的观测向量 - obs = (state['position'][:2] + # 位置 (x, y) - list(state['velocity']) + # 速度 (vx, vy) - [state['heading_theta']] + # 朝向角度 - lidar[0] + # 前向激光雷达数据 - side_lidar[0] + # 侧向激光雷达数据 - lane_line_lidar[0] + # 车道线检测数据 - list(vehicle.destination)) # 目标目的地 (x, y) - + lidar = self.engine.get_sensor("lidar").perceive(num_lasers=80, distance=30, base_vehicle=vehicle, + physics_world=self.engine.physics_world.dynamic_world) + side_lidar = self.engine.get_sensor("side_detector").perceive(num_lasers=10, distance=8, + base_vehicle=vehicle, + physics_world=self.engine.physics_world.static_world) + lane_line_lidar = self.engine.get_sensor("lane_line_detector").perceive(num_lasers=10, distance=3, + base_vehicle=vehicle, + physics_world=self.engine.physics_world.static_world) + + obs = (state['position'][:2] + list(state['velocity']) + [state['heading_theta']] + + lidar[0] + side_lidar[0] + lane_line_lidar[0] + [traffic_light] + + list(vehicle.destination)) self.obs_list.append(obs) - return self.obs_list def step(self, action_dict: Dict[AnyStr, Union[list, np.ndarray]]): - """ - 执行环境的一个时间步 - - 这是环境的核心步进函数,执行以下操作序列: - 1. 更新时间步计数器 - 2. 执行所有智能体的动作(before_step) - 3. 更新MetaDrive物理引擎状态 - 4. 执行智能体动作后的处理(after_step) - 5. 生成新的智能体(按时间步) - 6. 获取新的观测数据 - 7. 计算奖励和完成状态 - 8. 返回环境状态 - - Args: - action_dict: 智能体动作字典 {agent_id: action} - - Returns: - tuple: (观测数据, 奖励字典, 完成状态字典, 信息字典) - - obs: 所有智能体的观测数据列表 - - rewards: 每个智能体的奖励 {agent_id: reward} - - dones: 每个智能体的完成状态 {agent_id: done, "__all__": episode_done} - - infos: 每个智能体的额外信息 {agent_id: info} - """ - # 步骤1:更新时间步计数器 self.round += 1 - # 步骤2:执行所有智能体的动作(动作执行前处理) for agent_id, action in action_dict.items(): if agent_id in self.controlled_agents: - # 记录车辆状态并设置动作 self.controlled_agents[agent_id].before_step(action) - # 步骤3:更新MetaDrive物理引擎状态 - # 这是核心的物理仿真步骤,所有车辆状态都会根据动作更新 self.engine.step() - # 步骤4:执行智能体动作后的处理 for agent_id in action_dict: if agent_id in self.controlled_agents: - # 执行动作后的状态更新(如果有after_step方法) self.controlled_agents[agent_id].after_step() - # 步骤5:生成新的智能体(按时间步动态生成) self._spawn_controlled_agents() - - # 步骤6:获取新的观测数据 obs = self._get_all_obs() - - # 步骤7:计算奖励和完成状态 - # 初始化所有智能体的奖励为0(可根据需要实现奖励计算逻辑) rewards = {aid: 0.0 for aid in self.controlled_agents} - - # 初始化所有智能体的完成状态为False(可根据需要实现完成条件) dones = {aid: False for aid in self.controlled_agents} - - # 检查整个回合是否结束(达到最大步数) dones["__all__"] = self.episode_step >= self.config["horizon"] - - # 初始化所有智能体的额外信息为空字典 infos = {aid: {} for aid in self.controlled_agents} - - # 步骤8:返回环境状态 - return obs, rewards, dones, infos + return obs, rewards, dones, infos \ No newline at end of file diff --git a/Env/test_no_traffic_lights.py b/Env/test_no_traffic_lights.py deleted file mode 100644 index 97d5358..0000000 --- a/Env/test_no_traffic_lights.py +++ /dev/null @@ -1,76 +0,0 @@ -"""测试禁用红绿灯功能""" -from scenario_env import MultiAgentScenarioEnv -from simple_idm_policy import ConstantVelocityPolicy -from metadrive.engine.asset_loader import AssetLoader - -WAYMO_DATA_DIR = r"/home/huangfukk/MAGAIL4AutoDrive/Env" - -def test_no_traffic_lights(): - """测试禁用红绿灯""" - print("=" * 60) - print("测试:禁用红绿灯功能") - print("=" * 60) - - env = MultiAgentScenarioEnv( - config={ - "data_directory": AssetLoader.file_path(WAYMO_DATA_DIR, "exp_converted", unix_style=False), - "is_multi_agent": True, - "num_controlled_agents": 3, - "horizon": 300, - "use_render": True, - "sequential_seed": True, - "reactive_traffic": True, - "manual_control": True, - - # 车道检测与过滤配置 - "filter_offroad_vehicles": True, - "lane_tolerance": 3.0, - "max_controlled_vehicles": 2, - - # 禁用红绿灯 - "no_traffic_lights": True, # 关键配置 - - # 调试模式 - "debug_lane_filter": False, - "verbose_reset": False, - }, - agent2policy=ConstantVelocityPolicy(target_speed=50) - ) - - print("\n重置环境...") - obs = env.reset(0) - - # 检查红绿灯管理器状态 - if hasattr(env.engine, 'light_manager') and env.engine.light_manager is not None: - num_lights = len(env.engine.light_manager._lane_index_to_obj) - print(f"✓ 红绿灯管理器中的红绿灯数量: {num_lights}") - if num_lights == 0: - print("✅ 成功:所有红绿灯已被移除!") - else: - print(f"⚠️ 警告:仍有 {num_lights} 个红绿灯") - - print("\n运行几步测试...") - for step in range(100): - actions = { - aid: env.controlled_agents[aid].policy.act() - for aid in env.controlled_agents - } - - obs, rewards, dones, infos = env.step(actions) - env.render(mode="topdown") - - if step == 0: - print(f"步骤 {step}: 环境运行正常") - - if dones["__all__"]: - break - - print(f"\n测试完成,共运行 {step+1} 步") - print("请检查渲染窗口中是否还有红绿灯显示") - - env.close() - print("=" * 60) - -if __name__ == "__main__": - test_no_traffic_lights() - diff --git a/README.md b/README.md index 777e683..854f995 100644 --- a/README.md +++ b/README.md @@ -2,274 +2,97 @@ > 基于多智能体生成对抗模仿学习(MAGAIL)的自动驾驶训练系统 | MetaDrive + Waymo Open Motion Dataset -[![MetaDrive](https://img.shields [![Python](https://img.shields.io/io/badge/Dataset目实现了适配多智能体场景的GAIL算法训练系统,核心创新在于**改进判别器架构支持动态车辆数量**,利用Transformer处理1-100+辆车的交互场景。 +本项目利用 Waymo 真实驾驶数据,通过 MetaDrive 仿真环境构建专家回放系统,提取车辆状态与动作,用于训练多智能体模仿学习算法 (MAGAIL)。 -**核心特性:** -- ✅ 完整的Waymo数据处理pipeline(12,201个场景) -- ✅ 车道过滤和红绿灯检测优化 -- ✅ 支持5维简化/107维完整观测空间 -- ✅ 专家轨迹数据集(52K+训练样本) -- 🚧 MAGAIL算法实现(判别器+策略网络) +## 📁 核心模块 + +* **`Env/expert_replay_env.py`**: 专家回放环境。核心类 `ExpertReplayEnv`,负责读取 Waymo 轨迹,计算逆动力学动作,并过滤非道路/静态车辆。 +* **`Env/inverse_dynamics.py`**: 逆动力学模块。根据车辆位置和航向计算油门、刹车和转向动作。 +* **`scripts/generate_expert_data.py`**: 数据收集脚本。批量运行场景并保存训练数据。 +* **`scripts/visualize_replay.py`**: 可视化脚本。用于观察回放效果和数据质量。 *** -## 🚀 快速开始 +## 🚀 1. 数据收集 -### 环境安装 +### 生成专家数据 +使用 `generate_expert_data.py` 脚本从 Waymo 数据集中批量提取 (State, Action) 对。 ```bash -# 克隆项目 -git clone -cd MAGAIL4AutoDrive +# 设置 Python 路径 +export PYTHONPATH=$PYTHONPATH:.:./metadrive -# 安装依赖 -pip install metadrive-simulator==0.4.3 torch numpy matplotlib scenarionet - -# 创建必需目录 -mkdir -p analysis_results -touch scripts/__init__.py dataset/__init__.py Algorithm/__init__.py +# 运行生成脚本 +# --data_dir: Waymo 数据路径 (建议使用 exp_filtered) +# --output_dir: 结果保存路径 +# --num_scenarios: 要处理的场景数量 +python scripts/generate_expert_data.py \ + --data_dir data/exp_filtered \ + --output_dir data/training_data \ + --num_scenarios 100 \ + --start_index 0 ``` -### 数据准备 +**生成的 `.pkl` 文件结构**: +包含一个列表,每个元素是一条车辆轨迹(Trajectory Dictionary): +* `obs`: `(T, 45)` - 观测矩阵。包含 Ego 状态 (5维) + 10辆邻居车相对信息 (40维)。 +* `acts`: `(T, 2)` - 动作矩阵。`[Steering, Accel]`,归一化到 `[-1, 1]`。 +* `agent_id`: 车辆 ID。 +* `scenario_id`: 所属场景 ID。 + +**内置过滤器**: +脚本会自动过滤掉以下无效车辆: +1. **非道路车辆**:始终在停车场或路外行驶的车辆。 +2. **静态车辆**:全称移动距离小于 5米 且速度从未超过 1m/s 的车辆(作为背景流存在,不收集数据)。 + +--- + +## 🔍 2. 数据可视化与验证 + +### 回放可视化 +使用 `visualize_replay.py` 直观地观察回放效果,确认车辆行为是否自然,以及过滤逻辑是否生效。 ```bash -# 1. 转换Waymo数据 -python -m scenarionet.convert_waymo -d ~/mdsn/exp_converted --raw_data_path /path/to/waymo --num_files=150 - -# 2. 筛选场景(无红绿灯) -python -m scenarionet.filter --database_path ~/mdsn/exp_filtered --from ~/mdsn/exp_converted --no_traffic_light - -# 3. 验证数据集 -python scripts/check_database_info.py +# 运行可视化 +# --horizon: 回放的最大步数 (Waymo 场景通常为 90 或 198 步) +python scripts/visualize_replay.py \ + --data_dir data/exp_filtered \ + --start_index 0 \ + --num_scenarios 1 \ + --horizon 200 ``` -### 运行环境 +**观察要点**: +* **受控车辆 (Controlled Agents)**:控制台会显示数量(如 `Controlled agents: 2`)。这些是真正产生数据的车辆。 +* **背景车辆**:如果在渲染图中看到其他车(通常是路边停放的),但受控数量很少,说明静态过滤生效了。 + +### 数据分析 +使用 `analyze_expert_data.py` 查看生成数据的统计分布。 ```bash -# 测试多智能体环境 -python Env/run_multiagent_env.py - -# 收集专家数据(10个场景测试) -python dataset/expert_dataset.py +python scripts/analyze_expert_data.py --data_path data/training_data/expert_data_0_100.pkl ``` -*** +--- -## 📁 项目结构 +## 🧠 3. 模型训练 (Next Steps) -``` -MAGAIL4AutoDrive/ -├── Env/ # 仿真环境模块 -│ ├── scenario_env.py # 多智能体场景环境(含轨迹存储) -│ ├── run_multiagent_env.py# 环境运行脚本 -│ └── simple_idm_policy.py # 测试策略 -│ -├── dataset/ # 数据集模块 -│ └── expert_dataset.py # PyTorch Dataset(5维观测) -│ -├── scripts/ # 工具脚本 -│ ├── check_track_fields.py # 数据字段验证 -│ ├── check_database_info.py # 数据库信息检查 -│ ├── analyze_expert_data.py # 统计分析 -│ └── visualize_expert_trajectory.py # 轨迹可视化 -│ -├── Algorithm/ # MAGAIL算法(待完善) -│ ├── bert.py # Transformer判别器 -│ ├── disc.py # 判别器网络 -│ ├── policy.py # 策略网络 -│ ├── ppo.py # PPO优化器 -│ └── magail.py # MAGAIL训练循环 -│ -└── analysis_results/ # 分析输出 - ├── statistics.pkl # 数据统计 - └── distributions.png # 可视化图表 -``` +有了 `data/training_data/` 下的专家数据后,您可以开始训练 MAGAIL 模型。 -*** +### 训练流程 +1. **加载数据**:使用 `dataset/expert_dataset.py` 中的 `ExpertDataset` 类加载 `.pkl` 数据。 +2. **初始化 MAGAIL**: + * **Generator (Policy)**: 接收观测 `(B, 45)`,输出动作 `(B, 2)`。 + * **Discriminator**: 接收状态-动作对 `(s, a)`,判断是专家还是生成器。 +3. **交互采样**: + * 在 `MultiAgentScenarioEnv`(非回放模式)中运行 Policy。 + * 收集 Policy 生成的轨迹。 +4. **对抗更新**: + * 利用专家数据和 Policy 数据训练 Discriminator。 + * 利用 Discriminator 的输出作为 Reward (GAIL Reward) 训练 Policy (PPO/TRPO)。 -## 🎯 核心功能 - -### 1. 环境与数据处理 - -**scenario_env.py** - 多智能体场景环境 -- 专家轨迹完整存储(位置、速度、航向角、车辆尺寸) -- 车道区域过滤(自动移除非车道车辆) -- 红绿灯状态检测(双重保障机制) -- 107维完整观测空间(激光雷达+车道线) - -**expert_dataset.py** - 专家数据集 -- 状态-动作对提取(逆动力学) -- 批量采样和序列化 -- 支持PyTorch DataLoader - -### 2. 数据分析工具 - -| 脚本 | 功能 | 输出 | -|------|------|------| -| `check_database_info.py` | 验证数据库完整性 | 场景总数、映射关系 | -| `check_track_fields.py` | 检查可用字段 | 必需/可选字段列表 | -| `analyze_expert_data.py` | 统计分析 | 轨迹长度、速度、交互频率 | -| `visualize_expert_trajectory.py` | 轨迹可视化 | 动画展示车辆运动 | - -### 3. MAGAIL算法 - -**判别器** (Algorithm/bert.py + disc.py) -- Transformer编码器处理动态车辆数量 -- CLS标记或均值池化聚合特征 -- 支持集中式/去中心化/零和模式 - -**策略网络** (Algorithm/policy.py + ppo.py) -- Actor-Critic架构 -- 参数共享机制(所有车辆共享模型) -- PPO/TRPO优化器 - -*** - -## ⚙️ 配置说明 - -```python -# 环境配置 -config = { - # 数据路径 - "data_directory": "~/mdsn/exp_filtered", - - # 多智能体设置 - "num_controlled_agents": 3, # 初始车辆数 - "max_controlled_vehicles": 10, # 最大车辆数限制 - - # 车道过滤 - "filter_offroad_vehicles": True, # 启用车道过滤 - "lane_tolerance": 3.0, # 容差(米) - - # 场景加载 - "sequential_seed": True, # 顺序加载场景 - "horizon": 1000, # 最大步数 -} -``` - -*** - -## 📊 数据集统计 - -**当前数据规模**(基于exp_filtered): -- 场景总数: **12,201** -- 已收集场景: 10个测试场景 -- 轨迹数: 900条 -- 训练样本: **52,065**个(s,a)对 -- 观测维度: 5维(简化) / 107维(完整) -- 动作维度: 2维(油门/刹车, 转向) - -**数据质量**: -- 静止车辆占比: 54.8%(正常,包含停车场和路边停车) -- 平均轨迹长度: 67帧(6.7秒 @ 10Hz) -- 平均速度: 1.46 m/s -- 近距离交互(<5m): 1.92% - -*** - -## 🛠️ 使用示例 - -### 收集专家数据 - -```python -# dataset/expert_dataset.py -from expert_dataset import ExpertTrajectoryDataset - -# 收集1000个场景 -trajectories = ExpertTrajectoryDataset.collect_from_env( - env_config, - num_scenarios=1000, - save_path="./expert_trajectories.pkl" -) - -# 创建数据集 -dataset = ExpertTrajectoryDataset(trajectories, sequence_length=1) -``` - -### 环境测试 - -```python -from scenario_env import MultiAgentScenarioEnv - -env = MultiAgentScenarioEnv( - config=config, - agent2policy=your_policy -) - -obs = env.reset() -for step in range(1000): - actions = {aid: policy(obs[aid]) for aid in env.controlled_agents} - obs, rewards, dones, infos = env.step(actions) -``` - -*** - -## ❓ 常见问题 - -### Q1: KeyError: 'bbox' -**原因**: Waymo转换数据不含bbox字段 -**解决**: 使用length/width/height,代码已添加条件检查 - -### Q2: ModuleNotFoundError: scenario_env -**原因**: Python路径问题 -**解决**: 脚本开头添加: -```python -import sys, os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../Env")) -``` - -### Q3: 多次reset失败(clear_objects错误) -**原因**: MetaDrive对象管理bug -**解决**: 每次收集数据都重新创建环境(已实现) - -### Q4: 静止车辆占比过高 -**原因**: Waymo真实场景包含停车场等静止车辆 -**解决**: 可在数据收集时过滤平均速度<2m/s的轨迹 - -*** - -## 📈 开发路线图 - -### ✅ 已完成(Phase 1) -- [x] 数据转换与筛选 -- [x] 完整轨迹存储 -- [x] 数据质量分析 -- [x] PyTorch Dataset构建 - -### 🚧 进行中(Phase 2) -- [ ] 107维完整观测空间 -- [ ] 数据质量过滤 -- [ ] 轨迹可视化工具 - -### 📅 计划中(Phase 3-4) -- [ ] 判别器网络实现 -- [ ] Actor-Critic策略网络 -- [ ] MAGAIL训练循环 -- [ ] TensorBoard监控 -- [ ] 实验与评估 - -*** - -## 📚 参考资料 - -- [MetaDrive Documentation](https://metadrive-simulator.readthedocs.io/) -- [Waymo Open Dataset](https://waymo.com/open/) -- [MAGAIL Paper](https://arxiv.org/abs/1807.09936) -- [ScenarioNet](https://github.com/metadriverse/scenarionet) - -## 📄 License - -MIT License - -*** - -**💡 提示**: 项目处于活跃开发中,欢迎提Issue或PR贡献代码! - -[1](https://blog.csdn.net/BxuqBlockchain/article/details/133606934) -[2](https://blog.csdn.net/sinat_28461591/article/details/148351123) -[3](https://www.reddit.com/r/Python/comments/13kpoti/readmeai_autogenerate_readmemd_files/) -[4](https://www.reddit.com/r/learnprogramming/comments/1298ix8/what_does_a_good_readme_look_like_for_personal/) -[5](https://juejin.cn/post/7195763127883169853) -[6](https://jimmysong.io/trans/spec-driven-development-using-markdown/) -[7](https://www.showapi.com/news/article/66b602964ddd79f11a001e3c) -[8](https://learn.microsoft.com/zh-cn/nuget/nuget-org/package-readme-on-nuget-org) \ No newline at end of file +### 推荐配置 +* **Observation**: 45维 (Ego + 10 Neighbors) +* **Action**: 2维 Continuous (Steering, Accel) +* **Horizon**: 200 steps +* **Batch Size**: 1024+ (多智能体环境下数据量很大) diff --git a/TRAINING_ARCHITECTURE.md b/TRAINING_ARCHITECTURE.md new file mode 100644 index 0000000..9921756 --- /dev/null +++ b/TRAINING_ARCHITECTURE.md @@ -0,0 +1,498 @@ +# MAGAIL 训练方案架构文档 + +## 目录 +1. [训练数据结构](#1-训练数据结构) +2. [多智能体训练机制](#2-多智能体训练机制) +3. [完整训练流程](#3-完整训练流程) +4. [当前项目问题](#4-当前项目问题) +5. [TensorBoard 日志问题](#5-tensorboard-日志问题) + +--- + +## 1. 训练数据结构 + +### 1.1 数据维度 + +**观测空间 (Observation Space)** +- **维度**: 45维 +- **组成**: + - **Ego状态** (5维): `[position_x, position_y, velocity_x, velocity_y, heading_theta]` + - **邻居信息** (40维): 最多10个邻居,每个邻居4维特征 + - 每个邻居: `[relative_x, relative_y, velocity_x, velocity_y]` + - 如果邻居数量 < 10,用零填充 + +**动作空间 (Action Space)** +- **维度**: 2维 +- **组成**: `[steering, accel]` +- **范围**: 归一化到 `[-1, 1]` + +### 1.2 数据格式 + +**专家数据文件结构** (`.pkl` 文件): +```python +# 每个 .pkl 文件包含一个列表,每个元素是一条车辆轨迹 +trajectories = [ + { + 'obs': np.array, # Shape: (T, 45) - T为轨迹长度(可变) + 'acts': np.array, # Shape: (T, 2) - 对应的动作序列 + 'agent_id': str, # 车辆ID + 'scenario_id': int # 场景ID + }, + ... +] +``` + +**数据特点**: +- 轨迹长度 `T` 是**可变的**,取决于车辆在场景中的存活时间 +- 最小轨迹长度过滤: 只保留长度 > 10 的轨迹 +- 数据已通过静态车辆过滤(移动距离 < 5m 且最大速度 < 1m/s 的车辆被过滤) + +### 1.3 数据生成流程 + +**脚本**: `scripts/generate_expert_data.py` + +**流程**: +1. 从 Waymo 数据 (`data/exp_filtered`) 加载场景 +2. 使用 `ExpertReplayEnv` 回放专家轨迹 +3. 通过逆动力学 (`Env/inverse_dynamics.py`) 计算动作 +4. 构建45维观测(Ego + 10个最近邻居) +5. 过滤无效轨迹(长度 < 10) +6. 保存为 `.pkl` 文件到 `data/training_data/` + +**关键代码位置**: +- 观测构建: `Env/expert_replay_env.py` 的 `_get_all_obs()` 方法 +- 动作计算: `Env/inverse_dynamics.py` 的 `compute_action()` 方法 + +--- + +## 2. 多智能体训练机制 + +### 2.1 可变长度处理 + +**问题**: 不同场景中智能体数量不同,每个智能体的轨迹长度也不同。 + +**解决方案**: + +1. **数据层面** (`dataset/magail_dataset.py`): + - 将轨迹**展平**为独立的 `(state, action)` 对 + - 每个样本是独立的,不保留序列信息 + - 这样所有轨迹可以统一处理,不受长度限制 + +```python +# MAGAILExpertDataset 的处理方式 +for traj in self.trajectories: + obs = traj['obs'] # (T, 45) + acts = traj['acts'] # (T, 2) + # 展平为独立样本 + for i in range(len(obs)): + self.flat_data.append((obs[i], acts[i])) # 每个样本: (45,), (2,) +``` + +2. **训练环境层面** (`train_magail.py`): + - 每个 episode 动态处理不同数量的智能体 + - 在 rollout 循环中,为每个活跃智能体独立收集数据 + - 所有智能体的数据合并到一个 `memory` 中 + +```python +# Rollout 循环 +for agent_id, obs in obs_dict.items(): + act, logprob = ppo_agent.select_action(obs) + actions[agent_id] = act + # 所有智能体的数据都存入同一个 memory + memory['states'].append(obs) + memory['actions'].append(actions[agent_id]) + ... +``` + +3. **观测维度固定**: + - 通过 `MAGAILScenarioEnv` 确保观测维度始终为45维 + - 邻居数量不足时用零填充,保证维度一致 + +### 2.2 多智能体交互 + +**环境设置**: +- 使用 `MAGAILScenarioEnv` (继承自 `MultiAgentScenarioEnv`) +- 自定义 `_get_all_obs()` 方法,确保观测格式与专家数据一致 +- 每个智能体独立选择动作,环境统一执行 + +**关键点**: +- 所有智能体共享同一个策略网络(参数共享) +- 每个智能体独立计算动作和奖励 +- 数据收集时将所有智能体的经验合并 + +--- + +## 3. 完整训练流程 + +### 3.1 数据准备阶段 + +**步骤 1: 生成专家数据** +```bash +python scripts/generate_expert_data.py \ + --data_dir data/exp_filtered \ + --output_dir data/training_data \ + --num_scenarios 100 \ + --start_index 0 +``` + +**输出**: `data/training_data/expert_data_*.pkl` + +### 3.2 模型初始化 + +**网络架构**: + +1. **Actor (策略网络)**: + - 输入: 45维状态 + - 输出: 2维动作(连续) + - 结构: MLP (45 → 256 → 256 → 2) + - 输出分布: 高斯分布(均值 + 可学习标准差) + +2. **Critic (价值网络)**: + - 输入: 45维状态 + - 输出: 标量价值 + - 结构: MLP (45 → 256 → 256 → 1) + +3. **Discriminator (鉴别器)**: + - 输入: 45维状态 + 2维动作 = 47维 + - 输出: 标量(0-1之间,表示专家概率) + - 结构: MLP (47 → 256 → 256 → 1) + Sigmoid + +### 3.3 训练循环 + +**主循环** (`train_magail.py` 的 `train()` 函数): + +``` +For each episode: + 1. 收集 Rollout + - 重置环境(随机选择场景) + - 运行策略收集轨迹 + - 存储 (state, action, logprob, next_state, done) + + 2. 训练 Discriminator + - 采样专家批次 + - 采样策略批次 + - 更新鉴别器: + - Expert loss: BCE(D(s_e, a_e), 1) + - Policy loss: BCE(D(s_p, a_p), 0) + - Total: L_d = L_expert + L_policy + + 3. 计算 GAIL 奖励 + - 对所有策略状态-动作对: + reward = -log(1 - D(s, a) + ε) + - 替换环境奖励 + + 4. 更新策略 (PPO) + - 计算 GAE (Generalized Advantage Estimation) + - PPO 更新 (K epochs): + - 计算优势函数 + - 计算策略损失(带clip) + - 计算价值损失 + - 更新 Actor 和 Critic +``` + +### 3.4 训练目标 + +**Discriminator 目标**: +``` +L_D = E_{(s,a)~π_E}[-log(D(s,a))] + E_{(s,a)~π_θ}[-log(1-D(s,a))] +``` +- 最大化区分专家数据和策略数据的能力 + +**Policy (Generator) 目标**: +``` +L_π = E_{(s,a)~π_θ}[-log(D(s,a))] - λ_H(π_θ) +``` +- 通过 PPO 优化,使用 GAIL 奖励作为信号 +- 最大化鉴别器给出的"专家概率" +- 同时保持策略熵(探索) + +**PPO 更新**: +```python +# 优势函数 (GAE) +advantages = compute_gae(rewards, values, next_values, dones, gamma, lambda) + +# 策略损失 +ratios = exp(log_probs - old_log_probs) +surr1 = ratios * advantages +surr2 = clip(ratios, 1-ε, 1+ε) * advantages +policy_loss = -min(surr1, surr2) + 0.01 * entropy + +# 价值损失 +value_loss = MSE(critic(states), returns) + +# 总损失 +total_loss = policy_loss + 0.5 * value_loss +``` + +### 3.5 关键代码位置 + +- **训练主循环**: `train_magail.py:278-505` +- **PPO 更新**: `train_magail.py:90-146` +- **Discriminator 更新**: `train_magail.py:429-462` +- **GAIL 奖励计算**: `train_magail.py:472-477` + +--- + +## 4. 当前项目问题 + +### 4.1 环境重置问题 + +**问题描述**: +- MetaDrive 环境在快速重置时可能出现对象清理不完整的问题 +- 错误信息: "You should clear all generated objects..." + +**当前处理**: +- 代码中已有异常处理机制(`train_magail.py:288-342`) +- 重置失败时会尝试关闭并重新创建环境 +- 但可能导致训练不稳定 + +**建议修复**: +- 在每次重置前显式清理所有对象 +- 增加重置间隔,避免过于频繁的重置 +- 考虑使用环境池(Environment Pool)复用环境实例 + +### 4.2 观测维度对齐 + +**问题描述**: +- 原始 `MultiAgentScenarioEnv` 返回108维观测(包含Lidar) +- 专家数据使用45维观测 +- 维度不匹配会导致训练失败 + +**当前解决方案**: +- 通过 `MAGAILScenarioEnv` 重写 `_get_all_obs()` 方法 +- 确保训练环境与专家数据使用相同的观测格式 + +**代码位置**: `train_magail.py:223-262` + +### 4.3 数据收集效率 + +**问题描述**: +- 每个 episode 都需要完整运行环境收集数据 +- 可变长度轨迹导致 batch 大小不一致 +- 可能影响训练稳定性 + +**当前处理**: +- 使用展平的数据集,每个样本独立 +- 在 rollout 时收集所有智能体的数据,合并处理 + +**潜在改进**: +- 考虑使用经验回放缓冲区 +- 实现轨迹级别的采样(保留序列信息) + +### 4.4 内存管理 + +**问题描述**: +- 长时间训练可能导致内存泄漏 +- 环境对象可能没有完全释放 + +**当前处理**: +- 代码中有显式的 `gc.collect()` 和 `torch.cuda.empty_cache()` +- 但可能不够彻底 + +**建议**: +- 定期检查内存使用 +- 考虑限制 rollout 长度 +- 使用更激进的清理策略 + +### 4.5 训练稳定性 + +**问题描述**: +- Discriminator 可能过早收敛,导致策略无法学习 +- GAIL 奖励可能不稳定 + +**当前处理**: +- 使用标准的 GAIL 奖励公式: `-log(1 - D(s,a) + ε)` +- PPO 的 clip 机制提供稳定性 + +**潜在改进**: +- 考虑使用 WGAN-GP 或 LSGAN 损失 +- 实现 Discriminator 的预训练 +- 添加奖励归一化 + +--- + +## 5. TensorBoard 日志问题 + +### 5.1 问题分析 + +**现象**: +- `runs/magail_0112/` 目录下只有模型文件(`.pth`),没有 TensorBoard 事件文件(`events.out.tfevents.*`) +- 其他目录(`magail_full`, `magail_production`)有事件文件 + +**可能原因**: + +1. **TensorBoard 未安装**: + - 代码中有 try-except 处理(`train_magail.py:269-274`) + - 如果 TensorBoard 未安装,`writer` 会被设置为 `None` + - 训练会继续,但不会写入日志 + +2. **日志写入失败**: + - 即使 `SummaryWriter` 创建成功,如果写入时出错,可能不会生成文件 + - 需要检查是否有异常被静默捕获 + +3. **训练中断**: + - 如果训练在写入第一个日志前中断,可能没有事件文件 + - 但模型文件已保存,说明训练至少运行了一段时间 + +### 5.2 检查方法 + +**步骤 1: 检查 TensorBoard 安装** +```bash +python -c "import tensorboard; print(tensorboard.__version__)" +``` + +**步骤 2: 检查训练脚本中的日志写入** +查看 `train_magail.py:493-496`: +```python +if writer: + writer.add_scalar('Loss/Discriminator', disc_loss.item(), i_episode) + writer.add_scalar('Loss/Policy', ppo_loss, i_episode) + writer.add_scalar('Reward/Mean_GAIL', np.mean(all_gail_rewards), i_episode) +``` + +**步骤 3: 检查日志目录权限** +```bash +ls -la runs/magail_0112/ +``` + +### 5.3 解决方案 + +**方案 1: 确保 TensorBoard 已安装** +```bash +pip install tensorboard +``` + +**方案 2: 添加显式刷新** +在训练循环结束后,显式调用 `writer.flush()`: +```python +if writer: + writer.flush() # 确保数据写入磁盘 +``` + +**方案 3: 添加日志验证** +在训练开始时检查日志目录: +```python +if writer: + # 测试写入 + writer.add_scalar('Test/Initialization', 0.0, 0) + writer.flush() + print(f"TensorBoard logging enabled. Log dir: {args.log_dir}") +else: + print("WARNING: TensorBoard not available. Logging disabled.") +``` + +**方案 4: 使用文件日志作为备份** +即使 TensorBoard 不可用,也可以写入文本日志: +```python +import logging +logging.basicConfig( + filename=os.path.join(args.log_dir, 'training.log'), + level=logging.INFO +) +``` + +### 5.4 代码修复建议 + +**在 `train_magail.py` 中添加以下改进**: + +1. **确保 disc_loss 在 CPU 上**: +```python +# 第425行附近 +disc_loss = torch.tensor(0.0).cuda() # 改为 .cuda() 或保持 CPU +# 或者在使用时转换 +if writer: + disc_loss_value = disc_loss.item() if isinstance(disc_loss, torch.Tensor) else disc_loss + writer.add_scalar('Loss/Discriminator', disc_loss_value, i_episode) +``` + +2. **添加显式刷新**: +```python +# 第496行后添加 +if writer: + writer.flush() # 确保数据写入磁盘 +``` + +3. **添加初始化验证**: +```python +# 第271行后添加 +if writer: + # 测试写入 + writer.add_scalar('Test/Initialization', 0.0, 0) + writer.flush() + print(f"✓ TensorBoard logging enabled. Log dir: {args.log_dir}") + # 检查文件是否创建 + import glob + event_files = glob.glob(os.path.join(args.log_dir, "events.out.tfevents.*")) + if event_files: + print(f"✓ TensorBoard event file created: {event_files[0]}") +else: + print("⚠ WARNING: TensorBoard not available. Logging disabled.") +``` + +4. **在训练结束时确保关闭**: +```python +# 第505行后添加 +if writer: + writer.flush() # 最后一次刷新 + writer.close() + print(f"TensorBoard logs saved to {args.log_dir}") +``` + +### 5.5 验证修复 + +**重新训练测试**: +```bash +python train_magail.py \ + --expert_data_dir data/training_data \ + --data_dir data/exp_filtered \ + --batch_size 1024 \ + --max_episodes 10 \ + --log_dir runs/test_tensorboard +``` + +**检查输出**: +```bash +# 应该看到事件文件 +ls runs/test_tensorboard/events.out.tfevents.* + +# 启动 TensorBoard +tensorboard --logdir runs/test_tensorboard +``` + +**对于 magail_0112 训练**: +由于该训练已经完成且没有日志文件,建议: +1. 检查训练时的控制台输出,确认是否有 "TensorBoard not installed" 消息 +2. 如果确实没有 TensorBoard,可以重新运行少量 episode 来验证修复 +3. 或者查看是否有其他日志文件(如 `training.log`) + +--- + +## 附录: 关键文件清单 + +### 核心训练文件 +- `train_magail.py`: 主训练脚本 +- `dataset/magail_dataset.py`: 专家数据集加载 +- `Env/expert_replay_env.py`: 专家回放环境 +- `Env/scenario_env.py`: 多智能体场景环境 +- `Env/inverse_dynamics.py`: 逆动力学计算 + +### 数据生成文件 +- `scripts/generate_expert_data.py`: 专家数据生成 +- `scripts/visualize_replay.py`: 数据可视化 +- `scripts/analyze_expert_data.py`: 数据分析 + +### 配置文件 +- `README.md`: 项目说明 +- `TRAINING_ARCHITECTURE.md`: 本文档 + +--- + +## 总结 + +本项目的 MAGAIL 训练方案通过以下方式处理多智能体可变长度问题: + +1. **数据层面**: 将轨迹展平为独立样本,统一处理 +2. **环境层面**: 动态处理不同数量的智能体,合并经验 +3. **网络层面**: 固定输入维度(45维),通过零填充处理邻居不足的情况 + +训练流程遵循标准的 GAIL 框架,使用 PPO 作为策略优化算法。当前主要问题集中在环境稳定性和日志记录方面,需要进一步优化。 diff --git a/dataset/__pycache__/__init__.cpython-313.pyc b/dataset/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f489cb1b4fe380de5cde8f7f25f84d898053bfc5 GIT binary patch literal 149 zcmey&%ge<81R?IWnIQTxh=2h`DC08=kTI1Zok5e)ZzV$!6Oi{ABz4O~KO;XkRX?LN zF)ux>G&@`0*U{b4)5pZIv?Sl9D6=e8KP9mwu{gCvKR!M)FS8^*Uaz3?7Kcr4eoARh Ys$CH)&=8O%#UREg`kf)IDxOc4DTL?8o3AjbiSi&=m~3PUi1CZpd*((2>0{zpT9WTllv$RlpORRTSe#m-A0MBYmst`YuUAlci^C>2KczG$ M)edCZXCP((0J1+HjsO4v literal 0 HcmV?d00001 diff --git a/dataset/__pycache__/magail_dataset.cpython-313.pyc b/dataset/__pycache__/magail_dataset.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..63b02c9c872a4a621fed819d95b4f6f382b76794 GIT binary patch literal 3152 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 literal 0 HcmV?d00001 diff --git a/dataset/magail_dataset.py b/dataset/magail_dataset.py new file mode 100644 index 0000000..e753527 --- /dev/null +++ b/dataset/magail_dataset.py @@ -0,0 +1,61 @@ +import torch +from torch.utils.data import Dataset +import pickle +import numpy as np +import os +import glob + +class MAGAILExpertDataset(Dataset): + def __init__(self, data_dir, transform=None): + """ + Args: + data_dir (str): Directory containing .pkl files from generate_expert_data.py + transform (callable, optional): Optional transform to be applied on a sample. + """ + self.data_dir = data_dir + self.transform = transform + self.trajectories = [] + self.flat_data = [] # (obs, act) pairs + + # Load all .pkl files + pkl_files = glob.glob(os.path.join(data_dir, "*.pkl")) + print(f"Loading data from {len(pkl_files)} files in {data_dir}...") + + for pkl_file in pkl_files: + try: + with open(pkl_file, 'rb') as f: + data = pickle.load(f) + # data is a list of dicts: {'obs': (T, 45), 'acts': (T, 2), ...} + self.trajectories.extend(data) + except Exception as e: + print(f"Error loading {pkl_file}: {e}") + + # Flatten for training Discriminator/BC + print(f"Processing {len(self.trajectories)} trajectories...") + for traj in self.trajectories: + obs = traj['obs'] + acts = traj['acts'] + + # obs: (T, 45), acts: (T, 2) + # We pair them up + for i in range(len(obs)): + self.flat_data.append((obs[i], acts[i])) + + print(f"Total samples: {len(self.flat_data)}") + + def __len__(self): + return len(self.flat_data) + + def __getitem__(self, idx): + obs, act = self.flat_data[idx] + + # Convert to tensor + obs = torch.from_numpy(obs).float() + act = torch.from_numpy(act).float() + + sample = {'state': obs, 'action': act} + + if self.transform: + sample = self.transform(sample) + + return sample diff --git a/scripts/analyze_expert_data.py b/scripts/analyze_expert_data.py index 4c6b94c..74d73c8 100644 --- a/scripts/analyze_expert_data.py +++ b/scripts/analyze_expert_data.py @@ -247,7 +247,7 @@ class ExpertDataAnalyzer: print(f" ✓ 分布图已保存") if __name__ == "__main__": - WAYMO_DATA_DIR = r"/home/huangfukk/mdsn" + WAYMO_DATA_DIR = r"/home/huangfukk/MAGAIL4AutoDrive/data" data_dir = AssetLoader.file_path(WAYMO_DATA_DIR, "exp_filtered", unix_style=False) print("开始分析专家数据...") diff --git a/scripts/generate_expert_data.py b/scripts/generate_expert_data.py new file mode 100644 index 0000000..d08e137 --- /dev/null +++ b/scripts/generate_expert_data.py @@ -0,0 +1,162 @@ +import argparse +import os +import sys +import pickle +import numpy as np +from tqdm import tqdm + +# Add project root to Python path so we can import Env module +project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if project_root not in sys.path: + sys.path.insert(0, project_root) + +from metadrive.engine.asset_loader import AssetLoader +from Env.expert_replay_env import ExpertReplayEnv + +def generate_data(args): + data_path = os.path.abspath(args.data_dir) + if not os.path.exists(data_path): + raise ValueError(f"Data directory {data_path} not found") + + # MetaDrive's ScenarioDataManager asserts if config["num_scenarios"] > available scenarios in data_directory. + # So we always set it to -1 (load all available) and clamp the loop range by reading dataset summary. + from metadrive.scenario.utils import read_dataset_summary + _, summary_lookup, _ = read_dataset_summary(data_path) + if args.start_index >= len(summary_lookup): + raise ValueError( + f"start_index={args.start_index} out of range. Dataset has {len(summary_lookup)} scenarios." + ) + max_available = len(summary_lookup) - args.start_index + num_to_run = min(args.num_scenarios, max_available) + + env_config = { + "data_directory": data_path, + "is_multi_agent": True, + "num_controlled_agents": 100, # Set high to catch all vehicles in scenario + "horizon": 1000, + "use_render": False, + "sequential_seed": True, + "reactive_traffic": False, # Important: we replay, not react + "start_scenario_index": args.start_index, + # Load all scenarios available in the directory to avoid assertion failure. + # We will still only iterate `num_to_run` scenarios below. + "num_scenarios": -1, + "log_level": 50 # ERROR to reduce noise + } + + expert_trajectories = [] + + try: + # Loop through scenarios + for i in tqdm(range(args.start_index, args.start_index + num_to_run), desc="Scenarios"): + env = ExpertReplayEnv(config=env_config) + try: + obs_dict = env.reset(seed=i) + except Exception as e: + print(f"Error resetting scenario {i}: {e}") + try: + env.close() + except Exception: + pass + continue + + # Storage for current episode + # dict of lists: {agent_id: {'obs': [], 'acts': []}} + episode_data = {} + + # Map agent_id to original ID if possible, but agent_id is unique enough + + for step in range(env.config["horizon"]): + # Step with dummy actions + obs, rewards, dones, infos = env.step(None) + + # 'obs' is next observation (t+1) + # 'infos' contains 'expert_action' which took (t -> t+1) + # Wait, usually (obs_t, act_t) -> obs_{t+1} + # expert_replay_env.step(): + # calc action (t -> t+1) + # move agents to t+1 + # return obs_{t+1} + # So we have obs_dict (from reset or prev step) which is at 't' + # And we have 'infos' which has action at 't'. + + current_agents = list(obs_dict.keys()) + + for agent_id in current_agents: + if agent_id not in episode_data: + episode_data[agent_id] = {'obs': [], 'acts': []} + + # Check if we have action for this agent + if agent_id in infos and 'expert_action' in infos[agent_id]: + action = infos[agent_id]['expert_action'] + observation = obs_dict[agent_id] + + episode_data[agent_id]['obs'].append(observation) + episode_data[agent_id]['acts'].append(action) + + # Update obs_dict for next step + obs_dict = obs + + if dones["__all__"]: + break + + # Post-process episode data + for agent_id, data in episode_data.items(): + if len(data['obs']) > 10: # Minimum length filter + expert_trajectories.append({ + 'obs': np.array(data['obs']), + 'acts': np.array(data['acts']), + 'agent_id': agent_id, + 'scenario_id': i + }) + env.close() + + except Exception as e: + import traceback + traceback.print_exc() + print(f"Global error: {e}") + finally: + # env is closed per-scenario above (more robust for MetaDrive object lifecycle) + pass + + # Save data + output_file = os.path.join(args.output_dir, f"expert_data_{args.start_index}_{args.num_scenarios}.pkl") + os.makedirs(args.output_dir, exist_ok=True) + + print(f"Saving {len(expert_trajectories)} trajectories to {output_file}") + with open(output_file, 'wb') as f: + pickle.dump(expert_trajectories, f) + + # Verification stats + if len(expert_trajectories) > 0: + all_acts = np.concatenate([t['acts'] for t in expert_trajectories]) + print("Action Stats:") + print(f" Steering: min={all_acts[:,0].min():.3f}, max={all_acts[:,0].max():.3f}, mean={all_acts[:,0].mean():.3f}") + print(f" Accel: min={all_acts[:,1].min():.3f}, max={all_acts[:,1].max():.3f}, mean={all_acts[:,1].mean():.3f}") + + # Clipping ratio diagnostics (actions are normalized to [-1, 1]) + # If this ratio is high, it usually indicates max_acc/max_steering too small or noisy finite-difference. + eps = 1e-6 + steer = all_acts[:, 0] + accel = all_acts[:, 1] + steer_clipped = np.isclose(np.abs(steer), 1.0, atol=eps) + accel_clipped = np.isclose(np.abs(accel), 1.0, atol=eps) + print("Clipping Stats:") + print( + f" Steering clipped (|a|==1): {steer_clipped.mean()*100:.2f}% " + f"({steer_clipped.sum()}/{len(steer_clipped)})" + ) + print( + f" Accel clipped (|a|==1): {accel_clipped.mean()*100:.2f}% " + f"({accel_clipped.sum()}/{len(accel_clipped)})" + ) + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--data_dir", type=str, default="/home/huangfukk/MAGAIL4AutoDrive/data/exp_filtered", help="Path to Waymo pickles (or filtered index)") + parser.add_argument("--output_dir", type=str, default="/home/huangfukk/MAGAIL4AutoDrive/data/training", help="Output directory") + parser.add_argument("--start_index", type=int, default=0) + parser.add_argument("--num_scenarios", type=int, default=10) + + args = parser.parse_args() + generate_data(args) diff --git a/scripts/visualize_replay.py b/scripts/visualize_replay.py new file mode 100644 index 0000000..3c46690 --- /dev/null +++ b/scripts/visualize_replay.py @@ -0,0 +1,93 @@ +import argparse +import os +import sys +import time + +# Add project root to Python path so we can import Env module +project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if project_root not in sys.path: + sys.path.insert(0, project_root) + +from Env.expert_replay_env import ExpertReplayEnv + +def visualize_replay(args): + data_path = os.path.abspath(args.data_dir) + if not os.path.exists(data_path): + raise ValueError(f"Data directory {data_path} not found") + + # Same as data generation: avoid MetaDrive assertion when requested num_scenarios > available. + from metadrive.scenario.utils import read_dataset_summary + _, summary_lookup, _ = read_dataset_summary(data_path) + if args.start_index >= len(summary_lookup): + raise ValueError( + f"start_index={args.start_index} out of range. Dataset has {len(summary_lookup)} scenarios." + ) + max_available = len(summary_lookup) - args.start_index + num_to_run = min(args.num_scenarios, max_available) + + env_config = { + "data_directory": data_path, + "is_multi_agent": True, + "num_controlled_agents": 100, + "horizon": args.horizon, + "use_render": True, # Enable rendering + "sequential_seed": True, + "reactive_traffic": False, + "start_scenario_index": args.start_index, + "num_scenarios": -1, + "log_level": 40, # ERROR + # "pstats": True, # For performance debugging + } + + print(f"Initializing ExpertReplayEnv with data from {data_path}...") + env = ExpertReplayEnv(config=env_config) + + try: + for i in range(args.start_index, args.start_index + num_to_run): + print(f"\n--- Playing Scenario {i} ---") + try: + obs = env.reset(seed=i) + except Exception as e: + print(f"Error resetting scenario {i}: {e}") + continue + + print(f"Scenario loaded. Controlled agents: {len(env.controlled_agents)}") + + for step in range(args.horizon): + # Step + obs, rewards, dones, infos = env.step(None) + + # Render + env.render(mode="top_down", + text={ + "Step": step, + "Agents": len(env.controlled_agents), + "Scenario": i + }) + + # Sleep to control playback speed + time.sleep(0.05) + + if dones["__all__"]: + print(f"Scenario {i} finished at step {step}") + break + + except KeyboardInterrupt: + print("Interrupted by user") + except Exception as e: + import traceback + traceback.print_exc() + print(f"Global error: {e}") + finally: + env.close() + print("Environment closed.") + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--data_dir", type=str, default="/home/huangfukk/MAGAIL4AutoDrive/data/exp_filtered", help="Path to Waymo data") + parser.add_argument("--start_index", type=int, default=0) + parser.add_argument("--num_scenarios", type=int, default=1) + parser.add_argument("--horizon", type=int, default=500) + + args = parser.parse_args() + visualize_replay(args) diff --git a/train_magail.py b/train_magail.py new file mode 100644 index 0000000..19c74c5 --- /dev/null +++ b/train_magail.py @@ -0,0 +1,521 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +import torch.optim as optim +from torch.distributions import Normal +import numpy as np +import os +import argparse +import signal +import sys +from torch.utils.data import DataLoader +from dataset.magail_dataset import MAGAILExpertDataset + +# --- Networks --- + +class Actor(nn.Module): + def __init__(self, state_dim, action_dim, hidden_dim=256): + super(Actor, self).__init__() + self.net = nn.Sequential( + nn.Linear(state_dim, hidden_dim), + nn.Tanh(), + nn.Linear(hidden_dim, hidden_dim), + nn.Tanh(), + ) + self.mu_head = nn.Linear(hidden_dim, action_dim) + self.log_std_head = nn.Parameter(torch.zeros(1, action_dim)) + + def forward(self, state): + x = self.net(state) + mu = torch.tanh(self.mu_head(x)) # Action range [-1, 1] + if mu.dim() == 1: + mu = mu.unsqueeze(0) # Handle single sample + log_std = self.log_std_head.expand_as(mu) + std = torch.exp(log_std) + dist = Normal(mu, std) + return dist + +class Critic(nn.Module): + def __init__(self, state_dim, hidden_dim=256): + super(Critic, self).__init__() + self.net = nn.Sequential( + nn.Linear(state_dim, hidden_dim), + nn.Tanh(), + nn.Linear(hidden_dim, hidden_dim), + nn.Tanh(), + nn.Linear(hidden_dim, 1) + ) + + def forward(self, state): + return self.net(state) + +class Discriminator(nn.Module): + def __init__(self, state_dim, action_dim, hidden_dim=256): + super(Discriminator, self).__init__() + self.net = nn.Sequential( + nn.Linear(state_dim + action_dim, hidden_dim), + nn.Tanh(), + nn.Linear(hidden_dim, hidden_dim), + nn.Tanh(), + nn.Linear(hidden_dim, 1), + nn.Sigmoid() + ) + + def forward(self, state, action): + x = torch.cat([state, action], dim=-1) + return self.net(x) + +# --- PPO Algorithm --- + +class PPO: + def __init__(self, state_dim, action_dim, lr=3e-4, gamma=0.99, eps_clip=0.2, K_epochs=10): + self.actor = Actor(state_dim, action_dim).cuda() + self.critic = Critic(state_dim).cuda() + self.optimizer_actor = optim.Adam(self.actor.parameters(), lr=lr) + self.optimizer_critic = optim.Adam(self.critic.parameters(), lr=lr) + + self.gamma = gamma + self.eps_clip = eps_clip + self.K_epochs = K_epochs + self.mse_loss = nn.MSELoss() + + def select_action(self, state): + with torch.no_grad(): + state = torch.FloatTensor(state).cuda() + dist = self.actor(state) + action = dist.sample() + action_logprob = dist.log_prob(action).sum(dim=-1) + return action.cpu().numpy(), action_logprob.cpu().numpy() + + def update(self, memory): + # Convert memory to tensors + states = torch.FloatTensor(np.array(memory['states'])).cuda() + actions = torch.FloatTensor(np.array(memory['actions'])).cuda() + logprobs = torch.FloatTensor(np.array(memory['logprobs'])).cuda() + rewards = torch.FloatTensor(np.array(memory['rewards'])).cuda() + next_states = torch.FloatTensor(np.array(memory['next_states'])).cuda() + dones = torch.FloatTensor(np.array(memory['dones'])).cuda() + + # Monte Carlo estimate of state rewards (or GAE if implemented, simplistic here) + # Usually for PPO we use GAE. Let's do a simple discounted return for now or bootstrapping. + # Let's use bootstrapping from critic for returns. + + returns = [] + discounted_reward = 0 + # This simple loop assumes full episode or consistent batch. + # For multi-agent disjoint steps, bootstrapping is better. + # But let's calculate advantage using GAE for stability. + + values = self.critic(states).detach() + next_values = self.critic(next_states).detach() + + # GAE + advantages = [] + gae = 0 + for i in reversed(range(len(rewards))): + delta = rewards[i] + self.gamma * next_values[i] * (1 - dones[i]) - values[i] + gae = delta + self.gamma * 0.95 * (1 - dones[i]) * gae + advantages.insert(0, gae) + + advantages = torch.FloatTensor(advantages).cuda() + returns = advantages + values.squeeze() + + # Optimize policy for K epochs: + for _ in range(self.K_epochs): + # Evaluating old actions and values : + dist = self.actor(states) + action_logprobs = dist.log_prob(actions).sum(dim=-1) + dist_entropy = dist.entropy().sum(dim=-1) + state_values = self.critic(states).squeeze() + + # Finding the ratio (pi_theta / pi_theta__old): + ratios = torch.exp(action_logprobs - logprobs) + + # Finding Surrogate Loss: + surr1 = ratios * advantages + surr2 = torch.clamp(ratios, 1-self.eps_clip, 1+self.eps_clip) * advantages + loss = -torch.min(surr1, surr2) + 0.5*self.mse_loss(state_values, returns) - 0.01*dist_entropy + + # take gradient step + self.optimizer_actor.zero_grad() + self.optimizer_critic.zero_grad() + loss.mean().backward() + self.optimizer_actor.step() + self.optimizer_critic.step() + + return loss.mean().item() + + def save(self, checkpoint_path): + torch.save(self.actor.state_dict(), checkpoint_path + "_actor.pth") + torch.save(self.critic.state_dict(), checkpoint_path + "_critic.pth") + +# --- Training Loop --- + +def train(args): + # 1. Setup Environment (Dummy for now, usually you run simulation here) + # But for MAGAIL we need to collect generated trajectories. + # We need the Env class to be importable. + from Env.scenario_env import MultiAgentScenarioEnv + from Env.simple_idm_policy import ConstantVelocityPolicy # Just for init + + # Config for Env + env_config = { + "data_directory": args.data_dir, + "is_multi_agent": True, + "num_controlled_agents": 3, # Dynamic + "horizon": 200, + "use_render": False, + "sequential_seed": True, + "start_scenario_index": 0, + "num_scenarios": args.num_scenarios # Use argument + } + + # Ideally we use a wrapper for RL + # env = MultiAgentScenarioEnv(config=env_config) # This requires Waymo data loader setup + + # 2. Setup Models + state_dim = 45 + action_dim = 2 + + ppo_agent = PPO(state_dim, action_dim) + discriminator = Discriminator(state_dim, action_dim).cuda() + disc_optimizer = optim.Adam(discriminator.parameters(), lr=3e-4) + disc_criterion = nn.BCELoss() + + # 3. Load Expert Data + expert_dataset = MAGAILExpertDataset(args.expert_data_dir) + # Ensure batch_size is not larger than dataset + if len(expert_dataset) < args.batch_size: + print(f"Warning: Expert dataset size {len(expert_dataset)} < batch_size {args.batch_size}. Adjusting batch_size.") + args.batch_size = len(expert_dataset) + if args.batch_size == 0: + raise ValueError("Expert dataset is empty!") + + expert_loader = DataLoader(expert_dataset, batch_size=args.batch_size, shuffle=True, drop_last=True) + + # Create an infinite iterator + def cycle(loader): + while True: + for batch in loader: + yield batch + expert_iter = cycle(expert_loader) + + # 4. Initialize Env + from Env.expert_replay_env import ExpertReplayEnv # Using ReplayEnv for config, but we need ScenarioEnv for simulation? + # Actually we need MultiAgentScenarioEnv for interactive training, not Replay. + from Env.scenario_env import MultiAgentScenarioEnv + from Env.simple_idm_policy import ConstantVelocityPolicy # Placeholder policy for init + + # 2. Setup Models + # Determine state dim from environment if possible, or use fixed + # Expert data has 45 dim? + # But Env might return something else if we are using default ScenarioEnv settings. + # ScenarioEnv returns list of obs. + # The error says: "mat1 and mat2 shapes cannot be multiplied (1x108 and 45x256)" + # This means the Env is returning 108-dim observation (MetaDrive default + Lidar), + # but our Actor expects 45 (which is what we saved in expert data). + + # We must align the environment observation space with our expert data format. + # Our ExpertReplayEnv used a custom _get_all_obs. + # We need to inject that same logic into the training env, OR + # subclass MultiAgentScenarioEnv in the training script to override observation. + + class MAGAILScenarioEnv(MultiAgentScenarioEnv): + def _get_all_obs(self): + # Same logic as ExpertReplayEnv to ensure compatibility + obs_dict = {} + for agent_id, vehicle in self.controlled_agents.items(): + # 1. Ego State + ego_state = [ + vehicle.position[0], vehicle.position[1], + vehicle.velocity[0], vehicle.velocity[1], + vehicle.heading_theta + ] + + # 2. Neighbors + candidates = [] + for other_id, other_vehicle in self.engine.agent_manager.active_agents.items(): + if other_id == agent_id: + continue + dist = np.linalg.norm(vehicle.position - other_vehicle.position) + if dist < 30.0: + candidates.append((dist, other_vehicle)) + + candidates.sort(key=lambda x: x[0]) + top_10 = candidates[:10] + + neighbor_feats = [] + for _, neighbor in top_10: + neighbor_feats.extend([ + neighbor.position[0] - vehicle.position[0], + neighbor.position[1] - vehicle.position[1], + neighbor.velocity[0], + neighbor.velocity[1] + ]) + + missing = 10 - len(top_10) + if missing > 0: + neighbor_feats.extend([0.0] * (4 * missing)) + + obs = np.array(ego_state + neighbor_feats, dtype=np.float32) + obs_dict[agent_id] = obs + return obs_dict + + env = MAGAILScenarioEnv(config=env_config, agent2policy={}) # Pass empty dict if we control all externally + + print("Starting training...") + + # Tensorboard + try: + from torch.utils.tensorboard import SummaryWriter + writer = SummaryWriter(log_dir=args.log_dir) + except ImportError: + print("TensorBoard not installed. Logging to console only.") + writer = None + + global_step = 0 + + for i_episode in range(args.max_episodes): + # --- 1. Collect Rollouts (Interaction) --- + memory = {'states': [], 'actions': [], 'logprobs': [], 'rewards': [], 'next_states': [], 'dones': []} + + # Prepare seed + available_scenarios = env.config["num_scenarios"] + start_index = env.config["start_scenario_index"] + seed = np.random.randint(start_index, start_index + available_scenarios) + + # Reset Env + try: + # MetaDrive sometimes complains about uncleared objects if reset happens too fast or with lingering objs + # We can try to force clear before reset or handle exception + # But standard reset should handle it. + # The error "You should clear all generated objects..." means some manager didn't clear its objects. + # This is likely due to TrafficManager or AgentManager holding refs. + + # Re-creating env is safer but slower. + # Let's try closing and re-creating if reset fails frequently. + # Or just ignore this error and try reset again? No, reset failing is fatal usually. + + # Hack: Manually clear objects if we can access engine + if env.engine is not None: + env.engine.clear_objects(list(env.engine.get_objects().keys())) + + obs_dict = env.reset(seed=seed) + except Exception as e: + # print(f"Env reset failed: {e}. Recreating environment...") + try: + env.close() + except: + pass + + # Ensure engine is closed properly + from metadrive.engine.engine_utils import close_engine + try: + close_engine() + except Exception as e2: + # Force cleanup of singleton if close failed + from metadrive.engine.base_engine import BaseEngine + if BaseEngine.singleton is not None: + BaseEngine.singleton = None + + # Also need to clear ShowBase + try: + from direct.showbase.ShowBase import ShowBase + if hasattr(base, 'destroy'): + base.destroy() + except: + pass + + # Brutal force: delete base from builtins if it exists + import builtins + if hasattr(builtins, 'base'): + del builtins.base + + # print(f"Error closing engine: {e2}") + + # Explicitly delete old env object to free memory + del env + import gc + gc.collect() + + env = MAGAILScenarioEnv(config=env_config, agent2policy={}) + obs_dict = env.reset(seed=seed) + + episode_reward = 0 + steps = 0 + + # Rollout loop + while True: + # Select actions for all agents + actions = {} + action_logprobs = {} + + # obs_dict: {agent_id: obs} + # MultiAgentScenarioEnv usually returns a dict {agent_id: obs} + # BUT wait, check scenario_env.py implementation + + if isinstance(obs_dict, list): + # This happens if the environment returns a list instead of a dict + # MultiAgentScenarioEnv._get_all_obs returns a list in original implementation? + # Let's check scenario_env.py + # If it returns list, we need to map it to agent ids or just iterate + pass + + # Temporary fix if it returns list (which means my previous edit to Env/expert_replay_env.py + # changed it there, but maybe not in Env/scenario_env.py which we are using here!) + + if isinstance(obs_dict, list): + # We need agent IDs to step + # In MultiAgentScenarioEnv, controlled_agents is a dict. + # If obs is a list, it probably corresponds to controlled_agents.values() order? + # This is risky. + # Let's assume obs_dict is actually just observations. + # We need to keys to create action dict. + + current_agent_ids = list(env.controlled_agents.keys()) + # Ensure length matches + if len(obs_dict) != len(current_agent_ids): + # print(f"Warning: Obs list len {len(obs_dict)} != agents {len(current_agent_ids)}") + pass + + # Reconstruct dict + new_obs_dict = {} + for i, agent_id in enumerate(current_agent_ids): + if i < len(obs_dict): + new_obs_dict[agent_id] = obs_dict[i] + obs_dict = new_obs_dict + + for agent_id, obs in obs_dict.items(): + act, logprob = ppo_agent.select_action(obs) # Select action returns numpy + actions[agent_id] = act.flatten() # (2,) + action_logprobs[agent_id] = logprob # scalar + + # Step Env + next_obs_dict, rewards, dones, infos = env.step(actions) + + # Store in memory + for agent_id, obs in obs_dict.items(): + if agent_id in actions: + memory['states'].append(obs) + memory['actions'].append(actions[agent_id]) + memory['logprobs'].append(action_logprobs[agent_id]) + + # Store standard environmental reward for logging (not used for update in GAIL) + # For GAIL update we use Discriminator reward later + memory['rewards'].append(0) # Placeholder + + # Next state + if agent_id in next_obs_dict: + memory['next_states'].append(next_obs_dict[agent_id]) + memory['dones'].append(False) + else: + # Agent finished/vanished + # We need a dummy next state or handle done correctly + # Just duplicate current state and mark done? + memory['next_states'].append(obs) + memory['dones'].append(True) + + obs_dict = next_obs_dict + steps += 1 + + if dones["__all__"] or steps >= 200: # Limit horizon + break + + # Initialize losses to 0/None before potential loop skip + disc_loss = torch.tensor(0.0) + ppo_loss = 0.0 + all_gail_rewards = [0.0] + + # --- 2. Train Discriminator --- + # Convert policy memory to tensors + policy_states = torch.FloatTensor(np.array(memory['states'])).cuda() + policy_actions = torch.FloatTensor(np.array(memory['actions'])).cuda() + + # Sample expert batch + expert_batch = next(expert_iter) + + expert_states = expert_batch['state'].cuda() + expert_actions = expert_batch['action'].cuda() + + # Minibatch size matching + batch_size = min(policy_states.size(0), expert_states.size(0)) + + if batch_size > 0: # Only train if we have data + policy_states = policy_states[:batch_size] + policy_actions = policy_actions[:batch_size] + expert_states = expert_states[:batch_size] + expert_actions = expert_actions[:batch_size] + + # Update Discriminator + # Label 1 for Expert, 0 for Policy + # Train Expert + disc_optimizer.zero_grad() + + exp_preds = discriminator(expert_states, expert_actions) + exp_loss = disc_criterion(exp_preds, torch.ones_like(exp_preds)) + + pol_preds = discriminator(policy_states.detach(), policy_actions.detach()) # Detach policy data + pol_loss = disc_criterion(pol_preds, torch.zeros_like(pol_preds)) + + disc_loss = exp_loss + pol_loss + disc_loss.backward() + disc_optimizer.step() + + # --- 3. Update Policy with GAIL Rewards --- + # Reward = -log(1 - D(s, a)) + # Or more stable: log(D(s, a)) ? Original GAIL uses -log(1-D) which is log(D) roughly. + # Let's use -log(1 - D(s, a) + eps) + + # Actually PPO needs the full trajectory for GAE. + # So we should compute rewards for ALL policy samples in memory. + + all_policy_states = torch.FloatTensor(np.array(memory['states'])).cuda() + all_policy_actions = torch.FloatTensor(np.array(memory['actions'])).cuda() + + with torch.no_grad(): + all_d_val = discriminator(all_policy_states, all_policy_actions) + all_gail_rewards = -torch.log(1 - all_d_val + 1e-8).cpu().numpy().flatten() + + # Replace placeholders + memory['rewards'] = all_gail_rewards.tolist() + + # Update PPO + ppo_loss = ppo_agent.update(memory) + + # Clean up memory + del policy_states, policy_actions, expert_states, expert_actions, exp_preds, exp_loss, pol_preds, pol_loss + del all_policy_states, all_policy_actions, all_d_val + torch.cuda.empty_cache() + else: + print(f"Episode {i_episode}: No data collected (Env might have crashed or no agents). Skipping update.") + + # --- 4. Logging --- + if writer: + writer.add_scalar('Loss/Discriminator', disc_loss.item(), i_episode) + writer.add_scalar('Loss/Policy', ppo_loss, i_episode) + writer.add_scalar('Reward/Mean_GAIL', np.mean(all_gail_rewards), i_episode) + + print(f"Episode {i_episode}: Disc Loss {disc_loss.item():.4f} | PPO Loss {ppo_loss:.4f} | Mean Reward {np.mean(all_gail_rewards):.4f}") + + if i_episode % 50 == 0: + ppo_agent.save(os.path.join(args.log_dir, f"model_{i_episode}")) + + env.close() + if writer: + writer.close() + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("--expert_data_dir", type=str, default="data/training_data", help="Directory with .pkl expert data") + parser.add_argument("--data_dir", type=str, default="data/exp_filtered", help="Waymo data dir for Env") + parser.add_argument("--batch_size", type=int, default=1024) + parser.add_argument("--max_episodes", type=int, default=1000) + parser.add_argument("--num_scenarios", type=int, default=100) + parser.add_argument("--log_dir", type=str, default="runs/magail_exp") + + args = parser.parse_args() + + # Create log dir + os.makedirs(args.log_dir, exist_ok=True) + + train(args)