自从十多前年Darpa的Grand Challenge竞赛开始,自动驾驶汽车技术不断得以发展,尤其近年来随着深度学习技术的出现,技术进步越来越快。自动驾驶汽车的组成部分有很多,其中最关键的是传感器和驱动它们的AI系统。随着计算能力的不断增强,新兴的深度学习网络可以对路况细节、可视视野和遥测数据进行很好的学习,有望成为自动驾驶汽车强大的“大脑”,用来理解路况、环境及对车辆运行进行决策。
本文分享的算法,可以根据路况来预测汽车方向盘的角度,从而控制汽车正常行驶。本算法基于Keras开发,用Tensorflow作为后端。
开发环境
我们利用Unity开发了一套汽车驾驶的虚拟环境,其包括两种模式:
- 训练模型:人为的驾驶汽车并收集数据
- 自动模型:汽车自动驾驶,并根据训练好的模型进行控制
数据日志对路况图像的存储路径和方向盘转角、油门、速度进行记录,并存储于csv文件中。当然,我们关心的仅仅是方向盘转角和路况图像。
模拟系器界面如下图所示。可以看到,模拟器包含2类跑道。跑道2(右侧)包含了各种陡坡和急转角,难度较跑道1大很多。
本算法是根据NVIDIA研究团队的论文“End to End Learning for Self Driving Cars”开发的。该团队训练了一个卷积神经网络,可以根据汽车上的三个摄像头(左、中、右)抓取的图像和汽车转向角数据来预测方向盘的转动角度。实际测试中,该模型仅用中间摄像头的数据就很好的控制了车辆行进方向。下图给出了该模型的基本框架。
与NVIDIA在现实世界进行自动驾驶训练不同,我们在模拟器中对汽车自动运行进行学习,但都应用了相同的算法框架。最近,关于模拟技术在自动驾驶公司(如Waymo)的技术开发中扮演重要角色的报道,也让我们对自己的做法有了更大的信心。
数据集
我们最终采用的数据集有4个:
1. Udacity的赛道1数据集
2. 在跑赛道1上创建的人工数据集(我们称为标准集)
3. Recovery数据集:另一个在赛道1上训练的人工数据集,该数据集设置了汽车靠近边线并如何调整的内容,以便训练汽车如何避免冲出车道;
4. 在赛道2上创建的人工数据集
需要指出的是,以上所有数据集我们都从赛道的两个方向进行了驾驶,以便我们的模型更具有通用性。
数据分析
通过分析的数据集中的转角数据,我们很快发现一个问题:转角数据非常的不平均,绝大多数数据都是零。这意味着,除非我们采用某种修正方法,否则我们的模型将偏向于直线驾驶。
但我们注意到,赛道2数据集有着更多的变化,因为该路线有很多的急转弯。这种数据集正是我们所期待的类型,但训练出的模型仍有很大的概率是直线行驶。
数据集划分
最终,我们决定创建一个由Udacity数据、Recovery数据和赛道2数据所组成的混合数据集,作为训练集(Training Set)。然后将Tack1的标准数据集作为验证集(Validation Set)。
代码语言:javascript复制frames = [recovery_csv, udacity_csv, track2_csv]
ensemble_csv = pd.concat(frames)
validation_csv = standard_csv
通过上述数据集划分,我们获得了5.5万张训练图像和4.4万张验证图像。
数据增强
我们虽然获得了足够数量的数据,但由于大多数数据的转向角均是零值,所以训练出的模型趋向于控制汽车直线行驶。
我们发现,跑道上的阴影同样会影响模型的训练。此外,模型还需要学会在道路的左侧或右侧正确的转弯。因此,我们必须寻找到一种方法,可以有效的增加和改变训练图像和转向角。通过研究,我们发现数据增强可以实现这个目的。
相机和转向角校准
首先,我们分别向左右两侧相机拍摄的图像设置不同的转向角校准偏差:
对于左侧相机,我们希望汽车向右转弯(正偏差)
对于右侧相机,我们希望汽车向左转弯(负偏差)
代码语言:javascript复制st_angle_names = ["Center", "Left", "Right"]
st_angle_calibrations = [0, 0.25, -0.25]
以上的值是根据经验设置的。
图像水平翻转
因为我们希望汽车不论在道路的任何位置,都能正确的转向,所以我们采用了一种有效的图像增强方法:将一部分图像水平翻转,同时将其初始的转向角反向:
代码语言:javascript复制def fliph_image(img):
"""
Returns a horizontally flipped image
"""
return cv2.flip(img, 1)
图像暗化
某些段跑道因为阴影或其他原因的影响,会比较暗。所以我们同样对某些图像进行暗化处理:将图像的RGB通道同时乘以一个固定范围内的随机数:
代码语言:javascript复制def change_image_brightness_rgb(img, s_low=0.2, s_high=0.75):
"""
Changes the image brightness by multiplying all RGB values by the same scalacar in [s_low, s_high).
Returns the brightness adjusted image in RGB format.
"""
img = img.astype(np.float32)
s = np.random.uniform(s_low, s_high)
img[:,:,:] *= s
np.clip(img, 0, 255)
return img.astype(np.uint8)
随机阴影
由于某些赛道被阴影遮挡,因此不得不训练我们的模型去识别它们,并避免阴影对模型的影响。
代码语言:javascript复制def add_random_shadow(img, w_low=0.6, w_high=0.85):
"""
Overlays supplied image with a random shadow polygon
The weight range (i.e. darkness) of the shadow can be configured via the interval [w_low, w_high)
"""
cols, rows = (img.shape[0], img.shape[1])
top_y = np.random.random_sample() * rows
bottom_y = np.random.random_sample() * rows
bottom_y_right = bottom_y np.random.random_sample() * (rows - bottom_y)
top_y_right = top_y np.random.random_sample() * (rows - top_y)
if np.random.random_sample() <= 0.5:
bottom_y_right = bottom_y - np.random.random_sample() * (bottom_y)
top_y_right = top_y - np.random.random_sample() * (top_y)
poly = np.asarray([[ [top_y,0], [bottom_y, cols], [bottom_y_right, cols], [top_y_right,0]]], dtype=np.int32)
mask_weight = np.random.uniform(w_low, w_high)
origin_weight = 1 - mask_weight
mask = np.copy(img).astype(np.int32)
cv2.fillPoly(mask, poly, (0, 0, 0))
#masked_image = cv2.bitwise_and(img, mask)
return cv2.addWeighted(img.astype(np.int32), origin_weight, mask, mask_weight, 0).astype(np.uint8)
偏转图像
前面提过,数据集的大部分图像都是零转角数据。因此我们需要给数据集添加更多的变化。我们对图像进行了随机偏转,即给转向角设置一个随机的偏移,同时相应的调整图像的各个像素。在本算法中,我们根据经验对图像的每个像素加上(或减去)0.0035,从而使图像向左(或向右)偏转。同样,将图像向上或向下偏转会让模型认为汽车是在上坡或下坡。
经过试验,我们认为偏转图像的方法,可能是让汽车实现正确转向的最有效的图像增强技术。
代码语言:javascript复制# Read more about it here: http://docs.opencv.org/3.0-beta/doc/py_tutorials/py_imgproc/py_geometric_transformations/py_geometric_transformations.html
def translate_image(img, st_angle, low_x_range, high_x_range, low_y_range, high_y_range, delta_st_angle_per_px):
"""
Shifts the image right, left, up or down.
When performing a lateral shift, a delta proportional to the pixel shifts is added to the current steering angle
"""
rows, cols = (img.shape[0], img.shape[1])
translation_x = np.random.randint(low_x_range, high_x_range)
translation_y = np.random.randint(low_y_range, high_y_range)
st_angle = translation_x * delta_st_angle_per_px
translation_matrix = np.float32([[1, 0, translation_x],[0, 1, translation_y]])
img = cv2.warpAffine(img, translation_matrix, (cols, rows))
return img, st_angle
图像增强策略
我们的图像增强策略很简单:每个被增强的图像都经过一系列的处理,而图像是否被增强则由概率p(0到1之间)决定。代码如下:
代码语言:javascript复制def augment_image(img, st_angle, p=1.0):
"""
Augment a given image, by applying a series of transformations, with a probability p.
The steering angle may also be modified.
Returns the tuple (augmented_image, new_steering_angle)
"""
aug_img = img
if np.random.random_sample() <= p:
aug_img = fliph_image(aug_img)
st_angle = -st_angle
if np.random.random_sample() <= p:
aug_img = change_image_brightness_rgb(aug_img)
if np.random.random_sample() <= p:
aug_img = add_random_shadow(aug_img, w_low=0.45)
if np.random.random_sample() <= p:
aug_img, st_angle = translate_image(aug_img, st_angle, -60, 61, -20, 21, 0.35/100.0)
return aug_img, st_angle
Keras图像生成器
当我们训练模型时,会在fly上上生成新的增强图像,因此我们专门创建了一个Keras图像生成器来在每个训练批处理集上进行图像生成。
代码语言:javascript复制def generate_images(df, target_dimensions, img_types, st_column, st_angle_calibrations, batch_size=100, shuffle=True,
data_aug_pct=0.8, aug_likelihood=0.5, st_angle_threshold=0.05, neutral_drop_pct=0.25):
"""
Generates images whose paths and steering angle are stored in the supplied dataframe object df
Returns the tuple (batch,steering_angles)
"""
# e.g. 160x320x3 for target_dimensions
batch = np.zeros((batch_size, target_dimensions[0], target_dimensions[1], target_dimensions[2]), dtype=np.float32)
steering_angles = np.zeros(batch_size)
df_len = len(df)
while True:
k = 0
while k < batch_size:
idx = np.random.randint(0, df_len)
for img_t, st_calib in zip(img_types, st_angle_calibrations):
if k >= batch_size:
break
row = df.iloc[idx]
st_angle = row[st_column]
# Drop neutral-ish steering angle images with some probability
if abs(st_angle) < st_angle_threshold and np.random.random_sample() <= neutral_drop_pct :
continue
st_angle = st_calib
img_type_path = row[img_t]
img = read_img(img_type_path)
# Resize image
img, st_angle = augment_image(img, st_angle, p=aug_likelihood) if np.random.random_sample() <= data_aug_pct else (img, st_angle)
batch[k] = img
steering_angles[k] = st_angle
k = 1
yield batch, np.clip(steering_angles, -1, 1)
以下展示了在批处理集上的一部分增强图像:
可以看到,通过增强图像,数据集中的转向角变得更加的平均了。
训练模型
最初,我们采用了基于VGG框架的模型,缩减了模型的层数,并且未采用迁移学习,但难以得到满意的结果。最终,我们应用了NVIDIA论文中的模型框架,并获得了最佳的结果:
模型调整
在NVIDIA模型的基础上,我们进行了一些调整:
- 对图像的顶部进行裁剪,以排除地平线的影响(它不会立即决定转向角度)
- 将输入图像大小调整为66x200,以利用GPU的计算优势
- 在每个激活函数之后,我们使用批正则化(Batch Normalization)来加快收敛速度
- 将第二个全连接层的输出尺寸由100修改为200
激活与正则化
除了最后一层,所有层均使用了ReLU激活函数。我们也尝试了ELU,但是用ReLU 批量正则化得到了更好的结果。我们对输出层使用Mean Squared Error激活函数,因为这是一个回归问题,而不是一个分类问题。
如前一节所述,我们使用批正则化来加速收敛。我们还尝试了Dropout,但没有发现任何明显的差异。我们相信,在每个批训练集中生成新图像和剔除一些中性角度图像有助于减少过拟合。
此外,我们没有将任何最大池化(MaxPool)应用到模型上,因为它需要对模型框架进行重较大的更改,因为我们会在更早的时候减少维度。
训练与结果
我们采用Adam作为优化器来训练模型,并设置学习率为0.001。经过一系列的训练,我们获得了一个强力的模型,可以很好的控制汽车在虚拟的赛道上正确的行驶。
同样,我们的模型在赛道2上的陡坡路况也能很好的运行。
可以看到,自动驾驶汽车努力坚保持在车道内,而不是在中间行驶的,就像我们在人工控制时所做的一样,这表明模型确实学会了如何保持在车道内行驶。