《Deep Learning for Computer Vision withPython》阅读笔记-PractitionerBundle(第1 - 5章)

本章介绍了使用预训练的卷积神经网络(CNN)进行迁移学习,作为特征提取器,提高图像分类的准确性。讨论了如何在小数据集上应用数据增强来减少过拟合,并展示了如何使用VGG16提取特征以构建高精度的分类器。此外,解释了1级和5级精度的概念,以及如何在Flowers-17和CALTECH-101数据集上实现微调,从而获得更好的分类性能。
摘要由CSDN通过智能技术生成

1.介绍

欢迎使用Python学习计算机视觉的深度学习实践包!这本书是在完成Starter Bundle之后,您的计算机视觉教育深度学习的下一个合乎逻辑的步骤。

实践者包的目的是建立在您从Starter包中获得的知识的基础上,并介绍更高级的算法、概念和行业技巧——这些技术将在本书的三个不同部分中介绍。

第一部分将侧重于以某种方式提高分类准确性的方法。提高分类准确性的一个方法是应用迁移学习方法,如微调或将你的网络作为一个特征提取器。

我们还将探讨集成方法(例如,训练多个网络并结合结果),以及这些方法如何通过很少的额外努力来提高你的分类。正则化方法(如数据增强)用于生成额外的训练数据——在几乎所有情况下,数据增强可以提高模型的泛化能力。更高级的优化算法,如Adam[1]、RMSprop[2]和其他算法也可以在一些数据集上使用,以帮助您获得更低的损失。在我们回顾这些技术之后,我们将研究应用这些方法的最佳途径,以确保您以最少的努力获得最大的效益。

然后,我们将转向实践者包的第二部分,该部分主要关注更大的数据集和更奇特的网络架构。到目前为止,我们只处理了能够装入系统主存的数据集——但是如果我们的数据集太大而不能装入RAM呢?然后我们做什么?当我们使用HDF5时,我们将在第9章中讨论这个问题。

考虑到我们将使用更大的数据集,我们也将能够使用AlexNet、GoogLeNet、ResNet和VGGNet的更深层变体来讨论更高级的网络架构。这些网络架构将应用于更具挑战性的数据集和竞争,包括Kaggle: dog vs. Cats recognition challenge[3]以及cs231n Tiny ImageNet challenge[4],斯坦福CNN学生参加的任务完全相同。正如我们所发现的,我们将能够在Kaggle狗对猫排行榜上获得前25名并在cs231n挑战中获得我们技术类型的冠军。

本书的最后一部分涵盖了图像分类之外的计算机视觉深度学习的应用,包括基本的对象检测,深度做梦和神经风格,生成对抗网络(GANs),和图像超分辨率。同样,本卷中涉及的技术比Starter Bundle要先进得多——这是您将从深度学习新手中脱颖而出并转变为真正的深度学习实践者的地方。要开始向深度学习专家转变,只需翻开这一页。

2.数据增强

根据Goodfellow等人的说法,正则化是“我们对学习算法所做的任何修改,其目的是减少其泛化误差,而不是减少其训练误差”[5]。简而言之,正则化寻求减少我们的测试误差,可能以略微增加训练误差为代价。

我们已经在Starter Bundle的第9章中看到了不同形式的正则化;然而,这些都是正则化的参数化形式,要求我们更新损失/更新函数。事实上,还有其他类型的正则化:

  1. 修改网络架构本身;
  2. 增加传入网络进行训练的数据;

Dropout是通过实现更大的通用性来修改网络架构的一个很好的例子。在这里,我们插入一个层,该层随机地将节点从上一层断开到下一层,从而确保没有单个节点负责学习如何表示给定的类。

在本章的其余部分,我们将讨论另一种称为数据扩充的正则化。该方法在将训练样本送入网络进行训练之前,故意对其进行扰动,略微改变其外观。最终的结果是,网络始终可以看到由原始训练数据生成的“新的”训练数据点,这在一定程度上减轻了我们收集更多训练数据的需要(尽管一般来说,收集更多的训练数据很少会损害算法)。

2.1 数据增强

数据增强包括广泛的技术,通过应用随机抖动和扰动,从原始训练样本生成新的训练样本,这样类标签就不会改变。在应用数据扩充时,我们的目标是增加模型的通用性。鉴于我们的网络不断地看到新的、稍作修改的输入数据点,它能够学习到更健壮的特性。在测试时,我们不会应用数据增强和评估我们训练过的网络——在大多数情况下,您会看到测试准确性的提高,但可能会以训练准确性略微下降为代价。

左图:250个数据点的样本,完全符合正态分布。右:在分布中添加少量的随机“抖动”。这种类型的数据增强可以增加我们的网络的泛化性。

让我们考虑均值和单位方差为零的正态分布的图2.1(左)。在这些数据上训练一个机器学习模型可能会导致我们精确地建模分布——然而,在现实世界的应用程序中,数据很少遵循这样整洁的分布。

相反,为了增加我们的分类器的可泛化性,我们可以首先通过添加一些从随机分布(右)中提取的值来随机地使点抖动。图中仍然近似服从正态分布,但不像左边那样是完美分布。在此数据上训练的模型更有可能泛化到训练集中不包括的示例数据点。

在计算机视觉的背景下,数据增强是很自然的。例如,我们可以通过应用简单的几何变换从原始图像中获得额外的训练数据,如random:

  1. 翻译2。旋转3。4级的变化。剪切5。水平(在某些情况下,是垂直)翻转

对输入图像应用(少量)这些转换会稍微改变图像的外观,但不会改变类标签——因此使数据增强成为一种非常自然、简单的方法,可以应用于计算机视觉任务的深度学习。应用于计算机视觉的更先进的数据增强技术包括给定颜色空间中颜色的随机扰动[6]和非线性几何畸变[7]。

2.2 可视化数据增强

我们的脚本需要三个命令行参数,每个参数详细如下:

•-image:这是到输入图像的路径,我们想要应用数据增强并可视化结果。

•——output:在对给定的图像应用数据扩充后,我们希望将结果存储在磁盘上,以便我们可以检查它——这个开关控制输出目录。

•——prefix:一个字符串,将被添加到输出的imagefilename前。

现在,我们的命令行参数已经被解析,让我们加载我们的输入图像,将其转换为一个keras兼容的数组,并为图像添加一个额外的维度,就像我们在为图像分类做准备时所做的那样:

ImageDataGenerator类有很多参数,太多了,在本书中无法列举。有关参数的完整介绍,请参阅Keras的官方文档(http://pyimg.co/j8ad8)。

相反,我们将重点关注您最可能在自己的应用程序中使用的增强参数。参数rotation_range控制随机旋转的程度范围。在这里,我们将允许我们的输入图像随机旋转±30度。width_shift_range和height_shift_range分别用于水平和垂直移动。参数值是给定维度的一部分,在本例中为10%。

shear_range以弧度的形式控制逆时针方向的角度,我们的图像将允许被剪切。然后我们有zoom_range,这是一个浮点值,它允许图像根据以下值的均匀分布被“放大”或“缩小”:[1 - zoom_range, 1 + zoom_range]。

最后,horizontal_flip布尔值控制是否允许在训练过程中水平翻转给定的输入。对于大多数计算机视觉应用程序来说,图像的水平翻转不会改变生成的类标签——但在有些应用程序中,水平(或垂直)翻转会改变图像的语义。在应用这种类型的数据增强时要注意,因为我们的目标是略微修改输入图像,从而生成一个新的训练样本,而不改变类标签本身。关于图像转换的更详细的回顾,请参考PyImageSearch Gurus ([8], PyImageSearch Gurus)中的模块#1以及Szeliski[9]。

一旦ImageDataGenerator被初始化,我们就可以生成新的训练示例:

第34行和35行初始化了一个用于构造增强图像的Python生成器。我们将传入输入图像,batch_size为1(因为我们只增加一张图像),以及一些额外的参数来指定输出的imagefile路径、每个文件路径的前缀和imagefile格式。第38行然后开始在imageGen生成器中的每个图像上循环。在内部,imageGen会在每次通过循环请求一个样本时自动生成一个新的训练样本。然后增加写入磁盘的数据增强示例的总数,并在达到10个示例时停止执行脚本。

为了将数据增强可视化,我们将使用图2.2(左),这是我家的小猎犬Jemma的图像。要生成新的Jemma训练示例图像,只需执行以下命令:

我已经为这些图像构建了一个蒙版,所以你可以在图2.2(右)中看到它们。注意每个图像是如何被随机旋转、剪切、缩放和水平翻转的。在每一种情况下,图像保留原始的类别标签:狗;然而,每一幅图像都进行了轻微的修改,从而使我们的神经网络在训练时可以学习新的模式。由于输入图像会不断地变化(而类标签保持不变),与没有数据增强的训练相比,我们的训练准确度会下降,这是很常见的。

然而,正如我们在本章后面会发现的那样,数据增强可以大大减少过拟合,同时确保我们的模型更好地适用于新的输入样本。此外,当我们在处理数据集时,如果样本太少,无法应用深度学习,我们可以利用数据增强来生成额外的训练数据,从而减少训练深度学习网络所需的手动标记数据的数量。

2.3 使用数据增强与否对训练的影响

在本节的第一部分,我们将讨论Flowers-17数据集,这是一个非常小的数据集(就计算机视觉任务的深度学习而言),以及数据增强如何通过生成额外的训练样本来帮助我们人为地增加该数据集的大小。在此,我们将执行两个实验:

  1. 训练MiniVGGNet on Flowers-17使用数据增强;
  2. 训练MiniVGGNet on Flowers-17不使用数据增强。

我们会发现,应用数据增强可以显著减少过拟合,并允许MiniVGGNet获得更高的分类精度。

2.3.1 Flowers-17 数据集

Flowers-17数据集[10]是一个细粒度的分类挑战,我们的任务是识别17种不同的花卉。图像数据集非常小,每个类只有80幅图像,而总共有1,360幅图像。将深度学习应用于计算机视觉任务时,一般的经验法则是每节课要有1000 - 5000个示例,所以我们在这方面肯定存在巨大的不足。

我们称Flowers-17为细粒度分类任务,因为所有类别都非常相似(例如,种花)。事实上,我们可以把每一个类别看作是子类别。这些类别当然是不同的,但有很多共同的结构(例如,花瓣,雄蕊、雌蕊等)。细粒度的分类任务对于深度学习从业者来说是最具挑战性的,因为这意味着我们的机器学习模型需要学习极具鉴别性的特征,以区分非常相似的类。考虑到我们有限的训练数据,这个细粒度的分类任务变得更加困难。

 ​​​​​​​

2.3.2 创建预处理

到目前为止,我们只对图像进行了预处理,将其调整为固定大小,忽略了宽高比。在某些情况下,特别是对于基本的基准数据集,这样做是可以接受的。

然而,对于更有挑战性的数据集,我们仍然应该设法调整到固定大小,但保持高宽比。要将此操作可视化,请考虑图2.4。

在左边,我们有一个输入图像,我们需要将其大小调整为固定的宽度和高度。忽略长宽比,我们将图像的大小调整为256 × 256像素(中间),有效地挤压和扭曲图像,使其满足我们所需的尺寸。一个更好的方法是考虑图像的长宽比(右),我们首先沿着较短的尺寸调整大小,使其宽度为256像素,然后沿着高度裁剪图像,使其高度为256像素。

在裁剪过程中,我们有效地丢弃了部分图像,同时也保持了图像的原始长宽比。保持一致的纵横比可以让我们的卷积神经网络学习更有区别的、一致的特征。这是一种常见的技术,我们将在整个练习者Bundle和ImageNet Bundle的其余部分中使用它处理更高级的数据集。

就像在SimplePreprocessor中一样,我们的构造函数需要两个参数(目标输出图像的期望宽度和高度)以及在调整图像大小时使用的插值方法。然后我们可以定义预处理函数如下:

preprocess函数只接受一个参数,即我们希望进行预处理的图像。第16行抓取输入图像的宽度和高度,而第17行和第18行决定了我们将在沿着较大的尺寸裁剪时使用的delta偏移量。同样,我们的方面感知预处理器是一个两步算法:

  1. 步骤1:确定最短的尺寸并沿着它调整大小。
  2. 步骤2:沿着最大的尺寸裁剪图像,获得目标的宽度和高度。

下面的代码块处理检查宽度是否小于高度,如果是,则沿着宽度调整大小:

//截止到P22页

 # if the width is smaller than the height, then resize

21 # along the width (i.e., the smaller dimension) and then

22 # update the deltas to crop the height to the desired

23 # dimension

24 if w < h:

25 image = imutils.resize(image, width=self.width,

26 inter=self.inter)

27 dH = int((image.shape[0] - self.height) / 2.0)

# otherwise, the height is smaller than the width so

30 # resize along the height and then update the deltas

31 # to crop along the width

32 else:

33 image = imutils.resize(image, height=self.height,

34 inter=self.inter)

35 dW = int((image.shape[1] - self.width) / 2.0)

# now that our images have been resized, we need to

38 # re-grab the width and height, followed by performing

39 # the crop

40 (h, w) = image.shape[:2]

41 image = image[dH:h - dH, dW:w - dW]

42

43 # finally, resize the image to the provided spatial

44 # dimensions to ensure our output image is always a fixed

45 # size

46 return cv2.resize(image, (self.width, self.height),

47 interpolation=self.inter)

​​​​​​​2.3.3 Flowers-17:没哟使用数据增强的情况下的实验

第2-14行导入所需的Python包。这些进口产品你以前都见过,但我想提请你注意:1。第6行:这里我们导入新定义的AspectAwarePreprocessor。2. 第7行:尽管使用了单独的图像预处理器,我们仍然能够使用SimpleDatasetLoader从磁盘加载我们的数据集。3.第8行:我们将在我们的数据集上训练MiniVGGNet架构。

让我们继续从我们的输入图像中提取类标签:

数据集的形式:

因此,为了提取类标签,我们可以简单地在路径分隔符(第26行)上拆分后提取第二个到最后一个索引,从而产生文本bluebell。如果您很难理解这个路径和标签提取是如何工作的,我建议您打开一个Python shell,使用文件路径和路径分隔符。特别是,请注意如何根据操作系统的路径分隔符分割字符串,然后使用Python索引来提取数组的各个部分。然后,第27行从图像路径确定唯一的类标签集(在本例中,共有17个类)。

给定我们的imagepath,我们可以从磁盘加载Flowers-17数据集:

第30行初始化了AspectAwarePreprocessor,这样它处理的每个图像都将是64 × 64像素。然后在第31行初始化ImageToArrayPreprocessor,允许我们将图像转换为keras兼容的数组。然后,我们分别使用这两个预处理器实例化SimpleDatasetLoader(第35行)。

数据和相应的标签在第36行从磁盘加载。然后,通过将原始像素强度除以255,将数据数组中的所有图像归一化到范围[0,1]。

现在,我们的数据已经加载,我们可以执行一个训练和测试分割(75%的训练,25%的测试),以及一个热编码我们的标签:

为了训练我们的花分类器,我们将使用MiniVGGNet架构和SGD优化器:

# initialize the optimizer and model
print("[INFO] compiling model...")
opt = SGD(lr=0.05)
model = MiniVGGNet.MiniVGGNet.build(width=64, height=64, depth=3,
classes=len(classNames))
model.compile(loss="categorical_crossentropy", optimizer=opt,
metrics=["accuracy"])
# train the network
print("[INFO] training network...")
H = model.fit(trainX, trainY, validation_data=(testX, testY),
batch_size=32, epochs=100, verbose=1)

MiniVGGNet架构将接受空间尺寸为64 × 64 × 3(64像素宽,64像素高,3通道)的图像。类的总数是len(classNames),在本例中,它等于17,每个类对应Flowers-17数据集中的每个类别。

我们将使用SGD训练MiniVGGNet,初始学习率为α = 0.05。我们将故意忽略学习率衰减,以便在下一节中演示数据增强的影响。58、59号线列车MiniVGGNet共100个时代。

然后,我们评估我们的网络,并画出随着时间的推移我们的损失和准确性:

运行以下命令:

python minivggnet_flowers17.py --dataset ../flowers17/images

从输出中我们可以看到,我们能够获得64%的分类准确率,在我们有限的训练数据量下,这是相当合理的。然而,值得关注的是我们的loss and accuracy plot (Figure(2.5))。正如图中所示,我们的网络很快开始过拟合过去的epoch 20。这种行为的原因是,我们只有1,020个训练示例,每个类只有60张图片(其他图片用于测试)。请记住,在训练卷积神经网络时,理想情况下每个类应该有1000 - 5000个示例。

此外,训练精度在前几个阶段飙升至95%以上,最终在后几个阶段获得100%的精度——这一输出是一个明显的过拟合案例。由于缺乏大量的训练数据,MiniVGGNet正在对训练数据中的底层模式进行建模太接近,无法概括到测试数据。为了对抗过拟合,我们可以应用正则化技术-在本章的上下文中,我们的正则化方法将是数据扩充。在实践中,您还将包括其他形式的正则化(权重衰减、退出等),以进一步减少过拟合的影响。

​​​​​​​2.3.4 Flowers-17:数据增强

在本例中,我们将应用与前一节相同的训练过程,只是增加了一个内容:我们将应用数据增强。为了了解数据增强如何在防止过拟合的同时提高我们的分类精度,打开一个新文件,命名为minivggnet_flowers17_data_augy,让我们开始工作:

在这里,我们将允许图像为:随机旋转±30°2。水平和垂直移动0.2倍。剪切0.2 4。在范围内均匀采样放大[0.8,1.2]5。随机horizontallyflipped。

根据您的确切数据集,您将希望调整这些数据增强值。根据应用程序的不同,通常可以看到旋转范围在[10,30]之间。水平和垂直移动通常在范围[0.1,0.2](缩放值也是如此)。除非水平翻转你的图像会改变类标签,否则你也应该包括水平翻转。

就像在之前的实验中,我们将使用SGD优化器训练MiniVGGNet:

然而,用来训练我们的网络的代码需要稍微改变一下,因为我们现在使用的是一个图像生成器:

# train the network

63 print("[INFO] training network...")

64 H = model.fit_generator(aug.flow(trainX, trainY, batch_size=32),

65 validation_data=(testX, testY), steps_per_epoch=len(trainX) // 32,

66 epochs=100, verbose=1)

我们现在需要调用.fit_generator,而不是调用模型的.fit方法。.fit_generator的第一个参数是aug.flow,这是我们的数据增强函数,用于从训练数据生成新的训练样本。aug.flow要求我们传递训练数据和相应的标签。我们还需要提供批处理大小,以便生成器在训练网络时可以构造适当的批处理。

然后,我们将validation_data作为(testX, testY)的二元组提供——该数据在每个epoch的末尾用于验证。steps_per_epoch参数控制每个epoch的批处理数量——我们可以通过编程方式确定适当的steps_per_epoch值,方法是将训练样本的总数除以批处理大小,并将其转换为整数。最后,epoch控制我们的网络应该训练的epoch的总数,在本例中是100个epoch。

在网络完成训练后,你会发现准确率从64%提高到71%,比我们之前的测试提高了10.9%。然而,准确性并不是一切——真正的问题是数据增强是否有助于防止过拟合。为了回答这个问题,我们需要检查图2.6中的损失和准确性图。

在仍然存在过拟合现象的情况下,利用数据增强技术显著抑制了过拟合效应。同样,请记住,这两个实验是相同的——我们所做的唯一更改是是否应用了数据增强。作为正则化器,你还可以看到数据增强有影响。我们能够提高我们的验证准确性,从而提高我们的模型的通用性,尽管有降低的训练精度。

进一步的准确性可以通过随时间衰减学习速率来获得。本章没有特别提到学习速率,所以我们可以只关注正则化器在训练卷积神经网络时对数据增强的影响。

​​​​​​​2.4 总结

数据增强是一种对训练数据进行操作的正则化技术。顾名思义,数据增强通过应用一系列随机的翻译、旋转、剪切和翻转,随机地抖动我们的训练数据。应用这些简单的转换不会改变输入图像的类标签;然而,每个增强图像都可以被认为是训练算法以前没有见过的“新”图像。因此,我们的训练算法不断地被提出新的训练样本,使其学习到更鲁棒和更有区别的模式。

正如我们的结果所显示的,应用数据增强提高了我们的分类准确性,同时帮助减轻过拟合的影响。此外,数据增强还允许我们在每个类只使用60个样本来训练卷积神经网络,远低于建议的每类1000 - 5000个样本。

虽然收集“自然的”训练样本总是更好的,但在必要时,可以使用数据增强来克服小数据集的限制。当涉及到您自己的实验时,您应该将数据增强应用到您运行的几乎每个实验中。由于CPU现在负责随机转换你的输入,所以你必须承受轻微的性能冲击;但是,通过在将数据传递给负责培训网络的线程之前,在后台使用线程和增强数据,可以减轻这种性能冲击。

同样,在实践者Bundle和ImageNet Bundle的几乎所有剩余章节中,我们都将使用数据增强。现在就花点时间熟悉一下这项技术,因为它将帮助您更快地获得性能更好的深度学习模型(使用更少的数据)。

3. 网络作为特征提取器

在接下来的几章中,我们将讨论迁移学习的概念,即使用一个预先训练的模型作为从数据中学习模式的“捷径”的能力。

考虑一个传统的机器学习场景,我们面临两个分类挑战。在第一个挑战中,我们的目标是训练一个卷积神经网络来识别图像中的狗和猫(正如我们将在第10章中做的那样)。

然后,在第二个项目中,我们的任务是识别三种不同的熊:灰熊、北极熊和大熊猫。使用机器学习、神经网络和深度学习的标准实践,我们将这些挑战视为两个独立的问题。首先,我们将收集足够的狗和猫的标记数据集,然后在数据集上训练一个模型。然后,我们将重复这个过程第二次,只是这一次,收集我们的熊品种的图像,然后在标记的熊数据集上训练一个模型。

迁移学习提出了一种不同的训练范式——如果我们可以使用现有的预先训练的分类器,并将其作为一个新的分类任务的起点,会怎么样?在上述提出的挑战的背景下,我们将首先训练一个卷积神经网络来识别狗和猫。然后,我们将使用在狗和猫数据上训练过的相同CNN来区分熊的类别,即使没有熊的数据与狗和猫的数据混合。

这听起来好得令人难以置信吗?实际上并不是。在大规模数据集(如ImageNet)上训练的深度神经网络在迁移学习任务中表现出色。这些网络学习一组丰富的、具有鉴别性的特征,以识别1000种不同的对象类别。这些过滤器可以用于分类任务,而不是CNN最初训练的内容,这是有道理的。

一般来说,在应用于计算机视觉深度学习时,迁移学习有两种类型:

  1. 将网络视为任意特征提取器;
  2. 移除现有网络的全连接层,将新的FC层设置在CNN之上,并微调这些权重(以及之前的层)来识别对象类;

在这一章中,我们将主要关注迁移学习的第一种方法,将网络作为特征提取器。然后,我们将在第5章中讨论如何调整网络的权重以适应特定的分类任务。

​​​​​​​3.1 利用预先训练的CNN提取特征

到目前为止,我们一直将卷积神经网络视为端到端图像分类器:

  1. 我们像网络中输入一个图片;
  2. 图片前向转播网络;
  3. 得到一个最终的分类可能性从网络的最后输出层;

然而,没有“规则”说我们必须允许图像通过整个网络转发传播。相反,我们可以在任意一层停止传播,比如激活层或池化层,从这个时候的网络中提取值,然后使用它们作为特征向量。例如,让我们考虑Simonyan和Zisserman[11]的VGG16网络架构(图3.1,左)。

左图:原始的VGG16网络架构,输出1,000个ImageNet类标签的每个标签的概率。右:从VGG16中移除FC层,而不是返回最后一个POOL层的输出。这个输出将作为我们提取的特征。

除了网络中的层,我们还包括了每一层的输入和输出形状。当将网络作为一个特征提取器时,我们本质上是在任意点“切断”网络(通常是在完全连接的层之前,但这实际上取决于你的特定数据集)。

现在我们网络中的最后一层是最大池化层(图3.1,右),它的输出形状为7 × 7 × 512,这意味着有512个大小为7 × 7的滤波器。如果我们通过这个去掉FC头的网络转发一幅图像,我们将会得到5127 × 7次激活,这些激活或未激活都是基于图像内容的。因此,我们实际上可以取这7 × 7 × 512 = 25,088的值,将其作为一个特征向量来量化图像的内容。

如果我们对整个图像数据集重复这个过程(包括VGG16没有训练的数据集),我们将得到N个图像的设计矩阵,每个图像有25088列用于量化其内容(即特征向量)。给定我们的特征向量,我们可以在这些特征之上训练现成的机器学习模型,如线性支持向量机、逻辑回归分类器或随机森林,以获得识别新的图像类别的分类器。

请记住,CNN本身并不能够识别这些新类——相反,我们使用CNN作为一个中介特征提取器。下游的机器学习分类器将负责学习从CNN中提取的特征的基本模式。

在本章的后面,我将演示如何使用预先训练的cnn(特别是VGG16)和Keras库,以获得> 95%的分类精度的图像数据集,如Animals, cal技术-101和Flowers-17。这两个数据集都不包含VGG16训练过的图像,但通过应用迁移学习,我们能够轻松地构建超精确的图像分类器。诀窍是提取这些特征,并以有效的方式存储它们。为了完成这个任务,我们需要HDF5。

​​​​​​​3.1.1 HDF5是什么?

HDF5是由HDF5组[12]创建的二进制数据格式,用于在磁盘上存储巨大的数字数据集(太大了,无法存储在内存中),同时方便对数据集的行进行访问和计算。HDF5中的数据是分层存储的,类似于文件系统存储数据的方式。数据首先在组中定义,组是一种容器状的结构,可以保存数据集和其他组。一旦定义了组,就可以在组内创建数据集。数据集可以被看作是同构数据类型(整数、浮点数、unicode等)的多维数组(即NumPy数组)。图3.2显示了一个包含多个数据集的hdf5文件的例子。

HDF5是用C语言编写的;然而,通过使用h5py模块(h5py.org),我们可以使用Python编程语言访问底层的C API。h5py之所以如此令人惊叹,是因为它与数据的交互非常简单。我们可以在HDF5数据集中存储大量的数据,并以类似numpy的方式操作数据。例如,我们可以使用标准的Python语法来访问和切片存储在磁盘上的tb级数据集,就像它们是加载到内存中的简单NumPy数组一样。由于有了专门的数据结构,这些片和行访问非常快。当使用HDF5和h5py时,你可以把你的数据想象成一个巨大的NumPy数组,它太大了,不能放进主存,但仍然可以被访问和操作。

也许最重要的是,HDF5格式是标准化的,这意味着以HDF5格式存储的数据集天生就具有可移植性,并且可以被其他使用不同编程语言(如C、MATLAB和Java)的开发人员访问。

在本章的其余部分,我们将编写一个定制的Python类,它允许我们有效地接受输入数据并将其写入HDF5数据集。这个类有两个目的:

  1. 将我们从VGG16中提取的特征,以高效的方式写入HDF5数据集,为我们应用迁移学习提供了一种方法;
  2. 允许我们从原始图像生成HDF5数据集,以促进更快的训练(第9章);

一个包含三个数据集的hdf5文件的例子。第一个数据集包含CALTECH-101的label_names。然后我们有标签,将每个图像映射到相应的类标签。最后,特征数据集包含CNN提取的图像量化。

如果您的系统上还没有安装HDF5和h5py,请参阅Starter Bundle第6章的补充材料,了解如何配置您的系统。

​​​​​​​3.1.2 向HDF5数据集写入特性

我们从简单的进口开始。我们只需要两个Python包来构建这个类中的功能——内置os模块和h5py,这样我们就可以访问HDF5绑定了。

定义构造函数:

def __init__(self, dims, outputPath, dataKey="images",bufSize=1000):
    ## check to see if the output path exists, and if so, raise# an exception
    if os.path.exists(outputPath):
        raise ValueError("The supplied‘outputPath‘ already "
    "exists and cannot be overwritten. Manually delete "
    "the file before continuing.", outputPath)

    # open the HDF5 database for writing and create two datasets:
    # one to store the images/features and another to store the
    # class labels
    self.db = h5py.File(outputPath, "w")
    self.data = self.db.create_dataset(dataKey, dims,
    dtype="float")
    self.labels = self.db.create_dataset("labels", (dims[0],),
    dtype="int")

    # store the buffer size, then initialize the buffer itself
    # along with the index into the datasets
    self.bufSize = bufSize
    self.buffer = {"data": [], "labels": []}
    self.idx = 0

HDF5DatasetWriter的构造函数接受四个参数,其中两个是可选的。dim参数控制我们将存储在数据集中的数据的尺寸或形状。把dim看作NumPy数组的.shape。如果我们存储28 × 28 = 784 MNIST数据集的(平坦的)原始像素强度,那么dimms =(70000, 784),因为MNIST中有70000个示例,每个示例的维数为784。如果我们想存储原始的cifar10图像,那么我们将设置dims=(60000, 32, 32, 3),因为在cifar10数据集中总共有60000张图像,每一张都由一个32 × 32 × 3的RGB图像表示。

在迁移学习和特征提取的背景下,我们将使用VGG16架构,并在最终的POOL层之后获取输出。最后的POOL层的输出是512 × 7 × 7,将其平铺后,得到长度为25088的特征向量。因此,在使用VGG16进行特征提取时,我们设置dims=(N, 25088),其中N为我们数据集中图像的总数。

HDF5DatasetWriter构造函数的下一个参数是outputPath——这是我们的输出hdf5文件存储在磁盘上的路径。可选的dataKey是将存储我们的算法将从中学习的数据的数据集的名称。我们默认这个值为"images",因为在大多数情况下,我们将以HDF5格式存储原始图像。然而,在这个例子中,当我们实例化HDF5DatasetWriter时,我们将设置dataKey="features"来表示我们正在存储从CNN中提取的特征。

最后,bufSize控制内存缓冲区的大小,我们默认为1000个特征向量/图像。一旦达到bufSize,我们将把缓冲区刷新到HDF5数据集。

然后,第10-13行检查outputPath是否已经存在。如果是,则向最终用户抛出一个错误(因为我们不想覆盖现有的数据库)。

第18行打开hdf5文件,使用提供的outputPath进行写入。第19和20行创建了一个具有dataKey名称和提供的亮度的数据集——这是我们存储原始图像/提取的特征的地方。第21和22行创建了第二个数据集,这个数据集用于存储数据集中每条记录的(整数)类标签。第25-28行然后初始化我们的缓冲区。

接下来,让我们回顾一下添加数据到缓冲区的方法:

def add(self, rows, labels):

31 # add the rows and labels to the buffer

32 self.buffer["data"].extend(rows)

33 self.buffer["labels"].extend(labels)

34

35 # check to see if the buffer needs to be flushed to disk

36 if len(self.buffer["data"]) >= self.bufSize:

37 self.flush()

add方法需要两个参数:我们将添加到数据集的行,以及它们相应的类标签。行和标签都被添加到第32行和第33行各自的缓冲区中。如果缓冲区被填满,我们调用flush方法将缓冲区写入文件并重置它们。

定义实现flush()方法:

def flush(self):

40 # write the buffers to disk then reset the buffer

41 i = self.idx + len(self.buffer["data"])

42 self.data[self.idx:i] = self.buffer["data"]

43 self.labels[self.idx:i] = self.buffer["labels"]

44 self.idx = i

45 self.buffer = {"data": [], "labels": []}

如果我们认为我们的HDF5数据集是一个大的NumPy数组,那么我们需要跟踪当前的索引到下一个可用的行,我们可以存储数据(不覆盖现有的数据)-第41行决定了矩阵中的下一个可用行。然后,第42行和第43行应用NumPy数组切片将数据和标签存储在缓冲区中。第45行然后重置缓冲区。

我们还将定义一个名为storeClassLabels的实用函数,如果调用它,将在一个单独的数据集中存储类标签的原始字符串名称:

def storeClassLabels(self, classLabels):

48 # create a dataset to store the actual class label names,

49 # then store the class labels

50 dt = h5py.special_dtype(vlen=unicode)

51 labelSet = self.db.create_dataset("label_names",

52 (len(classLabels),), dtype=dt)

53 labelSet[:] = classLabels

最后,我们的最后一个函数close将被用来将缓冲区中的任何数据写入HDF5,并关闭数据集:

正如你所看到的,HDF5DatasetWriter与机器学习或深度学习没有太多关系——它只是一个用来帮助我们以HDF5格式存储数据的类。当您继续深入学习的时候,您会注意到,当您设置一个新问题时,大部分的初始劳动都是将数据转换成您可以处理的格式。一旦你有了易于操作的数据格式,将机器学习和深度学习技术应用到你的数据上就变得非常容易了。

综上所述,由于HDF5DatasetWriter类是一个非特定于深度学习和计算机视觉的实用程序类,因此我对该类的解释要比本书中的其他代码示例更简短。如果你发现自己很难理解这门课,我建议你:

  1. 读完本章剩下的部分,你就能理解我们如何在特征提取的上下文中使用它。
  2. 花点时间自学一些基本的Python编程范例——我在这里推荐一个Python编程源码列表:http://pyimg.co/ida57。
  3. 将这个类拆开,逐个手工实现,直到您理解在底层发生了什么。

现在我们的HDF5DatasetWriter已经实现,我们可以继续使用预先训练的卷积神经网络来实际提取特征。

​​​​​​​3.2 特征提取过程

第2-13行导入所需的Python包。请注意,在第2行,我们是如何导入经过训练的VGG16网络的Keras实现的——这是我们将用作特征提取器的体系结构。第6行上的LabelEncoder类将用于将类标签从字符串转换为整数。我们还在第7行导入HDF5DatasetWriter,这样我们就可以将从CNN提取的特征写入HDF5数据集。

您还没有看到的一个导入是第10行中的progressbar。这个包与深度学习无关,但我喜欢在长时间运行的任务中使用它,因为它会在你的终端上显示一个格式化良好的进度条,以及提供大致的时间,当你的脚本将完成执行:

然后我们可以提供—batch-size—这是批处理中每次通过VGG16传递的图像的数量。这里值为32是合理的,但是如果您的机器有足够的内存,您可以增加它。在为HDF5数据集编写缓冲区之前,——buffer-size开关控制我们将存储在内存中的提取特征的数量。同样,如果您的机器有足够的内存,您可以增加缓冲区大小。

下一步是从磁盘中抓取我们的图像路径,打乱它们,并对标签进行编码:

# 存储batch_size到一个方便的变量中
bs = args["batch_size"]

# 抓取我们将描述的图像列表,然后随机洗牌他们,以方便训练和测试分割通过32 #数组切片在训练时间

print("[INFO] loading images...")
imagePaths = list(paths.list_images(args["dataset"]))
random.shuffle(imagePaths)


# 从图像路径中提取类标签,然后对类标签进行编码
labels = [p.split(os.path.sep)[-2] for p in imagePaths]
le = LabelEncoder()
labels = le.fit_transform(labels)

在第34行,我们获取imagepath,数据集中所有图像的文件名。然后我们故意在第35行将它们洗牌。为什么我们要自找麻烦呢?请记住,在本书之前的例子中,我们在训练分类器之前计算了训练和测试分割。然而,由于我们要处理的数据集太大,无法装入内存,所以我们无法在内存中执行这种变换—因此,我们在提取特征之前变换图像路径。然后,在训练时,我们可以将75%的索引计算到HDF5数据集中,并使用该索引作为训练数据的结束和测试数据的开始(这一点将在下文中3.3节中变得更清楚)。

提供数据集,我们也遵循这个目录结构(本书中所有的例子一样),39行数组将道路分为基于路径分隔符(在Unix机器上' / '和' \ '在Windows上),然后抓住中的倒数第二个条目数组,这个操作收益率的类标签特定的形象。给定这些标签,然后在第40行和第41行将它们编码为整数(我们将在训练过程中执行1 -hot编码)。

第45行,我们从磁盘加载预训练的VGG16网络;但是,请注意我们是如何包含参数include_top=False的——提供这个值表示最终的全连接层不应该包含在体系结构中。因此,当图像通过网络向前传播时,我们得到的是最后一个POOL层之后的特征值,而不是softmax分类器在FC层产生的概率。

第49和50行实例化了HDF5DatasetWriter。第一个参数是数据集的维度,其中将有len(imagepath)总图像,每个图像的特征向量大小为512 × 7 × 7 = 25,088。第51行然后根据标签编码器存储类标签的字符串名称。

进行实际的特征提取:

# initialize the progress bar

54 widgets = ["Extracting Features: ", progressbar.Percentage(), " ",

55 progressbar.Bar(), " ", progressbar.ETA()]

56 pbar = progressbar.ProgressBar(maxval=len(imagePaths),

57 widgets=widgets).start()

58

59 # loop over the images in patches

60 for i in np.arange(0, len(imagePaths), bs):

61 # extract the batch of images and labels, then initialize the

62 # list of actual images that will be passed through the network

63 # for feature extraction

64 batchPaths = imagePaths[i:i + bs]

65 batchLabels = labels[i:i + bs]

66 batchImages = []

第54-57行初始化我们的进度条,以便我们可以可视化和估计特征提取过程将花费多长时间。同样,使用progressbar是可选的,所以请随意注释这些行。

在第60行,我们开始以-batch-size批量循环我们的imagepath。第64和65行提取对应批处理的图像路径和标签,而第66行初始化一个列表,以存储即将加载并输入到VGG16的图像。

准备一幅图像进行特征提取和准备一幅图像通过CNN进行分类是完全一样的:

# loop over the images and labels in the current batch

69 for (j, imagePath) in enumerate(batchPaths):

70 # load the input image using the Keras helper utility

71 # while ensuring the image is resized to 224x224 pixels

72 image = load_img(imagePath, target_size=(224, 224))

73 image = img_to_array(image)

74

75 # preprocess the image by (1) expanding the dimensions and

76 # (2) subtracting the mean RGB pixel intensity from the

77 # ImageNet dataset

78 image = np.expand_dims(image, axis=0)

79 image = imagenet_utils.preprocess_input(image)

80

81 # add the image to the batch

82 batchImages.append(image)

在第69行,我们循环遍历批处理中的每个图像路径。每个图像从磁盘加载并转换为与keras兼容的数组(第72和73行)。然后在第78行和第79行对图像进行预处理,然后将其添加到batchImages中(第82行)。

为了获得batchImages中图像的特征向量,我们只需要调用模型的.predict方法:

# 将图片传入到cnn中,然后将输出作为实际的特征
batchImages = np.vstack(batchImages)
features = model.predict(batchImages, batch_size=bs)

# 重塑特征,使每个图像由' maxpooling2d '输出的一个扁平的特征向量表示
features = features.reshape((features.shape[0], 512 * 7 * 7))

# 增加特征和标签到hdf5格式文件中
dataset.add(features, batchLabels)
pbar.update()

我们在第86行使用NumPy的.vstack方法来“垂直叠加”我们的图像,使它们具有(N, 224, 224, 3)的形状,其中N是批处理的大小。

通过我们的网络传递批处理图像会产生我们实际的特征向量——记住,我们在VGG16的头部切断了完全连接的层,所以现在我们只剩下最后的最大池操作(第87行)后的值。但是,POOL的输出具有(N, 512, 7, 7)的形状,这意味着有512个过滤器,每个过滤器的大小为7 × 7。为了将这些值视为特征向量,我们需要将它们扁平化成一个形状为(N, 25088)的数组,这正是第91行所实现的。第94行将我们的特性和batchLabels添加到我们的HDF5数据集。

关闭数据集:

​​​​​​​3.2.1 从Animals数据集上提取特征

注意。shape是怎样的(3000,25088)-这个结果意味着我们的Animals数据集中的3000张图像中的每一张都是通过长度为25088的特征向量量化的(即,在最终的POOL操作之后VGG16中的值)。在本章的后面,我们将学习如何根据这些特征训练分类器。

​​​​​​​3.2.2 从CALTECH-101数据集中提取特征

略。​​​​​​​

3.2.3 从Flowers-17数据集中提取特征

python extract_features.py --dataset ../datasets/flowers17/images \

--output ../datasets/flowers17/hdf5/features.hdf5

​​​​​​​3.3 在提取的特征上训练分类器

//截止到2022.1.21日晚上18:29

截止到p45页

//2022.1.21日晚上22:09开始阅读

现在,我们已经使用了一个预先训练的CNN来从一些数据集中提取特征,让我们看看这些特征到底有多大的区别,特别是考虑到VGG16是在ImageNet上训练的,而不是在Animals、CALTECH-101或Flowers-17上。

现在再来猜测一下,一个简单的线性模型在使用这些特征对图像进行分类时可能做得有多好——你猜会比60%的分类精度更好吗?70%看起来不合理吗?80%的可能性肯定不大吧?90%的分类精度是难以理解的,对吧?

# import the necessary packages

2 from sklearn.linear_model import LogisticRegression

3 from sklearn.model_selection import GridSearchCV

4 from sklearn.metrics import classification_report

5 import argparse

6 import pickle

7 import h5py

第2-7行导入所需的Python包。GridSearchCV类将用于帮助我们将参数转换为我们的logistic回归分类器。训练后,我们将使用pickle将我们的logistic回归模型序列化到磁盘。最后,h5py将被使用,这样我们就可以与HDF5特征数据集进行接口。

我们的脚本需要两个命令行参数,后面跟着第三个可选参数:1。——db:包含我们提取的特征和类标签的HDF5数据集的路径。2. ——model:这里我们为我们的输出逻辑回归分类器提供路径。3.——jobs:一个可选的整数,用于在运行网格搜索时指定并发作业的数量,以便将超参数调优到Logistic回归模型。 

正如我在本章前面提到的,在将相关的图像/特征向量写入HDF5数据集之前,我们有意地变换了图像路径——原因在第22行和第23行中变得很清楚。

考虑到我们的数据集太大,无法装入内存,我们需要一种有效的方法来确定我们的训练和测试分割。因为我们知道HDF5数据集中有多少条目(我们知道我们想要使用75%的数据进行训练,25%的数据进行评估),所以我们可以简单地将75%的索引i计算到数据库中。索引i之前的任何数据都被认为是训练数据——i之后的任何数据都是测试数据。

考虑到我们的训练和测试的分歧,让我们来训练我们的Logistic回归分类器:

索引i之后的任何东西都是测试集的一部分。即使我们的HDF5数据集驻留在磁盘上(而且太大了,无法装入内存),我们仍然可以把它当作NumPy数组来对待,这是在深度学习和机器学习任务中同时使用HDF5和h5py的巨大优势之一。

最后,我们将我们的LogisticRegression模型保存到磁盘并关闭数据库:

还请注意,这里没有与Animals、CALTECH-101或Flowers-17数据集相关的特定代码——只要我们的图像输入数据集符合上面3.2节中详细描述的目录结构,我们可以同时使用extract_feature .py和train_model.py来基于从cnn中提取的特征快速构建鲁棒的图像分类器。你会问,有多强大?让结果来说话吧。

​​​​​​​3.3. 1 Animals数据集上的结果

获得了98%的精度。

​​​​​​​3.3.2 CALTECH-101数据集上的结果

获得了96%的精度。

​​​​​​​3.3.3 Flowers-17数据集上的结果

最后,让我们将VGG16特征应用到Flowers-17数据集,在此之前,即使使用数据增强,我们也很难打破71%的准确性:

这次我们达到了93%的分类准确率,远远高于之前的71%。显然,像VGG这样的网络能够执行迁移学习,将它们的鉴别特征编码成输出激活,我们可以使用这些输出激活来训练我们自己的自定义图像分类器。

​​​​​​​3.4 总结

在本章中,我们开始探索迁移学习,即使用预训练卷积神经网络来对原始训练内容之外的类标签进行分类的概念。一般来说,在深度学习和计算机视觉中进行迁移学习的方法有两种:

  1. 把网络当作特征提取器,向前传播图像直到给定的层,然后把这些激活当作特征向量;
  2. 通过在网络前端添加一组全新的全连接层,并调整这些FC层来识别新的类(同时仍然使用相同的底层convfilter),从而对网络进行微调;

在本章中,我们严格关注了迁移学习的特征提取组件,证明了深度cnn,如VGG、Inception和ResNet能够作为强大的特征提取机器,甚至比手工设计的算法(如HOG[14]、SIFT[15]和Local Binary Patterns[16])更强大。仅举几个例子。每当使用深度学习和卷积神经网络处理一个新问题时,总是要考虑应用特征提取是否能够获得合理的准确性——如果是这样,您可以完全跳过网络训练过程,为您节省大量的时间、精力和头痛。

我们将在第8章中通过我的最佳途径来应用深度学习技术,如特征提取、微调和从头开始训练。在那之前,让我们继续研究迁移学习。

4.理解等级1和等级5的准确性

在我们深入讨论高级深度学习主题(如迁移学习)之前,让我们首先后退一步,讨论秩1、秩5和秩n的准确性概念。在阅读深度学习文献时,特别是在计算机视觉和图像分类空间中,您很可能会遇到排序精度的概念。例如,几乎所有在ImageNet数据集上评估了机器学习方法的论文都以1级和5级精度的形式呈现了他们的结果(我们将在本章的后面找到为什么1级和5级精度都被报告)。

到底什么是1级和5级精度?它们与传统的准确性(即精确度)有何不同?在本章中,我们将讨论排序的准确性,学习如何实现它,然后将其应用于在Flowers-17和CALTECH-101数据集上训练的机器学习模型。

4.1 排序准确性

//截止到2022.1.21晚上23:06

P51页

//2022.1.22 日晚上21:08开始阅读

排名准确度最好的解释是在一个例子。假设我们正在评估一个在CIFAR-10数据集上训练的神经网络,它包括10类:飞机、汽车、鸟,猫,鹿,狗,青蛙,马,船,卡车。对于下面的输入图像(图4.1,左),我们要求我们的神经网络计算每个类别标签的概率-神经网络然后返回表4.1(左)中列出的类别标签的概率。

概率最大的类标签是frog(97.3%),确实是正确的预测。如果我们重复这个过程:

  1. 步骤1:计算数据集中每个输入图像的类标签概率;
  2. 步骤2:确定ground-truth标签是否等于具有最大概率的预测类标签;
  3. 步骤3:计算步骤2正确的次数;

我们将达到我们的第一精确度。因此,排名1的准确性是指最高预测与地面真相标签匹配的预测所占的百分比——这是我们习惯于计算的“标准”准确性类型:将正确预测的总数除以数据集中的数据点数。

然后我们可以将这个概念扩展到5级精度。我们关注的不是排名第一的预测,而是排名前五的预测。我们的评估过程变成:

  1. 计算数据集中每个输入图像的类标签概率。
  2. 将预测的类标签概率按降序排序,这样概率较高的标签就会放在列表的前面。
  3. 确定ground-truth标签是否存在于步骤2的前5个预测标签中。
  4. 记录第三步正确的次数。

排名5只是排名1准确性的扩展:我们将考虑来自网络的排名前5的预测,而不是只关心来自分类器的排名1的预测。例如,让我们再次考虑一个输入图像,它将根据任意神经网络被归类到CIFAR-10类别(图4.1,右)。通过我们的网络后,我们得到了表4.1(右)中详细的类标签概率。

我们的形象显然是一辆汽车;然而,我们的网络报告卡车是最高的预测-这将被认为是一个不正确的预测,排名1的准确性。但如果我们检查网络中排名前五的预测,我们会发现汽车实际上是排名第二的预测,这在计算精度排名第五时是准确的。该方法也可以很容易地扩展到任意秩n精度;然而,我们通常只计算第1级和第5级精度——这就产生了一个问题,为什么要计算第5级精度呢?

对于CIFAR-10数据集,计算5级精度有点愚蠢,但对于大型数据集,挑战性数据集,尤其是细粒度的分类,看看CNN的前5个预测通常很有帮助。Szegedy et al.[17]是我们计算1级和5级精度的最好例子,我们可以在图4.2中看到左边是一只西伯利亚哈士奇犬,右边是一只爱斯基摩犬。大多数人都无法分辨出这两种动物之间的区别;然而,这两个类都是ImageNet数据集中的有效标签。

当处理包含许多具有相似特征的等级标签的大型数据集时,我们经常检查等级5的准确性作为等级1准确性的扩展,以了解我们的网络是如何运行的。在理想的情况下,我们的排名1的准确度会以与排名5的准确度相同的速度增长,但在具有挑战性的数据集上,情况并非总是如此。

因此,我们也检查了排名5的准确性,以确保我们的网络在以后的时代仍在“学习”。在训练接近尾声的时候,排名1的准确率可能会停滞不前,但排名5的准确率会随着我们的网络学习到更多的鉴别特征而不断提高(但没有足够的鉴别性来超过排名第一的预测)。最后,根据图像分类的要求(ImageNet是典型的例子),需要同时报告等级1和等级5的精度。

​​​​​​​4.1.1 测量rank-1和rank-5精度

//2022.1.23日上午 11:05开始阅读

1级和5级精度的计算可以通过建立一个简单的效用函数来完成。在我们的pyimagesearch模块中,我们将通过添加一个名为rank .py的文件,将这个功能添加到utils子模块中:

代码如下:

# 导入需要的包
import numpy as np

def rank5_accuracy(preds, labels):
    # 初始化rank1和rank5精确度
    rank1 = 0
    rank5 = 0

    # 循环预测和ground_truth
    for(p, gt) in zip(preds, labels):
        # 降序排序
        p = np.argsort(p)[::-1]

        # 检查是否和groud-truth框重合在top5之间
        if gt in p[:5]:
            rank5 += 1

        if gt == p[0]:
            rank1 += 1

        # 计算最终的rank-1和rank-5准确率
        rank1 /= float(len(labels))
        rank5 /= float(len(labels))

        # 返回结果
        return rank1, rank5

    ​​​​​​​

4.1.2 实现ranked 精确度

为了演示如何计算一个数据集的rank-1和rank-5的精度,让我们回到第3章,我们在ImageNet数据集上使用了一个预先训练的卷积神经网络作为特征提取器。基于这些提取的特征,我们对数据训练了一个Logistic回归分类器,并对模型进行了评估。我们现在会将我们的准确度报告扩展到包括5级准确度。

当我们计算Logistic回归模型的1级和5级精度时,请记住,任何机器学习,神经网络,或者深度学习模型——都可以计算1级和5级精度,在深度学习社区之外,这两个指标都很常见。说了这么多,打开一个新文件,命名为rank_accuracy.py,并插入以下代码:

第2-5行导入所需的Python包。我们将使用新定义的rank5_accuracy函数分别计算预测的第1和第5位精度。pickle包用于从磁盘加载我们的经过训练的scikit-learn分类器。最后,h5py将用于与我们的HDF5数据库的接口,这些特征是从我们的CNN在第三章中提取的。

我们的脚本将需要两个参数:——db,这是我们提取特征的HDF5数据库的路径,模型,我们的预先训练的逻辑回归分类器的路径。

下一个代码块处理从磁盘加载预训练模型,以及确定训练和测试分割到HDF5数据集的索引,假设75%的数据用于训练,25%的数据用于测试:

所有代码:

# 导入需要的库
from pyimagesearch.utils.ranked import rank5_accuracy
import argparse
import pickle
import h5py


# 构建命令行参数
ap = argparse.ArgumentParser()
ap.add_argument("-d", "--db", required=True, help="path HDF5 database")
ap.add_argument("-m", "--model", required=True, help="path to pre-trained model")
args = vars(ap.parse_args())



# 加载预训练模型
print("[INFO] loading pre-trained model...")
model = pickle.loads(open(args["model"], "rb").read())


# 打开HDF5数据集读取数据
db = h5py.File(args["db"], "r")
i = int(db["labels"].shape[0] * 0.75)



# 最后计算rank1和rank5精度
print("[INFO] predicting...")
preds = model.predict_proba(db["features"][i:])
(rank1, rank5) = rank5_accuracy(preds, db["labels"][i:])

# 显示两种精确度
print("[INFO] rank-1:{:.2f}%".format(rank1 * 100))
print("[INFO] rank-5:{:.2f}%".format(rank5 * 100))


# 关闭数据库
db.close()

第28行计算测试集中每个数据点的每个类别标签的概率。根据预测的概率和测试数据的ground-truth标签,我们可以计算第29行上的排序精度。然后,第32行和第33行分别向我们的终端显示秩-1和秩-5。

请注意,我们已经对这个例子进行了编码,这样它就可以与第三章中的任何例子一起工作,在第三章中,我们从CNN中提取特征,然后在这些特征的基础上训练scikit-learn模型。稍后在练习者包和ImageNet包中,我们也将计算卷积神经网络从头开始训练的秩1和秩5的精度。

​​​​​​​4.1.3 在Flowers-17数据集上的精确度排名

在Flowers-17数据集上,使用从VGG16体系结构中提取的特征训练的Logistic回归分类器,我们获得了92.06%的rank-1准确率。检查5级准确度,我们发现我们的分类器几乎是完美的,达到了99.41%的5级准确度。

4.1.4 ​​​​​​​精确度排名在CALTECH-101数据集上的测试

略。

​​​​​​​4.2 总结

在本章中,我们回顾了rank-1和rank-5准确性的概念。1级精度是ground-truth标签与类标签的概率最大的次数。等级5的准确性在等级1的准确性上扩展,允许它更“宽松”一些——在这里,我们计算等级5的准确性作为我们的ground-truth标签出现在前5个预测等级标签中概率最大的次数。

我们通常报告在大型的、具有挑战性的数据集(如ImageNet)上的准确性为5级,在这些数据集上,甚至连人类都很难正确地对图像进行标记。在本例中,如果ground-truth标签仅仅存在于前5个预测中,那么我们将认为我们的模型的预测是“正确的”。正如我们在Starter Bundle的第9章中所讨论的,一个真正具有良好泛化能力的网络将在其前5个概率中产生上下文相似的预测。

最后,请记住,rank-1和rank-5的准确性并不是深度学习和图像分类所特有的——你也会经常在其他分类任务中看到这些指标。

5.微调网络

在第三章中,我们学习了如何将预训练卷积神经网络作为特征提取器。利用该特征提取器,我们通过网络转发图像数据集,提取给定层的激活值,并将值保存到磁盘。标准的机器学习分类器(在本例中是Logistic回归)在CNN特征之上进行训练,就像我们使用手工设计的特征,如SIFT[15]、HOG[14]、lbp[16]等一样。这种CNN特征提取方法,被称为迁移学习,获得了显著的准确性,远高于我们之前在animal, CALTECH-101,或Flowers-17数据集上的任何实验。

但是还有另一种类型的迁移学习,如果你有足够的数据,它实际上可以比特征提取方法表现更好。这种方法被称为微调,需要我们进行“网络手术”。首先,我们拿起手术刀,从预先训练的卷积神经网络(如VGG、ResNet或Inception)中切断最后一组完全连接的层(即网络的“头”)。然后,我们用一组新的完全连接的随机初始化层替换头部。从那里,头部以下的所有层都被冻结,因此它们的权重不能被更新(即,反向传播中的向后传递不会到达它们)。

然后,我们使用非常小的学习率来训练网络,这样新的FC层就可以开始从网络中先前学习过的CONV层中学习模式。或者,我们可以解冻网络的其余部分并继续培训。应用微调可以让我们应用预先训练的网络来识别他们最初没有训练过的类别;此外,该方法比特征提取具有更高的准确率。

在本章的其余部分,我们将更详细地讨论微调,包括网络手术。最后,我们将提供一个将微调应用到Flowers-17数据集的例子,它的效果将超过我们在本书中尝试过的所有其他方法。

​​​​​​​5.1 迁移学习和微调

微调是迁移学习的一种。我们对已经在特定数据集上训练过的深度学习模型进行微调。通常,这些网络是最先进的架构,如在ImageNet数据集上训练过的VGG、ResNet和Inception。

正如我们在关于特征提取的第3章中所发现的,这些网络包含丰富的、有区别的过滤器,可以在它们已经训练过的数据集和类标签之外使用这些过滤器。然而,我们不是简单地应用特征提取,而是要进行网络手术,修改实际的架构,这样我们就可以重新训练部分网络。

如果这听起来像是一部糟糕的恐怖电影;别担心,不会有血淋淋的场面——但我们会从中得到乐趣,并从实验中学到很多东西。为了理解微调是如何工作的,请考虑图5.1(左),其中我们有VGG16网络的层。正如我们所知,最后一组层(即“头”)是我们的softmax分类器和完全连接的层。在进行微调时,我们实际上是从网络中去除头部,就像特征提取(中间)一样。然而,与特征提取不同的是,当我们进行微调时,我们实际上构建了一个新的完全连接的头部,并将其放在原来的结构上(右图)。

在大多数情况下,你的新FC头将比原来的参数更少;然而,这实际上取决于您的特定数据集。新的FC头被随机初始化(就像新网络中的任何其他层一样),并连接到原始网络的主体,我们已经准备好训练。

然而,有一个问题-我们的CONV层已经学习了丰富的,有区别的过滤器,而我们的FC层是全新的,完全随机的。如果我们允许梯度从这些随机值反向传播到整个网络,我们就有可能破坏它们强大的特征。为了规避这个问题,我们让我们的FC头部“热身”,方法是(具有讽刺意味的)“冻结”网络体内的所有层(我告诉过你,这里的尸体类比很好),如图5.2(左)所示。

左:当我们开始微调过程时,我们冻结网络中的所有CONV层,只允许梯度反向传播通过FC层。这样做可以让我们的网络“热身”。右图:在FC层有机会热身之后,我们可以选择解冻网络中的所有层,并允许每个层也进行微调。

训练数据像我们通常做的那样通过网络向前传播;然而,反向传播在FC层之后停止,这允许这些层开始从高度区分的CONV层学习模式。在某些情况下,我们可能永远不会解冻网络的主体,因为我们新的FC头可能获得足够的准确性。然而,对于一些数据集来说,允许在微调过程中修改原始的CONV层通常是有利的(图5.2,右)。

在FC头部开始学习我们数据集中的模式后,暂停训练,解冻身体,然后继续训练,但学习率非常低——我们不希望极大地偏离我们的convfilter。然后允许训练继续进行,直到获得足够的准确性。

微调是一种超级强大的方法,可以从自定义数据集上的预先训练的cnn中获得图像分类器,在大多数情况下甚至比特征提取更强大。微调的缺点是需要更多的工作和你的选择在FC头参数扮演重要角色在网络精度——你不能严格依赖于正则化技术是你的网络已经pre-trained你不能偏离正规化已经执行的网络。

其次,对于小型数据集,让你的网络开始“学习”一个“冷”FC启动,可能是一个挑战。这就是为什么我们首先冻结网络主体。即使如此,通过热身阶段仍然是一个挑战,可能需要你使用除SGD之外的优化器(见第7章)。虽然微调需要更多的努力,如果它做得正确,你几乎总是可以享受更高的准确性。

​​​​​​​5.1.1 索引和层

在进行网络手术之前,我们需要知道给定深度学习模型中每一层的层名和索引。我们需要这些信息,因为我们将被要求“冻结”和“解冻”某些层在一个预先训练的CNN。如果不提前知道图层名称和索引,我们就会“盲目切割”,就像一个失控的外科医生没有任何游戏计划。如果我们花几分钟来检查网络架构和实现,我们就可以更好地为手术做准备。

让我们来看看VGG16中的图层名称和索引。打开一个新文件,命名为inspect_model.py,并插入以下代码:

代码如下:

# 导入需要的库
from keras.applications import VGG16
import argparse


# 构建命令行参数
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--include-top", type=int, default=1, help="whether or not to include top of CNN")
args = vars(ap.parse_args())

# load the VGG16 network
print("[INFO] loading network...")
model = VGG16(weights="imagenet", include_top=args["include_top"] > 0)


print("[INFO] showing layers...")
# for循环显示网络中的每一层
for (i, layer) in enumerate(model.layers):
    print("[INFO] {}\t{}".format(i, layer.__class__.__name__))

 

第2行导入了我们的VGG16 Keras实现,我们将检查并为手术做准备的网络。第6-9行解析我们的命令行参数。这里需要一个单独的交换机——include-top,它用于指示是否应该将网络的头包括在模型摘要中。

第23和24行使用来自磁盘的预先训练的ImageNet权重加载VGG16——网络的头部是可选的。最后,第19和20行允许我们研究我们的模型。

对于网络中的每一层,我们打印其相应的索引i。有了这些信息,我们就知道FC头从哪里开始的索引(以及在哪里用新的FC头替换它)。

要了解VGG16的架构,只需执行以下命令:

注意,网络中的最后一层现在是POOL层(就像在第3章中描述的特征提取一样)。这个网络的主体将作为微调的起点。

​​​​​​​5.1.2 在网络中删除原来自带的FC层

在我们可以替换一个经过训练的CNN的头之前,我们需要用一些东西来替换它——因此,我们需要定义我们自己的完全连接的网络的头。首先,在nn中创建一个名为fcheadnet.py的新文件。pyimagesearch的Conv子模块:

设置的根据baseModel构建新的包含自定医德FC层的类:

class FCHeadNet:
    @staticmethod
    def build(baseModel, classes, D):
        # 初始化头部模型,它将被放置在基础之上,然后添加一个FC层
        headModel = baseModel.output
        headModel = Flatten(name="flatten")
        headModel = Dense(D, activation="relu")
        headModel = Dropout(0.5)(headModel)

        # 增加一个softmax层
        headModel = Dense(classes, activation="softmax")

        # 返回模型
        return headModel

正如在前面的网络实现中一样,我们定义了负责构建实际网络体系结构的构建方法。此方法需要三个参数:baseModel(网络主体)、数据集中的类总数,以及完全连接层中的节点数D。

//截止到2022.1.23日下午16:09

截止到P64页

//2022.1.23日晚上20:08开始阅读

第11行初始化了headModel,它负责将我们的网络与body的其余部分baseModel.output连接起来。从那里12-17行构建了一个非常简单的全连接架构:

同样,这个完全连接的头部与VGG16的原始头部相比非常简单,VGG16由两套4,096个FC层组成。然而,对于大多数微调问题,您不需要复制原始的网络头,而是简化它,以便更容易进行微调——头中的参数越少,我们就越有可能正确地将网络调优到新的分类任务中。最后,第20行将新构造的FC头返回给调用函数。

我们将在下一节中看到,我们将通过网络手术用我们新定义的FCHeadNet替换VGG16的头部。

​​​​​​​5.1.3 从开始到结束的微调

第10行和第11行导入了网络需要的优化器,以便从输入数据中学习模式。我们已经非常熟悉SGD,但我们还没有涉及到RMSprop -我们将把高级优化技术的讨论留到第7章,但暂时只需要理解RMSprop经常用于我们需要快速获得合理性能的情况下(就像我们试图“热身”一组FC层时的情况一样)。

正如我在第2章中提到的,在几乎所有的情况下,你都应该应用数据增强,因为它很少伤害准确性,而且常常有助于提高准确性和避免过拟合。当我们可能没有足够的数据从头开始训练一个深度CNN时,进行微调也是如此。

下一个代码块处理从磁盘抓取imagepath,并解析文件路径中的classNames:

假设使用的数据集的路径如下:

第80行初始化RMSprop优化器,我们将在第7章详细讨论这个算法。注意我们是如何使用1e - 3的小学习率来热身FC头部的。在应用微调时,您几乎总是使用比用于训练网络的原始学习速率小一个数量级(如果不是多个数量级的话)的学习速率。

然后,第88-91行使用我们的数据增强方法训练新的FC头。再次,请记住,虽然每个图像被完全向前传播,梯度只是部分backpropagated - FC层反向传播结束后,我们的目标是只“热身”的头,而不是改变网络的权重在体内。在这里,我们允许热身阶段训练25个epoch。通常情况下,根据你的数据集,你可以让自己的FC头在10-30个小时内热身。

在热身阶段之后,我们将暂停来评估测试集中的网络性能:

然后训练过FC头部之后,解冻CONV的其他层,作为整体继续训练

将body中的给定层设置为可训练的是将参数.trainable设置为True的一个例子。在某些情况下,你会希望整个身体都是可训练的;然而,对于具有许多参数的更深层的架构,如VGG,我建议只冻结顶层的CONV层,然后继续培训。如果分类精度继续提高(没有过度拟合),您可能想要考虑解冻身体中的更多层。

在这一点上,我们应该有一个温暖的训练开始,所以我们将切换到SGD(同样具有较小的学习率)并继续训练:

为了使对模型的修改生效,我们需要重新编译105 #的模型,这次使用的是具有*非常*小学习率的SGD:

请注意,我们在热身阶段的第一个epoch的初始精度是如何非常低的(≈36%)。这一结果是由于我们新头部的FC层是随机初始化的,并且仍然试图从之前训练的卷积滤波器中学习模式。然而,准确率迅速上升——到第10纪元,我们的分类准确率超过80%,到第25纪元结束时,我们已经达到了近90%的准确率。

现在我们的FCHeadNet已经获得了一个温暖的开始,我们切换到SGD,解冻主体中的第一组CONV层,允许网络再训练100个时代。准确率不断提高,一直到95%的分类准确率,高于我们使用特征提取获得的93%:

通过在VGG16中执行更积极的数据增强和不断解冻越来越多的CONV块,可以获得更高的精度。虽然微调肯定比特征提取工作更多,但它也使我们能够调整和修改CNN中特定数据集的权重——这是特征提取不允许的。因此,当有足够的训练数据时,考虑应用微调,因为你可能会获得比简单的特征提取更高的分类精度。

​​​​​​​5.2 总结

在本章中,我们讨论了第二种迁移学习,微调。微调的工作原理是用一个新的、随机初始化的网络头替换完全连接的网络头。当我们训练新的FC层时,原始网络主体中的层被冻结。

一旦我们的网络开始获得合理的精度,这意味着俱乐部层已经开始学习模式(1)底层的训练数据和(2)以前训练CONV过滤网络,早些时候我们解冻部分(如果不是全部)的身体,然后可以继续训练。

应用微调是一项非常强大的技术,因为我们不需要从头开始训练整个网络。相反,我们可以利用现有的网络架构,例如在ImageNet数据集上训练的最先进的模型,该数据集由丰富的、有区别的集过滤器组成。使用这些过滤器,我们可以“跳跃开始”我们的学习,允许我们执行网络手术,这最终导致一个更高精度的转移学习模型,比从头开始训练更少的努力(和头痛)。

要了解更多迁移学习和微调的实际例子,请参阅ImageNet Bundle,我在那里演示了如何:

  1. 识别车辆的制造商和型号。
  2. 自动识别和纠正图片的方向。

上述实验代码详见本人github地址:

GitHub - TheWangYang/Code_For_Deep_Learning_for_Computer_Vision_with_Python: A code repository for Deep Learning for Computer Vision with Python.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wyypersist

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值