1. 先把 Argoverse 是什么讲明白
如果你是做自动驾驶感知或轨迹预测的,绕不开的一站就是 Argoverse。这个名字在论文里出现的频率极高,很多开源模型的 README 里第一行就是"在 Argoverse 上训练"。但真正把几百 G 的数据拖到本地、打开目录之后,很多人会卡住:字段一堆、坐标系看不懂、可视化跑不起来、评估指标算出来和论文对不上。
我在第一次接触这套数据的时候,光是搞清楚"我要用哪个子数据集"就花了两天。这套数据不是一个单一的数据包,而是几条并行的任务线共享同一套采集和标注体系。轨迹预测给你的是鸟瞰视角下的目标运动历史与未来,3D 检测与跟踪给你的是激光雷达点云加环视图像的多传感器同步序列,高清地图给你的是车道级的矢量拓扑。三者可以单独用,也可以串起来做端到端的场景理解,这是它比很多单任务数据集更有价值的地方。
这篇文章面向三类人:刚进组、被导师丢了一份数据目录让你"先跑通"的同学;想把自家模型迁移到公开基准上做对比的工程师;以及想搞清楚轨迹预测评价体系到底怎么算的算法从业者。我会从目录结构讲起,一路拆到参数归一化、指标计算、常见报错,最后给几个我踩过的坑。文中提到的具体版本以近几年的主流版本为主,老版本字段有差异的地方我会单独标注。
1.1 三条主线各自解决什么问题
轨迹预测这条线,本质是给模型一段历史轨迹,让它输出未来几秒内多个可能的走向。它的难点不在于拟合,而在于多模态:前方路口左转、直行、右转都是合理的,模型必须同时给出多组假设并配上置信度,否则评估指标会很难看。这条线的数据规模最大,场景采样也是刻意挑过的——大量包含交互、变道、路口博弈的片段,而不是匀速直行的无聊路段。
3D 检测与跟踪这条线,考验的是多传感器融合与时间维度的关联。给你一段连续帧的激光雷达点云和同步图像,你要输出每一帧里每个目标的 3D 包围盒,并且保证同一个目标在帧与帧之间的 ID 不发生跳变。ID 跳变(ID switch)是这条线最典型的扣分项,也是实际部署中最头疼的问题——检测漏一帧,跟踪器就可能把 ID 换掉,后面整条轨迹全乱。
高清地图这条线,看起来最"静态",实际最吃工程。车道中心线、车道边界、路口内的虚拟连接线、人行横道、停止线,这些矢量要素的组织方式是分层的。你想做"目标所在的当前车道"查询,得先搞清楚车道段(lane segment)和路口连接(intersection)之间的父子关系,不然查询结果会莫名其妙地返回空。
1.2 三条线之间的耦合关系
很多人把这三个子集当成独立的三份数据,用完了事。但真正的价值在于它们共享同一套城市坐标系和采集时空。轨迹预测的场景片段,对应的就是某段真实道路上被记录下来的若干目标;如果你手上有这段路的激光雷达序列,可以反查同一个路口的地图要素是什么形状。
举个具体例子:你要判断某个预测轨迹是否"合规",也就是是否开到了不可行驶区域。你需要三样东西——预测出来的未来坐标点、当前城市的高清地图、以及地图里可行驶区域的几何描述。这三样分别来自预测数据集、地图数据集和地图 API。缺一样,合规性检查就做不了。所以我建议新手按"先地图、再轨迹、最后多传感器"的顺序上手,因为地图是另外两个任务共用的底座,先把它摸熟,后面能省很多时间。
1.3 为什么很多项目拿它当第一站
场景多样性是核心原因。它的采集覆盖了两个气候和道路风格差异很大的城市区域,一个是路网复杂、坡道多、车道线经常被遮挡的老城区,另一个是路宽、车流快、路口形态规整的新城区。这两种风格混在一起,模型不会因为只见过一种路况而过拟合。
标注质量是第二个原因。轨迹标注是按目标 ID 连续追踪的,不是逐帧独立打框,这意味着你可以直接拿来做时序建模,不用担心标签在帧间断裂。激光雷达标注也是如此,跟踪 ID 是全局维护的。这种连续性在中后期做序列模型训练时非常关键。
第三个原因是评估体系成熟。指标定义明确、官方提供评估脚本、社区里大量论文给出参考数值。你做出一个模型,只要在这个基准上跑出成绩,就能跟已有工作直接对比,省去了自己定义指标的麻烦。
2. 目录结构与核心字段的拆解
2.1 拿到数据先看目录,别急着写代码
我见过太多人上来就pd.read_csv然后报错,原因是路径里还藏着一层没解压的压缩包。拿到数据的第一件事永远是:tree -L 2或者du -sh *看一遍,搞清楚哪层是场景、哪层是文件。
轨迹预测部分的典型结构是这样的:根目录下有一堆以数字命名的子目录,每个子目录里放着若干个 CSV,还有一个地图相关的 JSON。每个 CSV 对应一个场景片段,文件名和它在地图里的位置没有直接关系,需要靠 CSV 里的城市字段去关联地图。
3D 跟踪部分的结构更复杂:按序列组织,每个序列下再分传感器。激光雷达是一堆二进制的点云帧,图像是若干路相机的 JPEG,标注文件是 feather 或 parquet 格式,标定参数是一个 JSON。这里最容易出错的是图像和点云的帧号对应关系——不是所有序列都是严格一一对应的,有时候某个相机掉帧,你得靠时间戳去对齐而不是靠序号。
argoverse_root/ ├── forecasting/ # 轨迹预测 │ ├── train/data/00000.csv ... │ ├── val/data/... │ └── test_obs/data/... ├── tracking/ # 3D 检测与跟踪 │ ├── train/seq_0000/ │ │ ├── lidar/*.ply │ │ ├── image/ring_front_center/*.jpg │ │ ├── annotations.feather │ │ └── vehicle_calibration_info.json │ └── test/... └── map/ # 高清地图 ├── PIT/... └── MIA/...提示:解压完先算一次校验和或者随手抽一个文件打开看看,别等训练跑了三个小时才发现数据是半截的。我就遇到过下载中断导致最后一个场景 CSV 只有表头的情况,损失函数直接 NaN。
2.2 轨迹预测 CSV 的六个字段到底怎么用
一个 CSV 里有六列,看起来简单,但每一列都有坑。时间戳是毫秒整数字,相邻采样点的间隔是固定的,但不同轨迹的起始时间戳不一样,你得先按轨迹分组再排序,不能全局排序。XY 坐标是城市坐标系下的米,不是经纬度,也不是以某个目标为原点的局部坐标,这点必须记牢——所有模型输入的归一化都得你自己做。城市字段只有两个取值,用来关联地图,做多城市训练时它是类别特征。轨迹 ID 是浮点数,注意它不是一个连续编号,同一个场景里可能只有几十个 ID,但数值可能很大。目标类型分三类:车辆、行人、其他,做检测头时不要把"其他"和背景搞混。
最关键的一点:CSV 里没有速度、加速度、朝向。这三个量全部要靠差分算出来。我一开始以为官方会提供,翻遍了文档才确认得自己算,而且算法不唯一——用一步差分还是多步平滑差分,会让速度序列的噪声水平差好几倍。
import pandas as pd import numpy as np df = pd.read_csv("00000.csv") df = df.sort_values(["TRACK_ID", "TIMESTAMP"]).reset_index(drop=True) # 按轨迹分组做差分,注意 dt 单位是毫秒,要换成秒 df["dt"] = df.groupby("TRACK_ID")["TIMESTAMP"].diff() / 1000.0 df["vx"] = df.groupby("TRACK_ID")["X"].diff() / df["dt"] df["vy"] = df.groupby("TRACK_ID")["Y"].diff() / df["dt"] df["heading"] = np.arctan2(df["vy"], df["vx"])2.3 高清地图的三层结构
地图数据最容易被误解成"一张图",实际上它是矢量的、分层的。最底层是车道段,每一段有自己的中心线折线、左右边界折线、以及若干属性标记(比如是不是路口内部、有没有红绿灯、限速方向)。往上一层是路口,它把若干个车道段串成一个拓扑,告诉你从哪条车道能进到哪个路口、从路口能出到哪条车道。再往上是可行驶区域,它是一个多边形,用来判断某个点是不是在合法行驶范围内。
这三层的查询方式完全不同。查车道段用坐标附近的最近邻,查路口用车道段之间的连接关系,查可行驶区域用点在多边形内的判定。你做轨迹合规性检查时用的是第三层,做行为预测(比如判断目标是否会左转)时用的是第二层。搞混了会得到很离谱的结果,比如明明在路口里,却查到了一条已经在身后的车道段。
注意:地图的坐标和轨迹的坐标必须来自同一个投影。不同批次的数据如果做过重新投影,混用会出现整体偏移几十米的情况。判断方法很简单:把轨迹点和地图车道线画在同一张图上,如果轨迹飘在车道外面,先怀疑坐标系,别怀疑标注。
3. 环境搭建与第一条轨迹的可视化
3.1 依赖环境怎么配才不打架
这套工具链的依赖主要集中在几何计算和点云处理上。Python 版本我建议固定在 3.8 到 3.10 之间,太新的版本容易在科学计算库的二进制包上翻车。核心依赖包括 pandas、numpy、shapely(做几何判定)、scipy(做插值和距离计算)、matplotlib(可视化)、以及一个做点云和 3D 框投影的库。
装的时候有个经验:先用 conda 建独立环境,再装依赖,别在系统 Python 里折腾。因为这套数据处理的库之间版本约束比较紧,尤其是几何库的大版本更新经常带来 API 变化。我试过一次在已有项目环境里直接装,结果把原本能跑的代码搞崩了,回滚了半天。
conda create -n av python=3.9 -y conda activate av pip install numpy pandas scipy shapely matplotlib pip install open3d # 看 3D 框和点云用 pip install argoverse-api # 官方工具包,版本号对齐你的数据版本官方的工具包实际提供了两个主要入口:一个是地图查询类,一个是数据加载类。新版包和老版包的命名空间不一样,老代码直接迁移过来大概率会报 ImportError。我的建议是:先确认你手上的数据是哪个版本,再去装对应版本的包,不要一上来就装最新。
3.2 加载并画出第一条轨迹
可视化是排查一切问题的起点。我习惯的第一步永远是:随便挑一个场景,把里面所有轨迹按 ID 分组,用不同的颜色画出来,看看形状是不是正常。如果画出来的轨迹是一堆散点、或者所有轨迹都挤在一个点上,说明字段读错了或者坐标有重复。
import matplotlib.pyplot as plt scene = pd.read_csv("00000.csv") fig, ax = plt.subplots(figsize=(8, 8)) for tid, g in scene.groupby("TRACK_ID"): g = g.sort_values("TIMESTAMP") ax.plot(g["X"], g["Y"], marker=".", markersize=2, label=str(tid)) ax.set_aspect("equal") ax.set_xlabel("X (m)") ax.set_ylabel("Y (m)") plt.show()正常的结果应该能看到几条或多条平滑的曲线,有的曲率小(直行),有的有明显拐弯。如果看到某条轨迹突然折返,先别激动,可能是同一轨迹 ID 中间断帧了——这种情况在原始数据里确实存在,处理时需要按时间戳的连续性做切分,而不是直接按 ID 分组。
3.3 归一化:为什么所有模型都在做坐标变换
几乎所有公开的基线模型都会做一步:把整场景的坐标平移到以目标对象为中心、并旋转到目标朝向朝正前方。这一步不是可选项,是必须项。
原因有两层。第一层是数值尺度。城市坐标系的绝对数值可能是几十万量级,直接喂给神经网络,梯度尺度会非常糟糕,学习率调都调不动。归一化到几十米的量级之后,训练稳定性会好一个台阶。第二层是平移旋转不变性。同一个场景,如果整体平移十米、旋转九十度,本质上是同一个交通场景,模型应该给出同样的预测。如果不做归一化,模型就得花大量容量去学这种"其实没变"的变换,纯属浪费。
def to_local(x, y, ox, oy, theta): """把城市坐标转到以 (ox, oy) 为原点、theta 为朝向的局部坐标""" c, s = np.cos(-theta), np.sin(-theta) dx, dy = x - ox, y - oy return c * dx - s * dy, s * dx + c * dy # ox, oy, theta 一般取目标对象最后一帧观测点的位置与朝向判断归一化做对没有,有个很省事的土办法:归一化之后,目标对象的历史轨迹应该从坐标原点出发,且最后一段切线方向大致朝正上方或者正右方(取决于你的约定)。如果画出来轨迹在原点附近打了个圈,说明朝向算错了,八成是 atan2 的参数顺序写反了。
4. 轨迹预测的完整实操流程
4.1 时间窗口与输入输出怎么定
这里是最容易出错的地方,因为不同版本的时间窗口和采样率不一样。老版本的设定是观察两秒、预测三秒,采样率十赫兹,也就是历史二十个点、未来三十个点。新版本把窗口拉长了,观察五秒、预测六秒,同样是十赫兹,历史五十个点、未来六十个点。
| 版本 | 采样率 | 历史窗口 | 未来窗口 | 历史帧数 | 未来帧数 |
|---|---|---|---|---|---|
| 早期版本 | 10 Hz | 2 s | 3 s | 20 | 30 |
| 后续版本 | 10 Hz | 5 s | 6 s | 50 | 60 |
为什么要把窗口拉长?因为三秒太短了,短到很多时候"未来"几乎就是当前速度的直线外推,模型学不到什么交互。五秒的历史能包含一次完整的变道或者一次路口排队,六秒的未来能覆盖穿过一个中等规模路口的全过程。窗口拉长之后,模型的输入复杂度上去了,但对长时序交互的建模能力要求也真正体现出来了。
实操上我建议:写死配置不要散落在代码各处。把历史帧数、未来帧数、采样间隔定义成三个常量,所有切片逻辑都引用它们。我见过有人一处写 20 一处写 50,训练的时候不报错,评估的时候指标莫名奇怪,查了一整天才发现是数据管道的长度对不上。
4.2 特征工程:从轨迹到张量的几步走
原始数据是长表,模型要的是定长张量。这中间要做四件事。
第一步是按场景切分。一个场景里可能有几十个目标,但真正要预测的只有被标定的那个关注对象,其余目标是上下文。你得先把关注对象挑出来。老版本没有显式字段,只能靠启发式规则,比如挑轨迹长度最长的、或者挑靠近场景中心的、或者挑速度最大的。这个启发式规则会直接影响你的成绩——挑错了,你训练的模型就是在预测一个边缘目标,而评估时用的是官方指定的另一个目标,结果自然对不上。新版本显式给出了关注目标的 ID,这一点省了很多事。
第二步是按时间切窗口。每个场景取前 N 帧作为历史,后 M 帧作为未来。边界情况要处理:历史不够长的目标直接丢掉,未来不够长的目标要标记出来,评估时不能算进指标。
第三步是归一化。以关注对象在历史最后一帧的位置和朝向为基准,把所有历史轨迹和上下文轨迹都变换过去。
第四步是补齐。上下文目标的历史长度参差不齐,要补齐到固定长度,同时给一个掩码标明哪些位置是真实的、哪些是填充的。掩码这一步极其关键,忘了加掩码,模型会把填充的零当成"目标停在原地",学出一个极度保守的预测器。
HIST, FUT = 50, 60 scene = scene.sort_values(["TRACK_ID", "TIMESTAMP"]) focal_id = scene["TRACK_ID"].iloc[0] # 老版本需要自己定规则 focal = scene[scene["TRACK_ID"] == focal_id].sort_values("TIMESTAMP") hist = focal.iloc[-HIST:] ox, oy = hist["X"].iloc[-1], hist["Y"].iloc[-1] theta = np.arctan2(hist["Y"].diff().iloc[-1], hist["X"].diff().iloc[-1])4.3 评估指标怎么算才不会和论文差一截
这套基准的指标有四个核心量:位移误差的平均值、位移误差的终点值、未命中率、以及可行驶区域合规率。前三个是误差类,第四个是规则类。
位移误差的平均值,是把每个预测步和真值之间的欧氏距离求平均。终点值只看最后一步。未命中率的定义是:终点误差超过一个阈值(通常是两米)的比例。因为模型会输出多条带置信度的候选轨迹,评估时一般对这些候选取最小值,也就是 minADE 和 minFDE。这里有个细节:取最小值的时候,用的是误差最小的那条,还是置信度最高的那条,结果差别很大。前者衡量的是"模型有没有想到正确方向",后者衡量的是"模型有没有押对注"。论文里通常报告前者,实际部署更关心后者。
可行驶区域合规率看的是预测终点有没有落在合法的可行驶多边形里。这个指标看起来简单,实现起来最容易出错:你的地图坐标和轨迹坐标必须是同一套,多边形判定的容差也要设对。我一开始按零容差做,结果合规率低得离谱,后来发现是地图要素在路口处有几十厘米的缝隙,正常车辆轨迹刚好从缝里穿过去,被判成了违规。加了半米容差之后数值就正常了。
提示:和别人对比数值之前,先确认三件事——用的是哪一版数据、用的是哪一套评估脚本、有没有做 min-over-k。这三项任何一项不一致,数值都不可比。
4.4 一个能跑通的最小 baseline
别一上来就上 Transformer。我强烈建议先做一个最笨的基线:把最后一帧的速度外推出去,得到一条直线轨迹,再复制六份当作多模态输出。这个基线的数值虽然不好看,但它是所有改进的参照物——如果你的复杂模型跑不过直线外推,那说明数据管道有问题,不是模型不行。
def constant_velocity(hist_xy, fut_len, dt=0.1): v = (hist_xy[-1] - hist_xy[-5]) / (4 * dt) return np.array([hist_xy[-1] + v * dt * (i + 1) for i in range(fut_len)])这个基线跑通之后,你会得到一批可用的中间产物:归一化后的历史张量、上下文掩码、真值未来。这些东西可以缓存下来复用,避免每次都重新解析 CSV。数据量大的时候,缓存能把迭代速度提升好几倍,这是我做了很久才养成的习惯。
5. 3D 检测与跟踪这条线的关键点
5.1 传感器配置与标定信息的读法
这条线的数据组织以序列为单位,每个序列对应一段连续行驶,包含同步的激光雷达点云和若干路环视图像。标定信息集中放在一个 JSON 里,分两部分:内参和外参。内参描述相机自身的焦距和光心,外参描述相机相对车体(或者相对激光雷达)的位置和姿态。
读外参最容易犯的错是搞错变换方向。外参矩阵通常有两份,一份是从车体到传感器,一份是从传感器到车体,互为逆矩阵。你在做点云投影到图像时,需要的是"点云坐标 → 相机坐标"这个方向。搞反了,投影出来的框会跑到图像外面去,你会以为是标定坏了。
调试方法很直接:找一辆明显在视野正中的车,把它的 3D 框八个顶点投影到图像上,看框是否贴合。不贴合就换个方向再试一次,一般两次之内就能定下来。
5.2 标注格式与跟踪 ID 的连续性
标注文件是按帧存的表,每行是一个目标在某一帧的 3D 框,字段包括中心点、尺寸、朝向、类别、以及跟踪 ID。这里有个很容易被忽略的点:朝向通常用四元数表示,转成偏航角的时候要注意正负号约定和旋转顺序。有些实现里四元数是 (w, x, y, z),有些是 (x, y, z, w),读错了会导致框的朝向整体转九十度,看起来像是标注错了,其实是解析错了。
跟踪 ID 的连续性体现在:同一个物理目标在整个序列里 ID 不变,中途离开视野再回来,ID 可能不变也可能变,取决于标注规则。做跟踪评估的时候,如果目标中途消失又出现,你的跟踪器复现的 ID 和标注 ID 对不上是正常的,评估脚本一般会做轨迹级别的匹配而不是逐帧比对。自己写评估的时候如果不做这个匹配,算出来的 ID 切换次数会虚高。
5.3 检测指标与跟踪指标的差异
检测只看单帧,衡量的是框的位置精度和类别正确性,常用的是不同 IoU 阈值下的平均精度。跟踪看的是整条轨迹,除了检测精度之外还要看 ID 保持能力。这两个指标的优化方向经常冲突:为了让检测更准,你可能会调低置信度阈值,多输出一些框,结果跟踪器就多出一堆碎片轨迹。
我在调这类系统时的经验是:先单独把检测调到满意,再冻结检测、只调跟踪的关联逻辑。反过来做,你永远不知道掉分是检测漏了还是关联错了。关联逻辑的核心是代价矩阵的设计,位置距离、尺寸相似度、朝向差异各占多少权重,没有通用答案,得按场景调。车辆的前后方向混淆是最常见的坑,因为很多车是前后对称的,单看框的形状分不出车头车尾,得靠运动方向来辅助判断。
6. 常见问题与排查实录
6.1 下载、解压与磁盘空间
这套数据的体积是劝退级别的。轨迹部分相对小,几个 G 级别;带传感器的部分轻松上百 G。我第一次下载的时候没算空间,解压到一半磁盘满了,解压出来的文件是损坏的但没有任何报错,直到训练时报了奇怪的格式错误才发现。
建议的做法是:先算总大小再决定放哪,解压后立刻抽查几个文件(点云能读、图像能开、标注能解析)。下载工具尽量用支持断点续传的,别用浏览器直接拖。我第一次用浏览器下了一个十几 G 的包,断在百分之九十九,白等两小时。
注意:有些压缩包是分卷的,必须全部下载完再解压,缺一卷解压工具可能不报错但内容不全。养成看文件列表里有没有 .part 或者 .001/.002 后缀的习惯。
6.2 版本混用带来的隐形错误
这是最阴的一类问题。老版本的包配新版本的数据,很多字段名对得上,代码能跑,但语义变了。比如时间戳的单位从毫秒变成纳秒,你的速度计算会整体偏小一百万倍;比如关注目标从启发式推断变成显式字段,你没改代码,用的还是老规则,指标就会莫名其妙地低。
我的做法是:在项目根目录放一个版本说明文件,写清楚用的哪一版数据、哪一版工具包、哪个提交号。每次换数据先跑一次冒烟测试——加载一个场景、算一遍指标、和上次的结果对比。数值差异超过预期就别往下走,先查版本。这个习惯帮我挡掉了很多次"代码没问题但结果就是不对"的迷之调试。
6.3 时间戳、坐标系、帧号的对不齐
三类对不齐的问题,我按踩坑频率排一下。
时间戳对不齐最典型的表现是:你按时间戳去索引另一路数据,结果查不到或者查到错位的。根源是不同传感器的采样时刻不完全相同,必须做最近邻匹配而不是精确相等匹配。匹配的容差要按采样间隔设,一般取半个采样周期。
坐标系对不齐的表现是:轨迹点和地图线整体错位一段距离,或者点云和图像投影对不上。根源通常是混用了不同的投影基准,或者把某一层数据做过后处理但另几层没做。排查方法还是可视化,画在一起一眼就能看出来。
帧号对不齐的表现最隐蔽:某一路数据中间丢了一帧,导致后续所有帧号错位,但文件命名是连续的。这种只能靠时间戳对齐来解决,不能信文件名。我的做法是永远用时间戳做主键,文件名只当提示。
6.4 常见问题速查表
| 现象 | 最可能的原因 | 排查动作 |
|---|---|---|
| 轨迹画出来是散点 | 未按 ID 分组排序 | 先 groupby 再 sort_values |
| 速度数值异常大或小 | 时间戳单位搞错(毫秒/纳秒) | 打印相邻时间戳差值确认 |
| 地图与轨迹整体错位 | 坐标系或投影版本不一致 | 叠图检查偏移方向与量级 |
| 训练损失直接 NaN | 数据含空文件或坐标未归一化 | 抽查文件 + 加归一化 |
| 评估指标比论文低很多 | 未做 min-over-k 或多版本混用 | 核对评估脚本与版本 |
| 投影框跑到图像外 | 外参矩阵方向搞反 | 求逆后重投影验证 |
| 合规率异常低 | 地图多边形有缝隙、容差为 0 | 加 0.5 m 缓冲再判定 |
| 跟踪 ID 频繁切换 | 检测漏帧或关联阈值过严 | 冻结检测单独调关联 |
7. 几个我踩过之后才明白的点
归一化这件事,看起来是数据预处理的一小步,实际决定了后面所有的上限。我一开始图省事没做朝向旋转,只做了平移,结果模型在预测曲率的时候一直学不好,改了归一化方式之后同样的网络结构指标涨了一截。原因是朝向不统一,模型必须额外花容量去学习"当前朝哪边"这个信息,而这本来是可以直接消掉的。
数据缓存比模型结构重要。前期我每次调参都重新跑一遍完整的数据解析,一个 epoch 光读数据就要很久。后来把归一化后的张量存成二进制,读一次几秒钟,迭代速度完全不是一个量级。这个投入在项目早期看起来是浪费时间,实际上是最划算的一笔。
指标对不上的时候,先怀疑自己的代码,最后才怀疑数据。我遇到过的指标异常,十次里有八次是自己实现有偏差——掩码没加、min-over-k 没做、终点取错帧。剩下两次确实是版本差异。所以排查顺序应该是:先和一个已知正确的实现逐行对比,再查数据版本,最后才考虑数据本身有问题。
场景可视化的价值被严重低估。一张叠了地图、历史轨迹、真值未来、预测未来的图,能在一秒钟内告诉你模型是"方向错了"还是"速度错了"还是"整体偏了"。数值指标只能告诉你差了零点几米,可视化能告诉你为什么差。我后来养成了一个习惯,每训练完一轮都随机抽二十个场景画图看一眼,比盯着 loss 曲线有用得多。
最后分享一个关于地图查询的小技巧:不要每次查询都重新加载地图对象。地图文件不小,加载一次要几秒,如果你在一个循环里对每个场景都加载一次,整个流程会被拖慢几十倍。正确做法是在进程启动时加载一次,全局复用。这个坑我在一个批处理脚本里踩过,本来五分钟能跑完的任务跑了两个小时,最后发现是重复加载地图。