1. 为什么在自动驾驶决策模块里塞进一棵搜索树
第一次把蒙特卡洛搜索树(Monte Carlo Tree Search,下文简称 MCTS)往自动驾驶决策模块里塞的时候,我心里是打鼓的。规划控制那边已经有成熟的采样规划器、有基于优化的方法,甚至有些场景直接上规则状态机也能跑得挺稳,为什么还要引入一个"靠随机模拟来选动作"的东西?这个问题如果不先想清楚,后面写出来的代码一定是照猫画虎,参数调不动,出了问题也不知道从哪儿查。
先说清楚 MCTS 到底解决的是什么问题。自动驾驶的决策层,本质上是在一个部分可观测、多智能体、连续状态空间的环境里做序贯决策:当前这一刻选左变道还是保持车道,会影响下一秒周围车辆的反应,而周围车辆的反应又反过来影响我下一步能选什么。这跟下棋很像——你落一子,对手会应一手,你要考虑的是若干步之后的局面,而不是眼前这一步的即时收益。MCTS 正是为这类"带对手、带未来推演"的决策问题设计的。
那为什么不用传统的博弈树搜索?因为博弈树的规模会指数爆炸。城市道路上一个四岔路口,假设每个决策周期有 6 个候选动作(加速、减速、左转、右转、保持、微调),周围有 4 辆关键交互车,每辆车也有类似的动作空间,那么往前推 5 步,节点数就是 (6×6^4)^5 这个量级,天文数字,穷举根本不可能。MCTS 的聪明之处在于:它不追求穷举,而是把有限的计算预算有偏地花在"看起来更有希望"的分支上,用统计的方式逼近最优决策。
这里有个很多人一开始会误解的点:MCTS 不是"随机乱试"。它的随机只是手段,核心是用随机模拟的结果去反向更新每个节点的价值估计,再根据价值估计决定下一次往哪个分支多花时间。这个"选择—扩展—模拟—回传"的循环,才是它的灵魂。我在项目里踩的第一个坑,就是早期版本只做了随机 rollout 却没有正确的回传和选择策略,结果搜索出来的动作跟随机选差不多,白白浪费算力。
从工程角度看,MCTS 在自动驾驶里最适合的场景是交互密集、规则难以穷举、但又需要可解释决策的地方,比如无保护左转、匝道汇入、环岛通行、窄路会车。这些场景里,纯规则容易写出一堆 if-else 还覆盖不全,纯学习的方法又缺乏可解释性和安全兜底。MCTS 恰好卡在中间:它的搜索树本身就是一份"决策依据",你能看到它为什么倾向某个动作,这对功能安全论证非常友好。
提示:如果你的场景里交互车辆很少、规则非常明确(比如封闭园区直线巡航),别硬上 MCTS,规则状态机或采样规划器更省事、更可控。MCTS 的价值在"交互复杂"这四个字上。
2. 把 MCTS 的四步循环翻译成驾驶决策语言
2.1 选择:从根节点往下走,走哪条路有讲究
选择阶段要做的事,是从当前搜索树的根节点出发,一路往下选子节点,直到到达一个"还没完全展开"的节点。选哪个子节点,靠的是UCT(Upper Confidence Bound for Trees)公式:
UCT = Q/N + c * sqrt( ln(N_parent) / N )其中 Q 是该节点的累计价值,N 是该节点被访问的次数,c 是探索常数。这个公式的直觉是:第一项代表"利用"——历史表现好的节点多走;第二项代表"探索"——访问次数少的节点给点机会。c 越大越爱探索,越小越保守。
在驾驶场景里,这个公式的每个量都要重新定义。Q 不能是简单的胜负分,而应该是多目标加权:安全性、通行效率、舒适性、合规性。我一般把安全性做成硬约束(碰撞直接给极大负值),效率和舒适性做成软加权。N 的计数也要注意,同一个动作在不同交通状态下被访问,价值可能完全不同,所以状态表征必须足够细,否则会把"直行畅通时的加速"和"前车急刹时的加速"混为一谈。
c 的取值我实测下来,在驾驶决策里比围棋那种纯博弈场景要小一些,通常 0.5 到 1.2 之间。原因很简单:驾驶动作的后果差异没有围棋那么剧烈,过大的探索会导致决策抖动,车开起来一顿一顿的。这个参数没有理论最优值,必须结合你的动作空间粒度和决策周期来调。
2.2 扩展:什么时候该长出新叶子
扩展阶段是在选中的节点上,把还没试过的动作加进树里。这里有个关键设计:动作空间怎么离散化。连续的方向盘转角和加速度不能直接喂给 MCTS,必须离散成有限个候选动作。
我常用的做法是分层离散:纵向用 {急减速, 减速, 保持, 加速, 急加速} 五档,横向用 {左变道, 左微调, 保持, 右微调, 右变道} 五档,组合起来 25 个候选。听起来不多,但配合周围车辆的动作组合,树还是会很快膨胀。所以实际工程里我会做动作剪枝:根据当前车道线、障碍物、交通规则,先把物理上不可行或违规的动作砍掉,通常能砍掉一半以上。
扩展的时机也很重要。不是每个节点一访问就立刻展开所有子节点,而是访问到一次才展开一个。这样树是渐进生长的,计算预算不会被浪费在明显没希望的分支上。我见过有同学一上来就把所有子节点全建出来,结果内存爆了,搜索深度还上不去。
2.3 模拟:rollout 策略决定了搜索质量的上限
模拟阶段,也叫 rollout,是从新扩展的节点开始,用某个默认策略一路走到终止条件,得到一个结果。这一步是 MCTS 里最"玄学"也最影响效果的部分。
最朴素的 rollout 是纯随机选动作,但在驾驶场景里这基本不可用——随机选动作大概率立刻撞车,模拟出来的价值全是垃圾。我的做法是用一个轻量级启发式策略做 rollout:比如"保持当前车道、跟车距离维持在安全阈值、遇到前车减速就减速"。这个策略不需要很聪明,但必须保证模拟出来的轨迹是物理可行、基本安全的,否则价值估计就没有参考意义。
rollout 的深度也要控制。理论上要模拟到 episode 结束,但驾驶场景没有明确的"结束",所以我会设一个滚动时域,比如往前模拟 3 到 5 秒,或者模拟到某个关键事件发生(比如完成变道、通过路口)。模拟步长通常跟决策周期对齐,0.1 到 0.2 秒一步。
这里有个经验:rollout 策略越强,需要的模拟次数越少。早期我用纯随机,要跑几千次才收敛;换成启发式策略后,几百次就能得到稳定决策。算力紧张的车载平台,这个优化非常值。
2.4 回传:价值怎么一路传回根节点
模拟得到一个结果后,要沿着刚才走过的路径,把结果反向更新到每个节点的 Q 和 N 上。这一步看似简单,但有个细节容易错:价值的方向。如果 Q 是从"自车视角"定义的,那么对手节点的价值应该是自车价值的反面(零和假设),还是各自独立(非零和)?
驾驶场景其实是混合动机的:有些车会让行,有些车会抢道,不是纯粹的零和博弈。我一般不做零和假设,而是给每个智能体维护独立的价值,自车节点更新自车价值,对手节点更新对手价值。这样虽然计算量翻倍,但决策更符合真实交通。
回传还有一个工程细节:折扣因子。越远的未来,价值折扣越多。驾驶里我通常取 0.95 到 0.99,取决于决策周期。周期短(0.1 秒)可以取大一点,周期长(0.5 秒)取小一点,避免过度看重远期不确定的收益。
3. 状态表征与动作空间:决定成败的隐形地基
3.1 状态里到底该放什么
MCTS 的搜索质量,很大程度上取决于状态表征是否抓住了决策的关键信息。放少了,树会把不同情况混为一谈;放多了,状态空间爆炸,搜索效率骤降。
我在项目里总结了一套最小充分状态集,经过多轮迭代后稳定下来大概是这些维度:
| 维度类别 | 具体内容 | 说明 |
|---|---|---|
| 自车状态 | 位置、速度、加速度、航向角、所在车道 | 基础运动学,必须有 |
| 周围车辆 | 相对位置、相对速度、相对加速度、车道归属 | 只保留一定范围内的关键交互车 |
| 道路结构 | 车道线曲率、路口拓扑、限速、停止线 | 决定动作可行性 |
| 交互历史 | 最近若干帧的相对运动趋势 | 判断对方意图的隐含线索 |
| 任务信息 | 目标车道、导航意图、剩余距离 | 决定长期目标 |
关键交互车的筛选是个技术活。全放进来不现实,我一般用影响域筛选:只保留那些在未来 3 秒内可能与自车产生冲突的车辆。判断方法很简单,做一次匀速外推,看轨迹是否相交或距离小于阈值。这样通常能把十几辆车筛到 3 到 5 辆,搜索规模立刻可控。
3.2 动作离散化的粒度怎么定
动作空间太粗,决策不精细,车开起来一顿一顿;太细,搜索树膨胀,算力扛不住。我试过几种粒度,最后落在纵向 5 档、横向 5 档这个配置上,理由是:
- 纵向加速度范围通常取 [-4, 2] m/s²,5 档对应约 1.5 m/s² 的间隔,这个间隔下乘客基本感觉不到档位切换的突兀。
- 横向用车道级加微调,变道是离散事件,微调用于车道内避让,粒度够用。
如果场景需要更精细的控制(比如窄路避障),我会在 MCTS 输出粗动作后,再接一层局部轨迹优化,把粗动作平滑成连续轨迹。这样 MCTS 负责"决策选哪条路",优化器负责"这条路怎么走得舒服",分工明确。
3.3 状态聚合与泛化
纯 MCTS 有个天然缺陷:它不泛化。同一个决策问题,换个绝对位置就得重新搜。解决办法是状态相对化:所有坐标都转成以自车为原点的相对坐标,所有速度都转成相对速度。这样不同地点、不同绝对速度下的相似场景,能共享搜索经验。
更进一步,我会做一个状态哈希表,把相似状态映射到同一个节点。哈希的粒度要小心,太粗会把不同场景混一起,太细又起不到聚合作用。我的经验是用关键特征的离散桶做哈希键,比如相对距离分 10 档、相对速度分 10 档、车道关系分几类,组合起来做键。
4. 并行化与实时性:车载平台上的算力博弈
4.1 为什么单线程 MCTS 在车上跑不动
MCTS 是** anytime 算法**,给的时间越多,决策越好。但车载平台的算力是有限的,决策周期通常要求 100 毫秒以内出结果。单线程跑 MCTS,几百次模拟就要几十毫秒,稍微复杂点的场景根本来不及。
我最早的单线程版本,在无保护左转场景下,100 毫秒只能跑 200 次左右的模拟,决策质量勉强能用,但遇到多车交互就明显不够。后来做了并行化,同样时间能跑到 1500 次以上,决策稳定性和通行效率都有肉眼可见的提升。
4.2 根并行、树并行与叶子并行怎么选
并行 MCTS 有三条主流路线,我逐个试过:
根并行是最简单的:每个线程独立跑一棵完整的树,最后投票或加权融合。优点是实现简单、无锁竞争;缺点是内存占用翻倍,且各线程的搜索不共享,效率有浪费。适合线程数少(4 到 8)的场景。
树并行是共享一棵树,多线程同时访问不同节点。效率最高,但需要处理节点访问的同步问题。我用的是原子操作加虚拟损失的方案:一个线程选中某节点时,先给它加一个虚拟损失,让其他线程暂时避开,模拟完再撤销。这样避免了锁,竞争开销小。
叶子并行是把 rollout 阶段并行化,因为 rollout 通常是最耗时的部分。适合 rollout 策略复杂、单次模拟成本高的场景。
我最终的选择是树并行加叶子并行混合:树结构共享,rollout 批量并行。实测在 8 核平台上,加速比能到 5 倍以上,比纯根并行高不少。
4.3 时间预算怎么分配
MCTS 的 anytime 特性意味着你必须设一个停止条件。常见的有三种:固定模拟次数、固定时间、以及基于收敛判断的自适应停止。
固定模拟次数的问题是不适应场景复杂度:简单场景浪费算力,复杂场景不够用。固定时间更实用,但要注意留出安全余量,因为搜索完之后还有轨迹生成、安全检查等环节。我一般把决策周期的 60% 到 70% 留给 MCTS,剩下的留给后处理。
自适应停止是我后来加上的优化:监控根节点最优动作的价值方差,方差小于阈值就提前停。这样简单场景能省下大量算力,复杂场景则跑满预算。
注意:并行 MCTS 的随机性会导致决策抖动——同一场景两次搜索可能给出不同动作。解决办法是加决策平滑:新决策与上一周期决策差异过大时,做一次一致性检查,除非有充分理由(比如突发障碍),否则倾向保持。这个在实车上非常关键,否则乘客会觉得车"犹豫不决"。
5. 奖励函数设计:安全、效率、舒适怎么权衡
5.1 安全性必须是硬约束而非软奖励
奖励函数是 MCTS 的指挥棒,设计不好,搜索出来的动作再"优"也是危险的。我的第一条原则是:安全性用硬约束表达,不用软奖励。
什么意思?如果碰撞只是给一个很大的负奖励,MCTS 在探索时仍可能"试探"碰撞路径,因为它的价值估计是统计的,偶尔的碰撞可能被平均掉。正确做法是在动作生成阶段就把会导致碰撞的动作直接剔除,让搜索空间里根本不存在碰撞选项。这样 MCTS 只需要在安全动作里优化效率和舒适。
具体实现上,我会对每个候选动作做一次快速前向安全检查:用简化的运动学模型外推 1 到 2 秒,看是否与障碍物包围盒相交。相交的直接剪掉。这一步计算量很小,但能极大提升搜索的安全性。
5.2 效率与舒适的加权艺术
安全动作确定后,剩下的就是效率和舒适的权衡。我的奖励函数大致长这样:
R = w1 * 进度奖励 + w2 * 速度奖励 - w3 * 加速度惩罚 - w4 * jerk 惩罚 - w5 * 偏离目标惩罚进度奖励鼓励朝目标前进,速度奖励鼓励保持合理车速,加速度和 jerk 惩罚保证舒适,偏离目标惩罚防止绕路。权重 w1 到 w5 需要根据场景调,没有万能值。
我调参的经验是:先保证安全,再调效率,最后调舒适。因为舒适权重调大了,车会变得过于保守,通行效率下降;效率权重调大了,车会激进,乘客不适。通常效率权重是舒适权重的 2 到 3 倍比较平衡。
5.3 不同驾驶风格的参数化
同一套算法,不同用户可能想要不同风格。我做了风格参数化:把权重暴露成几个高层参数(激进/均衡/保守),底层自动映射到具体权重。这样一套代码能适配多种需求,也方便做 A/B 测试。
风格切换时要注意平滑过渡,不能这一秒激进下一秒保守,否则决策会跳变。我的做法是风格参数本身做低通滤波,缓慢变化。
6. 实车调试中那些文档不会写的坑
6.1 搜索树规模失控与内存管理
实车跑久了,搜索树会越来越大,内存占用持续上升。原因是每帧都在扩展新节点,旧节点没及时释放。我踩过这个坑,跑半小时后内存告警。
解决办法是每帧重建树,而不是跨帧复用。虽然损失了一些跨帧经验,但内存可控,实现也简单。如果确实想复用,必须做节点淘汰:超过一定深度或一定时间没被访问的节点,定期清理。
6.2 决策抖动与一致性处理
前面提过抖动问题,这里展开说。抖动的根源是 MCTS 的随机性加上并行竞争。表现是车在两条车道间犹豫,或者速度忽高忽低。
我的处理分三层:第一层是增加模拟次数,次数越多,价值估计越稳定;第二层是决策平滑,新决策与旧决策做一致性检查;第三层是迟滞机制,动作切换需要连续若干周期都倾向新动作才真正执行。三层叠加后,抖动基本消除。
6.3 对手意图建模的简化陷阱
MCTS 假设对手会按某个策略行动,但真实交通里对手意图是未知的。我早期用"对手保持当前运动"这个最简假设,结果在对手突然变道时决策失效。
后来改成多意图假设:给每个对手维护几种可能的意图(保持、加速、减速、变道),在搜索时对每种意图分别模拟,最后按概率加权。这样虽然计算量增加,但鲁棒性大幅提升。意图概率可以用简单的贝叶斯更新,根据对手历史运动推断。
6.4 与下游规划控制的接口对齐
MCTS 输出的是离散动作,下游规划控制需要的是连续轨迹。这个接口如果没对齐,会出现"决策说变道、规划说不行"的矛盾。
我的做法是让 MCTS 输出带置信度的动作序列,规划器根据置信度决定是否执行。置信度低时,规划器可以降级到保守策略。同时,规划器反馈的可行性信息要回传给 MCTS,作为动作剪枝的依据,形成闭环。
7. 从仿真到实车的验证路径
7.1 仿真环境里要重点验证什么
仿真阶段不能只跑通就完事,要重点验证几件事:决策一致性(相似场景决策是否稳定)、边界处理(极端场景是否安全)、算力占用(最坏情况下是否超时)。
我一般会构造一批对抗性场景:前车急刹、旁车强行并线、路口突然窜出障碍物等,看 MCTS 的应对。这些场景在仿真里可以反复跑,统计成功率。
7.2 实车上的降级与兜底
实车必须有兜底。MCTS 超时、异常、或决策置信度过低时,要能无缝降级到规则策略或安全停车。这个降级逻辑要独立于 MCTS,不能依赖它的输出。
我还会加一个安全监控器,独立检查 MCTS 的决策是否违反安全约束,违反就拦截。这个监控器逻辑要极简,保证自身不出错。
7.3 数据回灌与持续迭代
实车跑出来的数据要回灌到仿真里,复现问题场景,迭代算法。我建了一套场景库,把实车遇到的困难场景提取出来,作为回归测试用例。每次算法更新,都跑一遍场景库,确保不退化。
这套流程跑下来,MCTS 的决策成功率从最初的七成多提升到九成以上,剩下的主要是感知不确定性导致的,不是决策本身的问题。
8. 一些个人体会
MCTS 在自动驾驶里不是银弹,它的价值在交互密集场景,在规则难以穷举的地方。用对了场景,它能给出可解释、可论证的决策;用错了场景,它就是白白烧算力。
我最大的体会是:MCTS 的效果,七分靠状态表征和奖励设计,三分靠搜索本身。很多人一上来就调 c 参数、调模拟次数,其实方向错了。状态里少了一个关键维度,或者奖励函数权重失衡,再怎么调搜索参数也救不回来。
另一个体会是并行化和工程优化的重要性不亚于算法本身。同样的算法,工程做得好,实时性达标,就能上车;工程做得差,算法再漂亮也只能停在仿真里。
最后分享一个小技巧:调试 MCTS 时,把搜索树可视化出来,看它实际访问了哪些分支、价值怎么分布。很多时候问题一眼就能看出来——比如某个明显该探索的分支访问次数是零,那多半是动作剪枝剪错了,或者状态哈希把不同情况混了。可视化比看日志高效得多。