文章目录(Table of Contents)
简介
这里会简单介绍一下 EPyMARL
的使用。最近在看多智能体强化学习的算法,但是感觉只看算法和数学式子理解还是不深刻,因此希望结合代码一起来看。最后选择了使用 EPyMARL
来入手进行学习。
EPyMARL 简单介绍
EPyMARL
是一个基于 Python 的代码库,专为合作型多智能体深度强化学习(MARL)算法的训练而设计。EPyMARL
对原有的 PyMARL
进行了扩展,后者已经包含了若干种 MARL 算法,如 IQL、COMA、VDN、QMIX 和 QTRAN。不过,原版的 PyMARL 仅支持 StarCraft 多智能体挑战,许多实现细节在 PyMARL 中被固定,不便于修改。举例来说,在 PyMARL 中,所有智能体共享相同的参数,策略网络的首层是一个 RNN,而且目标网络采用硬更新方式。
在 EPyMARL
代码库中,不仅兼容 OpenAI Gym,还增加了多种算法(如 IA2C、IPPO、MADDPG、MAA2C、MAPPO),提供了更多关于算法实现细节的灵活选项(例如可以选择不共享参数、采用硬更新或软更新、以及奖励的标准化),并且包括了用于超参数优化的代码。
参考资料
- epymarl,官方仓库代码;
- The Extended PyMARL Codebase for Multi-Agent Reinforcement Learning,介绍 EPyMARL 的使用;
多智能体强化学习算法
在多智能体强化学习的研究中,环境包含多个需要协作以实现共同目标(这一目标形式化为一个 Dec-POMDP)的智能体。在每一时间步骤中,每个智能体都能获得环境的部分观察,并依据其历史观察来采取行动,这些行动遵循各自的行为策略。环境接收到所有智能体的联合行动,并为每个智能体提供新的观察结果和一个所有智能体共享的单一标量奖励。合作型 MARL 的目标是为每个智能体计算出一套策略,以最大化整个游戏回合期间累积的折现奖励总和。
目前有九种常见的 MARL 算法(这是截至这篇论文,大致 2021 年)。这些算法分为三个类别:
独立学习算法
在这个类别里,每个智能体都独立进行训练,不考虑环境中其他智能体的影响(相当于把其他智能体当成了环境的一部分)。这一类别包括三种算法:
- 独立 Q 学习(IQL):每个智能体根据自己的行动轨迹,采用 DQN 算法进行训练。
- 独立优势演员-评论家(IA2C):每个智能体根据自己的行动轨迹,采用 A2C 算法进行训练。
- 独立近端策略优化(IPPO):每个智能体根据自己的行动轨迹,采用 PPO 算法进行训练。
集中式策略梯度算法
这一类别包含了 actor-critic 算法,其中 actor 部分是去中心化的(仅基于各个智能体的行动轨迹),而 critic 部分则是集中式的,基于所有智能体的联合行动轨迹来计算联合状态价值(joint state value function, V 值)或联合状态-行动价值(joint state-action value function, Q 值)。这一类别包括四种算法:
- 多智能体深度决定性策略梯度(MADDPG):这是 DDPG 算法的多智能体版本,评论家部分被集中式训练,用以估计联合状态-行动价值。这部分算法可以参考链接,【多智能体强化学习】MADDPG 论文笔记。
- 反事实多智能体策略梯度(COMA):这是一种 actor-critic 算法,critic 部分计算集中式状态-行动价值函数。COMA 的主要创新之处在于,它采用了改进的优势估计方法(advantage estimation),能够基于共享奖励进行合理的信用分配。
- 多智能体优势演员-评论家(MAA2C):这是 A2C 算法的多智能体版本,critic 部分是集中式训练的状态价值函数(centrally-trained state value function),取决于所有智能体的联合行动轨迹。
- 多智能体近端策略优化(MAPPO):这是 PPO 算法的多智能体版本,评论家部分是集中式训练的状态价值函数,取决于所有智能体的联合行动轨迹( critic is a centrally-trained state value function conditioned on the joint trajectory of all agents)。这部分算法内容可以参考,【多智能体强化学习】MAPPO 论文笔记。
价值分解算法
这一类别的算法尝试将所有智能体接收到的共享奖励,根据每个智能体的贡献分解为个体效用。这一类别包括两种算法:
- 价值分解网络(VDN):这是一种基于 IQL 的算法,每个智能体都有一个网络来估计自己的 Q 值。所有智能体的 Q 值相加,用以计算联合行动的 Q 值,通过标准 Q 学习算法进行训练。这部分算法内容可以参考,【多智能体强化学习】VDN 论文笔记。
- QMIX:这是另一种价值分解算法,不同于 VDN 的简单加和,QMIX 使用一个带有可学习参数的混合神经网络来估计联合行动的 Q 值,这允许对共享奖励进行更复杂的分解。这部分算法内容可以参考,【多智能体强化学习】QMIX 论文笔记。
EPyMARL 简单使用
EPyMARL 的安装
EPyMARL
和 PyMARL
都是基于 Python 3 编写的(推荐使用 Python 3.7 或更高版本进行的)。在开始安装 EPyMARL 的依赖之前,建议创建一个 Conda 环境或虚拟环境,这样可以帮助管理依赖并避免潜在的版本冲突。接着可以通过在终端中执行以下命令来安装 EPyMARL
的依赖组件:
- git clone https://github.com/uoe-agents/epymarl.git
- cd epymarl/
- pip install -r requirements.txt
我自己安装下来,有两个是需要注意的:
- gym 的版本不能使用最新的,需要使用
gym==0.21.0
; - setuptools 的版本需要注意,否则上面的 gym 会安装失败;pip: 'extras_require' must be a dictionary whose values are strings or lists of strings containing valid project/version requirement specifiers。
- pip install setuptools==65.5.0 pip==21
- pip install wheel==0.38.0
运行 Gym 环境的实验
在 EPyMARL
中,作者对九种前述的 MARL 算法在三个 Gym 环境中进行了基准测试,这三个环境分别是:多智能体粒子环境(MPE)、基于等级的觅食(LBF)和多机器人仓库(RWARE)。想要在这三个环境中的任何一个进行实验,首先需要确保相应的环境已经安装。可以通过以下 GitHub 仓库安装这些环境:
基于等级的觅食环境(level based foraging),也可以通过以下命令安装:
- pip install lbforaging
基于多机器人仓库环境(Multi-Robot Warehouse),也可以通过以下命令安装:
- pip install rware
多智能体粒子环境,需要 clone EPyMARL
作者的版本进行安装(我这里没有尝试):
- git clone https://github.com/semitable/multiagent-particle-envs
- cd multiagent-particle-envs
- pip install -e .
EPyMARL
支持所有在 Gym 中注册的环境。与 Gym 框架的唯一区别在于,返回的奖励应该是一个元组,包含每个智能体的奖励。
在这个合作框架中,我们会把这些奖励相加(相当于拿到的是一个团队的奖励,这个部分是在 _GymmaWrapper 里面实现的)。支持开箱即用的环境是那些在 Gym 中自动注册的环境,如 LBF 和 RWARE 环境。 要在 Gym 环境中运行实验,请执行以下命令:
- python src/main.py --config=qmix --env-config=gymma with
- env_args.time_limit=50 env_args.key="lbforaging:Foraging-10x10-3p-3f-v1"
在上述命令中:
--config
是指在src/config/algs
中的配置文件,这里是对应不同算法的配置文件。--env-config=gymma
表示使用与 Gym 兼容的多智能体合作环境包装器,这个环境的 Wrapper 会自动将奖励合并(与用于 StarCraft II 环境的 `sc2` 包装器相对)。env_args.time_limit=50
用来设置最大剧集长度为 50 个时间步。env_args.key="..."
用来提供 Gym 环境的 ID,在这个 ID 中,lbforaging
: 指的是模块名称(即会自动执行import lbforaging
)。
配置文件定义了算法或环境的默认设置,所有配置文件都存放在 src/config
目录下。所有实验结果将会被保存在 results 文件夹中。
EPyMARL 配置文件简单介绍
下面详细介绍一下 EPyMARL
的配置文件。
【默认参数】在 config
文件夹中,有一个 default.yaml
文件,包含了一些算法的默认超参数和不同模块。
【环境参数】在 env
文件夹中,包含了不同环境的参数。例如 gymma.yaml
,使用 gym wrapper,里面包括最大的仿真步等。
【算法参数】在 alg
文件夹中,为每种算法提供了一个配置文件。算法配置文件中的所有参数将覆盖 default.yaml
文件中的参数。以 IA2C 配置文件为例:
- action_selector: "soft_policies"
- mask_before_softmax: True
- runner: "parallel"
- buffer_size: 10
- batch_size_run: 10
- batch_size: 10
- env_args:
- state_last_action: False # critic adds last action internally
- # update the target network every {} training steps
- target_update_interval_or_tau: 0.01
- lr: 0.0005
- hidden_dim: 64
- obs_agent_id: True
- obs_last_action: False
- obs_individual_obs: False
- # use IA2C
- agent_output_type: "pi_logits"
- learner: "actor_critic_learner"
- agent: "rnn"
- entropy_coef: 0.01
- standardise_returns: False
- standardise_rewards: True
- use_rnn: True
- q_nstep: 5 # 1 corresponds to normal r + gammaV
- critic_type: "ac_critic"
- name: "ia2c"
- t_max: 20050000
在这个文件中,除了超参数外,我们还详细描述了算法的不同模块。例如,策略类型是 pi_logits
,对应于 soft policy。actor agent 的类型是 rnn
,意味着它是一个基于 RNN 的网络,并且在智能体之间共享参数。评论家的类型是 ac_critic
,表示 critic
是基于每个智能体的局部轨迹来条件化的。将这个参数改为 cv_critic
,我们就会得到 MAA2C 算法,它使用一个基于所有智能体的联合轨迹来条件化的集中式评论家。
EPyMARL 模型保存和测试
如果希望对模型进行保存,需要设置下面的两个参数:
- save_model: True # Save the models to disk
- save_model_interval: 50_000 # Save models after this many timesteps
如果希望对训练好的模型进行测试,设置下面的两个参数即可:
- checkpoint_path: "xxxx" # Load a checkpoint from this path
- evaluate: True # Evaluate model for test_nepisode episodes and quit (no training)
- render: True # Render the environment when evaluating (only when evaluate == True)
EPyMARL 代码理解
EPyMARL
是包含数个文件和上万行代码的庞大代码库。因此,初次使用它们来开发新算法可能会遇到一些挑战。在本节中,将介绍 EPyMARL
的基本代码结构,并说明如何在代码库中加入新的算法。
EPyMARL 主要文件结构
下面是 EPyMARL 代码库的主要文件夹结构:
- src
- |-- components (basic RL functionalities, such as the experience replay)
- |-- config (configuration files)
- | |-- algs (for the algorithms)
- | |-- envs (for the environments)
- |-- controllers (controllers for the action selection pipeline)
- |-- envs (environment wrappers)
- |-- learners (code for training different algorithms)
- |-- modules
- | |-- agents (network architecture for the policy networks)
- | |-- critics (network architecture for the critic networks)
- | |-- mixers (network architecture for the mixing networks)
- |-- pretrained
- |-- runners (code for the interaction between the agents and the environment)
- |-- utils
接下来我们对上面的每一个文件夹进行简单的介绍:
learners 文件夹包含实施所有网络训练的代码。首先,它在构造器中初始化了一些仅在训练期间使用的模型(如 critic 或 mixer)。它的主要功能是实现所有网络的训练(包括智能体网络和仅在训练期间使用的网络)。默认情况下,代码库在每个 episode 结束时执行梯度更新。因此,learner 类的 train 方法接收a batch of episodes 作为输入,并计算所有损失(例如,actor 和 critic 的损失),然后执行梯度更新步骤。
runner 文件夹包含两种智能体与环境交互的实现方式。第一种是 RL 的传统实现,智能体与环境的单一实例进行交互(默认使用是这一种,是 episode_runner.py
)。第二种则实现了多个并行环境实例,在每个时间步骤中,智能体与所有这些实例进行交互。
controllers 文件夹包含实现完整行动选择流程的文件。这些代码构造了网络的输入向量,更新任何 RNN 的隐藏状态(如果存在),并执行行动选择策略(例如 e-greedy、greedy 或 soft)。此外,它还负责初始化用于行动选择的智能体网络。简单来说,就是 controller 里面包含 agent 的网络,和最后动作选择的算法。比如 agent 是计算 Q 值,计算好了 Q 值之后,之后可以使用 greedy 的算法来选择 action。
modules 文件夹对应了不同算法里面的网络结构。例如在 mixer 文件夹包含了 vdn、qmix 算法的框架。在 critic 文件夹包含 central value function 等文件。
agents 文件夹属于 modules 文件夹下面,包含了用于智能体行动选择的网络架构。在 EPyMARL
中,我们设计了两种网络:一种是参数在所有智能体间共享的,另一种则没有参数共享(简称是 NS)。因此,如果我们想尝试一个不同的网络架构,例如基于 Transformer 的网络,我们只需在此文件夹中实施即可。需要注意的是,这个文件夹中的代码仅负责构建网络的输入和前向传播。所有其他的类变量,例如隐藏状态,都在其他文件中更新。
components 文件夹中有多个实现 RL 基础功能的文件,如经验回放、不同的行动选择方法(如 e-greedy 或软策略)、奖励标准化等。
EPyMARL 添加新的模块
在所有上述文件夹中,通常在 init.py
文件中有一部分代码用于将不同的实现注册到一个字典中。以 agents
文件夹为例,其 init.py
文件内容如下:
- REGISTRY = {}
- from .rnn_agent import RNNAgent
- from .rnn_ns_agent import RNNNSAgent
- REGISTRY["rnn"] = RNNAgent
- REGISTRY["rnn_ns"] = RNNNSAgent
假设想实现一个新的算法,例如 actor's network 的改进架构。如果我们为智能体开发了一个新架构,我们需要在这个字典中进行注册。例如,假设我们在 attention_agent.py
文件中实现了一个基于注意力机制的智能体架构。我们现在应该在 agents 文件夹的 init.py
文件中注册这个新架构:
- REGISTRY = {}
- from .rnn_agent import RNNAgent
- from .rnn_ns_agent import RNNNSAgent
- from .attention_agent import AttentionAgent
- REGISTRY["rnn"] = RNNAgent
- REGISTRY["rnn_ns"] = RNNNSAgent
- REGISTRY["attention"] = AttentionAgent
EPyMARL 代码运行逻辑
最后我们来看一下 EPyMARL
中各个文件之间的关系。这里的主文件是 main.py 文件,然后会调用 run.py
文件,其中的 run_sequential
是训练的核心代码。evaluate_sequential
是用于测试的。
接着会在 run_sequential
里面初始化:
runner
:用于和环境交互,将交互数据存储在 buffer 中(这里会使用 multi-agent controller 来与环境进行交互);learner
:用于更新 mac 中的 agent 的网络(根据环境收集的数据,和使用 mac 计算的值进行更新);multiagent controller(mac)
:用于根据 state 给出 action,会分别传入 runner 和 learner(这里不同的 controller 的模型是会从 agent 部分 load 进来)。在 runner 中 mac 用于根据环境的 state 给出 action。在 learner 中,会更新 mac 的参数。
下面我们每个部分来详细看一下数据大小,假设现在的参数如下所示(后面我们只看数据大小变化),我们以 VDN 为例:
- batch_size=32
- time_limit=25,一局游戏的步长是 26(算上 reset 的部分)
- agent_number=2,agent 的数量是 2
Step 1: 与环境交互,收集数据
首先在 episode_runner.py
会与环境进行交互收集数据,并将收集的数据存储在 buffer 里面:
- episode_batch = runner.run(test_mode=False)
- buffer.insert_episode_batch(episode_batch) # 将样本添加到 buffer 里面
这里收集的数据的大小分别是,相当于 obs 是每个 agent 自己的观测,state 是所有 agent 全局的观测,这里 reward 只有每一步环境的总体 reward,没有对应到每一个 agent 的奖励:
- (Pdb) pp self.batch["obs"].shape
- >> torch.Size([1, 26, 2, 15])
- (Pdb) pp self.batch["state"].shape
- >> torch.Size([1, 26, 30])
- (Pdb) pp self.batch["actions"].shape
- >> torch.Size([1, 26, 2, 1])
- (Pdb) pp self.batch["reward"].shape
- >> torch.Size([1, 26, 1])
Step 2: learner 开始训练
当收集了足够的数据之后,则会调用 learner.py
中的 train
。这里我们首先看一下从 buffer
中 sample 出来的样本的大小。可以看到和 Step 1
相比,就是 batchsize 变大了,现在是我们设置的 32:
- (Pdb) pp episode_sample["obs"].shape
- >> torch.Size([32, 26, 2, 15])
- (Pdb) pp episode_sample["state"].shape
- >> torch.Size([32, 26, 30])
- (Pdb) pp episode_sample["actions"].shape
- >> torch.Size([32, 26, 2, 1])
- (Pdb) pp episode_sample["reward"].shape
- >> torch.Size([32, 26, 1])
接着从 buffer
里面进行采样,得到 episode_sample
,然后传入 learner
中进行训练:
- episode_sample = buffer.sample(args.batch_size)
- learner.train(episode_sample, runner.t_env, episode)
这里我们以 q_learner 为例子进行说明。在有了 batch 之后,可以使用 self.mac 来计算处每个 state 和 action 的 Q 值:
- for t in range(batch.max_seq_length):
- agent_outs = self.mac.forward(batch, t=t) # 输出当前 state, 所有动作的 Q 值
- mac_out.append(agent_outs)
- mac_out = th.stack(mac_out, dim=1) # Concat over time
- chosen_action_qvals = th.gather(mac_out[:, :-1], dim=3, index=actions).squeeze(3)
此时我们计算得到了 mac_out
和 chosen_action_qvals
,大小分别是。可以看到此时每个 step 会有 2 个 Q 值,也就是对应两个 agent:
- (Pdb) pp mac_out.shape
- >> torch.Size([32, 26, 2, 6])
- (Pdb) pp chosen_action_qvals.shape
- >> torch.Size([32, 25, 2])
接着会使用 Mixer 将上面的 Q 值进行合并(这里根据算法的不同,会state 的信息不一定都会用上,例如 VDN 只需要求和,而 QMIX 需要根据 state 来进行计算):
- chosen_action_qvals = self.mixer(chosen_action_qvals, batch["state"][:, :-1])
此时 chosen_action_qvals
的最后一个维度就是 1 了,此时就可以通过全局的 reward 来计算 TD-Error 了。
- (Pdb) pp chosen_action_qvals.shape
- >> torch.Size([32, 25, 1])
Step 3: 深入理解 mac
虽然这个部分是 Step 3
,但是实际上已经在 Step 2
中使用了,也就是 learner 里面的 self.mac.forward
。这里我们在详细来看一下。mac 包含两个部分,分别是:self.agent
和 self.action_selector
。
对于 self.agent
,可以是一个 torch model。如下所示,输出的维度和 action 的维度一样,输出为每个动作的概率:
- class RNNAgent(nn.Module):
- def __init__(self, input_shape, args):
- super(RNNAgent, self).__init__()
- self.args = args
- self.fc1 = nn.Linear(input_shape, args.hidden_dim)
- self.rnn = nn.GRUCell(args.hidden_dim, args.hidden_dim)
- self.fc2 = nn.Linear(args.hidden_dim, args.n_actions)
- def init_hidden(self):
- # make hidden states on same device as model
- return self.fc1.weight.new(1, self.args.hidden_dim).zero_()
- def forward(self, inputs, hidden_state):
- x = F.relu(self.fc1(inputs))
- h_in = hidden_state.reshape(-1, self.args.hidden_dim)
- h = self.rnn(x, h_in)
- q = self.fc2(h)
- return q, h
对于 self.action_selector
,可以是根据 Q value 来进行动作的选择,例如是 EpsilonGreedyActionSelector
:
- class EpsilonGreedyActionSelector():
- def __init__(self, args):
- self.args = args
- self.schedule = DecayThenFlatSchedule(
- args.epsilon_start,
- args.epsilon_finish,
- args.epsilon_anneal_time,
- decay="linear"
- )
- self.epsilon = self.schedule.eval(0)
- def select_action(self, agent_inputs, avail_actions, t_env, test_mode=False):
- # Assuming agent_inputs is a batch of Q-Values for each agent bav
- self.epsilon = self.schedule.eval(t_env)
- # mask actions that are excluded from selection
- masked_q_values = agent_inputs.clone()
- masked_q_values[avail_actions == 0.0] = -float("inf") # should never be selected!
- random_numbers = th.rand_like(agent_inputs[:, :, 0])
- pick_random = (random_numbers < self.epsilon).long()
- random_actions = Categorical(avail_actions.float()).sample().long()
- picked_actions = pick_random * random_actions + (1 - pick_random) * masked_q_values.max(dim=2)[1]
- return picked_actions
以上是关于 EPyMARL
运行的一些顺序的介绍,希望可以帮助大家更好的对 EPyMARL
进行理解和运用。
- 微信公众号
- 关注微信公众号
- QQ群
- 我们的QQ群号
评论