强化学习入门-5(MAPPO)

多智能体强化学习项目-5-simple_spread-v3(MAPPO)

环境

本环境是 PettingZoo 库中 MPE (Multi-Agent Particle Environments) 系列的一个经典多智能体协作环境。

官网链接:https://pettingzoo.farama.org/environments/mpe/simple_spread/

任务描述:

这是一个 完全合作(Cooperative) 的任务。

  • 环境中有 N N N 个智能体(Agents,通常为紫色圆圈)和 N N N 个地标(Landmarks,通常为黑色圆点)。
  • 目标是让智能体通过移动,覆盖所有的地标(每个地标上至少有一个智能体)。
  • 同时,智能体之间需要避免发生碰撞

操作 (Action Space):

设置为离散动作空间 (continuous_actions=False) 时,每个智能体的动作空间大小为 5:

  • 0: 什么都不做 (No-op)
  • 1: 向左移动 (Move Left)
  • 2: 向右移动 (Move Right)
  • 3: 向下移动 (Move Down)
  • 4: 向上移动 (Move Up)

对应状态向量 (Observation Space)

对于环境中的每一个智能体 i i i,其观测向量 o i o_i oi 包含了自己的物理状态以及与其他物体(地标、队友)的相对位置信息。

假设有 N N N 个智能体和 N N N 个地标,单个智能体的观测向量结构如下:

o i = [ v x , v y p x , p y l 1 x , l 1 y ⋮ l N x , l N y a 1 x , a 1 y ⋮ a k x , a k y ] o_i = \left[ \begin{aligned} v_x, v_y \\[4pt] p_x, p_y \\[4pt] l_{1x}, l_{1y} \\[4pt] \vdots \\[4pt] l_{Nx}, l_{Ny} \\[4pt] a_{1x}, a_{1y} \\[4pt] \vdots \\[4pt] a_{kx}, a_{ky} \end{aligned} \right] oi= vx,vypx,pyl1x,l1y⋮lNx,lNya1x,a1y⋮akx,aky

  • v x , v y v_x, v_y vx,vy : 智能体自身的在 x, y 轴上的速度 (2维)
  • p x , p y p_x, p_y px,py : 智能体自身的在 x, y 轴上的绝对位置 (2维)
  • l j x , l j y l_{jx}, l_{jy} ljx,ljy : 所有地标 相对于该智能体的相对位置向量 ( N × 2 N \times 2 N×2 维)
  • a k x , a k y a_{kx}, a_{ky} akx,aky : 其他队友 相对于该智能体的相对位置向量 ( ( N − 1 ) × 2 (N-1) \times 2 (N−1)×2 维)
  • (通讯位): 在某些配置下可能包含其他智能体的通讯信息,但在标准 simple_spread 中通常为空或不启用。

维度计算示例 (3智能体 3地标):
2 ( 速度 ) + 2 ( 位置 ) + 3 × 2 ( 地标相对位置 ) + 2 × 2 ( 队友相对位置 ) = 18 2(速度) + 2(位置) + 3 \times 2(地标相对位置) + 2 \times 2(队友相对位置) = 18 2(速度)+2(位置)+3×2(地标相对位置)+2×2(队友相对位置)=18 维。

奖励函数 (Reward Function):

该环境采用全局共享奖励(所有智能体获得的奖励值是相同的),以鼓励合作。

  • 覆盖奖励(距离惩罚) :根据每个地标距离其最近 的智能体的距离之和计算。
    • R c o v e r = − ∑ j = 1 N min ⁡ i ( dist ( a g e n t i , l a n d m a r k j ) ) R_{cover} = -\sum\limits_{j=1}^{N} \min_{i} (\text{dist}(agent_i, landmark_j)) Rcover=−j=1∑Nmini(dist(agenti,landmarkj))
    • 也就是:大家要把所有坑都填上,距离坑越近,惩罚越小(分越高)。
  • 碰撞惩罚 :如果两个智能体发生重叠(碰撞)。
    • 每次碰撞: − 1 -1 −1 (该数值可能随版本微调,通常是固定的惩罚值)。

总奖励 = 覆盖奖励 + 碰撞惩罚

引入环境

下载包

PettingZoo 是 Gymnasium 的多智能体版本。

text 复制代码
pip install pettingzoo[mpe]
导入

注意:多智能体通常使用 parallel_env 来支持并行训练(如 MAPPO)。

python 复制代码
from pettingzoo.mpe import simple_spread_v3

# 创建并行环境
# continuous_actions=False 表示使用离散动作(0-4)
# render_mode="human" 用于显示画面,训练时建议设为 None
env = simple_spread_v3.parallel_env(N=3, local_ratio=0.5, max_cycles=25, continuous_actions=False, render_mode="human")

# 重置环境
observations, infos = env.reset()

# 获取维度信息 (以第一个智能体为例)
agents = env.possible_agents
n_agents = len(agents)
obs_dim = env.observation_space(agents[0]).shape[0] # 例如 18
action_dim = env.action_space(agents[0]).n          # 例如 5

MAPPO算法

与PPO的区别

虽然此时为多智能体环境,但此时仍然采用Actor-Critic算法作为基础,但此时两个网络的功能如下:

  • Actor: 输入局部信息,输出对应的动作(即对于每个智能体,均使用该网络对动作进行预测)
  • Critic: 输入全局信息(即所有智能体的状态集合),输出预测价值

激活函数选择

对于ReLu激活函数而言,一旦出现负数,那么输出为0,同时梯度也为0,那么在强化学习中可能会导致智能体在环境中的某些区域停止学习或无法探索新的动作,导致策略陷入局部最优解甚至是崩溃。

Tanh 的优势为:虽然 Tanh 在饱和区(极值)梯度也会变小,但它处处可导且几乎不会完全"死掉"(除非数值溢出),这保证了梯度的持续流动。

因此这里选择Tanh激活函数。

改进GAE

前面使用的GAE将两种结束直接通过或运算计算得到的值作为done值的选择,但是对于模型而言,正常到达回合上限的操作往往是较好的行动,而此时若乘上 0 0 0将优势消除反而会告诉模型这个行为一文不值,就可能会导致模型最终无法收敛。

因此,这里额外传入terminations作为结束标志,用于更准确的计算得到优势。

代码如下:

python 复制代码
    def GAE(self, global_obs, next_global_obs, rewards, dones, terminations):
        rewards = torch.as_tensor(rewards, dtype=torch.float32, device=self.device)
        dones = torch.as_tensor(dones, dtype=torch.float32, device=self.device)
        terminations = torch.as_tensor(terminations, dtype=torch.float32, device=self.device)
        with torch.no_grad():
            values = self.critic(global_obs)
            next_values = self.critic(next_global_obs)

        T = rewards.shape[0]
        advantages = torch.zeros_like(rewards, device=self.device)
        gae = torch.zeros(1, device=self.device)
        for t in reversed(range(T)):
            mask_value = 1.0 - terminations[t]
            mask_traj = 1.0 - dones[t]

            delta = rewards[t] + self.gamma * next_values[t] * mask_value - values[t]
            gae = delta + self.gamma * self.lamda * mask_traj * gae

            advantages[t] = gae

        returns = advantages + values
        return advantages, returns

MAPPO类

MAPPO的总体操作与PPO类似,但要注意此时数据的维度调整。

python 复制代码
class MAPPO():
    def __init__(self,obs_dim, action_dim, n_agents, hidden_dim=64, actor_lr = 3e-4, critic_lr = 3e-4, gamma = 0.99, lamda = 0.95, epsilon = 0.2, K_epochs = 4, batch_size = 128):
        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        self.n_agents = n_agents
        self.global_obs_dim = obs_dim * n_agents
        self.obs_dim = obs_dim
        self.action_dim = action_dim
        self.hidden_dim = hidden_dim
        self.gamma = gamma
        self.lamda = lamda
        self.epsilon = epsilon
        self.K_epochs = K_epochs
        self.batch_size = batch_size
        self.c1_vf = 0.5
        self.c2_entropy = 0.01
        self.actor = Actor(self.obs_dim, self.action_dim, self.hidden_dim).to(self.device)
        self.critic = Critic(self.global_obs_dim, self.hidden_dim).to(self.device)
        self.actor_optimizer = optim.Adam(self.actor.parameters(), actor_lr)
        self.critic_optimizer = optim.Adam(self.critic.parameters(), critic_lr)

    def GAE(self, global_obs, next_global_obs, rewards, dones, terminations):
        rewards = torch.as_tensor(rewards, dtype=torch.float32, device=self.device)
        dones = torch.as_tensor(dones, dtype=torch.float32, device=self.device)
        terminations = torch.as_tensor(terminations, dtype=torch.float32, device=self.device)
        with torch.no_grad():
            values = self.critic(global_obs)
            next_values = self.critic(next_global_obs)

        T = rewards.shape[0]
        advantages = torch.zeros_like(rewards, device=self.device)
        gae = torch.zeros(1, device=self.device)
        for t in reversed(range(T)):
            mask_value = 1.0 - terminations[t]
            mask_traj = 1.0 - dones[t]

            delta = rewards[t] + self.gamma * next_values[t] * mask_value - values[t]
            gae = delta + self.gamma * self.lamda * mask_traj * gae

            advantages[t] = gae

        returns = advantages + values
        return advantages, returns

    def act(self, obs):
        obs = torch.as_tensor(obs, dtype = torch.float32, device=self.device)
        return self.actor.act(obs)

    def train(self, obs, global_obs, next_obs, dones, terminations, actions, rewards, old_log_probs):
        obs = torch.as_tensor(np.array(obs), dtype = torch.float32, device=self.device)
        global_obs = torch.as_tensor(np.array(global_obs), dtype = torch.float32, device=self.device)
        rewards = torch.as_tensor(np.array(rewards), dtype = torch.float32, device=self.device)
        next_obs = torch.as_tensor(np.array(next_obs), dtype = torch.float32, device=self.device)
        dones = torch.as_tensor(np.array(dones), dtype = torch.long, device=self.device)
        share_next_obs = next_obs.reshape(next_obs.shape[0], -1)
        next_global_obs = share_next_obs.unsqueeze(1).repeat(1, self.n_agents, 1)
        actions = torch.stack(actions).to(self.device).view(-1, 1)
        old_log_probs = torch.stack(old_log_probs).to(self.device).detach().view(-1, 1)

        advantages, returns = self.GAE(global_obs, next_global_obs, rewards, dones, terminations)
        returns = returns.view(-1, 1)
        obs = obs.view(-1, self.obs_dim)
        global_obs = global_obs.view(-1, self.global_obs_dim)
        advantages = advantages.view(-1, 1)
        advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
        data_length = obs.size(0)
        for T in range(self.K_epochs):
            indices = torch.randperm(data_length).to(self.device)

            for start_index in range(0, data_length, self.batch_size):
                sample_indices = indices[start_index: start_index + self.batch_size]
                mb_obs = obs[sample_indices]
                mb_global_obs = global_obs[sample_indices]
                mb_actions = actions[sample_indices]
                mb_old_log_probs = old_log_probs[sample_indices]
                mb_returns = returns[sample_indices]
                mb_advantages = advantages[sample_indices]

                probs = self.actor(mb_obs)
                dist = Categorical(logits=probs)
                new_log_probs = dist.log_prob(mb_actions.squeeze(-1)).view(-1, 1)

                ratio = torch.exp(new_log_probs - mb_old_log_probs)

                surr1 = ratio * mb_advantages
                clipped_ratio = torch.clamp(ratio, 1 - self.epsilon, 1 + self.epsilon)
                surr2 = clipped_ratio * mb_advantages
                actor_loss = -torch.min(surr1, surr2).mean()

                entropy_loss = -self.c2_entropy * dist.entropy().mean()

                actor_loss += entropy_loss

                current_values = self.critic(mb_global_obs).view(-1, 1)
                critic_loss = self.c1_vf * nn.functional.mse_loss(current_values, mb_returns)

                self.actor_optimizer.zero_grad()
                actor_loss.backward()
                torch.nn.utils.clip_grad_norm_(self.actor.parameters(), 0.5)
                self.actor_optimizer.step()

                self.critic_optimizer.zero_grad()
                critic_loss.backward()
                torch.nn.utils.clip_grad_norm_(self.critic.parameters(), 0.5)
                self.critic_optimizer.step()

    def save(self, filename):
        """
        保存模型
        filename: 保存的文件路径,例如 'model_final.pth'
        """
        checkpoint = {
            'actor_state_dict': self.actor.state_dict(),
            'critic_state_dict': self.critic.state_dict(),
            'actor_optimizer': self.actor_optimizer.state_dict(),
            'critic_optimizer': self.critic_optimizer.state_dict()
        }
        torch.save(checkpoint, filename)
        print(f"Model saved to {filename}")

    def load(self, filename, evaluate=False):
        """
        加载模型
        filename: 文件路径
        evaluate: 如果是 True,只加载 Actor 用于演示;如果是 False,加载所有用于继续训练
        """
        checkpoint = torch.load(filename, map_location=self.device)

        # 1. 加载 Actor (必须的)
        self.actor.load_state_dict(checkpoint['actor_state_dict'])

        # 2. 如果要继续训练,还需要加载 Critic 和 优化器
        if not evaluate:
            self.critic.load_state_dict(checkpoint['critic_state_dict'])
            self.actor_optimizer.load_state_dict(checkpoint['actor_optimizer'])
            self.critic_optimizer.load_state_dict(checkpoint['critic_optimizer'])
            print(f"Full model loaded from {filename} (Ready to resume training)")
        else:
            # 如果只是预测,把模式设为 eval,冻结参数
            self.actor.eval()
            print(f"Actor loaded from {filename} (Ready for inference)")

数据收集以及训练

simple-spread包含顺序模式以及并行模式,区别就是对于每个智能体的状态以及动作选择是否统一进行,并行就是统一获取所有智能体的状态,同时返回动作要将所有智能体的动作一次性告诉环境。

这里选择并行模式,因此收到的数据以及返回的动作都是字典格式的,需要进行转换。

python 复制代码
from matplotlib import pyplot as plt
from pettingzoo.mpe import simple_spread_v3
from tqdm import tqdm
import numpy as np

from MADRL.MAPPO import MAPPO

env = simple_spread_v3.parallel_env(render_mode=None, continuous_actions=False)
agents = env.possible_agents
obs_dim = env.observation_space(agents[0]).shape[0]
act_dim = env.action_space(agents[0]).n
n_agents = len(agents)
model = MAPPO(obs_dim, act_dim, n_agents=n_agents)

episodes = 3000
T_step = 1024
scores = []
score = 0
obs_dict, _ = env.reset()
pbar = tqdm(range(episodes), desc="Training")
for episode in pbar:
    done = False
    observations, global_observations, actions, rewards, dones, terminations, next_observations, old_log_probs = [], [], [], [], [], [], [], []
    for t in range(T_step):
        obs_array = np.stack([obs_dict[agent] for agent in agents])
        share_obs = obs_array.reshape(1, -1).repeat(len(env.agents), axis=0)
        global_observations.append(share_obs)
        action, old_log_prob = model.act(obs_array)
        action_cpu = action.detach().cpu().numpy()
        action_dict = dict(zip(agents, action_cpu))
        next_observation, reward, termination, truncation, _ = env.step(action_dict)
        score += np.mean(list(reward.values()))
        observations.append(obs_array)
        actions.append(action)
        re_array = np.stack([reward[agent] for agent in agents])
        rewards.append(re_array)
        ter_array = np.stack([termination[agent] for agent in agents])
        tru_array = np.stack([truncation[agent] for agent in agents])
        done = np.bitwise_or(ter_array, tru_array)
        done_flag = np.any(done).astype(np.float32)
        dones.append(done_flag)
        terminations.append(ter_array)
        obs_array = np.stack([next_observation[agent] for agent in agents])
        next_observations.append(obs_array)
        old_log_probs.append(old_log_prob)
        obs_dict = next_observation
        if np.any(done_flag):
            obs_dict, _ = env.reset()
            scores.append(score)
            score = 0

    model.train(observations, global_observations, next_observations, dones, terminations, actions, rewards, old_log_probs)
    pbar.set_postfix(ep=episode, score=f"{(scores[-1] if scores else 0):.2f}", avg100=f"{np.mean(scores[-100:]):.2f}")

if np.mean(scores[-1000:]) >= -12:
    model.save("../../model/MAPPO-simple-spread-v3.pth")

def smooth(data, weight=0.99):
    """用于平滑曲线的函数 (Exponential Moving Average)"""
    last = data[0]
    smoothed = []
    for point in data:
        smoothed_val = last * weight + (1 - weight) * point
        smoothed.append(smoothed_val)
        last = smoothed_val
    return smoothed

# 使用方法:
smooth_scores = smooth(scores, weight=0.99)

plt.figure(figsize=(10, 5))
plt.plot(scores, alpha=0.3, color='lightblue', label='Raw Score') # 原始数据画成浅色背景
plt.plot(smooth_scores, color='blue', label='Smoothed Score')     # 平滑数据画成深色线条
plt.title("MAPPO on Simple Spread")
plt.xlabel("Episode")
plt.ylabel("Score")
plt.legend()
plt.show()

训练结果

经过3000轮训练后(每轮1024步),模型表现均值收敛在 − 10 -10 −10左右。

相关推荐
free-elcmacom1 天前
机器学习进阶<5>K-means智能客户分群与可视化分析系统
算法·机器学习·kmeans
爱学习的张大1 天前
大话机器学习-1.神经网络
人工智能·神经网络·机器学习
秋刀鱼 ..1 天前
2026年人机交互、神经网络与深度学习国际学术会议
人工智能·深度学习·神经网络·机器学习·自动化·人机交互·制造
TracyCoder1231 天前
机器学习与深度学习基础(一):监督学习、线性回归、逻辑回归
深度学习·学习·机器学习
s1ckrain1 天前
数字逻辑笔记—同步时序电路
笔记·fpga开发·嵌入式
Keep__Fighting1 天前
【机器学习:K-Means】
人工智能·python·算法·机器学习·kmeans·聚类·sklearn
飞Link1 天前
【模型与算法】Isolation Forest、Local Outlier Factor、One-Class SVM 三者系统对比与实战指南
人工智能·python·机器学习·数据挖掘
从零开始学习人工智能1 天前
机器学习中的“助推器”与“调参术”:动量与参数优化全解析
人工智能·深度学习·机器学习
渡我白衣1 天前
AI应用层革命(五)——智能体的自主演化:从工具到生命
人工智能·神经网络·机器学习·计算机视觉·目标跟踪·自然语言处理·知识图谱