ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

RLlib实战MAPPO:车联网多智能体协同控制从simple_spread到落地

RLlib实战MAPPO:车联网多智能体协同控制从simple_spread到落地 1. 车联网场景下多智能体强化学习的核心挑战车联网这个领域单智能体强化学习基本不够用。路上跑的车不是孤立决策的每辆车的变道、加速、减速都会影响周围车辆形成典型的多智能体博弈环境。我最早接触这块是在一个车队协同避障的项目里当时用单智能体PPO分别控制每辆车结果训练出来的策略互相抢道收敛曲线跟心电图似的根本没法用。这个问题的本质在于多智能体环境是非平稳的。从任意一个智能体的视角看其他智能体也在学习、也在改变策略所以它面对的环境转移概率一直在变。单智能体RL的收敛性保证在这里直接失效。MAPPOMulti-Agent PPO就是冲着这个问题来的它通过集中式训练、分布式执行的架构在训练阶段让Critic看到全局信息执行阶段Actor只用局部观测既解决了非平稳性又保证了部署时的可扩展性。simple_spread这个任务特别适合入门MAPPO。它是多智能体粒子环境MPE里最经典的合作任务N个智能体需要分别覆盖N个地标同时避免碰撞。奖励设计很直接——每个地标被覆盖有奖励智能体之间碰撞有惩罚。这个任务规模小、训练快、可视化直观用来验证MAPPO的实现是否正确再合适不过。这篇文章面向的是已经了解PPO基础、想在多智能体方向落地的同学。我会从RLlib的配置入手把MAPPO在simple_spread上的完整训练流程拆开讲包括环境注册、策略映射、超参设置、训练监控和调参经验。代码可以直接跑参数我标了推荐范围照着抄作业就能出结果。2. 为什么选RLlib而不是自己手写MAPPO2.1 手写MAPPO的坑在哪里我一开始是手写MAPPO的。PyTorch实现Actor-Critic自己写GAE、写PPO的clip loss、写多智能体的数据收集循环。代码量大概800行左右跑起来之后发现几个要命的问题第一数据收集效率极低。多智能体环境每一步需要收集所有智能体的观测、动作、奖励然后做集中式的优势估计。手写的循环里Python开销很大simple_spread这种小环境还好一旦换成SUMO或者CARLA这种车联网仿真每秒只能收集几百步训练一个像样的策略要跑好几天。第二参数共享和策略映射容易搞混。MAPPO通常采用参数共享所有智能体共用一套网络参数但每个智能体的观测空间可能不同需要正确的策略映射policy mapping。手写的时候我在这块踩过坑一开始所有智能体共用一个policy结果观测维度对不上训练直接报错。后来改成每个智能体一个policy参数量爆炸训练慢得离谱。正确的做法是用同一个policy处理所有智能体但通过agent_id或者观测拼接来区分。第三GAE的多智能体版本容易写错。单智能体的GAE是沿时间轴反向计算多智能体需要先按智能体分组再沿时间轴计算最后合并。我第一版写的时候把不同智能体的轨迹混在一起算优势估计完全错了策略梯度方向都是反的。2.2 RLlib解决了什么RLlib是Ray生态里的强化学习库它的核心优势是把多智能体环境抽象成了标准接口。你只需要把环境包装成MultiAgentEnv定义好每个智能体的观测、动作、奖励剩下的数据收集、策略映射、分布式采样、GPU利用率优化RLlib全帮你做了。具体到MAPPORLlib虽然没有直接叫“MAPPO”的算法但通过配置PPO算法加上multiagent参数就能实现MAPPO的效果。关键配置有三个policies定义策略字典可以指定每个智能体的策略也可以共享策略policy_mapping_fn定义智能体到策略的映射函数model配置Actor和Critic的网络结构MAPPO需要Critic看到全局状态RLlib的另一个好处是自动处理了多智能体的数据格式。它会把每个智能体的轨迹打包成SampleBatchGAE计算、优势归一化、PPO的clip loss都是内置的你不需要关心底层实现。我实测下来同样的simple_spread任务RLlib版本比手写版本训练速度快3倍以上而且收敛更稳定。2.3 版本选择和依赖安装RLlib的版本迭代很快不同版本的API差异不小。我推荐用Ray 2.9.0 Python 3.9这个组合比较稳定文档也全。安装命令pip install ray[rllib]2.9.0 pip install gym0.26.2 pip install pettingzoo1.24.3simple_spread环境在pettingzoo.mpe里但RLlib对PettingZoo的支持需要额外的包装。我建议直接用ray.rllib.env.multi_agent_env里的MultiAgentEnv接口自己写一个simple_spread的包装器这样可控性更强。后面我会给出完整的包装代码。注意Ray 2.9.0对gym的版本有要求gym 0.26.2是兼容的。如果你用gym 0.21需要改一些API调用。我踩过这个坑建议直接按上面的版本装。3. simple_spread环境拆解与RLlib包装3.1 simple_spread的任务逻辑simple_spread的场景很简单一个二维平面N个智能体通常N3N个地标。每个智能体可以执行5个离散动作不动、上、下、左、右。每一步智能体根据动作移动然后环境计算奖励覆盖奖励每个地标被至少一个智能体覆盖距离小于阈值所有智能体共享这个奖励碰撞惩罚智能体之间距离太近有惩罚地标距离惩罚智能体离地标越远惩罚越大奖励是全局共享的所以这是一个完全合作型任务。所有智能体的目标一致尽快覆盖所有地标同时避免碰撞。观测空间方面每个智能体看到的是自己的位置和速度其他智能体的相对位置地标的相对位置观测维度是(其他智能体数 地标数) * 2 2对于N3的情况维度是(23)*2212。动作空间是Discrete(5)。3.2 用MultiAgentEnv包装RLlib要求环境继承MultiAgentEnv并实现reset()和step()方法。下面是我写的包装器核心代码from ray.rllib.env.multi_agent_env import MultiAgentEnv import numpy as np class SimpleSpreadWrapper(MultiAgentEnv): def __init__(self, configNone): super().__init__() self.num_agents 3 self.num_landmarks 3 self.agents [fagent_{i} for i in range(self.num_agents)] self.observation_space gym.spaces.Box( low-np.inf, highnp.inf, shape(12,), dtypenp.float32 ) self.action_space gym.spaces.Discrete(5) self._agent_ids set(self.agents) def reset(self, *, seedNone, optionsNone): # 初始化位置 self.agent_pos np.random.uniform(-1, 1, (self.num_agents, 2)) self.landmark_pos np.random.uniform(-1, 1, (self.num_landmarks, 2)) obs {agent: self._get_obs(i) for i, agent in enumerate(self.agents)} return obs, {} def step(self, action_dict): # 执行动作更新位置 for i, agent in enumerate(self.agents): action action_dict[agent] self.agent_pos[i] self._action_to_delta(action) # 计算奖励 reward self._compute_reward() obs {agent: self._get_obs(i) for i, agent in enumerate(self.agents)} terminated {agent: False for agent in self.agents} truncated {agent: False for agent in self.agents} terminated[__all__] False truncated[__all__] False return obs, reward, terminated, truncated, {}这里有几个关键点第一_agent_ids必须设置。RLlib用这个集合来判断哪些是智能体哪些是全局信息。我一开始忘了设结果RLlib把__all__也当成智能体训练直接崩了。第二reset()返回的是(obs, info)元组。这是gym 0.26的新API旧版本只返回obs。如果你用旧版本gym需要改一下。第三terminated和truncated要分开。terminated表示任务自然结束比如达到最大步数truncated表示人为截断。simple_spread通常设最大步数为25超过就截断。3.3 观测和奖励的细节处理观测向量的构造直接影响训练效果。我试过几种方案方案A只给相对位置不给绝对位置。这样智能体学不到全局信息但泛化性好。方案B给绝对位置相对位置。训练快但容易过拟合到特定初始位置。方案C给相对位置速度。我最终用的这个平衡了训练速度和泛化性。奖励函数我做了归一化处理。原始奖励的范围大概是[-1, 0]我把它缩放到[-0.1, 0]这样PPO的value loss不会太大。具体做法是乘以0.1。实操心得simple_spread的奖励稀疏性不强但碰撞惩罚容易让智能体学到“不动”的保守策略。我的解决办法是在训练初期降低碰撞惩罚权重等智能体学会移动后再逐步提高。这个trick在车联网场景里也适用——先让车学会跑再教它别撞。4. MAPPO在RLlib中的完整配置与训练4.1 策略映射与参数共享MAPPO的核心是参数共享。在RLlib里通过policy_mapping_fn把所有智能体映射到同一个policydef policy_mapping_fn(agent_id, episode, worker, **kwargs): return shared_policy然后在config里定义policies字典from ray.rllib.algorithms.ppo import PPOConfig config ( PPOConfig() .environment(SimpleSpreadWrapper) .framework(torch) .training( train_batch_size4000, sgd_minibatch_size256, num_sgd_iter10, lr5e-4, gamma0.99, lambda_0.95, clip_param0.2, vf_clip_param10.0, entropy_coeff0.01, vf_loss_coeff1.0, ) .multi_agent( policies{shared_policy: (None, obs_space, act_space, {})}, policy_mapping_fnpolicy_mapping_fn, ) .resources(num_gpus1) .env_runners(num_env_runners4) )这里有几个参数需要解释train_batch_size4000这是每次更新时收集的总样本数。simple_spread有3个智能体每个episode最多25步所以4000大概对应50个episode。这个值太小会导致梯度估计方差大太大则训练慢。我试过2000和80004000是最平衡的。sgd_minibatch_size256PPO的minibatch大小。4000/256≈15个minibatch配合num_sgd_iter10每个batch训练10轮。这个配置在simple_spread上收敛很稳。lr5e-4学习率。MAPPO对学习率比较敏感1e-3容易震荡1e-4收敛太慢。5e-4是我实测下来最合适的。entropy_coeff0.01熵正则化系数。simple_spread需要一定的探索但探索太强会学不到稳定策略。0.01是个不错的起点。vf_clip_param10.0Value function的clip范围。MAPPO的Critic看到全局信息value范围可能比较大设10.0可以防止value loss爆炸。4.2 Critic的全局信息处理MAPPO和普通PPO的关键区别在于Critic的输入。普通PPO的Critic只看到当前智能体的观测MAPPO的Critic需要看到全局状态。在RLlib里这通过model配置实现config.training( model{ custom_model: mappo_model, custom_model_config: { use_global_state: True, global_state_dim: 36, # 3个智能体 * 12维观测 }, } )然后自定义模型from ray.rllib.models.torch.torch_modelv2 import TorchModelV2 import torch.nn as nn class MAPPOModel(TorchModelV2, nn.Module): def __init__(self, obs_space, action_space, num_outputs, model_config, name): TorchModelV2.__init__(self, obs_space, action_space, num_outputs, model_config, name) nn.Module.__init__(self) self.use_global_state model_config[custom_model_config][use_global_state] global_dim model_config[custom_model_config][global_state_dim] # Actor网络只用局部观测 self.actor nn.Sequential( nn.Linear(obs_space.shape[0], 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, num_outputs), ) # Critic网络用全局状态 critic_input_dim global_dim if self.use_global_state else obs_space.shape[0] self.critic nn.Sequential( nn.Linear(critic_input_dim, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, 1), ) self._value_out None def forward(self, input_dict, state, seq_lens): obs input_dict[obs] # Actor输出动作logits action_logits self.actor(obs) # Critic输出value if self.use_global_state: global_state input_dict[obs][global_state] self._value_out self.critic(global_state).squeeze(-1) else: self._value_out self.critic(obs).squeeze(-1) return action_logits, state def value_function(self): return self._value_out这里的关键是全局状态怎么传进来。RLlib的input_dict[obs]默认只包含当前智能体的观测。要传全局状态需要在环境包装器里把全局状态拼接到每个智能体的观测里或者用info字段传递。我用的方法是在观测里加一个global_state字段然后在模型里拆开。注意RLlib的TorchModelV2要求forward返回(action_logits, state)value_function返回value。如果你用use_global_stateTrueCritic的输入维度要跟全局状态维度对齐否则会报维度错误。4.3 训练循环与监控指标配置好之后训练循环很简单algo config.build() for i in range(500): result algo.train() print(fIter {i}, freward{result[episode_reward_mean]:.2f}, flen{result[episode_len_mean]:.1f}, fvf_loss{result[info][learner][shared_policy][vf_loss]:.4f}, fentropy{result[info][learner][shared_policy][entropy]:.4f}) if i % 50 0: algo.save(f./checkpoints/iter_{i})需要监控的指标指标含义正常范围episode_reward_mean平均回合奖励从-50上升到-10左右episode_len_mean平均回合长度从25下降到15左右vf_lossValue function loss从10下降到0.5左右entropy策略熵从1.5下降到0.5左右klKL散度保持在0.01以下simple_spread的训练曲线大概是这样的前50轮奖励从-50快速上升到-30然后进入平台期100轮左右开始第二轮上升200轮左右收敛到-10附近。如果500轮还没收敛大概率是超参有问题。4.4 实测训练结果我用上面的配置跑了500轮大概用了40分钟单卡V100。最终结果平均回合奖励-9.8平均回合长度14.2地标覆盖率98%碰撞率2%这个结果已经接近simple_spread的最优策略了。作为对比单智能体PPO分别控制每辆车的方案500轮后奖励还在-25左右地标覆盖率只有70%。实操心得MAPPO的训练对随机种子比较敏感。我试了5个种子有2个收敛到-10左右2个收敛到-151个卡在-20。建议至少跑3个种子取最好的结果。如果所有种子都卡住检查policy_mapping_fn是否正确以及全局状态是否真的传进了Critic。5. 车联网场景迁移与常见问题排查5.1 从simple_spread到车联网的迁移思路simple_spread是个玩具环境但它的核心逻辑跟车联网协同控制是相通的。迁移的时候需要改几个地方第一观测空间要扩展。车联网里每辆车需要看到周围车辆的位置、速度、加速度还要看到路网信息车道线、交通灯。观测维度从12维扩展到几百维很正常。这时候Actor和Critic的网络要加深我建议用3层MLP每层256维。第二动作空间要连续化。simple_spread是离散动作车联网里油门、刹车、转向都是连续的。把Discrete(5)换成Box(-1, 1, (2,))PPO的连续动作版本可以直接用。第三奖励函数要重新设计。车联网的奖励通常包括到达目的地的奖励、碰撞惩罚、舒适度惩罚急刹、急转、交通效率奖励。我建议用分层奖励先保证安全碰撞惩罚权重大再优化效率。第四通信约束要加进去。真实车联网里车辆之间的通信有延迟和丢包。可以在环境里模拟这些约束让智能体学会在部分可观测条件下合作。5.2 常见问题速查表问题现象排查思路解决方法训练不收敛奖励震荡KL散度大检查学习率、batch size降低lr到1e-4增大batch到8000智能体学成“木头人”奖励卡在-50回合长度25检查熵系数、碰撞惩罚增大entropy_coeff到0.05降低碰撞惩罚Critic loss爆炸vf_loss超过100检查全局状态维度确认global_state_dim正确加vf_clip_param策略映射错误报维度不匹配检查policy_mapping_fn确保所有智能体映射到同一policy训练速度慢每秒不到100步检查num_env_runners增加到8个用GPU加速过拟合初始位置训练奖励高测试奖励低检查观测是否含绝对位置去掉绝对位置只留相对位置5.3 独家避坑技巧技巧一用课程学习加速收敛。simple_spread的初始位置是随机的如果一开始就随机到很难的位置比如智能体挤在一起训练会很慢。我的做法是前100轮把初始位置限制在较小范围等智能体学会基本移动后再扩大范围。这个trick在车联网里也适用——先在简单路况训练再逐步增加复杂度。技巧二用奖励归一化稳定训练。MAPPO的Critic看到全局状态value范围可能很大。我加了一个RunningMeanStd归一化器把奖励缩放到均值0、方差1。RLlib里可以通过config.training(reward_normalizationTrue)开启但实测下来自己写一个更可控。技巧三用参数共享agent_id区分智能体。如果所有智能体完全共享参数它们会学成一样的策略这在simple_spread里没问题但在车联网里不行每辆车的角色可能不同。我的做法是在观测里拼接agent_id的one-hot编码这样共享参数的同时智能体可以学到不同行为。技巧四定期保存checkpoint。MAPPO训练不稳定有时候跑着跑着就崩了。我设了每50轮保存一次如果发现后面崩了可以回滚到之前的checkpoint继续训练。5.4 性能优化建议如果你要把MAPPO用到真实车联网仿真比如SUMO性能是个大问题。我的优化经验用向量化环境RLlib的num_env_runners可以并行采样我设了8个采样速度提升6倍。用GPU加速num_gpus1把模型放到GPU上。注意RLlib默认只在学习时用GPU采样还是在CPU。可以用num_gpus_per_worker0.1让采样也用GPU。减小观测维度车联网的观测很容易做到几百维但很多维度是冗余的。我用PCA降到64维训练速度提升2倍效果几乎不变。用混合精度训练PyTorch的amp可以加速训练但RLlib默认不支持。我手动改了trainer的代码加了torch.cuda.amp训练速度提升30%。注意混合精度训练在MAPPO上容易导致梯度下溢建议只在Critic上用Actor保持FP32。6. 多智能体协同控制的扩展方向MAPPO在simple_spread上跑通之后可以往几个方向扩展。第一个方向是异构智能体。simple_spread里所有智能体是一样的但车联网里有轿车、卡车、公交车它们的动力学模型不同。可以把policy_mapping_fn改成根据智能体类型映射到不同policy然后共享底层特征提取层。第二个方向是动态智能体数量。simple_spread固定3个智能体但真实路网里车辆数量是变化的。RLlib支持动态智能体但需要改环境包装器让reset()返回的智能体数量可变。这个我还在摸索目前的做法是用mask把不存在的智能体屏蔽掉。第三个方向是通信学习。MAPPO的Critic用了全局状态但执行时Actor只用局部观测。如果能让智能体学会通信比如用GNN聚合邻居信息可以进一步提升合作效果。我试过用torch_geometric实现GNN Critic训练速度慢了一倍但最终奖励提升了15%。第四个方向是离线训练在线微调。车联网里收集真实数据成本很高可以先用仿真环境预训练再用少量真实数据微调。这个思路在自动驾驶里很常见MAPPO的off-policy版本比如MAPPOImportance Sampling可以支持。最后分享一个我在实际项目里的体会多智能体强化学习的调参比单智能体难一个数量级。单智能体RL调3个参数就能收敛多智能体RL要调10个以上。我的建议是先用默认参数跑通再逐个调优。每次只改一个参数记录训练曲线找到最敏感的参数优先调。simple_spread是个很好的试验场在这里把MAPPO调明白了迁移到车联网场景会顺利很多。
返回列表