《Deep Learning for Computer Vision withPython》阅读笔记-StarterBundle(第13 - 17章)

13.保存和加载你的模型

在我们的最后一章中,您学习了如何使用Keras库训练您的第一个卷积神经网络。但是,您可能已经注意到,每当您想要评估您的网络或在一组图像上测试它时,首先需要对它进行训练,然后才能进行任何类型的评估。这个要求可能相当麻烦。

我们只是在一个小数据集上使用一个浅层网络,这样可以相对较快地训练它,但是如果我们的网络是深的,我们需要在一个更大的数据集上训练它,因此需要花费许多小时甚至几天的时间来训练它,那该怎么办呢?我们是否需要每次都投入这么多的时间和资源来训练我们的网络?或者有一种方法可以在训练完成后将我们的模型保存到磁盘,然后简单地从磁盘加载,当我们想要分类新的图像?

肯定有办法的。保存和加载经过训练的模型的过程称为模型序列化,这是本章的主要主题。

13.1 将一个模型序列化到磁盘

使用Keras库,模型序列化就像调用模型一样简单。保存一个训练过的模型,然后通过load_model函数加载它。在本章的第一部分中,我们将修改上一章的浅网训练脚本,在动物数据集上训练后将网络序列化。然后,我们将创建第二个Python脚本,演示如何从磁盘加载序列化的模型。

save方法获取优化器的权重和状态,并将它们以HDF5格式序列化到磁盘上。在下一节中我们将看到,从磁盘加载这些权重与保存它们一样简单。

你会看到一个名为shallownet_weights的文件。Hdf5 -这个文件是我们的串行网络。下一步是获取这个已保存的网络并从磁盘加载它。

13.2 从磁盘加载一个预训练的模型

现在我们已经训练了我们的模型并序列化了它,我们需要从磁盘上加载它。作为模型序列化的一个实际应用,我将演示如何对Animals数据集中的单个图像进行分类,然后将分类后的图像显示到屏幕上。

//截止到2022.1.17日晚上17:29

P216页

//2022.1.17日晚上21:29

13.2 从磁盘加载一个预训练模型

现在我们已经训练了我们的模型并序列化了它,我们需要从磁盘上加载它。作为模型序列化的一个实际应用,我将演示如何对Animals数据集中的单个图像进行分类,然后将分类后的图像显示到屏幕上。

我们首先导入所需的Python包。第2-4行导入了用于构建标准管道的类,该管道将图像的大小调整为固定大小,将其转换为Keras兼容数组,然后使用这些预处理程序将整个图像数据集加载到内存中。

实际用于从磁盘加载训练过的模型的函数是第5行上的load_model。这个函数负责接收到我们训练过的网络(一个hdf5文件)的路径,解码hdf5文件中的权重和优化器,并在我们的架构中设置权重,这样我们就可以(1)继续训练或(2)使用网络来分类新的图像。

我们也将在第9行导入OpenCV绑定,这样我们就可以在图像上绘制分类标签,并将它们显示在屏幕上。

这10个图像中的每一个都需要进行预处理,所以让我们初始化我们的预处理处理器,并从磁盘中加载这10个图像:

请注意,我们是如何预处理图像的,就像我们在训练时预处理图像的方式一样。如果不这样做,就会导致错误的分类,因为网络将会呈现它无法识别的模式。始终要特别注意,以确保您的测试图像预处理的方式与您的训练图像相同。

接下来,让我们从磁盘加载已保存的网络:

加载我们的序列化网络就像调用load_model并提供模型的hdf5文件驻留在磁盘上的路径一样简单。

一旦模型被加载,我们就可以对我们的10张图像进行预测:

请记住,模型的.predict方法将返回数据中每个图像的概率列表—每个类标签分别有一个概率。在坐标轴上取argmax =1,为每个图像找到具有最大概率的类标签的索引。

现在,可视化上述的预测结果:

在第48行,我们开始循环10个随机采样的图像路径。对于每个图像,我们从磁盘加载它(第51行),并在图像本身上绘制类标签预测(第52和53行)。然后,输出图像在第54行和第55行显示到屏幕上。

13.3 总结

本章学到:

  1. 训练网络;
  2. 序列化网络权重并优化状态保存到磁盘;
  3. 加载训练网络并分类图片;

在第18章的后面,我们将发现如何在每个epoch之后将模型的权值保存到磁盘上,从而允许我们对网络进行“检查点”,并选择性能最好的一个。在实际训练过程中节省模型权值,也使我们能够在网络开始出现过拟合迹象时,从特定的点重新开始训练。停止培训,调整参数,然后再次重新开始培训的过程在实践者Bundle和ImageNet Bundle中有深入的介绍。

14.LeNet:识别手写数字

LeNet体系结构是深度学习社区的一项开创性工作,最初由LeCun等人在1998年的论文《Gradient-Based learning Applied to Document Recognition[19]》中介绍。正如论文的名字所暗示的,作者实现LeNet的动机主要是为了光学字符识别(OCR)。

LeNet体系结构简单且很小(就内存占用而言),这使得它非常适合教授cnn的基础知识。

在本章中,我们将试图重复LeCun在1998年论文中所做的类似实验。我们将首先回顾LeNet架构,然后使用Keras实现网络。最后,我们将评估在MNIST数据集上的LeNet手写数字识别。

14.1 LeNet架构

//截止到2022.1.17日晚上22:54

P221

//2022.1.18日上午11:53

 

LeNet架构(图14.1)是一个优秀的“真实世界”网络。这个网络很小,很容易理解——但又足够大,可以提供有趣的结果。

此外,LeNet + MNIST的组合能够很容易地在CPU上运行,使得初学者很容易迈出深度学习和cnn的第一步。在许多方面,LeNet + MNIST是应用于图像分类的深度学习的“Hello, World”。LeNet架构由以下几层组成,使用章节11.3中的CONV => ACT => POOL模式:

请注意LeNet体系结构是如何使用tanh激活函数而不是更流行的ReLU的。早在1998年,ReLU还没有被用于深度学习——更常见的是使用tanh或sigmoid作为激活函数。今天在实现LeNet时,通常会将TANH换成RELU——我们将遵循同样的原则,并在本章后面使用RELU作为激活函数。

表14.1总结了LeNet体系结构的参数。我们的输入层获取一个具有28行、28列和一个单一通道(灰度)的输入图像作为深度(即MNIST数据集内图像的尺寸)。然后我们学习了20个过滤器,每个都是5 × 5。CONV层之后是ReLU激活,然后是2 × 2大小和2 × 2步幅的最大池化。

建筑的下一个块遵循同样的模式,这次学习了50个5 × 5的过滤器。随着实际空间输入维度的减小,可以很容易地看到在网络的更深层次中,CONV层的数量增加。

然后我们有两个FC层。第一个FC包含500个隐藏节点,随后激活ReLU。最后的FC层控制输出类标签的数量(0-9;每一个可能的十位数)。最后,我们应用softmax激活来获得类概率。

14.2 实现LeNet

根据上面的表14.1,我们现在可以使用Keras库来实现这个重要的LeNet架构了。首先在pyimagessearch .nn.conv子模块中添加一个名为LeNet .py的新文件——这个文件将存储我们实际的LeNet实现:代码如下:

# 导入需要库
from keras.models import Sequential
from keras.layers.convolutional import Conv2D
from keras.layers.convolutional import MaxPooling2D
from keras.layers.core import Activation
from keras.layers.core import Flatten
from keras.layers.core import Dense
from keras import backend as K



# 定义实际的LeNet网络架构
class LeNet:
    @ staticmethod
    def build(width, height, depth, classes):
        # 初始化模型
        model = Sequential()
        inputShape = (height, width, depth)

        # 如果使用的顺序是通道有限,那么depth在第一个位置上
        if K.image_data_format() == "channel_first":
            inputShape = (depth, height, width)


        # 第一层的conv -> relu -> pool定义如下
        model.add(Conv2D(20, (5, 5), padding="same", input_shape=inputShape))
        model.add(Activation("relu"))
        model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2)))

        # 继续定义新的一层conv->relu->pool层如下
        model.add(Conv2D(50, (5,5), padding="same"))
        model.add(Activation("relu"))
        model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2)))

        # 然后输入量被压平,应用到一个包含500个节点的FC层
        model.add(Flatten())
        model.add(Dense(500))
        model.add(Activation("relu"))

        # 接下来是最终的分类器
        model.add(Dense(classes))
        model.add(Activation("softmax"))

        # 最后返回该网络架构
        return model

14.3 LeNet在MNIST数据集上的测试

代码如下:

# 导入需要包
import sys
sys.path.append(r"E:\pythonNeedSoftware\PyCharmWorkPlace\Deep_learning_For_CV_With_Python")

from pyimagesearch.nn.conv import LeNet
from keras.optimizers import SGD
from sklearn.preprocessing import LabelBinarizer
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from sklearn import datasets
from keras import backend as K
import matplotlib.pyplot as plt
import numpy as np



# 加载MNIST数据集
print("[INFO] accessing MNIST...")
dataset = datasets.fetch_openml("mnist_784", data_home="./datasets")
data = dataset.data

if K.image_data_format() == "channel_first":
    data = data.reshape(data.shape[0], 1, 28,28)
else:
    data = data.reshape(data.shape[0], 28, 28, 1)


# 将上述的各个图片像素缩放到0,1之间
(trainX, testX, trainY, testY) = \
    train_test_split(data / 255.0, dataset.target.astype("int"), test_size=0.25, random_state=42)


# 将标签从整数转换为向量
le = LabelBinarizer()
trainY = le.fit_transform(trainY)
testY = le.fit_transform(testY)


# 初始化优化器和模型
print("[INFO] compiling model...")
opt = SGD(lr=0.01)
model = LeNet.LeNet.build(width=28, height=28, depth=1, classes=10)
model.compile(loss="categorical_crossentropy", optimizer=opt, metrics=["accuracy"])

# 训练网络
print("[INFO] training work...")
H = model.fit(trainX, trainY, validation_data=(testX,testY), batch_size=128, epochs=20, verbose=1)


# evaluate the network
print("[INFO] evaluating network...")
predictions = model.predict(testX, batch_size=128)
print(classification_report(testY.argmax(axis=1),
predictions.argmax(axis=1),
target_names=[str(x) for x in le.classes_]))

# plot the training loss and accuracy
plt.style.use("ggplot")
plt.figure()
plt.plot(np.arange(0, 20), H.history["loss"], label="train_loss")
plt.plot(np.arange(0, 20), H.history["val_loss"], label="val_loss")
plt.plot(np.arange(0, 20), H.history["acc"], label="train_acc")
plt.plot(np.arange(0, 20), H.history["val_acc"], label="val_acc")
plt.title("Training Loss and Accuracy")
plt.xlabel("Epoch #")
plt.ylabel("Loss/Accuracy")
plt.legend()
plt.show()
 

使用我的Titan X GPU,我获得了3秒的时间。仅使用CPU,每epoch的秒数就上升到30秒。训练完成后,我们可以看到LeNet获得了98%的分类准确率,比第10章中使用标准前馈神经网络时的92%有了很大的提高。

这张图显示了LeNet在MNIST上的损失和准确性,这可以说是我们正在寻找的典型图表:训练和验证损失和准确性(几乎)完全模拟彼此,没有过度拟合的迹象。正如我们将看到的,通常很难获得这种表现良好的训练图,这表明我们的网络正在学习底层的模式,而没有过度拟合。

​​​​​​​14.4 总结

在本章中,我们探讨了LeCun等人在1998年的论文《Gradient-Based Learning Applied to Document Recognition[19]》中介绍的LeNet体系结构。LeNet是深度学习文献中的一项开创性工作——它彻底展示了如何训练神经网络以端到端方式识别图像中的对象(即,无需进行特征提取,网络能够从图像本身学习模式)。

虽然很有创意,但LeNet以今天的标准来看仍然被认为是一个“浅层”网络。由于只有四个可训练的层(两个CONV层和两个FC层),LeNet的深度与当前最先进的架构(如VGG(16和19层)和ResNet(100+层))的深度相比就相形见绌了。

在下一章中,我们将讨论VGGNet架构的一种变体,我称之为“MiniVGGNet”。这种体系结构的变化使用了与Simonyan和Zisserman的工作完全相同的指导原则[95],但减少了深度,使我们能够在更小的数据集上训练网络。关于VGGNet架构的完整实现,你需要参考ImageNet Bundle的第6章,在那里我们从ImageNet上从头开始训练VGGNet。

15.MiniVGGNet:deep cnn

在前一章中,我们讨论了LeNet,一种在深度学习和计算机视觉文献中开创性的卷积神经网络。VGGNet(有时简称为VGG)首先由Simonyan和Zisserman在2014年的论文《Very Deep Learning Convolutional Neural Networks for large Image Recognition》中提出[95]。他们工作的主要贡献是证明了一个带有非常小(3 × 3)过滤器的架构可以训练到越来越高的深度(16-19层),并在具有挑战性的ImageNet分类挑战中获得最先进的分类。

之前,深度学习文献中的网络架构使用了混合滤波器尺寸:

CNN的第一层通常包括大小在7 × 7[94]和11 × 11[128]之间的过滤器。从那时起,过滤器的尺寸逐渐减少到5 × 5。最后,只有最深层的网络才使用3 × 3的过滤器。

VGGNet的独特之处在于它在整个架构中使用了3 × 3内核。这些小内核的使用可以证明是帮助VGGNet推广到网络最初训练之外的分类问题的原因(当我们讨论转移学习时,我们将在实践者Bundle和ImageNet Bundle中看到这一点)。

任何时候你看到一个完全由3 × 3过滤器组成的网络架构,你可以放心,它是受到VGGNet的启发。回顾VGGNet的全部16层和19层变体对于这个卷积神经网络的介绍来说太先进了-关于VGG16和VGG19的详细回顾,请参阅ImageNet Bundle的第11章。

相反,我们将回顾VGG网络家族,并定义CNN必须具备哪些特征才能适应这个家族。从那里,我们将实现一个较小版本的VGGNet,称为MiniVGGNet,可以很容易地在您的系统上训练。这个实现还将演示如何使用我们在第11章中讨论的两个重要层——批处理标准化(BN)和退出。

​​​​​​​15.1 VGG网络家族

卷积神经网络的VGG家族可以由两个关键组成部分来表征:

  1. 网络中的所有CONV层只使用3 × 3滤波器。
  2. 在应用POOL操作之前叠加多个CONV => RELU层集(其中连续CONV => RELU层的数量通常会随着深度的增加而增加)。

在本节中,我们将讨论VGGNet架构的一种变体,我将其称为“MiniVGGNet”,因为该网络实际上比它的老大哥更浅。关于Simonyan和Zisserman提出的原始VGG架构的详细回顾和实现,以及如何在ImageNet数据集上训练网络的演示,请参阅ImageNet Bundle的第11章。

​​​​​​​15.1.1 VGGNet (Mini)架构

在ShallowNet和LeNet中,我们都应用了一系列CONV => RELU =>池层。然而,在VGGNet中,我们在应用单个POOL层之前,先堆叠多个CONV => RELU层。这样做可以让网络在通过POOL操作降低空间输入大小之前,从CONV层学习更多丰富的特征。

总体来说,MiniVGGNet由两组CONV => RELU => CONV => RELU => POOL层和一组FC => RELU => FC => SOFTMAX层组成。前两个CONV层将学习32个过滤器,每个大小为3 × 3。第二个CONV层将学习64个过滤器,同样,每个大小为3 × 3。我们的POOL层将以2 × 2的步幅在2 × 2的窗口上执行最大池化。我们还将在激活之后插入批处理规范化层,并在POOL和FC层之后插入dropout层(DO)。

网络结构本身在表15.1中有详细说明,其中初始输入图像大小假设为32 × 32 × 3,因为我们将在本章后面的CIFAR-10上训练MiniVGGNet(然后比较性能与浅网)。

再次注意,批处理规范化和退出层是如何根据我在第11.3.2节中的“经验法则”包含在网络体系结构中的。批量归一化有助于减少过拟合的影响,提高CIFAR-10的分类精度。

 ​​​​​​​

15.2 实现MiniVGGNet

根据表15.1中MiniVGGNet的描述,我们现在可以使用Keras来实现网络架构。首先,在pyimagessearch .nn.conv子模块中添加一个名为MiniVGGNet .py的新文件——我们将在这里编写MiniVGGNet实现:

第19行引入了一个我们以前没有见过的变量chanDim,它是通道维度的索引。批量归一化对通道进行操作,所以为了应用BN,我们需要知道要归一化的轴是哪个。设置chanDim = -1意味着通道维度的索引在输入形状中最后(即通道最后排序)。然而,如果我们使用通道优先排序(第23-25行),我们需要更新inputShape并设置chanDim = 1,因为通道维度现在是输入形状的第一个条目。

这里我们可以看到我们的架构由(CONV => RELU => BN) * 2 => POOL => DO组成。第28行定义了一个CONV层,有32个滤镜,每个滤镜大小为3 × 3。然后,我们应用一个ReLU激活(第30行),它立即被输入到batchnormation层(第31行),使激活处于零中心。

然而,我们并没有使用POOL层来降低我们输入的空间维度,而是使用了另一组CONV => RELU => BN——这允许我们的网络学习更丰富的特征,这是在训练更深入的cnn时常见的做法。

在第35行,我们使用MaxPooling2D,其大小为2 × 2。因为我们没有明确设置一个步幅,Keras隐式地假设我们的步幅等于最大池大小(即2 × 2)。

然后,我们在第36行应用Dropout,其概率为p = 0.25,这意味着在训练期间,来自POOL层的节点将随机与下一层断开连接,其概率为25%。我们使用dropout来帮助减少过拟合的影响。您可以在第11.2.7节中阅读更多关于dropout的内容。然后我们将第二层块添加到下面的MiniVGGNet:

上面的代码遵循与上面完全相同的模式;然而,现在我们学习的是两组64个滤波器(每个大小为3 × 3),而不是32个滤波器。同样,随着网络中空间输入尺寸的减小,增加过滤器的数量是很常见的。

我们的FC层有512个节点,接下来是ReLU激活和BN。我们也将在这里应用dropout,增加概率到50% -通常你会看到dropout p = 0.5应用在FC层之间。

最后,我们应用softmax分类器,将网络结构返回给调用函数:

​​​​​​​15.3 MiniVGGNet在CIFAR_10数据集上的应用

第2行导入了matplotlib库,稍后我们将使用它来绘制我们的准确性和随时间的损失。我们需要将matplotlib后端设置为Agg,以指示创建一个只保存到磁盘的非交互式文件。根据默认的maplotlib后端是什么以及是否远程访问深度学习机器(例如,通过SSH), X11会话可能会超时。如果发生这种情况,当matplotlib试图显示你的图形时,它将出错。相反,我们可以简单地将背景设置为Agg,并在完成对网络的训练后将情节写入磁盘。

我们将使用SGD作为优化器,学习速率为α = 0.1,动量项为γ = 0.9。设置nestrov=True表示我们希望将nestrov加速梯度应用于SGD优化器(章节9.3)。

一个我们还没见过的优化术语是衰减参数。这个参数用于随着时间的推移慢慢降低学习速率。我们将在下一章学习速率调度器中详细讨论,降低学习率有助于减少过拟合和获得更高的分类精度-学习率越小,权值更新就越小。衰减的一个常见设置是将初始学习速率除以总的epoch数——在本例中,我们将以0.01的初始学习速率训练我们的网络,总共训练40个epoch,因此衰减= 0.01 / 40。

在评估MinIVGGNet时,我进行了两个实验:1。一个是批量标准化。2. 没有批处理规范化的。

​​​​​​​15.3.1 包含有BN的实验结果

;略

​​​​​​​15.3.2 没有包含BN的实验结果

左:MiniVGGNet在CIFAR-10上进行批处理归一化训练。右:MiniVGGNet在CIFAR-10上训练,没有批量归一化。采用批量归一化可以获得更高的分类精度,并减少过拟合的影响。

注意没有进行批量归一化的MiniVGGNet的损失在epoch 30之后开始增加,这表明网络对训练数据过拟合。我们还可以清楚地看到,到第25纪元,验证的准确性已经相当饱和。

另一方面,具有批处理规范化的MiniVGGNet实现更加稳定。虽然丢失和准确性在第35期之后开始趋于平坦,但我们并没有过度拟合得那么糟糕——这就是我为什么建议在您自己的网络架构中应用批处理规范化的众多原因之一。

​​​​​​​15.4 总结

在本章中,我们讨论了卷积神经网络的VGG家族。CNN可以被认为是VGG-net的条件,如:

  1. 无论网络深度如何,它只使用3 × 3个过滤器;
  2. 在单个POOL操作之前会应用多个CONV => RELU层,有时随着网络深度的增加会有更多CONV => RELU层相互叠加;

然后我们实现了一个基于VGG的网络,命名为MiniVGGNet。该网络架构由两组(CONV => RELU) * 2) => POOL层和FC => RELU => FC => SOFTMAX层组成。我们还在每次激活后应用了批处理规范化,在每个池和全连接层后应用了dropout。为了评估MiniVGGNet,我们使用了CIFAR-10数据集。

我们之前对CIFAR-10的最佳准确度仅为60%,来自于浅网网络(第12章)。然而,使用MiniVGGNet,我们能够将准确度提高到83%。

结论:

  1. 批量归一化可以获得更快、更稳定的收敛速度和更高的精度;
  2. 然而,这些优势将以训练时间为代价——批处理归一化将需要更多的“墙时间”来训练网络,尽管网络将在更短的时间内获得更高的精度;

也就是说,额外的培训时间往往超过了负面影响,我强烈建议您将批处理规范化应用到您自己的网络架构中。

16. 学习速率调度器

在我们的最后一章中,我们在CIFAR-10数据集上训练了MiniVGGNet架构。为了帮助减轻过拟合的影响,我在使用SGD训练网络时引入了在学习速率上增加衰减的概念。

在本章中,我们将讨论学习率计划的概念,有时也称为学习率退火或自适应学习率。通过在一个时代到另一个时代的基础上调整我们的学习速度,我们可以减少损失,提高准确性,甚至在某些情况下,减少训练一个网络所需的总时间。

​​​​​​​16.1 降低我们的学习速度

最简单和最繁重的学习率调度程序是那些随着时间的推移逐渐降低学习率的程序。要考虑为什么学习率计划是一种有助于提高模型精度的有趣方法,请考虑9.1.6节中的标准权重更新公式:

回想一下,学习率α控制着我们沿着梯度前进的“步长”。较大的α值意味着我们正在采取较大的步骤,而较小的α值将使微小的步骤-如果α是零,网络根本不能采取任何步骤(因为梯度乘以零是零)。

在本书前面的例子中,我们的学习速率是恒定的-我们通常设置α ={0.1, 0.01},然后在不改变学习速率的情况下训练网络以固定的epoch数。这种方法在某些情况下可能很有效,但随着时间的推移,它通常有助于降低我们的学习速度。

在训练我们的网络时,我们试图在我们的损失景观中找到一些位置,使网络获得合理的准确性。它不一定是一个全局极小值,甚至也不一定是一个局部极小值,但在实践中,只要找到一个损失程度相当低的区域就“足够好”了。

如果我们一直保持较高的学习率,我们可能会超过这些低损耗的区域,因为我们将采取太大的步骤进入这些区域。相反,我们能做的是降低我们的学习率,从而允许我们的网络采取更小的步骤——这个降低的速率使我们的网络下降到“更优”的损失区域,否则就会被我们更大的学习率完全错过。

因此,我们可以将学习率调度的过程看作:

  1. 在训练过程的早期找到一组合理的“好”权重,以获得较高的学习率;
  2. 稍后在过程中调整这些权重,以使用较小的学习率找到更优的权重;

你可能会遇到两种主要类型的学习率调度程序:

  1. 基于epoches数逐渐减少的学习率调度程序(如线性、多项式或指数函数);
  2. 基于特定epoch(例如分段函数)的学习率调度程序。在本章中,我们将回顾这两种学习速率调度器;

​​​​​​​16.1.1 Keras的标准衰变时间表

Keras库附带了一个基于时间的学习速率调度器——它是通过优化器类(如SGD)的衰减参数来控制的。

回到我们之前的章节,让我们看看初始化SGD和MiniVGGNet的代码块:

在内部,Keras应用以下学习速率计划来调整每个时代后的学习速率:

如果我们将衰减设置为0 (Keras优化器的默认值,除非我们明确提供它),我们会注意到这对学习速率没有影响(这里我们任意设置当前epoch e为e = 1来说明这一点):

但是如果我们使用衰减= 0.01 / 40,您会注意到学习率在每个epoch之后开始下降(表16.1)。

使用这种基于时间的学习率衰减,我们的MiniVGGNet模型获得了83%的分类精度,如第15章所示。我鼓励您在SGD优化器中设置decay=0,然后重新进行实验。可以注意到,该网络还获得了≈83%的分类精度;然而,通过研究图16.1中两个模型的学习图,您会注意到,当验证损失上升到第25个纪元(左)时,开始出现过拟合。

这一结果与我们设置衰减=0.01 / 40(正确)时的结果相反,并获得了更好的学习图(更不用说,更高的准确性)。通过使用学习率衰减,我们不仅可以提高分类精度,而且可以减少过拟合的影响,从而提高模型的泛化能力。

 ​​​​​​​16.1.2 基于步进的衰减

另一种流行的学习速率调度器是基于步进的衰减,在训练的特定阶段之后,我们系统地降低学习速率。步进衰减学习速率调度器可以被认为是一个分段函数,如图16.2所示。在这里,学习速率在若干个时期内保持不变,然后下降,再次保持不变,然后再次下降,等等。

当将步长衰减应用于我们的学习率时,我们有两个选择:

  1. 定义一个方程来模拟我们希望达到的学习速率的分段下降;
  2. 使用我称之为ctrl + c的方法来训练一个深度学习网络,在这个网络中,我们以给定的学习速率训练了一些时间,最终注意到验证性能已经停止,然后ctrl + c停止脚本,调整我们的学习速率,并继续训练;

在本章中,我们将主要关注基于方程的分段学习速率调度。ctrl + c方法更先进,通常应用于使用更深层次神经网络的更大的数据集,在这些数据集中,获得合理的精度所需的确切时间是未知的。我在本书的实践者包和ImageNet包中详细介绍了ctrl + c训练。

当应用步进衰减时,我们经常在每个固定数目的时间后将学习速率降低(1)一半或(2)一个数量级。例如,假设我们的初始学习率是α = 0.1。10个epoch之后,我们将学习速率降至α = 0.05。在另一个10个训练epoch(即第20个总epoch)之后,α再次下降0.5,这样α = 0.025,以此类推。事实上,这与上面图16.2(红线)中绘制的学习速度计划是完全相同的。蓝线显示的下降幅度更大,为0.25。

​​​​​​​16.1.3 在Keras中实现自定义学习速率计划

Keras库为我们提供了一个LearningRateScheduler类,它允许我们定义一个自定义的学习速率函数,然后在训练过程中自动应用它。这个函数应该以epoches数作为参数,然后根据我们定义的函数计算我们期望的学习率。

在这个例子中,我们将定义一个分段函数,它将使学习速率下降一个在每个D epoch之后的某个因子F。因此,我们的方程是这样的:

其中α i为初始学习速率,F为控制学习速率下降速率的因子值,D为drop every epoch的值,E为当前epoch。因子F越大,学习速率衰减越慢。相反,因子F越小,学习率下降得越快。

使用python编码:

让我们继续实现这个自定义的学习速率计划,然后将它应用到CIFAR-10上的MiniVGGNet。打开一个新文件,命名为cifar10_lr_decay.py,让我们开始编码:

第16行定义了step_decay函数,它接受一个必需的参数——当前epoch。然后我们定义初始学习速率(0.01),下降因子(0.25),设置dropEvery = 5,这意味着我们将每5个epoch降低我们的学习速率0.25倍(第19-21行)。

我们在第24行使用上面的公式16.3计算当前epoch的新学习率。这个新的学习率在第27行返回给调用函数,允许Keras内部更新优化器的学习率。

中间省略了一部分使用自定义学习率的代码解释。

​​​​​​​16.2 总结

本章的目的是回顾学习率调度器的概念,以及如何使用它们来提高分类的准确性。我们讨论了两种主要类型的学习率调度程序:

  1. 基于时间的调度程序,基于epoch数逐渐减少。

2. 基于删除的调度程序基于特定的epoch进行删除,类似于分段函数的行为。

具体应该使用哪个学习率调度器(如果您应该使用调度器的话)是实验过程的一部分。通常你的第一个实验不会使用任何类型的衰减或学习速率调度,所以你可以获得一个基线精度和损失/精度曲线。

从这里,您可以引入Keras提供的基于时间的标准时间表(经验法则是decay = alpha_init / epochs),并运行第二个实验来评估结果。接下来的几个实验可能涉及将基于时间的时间表替换为使用各种掉落因素的基于掉落的时间表。

取决于挑战你的分类数据集是随着网络的深度,你可能选择ctrl + c方法培训详细的医生包,ImageNet包方法最深度学习实践者当ImageNet数据集训练网络。

总之,准备好花大量的时间训练你的网络,评估不同的参数和学习惯例。即使是简单的数据集和项目也可能需要10到100次的实验来获得高精度的模型。

在学习深度学习的这一点上,你应该明白,训练深度神经网络部分是科学,部分是艺术。在这本书中,我的目标是为您提供培训网络背后的科学,以及我使用的常见的经验法则,以便您可以学习它背后的“艺术”——但请记住,没有什么比实际运行自己的实验更好的了。

你在训练神经网络方面做得越多,记录下哪些有用哪些没用的结果,你就会做得越好。掌握这门艺术是没有捷径的——你需要投入大量时间,熟悉SGD优化器(和其他工具)及其参数。

17.定位欠拟合和过拟合

我们在第9章中简要地谈到过拟合和过拟合。现在我们将深入探讨深度学习中的欠拟合和过拟合。帮助我们理解underfitting和过度拟合的概念,我将提供大量的图表和数字,这样你可以与自己的培训/精度损失曲线——这种做法尤其有用,如果这本书是你首要接触机器学习/深度学习和你没有发现underfitting /过度拟合。

从这里,我们将讨论如何为Keras创建一个(接近)实时的培训监视器,您可以使用它来照看您的网络的培训过程。到目前为止,我们必须等到我们的网络完成训练后,才能绘制出训练损失和准确性。

等到训练过程结束才将损失和准确性可视化是一种计算浪费,特别是我们的实验需要很长时间来运行,我们没有办法想象损失/在训练过程中精度本身(除了看原始的终端输出),我们可以花几个小时甚至几天训练网络时没有意识到,这个过程应该是停止后第一个几个时代。

相反,如果我们能够在每个阶段之后绘制出训练和损失,并将结果可视化,将会更加有益。从那里我们可以做出更好、更明智的决定,决定我们是应该提前终止实验还是继续训练。

​​​​​​​17.1 欠拟合和过拟合

当训练你自己的神经网络时,你需要高度关注欠拟合和过拟合。当你的模型不能在训练集上获得足够低的损耗时,就会发生欠拟合。在这种情况下,您的模型无法了解您的训练数据中的底层模式。在频谱的另一端,我们有过拟合,你的网络对训练数据建模得太好,不能推广到你的验证数据。

因此,在训练机器学习模型时,我们的目标是:

  1. 尽量减少训练损失;
  2. 在确保培训和测试之间的差距的同时,损失是相当小的;

可以通过调整神经网络的容量来控制模型是否可能过拟合。我们可以通过向我们的网络添加更多的层和神经元来增加容量。类似地,我们可以通过去除层次和神经元并应用正则化技术(权重衰减、退出、数据增加、早期停止等)来降低容量。

下面的图17.1(受Goodfellow等人图5.3的优秀例子启发,第112页[10]页)可以将欠拟合和过拟合与模型容量的关系可视化:

模型容量与损耗的关系。垂直的紫色线将最佳容量与欠拟合(左)和过拟合(右)分开。当我们拟合不足时,保持泛化差距。当训练和泛化的损失水平都超出时,就会出现最优容量。当泛化损失增加时,我们是过拟合的。注:图灵感来自Goodfellow等人,第112页[10]。

在x轴上,我们画出了网络的容量,在y轴上,我们画出了损耗,损耗越低越好。通常,当一个网络开始训练时,我们处于“欠拟合区”(图17.1,左)。在这一点上,我们只是试图学习底层数据中的一些初始模式,并将权重从它们的随机初始化转移到使我们能够实际“学习”数据本身的值上。理想情况下,训练损失和验证损失都会在这段时间同时下降——这种下降表明我们的网络实际上是在学习。

然而,随着我们的模型容量的增加(由于更深层次的网络,更多的神经元,没有规则化等),我们将达到网络的“最佳容量”。在这一点上,我们的训练和验证损失/准确性开始彼此偏离,并开始形成一个明显的差距。我们的目标是限制这一差距,从而保持我们模型的通用性。

如果我们不能限制这个间隙,我们就进入了“过拟合区域”(图17.1,右)。在这一点上,我们的训练损失将停滞或继续下降,而我们的验证损失则停滞并最终增加。验证损失在一系列连续时期的增加是过度拟合的一个重要指标。

避免过拟合的技巧:

  1. 减少模型的复杂性,选择一个更浅的网络,更少的层和神经元;
  2. 运用正则化方法;

使用较小的神经网络可能适用于较小的数据集,但一般来说,这不是首选的解决方案。相反,我们应该应用正则化技术,如权重衰减、丢失、数据增加等。在实践中,使用正则化技术来控制过拟合几乎总是比你的网络大小更好[129],除非你有很好的理由相信你的网络架构对于这个问题来说太大了。

​​​​​​​17.1.1 学习速率的影响

在前一节中,我们看了一个过拟合的例子——但是我们的学习率在过拟合中扮演了什么角色呢?是否有一种方法可以简单地通过检查损耗曲线来确定我们的模型是否过拟合了一组超参数?当然有。以图17.2为例(深受Karpathy等人[93]的启发)。

在x轴上,我们绘制了神经网络的时代,并在y轴上绘制了相应的损失。理想情况下,我们的训练损失和验证损失应该看起来像绿色曲线,在这条曲线上,损失下降得很快,但不会太快,以至于我们无法导航我们的损失景观,并进入一个低损失的区域。

此外,当我们同时绘制训练和验证损失时,我们可以更详细地了解训练进度。更好的是,我们的训练和验证损失将几乎相互模仿,只有很小的训练损失和验证损失之间的差距,表明有很小的过拟合。

然而,在许多真实世界的应用程序中,完全相同的、模仿的行为是不实际的,甚至是不可取的,因为它可能意味着需要很长时间来训练我们的模型。因此,我们只需要“注意培训和验证损失之间的差距”。只要差距没有急剧增加,我们就知道存在一个可接受的过拟合水平。然而,如果我们不能保持这个差距,并且训练和验证损失严重偏离,那么我们就知道我们面临过拟合的风险。一旦验证损失开始增加,我们就知道我们是强过拟合。

​​​​​​​17.1.2 注意你的训练曲线

在训练自己的神经网络时,要注意训练数据和验证数据的损失和准确性曲线。在最初的几个阶段,神经网络似乎跟踪得很好,可能稍微不太合适——但这种模式可以很快改变,你可能会开始看到训练和验证损失的分歧。当这种情况发生时,评估你的模型:

  1. 是否使用了正则化技术;
  2. 学习率是否太高;
  3. 网络是否太深;

训练深度学习网络是一门科学,也是一门艺术。学习如何阅读这些曲线的最好方法是训练尽可能多的网络,并检查它们的图。随着时间的推移,你会逐渐明白什么是可行的,什么是不可行的——但是不要指望第一次尝试就能“成功”。即使是经验最丰富的深度学习从业者也会进行10到100次的实验,检查过程中的损失/准确性曲线,注意什么可行,什么不可行,并最终锁定一个可行的解决方案。

最后,您还应该接受这样一个事实,即对某些数据集进行过拟合是不可避免的。例如,模型很容易与CIFAR-10数据集过拟合。如果你的训练损失和验证损失开始出现分歧,不要恐慌——只要尽可能地控制差距就好。

还要意识到,随着您在以后的时代中降低学习速率(例如使用学习速率调度程序时),也会变得更容易过拟合。这一点将在实践者Bundle和ImageNet Bundle的更高级章节中变得更清楚。

​​​​​​​17.1.3 如果验证损失低于培训损失呢?

原因:

  1. 训练数据全部都是最困难的数据集例子;
  2. 而您的验证数据由“容易”的数据点组成;

然而,除非您有意以这种方式对数据进行采样,否则随机训练和测试分割不太可能整齐地分割这些类型的数据点。

第二个原因是数据增强。我们在实践者Bundle中详细介绍了数据增强,但要点是,在训练过程中,我们通过对训练图像应用随机变换(如平移、旋转、调整大小和剪切)来随机改变训练图像。由于这些变化,网络不断增强训练数据的例子,这是一种正规化,使网络更好地推广验证数据虽然可能在训练集上执行糟糕(见9.4章正规化的更多细节)。

第三个原因可能是你训练“不够努力”。你可能要考虑提高你的学习速度和调整你的正则化力量。

​​​​​​​17.2 监督训练过程

在本节的第一部分中,我们将创建一个TrainingMonitor回调函数,当使用Keras进行网络训练时,它将在每个epoch的末尾被调用。这个监视器将把训练集和验证集的损失和准确性串行化到磁盘,然后构造数据图。

在训练期间应用这种回调将使我们能够照顾训练过程和及早发现过拟合,允许我们中止实验并继续尝试调整参数。

​​​​​​​17.2.1 创造一个训练监视器

TrainingMonitor类的构造函数在第9行定义。构造函数需要一个参数,后面跟着两个可选参数:

FigurePath:输出图的路径,我们可以用它来可视化损失和精度随时间的变化。

•jsonPath:一个可选的路径,用于将丢失值和准确性值序列化为JSONfile。如果您想使用培训历史来创建您自己的自定义场景,这个路径是有用的。

•startAt:当使用ctrl + c训练时,这是恢复训练的开始时间。我们在实践者包中介绍了ctrl + c训练,所以我们现在可以忽略这个变量。

接下来,我们来定义on_train_begin回调函数,顾名思义,它会在训练过程开始时调用一次:

在第19行,我们定义了H,用来表示损失的“历史”。我们将在下一个代码块中看到如何在on_epoch_end函数中更新这个字典。

第22行检查是否提供了JSON路径。如果是,那么我们检查这个JSONfile是否存在。如果JSONfile确实存在,我们将加载它的内容并更新历史字典H,直到开始阶段(因为我们将从开始阶段开始继续训练)。

现在我们来看看最重要的函数on_epoch_end,它在训练epoch完成时被调用:

on_epoch_end方法自动提供给Keras的参数。第一个是表示历元数的整数。第二个是字典,日志,包含当前epoch的训练和验证损失+准确性。我们循环日志中的每个条目,然后更新历史字典(第37-40行)。

这段代码执行后,字典H现在有四个键:1。train_loss 2。train_acc 3。val_loss 4。val_acc

我们为每个键维护一个值列表。每个表都在每个历元结束时更新,从而使我们能够在历元结束时绘制更新的损失和精度曲线。

在提供jsonPath的情况下,我们将历史记录H序列化到磁盘:

def on_epoch_end(self, epoch, logs={}):
    # 循环logs记录,更新Loss,accuracy等等。
    # 对于整个训练过程
    for (k, v) in logs.items():
        l = self.H.get(k, [])
        l.append(v)
        self.H[k] = l


    # 看是否可以序列化到磁盘
    if self.jsonPath is not None:
        f = open(self.jsonPath, "w")
        f.write(json.dumps(self.H))
        f.close()


    # 绘制实时的loss和accuracy
    if len(self.H["loss"] > 1):
        N = np.arange(0, len(self.H["loss"]))
        plt.style.use("ggplot")
        plt.figure()
        plt.plot(N, self.H["loss"], label="train_loss")
        plt.plot(N, self.H["val_loss"], label="val_loss")
        plt.plot(N, self.H["acc"], label="train_acc")
        plt.plot(N, self.H["val_acc"], label="val_acc")
        plt.title("Training Loss and Accuracy [Epoch {}]".format(len(self.H["loss"])))
        plt.xlabel("Epoch #")
        plt.ylabel("Loss/Accuracy")
        plt.legend()
        plt.savefig(self.figPath)
        plt.close()

​​​​​​​17.2.2 监控训练过程

我喜欢使用操作系统分配的进程ID来命名我的plot和JSONfiles。如果我注意到培训进行得很糟糕,我可以简单地打开我的任务管理器并杀死与我的脚本相关联的进程ID。如果您同时进行多个实验,这种能力尤其有用。第21行只是在屏幕上显示进程ID。

然后,我们执行我们的标准管道,加载CIFAR-10数据集,并准备用于培训的数据+标签:

# construct the set of callbacks

figPath = os.path.sep.join([args["output"], "{}.png".format(os.getpid())])
jsonPath = os.path.sep.join([args["output"], "{}.json".format(os.getpid())])
callbacks = [TrainingMonitor(figPath, jsonPath=jsonPath)]

# train the network
print("[INFO] training network...")
model.fit(trainX, trainY, validation_data=(testX, testY),
batch_size=64, epochs=100, callbacks=callbacks, verbose=1)

第47-50行分别初始化了输出图和JSON序列化文件的路径。请注意,每个文件路径都包含进程ID,这使我们能够轻松地将实验与进程ID关联起来——在实验进展不顺利的情况下,我们可以使用任务管理器终止脚本。给定图形和JSON路径,第51行构建了我们的回调列表,其中包含一个单一的条目,即TrainingMonitor本身。

最后,55号线和56号线总共训练我们的网络100个时代。我故意把时代设定得很高,以鼓励我们的网络过度契合。

这是你的训练经历和学习情节。每个文件都以创建它们的进程ID命名。使用TrainingMonitor的好处是,我现在可以照看学习过程,并在每个阶段完成后监控训练。

如果我们让网络一直训练到第100纪元,过拟合只会变得更糟(图17.3,右下)。训练损失和验证损失之间的差距是巨大的,而验证损失还在继续增加。虽然该网络的验证精度在80%以上,但该模型的泛化能力很差。根据这些图,我们可以清楚地看到过拟合开始发生的时间和地点。当您自己进行实验时,请确保您使用TrainingMonitor来帮助您照看培训过程。

最后,当你开始认为存在过度拟合的迹象时,不要太高兴而放弃实验。让网络再训练10-15个时代,以确保你的预感是正确的没错,过度拟合正在发生——我们经常需要这些epoch的背景来帮助我们做出最后的决定。

我经常看到刚接触机器学习的深度学习实践者过早地启动和终止实验。等待,直到您看到过拟合的明显迹象,然后终止该过程。随着你的深度学习技能的磨练,你会发展出第六感来指导你训练你的人际网络,但在那之前,请相信额外时期的背景,使你能做出更好、更明智的决定。

​​​​​​​17.3 总结

在本章中,我们回顾了欠拟合和过拟合。当你的模型不能在训练集上获得足够低的损耗时,就会发生欠拟合。同时,当你的训练损失和验证损失之间的差距太大时,就会发生过拟合,表明网络对训练数据中的底层模式建模的能力太强。

不适应相对容易解决:只需在网络中添加更多的层/神经元。但过度拟合是一个完全不同的问题。当出现过拟合时,应考虑:

  1. 通过删除层/神经元来减少网络的容量(不推荐,除非是小数据集)。
  2. 应用更强的正则化技术。

在几乎所有的情况下,你应该首先尝试应用更强的正则化,而不是减少你的网络的规模-例外是,如果你试图在一个小数据集上训练一个大规模的深度网络。

在理解了模型容量与欠拟合和过拟合之间的关系之后,我们学会了如何监控我们的训练过程,并在发生过拟合时发现过拟合过程允许我们在早期停止网络训练,而不是浪费时间让网络过度适应。最后,我们通过看一些说明过拟合的例子来结束这一章。

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

TheWangYang/Code_For_Deep_Learning_for_Computer_Vision_with_Python: A code repository for Deep Learning for Computer Vision with Python. (github.com)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

wyypersist

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

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

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

打赏作者

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

抵扣说明:

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

余额充值