多智能体强化学习路径跟随控制:MATLAB/Simulink工程实战
简介面向MATLAB强化学习与路径跟踪控制的学习者这份示例压缩包围绕多智能体协同沿预设路径行驶展开覆盖智能体创建、参数配置、环境重置、模型训练与Simulink仿真等环节适合具备一定MATLAB基础、希望理解多智能体强化学习落地流程的开发者与研究者。包内共7个文件以4个m脚本、1个mat模型数据、1个slx仿真模型和1个mlx交互式脚本为主分别承担算法实现、训练结果保存、系统级仿真与过程演示等用途压缩包约595KB。已有1128人学习说明该示例在多智能体路径跟踪方向具有一定参考热度。读者可据此梳理ACC与LKA两类智能体的策略构建思路观察参数设置对训练效果的影响并借助Simulink模型理解智能体与环境的交互关系为后续复现、调参与扩展实验提供较完整的骨架。1. 多智能体路径跟随控制先别急着上算法先认清这条链路有多长拿一辆车做路径跟随单智能体强化学习跑个几百回合基本能看出趋势可一旦把车加到两辆以上同样的 DDPG 参数直接翻车——两辆车互相抢道、在弯道画龙奖励曲线像心电图。这份示例工程解决的就是这个落差它把多智能体强化学习落到路径跟随控制这样一个有明确物理约束的场景里用多车也可替换为多机器人同时沿参考路径行驶训练、评估、可视化一条链路全都摆出来谁都能照着跑一遍。适合两类人一类是想把强化学习从倒立摆、赛车这类玩具环境迁移到真实控制问题的开发者另一类是已经会写单智能体、但没搞明白多智能体里观测、奖励、训练配置该怎么改的控制方向同学。项目环境搭在 MATLAB/Simulink 体系下先跑通再谈改是我建议的节奏。2. 工程骨架与环境建模从 Simulink 车辆模型到多智能体训练配置多智能体路径跟随跟普通强化学习示例最大的区别不是算法本身而是「环境要同时吐出 N 份观测和 N 份奖励」。所以拆这个工程我从目录和三元组定义入手而不是先去抠网络层数。先把骨架摸清后面调参才有坐标系。2.1 目录结构与各文件职责这份工程的文件组织是典型「入口脚本 环境类 工具函数包 模型文件」四层结构跑之前先认一遍TrainMultipleAgentsForPathFollowingControlExample/ ├── runTraining.m % 训练入口一条命令拉起整个流程 ├── runEvaluation.m % 评估入口加载训练好的 agent 跑仿真 ├── model/ │ └── pathFollowingMultiAgent.slx % Simulink 车辆运动学模型单车 参考路径 ├── env/ │ ├── PathFollowingEnv.m % 自定义环境类实现 reset/step │ ├── computeObservations.m % 观测构造自车状态 邻居相对位姿 │ └── computeReward.m % 奖励构造横向误差 / 航向误差 / 速度误差 ├── agents/ │ ├── createAgents.m % 批量创建 DDPG 智能体 │ └── createNetworks.m % actor / critic 网络定义 ├── utils/ │ ├── loadReferencePath.m % 读取参考路径点含插值 │ └── plotTrajectories.m % 轨迹可视化 └── results/ % 训练过程中保存的 .mat 检查点职责划分很清楚env/只管物理和奖励agents/只管网络和超参runTraining.m只负责把它们串起来。这个边界很重要我见过不少把奖励写进 agent 创建函数的写法改一次惩罚系数要动三个文件血泪经验。阅读顺序上我一般先看PathFollowingEnv.m的reset和step再看createAgents.m的网络规格最后才回到runTraining.m看训练配置。因为观测维度、动作维度这两个数字必须和网络输入输出严格对齐先看前面的能省掉大量维度不匹配的排查时间。参考路径这块loadReferencePath.m通常返回的是一组离散点[x, y]车辆当前位置需要通过最近点搜索找到投影索引idx再据此算横向误差。索引搜索别小看实现方式有两种常见做法全量欧氏距离最小简单但慢或者基于上一帧索引做局部窗口搜索快但初始帧要特殊处理。工程里如果只跑几辆车前者完全够用。2.2 观测、动作、奖励三元组怎么定义强化学习示例想读懂本质就是把这三样东西讲清楚。这份工程的三元组设计如下组成内容维度示例观测自车[x, y, theta, v] 最近参考点索引偏移 邻居相对位置[dx, dy, dv]单车 12 维左右随邻居数变化动作前轮转向角 纵向加速度2 维连续动作奖励横向误差惩罚 航向误差惩罚 速度跟踪惩罚 偏离硬惩罚标量观测构造的代码大致长这样function obs computeObservations(selfState, neighborStates, refPath, idx) % selfState : 4x1 [x; y; theta; v] % neighborStates : Nx4 邻居车辆状态矩阵可能为空 % refPath : Mx2 参考路径点 % idx : 当前最近参考点索引 % 1) 自车基础状态航向做归一化避免角度回绕跳变 obsSelf [selfState(1:2) / 10; ... % 位置缩放量纲对齐 wrapToPi(selfState(3)); ... % 航向限制在 [-pi, pi] selfState(4) / 20]; % 速度归一化 % 2) 参考路径局部信息取当前点前后各 3 个点的纵向/横向方向 winIdx max(1, idx-3) : min(size(refPath,1), idx3); localRef refPath(winIdx, :); obsRef reshape((localRef - refPath(idx,:)) / 10, [], 1); % 3) 邻居相对位姿按距离排序后取最近 2 个缺位补零 if isempty(neighborStates) obsNb zeros(6, 1); else rel neighborStates(:, 1:2) - selfState(1:2); [~, order] sort(vecnorm(rel, 2, 2)); rel rel(order(1:min(2,end)), :); obsNb reshape([rel; neighborStates(1:min(2,end),4)] / 10, [], 1); obsNb(end1:6) 0; end obs [obsSelf; obsRef; obsNb]; end这段的关键在三个地方。第一是量纲对齐位置、速度都做缩放否则横向误差这个 0.5 米量级的数和 20 m/s 的速度混在一个向量里actor 网络的第一层权重会被大数主导。第二是航向归一化wrapToPi不加theta从 π 跳到 -π 时观测会出现一个阶跃训练时表现为偶发的动作抖动很难查。第三是邻居定长补齐多智能体观测维度必须固定邻居数量随场景变化所以按最近 N 个填充、缺位补零是常见做法。奖励函数决定了训练出来的车是贴着路径走还是在路径附近晃写法如下function r computeReward(selfState, refPath, idx, prevIdx, w) % w 为权重结构体w.lat / w.head / w.speed / w.deviate / w.progress refPt refPath(idx, :); % 横向误差自车到参考路径最近点的欧氏距离 latErr norm(selfState(1:2) - refPt); % 航向误差车头方向与参考路径切线方向的夹角 if idx size(refPath, 1) tangent refPath(idx1, :) - refPt; else tangent refPt - refPath(idx-1, :); end headErr wrapToPi(selfState(3) - atan2(tangent(2), tangent(1))); % 速度误差与设定巡航速度的偏差 speedErr selfState(4) - w.vRef; % 前进奖励鼓励沿路径推进抑制原地打转 progress max(0, idx - prevIdx) / 10; % 硬约束偏离超过 2 米直接重罚 deviatePenalty 10 * (latErr 2); r - (w.lat*latErr^2 w.head*headErr^2 w.speed*speedErr^2) ... w.progress*progress - deviatePenalty; end参数说明w.lat一般给到 1~3w.head在 0.5~1 之间w.speed小一点0.1 左右即可不然车为了保速度会牺牲横向精度。w.progress是防止摆烂的关键——只给惩罚不给进度奖励智能体发现停在原地误差也不会变大就容易学出一个静止不动的策略。deviatePenalty是硬约束把车拉回可行域但阈值别设太小否则训练初期大面积触发奖励信号变成一片 -10网络学不到梯度。2.3 多智能体训练配置的关键参数单智能体训练用rlTrainingOptions多智能体要换成rlMultiAgentTrainingOptions这是最容易漏的一步。核心配置如下numAgents 3; trainOpts rlMultiAgentTrainingOptions(... AgentGroups, 1:numAgents, ... % 每个 agent 独立一组 MaxEpisodes, 2000, ... % 总回合数 MaxStepsPerEpisode, 500, ... % 单回合最大步数 ScoreAveragingWindowLength, 50, ... % 平滑窗口看趋势别看单点 UseParallel, true, ... % 并行采样多核提速 StopTrainingCriteria, AverageReward, ... % 停止条件 StopTrainingValue, -80, ... % 平均奖励达到即停 SaveAgentCriteria, AverageReward, ... % 检查点保存条件 SaveAgentValue, -120, ... SaveAgentDirectory, results, ... Plots, training-progress, ... Verbose, false); trainingStats train(agents, env, trainOpts);AgentGroups这个参数值得单独说它决定哪些 agent 共享同一套训练统计和停止条件。三辆车同构、任务相同就分成三组各自统计如果做的是领航车 跟随车这种异构编队按角色分组更合理。ScoreAveragingWindowLength设 50 是因为多智能体奖励波动天然比单智能体大窗口太小会被噪声带偏判断太大又看不出改进。StopTrainingValue别照抄它强依赖于奖励函数的具体量级换一套权重就得重新标定一个能接受但不容易达到的值我一般先跑 200 回合看平均奖励落在哪个区间再往上抬 20% 作为目标。UseParallel打开前记得先起并行池否则训练会卡在启动阶段表现为进度条一动不动这个问题在排查章节还会展开。3. 训练主循环拆解createAgents、reset、step、learn 一条链路骨架看完接下来把线拉直一个回合从 reset 开始动作从 agent 里取环境 step 回填奖励再回到 agent 更新网络。多智能体的复杂度全在这条链路的「多份数据同时流动」上代码写错一处症状往往在几百回合之后才暴露。3.1 智能体创建与网络结构三个 agent 结构同构只是初始化参数不同用循环创建即可function agents createAgents(obsInfo, actInfo, numAgents, seed) % obsInfo / actInfo 来自环境的观测与动作规格 % seed 用于让每个 agent 的网络初始化产生差异 rng(seed); agents cell(numAgents, 1); for k 1:numAgents % actor观测 - 动作最后接 tanh 保证输出在 [-1,1] actorLayers [ featureInputLayer(obsInfo.Dimension(1), Name, obs) fullyConnectedLayer(128, Name, fc1) reluLayer(Name, relu1) fullyConnectedLayer(64, Name, fc2) reluLayer(Name, relu2) fullyConnectedLayer(actInfo.Dimension(1), Name, fc3) tanhLayer(Name, tanh)]; actor rlContinuousDeterministicActor( ... actorLayers, obsInfo, actInfo); % critic观测 动作 - Q 值 critLayers [ featureInputLayer(obsInfo.Dimension(1) actInfo.Dimension(1), ... Name, obsAct) fullyConnectedLayer(128, Name, fc1) reluLayer(Name, relu1) fullyConnectedLayer(64, Name, fc2) reluLayer(Name, relu2) fullyConnectedLayer(1, Name, q)]; critic rlContinuousQValueFunction( ... critLayers, obsInfo, actInfo); agentOpts rlDDPGAgentOptions( ... SampleTime, 0.1, ... DiscountFactor, 0.99, ... TargetSmoothFactor, 1e-3, ... ExperienceBufferLength, 1e6, ... MiniBatchSize, 128, ... NoiseOptions, rl.option.GaussianActionNoise( ... StandardDeviation, 0.3, ... StandardDeviationDecayRate, 1e-5)); agents{k} rlDDPGAgent(actor, critic, agentOpts); end end几个参数值得抠。TanhLayer放在 actor 末端是连续动作的标准做法把无界输出压到 [-1,1]再在环境里乘以转向角上限和加速度上限做缩放——这个缩放必须和actInfo的上下界一致否则网络学到的动作在环境里被截断梯度会失真。ExperienceBufferLength给到 1e6 是保险值多智能体总样本量涨得快缓冲区太小会导致早期经验被过早覆盖出现学着学着忘了怎么起步的现象。StandardDeviationDecayRate控制探索噪声衰减多车场景下这个值不能太大衰减太快三辆车动作趋同学不出避让行为。每个 agent 独立一套网络是最直接的做法代价是参数量和显存线性增长。跑三辆车没问题上到八辆以上我一般会考虑让 critic 共享观测把邻居信息全部拼进去这就是后面二次开发里要谈的 CTDE 改造。3.2 环境步进与奖励回填环境类的step是多智能体里最容易写错的地方。单智能体返回一个标量奖励多智能体必须返回与 agent 数量对齐的奖励向量或 cell 数组顺序一旦错位训练出来的车会互相「抢功劳」。function [nextObs, rewards, isDone, loggedSignals] step(env, actions) % actions: numAgents x 1 cell每个元素是该 agent 的动作向量 numAgents numel(actions); rewards zeros(numAgents, 1); for k 1:numAgents % 1) 动作反归一化到物理量程 steer actions{k}(1) * env.SteerLimit; % 前轮转角上限 accel actions{k}(2) * env.AccelLimit; % 纵向加速度上限 % 2) 单车运动学推进也可换成调用 Simulink 模型单步仿真 env.States(k, :) kinematicsStep( ... env.States(k, :), steer, accel, env.Ts); % 3) 最近参考点索引更新基于上一帧索引做局部搜索 env.Indices(k) searchNearestIndex( ... env.RefPath, env.States(k, 1:2), env.Indices(k), 15); % 4) 逐车计算奖励权重结构体全局共享 rewards(k) computeReward( ... env.States(k, :), env.RefPath, ... env.Indices(k), env.PrevIndices(k), env.Weights); end env.PrevIndices env.Indices; % 5) 回合终止任一车偏离过远或达到最大步数 latErrs vecnorm(env.States(:,1:2) - ... env.RefPath(env.Indices,:), 2, 2); isDone any(latErrs env.MaxLateralError) || ... env.StepCount env.MaxSteps; % 6) 构造下一帧观测邻居信息按距离取最近两个 nextObs cell(numAgents, 1); for k 1:numAgents others env.States(setdiff(1:numAgents, k), :); nextObs{k} computeObservations( ... env.States(k,:), others, env.RefPath, env.Indices(k)); end end逻辑上有几个点必须讲清楚。动作反归一化必须在环境里做不能丢给网络SteerLimit一般取 0.5 rad 左右AccelLimit取 2~3 m/s²不同车辆底盘差别很大换平台时只改这两个常数就行。索引更新用局部搜索searchNearestIndex的第四个参数 15 是搜索窗口比全量搜索快一个量级但要保证车辆单步位移不超过窗口覆盖的弧长否则会锁死在错误的索引上车会朝反方向跑。终止条件用any而不是all多车场景下一辆车冲出边界就该重置否则会训练出「牺牲一辆车保全整体」的畸形策略。奖励向量顺序必须和 agents 顺序严格一致这一点没有编译器帮你检查是三辆车互相抢功劳问题的唯一根源。3.3 并行训练与经验回放打开UseParallel之后train会用并行池做环境采样主进程只做网络更新。这个模式对多智能体尤其划算因为每回合的计算量是单车的好几倍。开启方式% 训练前先起池进程数按物理核数的一半到全部都可以 pool gcp(nocreate); if isempty(pool) parpool(Processes, feature(numcores)); end % 然后把 UseParallel 打开再调 train trainOpts.UseParallel true; trainingStats train(agents, env, trainOpts);并行采样的代价是随机数流被拆散同一次运行的结果不可复现。调试阶段我建议先关掉并行、用小回合数把链路跑通确认观测维度、奖励量级、终止条件都正常再开并行提速度。开了并行之后如果进度条长时间不动十有八九是 worker 上某个依赖没加载到比如自定义函数不在路径里、Simulink 模型缓存没同步这类问题在单进程下根本不会出现。经验回放的细节方面DDPG 默认从缓冲区随机采样MiniBatchSize个转移。多智能体里如果三辆车共用一份缓冲区有些实现会这么做以求样本利用率采样出来的「观测-动作-奖励-下一观测」四元组必须来自同一时刻、同一车辆混了就会造成奖励错配。这份工程里每个 agent 保有自己的缓冲区结构上更清晰代价是训练初期样本效率略低。4. 评估与可视化训练完怎么证明它真的在跟随路径训练跑完不等于项目跑通。很多人看到奖励曲线抬头就以为成了结果一开车发现三辆车在起点并排打转。评估这一步要多花点心思否则前面几千回合都白费。4.1 评估脚本与探索噪声关闭评估和训练最大的区别是动作要确定性输出。可以在训练结束后直接取 actor 的均值动作或者在 agent 选项里把噪声标准差临时置零function results runEvaluation(agentFile, env, numEpisodes) % 加载训练好的 agent关闭探索噪声跑若干回合收集轨迹 loaded load(agentFile); % 内含 agents 变量 agents loaded.agents; for k 1:numel(agents) % 关闭探索噪声评估阶段只要确定性策略 agents{k}.AgentOptions.NoiseOptions.StandardDeviation 0; end results struct(states, {}, rewards, {}, done, {}); for ep 1:numEpisodes obs reset(env); epRewards 0; epStates []; for t 1:env.MaxSteps % 每个 agent 各自从观测出动作cell 顺序必须与训练时一致 actions cell(numel(agents), 1); for k 1:numel(agents) actions{k} getAction(agents{k}, obs{k}); end [obs, rewards, isDone, ~] step(env, actions); epRewards epRewards sum(rewards); epStates [epStates; env.States(:, 1:2)]; % 记录每步位置 if isDone, break; end end results(ep).states epStates; results(ep).rewards epRewards; results(ep).done isDone; end endgetAction在不带噪声时等价于取 actor 网络的输出注意它返回的一般是归一化后的动作环境里的反归一化仍然照常执行。epStates每步追加所有车的位置这是后面画轨迹图的数据源。评估回合数取 5~10 足够重点看每回合的平均横向误差和是否有车提前终止。4.2 轨迹图与奖励曲线怎么读plotTrajectories.m一般会把参考路径和各车轨迹画在同一张图上。判断标准很直接轨迹应该与参考路径基本重合弯道处允许有 0.2~0.5 米的外扩车与车之间在交叉口和汇入段不能出现轨迹交点。如果轨迹在直道上画龙问题出在航向误差权重如果在弯道切内道问题出在横向误差权重偏小或者速度奖励过大。奖励曲线的读法要分两层。单回合总奖励看是否随回合单调上升中间有几次下探是正常的因为探索噪声在衰减策略会阶段性变差再变好。平滑后的平均奖励看是否在某个区间收敛。如果训练到一千回合还在上下大幅波动先别调网络结构检查三个数据面症状先看什么常见原因平均奖励长期贴在某个常数上奖励函数各权重是否量级失衡某一项惩罚主导了总奖励奖励上升但轨迹不收敛观测里是否缺了航向或速度信息马尔可夫性不足三辆车奖励交替高低起伏奖励向量的顺序是否与 agent 顺序一致数据错位回合长度越来越短偏离惩罚阈值是否过小训练初期就被大量终止5. 常见问题排查训练不收敛、车画龙、并行报错的那几个坑这套流程我前后跑过好几轮踩到的坑集中在数据对齐、量纲和并行环境三类。下面的现象都是真实会遇到的按「现象 → 原因 → 解决」整理。5.1 训练几百回合奖励不涨一直停在初始水平现象进度条在跑平均奖励几乎水平偶尔还往下掉。原因最常见的是奖励量级失衡。横向误差一项乘上权重后动辄负几百其它项贡献微乎其微critic 学到的 Q 值全被这一项主导actor 收到的梯度方向单一。另一种可能是DiscountFactor设得太小多车协同需要看未来几十步的避让收益折扣因子 0.9 以下基本学不出协同。解决先把每项奖励拆开打印看重启后第一回合各项的典型量级让总量级落在 -10 到 -50 之间比较健康。横向和航向权重比值控制在 2:1 左右速度项保持小权重。折扣因子多车场景给 0.99 起步路径长、需要提前避让的给到 0.995。5.2 直道上车画龙方向盘左右小幅抖现象轨迹图在直线段呈明显的正弦形频率高、幅度小。原因航向误差权重过大横向误差权重过小。策略为了立刻压低航向误差每一步都在做反向修正形成极限环。另一个诱因是动作噪声衰减太慢训练后期的残余噪声在确定性策略上表现为持续抖动。解决先把w.head降一档同时把w.lat提一档重训 300 回合看轨迹。噪声方面把StandardDeviationDecayRate调大一两个数量级让训练后期噪声真正逼近零。如果还抖检查SampleTime是否与环境步长一致不一致会导致动作在环境里被重复执行。5.3 打开 UseParallel 后进度条卡住或直接报池错误现象训练启动后长时间无输出或者报parpool无法启动、worker 找不到函数。原因自定义函数或 Simulink 模型没有随 worker 一起加载。并行池的 worker 是独立进程主进程路径上的函数它们不一定能看到Simulink 模型首次编译的缓存也不会自动同步到 worker。解决训练前用addAttachedFiles(pool, {...})把环境函数和模型文件显式挂上去或者确保所有依赖都在统一的工程路径下并调用addpath同步。先关并行跑通 5 个回合再开并行是能最快定位这类问题的方法。5.4 训练时表现不错评估时三辆车全部冲出路径现象训练平均奖励已经收敛评估跑出来第一回合就终止。原因评估时忘记关探索噪声或者加载的 agent 和环境的观测维度不匹配但没报错。前者表现为动作幅度异常大后者表现为动作看起来很随机。解决评估脚本里显式把NoiseOptions.StandardDeviation置零加载后先打印agents{k}.ObservationInfo.Dimension和环境的观测维度做一次断言。这个断言十几行代码能省掉一整轮重新训练。5.5 保存的检查点加载回来动作全是零或常数现象load之后 agent 结构正常但getAction输出恒定。原因保存的是带噪声的 agent而噪声标准差衰减到接近零时若网络本身没学好输出就退化为常数。更隐蔽的一种是保存了 critic 但 actor 没同步SaveAgentCriteria触发时机在多智能体里是按平均奖励判断的个别 agent 可能还没跟上。解决检查点别只看平均奖励保存后立刻跑一次短评估确认这个检查点可用。SaveAgentValue不要设得过于激进宁可多存几份。加载后先做维度断言再做一次单回合仿真两步都过才算这个文件可用。6. 二次开发改造换场景、换奖励、换算法的三条路径跑通之后想往自己的课题或产品上搬改动会集中在三个地方按投入产出比排序是环境模型 → 奖励设计 → 算法结构。换环境模型是最直接的。把kinematicsStep换成自己的车辆动力学、或者把 Simulink 模型替换成机器人差速底盘模型只要保证「输入动作、输出新状态」这个接口不变其它文件基本不用动。需要同步改的只有三个参数动作的SteerLimit和AccelLimit、观测里的状态维度、以及MaxLateralError。我一般会先写一个纯 MATLAB 的运动学版本跑通闭环再接 Simulink因为 Simulink 编译一次的时间成本比纯代码高得多。改奖励是调行为性价比最高的手段。想让它更贴线加大w.lat想让它别急刹减小w.speed并给加速度变化率加一项平滑惩罚想让它学会排队而不是并排就在奖励里加一条「与前车纵向距离小于阈值且横向偏移小于阈值」的罚项。改完别急着长训先跑 50 回合看奖励量级有没有跑偏。换算法结构是进阶路线。当前每个 agent 独立 actor 独立 critic属于完全分散式。如果想上集中训练分散执行把三辆车的观测拼成一个联合观测喂给各自的 criticactor 仍然只吃本地观测。改动点在createAgents.m里 critic 的输入维度以及环境返回的观测里额外附带一份联合观测。这条路训练更慢但协同更稳车多的时候收益明显。再往上就是换 MADDPG 或 PPO 这类支持多智能体的实现网络结构和训练选项都要重写建议先把当前这套跑出稳定基线再动。验证方法上我有两个固定动作。一是固定随机种子改一处参数跑两次确认结果可复现排除随机性带来的假改进。二是做单 agent 回退测试把numAgents改成 1用同一套奖励和网络重训如果单 agent 都跑不好多 agent 的结论就不可信。这两步花不了多少时间但能挡住大部分「改了半天其实是噪声」的自我感动。最后说个习惯。这套工程我每次改完奖励或者观测都会先把回合数压到 20、关掉并行、打开Verbose把每一步的观测、动作、奖励打出来看一遍确认数据在物理上说得通再放开长训。这个动作救过我很多次尤其是观测维度改了但网络没同步那种低级错误一旦进到长训几百回合之后才发现后悔药是没有的。希望帮到你。本文还有配套的精品资源点击获取