端到端自动驾驶:从感知到控制

智能无人车核心课程 · 大学前沿篇

第一章:端到端范式的崛起

1.1 模块化 vs 端到端

传统自动驾驶采用模块化流水线:

传统模块化架构:

摄像头/激光雷达 → [感知模块] → 目标列表
                                      ↓
高精地图 + 定位 → [预测模块] → 轨迹预测
                                      ↓
                              [规划模块] → 期望轨迹
                                      ↓
                              [控制模块] → 方向盘/油门/刹车

问题:

  • 模块间信息传递有损(感知只输出目标框,丢失了原始场景的丰富信息)
  • 误差逐级累积(感知误差 → 预测偏差 → 规划失败)
  • 难以处理"长尾场景"(训练数据中罕见的情况)

1.2 端到端的核心思想

端到端架构:

原始传感器输入 → [神经网络] → 直接输出控制指令

核心优势:
├── 信息无损:直接从原始像素/点云学习到最终控制
├── 全局优化:损失函数直接定义在最终任务上
├── 隐式学习:网络自动学习中间表示(无需人工定义"目标检测""语义分割"等子任务)
└── 泛化能力:对未见过的场景组合有更好的鲁棒性

1.3 端到端自动驾驶发展史

年份系统机构核心贡献
2016End-to-End LearningNVIDIA首次用 CNN 从摄像头直接预测转向角
2019Learning by CheatingPrinceton用"上帝视角"训练,再迁移到真实场景
2020L5KitToyota Research大规模自动驾驶数据集与学习框架
2021UniAD 前身上海 AI Lab统一感知-预测-规划框架
2022FSD BetaTesla纯视觉端到端,大规模量产部署
2023UniAD上海 AI Lab首个端到端感知-规划联合优化框架
2023DriveVLM理想汽车视觉语言模型驱动的驾驶决策
2024VAD / SparseDrive多机构高效稀疏特征表示的端到端规划

第二章:NVIDIA 的开创性工作

2.1 原始端到端模型(2016)

# NVIDIA 端到端驾驶网络(简化版)
# 输入:3 个摄像头的图像(左/中/右)
# 输出:转向角

import torch
import torch.nn as nn

class NVIDIAEndToEnd(nn.Module):
    def __init__(self):
        super().__init__()
        # 5 层卷积 + 4 层全连接
        self.conv = nn.Sequential(
            # Conv1: 200×66×3 → 98×31×24
            nn.Conv2d(3, 24, kernel_size=5, stride=2),
            nn.ELU(),
            # Conv2: 98×31×24 → 48×14×36
            nn.Conv2d(24, 36, kernel_size=5, stride=2),
            nn.ELU(),
            # Conv3: 48×14×36 → 22×5×48
            nn.Conv2d(36, 48, kernel_size=5, stride=2),
            nn.ELU(),
            # Conv4: 22×5×48 → 20×3×64
            nn.Conv2d(48, 64, kernel_size=3, stride=1),
            nn.ELU(),
            # Conv5: 20×3×64 → 18×1×64
            nn.Conv2d(64, 64, kernel_size=3, stride=1),
            nn.ELU(),
        )
        self.fc = nn.Sequential(
            nn.Linear(18 * 1 * 64, 1164),
            nn.ELU(),
            nn.Dropout(0.5),
            nn.Linear(1164, 100),
            nn.ELU(),
            nn.Dropout(0.5),
            nn.Linear(100, 50),
            nn.ELU(),
            nn.Dropout(0.5),
            nn.Linear(50, 10),
            nn.ELU(),
            nn.Linear(10, 1),  # 输出:转向角
        )
    
    def forward(self, x):
        x = self.conv(x)
        x = x.view(x.size(0), -1)
        x = self.fc(x)
        return x

训练技巧:

def training_tricks():
    """NVIDIA 论文中的关键训练技巧"""
    
    tricks = {
        "1. 数据增强": {
            "随机左右翻转": "配合转向角取反",
            "随机亮度调整": "模拟不同光照",
            "随机添加阴影": "模拟树荫/建筑阴影",
            "使用右摄像头训练": "转向角 + 修正值(如 0.2 弧度)",
            "使用左摄像头训练": "转向角 - 修正值",
        },
        "2. 损失函数": {
            "MSE Loss": "L = (ŷ - y)²",
            "为何不用分类": "转向角是连续值,分类会丢失精度",
        },
        "3. 训练策略": {
            "batch_size": "100(大 batch 稳定梯度)",
            "optimizer": "Adam, lr=1e-4",
            "epochs": "约 30-50",
            "early_stopping": "验证集 loss 不降时停止",
        },
    }
    return tricks

2.2 从模拟到现实(Sim-to-Real)

迁移学习流程:

阶段 1:在模拟器中训练
├── 使用 GTA V / CARLA / Unity 等模拟器
├── 优势:可获取完美标注、可重置场景、可并行采集
├── 数据量:数百万帧
└── 模型学习到基本驾驶策略

阶段 2:域适应(Domain Adaptation)
├── 问题:模拟器图像与真实图像分布不同(Sim-to-Real Gap)
├── 方法:
│   ├── 无监督域适应:对齐特征分布
│   ├── 风格迁移:将真实图像转为模拟风格
│   └── 微调:用少量真实数据 fine-tune
└── 数据量:仅需数千帧真实数据

阶段 3:真实道路部署
├── 安全约束:输出层加限幅、加入安全兜底策略
├── 持续学习:收集难例数据,定期更新模型
└── 监控:实时检测模型置信度,低置信度时交还人类

第三章:Tesla FSD 的端到端实践

3.1 纯视觉架构

Tesla FSD 感知栈(2023+):

8 个摄像头(120 万像素,360° 覆盖)
├── 前视主摄像头(FOV 50°):远距离目标
├── 前视广角摄像头(FOV 120°):近距离大视野
├── 左/右 柱摄像头(FOV 100°):侧方
├── 左/右 前翼子板摄像头(FOV 90°):侧前方
└── 左/右 后翼子板摄像头(FOV 100°):侧后方
       ↓
[BEV 特征提取]
├── 每个摄像头图像独立提取特征
├── 通过 Transformer 进行跨摄像头注意力
├── 投影到统一鸟瞰图(BEV)空间
└── 输出:BEV 特征图
       ↓
[多任务头]
├── 3D 目标检测(车辆/行人/自行车/...)
├── 车道线检测
├── 交通标志/信号灯识别
├── 可行驶区域分割
├── 深度估计(单目深度)
└── 运动预测(每个目标的速度/航向变化)
       ↓
[规划网络]
├── 输入:BEV 特征 + 导航目标点
├── 输出:未来 7 秒的规划轨迹(每 0.1 秒一个点)
└── 约束:碰撞检测 + 舒适度 + 交通规则

3.2 HydraNet:共享骨干 + 任务分支

HydraNet 架构设计:

                    输入图像
                       ↓
              ┌────────────────┐
              │   共享骨干网络   │  ← 所有任务共享
              │  (ResNet/Swin)  │
              └────────────────┘
                       ↓
              ┌────────┼────────┬────────┐
              ↓        ↓        ↓        ↓
         ┌────────┐┌────────┐┌────────┐┌────────┐
         │目标检测││车道线  ││深度估计││运动预测│ ...
         │  头    ││  头    ││  头    ││  头    │
         └────────┘└────────┘└────────┘└────────┘
              ↓        ↓        ↓        ↓
         各任务独立 Loss → 加权求和 → 反向传播

优势:
├── 共享骨干减少计算量(只跑一次特征提取)
├── 各任务头可独立训练和更新
├── 不同任务间隐式共享特征表示
└── 部署灵活:可只启用部分任务头

第四章:UniAD:统一自主驾驶

4.1 架构设计

UniAD 是首个将感知、预测、规划统一优化的框架。

UniAD 架构:

输入:多摄像头图像 I₁, I₂, ..., Iₙ
       ↓
[Scene Encoder]
├── Image Backbone(ResNet-101)
├── 多尺度特征提取(FPN)
├── View Transformer(LSS):图像 → BEV
└── 输出:BEV 特征 F_bev
       ↓
[Track Query 初始化]
├── 可学习的 track queries:Q₁, Q₂, ..., Qₘ
├── 每个 query 编码一个目标的信息
├── 包含:位置、速度、类别、历史轨迹
└── 通过跨帧注意力实现目标跟踪
       ↓
[Task Heads(顺序执行)]
│
├── Track Head(跟踪头)
│   ├── 输入:BEV 特征 + track queries
│   ├── 操作:自注意力(同一帧内目标交互)
│   │        + 时间注意力(跨帧跟踪)
│   └── 输出:更新后的 track queries(含跟踪信息)
│
├── Map Head(地图头)
│   ├── 输入:BEV 特征
│   ├── 操作:Transformer Decoder 查询车道线
│   └── 输出:车道线向量(多项式系数)
│
├── Motion Head(运动预测头)
│   ├── 输入:更新后的 track queries + 地图信息
│   ├── 操作:预测每个目标的多条未来轨迹
│   └── 输出:K 条候选轨迹 + 概率
│
└── Planning Head(规划头)
    ├── 输入:自车 track query + 地图 + 其他目标预测
    ├── 操作:基于交互的轨迹规划
    ├── 损失函数:
    │   ├── 碰撞损失:规划轨迹与其他目标预测轨迹的距离
    │   ├── 进度损失:鼓励向目标点前进
    │   └── 舒适度损失:限制加速度/加加速度
    └── 输出:自车的规划轨迹

4.2 联合训练策略

class UniADLoss(nn.Module):
    """UniAD 联合损失函数"""
    
    def __init__(self):
        super().__init__()
        # 各任务权重
        self.w_track = 1.0
        self.w_map = 1.0
        self.w_motion = 1.0
        self.w_plan = 2.0  # 规划损失权重更高
    
    def forward(self, predictions, targets):
        # 跟踪损失:分类 + 回归
        loss_track = (
            self.track_cls_loss(predictions['track'], targets['track']) +
            self.track_reg_loss(predictions['track'], targets['track'])
        )
        
        # 地图损失:车道线预测
        loss_map = self.map_loss(predictions['map'], targets['map'])
        
        # 运动损失:多模态轨迹预测
        loss_motion = self.motion_loss(
            predictions['motion'],  # K 条候选轨迹
            targets['motion'],      # GT 轨迹
            mode='minADE'           # 取最小 ADE 的轨迹计算损失
        )
        
        # 规划损失:带安全约束的轨迹规划
        loss_plan = self.planning_loss(
            predictions['plan'],
            targets['plan'],
            predictions['motion']  # 需要考虑其他目标的预测
        )
        
        total = (self.w_track * loss_track + 
                 self.w_map * loss_map + 
                 self.w_motion * loss_motion + 
                 self.w_plan * loss_plan)
        
        return total
    
    def planning_loss(self, pred_traj, gt_traj, other_preds):
        """规划损失:包含碰撞惩罚"""
        # L2 损失
        l2 = F.smooth_l1_loss(pred_traj, gt_traj)
        
        # 碰撞惩罚
        collision_penalty = 0
        for other_traj in other_preds:
            # 计算最小距离
            dist = torch.norm(pred_traj - other_traj, dim=-1).min()
            if dist < 1.0:  # 距离小于 1 米
                collision_penalty += (1.0 - dist) * 10.0
        
        return l2 + collision_penalty

第五章:大语言模型与自动驾驶

5.1 为什么需要 LLM?

传统自动驾驶的局限:
├── 无法理解自然语言指令("在前面那栋红色建筑前右转")
├── 无法处理"长尾"场景(训练数据中没见过的情况)
├── 无法进行常识推理("那个球滚到路上了,可能有小孩追出来")
└── 无法解释决策("为什么突然刹车?" → "因为...")

LLM 带来的能力:
├── 语言理解:将自然语言指令转化为驾驶目标
├── 常识推理:利用预训练知识处理罕见场景
├── 场景描述:用自然语言描述当前驾驶场景
└── 决策解释:解释为什么做出某个驾驶决策

5.2 DriveGPT4:多模态驾驶大模型

DriveGPT4 架构:

输入:
├── 多摄像头图像(视觉 token)
├── 车辆状态(速度、转向角、GPS)
├── 导航指令("在下一个路口左转")
└── 驾驶场景问答("前方发生了什么?")
       ↓
[视觉编码器]
├── CLIP ViT-L/14 提取图像特征
├── 线性投影层将视觉特征映射到语言空间
└── 输出:视觉 token 序列
       ↓
[大语言模型(Vicuna/LLaMA)]
├── 输入:视觉 token + 文本 token(拼接)
├── 自回归生成:逐 token 预测
└── 输出:
    ├── 场景描述:"前方有一辆红色卡车正在变道"
    ├── 驾驶决策:"减速并保持车距,等待卡车完成变道"
    └── 轨迹预测:将文本解码为轨迹坐标

5.3 实际应用场景

class DrivingLLMAgent:
    """基于 LLM 的驾驶智能体"""
    
    def __init__(self, llm, perception_module, planner):
        self.llm = llm
        self.perception = perception_module
        self.planner = planner
    
    def reason_about_scene(self, images, vehicle_state, question=None):
        """对当前场景进行推理"""
        
        # 1. 感知模块提取结构化信息
        perception_result = self.perception.process(images)
        
        # 2. 构建 prompt
        prompt = f"""
当前驾驶场景分析:
- 自车状态:速度 {vehicle_state.speed} m/s,车道 {vehicle_state.lane}
- 感知结果:
  - 前方 {perception_result.front_distance}m 有 {perception_result.front_object}
  - 左侧:{perception_result.left_description}
  - 右侧:{perception_result.right_description}
- 交通信号:{perception_result.traffic_light}
- 天气:{perception_result.weather}

请分析:
1. 当前场景的主要风险是什么?
2. 推荐的驾驶策略是什么?
3. 未来 5 秒内最可能发生什么?
"""
        if question:
            prompt += f"\n额外问题:{question}"
        
        # 3. LLM 推理
        response = self.llm.generate(prompt)
        
        # 4. 解析响应,生成规划
        plan = self.parse_and_plan(response, vehicle_state)
        
        return plan
    
    def handle_edge_case(self, scenario_description):
        """处理长尾场景"""
        prompt = f"""
你是一位自动驾驶系统的决策引擎。请分析以下罕见场景并给出安全策略:

场景:{scenario_description}

请给出:
1. 可能的风险分析
2. 推荐的保守策略
3. 需要额外确认的信息
"""
        return self.llm.generate(prompt)

第六章:仿真与测试

6.1 CARLA 仿真平台

# CARLA 中使用端到端模型进行测试
import carla
import numpy as np

class CarlaEndToEndEvaluator:
    """在 CARLA 中评估端到端模型"""
    
    def __init__(self, model, town='Town05', weather='clear'):
        self.client = carla.Client('localhost', 2000)
        self.world = self.client.load_world(town)
        self.model = model
        
        # 设置天气
        self.set_weather(weather)
        
        # 生成车辆
        self.vehicle = self.spawn_vehicle()
        
        # 安装摄像头
        self.camera = self.attach_camera()
    
    def run_episode(self, route, max_steps=1000):
        """运行一个测试 episode"""
        # 设置导航路线
        waypoint = self.world.get_map().get_waypoint(route.start)
        
        observations = []
        actions = []
        collisions = 0
        
        for step in range(max_steps):
            # 获取摄像头图像
            image = self.camera.get_image()
            
            # 模型推理
            steering, throttle = self.model.predict(image)
            
            # 执行动作
            self.vehicle.apply_control(carla.VehicleControl(
                steer=steering,
                throttle=throttle,
                brake=0.0
            ))
            
            # 记录
            observations.append(image)
            actions.append((steering, throttle))
            
            # 检查碰撞
            if self.check_collision():
                collisions += 1
                break
            
            # 检查是否到达
            if self.distance_to_goal(route.end) < 5.0:
                break
        
        return {
            'distance': self.distance_traveled(),
            'collisions': collisions,
            'steps': step,
            'success': self.distance_to_goal(route.end) < 5.0
        }

6.2 闭环测试指标

评估指标体系:

安全性指标:
├── 碰撞率(Collision Rate):每千公里碰撞次数
├── 最小 TTC(Time-to-Collision):最小碰撞时间
├── 紧急制动次数(Hard Brake):减速度 > 4m/s² 的次数
└── 偏离道路次数(Off-road):驶出可行驶区域的次数

效率指标:
├── 到达率(Success Rate):成功到达目的地的比例
├── 平均速度(Average Speed)
├── 路线完成率(Route Completion):行驶距离 / 总距离
└── 平均行驶时间

舒适度指标:
├── 加速度标准差( Jerk):加加速度的标准差
├── 转向平滑度(Steering Smoothness):转向角变化率
└── 横向偏移(Lateral Offset):与车道中心的偏差

鲁棒性指标:
├── 不同天气下的表现(晴天/雨天/雾天/夜间)
├── 不同交通密度下的表现
└── 不同城镇地图的泛化能力

课后项目

项目 1:在 CARLA 中训练端到端模型

# 环境搭建
pip install carla==0.9.15
pip install torch torchvision
pip install wandb  # 实验追踪

# 数据采集
python collect_data.py --town Town05 --routes 100 --weather clear

# 训练
python train_end2end.py --model cnn --epochs 50 --batch_size 64

# 评估
python evaluate.py --model checkpoints/best.pth --town Town05 --weather rain

项目 2:实现简单的行为克隆

class BehavioralCloning:
    """行为克隆:模仿人类驾驶"""
    
    def __init__(self, model):
        self.model = model
    
    def train(self, expert_demos):
        """
        expert_demos: [(image, steering, throttle), ...]
        从人类驾驶演示中学习
        """
        dataset = DemoDataset(expert_demos)
        dataloader = DataLoader(dataset, batch_size=64, shuffle=True)
        
        optimizer = torch.optim.Adam(self.model.parameters(), lr=1e-4)
        criterion = nn.MSELoss()
        
        for epoch in range(50):
            total_loss = 0
            for images, steerings, throttles in dataloader:
                pred_steer, pred_throttle = self.model(images)
                
                loss = (
                    criterion(pred_steer, steerings) +
                    0.5 * criterion(pred_throttle, throttles)
                )
                
                optimizer.zero_grad()
                loss.backward()
                optimizer.step()
                
                total_loss += loss.item()
            
            print(f"Epoch {epoch}: Loss = {total_loss / len(dataloader):.4f}")

前沿论文推荐

  • UniAD (CVPR 2023 Best Paper)
  • "Planning-Oriented Autonomous Driving"
  • 首次将规划作为端到端优化的目标
  • DriveVLM (2024)
  • "The Convergence of Large Language Models and Autonomous Driving"
  • 理想汽车的多模态驾驶大模型实践
  • VAD (NeurIPS 2023)
  • "Vectorized Scene Representation for Efficient Online Planning"
  • 向量化场景表示的高效规划
  • Occupancy Networks (2023)
  • "Tesla AI Day 2022: Occupancy Networks for the Real World"
  • Tesla 的占用网络表示方法
  • GPT-Driver (2023)
  • "Language Model Meets Motion Planning"
  • 将运动规划转化为语言建模问题

参考资源:
- CARLA Simulator
- nuScenes Devkit
- UniAD 官方代码
- Tesla AI Day 2022 视频
- Apollo 开放平台