0


【MADRL】反事实多智能体策略梯度(COMA)算法

  1. 本篇文章是博主强化学习RL领域学习时,用于个人学习、研究或者欣赏使用,并基于博主对相关等领域的一些理解而记录的学习摘录和笔记,若有不当和侵权之处,指出后将会立即改正,还望谅解。文章分类在强化学习专栏:
  2. 强化学习(9)---《【MADRL】反事实多智能体策略梯度(COMA)算法》

【MADRL】反事实多智能体策略梯度(COMA)算法

目录


0.介绍

** 反事实多智能体策略梯度法COMA (Counterfactual Multi-Agent Policy Gradient)** 是一种面向多智能体协作问题的强化学习算法,旨在通过减少策略梯度的方差,来提升去中心化智能体的学习效果。

  1. COMA 算法最早由 DeepMind 团队提出,论文标题为 **"Counterfactual Multi-Agent Policy Gradients"**,由 Jakob Foerster 等人于 2017 年在 AAAI 会议上发表。适用于局部观察、去中心化决策的多智能体环境,特别是策略梯度方法下的合作问题。

参考文献:Counterfactual Multi-Agent Policy Gradients


1.算法背景和思想

  1. 在多智能体强化学习场景中,每个智能体在某一时刻只掌握局部的信息,无法全局观测环境状态。为了促进合作,各个智能体的动作对全局奖励有不同的贡献,因此需要一种有效的方法来分配奖励。COMA 引入了“反事实基线”(Counterfactual Baseline)的概念,专门用于降低多智能体策略梯度方法中的方差。
  2. COMA 的核心思想是通过引入一个基线,该基线模拟在固定其他智能体动作的前提下,某个智能体选择不同动作时对全局奖励的影响,从而更精确地衡量当前动作的贡献,减少策略梯度更新中的方差。

07286bd3fb604b36a2acc3ac5d29a5e5.png


2.公式推导

  1. 全局策略梯度:对于多智能体问题,每个智能体eq?%28%20i%20%29的策略梯度可以表示为:eq?%5B%20%5Cnabla_%7B%5Ctheta_i%7D%20J%28%5Ctheta_i%29%20%3D%20%5Cmathbb%7BE%7D%5Cleft%5B%20%5Cnabla_%7B%5Ctheta_i%7D%20%5Clog%20%5Cpi_%7B%5Ctheta_i%7D%28a_i%7Cs%29%20Q_i%28s%2C%20a%29%20%5Cright%5D%20%5D其中,eq?%28%20%5Cpi_%7B%5Ctheta_i%7D%28a_i%7Cs%29%20%29是智能体eq?%28%20i%20%29在状态eq?%28%20s%20%29下选择动作eq?%28%20a_i%20%29的概率,eq?%28%20Q_i%28s%2C%20a%29%20%29是该智能体在执行动作eq?%28%20a_i%20%29时的动作价值函数。
  2. 反事实基线:为了减小方差,COMA 提出了反事实基线eq?%28%20b%28s%2C%20a_%7B-i%7D%29%20%29,该基线衡量在保持其他智能体动作eq?%28%20a_%7B-i%7D%20%29不变的情况下,智能体eq?%28%20i%20%29 选择其他动作时的期望收益。具体公式为:eq?%5B%20b%28s%2C%20a_%7B-i%7D%29%20%3D%20%5Csum_%7Ba_i%7D%20%5Cpi_%7B%5Ctheta_i%7D%28a_i%7Cs%29%20Q_i%28s%2C%20a_i%2C%20a_%7B-i%7D%29%20%5D这里的eq?%28%20a_%7B-i%7D%20%29表示除智能体eq?%28%20i%20%29之外,其他智能体的动作组合。
  3. 策略更新:有了反事实基线之后,COMA 中智能体eq?%28%20i%20%29的策略更新公式变为:eq?%5B%20%5Cnabla_%7B%5Ctheta_i%7D%20J%28%5Ctheta_i%29%20%3D%20%5Cmathbb%7BE%7D%5Cleft%5B%20%5Cnabla_%7B%5Ctheta_i%7D%20%5Clog%20%5Cpi_%7B%5Ctheta_i%7D%28a_i%7Cs%29%20%28Q_i%28s%2C%20a%29%20-%20b%28s%2C%20a_%7B-i%7D%29%29%20%5Cright%5D%20%5D其中,eq?%28%20Q_i%28s%2C%20a%29%20%29是在当前状态下,所有智能体执行一组动作eq?%28%20a%20%29时的全局奖励,而eq?%28%20b%28s%2C%20a_%7B-i%7D%29%20%29是该动作的反事实基线。
  4. 全局值函数:COMA 中的值函数 eq?%28%20Q%28s%2C%20a%29%20%29和基线 eq?%28%20b%28s%2C%20a_%7B-i%7D%29%20%29都是通过集中化的学习进行优化的,虽然决策是去中心化的,但值函数和基线都依赖于全局的状态和动作信息。

3.COMA 算法步骤

  1. 初始化智能体策略和集中式的全局值函数。
  2. 智能体与环境交互,收集经验数据。
  3. 使用经验数据更新全局值函数 eq?%28%20Q%28s%2C%20a%29%20%29
  4. 计算反事实基线 eq?%28%20b%28s%2C%20a_%7B-i%7D%29%20%29
  5. 计算每个智能体的策略梯度,并更新策略参数。
  6. 重复上述过程,直至智能体策略收敛。

4.优势

  • 减少策略梯度的方差:通过引入反事实基线,有效地减少了策略更新过程中的高方差问题,使得策略更新更加稳定。
  • 适合多智能体协作环境:COMA 尤其适合智能体需要紧密合作的场景,比如多智能体联合行动或团队任务。

5.应用场景

  • 多智能体合作任务,如多机器人协作、多无人机编队等。
  • 需要去中心化控制、但全局奖励信息可用的场景。

[Python] COMA 算法实现

  1. 若是下面代码复现困难或者有问题,**欢迎评论区留言**;需要以整个项目形式的代码,请**在评论区留下您的邮箱**,以便于及时分享给您(私信难以及时回复)。

主要代码:

  1. import torch
  2. import os
  3. from network.base_net import RNN
  4. from network.commnet import CommNet
  5. from network.g2anet import G2ANet
  6. from network.coma_critic import ComaCritic
  7. from common.utils import td_lambda_target
  8. class COMA:
  9. def __init__(self, args):
  10. self.n_actions = args.n_actions
  11. self.n_agents = args.n_agents
  12. self.state_shape = args.state_shape
  13. self.obs_shape = args.obs_shape
  14. actor_input_shape = self.obs_shape # actor网络输入的维度,和vdn、qmix的rnn输入维度一样,使用同一个网络结构
  15. critic_input_shape = self._get_critic_input_shape() # critic网络输入的维度
  16. # 根据参数决定RNN的输入维度
  17. if args.last_action:
  18. actor_input_shape += self.n_actions
  19. if args.reuse_network:
  20. actor_input_shape += self.n_agents
  21. self.args = args
  22. # 神经网络
  23. # 每个agent选动作的网络,输出当前agent所有动作对应的概率,用该概率选动作的时候还需要用softmax再运算一次。
  24. if self.args.alg == 'coma':
  25. print('Init alg coma')
  26. self.eval_rnn = RNN(actor_input_shape, args)
  27. elif self.args.alg == 'coma+commnet':
  28. print('Init alg coma+commnet')
  29. self.eval_rnn = CommNet(actor_input_shape, args)
  30. elif self.args.alg == 'coma+g2anet':
  31. print('Init alg coma+g2anet')
  32. self.eval_rnn = G2ANet(actor_input_shape, args)
  33. else:
  34. raise Exception("No such algorithm")
  35. # 得到当前agent的所有可执行动作对应的联合Q值,得到之后需要用该Q值和actor网络输出的概率计算advantage
  36. self.eval_critic = ComaCritic(critic_input_shape, self.args)
  37. self.target_critic = ComaCritic(critic_input_shape, self.args)
  38. if self.args.cuda:
  39. self.eval_rnn.cuda()
  40. self.eval_critic.cuda()
  41. self.target_critic.cuda()
  42. self.model_dir = args.model_dir + '/' + args.alg + '/' + args.map
  43. # 如果存在模型则加载模型
  44. if self.args.load_model:
  45. if os.path.exists(self.model_dir + '/rnn_params.pkl'):
  46. path_rnn = self.model_dir + '/rnn_params.pkl'
  47. path_coma = self.model_dir + '/critic_params.pkl'
  48. map_location = 'cuda:0' if self.args.cuda else 'cpu'
  49. self.eval_rnn.load_state_dict(torch.load(path_rnn, map_location=map_location))
  50. self.eval_critic.load_state_dict(torch.load(path_coma, map_location=map_location))
  51. print('Successfully load the model: {} and {}'.format(path_rnn, path_coma))
  52. else:
  53. raise Exception("No model!")
  54. # 让target_net和eval_net的网络参数相同
  55. self.target_critic.load_state_dict(self.eval_critic.state_dict())
  56. self.rnn_parameters = list(self.eval_rnn.parameters())
  57. self.critic_parameters = list(self.eval_critic.parameters())
  58. if args.optimizer == "RMS":
  59. self.critic_optimizer = torch.optim.RMSprop(self.critic_parameters, lr=args.lr_critic)
  60. self.rnn_optimizer = torch.optim.RMSprop(self.rnn_parameters, lr=args.lr_actor)
  61. self.args = args
  62. # 执行过程中,要为每个agent都维护一个eval_hidden
  63. # 学习过程中,要为每个episode的每个agent都维护一个eval_hidden
  64. self.eval_hidden = None
  65. def _get_critic_input_shape(self):
  66. # state
  67. input_shape = self.state_shape # 48
  68. # obs
  69. input_shape += self.obs_shape # 30
  70. # agent_id
  71. input_shape += self.n_agents # 3
  72. # 所有agent的当前动作和上一个动作
  73. input_shape += self.n_actions * self.n_agents * 2 # 54
  74. return input_shape
  75. def learn(self, batch, max_episode_len, train_step, epsilon): # train_step表示是第几次学习,用来控制更新target_net网络的参数
  76. episode_num = batch['o'].shape[0]
  77. self.init_hidden(episode_num)
  78. for key in batch.keys(): # 把batch里的数据转化成tensor
  79. if key == 'u':
  80. batch[key] = torch.tensor(batch[key], dtype=torch.long)
  81. else:
  82. batch[key] = torch.tensor(batch[key], dtype=torch.float32)
  83. u, r, avail_u, terminated = batch['u'], batch['r'], batch['avail_u'], batch['terminated']
  84. mask = (1 - batch["padded"].float()).repeat(1, 1, self.n_agents) # 用来把那些填充的经验的TD-error置0,从而不让它们影响到学习
  85. if self.args.cuda:
  86. u = u.cuda()
  87. mask = mask.cuda()
  88. # 根据经验计算每个agent的Q值,从而跟新Critic网络。然后计算各个动作执行的概率,从而计算advantage去更新Actor。
  89. q_values = self._train_critic(batch, max_episode_len, train_step) # 训练critic网络,并且得到每个agent的所有动作的Q值
  90. action_prob = self._get_action_prob(batch, max_episode_len, epsilon) # 每个agent的所有动作的概率
  91. q_taken = torch.gather(q_values, dim=3, index=u).squeeze(3) # 每个agent的选择的动作对应的Q值
  92. pi_taken = torch.gather(action_prob, dim=3, index=u).squeeze(3) # 每个agent的选择的动作对应的概率
  93. pi_taken[mask == 0] = 1.0 # 因为要取对数,对于那些填充的经验,所有概率都为0,取了log就是负无穷了,所以让它们变成1
  94. log_pi_taken = torch.log(pi_taken)
  95. # 计算advantage
  96. baseline = (q_values * action_prob).sum(dim=3, keepdim=True).squeeze(3).detach()
  97. advantage = (q_taken - baseline).detach()
  98. loss = - ((advantage * log_pi_taken) * mask).sum() / mask.sum()
  99. self.rnn_optimizer.zero_grad()
  100. loss.backward()
  101. torch.nn.utils.clip_grad_norm_(self.rnn_parameters, self.args.grad_norm_clip)
  102. self.rnn_optimizer.step()
  103. # print('Training: loss is', loss.item())
  104. # print('Training: critic params')
  105. # for params in self.eval_critic.named_parameters():
  106. # print(params)
  107. # print('Training: actor params')
  108. # for params in self.eval_rnn.named_parameters():
  109. # print(params)
  110. def _get_critic_inputs(self, batch, transition_idx, max_episode_len):
  111. # 取出所有episode上该transition_idx的经验
  112. obs, obs_next, s, s_next = batch['o'][:, transition_idx], batch['o_next'][:, transition_idx],\
  113. batch['s'][:, transition_idx], batch['s_next'][:, transition_idx]
  114. u_onehot = batch['u_onehot'][:, transition_idx]
  115. if transition_idx != max_episode_len - 1:
  116. u_onehot_next = batch['u_onehot'][:, transition_idx + 1]
  117. else:
  118. u_onehot_next = torch.zeros(*u_onehot.shape)
  119. # s和s_next是二维的,没有n_agents维度,因为所有agent的s一样。其他都是三维的,到时候不能拼接,所以要把s转化成三维的
  120. s = s.unsqueeze(1).expand(-1, self.n_agents, -1)
  121. s_next = s_next.unsqueeze(1).expand(-1, self.n_agents, -1)
  122. episode_num = obs.shape[0]
  123. # 因为coma的critic用到的是所有agent的动作,所以要把u_onehot最后一个维度上当前agent的动作变成所有agent的动作
  124. u_onehot = u_onehot.view((episode_num, 1, -1)).repeat(1, self.n_agents, 1)
  125. u_onehot_next = u_onehot_next.view((episode_num, 1, -1)).repeat(1, self.n_agents, 1)
  126. if transition_idx == 0: # 如果是第一条经验,就让前一个动作为0向量
  127. u_onehot_last = torch.zeros_like(u_onehot)
  128. else:
  129. u_onehot_last = batch['u_onehot'][:, transition_idx - 1]
  130. u_onehot_last = u_onehot_last.view((episode_num, 1, -1)).repeat(1, self.n_agents, 1)
  131. inputs, inputs_next = [], []
  132. # 添加状态
  133. inputs.append(s)
  134. inputs_next.append(s_next)
  135. # 添加obs
  136. inputs.append(obs)
  137. inputs_next.append(obs_next)
  138. # 添加所有agent的上一个动作
  139. inputs.append(u_onehot_last)
  140. inputs_next.append(u_onehot)
  141. # 添加当前动作
  142. '''
  143. 因为coma对于当前动作,输入的是其他agent的当前动作,不输入当前agent的动作,为了方便起见,每次虽然输入当前agent的
  144. 当前动作,但是将其置为0相量,也就相当于没有输入。
  145. '''
  146. action_mask = (1 - torch.eye(self.n_agents)) # th.eye()生成一个二维对角矩阵
  147. # 得到一个矩阵action_mask,用来将(episode_num, n_agents, n_agents * n_actions)的actions中每个agent自己的动作变成0向量
  148. action_mask = action_mask.view(-1, 1).repeat(1, self.n_actions).view(self.n_agents, -1)
  149. inputs.append(u_onehot * action_mask.unsqueeze(0))
  150. inputs_next.append(u_onehot_next * action_mask.unsqueeze(0))
  151. # 添加agent编号对应的one-hot向量
  152. '''
  153. 因为当前的inputs三维的数据,每一维分别代表(episode编号,agent编号,inputs维度),直接在后面添加对应的向量
  154. 即可,比如给agent_0后面加(1, 0, 0, 0, 0),表示5个agent中的0号。而agent_0的数据正好在第0行,那么需要加的
  155. agent编号恰好就是一个单位矩阵,即对角线为1,其余为0
  156. '''
  157. inputs.append(torch.eye(self.n_agents).unsqueeze(0).expand(episode_num, -1, -1))
  158. inputs_next.append(torch.eye(self.n_agents).unsqueeze(0).expand(episode_num, -1, -1))
  159. # 要把inputs中的5项输入拼起来,并且要把其维度从(episode_num, n_agents, inputs)三维转换成(episode_num * n_agents, inputs)二维
  160. inputs = torch.cat([x.reshape(episode_num * self.n_agents, -1) for x in inputs], dim=1)
  161. inputs_next = torch.cat([x.reshape(episode_num * self.n_agents, -1) for x in inputs_next], dim=1)
  162. return inputs, inputs_next
  163. def _get_q_values(self, batch, max_episode_len):
  164. episode_num = batch['o'].shape[0]
  165. q_evals, q_targets = [], []
  166. for transition_idx in range(max_episode_len):
  167. inputs, inputs_next = self._get_critic_inputs(batch, transition_idx, max_episode_len)
  168. if self.args.cuda:
  169. inputs = inputs.cuda()
  170. inputs_next = inputs_next.cuda()
  171. # 神经网络输入的是(episode_num * n_agents, inputs)二维数据,得到的是(episode_num * n_agents, n_actions)二维数据
  172. q_eval = self.eval_critic(inputs)
  173. q_target = self.target_critic(inputs_next)
  174. # 把q值的维度重新变回(episode_num, n_agents, n_actions)
  175. q_eval = q_eval.view(episode_num, self.n_agents, -1)
  176. q_target = q_target.view(episode_num, self.n_agents, -1)
  177. q_evals.append(q_eval)
  178. q_targets.append(q_target)
  179. # 得的q_evals和q_targets是一个列表,列表里装着max_episode_len个数组,数组的的维度是(episode个数, n_agents,n_actions)
  180. # 把该列表转化成(episode个数, max_episode_len, n_agents,n_actions)的数组
  181. q_evals = torch.stack(q_evals, dim=1)
  182. q_targets = torch.stack(q_targets, dim=1)
  183. return q_evals, q_targets
  184. def _get_actor_inputs(self, batch, transition_idx):
  185. # 取出所有episode上该transition_idx的经验,u_onehot要取出所有,因为要用到上一条
  186. obs, u_onehot = batch['o'][:, transition_idx], batch['u_onehot'][:]
  187. episode_num = obs.shape[0]
  188. inputs = []
  189. inputs.append(obs)
  190. # 给inputs添加上一个动作、agent编号
  191. if self.args.last_action:
  192. if transition_idx == 0: # 如果是第一条经验,就让前一个动作为0向量
  193. inputs.append(torch.zeros_like(u_onehot[:, transition_idx]))
  194. else:
  195. inputs.append(u_onehot[:, transition_idx - 1])
  196. if self.args.reuse_network:
  197. # 因为当前的inputs三维的数据,每一维分别代表(episode编号,agent编号,inputs维度),直接在dim_1上添加对应的向量
  198. # 即可,比如给agent_0后面加(1, 0, 0, 0, 0),表示5个agent中的0号。而agent_0的数据正好在第0行,那么需要加的
  199. # agent编号恰好就是一个单位矩阵,即对角线为1,其余为0
  200. inputs.append(torch.eye(self.args.n_agents).unsqueeze(0).expand(episode_num, -1, -1))
  201. # 要把inputs中的三个拼起来,并且要把episode_num个episode、self.args.n_agents个agent的数据拼成40条(40,96)的数据,
  202. # 因为这里所有agent共享一个神经网络,每条数据中带上了自己的编号,所以还是自己的数据
  203. inputs = torch.cat([x.reshape(episode_num * self.args.n_agents, -1) for x in inputs], dim=1)
  204. return inputs
  205. def _get_action_prob(self, batch, max_episode_len, epsilon):
  206. episode_num = batch['o'].shape[0]
  207. avail_actions = batch['avail_u'] # coma不用target_actor,所以不需要最后一个obs的下一个可执行动作
  208. action_prob = []
  209. for transition_idx in range(max_episode_len):
  210. inputs = self._get_actor_inputs(batch, transition_idx) # 给obs加last_action、agent_id
  211. if self.args.cuda:
  212. inputs = inputs.cuda()
  213. self.eval_hidden = self.eval_hidden.cuda()
  214. outputs, self.eval_hidden = self.eval_rnn(inputs, self.eval_hidden) # inputs维度为(40,96),得到的q_eval维度为(40,n_actions)
  215. # 把q_eval维度重新变回(8, 5,n_actions)
  216. outputs = outputs.view(episode_num, self.n_agents, -1)
  217. prob = torch.nn.functional.softmax(outputs, dim=-1)
  218. action_prob.append(prob)
  219. # 得的action_prob是一个列表,列表里装着max_episode_len个数组,数组的的维度是(episode个数, n_agents,n_actions)
  220. # 把该列表转化成(episode个数, max_episode_len, n_agents,n_actions)的数组
  221. action_prob = torch.stack(action_prob, dim=1).cpu()
  222. action_num = avail_actions.sum(dim=-1, keepdim=True).float().repeat(1, 1, 1, avail_actions.shape[-1]) # 可以选择的动作的个数
  223. action_prob = ((1 - epsilon) * action_prob + torch.ones_like(action_prob) * epsilon / action_num)
  224. action_prob[avail_actions == 0] = 0.0 # 不能执行的动作概率为0
  225. # 因为上面把不能执行的动作概率置为0,所以概率和不为1了,这里要重新正则化一下。执行过程中Categorical会自己正则化。
  226. action_prob = action_prob / action_prob.sum(dim=-1, keepdim=True)
  227. # 因为有许多经验是填充的,它们的avail_actions都填充的是0,所以该经验上所有动作的概率都为0,在正则化的时候会得到nan。
  228. # 因此需要再一次将该经验对应的概率置为0
  229. action_prob[avail_actions == 0] = 0.0
  230. if self.args.cuda:
  231. action_prob = action_prob.cuda()
  232. return action_prob
  233. def init_hidden(self, episode_num):
  234. # 为每个episode中的每个agent都初始化一个eval_hidden
  235. self.eval_hidden = torch.zeros((episode_num, self.n_agents, self.args.rnn_hidden_dim))
  236. def _train_critic(self, batch, max_episode_len, train_step):
  237. u, r, avail_u, terminated = batch['u'], batch['r'], batch['avail_u'], batch['terminated']
  238. u_next = u[:, 1:]
  239. padded_u_next = torch.zeros(*u[:, -1].shape, dtype=torch.long).unsqueeze(1)
  240. u_next = torch.cat((u_next, padded_u_next), dim=1)
  241. mask = (1 - batch["padded"].float()).repeat(1, 1, self.n_agents) # 用来把那些填充的经验的TD-error置0,从而不让它们影响到学习
  242. if self.args.cuda:
  243. u = u.cuda()
  244. u_next = u_next.cuda()
  245. mask = mask.cuda()
  246. # 得到每个agent对应的Q值,维度为(episode个数, max_episode_len, n_agents,n_actions)
  247. # q_next_target为下一个状态-动作对应的target网络输出的Q值,没有包括reward
  248. q_evals, q_next_target = self._get_q_values(batch, max_episode_len)
  249. q_values = q_evals.clone() # 在函数的最后返回,用来计算advantage从而更新actor
  250. # 取每个agent动作对应的Q值,并且把最后不需要的一维去掉,因为最后一维只有一个值了
  251. q_evals = torch.gather(q_evals, dim=3, index=u).squeeze(3)
  252. q_next_target = torch.gather(q_next_target, dim=3, index=u_next).squeeze(3)
  253. targets = td_lambda_target(batch, max_episode_len, q_next_target.cpu(), self.args)
  254. if self.args.cuda:
  255. targets = targets.cuda()
  256. td_error = targets.detach() - q_evals
  257. masked_td_error = mask * td_error # 抹掉填充的经验的td_error
  258. # 不能直接用mean,因为还有许多经验是没用的,所以要求和再比真实的经验数,才是真正的均值
  259. loss = (masked_td_error ** 2).sum() / mask.sum()
  260. # print('Loss is ', loss)
  261. self.critic_optimizer.zero_grad()
  262. loss.backward()
  263. torch.nn.utils.clip_grad_norm_(self.critic_parameters, self.args.grad_norm_clip)
  264. self.critic_optimizer.step()
  265. if train_step > 0 and train_step % self.args.target_update_cycle == 0:
  266. self.target_critic.load_state_dict(self.eval_critic.state_dict())
  267. return q_values
  268. def save_model(self, train_step):
  269. num = str(train_step // self.args.save_cycle)
  270. if not os.path.exists(self.model_dir):
  271. os.makedirs(self.model_dir)
  272. torch.save(self.eval_critic.state_dict(), self.model_dir + '/' + num + '_critic_params.pkl')
  273. torch.save(self.eval_rnn.state_dict(), self.model_dir + '/' + num + '_rnn_params.pkl')

  1. 文章若有不当和不正确之处,还望理解与指出。由于部分文字、图片等来源于互联网,无法核实真实出处,如涉及相关争议,请联系博主删除。如有错误、疑问和侵权,欢迎评论留言联系作者,或者关注VX公众号:**Rain21321,**联系作者。

本文转载自: https://blog.csdn.net/qq_51399582/article/details/142109244
版权归原作者 不去幼儿园 所有, 如有侵权,请联系我们删除。

“【MADRL】反事实多智能体策略梯度(COMA)算法”的评论:

还没有评论