论文来源: End to End Learning for Self-Driving Cars
2016 年,NVIDIA 发表了论文 End to End Learning for Self-Driving Cars。论文训练了一个卷积神经网络(CNN),将汽车前部摄像头捕捉到的原始像素直接映射为车辆的转向控制命令。
与显式分解驾驶问题的方法——例如车道标志检测、路径规划和控制——相比,端到端系统可以同时优化所有处理步骤(processing steps)。
1. 数据采集与训练
数据采集系统记录挡风玻璃后方三台摄像机的画面,以及驾驶员操控方向盘的偏转角度。摄像机生成的每一帧视频数据(30 FPS)都与人类驾驶员的转向角度进行时间同步。
为了使系统独立于汽车的几何尺寸,方向控制命令使用 1/r 表示,其中 r 是以米为单位的转弯半径。
仅有人类驾驶员的正确驾驶数据还不足以完成训练。神经网络还必须学习如何从错误中恢复,否则自动驾驶汽车会逐渐偏离道路。因此,训练集还加入了车辆偏离车道中心、处于不同道路朝向时的额外图像。左右摄像机提供了两种特定的偏离中心视角。准备完成后,训练数据被送入 CNN。
模型将预测的方向控制命令与理想控制命令比较,再调整 CNN 的权重,使预测值尽可能接近理想值。权重通过机器学习库 Torch 7 的反向传播算法进行调整。
在这一框架中,只要提供足够的训练数据——包括人类驾驶员驾驶装有摄像头的车辆累计行驶的大量里程,以及人为构造的偏离车道线等“极限”道路状态——CNN 就能得到充分训练。训练完成后,网络可以根据单个中央摄像机(single center camera)的视频图像生成转向命令。
2. 网络结构
训练目标是使网络输出的方向控制命令与人工驾驶或校正后的控制命令之间的均方误差最小。论文中的网络包括 9 层:1 个归一化层、5 个卷积层和 3 个全连接层。输入图像先映射到 YUV 色彩空间,再传入网络。
网络最终输出转弯半径的倒数 1/r。
3. 数据选择
训练神经网络的第一步是选择视频中用于训练的帧。采集数据包含道路类型、天气条件、驾驶员行为(保持车道、更换车道、转弯等)标签。
为了训练 CNN 保持车道,研究者只选择驾驶员在同一车道内行驶的数据,并舍弃其他片段。同时将视频降采样至 10 FPS,因为高采样率下相邻图像非常相似,无法提供太多额外信息。为了减弱直线行驶数据带来的偏置,训练图像中还包含了较大比例的弯曲道路。
选定最终图像集之后,研究者人工添加随机位移和旋转,以教会网络修正较差的车辆姿态与视角。调整幅度从正态分布中随机采样:均值为零,标准差为驾驶员操作数据标准差的两倍。
4. 仿真
在进行实际道路测试之前,研究者首先通过仿真评估 CNN 的性能。
仿真器发送测试视频的第一帧,并根据采集过程中车辆相对车道中心的位置校正真值偏差,随后将图像输入训练好的 CNN。CNN 输出的转向信号与采集到的驾驶员转向数据一起送入车辆动力学模型,用于更新虚拟车辆的位置和朝向。
仿真器根据车辆的新位置和朝向生成测试视频的下一帧,并记录车辆到车道中心线的距离、偏航角(yaw)以及虚拟车辆行驶的距离。当车辆到车道中心线的距离超过阈值时,虚拟驾驶员会进行干预,将车辆的位置和朝向重置为原测试视频中的真值。
仿真器使用数据采集车前置摄像机预先录制的视频,并通过 CNN 预测控制命令。视频时间轴与驾驶员控制命令保持同步。由于驾驶员并不总能将车辆保持在车道中心,因此研究者需要逐帧人工校准车道中心,并将这一姿态作为“对照数据”。
网络的自动驾驶时间占比通过虚拟人工干预次数来估计。论文假设现实中的每次人工干预需要 6 秒,包括驾驶员接管车辆、修正位置以及重新进入自动驾驶模式:
autonomy = (1 - number_of_interventions × 6 seconds / elapsed_time) × 100
例如,在 600 秒内触发 10 次干预,则自动驾驶比例为:
autonomy = (1 - 10 × 6 / 600) × 100 = 90%
4.1 道路测试
实际道路测试的统计不包括变换车道和转弯,只评估车辆保持车道居中的能力。论文提供了 道路测试视频。
5. 总结
实验表明,神经网络能够学习完整的车道保持驾驶任务,而不需要人工将任务分解为道路与车道检测、语义抽象、路径规划和控制。
不到 100 小时的训练数据,就足以训练车辆在多种道路和天气条件下行驶,包括高速公路、普通公路、居民区道路,以及晴天、多云和雨天。
CNN 模型还能从非常稀疏的训练信号——只有方向控制命令——中学到有意义的道路特征。例如,系统在没有显式标注的情况下学会了识别道路边界。
6. 附录:NVIDIA 模型
下面是使用 Keras 实现的 NVIDIA 模型结构示例:
def createNvidiaModel():
# 构建模型
model = Sequential(
[
Conv2D(
24,
(5, 5),
strides=(2, 2),
activation="relu",
input_shape=(66, 200, 3),
),
Conv2D(36, (5, 5), strides=(2, 2), activation="relu"),
Conv2D(48, (5, 5), strides=(2, 2), activation="relu"),
Conv2D(64, (3, 3), activation="relu"),
Conv2D(64, (3, 3), activation="relu"),
Flatten(),
Dense(100, activation="relu"),
Dense(50, activation="relu"),
Dense(10, activation="relu"),
Dense(1), # 输出方向盘角度
]
)
# 使用均方误差作为损失函数
model.compile(optimizer="adam", loss="mse")
return model