Closed-Loop Eval: The Bench2Drive Protocol & Case Studies
| 维度 | 开环 (Open-loop) | 闭环 (Closed-loop) |
|---|---|---|
| 数据来源 | 回放预录数据集 | 实时仿真,Agent 决策驱动 |
| Agent 行为影响环境? | 否 | 是 |
| 评测指标 | L2 轨迹偏差(ADE/FDE) | Driving Score(路线完成 + 违规惩罚) |
| 协议感受性 | 不暴露累积误差 | 暴露所有累积误差 |
| 计算成本 | 低(一次前向) | 高(持续运行 CARLA) |
开环评测的流程:给定 nuScenes/NAVSIM 的图像序列 → 模型预测轨迹 → 和数据集里记录的真实轨迹比 L2 误差。问题:
模型在第 帧预测了一条偏离 GT 的轨迹,但第 帧的输入仍然是 GT 轨迹对应的图像——回放数据集不会因为模型走偏而改变。这掩盖了一个致命问题:累积误差。
真实驾驶里,第 帧的偏离会让第 帧看到完全不同的场景(车开到了别的位置),进而触发连锁反应。开环评测看不到这种连锁——它在”平行宇宙”里评测,假设模型永远在 GT 轨迹上。
两条轨迹 L2 误差都是 0.5 米,但:
L2 误差是平均距离,抹平了场景依赖性。开环没法区分这两种情况。
闭环评测里,模型的每个决策真实执行:
apply_control → CARLA 物理执行 → Ego 真的移动。这种”决策影响环境、环境影响下一帧决策”的循环,是真实驾驶的本质。闭环评测忠实反映了模型的实际驾驶能力,包括累积误差、长尾失败、对场景的鲁棒性。
闭环评测远比开环贵:
这是为什么很多方法只做开环——闭环太贵。SparseDriveV2 是少数两种都做的方案,这本身是工程实力的体现。
闭环评测特有的失败(开环看不到):
EP06 的失败案例(ALG 域差、SCN 场景几何)就是闭环才暴露的——开环平均分数高,但闭环里某些场景直接失败。
Bench2Drive(2024 年 arXiv,NeurIPS 2024)是建立在 CARLA Leaderboard 2.0 之上的标准化闭环考试协议。它的目标是:给端到端自动驾驶模型一个统一、可复现、覆盖面广的闭环评测。
协议核心是把”跑一圈看分数”这个黑盒标准化成五步流水线:
① 加载 checkpoint → ② 连 CARLA + 同步模式 → ③ 加载路线 → ④ Agent 自动驾驶 → ⑤ 汇总分数
Bench2Drive 预定义了 220 条测试路线,覆盖 44 种交互场景。每条路线是一条从起点到终点的行驶路径,沿途会触发预定的场景(由 ScenarioRunner 的行为树编排)。
44 种场景覆盖自动驾驶的主要交互类型:
| 类别 | 场景示例 |
|---|---|
| 切入/切出 | cut_in_left、cut_in_right、opposite_lane_cut_in |
| 横穿 | object_crossing、pedestrian_crossing |
| 路口 | junction_left_turn、junction_right_turn、signalized_junction |
| 超车/让行 | overtake、yield_to_emergency_vehicle |
| 静态障碍 | static_object、construction |
| … | 共 44 种 |
220 条 = 44 种场景 × 5 条左右变种(不同地图、不同天气、不同触发位置)。这种覆盖让评测不只看平均分,还能看每种场景的专项能力——模型在哪类交互上弱。
model = SparseDriveV2(cfg)
model.load_checkpoint('sparsedrive_v2.pth')
model.eval()
加载训练好的模型权重,设成 eval 模式(关闭 dropout/batchnorm 更新)。
client = carla.Client('localhost', 2000)
world = client.get_world()
settings = world.get_settings()
settings.synchronous_mode = True
settings.fixed_delta_seconds = 0.05 # 20 Hz
world.apply_settings(settings)
同步模式是闭环评测的前提(EP03 详述)——保证时序确定、可复现。
route = load_route('Town04_cut_in_right_001.xml')
tm = client.get_trafficmanager(8000)
tm.set_synchronous_mode(True)
scenario_runner = ScenarioRunner(world, route, tm) # 行为树编排 NPC
路线 XML 定义了起点、终点、沿途触发的场景。ScenarioRunner 把这些转成 py_trees 行为树,按 Ego 的位置触发对应 NPC 动作。
每 50 ms(一帧):
while not route_finished:
world.tick() # 推进仿真
images = collect_six_cameras() # 6 路图像
trajectory = model.infer(images) # 模型推理
control = pure_pursuit(trajectory, ego) # 轨迹跟踪
ego.apply_control(control) # 执行控制
check_violations(frame) # 违规检测
Agent 完全自主决策,没有任何 GT 辅助——这就是”考试”。
220 条路线跑完,每条都有 Route Completion 和 Infraction 记录,汇总成三个核心分数(详见 B06):
220 是覆盖度和评测成本的平衡:
220 条让每种场景平均 5 条,统计上稳定,又能在一夜跑完。这是 Bench2Drive 工程上的妥协。
每帧(50 ms)的完整数据流,和 EP03 的 tick 循环一致,但闭环评测额外强调违规检测:
world.tick() ① 仿真推进 0.05s
│
▼
6 相机同步采集 ② 传感器采样(同 frame ID)
│
▼
SparseDriveV2 推理 ③ 感知 → 建图 → 规划
│
▼
Pure Pursuit → apply_control ④ 轨迹转控制,发给 Ego
│
▼
check_violations(frame) ⑤ 违规检测(每帧都查)
│
▼
(下一帧)
注意违规检测是每帧进行的,不是事后总结。碰撞检测器、Lane Invasion 检测器在事件发生那一帧就触发回调,把违规记进日志。这是闭环评测能精确归因的前提。
| 违规 | 检测方式 | 触发时机 |
|---|---|---|
| 碰撞(车/人/物) | sensor.other.collision 检测器 | 碰撞瞬间 |
| 压线 | sensor.other.lane_invasion 检测器 | 轮子压线 |
| 闯红灯 | 协议层判断(红绿灯状态 + 停车线) | Ego 越线时 |
| 偏离路线 | 协议层判断(Ego 位置 vs 路线多边形) | 持续偏出 |
| 超时 | 时间统计 | 超过路线时限 |
前两个用 CARLA 的物理事件检测器(EP02 详述),后三个是 Bench2Drive 协议层自己实现的——因为它们依赖 OpenDRIVE 拓扑和路线定义,CARLA 单个传感器判不了。
闭环评测不是跑一条就完,是220 条全跑:
results = []
for route_xml in all_220_routes:
reset_world(world) # 重置仿真状态
load_route(route_xml) # 加载路线和场景
route_result = run_one_route(model, world, route_xml) # 跑完整条
results.append(route_result)
final_score = aggregate(results) # 汇总成 DS/RC/SR
每条路线之间要彻底重置:
漏掉重置会导致状态泄漏(上一轮的 NPC 还在,干扰下一轮),分数不可信。
Bench2Drive 要求同一 checkpoint 两次评测分数接近。这要求:
即使如此,闭环评测仍有少量随机性(浮点累积、TM 内部抖动),所以严格对比时要多次跑取平均。
每帧必须在 50 ms 内完成推理和控制,否则:
tick() 卡住 → 仿真冻结(EP03 详述)。SparseDriveV2 的 sparse + scoring 设计让推理能在预算内完成(详见 EP05 的 Coarse-to-Fine)。如果模型推理要 100 ms,要么被迫降到 10 Hz(控制变糙),要么超时被判失败。这是为什么”轻量高效”不只是工程优化,是闭环可行的硬要求。
Bench2Drive(继承 CARLA Leaderboard 2.0)的最终分数:
注意是乘法——一次严重违规就能把分数砍半,不会因为”开得远”被稀释。这体现了”既要开得远,又要开得安全”。
来自 CARLA Leaderboard 2.0 的标准定义(Bench2Drive 完全沿用):
| 违规类型 | 惩罚系数 | 说明 |
|---|---|---|
| 碰撞行人 | 最严重 | |
| 碰撞车辆 | ||
| 碰撞静态物体 | ||
| 闯红灯 | ||
| 闯 stop sign | ||
| 偏离路线车道 | (按时间窗累乘) | 持续违规 |
碰撞是一次性事件:发生一次就乘一次系数。压线/偏离是持续违规:每持续一段时间(时间窗)就乘一次 ,所以压线越久惩罚越重:
假设一条路线:
满分 1.0,这一条路线只有 0.342。可见违规的乘法惩罚非常严厉——一次撞车 + 一次闯灯就砍到三分之一。这就是为什么闭环评测区分”开得远”和”开得好”。
超时不进乘法公式,而是直接判该路线零分:
为什么这么严?因为超时通常意味着模型卡住了(在路口死等、原地不动),这种”开不动”比”开得慢但安全”严重得多——它根本没在完成任务。乘法惩罚对超时不合适(超时 RC 可能还很高),所以单独判零。
Bench2Drive 在 Leaderboard 2.0 基础上做了一处调整:移除了 minimum speed penalty(最低速度惩罚)。原 Leaderboard 2.0 会惩罚开得太慢(低于某速度就扣分),但 Bench2Drive 认为端到端模型安全优先,慢一点不该扣分,所以去掉了这条。
另外 Bench2Drive 把 TickRunTime(单路线最大帧数)从 2000 扩展到 4000,给模型更多时间完成长路线。
汇总 220 条路线后,得到三个指标:
| 指标 | 计算 | 含义 |
|---|---|---|
| Driving Score (DS) | 所有路线 DS 的平均 | 综合能力 |
| Route Completion (RC) | 所有路线 RC 的平均 | 开得远不远 |
| Success Rate (SR) | 完整完成路线的比例 | 完成能力 |
SparseDriveV2 的 DS 89.15 意味着平均每条路线的”完成率 × 违规惩罚”是 0.8915——这是个非常高的分数(多数方案在 0.5–0.7)。
Bench2Drive 的自动诊断脚本(verify_batch)把每个失败 case 归到三类身份(EP07 详述):
ALG 类失败的特征是:模型在某个数据分布外的场景上系统性失败,不是偶发 bug。
Town04 的弯道路段,Ego 进入弯道后:
steer 全程为 0(控制层从未发转向指令)。这是一个典型的”模型看不见弯道”的失败。
SparseDriveV2 在 nuScenes 和 NAVSIM 数据上训练。这两个数据集的特点:
模型从这些数据学到的”驾驶先验”是:大部分时候直行就行,偶尔微调方向。它在训练分布内表现优秀(NAVSIM EPDMS 90.1),但分布外(OOD)的 CARLA Town04 急弯上,模型没见过这种几何,输出的轨迹自然偏向它熟悉的”直行”。
形式化地说,设训练数据分布为 ,测试分布为 。当 含有 里几乎没有的弯道样本时,模型在这些样本上的输出不可信——这是经典的**协变量偏移(covariate shift)**问题。
steer 由 Pure Pursuit 根据模型输出轨迹计算(EP03 详述):
如果模型输出的轨迹是直线(前方所有目标点都在 Ego 正前方),那么前瞻点的横向偏移 ,于是 ——纯跟踪忠实地把”直线轨迹”转成”零转向”。所以 steer = 0 是控制层忠实地执行了模型的(错误)决策,不是控制器 bug。
这印证了 ALG 诊断:问题在模型的轨迹输出,不在工程管道。
车进入弯道后,模型输出直线 → 车继续直行 → 物理上车冲出弯道 → off-road。一旦 off-road,后续帧的图像全是路外场景(草地、护栏),模型更没见过,继续输出直线——形成恶性循环。81% tick off-road 说明失败一旦发生就不可恢复,模型没有”我已经偏了,赶紧修正”的能力。
这也是闭环评测特有的现象:开环里走偏了下一帧还是 GT 图像,闭环里走偏了就真的回不来。
视频给的解法是轻量微调:
这是处理域差的常用方法——目标域微调(target domain fine-tuning)。代价是:需要采集 CARLA 弯道数据(带 GT 监督),且微调过多可能牺牲原训练分布上的性能(灾难性遗忘)。
ALG 域差是端到端自动驾驶的核心挑战:
cut_in_right 这条 case 看上去也是 Ego 表现不好,但诊断脚本把它归为 SCN(场景几何问题)——不是模型的错,是 Bench2Drive 的场景设计在这个路段失效了。区分 ALG 和 SCN 是诚实评测的关键:不能把场景设计的锅扣到模型头上。
仔细看 NPC 的轨迹:
也就是说,NPC 离 Ego 越来越远,根本没有”切入”这个动作发生。Ego 的减速其实是合理的(看到右侧有车接近,谨慎减速),但场景设计的”NPC 切入 Ego 前方”这个考题根本没被执行。
这是 EP03 提过的同一类问题的具体表现。cut_in_right 场景由 ScenarioRunner 的行为树编排,行为树里有个 ChangeLane 节点要求 NPC 切到 Ego 车道。但这个节点依赖 CARLA 在该路段生成合法的 LaneChange waypoint——即 OpenDRIVE 里两个相邻车道之间有合法的变道拓扑。
Town04 的这段弯道上,OpenDRIVE 的车道定义没有提供 LaneChange 拓扑(可能是弯道曲率太大、或车道类型不允许变道)。CARLA 拒绝生成变道 waypoint,于是:
ChangeLane 节点永远返回 FAILURE。形式化地说,场景设计假设了一个前提” 合法 LaneChange 拓扑”,但这个前提在特定路段不成立,整个场景就空转了。
弯道的几何特性:两辆车沿不同曲率的车道走时,弯道越深,车道之间的横向距离变化越大。NPC 在外侧车道、Ego 在内侧车道,沿弯道前进时两者的横向距离因弯道几何被动拉开——这不是 NPC 主动远离 Ego,是几何必然。
12 米意味着 NPC 已经离 Ego 很远,根本构不成”切入”场景。诊断脚本看到”NPC 从未进入 Ego 车道”,判定为 SCN。
区分的关键:模型有没有做错。
把这种 case 归为 ALG 会冤枉模型——模型的实际能力被低估了。SCN 诊断的意义就是剔除场景设计缺陷造成的虚假失败,让分数反映模型真实能力。
SCN 类问题暴露了仿真评测协议本身的局限:
ChangeLane 依赖 CARLA 的 waypoint 生成,而 waypoint 依赖 OpenDRIVE 拓扑——多层依赖,任一层失效场景就空转。EP07 的整个调试主线就是修这类 SCN 问题——让场景真的发生它该发生的交互,否则评测分数没有意义。
SparseDriveV2 的训练分两个阶段,这是端到端模型的标准做法(UniAD、VAD 等都用类似策略):
阶段 1:感知预训练 阶段 2:端到端联合训练
───────────────── ─────────────────────
冻结规划模块 解冻所有模块
只训练感知(检测+建图) 梯度从规划回传到感知
100 epoch 10 epoch
目标:感知充分收敛 目标:联合优化,感知为规划服务
如果从一开始就让所有模块(感知 + 规划)一起训练,会遇到两个问题:
规划模块需要的输入是有意义的感知特征。但训练初期感知模块输出的是垃圾(随机初始化),规划模块基于垃圾特征学规划,梯度回传给感知的信号也是混乱的——“我应该怎么调整感知才能让这个垃圾规划更好?” 没有意义。两个模块互相拖累,训练不收敛。
感知 loss(检测 mAP)和规划 loss(轨迹误差)在训练初期方向可能冲突。感知模块同时被两个目标拉扯,收敛慢。
# 伪代码
freeze(planning_module)
for epoch in range(100):
for batch in dataloader:
images, gt_det, gt_map = batch
det_pred, map_pred = perception_module(images)
loss = det_loss(det_pred, gt_det) + map_loss(map_pred, gt_map)
loss.backward()
optimizer.step()
只训练感知模块(detection + mapping),用它们各自的 GT 监督。100 个 epoch 让检测和建图充分收敛——这一阶段结束时,感知模块能输出高质量的目标和地图。
这相当于让”感知老师”先学会本职工作,再去配合”规划学生”。
# 伪代码
unfreeze(planning_module) # 解冻所有模块
for epoch in range(10):
for batch in dataloader:
images, gt_det, gt_map, gt_traj = batch
det_pred, map_pred, plan_traj = full_model(images)
loss = (det_loss(det_pred, gt_det)
+ map_loss(map_pred, gt_map)
+ motion_loss(...)
+ plan_loss(plan_traj, gt_traj))
loss.backward()
optimizer.step()
解冻所有模块,规划 loss 的梯度回传到感知。因为感知已经预训练好(输出有意义特征),规划模块能基于稳定输入学习,梯度信号清晰。
只训 10 epoch 是关键——感知已经收敛,不需要再大改;规划模块在好的感知基础上少量迭代就能学会。过多 epoch 会过拟合或灾难性遗忘(感知为了规划牺牲检测精度)。
| 模型 | GPU | 时长 | 总 GPU 小时 |
|---|---|---|---|
| UniAD | A100 × 8 | ~144h | ~1152 |
| SparseDriveV2 | L20 × 8 | ~10h | ~80 |
效率提升超过一个数量级(~14×)。原因:
注意 L20 和 A100 算力不同(A100 更强),即使折算后 SparseDriveV2 的训练效率仍显著优于 UniAD。这是 sparse 范式在训练侧的红利——不只是推理快,训练也快。
L20 是 48GB 显存、算力介于 A10 和 A100 之间的 GPU。SparseDriveV2 能在 L20 上训是因为:
这让 SparseDriveV2 的训练更亲民——不需要顶配 A100/H100 集群,普通实验室的 L20/A10 就能复现。
SparseDriveV2 的总损失是五个分量的加权和:
| 分量 | 监督什么 | GT 来源 |
|---|---|---|
| 检测:3D 包围盒(位置/尺寸/朝向/速度)+ 类别 | 数据集标注 / CARLA Actor 状态 | |
| 建图:20 点 polyline | 矢量化地图标注 | |
| 运动预测:NPC 未来轨迹 | 数据集记录的真实未来轨迹 | |
| 规划:Ego 未来轨迹 | Ego 实际走过的轨迹 | |
| 深度监督(辅助):图像深度 | CARLA Depth 相机 / 立体匹配 |
运动预测和规划都输出多个候选(运动预测对每个 NPC 输出多条轨迹,规划在 26 万候选里打分)。直接对所有候选算 loss 会让模型困惑——“我应该让哪一条接近 GT?”
WTA 的解法:只有最接近 GT 的那个 mode(候选)参与 loss:
即从所有候选 mode 里选 ADE(平均位移误差)最小的那个,只对它算回归 loss。
这个设计的意义:
对 SparseDriveV2 的规划来说,WTA 体现在:在 26 万候选里,只让最接近 GT 轨迹的那条候选的分数最大化(其他候选作为负样本对比)。这就是 B08 提到的对比学习式 loss。
是辅助任务——它不直接服务最终规划,但帮助 backbone 学到更好的几何表示。
形式化地说,让 Image Encoder 同时预测每个像素的深度:
来自 CARLA 的 Depth 相机(训练时有,推理时无)。这个 loss 强迫 backbone 从单目图像学到深度感知能力——这对后续 3D 几何任务(检测、规划)有迁移帮助。辅助任务用较小的 ,避免喧宾夺主。
理解 loss 的关键是知道每个分量的 GT 从哪来:
| 分量 | 训练数据 GT 来源 | 推理时可见? |
|---|---|---|
| 检测 | nuScenes 3D 标注 | 否 |
| 建图 | 矢量化地图 | 否 |
| 运动预测 | 数据集记录的未来轨迹 | 否 |
| 规划 | Ego 实际未来轨迹 | 否 |
| 深度 | CARLA Depth / 立体匹配 | 否 |
所有 GT 都只在训练时用。推理时模型只看 6 张 RGB(EP02 强调过)。这是端到端的标准约束——GT 是训练的老师,不是考试的答案。
所有 loss 一起回传,梯度共享 backbone:
backbone 同时被五个任务优化——它学到的特征要同时利于检测、建图、运动预测、规划、深度。这种多任务正则化让 backbone 学到更通用、更鲁棒的低层表示,比单任务训练泛化性好。
这就是端到端优于模块化的根本原因:模块化方案里每个模块独立训练自己的 backbone(或共享但目标单一),而端到端用一个 backbone 同时服务所有任务,特征利用效率高得多。
min 操作不可微,要用软最小(softmax 加权)或 hard 选择 + 直通梯度(straight-through)。| 维度 | UniAD | VAD | SparseDrive v1 | SparseDriveV2 |
|---|---|---|---|---|
| 表示 | Dense BEV | Vectorized(仍需 BEV encoder) | Sparse query | Sparse query |
| 规划 | 单条回归 | 单条回归 | 6 条生成 | 26 万打分 |
| Backbone | ResNet-101 (44.5M) | ResNet-50/101 | ResNet-50 | ResNet-34 (21.8M) |
| 训练 | A100×8, ~144h | 中等 | 中等 | L20×8, ~10h |
| 推理 | ~1.8 FPS | 较快 | 实时 | 实时(>20 Hz) |
| 评测 | 开环 | 开环 | 开环 | 闭环 + 开环 |
每一行都是从重到轻、从慢到快、从开环到闭环的演进。
这条主线是计算量的逐级压缩:
Sparse 的计算量比 Dense 小约 4 个数量级(详见 EP04 B05)。表示层面的稀疏化是效率提升的根本来源——backbone 能瘦身、训练能加速、推理能实时,都源于此。
v1 的 6 条生成轨迹 vs V2 的 26 万候选打分,是优化难度的根本降低:
打分式的前提是候选池足够密,V2 的 Factorized Vocabulary 解决了这个前提(EP05 B08)。
ResNet-101 (44.5M) → ResNet-34 (21.8M),参数量减半。这看似只是工程优化,实则是 sparse 范式的红利:
backbone 瘦身直接带来训练快、推理快、显存省。
14× 加速来自三个因素叠加:
这是范式红利,不是单点 trick——同样的算法用更重的 backbone 训练会慢,但 SparseDriveV2 不需要重 backbone。
UniAD、VAD、SparseDrive v1 都主要做开环(nuScenes planning)。SparseDriveV2 是唯一在 CARLA 上做真正闭环评测的。
这不是小事——闭环评测:
SparseDriveV2 能做闭环,前提是它够快(实时推理)+ 够鲁棒(不崩溃)。这反过来验证了 sparse + scoring 范式的工程可行性——不只是 paper 数字好看,是真的能开车。
除了上述硬指标,还有一个软维度:可调试性。
这种可解释性在工程迭代里价值巨大——EP07 的失败分类(ENG/ALG/SCN)就依赖这种可调试性。