1. 单机Q-Learning做得再漂亮,多机协同仍会撞成一团
前几天在某交流群里看到一位同学发来标题,问我“基于多智能体Q-Learning强化学习的多无人机协同路径规划与防撞Matlab仿真”到底该怎么下手。一开始他信誓旦旦地说,自己单机Q-Learning已经很熟,给每架飞机各算一条最优路径,再额外写一个防撞规则不就行了。我没有直接否定他,而是让他先跑一个最简单的实验:两架无人机在同一张地图里,起点分别放在地图左右两侧,目标点互换。他跑完以后发来截图——两架飞机在中间区域互相堵死,奖励曲线虽然在涨,但最终谁也没到目标。
这个实验非常典型。单机路径规划只解决“我如何避开静态障碍,最省时地到达目标”,但多无人机协同面对的是动态环境:其他无人机会动,而且它们的动作也在持续改变你周围的空域状况。如果你把队友当成随机移动的障碍,策略会非常保守;如果你完全不考虑队友,训练时表面看起来每条路径都是最优的,合在一起却会碰撞。多无人机协同路径规划与防撞,本质上是两个目标的联立优化:每架无人机要尽量到达自己目标点,同时任何时刻任何两架无人机都不能进入危险距离。
Q-Learning在这里扮演的角色,不是用来替代经典寻路算法去追求单机最优,而是通过奖励信号把“个体目标”和“团队安全”这两个诉求揉到同一个策略里。这也是标题里“协同”二字的真正含义。读者如果是准备做毕业设计、课程项目,或者刚入门多智能体强化学习,把这一层理解透了,后面的Matlab仿真才有方向,否则很容易变成对着代码调库、调完也不明白为什么收敛。
2. 环境建模的取舍:网格、运动模型与可观察状态
2.1 状态空间到底该放哪些信息
我先说结论:多智能体Q-Learning在Matlab里能不能收敛,状态设计占了七成功劳。环境我建议直接用二维网格,而不是连续坐标。连续坐标意味着动作选择是连续控制问题,Q表这种离散查表方法根本存不下;网格离散后,每架无人机的位置坐标都能映射成一个整数索引,Q表的行号才成立。
单机状态非常简单,只有“自己坐标 + 目标坐标 + 静态障碍分布”。多机状态多出来的东西是队友信息。我做过一个模拟项目X,一开始只把“自身坐标”放进状态,队友信息完全靠奖励惩罚去隐式表达,结果两架无人机会反复在同一个走廊里迎面撞上,因为Q表并不知道队友在对面。后来我把状态扩展成:自己坐标、自身目标坐标、每一架队友相对自己的距离档位。
这里有个极其容易犯的维度爆炸问题。10×10网格里,三架无人机如果直接把所有队友的精确坐标全部装进联合状态,状态数是100的三次方量级,再加动作组合,Q表能撑爆内存。我的做法是:不存队友的精确坐标,只存相对距离分档。比如队友距离自己3格以内算“近”,3到6格算“中”,大于6格算“远”,每一对队友给3个离散档位。三架无人机时,自己坐标100 × 两对队友相对距离3×3 = 900个状态,每架飞机一张900×5的Q表,完全跑得动。这套思路本质上是把连续相对距离做粗粒度离散化,以牺牲一点精度换取可训练性,在工程上非常值得。
另一个建议是:初始阶段把每个episode的目标点固定,不要每轮随机重设。目标一旦随机,状态里目标坐标的取值组合会大幅膨胀,训练量成倍上涨,但迭代初期的探索还远没有充分,你根本分辨不出是策略没学好还是状态空间太稀疏。先把固定场景跑通,再逐步增加目标随机性。
2.2 动作空间:悬停动作远比想象中重要
动作空间我建议设置成5个离散动作:上、下、左、右移动一格,外加原地悬停。很多人只设4个移动方向,忽略悬停。悬停这个动作单独拎出来讲,是因为在多机防撞里,它不是“浪费一步”,而是避撞的重要选择。两架无人机在狭窄走廊迎面相遇时,如果动作空间里没有“停下等待”,策略即使想避让,也只能往墙上撞或者绕远路,训练会更难收敛。
转移模型在网格里是确定性的:给定当前坐标和执行动作,下一坐标唯一。可以对移动加上边界约束,地图边界走不出去。但我要强调另一个细节:如果目标格被静态障碍占据,或者被另一架无人机当前占据,该怎么处理?有两种方案。第一种是禁止移动,视作停留并给一个小惩罚;第二种是允许移动到冲突格,然后给一个很大的撞机惩罚。初学者往往直接选第一种,认为“既然撞机不行就不让它撞”,实际上这样会让策略偷懒,学会怎么用规则回避而非在动态环境中主动避让。我更推荐第二种:让它撞,撞了给大惩罚,Q-Learning才能从失败经验里真正学到“危险格不可入”。
2.3 奖励函数:三类信号怎么定权重
奖励设计是整个仿真最能体现经验的地方。我把奖励拆成三个来源:目标任务、路径代价、防撞安全。
| 事件 | 奖励值 | 设计理由 |
|---|---|---|
| 到达目标点 | +50 | 强正反馈,让策略知道成功方向 |
| 与队友同格碰撞 | -100 | 强惩罚,防止策略无视队友位置 |
| 与队友相邻进入警告区 | -10 | 中等惩罚,让策略提前远离危险 |
| 与静态障碍相撞 | -20 | 障碍规避的基础要求 |
| 执行一次移动 | -0.1 | 时间代价,鼓励用尽量少的步数 |
| 距离目标点变近 | +0.5 | 缓解稀疏奖励,给中期引导 |
| 距离目标点变远 | -0.5 | 与上一条对应,约束绕路幅度 |
这套数值不需要一次就能定对。我调试时最典型的错误是距离导引权重给得太高,结果无人机学成“贪快直线冲”,遇到队友挡路宁愿硬撞,因为每步接近目标的奖励累积超过了碰撞惩罚。碰撞惩罚-100看起来很大,但如果路径较长,多走几步的距离惩罚和接近奖励一抵消,碰撞风险反而变得可接受。所以我后来把碰撞惩罚调到-100的同时,把距离引导压到±0.5,保证“撞一次”的损失远大于“多绕几格”的收益。
对于防撞奖励,我还加过一个“保持间距奖励”:只要当步所有无人机两两距离大于安全距离,每架飞机额外获得+0.2。这个设计能明显减少训练出现“各飞各的、偶尔凑巧相撞”的随机事故,让策略更主动地保持队形间距。代价是如果场景里有需要两架无人机接近同一片区域的任务,这个奖励会误导它们互相排斥,具体要不要加,取决于你的场景是否允许无人机近距离协同作业。
3. 多智能体Q-Learning的训练框架:独立Q表也能学出协作行为
3.1 为什么不用联合Q表
在介绍训练框架前,必须说清楚多智能体Q-Learning的两个主要路线。第一个是联合Q表,把所有无人机的位置和动作拼成一个超级状态和超级动作,训练一张全局Q表。理论完美,能学到严格意义上的协作最优策略,但状态空间随无人机数量指数增长。三架无人机在10×10地图上勉强能算,四架以上基本别想在Matlab里跑完一个实验。第二个是独立Q-Learning,即每架无人机维护一张自己的Q表,把其他无人机当作环境的一部分,不显式学习队友策略。显然更接近工程现实。
我选择独立Q表,理由是:训练复杂度可控,代码逻辑清晰,而且只要在状态里显式加入队友相对距离信息,策略依然能表现出明显的协同避让行为。这个结论我在模拟项目X里反复验证过:两架无人机在通道相遇时,训练好的策略会出现“一架主动悬停,另一架先通过”的次序,而不是各自随机躲闪。这种协作不是靠队友建模学出来的,而是奖励函数里“碰撞大惩罚”逼出来的隐式默契。
3.2 训练循环的Matlab骨架与更新顺序细节
整个训练流程我用一段伪代码概括。核心循环在Matlab里大概长这样:
% 初始化 nUAV = 3; gridSize = 10; obsMap = zeros(gridSize, gridSize); Q = cell(nUAV, 1); for i = 1:nUAV Q{i} = zeros(nStateNum(i), 5); end alpha = 0.1; gamma = 0.9; epsilon = 0.9; epsMin = 0.05; for ep = 1:maxEpisodes % 重置所有无人机到初始位置和目标点 resetEnv(); totalReward = zeros(1, nUAV); for step = 1:maxSteps % 关键:按顺序逐架决策,而非同时决策 for i = 1:nUAV s = getStateIndex(i); if rand < epsilon a = randi(5); else [~, a] = max(Q{i}(s, :)); end [s2, r, done] = stepEnv(i, a); Q{i}(s, a) = Q{i}(s, a) + alpha * ... (r + gamma * max(Q{i}(s2, :)) - Q{i}(s, a)); totalReward(i) = totalReward(i) + r; end if all(done) break; end end epsilon = max(epsMin, epsilon * 0.998); recordEpisode(ep, totalReward, collisionCount); end这段代码里有一个非常容易被忽略但影响很大的细节:每步内所有无人机的更新顺序。我一开始图省事,让每架无人机先各自根据自己的当前状态选动作,再一起执行、一起更新Q表。结果训练曲线震荡得很厉害,因为同一时刻多架无人机的Q表都在朝不同方向拉扯,经验样本的“马尔可夫性”被破坏了。改成按编号顺序逐架更新后,每一架更新时都基于最新环境状态,训练稳定性明显提升。
3.3 状态索引的工程实现
训练代码里最核心的getStateIndex函数,要把多维信息压成一个整数。我把每架无人机的状态定义为:自身网格坐标 + 目标坐标 + 两队相对距离档位,然后按固定顺序编码。例如在10×10网格中,自身坐标编号用sub2ind([10 10], x, y),目标坐标同理,相对距离档位用1到3表示,最终组合公式:
stateIndex = sub2ind([100, 100, 3, 3], selfPosIdx, targetIdx, distLevel1, distLevel2);注意sub2ind的参数必须是索引编号,不能直接传坐标,否则组合维度会乱。这个函数建议单独写成脚本,并在每次训练前用几个手工输入验证一遍映射是否唯一。别小看这一步,我见过不少人卡在“明明Q表在更新,奖励却一直不涨”,最后发现是状态编码撞号了,不同的状态映射成了同一个索引,策略根本区分不开。
3.4 epsilon衰减调度
探索率调度也是个经验活。我推荐从0.9开始,每个episode乘一个小于1的衰减系数,最终下限设在0.05。衰减系数可以按总episode数估算:如果你打算训练3000个episode,想在第2000个episode时把epsilon降到0.1左右,那么decay = (0.1 / 0.9) ^ (1 / 2000) ≈ 0.9989。如果衰减太快,比如500个episode就衰减到0.1,策略会在还没探索到好的避让模式时过早收敛到“自闭”行为——比如原地悬停不动,因为没有碰撞惩罚也没有到达奖励,奖励曲线稳定但毫无意义。
4. 防撞逻辑的工程落地:距离分层、动态优先级与对角线穿插
4.1 相对距离三分层的奖罚机制
上一章的奖励表里已经出现碰撞和警示区的惩罚,这里展开讲讲为什么分层而不是简单地“撞就罚”。真实飞行的安全概念是有缓冲区的:绝对碰撞区、警告接近区、安全区。网格仿真也应该模拟这种缓冲逻辑。我把相对距离分成三层:
| 相对距离 | 含义 | 奖励/惩罚 |
|---|---|---|
| 0格 | 同一网格,碰撞 | -100 |
| 1格 | 相邻网格,进入警告区 | -10 |
| 2格及以上 | 安全距离 | +0.2(前述的间距维持奖励) |
分层的好处是,策略能在真正撞上前几步就开始规避,而不只是在碰撞发生后挨打。如果你只在碰撞时给出惩罚,状态空间中“即将相撞”的位置几乎没有训练样本,因为一旦相撞就结束或者被大惩罚,Q-Learning很难从稀疏的负反馈中学到提前避让。警告区的-10提供了一个平滑的“风险梯度”,引导Q表在靠近队友时主动降低动作价值。
4.2 动态优先级避让:谁让行
即便奖励设计好了,训练初期依然会出现两架无人机同时选择同一个空闲格的情形。如果环境允许它们同时移动过去,那这一格会出现重叠,直接判定碰撞即可。但还有一个更隐蔽的问题:两架无人机同时向对方的当前格移动时,如果双方都被“目标格被占据则禁止移动”的硬规则挡住,就会双双停在原地形成死锁。
我的处理方式是加入动态优先级规则:每个时间步内,按“距离终点更近者优先”排序,先决策的无人机先占用目标格,后决策的无人机如果目标格已被占用,则自动改为本步悬停。这个规则只在冲突时刻生效,不介入正常巡航策略,相当于给训练过程加了一个兜底保护。要提醒的是,动态优先级规则本身不能替代Q-Learning的防撞学习,它只保证仿真不会因死锁而废掉大量episode,真正学会主动避让还得靠奖励引导。
4.3 网格交换碰撞检测
这是网格仿真特有的坑:多无人机同时相向穿过相邻格。假设无人机A从(2,2)移动到(2,3),无人机B同时从(2,3)移动到(2,2),按最终位置判断,两架飞机没有重叠,奖励判定会认为没有碰撞。但真实飞行中,这两架无人机必然在路径交汇处擦身而过,距离几乎为零,必须算碰撞。我在某次实验里就吃过亏,训练完成后重放动画,发现两架飞机经常在一条线上互相穿过去,视觉上极不自然,但程序不报碰撞。
解决办法是在碰撞检测里增加一步“上一时刻状态比对”:如果本步结束后两架无人机的位置刚好是对方上一步的位置,且它们在上一步的曼哈顿距离为1,则判定为“交叉碰撞”,执行碰撞惩罚。千万不要省略这一步,否则你的防撞策略学出来是一套“允许擦身而过”的错误逻辑,放到实际场景根本没有参考价值。检测逻辑在Matlab里可以这样写:
% oldPos: 上一时刻所有无人机位置 for i = 1:nUAV for j = i+1:nUAV if newPos(i,:) == oldPos(j,:) && newPos(j,:) == oldPos(i,:) && ... abs(oldPos(i,1)-oldPos(j,1)) + abs(oldPos(i,2)-oldPos(j,2)) == 1 collisionFlag = true; end end end类似地,同一时间步内两架无人机沿对角线方向互换位置,比如A从(2,2)到(3,3),B从(3,3)到(2,2),也应该检测为危险接近。网格里对角线穿插的物理意义是两机在角点交汇,虽然曼哈顿距离变化超过了1,但实景确实危险。保守处理是统一纳入交叉碰撞判定。
5. 完整Matlab仿真项目搭建步骤:从零开始跑通一个可复现的项目
5.1 模块划分:不要把所有代码堆进一个脚本
仿真项目做到后面,必然会反复调试奖励参数、地图布局、飞机数量。全部堆在单个主脚本里会改到崩溃。我建议一开始就按函数模块拆开,分五个文件:
| 文件 | 职责 |
|---|---|
| initEnv.m | 初始化地图、障碍、无人机初始与目标位置,生成Q表 |
| getStateIndex.m | 将多维状态映射为Q表行号 |
| stepEnv.m | 根据动作返回下一状态、奖励、终止标志 |
| trainUAV.m | 主训练循环,调用上述函数并记录指标 |
| visualizeResult.m | 训练结束后重放最优策略,绘制轨迹动画和训练曲线 |
把状态编码和环境转移独立成函数还有一个好处:后续如果你想把网格从10×10扩到15×15,或者把无人机数量从2架调到4架,只需要改initEnv里的参数和getStateIndex里的编码维度,训练主循环一行不用动。
5.2 训练与可视化独立:先跑完再重放
训练过程中如果实时绘制动画,Matlab的绘图开销会让单episode耗时翻好几倍,严重影响你调参的速度。我的习惯是:训练阶段只记录每episode的平均奖励、碰撞次数、成功到达数,不画图;训练结束后,用学习到的Q表按贪心策略重放一次完整过程,再在地图上画出每架飞机的轨迹折线,并动态标注每一时刻各机的相对距离。这样既能看清楚策略是否真的学会了防撞,又不会把训练速度拖垮。
重放时的轨迹可视化,我用的方法比较简单:先画出网格地图背景,障碍位置用黑色方块标出,各无人机初始位置用不同颜色的圆形标出,目标点用星号标出;每走一步更新圆点位置,并记录坐标供最终画轨迹线。Matlab里圆点和轨迹线都是图形句柄,更新位置用set函数,循环配合drawnow就行。
5.3 训练记录指标怎么设计
除了平均奖励,我还强烈建议记录三类指标:每episode的成功率、碰撞次数、到达所有目标所需总步数。平均奖励曲线有时候会骗人,比如策略学会悬停后奖励趋于稳定,但成功率极低;或者策略经常撞机但很快就到达目标,奖励反而不错。把成功率和碰撞次数放到一起看,才能判断训练是否真的有效。我一般在每个episode末尾记录三个数值,训练完成后画出三条曲线叠在同一张图里,一眼能看出收敛态势。
成功率的定义是:一个episode内所有无人机在步数上限内都到达目标且没有碰撞,记为成功。这个指标在训练前期几乎为零,看着焦虑,但完全正常。我通常以“连续50个episode成功率达到90%以上”作为停止训练的标准,而不是盲目跑满预设次数。
6. 收敛调试与避坑实录:那些跑完才发现的问题
6.1 训练不收敛的头号原因:稀疏奖励与状态粗粒度
我做模拟项目X时,第一次设定奖励只有“到达目标+50”和“碰撞-100”,其他一律为零,结果前500个episode平均奖励非常平稳地保持在-10附近,Q表几乎没有有效更新。原因是网格地图里随机探索走到目标点的概率太低,正反馈信号根本传不回来,Q表里绝大多数状态的动作价值一直是零。
解决方法是奖励塑形。把“接近目标+0.5、远离目标-0.5”加进去之后,训练明显有了方向性。但如前面所说,塑形比例要控制好。我调试时的经验判断标准很简单:跑完500个episode后,随便抽几个状态看Q表,如果所有动作Q值差都在0.1以内,说明信号太弱;如果某个动作明显高出其他动作,才能继续训练。奖励塑形值可以按“Q值差”反推,当你发现策略会为了拿接近奖励而在目的地旁边反复横跳,说明距离奖励权重过高,需要下调一半再试。
6.2 状态维度爆炸的预警信号与缓解思路
训练中遇到Matlab报“内存不足”或者单episode耗时骤增,十有八九是状态维度失控。这个问题在3架无人机时还不明显,到4架以上就非常突出。4架无人机的两两相对距离组合数是3的6次方等于729,乘以自身坐标和目标的组合,Q表很容易突破百万行,训练时间完全不可接受。这时有两个方向:一是继续做状态压缩,把网格从10×10减到8×8,相对距离档位从3档减到2档;二是放弃Q表,改用函数近似,比如线性值函数或小型神经网络,这就是DQN的路线。
DQN过渡思路我不展开写,但可以给个方向:用向量输入替代整数索引,输入为自身坐标、目标坐标、所有队友相对距离,输出为5个动作的Q值,再加上经验回放和目标网络。只要你的Q-Learning环境建模和奖励设计没变,换DQN时环境代码可以完全复用,只改训练逻辑。
6.3 一组可直接启动的推荐参数
如果你不想从零调参,下面这组参数是我验证过能稳定收敛到“三架无人机全部到达目标且基本不碰撞”的配置。地图尺寸10×10,3架无人机,静态障碍3个,初始位置分散在地图四角区,目标点放在互不重叠的中远位置,单episode最大步数200,训练3000个episode。学习率alpha取0.1,折扣因子gamma取0.9,epsilon从0.9衰减到0.05,衰减系数0.998。奖励表按第2.3小节的数值设置。跑完大约几分钟到十几分钟,取决于电脑配置和Matlab版本。最终成功的策略表现是:平均步数80到120步,碰撞率低于2%,成功率达到九成以上。不同随机种子结果会有波动,看趋势而非绝对值。
6.4 对“策略看起来学会了,实际却没学会”的判别经验
最后分享一个判断策略是否真正学会防撞的土办法。只看平均奖励很容易被迷惑,因为一种“两架飞机互相绕远、从不同路线各自到达”的策略也能拿到不错的奖励,但效率很低。我会在训练结束后把所有无人机初始位置改放到一条窄通道的两端,目标点互换,让它们必须面对面通过。如果这个测试场景下策略还能顺畅完成而碰撞率不高,说明它真正理解了“等待”和“让行”;如果这个测试场景里飞机卡死或互撞,说明之前只是碰巧学会了一个特定布局下的走位。
这个压力测试成本很低,但非常能暴露问题。我做项目时,基本每调一次奖励参数都会跑一遍这个测试,比单纯看训练曲线靠谱得多。多智能体协同路径规划的学习效果,说到底不取决于你写了多复杂的代码,而取决于你有没有在失败样本里留下足够多的“为什么撞”的信息,并让Q-Learning把这些信息转化成提前N步的避让习惯。把这一点想明白,Matlab仿真里很多看似玄学的收敛问题都能逐一拆掉。