🎯 一句话概括
把自动驾驶的多智能体轨迹预测,变成一场"文字接龙"游戏——用语言模型预测下一个动作词的方式,来预测车辆和行人的未来轨迹。
🌟 核心洞察:马路上的"聊天室"
想象一下,繁忙的十字路口就像是一个喧闹的**“大型聊天室”**。汽车、自行车、行人都在用他们的肢体语言和移动轨迹进行着高频的"对话"——“我要变道了”、“你先走”、“我要加速了”。
既然这些交互如此像人类的语言交流,Waymo 的研究员们脑洞大开:为什么不直接用大语言模型(LLM,比如 ChatGPT 的底层逻辑)的方式,来预测这些车辆和行人的未来轨迹呢?
于是,MotionLM 诞生了。它抛弃了传统轨迹预测中那些繁琐的设定,直接把多智能体轨迹预测(Multi-Agent Motion Prediction)变成了一场"文字接龙"游戏。
🔧 技术实现详解
1. 核心魔法:把"连续的轨迹"变成"离散的单词"
以前的模型在预测轨迹时,通常是在预测连续的坐标点(x, y)。但 MotionLM 说:“不,我要把它变成词汇表!”
研究团队把连续的轨迹坐标转换成了离散的运动 Token(Discrete Motion Tokens)。这就好比把一段连续的路线切成了一个个特定的"动作单词"。
技术收益: 这样一来,模型在每一个时间步预测下一步去哪,就不再是复杂的回归任务了,而是变成了一个纯粹的分类任务。直接在网络最后加上一个标准的 Softmax 层,算出下一个"动作单词"的概率分布即可,简单粗暴且极其有效。
Tokenization 实现细节
class MotionTokenizer:
def __init__(self, vocab_size, grid_range):
self.vocab_size = vocab_size # 例如 128x128 个网格类别 + 特殊 Token
self.grid_range = grid_range # 物理位移极限,例如 [-18m, 18m]
self.ZERO_ACTION_TOKEN = 0 # 特殊单词:代表"保持匀速直线运动"
def encode(self, continuous_traj):
"""
输入: continuous_traj 形状[Agent数量, 时间步T, 2(x,y)]
输出: discrete_tokens 形状 [Agent数量, 时间步T]
"""
tokens = []
# 计算每一步的位移变化 (Delta x, Delta y)
displacements = compute_diff(continuous_traj)
for t in range(T):
if t > 0 and is_almost_equal(displacements[:, t], displacements[:, t-1]):
# 核心魔法:如果当前速度/位移跟上一步一样,直接输出"零动作"单词
token = self.ZERO_ACTION_TOKEN
else:
# 否则,将 映射到均匀划分的离散网格中,得到对应的类别ID
token = self.quantize_to_grid(displacements[:, t], self.grid_range)
tokens.append(token)
return torch.stack(tokens)
关键参数设定:
- 预测频率:2Hz(每 0.5 秒预测一步)
- 位移范围:$[-18.0m, 18.0m]$
- 网格数量:128 个 Bin
- 二维位移映射成离散的类别组合(如 $13 \times 13 = 169$ 个核心动作 Token)
神来之笔——Verlet 积分技巧: 这是一个非常取巧的细节!由于真实的车辆和行人具有惯性,速度通常是平滑过渡的。MotionLM 设计了一个特殊的**“零动作 Token”。如果模型输出这个 Token,它的意思不是"停下",而是“保持上一个时间步的相对位移(即匀速直线运动)”**。这极大压缩了有效词汇表的复杂度,让模型更容易学到平滑的轨迹。
💡 Verlet 技巧的物理含义: 这相当于在词表里直接内嵌了牛顿第一定律(惯性)!模型不需要去费力学习最基础的运动学平滑性,可以把宝贵的网络容量用来学习更高级的场景理解。
2. 扔掉历史包袱:无需锚点或隐变量
在自动驾驶中,未来的可能性是多样的(Multimodal distributions,比如到了路口可能左转、直行或右转)。过去为了让模型学会这种"多种可能性",工程师们必须绞尽脑汁地设计预定义的"锚点轨迹",或者使用非常复杂的"显式潜在变量优化"。
MotionLM 直接掀桌子了! 它根本不需要这些复杂的设定。它只用了一个最标准、最经典的语言模型目标函数——最大化序列 Token 的平均对数概率。就像 ChatGPT 预测下一个词一样,它通过海量数据的自回归训练,自然而然地就学会了所有可能的未来轨迹分布。
3. 网络架构:Transformer 驱动的"听与说"
MotionLM 的骨架是一个经典的 Transformer 架构,分为两大部分:
场景编码器
任务: “察言观色”。采用早期融合网络的设计。
输入大杂烩:
- 矢量化的高精地图
- 红绿灯的实时状态和历史序列
- 目标智能体和周围所有其他车辆/行人的历史轨迹
输出: 经过深度 Transformer 编码,这些异构数据被融合压缩,输出一个带有极强空间和语义上下文的 Scene Embeddings。形状为 $R \times N \times \dots \times H$,其中 $R$ 是 Rollout 数量,$N$ 是联合建模的智能体数量,$H$ 是维度。
这就像是给了模型一个"当前棋局的高清快照"。
联合轨迹解码器
这是一个自回归解码器。它一边通过交叉注意力时刻盯着场景编码器给出的环境信息,一边通过自注意力关注各个智能体已经生成的运动 Token,然后一口气为多个智能体生成接下来的 $T$ 个动作 Token。
class MotionLM(nn.Module):
def __init__(self, num_agents, vocab_size, embed_dim):
super().__init__()
self.num_agents = num_agents
# 1. 场景编码器 (借用 Wayformer 的 Early Fusion 架构)
self.scene_encoder = WayformerEncoder(embed_dim)
# 2. 各种 Embedding 层 (为"单词"赋予意义)
self.token_embed = nn.Embedding(vocab_size, embed_dim) # 动作值编码
self.time_embed = nn.Embedding(MAX_TIME_STEPS, embed_dim) # 时间位置编码
self.agent_embed = nn.Embedding(num_agents, embed_dim) # 智能体身份编码
# 3. 标准的 Transformer 解码器
self.transformer_decoder = nn.TransformerDecoder(
decoder_layer=nn.TransformerDecoderLayer(d_model=embed_dim, nhead=8),
num_layers=6
)
# 4. 输出头:预测下一个单词的概率分布
self.lm_head = nn.Linear(embed_dim, vocab_size)
def forward(self, map_data, traffic_lights, history_traj, target_tokens):
B, N, T = target_tokens.shape
# 第一步:察言观色
scene_memory = self.scene_encoder(map_data, traffic_lights, history_traj)
# 第二步:准备要接龙的"单词"序列(三种 Embedding 逐元素相加)
val_emb = self.token_embed(target_tokens)
time_emb = self.time_embed(torch.arange(T))
agent_emb = self.agent_embed(torch.arange(N))
combined_emb = val_emb + time_emb + agent_emb
# 第三步:降维打击 (Flattening) - 把所有智能体在所有时间的 Token 拉平
flattened_sequence = combined_emb.view(B, N * T, -1)
# 第四步:因果掩码(确保 t 时刻的预测只能看到 t-1 及以前的所有人的动作)
causal_mask = generate_agent_time_causal_mask(N, T)
# 第五步:Transformer 解码
decoder_out = self.transformer_decoder(
tgt=flattened_sequence,
memory=scene_memory,
tgt_mask=causal_mask
)
# 第六步:输出预测
logits = self.lm_head(decoder_out)
return logits
输入嵌入三合一: 每一个输入解码器的 Token,由三个向量逐元素相加组成:
- 动作本身的值嵌入(Value Embedding,比如"向左偏一点")
- 时间位置编码:告诉模型现在预测的是未来第几秒
- 智能体身份编码:告诉模型这个动作是属于车辆 A 还是行人 B
全家桶式"展平自注意力": 过去很多模型会分别算"时间轴上的注意力"和"智能体之间的注意力"。MotionLM 嫌麻烦,直接把所有智能体在所有时间步的 Token 拉平成一条长长的超级序列。在算自注意力时,只通过严格的因果掩码来限制:任何人在 $t$ 时刻的动作,只能参考自己和其他人 $t-1$ 时刻及以前的动作。绝对禁止"穿越"看未来,保证了严格的时序因果关系。
4. 降维打击:单次自回归生成"联合分布"
这是 MotionLM 最引以为傲的一点。
过去的主流做法往往是"事后诸葛亮":先让每个智能体各顾各地生成几条边缘轨迹,然后再用启发式算法打分,看看它们会不会撞在一起。
MotionLM 通过单一的自回归解码过程,直接输出所有交互智能体未来的联合分布。大家在每一步生成时都在互相"看着"对方,完全符合真实世界里大家边走边互相博弈的逻辑。
5. 时序因果的条件推演
因为 MotionLM 在时间序列上是严格的时序因果分解——即未来的动作严格依赖过去的动作,它解锁了一个超强的功能:条件推演。
这意味着你可以用它来做"如果…那么…“的沙盘推演。比如你可以在解码时,强行给车辆 A 设定一个动作(“如果 A 突然急刹车”),模型就能根据这个因果关系,顺滑地推演出后面跟着的车辆 B、C、D 会做出什么样的反应。这对于自动驾驶的规划系统来说,简直是梦寐以求的神器。
6. 训练:极简的交叉熵损失
def train_step(model, batch_data, optimizer):
model.train()
optimizer.zero_grad()
# 提取并分词真实未来的轨迹
ground_truth_traj = batch_data['future_traj']
tokenizer = MotionTokenizer()
target_tokens = tokenizer.encode(ground_truth_traj)
# 前向传播 (Teacher Forcing 模式)
input_tokens = shift_right(target_tokens, pad_value='<BOS>')
logits = model(batch_data['map'], batch_data['tl'], batch_data['hist'], input_tokens)
# 【极致的极简主义】
# 抛弃 Huber, 抛弃 L2 距离,抛弃一切复杂的轨迹 loss
# 只有最经典的交叉熵损失
loss_fn = nn.CrossEntropyLoss()
loss = loss_fn(logits.view(-1, VOCAB_SIZE), target_tokens.view(-1))
loss.backward()
optimizer.step()
return loss.item()
核心: 丢掉一切复杂的轨迹回归损失函数(比如 Huber Loss、L2 距离)。整个庞大网络的训练目标只有一个极其纯粹的函数——标准的交叉熵损失,即最大化真实序列 Token 的平均对数概率。
7. 推理与后处理
@torch.no_grad()
def inference(model, scene_data, num_rollouts=512, top_k=6):
model.eval()
# 1. 编码场景
scene_memory = model.scene_encoder(...)
scene_memory = scene_memory.repeat(num_rollouts, 1, 1) # 复制 512 份
# 2. 自回归解码
generated_tokens = torch.full((num_rollouts, N, 1), '<BOS>')
for t in range(MAX_TIME_STEPS):
logits = model(scene_memory, generated_tokens)
next_step_logits = logits[:, :, -1, :] # [Rollouts, N, Vocab]
# 按概率分布采样(保证多样性)
probs = torch.softmax(next_step_logits, dim=-1)
next_tokens = torch.multinomial(probs.view(-1, VOCAB_SIZE), 1)
next_tokens = next_tokens.view(num_rollouts, N, 1)
generated_tokens = torch.cat([generated_tokens, next_tokens], dim=2)
# 3. 翻译回坐标系
tokenizer = MotionTokenizer()
continuous_rollouts = tokenizer.decode(generated_tokens)
# 4. 后处理提炼
final_trajs, final_probs = aggregate_rollouts(continuous_rollouts, top_k)
return final_trajs, final_probs
def aggregate_rollouts(rollouts, k=6):
"""使用 NMS 和 K-Means 聚类"""
distance_matrix = compute_pairwise_distances(rollouts)
filtered_rollouts = apply_nms(rollouts, distance_matrix, threshold=2.0)
kmeans = KMeans(n_clusters=k)
cluster_centers = kmeans.fit(filtered_rollouts)
cluster_probs = compute_cluster_probabilities(kmeans.labels_)
return cluster_centers, cluster_probs
推理狂飙: 在给定的场景下,模型会平行推演出 512 条不同的未来宇宙。因为是按照概率分布采样的,所以有的宇宙里车辆 A 抢行了,有的宇宙里车辆 A 刹车让行了。
后期提炼: 面对几百条推演出来的联合轨迹,MotionLM 引入了 NMS(非极大值抑制) 结合 K-Means 聚类 算法,最终聚类出 6 个截然不同的核心"模式”,并根据每个簇包含的样本数量给出置信度概率。
模型集成: 为了拿榜单第一,Waymo 还把几个独立训练的 MotionLM 模型集成在一起同时做 Rollout,利用认知不确定性让生成的聚类结果更稳固、更可靠。
📊 核心张量维度解析
next_step_logits 的形状:[num_rollouts, N, vocab_size]
| 维度 | 含义 | 示例值 |
|---|---|---|
num_rollouts |
“平行宇宙"数量 | 512 |
N |
联合建模的智能体数量 | 8 |
vocab_size |
动作词汇表大小 | 169 |
🤔 深度讨论:简单 Loss 的底气
为什么只有交叉熵损失就够了?
你可能会担心:监督信号会不会太稀疏?模型会不会只是在"瞎猜盲盒”,根本不懂物理规律和交通规则?
Waymo 的研究员们敢这么做,底气来自于四大杀手锏:
1. 交叉熵不仅不稀疏,反而是"极其密集"的时序监督
传统模型的回归 Loss 往往只在轨迹终点或几个关键点算一次 L2 距离。而 MotionLM 的交叉熵是在每一个时间步、为每一个智能体都在做惩罚和奖励!
- 如果一辆车未来有 80 个时间步,旁边有 8 辆车
- 传统方法:几个关键点的 Loss
- MotionLM:$80 \times 8 = 640$ 个节点的步步紧逼核对
这种"沿途的每一步都在纠错"的机制,提供的梯度信号实际上比传统的回归 Loss 还要密集和强劲。
2. “大力出奇迹”:用海量真实数据倒逼出物理规则
这是 ChatGPT 震惊世界的底层逻辑,也是大名鼎鼎的**“苦涩的教训”**:与其让人类专家去写复杂的物理规则,不如让模型自己从海量数据里去悟。
- Waymo Open Motion Dataset (WOMD) 包含了数以千万计的真实人类驾驶轨迹
- 人类司机的真实轨迹,本身就完美包含了所有的物理规律和交通规则
- 核心逻辑: 如果 MotionLM 只是死记硬背或者瞎猜,它在这么庞大且复杂的数据集上,交叉熵 Loss 绝对降不下来。它为了把 Loss 降到最低,唯一的出路就是——被迫在神经网络的参数里,内化这些物理法则和几何约束。
3. 交叉注意力的强制绑定
模型怎么知道哪是马路、哪是墙?全靠网络架构的"强制看图"机制。
- 如果模型预测一辆车要"向左偏",但地图特征显示左边是一堵墙
- 交叉熵误差会飙升,梯度顺着 Cross-Attention 的权重一路回传
- 模型被迫修正对地图的理解:地图的几何约束被隐式地刻进了注意力权重里
4. “联合序列"倒逼出博弈与交互理解
MotionLM 把所有车辆的动作拉平成一条长序列。当模型在预测 Agent B 第 3 秒的动作时,它的输入序列里已经包含了 Agent A 在前 2 秒的动作(比如 A 正在加速抢道)。
为了降低预测误差,模型的 Self-Attention 机制被迫学会了去关注序列前面其他车辆的动作。它自己领悟出了"当别人抢道时,我必须减速"的因果交互逻辑。
简单 Loss 的陷阱
但是,简单的 Loss 是一项昂贵的特权。如果以下几个"地基"没打好,简单的 Loss 就会变成一场噩梦:
陷阱一:Tokenization 的灾难
如果不加设计的暴力切分,模型会觉得下一个动作的跨度极大、毫无规律,预测会变成抛硬币。MotionLM 的 Verlet 积分技巧是救命稻草——把绝大多数常规的平滑行驶,全都归结为一个固定的 Token。
陷阱二:感知噪音导致的"不可解之谜”
简单的交叉熵 Loss 极其依赖高质量、无歧义的输入上下文。如果感知数据有延迟,或者高精地图有几厘米的偏移,模型会把人类的合理驾驶行为当成"随机噪音"。
陷阱三:注意力崩溃与维度灾难
序列长度一翻倍,计算复杂度和寻找规律的难度呈平方级爆炸。MotionLM 通过因果掩码和预先训练好的 Wayformer 场景编码器来破局——把繁杂的地图和历史信息提前压缩成凝练的 Scene Embeddings。
🏆 最终战绩
MotionLM 在目前最权威、最硬核的自动驾驶预测数据集 WOMD 上大杀四方:
- 🥇 交互挑战排行榜第一名
- 📈 联合平均精度均值(ranking joint mAP metric)提升 6%
💎 总结
《MotionLM》的迷人之处,在于它做了一次极其优雅的"跨界"。它证明了,不用再去死磕复杂的几何约束和物理方程,只要把连续的驾驶动作变成"词汇",用语言模型"预测下一个词"的自回归降维打击,就能让自动驾驶汽车学会看懂马路上的这盘"大棋"!
整套组合拳:
把坐标变成"格点"(Tokenization) → 加入"保持惯性"的快捷词汇 → 用 Wayformer 吃透地图 → 把所有车、所有时间的动作拉平成一条序列做接龙 → 狂暴采样 500 次 → 聚类提炼出 6 条核心剧本
🔗 相关论文
- [[Wayformer - Waymo的早期融合场景编码器]]
- [[MultiPath++ - 多模态轨迹预测]]