☰
蒙特卡洛搜索树在自动驾驶决策规划中的工程实践与调参指南
2026/10/17 14:23:57 网站建设 项目流程

1. 为什么在自动驾驶里聊蒙特卡洛搜索树

第一次在自动驾驶规划模块的评审会上听到有人提蒙特卡洛搜索树(Monte Carlo Tree Search,MCTS),我下意识觉得这玩意儿不是下围棋用的吗,怎么跑到车上了。后来真正把一段城区无保护左转的决策逻辑啃下来,才发现这个疑问本身就问错了方向——MCTS从来不是某个特定领域的专属工具,它是一套在巨大且不确定的搜索空间里做序贯决策的通用框架。自动驾驶的决策规划,本质上就是这样一个问题:状态空间连续且维度极高,其他交通参与者的意图不可完全观测,动作一旦执行就不可撤销,还得在几十到几百毫秒内给出结果。这几个约束叠在一起,恰好落在MCTS最擅长也最头疼的区间里。

这篇东西我想聊的不是教科书上那套UCB公式推导,而是把它放回自动驾驶决策规划的真实语境里:它到底解决什么问题、和传统的有限状态机或者采样规划器比优势在哪、工程落地时哪些参数是命门、哪些坑我踩过。适合已经了解基础规划概念、想深入决策层算法的同学,也适合刚接触自动驾驶、想搞明白"决策"和"规划"到底怎么分工的入门者。我会尽量把每个选择背后的"为什么"讲透,而不是甩一堆公式让你自己悟。

需要先明确一个前提:MCTS在自动驾驶里通常不直接输出油门刹车方向盘,它更多出现在行为决策层或者粗粒度轨迹选择层,负责在几个候选行为(跟车、换道、让行、抢行)之间做带前瞻的权衡,把选出来的高层意图交给下游的优化器或控制器去细化。理解这个定位,后面很多设计取舍就顺了。

2. 蒙特卡洛搜索树到底在搜什么

2.1 从博弈树到行车决策树的思维迁移

传统博弈树搜索(比如Minimax)要求把整棵树展开到终局,然后自底向上回传价值。围棋的状态空间是10的170次方量级,象棋也有10的40次方以上,全展开根本不可能。MCTS的思路转变在于:我不求全局最优,我用随机采样的方式,把计算预算集中投到看起来最有希望的分支上。它通过反复执行"选择-扩展-模拟-回传"四个步骤,逐步把一棵不对称的树建起来,树越深的地方说明那条路径越值得深挖。

迁移到自动驾驶,这棵树的节点就是"某个时刻的自车状态+环境状态",边就是"自车采取的一个动作或行为"。比如根节点是"当前我在路口等待左转,对向有直行车流",它的子节点可能是"我加速抢在头车之前通过""我等这辆车过去再走""我缓慢蠕行试探对方意图"。每个子节点继续往下展开,就形成了一棵描述未来若干秒可能演化路径的树。MCTS要做的,是在有限的计算时间内,判断哪条根分支的期望回报最高。

这里有个关键认知:MCTS输出的不是一条精确轨迹,而是一个动作分布的统计估计。它告诉你"在当前局面下,抢行这条分支被访问了800次,平均回报0.3;让行被访问了1200次,平均回报0.7",于是你选让行。这种"用访问次数和平均价值共同决定"的机制,天然带有探索与利用的平衡,比单纯贪心要稳。

2.2 四个核心步骤在行车场景里的具体含义

选择(Selection):从根节点出发,每次选一个子节点往下走,直到遇到一个还没完全展开的节点。选哪个子节点的依据是UCT(Upper Confidence Bound for Trees)公式,它把节点的平均回报和探索奖励加起来。探索奖励那一项和"父节点访问次数/该子节点访问次数"的比值有关,访问得少的节点会被"补偿"一个较大的探索值,从而有机会被重新光顾。在行车场景里,这意味着即使某个行为当前看起来不好(比如让行导致通行效率低),只要它被访问得少,算法仍会给它机会去验证,避免过早陷入局部最优。

扩展(Expansion):到达一个未完全展开的节点后,给它添加一个或多个新的子节点。自动驾驶里这一步通常不是随机加,而是从预定义的行为原语集合里挑,或者从采样器里采一批候选动作。这里就有工程取舍了:动作空间设计得太粗(只有加速/减速/保持),树会很浅、决策粒度不够;设计得太细(连续加速度值),分支因子爆炸,计算扛不住。

模拟(Simulation/Rollout):从新节点开始,用一个相对简单的策略(比如固定跟车模型、恒定速度假设)快速推演到某个终止条件(到达规划时域末端、发生碰撞、到达目标)。这一步是MCTS的灵魂,也是最容易被误解的地方。很多人以为模拟必须很精确,其实恰恰相反——模拟要的是快和相对无偏,用大量粗糙但独立的采样来逼近期望。如果每个rollout都跑一遍完整的动力学优化,那计算量直接爆炸,MCTS的采样优势就没了。

回传(Backpropagation):把模拟得到的回报沿着刚才走过的路径反向更新每个节点的访问次数和累计价值。注意,回报的定义直接决定了算法行为。碰撞给大负值、到达目标给正值、通行效率给小的正奖励、舒适性给惩罚项,这些权重的相对大小会显著改变决策风格。

2.3 为什么它适合自动驾驶的不确定性场景

自动驾驶最难受的地方在于其他交通参与者是"活的"。一个行人站在路边,他可能马上过马路,也可能只是等人。传统确定性规划器要么假设最坏情况(永远假设他会冲出来,导致过度保守、频繁急刹),要么假设最可能情况(赌他不出来,风险高)。MCTS的随机模拟天然能表达这种多模态:在模拟阶段,我可以让行人以一定概率过马路、一定概率不动,跑很多次rollout,最后统计出来的期望回报就隐含了对这种不确定性的加权。

再一个优势是随时可中断。MCTS是 anytime 算法,给它10毫秒它能给个粗糙答案,给它100毫秒它能给个更靠谱的答案。这对车载实时系统太重要了——规划周期是硬约束,算法必须在预算内出结果,而不是"算完为止"。相比之下,很多基于优化的方法在复杂场景下可能迭代不收敛,你得额外加超时兜底逻辑。

3. 和传统决策方法硬碰硬对比

3.1 有限状态机:简单可靠但组合爆炸

绝大多数量产车的决策层用的是有限状态机(FSM)或者层次状态机。它的好处是可解释、可验证、行为确定,出了问题能精确定位到哪条转移边。比如"跟车状态下,如果前车距离大于阈值且相邻车道空闲,转移到换道状态"。这套逻辑在高速巡航这种场景里非常成熟。

但它的软肋在城区复杂交互里暴露得很彻底。状态和转移条件是人手写的,场景一多,状态数呈组合爆炸。左转要处理对向直行、对向左转、行人、非机动车、路口内滞留车辆,每个因素都要加状态和条件,最后维护成本极高,而且状态之间的优先级冲突很难调和。更致命的是,FSM没有前瞻——它只看当前满足哪个转移条件,不会去评估"我如果现在抢行,三秒后会不会被卡在路口中间"。

MCTS在这里的补位价值就是用搜索代替手写规则,把前瞻评估交给模拟。你不需要穷举所有交互组合,只需要定义好动作原语和回报函数,让搜索自己去发现"抢行三秒后会被堵"这件事。

3.2 采样规划器:会采样但不会权衡意图

采样类规划器(比如RRT、Lattice Planner)在轨迹层很能打,它们撒一堆候选轨迹,用代价函数打分选最优。但它们的短板在于缺乏对其他智能体反应的建模。一条轨迹的代价通常只考虑静态障碍和自车动力学,对"我这么走,旁边那辆车会不会加速挤我"这类博弈性交互,基本无能为力。

MCTS把"对方会反应"这件事显式地放进了树结构里。虽然标准MCTS是单智能体的,但通过把其他车辆的行为也作为环境随机性建模(在rollout里让它们按某种策略动作),就能近似出交互效果。更进阶的做法是把它扩展成多智能体博弈树,每个智能体轮流做决策,这就接近了博弈论里的纳什均衡求解思路。

3.3 各类方法横向对比

维度有限状态机采样规划器蒙特卡洛搜索树
前瞻能力无,只看当前条件有限,看单条轨迹代价强,多步模拟
交互建模手写规则,难扩展弱,通常忽略中到强,可建模反应
不确定性处理靠保守阈值靠代价加权天然支持概率采样
可解释性极高中中,靠访问统计
实时性极高中到高取决于预算,可调
调参难度低但规则维护累中高,回报函数敏感
复杂场景表现差中好

这张表不是要证明MCTS全面碾压,而是说明它补的是前瞻和交互这两块短板。实际工程里更常见的是混合架构:FSM管高层模式切换和兜底,MCTS在关键博弈场景(无保护左转、匝道汇入、环岛)里做精细决策,采样规划器负责轨迹细化。

4. 工程落地的核心细节与参数命门

4.1 动作空间设计:粗一点还是细一点

这是第一个要拍板的事。动作空间太粗,比如只有{加速,减速,保持}三档,树的分支因子是3,深度10就是3的10次方约6万节点,搜索能覆盖,但决策粒度太糙,选出来的"加速"到底加多少?下游还得再猜。动作空间太细,比如加速度离散成-3到3每隔0.5一档,13个动作,深度10就是13的10次方,天文数字,采样根本覆盖不过来。

我的经验是分层设计:高层用粗粒度行为原语(5到8个),每个原语内部再挂一个参数化的低级控制器。比如"跟车"这个原语,内部用IDM模型根据前车距离自动算加速度,MCTS只需要决定"选跟车还是选换道",不需要决定具体加速度值。这样分支因子控制在个位数,深度可以做到8到12步,每步0.5秒,覆盖4到6秒的决策时域,对城区场景基本够用。

注意:动作原语之间要有明确的语义区分,避免两个原语在大多数情况下产生几乎相同的轨迹,那等于白白浪费分支预算。设计完可以用离线数据统计一下各原语被选中的频率,如果某个原语几乎从不被选,要么是它没用,要么是回报函数把它压死了。

4.2 回报函数:决定算法性格的地方

回报函数是MCTS的"价值观",它直接决定车开得激进还是保守。我见过一个团队把碰撞惩罚设成-1000,通行效率奖励设成+1,结果车在路口怂得不敢动,因为任何有碰撞风险的行动期望值都被压到负无穷。后来把碰撞惩罚调到-100,效率奖励调到+5,行为就正常多了。

一个可用的回报函数通常包含这几项:

  • 安全性:碰撞、接近碰撞(TTC低于阈值)、违反交规,给大负值
  • 通行效率:向目标前进的距离、通过路口耗时,给正值
  • 舒适性:加速度、 jerk(加加速度)、曲率变化,给惩罚
  • 合法性:压线、闯红灯、超速,给惩罚
  • 进度奖励:到达目标区域给大正值

权重量级要拉开,安全项必须比效率项高至少一个数量级,否则算法会为了省时间而冒险。但也不能高太多,否则过度保守。这个平衡没有理论最优解,只能靠场景测试调。

4.3 UCT探索常数:探索与利用的旋钮

UCT公式里的探索常数c,理论推荐值是√2≈1.414(针对奖励归一化到[0,1]的情况)。但实际调的时候,这个值对行为影响很大。c越大,算法越倾向于尝试访问少的节点,行为越"好奇"、越多样;c越小,越倾向于利用当前已知的好分支,行为越"固执"。

在行车场景里,我倾向于把c设得比理论值略小,大概0.7到1.0之间。原因是行车决策的试错成本太高,过度探索意味着频繁尝试奇怪的行为,乘客体验差。但也不能太小,太小就退化成贪心,遇到局部最优出不来。这个值最好结合具体场景做网格搜索,用离线回放数据评估不同c值下的通行效率和舒适性指标。

4.4 模拟策略:快而不过度简化

Rollout策略的选择是个技术活。最粗暴的是随机策略,但随机在连续动作空间里几乎必然导致车辆行为荒谬(随机加减速),模拟出来的回报没有参考价值。稍微好点的是固定策略,比如"所有车都保持当前速度",简单但忽略了交互。

比较实用的折中方案是用简化的IDM跟车模型+恒定速度假设做rollout。IDM计算量极小,能捕捉基本的跟车和避撞行为,比随机策略靠谱得多,又比完整优化快几个数量级。另一个技巧是限制rollout深度,不一定要模拟到规划时域末端,模拟2到3秒往往就能区分出好坏分支,再深边际收益递减。

实操心得:rollout的随机性来源要设计好。如果所有rollout都用同一个确定性策略,那模拟结果就没有方差,MCTS退化成确定性搜索,失去处理不确定性的能力。正确做法是在rollout里对关键不确定量(其他车意图、行人行为)做随机采样,让每次模拟走不同的分支。

5. 完整实操流程与关键环节实现

5.1 环境建模与状态表示

先把状态定义清楚。一个可用的状态向量通常包括:自车位置、速度、加速度、航向角;周围车辆的位置、速度、航向;车道线信息;交通灯状态;目标点。维度轻松上几十维。MCTS不需要把整个状态向量塞进节点,节点只需要存决策相关的关键量,比如自车在哪个车道、相对目标点的进度、与最近交互对象的相对关系。

状态离散化是个绕不开的问题。连续状态没法直接建树,得做聚合。常见做法是按"语义区间"离散:距离前车分{近,中,远},相对速度分{接近,保持,远离},组合起来形成有限的状态标签。粒度太细树爆炸,太粗决策失真。我的经验是每个维度分3到5档,总状态数控制在几百到几千量级。

5.2 树的初始化与迭代控制

根节点就是当前状态。每次规划周期开始时,可以复用上一周期的树——把上一周期选中的分支对应的子树提升为新的根,其余丢弃。这叫树复用,能显著提升搜索效率,因为相邻周期的场景高度相关。实测下来,树复用能让有效搜索深度提升30%以上。

迭代次数由时间预算决定。典型车载计算平台,给决策层50到100毫秒,单次rollout如果控制在0.1到0.5毫秒,那能跑100到1000次迭代。迭代次数不是越多越好,超过一定次数后边际收益递减,而且可能因为过度搜索而"想太多",反而错过最佳决策时机。

# MCTS主循环伪代码(简化版) def mcts_search(root_state, time_budget_ms): root = Node(root_state) deadline = now() + time_budget_ms while now() < deadline: node = select(root) # UCT选择 if not node.is_terminal(): node = expand(node) # 扩展一个子节点 reward = rollout(node) # 随机模拟 backpropagate(node, reward) # 回传更新 return best_child(root) # 返回访问次数最多的子节点

5.3 回报计算与安全兜底

回报计算要在rollout结束时进行。碰撞检测用简单的包围盒或圆形近似,别用精确多边形,太慢。TTC(碰撞时间)计算用相对距离除以相对速度,低于阈值就给惩罚。到达目标用距离阈值判断。

安全兜底是必须的。MCTS再聪明也可能因为搜索不充分而选出危险动作,所以下游必须有一层独立的安全校验:选出来的动作如果会导致TTC低于硬阈值,或者违反交规,直接否决,退回到保守动作(比如最大减速度刹车)。这层校验不能依赖MCTS自己的回报函数,必须是独立的、确定性的规则,这样才能保证底线安全。

5.4 参数选择过程实录

拿一个无保护左转场景举例。规划时域设5秒,每步0.5秒,深度10。动作原语设6个:加速通过、匀速通过、减速让行、停车等待、蠕行试探、退回等待区。探索常数c从0.5到1.5做网格搜索,步长0.25。rollout策略用IDM,其他车辆意图按70%直行、20%减速、10%让行的概率采样。

测试下来,c=0.75时通行效率和舒适性的综合得分最高。c=0.5时过于保守,经常无谓等待;c=1.5时过于激进,有几次模拟出抢行导致冲突。迭代次数从100增加到500,决策质量提升明显;从500增加到1000,提升很小但耗时翻倍,所以定在500次,配合树复用,实际单周期耗时约60毫秒,满足100毫秒预算。

6. 常见问题与排查技巧实录

6.1 决策抖动:相邻周期选不同行为

这是最常被抱怨的问题。车在路口一会儿想走一会儿想等,行为不连贯。根因通常是搜索的随机性导致每次结果略有不同,加上场景本身处于决策边界。解决办法有几个:一是树复用,让上一周期的搜索成果影响本周期的先验;二是加行为迟滞,如果上一周期选了让行,本周期让行的初始价值给个小加成,除非有强证据表明该换;三是在回报函数里加行为切换惩罚,频繁切换行为本身扣分。

6.2 计算超时:复杂场景迭代不够

场景一复杂,分支因子和rollout耗时都上升,固定迭代次数可能跑不完时间预算。对策是动态调整迭代次数:设一个最小迭代数保证基本质量,然后看剩余时间继续跑,时间到就停。另外rollout要做早停,一旦检测到碰撞或明显失败就立即返回,不用跑完整个时域。

6.3 回报函数调参困难

回报函数权重多,互相耦合,调一个影响一片。我的做法是分层调:先只开安全项,确保车不撞;再加效率项,调到通行合理;最后加舒适项,微调平顺性。每次只动一个维度,用固定的测试场景集评估,别凭感觉。

6.4 常见问题速查表

现象可能原因排查方向解决手段
决策抖动搜索随机性+边界场景看相邻周期选中分支树复用+迟滞+切换惩罚
过度保守碰撞惩罚过大检查回报权重降低安全项量级
过度激进效率奖励过大检查回报权重提高安全项量级
计算超时分支因子大/rollout慢profile各阶段耗时动态迭代+rollout早停
行为单一探索常数太小看访问分布增大c值
模拟失真rollout策略太简对比模拟与真实换IDM+限制深度

避坑技巧:别在真车上直接调MCTS参数,先用离线回放数据做批量评估。把历史场景数据喂进去,统计不同参数下的碰撞率、通行时间、jerk均值,用数据说话。真车测试只用来验证最终选定的参数组合。

7. 我对这套方法的一些真实体会

MCTS在自动驾驶决策里不是银弹,它的计算开销和调参复杂度决定了它更适合作为关键场景的增强模块,而不是全场景的主力。高速巡航用FSM就够了,没必要上MCTS徒增算力负担。但在城区无保护左转、环岛、匝道汇入这些博弈性强、规则难穷举的场景,MCTS的前瞻和交互建模能力确实能带来质变。

我踩过最大的坑是早期把回报函数设计得太复杂,十几项加权,结果调参调到怀疑人生,而且各项之间互相干扰,根本理不清谁在起作用。后来砍到五项核心指标,反而效果更好。这件事让我明白,MCTS的威力来自搜索,不是来自回报函数的精巧。回报函数只要能把好坏区分开就行,剩下的交给搜索去发现。

另一个体会是树复用的价值被严重低估。很多人每次规划都从零建树,浪费了大量计算。相邻周期的场景相关性极高,复用上一周期的树相当于免费获得了先验知识,实测能省30%到50%的迭代次数达到同等决策质量。这个优化实现起来不难,但收益很实在。

后续如果想继续深入,可以往两个方向走:一是把单智能体MCTS扩展成多智能体博弈搜索,显式建模其他车的策略;二是把rollout策略从固定模型换成学习出来的策略网络,用数据驱动的方式提升模拟的真实性。这两个方向都有不少公开研究可以参考,但工程落地的复杂度也会相应上升,得权衡投入产出。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询