做轨迹预测这块,绕不开的一个基础活儿就是把数据集吃透。我自己的实验笔记里,数据集那一节被反复改了七八版,最后一版干脆单独拎出来,编号叫“1.6.1 Argoverse”。之所以给这么重的篇幅,是因为后面模型结构、损失函数、评测脚本全都长在它上面——数据没读懂,模型再花哨也是白跑。Argoverse 是自动驾驶领域一套公开的多任务数据集与开发工具链,核心包含轨迹预测、3D 检测与跟踪、传感器序列、高精地图四大块,其中轨迹预测那部分,是绝大多数人做多模态预测的第一个正式练手场。它能解决什么问题?简单说:给你一辆车和它周围一群交通参与者过去几秒的轨迹,再加一张矢量化的车道级地图,让你预测其中某个目标接下来几秒会怎么走。听起来像算命,但它有明确的物理约束、有地图约束、有可量化的评测口径。这套东西适合谁:刚进自动驾驶感知预测方向、需要跑通一条端到端流水线的同学;做时序多模态建模、想找个真实场景验证想法的算法工程师;还有做毕设、需要一份结构完整又能复现的数据支撑的研究者。下面就按我自己的理解顺序,把它一层层拆开。
1. 先把这个数据集的位置摆正
1.1 它到底想解决什么样的问题
单车的轨迹预测和行人轨迹预测最大的区别,在于前者不是“一条曲线拟合”,而是一个被结构强约束的多智能体问题。红绿灯、车道线、路口渠化、前车刹车、侧向加塞,每一个都在给目标车辆的未来加约束。所以一个能打的预测数据集,不能只给坐标点,必须同时具备三样东西:足够长的历史与未来窗口、与轨迹严格对齐的高精地图、以及一套能区分“简单场景”和“地狱场景”的划分方式。Argoverse 的价值就在这三样上都给了比较完整的答案——历史窗口够长,地图是矢量级的车道中心线和车道边界,场景采样上刻意偏向有交互的路口和变道片段。我个人的判断是,它不算最庞大的数据集,但它的“结构完整度”很高,非常适合用来建立对预测任务的第一手直觉。
1.2 三类子任务,别一锅端
很多人第一次接触会犯一个迷糊:把检测跟踪和轨迹预测当成一件事。其实它分得挺清楚。轨迹预测任务给的是已经标注好的历史轨迹,你只需要输出未来;3D 跟踪任务给的是原始传感器序列,你得自己先从点云和图像里把目标框出来、再关联成轨迹;传感器序列那部分则是给感知、标定、时序融合这类任务准备的素材库。我建议的入门顺序是:先啃轨迹预测,因为它的输入是结构化表格,不需要碰点云,能把全部精力放在“怎么建模交互”上;等预测这条线跑通了,再回去看跟踪任务,你会立刻明白为什么预测的上游误差那么致命——框抖一下,预测的输入就脏一片。
1.3 版本怎么选:v1 和 v2 别混着用
这里有个坑必须先说清楚。两个大版本在采样频率、窗口长度、坐标系约定上都不一样,网上很多开源实现是混着写的,你抄过来会莫名其妙 loss 不降。我整理了张表,是我自己踩过之后记下来的:
| 维度 | v1 预测集 | v2 预测集 |
|---|---|---|
| 采样频率 | 10Hz | 10Hz |
| 历史窗口 | 2 秒(20 帧) | 5 秒(50 帧) |
| 未来窗口 | 3 秒(30 帧) | 5 秒(50 帧) |
| 单场景总长 | 51 帧 | 110 帧 |
| 场景规模 | 约 32 万条 | 约 25 万条 |
| 覆盖范围 | 两座城市 | 六座城市 |
| 地图形式 | 矢量车道图层 | 矢量车道图层 + 可行驶区域多边形 |
| 官方看板指标 | minADE / minFDE / MR / 可行驶区域合规 | 增加概率校准项与区域合规项 |
选型上我的建议很直接:如果只是要验证一个交互建模的想法,用 v1,数据量小、读取快、教程多;如果目标是往论文或正式项目上靠,用 v2,因为它的评估口径更严格,概率校准那一项会逼着你把多模态的置信度真正学出来,而不是随便撒六条线取最近的那条。
1.4 为什么它值得花一周时间精读
我见过太多人上来就 clone 一个开源仓库、改改配置文件、跑出个数就交差。这种做法的天花板很低,因为你不知道数据里哪些是噪声、哪些是刻意设计。举个例子,v2 里有些场景的目标车全程静止,如果你不筛掉,模型会学到一个“预测不动”的偷懒解,在验证集上看着还行,一上真实数据就崩。再比如地图里的车道段是有方向属性的,你把方向丢了,左右转就分不清。这些细节官方文档不会重点强调,但决定了你的实验能不能复现。花一周时间把数据结构、坐标系、采样逻辑过一遍,后面能省下至少一个月的瞎调参。
2. 一份场景文件里到底装了些什么
2.1 目录组织与命名约定
v2 的预测数据是按分片打包的,解压之后每个场景一个独立文件夹,文件夹里通常有三个关键文件:一个是存轨迹表的列式文件,一个是存地图的 JSON,还有一个是记录元信息的小文件。命名上有个规律,场景 ID 会在三个文件名里重复出现,所以写加载器的时候别硬编码文件名,用“目录 + 通配”的方式去找,否则遇到个别命名不一致的分片就直接崩。我早期的加载脚本就是写死了后缀,跑到第 300 多个分片时报错,排查了半小时才发现是某个分片的元信息文件扩展名不同。这种坑很小,但很耗人。
2.2 轨迹表字段逐个说透
轨迹表是整份数据的核心,我按重要性排一下。时间戳字段决定一切时序对齐,v2 是 10Hz,也就是相邻帧差 100 毫秒,但是要注意——不是所有目标都在每一帧有观测,中间会有空洞,官方是允许缺帧的。目标标识字段用来把同一个交通参与者串成一条轨迹,这里要特别注意“焦点目标”这个概念:每个场景只指定一个焦点目标作为预测对象,其他目标都是上下文。类别字段区分轿车、行人、骑行者、卡车、公交等等,实测下来这个字段里混着不少“未知”类别,训练前必须过滤,不然分类头会学出一个永远不用的空类。位置字段是城市坐标系下的绝对坐标,速度与朝向字段是基于位置差分算出来的,我一般会自己重算一遍,因为官方的朝向在低速时抖动很明显。
注意:低速或静止目标的角度字段基本不可信。目标速度低于 1 米每秒时,朝向会在正负一百八十度之间跳,这时候别拿它当真值监督,用速度方向或者直接屏蔽该帧的角度损失更稳。
2.3 高精地图的矢量语义
地图这块是 Argoverse 比较下功夫的地方。它以车道段为基本单元,每个车道段带左右边界线、中心线、前驱和后继关系、是否位于路口、以及是机动车道还是非机动车道。可行驶区域是单独的多边形图层,用来做区域合规判断。我一开始只用了中心线,模型学出来的轨迹老是贴着车道边缘走,后来把左右边界也加进去,轨迹就居中多了。原因不难理解:中心线只告诉你“这条路往哪拐”,边界才告诉你“这条路有多宽”,而宽度直接决定了变道的可行性和转弯的曲率上限。另外前驱后继关系很重要,它是构建车道拓扑图的天然素材,做图神经网络的时候可以直接当边用。
2.4 场景切分与采样逻辑背后的考量
为什么是 5 秒历史加 5 秒未来,而不是 3 秒?这里有个很实际的工程权衡。历史太短,模型看不到目标的加速意图和变道前兆;历史太长,输入序列变长,训练成本上升,而且老早以前的信息对当下决策的意义在衰减。5 秒大概是城市道路上一个完整操作(打灯、观察、变道、回正)的时间尺度。未来 5 秒的选择逻辑不一样,它是在保证“有意义”和“可预测”之间取平衡:1 秒内的未来几乎是确定的,预测难度太低;10 秒后的未来受随机因素影响太大,人自己都猜不准。5 秒刚好覆盖“会做出什么机动”的层面,而不要求精确到每一厘米。这个设计思路值得记住,你自己做任何时序预测任务,窗口长度的选择都应该回到“我的任务需要多长的因果链”这个问题上。
3. 从零跑通:加载、归一化与第一次可视化
3.1 环境搭建与依赖版本
先说环境。这类官方工具链对 Python 版本和依赖版本比较敏感,我一般会单独建一个环境,Python 用 3.9,避免太新的版本把老依赖顶掉。
conda create -n av2 python=3.9 -y conda activate av2 pip install av2 shapely numpy pandas matplotlib torch装完之后先别急着写模型,跑一遍导入检查:
import av2 from av2.datasets.motion_forecasting import scenario_serialization from av2.map.map_api import ArgoverseStaticMap print("ok")注意:不同版本的开发工具包,模块路径和函数签名会变。你从网上抄的代码跑不通,八成不是代码错,是版本不匹配。先确认自己装的版本号,再去对应该版本的文档,别硬改。
3.2 读一个场景并检查完整性
读取一个场景的标准流程我固定成四步:加载轨迹、加载地图、拿到焦点目标、做一次完整性检查。
from pathlib import Path import numpy as np scenario_path = Path("./train/0000abcd-1234/scenario_0000abcd-1234.parquet") scenario = scenario_serialization.load_argoverse_scenario_parquet(scenario_path) map_path = scenario_path.parent / f"log_map_archive_{scenario.scenario_id}.json" static_map = ArgoverseStaticMap.from_json(map_path) focal = scenario.get_focal_track() print("焦点目标类别:", focal.object_type) print("轨迹点数:", len(focal.timestamps)) # 完整性检查:历史窗口是否够 50 帧 hist_mask = focal.timestamps <= focal.timestamps[0] + 5_000_000_000 print("历史帧数:", hist_mask.sum())这里的5_000_000_000是纳秒,因为时间戳单位是纳秒。我见过有人直接拿它当毫秒算,结果历史窗口只有 5 毫秒,模型输入全空,loss 却还在降——因为模型学会了输出均值。这种错误最阴,不报错,只是结果烂。检查的时候顺手看一眼焦点目标的历史轨迹是不是几乎不动,如果是,直接跳过这个场景,别让它污染你的训练集。
3.3 坐标归一化:为什么必须平移到焦点目标原点
这一步是新手最容易忽略、也最容易埋雷的地方。原始数据是城市坐标系的绝对坐标,数值能到几万几十万米。你直接喂给网络,一是数值范围太大不利于收敛,二是每个场景的绝对位置都不同,模型会学到一堆无意义的偏置。正确做法是把整个场景做一次刚体变换:以焦点目标在当前时刻的位置为原点,以它的朝向为 x 轴正方向,把所有目标轨迹和地图矢量一起旋转平移过来。
def to_focal_frame(xy, origin, heading): c, s = np.cos(-heading), np.sin(-heading) R = np.array([[c, -s], [s, c]]) return (xy - origin) @ R.T写这段代码有两个点要盯死:旋转角度的正负号,以及 y 轴是左手还是右手。我在这上面栽过一次,模型训练三十轮 loss 卡在 1.2 不动,最后发现是 y 轴方向反了,整个地图镜像了,左转场景全被当成右转学。判断方法很简单:归一化后,焦点目标的历史轨迹应该基本沿着 x 轴正向分布,y 值在零附近小幅波动。如果你看到历史轨迹是竖着的,那百分之百转错了。
3.4 矢量编码和栅格编码,选哪个
地图怎么喂给模型,是第二个分岔口。我把两种方式的取舍整理如下:
| 对比项 | 矢量编码 | 栅格编码 |
|---|---|---|
| 表达精度 | 高,保留车道拓扑 | 低,受分辨率限制 |
| 计算开销 | 中等,随车道段数量增长 | 固定,取决于画布尺寸 |
| 拓扑关系 | 天然保留 | 需要卷积隐式学习 |
| 实现难度 | 偏高,要做采样和聚合 | 低,画出来就行 |
| 适合的模型 | 图网络、注意力 | 卷积、多任务共享主干 |
我的实际做法是前期用栅格跑通全流程,因为可视化直观、调试方便,错误一眼能看出来;等流程稳了再切矢量,把车道段采样成固定数量的点,每点带坐标、方向、类型三个特征。切换的过程会带来一次指标波动,别慌,那是模型在适应新的输入分布,不是你的实现错了。
4. 拼一个能打的基线:输入、损失与评估
4.1 输入特征怎么构造
输入特征我一般分成三组。第一组是目标自身的历史状态:归一化后的 xy 坐标、速度、加速度、朝向的正余弦。加速度一定要算,因为刹车和匀速在位置序列上几乎看不出区别,但加速度一阶差分立刻暴露意图。第二组是邻车上下文:只取焦点目标一定半径内的目标,按距离排序取前若干个,每个目标带相对位置、相对速度、类别独热。半径别设太大,五十米外目标对当前决策的影响基本可以忽略,反而增加噪声和算力。第三组是地图上下文:沿焦点目标当前朝向,取前方一定范围内的车道段采样点。
feat_self = np.concatenate([xy_norm, vel_norm, acc_norm, np.stack([np.cos(h), np.sin(h)], -1)], axis=-1)特征工程这块我有条经验:不要一次性把所有特征都加上。先跑一个只有目标自身历史的版本,记下指标;然后加邻车,再加地图,每加一组看一次提升幅度。这样你能明确知道收益来自哪里,写报告、写论文都有话说。我见过的多数“模型不 work”的案例,问题不在模型,在于特征里混进了大量噪声或者归一化做错了。
4.2 输出头与多模态的坑
多模态输出的标准做法是出 K 条候选轨迹,每条带一个置信度。K 取 6 是这套数据集的传统,因为官方评测就是按 K 算的。损失一般是两部分相加:分类损失用交叉熵,回归损失用对最近真值轨迹的平滑 L1 或者高斯负对数似然。这里有个很隐蔽的坑叫“模态坍缩”——训练几十轮之后,六条轨迹慢慢收敛到几乎重合,分类头退化成一个常数输出,minADE 看着还行,其实模型已经完全放弃了多模态。
我的应对手段有三个。一是用赢家通吃的硬分配,每轮只让离真值最近的那条轨迹回传梯度,逼着不同模态去覆盖不同区域;二是加多样性正则,惩罚六条轨迹之间的过度相似;三是分类头用温度系数的软化标签,别用独热,让它有一点容错空间。实测下来,第一条效果最直接,第三条最稳。
4.3 评估指标自己实现一遍
官方看板固然权威,但我强烈建议自己实现一遍指标,因为你需要理解每个数字背后的含义,也需要在本地快速迭代。
def min_ade(pred, gt): # pred: (K, T, 2) gt: (T, 2) d = np.linalg.norm(pred - gt[None], axis=-1) # (K, T) return d.mean(axis=1).min() def min_fde(pred, gt): d = np.linalg.norm(pred[:, -1] - gt[-1][None], axis=-1) return d.min() def miss_rate(pred, gt, thr=2.0): return float(min_fde(pred, gt) > thr)minADE 是各模态平均位移误差的最小值,minFDE 是终点误差的最小值,miss rate 是终点误差超过两米的样本比例。这三个指标有个共同特点:都取最小值,意味着只要六条轨迹里有一条蒙对了,指标就好看。这就是所谓的“撒网式作弊”。所以只看 minADE 会高估模型能力,你还得看分布——比如六条轨迹的置信度和准确率是否一致。v2 引入概率校准项,本质上就是在治这个病:你把某条轨迹的置信度标成九成,结果它错了,惩罚会很重。
4.4 训练配置上的几个取舍
批大小我一般设 64 到 128,取决于显存。学习率用 1e-3 配余弦退火,warmup 一千步左右。训练轮数别贪多,这套数据在 v2 上三十轮左右就能看出趋势,超过五十轮基本是过拟合,验证集指标开始原地打转。硬件上,单张消费级显卡跑 v2 的轻量模型是够的,但如果你想做大规模对比实验,建议先用 v1 或者 v2 的一个子集把想法筛一遍,再上全量。我自己的习惯是固定一个两千场景的小子集做“冒烟测试”,任何改动先在这个子集上跑五轮看趋势,趋势不对立刻停,别浪费算力。
提示:每次改完数据管线,第一件事是把一个 batch 的输入可视化出来,看轨迹、地图、邻车是否对得上。这一步花两分钟,能省掉后面几小时的困惑。
5. 常见问题与排查实录
5.1 症状对照表
我把这几年被问得最多、自己也踩过的问题整理成表,按症状反查原因,效率高很多:
| 症状 | 最可能的原因 | 处理办法 |
|---|---|---|
| loss 完全不降 | 坐标系旋转方向反了,或 y 轴镜像 | 可视化历史轨迹,应沿 x 正向 |
| loss 降到某个值就卡住 | 多模态坍缩,六条轨迹重合成一条 | 改硬分配,加多样性正则 |
| 训练指标好、验证指标崩 | 场景级泄漏,或过滤逻辑不一致 | 检查划分,统一过滤规则 |
| 时序对齐错位 | 用索引下采样而不是按时间戳 | 一律按时间戳取窗口 |
| 地图和轨迹对不上 | 地图未随轨迹同步做刚体变换 | 变换矩阵要作用于所有几何 |
| 内存爆掉 | 一次性加载全部场景 | 改流式迭代加载 |
| 个别分片读取报错 | 文件名硬编码 | 用通配符匹配 |
5.2 几条只在真跑起来才会遇到的坑
第一个坑是缺帧。官方允许目标在序列中某些帧缺失观测,你如果假设每帧都有数据、直接用索引切片,就会把后面所有帧整体错位。正确做法是先按时间戳重采样到固定网格,缺的地方用插值补,并且给补出来的帧打上掩码,让损失函数跳过它们。我早期没做掩码,模型把插值出来的平滑轨迹当真值学,结果预测出来的轨迹异常平滑,转弯半径大得离谱。
第二个坑是静止场景。城市数据里有相当比例的目标全程几乎不动,比如等红灯。这类样本如果占比过高,模型会退化成“预测不动”的局部最优。我的做法是统计焦点目标历史位移,低于阈值的降采样或者直接剔除,同时保留一部分作为难例,避免模型对静止目标完全失去感知。
第三个坑是地图与轨迹的时间一致性。高精地图是静态文件,但采集时的车道可能已经变了,这种不一致在数据里是客观存在的。你不用去修它,但要知道指标的天花板在哪里——有些场景你怎么调都上不去,不是模型的问题。
第四个坑是评测口径。自己写的指标和官方看板经常差个零点零几,原因是边界情况的处理不同:有的实现把无效样本剔除了,有的保留了。我的经验是,提交前一定用官方脚本跑一遍,自己的实现只用于日常迭代。
5.3 数据增强的边界在哪里
增强能提点,但这类任务的增强很容易做过头。平移和旋转是安全的,因为场景整体刚体变换不改变语义。加高斯噪声也还行,模拟定位误差,但幅度别超过半米,否则轨迹会穿出车道。镜像要特别小心:左右镜像之后,车道拓扑的连通关系、路口渠化都可能变得不合理,左转变右转本身没错,但如果你的地图没有跟着一起镜像,模型学到的是矛盾的信号。我的实践是镜像只用在纯轨迹的预训练阶段,正式训练不开。
5.4 从实验到落地的量级意识
最后说个容易被忽略的点。实验室里我们关心的是 minADE 小数点后第二位,真到工程里,更关心的是单帧推理延迟和内存占用。这套数据集的场景是切好的、干净的、标注完备的,真实车上拿到的是带噪的、有时延的、可能丢帧的输入。我的建议是,在模型定型之前就做一次“退化测试”:人为把输入轨迹的精度降一半、随机丢掉两成历史帧、给邻车加个百毫秒延迟,看指标掉多少。掉得少的模型才是能用的模型。我自己做过一次这样的测试,minADE 从零点八掉到一点四,才知道之前的漂亮数字有多脆弱——那次之后,我所有实验都会在报告里附上退化测试的结果。
我个人的体会是,Argoverse 这套东西的价值不在于它有多大,而在于它把“地图约束、多智能体交互、多模态不确定性”这三件事用一套统一的格式串起来了。你把它读透一遍,再去看其他同类数据集,会发现结构大同小异,迁移成本很低。真正需要花时间的,是那几条永远不会写在文档里的细节:时间戳单位、坐标轴方向、缺帧处理、场景过滤。这些地方不出错,实验就已经成功了一半。