原文:
annas-archive.org/md5/9b92075f71de367fabcae691ae8a60bd译者:飞龙
第五章:第五章:深度学习工作流程
在本章中,我们将介绍您在训练神经网络和将其投入生产过程中可能执行的操作步骤。我们将更深入地讨论深度学习背后的理论,以更好地解释我们在第四章,“使用神经网络的深度学习”中实际所做的工作,但我们主要关注与自动驾驶汽车相关的论点。我们还将介绍一些有助于我们在 CIFAR-10(一个小型图像的著名数据集)上获得更高精度的概念。我们相信,本章中提出的理论,加上与第四章,“使用神经网络的深度学习”和第六章,“改进您的神经网络”相关的更实际的知识,将为您提供足够的工具来执行自动驾驶汽车领域中的常见任务。
在本章中,我们将涵盖以下主题:
-
获取或创建数据集
-
训练、验证和测试数据集
-
分类器
-
数据增强
-
定义模型
-
如何调整卷积层、
MaxPooling层和密集层 -
训练和随机性的作用
-
欠拟合和过拟合
-
激活的可视化
-
运行推理
-
重新训练
技术要求
要能够使用本章中解释的代码,您需要安装以下工具和模块:
-
Python 3.7
-
NumPy 模块
-
Matplotlib 模块
-
TensorFlow 模块
-
Keras 模块
-
OpenCV-Python 模块
本章的代码可以在github.com/PacktPublishing/Hands-On-Vision-and-Behavior-for-Self-Driving-Cars/tree/master/Chapter5找到。
本章的“代码在行动”视频可以在以下位置找到:
获取数据集
一旦您有一个想要用神经网络执行的任务,通常的第一步是获取数据集,这是您需要喂给神经网络的那些数据。在我们这本书中执行的任务中,数据集通常由图像或视频组成,但它可以是任何东西,或者图像和其他数据的混合。
数据集代表您喂给神经网络的输入,但如您所注意到的,您的数据集还包含期望的输出,即标签。我们将用x表示神经网络的输入,用y表示输出。数据集由输入/特征(例如,MNIST 数据集中的图像)和输出/标签(例如,与每个图像关联的数字)组成。
我们有不同的数据集类型。让我们从最简单的开始——Keras 中包含的数据集——然后再继续到下一个。
Keras 模块中的数据集
通常,数据库包含大量数据。在成千上万张图片上训练神经网络是正常的,但最好的神经网络是用数百万张图片训练的。那么我们如何使用它们呢?
最简单的方法,通常对实验很有帮助,是使用 Keras 中包含的数据库,就像我们在第四章中做的那样,使用神经网络进行深度学习,使用load_data(),如下所示:
from keras.datasets import mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
Keras 提供了各种数据库:
-
MNIST – 数字分类
-
CIFAR10 – 小图像的分类
-
CIFAR100 – 小图像的分类
-
IMDB 电影评论情感分类
-
路透社新闻社分类
-
Fashion MNIST 数据库
-
波士顿房价
这些数据库对于学习如何构建神经网络非常有用。在下一节中,我们将探讨一些对自动驾驶汽车更有用的数据库。
现有数据库
幸运的是,有几个有趣的公共数据库可供使用,但你必须始终仔细检查许可证,以了解你可以用它做什么,并最终获取或获得更宽松的许可证。
以下是一些你可能想要检查的与自动驾驶汽车相关的数据库:
-
BDD100K,一个大规模多样化的驾驶视频数据库;参考
bdd-data.berkeley.edu/。 -
博世小型交通信号灯数据库;参考
hci.iwr.uni-heidelberg.de/content/bosch-small-traffic-lights-dataset。 -
CULane,一个用于交通车道检测学术研究的大规模数据库;参考
xingangpan.github.io/projects/CULane.html。 -
KITTI 视觉基准套件;参考
www.cvlibs.net/datasets/kitti/。 -
Mapillary 交通标志数据库;参考
www.mapillary.com/dataset/trafficsign。
此外,还有一些其他更通用的数据库你可能觉得很有趣,特别是 WordNet 层次结构组织的图像数据库 ImageNet,www.image-net.org/。
这个数据库包含数百万个指向互联网上图片的 URL,对神经网络的发展产生了重大影响。我们将在稍后详细讨论。
公共数据库很棒,但你也可能需要考虑其内容,因为有些图片被错误分类并不罕见。这对你的神经网络来说可能不是什么大问题,但你可能仍然希望获得尽可能好的数据库。
如果找不到令人满意的数据库,你总是可以生成一个。让我们看看如何快速构建用于自动驾驶汽车任务的优质数据库。
合成数据库
当可能时,您可能希望从可以创建足够好的图像的程序中生成数据集。我们在第三章 车道检测中使用这项技术,我们从 Carla 中检测行人,从开源视频游戏 Speed Dreams 中获取图像,您也可以使用 3D 引擎或 3D 建模软件编写自己的生成器。
这些方法相对简单、快捷且非常便宜,可以生成大量数据集,实际上有时是无价的,因为在许多情况下,你可以自动标注图像并节省大量时间。然而,合成图像通常比真实图像简单,结果可能是你的网络在现实世界场景中的表现可能不如你想象的那么好。我们将在第八章 行为克隆中使用这项技术。
如果不是最好的话,Carla 是当前最好的模拟器之一。
Carla,自动驾驶研究的开源模拟器,使用了以下网站:
您可以使用它来生成您任务所需的图像。
当这还不够时,您必须遵循手动流程。
您的定制数据集
有时,您可能没有令人满意的替代方案,您需要自己收集图像。这可能需要收集录像并对数千张图像进行分类。如果图像是从视频中提取的,您可能只需对视频进行分类,然后从中提取数百或数千张图像。
有时情况并非如此,您可能需要自己浏览成千上万张图片。或者,您可以使用专业公司的服务来为您标注图像。
有时您可能拥有图像,但分类可能很困难。想象一下,如果您可以访问汽车的录像,然后需要标注图像,添加汽车所在的框。如果您很幸运,您可能可以访问一个可以为您完成这项工作的神经网络。您可能仍然需要手动检查结果,并对一些图像进行重新分类,但这仍然可以节省大量工作。
接下来,我们将深入了解这些数据集。
理解三个数据集
实际上,你可能不需要一个数据集,但理想情况下需要三个。这些数据集用于训练、验证和测试。在定义它们之前,请考虑不幸的是,有时关于验证和测试的含义存在一些混淆,通常情况下只有两个数据集可用,就像这种情况,验证集和测试集是重合的。我们在第四章 使用神经网络的深度学习中也做了同样的事情,我们使用了测试集作为验证集。
让我们现在定义这三个数据集,然后我们可以解释理想情况下我们应该如何测试 MNIST 数据集:
-
训练数据集:这是用于训练神经网络的数据库,通常是三个数据集中最大的一个。
-
验证数据集:这通常是指训练数据集的一部分,这部分数据不用于训练,而仅用于评估模型的性能和调整其超参数(例如,网络的拓扑结构或优化器的学习率)。
-
测试数据集:理想情况下,这是一个一次性数据集,用于在所有调整完成后评估模型的性能。
您不能使用训练数据集来评估模型的性能,因为训练数据集被优化器用于训练网络,所以这是最佳情况。然而,我们通常不需要神经网络在训练数据集上表现良好,而是在用户抛给它的任何东西上表现良好。因此,我们需要网络能够泛化。回到第四章“使用神经网络的深度学习”中的学生比喻,训练数据集上的高分意味着学生已经把书(训练数据集)背得滚瓜烂熟,但我们希望的是学生能够理解书的内容,并能够将这种知识应用到现实世界的情境中(验证数据集)。
那么,如果验证代表现实世界,为什么我们还需要测试数据集呢?问题是,在调整您的网络时,您将做出一些选择,这些选择将偏向于验证数据集(例如,根据其在验证数据集上的性能选择一个模型而不是另一个模型)。结果,验证数据集上的性能可能仍然高于现实世界。
测试数据集解决了这个问题,因为我们只在所有调整完成后才应用它。这也解释了为什么理想情况下,我们希望在仅使用一次后丢弃测试数据集。
这可能不切实际,但并非总是如此,因为有时您可以很容易地根据需要生成一些样本。
那么,我们如何在 MNIST 任务中使用三个数据集呢?也许您还记得从第四章,“使用神经网络的深度学习”,MNIST 数据集有 60,000(60 K)个样本用于训练,10,000(10 K)个用于测试。
理想情况下,您可以采用以下方法:
-
训练数据集可以使用为训练准备的 60,000 个样本。
-
验证数据集可以使用为测试准备的 10,000 个样本(正如我们所做的那样)。
-
测试数据集可以根据需要生成,现场写数字。
在讨论了三个数据集之后,我们现在可以看看如何将您的完整数据集分成三部分。虽然这似乎是一个简单的操作,但在如何进行操作方面您需要小心。
数据集拆分
给定你的完整数据集,你可能需要将其分成训练、验证和测试三个部分。如前所述,理想情况下,你希望测试是在现场生成的,但如果这不可能,你可能会选择使用总数据集的 15-20%进行测试。
在剩余的数据集中,你可能使用 15-20%作为验证。
如果你有很多样本,你可能为验证和测试使用更小的百分比。如果你有较小的数据集,在你对模型性能满意后(例如,如果你选择它是因为它在验证和测试数据集上都表现良好),你可能会将测试数据集添加到训练数据集中以获得更多样本。如果你这样做,就没有必要在测试数据集中评估性能,因为实际上它将成为训练的一部分。在这种情况下,你信任验证数据集的结果。
但即使大小相同,并不是所有的分割都是一样的。让我们用一个实际的例子来说明。
你想检测猫和狗。你有一个包含 10,000 张图片的数据集。你决定使用 8,000 张进行训练,2,000 张进行验证,测试是通过你家中 1 只狗和 1 只猫的视频实时记录来完成的;每次测试时,你都会制作一个新的视频。看起来完美。可能出什么问题?
首先,你需要在每个数据集中大致有相同数量的猫和狗。如果不是这样,网络将偏向于其中之一。直观地说,如果在训练过程中,网络看到 90%的图像是狗的,那么总是预测狗将给你 90%的准确率!
你读到随机化样本顺序是一种最佳实践,你就这样做了。然后你进行了分割。你的模型在训练、验证和测试数据集上表现良好。一切看起来都很不错。然后你尝试使用几个朋友的宠物,但什么都没发生。发生了什么?
一种可能性是,你的分割在衡量泛化方面并不好。即使你有 1 万张图片,它们可能只来自 100 只宠物(包括你的),每只狗和猫都出现了 100 次,位置略有不同(例如,来自视频)。如果你打乱样本,所有的狗和猫都会出现在所有数据集中,因此验证和测试将相对容易,因为网络已经知道那些宠物。
如果,相反,你为了验证而养了 20 只宠物,并且注意不要在训练或验证数据集中包含你的宠物照片,那么你的估计将更加现实,你有机会构建一个在泛化方面更好的神经网络。
现在我们有了三个数据集,是时候定义我们需要执行的任务了,这通常将是图像分类。
理解分类器
深度学习可以用于许多不同的任务。对于图像和 CNN 来说,一个非常常见的任务是分类。给定一个图像,神经网络需要使用在训练期间提供的标签之一对其进行分类。不出所料,这种类型的网络被称为分类器。
要做到这一点,神经网络将为每个标签有一个输出(例如,在 10 个数字的 MNIST 数据集上,我们有 10 个标签和 10 个输出),而只有一个输出应该是 1,其他所有输出应该是 0。
神经网络如何达到这种状态呢?其实,它并不能。神经网络通过内部乘法和求和产生浮点输出,而且很少能得到相似的输出。然而,我们可以将最大值视为热点(1),而所有其他值都可以视为冷点(0)。
我们通常在神经网络的末尾应用一个 softmax 层,它将输出转换为概率,这意味着 softmax 后的输出总和将是 1.0。这非常方便,因为我们可以很容易地知道神经网络对预测的信心程度。Keras 在模型中提供了一个获取概率的方法predict(),以及一个获取标签的方法predict_classes()。如果需要,可以使用to_categorical()将标签轻松转换为独热编码格式。
如果你需要从独热编码转换到标签,可以使用 NumPy 的argmax()函数。
现在我们知道了我们的任务是图像分类,但我们需要确保我们的数据集与我们的网络在生产部署时需要检测的内容相似。
创建真实世界的数据集
当你收集数据集时,无论是使用自己的图像还是其他合适的数据集,你需要确保图像反映了你可能在现实生活中遇到的条件。例如,你应该尝试获取以下列出的问题图像,因为你很可能在生产中遇到这些问题:
-
恶劣的光线(过曝和欠曝)
-
强烈的阴影
-
障碍物遮挡物体
-
物体部分出图
-
物体旋转
如果你不能轻松地获得这些类型的图像,可以使用数据增强,这正是我们下一节要讨论的内容。
数据增强
数据增强是增加你数据集中样本的过程,并从你已有的图像中派生新的图像;例如,降低亮度或旋转它们。
Keras 包括一个方便的方式来增强你的数据集,ImageDataGenerator(),它可以随机应用指定的转换,但不幸的是,它的文档并不特别完善,并且在参数方面缺乏一致性。因此,我们现在将分析一些最有用的转换。为了清晰起见,我们将构建一个只有一个参数的生成器,以观察其效果,但你很可能希望同时使用多个参数,我们将在稍后这样做。
ImageDataGenerator() 构造函数接受许多参数,例如以下这些:
-
brightness_range:这将改变图像的亮度,它接受两个参数的列表,分别是最小和最大亮度,例如 [0.1, 1.5]。 -
rotation_range:这将旋转图像,并接受一个表示旋转范围的度数参数,例如 60。 -
width_shift_range:这将使图像水平移动;它接受不同形式的参数。我建议使用可接受值的列表,例如 [-50, -25, 25, 50]。 -
height_shift_range:这将使图像垂直移动;它接受不同形式的参数。我建议使用可接受值的列表,例如 [-50, -25, 25, 50]。 -
shear_range:这是剪切强度,接受以度为单位的一个数字,例如 60。 -
zoom_range:这将放大或缩小图像,它接受两个参数的列表,分别是最小和最大缩放,例如 [0.5, 2]。 -
horizontal_flip:这将水平翻转图像,参数是一个布尔值。 -
vertical_flip:这将垂直翻转图像,参数是一个布尔值。
其中,水平翻转通常非常有效。
下图显示了使用亮度、旋转、宽度移动和高度移动增强图像的结果:
图 5.1 – ImageDataGenerator() 结果。从上到下:brightness_range=[0.1, 1.5], rotation_range=60, width_shift_range=[-50, -25, 25, 50], 和 height_shift_range=[-75, -35, 35, 75]
下图是使用剪切、缩放、水平翻转和垂直翻转生成的图像:
图 5.2 – ImageDataGenerator() 结果。从上到下:shear_range=60, zoom_range=[0.5, 2], horizontal_flip=True, 和 vertical_flip=True
这些效果通常会组合使用,如下所示:
datagen = ImageDataGenerator(brightness_range=[0.1, 1.5], rotation_range=60, width_shift_range=[-50, -25, 25, 50], horizontal_flip=True)
这是最终结果:
图 5.3 – ImageDataGenerator() 结果。应用参数:brightness_range=[0.1, 1.5], rotation_range=60, width_shift_range=[-50, -25, 25, 50], 和 horizontal_flip=True
直观上,网络应该对图像的变化更加宽容,并且应该学会更好地泛化。
请记住,Keras 的数据增强更像是数据替换,因为它替换了原始图像,这意味着原始的、未更改的图像不会被发送到神经网络,除非随机组合是这样的,它们以未更改的形式呈现。
数据增强的巨大效果是样本会在每个周期改变。所以,为了清楚起见,Keras 中的数据增强不会在每个周期增加样本数量,但样本会根据指定的转换在每个周期改变。你可能想训练更多的周期。
接下来,我们将看到如何构建模型。
模型
现在你有一个图像数据集,你知道你想做什么(例如,分类),是时候构建你的模型了!
我们假设你正在工作于一个卷积神经网络,所以你可能甚至只需要使用卷积块、MaxPooling和密集层。但如何确定它们的大小?应该使用多少层?
让我们用 CIFAR-10 做一些测试,因为 MINST 太简单了,看看会发生什么。我们不会改变其他参数,但只是稍微玩一下这些层。
我们还将训练 5 个周期,以加快训练速度。这并不是为了得到最好的神经网络;这是为了衡量一些参数的影响。
我们的起点是一个包含一个卷积层、一个 MaxPooling 层和一个密集层的网络,如下所示:
model = Sequential()
model.add(Conv2D(8, (3, 3), input_shape=x_train.shape[1:], activation='relu'))
model.add(MaxPooling2D())
model.add(Flatten())
model.add(Dense(units = 256, activation = "relu"))
model.add(Dense(num_classes))
model.add(Activation('softmax'))
以下是对此的总结:
_______________________________________________________________
Layer (type) Output Shape Param #
===============================================================
conv2d_1 (Conv2D) (None, 30, 30, 8) 224
_______________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 15, 15, 8) 0
_______________________________________________________________
flatten_1 (Flatten) (None, 1800) 0
_______________________________________________________________
dense_1 (Dense) (None, 256) 461056
_______________________________________________________________
dense_2 (Dense) (None, 10) 2570
_______________________________________________________________
activation_1 (Activation) (None, 10) 0
===============================================================
Total params: 463,850
Trainable params: 463,850
Non-trainable params: 0
你可以看到这是一个如此简单的网络,它已经有了 463 K 个参数。层数的数量是误导性的。你并不一定需要很多层来得到一个慢速的网络。
这是性能:
Training time: 90.96391367912292
Min Loss: 0.8851623952198029
Min Validation Loss: 1.142119802236557
Max Accuracy: 0.68706
Max Validation Accuracy: 0.6068999767303467
现在,下一步是调整它。所以,让我们试试吧。
调整卷积层
让我们在卷积层中使用 32 个通道:
Total params: 1,846,922
Training time: 124.37444043159485
Min Loss: 0.6110964662361145
Min Validation Loss: 1.0291267457723619
Max Accuracy: 0.78486
Max Validation Accuracy: 0.6568999886512756
还不错!准确率提高了,尽管比之前大 4 倍,但它的速度慢了不到 50%。
现在我们尝试堆叠 4 层:
model.add(Conv2D(8, (3, 3), input_shape=x_train.shape[1:], activation='relu'))
model.add(Conv2D(8, (3, 3), input_shape=x_train.shape[1:], activation='relu', padding = "same"))
model.add(Conv2D(8, (3, 3), input_shape=x_train.shape[1:], activation='relu', padding = "same"))
model.add(Conv2D(8, (3, 3), input_shape=x_train.shape[1:], activation='relu', padding = "same"))
让我们用model.summary()检查网络的大小,就像通常那样:
Total params: 465,602
它只是比初始模型稍微大一点!原因是由于密集层,大多数参数都存在,并且堆叠相同大小的卷积层并不会改变密集层所需的参数。这就是结果:
Training time: 117.05060386657715
Min Loss: 0.6014562886440754
Min Validation Loss: 1.0268916247844697
Max Accuracy: 0.7864
Max Validation Accuracy: 0.6520000100135803
它非常相似——稍微快一点,准确率基本上相同。由于网络有多个层,它可以学习更复杂的函数。然而,它有一个更小的密集层,因此由于这个原因,它失去了一些准确率。
我们不使用相同的填充,而是尝试使用valid,这将每次减少卷积层的输出大小:
model.add(Conv2D(8, (3, 3), input_shape=x_train.shape[1:], activation='relu'))
model.add(Conv2D(8, (3, 3), input_shape=x_train.shape[1:], activation='relu', padding="valid"))
model.add(Conv2D(8, (3, 3), input_shape=x_train.shape[1:], activation='relu', padding="valid"))
model.add(Conv2D(8, (3, 3), input_shape=x_train.shape[1:], activation='relu', padding="valid"))
参数数量显著减少,从 465,602:
Total params: 299,714
我们现在使用不到 300 K 个参数,如下所示:
Training time: 109.74382138252258
Min Loss: 0.8018992121839523
Min Validation Loss: 1.0897881112098693
Max Accuracy: 0.71658
Max Validation Accuracy: 0.6320000290870667
非常有趣的是,训练准确率下降了 7%,因为网络对于这个任务来说太小了。然而,验证准确率只下降了 2%。
现在我们使用初始模型,但使用相同的填充,因为这会给我们在卷积后处理一个稍微大一点的图像:
model.add(Conv2D(8, (3, 3), input_shape=x_train.shape[1:], padding="same", activation='relu'))
Total params: 527,338
我们现在有更多的参数,这是性能:
Training time: 91.4407947063446
Min Loss: 0.7653912879371643
Min Validation Loss: 1.0724352446556091
Max Accuracy: 0.73126
Max Validation Accuracy: 0.6324999928474426
与参考模型相比,准确度都有所提高,而时间几乎保持不变,因此这是一个积极的实验。
现在我们将核的大小增加到 7x7:
model.add(Conv2D(8, (7, 7), input_shape=x_train.shape[1:], padding="same", activation='relu'))
Total params: 528,298
由于核现在更大,参数的数量增加是可以忽略不计的。但是它的表现如何?让我们检查一下:
Training time: 94.85121083259583
Min Loss: 0.7786661441159248
Min Validation Loss: 1.156547416305542
Max Accuracy: 0.72674
Max Validation Accuracy: 0.6090999841690063
不太理想。它稍微慢一些,准确度也稍微低一些。很难知道原因;也许是因为输入图像太小。
我们知道在卷积层之后添加 MaxPooling 层是一个典型的模式,所以让我们看看我们如何调整它。
调整 MaxPooling
让我们回到之前的模型,并且只去掉MaxPooling:
Total params: 1,846,250
移除MaxPooling意味着密集层现在大了 4 倍,因为卷积层的分辨率不再降低:
Training time: 121.01851439476013
Min Loss: 0.8000291277170182
Min Validation Loss: 1.2463579467773438
Max Accuracy: 0.71736
Max Validation Accuracy: 0.5710999965667725
这看起来并不太高效。与原始网络相比,它更慢,准确度有所提高,但验证准确度却下降了。与具有四个卷积层的网络相比,它具有相同的速度,但验证准确度却远远低于后者。
看起来 MaxPooling 在减少计算的同时提高了泛化能力。毫不奇怪,它被广泛使用。
现在我们增加 MaxPooling 层的数量:
model.add(Conv2D(8, (3, 3), input_shape=x_train.shape[1:], activation='relu'))
model.add(Conv2D(8, (3, 3), input_shape=x_train.shape[1:], activation='relu', padding = "same"))
model.add(MaxPooling2D())
model.add(Conv2D(8, (3, 3), input_shape=x_train.shape[1:], activation='relu', padding = "same"))
model.add(Conv2D(8, (3, 3), input_shape=x_train.shape[1:], activation='relu', padding = "same"))
model.add(MaxPooling2D())
由于第二卷积层现在是原来大小的四分之一,因此大小现在要小得多:
Total params: 105,154
让我们检查一下性能:
Training time: 105.30972981452942
Min Loss: 0.8419396163749695
Min Validation Loss: 0.9395202528476715
Max Accuracy: 0.7032
Max Validation Accuracy: 0.6686999797821045
虽然训练准确度并不高,但验证准确度是我们所达到的最佳水平,而且所有这些只使用了 100 K 个参数!
在调整网络的卷积部分之后,现在是时候看看我们如何调整由密集层组成的部分。
调整密集层
让我们回到初始模型,并将密集层增加到 4 倍,即 1,024 个神经元:
Total params: 1,854,698
如预期的那样,参数的数量几乎增加了四倍。但是性能如何?
Training time: 122.05767631530762
Min Loss: 0.6533840216350555
Min Validation Loss: 1.093649614238739
Max Accuracy: 0.7722
Max Validation Accuracy: 0.630299985408783
训练准确度还不错,但与最佳模型相比,验证准确度较低。
让我们尝试使用三个密集层:
model.add(Dense(units = 512, activation = "relu"))
model.add(Dense(units = 256, activation = "relu"))
model.add(Dense(units = 128, activation = "relu"))
现在我们得到了以下参数:
Total params: 1,087,850
参数的数量现在更少了:
Training time: 111.73353481292725
Min Loss: 0.7527586654126645
Min Validation Loss: 1.1094331634044647
Max Accuracy: 0.7332
Max Validation Accuracy: 0.6115000247955322
结果可能有些令人失望。我们可能不应该过多地依赖增加密集层的数量。
下一步现在是要训练网络。让我们开始。
训练网络
我们现在可以更深入地讨论训练阶段,这是“魔法”发生的地方。我们甚至不会尝试描述其背后的数学概念。我们只会用非常通用和简化的术语讨论用于训练神经网络的算法。
我们需要一些定义:
-
损失函数或代价函数:一个计算神经网络预测与预期标签之间距离的函数;它可能是MSE(即均方误差)或更复杂的某种函数。
-
导数:函数的导数是一个新函数,可以测量函数在特定点上的变化程度(以及变化方向)。例如,如果你想象自己在一辆车上,速度可以是你的初始函数,其导数是加速度。如果速度是恒定的,导数(例如,加速度)为零;如果速度在增加,导数将是正的,如果速度在减少,导数将是负的。
-
局部最小值:神经网络的工作是使损失函数最小化。考虑到参数数量巨大,神经网络的函数可以非常复杂,因此达到全局最小值可能是不可能的,但网络仍然可以达到一个很好的局部最小值。
-
收敛:如果网络持续接近一个好的局部最小值,那么它就是在收敛。
使用这些定义,我们现在将看到训练实际上是如何进行的。
如何训练网络
算法由两部分组成,为了简化,让我们说它是为每个样本执行的,当然,整个过程在每个 epoch 都会重复。所以,让我们看看它是如何工作的:
-
前向传播:最终,你的神经网络只是一个具有许多参数(权重和可能的偏差)以及许多操作的函数,当提供输入时,可以计算一些输出。在前向传播中,我们计算预测和损失。
-
反向传播:优化器(例如,Adam 或随机梯度下降)向后(从最后一层到第一层)更新所有权重(例如,所有参数),试图最小化损失函数;学习率(一个介于 0 和 1.0 之间的数字,通常为 0.01 或更小)决定了权重将调整多少。
更大的学习率可以使它们训练得更快,但可能会跳过局部最小值,而较小的学习率可能会收敛,但花费太多时间。优化器正在积极研究,以尽可能提高训练速度,并且它们会动态地改变学习率来提高速度和精度。
Adam 是一个可以动态改变每个参数学习率的优化器示例:
图 5.4 – 梯度下降寻找最小值
虽然编写训练神经网络的算法非常复杂,但从某种意义上说,这个概念与某人试图学习打台球类似,直到重复相同的复杂射击直到成功。你选择你想要击中的点(标签),你做出你的动作(前向传播),你评估你离目标有多远,然后你尝试调整力量、方向以及所有其他变量(权重)。我们也有一种随机初始化的方法。让我们试试下一个。
随机初始化
你可能会想知道第一次运行神经网络时参数的值。将权重初始化为零效果不佳,而使用小的随机数则相当有效。Keras 提供了多种算法供你选择,你也可以更改标准差。
这个有趣的结果是,神经网络开始时带有相当数量的随机数据,你可能注意到在同一个数据集上用同一个模型训练实际上会产生不同的结果。让我们用我们之前的基本 CIFAR-10 CNN 来尝试。
第一次尝试产生了以下结果:
Min Loss: 0.8791077242803573
Min Validation Loss: 1.1203862301826477
Max Accuracy: 0.69174
Max Validation Accuracy: 0.5996000170707703
第二次尝试产生了以下结果:
Min Loss: 0.8642362675189972
Min Validation Loss: 1.1310886552810668
Max Accuracy: 0.69624
Max Validation Accuracy: 0.6100000143051147
你可以尝试使用以下代码来减少随机性:
from numpy.random import seed
seed(1)
import tensorflow as tf
tf.random.set_seed(1)
然而,如果你在 GPU 上训练,仍然可能存在许多变化。在调整你的网络模型时,你应该考虑这一点,否则你可能会因为随机性而排除小的改进。
下一个阶段是了解过拟合和欠拟合是什么。
过拟合和欠拟合
在训练神经网络时,你将在欠拟合和过拟合之间进行斗争。让我们看看如何:
-
欠拟合是指模型过于简单,无法正确学习数据集。你需要添加参数、滤波器和神经元来增加模型的容量。
-
过拟合是指你的模型足够大,可以学习训练数据集,但它无法泛化(例如,它记忆了数据集,但当你提供其他数据时效果不佳)。
你也可以从准确率和损失随时间变化的图中看到:
图 5.5 – 欠拟合:模型太小(7,590 个参数)且没有学习到很多
前面的图表显示了一个极端的欠拟合,准确率保持非常低。现在参考以下图表:
图 5.6 – 过拟合:模型非常大(29,766,666 个参数)且没有很好地泛化
图 5.6 展示了一个神经网络过拟合的相对极端例子。你可能注意到,虽然训练损失随着 epoch 的增加而持续下降,但验证损失在第一个 epoch 达到最小值,然后持续增加。验证损失的最小值是你想要停止训练的地方。在第六章,“改进你的神经网络”中,我们将看到一个允许我们做到这一点或类似的技术——提前停止。
虽然你可能听说过过拟合是一个大问题,但实际上,首先尝试得到一个可以过拟合训练数据集的模型,然后应用可以减少过拟合并提高泛化能力的技术的策略可能是一个好方法。然而,这只有在你能以非常高的精度过拟合训练数据集的情况下才是好的。
我们将在第六章“改进你的神经网络”中看到非常有效的方法来减少过拟合,但有一点需要考虑的是,较小的模型不太容易过拟合,而且通常也更快。所以,当你试图过拟合训练数据集时,尽量不要使用一个非常大的模型。
在本节中,我们看到了如何使用损失图来了解我们在网络训练中的位置。在下一节中,我们将看到如何可视化激活,以了解我们的网络正在学习什么。
可视化激活
现在我们可以训练一个神经网络。太好了。但神经网络能看懂和理解什么?这是一个很难回答的问题,但既然卷积输出一个图像,我们可以尝试展示这一点。现在让我们尝试展示 MINST 测试数据集前 10 个图像的激活:
-
首先,我们需要构建一个模型,这个模型是从我们之前的模型派生出来的,它从输入读取并得到我们想要的卷积层作为输出。名称可以来自摘要。我们将可视化第一个卷积层,
conv2d_1:conv_layer = next(x.output for x in model.layers if x.output.name.startswith(conv_name))act_model = models.Model(inputs=model.input, outputs=[conv_layer])activations = act_model.predict(x_test[0:num_predictions, :, :, :]) -
现在,对于每个测试图像,我们可以取所有激活并将它们连接起来以得到一个图像:
col_act = [] for pred_idx, act in enumerate(activations): row_act = [] for idx in range(act.shape[2]): row_act.append(act[:, :, idx]) col_act.append(cv2.hconcat(row_act)) -
然后我们可以展示它:
plt.matshow(cv2.vconcat(col_act), cmap='viridis')plt.show()
这是第一卷积层conv2d_1的结果,它有 6 个通道,28x28:
图 5.7 – MNIST,第一卷积层的激活
这看起来很有趣,但试图理解激活,以及通道学习识别的内容,总是涉及一些猜测工作。最后一个通道似乎专注于水平线,第三和第四个通道不是很强,这可能意味着网络没有正确训练,或者它已经比所需的要大。但看起来不错。
让我们现在检查第二层,conv2d_2,它有 16 个通道,10x10:
图 5.8 – MNIST,第二卷积层的激活
现在更复杂了——输出要小得多,我们有的通道也更多。看起来有些通道正在检测水平线,而有些则专注于对角线或垂直线。那么第一个最大池化层,max_pooling2d_1呢?它的分辨率低于原始通道,为 10x10,但它选择最大激活,应该是可以理解的。参考以下截图:
图 5.9 – MNIST,第一个最大池化层的激活状态
的确,激活状态看起来很好。为了好玩,让我们检查第二个最大池化层,max_pooling2d_2,它的大小是 5x5:
图 5.10 – MNIST,第二个最大池化层的激活状态
现在看起来很混乱,但仍然可以看出一些通道正在识别水平线,而另一些则专注于垂直线。这就是密集层发挥作用的时候,因为它们试图理解这些难以理解但并非完全随机的激活状态。
可视化激活状态对于了解神经网络正在学习什么、通道是如何被使用的非常有用,并且它是你在训练神经网络时可以使用的另一个工具,尤其是当你觉得它学习得不好,正在寻找问题时。
现在我们将讨论推理,这实际上是训练神经网络的全部目的。
推理
推理是将输入提供给你的网络并得到分类或预测的过程。当神经网络经过训练并部署到生产环境中时,我们使用它,例如,来分类图像或决定如何在道路上驾驶,这个过程称为推理。
第一步是加载模型:
model = load_model(os.path.join(dir_save, model_name))
然后,你只需调用 predict(),这是 Keras 中的推理方法。让我们用 MNIST 的第一个测试样本来试一试:
x_pred = model.predict(x_test[0:1, :, :, :])print("Expected:", np.argmax(y_test))print("First prediction probabilities:", x_pred)print("First prediction:", np.argmax(x_pred))
这是我的 MNIST 网络的结果:
Expected: 7
First prediction probabilities: [[6.3424804e-14 6.1755254e-06 2.5011676e-08 2.2640785e-07 9.0170204e-08 7.4626680e-11 5.6195684e-13 9.9999273e-01 1.9735349e-09 7.3219508e-07]]
First prediction: 7
predict() 函数的结果是一个概率数组,这对于评估网络的置信度非常方便。在这种情况下,所有数字都非常接近零,除了数字 7,因此它是网络的预测,置信度超过 99.999%!在现实生活中,不幸的是,你很少看到网络工作得如此好!
在推理之后,有时你可能想要定期在新样本上重新训练,以改进网络。让我们看看这需要做什么。
重新训练
有时候,一旦你得到了一个表现良好的神经网络,你的工作就完成了。然而,有时候你可能想要在新样本上重新训练它,以获得更高的精度(因为你的数据集现在更大了),或者如果你的训练数据集变得相对快速过时,以获得更新的结果。
在某些情况下,你可能甚至想要持续不断地重新训练,例如每周一次,并将新模型自动部署到生产环境中。
在这种情况下,你有一个强大的程序来验证你在验证数据集上新的模型性能,以及在新的、可丢弃的测试数据集上性能是至关重要的。也许还建议保留所有模型的备份,并尝试找到一种方法来监控生产中的性能,以便快速识别异常。在自动驾驶汽车的情况下,我预计模型在投入生产之前将经历严格的自动和手动测试,但其他没有安全问题的行业可能要宽松得多。
有了这些,我们结束了关于重新训练的主题。
摘要
这是一章内容密集的章节,但希望你能更好地了解神经网络是什么以及如何训练它们。
我们讨论了很多关于数据集的内容,包括如何获取用于训练、验证和测试的正确数据集。我们描述了分类器是什么,并实现了数据增强。然后我们讨论了模型以及如何调整卷积层、MaxPooling 层和密集层。我们看到了训练是如何进行的,什么是反向传播,讨论了随机性在权重初始化中的作用,并展示了欠拟合和过度拟合网络的图表。为了了解我们的 CNN 表现如何,我们甚至可视化激活。然后我们讨论了推理和重新训练。
这意味着你现在有足够的知识来选择或创建一个数据集,从头开始训练一个神经网络,并且你将能够理解模型或数据集的变化是否提高了精度。
在第六章 提高你的神经网络中,我们将看到如何将这些知识应用到实践中,以便显著提高神经网络的精度。
问题
在阅读这一章后,你应该能够回答以下问题:
-
你可以重复使用测试数据集吗?
-
数据增强是什么?
-
Keras 中的数据增强是否向你的数据集添加图像?
-
哪一层倾向于具有最多的参数?
-
观察损失曲线,你如何判断一个网络正在过度拟合?
-
网络过度拟合是否总是不好的?
第六章:第六章:改进你的神经网络
在第四章“使用神经网络的深度学习”中,我们设计了一个能够在训练数据集中达到几乎 93%准确率的网络,但它在验证数据集中的准确率却低于 66%。
在本章中,我们将继续改进那个神经网络,目标是显著提高验证准确率。我们的目标是达到至少 80%的验证准确率。我们将应用在第五章“深度学习工作流程”中获得的一些知识,我们还将学习一些对我们非常有帮助的新技术,例如批量归一化。
我们将涵盖以下主题:
-
通过数据增强减少参数数量
-
增加网络大小和层数
-
理解批量归一化
-
使用提前停止提高验证准确率
-
通过数据增强几乎增加数据集大小
-
使用 dropout 提高验证准确率
-
使用空间 dropout 提高验证准确率
技术要求
本章的完整源代码可以在以下位置找到:github.com/PacktPublishing/Hands-On-Vision-and-Behavior-for-Self-Driving-Cars/tree/master/Chapter6
本章需要以下软件先决条件,以及以下基本知识将有助于更好地理解本章:
-
Python 3.7
-
NumPy 模块
-
Matplotlib 模块
-
TensorFlow 模块
-
Keras 模块
-
OpenCV-Python 模块
-
推荐的 GPU
本章的“代码实战”视频可以在以下位置找到:
更大的模型
训练自己的神经网络是一种艺术;你需要直觉、一些运气、大量的耐心,以及你能找到的所有知识和帮助。你还需要资金和时间,要么购买更快的 GPU,使用集群测试更多配置,或者付费获取更好的数据集。
但没有真正的食谱。话虽如此,我们将把我们的旅程分为两个阶段,如第五章“深度学习工作流程”中所述:
-
过拟合训练数据集
-
提高泛化能力
我们将从第四章“使用神经网络的深度学习”中我们留下的地方开始,我们的基本模型在 CIFAR-10 上达到了 66%的验证准确率,然后我们将显著改进它,首先是让它更快,然后是让它更精确。
起始点
以下是我们第四章“使用神经网络的深度学习”中开发的模型,该模型由于在相对较低的验证准确率下实现了较高的训练准确率,因此过拟合了数据集:
model.add(Conv2D(filters=64, kernel_size=(3, 3), activation='relu', input_shape=x_train.shape[1:]))
model.add(AveragePooling2D())
model.add(Conv2D(filters=256, kernel_size=(3, 3), activation='relu'))
model.add(AveragePooling2D())
model.add(Flatten())
model.add(Dense(units=512, activation='relu'))
model.add(Dense(units=256, activation='relu'))
model.add(Dense(units=num_classes, activation = 'softmax'))
它是一个浅但相对较大的模型,因为它有以下数量的参数:
Total params: 5,002,506
我们之前训练了 12 个 epoch,结果如下:
Training time: 645.9990749359131
Min Loss: 0.12497963292273692
Min Validation Loss: 0.9336215916395187
Max Accuracy: 0.95826
Max Validation Accuracy: 0.6966000199317932
训练准确率实际上对我们来说已经足够好了(在这里,在这个运行中,它高于第五章,“深度学习工作流程”,主要是因为随机性),但验证准确率也很低。它是过拟合的。因此,我们甚至可以将其作为起点,但最好对其进行一点调整,看看我们是否可以做得更好或使其更快。
我们还应该关注五个 epoch,因为我们可能会在较少的 epoch 上进行一些测试,以加快整个过程:
52s 1ms/step - loss: 0.5393 - accuracy: 0.8093 - val_loss: 0.9496 - val_accuracy: 0.6949
当你使用较少的 epoch 时,你是在赌自己能够理解曲线的演变,因此你是在用开发速度换取选择准确性。有时,这很好,但有时则不然。
我们的模型太大,所以我们将开始减小其尺寸并稍微加快训练速度。
提高速度
我们的模式不仅非常大——事实上,它太大了。第二个卷积层有 256 个过滤器,与密集层的 512 个神经元结合,它们使用了大量的参数。我们可以做得更好。我们知道我们可以将它们分成 128 个过滤器的层,这将节省几乎一半的参数,因为密集层现在只需要一半的连接。
我们可以尝试一下。我们在第四章,“使用神经网络的深度学习”中了解到,为了在卷积后不丢失分辨率,我们可以在两层(密集层省略)上以相同的方式使用填充,如下所示:
model.add(Conv2D(filters=64, kernel_size=(3, 3), activation='relu', input_shape=x_train.shape[1:]))
model.add(AveragePooling2D())
model.add(Conv2D(filters=128, kernel_size=(3, 3), activation='relu', padding="same"))
model.add(Conv2D(filters=128, kernel_size=(3, 3), activation='relu', padding="same"))
model.add(AveragePooling2D())
在这里,我们可以看到现在的参数数量更低:
Total params: 3,568,906
让我们检查完整的结果:
Training time: 567.7167596817017
Min Loss: 0.1018450417491654
Min Validation Loss: 0.8735350118398666
Max Accuracy: 0.96568
Max Validation Accuracy: 0.7249000072479248
太好了!它更快了,准确率略有提高,而且,验证也提高了!
让我们在第一层做同样的操作,但这次不增加分辨率,以免增加参数,因为,在两个卷积层之间,增益较低:
model.add(Conv2D(filters=32, kernel_size=(3, 3), activation='relu', input_shape=x_train.shape[1:]))model.add(Conv2D(filters=32, kernel_size=(3, 3), activation='relu', input_shape=x_train.shape[1:], padding="same"))model.add(AveragePooling2D())model.add(Conv2D(filters=128, kernel_size=(3, 3), activation='relu', padding="same"))model.add(Conv2D(filters=128, kernel_size=(3, 3), activation='relu', padding="same"))model.add(AveragePooling2D())
当我们尝试这样做时,我们得到以下结果:
Training time: 584.955037355423
Min Loss: 0.10728564778155182
Min Validation Loss: 0.7890052844524383
Max Accuracy: 0.965
Max Validation Accuracy: 0.739300012588501
这与之前类似,尽管验证准确率略有提高。
接下来,我们将添加更多层。
增加深度
之前的模型实际上是一个很好的起点。
但我们将添加更多层,以增加非线性激活的数量,并能够学习更复杂的函数。这是模型(密集层省略):
model.add(Conv2D(filters=32, kernel_size=(3, 3), activation='relu', input_shape=x_train.shape[1:], padding="same"))model.add(Conv2D(filters=32, kernel_size=(3, 3), activation='relu', input_shape=x_train.shape[1:], padding="same"))model.add(AveragePooling2D())model.add(Conv2D(filters=128, kernel_size=(3, 3), activation='relu', padding="same"))model.add(Conv2D(filters=128, kernel_size=(3, 3), activation='relu', padding="same"))model.add(AveragePooling2D())model.add(Conv2D(filters=256, kernel_size=(3, 3), activation='relu', padding="same"))model.add(Conv2D(filters=256, kernel_size=(3, 3), activation='relu', padding="same"))model.add(AveragePooling2D())
这是结果:
Training time: 741.1498856544495
Min Loss: 0.22022022939510644
Min Validation Loss: 0.7586277635633946
Max Accuracy: 0.92434
Max Validation Accuracy: 0.7630000114440918
网络现在明显变慢,准确率下降(可能因为需要更多的 epoch),但验证准确率提高了。
让我们现在尝试减少密集层,如下所示(卷积层省略):
model.add(Flatten())model.add(Dense(units=256, activation='relu'))model.add(Dense(units=128, activation='relu'))model.add(Dense(units=num_classes, activation = 'softmax'))
现在,我们有更少的参数:
Total params: 2,162,986
但发生了一件非常糟糕的事情:
Training time: 670.0584089756012
Min Loss: 2.3028031995391847
Min Validation Loss: 2.302628245162964
Max Accuracy: 0.09902
Max Validation Accuracy: 0.10000000149011612
两个验证都下降了!事实上,它们现在为 10%,或者如果你愿意,网络现在正在产生随机结果——它没有学习!
你可能会得出结论说我们把它搞坏了。实际上并非如此。只需再次运行它,利用随机性来利用它,我们的网络就会按预期学习:
Training time: 686.5172057151794
Min Loss: 0.24410496438018978
Min Validation Loss: 0.7960220139861107
Max Accuracy: 0.91434
Max Validation Accuracy: 0.7454000115394592
然而,这并不是一个好兆头。这可能是由于层数的增加,因为具有更多层的网络更难训练,因为原始输入在向上层传播时可能存在问题。
让我们检查一下图表:
图 6.1 – 损失和准确率图
你可以看到,虽然训练损失(蓝色线)持续下降,但经过一些个 epoch 后,验证损失(橙色线)开始上升。正如在 第五章 的 深度学习工作流程 中解释的那样,这意味着模型过拟合了。这不一定是最优模型,但我们将继续开发它。
在下一节中,我们将简化这个模型。
一个更高效的网络
在我的笔记本电脑上训练先前的模型需要 686 秒,并达到 74.5% 的验证准确率和 91.4% 的训练准确率。理想情况下,为了提高效率,我们希望在保持准确率不变的同时减少训练时间。
让我们检查一些卷积层:
图 6.2 – 第一卷积层,32 个通道
我们已经在 第五章 的 深度学习工作流程 中看到了这些激活图,并且我们知道黑色通道没有达到大的激活,因此它们对结果贡献不大。在实践中,看起来一半的通道都没有使用。让我们尝试在每个卷积层中将通道数减半:
model.add(Conv2D(filters=16, kernel_size=(3, 3), activation='relu', input_shape=x_train.shape[1:], padding="same"))
model.add(Conv2D(filters=16, kernel_size=(3, 3), activation='relu', input_shape=x_train.shape[1:], padding="same"))
model.add(AveragePooling2D())
model.add(Conv2D(filters=32, kernel_size=(3, 3), activation='relu', padding="same"))
model.add(Conv2D(filters=32, kernel_size=(3, 3), activation='relu', padding="same"))
model.add(AveragePooling2D())
model.add(Conv2D(filters=64, kernel_size=(3, 3), activation='relu', padding="same"))
model.add(Conv2D(filters=64, kernel_size=(3, 3), activation='relu', padding="same"))
这是我们的结果:
Total params: 829,146
如预期,参数数量现在少得多,训练速度也快得多:
Training time: 422.8525400161743
Min Loss: 0.27083665314182637
Min Validation Loss: 0.8076118688702584
Max Accuracy: 0.90398
Max Validation Accuracy: 0.7415000200271606
我们可以看到,我们失去了一些准确率,但不是太多:
图 6.3 – 第一卷积层,16 个通道
现在好一些了。让我们检查第二层:
图 6.4 – 第二卷积层,16 个通道
这也好一些。让我们检查一下第四卷积层:
图 6.5 – 第四卷积层,64 个通道
它看起来有点空。让我们将第三层和第四层减半:
Total params: 759,962
我们得到了以下结果:
Training time: 376.09818053245544
Min Loss: 0.30105597005218265
Min Validation Loss: 0.8148738072395325
Max Accuracy: 0.89274
Max Validation Accuracy: 0.7391999959945679
训练准确率下降了,但验证准确率仍然不错:
图 6.6 – 第四卷积层,32 个通道
让我们检查第六卷积层:
图 6.7 – 第六卷积层,128 个通道
它有点空。让我们也将最后两个卷积层减半:
Total params: 368,666
它要小得多,这些结果是:
Training time: 326.9148383140564
Min Loss: 0.296858479853943
Min Validation Loss: 0.7925313812971115
Max Accuracy: 0.89276
Max Validation Accuracy: 0.7425000071525574
它看起来仍然不错。让我们检查一下激活情况:
图 6.8 – 第六卷积层,64 个通道
你可以看到现在,许多通道被激活了,这希望是神经网络更好地利用其资源的指示。
将这个模型与上一节中构建的模型进行比较,你可以看到这个模型可以在不到一半的时间内训练完成,验证准确率几乎未变,训练准确率略有下降,但不是很多。所以,它确实更有效率。
在下一节中,我们将讨论批量归一化,这是一个在现代神经网络中非常常见的层。
使用批量归一化构建更智能的网络
我们将提供给网络的输入进行归一化,将范围限制在 0 到 1 之间,因此在中部网络中也这样做可能是有益的。这被称为批量归一化,它确实很神奇!
通常,你应该在想要归一化的输出之后、激活之前添加批量归一化,但添加在激活之后可能会提供更快的性能,这正是我们将要做的。
这是新的代码(省略了密集层):
model.add(Conv2D(filters=16, kernel_size=(3, 3), activation='relu', input_shape=x_train.shape[1:], padding="same"))model.add(Conv2D(filters=16, kernel_size=(3, 3), activation='relu', input_shape=x_train.shape[1:], padding="same"))model.add(BatchNormalization())model.add(AveragePooling2D())model.add(Conv2D(filters=32, kernel_size=(3, 3), activation='relu', padding="same"))model.add(Conv2D(filters=32, kernel_size=(3, 3), activation='relu', padding="same"))model.add(BatchNormalization())model.add(AveragePooling2D())model.add(Conv2D(filters=64, kernel_size=(3, 3), activation='relu', padding="same"))model.add(Conv2D(filters=64, kernel_size=(3, 3), activation='relu', padding="same"))model.add(BatchNormalization())model.add(AveragePooling2D())
参数数量仅略有增加:
Total params: 369,114
这是结果:
Training time: 518.0608556270599
Min Loss: 0.1616916553277429
Min Validation Loss: 0.7272815862298012
Max Accuracy: 0.94308
Max Validation Accuracy: 0.7675999999046326
还不错,尽管不幸的是,现在它要慢得多。但我们可以添加更多的批量归一化,看看是否可以改善情况:
Training time: 698.9837136268616
Min Loss: 0.13732857785719446
Min Validation Loss: 0.6836542286396027
Max Accuracy: 0.95206
Max Validation Accuracy: 0.7918999791145325
是的,准确率都有所提高。我们实际上非常接近我们最初的 80%准确率的目标。但让我们更进一步,看看我们能做什么。
到目前为止,我们只使用了 ReLU 激活函数,但即使它被广泛使用,它也不是唯一的。Keras 支持多种激活函数,有时进行实验是值得的。我们将坚持使用 ReLU。
让我们检查一些激活情况:
图 6.9 – 第二卷积层,16 个通道,批量归一化
现在,第二层的所有通道都在学习。非常好!
这是第四层的成果:
图 6.10 – 第四卷积层,32 个通道,批量归一化
这是第六层的成果:
图 6.11 – 第六卷积层,64 个通道,批量归一化
它开始看起来不错了!
让我们尝试可视化批量归一化对第一层在批量归一化前后激活情况的影响:
图 6.12 – 第一卷积层,16 个通道,批标准化前后的对比
你可以看到,通道的强度现在更加均匀;不再有既无活动又没有非常强烈的激活的通道。然而,那些不活跃的通道仍然没有真实信息。
让我们也检查第二层:
图 6.13 – 第二卷积层,16 个通道,批标准化前后的对比
这里可能不太明显,但你仍然可以看到通道之间的差异减少了,因为很明显,它们已经被归一化。直观上看,这有助于通过层传播较弱的信号,并且它有一些正则化效果,这导致更好的验证准确率。
既然我们已经讨论了批标准化,现在是时候进一步讨论什么是批,以及批的大小有什么影响。
选择合适的批大小
在训练过程中,我们有大量的样本,通常从几千到几百万。如果你还记得,优化器将计算损失函数并更新超参数以尝试减少损失。它可以在每个样本之后这样做,但结果可能会很嘈杂,连续的变化可能会减慢训练。在另一端,优化器可能只在每个 epoch 更新一次超参数——例如,使用梯度的平均值——但这通常会导致泛化不良。通常,批大小有一个范围,比这两个极端表现更好,但不幸的是,它取决于特定的神经网络。
较大的批量大小的确可以略微提高 GPU 上的训练时间,但如果你的模型很大,你可能会发现你的 GPU 内存是批量大小的限制因素。
批标准化也受到批量大小的影響,因为小批量会降低其有效性(因为没有足够的数据进行适当的归一化)。
考虑到这些因素,最好的做法是尝试。通常,你可以尝试使用 16、32、64 和 128,如果看到最佳值在范围的极限,最终可以扩展范围。
正如我们所见,最佳批大小可以提高准确率并可能提高速度,但还有一种技术可以帮助我们在加快速度的同时提高验证准确率,或者至少简化训练:提前停止。
提前停止
我们应该在什么时候停止训练?这是一个好问题!理想情况下,你希望在最小验证错误时停止。虽然你事先不知道这一点,但你可以通过检查损失来获得需要多少个 epoch 的概览。然而,当你训练你的网络时,有时你需要更多的 epoch,这取决于你如何调整你的模型,而且事先知道何时停止并不简单。
我们已经知道我们可以使用ModelCheckpoint,这是 Keras 的一个回调,在训练过程中保存具有最佳验证错误的模型。
但还有一个非常有用的回调,EarlyStopping,当预定义的一组条件发生时,它会停止训练:
stop = EarlyStopping(min_delta=0.0005, patience=7, verbose=1)
配置提前停止最重要的参数如下:
-
monitor: 这决定了要监控哪个参数,默认为验证损失。 -
min_delta: 如果 epoch 之间的验证损失差异低于此值,则认为损失没有变化。 -
耐心: 这是允许在停止训练之前没有验证改进的 epoch 数量。 -
verbose: 这是指示 Keras 提供更多信息。
我们需要提前停止的原因是,在数据增强和 dropout 的情况下,我们需要更多的 epoch,而不是猜测何时停止,我们将使用提前停止来为我们完成这项工作。
现在让我们来谈谈数据增强。
使用数据增强改进数据集
是时候使用数据增强了,基本上,增加我们数据集的大小。
从现在起,我们将不再关心训练数据集的精度,因为这项技术会降低它,但我们将关注验证精度,预计它会提高。
我们也预计需要更多的 epoch,因为我们的数据集现在更难了,所以我们将 epoch 设置为500(尽管我们并不打算达到它)并使用具有7个 patience 的EarlyStopping。
让我们尝试这个增强:
ImageDataGenerator(rotation_range=15, width_shift_range=[-5, 0, 5], horizontal_flip=True)
您应该注意不要过度操作,因为网络可能会学习到一个与验证集差异很大的数据集,在这种情况下,您将看到验证精度停滞在 10%。
这是结果:
Epoch 00031: val_loss did not improve from 0.48613
Epoch 00031: early stopping
Training time: 1951.4751739501953
Min Loss: 0.3638068118467927
Min Validation Loss: 0.48612626193910835
Max Accuracy: 0.87454
Max Validation Accuracy: 0.8460999727249146
提前停止在31个 epoch 后中断了训练,我们达到了 84%以上的验证精度——还不错。正如预期的那样,我们现在需要更多的 epoch。这是损失图:
图 6.14 – 使用数据增强和提前停止的损失
您可以看到训练精度一直在增加,而验证精度在某些时候下降了。网络仍然有点过拟合。
让我们检查第一卷积层的激活:
图 6.15 – 使用数据增强和提前停止的第一卷积层,16 个通道
它略有改善,但可能还会再次提高。
我们可以尝试稍微增加数据增强:
ImageDataGenerator(rotation_range=15, width_shift_range=[-8, -4, 0, 4, 8], horizontal_flip=True, height_shift_range=[-5, 0, 5], zoom_range=[0.9, 1.1])
这是结果:
Epoch 00040: early stopping
Training time: 2923.3936190605164
Min Loss: 0.5091392234659194
Min Validation Loss: 0.5033097203373909
Max Accuracy: 0.8243
Max Validation Accuracy: 0.8331999778747559
这个模型速度较慢且精度较低。让我们看看图表:
图 6.16 – 增加数据增强和提前停止的损失
可能需要更多的耐心。我们将坚持之前的数据增强。
在下一节中,我们将分析一种简单但有效的方法,通过使用 dropout 层来提高验证准确率。
使用 dropout 提高验证准确率
过拟合的一个来源是神经网络更依赖于一些神经元来得出结论,如果这些神经元出错,网络也会出错。减少这种问题的一种方法是在训练期间随机关闭一些神经元,而在推理期间保持它们正常工作。这样,神经网络就能学会更加抵抗错误,更好地泛化。这种机制被称为dropout,显然,Keras 支持它。Dropout 会增加训练时间,因为网络需要更多的 epoch 来收敛。它可能还需要更大的网络,因为在训练期间一些神经元会被随机关闭。当数据集对于网络来说不是很大时,它更有用,因为它更有可能过拟合。实际上,由于 dropout 旨在减少过拟合,如果你的网络没有过拟合,它带来的好处很小。
对于密集层,dropout 的典型值是 0.5,尽管我们可能使用略小的值,因为我们的模型过拟合不多。我们还将增加耐心到20,因为现在模型需要更多的 epoch 来训练,验证损失可能会波动更长的时间。
让我们尝试在密集层中添加一些 dropout:
model.add(Flatten())
model.add(Dense(units=256, activation='relu'))
model.add(Dropout(0.4))
model.add(Dense(units=128, activation='relu'))
model.add(Dropout(0.2))
model.add(Dense(units=num_classes, activation = 'softmax'))
这是结果:
Epoch 00097: early stopping
Training time: 6541.777503728867
Min Loss: 0.38114651718586684
Min Validation Loss: 0.44884318161308767
Max Accuracy: 0.87218
Max Validation Accuracy: 0.8585000038146973
这有点令人失望。训练花费了很长时间,但只有微小的进步。我们假设我们的密集层有点小。
让我们将层的尺寸增加 50%,同时增加第一个密集层中的 dropout,并减少第二个密集层中的 dropout:
model.add(Flatten())model.add(Dense(units=384, activation='relu'))model.add(Dropout(0.5))model.add(Dense(units=192, activation='relu'))model.add(Dropout(0.1))model.add(Dense(units=num_classes, activation='softmax'))
当然,它更大,正如我们在这里可以看到的:
Total params: 542,426
它的结果略好一些:
Epoch 00122: early stopping
Training time: 8456.040931940079
Min Loss: 0.3601766444931924
Min Validation Loss: 0.4270844452492893
Max Accuracy: 0.87942
Max Validation Accuracy: 0.864799976348877
随着我们提高验证准确率,即使是小的进步也变得困难。
让我们检查一下图表:
图 6.17 – 在密集层上使用更多数据增强和 dropout 的损失
存在一点过拟合,所以让我们尝试修复它。我们也可以在卷积层中使用Dropout。
让我们尝试这个:
model.add(Conv2D(filters=16, kernel_size=(3, 3), activation='relu', input_shape=x_train.shape[1:], padding="same"))
model.add(BatchNormalization())
model.add(Dropout(0.5))
model.add(Conv2D(filters=16, kernel_size=(3, 3), activation='relu', input_shape=x_train.shape[1:], padding="same"))
model.add(BatchNormalization())
model.add(AveragePooling2D())
model.add(Dropout(0.5))
这是令人失望的结果:
Epoch 00133: early stopping
Training time: 9261.82032418251
Min Loss: 0.6104169194960594
Min Validation Loss: 0.4887285701841116
Max Accuracy: 0.79362
Max Validation Accuracy: 0.8417999744415283
网络没有改进!
这里,我们看到一个有趣的情况——验证准确率显著高于训练准确率。这是怎么可能的?
假设你的分割是正确的(例如,你没有包含与训练数据集太相似图像的验证集),两个因素可以造成这种情况:
-
数据增强可能会使训练数据集比验证数据集更难。
-
Dropout 在训练阶段是激活的,而在预测阶段是关闭的,这意味着训练数据集可以比验证数据集显著更难。
在我们这个例子中,罪魁祸首是Dropout。你不必一定避免这种情况,如果它是合理的,但在这个例子中,验证准确率下降了,所以我们需要修复我们的Dropout,或者也许增加网络的大小。
我发现Dropout在卷积层中使用起来更困难,我个人在那个情况下不会使用大的Dropout。这里有一些指导方针:
-
不要在
Dropout之后立即使用批量归一化,因为归一化会受到影响。 -
Dropout在MaxPooling之后比之前更有效。 -
在卷积层之后的
Dropout会丢弃单个像素,但SpatialDropout2D会丢弃通道,并且建议在神经网络开始的第一几个层中使用。
我又进行了一些(长!)实验,并决定增加卷积层的大小,减少Dropout,并在几个层中使用Spatial Dropout。最终我得到了这个神经网络,这是我认为的最终版本。
这是卷积层的代码:
model = Sequential()
model.add(Conv2D(filters=32, kernel_size=(3, 3), activation='relu', input_shape=x_train.shape[1:], padding="same"))
model.add(BatchNormalization())
model.add(Conv2D(filters=32, kernel_size=(3, 3), activation='relu', input_shape=x_train.shape[1:], padding="same"))
model.add(BatchNormalization())
model.add(AveragePooling2D())
model.add(SpatialDropout2D(0.2))
model.add(Conv2D(filters=48, kernel_size=(3, 3), activation='relu', padding="same"))
model.add(BatchNormalization())
model.add(Conv2D(filters=48, kernel_size=(3, 3), activation='relu', padding="same"))
model.add(BatchNormalization())
model.add(AveragePooling2D())
model.add(SpatialDropout2D(0.2))
model.add(Conv2D(filters=72, kernel_size=(3, 3), activation='relu', padding="same"))
model.add(BatchNormalization())
model.add(Conv2D(filters=72, kernel_size=(3, 3), activation='relu', padding="same"))
model.add(BatchNormalization())
model.add(AveragePooling2D())
model.add(Dropout(0.1))
And this is the part with the dense layers:model.add(Flatten())
model.add(Dense(units=384, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(units=192, activation='relu'))
model.add(Dropout(0.1))
model.add(Dense(units=num_classes, activation='softmax'))
这些是结果:
Epoch 00168: early stopping
Training time: 13122.931826591492
Min Loss: 0.4703261657243967
Min Validation Loss: 0.3803714614287019
Max Accuracy: 0.84324
Max Validation Accuracy: 0.8779000043869019
验证准确率有所提高:
图 6.18 – 在密集和卷积层上使用更多数据增强和Dropout的损失
恭喜!现在你有了如何训练神经网络的思路,并且可以自由地实验和发挥创意!每个任务都是不同的,可能性真的是无限的。
为了好玩,让我们再次训练它,看看相同的逻辑模型在 MNIST 上的表现如何。
将模型应用于 MNIST
我们之前的 MNIST 模型达到了 98.3%的验证准确率,正如你可能已经注意到的,你越接近 100%,提高模型就越困难。
我们的 CIFAR-10 是在与 MNIST 不同的任务上训练的,但让我们看看它的表现:
Epoch 00077: early stopping
Training time: 7110.028198957443
Min Loss: 0.04797766085289389
Min Validation Loss: 0.02718053938352254
Max Accuracy: 0.98681664
Max Validation Accuracy: 0.9919000267982483
这是它的图表:
图 6.19 – MNIST,损失
我希望每个任务都像 MNIST 一样简单!
出于好奇,这是第一层的激活:
图 6.20 – MNIST,第一卷积层的激活
如你所见,许多通道被激活,并且它们很容易检测到数字的最重要特征。
这可能是一个尝试 GitHub 上的代码并对其进行实验的绝佳时机。
现在轮到你了!
如果你有些时间,你真的应该尝试一个公共数据集,或者甚至创建你自己的数据集,并从头开始训练一个神经网络。
如果你已经没有想法了,可以使用 CIFAR-100。
记住,训练神经网络通常不是线性的——你可能需要猜测什么可以帮助你,或者你可能尝试许多不同的事情。并且记住要反复试验,因为当你的模型发展时,不同技术和不同层的重要性可能会改变。
摘要
这是一个非常实用的章节,展示了在训练神经网络时的一种进行方式。我们从一个大模型开始,实现了 69.7%的验证准确率,然后我们减小了其尺寸并添加了一些层来增加非线性激活的数量。我们使用了批量归一化来均衡所有通道的贡献,然后我们学习了提前停止,这有助于我们决定何时停止训练。
在学习如何自动停止训练后,我们立即将其应用于数据增强,这不仅增加了数据集的大小,还增加了正确训练网络所需的 epoch 数量。然后我们介绍了Dropout和SpatialDropout2D,这是一种强大的减少过拟合的方法,尽管使用起来并不总是容易。
我们最终得到了一个达到 87.8%准确率的网络。
在下一章中,我们将训练一个能够在空旷的轨道上驾驶汽车的神经网络!
问题
在本章之后,你将能够回答以下问题:
-
我们为什么想使用更多层?
-
具有更多层的网络是否自动比较浅的网络慢?
-
我们如何知道何时停止训练模型?
-
我们可以使用哪个 Keras 函数在模型开始过拟合之前停止训练?
-
你如何归一化通道?
-
你如何有效地使你的数据集更大、更难?
-
dropout 会使你的模型更鲁棒吗?
-
如果你使用数据增强,你会期望训练变慢还是变快?
-
如果你使用 dropout,你会期望训练变慢还是变快?
第七章:第七章:检测行人和交通灯
恭喜您完成了深度学习的学习,并进入了这个新的章节!现在您已经了解了如何构建和调整神经网络的基础知识,是时候转向更高级的主题了。
如果您还记得,在 第一章,OpenCV 基础和相机标定,我们已经使用 OpenCV 检测了行人。在本章中,我们将学习如何使用一个非常强大的神经网络——单次多框检测器(SSD)来检测对象,我们将使用它来检测行人、车辆和交通灯。此外,我们将通过迁移学习训练一个神经网络来检测交通灯的颜色,迁移学习是一种强大的技术,可以帮助您使用相对较小的数据集获得良好的结果。
在本章中,我们将涵盖以下主题:
-
检测行人和交通灯
-
使用 CARLA 收集图像
-
使用 单次多框检测器(SSD)进行目标检测
-
检测交通灯的颜色
-
理解迁移学习
-
Inception 的理念
-
识别交通灯及其颜色
技术要求
要能够使用本章中解释的代码,您需要安装以下工具和模块:
-
Carla 模拟器
-
Python 3.7
-
NumPy 模块
-
TensorFlow 模块
-
Keras 模块
-
OpenCV-Python 模块
-
一个 GPU(推荐)
本章的代码可以在 github.com/PacktPublishing/Hands-On-Vision-and-Behavior-for-Self-Driving-Cars/tree/master/Chapter7 找到。
本章的“代码实战”视频可以在以下位置找到:
使用 SSD 检测行人、车辆和交通灯
当自动驾驶汽车在道路上行驶时,它肯定需要知道车道在哪里,并检测可能存在于道路上的障碍物(包括人!),它还需要检测交通标志和交通灯。
在本章中,我们将迈出重要的一步,因为我们将学习如何检测行人、车辆和交通灯,包括交通灯的颜色。我们将使用 Carla 生成所需的图像。
解决我们的任务是两步过程:
-
首先,我们将检测车辆、行人和交通灯(无颜色信息),我们将使用一个名为 SSD 的预训练神经网络。
-
然后,我们将检测交通灯的颜色,这需要我们从名为 Inception v3 的预训练神经网络开始训练一个神经网络,使用迁移学习技术,并且我们还需要收集一个小数据集。
因此,让我们首先使用 Carla 来收集图像。
使用 Carla 收集一些图像
我们需要一些带有行人、车辆和交通灯的街道照片。我们可以使用 Carla 来做这件事,但这次,我们将更详细地讨论如何使用 Carla 收集数据集。您可以在 carla.org/ 找到 Carla。
您可以在 Carla GitHub 页面上找到 Linux 和 Windows 的二进制文件:
github.com/carla-simulator/carla/releases
安装说明可以在 Carla 网站上找到:
carla.readthedocs.io/en/latest/start_quickstart/
如果您使用 Linux,则使用 CarlaUE4.sh 命令启动 Carla,而在 Windows 上,它被称为 CarlaUE4.exe。我们将其称为 CarlaUE4。您可以在没有参数的情况下运行它,或者您可以手动设置分辨率,如下所示:
CarlaUE4 -windowed -ResX=640 -ResY=480
在 Carla 中,您可以使用一些键在轨道周围移动:
-
W:向前
-
S:向后
-
A:向左,侧向
-
D:向右,侧向
此外,在 Carla 中,您可以使用鼠标,按住左键并移动光标来改变视角的角度并沿其他角度移动。
您应该看到以下类似的内容:
图 7.1 – Carla – 默认轨道
虽然服务器有时很有用,但您可能想运行一些位于 PythonAPI\util 和 PythonAPI\examples 中的文件。
对于这个任务,我们将使用 Town01 改变轨道。您可以使用 PythonAPI\util\config.py 文件这样做,如下所示:
python config.py -m=Town01
您现在应该看到一个不同的轨道:
图 7.2 – Town01 轨道
您的城市是空的,因此我们需要添加一些车辆和一些行人。我们可以使用 PythonAPI\examples\spawn_npc.py 来完成,如下所示:
python spawn_npc.py -w=100 -n=100
-w 参数指定要创建的行人数量,而 –n 指定要创建的车辆数量。现在,您应该看到一些动作:
图 7.3 – 带有车辆和行人的 Town01 轨道
好多了。
Carla 设计为作为服务器运行,您可以连接多个客户端,这应该允许进行更有趣的模拟。
当您运行 Carla 时,它启动一个服务器。您可以使用服务器四处走动,但您可能更希望运行一个客户端,因为它可以提供更多功能。如果您运行一个客户端,您将有两个带有 Carla 的窗口,这是预期的:
-
让我们使用
PythonAPI\examples\manual_control.py运行一个客户端,如下所示:python manual_control.py您可能会看到以下类似的内容:
图 7.4 – 使用 manual_control.py 的 Town01 轨道
您可以在左侧看到很多统计数据,并且可以使用 F1 键切换它们。您会注意到现在您有一辆车,并且可以使用退格键更改它。
-
你可以使用与之前相同的按键移动,但这次,行为更加有用和逼真,因为有一些物理模拟。你还可以使用箭头键进行移动。
你可以使用Tab键切换相机,C键更改天气,正如我们可以在以下截图中所见:
图 7.5 – Town01 赛道;中午大雨和日落时晴朗的天空
Carla 有许多传感器,其中之一是 RGB 相机,你可以使用`(反引号键)在它们之间切换。现在,请参考以下截图:
图 7.6 – Town01 赛道 – 左:深度(原始),右:语义分割
上述截图显示了几款非常有趣的传感器:
-
深度传感器,为每个像素提供从摄像机到距离
-
语义分割传感器,使用不同的颜色对每个对象进行分类
在撰写本文时,完整的摄像机传感器列表如下:
-
摄像机 RGB
-
摄像机深度(原始)
-
摄像机深度(灰度)
-
摄像机深度(对数灰度)
-
摄像机语义分割(CityScapes 调色板)
-
激光雷达(射线投射)
-
动态视觉传感器(DVS)
-
摄像机 RGB 畸变
激光雷达是一种使用激光检测物体距离的传感器;DVS,也称为神经形态相机,是一种记录亮度局部变化的相机,克服了 RGB 相机的一些局限性。摄像机 RGB 畸变只是一个模拟镜头效果的 RGB 相机,当然,你可以根据需要自定义畸变。
以下截图显示了激光雷达摄像机的视图:
图 7.7 – 激光雷达摄像机视图
以下截图显示了 DVS 的输出:
图 7.8 – DVS
你现在可以四处走动,从 RGB 相机收集一些图像,或者你可以使用 GitHub 仓库中的那些。
现在我们有一些图像了,是时候使用名为 SSD 的预训练网络来检测行人、车辆和交通灯了。
理解 SSD
在前面的章节中,我们创建了一个分类器,一个能够从预定义的选项集中识别图片中内容的神经网络。在本章的后面部分,我们将看到一个预训练的神经网络,它能够非常精确地对图像进行分类。
与许多神经网络相比,SSD 脱颖而出,因为它能够在同一张图片中检测多个对象。SSD 的细节有些复杂,如果你感兴趣,可以查看“进一步阅读”部分以获取一些灵感。
不仅 SSD 可以检测多个物体,它还可以输出物体存在的区域!在内部,这是通过检查不同宽高比下的 8,732 个位置来实现的。SSD 也足够快,在有良好 GPU 的情况下,它可以用来实时分析视频。
但我们可以在哪里找到 SSD?答案是 TensorFlow 检测模型动物园。让我们看看这是什么。
发现 TensorFlow 检测模型动物园
TensorFlow 检测模型动物园是一个有用的预训练神经网络集合,它支持在多个数据集上训练的多个架构。我们感兴趣的是 SSD,因此我们将专注于这一点。
在模型动物园支持的各个数据集中,我们感兴趣的是 COCO。COCO是微软的Common Objects in Context数据集,一个包含 2,500,000(250 万)张图片的集合,按类型分类。你可以在进一步阅读部分找到 COCO 的 90 个标签的链接,但我们感兴趣的是以下这些:
-
1:person -
3:car -
6:bus -
8:truck -
10:traffic light
你可能还对以下内容感兴趣:
-
2:bicycle -
4:motorcycle -
13:stop sign
值得注意的是,在 COCO 上训练的 SSD 有多个版本,使用不同的神经网络作为后端以达到所需的速度/精度比。请参考以下截图:
图 7.9 – 在 COCO 上训练的 SSD 的 TensorFlow 检测模型动物园
在这里,mAP列是平均精度均值,所以越高越好。MobileNet 是一个专为在移动设备和嵌入式设备上表现良好而开发的神经网络,由于其性能,它是在需要实时进行推理时 SSD 的经典选择。
为了检测道路上的物体,我们将使用以ResNet50作为骨干网络的 SSD,这是一个由微软亚洲研究院开发的具有 50 层的神经网络。ResNet 的一个特点是存在跳跃连接,这些捷径可以将一层连接到另一层,跳过中间的一些层。这有助于解决梯度消失问题。在深度神经网络中,训练过程中的梯度可能会变得非常小,以至于网络基本上停止学习。
但我们如何使用我们选定的模型ssd_resnet_50_fpn_coco?让我们来看看!
下载和加载 SSD
在模型动物园页面,如果你点击ssd_resnet_50_fpn_coco,你会得到一个 Keras 需要从中下载模型的 URL;在撰写本文时,URL 如下:
http://download.tensorflow.org/models/object_detection/ssd_resnet50_v1_fpn_shared_box_predictor_640x640_coco14_sync_2018_07_03.tar.gz
模型的全名如下:
ssd_resnet50_v1_fpn_shared_box_predictor_640x640_coco14_sync_2018_07_03.
要加载模型,你可以使用以下代码:
url = 'http://download.tensorflow.org/models/object_detection/'
+ model_name + '.tar.gz'
model_dir = tf.keras.utils.get_file(fname=model_name,
untar=True, origin=url)
print("Model path: ", model_dir)
model_dir = pathlib.Path(model_dir) / "saved_model"
model = tf.saved_model.load(str(model_dir))
model = model.signatures['serving_default']
如果你第一次运行这段代码,它将花费更多时间,因为 Keras 将下载模型并将其保存在你的硬盘上。
现在我们已经加载了模型,是时候用它来检测一些物体了。
运行 SSD
运行 SSD 只需要几行代码。你可以使用 OpenCV 加载图像(分辨率为 299x299),然后你需要将图像转换为张量,这是一种由 TensorFlow 使用的多维数组类型,类似于 NumPy 数组。参考以下代码:
img = cv2.imread(file_name)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
input_tensor = tf.convert_to_tensor(img)
input_tensor = input_tensor[tf.newaxis, ...]
# Run inference
output = model(input_tensor)
请注意,我们向网络输入的是RGB图像,而不是BGR。你可能还记得前几章中提到的 OpenCV 使用的是BGR格式的图片,因此我们需要注意通道的顺序。
如你所见,运行 SSD 相当简单,但输出相对复杂,需要一些代码将其转换为有用且更紧凑的形式。output变量是一个 Python 字典,但它包含的值是张量,因此你需要将它们转换。
例如,打印output['num_detections'],它包含预测的数量(例如,图像中找到的对象),将得到以下结果:
tf.Tensor([1.], shape=(1,), dtype=float32)
对于转换,我们可以使用int()。
所有其他张量都是数组,并且可以使用它们的numpy()函数进行转换。因此,你的代码可能看起来像这样:
num_detections = int(output.pop('num_detections'))
output = {key: value[0, :num_detections].numpy()
for key, value in output.items()}
output['num_detections'] = num_detections
仍然有以下两个问题需要修复:
-
检测类别是浮点数,而作为我们的标签,它们应该是整数。
-
框的坐标以百分比形式表示。
我们可以用几行代码修复这些问题:
output['detection_classes'] = output['detection_classes'].astype(np.int64)
output['boxes'] = [
{"y": int(box[0] * img.shape[0]),
"x": int(box[1] * img.shape[1]),
"y2": int(box[2] * img.shape[0]),
"x2": int(box[3] * img.shape[1])}
for box in output['detection_boxes']]
让我们将 SSD 应用于这张图像:
图 7.10 – 来自 Town01 的图像
我们得到以下输出:
{ 'detection_scores': array([0.4976843, 0.44799107, 0.36753723, 0.3548107 ], dtype=float32), 'detection_classes': array([ 8, 10, 6, 3], dtype=int64), 'detection_boxes': array([ [0.46678272, 0.2595877, 0.6488052, 0.40986294], [0.3679817, 0.76321596, 0.45684734, 0.7875406], [0.46517858, 0.26020002, 0.6488801, 0.41080648], [0.46678272, 0.2595877, 0.6488052, 0.40986294]], dtype=float32), 'num_detections': 4, 'boxes': [{'y': 220, 'x': 164, 'y2': 306, 'x2': 260}, {'y': 174, 'x': 484, 'y2': 216, 'x2': 500}, {'y': 220, 'x': 165, 'y2': 306, 'x2': 260}, {'y': 220, 'x': 164, 'y2': 306, 'x2': 260}]}
这就是代码的含义:
-
detection_scores:分数越高,预测的置信度越高。 -
detection_classes:预测的标签 – 在这种情况下,卡车(8)、交通灯(10)、公交车(6)和汽车(3)。 -
detection_boxes:原始框,坐标以百分比形式表示。 -
num_detections:预测的数量。 -
boxes:将坐标转换为原始图像分辨率的框。
请注意,三个预测基本上在同一区域,并且它们按分数排序。我们需要修复这种重叠。
为了更好地看到检测到的内容,我们现在将标注图像。
标注图像
为了正确标注图像,我们需要执行以下操作:
-
只考虑对我们有意义的标签。
-
移除标签的重叠。
-
在每个预测上画一个矩形。
-
写上标签及其分数。
要移除重叠的标签,只需比较它们,如果框的中心相似,我们只保留分数更高的标签。
这是结果:
图 7.11 – 来自 Town01 的图像,仅使用 SSD 进行标注
这是一个很好的起点,即使其他图像的识别效果不是很好。车辆被识别为卡车,这并不完全准确,但我们并不真的关心这一点。
主要问题是我们知道有交通灯,但我们不知道它的颜色。不幸的是,SSD 无法帮助我们;我们需要自己来做。
在下一节中,我们将开发一个能够通过称为迁移学习的技术检测交通灯颜色的神经网络。
检测交通灯的颜色
原则上,我们可以尝试使用一些计算机视觉技术来检测交通灯的颜色——例如,检查红色和绿色通道可能是一个起点。此外,验证交叉灯底部和上部的亮度也有助于。这可能会有效,即使一些交叉灯可能会有问题。
然而,我们将使用深度学习,因为这项任务非常适合探索更高级的技术。我们还将不遗余力地使用一个小数据集,尽管我们很容易创建一个大数据集;原因在于我们并不总是有轻松增加数据集大小的奢侈。
要能够检测交通灯的颜色,我们需要完成三个步骤:
-
收集交叉灯的数据集。
-
训练一个神经网络来识别颜色。
-
使用带有 SSD 的网络来获取最终结果。
有一个你可以使用的交通灯数据集,即Bosch Small Traffic Lights数据集;然而,我们将使用 Carla 生成我们自己的数据集。让我们看看怎么做。
创建交通灯数据集
我们将使用 Carla 创建一个数据集。原则上,它可以大如我们所愿。越大越好,但大数据集会使训练变慢,当然,创建它也需要更多的时间。在我们的案例中,由于任务简单,我们将创建一个相对较小的、包含数百张图片的数据集。我们将在稍后探索迁移学习,这是一种在数据集不是特别大时可以使用的技巧。
小贴士
在 GitHub 上,你可以找到我为这个任务创建的数据集,但如果你有时间,自己收集数据集可以是一项很好的练习。
创建这个数据集是一个三步任务:
-
收集街道图片。
-
找到并裁剪所有的交通灯。
-
对交通灯进行分类。
收集图片的第一项任务非常简单。只需使用manual_control.py启动 Carla 并按R键。Carla 将开始录制,并在你再次按R键后停止。
假设我们想要记录四种类型的图片:
-
红灯
-
黄灯
-
绿灯
-
交通灯背面(负样本)
我们想要收集交通灯背面的原因是因为 SSD 将其识别为交通灯,但我们没有用,所以我们不希望使用它。这些都是负样本,它们也可能包括道路或建筑或任何 SSD 错误分类为交通灯的东西。
因此,在你记录图片时,请尽量为每个类别收集足够的样本。
这些是一些你可能想要收集的图片示例:
图 7.12 – Town01 – 左:红灯,右:绿灯
第二步是应用 SSD 并提取交叉灯的图像。这很简单;参考以下代码:
obj_class = out["detection_classes"][idx]if obj_class == object_detection.LABEL_TRAFFIC_LIGHT: box = out["boxes"][idx] traffic_light = img[box["y"]:box["y2"], box["x"]:box["x2"]]
在前面的代码中,假设 out 变量包含运行 SSD 的结果,调用 model(input_tensor),并且 idx 包含当前预测中的当前检测,你只需要选择包含交通灯的检测,并使用我们之前计算的坐标进行裁剪。
我最终得到了 291 个检测,图像如下:
图 7.13 – Town01,从左至右:小红灯,小绿灯,绿灯,黄灯,交通灯背面,被错误分类为交通灯的建筑的一部分
如你所见,图像具有不同的分辨率和比例,这是完全可以接受的。
也有一些图像与交通灯完全无关,例如一块建筑的一部分,这些是很好的负样本,因为 SSD 将它们错误分类,因此这也是提高 SSD 输出的方法之一。
最后一步只是对图像进行分类。有了几百张这种类型的图片,只需要几分钟。你可以为每个标签创建一个目录,并将适当的图片移动到那里。
恭喜你,现在你有一个用于检测交通灯颜色的自定义数据集了。
如你所知,数据集很小,因此,正如我们之前所说的,我们将使用转移学习。下一节将解释它是什么。
理解转移学习
转移学习是一个非常恰当的名字。从概念上看,的确,它涉及到将神经网络在一个任务上学习到的知识转移到另一个不同但相关的任务上。
转移学习有几种方法;我们将讨论两种,并选择其中一种来检测交通灯的颜色。在两种情况下,起点都是一个在类似任务上预训练过的神经网络 – 例如,图像分类。我们将在下一节“了解 ImageNet”中更多地讨论这一点。我们专注于用作分类器的 卷积神经网络 (CNN),因为这是我们识别交通灯颜色的需要。
第一种方法是加载预训练的神经网络,将输出的数量调整到新问题(替换一些或所有密集层,有时只是添加一个额外的密集层),并在新的数据集上基本保持训练。你可能需要使用较小的学习率。如果新数据集中的样本数量小于用于原始训练的数据集,但仍然相当大,这种方法可能有效。例如,我们自定义数据集的大小可能是原始数据集位置的 10%。一个缺点是训练可能需要很长时间,因为你通常在训练一个相对较大的网络。
第二种方法,我们将要采用的方法,与第一种方法类似,但你将冻结所有卷积层,这意味着它们的参数是固定的,在训练过程中不会改变。这有一个优点,即训练速度更快,因为你不需要训练卷积层。这里的想法是,卷积层已经在大型数据集上进行了训练,并且能够检测到许多特征,对于新任务来说也将是可行的,而真正的分类器,由密集层组成,可以被替换并从头开始训练。
中间方法也是可能的,其中你训练一些卷积层,但通常至少保持第一层冻结。
在了解如何使用 Keras 进行迁移学习之前,让我们再思考一下我们刚才讨论的内容。一个关键假设是我们想要从中学习的这个假设网络已经在大型数据集上进行了训练,其中网络可以学习识别许多特征和模式。结果发现,有一个非常大的数据集符合要求——ImageNet。让我们再谈谈它。
了解 ImageNet
ImageNet 是一个巨大的数据集,在撰写本文时,它由 14,197,122(超过 1400 万)张图片组成!实际上,它并不提供图片,只是提供下载图片的 URL。这些图片被分类在 27 个类别和总共 21,841 个子类别中!这些子类别,称为 synsets,基于称为WordNet的分类层次结构。
ImageNet 已经是一个非常具有影响力的数据集,这也要归功于用于衡量计算机视觉进步的竞赛:ImageNet 大规模视觉识别挑战(ILSVRC)。
这些是主要类别:
-
两栖动物 -
动物 -
家用电器 -
鸟类 -
覆盖物 -
设备 -
织物 -
鱼类 -
花卉 -
食物 -
水果 -
真菌 -
家具 -
地质构造 -
无脊椎动物 -
哺乳动物 -
乐器 -
植物 -
爬行动物 -
运动 -
结构 -
工具 -
树木 -
器具 -
蔬菜 -
车辆 -
人物
子类别的数量非常高;例如,树木类别有 993 个子类别,覆盖了超过五十万张图片!
当然,在这个数据集上表现良好的神经网络将非常擅长识别多种类型图像上的模式,并且它可能也有相当大的容量。所以,是的,它将过度拟合你的数据集,但正如我们所知如何处理过拟合,我们将密切关注这个问题,但不会过于担心。
由于大量研究致力于在 ImageNet 上表现良好,因此许多最有影响力的神经网络都在其上进行了训练,这并不令人惊讶。
在 2012 年首次出现时,其中一个特别引人注目的是 AlexNet。让我们看看原因。
发现 AlexNet
当 AlexNet 在 2012 年发布时,它的准确率比当时最好的神经网络高出 10%以上!显然,这些解决方案已经得到了广泛的研究,其中一些现在非常常见。
AlexNet 引入了几个开创性的创新:
-
多 GPU 训练,其中 AlexNet 在一半的 GPU 上训练,另一半在另一个 GPU 上,使得模型的大小翻倍。
-
使用 ReLU 激活而不是 Tanh,这显然使得训练速度提高了六倍。
-
添加了重叠池化,其中 AlexNet 使用了 3x3 的最大池化,但池化区域仅移动 2x2,这意味着池化区域之间存在重叠。根据原始论文,这提高了 0.3–0.4%的准确率。在 Keras 中,你可以使用
MaxPooling2D(pool_size=(3,3), strides=(2,2))实现类似的重叠池化。
AlexNet 拥有超过 6000 万个参数,相当大,因此为了减少过拟合,它广泛使用了数据增强和 dropout。
虽然 2012 年的 AlexNet 在当时的标准下是当时最先进和开创性的,但按照今天的标准,它相当低效。在下一节中,我们将讨论一个神经网络,它只需 AlexNet 十分之一的参数就能实现显著更高的准确率:Inception。
Inception 背后的理念
拥有一个像 ImageNet 这样的大型数据集是非常好的,但有一个已经用这个数据集训练好的神经网络会更容易。结果发现,Keras 提供了几个这样的神经网络。一个是 ResNet,我们已经遇到了。另一个非常有影响力且具有重大创新的是 Inception。让我们简单谈谈它。
Inception 是一系列神经网络,意味着有多个,它们对初始概念进行了细化。Inception 是由谷歌设计的,在 2014 年 ILSVRC(ImageNet)竞赛中参赛并获胜的版本被称为GoogLeNet,以纪念 LeNet 架构。
如果你想知道 Inception 的名字是否来自著名的电影Inception,是的,它确实如此,因为他们想要更深入!Inception 是一个深度网络,有一个名为InceptionResNetV2的版本达到了惊人的 572 层!当然,这是如果我们计算每个层,包括激活层的话。我们将使用 Inception v3,它只有159 层。
我们将重点关注 Inception v1,因为它更容易,我们还将简要讨论后来添加的一些改进,因为它们可以成为你的灵感来源。
谷歌的一个关键观察是,由于一个主题在图片中可能出现的各种位置,很难提前知道卷积层的最佳核大小,所以他们并行添加了 1x1、3x3 和 5x5 卷积,以覆盖主要情况,加上最大池化,因为它通常很有用,并将结果连接起来。这样做的一个优点是网络不会太深,这使得训练更容易。
我们刚才描述的是天真的 Inception 块:
图 7.14 – 天真的 Inception 块
你可能已经注意到了一个 1x1 的卷积。那是什么?只是将一个通道乘以一个数字?并不完全是这样。1x1 卷积执行起来非常便宜,因为它只有 1 次乘法,而不是 3x3 卷积中的 9 次或 5x5 卷积中的 25 次,并且它可以用来改变滤波器的数量。此外,你可以添加一个 ReLU,引入一个非线性操作,这会增加网络可以学习的函数的复杂性。
这个模块被称为天真,因为它计算成本太高。随着通道数的增加,3x3 和 5x5 卷积变得缓慢。解决方案是在它们前面放置 1x1 卷积,以减少更昂贵的卷积需要操作的通道数:
图 7.15 – 包含维度减少的 Inception 块
理解这个块的关键是要记住,1x1 卷积用于减少通道数并显著提高性能。
例如,GoogLeNet 中的第一个 Inception 块有 192 个通道,5x5 的卷积会创建 32 个通道,所以乘法次数将与 25 x 32 x 192 = 153,600 成比例。
他们添加了一个输出为 16 个滤波器的 1x1 卷积,所以乘法次数将与 16 x 192 + 25 x 32 x 16 = 3,072 + 12,800 = 15,872 成比例。几乎减少了 10 倍。不错!
还有一件事。为了使连接操作生效,所有的卷积都需要有相同大小的输出,这意味着它们需要填充,以保持输入图像相同的分辨率。那么最大池化呢?它也需要有与卷积相同大小的输出,所以即使它在 3x3 的网格中找到最大值,也无法减小尺寸。
在 Keras 中,这可能是这样的:
MaxPooling2D(pool_size=(3, 3), padding='same', strides=(1, 1))
strides 参数表示在计算最大值后移动多少像素。默认情况下,它设置为与 pool_size 相同的值,在我们的例子中这将使大小减少 3 倍。将其设置为 (1, 1) 并使用相同的填充将不会改变大小。Conv2D 层也有一个 strides 参数,可以用来减小输出的大小;然而,通常使用最大池化层这样做更有效。
Inception v2 引入了一些优化,其中以下是一些:
-
5x5 卷积类似于两个堆叠的 3x3 卷积,但速度较慢,因此他们用 3x3 卷积重构了它。
-
3x3 卷积相当于一个 1x3 卷积后跟一个 3x1 卷积,但使用两个卷积要快 33%。
Inception v3 引入了以下优化:
-
使用几个较小的、较快的卷积创建的分解 7x7 卷积
-
一些批量归一化层
Inception-ResNet 引入了 ResNet 典型的残差连接,以跳过一些层。
既然你对 Inception 背后的概念有了更好的理解,让我们看看如何在 Keras 中使用它。
使用 Inception 进行图像分类
在 Keras 中加载 Inception 简单得不能再简单了,正如我们在这里可以看到的:
model = InceptionV3(weights='imagenet', input_shape=(299,299,3))
由于 Inception 可以告诉我们图像的内容,让我们尝试使用我们在本书开头使用的测试图像:
img = cv2.resize(preprocess_input(cv2.imread("test.jpg")),
(299, 299))
out_inception = model.predict(np.array([img]))
out_inception = imagenet_utils.decode_predictions(out_inception)
print(out_inception[0][0][1], out_inception[0][0][2], "%")
这是结果:
sea_lion 0.99184495 %
的确是正确的:我们的图像描绘了来自加拉帕戈斯群岛的海狮:
图 7.16 – Inception 以 0.99184495 的置信度识别为海狮
但我们想使用 Inception 进行迁移学习,而不是图像分类,因此我们需要以不同的方式使用它。让我们看看如何。
使用 Inception 进行迁移学习
迁移学习的加载略有不同,因为我们需要移除 Inception 上方的分类器,如下所示:
base_model = InceptionV3(include_top=False, input_shape= (299,299,3))
使用 input_shape,我们使用 Inception 的原始大小,但你可以使用不同的形状,只要它有 3 个通道,并且分辨率至少为 75x75。
重要的参数是 include_top,将其设置为 False 将会移除 Inception 的顶部部分——具有密集滤波器的分类器——这意味着网络将准备好进行迁移学习。
我们现在将创建一个基于 Inception 但可以由我们修改的神经网络:
top_model = Sequential()top_model.add(base_model) # Join the networks
现在,我们可以在其上方添加一个分类器,如下所示:
top_model.add(GlobalAveragePooling2D())top_model.add(Dense(1024, activation='relu'))top_model.add(Dropout(0.5))top_model.add(Dense(512, activation='relu'))top_model.add(Dropout(0.5))top_model.add(Dense(n_classes, activation='softmax'))
我们添加了一些 dropout,因为我们预计 Inception 在我们的数据集上会过度拟合很多。但请注意 GlobalAveragePooling2D。它所做的就是计算通道的平均值。
我们可以使用Flatten,但由于 Inception 输出 2,048 个 8x8 的卷积通道,而我们使用了一个有 1,024 个神经元的密集层,参数数量会非常大——134,217,728!使用GlobalAveragePooling2D,我们只需要 2,097,152 个参数。即使计算 Inception 的参数,节省也是相当显著的——从 156,548,388 个参数减少到 24,427,812 个参数。
我们还需要做一件事:冻结我们不想训练的 Inception 层。在这种情况下,我们想冻结所有层,但这可能并不总是这样。这是冻结它们的方法:
for layer in base_model.layers:
layer.trainable = False
让我们检查一下我们的网络看起来如何。Inception 太大,所以我只会显示参数数据:
Total params: 21,802,784
Trainable params: 21,768,352
Non-trainable params: 34,432
请注意,summary()实际上会打印出两个摘要:一个用于 Inception,一个用于我们的网络;这是第一个摘要的输出:
Model: "sequential_1"
____________________________________________________________
Layer (type) Output Shape Param #
============================================================
inception_v3 (Model) (None, 8, 8, 2048) 21802784
____________________________________________________________
global_average_pooling2d_1 ( (None, 2048) 0
____________________________________________________________
dense_1 (Dense) (None, 1024) 2098176
____________________________________________________________
dropout_1 (Dropout) (None, 1024) 0
____________________________________________________________
dense_2 (Dense) (None, 512) 524800
____________________________________________________________
dropout_2 (Dropout) (None, 512) 0
____________________________________________________________
dense_3 (Dense) (None, 4) 2052
============================================================
Total params: 24,427,812
Trainable params: 2,625,028
Non-trainable params: 21,802,784
____________________________________________________________
如您所见,第一层是 Inception。在第二个摘要中,您也确认了 Inception 的层被冻结了,因为我们有超过 2100 万个不可训练的参数,正好与 Inception 的总参数数相匹配。
为了减少过拟合并补偿小数据集,我们将使用数据增强:
datagen = ImageDataGenerator(rotation_range=5, width_shift_
range=[-5, -2, -1, 0, 1, 2, 5], horizontal_flip=True,
height_shift_range=[-30, -20, -10, -5, -2, 0, 2, 5, 10, 20,
30])
我只进行了一小点旋转,因为交通灯通常很直,我也只添加了很小的宽度偏移,因为交通灯是由一个神经网络(SSD)检测的,所以切割往往非常一致。我还添加了更高的高度偏移,因为我看到 SSD 有时会错误地切割交通灯,移除其三分之一。
现在,网络已经准备好了,我们只需要给它喂入我们的数据集。
将我们的数据集输入到 Inception 中
假设您已经将数据集加载到两个变量中:images和labels。
Inception 需要一些预处理,将图像的值映射到[-1, +1]范围。Keras 有一个函数可以处理这个问题,preprocess_input()。请注意,要从keras.applications.inception_v3模块导入它,因为其他模块中也有相同名称但不同行为的函数:
from keras.applications.inception_v3 import preprocess_input
images = [preprocess_input(img) for img in images]
我们需要将数据集分为训练集和验证集,这很简单,但我们还需要随机化顺序,以确保分割有意义;例如,我的代码加载了所有具有相同标签的图像,所以如果没有随机化,验证集中只会有一两个标签,其中一个甚至可能不在训练集中。
NumPy 有一个非常方便的函数来生成新的索引位置,permutation():
indexes = np.random.permutation(len(images))
然后,您可以使用 Python 的一个特性——for comprehension,来改变列表中的顺序:
images = [images[idx] for idx in indexes]
labels = [labels[idx] for idx in indexes]
如果您的标签是数字,您可以使用to_categorical()将它们转换为 one-hot 编码。
现在,只是切片的问题。我们将使用 20%的样本进行验证,所以代码可以像这样:
idx_split = int(len(labels_np) * 0.8)
x_train = images[0:idx_split]
x_valid = images[idx_split:]
y_train = labels[0:idx_split]
y_valid = labels[idx_split:]
现在,您可以像往常一样训练网络。让我们看看它的表现如何!
迁移学习的性能
模型的性能非常好:
Min Loss: 0.028652783162121116
Min Validation Loss: 0.011525456588399612
Max Accuracy: 1.0
Max Validation Accuracy: 1.0
是的,100%的准确率和验证准确率!不错,不错。实际上,这非常令人满意。然而,数据集非常简单,所以预期一个非常好的结果是公平的。
这是损失图的图表:
图 7.17 – 使用 Inception 迁移学习的损失
问题在于,尽管结果很好,但网络在我的测试图像上表现并不太好。可能是因为图像通常比 Inception 的原生分辨率小,网络可能会遇到由插值产生的模式,而不是图像中的真实模式,并且可能被它们所困惑,但这只是我的理论。
为了获得好的结果,我们需要更加努力。
改进迁移学习
我们可以假设网络正在过拟合,标准的应对措施是增加数据集。在这种情况下,这样做很容易,但让我们假设我们无法这样做,因此我们可以探索其他在这种情况下可能对你有用的选项。
我们可以做一些非常简单的事情来减少过拟合:
-
增加数据增强的多样性。
-
增加 dropout。
尽管 Inception 显然能够处理比这更复杂的任务,但它并不是为此特定任务优化的,而且它也可能从更大的分类器中受益,所以我将添加一个层:
-
这是经过几次测试后的新数据增强:
datagen = ImageDataGenerator(rotation_range=5, width_ shift_range= [-10, -5, -2, 0, 2, 5, 10], zoom_range=[0.7, 1.5], height_shift_range=[-10, -5, -2, 0, 2, 5, 10], horizontal_flip=True) -
这是新模型,具有更多的 dropout 和额外的层:
top_model.add(GlobalAveragePooling2D())top_model.add(Dropout(0.5))top_model.add(Dense(1024, activation='relu'))top_model.add(BatchNormalization())top_model.add(Dropout(0.5))top_model.add(Dense(512, activation='relu'))top_model.add(Dropout(0.5))top_model.add(Dense(128, activation='relu'))top_model.add(Dense(n_classes, activation='softmax')) -
我在全局平均池化之后添加了一个 dropout,以减少过拟合,我还添加了一个批量归一化层,这也有助于减少过拟合。
-
然后,我添加了一个密集层,但我没有在上面添加 dropout,因为我注意到网络在大量 dropout 的情况下训练存在问题。
即使我们不希望增加数据集,我们仍然可以对此采取一些措施。让我们看看类别的分布:
print('Labels:', collections.Counter(labels))这是结果:
Labels: Counter({0: 123, 2: 79, 1: 66, 3: 23})
如你所见,数据集中绿色比黄色或红色多得多,而且负面样本不多。
通常来说,标签不平衡并不是一个好的现象,因为实际上网络预测的绿灯比实际存在的绿灯要多,这在统计上意味着预测绿灯比预测其他标签更有利可图。为了改善这种情况,我们可以指导 Keras 以这种方式自定义损失函数,即预测错误的红灯比预测错误绿灯更糟糕,这样会产生类似于使数据集平衡的效果。
你可以用这两行代码做到这一点:
n = len(labels)
class_weight = {0: n/cnt[0], 1: n/cnt[1], 2: n/cnt[2], 3: n/cnt[3]}
以下结果是:
Class weight: {0: 2.365, 1: 4.409, 2: 3.683, 3: 12.652}
如你所见,对于绿色(标签 0)的损失惩罚比其他标签要小。
这是网络的表现方式:
Min Loss: 0.10114006596268155
Min Validation Loss: 0.012583946840742887
Max Accuracy: 0.99568963
Max Validation Accuracy: 1.0
与之前没有太大不同,但这次,网络表现更好,并准确识别了我测试图像中的所有交通灯。这应该是一个提醒,不要完全相信验证准确率,除非你确信你的验证数据集非常优秀。
这是损失图的图表:
图 7.18 – 使用 Inception 进行迁移学习后的损失,改进
现在我们有一个好的网络,是时候完成我们的任务了,使用新的网络结合 SSD,如下一节所述。
识别交通灯及其颜色
我们几乎完成了。从使用 SSD 的代码中,我们只需要以不同的方式管理交通灯。因此,当标签是10(交通灯)时,我们需要做以下操作:
-
裁剪带有交通灯的区域。
-
调整大小为 299x299。
-
预处理它。
-
通过我们的网络运行它。
然后,我们将得到预测结果:
img_traffic_light = img[box["y"]:box["y2"], box["x"]:box["x2"]]img_inception = cv2.resize(img_traffic_light, (299, 299))img_inception = np.array([preprocess_input(img_inception)])prediction = model_traffic_lights.predict(img_inception)label = np.argmax(prediction)
如果你运行 GitHub 上本章的代码,标签0代表绿灯,1代表黄灯,2代表红灯,而3表示不是交通灯。
整个过程首先涉及使用 SSD 检测物体,然后使用我们的网络检测图像中是否存在交通灯的颜色,如以下图解所示:
图 7.19 – 展示如何结合使用 SSD 和我们的网络的图解
这些是在运行 SSD 后运行我们的网络获得的示例:
图 7.20 – 带有交通灯的一些检测
现在交通灯的颜色已经正确检测。有一些误检:例如,在前面的图中,右边的图像标记了一个有树的地方。不幸的是,这种情况可能发生。在视频中,我们可以在接受之前要求检测几帧,始终考虑到在真正的自动驾驶汽车中,你不能引入高延迟,因为汽车需要快速对街道上发生的事情做出反应。
摘要
在本章中,我们专注于预训练的神经网络,以及我们如何利用它们来实现我们的目的。我们将两个神经网络结合起来检测行人、车辆和交通灯,包括它们的颜色。我们首先讨论了如何使用 Carla 收集图像,然后发现了 SSD,这是一个强大的神经网络,因其能够检测物体及其在图像中的位置而突出。我们还看到了 TensorFlow 检测模型库以及如何使用 Keras 下载在名为 COCO 的数据集上训练的 SSD 的所需版本。
在本章的第二部分,我们讨论了一种称为迁移学习的强大技术,并研究了 Inception 神经网络的一些解决方案,我们使用迁移学习在我们的数据集上训练它,以便能够检测交通灯的颜色。在这个过程中,我们还讨论了 ImageNet,并看到了达到 100%验证准确率是如何具有误导性的,因此,我们必须减少过拟合以提高网络的真正精度。最终,我们成功地使用两个网络一起工作——一个用于检测行人、车辆和交通灯,另一个用于检测交通灯的颜色。
既然我们已经知道了如何构建关于道路的知识,那么现在是时候前进到下一个任务——驾驶!在下一章中,我们将真正地坐在驾驶座上(Carla 的驾驶座),并使用一种称为行为克隆的技术来教我们的神经网络如何驾驶,我们的神经网络将尝试模仿我们的行为。
问题
你现在应该能够回答以下问题:
-
什么是 SSD?
-
什么是 Inception?
-
冻结一层意味着什么?
-
SSD 能否检测交通灯的颜色?
-
什么是迁移学习?
-
你能列举一些减少过拟合的技术吗?
-
你能描述 Inception 块背后的想法吗?
进一步阅读
-
TensorFlow 模型库:
github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md -
COCO 标签:
github.com/tensorflow/models/blob/master/research/object_detection/data/mscoco_label_map.pbtxt -
博世小型交通灯数据集:
hci.iwr.uni-heidelberg.de/content/bosch-small-traffic-lights-dataset -
ImageNet:
www.image-net.org/ -
Inception 论文:
static.googleusercontent.com/media/research.google.com/en//pubs/archive/43022.pdf -
AlexNet 论文:
papers.nips.cc/paper/4824-imagenet-classification-with-deep-convolutional-neural-networks.pdf
第八章:第八章: 行为克隆
在本章中,我们将训练一个神经网络来控制汽车的转向盘,有效地教会它如何驾驶汽车!希望你会对这项任务的核心如此简单感到惊讶,这要归功于深度学习。
为了实现我们的目标,我们不得不修改 CARLA 模拟器的一个示例,首先保存创建数据集所需的图像,然后使用我们的神经网络来驾驶。我们的神经网络将受到英伟达 DAVE-2 架构的启发,我们还将看到如何更好地可视化神经网络关注的区域。
在本章中,我们将涵盖以下主题:
-
使用行为克隆教神经网络如何驾驶
-
英伟达 DAVE-2 神经网络
-
从 Carla 记录图像和转向盘
-
记录三个视频流
-
创建神经网络
-
训练用于回归的神经网络
-
可视化显著性图
-
与 Carla 集成以实现自动驾驶
-
使用生成器训练更大的数据集
技术要求
为了能够使用本章中解释的代码,您需要安装以下工具和模块:
-
Carla 模拟器
-
Python 3.7
-
NumPy 模块
-
TensorFlow 模块
-
Keras 模块
-
keras-vis模块 -
OpenCV-Python 模块
-
一个 GPU(推荐)
本章的代码可以在以下链接找到:github.com/PacktPublishing/Hands-On-Vision-and-Behavior-for-Self-Driving-Cars/tree/master/Chapter8。
本章的《代码实战》视频可以在以下链接找到:
使用行为克隆教神经网络如何驾驶
自动驾驶汽车是一个复杂的硬件和软件集合。普通汽车的硬件已经很复杂了,通常有成千上万的机械部件,而自动驾驶汽车又增加了许多传感器。软件并不简单,事实上,据说早在 15 年前,一家世界级的汽车制造商不得不退步,因为软件的复杂性已经失控。为了给您一个概念,一辆跑车可以有超过 50 个 CPU!
显然,制造一个既安全又相对快速的自动驾驶汽车是一个难以置信的挑战,但尽管如此,我们将看到一行代码可以有多强大。对我来说,意识到如此复杂的事情如驾驶可以用如此简单的方式编码,是一个启发性的时刻。但我并不应该感到惊讶,因为,在深度学习中,数据比代码本身更重要,至少在某种程度上。
我们没有在真实自动驾驶汽车上测试的奢侈条件,所以我们将使用 Carla,并且我们将训练一个神经网络,在输入摄像头视频后能够生成转向角度。尽管如此,我们并没有使用其他传感器,原则上,你可以使用你想象中的所有传感器,只需修改网络以接受这些额外的数据。
我们的目标是教会 Carla 如何进行一次“绕圈”,使用 Town04 轨道的一部分,这是 Carla 包含的轨道之一。我们希望我们的神经网络能够稍微直行,然后进行一些右转,直到到达初始点。原则上,为了训练神经网络,我们只需要驾驶 Carla,记录道路图像和我们应用的相应转向角度,这个过程被称为行为克隆。
我们的任务分为三个步骤:
-
构建数据集
-
设计和训练神经网络
-
在 Carla 中集成神经网络
我们将借鉴 Nvidia 创建的 DAVE-2 系统。那么,让我们开始描述它。
介绍 DAVE-2
DAVE-2 是 Nvidia 设计的一个系统,用于训练神经网络驾驶汽车,旨在作为一个概念验证,证明原则上一个单一的神经网络能够在一个道路上控制汽车。换句话说,如果提供足够的数据,我们的网络可以被训练来在真实的道路上驾驶真实的汽车。为了给你一个概念,Nvidia 使用了大约 72 小时的视频,每秒 10 帧。
这个想法非常简单:我们给神经网络提供视频流,神经网络将简单地生成转向角度,或者类似的东西。训练是由人类驾驶员创建的,系统从摄像头(训练数据)和驾驶员操作的转向盘(训练标签)收集数据。这被称为行为克隆,因为网络试图复制人类驾驶员的行为。
不幸的是,这会过于简单,因为大部分标签将简单地是 0(驾驶员直行),因此网络将难以学习如何移动到车道中间。为了缓解这个问题,Nvidia 使用了三个摄像头:
-
车辆中央的一个,这是真实的人类行为
-
左侧的一个,模拟如果汽车过于靠左时应该怎么做
-
右侧的一个,模拟如果汽车过于靠右时应该怎么做
为了使左右摄像头有用,当然有必要更改与它们视频相关的转向角度,以模拟一个校正;因此,左摄像头需要与一个更向右的转向相关联,而右摄像头需要与一个更向左的转向相关联。
下面的图示显示了系统:
图 8.1 – Nvidia DAVE-2 系统
为了使系统更健壮,Nvidia 添加了随机的平移和旋转,并调整转向以适应,但我们将不会这样做。然而,我们将使用他们建议的三个视频流。
我们如何获取三个视频流和转向角度?当然是从 Carla 获取的,我们将在本章中大量使用它。在开始编写代码之前,让我们熟悉一下manual_control.py文件,这是一个我们将复制并修改的文件。
了解manual_control.py
我们不会编写一个完整的客户端代码来完成我们需要的操作,而是会修改manual_control.py文件,从PythonAPI/examples。
我通常会说明需要修改的代码位置,但您实际上需要检查 GitHub 以查看它。
在开始之前,请考虑本章的代码可能比通常更严格地要求版本,特别是可视化部分,因为它使用了一个尚未更新的库。
我的建议是使用 Python 3.7,并安装 TensorFlow 版本 2.2、Keras 2.3 和scipy 1.2,如下所示:
pip install tensorflow==2.2.0 pip install keras==2.3.1 pip install scipy==1.2.3
如果您现在查看manual_control.py,您可能会注意到的第一件事是这个代码块:
try: sys.path.append(glob.glob('../carla/dist/carla-*%d.%d-%s.egg' % ( sys.version_info.major, sys.version_info.minor, 'win-amd64' if os.name == 'nt' else 'linux-x86_64'))[0])except IndexError: pass
它加载一个包含 Carla 代码的egg文件,该文件位于PythonAPI/carla/dist/文件夹中。作为替代方案,您也可以使用以下命令安装 Carla,当然需要使用您的egg文件名:
python -m easy_install carla-0.9.9-py3.7-win-amd64.egg
在此之后,您可能会注意到代码被组织成以下类:
-
World: 我们车辆移动的虚拟世界,包括地图和所有演员(车辆、行人和传感器)。 -
KeyboardControl: 这个类会响应用户按下的键,并且包含一些逻辑,将转向、制动和加速的二进制开/关键转换为更广泛的值范围,这取决于它们被按下的时间长短,从而使汽车更容易控制。 -
HUD: 这个类渲染与模拟相关的所有信息,如速度、转向和油门,并管理可以显示一些信息给用户的提示,持续几秒钟。 -
FadingText: 这个类被 HUD 类使用,用于显示几秒钟后消失的通知。 -
HelpText: 这个类使用pygame(Carla 使用的游戏库)显示一些文本。 -
CollisionSensor: 这是一个能够检测碰撞的传感器。 -
LaneInvasionSensor: 这是一个能够检测您是否跨越了车道线的传感器。 -
GnssSensor: 这是一个 GPS/GNSS 传感器,它提供了 OpenDRIVE 地图内的 GNSS 位置。 -
IMUSensor: 这是一个惯性测量单元,它使用陀螺仪来检测施加在汽车上的加速度。 -
RadarSensor: 一个雷达,提供检测到的元素(包括速度)的二维地图。 -
CameraManager: 这是一个管理相机并打印其信息的类。
此外,还有一些其他值得注意的方法:
-
main(): 这部分主要致力于解析操作系统接收到的参数。 -
game_loop():这个函数主要初始化 pygame、Carla 客户端以及所有相关对象,并且实现了游戏循环,其中每秒 60 次,分析按键并显示最新的图像在屏幕上。
帧的可视化是由game_loop()触发的,以下是一行:
world.render(display)
world.render()方法调用CameraManager.render(),显示最后一帧可用的图像。
如果你检查了代码,你可能已经注意到 Carla 使用弱引用来避免循环引用。弱引用是一种不会阻止对象被垃圾回收的引用,这在某些场景中很有用,例如缓存。
当你与 Carla 一起工作时,有一件重要的事情需要考虑。你的一些代码在服务器上运行,而另一些代码在客户端上运行,可能不容易在这两者之间划清界限。这可能会导致意想不到的后果,例如你的模型运行速度慢 10 到 30 倍,这可能是由于它被序列化到服务器上,尽管这只是我在看到这个问题后的推测。因此,我在game_loop()方法中运行我的推理,这肯定是在客户端上运行的。
这也意味着帧是在服务器上计算并发送到客户端的。
另一个不幸需要考虑的事情是,Carla 的 API 并不稳定,版本 0.9.0 删除了许多应该很快就会恢复的功能。
文档也没有特别更新这些缺失的 API,所以如果你发现事情没有按预期工作,请不要感到惊讶。希望这很快就会得到修复。同时,你可以使用旧版本。我们使用了 Carla 0.9.9.2,还有一些粗糙的边缘,但对于我们的需求来说已经足够好了。
现在我们对 CARLA 有了更多的了解,让我们看看我们如何录制我们的数据集,从只有一个视频流开始。
录制一个视频流
在原则上,使用 Carla 录制一个视频流非常简单,因为已经有一个选项可以这样做。如果你从PythonAPI/examples目录运行manual_control.py,当你按下R键时,它就开始录制。
问题是我们还想要转向角度。通常,你可以将此类数据保存到某种类型的数据库中,CSV 文件或 pickle 文件。为了使事情更简单并专注于核心任务,我们只需将转向角度和一些其他数据添加到文件名中。这使得你构建数据集变得更容易,因为你可能想要记录多个针对特定问题修复的运行,你只需将文件移动到新目录中,就可以轻松地保留所有信息,而无需在数据库中更新路径。
但如果你不喜欢,请随意使用更好的系统。
我们可以从头开始编写一个与 Carla 服务器集成的客户端,并完成我们所需的功能,但为了简单起见,并更好地隔离所需更改,我们只需将manual_control.py复制到一个名为manual_control_recording.py的文件中,然后我们只需添加所需的内容。
请记住,这个文件应该在 PythonAPI/examples 目录下运行。
我们首先想做的事情是将轨道改为 Town04,因为它比默认轨道更有趣:
client.load_world('Town04')
client.reload_world()
之前的代码需要放入 game_loop() 方法中。
变量 client 明显是连接到 Carla 服务器的客户端。
我们还需要将出生点(模拟开始的地方)改为固定,因为通常,它会每次都改变:
spawn_point = spawn_points[0] if spawn_points else carla.Transform()
现在,我们需要更改文件名。在此过程中,我们不仅会保存转向角度,还会保存油门和刹车。我们可能不会使用它们,但如果你想进行实验,它们将为你提供。以下方法应在 CameraManager 类中定义:
def set_last_controls(self, control):
self.last_steer = control.steer
self.last_throttle = control.throttle
self.last_brake = control.brake
现在,我们可以按以下方式保存文件:
image.save_to_disk('_out/%08d_%s_%f_%f_%f.jpg' % (image.frame, camera_name, self.last_steer, self.last_throttle, self.last_brake))
image.frame 变量包含当前帧的编号,而 camera_name 目前并不重要,但它的值将是 MAIN。
image 变量还包含我们想要保存的当前图像。
你应该得到类似以下的名字:
00078843_MAIN_0.000000_0.500000_0.000000.jpg
在上一个文件名中,你可以识别以下组件:
-
帧编号(
00078843) -
相机(
MAIN) -
转向角度(
0.000000) -
油门(
0.500000) -
刹车(
0.000000)
这是图像,以我的情况为例:
图 8.2 – Carla 的一帧,转向 0 度
这帧还可以,但并不完美。我应该待在另一条车道上,或者转向应该稍微指向右边。在行为克隆的情况下,汽车会从你那里学习,所以你的驾驶方式很重要。用键盘控制 Carla 并不好,而且在记录时,由于保存图像所花费的时间,效果更差。
真正的问题是,我们需要记录三个相机,而不仅仅是其中一个。让我们看看如何做到这一点。
记录三个视频流
要记录三个视频流,起点是拥有三个相机。
默认情况下,Carla 有以下五个相机:
-
一个经典的 第三人称 视角,从车后上方
-
从车前朝向道路(向前看)
-
从车前朝向汽车(向后看)
-
从高空
-
从左侧
在这里,你可以看到前三个相机:
图 8.3 – 从上方、朝向道路和朝向汽车的相机
第二个相机对我们来说非常有趣。
以下是从剩余的两个相机中获取的:
图 8.4 – 从高空和左侧的 Carla 相机
最后一个相机也有些有趣,尽管我们不想在我们的帧中记录汽车。由于某种原因,Carla 的作者没有将其添加到列表中,所以我们缺少右侧的相机。
幸运的是,更换相机或添加新相机相当简单。这是原始相机的定义,CameraManager构造函数:
bound_y = 0.5 + self._parent.bounding_box.extent.y
self._camera_transforms = [
(carla.Transform(carla.Location(x=-5.5, z=2.5), carla.Rotation(pitch=8.0)), Attachment.SpringArm),
(carla.Transform(carla.Location(x=1.6, z=1.7)), Attachment.Rigid),
(carla.Transform(carla.Location(x=5.5, y=1.5, z=1.5)), Attachment.SpringArm),
(carla.Transform(carla.Location(x=-8.0, z=6.0), carla.Rotation(pitch=6.0)), Attachment.SpringArm),
(carla.Transform(carla.Location(x=-1, y=-bound_y, z=0.5)), Attachment.Rigid)]
作为第一次尝试,我们可以只保留第二和第五个相机,但我们希望它们处于可比较的位置。Carla 是用一个非常著名的游戏引擎编写的:Unreal Engine 4。在Unreal Engine中,z轴是垂直轴(上下),x轴用于前后,y轴用于横向移动,左右。因此,我们希望相机具有相同的x和z坐标。我们还想有一个第三个相机,从右侧。为此,只需改变y坐标的符号即可。这是仅针对相机的结果代码:
(carla.Transform(carla.Location(x=1.6, z=1.7)), Attachment.Rigid),(carla.Transform(carla.Location(x=1.6, y=-bound_y, z=1.7)), Attachment.Rigid),(carla.Transform(carla.Location(x=1.6, y=bound_y, z=1.7)), Attachment.Rigid)
你可能可以在这里停止。我最终将侧向相机移动得更靠边,这可以通过更改bound_y来实现。
bound_y = 4
这些是我们现在得到的图像:
图 8.5 – 新相机:从左、从正面(主相机)和从右
现在,应该更容易理解,与主相机相比,左右相机可以用来教神经网络如何纠正轨迹,如果它不在正确的位置。当然,这假设主相机录制的流是预期的位置。
即使现在有了正确的相机,它们也没有在使用。我们需要在World.restart()中添加它们,如下所示:
self.camera_manager.add_camera(1)self.camera_manager.add_camera(2)
CameraManager.add_camera()方法定义如下:
camera_name = self.get_camera_name(camera_index)if not (camera_index in self.sensors_added_indexes): sensor = self._parent.get_world().spawn_actor( self.sensors[self.index][-1], self._camera_transforms[camera_index][0], attach_to=self._parent, attachment_type=self._camera_transforms[camera_index][1]) self.sensors_added_indexes.add(camera_index) self.sensors_added.append(sensor) # We need to pass the lambda a weak reference to self to avoid # circular reference. weak_self = weakref.ref(self) sensor.listen(lambda image: CameraManager._save_image(weak_self, image, camera_name))
这段代码的作用如下:
-
使用指定的相机设置传感器
-
将传感器添加到列表中
-
指示传感器调用一个 lambda 函数,该函数调用
save_image()方法
下面的get_camera_name()方法用于根据其索引为相机获取一个有意义的名称,该索引依赖于我们之前定义的相机:
def get_camera_name(self, index): return 'MAIN' if index == 0 else ('LEFT' if index == 1 else ('RIGHT' if index == 2 else 'UNK'))
在查看save_image()的代码之前,让我们讨论一个小问题。
每一帧录制三个相机有点慢,导致每秒帧数(FPS)低,这使得驾驶汽车变得困难。因此,你会过度纠正,录制一个次优的数据集,基本上是在教汽车如何蛇形行驶。为了限制这个问题,我们将为每一帧只录制一个相机视图,然后在下一帧旋转到下一个相机视图,我们将在录制过程中循环所有三个相机视图。毕竟,连续的帧是相似的,所以这不是一个大问题。
英伟达使用的相机以 30 FPS 的速度录制,但他们决定跳过大多数帧,只以 10 FPS 的速度录制,因为帧非常相似,这样会增加训练时间而不会增加太多信息。你不会以最高速度录制,但你的数据集会更好,如果你想有一个更大的数据集,你总是可以多开一些车。
save_image() 函数需要首先检查这是否是我们想要记录的帧:
if self.recording:
n = image.frame % 3
# Save only one camera out of 3, to increase fluidity
if (n == 0 and camera_name == 'MAIN') or (n == 1 and camera_name == 'LEFT') or (n == 2 and camera_name == 'RIGHT'):
# Code to convert, resize and save the image
第二步是将图像转换为适合 OpenCV 的格式,因为我们将要使用它来保存图像。我们需要将原始缓冲区转换为 NumPy,我们还需要删除一个通道,因为 Carla 产生的图像是 BGRA,有四个通道:蓝色、绿色、红色和透明度(不透明度):
img = np.frombuffer(image.raw_data, dtype=np.dtype('uint8'))
img = np.reshape(img, (image.height, image.width, 4))
img = img[:, :, :3]
现在,我们可以调整图像大小,裁剪我们需要的部分,并保存它:
img = cv2.resize(img, (200, 133))
img = img[67:, :, :]
cv2.imwrite('_out/%08d_%s_%f_%f_%f.jpg' % (image.frame, camera_name, self.last_steer, self.last_throttle, self.last_brake), img).
你可以在 GitHub 的代码仓库中看到,我记录了大量帧,足以驾驶一两个转弯,但如果你想沿着整个赛道驾驶,你需要更多的帧,而且你开得越好,效果越好。
现在,我们有了摄像头,我们需要使用它们来构建我们所需的数据集。
记录数据集
显然,为了构建数据集,你需要记录至少你期望你的网络做出的转弯。越多越好。但你也应该记录有助于你的汽车纠正轨迹的运动。左右摄像头已经帮助很多,但你应该也记录一些汽车靠近道路边缘,方向盘将其转向中心的段。
例如,考虑以下内容:
图 8.6 – 车辆靠近左侧,转向右侧
如果有转弯没有按照你想要的方式进行,你可以尝试记录它们多次,就像我这样做。
现在,你可能看到了将方向盘编码到图像名称中的优势。你可以将这些纠正段,或者你喜欢的任何内容,放在专用目录中,根据需要将它们添加到数据集中或从中移除。
如果你愿意,甚至可以手动选择图片的一部分,以纠正错误的转向角度,尽管如果只有有限数量的帧有错误的角度,这可能不是必要的。
尽管每帧只保存一个摄像头,但你可能仍然发现驾驶很困难,尤其是在速度方面。我个人更喜欢限制加速踏板,这样汽车不会开得太快,但当我想要减速时,我仍然可以减速。
加速踏板通常可以达到1的值,所以要限制它,只需要使用类似以下的一行代码,在KeyboardControl ._parse_vehicle_keys()方法中:
self._control.throttle = min(self._control.throttle + 0.01, 0.5)
为了增加流畅性,你可能需要以较低的分辨率运行客户端:
python manual_control_packt.py --res 480x320
你也可以降低服务器的分辨率,如下所示:
CarlaUE4 -ResX=480-ResY=320
现在你有了原始数据集,是时候创建真实数据集了,带有适当的转向角度。
预处理数据集
我们记录的数据集是原始的,这意味着在使用之前需要一些预处理。
最重要的是要纠正左右摄像头的转向角度。
为了方便,这是通过一个额外的程序完成的,这样你最终可以更改它,而无需再次记录帧。
首先,我们需要一种方法从名称中提取数据(我们假设文件是 JPG 或 PNG 格式):
def expand_name(file):
idx = int(max(file.rfind('/'), file.rfind('\\')))
prefix = file[0:idx]
file = file[idx:].replace('.png', '').replace('.jpg', '')
parts = file.split('_')
(seq, camera, steer, throttle, brake, img_type) = parts
return (prefix + seq, camera, to_float(steer), to_float(throttle), to_float(brake), img_type)
to_float方法只是一个方便的转换,将-0 转换为 0。
现在,改变转向角度很简单:
(seq, camera, steer, throttle, brake, img_type) = expand_name(file_name)
if camera == 'LEFT':
steer = steer + 0.25
if camera == 'RIGHT':
steer = steer - 0.25
我添加了 0.25 的校正。如果你的相机离车更近,你可能想使用更小的数字。
在此过程中,我们还可以添加镜像帧,以稍微增加数据集的大小。
现在我们已经转换了数据集,我们准备训练一个类似于 DAVE-2 的神经网络来学习如何驾驶。
神经网络建模
要创建我们的神经网络,我们将从 DAVE-2 中汲取灵感,这是一个出奇简单的神经网络:
-
我们从一个 lambda 层开始,将图像像素限制在(-1, +1)范围内:
model = Sequential() model.add(Lambda(lambda x: x/127.5 - 1., input_shape=(66, 200, 3))) -
然后,有三个大小为
5和步长(2,2)的卷积层,它们将输出分辨率减半,以及三个大小为3的卷积层:model.add(Conv2D(24, (5, 5), strides=(2, 2), activation='elu')) model.add(Conv2D(36, (5, 5), strides=(2, 2), activation='relu')) model.add(Conv2D(48, (5, 5), strides=(2, 2), activation='relu')) model.add(Conv2D(64, (3, 3), activation='relu')) model.add(Conv2D(64, (3, 3), activation='relu')) -
然后,我们有密集层:
model.add(Flatten()) model.add(Dense(1164, activation='relu')) model.add(Dense(100, activation='relu')) model.add(Dense(50, activation='relu')) model.add(Dense(10, activation='relu')) model.add(Dense(1, activation='tanh'))
当我想这些几行代码足以让汽车在真实道路上自动驾驶时,我总是感到惊讶!
虽然它看起来与之前我们看到的其他神经网络或多或少相似,但有一个非常重要的区别——最后的激活函数不是 softmax 函数,因为这不是一个分类器,而是一个需要执行回归任务的神经网络,根据图像预测正确的转向角度。
当神经网络试图在一个可能连续的区间内预测一个值时,我们称其为回归,例如在-1 和+1 之间。相比之下,在分类任务中,神经网络试图预测哪个标签更有可能是正确的,这很可能代表了图像的内容。因此,能够区分猫和狗的神经网络是一个分类器,而试图根据大小和位置预测公寓成本的神经网络则是在执行回归任务。
让我们看看我们需要更改什么才能使用神经网络进行回归。
训练回归神经网络
正如我们已经看到的,一个区别是缺少 softmax 层。取而代之的是,我们使用了 Tanh(双曲正切),这是一个用于生成(-1, +1)范围内值的激活函数,这正是我们需要用于转向角度的范围。然而,原则上,你甚至可以没有激活函数,直接使用最后一个神经元的值。
下图显示了 Tanh 函数:
图 8.7 – tanh 函数
如您所见,Tanh 将激活函数的范围限制在(-1, +1)范围内。
通常情况下,当我们训练一个分类器,例如 MNIST 或 CIFAR-10 的情况,我们使用categorical_crossentropy作为损失函数,accuracy作为指标。然而,对于回归问题,我们需要使用mse作为损失函数,并且我们可以选择性地使用cosine_proximity作为指标。
余弦相似度是向量的相似性指标。所以,1 表示它们是相同的,0 表示它们是垂直的,-1 表示它们是相反的。损失和度量代码片段如下:
model.compile(loss=mse, optimizer=Adam(), metrics= ['cosine_proximity'])
其余的代码与分类器相同,只是我们不需要使用 one-hot 编码。
让我们看看训练的图表:
图 8.8 – 使用 DAVE-2 的行为克隆,训练
你可以看到轻微的过拟合。这是损失值:
Min Loss: 0.0026791724107401277
Min Validation Loss: 0.0006011795485392213
Max Cosine Proximity: 0.72493887
Max Validation Cosine Proximity: 0.6687041521072388
在这种情况下,损失是训练中记录的转向角与网络计算的角度之间的均方误差。我们可以看到验证损失相当好。如果你有时间,你可以尝试对这个模型进行实验,添加 dropout 或甚至改变整个结构。
很快,我们将把我们的神经网络与 Carla 集成,看看它是如何驾驶的,但在那之前,质疑神经网络是否真的专注于道路的正确部分可能是合理的。下一节将展示我们如何使用称为显著性图的技术来做这件事。
可视化显著性图
要理解神经网络关注的是什么,我们应该使用一个实际例子,所以让我们选择一张图片:
图 8.9 – 测试图像
如果我们作为人类必须在这条路上驾驶,我们会注意车道和墙壁,尽管诚然,墙壁的重要性不如之前的最后一个车道。
我们已经知道如何了解一个CNN(卷积神经网络的简称)如 DAVE-2 在考虑什么:因为卷积层的输出是一个图像,我们可以这样可视化:
图 8.10 – 第一卷积层的部分激活
这是一个好的起点,但我们希望得到更多。我们希望了解哪些像素对预测贡献最大。为此,我们需要获取一个显著性图。
Keras 不支持它们,但我们可以使用keras-vis。你可以用pip安装它,如下所示:
sudo pip install keras-vis
获取显著性图的第一步是创建一个从我们模型的输入开始但以我们想要分析的层结束的模型。生成的代码与我们所看到的激活非常相似,但为了方便,我们还需要层的索引:
conv_layer, idx_layer = next((layer.output, idx) for idx, layer in enumerate(model.layers) if layer.output.name.startswith(name))
act_model = models.Model(inputs=model.input, outputs=[conv_layer])
虽然在我们的情况下不是必需的,但你可能想将激活变为线性,然后重新加载模型:
conv_layer.activation = activations.linear
sal_model = utils.apply_modifications(act_model)
现在,只需要调用visualize_saliency():
grads = visualize_saliency(sal_model, idx_layer, filter_indices=None, seed_input=img)
plt.imshow(grads, alpha=.6)
我们对最后一层,即输出的显著性图感兴趣,但作为一个练习,我们将遍历所有卷积层,看看它们理解了什么。
让我们看看第一卷积层的显著性图:
图 8.11 – 第一卷积层的显著性图
并不是很令人印象深刻,因为没有显著性,我们只能看到原始图像。
让我们看看第二层的地图:
图 8.12 – 第二卷积层的显著性图
这是一种改进,但即使我们在中间线、墙上和右车道后的土地上看到了一些注意力,也不是很清晰。让我们看看第三层:
图 8.13 – 第三卷积层的显著性图
现在才是重点!我们可以看到对中央线和左线的极大关注,以及一些注意力集中在墙上和右线上。网络似乎在试图理解道路的尽头在哪里。让我们也看看第四层:
图 8.14 – 第四卷积层的显著性图
在这里,我们可以看到注意力主要集中在中央线上,但左线和墙上也有注意力的火花,以及在整个道路上的少许注意力。
我们还可以检查第五个也是最后一个卷积层:
图 8.15 – 第五卷积层的显著性图
第五层与第四层相似,并且对左线和墙上的注意力有所增加。
我们还可以可视化密集层的显著性图。让我们看看最后一层的成果,这是我们认为是该图像真实显著性图的地方:
图 8.16 – 输出层的显著性图
最后一个显著性图,最重要的一个,对中央线和右线给予了极大的关注,加上对右上角的少许关注,这可能是一次尝试估计右车道的距离。我们还可以看到一些注意力在墙上和左车道上。所以,总的来说,这似乎很有希望。
让我们用另一张图片试试:
图 8.17 – 第二测试图像
这是一张很有趣的图片,因为它是从网络尚未训练的道路部分拍摄的,但它仍然表现得很出色。
让我们看看第三卷积层的显著性图:
图 8.18 – 第三卷积层的显著性图
神经网络似乎非常关注道路的尽头,并且似乎还检测到了几棵树。如果它被训练用于制动,我敢打赌它会这么做!
让我们看看最终的地图:
图 8.19 – 输出层的显著性图
这与之前的一个非常相似,但更加关注中央线和右侧线,以及一般道路上的一小部分。对我来说看起来不错。
让我们尝试使用最后一张图像,这张图像是从训练中取出的,用于教授何时向右转:
图 8.20 – 第三测试图像
这是它的最终显著性图:
图 8.21 – 输出层的显著性图
你可以看到神经网络主要关注右侧线,同时关注整个道路,并对左侧线投入了一些注意力的火花。
如你所见,显著性图可以是一个有效的工具,帮助我们更好地理解网络的行为,并对其对世界的解释进行一种合理性检查。
现在是时候与 Carla 集成,看看我们在现实世界中的表现如何了。系好安全带,因为我们将要驾驶,我们的神经网络将坐在驾驶座上!
将神经网络与 Carla 集成
我们现在将我们的神经网络与 Carla 集成,以实现自动驾驶。
如前所述,我们首先复制manual_control.py文件,可以将其命名为manual_control_drive.py。为了简化,我将只编写你需要更改或添加的代码,但你可以在 GitHub 上找到完整的源代码。
请记住,这个文件应该在PythonAPI/examples目录下运行。
从原则上讲,让我们的神经网络控制方向盘相当简单,因为我们只需要分析当前帧并设置转向。然而,我们还需要施加一些油门,否则汽车不会移动!
同样重要的是,你需要在游戏循环中运行推理阶段,或者你确实确信它在客户端上运行,否则性能将大幅下降,你的网络将难以驾驶,因为接收帧和发送驾驶指令之间的延迟过多。
由于 Carla 客户端每次都会更换汽车,油门的效果也会改变,有时会使你的汽车速度过快或过慢。因此,你需要一种方法通过按键来改变油门,或者你可以始终使用同一辆汽车,这将是我们的解决方案。
你可以使用以下代码行获取 Carla 中可用的汽车列表:
vehicles = world.get_blueprint_library().filter('vehicle.*')
在撰写本文时,这会产生以下列表:
vehicle.citroen.c3
vehicle.chevrolet.impala
vehicle.audi.a2
vehicle.nissan.micra
vehicle.carlamotors.carlacola
vehicle.audi.tt
vehicle.bmw.grandtourer
vehicle.harley-davidson.low_rider
vehicle.bmw.isetta
vehicle.dodge_charger.police
vehicle.jeep.wrangler_rubicon
vehicle.mercedes-benz.coupe
vehicle.mini.cooperst
vehicle.nissan.patrol
vehicle.seat.leon
vehicle.toyota.prius
vehicle.yamaha.yzf
vehicle.kawasaki.ninja
vehicle.bh.crossbike
vehicle.tesla.model3
vehicle.gazelle.omafiets
vehicle.tesla.cybertruck
vehicle.diamondback.century
vehicle.audi.etron
vehicle.volkswagen.t2
vehicle.lincoln.mkz2017
vehicle.mustang.mustang
在World.restart()中,你可以选择你喜欢的汽车:
bp=self.world.get_blueprint_library().filter(self._actor_filter)
blueprint = next(x for x in bp if x.id == 'vehicle.audi.tt')
Carla 使用演员,可以代表车辆、行人、传感器、交通灯、交通标志等;演员是通过名为try_spawn_actor()的模板创建的:
self.player = self.world.try_spawn_actor(blueprint, spawn_point)
如果你现在运行代码,你会看到汽车,但视角是错误的。按下Tab键可以修复它:
图 8.22 – 左:默认初始相机,右:自动驾驶相机
如果你想要从我在训练汽车的地方开始,你也需要在相同的方法中设置起始点:
spawn_point = spawn_points[0] if spawn_points else carla.Transform()
如果你这样做,汽车将在随机位置生成,并且可能驾驶时会有更多问题。
在game_loop()中,我们还需要选择合适的赛道:
client.load_world('Town04')
client.reload_world()
如果你现在运行它,在按下Tab之后,你应该看到以下类似的内容:
图 8.23 – Carla 的图像,准备自动驾驶
如果你按下F1,你可以移除左侧的信息。
为了方便起见,我们希望能够触发自动驾驶模式的开和关,因此我们需要一个变量来处理,如下所示,以及一个在KeyboardControl构造函数中保存计算出的转向角度的变量:
self.self_driving = False
然后,在KeyboardControl.parse_events()中,我们将拦截D键,并切换自动驾驶功能的开和关:
elif event.key == K_d:
self.self_driving = not self.self_driving
if self.self_driving:
world.hud.notification('Self-driving with Neural Network')
else:
world.hud.notification('Self-driving OFF')
下一步是将从服务器接收到的最后一张图像进行缩放并保存,当它仍然是 BGR 格式时,在CameraManager._parse_image()中。这在这里展示:
array_bgr = cv2.resize(array, (200, 133))
self.last_image = array_bgr[67:, :, :]
array = array[:, :, ::-1] # BGR => RGB
array变量最初包含 BGR 格式的图像,而 NumPy 中的::-1反转了顺序,所以最后一行代码实际上在可视化之前将图像从 BGR 转换为 RGB。
现在,我们可以在game_loop()函数外部,主循环之外加载模型:
model = keras.models.load_model('behave.h5')
然后,我们可以在game_loop()主循环内部运行模型,并保存转向,如下所示:
if world.camera_manager.last_image is not None:
image_array = np.asarray(world.camera_manager.last_image)
controller.self_driving_steer = model.predict(image_array[ None, :, :, :], batch_size=1)[0][0].astype(float)
最后要做的事情就是使用我们计算出的转向,设置一个固定的油门,并限制最大速度,同时进行:
if self.self_driving:
self.player_max_speed = 0.3
self.player_max_speed_fast = 0.3
self._control.throttle = 0.3
self._control.steer = self.self_driving_steer
return
这听起来很好,但是它可能因为 GPU 错误而无法工作。让我们看看是什么问题以及如何克服它。
让你的 GPU 工作
你可能会得到类似于以下错误的错误:
failed to create cublas handle: CUBLAS_STATUS_ALLOC_FAILED
我对发生的事情的理解是,与 Carla 的某些组件存在冲突,无论是服务器还是客户端,这导致 GPU 内存不足。特别是,TensorFlow 在尝试在 GPU 中分配所有内存时造成了问题。
幸运的是,这可以通过以下几行代码轻松修复:
import tensorflow
gpus = tensorflow.config.experimental.list_physical_devices('GPU')
if gpus:
try:
for gpu in gpus:
tensorflow.config.experimental.set_memory_growth(gpu, True)
print('TensorFlow allowed growth to ', len(gpus), ' GPUs')
except RuntimeError as e:
print(e)
set_memory_growth()的调用指示 TensorFlow 只分配 GPU RAM 的一部分,并在需要时最终分配更多,从而解决问题。
到目前为止,你的汽车应该能够驾驶,让我们讨论一下它是如何工作的。
自动驾驶!
现在,你可以开始运行manual_control_drive.py,可能需要使用--res 480x320参数来降低分辨率。
如果你按下 D 键,汽车应该会开始自动行驶。它可能相当慢,但应该会运行,有时运行得很好,有时则不那么好。它可能不会总是按照它应该走的路线行驶。你可以尝试向数据集中添加图像或改进神经网络的架构——例如,通过添加一些 dropout 层。
你可以尝试更换汽车或提高速度。你可能会注意到,在更高的速度下,汽车开始更加无规律地移动,就像司机喝醉了一样!这是由于汽车进入错误位置和神经网络对其做出反应之间的过度延迟造成的。我认为这可以通过一个足够快的计算机来部分解决,以便处理许多 FPS。然而,我认为真正的解决方案还需要记录更高速度的运行,其中校正会更强烈;这需要一个比键盘更好的控制器,你还需要在输入中插入速度,或者拥有多个神经网络,并根据速度在它们之间切换。
有趣的是,有时即使我们使用外部摄像头,它也能以某种方式驾驶,结果是我们的汽车成为了图像的一部分!当然,结果并不好,即使速度很低,你也会得到 醉酒驾驶 的效果。
出于好奇,让我们检查一下显著性图。这是我们发送给网络的图像:
图 8.24 – 后视图
现在,我们可以检查显著性图:
图 8.25 – 显著性图:第三卷积层和输出层
网络仍然能够识别线条和道路;然而,它非常关注汽车。我的假设是神经网络 认为 它是一个障碍物,道路在结束。
如果你想要教汽车如何使用这个摄像头或任何其他摄像头来驾驶,你需要用那个特定的摄像头来训练它。如果你想汽车在另一条赛道上正确驾驶,你需要在那条特定的赛道上训练它。最终,如果你在许多赛道和许多条件下训练它,它应该能够去任何地方驾驶。但这意味着构建一个包含数百万图像的巨大数据集。最终,如果你的数据集太大,你会耗尽内存。
在下一节中,我们将讨论生成器,这是一种可以帮助我们克服这些问题的技术。
使用生成器训练更大的数据集
当训练大数据集时,内存消耗可能成为一个问题。在 Keras 中,解决这个问题的方法之一是使用 Python 生成器。Python 生成器是一个可以懒加载地返回可能无限值流的函数,具有非常低的内存占用,因为你只需要一个对象的内存,当然,还需要所有可能需要的支持数据;生成器可以用作列表。典型的生成器有一个循环,并且对于需要成为流一部分的每个对象,它将使用yield关键字。
在 Keras 中,生成器需要知道批次大小,因为它需要返回一个样本批次和一个标签批次。
我们将保留一个要处理的文件列表,我们将编写一个生成器,可以使用这个列表返回与之关联的图像和标签。
我们将编写一个通用的生成器,希望你在其他情况下也能重用,它将接受四个参数:
-
一个 ID 列表,在我们的例子中是文件名
-
一个从 ID 检索输入(图像)的函数
-
一个从 ID 检索标签(方向盘)的函数
-
批次大小
首先,我们需要一个函数,可以返回给定文件的图像:
def extract_image(file_name):
return cv2.imread(file_name)
我们还需要一个函数,给定一个文件名,可以返回标签,在我们的例子中是转向角度:
def extract_label(file_name):
(seq, camera, steer, throttle, brake, img_type) = expand_name(file_name)
return steer
我们现在可以编写生成器,如下所示:
def generator(ids, fn_image, fn_label, batch_size=32):
num_samples = len(ids)
while 1: # The generator never terminates
samples_ids = shuffle(ids) # New epoch
for offset in range(0, num_samples, batch_size):
batch_samples_ids = samples_ids[offset:offset + batch_size]
batch_samples = [fn_image(x) for x in batch_samples_ids]
batch_labels = [fn_label(x) for x in batch_samples_ids]
yield np.array(batch_samples), np.array(batch_labels)
while循环中的每次迭代对应一个周期,而for循环生成完成每个周期所需的全部批次;在每个周期的开始,我们随机打乱 ID 以改善训练。
在 Keras 中,过去你必须使用fit_generator()方法,但现在fit()能够理解如果参数是一个生成器,但你仍然需要提供一些新的参数:
-
steps_per_epoch:这表示单个训练周期中批次的数量,即训练样本数除以批次大小。 -
validation_steps:这表示单个验证周期中批次的数量,即验证样本数除以批次大小。
这是你需要使用我们刚刚定义的generator()函数的代码:
files = shuffle(files)idx_split = int(len(files) * 0.8)
val_size = len(files) - idx_split
train_gen = generator(files[0:idx_split], extract_image, extract_label, batch_size)
valid_gen = generator(files[idx_split:], extract_image, extract_label, batch_size)
history_object = model.fit(train_gen, epochs=250, steps_per_epoch=idx_split/batch_size, validation_data=valid_gen, validation_steps=val_size/batch_size, shuffle=False, callbacks= [checkpoint, early_stopping])
多亏了这段代码,你现在可以利用非常大的数据集了。然而,生成器还有一个应用:自定义按需数据增强。让我们简单谈谈这个话题。
硬件方式增强数据
我们已经看到了一种简单的方法来进行数据增强,使用第七章中的ImageDataGenerator,检测行人和交通灯。这可能适用于分类器,因为应用于图像的变换不会改变其分类。然而,在我们的情况下,这些变换中的一些会需要改变预测。实际上,英伟达设计了一种自定义数据增强,其中图像被随机移动,方向盘根据移动量相应更新。这可以通过生成器来完成,其中我们取原始图像,应用变换,并根据移动量调整方向盘。
但我们不仅限于复制输入中相同数量的图像,我们还可以创建更少(过滤)或更多;例如,镜像可以在运行时应用,结果是在内存中重复图像,而不必存储双倍数量的图像和保存,因此节省了文件访问和 JPEG 解压缩的一半;当然,我们还需要一些 CPU 来翻转图像。
摘要
在本章中,我们探讨了众多有趣的主题。
我们首先描述了 DAVE-2,这是英伟达的一个实验,旨在证明神经网络可以学会在道路上驾驶,我们决定在更小的规模上复制相同的实验。首先,我们从 Carla 收集图像,注意不仅要记录主摄像头,还要记录两个额外的侧摄像头,以教会网络如何纠正错误。
然后,我们创建了我们的神经网络,复制了 DAVE-2 的架构,并对其进行回归训练,这与其他我们迄今为止所做的训练相比需要一些改变。我们学习了如何生成显著性图,并更好地理解神经网络关注的地方。然后,我们与 Carla 集成,并使用该网络来自动驾驶汽车!
最后,我们学习了如何使用 Python 生成器训练神经网络,并讨论了如何利用这种方法实现更复杂的数据增强。
在下一章中,我们将探索一种用于在像素级别检测道路的尖端技术——语义分割。
问题
阅读本章后,你应该能够回答以下问题:
-
英伟达为自动驾驶训练的神经网络的原始名称是什么?
-
分类和回归任务之间的区别是什么?
-
你可以使用哪个 Python 关键字来创建生成器?
-
什么是显著性图?
-
为什么我们需要记录三个视频流?
-
为什么我们从
game_loop()方法中进行推理?
进一步阅读
-
Nvidia DAVE-2:
devblogs.nvidia.com/deep-learning-self-driving-cars/ -
与 Carla 0.9.0 API 变更相关的笔记:
carla.org/2018/07/30/release-0.9.0/ -
Carla:
carla.org -
keras-vis:github.com/raghakot/keras-vis
1330

被折叠的 条评论
为什么被折叠?



