多智能体强化学习路径跟随控制: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), idx+3); 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(end+1: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(idx+1, :) - 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~3,w.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,属于完全分散式。如果想上集中训练分散执行,把三辆车的观测拼成一个联合观测喂给各自的 critic,actor 仍然只吃本地观测。改动点在createAgents.m里 critic 的输入维度,以及环境返回的观测里额外附带一份联合观测。这条路训练更慢但协同更稳,车多的时候收益明显。再往上就是换 MADDPG 或 PPO 这类支持多智能体的实现,网络结构和训练选项都要重写,建议先把当前这套跑出稳定基线再动。
验证方法上我有两个固定动作。一是固定随机种子,改一处参数跑两次,确认结果可复现,排除随机性带来的假改进。二是做单 agent 回退测试:把numAgents改成 1,用同一套奖励和网络重训,如果单 agent 都跑不好,多 agent 的结论就不可信。这两步花不了多少时间,但能挡住大部分「改了半天其实是噪声」的自我感动。
最后说个习惯。这套工程我每次改完奖励或者观测,都会先把回合数压到 20、关掉并行、打开Verbose,把每一步的观测、动作、奖励打出来看一遍,确认数据在物理上说得通,再放开长训。这个动作救过我很多次,尤其是观测维度改了但网络没同步那种低级错误,一旦进到长训,几百回合之后才发现,后悔药是没有的。希望帮到你。
本文还有配套的精品资源,点击获取