looyifan / MotionLM: Multi-Agent Motion Forecasting as Language Modeling

Created Tue, 17 Mar 2026 00:00:00 +0000 Modified Fri, 15 May 2026 14:43:15 +0800

🎯 一句话概括

把自动驾驶的多智能体轨迹预测,变成一场"文字接龙"游戏——用语言模型预测下一个动作词的方式,来预测车辆和行人的未来轨迹。


🌟 核心洞察:马路上的"聊天室"

想象一下,繁忙的十字路口就像是一个喧闹的**“大型聊天室”**。汽车、自行车、行人都在用他们的肢体语言和移动轨迹进行着高频的"对话"——“我要变道了”、“你先走”、“我要加速了”。

既然这些交互如此像人类的语言交流,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 架构,分为两大部分:

场景编码器

任务: “察言观色”。采用早期融合网络的设计。

输入大杂烩:

  1. 矢量化的高精地图
  2. 红绿灯的实时状态和历史序列
  3. 目标智能体和周围所有其他车辆/行人的历史轨迹

输出: 经过深度 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,由三个向量逐元素相加组成:

  1. 动作本身的值嵌入(Value Embedding,比如"向左偏一点")
  2. 时间位置编码:告诉模型现在预测的是未来第几秒
  3. 智能体身份编码:告诉模型这个动作是属于车辆 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++ - 多模态轨迹预测]]