资讯详情

资讯详情

MADDPG多智能体博弈对抗算法:原理、代码与调参实战

简介一套基于Python与MADDPG多智能体深度确定性策略梯度算法实现的多智能体博弈对抗项目面向希望入门强化学习或多智能体协作/对抗场景的学习者也适合作为毕业设计、课程设计或工程实训的参考实现。资源包共14个文件包含10个Python脚本、1个配置文件、1个说明文档等压缩包仅19KB代码结构紧凑。Python脚本覆盖了MADDPG核心网络network.py、经验回放缓冲buffer.py、DDPG基础策略以及主训练流程main.py另有测试环境与示例脚本便于快速运行验证。目前已有267人学习下载尤其适合需要对照源码理解多智能体训练过程、策略更新与奖励设计的读者。通过阅读README和代码注释可以快速复现训练流程并在此基础上扩展自己的对抗博弈任务。1. 多智能体博弈对抗算法用 MADDPG 把非平稳对抗训练拉回正轨做多智能体博弈对抗算法最怕的往往不是模型复杂而是环境非平稳——对手在变、策略在变、奖励也在变你拿单智能体 RL 的惯性思维硬套进去奖励曲线跟心电图似的训练完全不可控。MADDPG 之所以成为这个场景里的常见解法是因为它把“中心化训练、去中心化执行”带进了连续动作对抗任务训练时让每个智能体的 Critic 看到全部智能体的状态和动作执行时却只让 Actor 依据自己的局部观测做决策。这套基于 Python 的 MADDPG 多智能体博弈对抗算法代码包把网络定义、缓冲区、环境、训练与测试脚本拆成了可直接运行的文件适合做毕设、课程设计、大作业也适合刚接触强化学习、想在多智能体方向落地一个项目的学习者。本文就按源码包的文件组织把这套代码从原理到排错拆开讲。2. 从 DDPG 到 MADDPG网络结构、经验回放与 CTDE 实现2.1 单智能体基线network.py 里面 Actor-Critic 怎么定义拿到代码包建议先读 network.py。MADDPG 的底层策略网络基本沿用 DDPG 的结构差别只在网络的输入输出拼接方式上。这个文件里一般就是一个多层感知机用来承担 Actor 和 Critic 的主体计算。代码包的典型实现是这样的import torch import torch.nn as nn import torch.nn.functional as F class MLP(nn.Module): def __init__(self, input_dim, hidden_dim, output_dim, hidden_layers2): super(MLP, self).__init__() layers [] dims [input_dim] [hidden_dim] * hidden_layers [output_dim] for i in range(len(dims) - 2): layers.append(nn.Linear(dims[i], dims[i1])) layers.append(nn.ReLU()) layers.append(nn.Linear(dims[-2], dims[-1])) self.net nn.Sequential(*layers) def forward(self, x): return self.net(x)这段代码定义了一个可配置层数的全连接网络中间层用 ReLU 激活。Actor 在调用这个 MLP 之后通常还会接一个 tanh 层把连续动作压缩到 [-1, 1]Critic 则直接输出一个标量 Q 值。实际使用中hidden_dim 取 64 还是 128要看观测维度有多大。我一般会先看 obs 的维度如果单个智能体的观测在 6 维以内64 足够如果观测到了 20 维以上就要把 hidden_dim 提到 128同时把 hidden_layers 从 2 提到 3否则 Critic 对全局状态的拟合能力不够训练出来的 Q 值会发飘。2.2 MADDPG.py 里 Critic 如何共享观测CTDE 的核心MADDPG 与 DDPG 最大的区别在 MADDPG.py 里体现得最明显。每个智能体拥有自己的 Actor但 Critic 的输入是所有智能体的 obs 和 action 拼接在一起。这样训练时 Critic 能看到全局信息评估动作价值时能把对手的策略变化当作环境状态的一部分从而缓解环境非平稳带来的误导。class MADDPG: def __init__(self, n_agents, obs_dims, act_dims, hidden_dim64, lr_actor1e-4, lr_critic1e-3, gamma0.95, tau0.01): self.n_agents n_agents self.actors [Actor(obs_dims[i], act_dims[i], hidden_dim) for i in range(n_agents)] self.target_actors [Actor(obs_dims[i], act_dims[i], hidden_dim) for i in range(n_agents)] total_obs sum(obs_dims) total_act sum(act_dims) self.critics [Critic(total_obs total_act, hidden_dim) for _ in range(n_agents)] self.target_critics [Critic(total_obs total_act, hidden_dim) for _ in range(n_agents)] self.actor_optim [torch.optim.Adam(self.actors[i].parameters(), lrlr_actor) for i in range(n_agents)] self.critic_optim [torch.optim.Adam(self.critics[i].parameters(), lrlr_critic) for i in range(n_agents)]这里最关键的参数是total_obs total_act。它表示 Critic 输入维度是全体智能体的观测与动作拼接宽度。假设两个智能体各自的 obs 是 6 维、动作是 2 维那么 Critic 的输入就是 662216 维。执行阶段的 Actor 却只看自己的 obs这就是中心化训练、去中心化执行。这个设计的直接收益是训练阶段 Critic 能看到对手的动作因此可以学习“如果我这么做、对手那么做结果是什么样”执行阶段去掉 Critic只保留 Actor让每个智能体独立决策也方便部署到实际环境里。2.3 buffer.py 经验回放多智能体转移数据怎么组织经验回放在多智能体里特别容易写错。单智能体 DDPG 的缓冲区按一条条 (s, a, r, s) 存没问题但 MADDPG 里同一个 step 所有智能体的数据必须绑在一起存。理由很简单Critic 更新时要拿同一个时刻的全局数据来拼接如果不同智能体的样本来自不同 step时间对不上训练的输入就是乱序的。class ReplayBuffer: def __init__(self, buffer_size, n_agents): self.buffer_size buffer_size self.n_agents n_agents self.buffer [] self.pos 0 def push(self, obs_n, act_n, rew_n, next_obs_n, done): data (obs_n, act_n, rew_n, next_obs_n, done) if len(self.buffer) self.buffer_size: self.buffer.append(data) else: self.buffer[self.pos] data self.pos (self.pos 1) % self.buffer_size def sample(self, batch_size): batch random.sample(self.buffer, batch_size) obs_n [[exp[0][i] for exp in batch] for i in range(self.n_agents)] act_n [[exp[1][i] for exp in batch] for i in range(self.n_agents)] rew_n [[exp[2][i] for exp in batch] for i in range(self.n_agents)] next_obs_n [[exp[3][i] for exp in batch] for i in range(self.n_agents)] return obs_n, act_n, rew_n, next_obs_n, batch[0][4]sample 返回的 obs_n 形状是 (n_agents, batch_size, obs_dim)每个智能体都能拿到属于自己的 batch 数据。这里容易翻车的一个点有的实现把 buffer_size 设很大但 batch_size 很小导致采样出来的样本时间跨度太大相邻样本之间相关性太低Critic 学习起来反而吃力。一般 buffer_size 50000 配 batch_size 64 起步卡了再调。这段代码里 sample 出的 done 直接取 batch[0] 的这是简化处理因为多智能体环境所有智能体通常共享同一个 done。如果你的场景允许单个智能体提前退出需要给每个智能体单独存 done。超参数建议值调参方向hidden_dim64~128观测维度大或任务复杂时上调lr_actor1e-4训练初期 loss 不降可调大发散则调小lr_critic1e-3Critic 比 Actor 学得快是正常的gamma0.95~0.99任务回合长则调大tau0.01Q 值震荡严重时调小到 0.0013. 环境、噪声与训练主循环从 mag.py 到 main.py 跑通对抗场景3.1 对抗环境mag.py 里如何模拟博弈对抗的胜负mag.py 在代码包里通常是多智能体对抗环境。MADDPG 不限定具体任务代码包给出的环境多是一个连续动作空间的博弈场景比如追击-逃跑一个智能体负责追一个负责逃两者的奖励在设计上形成对抗关系。class PursuitEvadeEnv: def __init__(self, max_steps200, dt0.05): self.max_steps max_steps self.dt dt self.action_dim 2 # 水平方向的 dx, dy self.obs_dim 6 # 自身位置、速度、目标相对位置 self.n_agents 2 self.capture_radius 0.3 def reset(self): self.pursuer np.array([0.0, 0.0]) self.evader np.array([3.0, 3.0]) self.steps 0 return self._get_obs() def step(self, actions): self.pursuer actions[0] * self.dt self.evader actions[1] * self.dt dist np.linalg.norm(self.pursuer - self.evader) reward_pursuer 1.0 if dist self.capture_radius else -0.01 reward_evader -reward_pursuer done (dist self.capture_radius) or (self.steps self.max_steps) self.steps 1 return self._get_obs(), [reward_pursuer, reward_evader], done, {} def _get_obs(self): return [np.concatenate([self.pursuer, self.pursuer, self.evader - self.pursuer]), np.concatenate([self.evader, self.evader, self.pursuer - self.evader])]这里尤其要注意 reward 的设计追击者靠近目标得正奖励逃跑者得到相反奖励这样才能形成博弈。很多人第一次写环境时喜欢给两个智能体都设置正向稀疏奖励那样 MADDPG 学出来的策略是“各管各的”对抗关系完全不存在训练结果也没法解释。step 函数 return 的 rewards 是列表长度必须等于智能体数量。环境写完第一件事就是用随机动作连续跑几十步确认不会报维度错误再进训练。3.2 动作探索rl_utils.py 里的噪声与探索策略MADDPG 的 Actor 输出的是确定性动作如果训练时完全不加入噪声策略会一直沿着初始方向走探索能力很弱。代码包通常在 rl_utils.py 里提供 Ornstein-Uhlenbeck 噪声作为探索信号。class OrnsteinUhlenbeckNoise: def __init__(self, action_dim, mu0.0, theta0.15, sigma0.2): self.theta theta self.sigma sigma self.mu mu self.state np.ones(action_dim) * mu def reset(self): self.state np.ones(self.action_dim) * self.mu def sample(self): dx self.theta * (self.mu - self.state) self.sigma * np.random.randn(self.action_dim) self.state dx return self.stateOU 噪声的特点是状态在时间上相关不会像独立高斯噪声那样来回高频抖动。对连续控制来说这个特性让动作更平滑也更符合物理环境的控制逻辑。sigma决定噪声幅度theta决定噪声向 0 回拉的强度。训练初期 sigma 用 0.2 没问题训练到中期 reward 曲线开始上升后我一般把 sigma 减半让策略更多依赖已学到的 Actor 输出而不是靠噪声瞎撞。这个衰减很多新人不做结果就是模型快收敛了又被大噪声扰动弹出去。3.3 训练主循环main.py 中 MADDPG 的训练节奏与参数配置main.py 把前面的模块串起来。训练循环的骨架是每回合 reset 环境和噪声收集每一步所有智能体的转移数据存入缓冲区缓冲区样本足够多后再批量更新网络然后软更新 target 网络。for episode in range(total_episodes): obs_n env.reset() noise.reset() episode_rewards [0.0] * n_agents while not done: act_n [] for i in range(n_agents): action maddpg.actors[i](torch.FloatTensor(obs_n[i])).detach().numpy() action noise.sample() act_n.append(np.clip(action, -1, 1)) next_obs_n, rew_n, done, info env.step(act_n) buffer.push(obs_n, act_n, rew_n, next_obs_n, done) if len(buffer.buffer) batch_size: maddpg.update(buffer, batch_size) maddpg.soft_update(tau) obs_n next_obs_n for idx, r in enumerate(rew_n): episode_rewards[idx] r这里有几个容易被忽略的细节。动作在送入环境前要 clip 到 [-1, 1]否则策略网络偶尔输出一个超出动作空间的值环境就会收到非法控制量。噪声加在 Actor 的输出上、在 clip 之前符合探索逻辑。soft_update(tau)是所有 target 网络共享同一个 tau不需要每个智能体单独调。update 函数里Critic 的 loss 是均方差形式用 target_critic 算出的目标 Q 值减去当前 critic 的 Q 值Actor 的 loss 是负的 Q 值均值梯度从 Critic 反传到 Actor。代码包里 DEFAULT 超参一般是 total_episodes500、batch_size64、buffer_size50000。训练间隔不是每步都 update而是缓冲区数据超过 batch_size 就开始每个 step 更新一次这是常见做法。4. 测试与评估用 test_env.py 和 ceshi.py 检验训练效果4.1 环境自检先验证环境是否可交互训练之前先把 test_env.py 跑通。这个脚本的作用不是评估模型而是确认环境本身没有维度错位或者 reward 逻辑错误。用随机策略跑几步是排查环境 bug 最直接的方式。env PursuitEvadeEnv() obs env.reset() for step in range(10): actions [np.random.uniform(-1, 1, env.action_dim) for _ in range(env.n_agents)] next_obs, rewards, done, info env.step(actions) assert len(next_obs) env.n_agents assert len(rewards) env.n_agents assert next_obs[i].shape obs[i].shape print(fstep {step}: rewards {rewards}, done {done})如果 assert 不通过问题大概率出在环境 step 返回的数据组织方式上。最常见的是 obs 维度没有按智能体数量拆分导致后续训练循环里obs_n[i]取到的是完整观测而不是单个智能体的观测。test_env.py 里跑随机动作还有一个额外作用观察 reward 的量级。如果 reward 数量级差到 1e3 以上训练时梯度很容易爆炸需要在环境里做 clip。4.2 模型评估不看 loss看两个智能体的胜率变化训练过程中的 actor loss 下降不代表策略真的变好因为对手也在学习Q 值本身是相对估计。代码包里的 ceshi.py 和 test.py 做的事情基本一致关掉探索噪声、加载训练好的模型让两个智能体对打若干个回合统计胜负比例。def evaluate(maddpg, env, episodes100): wins 0 for _ in range(episodes): obs_n env.reset() done False while not done: act_n [] for i in range(env.n_agents): action maddpg.actors[i](torch.FloatTensor(obs_n[i])).detach().numpy() act_n.append(np.clip(action, -1, 1)) obs_n, rew_n, done, _ env.step(act_n) if rew_n[0] rew_n[1]: wins 1 return wins / episodes评估时必须去掉噪声用纯 Actor 输出。rew_n在这里是最后一步的 reward如果环境是稀疏奖励最后一步的数值就代表胜负结果。统计 100 个回合追击者胜率稳定在 80% 以上才算策略成型。若训练 reward 在涨但胜率不涨说明 reward 设计与胜负目标不一致比如奖励太容易累积而胜负条件太苛刻。4.3 参数调整网络层数、学习率、回放缓冲区怎么调test.py 里看到的输出除了胜率还会打印平均回合长度。回合长度过长说明策略效率低只会在环境里拖延时间过短说明可能刚开局就分出胜负策略还没学会迂回。现象调整参数调整方向Q 值发散、loss 上升lr_critic、tau调小 lr_critic 或 tau训练 reward 不涨hidden_dim、buffer_size减小缓冲区或增加隐藏层宽度动作过激、来回抖动噪声 sigma调小 sigma 或加大 theta回合过早结束reward 稀疏给中间状态加 shaped reward调参顺序我一般遵循先调环境 reward再调噪声和网络宽度最后动学习率。直接改学习率经常把原本能收敛的训练搞得震荡而 reward 层面的问题才是多智能体对抗里最普遍的根因。5. 避坑与常见问题MADDPG 调试的四个典型坑5.1 训练不收敛奖励一直在震荡刚开始跑 main.pyreward 曲线像心电图甚至 300 个回合后还在来回跳没有任何上升趋势这是最常见的现象。原因多智能体环境非平稳 奖励设计不对称。双方都在学习训练早期奖励方差天然比单智能体大但如果 reward 给得太稀疏智能体根本无法直接从奖励里获得梯度信号。解决先把其中一个智能体固定为随机策略或固定脚本策略只训练另一个确认单智能体在这种环境里能收敛再放开对抗两个智能体同时训练。代码包里 mag.py 设计成可插拔策略就是为了方便这个调试步骤。5.2 动作越界与环境不匹配训练时把动作 clip 到 [-1, 1]但环境内部实际期望的动作范围是 [-2, 2] 或者 [0, 1]导致策略学会的动作范围和环境有效动作范围不匹配行为看起来像在抽搐。原因Actor 输出层 tanh 强制压缩到 [-1, 1]环境的物理控制量却没有做对应缩放。解决在 env.step 里对动作做线性映射比如action[0] * max_speed或者在策略输出后、送入环境前乘一个缩放系数。检查代码包确认 mag.py 的 step 里是否有 action scaling 这一步没有就补上。5.3 目标网络更新太勤或太懒导致 Q 值发散训练到中途Critic loss 突然跳到 1e4 以上之后 train 不出有效策略。原因target 网络跟随在线网络更新的速度由 tau 控制。tau 太大target 变成在线网络的影子时序差分目标不断漂移tau 太小target 更新滞后又会让 Q 值估计滞后过久。解决tau 从 0.01 改成 0.001并把打印出来的 Q 值曲线作为监测指标。如果 Q 值曲线从平滑变成剧烈跳动优先调 tau 而不是降学习率。5.4 复现性差同一份代码两次结果不同两次训练一次胜率 85%一次胜率 40%代码没改结果差距很大。原因torch、numpy、random 三个库没有同时固定随机种子网络参数初始化顺序在不同运行里不同导致收敛点完全不同。解决在 main.py 开头依次设置torch.manual_seed(seed)、np.random.seed(seed)、random.seed(seed)并固定环境初始状态的生成顺序。这样至少能在同一台机器上复现同一套结果。6. 进阶用法把 MADDPG 改造成你自己的博弈对抗实验拿到这份代码包最值得做的事不是调参跑通而是把它的环境层替换成你自己的对抗任务。MADDPG.py 和 buffer.py 这两个文件基本不需要动真正要改的就是 mag.py 的环境定义。之前我在模拟项目 X 里要把追击-逃跑场景改成三智能体攻防做法是先保留原来的 obs_dim 和 action_dim 结构只改 reset 和 step 的内部逻辑。三个智能体时Critic 的输入维度会自动变成三个 obs 和三个 action 的拼接MADDPG.py 里的初始化逻辑能直接适配。改造时有个习惯值得保留每改一次环境先用 test_env.py 跑通再对比新旧环境的 obs 维度是否一致。如果新环境新增了观测信息比如增加一个墙体的位置向量obs_dim 从 6 变到 8MADDPG 初始化时传入的 obs_dims 要同步修改。这类维度不一致问题训练时不会直接报错而是会在某个 update 步骤崩在矩阵乘法上。另外可以给环境加一个 episode 级 Q 值记录器训练过程中定期保存每个智能体的平均 Q 值这样就能看到 Critic 对策略的评价是持续上涨还是虚高虚高就要回看 reward 设计。做多智能体对抗实验最重要的是把“评估”和“训练”看成两套独立流程。训练时探索、噪声、软更新都有它们的理由评估时必须全部关掉只看策略本身的表现。代码包里 test.py 和 ceshi.py 就是干这个的。从那以后我每次拿到这类多智能体源码第一件事就是先跑通 test_env.py再动网络结构最后才碰超参数这个顺序能省掉大量排查时间。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →