
简介基于Python与MADDPG的多智能体博弈对抗算法学习包面向希望掌握多智能体强化学习与对抗博弈的初学者或进阶者适合用于毕业设计、课程设计、工程实训等场景。包内共14个文件包括10个Python脚本、1个配置文件、1个说明文本、1个gitignore与1个README文档整体体积仅19KB便于快速阅读与二次开发。Python脚本中涉及DDPG.py、MADDPG.py、network.py、buffer.py等核心模块覆盖经验回放、网络构建、智能体训练与测试等环节并附有ceshi.py与test_env.py等测试文件可辅助理解算法运行流程。mag.py与rl_utils.py分别承担对抗匹配逻辑和常用强化学习工具封装main.py用于启动整体训练结构按功能拆分便于定位与修改已有267人学习具备一定参考价值。读者可从中了解MADDPG在多智能体对抗中的项目组织方式并在此基础上扩展自己的实验思路也可作为相关算法对比与改进的起点。1. 为什么说MADDPG是搞多智能体博弈绕不开的基线做多智能体博弈对抗算法的人迟早会撞上MADDPG。我第一次跑通这个基于Python的MADDPG工程时盯着终端里的训练日志心里只有一个念头原来“集中训练、分散执行”是真的能把追逐-躲避这类对抗场景训出策略的。这份工程不是一段教程代码而是一套完整可运行的多智能体博弈对抗算法包里面同时包含DDPG单智能体基线、MADDPG多智能体训练主流程、经验回放池和多个测试脚本。它能直接解决“环境搭不起来、算法调不通、训练不收敛”这三类问题适合正在准备毕设、课程设计或者想从单智能体强化学习过渡到多智能体博弈的人。2. 集中训练与分散执行为什么MADDPG能处理非平稳博弈2.1 非平稳性问题与CTDE的破解思路多智能体环境和单智能体环境的本质区别在于环境不是静止的。你在训练自己的智能体时其它智能体也在动、也在学于是每个智能体眼里的转移概率都随着别人的策略变化而变化这就是非平稳性。DDPG、PPO这类单智能体算法默认环境是平稳的放到多智能体场景里训练Q值梯度会来回震荡表现为奖励曲线一直不收敛或者明明学到了策略又突然崩掉。MADDPG走的是另一条路训练阶段让每个智能体的Critic拿到所有智能体的观测和动作相当于给每个智能体配了一个“上帝视角”的裁判执行阶段所有智能体只用自己的Actor、只依赖自己的局部观测做决策。这就是CTDE即集中训练、分散执行。Critic因为信息完整可以在训练阶段把“其它智能体的行为”当作环境动态的一部分来建模从而缓解非平稳性。具体到这份工程里MADDPG.py和DDPG.py的结构差异恰好能说明问题。DDPG.py里每个Actor对应一个CriticCritic的输入只有自己的观测和动作MADDPG.py里每个Agent的Critic输入做了拼接把环境中所有Agent的观测和动作都并进去。这个改动看起来不大但它是整个算法的灵魂。2.2 从DDPG到MADDPG网络结构与经验回放的差异写代码之前先看一张表这份工程里两类算法的对应关系组件DDPG.pyMADDPG.py网络定义network.py 中的 Actor / Criticnetwork.py 复用结构相同Critic 输入本智能体 obs action所有智能体 obs action经验回放buffer.py 单条元组buffer.py 联合转移元组动作选择Actor 直接输出确定性动作Actor 输出 探索噪声DDPG是MADDPG的单智能体底座。Actor负责把观测映射成确定性动作Critic负责给当前状态-动作对打分。MADDPG在每一个Agent内部仍然使用Actor-Critic架构只是把Critic的输入空间扩大了。以network.py里的Critic为例常见写法是class Critic(nn.Module): def __init__(self, obs_dim, act_dim, hidden_dim128): super(Critic, self).__init__() # 注意obs_dim 在MADDPG中会被替换成所有智能体的观测总和 # act_dim 同理是所有智能体动作维度的总和 self.fc1 nn.Linear(obs_dim act_dim, hidden_dim) self.fc2 nn.Linear(hidden_dim, hidden_dim) self.fc3 nn.Linear(hidden_dim, 1) def forward(self, obs, act): x torch.cat([obs, act], dim-1) x F.relu(self.fc1(x)) x F.relu(self.fc2(x)) return self.fc3(x)Critic的第一层输入维度是obs_dim act_dim。在DDPG里这两个维度都是单个智能体的但在MADDPG里调用方会把所有智能体的观测和动作按顺序拼接成一个长向量再喂进来。代码里没有花哨的网络结构核心全在维度处理和训练流程上。经验回放也在变。buffer.py里存的不再是单条(obs, action, reward, next_obs, done)而是一个时间步内所有智能体的联合转移元组。采样时取出整条元组分别给每个Agent更新自己的Critic。如果只存单个Agent的转移MADDPG的Critic就无法获得全局拼接信息训练逻辑在第一步就会断掉。2.3 目标网络与软更新稳定训练的三个细节MADDPG稳定训练靠三个细节目标网络、软更新、延迟更新频率。目标网络不直接复制参数而是每步做一个线性插值常见实现如下def soft_update(target_net, source_net, tau): # tau 是软更新系数通常取 0.01 或 0.005 # 作用让目标网络缓慢跟随在线网络避免Q值估计剧烈波动 for target_param, source_param in zip(target_net.parameters(), source_net.parameters()): target_param.data.copy_(tau * source_param.data (1.0 - tau) * target_param.data)其中tau是软更新系数。取0.01表示每一步只往在线网络的方向移动1%目标网络的变化幅度很小Q值目标就稳定得多。还有一个容易忽略的点Actor也有目标网络。因为Critic在计算目标Q值时要用到下一个状态下所有智能体的目标Actor输出动作这些动作来自目标Actor而不是训练中的在线Actor。实际调试时有人把Agent的目标网络禁用或者直接复制参数很快就会出现两个问题一个是Q值过估计奖励曲线先骗高后崩盘另一个是经验回放失效因为目标值变化太快缓冲池里的老经验失去参考意义。这属于运行顺利时感觉不到、翻车时让人头疼的细节。3. 把这份源码跑起来文件结构与训练入口3.1 代码文件清单每一份文件负责什么这份工程的文件不算多但职责划分很清晰。我拿到手第一件事就是逐个打开文件确认边界避免后面改错地方。文件职责说明MADDPG.py多智能体训练主算法Agent类、训练循环、目标网络管理DDPG.py单智能体DDPG基线用作对比和单智能体实验network.pyActor / Critic网络定义MADDPG和DDPG共用buffer.py经验回放池存储多智能体联合转移元组rl_utils.py工具函数含soft_update、reward归一化等mag.py多智能体博弈环境Multi-Agent Game环境交互核心main.py训练入口配置参数、创建环境、调用MADDPGtest.py测试入口加载模型跑对局test_env.py环境自检脚本验证环境step/reset是否正常ceshi.py调试脚本作者自己写的临时测试相当于草稿纸test.txt运行日志记录训练或测试的输出pyvenv.cfg虚拟环境配置记录Python路径和版本信息README.md使用说明作者对项目的补充说明最能看出作者思路的是mag.py和test_env.py。mag.py提供一个符合Gym接口的多智能体环境内部的reset()返回每个Agent的初始观测step()同时处理所有Agent的动作并返回对应的奖励和下一个观测。test_env.py就是用来验证这个环境的脚本如果环境自己都没跑通后面算法调得再多也是白搭。3.2 环境准备Python版本与依赖安装资源包里的pyvenv.cfg说明作者用了虚拟环境这是一个好习惯。我建议收到资源后不要直接在全局环境里装依赖而是从Python 3.8或3.9重新建一个虚拟环境再装PyTorch、numpy、gym这三个核心依赖。# 建议用 conda 或 venv 创建独立环境避免污染全局 Python conda create -n maddpg python3.9 conda activate maddpg # 安装 CPU 版 torch 避免 GPU 相关兼容问题 pip install torch --index-url https://download.pytorch.org/whl/cpu # 其余依赖按 README 里的 requirements 装核心是 numpy 和 gym pip install numpy gym安装时最容易翻车的是torch版本和Python版本不匹配。比如Python 3.7装新版torch会直接报No matching distribution found这时候不要硬刚换Python 3.9或者装低版本torch就好。还有gym的API版本差异老工程里的gym.make在新版gym里可能需要加render_mode参数遇到报错先看README里锁没锁版本。如果是在VSCode里跑记得把解释器切换到新建的conda环境。很多初学者代码没问题但终端里用的还是系统默认Python导致import torch失败这属于环境变量配置的问题不是代码的问题。3.3 启动训练main.py的参数与第一次跑通环境准备好后直接运行main.py就可以看到训练流程。在没有GPU的情况下这份工程的网络规模很小CPU也能跑只是慢一些。python main.py如果main.py里已配置环境名参数可以按下面这种格式指定# 用simple_tag环境跑10000个episode每轮最多25步 python main.py --env-name simple_tag --episode-num 10000 --max-episode-len 25打开main.py可以看到超参基本集中在文件头部常见的关键参数如下# 训练主流程的典型配置 env_name simple_tag # 环境名追击-躲避 n_agents 3 # 智能体数量1个追击者 2个躲避者 max_episode_len 25 # 每个episode最大步数 batch_size 256 # 经验回放采样批大小 buffer_size 100000 # 经验回放容量 episode_num 10000 # 训练总episode数 gamma 0.95 # 折扣因子 tau 0.01 # 目标网络软更新系数 lr_actor 1e-4 # Actor学习率 lr_critic 1e-3 # Critic学习率 noise_scale 0.1 # 探索噪声标准差代码逻辑是先初始化环境、创建MADDPG对象、然后进入训练循环。每个episode开始调用环境的reset()拿到初始观测训练中每个时间步先根据当前观测和噪声选择动作再执行step()拿到奖励和下一个观测。这些转移数据统一存入buffer.py中的经验回放池当缓冲池里的数据量超过batch_size后开始采样更新。noise_scale是最值得玩味的参数。取0.1时训练早期探索比较充分后期靠衰减让策略稳定取0.3以上时前期探索强但收敛变慢取0.05以下时可能陷入局部最优具体要看环境复杂度。3.4 第一次运行看什么日志、曲线与模型保存训练跑起来后终端里会打印每一轮或每几轮的奖励均值。第一次运行我建议只看三个东西第一是reward曲线整体趋势是上升还是横盘第二是模型文件有没有按预期权重保存第三是test.txt里作者留下的日志格式。# 观察控制台输出正常训练会逐步打印 episode 和 reward # 如果没有任何打印先确认 main.py 里有没有配置日志输出 tail -f training_log.txt模型保存一般会放在checkpoints或models目录下的.pt或.pth文件。加载模型的入口在test.py后面的章节会具体讲怎么验证效果。如果跑500个episode奖励曲线还没有任何上升趋势建议先不要调代码先跑一个更短的小实验比如episode_num500噪声固定为0确认环境本身能玩起来再考虑是不是策略网络梯度出了问题。4. MADDPG避坑指南训练不收敛、崩溃与维度不匹配的常见问题这一章都是我自己跑这份工程时踩过的坑。每条按“现象、原因、解决”说清楚省得看完原理后卡在玄学调参上。4.1 奖励一直为负且震荡不止探索噪声、奖励尺度与经验回放现象训练日志里的reward均值一直在-30到-10之间来回跳跑了2000个episode也没有收敛趋势。原因可能性很多最常见的是探索噪声过大导致策略始终在随机游走其次是奖励尺度太大Critic的Q值预测随reward尺度一起放大梯度更新失去稳定性还有一种情况是经验回放池中的老数据没有清理导致采样分布和新策略严重不匹配。解决先看每个时间步的reward范围。如果单步奖励在[-10, 10]这个量级问题不大如果在[-100, 100]以上建议在rl_utils.py里对reward做归一化把所有奖励除以一个常数把范围压到[-1, 1]附近。然后把noise_scale从0.1降到0.05观察1000个episode如果曲线稍微稳定一些说明探索噪声是主因。4.2 训练到一半出现nan loss梯度爆炸与Q值发散现象前几百个episode训练正常突然某个时刻loss变成nan之后所有网络权重变成nan训练彻底崩溃。原因这通常不是单一原因而是Q值过估计加上梯度爆炸。Critic对未来奖励的估计值越来越大导致目标Q值也在增长形成一个正反馈循环。网络层数少、没有梯度裁剪时会先出现loss震荡然后一步崩盘。解决在network.py的Critic输出层前加一个torch.clamp限制Q值范围或者在训练循环中对Critic loss做梯度裁剪。最有效的方法是把gamma从0.95降到0.9因为0.95在长周期任务里会让未来奖励的权重过大稍微降低之后Q值扩散的累积效应会立刻减弱。4.3 维度不匹配环境返回的obs和action没对上现象训练一运行就报size mismatch或者RuntimeError: mat1 and mat2 shapes cannot be multiplied通常是Actor或Critic的输入维度与环境的观测维度不一致。原因这类错误绝大多数是环境封装的问题。mag.py里返回的观测是二维(n_agents, obs_dim)但MADDPG.py里某个位置直接取了obs[0]而不是obs导致第一个Agent的网络输入维度只有obs_dim再或者simple_tag环境中每个Agent的观测维度本来就是拼接过的又被重复拼接了一次。解决不用猜直接在main.py训练循环里加断言让程序自己告诉你哪里不对# 在第一次环境交互前加上维度检查 obs env.reset() assert len(obs) n_agents, fobs数量 {len(obs)} 与 n_agents {n_agents} 不一致 assert obs[0].shape[0] obs_dim, fobs维度 {obs[0].shape[0]} 与配置 {obs_dim} 不一致 print(环境观测维度检查通过:, obs[0].shape)如果断言报错把mag.py里的reset()和step()返回的维度打印出来对比main.py里初始化的obs_dim值通常马上就能定位到是哪一层封装出了问题。4.4 策略学不会“博弈”对手太单一或episode太短现象训练结束后攻击者只会追着最近的躲避者跑躲避者只会往墙边躲换一个场景就跑不动了。原因这是典型的过拟合到单一对手策略。环境里有一个内置的随机对手策略如果这个策略的模式只有一两种MADDPG学到的就是针对这两种子模式的机械反应而不是真正的对抗策略。另外max_episode_len25对某些博弈任务太短双方还没形成有效对抗就强制结束了。解决给环境增加对手策略池每隔若干episode换一种策略让Agent去适应或者把max_episode_len调大到50给策略更多的时间去探索和反应。这两种做法都不需要改算法核心只是在环境交互层面增加多样性。4.5 缓冲池采样时报IndexErrorbuffer数据不足就开始学习现象训练早期报IndexError: list index out of range或者buffer sample size mismatch。原因main.py里训练逻辑是每个episode结束后立刻采样更新但刚开始的episode步数少缓冲池里的数据量还不到batch_size采样函数就取不了那么多数据。解决在训练循环里加一个条件判断# 训练循环内部buffer 未满时只填充不更新 if replay_buffer.size() batch_size: continue让前面的episode只做环境交互、填buffer达到批量大小后才开始更新网络。这是强化学习训练的常见保护如果作者没有在main.py里做好这个判断第一次跑必然踩坑。5. 改造成自己的博弈场景环境替换、奖励塑形与超参调优5.1 环境替换的接口约定reset、step与维度对齐如果想换成自己的博弈场景不要改MADDPG.py重点改mag.py和main.py里的环境参数。MADDPG需要环境提供两个接口class MyMAGEnv: def __init__(self, n_agents, obs_dim, act_dim): self.n_agents n_agents self.obs_dim obs_dim self.act_dim act_dim def reset(self): # 返回一个 list长度是 n_agents # 每个元素是 shape(obs_dim,) 的 np.ndarray obs [self._get_obs(i) for i in range(self.n_agents)] return obs def step(self, actions): # actions 是一个 list长度是 n_agents # 每个元素是 shape(act_dim,) 的 np.ndarray # 返回值必须是 obs_list, reward_list, done_list, info_list obs_list [self._get_obs(i) for i in range(self.n_agents)] reward_list [self._compute_reward(i) for i in range(self.n_agents)] done_list [False] * self.n_agents info_list [{} for _ in range(self.n_agents)] return obs_list, reward_list, done_list, info_list这里有一个容易踩的接口细节done的判断。多智能体场景里通常有两种做法一种是所有Agent共享同一个全局done另一种是每个Agent独立判断自己的done。MADDPG原始论文里用的是全局done即一人出界全部结束这样有利于Critic学习全局态势。如果需要在你的场景里每个Agent独立结束那就要修改MADDPG.py里目标Q值的计算逻辑处理的done要从单值变成向量否则某一维的错误信号会污染所有Agent的更新。5.2 奖励塑形把稀疏博弈信号变成可学习的梯度博弈环境里最常见的奖励设计是“赢了给10输了给-10”但MADDPG在这类稀疏奖励下很难收敛因为中间过程没有梯度信号。我自己在改追逐-躲避场景时一定会加一层塑形奖励。def compute_reward(agent_id, dist_to_goal, prev_dist_to_goal, is_capture): # 每个Agent的奖励由三部分构成 # 个体进度奖励鼓励朝目标移动 progress_reward (prev_dist_to_goal - dist_to_goal) * 2.0 # 博弈胜负奖励稀疏但强烈 if is_capture: return 10.0 progress_reward # 存活惩罚避免原地罚站 return -0.1 progress_reward进度奖励的系数2.0是经验值系数太大容易让Agent贪图近处收益而忽略整体策略太小又起不到引导作用。存活惩罚-0.1主要用来对抗“静止不动”这种退化策略。想要让Agent学会配合还要在进度奖励里加入团队成分比如我方任意Agent靠近目标就给全队加0.5这就是从个体奖励走向团队奖励的塑形思路。5.3 超参调优从默认值出发的调试顺序MADDPG的超参数量不算多但每个参数的影响范围互相关联。下面这张表是我调试时用的参考顺序参数默认值影响调整建议gamma0.95未来奖励权重训练发散时降到0.9任务周期长时升到0.99tau0.01目标网络跟随速度震荡严重时降到0.005lr_actor1e-4Actor步长策略更新太慢且reward平稳时尝试1e-3lr_critic1e-3Critic步长出现nan先降到1e-4batch_size256采样稳定性环境简单时128也能跑复杂场景256更稳noise_scale0.1探索强度探索不足时初始0.2用余弦衰减到0.01buffer_size100000样本多样性新策略和旧策略差异大时调到1e6调参顺序我一般按“先稳后优”先保证训练不崩溃再来谈效果。第一步把lr_critic降到1e-4或者加梯度裁剪确保loss不nan第二步看reward曲线是否单调上升如果不升就调整gamma和noise_scale第三步才考虑把batch_size和buffer_size调大提升数据利用率。很多人一上来就调学习率结果越调越乱反而耽误时间。6. 验证与可视化让曲线告诉你模型到底学没学会训练结束后最怕的事是模型文件保存了但不知道它到底学得怎么样。我一般不会只看训练日志里的reward曲线而是用一个测试脚本加载模型跑对局同时画一条测试阶段的奖励曲线对比训练阶段。import torch from MADDPG import MADDPG from mag import MAG # 加载训练好的模型按训练时的配置恢复网络 env MAG(n_agents3) maddpg MADDPG(env) maddpg.load_models(checkpoints/) # 跑20个测试对局看平均胜率而不是单局奖励 win_count 0 for episode in range(20): obs env.reset() done False while not done: actions maddpg.select_actions(obs, noise_scale0) obs, reward, done, _ env.step(actions) if reward[0] 0: win_count 1 print(f测试胜率: {win_count / 20 * 100:.1f}%)测试时noise_scale0很关键表示完全靠策略网络决策不添加探索噪声。只看单局奖励容易被随机性误导统计20局胜率比看单局数值可靠得多。训练曲线的可视化可以用最简单的matplotlib。如果训练日志里已经记录了每个episode的reward均值直接读入画图即可import matplotlib.pyplot as plt with open(training_log.txt) as f: rewards [float(line.strip()) for line in f if episode in line] plt.plot(rewards) plt.xlabel(episode) plt.ylabel(average reward) plt.title(MADDPG training curve) plt.savefig(training_curve.png)一旦发现测试胜率不错但训练曲线末尾还在明显波动说明训练过程不够稳需要回到上一章调低tau或noise_scale再训一轮。这个“训练→测试→看曲线→再调参”的闭环我每次都会强制走一遍从那以后基本没有出现过“模型没训好就误以为成功”的尴尬。希望帮到你。本文还有配套的精品资源点击获取