简介:基于Python与MADDPG的多智能体博弈对抗算法资源包,面向系统学习多智能体强化学习的学生和开发者,适用于毕业设计、课程设计、大作业、工程实训或初期项目立项,能够帮助解决单智能体算法无法处理多智能体交互与对抗场景的问题。压缩包内共14个文件,其中10个Python脚本构成算法核心,另包含配置文件、说明文档、gitignore与环境配置信息,整体压缩包约19KB,代码体量小、模块边界清晰,便于快速阅读与复用。核心模块涵盖DDPG与MADDPG算法主体、神经网络结构、经验回放缓存、强化学习工具函数、主流程入口与测试脚本,并附有README说明文档,方便读者直接运行观察对抗效果,也可按文件调试学习完整调用链。通过研读和改写,可掌握集中训练与分布式执行的核心思想、策略梯度更新流程以及奖励设计思路,适合作为从入门到进阶的算法参考。目前已有267人学习下载,值得需要的学习者实践参考。
1. 基于Python+MADDPG的多智能体博弈对抗算法:一个毕设代码包的完整拆解
做过多智能体博弈项目的人应该都体会过那种尴尬:论文里的MADDPG原理看懂了,流程图也画得出,可真要自己从零撸一套能跑的代码,光是多智能体经验回放和Critic拼接就能卡上两三天。这份基于Python+MADDPG的多智能体博弈对抗算法毕业设计代码包解决的就是这件事——它把网络定义、单智能体DDPG底座、MADDPG主算法、经验回放缓冲区、对抗测试环境全部拆成了独立模块,入口脚本一跑就能看到两个智能体在你来我往的对抗中逐步学到策略。适合拿来做毕设、课程设计或工程实训的起点,也适合想弄懂「中心化训练、去中心化执行」到底怎么落地的进阶学习者。下面我按自己拆类似项目的习惯,从原理、代码结构、复现步骤到常见坑一层层过一遍。
2. 从DDPG到MADDPG:为什么多智能体对抗不能直接套单智能体算法
2.1 单智能体DDPG的局限性:非平稳环境问题
先看最基础的底座。DDPG是深度确定性策略梯度算法,适合连续动作空间。它用Actor网络输出确定性动作,用Critic网络评估状态-动作对的价值,再靠经验回放打破样本相关性。单智能体场景下这套逻辑没问题,但搬到多智能体对抗里就会翻车。
原因在于环境非平稳。一个智能体在训练中不断更新策略,它的行为模式一直在变,对另一个智能体来说,这个变化直接改变了它观察到的转移概率和奖励分布。也就是说,每个智能体面对的环境里都藏着一个「会动的对手」。如果把对手当成静态环境的一部分,Critic估算出来的Q值就会失真,训练中期经常出现刚感觉策略在进步,下一批样本又全盘崩掉的玄学现象。
MADDPG解决这个问题的思路很直接:给每个智能体的Critic开天眼,让它能观测所有智能体的状态和动作。但Actor仍然只用自己的局部观测去决策。这就是所谓的「中心化训练、去中心化执行(CTDE)」。测试执行时,每个智能体看到的还是自己的局部信息,不依赖通信;训练时Critic则把全部信息汇总起来,价值估计一下子稳定很多。
2.2 MADDPG的训练逻辑:Critic拼接与逐个更新
刚接触MADDPG时最容易绕晕的是更新流程。它不是把多个DDPG并行跑那么简单,关键在于更新某个智能体时,Critic要拼接所有智能体的状态和动作作为输入。
常见的实现是,先给每个智能体各自建一套Actor和Critic网络,外加对应的Target网络。对局中把每个智能体的观测、动作、奖励、下一观测、是否结束,全部存进一个共享的经验回放缓冲区。训练时从缓冲区采一批样本,对每个智能体分别计算Critic损失和Actor策略梯度。
用伪代码说,某个智能体i的Critic更新目标大致长这样:
# 计算目标Q值,target_q_i是智能体i的target网络 next_actions = [target_actor_j(next_obs_j) for j in range(agent_num)] next_q = target_critic_i(next_obs_concat, next_actions_concat) target_q = reward_i + gamma * (1 - done) * next_q这段代码里,next_actions是把所有智能体target Actor对下一时刻观测的预测动作收集起来,然后拼接成target Critic的输入。注意这里的拼接顺序,在写代码时要保证后续所有智能体的拼接维度顺序一致,否则训练过程会出现梯度计算错误但又不报错的情况,这是后面排查的重点。
参数上,gamma衰减因子一般取0.95到0.99,取值越接近1说明智能体越看重长期收益。做对抗博弈和简单协作场景有很大区别,对抗中如果gamma太大会让智能体过度看重远期收益,前期探索不足;gamma太小又会让策略显得短视。用过几个项目后,我一般取0.95起步。
2.3 经验回放在多智能体场景下的设计差异
单智能体经验回放存的是「状态、动作、奖励、下一状态」四元组。多智能体版本要存的是每个智能体各自的四元组,但必须放在同一条样本记录里,因为训练时Critic需要所有智能体同一步的观测和动作去拼接。
这份代码包的buffer.py就是干这个事的。存储结构一般是这样的思路:
class ReplayBuffer: def __init__(self, capacity, agent_num, obs_dim, action_dim): self.buffer = deque(maxlen=capacity) # 每条样本包含: # obs: [agent_num, obs_dim] # action: [agent_num, action_dim] # reward: [agent_num, 1] # next_obs: [agent_num, obs_dim] # done: bool def sample(self, batch_size): batch = random.sample(self.buffer, batch_size) # 把每条样本拆开,按agent_num维度重组为批数据 return (np.array([b[0] for b in batch]), np.array([b[1] for b in batch]), ...)采样出来的每个字段第一维是batch_size,第二维是agent_num,后面才是观测维度。很多初学者在这容易踩坑:把单个智能体的向量直接拼接成整体,忽视agent_num那一维,导致后面network.py里全连接层输入维度对不上,报错还得回头查半天。
设计经验回放时还要注意缓冲区容量。对抗场景里,旧的策略行为样本对当前策略的参考价值衰减很快,缓冲区太大反而会稀释近期有效样本;但太小又会导致样本相关性高,训练震荡。我一般设50000到100000,具体看单局步数。
3. 代码结构拆解:这个压缩包里每个文件到底干什么
3.1 文件清单与模块职责划分
打开graduate_design-main.zip,文件不算多,但划分得很清晰。先把每个文件的角色捋一遍,再决定从哪个文件入手读代码。
| 文件 | 职责 | 阅读优先级 |
|---|---|---|
| main.py | 训练入口,负责创建环境、初始化智能体、跑训练循环 | 最高 |
| MADDPG.py | 多智能体算法主类,管理多个DDPG智能体 | 最高 |
| DDPG.py | 单个智能体的Actor-Critic实现,含目标网络与软更新 | 高 |
| network.py | 定义Actor和Critic的神经网络结构 | 高 |
| buffer.py | 多智能体经验回放缓冲区 | 中 |
| test_env.py | 对抗博弈测试环境,定义了状态、动作、奖励 | 中 |
| rl_utils.py | 工具函数,比如Ornstein-Uhlenbeck噪声等 | 中 |
| mag.py | 辅助模块,做一些维度处理和转换 | 低 |
| test.py / ceshi.py | 测试脚本,加载训练好的模型跑对局回放 | 低 |
| pyvenv.cfg / .gitignore | Python虚拟环境与Git配置 | 可忽略 |
| README.md | 项目说明 | 低 |
从阅读顺序建议是:先看test_env.py搞懂任务定义,再看network.py知道网络规模,接着过DDPG.py理解单智能体更新细节,最后读MADDPG.py看多智能体如何协同更新。README如果写得好可以先扫一遍,但很多毕设项目的README只是跑通记录,不必花太多时间。
3.2 network.py:Actor与Critic的定义方式
network.py里的结构通常是两个类。Actor网络输入是某个智能体的局部观测,输出是一个连续动作。Critic网络在MADDPG里的输入就不一样了,它接收所有智能体的观测拼接和所有智能体的动作拼接,输出一个Q值。
写神经网络时几个细节值得注意。首先激活函数选择,Actor最后一层输出的是动作,对抗场景里动作范围一般在[-1,1]或[-2,2],所以最后一层用tanh把输出限幅。Critic最后一层不接激活函数,直接输出线性Q值。中间隐藏层用ReLU居多,也有项目用tanh,效果差异不大。
import torch.nn as nn import torch class Actor(nn.Module): def __init__(self, obs_dim, action_dim, hidden_dim=64): super(Actor, self).__init__() self.fc1 = nn.Linear(obs_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, hidden_dim) self.fc3 = nn.Linear(hidden_dim, action_dim) def forward(self, obs): x = torch.relu(self.fc1(obs)) x = torch.relu(self.fc2(x)) return torch.tanh(self.fc3(x)) class Critic(nn.Module): def __init__(self, obs_dim, action_dim, hidden_dim=64): super(Critic, self).__init__() self.fc1 = nn.Linear(obs_dim + action_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, hidden_dim) self.fc3 = nn.Linear(hidden_dim, 1) def forward(self, obs_all, act_all): x = torch.cat([obs_all, act_all], dim=-1) x = torch.relu(self.fc1(x)) x = torch.relu(self.fc2(x)) return self.fc3(x)注意Critic的输入维度是所有智能体状态和动作拼接后的总和,不是单个智能体的维度。这也是很多人改代码时最容易失手的地方:环境里智能体数量一变,这里的输入维度必须跟着变,否则torch会直接报维度错误,但有时候torch的广播机制会静默地把错误维度「消化」掉,训练看不出毛病,实际学的策略完全不对。
hidden_dim用64还是128,取决于任务的复杂程度。简单的追逐-躲避对抗64就够了,遇到需要长时程推理的任务可以加到128或256。隐藏层加宽不一定总是好事,小任务上过大的网络反而容易过拟合到经验池里的旧样本。
3.3 DDPG.py与MADDPG.py:目标网络和软更新的工程实现
DDPG.py里定义的是单个智能体的Agent类,每个Agent内部维护四份网络:Actor、Critic以及各自对应的Target网络。这类实现里最关键的工程细节是target网络初始化时要和原网络保持一致,之后靠软更新逐步逼近。
软更新的公式是 θ_target = τ·θ + (1-τ)·θ_target,τ一般在0.01到0.001之间。τ太小,target网络更新太慢,价值估计收敛慢;τ太大,target网络跟得太紧,起不到稳定作用。做对抗任务我习惯设0.01,如果发现训练初期loss爆发式增长,优先把τ降到0.005试试。
MADDPG.py做的事情是管理一个Agent列表,暴露一个update方法,在训练循环里被main.py调用。它要做的事包括:从buffer采样、为每个智能体计算target动作、拼接所有智能体的状态和动作给Critic、逐个更新。工程上要注意的是所有智能体的更新不能串行地共用一个梯度流,每个智能体的Actor和Critic要独立更新,否则会出现梯度耦合,几个智能体学出来的策略趋同。
4. 复现全流程:从环境配置到训练观测
4.1 环境准备与依赖安装
这份代码依赖PyTorch和Numpy,没有复杂的外部库。建议新建一个干净的虚拟环境再装,不要直接放到全局Python环境里,避免和系统里其他项目的包版本打架。
python -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate pip install torch numpy如果机器有NVIDIA显卡,PyTorch会自动装CUDA版本。没有显卡纯CPU训练也能跑,只是慢一些,小规模的对抗环境CPU跑几百个episode是能接受的。装完可以先跑一个最小测试:
python -c "import torch; print(torch.__version__)"4.2 入口脚本main.py的训练循环
main.py是典型的多智能体训练循环结构。初始化环境、初始化MADDPG、建buffer、循环episode跑对局、采集样本、更新网络。理论上直接执行python main.py就能看到训练输出。
训练循环的核心框架大概长这样:
for episode in range(max_episodes): obs_n = env.reset() # obs_n是一个列表,每个元素是某个智能体的观测 episode_reward = np.zeros(agent_num) while not done: actions = [] for i in range(agent_num): obs = torch.FloatTensor(obs_n[i]).unsqueeze(0) action = agent.select_action(obs, noise=True) actions.append(action) next_obs_n, reward_n, done, _ = env.step(actions) buffer.push(obs_n, actions, reward_n, next_obs_n, done) obs_n = next_obs_n episode_reward += reward_n if buffer.size() > batch_size: maddpg.update(buffer, batch_size)重点解释一下其中几个逻辑。select_action里noise参数控制是否加探索噪声,训练前期要开,后期要慢慢关掉。reward_n是一个向量,每个智能体拿到自己的奖励,reward不一定是对称的,对抗场景常见的是你+1我就-1。buffer.push存的是这一整步里所有智能体的数据,结构符合第2章说的样本格式。
训练过程建议盯着两个指标:每个episode的累计奖励曲线,以及每个智能体的Critic loss。reward曲线整体向上说明策略在变好;Critic loss持续降不下去,大概率是学习率或网络规模有问题。在你确认奖励收敛之前不要中断训练,多智能体对抗的收敛往往不是一条平滑上升直线,中间会出现一段平台期甚至短暂下跌,这是探索噪声和对手策略变化共同造成的,属于正常现象。
4.3 对局观测与模型保存
训练结束后,test.py或ceshi.py负责加载训练好的模型跑对局。这类测试脚本的常见逻辑是:创建环境、加载每个智能体的Actor参数、关闭探索噪声、让智能体用纯确定性策略对局,并打印每个智能体每步的动作和奖励。
模型保存一般用PyTorch的torch.save,保存的是state_dict。加载的时候要先构造相同结构的网络再load,否则会报错。
model = Actor(obs_dim, action_dim) model.load_state_dict(torch.load("actor_0.pt")) model.eval()测试时把squeeze和unsqueeze处理好是一个高频翻车点。PyTorch的nn.Linear接收的输入是[batch, features]二维张量,单步预测时观测是一维数组,必须转成[batch, features]再进网络,输出后再降维。忘记这一步,模型有时能跑通但结果明显不对,因为维度被广播处理了,属于暗坑。
测试对局跑完,建议把每个智能体的累计奖励打印出来对比,好判断双方是形成了一种均衡,还是一方碾压另一方。如果出现碾压,就要回到环境设计里去调奖励函数,这就是第6章的内容了。
5. 常见问题与避坑:训练不稳定时的排查清单
5.1 Critic输入维度对不上但又不报错
现象:训练能跑,loss也在降,但智能体策略表现非常差,像是随机行为。
原因:PyTorch中nn.Linear对最后一维的匹配是严格校验的,但对batch维度有广播容忍。当多智能体状态拼接出错时,比如某个智能体只传入了自己的状态而不是全部智能体的状态,拼接后的维度恰好和某个合法维度一致时,矩阵乘法会静默地算出一个结果。这个结果数值是错的,但不会让程序停下来。
解决:在MADDPG的update方法里加断言,检查每个智能体Critic的输入维度是否等于agent_num * (obs_dim + action_dim)。写一个便于快速定位维度问题的辅助函数,把实际拼接的张量shape打印出来对照一下。这一步能省掉大量后期调试时间。
5.2 训练中期reward突然全部暴跌
现象:前几百个episode奖励在稳定上升,某次之后突然大幅下降,且保持低位很久不恢复。
原因:多智能体对抗环境里双方都在学习,某一方策略突变时,另一方的经验池里积累了大量基于旧对手行为的样本,价值估计瞬间失效。这在MADDPG里属于正常现象,但要注意区分是正常的策略跳变还是网络bug。
解决:几种手段配合使用。噪声从0.3逐步衰减到0.02,衰减周期拉长到训练总episode数的80%。降低单次更新步数,比如每个episode结束后只更新一批,减少旧样本被过度学习。如果暴跌后长时间不恢复,检查Critic learning rate,大于1e-3的调小到3e-4再重训。
5.3 软更新τ值过大的震荡问题
现象:奖励曲线高频震荡,像锯齿一样,看不到整体上升趋势。
原因:τ设得太大,target network追着原网络快速变,价值估计失去稳定锚点。在对抗任务里,对手策略也在变,主网络本身就不稳定,target再追得太紧,Q值目标就跟着剧烈摆动,训练自然稳不住。
解决:把τ从默认的0.01往0.001方向调。判断τ是否合适的办法是打印每个智能体的target Q值和在线Q值的差值,差值持续波动剧烈就是τ偏大了,平稳下降则说明设置合理。
5.4 探索噪声几乎不衰减导致收敛性差
现象:reward曲线在某个水平附近来回摆动,既不上升也不下降。
原因:Ornstein-Uhlenbeck噪声里的θ和σ参数在整个训练过程中是恒定不变的,或者衰减策略过于保守。探索噪声太大,即使策略学好也始终在做大幅偏离动作;噪声太小,动作过早确定,就找不到更好的对抗策略。
解决:按episode进度线性衰减噪声乘子。训练前期用0.5倍最大噪声,训练中期降到0.2倍,训练后期直接关闭噪声用纯确定性策略跑几轮验证。这里的核心教训是把噪声当成超参数来管理,不要放着不管。
5.5 CPU和GPU版本混用导致的设备不匹配
现象:在GPU上训练保存的模型,换到CPU机器上测试时报device mismatch。
原因:模型保存时参数在CUDA显存里,加载时当前设备只有CPU,自然对不上。
解决:保存时统一保存为CPU版本:
torch.save(model.state_dict(), "actor.pt", _use_new_zipfile_serialization=False)加载时代码里加一个map_location参数,指定加载到当前设备。这类问题虽然简单,但在毕设答辩前突然遇到还是很搞心态的,提前处理掉比较安心。
6. 进阶改造:换环境、调奖励与验证收敛
代码跑通只是开始,真正拉开差距在于两步改造。第一步是修改test_env.py里的对抗场景定义,目前代码包自带的应该是追逐-躲避或对称对抗这类经典设置,你可以把它换成更贴合自己课题的任务,比如双方争夺目标点,或者攻防两阶段博弈。改环境的关键是奖励函数设计,多智能体对抗的一个常见玄学是:奖励给太密,学习快但策略早熟;奖励给太稀,学习慢甚至不学习。我的习惯是先给稀疏基础奖励保证网络能收敛到「有策略」,再逐步增加奖励项引导出更复杂的攻防行为。
第二步是做一个直观的收敛验证手段。在main.py训练循环里每20个episode打印一次各智能体累计奖励均值,同时把每个智能体单独跟一个随机策略的对手对局,看是否稳定获胜。这个方法比单纯看训练曲线要可靠得多,因为它能排除双方同时变差但差值不变带来的假收敛。
多智能体对抗项目调试到后面,最常见的挫败感来源是「调一个参数好了,动一下别的地方又坏了」。把训练充分性、超参数、环境设计三者的变量尽量分开控制,不要同时改两处。从那以后我每次拿到这类MADDPG代码包,都强制自己先画一遍数据流图再动手改代码,这能省下大量「看似能跑实际上策略是乱来」的排查时间。希望这份拆解对你复现和改造这个项目有点实际帮助。
本文还有配套的精品资源,点击获取