由于这段时间的学习内容涉及到MAPPO算法,并且我对MAPPO算法这种多智能体算法的信息交互机制不甚了解,于是写了这个系列的笔记,目的是巩固知识,并且进行一些粗浅又滑稽的总结。

1.PPO算法的介绍

1.1.PG算法

如果要分类的话,根据学习方法的不同,可以将强化学习分为Value-based方法、Policy-based方法,以及基于AC框架的Actor-Critic方法。下面做一个简单的介绍。

Value-based方法:典型代表为------Q-learning、Sarsa与DQN,之所以叫“Value-based”方法,是因为在这种类型的方法中,我们的所有决策都是基于一张“表格”采取的,这里的“表格”是一种较为抽象的说法,大致可以理解为:我们现在在一个什么样的状态下,在这个状态下,我们采取一个什么样的动作会获得多大的“收益”。然后我们会根据这样一个“收益”情况采取我们的动作。

Policy-based方法:这里的典型代表就是PPO算法,在Q-learning与DQN中,我们输出的会是一个代表状态当前动作收益的Q值,但Policy-based算法摒弃了这种Q值的思想,转而输出当前状态可以采取的所有动作的概率,这样的做法意思就是:此刻的你非常饿,但你不知道自己想吃什么,你只知道你是个肉食主义者,想多吃点肉肉。那么“想吃肉”就是你的策略(Policy),至于吃哪种肉,鸡肉鸭肉鱼肉猪肉牛肉羊肉......都看你心情。

Actor-Critic方法:这个方法最显著的特点就是AC框架,简单来说,最简单的AC框架由两个部分组成:Actor与Critic,如果我们将Critic看成老师,Actor就可以看成学生,老师会对学生在当前状态的动作进行评价,然后学生会用这个“评价”更新自己的参数。这就是一个最简单的AC框架的更新方式。

我们这里展开讲讲PG算法,我们不妨近似地将一个“策略”看成一个概率分布,也许每一个不同的情况都对应着一个概率分布,这时我们的目的就变成了使用带有θ参数的函数对策略进行近似,可以通过调整参数θ从而逼近最优策略。

那接下来要做的就是去优化这个策略函数,并且让这个目标函数朝着最大值的方向去优化。同样以吃东西作为例子:你还是一个很饿的人,并且不知道该去吃啥,于是你去尝试了一家你从来没吃过的餐厅,突然发现这一家餐厅很好吃,那么你以后可能就会经常来。这又何尝不是一个策略更新的过程呢?

说了那么多,其实我们的目的只有一个:通过更新策略参数θ来使我们所获得的收益(Reward)最大化。当然这其中有一堆的公式可以进行推导,但我最不喜欢的就是推公式,所以就不从头推到尾了,直接说结论。就是我们可以通过一个更新公式对策略参数θ进行更新,从而达到优化策略的目的。

1.2. PPO算法

上面介绍了PG算法,看起来十分地理想且可行,但是PG算法有一个较大的缺点:每当我们更新了一次策略参数θ,那么我们之前的策略可以这样说,“直接作废”。那这样带来的后果是什么呢,已知我们的策略更新是通过旧的策略在环境中采样得到,一旦策略更新了之后,旧的数据——由旧策略采样得到的数据,就不再对现在的新策略的更新有任何的指导意义,于是我们就需要进行新一轮的采样。这就使得传统的PG方法需要花费大量的时间进行采样,极大地减少了数据的利用率。

为了解决这一个问题,“异策略”这一概念就出现了。简单来说就是用另外一个策略去和环境进行互动,来完成“收集数据”这个任务。然后需要进行训练的策略就可以利用这些数据进行高效的更新。但是!就像上一段中所说的,传统的PG方法无法使用不同策略采集得到的数据进行自我更新,这就需要我们接着引入一些其它的技巧,使得这种“高效”的更新方式变为可能。

第一个技巧是重要性采样(importance sampling)。假设我们有两个策略,一个我们把它叫做$p$,另一个我们把它叫做$q$。已知我们这里有一个函数$f(x)$,且函数中的$x$就是由策略$p$采样得到。那么如果我们要得到函数$f(x)$的均值,就可以从分布$p$中尽量多地采集样本数据$x^i$,通过计算所有$f(x^i)$的均值,我们就可以近似地得到$f(x)$的期望值。就像下面这个公式所表达的那样:

$E_{x \sim p}[f(x)] \approx \frac{1}{N} \sum_{i=1}^{N} f\left(x^{i}\right)$
但是注意!因为我们使用的是异策略方法,所以这里我们使用的是策略$q$进行采样,那么我们就没有办法直接使用上面的公式得到$f(x)$的近似值。这时候就需要对求期望的公式进行一些变换。
现在我们还是假设所有的$x$都是由策略$p$采样得到,那么我们可以将$f(x)$的期望值表示为下面这个公式:
$\int f(x) p(x) d x$
对其进行变换,得到:
$\int f(x) p(x) d x=\int f(x) \frac{p(x)}{q(x)} q(x) d x=E_{x \sim q}\left[f(x) \frac{p(x)}{q(x)}\right]$
整理可得:
$E_{x \sim p}[f(x)]=E_{x \sim q}\left[f(x) \frac{p(x)}{q(x)}\right]$
通过这样的变换,我们就可以通过策略$q$进行采样,然后计算$f(x) \frac{p(x)}{q(x)}$,最后求其平均值,同样也可以得到$f(x)$期望的近似值。
看!这样就实现了,在异策略前提下,通过$q$策略收集的数据,计算$p$策略下,函数$f(x)$的期望近似。
我们将这个方法简单地概括为:通过给策略$q$采集得到的每一个数据,乘以一个“重要性权重”:$\frac{p(x)}{q(x)}$,就可以对两个策略之间的差异进行修正。有了这样一个修正手段之后,策略$p$与策略$q$之间的关系,就可以近似地看作“行动者”与“信息采集器”,行动者通过信息采集器获取的数据优化自身的策略。
 
第二个技巧就是KL散度,当我们引入上面的重要性采样后,看似所有的问题都迎刃而解,但是要注意的是:采样所使用的策略$q$与决策所使用的策略$p$之间的差距不能过大。因为即使两个分布之间如果均值与方差差异过大,那么就会是训练过程产生各种各样的问题。就好比我是一个肉食主义者,但我却从一个素食主义者朋友喜欢的餐厅列表中来决定今天吃什么。因此就需要引入KL散度来对训练过程进行约束,使得策略$p$与策略$q$之间的差异不能过大。
 
第三个技巧是优势函数。首先我们必须要明白的一点是,PPO生成的是一个随机策略,即在每一种情况下,行动者会采取某些行为的概率。但这样就会产生一个较为严重的问题:好的越来越好,差的越来越差,以至于我们在一些状态下会忽略一些更为合适的动作。通俗的来讲,我们生成的分布会更乐意选择那些在很多情况下表现更为优异的动作,并且以一个极大的概率去采取这样的动作,而以一个极小的概率采取一些表现较差的动作,甚至是“次优解”。这就使得我们“千辛万苦”得到的随机性优势不复存在。于是我们在计算状态-动作对得分时,要人为地引入一个“基线”,这个“基线”就是这个状态下的状态-动作对得分的均值。通过引入这样一个“基线”,我们就可以得到优势函数:即,我们在这个状态下采取的策略要比一般情况下好多少。
现在可以将优势函数与重要性采样结合起来(KL散度稍后再加),那么更新策略所使用的梯度就可以表示为:
$E_{\left(s_{t}, a_{t}\right) \sim \pi_{\theta^{\prime}}}\left[\frac{p_{\theta}\left(s_{t}, a_{t}\right)}{p_{\theta^{\prime}}\left(s_{t}, a_{t}\right)} A^{\theta}\left(s_{t}, a_{t}\right) \nabla \log p_{\theta}\left(a_{t}^{n} \mid s_{t}^{n}\right)\right]$
其中$\theta^{\prime}$代表旧策略,并且也是我们用来采样的策略,而$θ$代表我们当前的策略,上面这个式子就是我们在使用策略梯度对$θ$进行更新时所用到的梯度。这里的$A$是优势函数。
那么我们之前还说过两个策略间的差距不能过大,于是我们将KL散度引入PPO算法,得到的优化目标如下:
$\begin{aligned} J_{\mathrm{PPO}}^{\theta^{\prime}}(\theta) &=J^{\theta^{\prime}}(\theta)-\beta \mathrm{KL}\left(\theta, \theta^{\prime}\right) \\ J^{\theta^{\prime}}(\theta) &=E_{\left(s_{t}, a_{t}\right) \sim \pi_{\theta^{\prime}}}\left[\frac{p_{\theta}\left(a_{t} \mid s_{t}\right)}{p_{\theta^{\prime}}\left(a_{t} \mid s_{t}\right)} A^{\theta^{\prime}}\left(s_{t}, a_{t}\right)\right] \end{aligned}$
在构建代码时,我们可以直接将loss设为负的$J(θ)$,这样就实现了PG中的梯度更新。
值得一提的是,一般情况下我们并不直接构造KL散度,而是采用近端优化惩罚(PPO-penalty)或者近端优化裁剪(PPO-clip)来优化、替代传统的KL散度。
关于这两种方法,可以参考这位大佬的文章:详解近端策略优化(ppo,干货满满) - 简书 (jianshu.com)  大佬写的干货满满!!!我这样的温斯顿都能够较为顺利地理解。
 

2.PPO算法代码

PPO算法的代码较为通俗易懂,这里不多加赘述,只是贴出来看一下,方便理解。但是要注意的是,这段代码中所使用的就是我们之前提到的近端优化裁剪方法(PPO-clip).

网络结构:

点击查看代码
  1. class FeedForwardNN(nn.Module):
  2. def __init__(self, in_dim, out_dim):
  3. super(FeedForwardNN, self).__init__()
  4. self.layer1 = nn.Linear(in_dim, 64)
  5. self.layer2 = nn.Linear(64, 64)
  6. self.layer3 = nn.Linear(64, out_dim)
  7. def forward(self, obs):
  8. if isinstance(obs, np.ndarray):
  9. obs = torch.tensor(obs, dtype=torch.float)
  10. activation1 = F.relu(self.layer1(obs))
  11. activation2 = F.relu(self.layer2(activation1))
  12. output = self.layer3(activation2)
  13. return output

PPO算法类:

点击查看代码
  1. class PPO:
  2. def __init__(self, policy_class, env, **hyperparameters):
  3. # PPO 初始化用于训练的超参数
  4. self._init_hyperparameters(hyperparameters)
  5. # 提取环境信息
  6. self.env = env
  7. self.obs_dim = env.observation_space.shape[0]
  8. self.act_dim = env.action_space.shape[0]
  9. # 初始化演员和评论家网络
  10. self.actor = policy_class(self.obs_dim, self.act_dim)
  11. self.critic = policy_class(self.obs_dim, 1)
  12. # 为演员和评论家初始化优化器
  13. self.actor_optim = Adam(self.actor.parameters(), lr=self.lr)
  14. self.critic_optim = Adam(self.critic.parameters(), lr=self.lr)
  15. # 初始化协方差矩阵,用于查询actor网络的action
  16. self.cov_var = torch.full(size=(self.act_dim,), fill_value=0.5)
  17. self.cov_mat = torch.diag(self.cov_var)
  18. # 这个记录器将帮助我们打印出每个迭代的摘要
  19. self.logger = {
  20. 'delta_t': time.time_ns(),
  21. 't_so_far': 0, # 到目前为止的时间步数
  22. 'i_so_far': 0, # 到目前为止的迭代次数
  23. 'batch_lens': [], # 批次中的episodic长度
  24. 'batch_rews': [], # 批次中的rews回报
  25. 'actor_losses': [], # 当前迭代中演员网络的损失
  26. }
  27. def learn(self, total_timesteps):
  28. print(f"Learning... Running {self.max_timesteps_per_episode} timesteps per episode, ", end='')
  29. print(f"{self.timesteps_per_batch} timesteps per batch for a total of {total_timesteps} timesteps")
  30. t_so_far = 0 # 到目前为止仿真的时间步数
  31. i_so_far = 0 # 到目前为止,已运行的迭代次数
  32. while t_so_far < total_timesteps:
  33. # 收集批量实验数据
  34. batch_obs, batch_acts, batch_log_probs, batch_rtgs, batch_lens = self.rollout()
  35. # 计算收集这一批数据的时间步数
  36. t_so_far += np.sum(batch_lens)
  37. # 增加迭代次数
  38. i_so_far += 1
  39. # 记录到目前为止的时间步数和到目前为止的迭代次数
  40. self.logger['t_so_far'] = t_so_far
  41. self.logger['i_so_far'] = i_so_far
  42. # 计算第k次迭代的advantage
  43. V, _ = self.evaluate(batch_obs, batch_acts)
  44. A_k = batch_rtgs - V.detach()
  45. # 将优势归一化 在理论上不是必须的,但在实践中,它减少了我们优势的方差,使收敛更加稳定和快速。
  46. # 添加这个是因为在没有这个的情况下,解决一些环境的问题太不稳定了。
  47. A_k = (A_k - A_k.mean()) / (A_k.std() + 1e-10)
  48. # 在其中更新我们的网络。
  49. for _ in range(self.n_updates_per_iteration):
  50. V, curr_log_probs = self.evaluate(batch_obs, batch_acts)
  51. # 重要性采样的权重
  52. ratios = torch.exp(curr_log_probs - batch_log_probs)
  53. surr1 = ratios * A_k
  54. surr2 = torch.clamp(ratios, 1 - self.clip, 1 + self.clip) * A_k
  55. # 计算两个网络的损失。
  56. actor_loss = (-torch.min(surr1, surr2)).mean()
  57. critic_loss = nn.MSELoss()(V, batch_rtgs)
  58. # 计算梯度并对actor网络进行反向传播
  59. # 梯度清零
  60. self.actor_optim.zero_grad()
  61. # 反向传播,产生梯度
  62. actor_loss.backward(retain_graph=True)
  63. # 通过梯度下降进行优化
  64. self.actor_optim.step()
  65. # 计算梯度并对critic网络进行反向传播
  66. self.critic_optim.zero_grad()
  67. critic_loss.backward()
  68. self.critic_optim.step()
  69. self.logger['actor_losses'].append(actor_loss.detach())
  70. self._log_summary()
  71. if i_so_far % self.save_freq == 0:
  72. torch.save(self.actor.state_dict(), './ppo_actor.pth')
  73. torch.save(self.critic.state_dict(), './ppo_critic.pth')
  74. def rollout(self):
  75. """
  76. 这就是我们从实验中收集一批数据的地方。由于这是一个on-policy的算法,我们需要在每次迭代行为者/批评者网络时收集一批新的数据。
  77. """
  78. batch_obs = []
  79. batch_acts = []
  80. batch_log_probs = []
  81. batch_rews = []
  82. batch_rtgs = []
  83. batch_lens = []
  84. # 一回合的数据。追踪每一回合的奖励,在回合结束的时候会被清空,开始新的回合。
  85. ep_rews = []
  86. # 追踪到目前为止这批程序我们已经运行了多少个时间段
  87. t = 0
  88. # 继续实验,直到我们每批运行超过或等于指定的时间步数
  89. while t < self.timesteps_per_batch:
  90. ep_rews = [] 每回合收集的奖励
  91. # 重置环境
  92. obs = self.env.reset()
  93. done = False
  94. # 运行一个回合的最大时间为max_timesteps_per_episode的时间步数
  95. for ep_t in range(self.max_timesteps_per_episode):
  96. if self.render and (self.logger['i_so_far'] % self.render_every_i == 0) and len(batch_lens) == 0:
  97. self.env.render()
  98. # 递增时间步数,到目前为止已经运行了这批程序
  99. t += 1
  100. # 追踪本批中的观察结果
  101. batch_obs.append(obs)
  102. # 计算action,并在env中执行一次step。
  103. # 注意,rew是奖励的简称。
  104. action, log_prob = self.get_action(obs)
  105. obs, rew, done, _ = self.env.step(action)
  106. # 追踪最近的奖励、action和action的对数概率
  107. ep_rews.append(rew)
  108. batch_acts.append(action)
  109. batch_log_probs.append(log_prob)
  110. if done:
  111. break
  112. # 追踪本回合的长度和奖励
  113. batch_lens.append(ep_t + 1)
  114. batch_rews.append(ep_rews)
  115. # 将数据重塑为函数描述中指定形状的张量,然后返回
  116. batch_obs = torch.tensor(batch_obs, dtype=torch.float)
  117. batch_acts = torch.tensor(batch_acts, dtype=torch.float)
  118. batch_log_probs = torch.tensor(batch_log_probs, dtype=torch.float)
  119. batch_rtgs = self.compute_rtgs(batch_rews)
  120. # 在这批中记录回合的回报和回合的长度。
  121. self.logger['batch_rews'] = batch_rews
  122. self.logger['batch_lens'] = batch_lens
  123. return batch_obs, batch_acts, batch_log_probs, batch_rtgs, batch_lens
  124. def compute_rtgs(self, batch_rews):
  125. batch_rtgs = []
  126. # 遍历每一回合,一个回合有一批奖励
  127. for ep_rews in reversed(batch_rews):
  128. # 到目前为止的折扣奖励
  129. discounted_reward = 0
  130. # 遍历这一回合的所有奖励。我们向后退,以便更顺利地计算每一个折现的回报
  131. for rew in reversed(ep_rews):
  132. discounted_reward = rew + discounted_reward * self.gamma
  133. batch_rtgs.insert(0, discounted_reward)
  134. # 将每个回合的折扣奖励的数据转换成张量
  135. batch_rtgs = torch.tensor(batch_rtgs, dtype=torch.float)
  136. return batch_rtgs
  137. def get_action(self, obs):
  138. mean = self.actor(obs)
  139. # 用上述协方差矩阵中的平均行动和标准差创建一个分布。
  140. dist = MultivariateNormal(mean, self.cov_mat)
  141. action = dist.sample()
  142. log_prob = dist.log_prob(action)
  143. return action.detach().numpy(), log_prob.detach()
  144. def evaluate(self, batch_obs, batch_acts):
  145. """
  146. 估算每个观察值,以及最近一批actor网络迭代中的每个action的对数prob。
  147. """
  148. # 为每个batch_obs查询critic网络的V值。V的形状应与batch_rtgs相同。
  149. V = self.critic(batch_obs).squeeze()
  150. # 使用最近的actor网络计算批量action的对数概率。
  151. mean = self.actor(batch_obs)
  152. dist = MultivariateNormal(mean, self.cov_mat)
  153. log_probs = dist.log_prob(batch_acts)
  154. # 返回批次中每个观察值的值向量V和批次中每个动作的对数概率log_probs
  155. return V, log_probs

MAPPO学习笔记(1):从PPO算法开始的更多相关文章

  1. 机器学习实战(Machine Learning in Action)学习笔记————08.使用FPgrowth算法来高效发现频繁项集

    机器学习实战(Machine Learning in Action)学习笔记————08.使用FPgrowth算法来高效发现频繁项集 关键字:FPgrowth.频繁项集.条件FP树.非监督学习作者:米 ...

  2. 机器学习实战(Machine Learning in Action)学习笔记————07.使用Apriori算法进行关联分析

    机器学习实战(Machine Learning in Action)学习笔记————07.使用Apriori算法进行关联分析 关键字:Apriori.关联规则挖掘.频繁项集作者:米仓山下时间:2018 ...

  3. 机器学习实战(Machine Learning in Action)学习笔记————02.k-邻近算法(KNN)

    机器学习实战(Machine Learning in Action)学习笔记————02.k-邻近算法(KNN) 关键字:邻近算法(kNN: k Nearest Neighbors).python.源 ...

  4. [ML学习笔记] 朴素贝叶斯算法(Naive Bayesian)

    [ML学习笔记] 朴素贝叶斯算法(Naive Bayesian) 贝叶斯公式 \[P(A\mid B) = \frac{P(B\mid A)P(A)}{P(B)}\] 我们把P(A)称为"先 ...

  5. Effective STL 学习笔记 31:排序算法

    Effective STL 学习笔记 31:排序算法 */--> div.org-src-container { font-size: 85%; font-family: monospace; ...

  6. HMM模型学习笔记(前向算法实例)

    HMM算法想必大家已经听说了好多次了,完全看公式一头雾水.但是HMM的基本理论其实很简单.因为HMM是马尔科夫链中的一种,只是它的状态不能直接被观察到,但是可以通过观察向量间接的反映出来,即每一个观察 ...

  7. 【视频编解码·学习笔记】8. 熵编码算法:基本算法列举 & 指数哥伦布编码

    一.H.264中的熵编码基本方法: 熵编码具有消除数据之间统计冗余的功能,在编码端作为最后一道工序,将语法元素写入输出码流 熵解码作为解码过程的第一步,将码流解析出语法元素供后续步骤重建图像使用 在H ...

  8. 学习笔记 --- 最大流Dinic算法

    为与机房各位神犇同步,学习下网络流,百度一下发现竟然那么多做法,最后在两种算法中抉择,分别是Dinic和ISAP算法,问过 CA爷后得知其实效率上无异,所以决定跟随Charge的步伐学习Dinic,所 ...

  9. Pytorch学习笔记08----优化器算法Optimizer详解(SGD、Adam)

    1.优化器算法简述 首先来看一下梯度下降最常见的三种变形 BGD,SGD,MBGD,这三种形式的区别就是取决于我们用多少数据来计算目标函数的梯度,这样的话自然就涉及到一个 trade-off,即参数更 ...

  10. 《机器学习实战》学习笔记一K邻近算法

     一. K邻近算法思想:存在一个样本数据集合,称为训练样本集,并且每个数据都存在标签,即我们知道样本集中每一数据(这里的数据是一组数据,可以是n维向量)与所属分类的对应关系.输入没有标签的新数据后,将 ...

随机推荐

  1. IT工具知识-11:一种安卓投屏到Win10失败的解决方法

    软硬件平台 电脑:WIN10 LTSC 手机:红米K30Pro/MIUI 11.0.26 投屏软件:安卓端-自带投屏,WIN10-自带投屏(连接) 故障描述 之前还能用的,但是在换了个路由器之后就不能 ...

  2. kafka 学习

    https://kafka.apache.org/quickstart C:\W_O_R_K\kafka_2.12-2.2.0\kafka_2.12-2.2.0\bin\windows\zookeep ...

  3. format UTF-8 BOM by AX

    #File CommaTextIo commaTextIo; FileIOPermission permission; CustTable custTable; str fileName = @&qu ...

  4. 第3章---数据探索(python数据挖掘)

    1.缺失值分析及箱型图 数据:catering_sale.xls(餐饮日销额数) 缺失值使用函数:describe()函数,能算出数据集的八个统计量 import pandas as pd cater ...

  5. shader graph 制作的双面shader

  6. 单例和mono单例

    单例 public class Singleton<T> where T : new() { private static T instance; public static T Inst ...

  7. bytes转化为字典

    import requestsurl='https://su.ke.com/api/listtop?type=resblock&resblock_id=2311062653496924& ...

  8. Django中间件的介绍及使用

    1.中间件的理解: 是用来处理Django请求与响应的框架级别的钩子,处于wsgi模块与视图函数之间,在执行视图函数之前和之后所做      的动作,是一个轻量级.低级别的插件,作用于全局,使用不当很 ...

  9. 【学习】蓝牙的一些基础知识or什么是蓝牙

    蓝牙----Bluetooth(短距离无线通信技术)  2022-07-29   14:31:27 蓝牙技术有什么特点(体积小,易集成,低功耗,适用广,抗干扰,成本低,开放性) (1) 蓝牙模块体积很 ...

  10. git rebase时出现的提示信息

    root@host: /home/wkxnk/project$  git rebase master First, rewinding head to replay your work on top ...