简介:面向强化学习与自动驾驶/机器人控制方向的开发者,这份MATLAB/Simulink示例包完整展示了多智能体路径跟踪控制的训练流程,可帮助读者理解如何让多个智能体沿着预设路径协同运动,并解决多智能体协同训练中的目标跟踪问题。压缩包共7个文件,体积仅595KB,其中4个m脚本分别承担智能体创建(如ACC、LKA)、参数配置与状态重置等功能,1个mat文件保存训练好的策略模型,1个mlx实时脚本用于交互式运行训练过程,1个slx仿真模型用于可视化系统级协同效果。已有1128人学习,验证了其实用性。借助这个示例,读者不仅能掌握ACC(Actor-Critic Controller)与LKA(Lane Keeping Assist)两种控制器的构建思路,还能学习奖励函数设计、探索策略调整等关键环节,同时获得训练好的模型与Simulink仿真环境,便于直接复现和二次开发,为后续开展多智能体协同控制研究提供可复用的代码框架。
1. TrainMultipleAgentsForPathFollowingControlExample:多个智能体如何学会沿同一条路走而不打架
多智能体路径跟随是无人机编队、仓储机器人调度里绕不开的底层任务,难点不在“让一个智能体走直线”,而在多个智能体同时在线学习、互相都在移动的场景下,策略还能保持稳定。这个资源包提供的就是一套完整的强化学习路径跟随控制示例:自定义 2D 仿真环境、路径生成器、基于 MADDPG 的多智能体算法实现、训练脚本、评估脚本和可视化工具。目标很直接——让 N 个智能体既能贴着参考路径走,也能保持彼此之间的安全距离。适合已经跑过单智能体强化学习、想切入多智能体控制方向的开发者,也适合拿它当实验基线的算法工程师。
2. 项目架构与技术选型:环境、算法、训练如何分层解耦
拿到压缩包的第一件事,是把目录铺开看各部分怎么拆。这个项目的模块拆得规矩,环境、算法、工具、训练入口各管各的一块,后面换算法或加奖励项不会伤筋动骨。
2.1 目录结构与模块化设计
TrainMultipleAgentsForPathFollowingControlExample/ ├── envs/ │ ├── __init__.py │ ├── path_env.py # 多智能体路径跟随仿真环境 │ └── config.py # 环境侧参数(智能体数量、安全距离等) ├── agents/ │ ├── memory.py # 经验回放缓冲区 │ ├── networks.py # Actor / Critic 网络定义 │ └── maddpg.py # MADDPG 算法核心逻辑 ├── utils/ │ ├── path_generator.py # 参考路径生成工具 │ └── visualization.py # 轨迹绘制与指标曲线 ├── train.py # 训练入口 ├── evaluate.py # 评估与模型导出入口 ├── config.yaml # 训练超参数统一配置 └── requirements.txt # 依赖清单这种拆法的好处是职责边界很清晰。我拿到强化学习项目会先看 agents 目录,如果网络结构和训练循环写在一个文件里,后面加奖励项或换算法会非常痛苦。这套代码把环境的改动和策略更新拆开了:你在envs/path_env.py里加障碍物,不需要去动agents/maddpg.py的策略梯度逻辑。
训练流程上,train.py读config.yaml,创建环境实例和 MADDPG 智能体,循环收集经验、更新网络,阶段性调用utils/visualization.py画轨迹和指标曲线,并保存 checkpoint。evaluate.py负责加载权重跑固定回合数,只评估不更新梯度,顺便导出轨迹图。
2.2 环境建模:路径生成、智能体动力学与观测空间设计
路径跟随任务和普通导航不同,智能体不知道终点在哪,只需要沿一条参数化的参考路径走。因此环境里最重要的一块是路径生成。
# utils/path_generator.py import numpy as np def generate_path(path_type: str, num_points: int = 200): """生成参考路径点序列, 返回 shape=(num_points, 2) 的路径点数组""" if path_type == "circle": t = np.linspace(0, 2 * np.pi, num_points) x = 5.0 * np.cos(t) y = 5.0 * np.sin(t) elif path_type == "lemniscate": t = np.linspace(0, 2 * np.pi, num_points) x = 3.5 * np.cos(t) y = 2.0 * np.sin(2 * t) elif path_type == "random": # 随机控制点生成三次样条, 增加路径多样性 ctrl = np.random.uniform(-6, 6, size=(6, 2)) path = sample_catmull_rom(ctrl, num_points) # 平滑插值, 保证可导 x, y = path[:, 0], path[:, 1] else: raise ValueError(f"未知路径类型: {path_type}") return np.stack([x, y], axis=-1)路径点序列会作为参考轨迹存在环境里,每个智能体从路径上选择自己的目标点。路径生成不只是为了好看,更重要的是给训练过程提供多样性:只训练圆形路径,策略极容易过拟合,混入随机路径才能学到泛化的路径跟随能力。
智能体在环境里用的是简化的动力学模型,每个智能体有位置(x, y)和速度(vx, vy),动作是加速度指令(ax, ay)。这是路径跟随仿真里最常用的一层抽象,既保留了连续控制难度,又不会让物理仿真细节抢走算法本身的注意力。
# envs/path_env.py 中单个智能体的位置更新 def step(self, actions): for i, agent in enumerate(self.agents): ax, ay = actions[i] # 加速度指令, 网络输出范围 [-1, 1] agent.vx += ax * self.dt agent.vy += ay * self.dt agent.x += agent.vx * self.dt agent.y += agent.vy * self.dt这里的dt是仿真步长,我一般取 0.1 秒,动作范围映射到实际加速度幅值[-2, 2] m/s^2。dt太大会让智能体一步跨出半米,跟踪精度无从谈起;太小则单条轨迹的仿真步数变多,训练速度被拖慢。
观测空间决定策略能感知多少信息。这套代码的做法是把局部状态和编队信息叠在一起,以 3 个智能体为例,单个智能体的观测向量大致是:
[自身位置 x, y, 速度 vx, vy, 与目标路径点的相对距离和方位角, 与最近邻居的相对位置差值]把“与邻居的相对位置”放进观测是一个很关键的设计,否则多智能体编队没有信息基础,训练出来的策略只能是各自飞各自的,谈不上协作跟线。
2.3 算法选型:为什么多智能体路径跟随不适用独立 DDPG / PPO
如果直接用单智能体 DDPG 或 PPO 给每个智能体各训练一套,大多数情况会翻车。原因是多智能体场景里每个智能体都在并行改变策略,站在某一个智能体的视角看,其他智能体相当于环境的动态部分,环境是非平稳的;Q 值回归的标签不停变化,策略还没收敛就已经震荡。
MADDPG(Multi-Agent Deep Deterministic Policy Gradient)专门解决这个问题,核心思路叫“集中训练,分布执行”。训练阶段,每个智能体的 Critic 网络能看到所有智能体的观测和动作;执行阶段,Actor 只依赖本地观测输出动作。这样既稳定了 Q 值估计,又保留了执行时的独立性。
| 算法 | 训练时 Critic 输入 | 执行时 Actor 输入 | 适用规模 | 协作能力 |
|---|---|---|---|---|
| 独立 DDPG | 仅本智能体观测和动作 | 仅本智能体观测 | 小 | 弱 |
| MADDPG | 全部智能体的观测和动作 | 仅本智能体观测 | 中 | 强 |
| PPO | 本智能体观测 | 本智能体观测 | 中 | 一般 |
所以这个项目选 MADDPG 是顺理成章的:连续动作空间、多智能体协作路径跟随、希望训练稳定收敛,正好落在 MADDPG 的适用区间。代码里也保留了更换算法的接口,后面会提到如何替换。
3. 核心模块拆解:从环境奖励到策略更新的完整链路
二次开发时最需要精读的是核心逻辑。这一章按“奖励怎么算、网络怎么建、参数怎么更新”三块拆开讲,每段代码都对应包里的实际实现。
3.1 奖励函数:跟踪误差、碰撞惩罚与进度激励的组合
路径跟随最简单的奖励写法是“离目标点越近奖励越高”,但只写这一项会在多智能体场景翻车:智能体们会互相挤在一起,因为那同样是降低跟踪误差的一种方式。这套环境里的奖励由三项组成。
# envs/path_env.py 中奖励计算核心逻辑 def _compute_reward(self): rewards = np.zeros(self.num_agents) # 1. 跟踪误差项: 距离目标点越远, 扣分越多 for i, agent in enumerate(self.agents): dist_err = np.linalg.norm(agent.pos - agent.target_pos) rewards[i] -= self.w_track * dist_err # 2. 路径进度项: 沿路径前进越多, 给正奖励 for i, agent in enumerate(self.agents): progress = self._get_path_progress(agent) rewards[i] += self.w_progress * progress # 3. 碰撞惩罚项: 两两检查, 小于安全距离开始扣分 for i in range(self.num_agents): for j in range(i + 1, self.num_agents): d = np.linalg.norm(self.agents[i].pos - self.agents[j].pos) if d < self.safe_dist: penalty = self.w_collision * (self.safe_dist - d) rewards[i] -= penalty rewards[j] -= penalty return rewards三个系数的作用要分开理解:
w_track控制“贴路径”在奖励体系里的重要性,默认取 1.0。值越大智能体越贴近参考路径,但过大容易忽略避碰。w_progress取 0.2 左右,作用是解决路径长、奖励稀疏时的中途停滞问题,相当于每步给一定方向性引导。w_collision取 3.0,让避碰成为优先级较高的硬约束。惩罚系数确实能让模型快速学会躲开,但过大会让奖励信号被碰撞项淹没,智能体干脆站在原地不动,因为不动就不会撞。遇到这种情况把w_collision调回 2~3,并保证进度项持续存在。
# envs/path_env.py 中目标点选择逻辑 def _get_target_point(self, agent, lookahead=3): # 找到路径上离智能体最近的点索引 idx = np.argmin(self._distance_to_path(agent.pos)) # 朝路径前方第 lookahead 个点前进, 避免追着局部点抖动 next_idx = min(idx + lookahead, len(self.path) - 1) return self.path[next_idx]lookahead参数很值得关注。如果目标点取离自身最近的那个路径点,智能体会朝一个时变目标猛追,产生高频抖动;取前方 2~3 个点时控制更平滑,这也是线性跟踪控制里“前视距离”概念的强化学习版本。
3.2 Actor-Critic 网络结构与经验回放
MADDPG 的网络分两个角色:Actor 负责输出动作,Critic 负责评估当前策略的价值。这个项目里 Actor 是三层 MLP,输入本地观测,输出二维动作;Critic 输入所有智能体观测和动作的拼接。
# agents/networks.py import torch import torch.nn as nn class Actor(nn.Module): """策略网络: 输入局部观测, 输出连续动作""" def __init__(self, obs_dim, action_dim, hidden=256): super().__init__() self.net = nn.Sequential( nn.Linear(obs_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, action_dim), nn.Tanh() # 动作压缩到 [-1, 1], 交给环境映射到实际加速度 ) def forward(self, obs): return self.net(obs) class Critic(nn.Module): """价值网络: 输入全局观测 + 全局动作, 输出 Q(s, a)""" def __init__(self, global_obs_dim, global_action_dim, hidden=256): super().__init__() self.net = nn.Sequential( nn.Linear(global_obs_dim + global_action_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, 1) ) def forward(self, obs_all, action_all): return self.net(torch.cat([obs_all, action_all], dim=-1))有一个设计细节必须注意:Critic 输入中的action_all必须拼接其他智能体的动作,不能只有自己的动作。MADDPG 的集中式 Critic 条件依赖联合动作,少任何一个人的动作,梯度方向就不准确。实现上global_obs_dim = num_agents * obs_dim,global_action_dim = num_agents * action_dim。
经验回放是标准的环形缓冲区,保存每个时间步的(obs, action, reward, next_obs, done)五元组。MADDPG 属于 off-policy 算法,可以在大缓冲区里随机采样,切断样本间的时间相关性。
# agents/memory.py 核心逻辑 class ReplayBuffer: def __init__(self, capacity=100000): self.capacity = capacity self.data = [] self.pos = 0 def push(self, transition): # 每个 transition 里包含所有智能体的经验 if len(self.data) < self.capacity: self.data.append(transition) else: self.data[self.pos] = transition self.pos = (self.pos + 1) % self.capacity def sample(self, batch_size): batch = random.sample(self.data, batch_size) obs = torch.stack([item.obs for item in batch]) # (batch, n_agents, obs_dim) action = torch.stack([item.action for item in batch]) reward = torch.stack([item.reward for item in batch]) next_obs = torch.stack([item.next_obs for item in batch]) done = torch.stack([item.done for item in batch]) return obs, action, reward, next_obs, done经验回放容量默认 10 万条。缓冲区太小会让采样批次里全是最近几十步的经验,相关性高、收敛慢;太大又会拖慢对新策略的适应。路径跟随任务 10 万到 50 万条之间一般够用,具体取决于max_steps和 episode 数。
3.3 MADDPG 更新逻辑:联合目标 Q 与软更新
标准 MADDPG 中每个智能体都有自己的 Actor 和 Critic,只是 Critic 的输入是所有智能体信息的拼接。更新时各智能体独立计算损失,分别做梯度下降。下面这段是其中一个智能体的更新逻辑,实际代码会循环处理所有智能体。
# agents/maddpg.py def update(self, replay_buffer, batch_size): obs, action, reward, next_obs, done = replay_buffer.sample(batch_size) # 1. 用目标 Actor 计算下一时刻动作 all_next_actions = [] for i in range(self.num_agents): with torch.no_grad(): next_act = self.target_actors[i](next_obs[:, i]) all_next_actions.append(next_act) next_actions = torch.cat(all_next_actions, dim=-1) # 2. 计算 TD target: y = r + gamma * (1 - done) * target_Q with torch.no_grad(): target_q = self.target_critics[0]( torch.cat([next_obs[:, i] for i in range(self.num_agents)], dim=-1), next_actions ) y = reward[:, 0:1] + self.gamma * (1 - done) * target_q # 3. 更新 Critic: 最小化 TD 误差 q = self.critics[0]( torch.cat([obs[:, i] for i in range(self.num_agents)], dim=-1), action.reshape(batch_size, -1) ) critic_loss = nn.MSELoss()(q, y) self.critic_optimizers[0].zero_grad() critic_loss.backward() torch.nn.utils.clip_grad_norm_(self.critics[0].parameters(), 0.5) self.critic_optimizers[0].step() # 4. 更新 Actor: 策略梯度最大化 Q 值 new_actions = torch.cat( [self.actors[i](obs[:, i]) for i in range(self.num_agents)], dim=-1 ) actor_loss = -self.critics[0]( torch.cat([obs[:, i] for i in range(self.num_agents)], dim=-1), new_actions ).mean() self.actor_optimizers[0].zero_grad() actor_loss.backward() torch.nn.utils.clip_grad_norm_(self.actors[0].parameters(), 0.5) self.actor_optimizers[0].step() # 5. 软更新目标网络 for target_param, param in zip( self.target_actors[0].parameters(), self.actors[0].parameters() ): target_param.data.mul_(self.tau).add_(param.data * (1 - self.tau)) # Critic 目标网络同上几个值得重点理解的细节:
- 下一步动作为什么用
target_actor而不是actor?因为目标网络提供的是相对稳定的下一状态动作估计,如果每一步都用实时 Actor 算,参数跟着变,TD target 也会抖。 - 梯度裁剪
max_norm=0.5建议保留。多智能体项目动辄遇到 loss 激增,裁剪解决不了根源问题,但至少不会让一个离群样本把网络一步打崩。 tau=0.01控制目标网络更新速度,相当于每次只往当前网络方向挪 1%,避免目标网络剧烈波动。调大收敛快但稳定性下降,调小更稳但慢。
经验超参参考:actor 和 critic 学习率 1e-4,batch_size 256,gamma 0.95,tau 0.01。
4. 训练运行与避坑记录:从零跑通多智能体路径跟随
这个包的运行门槛不高,但“跑起来”和“收敛”之间隔着一堆容易被忽略的细节。这一章讲怎么装环境、怎么启动训练、怎么看结果,以及实际踩过的几个坑。
4.1 依赖安装与训练启动
先看requirements.txt,依赖很精简:
numpy pyyaml # 深度学习框架使用 PyTorch, 按自己 CUDA 版本安装对应轮子 torch tensorboard安装完依赖后,训练入口是train.py,默认从config.yaml读取超参数:
python train.py --config config.yamlconfig.yaml里值得关注的配置项:
env: num_agents: 3 dt: 0.1 max_steps: 200 safe_dist: 0.5 algorithm: name: "maddpg" lr_actor: 0.0001 lr_critic: 0.0001 batch_size: 256 gamma: 0.95 tau: 0.01 train: episodes: 3000 save_interval: 200 render: false各参数的作用和调整经验:
| 参数 | 值 | 作用 | 经验调整策略 |
|---|---|---|---|
| num_agents | 3 | 同时训练的智能体数量 | 增大时 Critic 输入维度变长,需加大 buffer |
| dt | 0.1s | 仿真步长 | 太大跟踪误差难收敛,太小训练变慢 |
| max_steps | 200 | 每个 episode 最大步数 | 过小导致走不完全程,策略不完整 |
| safe_dist | 0.5m | 智能体间距下限 | 按编队间距调,不可过小 |
| gamma | 0.95 | 折扣因子 | 路径长时可适当提高到 0.99 |
训练开始后会出现一种正常的“错觉”:第一波 reward 持续在负值区间,这是对的,别慌。大约 500 个 episode 后 reward 曲线单调抬升,说明策略开始摸到路线;如果到 1500 个 episode 还是一条水平线,基本可以确定是奖励设计或超参的问题。
训练期间开 TensorBoard 看曲线:
tensorboard --logdir logs/4.2 训练指标解读与结果验证
项目日志主要记录三个指标:平均奖励、平均跟踪误差、碰撞发生次数。我一般用跟踪误差确认路径跟随质量:单个智能体平均误差能收敛到 0.2 米以内算合格;多智能体因为要兼顾间距约束,误差会略高,0.3 米以内可以接受。
碰撞次数的曲线形状很有特点,训练初期很高,因为智能体在盲目探索;中期策略虽然学到了“躲人”但速度控制还不精准,碰撞率不会立刻下降;到了后期曲线应该快速收敛,最终每 100 个 episode 碰撞次数接近零。
跑完训练后,用评估脚本做固定回合评估:
python evaluate.py --checkpoint checkpoints/agent_3000.pt --render --save_plot验证时重点看生成的轨迹图:三条轨迹是否均匀分布在参考路径两侧,而不是完全贴合或绕大圈。如果轨迹完全贴合但间距很小,说明安全约束还没学好;如果轨迹间距很好但平均误差很高,说明跟线能力不足,要回头调w_track。
4.3 常见问题排查与避坑记录
以下是我在这个项目上实际踩过、也在其他强化学习包里反复遇见的五类问题,每条按“现象 → 原因 → 解决”列出。
训练开始没多久 loss 变 NaN,曲线直接消失。原因是学习率过高或奖励量级太大,尤其是把碰撞惩罚系数调到 10 以上后梯度瞬间爆炸。解决方式是把 actor 和 critic 的学习率压到 1e-4,奖励保持在同一数量级,并保留clip_grad_norm梯度裁剪。
平均奖励在涨,但三个智能体全挤在一起走。现象是不发生碰撞,因为它们已经贴得非常近。原因往往是奖励函数里只有跟踪误差,没有加智能体间距惩罚,模型发现“挤在一起”和“沿途排队”的跟踪误差一样,自然会选稳定省事的方案。解决方式是在_compute_reward中加入碰撞惩罚项,并把safe_dist调到 0.5 米左右。
圆形路径收敛得很好,换一条八字路径就废了。原因是训练阶段path_type固定为 circle,策略严重过拟合。后来我在环境每次重置时按 30% 概率随机切换路径类型,训练出的模型在圆、八字、随机路径上都能保持误差在 0.4 米以内。
跟踪误差卡在 0.2 米附近降不下去,曲线像被锁住了。这通常不是没学好,而是目标点选取逻辑的问题:目标点取离自身最近的那个路径点,智能体会朝时变目标猛追,产生高频抖动。解决方式是启用lookahead机制,目标点取路径前方 2~3 个点,控制会更平滑。
GPU 占用很高,但每秒训练步数上不去。多智能体环境的 step 是纯 Python 循环,数据在主内存和显存之间反复拷贝,GPU 再强也被环境拖死。解决方法是先确认step()里没有大循环和临时 List 分配,或者用向量化环境一次算一整批环境,再统一喂给网络。
5. 二次开发:换路径、加避障、换算法的改造思路
拿到这套代码后,通常不会只跑原样就收工。这里讲三个实用的改造方向,都建立在已有模块之上,改动量可控。
5.1 自定义路径:在 path_generator 里加自己的轨迹
如果想让智能体走一条特定参考轨迹,比如回形走廊,只需要在path_generator.py增加分支,直线段拼接并对拐点做圆弧过渡:
elif path_type == "square_loop": segs = [np.array([0, 0]), np.array([6, 0]), np.array([6, 4]), np.array([0, 4]), np.array([0, 0])] path = smooth_waypoints(segs, radius=1.0, num_points=num_points)改完把config.yaml里的path_type指到新值,同时确认目标点索引逻辑仍然有效,即可直接训练。这是最简单的二次开发,不涉及观测维度和网络结构变动。
5.2 加入动态障碍物与避障观测
要让智能体躲开移动障碍物,需要同时改三处:环境里增加障碍物状态和碰撞检测,观测向量里追加障碍物相对位置,网络输入维度相应加长。核心改动集中在path_env.py,网络侧只需在 Actor 和 Critic 的输入维度参数上加一个 offset。
观测里加障碍物相对位置时,不要只给绝对坐标。绝对坐标在不同起始位置下没有泛化性,给相对位置才能让策略在不同场景复用。奖励侧同样新增障碍物碰撞惩罚项,权重要和智能体间碰撞惩罚放在同一数量级,否则模型会为了躲障碍物把所有智能体聚到另一侧。
5.3 从 MADDPG 换到 QMIX 或 MAPPO
如果你想把算法换成值分解类的 QMIX,或纯 on-policy 的 MAPPO,需要动的部分集中在agents/maddpg.py到agents下的新文件。换 MAPPO 时,经验回放要从 off-policy 随机缓冲区换成 on-policy 轨迹组,ReplayBuffer直接丢掉;换 QMIX 时,Critic 不再是对联合动作直接评估的 Q 函数,需要加入 mixing network 并把单调性约束考虑进去。好在网络结构和环境接口分离,环境侧一行不用动,只需要在train.py里替换 agent 类名并适配update接口。
真正值得提醒的一件事:换算法后先跑小规模冒烟测试,只跑 10 个 episode 看有没有 NaN 和维度不匹配,再开长训练。我在实际开发中吃过好几次暗亏,改了 Actor 输入维度忘了改 Critic,训练脚本里维度没报错,直到收敛失败才排查到问题。从那以后,我每次改环境都强制把“维度改动清单”单独列一遍,从观测维度反推 Actor、Critic、ReplayBuffer 到 batch 拼接处逐行核对,确认无误再训正式模型。这套路径跟随包的价值就在于把多智能体强化学习最烦的工程细节摊开摆平了,让你能更快走到调策略、改算法那一步。希望帮到你。
本文还有配套的精品资源,点击获取