如何设计自动驾驶软件栈

这篇文章总结了我对如何设计自动驾驶汽车软件栈的看法。它侧重于行为方面,因为我已在其他地方分享了我对感知的看法。

如何设计自动驾驶软件栈
AI模型价格对比 | AI工具导航 | ONNX模型库 | Vibe Coding教程 | PLC在线仿真器 | Tripo 3D | Meshy AI | ElevenLabs | KlingAI | ArtSpace | Phot.AI | InVideo

在这篇文章中,我将回顾自动驾驶汽车(AV)软件栈的主要设计问题,并就我认为的最佳选择发表看法。文章的结构是:首先讨论输入,然后是架构、训练策略,最后是基础设施方面的考虑,如数据挖掘、仿真的作用以及需要追踪的指标。

1、输入

正如我在关于现代感知的文章中所论述的,虽然完全端到端的软件栈最终可能是理想选择,但在可预见的未来,大多数参与者仍然会将感知栈和规划栈解耦。由于我已经讨论了感知方面的问题,本文将重点放在规划上。规划器有三个主要输入:参与者数据(轨迹)、地图数据和路线信息。

由于我们希望能够以开环和闭环两种方式训练规划器(稍后详述),我们应该以这样的方式组织输入:只需将数据采样器从随机帧采样器改为顺序帧采样器,就可以以快照形式(用于开环训练)或"视频"形式(用于闭环训练)获取数据。在开环训练中,我们从驾驶日志中随机抽取帧,模型根据场景的近期历史预测自我(自动驾驶车辆)未来的位置。因此,我们获取的每个训练样本对应一个特定的帧/时间戳,需要同时包含场景中每个参与者的过去数据窗口和未来数据窗口(我们试图预测的 ground truth)。地图数据将是样本时间戳处自我位置周围的一个半径范围内的数据。路线数据对应于输入到 GPS 的最新任务。

1.1 参与者数据

感知栈负责为规划器提供参与者数据。参与者被表示为带时间戳的边界框序列,大致如下所示:

图 1:轨道以一定的时间间隔相交。

过去参与者数据的数据源不必与未来参与者数据的数据源相同。大致有两种选择:感知轨迹(由你的感知栈生成)或标注轨迹(由人类标注员绘制的边界框)。它们可以以任何方式组合,因此一般建议是,为过去和未来参与者数据分别选择不同数据源的能力进行设计。我的建议是使用感知轨迹作为模型输入,使用标注轨迹作为模型目标。当然,当你扩大训练数据规模时,你不能把自己限制在标注场景上,而应精心设计训练课程,混合来自在线感知栈的目标和人工标注的目标。

1.2 地图数据

虽然参与者数据必然是在线生成的,但地图数据可以是在线或离线生成的。离线地图的风险在于它们可能过时,且通常局限于一个狭窄的地理网络。在线地图的风险在于它们可靠性较低,并占用宝贵的车载计算资源。就我个人而言,我会选择在线方案,因为可扩展性胜过大多数其他考虑因素。最终,你会发现你的机器学习系统受限于数据的多样性,而多样性最好通过广泛撒网来实现,而不是在一个狭窄的地理网络中收集大量纵向数据。在线建图系统不在本文讨论范围内,但总体趋势是倾向于矢量地图(参见这篇论文作为整体方法的良好代表)。地图数据因此看起来如下所示:

图 2:包含于当前自我位置周围半径内的地图元素,或与该半径相交的地图元素。

究竟应该包含哪些矢量化地图元素?这里有多种选择,但车道级信息可能已经足够精细了。在线地图系统通常会检测车道边界,然后将其简化为车道中心线,也常被称为“基线路径”。除此之外,我们还需要一些关键的交通信号,例如交通信号灯、停车标志和限速标志。其中一些元素具有动态属性(例如:交通信号灯),这需要专门的感知系统来处理(超出本文讨论范围)。总而言之,我们假设这些地图元素以基线路径图的形式返回。每条基线路径都包含一些属性,例如它是否包含停车标志或交通信号灯(以及交通信号灯的状态)。

1.3 路线信息

除了提供行动者数据的感知模块和提供地图数据的地图模块之外,规划的第三个输入——路线信息——由另一个模块提供。路线本质上就是您输入目的地后谷歌地图生成的路线,它对规划器的轨迹生成至关重要,赋予其任务意义。例如,到达十字路口时,规划器需要知道是优先选择直行路线还是转弯路线。

路线信息可以采用不同程度的复杂方式进行封装。它可以像“右转”、“左转”等带有时间戳的字符串序列一样粗略,也可以像地图数据上的成本叠加层一样精细。成本叠加层通常由基于规则的系统生成,该系统对基准路径的有向图进行一次性反向运行Dijkstra最短路径算法,从对应于最终目的地(或“任务目标”)的基准路径开始。这会将图转换为一棵树,其中每条路径代表到达目的地的最佳路线。节点对应于基准路径段,如果自我从一个路径段移动到另一个路径段是合法的,则这些路径段之间用边连接。节点和边可以根据启发式方法添加信息,例如到达目的地的旅行时间/距离、执行变道的成本等。最终,地图数据上会叠加一个成本信息层,如下所示:

图 3:自我周围的地图数据片段,带有成本信息层。在右下角,您可以看到任务目标。车道使用红色渐变进行颜色编码,从高成本(深红色)到低成本(浅红色)表示到达任务目标的成本。

车道段的成本可以解释为,如果您被放置在该车道段上,到达任务目标的成本。如果我们放大自我周围的区域(图 4),就可以更清楚地了解成本信息层。远离任务目标的车道成本最高,因为在这种情况下,驾驶员必须绕行整个街区才能驶入成本低得多的、朝向任务目标的车道。

图 4:图 3 的放大版本。我们可以更清晰地区分车道级成本。

在粗粒度和细粒度路线信息之间进行选择时,我会选择后者。它为规划器提供了更大的操作空间:规划器可以分析附近所有车道,并利用更精细的信息提供多种选择。它允许规划器权衡各种动态因素:如果成本叠加图考虑了变道成本,我们就可以更好地把握变道时机,并根据变道成本和保持车道成本之间的差异来权衡让行和切入。我们越接近无法变道的临界点,成本差异就越大,规划器就越有可能生成更激进的切入轨迹。更一般地说,更精细的路线信息表示更有利于处理长尾问题,但这会以维护路径规划模块中的启发式算法为代价。此外,它还需要一个离线地图(因为反向 Dijkstra 算法需要超出我们感知范围的地图数据),该地图可以与在线地图连接。

在我上面的描述中,路线信息用于影响轨迹生成,但其他方法则将路线信息作为轨迹生成的硬约束。在前一种方法中,路线信息是模型的输入;在后一种方法中,它是规划器模型输出(轨迹)的过滤器。一般来说,始终优先将信息用作模型输入,而不是作为启发式实现的硬约束。

1.5 自动里程与手动里程

关于输入的这一部分,另一个重要的设计考虑因素是需要收集哪些数据。我之前已经提到过,从地域上来说,你应该尽可能扩大覆盖范围。第二个主要考虑因素是“自动行驶里程”和“手动行驶里程”之间的比例,也就是所谓的自动驾驶里程和人工驾驶里程。自动行驶里程有利于获取接管反馈,这通常对强化学习很有帮助,但无法为模仿学习提供可靠的数据。相比之下,手动行驶里程有利于模仿学习,但我们无法获得关于自动驾驶系统质量的反馈。我的经验法则是,两者的比例应该大致反映模仿学习和强化学习的相对重要性,这一点我们稍后会详细讨论。

2、架构

关于感知部分的架构建议,请参见我的另一篇文章。主要思路是,你现在就选择基于查询(隐式)的追踪,以便为感知栈的未来发展做好准备。这样一来,日后连接感知栈和规划栈就会变得容易得多。目前可以明确地将查询解码为边界框,当感知栈和规划栈都成熟后,跳过查询解码,直接将查询输入规划栈,一路反向传播。

对于规划器的架构,我认为合适的抽象层次是:场景编码器、轨迹解码器和轨迹排序器。这种划分既足够有倾向性以发挥作用,又足够通用以支持创造性。场景编码器负责生成上下文感知的参与者特征。通常,其架构是某种 Transformer,结合了对每个输入的自注意力以及轨迹与其他输入之间的交叉注意力。解码器负责基于这些参与者特征生成轨迹。排序器是一个可选模块,取决于解码器是否是多模态的。多模态解码器意味着它为给定参与者生成多个候选未来轨迹。每个可能的未来是一个"模态"。排序器本质上是一个对模态进行分类的分类器。

图 5:整体架构。

在接下来的几节中,我将详细介绍这些不同组件。

2.1 编码

场景编码器有两种主要类型:向量编码器和栅格编码器,它们对应不同的输入模态。向量编码器通常对由简单几何体(线条、多边形等)表示的输入进行编码,而栅格编码器则对类似图像的输入进行编码。向量编码器处理具有明确身份的输入,因此将该身份映射到某种向量,而栅格编码器将输入表示为场景鸟瞰图中的区块。

向量编码器适用于低基数、高相关性、高度个体化的智能体(这里的"相关性"主要指物理距离和交互可能性)。栅格编码器适用于高基数、低相关性、低个体化的智能体(行人因此是"低相关性"的,因为通常来说,他们不太可能直接与自我交互)。某些智能体的高基数可能是个问题,因为它会显著增加神经网络的延迟。行人也可能难以个体化为清晰的轨迹,因此这些轨迹的质量可能会影响向量表示的有用性。

这是否意味着你应该使用多模态编码策略?例如,对附近的机动车辆和骑自行车的人使用向量编码器,对行人和远离自我的其他智能体使用栅格编码器?这是我的直觉,但文献中对此并不明确。此外,如何将多模态编码融合为单一的场景理解仍然是一个未解决的问题。我们是否需要有一个占用流解码器与我们的轨迹预测解码器并行,还是轨迹预测解码器就足够了?

图 6:两种多模态编码策略。

目前,我建议坚持使用全向量化编码器,并通过仅对每个参与者周围一定半径内的行人进行编码,或仅对每个参与者周围固定数量的行人进行编码,来应对延迟风险。这仍然没有解决轨迹质量问题,但也许这是感知栈的问题。一个简单的缓解措施是将智能体类型嵌入添加到轨迹嵌入中,这应该能让模型学习到某些智能体类型的感知质量较低。

编码器应生成参与者特征,这些特征同时考虑了时间和上下文信息。通常,编码器将是 Transformer 或某种图神经网络。其关键工作是让每个参与者与不同的输入进行交互。这包括参与者与其自身过去的交互,也包括与其他智能体的交互,最后是与地图信息的交互。有很多方法可以构建这些交互。最简单的方法是将时间和上下文信息都放入一个大袋子中,应用自注意力机制,让所有内容相互关注。

图 7:原始输入被嵌入并展平,然后连接起来并通过自注意力机制发送。

我们将原始输入表示为三个张量:(1) 参与者历史(num_actors, num_timestamp, 1, xy),(2) 参与者-参与者特征(num_actors, num_timestamps, num_closest_actors, dxdy),以及 (3) 参与者-车道特征(num_actors, 1, num_closest_lane_segments, dxdy)。这三个输入中的每一个都是一个 4D 张量,其中第一个维度对应场景中的参与者,第二个维度是时间信息,第三个维度是物理上下文信息,第四个维度是原始特征。例如,此图中的参与者历史特征被简化为每个时间戳处参与者的位置。有些输入没有时间信息。例如,参与者-车道特征仅针对当前时间戳计算,因此时间维度是一个虚拟维度。在 Transformer 架构中,自注意力的输入是一个 3D 张量,其中第一个维度是批量大小,第二个维度是"序列"维度,第三个维度是特征。"序列"这个术语只是 Transformer 被发明用于处理文本令牌序列这一事实的产物。但 Transformer 架构远不止于此,因此"序列"必须被理解为只是"可以相互关注的事物"。不一定是序列;自注意力只作用于一堆事物。在原始 Transformer 中,使一堆事物成为"序列"的是位置嵌入。在嵌入步骤中,我们还添加了捕获时间信息的特征。但位置嵌入也可以是空间性的。总而言之,场景编码器大致是一个带有时间或空间嵌入的向量集合,其中所有内容相互关注。

类似的方法可以使用图神经网络代替,但本质上是相同的。需要注意的重要一点是,场景编码器将生成一个 3D 张量,为每个参与者捕获一个"序列"(结合了空间和时间信息的抽象特征)。总的来说,我建议你的场景编码器生成这样的 3D 张量。这将为你的解码器提供灵活性,以决定如何利用这些时空信息。

2.2 解码

解码涉及规划器生成什么,或者粗略地说,它的"动作空间"。这里第一个设计选择是在生成控制命令和生成路径点之间。生成控制命令的好处是它能保证运动学可行性,而生成路径点则没有这样的保证。使用路径点,你将需要一个额外的模块来平滑生成的轨迹并将其转换为控制命令(参见线控驱动)。我认为无论哪种方式都没有太大区别,所以我建议直接使用路径点,因为大多数数据无论如何都是以路径点形式记录的。而且,如果你想在训练中利用非自我参与者的数据,它们当然也只能以路径点形式记录。你可以从它们的路径点反向工程出控制命令,但我不打算费这个事。

第二个设计选择是否要为非自我参与者解码轨迹。归根结底,我们只关心驾驶自我,所以我们实际上不关心为了预测而预测其他参与者的未来。只有在某种程度上有助于规划时,我们才应该这样做。换句话说,预测是规划的辅助任务。从这个意义上说,我建议确实要预测非自我参与者的未来。

解码器的第三个设计选择是使其为单模态还是多模态。单模态解码器为每个参与者生成单个未来(以轨迹形式)。多模态解码器为每个参与者生成多个未来轨迹(称为"模态")。多模态的价值尚不明确,至少对于规划任务而言。对于预测任务,这很有意义:我们不知道别人脑子里在想什么,从我们的角度来看,他们有许多可能的、大致同样有效的未来。但就规划而言,我们确实知道自己脑子里在想什么,因此对可能未来的推理是完全已知的。那么,多模态生成的意义何在?例如,有人可能会认为,由于我们周围的参与者有多个可能的未来,因此我们也有多个有效的未来:每个自我轨迹对应于与周围参与者的某种假设性纠缠。但事实是,我们现在必须做出一个选择。因此,即使你采用多模态,你仍然需要一个额外的模块来选择一个候选未来。这个模块就是排序器(稍后详述)。所以这类似于将人类可读的结构强加到决策过程中:首先生成多个模态,然后选择其中一个。但没有理由认为生成器的内部结构不能自行学习进行这种推理,除非排序器的训练方式不同,或者可以访问不同类型的信息。

现在,让我们假设我们要采用多模态。虽然多模态对自我的用处值得怀疑,但对非自我参与者来说是有意义的。多模态轨迹生成的主要挑战是生成式 AI 中一个普遍问题的特例,即:模态坍塌。当生成器的所有输出看起来都一样时,就会发生模态坍塌,即所有模态都坍缩为单一模态。

图 8:这是一个环岛场景。自我位于原点。绿线代表车道中心线。红线代表过去的轨迹。图中仅显示自我可能的未来轨迹。蓝线代表自我的真实未来轨迹,而粉线代表模态预测。我们注意到所有模态基本相同,这就是所谓的“模态坍缩”。

由于解码器训练损失(未来路径点与 ground truth 路径点之间的均方误差)的性质,朴素的多模态解码器会出现模态坍塌。解决方案是使用锚点。用 Transformer 的行话来说,锚点可以被建模为查询(如注意力机制中的查询、键和值)。正如我在关于感知的文章中所解释的,DETR 架构的主要创新之一是在用于目标检测的注意力机制中使用了查询。该注意力机制的一个关键要素是,查询不仅与图像像素进行交叉注意力,而且彼此之间还进行自注意力。自注意力部分防止了重叠检测。同样的逻辑可以应用于多模态轨迹生成:我们将模态表示为查询,将查询与参与者特征进行交叉注意力,并让它们在彼此之间进行自注意力。这些查询可以是完全抽象且随机初始化的,也可以由数据集中的统计信息初始化:在历史数据中随机时间戳处采样轨迹,对其进行聚类并为每个聚类生成代表性路径点,然后以某种方式嵌入这些路径点以生成锚点。

图 9:以随机抽取的时间戳为中心,对随机抽取的演员(800 个场景)绘制的历史轨迹。
图 10:使用不同简单方法对轨迹进行聚类和概括。行对应于概括方法,列对应于聚类数量。

你还可以将历史锚点路径点和抽象查询结合起来,这与这篇感知论文将锚点框和抽象"实例特征"结合的方式有些类似。

图 11:具体的锚框嵌入到张量 E 中,抽象查询由张量 F 捕获。E 和 F 相加得到注意力机制的查询和键,而 F 则成为值。最终得到一个结合了具体信息和抽象信息的单一查询。来源:Sparse4D v3。

在基于查询的解码之上,你还可以对模仿损失进行一个简单的干预。不采用排序器选择的模态中所有路径点的均方误差(MSE),而是忽略排序器的选择,计算每个模态的 MSE,然后取最小的 MSE。这应该会引导生成器通过生成多样化的模态来分散风险。

还有其他解决模态坍塌的方法,比如假设模态大致映射到某个参与者可以到达的车道。基于这个假设,我们可以强制生成器坚持使用与每个可到达车道的中心线对应的基线路径,从而避免模态坍塌。通过在每个可到达车道的 Frenet 坐标系中回归路径点来避免模态坍塌。

图 12:从道路图中选取可能的前进路径样本,并在这些参考路径的 Frenet 坐标系中回归航路点。来源:基于路径的轨迹预测。

在这种方法中,锚点是对轨迹回归的硬约束。在 Frenet 坐标中,你默认参考路径是完美的,只能预测相对于它的偏差,因此不仅参考路径被认为是黄金标准,我们还有一个强烈的偏见,即假设参与者是"符合地图的"(这对所有参与者来说可能并非如此)。这引入了对高清地图的依赖。总的来说,我建议基于可扩展性的理由反对这种依赖。在上述方法中,历史轨迹被用作模糊锚点,它们不是作为硬约束,而是作为模型输入特征,并且不依赖高清地图。

图 13:轨迹生成中的硬约束与软偏差。前者涉及在参考路径的 Frenet 坐标系中进行回归(因此需要高分辨率),后者涉及使用参考路径作为模型输入(因此不需要高分辨率)。

像往常一样,押注简单且可扩展的解决方案。硬约束可能在短期内有益,但我预计它们会在未来带来麻烦。因此,我避免模态坍塌的建议是采用基于查询的轨迹解码,可选地混合使用历史轨迹锚点与查询。

2.3 迁移学习

将预测作为辅助任务有什么意义?辅助任务对于训练是有用的。在运行时,我们完全丢弃辅助任务。因此,这个辅助任务有两个主要原因。首先,它推动编码器主干网络倾向于对其他参与者有用的表示,即包含对其未来轨迹推测的表示。其次,它使我们能够利用迁移学习。实际上,对于神经网络来说,预测非自我参与者的轨迹与预测自我的轨迹大致等价。因此,学习做一件事有助于你更好地做另一件事。所以迁移学习相当于倍增了你用于规划的培训数据量:你可以通过尝试为其他参与者做规划来学习为自我做规划!

然而,这种方法也存在风险。首先也是最重要的,感知数据的质量与我们为自我拥有的状态估计数据相比相形见绌。在非自我智能体上进行训练的一个更棘手的问题是,这将使我们无法使用仅适用于自我的模型输入特征。特别是,路线信息仅适用于自我,而我们已将其确定为学习规划的关键信号,用于将规划偏向于路线。如果我们将路线信息建模为地图上的到达代价叠加层,那么我们将在编码器级别摄入路线信息。这引入了一个不对称性,因为只有自我才能关注增强后的地图;其他参与者只能关注常规地图。如果编码器不对称地编码场景(即为不同参与者以不同方式编码场景),那么我们就不能有一个对所有参与者通用的解码器,以免解码器混淆是否可以利用不对称特征。如果编码器对称性被破坏,解码器必须分成两个独立的头(一个用于自我,一个用于非自我):

图 14:非对称场景编码器强制执行分离解码器,继续在预测和规划之间进行迁移学习。

这种方法的缺点是我们放弃了自我和非自我参与者之间的迁移学习。为了继续受益于迁移学习,我们必须恢复编码器的对称性。这可以通过两种方式之一完成:(1) 不在编码器级别摄入路线信息,或者 (2) 为非自我参与者伪造路线信息。在选项 1 中,我们可以在解码器中进行"后期拆分",在拆分之后摄入路线信息。这样,我们仍然可以从一些迁移学习中受益。

图 15:对称场景编码器允许后期分割解码器延迟摄取自我路由信息。

在选项 2 中,我们仍然在编码器级别摄入路线信息,但必须为非自我参与者伪造路线信息:

图 16:一个对称的场景编码器,它同时接收真实的自我路径信息和伪造的非自我路径信息,从而实现统一的解码器。由于预测和规划之间的损失可能不同,我们可能仍然需要在解码器中进行后期拆分。

在缺乏针对非自我智能体的路线信息时,我们可以将已实现的路线用作计划路线。换句话说,我们查看未来,假装未来发生的事情就是路线,然后运行相同的反向 Dijkstra 过程,以获得非自我参与者的到达代价叠加层(如果我们选择以这种方式表示路线信息的话)。

请注意,在图 14-16 中,我还指出了自我解码器和非自我解码器之间的一个额外潜在差异,即应用于每个解码器的损失。我将在讨论强化学习时再回到这一点。现在,只需说,虽然场景编码器可以是对称的,从而允许统一的解码器,但如果规划损失与预测损失不同,你仍然需要在解码器中进行某种拆分。如果我们引入强化学习,那么规划损失确实会与预测损失不同。稍后将详细介绍。

2.4 排序

如前所述,多模态解码对预测很有用,但对规划是否有用尚不清楚。它是否对规划有用取决于排序器的性质,排序器是负责选择其中一个模态的模块。

一般来说,如果生成器和排序器可以访问完全相同的信息,我认为排序器是多余的。排序器发挥作用的最佳方式是利用生成器无法访问的信息。例如,可以使用几个独立的生成器,关注略有不同的信息。这在混合系统中会很有用,该系统结合了经典生成器(如模型预测控制方法)和机器学习生成器。但本文主要讨论机器学习规划,因此我将搁置与混合系统相关的设计问题。

但在非混合、纯机器学习的系统中,排序器是否仍有存在的空间?肯定回答的一个理由是,如果排序器可以利用未提供给生成器的信息(尽管在这种情况下,人们可能会考虑也将该信息提供给生成器)。排序器证明有用的另一个潜在方式是,如果排序器独立于生成器进行训练(可能是迭代式的),使其能够真正专注于关注不同的事物,即使生成器和排序器可以访问相同的信息。实际上,强制性的显式结构——"让一个神经网络生成多个轨迹,让另一个神经网络选择其中一个"——可能在迭代训练过程中是有帮助的,就像生成器-鉴别器架构通常被证明有用一样,利用了博弈论动力学。从这个角度来看,迭代训练(反复交替训练两者)可能会比联合训练(同时训练两个模块)产生更好的结果。

或者,我们可以采用一个排序器重型架构,即我们使用一堆"傻瓜生成器"(例如,忽略大部分场景信息的启发式生成器)并完全依赖排序器在大量廉价生成的轨迹中选择正确的轨迹,同时考虑详细的场景信息。这对我来说似乎是一种奇怪的方法,我通常会建议不要这样做,因为廉价生成无论如何廉价,仍然很笨,因此可能仍然达不到目标,让排序器只能在糟糕的备选方案中进行选择。

因此,我的建议是使用一个强大的生成器配合一个排序器,主要是基于有益的博弈论动力学,但我会关注仅使用单模态生成器的架构的性能。排序器应考虑场景以及参与者的预测轨迹。对于场景,我们可以重用编码器生成的场景感知参与者嵌入。我将在后面的部分回到训练过程(迭代 vs 联合训练)。

排序器本质上是对生成的轨迹进行分类的分类器,因此为每个生成的轨迹产生一个分数/逻辑值。一种选择是让生成器将轨迹完全解码为路径点,然后在排序器内部重新编码这些路径点。

图 17:完全解码轨迹并在排序器内部重新编码。

另一种选择是重用法解码器中的多模态参与者特征。实际上,多模态解码器必然从编码器产生的参与者特征中产生多模态参与者特征。

图 18:在排序器中重用解码器的多模态演员特征。

我认为哪种方式都无关紧要。也许在排序器内部重新编码完全解码的轨迹有助于排序器为参与者的未来开发更独立的隐藏表示,但我认为这并不关键。

关于排序器的最后一个问题是它的目标是如何构建的。目标是在生成的轨迹上的一个独热向量(因为它是一个分类问题)。因此,目标构造函数需要有一种方法来判断哪个模态轨迹是"最好的"。这个判断权保留给评判器。评判器不是神经网络架构的一部分,它是排序器目标构造函数的一部分,因此可以利用未来。最直接的评判器是查看预测轨迹的路径点与专家轨迹的路径点之间的欧氏距离。另一个评判器可能偏好表现出最小加加速度的轨迹。另一个可能偏好避免不同参与者预测轨迹之间的碰撞。所有这些评判器被组合成一个排序器目标构造函数,为每个预测轨迹生成一个分数,然后通过 argmax 变成独热向量或通过 softmax 变成概率分布。无论哪种方式,它都会为交叉熵损失生成一个硬目标或软目标概率分布。

将排序器建模为分类器的替代方法是将其建模为逆强化学习问题,其中排序器为每个轨迹发出一个"奖励",我们教导排序器为最接近专家演示的轨迹发出更高的奖励。一种方法是通过负对数似然,其中似然来自排序器发出的奖励。由于尚不清楚负对数似然是否比交叉熵产生更好的结果,我建议基于简单性原则坚持使用交叉熵。

2.5 状态性

另一个设计考虑是隐式记忆和显式记忆之间的选择。规划器可能需要在不同的时间尺度上进行规划,并且可能需要较低分辨率的规划(更长的时间范围)来影响较高分辨率的规划(更短的时间范围)。这与承诺的概念有关:车辆操作员有时需要承诺一个行动路线并表明其意图。例如,交通法规要求车辆操作员提前表明其改变车道的意图。这是一种承诺形式。建模承诺的一种方法是写下意图,然后将该意图反馈给规划器,以影响其战术决策。换句话说,规划器需要某种工作记忆。

一种实现方案是简单地信任规划器的内部结构自动处理这个问题。毕竟,Transformer 可以关注大的上下文窗口,自然允许它们在多个时间尺度上进行推理。Transformer 的内部表示可以被视为一种内部状态,它考虑了来自不同时间尺度的元素(即隐式记忆)。

或者,可以设计一个显式的读写记忆。显式记忆有两种主要方法:可微分的和不可微分的。一般来说,当访问模型使用某种"通用 I/O"时,非可微分记忆访问是可行的。以语言模型为例。它们不需要特殊训练来读取记忆:记忆只是合并到输入模型的主要文本令牌流中的另一个文本令牌流。它们不需要特殊训练来写入记忆:使用工具的上下文学习就足够了。这是可能的,因为它们的 I/O 是通用的:你可以轻松地将两个文本流组合起来:给出即时上下文,然后直接注入"哦,这里有一些相关的记忆要点……"这样的文字。但自动驾驶规划器的 I/O 不是通用的。以轨迹为例:轨迹是一系列边界框。虽然你可能能够将意图表示为一系列边界框,但它不仅表达能力有限,而且也不清楚它能否在不破坏模型训练期间学习到的该边界框序列的含义的情况下,与轨迹合并成一个公共的边界框序列。相比之下,将记忆附加到语言模型的提示中不会破坏任何东西。

因此,在自动驾驶规划中,显式记忆访问(读写)要么需要是基于规则的附加组件,要么需要被学习。让我们基于可扩展性排除启发式附加组件。学习记忆访问只需通过为记忆添加一个新的监督流即可完成。但通常这种监督不可用,因此我们只能通过引导来学习记忆访问,这需要带时间反向传播的闭环训练。然而,这种可学习的显式记忆访问在自动驾驶行业中几乎闻所未闻,因此我不建议走这条路。所以我们只能押注于使用大上下文窗口的隐式记忆。

2.6 搜索

最后一个架构考虑因素是搜索的作用。搜索是运行时增强生成的过程,与训练过程无关。如果生成器是随机的,它可以与排序器结合以生成运行时搜索算法。蒙特卡洛树搜索(MCTS)就是这样的算法之一。我们不生成一系列未来的路径点,而是生成一个路径点树,贪婪地选择下一个要扩展的节点,基于聚合从该子节点开始的几个完全实现的轨迹的排序器奖励来计算每个子节点的值,然后将这些值反向传播到树的上层。这种技术的某个版本可能用于指令调优的大型语言模型的运行时。

搜索模块是可选的锦上添花模块。它确实需要使你的生成器具有随机性。这可以通过回归一个分布的参数来实现,每个未来的路径点从该分布中抽取,而不是直接回归未来的路径点。这意味着每个未来路径点对应一组分布参数。在搜索过程中,扩展步骤可以涉及任意数量的抽取路径点。

3、训练

我已经讨论了架构,现在是时候谈谈训练流程了。这里有两个主要的设计考虑:开环与闭环训练,以及模仿学习与强化学习。

3.1 闭环训练

如前所述,我建议以支持开环和闭环两种训练方式组织模型输入,分别通过随机获取帧或顺序流式传输帧来实现。从一种训练模式切换到另一种训练模式应该只需要更换数据采样器即可。

开环训练的主要问题在于,在运行时,规划器为自我选择的动作会成为规划器下一次迭代的输入。换句话说,规划器会自我反馈。这引出了开环训练的第一个问题:协变量偏移。这是机器学习中一个非常普遍的问题,即测试时(或运行时)的输入("协变量")与训练时的输入分布不同("偏移"),破坏了训练时所学的有效性。这就是为什么开环评估结果不能预测闭环评估结果,甚至可能是一个反信号。

解决这种差异的一个简单方案是使用数据增强技术,我们在自我的位置或其他属性中注入扰动,以学习从错误中恢复。另一个选择是在训练时闭环。当"闭环"时,我们使用一个帧的模型输出来更新下一帧的模型输入。换句话说,前向传播本身会引入扰动,因为前向传播不可避免地会与专家轨迹略有不同。

闭环训练可以解决的另一个问题是因果混淆。当模型仅在随机快照上训练时,它有可能混淆相关性和因果关系。例如,这样训练的规划器可能会学到停车的原因是因为之前的刹车轻踩。结果,一个常见的问题是规划器的小随机动作会在运行时被放大并滚雪球般地变成无意义的行为:在畅通的交通中,一个小的随机刹车轻踩变成完全停车;方向盘的一个小随机旋转导致汽车打转。在闭环训练时,模型会迅速意识到随机刹车轻踩并不表示需要停车。同样,因果混淆可以通过更简单的干预措施来解决,例如过去运动丢弃:一种数据增强技术,即随机丢弃来自参与者轨迹的观察结果,迫使规划器不过度依赖过去的观察。

最后,闭环训练为强化学习或与强化学习相邻的学习方法打开了大门。强化学习的高级概念是将生成器重构为"策略"来决定如何行动,然后将该策略"推出"几步,看看会发生什么。基于发生的情况,我们希望策略自我更新。在严格意义上的强化学习中,推出过程不必是可微分的,并且已经开发了几种技术来仍然允许策略基于推出的结果进行自我更新。这些技术包括 Q-learning近端策略优化(PPO)。因此,在训练期间进行推出的能力对于启用强化学习是必要的。虽然我们可以不用它来缓解协变量偏移和因果混淆,但闭环训练不仅一石二鸟,还为我们日后利用强化学习奠定了基础。

为了实现闭环训练,我们需要一个模拟器。模拟器就是一个以规划器动作作为输入并输出观察结果的模块。在强化学习中,这也被称为"gym"或"环境"。模拟器的主要设计问题是要使其有多复杂。一些公司在原始传感器数据级别投资于完整的世界模拟。一个更温和的方法是将模拟锚定在日志回放上,专注于回放已记录的语义数据。所谓"语义数据",我指的是感知栈的输出,即一个由检测、轨迹和矢量地图元素组成的世界(一个由基本几何体而非原始传感器数据组成的世界)。(这里我假设你正在记录感知栈的输出。)下一个问题是是否让非自我参与者对训练中的策略产生的自我推出做出反应。为了简单起见,我建议保持智能体非反应性。如果我们将自己限制在锚定于日志回放的模拟,那么模拟器就简化为一个从驾驶日志加载帧并根据前一帧的模型输出更新模型输入的机制。

非反应性模拟的主要风险是我们可能学到错误的东西,特别是当与模仿学习结合时。例如,模拟的自我可能偏离回放中的自我。在这种情况下,我们试图模仿的回放自我的 ground truth 未来轨迹可能与模拟自我的当前位置相去甚远,因此模仿损失会很大。此外,模拟自我的正确做法在它开始偏离回放自我后可能会有所不同。例如,如果模拟自我的速度恰好慢于回放自我,那么原本要转入我们车道的智能体现在可能会插到我们前面而不是后面,现在正确的做法是刹车而不是继续前进。

虽然这种模拟-回放差距代表了一个潜在问题,但闭环训练的主要目标与其说是学习智能体间的动力学,不如说是学习我所说的"自我动力学"。训练时间和运行时之间协变量偏移的主要来源是自我自身的过去,这也是因果混淆的主要来源。此外,模拟-回放差距的缺点可以通过开环预训练和在较短的日志上训练来缓解。同时,在模仿学习环境中,模拟-回放差距在某种程度上是可以自我缓解的。实际上,在收敛之后,模型可能会越来越少地偏离专家轨迹。因此,基于模仿学习的闭环训练就像一种随时间衰减的数据增强技术。

模拟器的第二个设计问题是是否使其可微分。非可微分闭环训练意味着推出的每一步可能会产生某种损失/反馈,但该损失不会反向传播到前面的步骤,只会反向传播到产生它的即时步骤。这种闭环训练有时被称为"多步训练",类似于一种简单地向输入注入噪声的数据增强技术。实际上,前向传播可以被视为对专家轨迹的一种扰动,然后被输入到下一个前向传播。UrbanDriver 论文展示了一些证据,表明噪声注入和多步训练产生相似的结果。

表1:BC扰动是一种注入噪声的行为克隆方法。MS预测是一种多步预测方法。来源:UrbanDriver论文。

鉴于我主张一个相当简单的非反应性模拟器,我认为投入额外的精力使其可微分是有意义的。可微分模拟器意味着我们可以进行时间反向传播(BPTT),UrbanDriver 显示这提升了结果(表 1 中的 Ours 行使用了可微分模拟器)。带有 BPTT 的闭环训练让你获得强化学习的许多好处,而无需实际进行强化学习。我会稍后再尝试"真正的强化学习",因为它以样本效率低下而闻名,如果你要使用 PPO 等强化学习算法训练场景编码器,这可能是 prohibitive 的。强化学习还需要潜在的重大基础设施调整,例如数据集不仅需要可读,还需要可写(有时称为"回放缓冲区")。我认为这些投资短期内不值得。

总而言之,我的建议是设计一个基于日志回放、非反应性、可微分的模拟器。这意味着开环和闭环之间唯一会改变的是自我的状态。在开环训练中,某个帧的自我状态就是该帧记录的状态。在闭环训练中,某个帧的自我状态是前一帧规划器产生的状态。关键的设计问题是在我们数据管道的哪个位置更新模型输入,即在哪个位置闭环。最简单的选择是在特征化之前闭环,如图所示。这是最简单的选择,因为更新操作只是将回放自我状态替换为模拟自我状态(前一次前向传播的输出)。

图 19:特征化前完成训练循环。

这种方法的问题是,如果你想要一个可微分的模拟器,你基本上必须使特征化步骤成为前向传播计算图的一部分,这会增加延迟,并且通常会使部署更加复杂。使用 TensorRT 等产品以边缘优化方式部署模型可能很麻烦,减少麻烦的最佳方法是减少需要"部署"的操作数量,即将尽可能多的操作卸载到 CPU,并保持 GPU 上的前向传播精简。如果你确实想将特征构建器保留在 CPU 上,那么在特征化之前闭环意味着你放弃了模拟器的可微分性,回到了简单的多步训练。

我的建议是在特征化之后闭环,如图所示。更新操作更复杂,因为你必须基于模拟的自我来更新基于回放自我计算的特征。如果你的特征构建器很复杂,这个更新操作可能会变得相当复杂。但延迟方面的收益(在闭环训练中会累积)是值得的,而且通过将特征化保留在 CPU 上可以更容易部署。

图 20:特征化后闭合训练循环。

更一般地说,CPU 特征化有几个好处。它让你对内存使用有更精细的控制,因为 TensorRT 等部署框架可能在内核分配上采取防御性策略,因此留给 TensorRT 的特征化越多,内存膨胀的风险就越大。CPU 特征化的其他好处包括能够在训练和运行时之间共享特征构建器,以及能够解耦特征化和前向传播。后者意味着你可以在 GPU 忙于前向传播时在 CPU 上获取接下来几个样本(PyTorch 数据加载器可以轻松配置为这样做),因此你可以在前一个样本的前向传播进行中时为下一个样本进行特征化——这是特征化也在 GPU 上时无法做到的。

关于开环与闭环训练的最后一句话。我一开始说我们应该保持轻松切换两种训练机制的能力,但为什么呢?为什么不完全采用闭环训练,将代码库设计为仅针对闭环训练进行优化?这是因为在开启闭环训练之前,以开环方式预训练规划器可能是有益的。至少,我们希望有能力设计这样的课程并进行实验。

3.2 强化损失

通过闭环训练,你可以开始利用强化损失,例如碰撞避免(包括驶离道路)和向目标前进。这样的损失在开环和闭环训练机制中都可能有用。例如,ChauffeurNet 在闭环设置中使用了非模仿损失。这些损失可以结合排序器或不结合排序器来实现。有了排序器,这种非模仿信号可以作为评判器被纳入。或者,非模仿损失可以直接应用于生成器输出,完全绕过对排序器的需求。由于非模仿损失仅针对自我,我们在参与者之间(自我和非自我参与者之间)引入了不对称性,因此这些损失只应在解码器中的拆分之后应用(参见迁移学习部分)。

关键的见解是闭环训练使模型能够强化安全行为。通过时间反向传播,困扰强化学习的信用分配问题(也是其样本效率低下的根源)得到了缓解,尽管被梯度消失问题所取代(而梯度消失问题本身可以通过截断推出得到缓解)。模型有效地学习到什么动作会导致安全问题,并学会避免它们。

3.3 迭代训练

如前所述,虽然生成器和排序器的联合训练可能会混淆规划器,但迭代训练可能使我们能够利用有益的博弈论动力学。在联合训练中,两组模型参数在每一步都更新,而在迭代训练中,每一步只更新一组模型参数。在迭代训练中,先更新生成器一定的步数,然后,在类似的步数中,只更新排序器。

这,加上从开环训练过渡到闭环训练的能力,突显了课程设计的重要性。因此,我这里的建议是设计你的代码库以允许对各种课程进行轻松实验。避免为开环与闭环或为训练生成器与训练排序器手动运行不同的流程。尽可能使你的训练循环可定制。有很多现代框架可以帮助做到这一点。

4、基础设施

我已经介绍了架构和训练策略。现在我想分享一些关于"基础设施"的技巧,我指的是关于开发流程、仿真的作用、数据挖掘和指标的大杂烩。

4.1 开发流程

我关于开发流程的第一个建议是尽可能使其端到端("集成化")。我会避免将流程分成不同的团队,比如数据团队、机器学习团队和部署团队。所有这些都应该由同一个团队中的同一个人完成。这应该反映在代码本身:偏好单一代码库而非分散的代码库。如果你有好的工具并且是"集成化"的,你不需要那么多人。

集成的开发流程意味着你的数据加载器直接从驾驶日志加载(希望有一个缓存步骤)。这意味着规划器的输入语义数据在训练时和运行时是相同的。这意味着你可以在运行时和训练时之间共享你的特征构建代码。由于运行时代码是 C++ 的,我的第二个建议是直接用 C++ 编写特征构建器,并创建 Python 绑定,以便在训练时使用特征构建器。

我关于开发流程的最后一个建议是尽可能简化部署。没有什么比从运行时栈本身获得反馈更好的了。虽然训练模拟器是不可避免的,但评估应尽可能使用运行时栈进行。促进这一点的一大步骤是在每次训练运行结束时自动化模型部署(例如,将规划器转换为 TensorRT)。 矢量化的地图元素应该具体包括什么?这里有多个有效的选择,但车道级别的信息可能就是你需要的精细程度。在线建图系统通常会检测车道边界,然后可以简化为车道中心线,通常也称为"基线路径"。除此之外,我们还需要一些关键的交通信号信息,如交通灯、停车标志和限速标志。其中一些元素具有动态属性(例如:交通灯),你需要一个专门的感知系统来处理(也不在本文讨论范围内)。总而言之,我们假设这些地图元素以基线路径图的形式返回。每条基线路径都携带属性,例如它是否包含停车标志或交通灯(以及交通灯的状态)。

4.2 仿真器的作用

有两种类型的仿真器,您不应混淆。第一种是我称之为“训练仿真器”的,我在闭环训练部分已经描述过。这种仿真器本质上就是一个“模型输入更新器”,负责在训练过程中“闭环”,它根据前一帧的模型输出,更新回放日志中下一帧的模型输入。由于训练仿真器在语义空间中运行,因此其关键功能在于能够使用最新的感知堆栈更新仿真器中的语义数据。这意味着需要从原始传感器数据回放日志并重新记录感知输出。这些新的日志就是您在训练仿真器中回放的日志。

第二种仿真器是我称之为“数据仿真器”的,它不是更新回放日志,而是从头开始生成日志。训练模拟器的目的是进行闭环训练,而数据模拟器的目的是提供廉价且多样化的训练数据。与其带着传感器到现实世界中四处奔波收集数据,不如直接启动数据模拟器来获取训练数据。我认为,无论是原始数据还是语义数据层面的模拟器,目前都还不够成熟,因此,至少在可预见的未来,我建议投资建设一支运营良好、精简高效、规模庞大的真实车队来收集训练数据。如果您倾向于手动驾驶,则无需那么多高技能的驾驶员,从而降低成本。但如果是自动行驶,则仍然需要雇用熟练的驾驶员。

那么,从长远来看,是否应该投资数据模拟器呢?说实话,我并不确定它是否值得,我宁愿等待,然后将其外包给市场。但如果你打算走这条路,自行构建,我建议你最好一步到位,直接模拟原始传感器数据,而不是语义数据。考虑到生成式人工智能的最新进展,构建一个完整的原始传感器数据模拟器是否比构建一个响应式语义模拟器成本高得多,这一点尚不明确。在某些方面,构建一个响应式语义模拟器可能比构建一个响应式原始数据模拟器更复杂,因为它可能涉及更多启发式编程,所以我建议直接跳过原始数据模拟器,直接使用后者,随着数据中心规模的扩大,后者的性能似乎也在稳步提升。

4.3 数据挖掘

关于数据挖掘,我没有什么独到的见解。不过,它确实是一个至关重要的工具。以我的经验来看,数据挖掘很容易演变成一系列定制工具的涌现,这些工具旨在满足你各种机器学习产品的特定需求。我会尽可能地避免这种趋势。

数据挖掘大致可以分为两个层次:原始数据层次和语义数据层次。两者各有优缺点。前者具有高召回率,后者具有高精确率。查询原始数据源意味着需要投资建设优质的向量数据库。在查询方面,应该支持文本查询,也应该支持基于图像的查询。

对于语义挖掘,主要有两种方法:查询密集型和数据库密集型。在查询密集型方法中,挖掘的数据与记录的数据保持紧密联系,查询语句编写得非常简洁易懂,并且尽可能地并行执行。在数据库密集型方法中,需要投入大量资源进行日志预处理,以提取预期会在多个查询中使用的属性和特征,并将其缓存起来,从而将属性提取的计算成本分摊到多个查询中。

然而,在实践中,属性在不同查询之间的共享频率尚不明确。数据库密集型方法也容易导致组织效率低下,例如,一个团队负责缓存步骤,而(通常情况下)机器学习工程师(MLE)负责编写查询语句。这两个团队之间的沟通最终会中断,MLE(多级查询引擎)花费在向缓存团队提交需求和等待新缓存上的时间,比实际进行数据挖掘的时间还要多。与其如此,不如投资一个优秀的、灵活的查询引擎,并尽可能减少预处理数据。计算成本最终可能会更高,但灵活性将提高生产力。

以上是关于主动数据挖掘的内容。当一个查询被频繁使用时,应将其执行自动化。这样就将查询转化为一个指标。我力求将主动数据挖掘基础设施与被动数据挖掘基础设施整合起来。它们本质上是一回事。再次强调,指标就是我们经常运行的查询。然而,指标和数据挖掘常常被分开考虑,并拥有各自独立的基础设施。

5、结束语

这篇文章总结了我对如何设计自动驾驶汽车软件栈的看法。它侧重于行为方面,因为我已在其他地方分享了我对感知的看法。最后我想说,尽管有人声称自动驾驶已经"被解决",但自动驾驶行为学习仍然是一个非常开放的问题。如果这篇文章能传达出这个问题的复杂性和解决方案空间的复杂性,那就达到了我的目的。欢迎提供任何反馈。


原文链接: How to design an autonomous vehicle software stack

汇智网翻译整理,转载请标明出处