简介:面向计算机相关专业毕业设计与项目实战需求,这份资源提供基于MADDPG(多智能体深度确定性策略梯度)的多智能体博弈对抗算法Python实现,适合正在完成大作业、毕业设计或希望系统掌握多智能体强化学习代码实现的学习者。项目源码均通过本地编译并严格调试,可独立运行,代码内附详细注释,有助于理解DDPG/MADDPG的核心网络结构、经验回放机制、智能体交互与训练循环。资源共14个文件,包含10个Python脚本(如MADDPG.py、DDPG.py、buffer.py、network.py、rl_utils.py、测试脚本等),1个示例环境压缩包,以及配置文件与说明文档,整体仅1.6MB,目录结构清晰紧凑。内容涵盖了从环境构建、参数配置到训练验证的完整流程,可作为课程设计、毕业设计或算法对比实验的参考基线。目前已有72人学习浏览,该项目经导师指导并获评高分,具备较高的完整性与实用价值。
1. 多智能体博弈对抗与MADDPG:毕业设计把“戏台”搭对才是正经事
老读者知道我一贯劝人别一上来就捧着论文死磕公式。毕业设计要做强化学习方向,最怕的就是理论说得头头是道、演示时却跑不出像样的对抗效果。MADDPG(Multi-Agent Deep Deterministic Policy Gradient)恰好卡在这个需求点上:它能让你用Python写一套多智能体博弈对抗算法,在粒子环境里看到“攻防双方都在学习”的动态过程;既不会像端到端大模型那样吃算力,又有足够的展示空间给答辩评委看。适合想把算法原理和工程实现都摆在台面上的人。这篇就直接从最干净的环境搭建往对抗逻辑推,把代码路径、参数边界和翻车点都摊开讲。
2. 跑通最小环境:MPE安装、项目骨架和第一个可运行的训练脚本
2.1 粒子环境(MPE)的安装与确认
多智能体强化学习做博弈,最常用的试验场是 OpenAI 的 particle environment,也就是常说的 MPE。它的地图是二维连续空间,agent 用连续动作控制加速度,天然匹配 DDPG 这类确定性策略算法,做对抗博弈演示比 gridworld 直观得多。安装它的前提是 Python 3.8 或 3.9,搭配 torch 1.10 到 2.0 之间的版本——太新的 torch 在某些老机器上容易出算子兼容问题。
# 创建虚拟环境,避免把系统 Python 搞乱 python3.9 -m venv maddpg_env source maddpg_env/bin/activate # 安装 PyTorch CPU 版(学习和训练足够,不需要 CUDA) pip install torch==1.13.1 --index-url https://download.pytorch.org/whl/cpu # MPE 依赖 pip 安装,注意它内部用了 old-style setup.py pip install multi-agent-particle-envs逻辑说明:虚拟环境是为了隔离依赖,MADDPG 涉及的包较多,直接装在系统环境里容易和别的项目打架。PyTorch 用 CPU 版是为了降低上手成本,粒子环境本身计算量小,CPU 跑足够。MPE 包内部依赖一些旧式 setup.py 脚本,Python 3.10 及以上经常装不上,这就是为什么建议用 Python 3.9。
安装完成后,用一条命令确认环境里有没有真正挂上粒子场景:
# 快速验证 MPE 是否可用 from multiagent.environment import MultiAgentEnv import multiagent.scenarios as scenarios # 加载经典的 SimpleSpread 场景 scenario = scenarios.load("simple_spread.py").Scenario() world = scenario.make_world() env = MultiAgentEnv(world, scenario.reset_world, scenario.reward, scenario.observation) print(f"环境创建成功,agent 数量 = {env.n}")逻辑说明:MultiAgentEnv 是 MPE 的基类,它把 reset、reward、observation 三个回调函数接在一起,对外提供一个类似 Gym 的接口。scenario.reset_world(world)负责每回合重置位置,scenario.reward(agent, world)计算单个 agent 的奖励,scenario.observation(agent, world)拼接局部观测。madDPG 在训练时不直接访问这些函数,而是通过 env.step() 统一推进。
参数说明:这里的 agent 数量由场景决定,simple_spread 默认是 3 个 agent 加 3 个 landmark,做博弈演示时需要换成 simple_tag 或 simple_adversary 场景。换场景的方法是修改load("xxx.py")的参数,代码骨架完全不用动。
2.2 源码目录结构:读代码从入口文件开始
拿到一份 MADDPG 源码,先别急着打开算法文件。我一般会先用 tree 命令把目录结构打出来,找清楚训练入口在哪里,再顺着调用链往下摸。常见做法是分为 buffer 模块、网络模块、agent 模块和训练主脚本四层。
tree -L 2 maddpg_project/ # 预期结构如下 maddpg_project/ ├── README.md ├── maddpg/ │ ├── __init__.py │ ├── ddpg.py │ ├── maddpg.py │ ├── buffer.py │ └── networks.py ├── train.py ├── evaluate.py └── configs.py逻辑说明:buffer.py负责经验回放,也就是把每个 agent 的 (observation, action, reward, next_observation) 存到共享缓冲里;networks.py定义 actor 和 critic 的神经网络结构;ddpg.py是一个 agent 的“大脑”,包含 actor、critic 以及它们对应的 target 网络;maddpg.py是对外暴露的多智能体调度器,负责协调多个 agent 各自更新;train.py把环境和调度器串起来,跑出整个训练循环。很多初学的人直接改maddpg.py里的函数,却不知道真正控制训练节奏的是train.py里的 for 循环。
2.3 第一个训练脚本:跑通最小可执行的动作网络与目标网络初始化
与其一上来就训练完整博弈,不如先写一个最小脚本,确认 actor 网络能输出动作、critic 能打分、target 网络能跟着更新。这一步能过滤掉 80% 的维度报错和初始化疏漏。
import torch import torch.nn as nn import numpy as np # 单 agent 的 actor 网络 class Actor(nn.Module): def __init__(self, obs_dim, act_dim, hidden=64): super(Actor, self).__init__() self.net = nn.Sequential( nn.Linear(obs_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, act_dim), nn.Tanh() # 把动作压缩到 [-1, 1] ) def forward(self, obs): return self.net(obs) # 单 agent 的 critic 网络:输入拼接 所有agent的观测 和 所有agent的动作 class Critic(nn.Module): def __init__(self, obs_dim_all, act_dim_all, hidden=64): super(Critic, self).__init__() self.net = nn.Sequential( nn.Linear(obs_dim_all + act_dim_all, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, 1) # 输出 Q(s, a) ) def forward(self, obs_all, act_all): return self.net(torch.cat([obs_all, act_all], dim=-1)) # 初始化一个 agent obs_dim = 4 # 单个 agent 自己的观测维度 act_dim = 2 # 连续动作空间的维度,例如 x 方向和 y 方向的加速度 obs_dim_all = 4 * 3 # 3 个 agent 的观测拼起来 act_dim_all = 2 * 3 # 3 个 agent 的动作拼起来 actor = Actor(obs_dim, act_dim) actor_target = Actor(obs_dim, act_dim) critic = Critic(obs_dim_all, act_dim_all) critic_target = Critic(obs_dim_all, act_dim_all) # 把 target 网络的参数拷贝到和在线网络一样,这是 MADDPG 的常规起点 actor_target.load_state_dict(actor.state_dict()) critic_target.load_state_dict(critic.state_dict()) # 构造一批假数据,走一次前向,确认维度都通 obs = torch.randn(32, obs_dim) # batch=32 obs_all = torch.randn(32, obs_dim_all) act = actor(obs) act_all = torch.randn(32, act_dim_all) q = critic(obs_all, act_all) print("动作输出维度:", act.shape) # [32, 2] print("Q值输出维度:", q.shape) # [32, 1]逻辑说明:这段代码验证了 MADDPG 最核心的“集中训练,分布执行”结构。actor 只看自己的 obs 做决策,这是执行时每个 agent 的权限;critic 在训练时拼接所有 agent 的观测和动作,这是集中式评估的核心。target 网络加载在线网络参数,保证训练初期 Q 值估算不会太小或太大。Tanh()激活函数把动作限制在 [-1, 1],MPE 环境默认也是用这个范围。
参数说明:obs_dim 和 act_dim 必须从环境里读。你在真实项目中不要硬编码 4 和 2,而是通过env.observation_space[0].shape[0]和env.action_space[0].shape[0]动态获取。如果拼接维度时 obs_all 或 act_all 的维度算错,critic 的输入层对不上,前向传播直接报错,所以先跑这段最小脚本就是打预防针。
3. 剖析MADDPG核心组件:集中式Critic、Actor更新节奏与经验回放的参数细节
3.1 为什么是“集中训练、分布执行”:Critic拿全局状态的核心逻辑
多智能体博弈最麻烦的问题是环境不稳定:每个 agent 的策略都在变,对某个 agent 来说,其他人的策略改变会让自己的 Q 值估算失效。MADDPG 的解法非常直白——训练 critic 时开“上帝视角”,把所有 agent 的观测和动作都喂进去,这样 critic 就能感知到对手策略的变化,给出一个相对稳定的 Q 值。
# maddpg.py 中多智能体调度的核心伪代码结构 class MADDPG: def __init__(self, agent_ids, obs_dim, act_dim, hidden=64, tau=0.01): self.agents = {} for agent_id in agent_ids: self.agents[agent_id] = DDPGAgent(obs_dim, act_dim, hidden) self.tau = tau # target 网络软更新系数 def update(self, sample, agent_id): obs, act, rew, next_obs, done = sample agent = self.agents[agent_id] # 当前 Q 值 current_q = agent.critic(obs, act) # 下一步动作用 target actor 算 next_action = agent.actor_target(next_obs) # 下一步 Q 值用 target critic 算,注意拼接所有 agent 的 next obs 和 next action next_q = agent.critic_target(next_obs, next_action) # TD 目标 target_q = rew + self.gamma * next_q * (1 - done) # critic loss 与反向传播 critic_loss = nn.MSELoss()(current_q, target_q.detach()) agent.critic_optimizer.zero_grad() critic_loss.backward() agent.critic_optimizer.step()逻辑说明:这段代码是整个算法的“心脏”。current_q是当前状态下 critic 的打分,next_q是用 target 网络估算的下一步价值,TD目标是两者加权的结果。target_q.detach()很关键,它把目标值从计算图中分离出来,防止梯度流经 target 网络。集中式训练的意义就在这一步:虽然 actor 只用自己的 obs,但 critic 在训练时能看到所有 agent 的 next_obs 和 next_action,相当于把博弈对手的意图纳入了评估。
参数说明:gamma通常是 0.95 到 0.99 之间。粒子环境里的回合不长,gamma=0.95 够用;如果你改成对抗场景,回合拉长,可以调到 0.99。done是一个 float 数组,MPE 的结束信号通常是碰撞或逃生成功,乘以(1 - done)的目的是让终止状态不需要估计未来价值。
3.2 策略网络(Actor)与目标网络的更新节奏
Critic 学习了如何评估,actor 的学习则要顺着 critic 的梯度往“更高分”的方向调整参数。DDPG 类的算法用的是确定性策略梯度,更新 actor 时只传自己 agent 的动作路径。
# DDPGAgent 中的 actor 更新 def update_actor(self, obs_all, act_all, agent_id): # 重新算当前策略的动作 new_action = self.actor(obs_all[agent_id]) # 拼接所有 agent 的动作,但只替换当前 agent 的动作,其他 agent 的动作用旧数据 act_all_combined = act_all.clone() act_all_combined[:, agent_id * act_dim: (agent_id + 1) * act_dim] = new_action # critic 给这批“混合动作”打分,actor 的梯度方向是让这个 Q 值变大 policy_loss = -self.critic(obs_all, act_all_combined).mean() self.actor_optimizer.zero_grad() policy_loss.backward() self.actor_optimizer.step()逻辑说明:这段实现展示了 MADDPG 和单智能体 DDPG 的核心区别——actor 更新时要把自己的新动作替换进“全局动作向量”里,让 critic 对这个混合动作打分。如果只拿自己的旧动作去算 Q 值,梯度方向会偏差,训练容易不稳定。act_all_combined是改造的关键:先 clone 出旧动作,再替换当前 agent 那段。很多翻车现场就是忘记 clone,导致其他 agent 的动作也被改了。
target 网络更新用软更新方式,每个训练 step 都向在线网络靠近一小步:
def soft_update(self, target, source, tau): for target_param, param in zip(target.parameters(), source.parameters()): target_param.data.copy_(tau * param.data + (1.0 - tau) * target_param.data)参数说明:tau取值通常在 0.01 到 0.05 之间,tau 越大,target 网络跟得越紧。这里有个反直觉的坑:tau=0.01 看起来无害,但如果你每步都更新 target,前期 Q 值变化太慢,actor 很容易在错误的方向上走很久,出现“训练很久但 reward 纹丝不动”的怪象。实践里我会在训练前 5000 步用 tau=0.05 快速拉近,之后再降到 0.01。
3.3 经验回放采样:把博弈数据做成数据集的三个关键参数
经验回放是稳定训练的重要保障,多智能体场景里 buffer 的设计又比单智能体复杂一些,因为一次 step 会产生多个 agent 的数据,它们之间还有时间上的关联性。
# buffer.py 中经验回放的核心结构 class ReplayBuffer: def __init__(self, capacity=1000000): self.capacity = capacity self.buffer = [] self.position = 0 def push(self, obs_all, act_all, rew_all, next_obs_all, done_all): # obs_all 是 dict,key 是 agent_id if len(self.buffer) < self.capacity: self.buffer.append(None) self.buffer[self.position] = (obs_all, act_all, rew_all, next_obs_all, done_all) self.position = (self.position + 1) % self.capacity def sample(self, batch_size): batch = random.sample(self.buffer, batch_size) return (torch.cat([item[0][agent_id] for item in batch]) for agent_id in ...)逻辑说明:capacity控制历史经验的总量,position用环形数组覆盖旧数据。采样时不能只取单个 agent 的经验,因为 critic 训练需要“所有 agent 当前时刻的数据”;所以 sample 返回的是五个大的 tensor,每个 tensor 的第一维是 batch_size,第二维是所有 agent 数据的拼接或堆叠。
参数说明:三个关键参数分别影响训练速度和稳定性:
capacity(经验池容量):粒子环境默认 10^6 够用,但你的机器内存吃不消的话可以降到 5 * 10^5。容量太小会让训练过程“失忆”,早期经验还没被充分利用就被覆盖。batch_size:常用 256 或 512。太大梯度稳定但每个 step 的更新偏保守,太小训练波动大。我一般从 256 起步,不收敛再减半。采样间隔:每次环境 step 后不一定立刻更新,常见做法是每 100 步更新一次,batch_size 次数的梯度下降。一次 step 就立刻更新的做法容易让样本之间有强相关性,训练震荡。
4. 训练与调参:从收敛曲线到对抗强度,哪些超参数决定“赢不赢”
4.1 奖励设计:博弈对抗的关键是“给对手编号”
同样一套 MADDPG 代码,换奖励函数就可能从“两边一起摆烂”变成“攻守双方越打越精”。粒子环境里每个 agent 的 reward 是独立的,传统做法是把 reward 定义为稀疏的即时代价,比如碰撞就给 +1、逃逸给 +10。但博弈对抗不是单纯让每边 reward 变大,而是让双方的期望相反——比如追逐者吃到一个“距离惩罚”,被追者吃到“距离奖励”,两边目标天然冲突。
# simple_tag 场景中追逐者的奖励设计 def reward(self, agent, world): reward = 0 for other in world.agents: if other.landmark and other.adversary: # 判断是否碰撞 if self.is_collision(agent, other): reward += 10 # 抓到猎物加分 else: # 距离越小越好,用负距离作为惩罚 reward -= self.dist(agent, other) return reward逻辑说明:用“负距离”作为连续惩罚是让 agent 学到追赶方向的关键。稀疏的“碰撞加 10”的问题在于,初期的随机动作几乎不可能碰到猎物,reward 全是 0,agent 学不到任何梯度信号。负距离则让每一步都能感受到“我接近了一点”或“我离远了”,训练初期更容易引导策略走向正确方向。
参数说明:奖励幅度的绝对值不宜太大。很多毕设项目把捕食奖励设为 100,逃离奖励也设为 100,结果两边 reward 数值巨大,Q 值估算不稳定。常见做法是把单步奖励控制在 [-1, 1] 之间,碰撞事件奖励设为 5 到 10 倍。这样 TD 误差的数值范围健康,critic 更容易收敛。
4.2 关键超参数表:learning_rate、gamma、tau、噪声
MADDPG 超参数不像 CNN 那样有很多公开经验值,更多是试出来。下面这张表是在粒子环境里比较稳妥的起始配置,照着改能省掉很多试错时间:
| 超参数 | 推荐起始值 | 调整方向 | 影响范围 |
|---|---|---|---|
| actor_learning_rate | 1e-4 | 不收敛则降到 5e-5 | 策略更新的步长,太大会震荡 |
| critic_learning_rate | 1e-3 | 可先调大到 2e-3 | 评估网络的适应速度,过大会导致 Q 值爆炸 |
| gamma | 0.95 | 回合长则调 0.99 | 长期回报权重,值越大越看长远 |
| tau | 0.01 | 前期用 0.05 加速逼近 | target 网络更新速度 |
| noise_scale | 0.1 | 探索不足则加到 0.3 | 动作空间的探索范围 |
| hidden | 64 | 复杂场景加到 128 | 网络表达能力 |
表格之外还有两个容易忽略的点:一是 actor 的优化器用 Adam 就不用调 momentum,二是 critic 和 actor 的 learning_rate 不要设成一样大。Critic 需要快速适应环境变化,actor 要慢一点保持稳定,两者差 5 到 10 倍是常见比例。
4.3 训练回放:观察 loss、reward 的分布位置来判断何时“翻车”
训练循环本身不复杂,但每次训练后要从输出的 log 里判断有没有出问题。我把常用的训练循环和判断逻辑放在一起:
# train.py 中的训练主循环 for episode in range(episode_limit): obs_all = env.reset() episode_reward = 0 for step in range(max_steps): # 每个 agent 选动作,加上探索噪声 action_all = {} for agent_id in agent_ids: action = maddpg.agents[agent_id].actor(torch.Tensor(obs_all[agent_id])) noise = np.random.normal(0, 0.1, action.shape) action_all[agent_id] = np.clip(action.numpy() + noise, -1, 1) # 环境推进 next_obs_all, reward_all, done_all, _ = env.step(action_all) # 存入经验池 buffer.push(obs_all, action_all, reward_all, next_obs_all, done_all) # 每 100 步更新一次网络 if buffer.size() > batch_size and step % 100 == 0: for agent_id in agent_ids: sample = buffer.sample(batch_size) maddpg.update(sample, agent_id) obs_all = next_obs_all episode_reward += sum(reward_all.values()) # 每 100 个 episode 打印一次 if episode % 100 == 0: print(f"Episode {episode}, reward = {episode_reward:.2f}")逻辑说明:动作加噪声是 MADDPG 的探索机制,noise_scale=0.1表示在输出动作上叠一个均值为 0、标准差为 0.1 的高斯噪声。np.clip保证动作不超出环境边界 [-1, 1]。更新频率是每 100 步一次,这样收集到的样本有一定的多样性,不会因为连续几步高度相关而影响梯度。
观察 log 时,我会特别看三个位置:
- reward 的整体趋势:前 1000 局有波动很正常,但 3000 局之后如果还在剧烈震荡,说明 noise 太大或 learning_rate 太高。
- critic loss 的数值:如果 loss 在 0 附近,说明 critic 已经能比较准确地预测 Q 值,但此时 actor 不一定好;如果 loss 持续上升,说明 TD 目标本身不稳定,要检查经验池是否污染严重。
- 动作输出分布:如果某个 agent 的动作几乎永远卡在边界(全输出 1 或全输出 -1),可能是 reward 设计有问题,导致策略学成了“猛冲”或“摆烂”,需要在奖励里加一个小的动作代价约束。
5. 常见问题与排查:MADDPG训练不收敛的五个原因与对策
5.1 环境能跑但 reward 朝负无穷方向走——奖励函数里隐含的“零和陷阱”
现象:训练一开始还能看到一点轻微波动,之后 reward 一路往下掉,5000 局后几乎变成 -500 的“负数大坑”。
原因:奖励函数中双方目标完全相反,但“负距离惩罚”被加在了每一步,导致每个 step 都会产生一个负的即时代价,回合越长累积的惩罚越大。与此同时 actor 又没有快速学到有效躲避/追赶,于是整体奖励曲线呈永续下跌。
解决:给奖励加一个“时间衰减”或者“归一化”。常见做法是把每一步的负距离除以最大可能距离,把单步奖励控制在 [-1, 0] 区间;同时设置最大回合步数,达到步数就强制结束并给一个结束补偿。这样累计奖励的绝对值不会无限膨胀,actor 才能从有限的负奖励中找到改进方向。
5.2 维度对不上或维度正确但 loss 一直不下降——Critic输入拼接维度错位
现象:程序能跑,但 critic loss 在 0.5 左右抖动,怎么调 learning_rate 都压不下去。
原因:Critic 的输入需要按固定顺序拼接“所有 agent 的观测”和“所有 agent 的动作”,如果经验回放里存的动作顺序和 actor 更新时替换动作的顺序不一致,critic 接收到的“哪个动作属于哪个 agent”是错乱的,等于在用一个错位的训练信号更新网络。
解决:在 buffer.push 里就把动作按 agent_id 排好序再拼接,不要依赖 dict 的迭代顺序。具体做法是:
# 拼接所有 agent 的动作并保持固定顺序 act_all = torch.cat([torch.Tensor(action_all[k]) for k in sorted(action_all.keys())], dim=-1)这样不管 dict 内部顺序怎么变,拼接顺序始终一致,critic 输入的意义也就稳定了。
5.3 训练到一半 loss 逐渐升高——经验回放容量与 buffer 污染
现象:训练初期 reward 在上升,3000 局之后突然掉头向下,critic loss 同步升高。
原因:经验池容量太小或者采样不均匀。当 buffer 只有 10 万容量,训练到后期新经验不断覆盖早期“优质”数据,如果覆盖掉的刚好是训练初期宝贵的多样样本,模型就容易突然“失忆”。另外,如果 buffer 采样完全随机,而早期数据大多是没有意义的探索动作,它们占比太高会把 critic 带偏。
解决:在训练中分段调整采样策略。前 2000 局用随机采样保留多样性和探索性,之后改为优先采样 TD 误差较大的样本(类似 PER 思想)。最简单的改动是加大容量到 10^6,且在前 5000 局不更新网络,只收集数据。很多 MADDPG 开源实现默认训练开始就更新,这种做法在博弈场景下特别容易踩坑。
5.4 复现结果波动大——随机种子与 GPU 卡死
现象:同一份代码跑两次,reward 曲线完全不同,有时第二次甚至完全学不会。
原因:缺乏随机种子控制。MPE 环境在 reset 时随机生成坐标,actor 和 critic 的初始化权重也带随机性,再加上噪声采样,三次随机叠加,结果自然不可控。
解决:在所有可能入口统一设置随机种子:
import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) # 如果用了 CUDA torch.cuda.manual_seed_all(seed)注意:光设置 seed 还不够,MPE 内部可能使用全局 numpy 随机状态,所以还需要在 reset 后额外调用env.seed(seed)。我踩过最深的坑是只 torch 和 numpy 各设了一遍,但 MPE 场景里用的是 Python 内置 random,导致坐标初始化每次还是不同。
5.5 训完的 agent 傻站着不动——target 更新频率和 tau 设太小的过拟合
现象:reward 曲线很好看,但演示时视觉上 agent 就是原地微动。测试时把 noise 关掉,动作几乎全为 0。
原因:target 网络更新太慢,导致 critic 对动作的偏好非常驼峰化。动作空间是连续二维的,初期网络把所有“改变位置”的输出都评估为低值,只有“原地不动”评估为正,加上 tau=0.01 的缓慢更新,actor 就被锁死在原点附近。
解决:把 tau 调到 0.05,并且每 100 步做一次平滑更新,而不是每步都更新。“每步都更新”听起来更勤奋,实际会让 target 追踪同一个早期错误目标,形成路径依赖。另外要在 actor 的 loss 里加一个小的动作正则项,比如0.01 * (action ** 2).mean(),让策略不要蜷缩在边界上,同时也避免输出过于极端的动作。
6. 进阶玩法与验证:多策略交替、场景迁移与稳定性检查
6.1 多策略交替训练:用老 snapshot 做“陪练”
单次训练的 MADDPG 很容易陷入“双方策略共同漂移”的状态——两个 agent 一起变强或者一起退化,你不知道到底是谁在进步。更实用的做法是每 500 局存一个 checkpoint,然后用上一轮的旧策略去“陪练”当前策略。
# 保存一轮快照 torch.save(maddpg.agents['agent_0'].actor.state_dict(), f"actor_agent0_ep{episode}.pth") # 陪练时用旧快照 old_actor.load_state_dict(torch.load(f"actor_agent0_ep{episode-500}.pth"))逻辑说明:这种交替训练能让你看到不同世代的策略强度差异。如果当前策略能稳定压制 500 局前的旧策略,说明对抗学习真的在发生;如果两边数据接近基尼系数式的互相拉扯,说明奖励设计有问题。毕设答辩时拿出这样的对比曲线,比单一 reward 曲线有说服力得多。
6.2 用 TensorBoard 可视化与 checkpoint 恢复:给自己的毕业设计留“后悔药”
训练 MADDPG 时,reward 曲线平滑不能说明全部问题。把 critic loss、actor loss、每个 agent 的 action 分布都记录到 TensorBoard 里,方便随时回看是哪一步开始崩的。
from torch.utils.tensorboard import SummaryWriter writer = SummaryWriter("./logs") # 每个 episode 记录一次 writer.add_scalar("reward/agent0", episode_reward_agent0, episode) writer.add_scalar("loss/critic", critic_loss.item(), episode) writer.add_scalar("action/noise_scale", current_noise_scale, episode)checkpoint 的恢复是一个极其实用的技巧。训练到一半发现参数配坏了,用恢复脚本回到上一个稳定点,不至于从头再来:
def load_checkpoint(maddpg, path): state = torch.load(path) for agent_id in maddpg.agents: maddpg.agents[agent_id].actor.load_state_dict(state[f"actor_{agent_id}"]) maddpg.agents[agent_id].critic.load_state_dict(state[f"critic_{agent_id}"])逻辑说明:恢复时只加载在线网络,target 网络可以重新从在线网络复制,这样避免 target 网络和在线网络续接不上。如果连 target 也恢复,需要 E 大模型那样精确的状态字典,反而容易出错。
6.3 在 SimpleTag 与 Spread 场景验证:博弈对抗到底是不是“对抗”
MADDPG 的一个验证维度是同一套代码跑不同博弈场景,用来区分“合作”和“对抗”。SimpleTag 是捕食者-猎物对抗场景,SimpleSpread 是三个 agent 合作覆盖目标点的场景。把两份实验的 reward 曲线画在一起,可以很清晰地展示算法在两种目标模式下的行为差异。
# 切换场景的简单封装 def make_env(scenario_name): scenario = scenarios.load(f"{scenario_name}.py").Scenario() world = scenario.make_world() env = MultiAgentEnv(world, scenario.reset_world, scenario.reward, scenario.observation) return env如果你做的毕设题目强调“博弈”,那最好把两个场景都跑一遍。合作场景验证算法的协调能力,对抗场景验证博弈能力,分开陈述比混在一起讲更清晰。我的习惯是最后用一组对比表列出:合作场景中回合结束时所有 agent 的覆盖面积、对抗场景中捕食者胜率、经验池容量、单局耗时,用来和论文里的理论分析互相印证。
这套东西做到这里,MADDPG 的源码已经不是一个黑匣子了。你手里有可以跑的训练脚本、有能解释的组件拆分、有遇到问题能反推的排查依据。希望这个方向能成为你毕设的一个稳定支点。
本文还有配套的精品资源,点击获取