简介:基于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对应一个Critic,Critic的输入只有自己的观测和动作;MADDPG.py里每个Agent的Critic输入做了拼接,把环境中所有Agent的观测和动作都并进去。这个改动看起来不大,但它是整个算法的灵魂。
2.2 从DDPG到MADDPG:网络结构与经验回放的差异
写代码之前先看一张表,这份工程里两类算法的对应关系:
| 组件 | DDPG.py | MADDPG.py |
|---|---|---|
| 网络定义 | network.py 中的 Actor / Critic | network.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_dim=128): 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.py | Actor / Critic网络定义 | MADDPG和DDPG共用 |
buffer.py | 经验回放池 | 存储多智能体联合转移元组 |
rl_utils.py | 工具函数 | 含soft_update、reward归一化等 |
mag.py | 多智能体博弈环境 | Multi-Agent Game,环境交互核心 |
main.py | 训练入口 | 配置参数、创建环境、调用MADDPG |
test.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 python=3.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_num=500,噪声固定为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, f"obs数量 {len(obs)} 与 n_agents {n_agents} 不一致" assert obs[0].shape[0] == obs_dim, f"obs维度 {obs[0].shape[0]} 与配置 {obs_dim} 不一致" print("环境观测维度检查通过:", obs[0].shape)如果断言报错,把mag.py里的reset()和step()返回的维度打印出来,对比main.py里初始化的obs_dim值,通常马上就能定位到是哪一层封装出了问题。
4.4 策略学不会“博弈”:对手太单一或episode太短
现象:训练结束后,攻击者只会追着最近的躲避者跑,躲避者只会往墙边躲,换一个场景就跑不动了。
原因:这是典型的过拟合到单一对手策略。环境里有一个内置的随机对手策略,如果这个策略的模式只有一两种,MADDPG学到的就是针对这两种子模式的机械反应,而不是真正的对抗策略。另外max_episode_len=25对某些博弈任务太短,双方还没形成有效对抗就强制结束了。
解决:给环境增加对手策略池,每隔若干episode换一种策略让Agent去适应;或者把max_episode_len调大到50,给策略更多的时间去探索和反应。这两种做法都不需要改算法核心,只是在环境交互层面增加多样性。
4.5 缓冲池采样时报IndexError:buffer数据不足就开始学习
现象:训练早期报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的超参数量不算多,但每个参数的影响范围互相关联。下面这张表是我调试时用的参考顺序:
| 参数 | 默认值 | 影响 | 调整建议 |
|---|---|---|---|
gamma | 0.95 | 未来奖励权重 | 训练发散时降到0.9,任务周期长时升到0.99 |
tau | 0.01 | 目标网络跟随速度 | 震荡严重时降到0.005 |
lr_actor | 1e-4 | Actor步长 | 策略更新太慢且reward平稳时,尝试1e-3 |
lr_critic | 1e-3 | Critic步长 | 出现nan先降到1e-4 |
batch_size | 256 | 采样稳定性 | 环境简单时128也能跑,复杂场景256更稳 |
noise_scale | 0.1 | 探索强度 | 探索不足时初始0.2,用余弦衰减到0.01 |
buffer_size | 100000 | 样本多样性 | 新策略和旧策略差异大时调到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_agents=3) 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_scale=0) obs, reward, done, _ = env.step(actions) if reward[0] > 0: win_count += 1 print(f"测试胜率: {win_count / 20 * 100:.1f}%")测试时noise_scale=0很关键,表示完全靠策略网络决策,不添加探索噪声。只看单局奖励容易被随机性误导,统计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再训一轮。这个“训练→测试→看曲线→再调参”的闭环我每次都会强制走一遍,从那以后基本没有出现过“模型没训好就误以为成功”的尴尬。希望帮到你。
本文还有配套的精品资源,点击获取