自动驾驶-第三阶段-一段式端到端 + 扩散模型规划

第三阶段:一段式端到端 + 扩散模型规划

从两段式到一段式,从单轨迹回归到多模态生成

前置要求: 完成第二阶段(BEV感知 + VAD复现)
时间规划: 3周(每天2-4小时)
硬件要求: RTX 3090 24GB


本阶段你将掌握的核心技术

第一阶段学的:  单摄像头 → ResNet → steer/throttle(最简模仿学习)
第二阶段学的:  6摄像头 → BEV → 向量化表示 → waypoints(VAD,两段式)
本阶段要学的:  6摄像头 → 稀疏表示 → 扩散模型生成多条轨迹(一段式)
                ↑ 这就是当前工业界的主流方案

Week 1:从两段式到一段式——SparseDrive

Day 1:理解"一段式"和"两段式"的区别

什么是两段式?什么是一段式?

┌─ 两段式(UniAD、VAD)─────────────────────────────────────────┐
│                                                                │
│  第一阶段训练:感知模块(BEVFormer + 检测 + 建图)             │
│      ↓ 冻结权重                                                │
│  第二阶段训练:预测 + 规划模块                                  │
│                                                                │
│  问题:感知的错误无法被规划阶段纠正(误差单向传播)             │
└────────────────────────────────────────────────────────────────┘

┌─ 一段式(SparseDrive、DiffusionDrive)────────────────────────┐
│                                                                │
│  所有模块同时训练:感知 + 预测 + 规划 端到端联合优化            │
│                                                                │
│  好处:规划的loss可以反向传播到感知,让感知"学会"关注对规划     │
│  有用的信息(比如更关注前方即将变道的车,而不是路边的树)        │
└────────────────────────────────────────────────────────────────┘

类比理解:

两段式就像高考分两次考——先考语数外(感知),成绩定了就不能改,然后用这个成绩去选专业(规划)。如果语文考砸了,选专业时也没办法补救。

一段式就像综合评价——所有科目一起考、一起打分,语文不好可以用数学补,最终看总分。系统能自动找到最优的科目配比。

什么是"稀疏表示"?为什么比BEV Dense更好?

在第二阶段中,BEVFormer生成的是一个200×200×256的Dense BEV特征图——这意味着即使某个位置什么都没有(空地),也要计算和存储特征。这很浪费。

Dense BEV(BEVFormer/VAD):           Sparse表示(SparseDrive):
┌──────────────────────┐              只关注有意义的位置:
│██░░░░██░░░░░░░░░░░░░│               ● 车1 → 一个Query向量
│░░░░░░░░██░░░░░░░░░░░│               ● 车2 → 一个Query向量
│░░░░░░░░░░░░░░░░░░░░░│               ● 行人 → 一个Query向量
│░░░░░░░░░░░░░░░░░░░░░│               ● 车道线1 → 一组Query向量
│░░░░░░██░░░░░░░░░░░░░│               
│░░░░░░░░░░░░░░░░░░░░░│              总共可能只有100-300个Query
└──────────────────────┘              而不是200×200=40000个像素
200×200 = 40000个位置都要计算
大部分是空的(░),浪费算力

类比理解:

Dense BEV就像拍一张超大的全景照片,然后在照片的每个像素上都标注"这里有什么"——绝大部分像素标注的都是"空地"。

Sparse表示就像记笔记——只记录有意义的信息:"路口北边有一辆白色SUV在等红灯,距我30米"。信息量更小但更精准。

SparseDrive正是用这种稀疏表示,将检测、跟踪、建图、预测、规划统一在一个框架里,实现了一段式端到端训练。

Day 2-3:SparseDrive架构详解与代码复现

SparseDrive的整体架构

6个摄像头图像
    ↓
ResNet-50 提取多尺度图像特征
    ↓
┌─────────────────────────────────────────────────┐
│        对称稀疏感知(Symmetric Sparse Perception)│
│                                                   │
│  Detection Query ←→ 图像特征 → 检测3D目标         │
│  Map Query       ←→ 图像特征 → 在线建图           │
│                                                   │
│  两者共享相同的注意力结构(所以叫"对称")          │
└─────────────────┬───────────────────────────────┘
                  ↓
┌─────────────────────────────────────────────────┐
│         并行运动规划器(Parallel Motion Planner)  │
│                                                   │
│  Motion Query → 预测每个Agent未来轨迹              │
│  Planning Query → 生成自车多条候选轨迹             │
│  Collision-Aware Rescore → 选出最安全的那条         │
│                                                   │
│  预测和规划是并行的!不是串行的!                   │
└─────────────────────────────────────────────────┘
    ↓
输出:自车规划轨迹(waypoints)

关键概念:Instance Memory Queue(实例记忆队列)

SparseDrive为每个被跟踪的目标维护一个"记忆"——过去几帧它在哪里、长什么样。这就像你开车时脑子里记着"右边那辆红色宝马刚才在加速"。这个记忆让模型不需要每一帧都从零开始检测,大大提高了效率和时序一致性。

环境搭建与代码运行

# 克隆SparseDrive
git clone https://github.com/swc-17/SparseDrive.git
cd SparseDrive

# 创建环境
conda create -n sparsedrive python=3.8 -y
conda activate sparsedrive

# 安装依赖
pip install torch==1.13.0+cu116 torchvision==0.14.0+cu116 \
    --extra-index-url https://download.pytorch.org/whl/cu116
pip install -r requirement.txt

# 编译自定义算子
cd projects/mmdet3d_plugin/ops
python setup.py develop
cd ../../../

# 数据准备(同VAD,使用nuScenes)
ln -s /path/to/nuscenes ./data/nuscenes

# 生成标注pkl文件
python tools/data_converter/nuscenes_converter.py \
    --root-path ./data/nuscenes \
    --out-dir ./data/infos \
    --version v1.0-mini

下载预训练权重并运行评估

# 从SparseDrive的GitHub Releases下载预训练权重
# 放到 ckpts/ 目录下

# 运行评估
python tools/test.py \
    projects/configs/sparsedrive_small_stage2.py \
    ckpts/sparsedrive_stage2.pth \
    --eval bbox

Day 4-5:SparseDrive代码深度阅读

重点阅读三个文件

文件1:projects/configs/sparsedrive_small_stage2.py

这是配置文件,定义了整个模型的结构。重点关注:

# 理解这些关键参数的含义:
num_det_query = 900       # 检测用的Query数量(最多检测900个目标)
num_map_query = 200       # 地图用的Query数量
num_motion_mode = 6       # 每个Agent预测6种可能的运动模式
plan_num_future = 6       # 规划未来6个时间步的轨迹
planning_num_anchor = 1024 # 规划用的锚点轨迹数量

概念解释:Motion Mode(运动模式)

一辆车在路口可能直行、左转或右转——这就是3种"运动模式"。MotionFormer为每个Agent预测K种可能的模式,每种模式对应一条轨迹和一个概率。这就是"多模态预测"——不是预测一条确定的轨迹,而是预测多种可能性。

文件2:projects/mmdet3d_plugin/models/sparsedrive.py

这是主模型文件。关注forward_train()函数中各个模块的调用顺序:

# 伪代码
def forward_train(self, images, ...):
    # 1. 图像特征提取
    img_feats = self.backbone(images)      # ResNet-50
    img_feats = self.neck(img_feats)       # FPN多尺度特征

    # 2. 稀疏感知(检测 + 建图)
    det_output, map_output = self.sparse_perception(
        img_feats, det_query, map_query
    )

    # 3. 并行运动规划
    motion_output, plan_output = self.motion_planner(
        det_output, map_output, ego_status
    )

    # 4. 计算所有损失
    losses = self.compute_losses(det_output, map_output, 
                                  motion_output, plan_output)
    return losses

文件3:projects/mmdet3d_plugin/models/planning_head.py

这是规划头。关注它如何从1024条候选轨迹中选出最优的:

# 伪代码:分层规划选择(Hierarchical Planning Selection)
def select_best_trajectory(self, candidate_trajectories, scene_features):
    # Step 1: 用MLP为每条候选轨迹打分
    scores = self.scoring_mlp(candidate_trajectories, scene_features)

    # Step 2: 碰撞感知重打分
    for i, traj in enumerate(candidate_trajectories):
        if self.check_collision(traj, predicted_agent_trajectories):
            scores[i] *= 0.1  # 大幅降低碰撞轨迹的分数

    # Step 3: 选分数最高的
    best_idx = scores.argmax()
    return candidate_trajectories[best_idx]

概念解释:为什么用1024条候选轨迹?

这些候选轨迹是从训练数据中用K-Means聚类预先提取的"典型轨迹模式"。比如直行、左转30度、右转60度、急刹车等。模型不是从零开始生成轨迹,而是从这1024种"模板"中选一条最合适的并微调。这就像你学开车时,教练教你"这种路口一般这样过"——你不需要每次都重新发明开车方法。


Week 2:NAVSIM评测基准——行业标准的竞技场

Day 6-7:理解NAVSIM

NAVSIM是什么?为什么它这么重要?

NAVSIM是由Tübingen大学、NVIDIA、OpenDriveLab等联合推出的端到端自动驾驶评测基准(NeurIPS 2024)。它解决了一个行业痛点:

之前的评测方式:

开环评测(nuScenes L2 Error)          闭环评测(CARLA)
├── 简单快速                          ├── 最真实
├── 但不反映真实驾驶能力               ├── 但计算量巨大
└── 一个"瞎子"模型也能拿低L2          └── 且仿真和真实有差距

NAVSIM的方案:伪闭环评测(Pseudo-Simulation)

├── 用真实数据(来自nuPlan数据集的120小时驾驶日志)
├── 在BEV抽象空间中展开短期模拟(4秒)
├── 计算类似闭环的指标(碰撞、进度、舒适度等)
├── 但不需要真正的交互式仿真
└── 结果:计算量只有闭环的1/6,但相关性很高!

类比理解:

开环评测就像"看录像判断你的开车水平"——给你看一段路况视频,让你说"接下来方向盘怎么打",但你的回答不会影响视频播放。

闭环评测就像"真的让你上路"——你打方向盘会真的让车转弯,然后新的路况出现。

NAVSIM就像"桌面推演"——在地图上用小模型推演你的驾驶决策在接下来4秒会怎样,有没有撞车、有没有偏离车道。比看录像更真实,比真上路更高效。

NAVSIM的核心指标:PDMS(Planning-oriented Driving Metric Score)

PDMS是一个综合评分(0-100),由多个子指标加权组成:

PDMS = EP × (w1×NC + w2×DAC + w3×TTC + w4×Comfort + ...)

EP   = Ego Progress         自车行驶进度(你到底有没有在往前开?)
NC   = No Collision         没有碰撞
DAC  = Drivable Area        是否在可行驶区域内
TTC  = Time to Collision    距碰撞的时间裕度
Comfort = 舒适度            加速度/转向是否平滑

EP是乘法关系——如果你根本不动(EP=0),其他再好也是0分!

为什么EP很重要?

以前很多模型学到了一个"作弊"策略:原地不动就不会碰撞。在nuScenes的L2评测里,这种"怯场"模型反而能得到不错的分数。但在NAVSIM中,EP=0意味着总分=0——你必须往前开才能得分。这迫使模型学会真正的驾驶决策,而不是"苟住不动"。

Day 8-9:跑通NAVSIM Baseline

环境搭建

# 克隆NAVSIM
git clone https://github.com/autonomousvision/navsim.git
cd navsim

# 创建环境
conda create -n navsim python=3.9 -y
conda activate navsim

# 安装依赖
pip install -e .

# 下载数据(NAVSIM使用OpenScene/nuPlan数据的子集)
# 按照 docs/install.md 中的说明从HuggingFace下载
# 大约需要50-100GB空间

运行Baseline模型

NAVSIM提供了几个baseline,从简单到复杂:

# Baseline 1: EgoStatusMLP("瞎子"baseline)
# 只看自车速度/加速度,完全不看摄像头
# 用来理解"不看路也能得多少分"
python navsim/run.py \
    agent=ego_status_mlp \
    split=navtest

# Baseline 2: TransFuser(图像+LiDAR融合)
# 经典的端到端方法,使用CNN+Transformer
python navsim/run.py \
    agent=transfuser \
    split=navtest

惊人的发现: EgoStatusMLP这个"瞎子"模型在未过滤的数据上能拿到约91%的PDMS!这说明大多数驾驶场景都是"无聊"的直行——所以NAVSIM会对数据做过滤(filtering),只保留挑战性场景(急转弯、避障、路口等)。过滤后的数据上,"瞎子"模型的分数会大幅下降。

创建你自己的Agent

NAVSIM的Agent接口非常简洁:

from navsim.agents.abstract_agent import AbstractAgent
from navsim.common.dataclasses import Trajectory, TrajectorySampling

class MyAgent(AbstractAgent):
    """你的自定义端到端驾驶Agent"""

    def name(self) -> str:
        return "my_first_agent"

    def initialize(self) -> None:
        """加载模型权重"""
        self.model = load_your_model()

    def compute_trajectory(self, features) -> Trajectory:
        """
        核心方法:给定传感器输入,输出规划轨迹

        features包含:
        - features.camera_images: 多视角摄像头图像
        - features.ego_status: 自车速度、加速度
        - features.driving_command: 导航指令(直行/左转/右转)

        需要输出:
        - Trajectory: 未来4秒、10Hz的BEV位姿序列(40个点)
        """
        # 你的模型推理逻辑
        predicted_waypoints = self.model(
            features.camera_images,
            features.ego_status,
            features.driving_command
        )

        return Trajectory(
            poses=predicted_waypoints,  # (40, 3) 每个点有x, y, heading
            sampling=TrajectorySampling(
                time_horizon=4.0,  # 4秒
                interval_length=0.1  # 10Hz
            )
        )

实操建议: 先把EgoStatusMLP跑通,看看它的PDMS分数。然后试着写一个简单的改进版:在EgoStatusMLP的基础上加入前视摄像头图像(用第一阶段学的ResNet提特征),看PDMS能提高多少。这个"最小改进实验"能帮你快速理解NAVSIM的评测流程。


Week 3:扩散模型做规划——DiffusionDrive

Day 10-11:理解扩散模型的核心思想

什么是扩散模型?

扩散模型是一种生成模型——它能学会"创造"数据,而不只是"分类"数据。

扩散模型的两个过程:

正向过程(加噪声):逐步往干净数据上加随机噪声,直到变成纯噪声
清晰轨迹 → 稍微模糊 → 很模糊 → 完全看不出 → 纯随机噪声
   x₀    →    x₁    →   x₂   →    ...    →    xₜ

反向过程(去噪声):学习从噪声中恢复出干净数据
纯随机噪声 → 有点形状 → 越来越清晰 → 干净轨迹
   xₜ      →   xₜ₋₁   →    ...     →    x₀

类比理解(墨水类比):

想象你在白纸上画了一条漂亮的行车轨迹线(x₀),然后往上面不断泼水,墨迹越来越模糊(x₁, x₂...),最终完全变成均匀的灰色(xₜ,纯噪声)。

扩散模型要学的就是"逆向"这个过程——给你一张均匀灰色的纸,它能一步步把轨迹线"恢复"出来。关键是:这个恢复过程是有条件的——给不同的路况条件,它会恢复出不同的轨迹(直行、转弯、避障等)。

为什么用扩散模型做规划?——Mode Collapse问题

第一阶段和第二阶段中,我们用的都是回归方法(MSE Loss),让模型预测一条轨迹。但这有一个致命问题:

场景:你在路口,可以直行也可以右转,两种都是合理的。

训练数据中:
  - 50%的情况人类选择了直行
  - 50%的情况人类选择了右转

MSE回归模型的学习结果:
  - 预测一条"平均轨迹"——既不直行也不右转,而是斜着冲向路中间!

这就是 Mode Collapse(模式坍缩)——模型把多种合理选择"平均"成了
一种不合理的选择。
┌─ MSE回归 ──────────┐     ┌─ 扩散模型 ──────────────┐
│                     │     │                          │
│    ╱ 直行(GT)       │     │    ╱ 直行 (概率50%)       │
│   ╱                 │     │   ╱                      │
│  ●──── 预测(平均)    │     │  ●                       │
│   ╲                 │     │   ╲                      │
│    ╲ 右转(GT)       │     │    ╲ 右转 (概率50%)       │
│                     │     │                          │
│  ❌ 预测出不合理轨迹 │     │  ✅ 生成两种合理轨迹      │
└─────────────────────┘     └──────────────────────────┘

扩散模型能生成多条轨迹,每条对应一种合理的驾驶模式——直行的概率50%、右转的概率50%。然后用碰撞检测等安全约束从中选出最优的那条。

Day 12-13:DiffusionDrive详解——截断扩散策略

DiffusionDrive的核心创新

普通扩散模型需要20-50步去噪,太慢了(自动驾驶要求实时)。DiffusionDrive的核心创新是截断扩散(Truncated Diffusion)

普通扩散模型(20步去噪,太慢):
纯噪声 →→→→→→→→→→→→→→→→→→→→ 轨迹
 xₜ                              x₀
                20步

DiffusionDrive(2步去噪,实时!):
锚点轨迹 + 少量噪声 →→ 轨迹
    x₂                  x₀
         只需2步!

关键洞察: 人类的驾驶行为是有规律的——绝大多数情况下,你要么直行、要么转弯、要么换道。这些"典型行为"可以预先从数据中提取出来作为"锚点"(Anchor)。模型不需要从纯噪声开始生成轨迹,只需要从最接近的锚点开始,做少量调整即可。

类比理解: 普通扩散模型就像用粘土从零开始捏一辆车——需要很多步骤。DiffusionDrive就像拿一个已有的模型车做微调——换个颜色、调整一下轮子角度,两步就搞定了。

DiffusionDrive的完整架构

6个摄像头图像 + LiDAR BEV
         ↓
  Transfuser Backbone(图像+LiDAR特征融合)
         ↓
  场景特征 (Scene Context Features)
         ↓
┌────────────────────────────────────────┐
│         截断扩散解码器                   │
│                                        │
│  1. 从K个锚点轨迹中采样                 │
│  2. 加少量噪声 → 得到初始噪声轨迹        │
│  3. 级联去噪(2步):                    │
│     └→ 第1步:粗去噪 + 场景特征交互      │
│     └→ 第2步:精去噪 + 碰撞感知调整      │
│  4. 输出K条候选轨迹 + 置信度分数          │
│                                        │
│  选分数最高的轨迹作为最终输出             │
└────────────────────────────────────────┘
         ↓
  最优规划轨迹(waypoints)

跑通DiffusionDrive

# 克隆DiffusionDrive
git clone https://github.com/hustvl/DiffusionDrive.git
cd DiffusionDrive

# DiffusionDrive提供了NAVSIM和nuScenes两个版本
# 推荐先跑NAVSIM版本(main分支)

# 安装依赖(参考README)
pip install -r requirements.txt

# 下载预训练权重
# 从GitHub Releases下载

# 在NAVSIM上评测
python run_navsim.py \
    agent=diffusion_drive \
    split=navtest

Day 14:实操——自己搭建一个简化版扩散规划器

为了真正理解扩散模型如何生成轨迹,我们来写一个最简单的2D轨迹扩散模型:

"""
mini_diffusion_planner.py
一个最简化的扩散模型轨迹生成器
目的:帮你理解扩散模型的核心数学,而不是直接用别人的代码
"""

import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt

# ===== 1. 定义噪声调度 =====
def get_noise_schedule(T=20, beta_start=1e-4, beta_end=0.02):
    """
    线性噪声调度:随着时间步增加,噪声越来越大

    T: 总时间步数(这里用20步,比DiffusionDrive的2步多,便于理解)
    beta_t: 每一步添加的噪声量
    alpha_t: 1 - beta_t,表示保留的信号量
    alpha_bar_t: 所有alpha的累积乘积,表示从x₀到xₜ总共保留了多少信号
    """
    betas = torch.linspace(beta_start, beta_end, T)
    alphas = 1 - betas
    alpha_bars = torch.cumprod(alphas, dim=0)
    return betas, alphas, alpha_bars

# ===== 2. 正向过程:给轨迹加噪声 =====
def forward_diffusion(x0, t, alpha_bars):
    """
    一步到位地给x0加噪声到第t步

    公式:x_t = sqrt(alpha_bar_t) * x_0 + sqrt(1 - alpha_bar_t) * noise

    直觉:alpha_bar_t越小(t越大),噪声成分越大,信号成分越小
    """
    noise = torch.randn_like(x0)
    alpha_bar_t = alpha_bars[t].view(-1, 1, 1)  # (B, 1, 1)
    x_t = torch.sqrt(alpha_bar_t) * x0 + torch.sqrt(1 - alpha_bar_t) * noise
    return x_t, noise

# ===== 3. 去噪网络:学习预测噪声 =====
class NoisePredictor(nn.Module):
    """
    输入:带噪声的轨迹 x_t + 时间步 t + 场景条件 c
    输出:预测的噪声 epsilon

    这是一个非常简化的版本。真正的DiffusionDrive用的是
    级联Transformer解码器。
    """
    def __init__(self, traj_dim=12, hidden_dim=128, condition_dim=32):
        super().__init__()
        # 时间编码:把离散的时间步t编码成向量
        self.time_embed = nn.Sequential(
            nn.Embedding(100, hidden_dim),
            nn.SiLU(),
            nn.Linear(hidden_dim, hidden_dim)
        )
        # 条件编码:场景信息(简化版只用自车速度)
        self.cond_embed = nn.Linear(condition_dim, hidden_dim)
        # 主网络
        self.net = nn.Sequential(
            nn.Linear(traj_dim + hidden_dim * 2, hidden_dim),
            nn.SiLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.SiLU(),
            nn.Linear(hidden_dim, traj_dim)  # 输出维度和输入轨迹一样
        )

    def forward(self, x_t, t, condition):
        """
        x_t: (B, 6, 2)  带噪声的轨迹(6个点,每个点xy)
        t: (B,)          当前时间步
        condition: (B, 32) 场景条件
        """
        B = x_t.shape[0]
        x_flat = x_t.view(B, -1)              # (B, 12)
        t_emb = self.time_embed(t)              # (B, 128)
        c_emb = self.cond_embed(condition)      # (B, 128)
        inp = torch.cat([x_flat, t_emb, c_emb], dim=1)  # (B, 12+128+128)
        noise_pred = self.net(inp)              # (B, 12)
        return noise_pred.view(B, 6, 2)         # (B, 6, 2)

# ===== 4. 训练循环 =====
def train_diffusion(model, trajectories, conditions, epochs=500):
    """
    trajectories: (N, 6, 2) 真实的驾驶轨迹数据
    conditions: (N, 32) 对应的场景条件
    """
    T = 20
    _, _, alpha_bars = get_noise_schedule(T)
    optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

    for epoch in range(epochs):
        # 随机选一批数据
        idx = torch.randint(0, len(trajectories), (64,))
        x0 = trajectories[idx]        # (64, 6, 2)
        cond = conditions[idx]         # (64, 32)

        # 随机选一个时间步
        t = torch.randint(0, T, (64,))

        # 加噪声
        x_t, true_noise = forward_diffusion(x0, t, alpha_bars)

        # 预测噪声
        pred_noise = model(x_t, t, cond)

        # Loss:预测的噪声和真实噪声之间的MSE
        loss = nn.MSELoss()(pred_noise, true_noise)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if epoch % 100 == 0:
            print(f"Epoch {epoch}, Loss: {loss.item():.4f}")

# ===== 5. 反向采样:从噪声生成轨迹 =====
@torch.no_grad()
def sample_trajectory(model, condition, num_samples=10):
    """
    给定场景条件,生成多条候选轨迹

    这就是扩散模型的"多模态"能力:
    同一个条件下,每次采样都会生成不同的合理轨迹
    """
    T = 20
    betas, alphas, alpha_bars = get_noise_schedule(T)

    # 从纯噪声开始
    x = torch.randn(num_samples, 6, 2)  # (10, 6, 2) 10条轨迹
    cond = condition.unsqueeze(0).expand(num_samples, -1)  # (10, 32)

    # 逐步去噪
    for t in reversed(range(T)):
        t_tensor = torch.full((num_samples,), t, dtype=torch.long)

        # 预测噪声
        pred_noise = model(x, t_tensor, cond)

        # 去噪一步(DDPM采样公式)
        alpha_t = alphas[t]
        alpha_bar_t = alpha_bars[t]

        x = (1 / torch.sqrt(alpha_t)) * (
            x - (betas[t] / torch.sqrt(1 - alpha_bar_t)) * pred_noise
        )

        # 除了最后一步,都要加一点噪声(保持随机性)
        if t > 0:
            x += torch.sqrt(betas[t]) * torch.randn_like(x)

    return x  # (10, 6, 2) 10条候选轨迹

运行这个脚本后,你应该做什么?

  1. 用第一阶段在CARLA中采集的数据(轨迹坐标)作为训练数据
  2. 训练完后,给同一个场景条件多次采样,观察生成的10条轨迹是否呈现多样性
  3. 可视化这些轨迹——你应该能看到直行、微左转、微右转等不同模式
  4. 把最好的轨迹(比如离真实轨迹最近的)作为规划输出

Day 15:Flow Matching vs DDPM

什么是Flow Matching?

GoalFlow(CVPR 2025,地平线)使用的不是DDPM,而是Flow Matching——一种更新、更高效的生成方法。

DDPM(DiffusionDrive用的):
  - 加噪声过程是随机的马尔可夫链
  - 去噪需要多步迭代
  - 数学上:学习预测噪声 ε

Flow Matching(GoalFlow用的):
  - 加噪声过程是确定性的直线插值
  - 理论上1步就能生成
  - 数学上:学习预测速度场 v

直觉区别:
  DDPM像是在迷宫里随机走,需要很多步才能走出来
  Flow Matching像是画一条直线连接起点和终点,一步到位

对你面试的意义: 面试官经常问"DDPM和Flow Matching的区别是什么?"。答出"DDPM学预测噪声ε,Flow Matching学预测速度场v;Flow Matching的采样路径是直线,训练更稳定,采样更高效"就够了。


总结与下一步

第三阶段学到了什么?

周次 内容 核心概念
Week 1 SparseDrive 一段式vs两段式、稀疏表示、并行规划
Week 2 NAVSIM 伪闭环评测、PDMS指标、创建Agent
Week 3 DiffusionDrive 扩散模型、Mode Collapse、截断扩散、Flow Matching

你现在的技术栈

第一阶段 ✅  CARLA仿真 + 简单模仿学习 + 闭环测试
第二阶段 ✅  BEV感知 + VAD端到端 + nuScenes评测
第三阶段 ✅  一段式端到端 + 扩散模型规划 + NAVSIM评测
第四阶段 →   VLA + 强化学习 + 面试准备(下一步!)

面试中你可以讲的项目经历

到这里你已经有一个完整的故事线可以讲了:

"我从CARLA仿真入手搭建了端到端自动驾驶的闭环pipeline,然后在nuScenes上复现了BEVFormer和VAD,理解了从BEV Dense到Sparse表示的演进。接着在NAVSIM基准上实现了基于扩散模型的多模态轨迹规划,理解了为什么DiffusionDrive的截断扩散策略能在只用2步去噪的情况下达到88.1 PDMS。"

这个叙事覆盖了感知→预测→规划的完整技术栈,且每一步都有代码实现和评测结果支撑,在面试中非常有说服力。

第四阶段预告:VLA + 强化学习

下一阶段将进入当前最前沿且招聘需求最旺盛的方向:

  • ORION(小米开源VLA)推理与评测
  • Qwen2.5-VL 做驾驶场景理解
  • 多层次CoT SFT(链式思维监督微调)
  • GRPO/RLHF 强化学习微调驾驶策略
  • 模型部署(TensorRT加速、ONNX导出)

为者常成,行者常至