《Deep Learning for Computer Vision withPython》阅读笔记-StarterBundle(第8 - 9章)
作者:互联网
从本章开始将仅记录书中的重点内容翻译并记录,供日后复习使用
8. 参数化学习
本章的大部分灵感来自Andrej Karpathy在斯坦福cs231n课堂中出色的线性分类注释[74]。非常感谢卡帕西和其他的cs231n助教把这些可访问的笔记放在一起。
8.1线性分类简介
8.1.1参数化学习的四个组成部分
简单地说:参数化是定义给定模型的必要参数的过程。在机器学习的任务中,参数化涉及到根据四个关键组件定义问题:数据、评分函数、损失函数、权重和偏差。我们将在下面逐一回顾。
数据
这个组件是我们将要学习的输入数据。这些数据包括数据点(例如,来自图像的原始像素强度,提取的特征等)和它们相关的类标签。通常我们用多维设计矩阵[10]表示我们的数据。
设计矩阵中的每一行代表一个数据点,而矩阵的每一列(它本身可以是一个多维数组)对应一个不同的特征。例如,考虑一个RGB颜色空间中100张图像的数据集,每张图像的大小为32 × 32像素。该数据集的设计矩阵为X三角形数据集R100×(32×32×3),其中Xi定义了r中的第i张图像。使用这种表示法,X1是第一张图像,X2是第二张图像,以此类推。
除了设计矩阵,我们还定义了一个向量y,其中yi为数据集中的第i个示例提供了类标签。
得分函数
计分函数接受我们的数据作为输入,并将数据映射到类标签。例如,给定我们的一组输入图像,评分函数获取这些数据点,应用某个函数f(我们的评分函数),然后返回预测的类标签,类似于下面的伪代码:
损失函数
损失函数量化了我们预测的类别标签与基本事实标签的吻合程度。这两组标签之间的一致性水平越高,我们的损失就越低(并且我们的分类准确性就越高,至少在训练集上是这样)。
我们在训练机器学习模型时的目标是最小化损失函数,从而提高分类精度。
权重和偏差
权重矩阵,通常记作W和偏差向量b被称为我们要优化的分类器的权重或参数。根据我们的评分函数和损失函数的输出,我们将调整和摆弄权重和偏差的值,以提高分类准确性。
8.1.2线性分类:从图像到标签
首先,我们需要我们的数据。让我们假设我们的训练数据集表示为xi,其中每个图像都有一个相关的类标签yi。我们假设i = 1,…,N和yi = 1,…,K,这意味着我们有N个维数为D的数据点,分为K个独特的类别。
为了使这个想法更具体,考虑我们第7章中的“动物”数据集。在这个数据集中,我们总共有N = 3000幅图像。每幅图像为32 × 32像素,以RGB颜色空间表示。每张图像三个通道)。我们可以将每个图像表示为D = 32 × 32 × 3 = 3072个不同的值。最后,我们知道总共有K = 3个类标签:一个分别用于狗、猫和熊猫类。
给定这些变量,我们现在必须定义一个评分函数f,它将图像映射到类标签scores。一种实现这种得分的方法是通过一个简单的线性映射:
让我们假设每个xi被表示为一个形状为[D × 1]的列向量(在这个例子中,我们将把32 × 32 × 3的图像平展成一个包含3,072个整数的列表)。权重矩阵W的形状为[K × D](按输入图像的维数计算的类别标签的数量)。最后b,偏差向量的大小为[K × 1]。偏差向量允许我们在不影响权重矩阵W的情况下,向一个或另一个方向移动和转换我们的评分函数。偏差参数对于成功的学习通常是至关重要的。
回到Animals数据集示例,每个xi由一个包含3072个像素值的列表表示,因此xi的形状为[3072 × 1]。权矩阵W的形状为[3 × 3072],最后偏差向量b的大小为[3 × 1]。
图8.1为线性分类评分函数f。在左边,我们有我们的原始输入图像,表示为32 × 32 × 3图像。然后,我们通过获取3D数组并将其重塑为1D列表,将该图像平展成3072像素强度的列表。
我们的权重矩阵W包含三行(每个类标签对应一行)和3072列(图像中的每个像素对应一行)。在计算W和的点积之后,我们加上偏置向量b -结果就是我们实际的得分函数。我们的评分函数在右边产生三个值:分别与狗、猫和熊猫标签相关的分数。
不熟悉点乘的读者应该阅读这个快速简明的教程:http://pyimg.co/fgcvp。对于有兴趣深入学习线性代数的读者,我强烈推荐阅读Philip N. Klein的《通过应用于计算机科学来编码矩阵线性代数》[75]。
看看上面的图和方程,你可以确信输入xi和yi是固定的,不是我们可以修改的。当然,我们可以通过对输入图像应用不同的转换来获得不同的轴向坐标——但是一旦我们将图像传递给评分函数,这些值就不会改变。事实上,我们有控制的唯一参数(参数化的学习)是我们的权重矩阵W和偏差向量b。因此,我们的目标是利用我们的得分函数和损失函数优化(例如,以系统的方式修改)重量和偏见向量,这样我们的分类精度提高。
8.1.3参数化学习和线性分类的优点
参数学习的优势:
- 一旦我们完成了对模型的训练,我们就可以丢弃输入数据,只保留权重矩阵W和偏差向量b。这大大减少了模型的大小,因为我们需要存储两组向量(而不是整个训练集)。
- 分类新的测试数据是快速的。为了进行分类,我们所需要做的就是取W和xi的点积,然后加上偏差b(即应用我们的评分函数)。这样做比需要比较每个测试点和每个训练示例的k-NN算法要快得多。
8.1.4 Python中的简单线性分类器
python代码实现:
import numpy as np
import cv2
# 初始化类标签并设置伪随机的种子数字生成器,这样我们就可以重现我们的结果
labels = ["dog", "cat", "panda"]
np.random.seed(1)
# 初始化权重矩阵和偏差向量
# 第14行初始化权矩阵W,其值来自均匀分布的随机值,采样范围为[0,1]。这个权重矩阵有3行(每个类标签对应一行)和3072列(32 × 32 × 3图像中的每个像素对应一行)。
# 然后我们在第15行初始化偏置向量——这个向量也被随机填充为分布[0,1]上均匀采样的值。偏差向量有3行(对应于类标签的数量)和1列。
W = np.random.randn(3, 3072)
b = np.random.randn(3)
# 加载图像
# 第19行通过cv2.imread从磁盘加载图像。然后我们在第20行将图像的大小调整为32×32像素(忽略长宽比)——我们的图像现在被表示为(32,32,3)NumPy数组,我们将其平化为3072 -dim向量。
#
orig = cv2.imread("beagle.png")
image = cv2.resize(orig, (32, 32)).flatten()
# 计算分类标签的分数
scores = W.dot(image) + b
# 将每一个评分值写入终端
for (label, score) in zip(labels, scores):
print("[INFO] {} : {:.2f}".format(label, score))
cv2.putText(orig, "Label : {}".format(labels[np.argmax(scores)]), (10, 30),cv2.FONT_HERSHEY_SCRIPT_SIMPLEX,0.9,(0, 255, 0), 2)
cv2.imshow("Image",orig)
cv2.waitKey(0)
import numpy as np
import cv2
# 初始化类标签并设置伪随机的种子数字生成器,这样我们就可以重现我们的结果
labels = ["dog", "cat", "panda"]
np.random.seed(1)
# 初始化权重矩阵和偏差向量
# 第14行初始化权矩阵W,其值来自均匀分布的随机值,采样范围为[0,1]。这个权重矩阵有3行(每个类标签对应一行)和3072列(32 × 32 × 3图像中的每个像素对应一行)。
# 然后我们在第15行初始化偏置向量——这个向量也被随机填充为分布[0,1]上均匀采样的值。偏差向量有3行(对应于类标签的数量)和1列。
W = np.random.randn(3, 3072)
b = np.random.randn(3)
# 加载图像
# 第19行通过cv2.imread从磁盘加载图像。然后我们在第20行将图像的大小调整为32×32像素(忽略长宽比)——我们的图像现在被表示为(32,32,3)NumPy数组,我们将其平化为3072 -dim向量。
#
orig = cv2.imread("beagle.png")
image = cv2.resize(orig, (32, 32)).flatten()
# 计算分类标签的分数
scores = W.dot(image) + b
# 将每一个评分值写入终端
for (label, score) in zip(labels, scores):
print("[INFO] {} : {:.2f}".format(label, score))
cv2.putText(orig, "Label : {}".format(labels[np.argmax(scores)]), (10, 30),cv2.FONT_HERSHEY_SCRIPT_SIMPLEX,0.9,(0, 255, 0), 2)
cv2.imshow("Image",orig)
cv2.waitKey(0)
同样,请记住,这是一个有效的例子。我故意设置了Python脚本的随机状态,以生成W和b值,这些值将导致正确的分类(您可以在第8行更改伪随机种子值,亲自看看不同的随机初始化将如何产生不同的输出预测)。
在实践中,您永远不会初始化W和b值,并假定它们在没有某种学习过程的情况下会给您正确的分类。相反,当我们从头开始训练我们自己的机器学习模型时,我们需要通过一个优化算法来优化和学习W和b,比如梯度下降。
我们将讨论优化和梯度下降在接下来的一章,但与此同时,只是花时间,以确保你理解第24行以及一个线性分类器进行分类的点积权重矩阵和一个输入数据点,然后通过添加的偏见。因此,我们的整个模型可以通过两个值来定义:权重矩阵和偏差向量。当我们从零开始训练机器学习模型时,这种表示不仅紧凑,而且相当强大。
8.2 损失函数的作用
在上一节中,我们讨论了参数化学习的概念。这种类型的学习允许我们获取一组输入数据和类标签,并通过定义一组参数并对它们进行优化,实际学习一个将输入映射到输出预测的函数。
但为了通过评分函数真正“学习”从输入数据到类标签的映射,我们需要讨论两个重要的概念:
- 损失函数;
- 参数优化方法
8.2.1什么是损失函数
就目前而言,简单地理解损失函数可以用来量化我们的评分函数在分类输入数据点方面的表现。
理想情况下,当我们调整模型参数时,我们的损失应该随着时间的推移而减少。如图8.4所示,模型1的损失开始时略高于模型2,但随后迅速减少,并在CIFAR-10数据集上继续保持较低的水平。相反,模型2的损失最初有所减少,但很快就停滞了。在这个特定的示例中,模型#1实现了更低的总体损失,可能是用于从CIFAR-10数据集对其他图像进行分类的更理想的模型。我说“可能”是因为模型1有可能对训练数据过度拟合。我们将在第17章中介绍过拟合的概念以及如何发现它。
8.2.2多类SVM损失
多类SVM Loss(顾名名状)的灵感来自于(Linear) Support V ector Machines (SVM)[43],它使用一个评分函数f将我们的数据点映射到每个类别标签的数字分数。函数f是一个简单的学习映射:
现在我们有了评分函数,我们需要确定这个函数(给定权重矩阵W和偏差向量b)在进行预测时是“好”还是“坏”。为了做出这个决定,我们需要一个损失函数。
回想一下,在创建机器学习模型时,我们有一个设计矩阵X,其中X中的每一行都包含一个我们希望分类的数据点。在图像分类的背景下,X中的每一行都是一个图像,我们寻求正确的标签这个图像。我们可以通过语法xi访问X中的第i个图像。
类似地,我们还有一个向量y,它包含每个x的类标签。这些y值是我们的基本事实标签,以及我们希望评分函数能够正确预测的值。就像我们可以访问一个给定的图像xi一样,我们可以通过yi访问相关的类标签。
为了简单起见,我们将我们的评分函数缩写为s:
这意味着我们可以通过第i个数据点得到第j类的预测分数:
使用这个语法,我们可以把它们放在一起,得到铰链损失函数:
几乎所有的损失函数都包含一个正则化项。我现在跳过这个想法,因为我们将在第九章中回顾正则化,一旦我们更好地理解损失函数。
看看上面的铰链损耗方程,你可能会对它的实际作用感到困惑。实际上,hinge loss函数是对所有不正确的类(i6 = j)求和,并比较评分函数s为第j个类标签(不正确的类)和第i个类(正确的类)返回的输出。我们对零处的值应用最大值操作,这对于确保不对负值求和很重要。
当损失Li = 0时,给定的xi被正确分类(在下一节中我将提供一个数值示例)。为了得出整个训练集的损失,我们只需取每个个体的平均值。
你可能遇到的另一个相关的损失函数是铰链损失的平方:
平方项通过对输出的平方来惩罚我们的损失,这导致了错误预测中损失的二次增长(相对于线性增长)。
至于应该使用哪个丢失函数,这完全取决于您的数据集。标准铰链损失函数的使用较多,但在一些数据集上,平方变异可以获得较好的精度。总之,这是一个您应该考虑调优的超参数。
一个多类SVM损失实例
注意我们这里的方程是如何包含两个术语的——预测的狗的分数与猫和熊猫的分数之间的差异。同时观察一下“狗”的损失是如何为零的——这意味着狗的预测是正确的。对图8.5中的图像1的快速调查证明了这个结果是正确的:“狗”的得分大于“猫”和“熊猫”的得分。
类似地,我们可以计算图片#2的铰链损失,这张图片包含一只猫:
在这种情况下,我们的损失函数大于零,表明我们的预测是不正确的。看看我们的评分函数,我们看到我们的模型预测dog是得分为3.76的提议标签(因为这是得分最高的标签)。我们知道这个标签是不正确的——在第9章中,我们将学习如何自动调整权重来纠正这些预测。
同样,我们的损失是非零的,所以我们知道我们的预测是错误的。看看我们的评分函数,我们的模型错误地将这个图像标记为“猫”,而它应该是“熊猫”。
然后我们可以通过取平均值来得到这三个例子的总损失:
因此,给定我们的三个训练示例,对于参数W和b,总的铰链损耗为3.72。
还请注意,我们的损失为零,只有一个输入图像,这意味着我们的两个预测是不正确的。在下一章中,我们将学习如何优化W和b,通过使用损失函数来帮助引导我们走向正确的方向,从而做出更好的预测。
8.2.3交叉熵损失和Softmax分类器
重点:
虽然铰链丢失非常流行,但在深度学习和卷积神经网络的环境中,你更有可能遇到交叉熵丢失和Softmax分类器。
Softmax分类器给你每个类别标签的概率,而铰链损失给你的边际。
作为人类,我们更容易解释概率而不是边际得分。此外,对于像ImageNet这样的数据集,我们经常查看卷积神经网络的5级精度(我们检查ground-truth标签是否在给定输入图像的网络返回的前5个预测标签中)。看看(1)真实的类标签是否存在于前5个预测中,(2)与每个标签相关的概率是一个很好的属性。
理解熵损失
Softmax分类器是Logistic回归的二元形式的推广。就像在铰链损失或平方铰链损失中,我们的映射函数f的定义是,它取一个数据xi的输入集,并通过数据xi与权重矩阵W(为简洁起见省略了偏差项)的点积将它们映射到输出类标签:
然而,与铰链损失不同的是,我们可以将这些分数解释为每个类别标签的未归一化对数概率,这相当于用交叉熵损失交换铰链损失函数:
那么,我是如何来到这里的呢?让我们把函数拆开来看一下。首先,我们的loss函数应该最小化正确类的负对数可能性:
概率语句可以解释为:
我们使用标准的得分函数形式:
总的来说,这将生成单个数据点的最终损失函数,如上所示:
注意这里的对数实际上是以e为底(自然对数)因为我们之前求的是e的幂的逆。通过指数和进行的实际指数化和归一化是Softmax函数。负对数产生了实际的交叉熵损失。
就像铰链损失和平方铰链损失一样,计算整个数据集的交叉熵损失是通过取平均值:
我故意省略了损失函数中的正则化项。我们将回到正则化,解释它是什么,如何使用它,以及为什么它对神经网络和深度学习至关重要。如果上面的等式看起来很可怕,不要担心——我们将在下一节通过数值示例来确保您理解交叉熵损失是如何工作的。
解释:
第一个表分别包含三个类中每个类的得分函数f的输出。这些值是三个类的非标准化对数概率。让我们对得分函数(es,其中s是我们的得分函数值)的输出取幂,得到我们的非标准化概率(第二个表)。
下一步是取分母,对指数求和,然后除以总和,从而得到与每个类标签相关联的实际概率(第三张表)。注意概率总和是1。最后,我们可以取负自然对数- ln(p),其中p是归一化概率,产生最终的损失(第4张表,也是最后一张表)。
在这种情况下,我们的Softmax分类器能够以93.93%的信誉度正确地报告图像为熊猫。然后,我们可以对训练集中的所有图像重复这个过程,取平均值,得到训练集中的总体交叉熵损失。这个过程允许我们量化一组参数在训练集中表现的好坏。
我使用一个随机数生成器来获取这个特定示例的得分函数值。这些值只是用来演示Softmax分类器/交叉熵损失函数的计算是如何执行的。在现实中,这些值不会随机生成,取而代之的应该是你的得分函数f的输出基于参数W和b。我们将看到如何参数化学习的所有组件组合在一起在我们的下一章,但就目前而言,我们正在与示例数据来演示损失函数是如何工作的。
8.3 总结
在本章中,我们回顾了参数化学习的四个组成部分:
- 数据
- 评分函数
- 损失函数
- 权重和偏差
在图像分类的上下文中,我们的输入数据是我们的图像数据集。评分函数生成对给定输入图像的预测。然后损失函数量化一组预测对数据集的好坏程度。最后,权重矩阵和偏差向量使我们能够真正地从输入数据中“学习”——这些参数将通过优化方法进行调整和调整,以获得更高的分类精度。
然后我们回顾了两个流行的损失函数:铰链损失和交叉熵损失。虽然铰链损失在许多机器学习应用程序中使用(如svm),但我几乎可以绝对肯定地保证,您将看到更多的交叉熵损失,主要是因为Softmax分类器输出的是概率而不是边际。概率对于我们人类来说更容易解释,所以这个事实是交叉熵损失和Softmax分类器的一个特别好的特性。关于损失铰链损失和交叉熵损失的更多信息,请参考斯坦福大学的cs231n课程[57,74]。
在下一章中,我们将回顾用于调整权重矩阵和偏置向量的优化方法。优化方法允许我们的算法通过更新基于得分和损失函数输出的权重矩阵和偏差向量,从我们的输入数据中实际学习。使用这些技术,我们可以逐步获得更低的损耗和更高的精度的参数值。优化方法是现代神经网络和深度学习的基石,没有它们,我们将无法从输入数据中学习模式,所以请务必关注下一章。
重点:
9. 优化方法和正则化
“几乎所有的深度学习都是由一个非常重要的算法驱动的:随机梯度下降(SGD)”- Goodfellow等人[10]
有了这些构建模块,我们现在可以转移到机器学习、神经网络和深度学习最重要的方面——优化。优化算法是驱动神经网络的引擎,使它们能够从数据中学习模式。在整个讨论中,我们已经了解到,获得一个高精度的分类器依赖于找到一组权重W和b,以便我们的数据点被正确分类。
与其依赖于纯粹的随机性,我们需要定义一种优化算法,允许我们从字面上改进W和b。在本章中,我们将看看最常用的用于训练神经网络和深度学习模型的算法——梯度下降。梯度下降有许多变体(我们也会提到),但在每种情况下,其理念都是相同的:迭代评估你的参数,计算你的损失,然后朝着最小化损失的方向迈出一小步。
9.1梯度下降
两种形式:
- 标准的“香草”实现。
- 更常用的优化“随机”版本。
在本节中,我们将回顾基本的基本实现,为我们的理解形成一个基线。在我们理解了梯度下降的基本原理之后,我们将继续学习随机版本。然后,我们将回顾一些我们可以添加到梯度下降的“bells and whistles”,包括动量和Nesterov加速度。
9.1.1损失景观及优化面
梯度下降法是一种迭代优化算法,可在损失景观(也称为优化表面)上运行。标准梯度下降的例子是将我们的权值沿着x轴可视化,然后将给定的一组权值沿着y轴的损失可视化(图9.1,左):
整个损失景观中损失最小的局部最小值就是我们的全局最小值。在理想世界中,我们希望找到这个全局最小值,确保我们的参数具有最优的可能值。
问题就在这里——损失的景象是我们看不见的。我们不知道它长什么样。如果我们是一个优化算法,我们就会被盲目地放置在某个地方,不知道我们面前的风景是什么样子的,我们将不得不导航到损失最小的地方,而不意外地爬到局部最大值的顶部。
沿碗表面的每个位置对应于给定一组参数W(权重矩阵)和b(偏差向量)的特定损失值。我们的目标是尝试不同的W和b值,评估它们的损失,然后向更理想的损失更低的最优值迈进一步。
9.1.2梯度下降中的“梯度”
为了让我们更直观地解释梯度下降,让我们假设我们有一个机器人——让我们把它命名为Chad(图9.2,左)。当我们执行梯度下降时,我们会随机将Chad放置在我们的损失区域的某个地方。
查德该怎么办?答案是运用梯度下降法。查德要做的就是沿着W的斜率走。我们可以使用以下公式计算所有维度的梯度W:
在> 1维中,梯度变成了偏导数的向量。这个方程的问题是:
1. 这是梯度的近似值。2. 它是缓慢。
为了便于讨论,我们只需内化梯度下降是什么:尝试通过迭代过程,在损失最小化的方向上迈出一步,以优化我们的参数,实现低损失和高分类精度。
9.1.3把它当作一个凸问题(即使它不是)
使用图9.1(右)中的碗形图作为损失景观的可视化,也可以让我们在现代神经网络中得出一个重要的结论——我们将损失景观视为一个凸问题,即使它不是。如果某个函数F是凸的,那么所有的局部极小值也是全局极小值。这个想法很好地符合碗的可视化。我们的优化算法只需要在碗的顶部绑上一对滑雪板,然后慢慢地沿着斜坡向下滑行,直到到达底部。
问题是,几乎所有我们应用神经网络和深度学习算法解决的问题都不是简洁的凸函数。相反,在这个碗状区域内,我们会发现尖峰状的山峰,更类似于峡谷的山谷,陡峭的下降,甚至是损失急剧下降又急剧上升的槽。
“优化算法可能不能保证在合理的时间内达到局部最小值,但它通常会在足够快的时间内找到一个非常低的[损失]函数值,从而变得有用。”
在训练深度学习网络时,我们可以设定很高的期望,即找到局部/全局最小值,但这种期望很少与现实相符。相反,我们最终找到了一个低损耗的区域——这个区域甚至可能不是一个局部最小值,但在实践中,这已经足够了。
9.1.4偏差技巧
在我们继续执行梯度下降之前,我想花点时间讨论一种叫做“偏差技巧”的技术,一种将我们的权矩阵W和偏差向量b组合成一个单一参数的方法。回想一下我们之前的决定,我们的评分函数定义为:
在解释和实现方面,跟踪两个独立的变量通常很繁琐——为了完全避免这种情况,我们可以将W和b结合在一起。为了结合偏差和权重矩阵,我们在输入数据X中添加一个额外的维度(即列),它持有一个常量1——这是我们的偏差维度。
通常,我们要么将新维度附加到每个xi上作为第一个维度,要么将其作为最后一个维度。事实上,这并不重要。我们可以选择任意位置插入一列1,到我们设计的矩阵中,只要它存在。这样做可以让我们通过一个矩阵乘法来重写我们的评分函数:
同样,我们可以省略这里的b项因为它被嵌入到我们的权重矩阵中。在前面的“Animals”数据集示例中,我们使用了32 × 32 × 3的图像,总像素为3,072。每个xi都用一个向量[3072 × 1]表示。增加一个常数值为1的维度,现在将向量扩展为[3073 × 1]。同样,将偏差矩阵和权值矩阵结合起来,也将权值矩阵W扩展为[3 × 3073],而不是[3 × 3072]。通过这种方式,我们可以将偏差视为权值矩阵中的一个可学习参数,而不必明确地在一个单独的变量中跟踪它。
为了将偏差技巧可视化,请考虑图9.3(左),在图9.3中,我们将权重矩阵和偏差分开。到目前为止,这个图描述的是我们如何看待我们的评分函数。但是,我们可以将W和b组合在一起,只要我们在每个xi中插入一个新列,其中每个条目都是1(图9.3,右)。使用偏差技巧可以让我们只学习一个权重矩阵,因此我们倾向于使用这种方法来实现。对于本书以后的所有例子,当我提到W时,假设偏差向量b也隐含在权重矩阵中。
9.1.5 梯度下降的伪代码
This pseudocode is what all variations of gradient descent are built off of. We start off on Line
1 by looping until some condition is met, typically either:
- 已经经过了指定数量的epoch(这意味着我们的学习算法已经“看过”每个训练数据点N次)。
- 我们的损失已经变得足够低或训练精度满足高。
- 在随后的M时代,损失并没有得到改善。
然后,第2行调用一个名为evaluate_gradient的函数。这个函数需要三个参数:
- loss:一个函数,用来计算当前参数W和输入数据的损失。
- data:我们的训练数据,其中每个训练样本由一个图像(或特征向量)表示。
- W:我们正在优化的实际权重矩阵。我们的目标是应用梯度下降来找到一个产生最小损失的W。
evaluate_gradient函数返回一个K维的向量,其中K是图像/特征向量的维数。Wgradient变量是实际的梯度,每个维度都有一个梯度入口。
然后在直线3上应用梯度下降法。我们用Wgradient乘以α,这是我们的学习速率。学习速率控制着我们的步长。
在实践中,您将花费大量的时间来寻找α的最优值——它是您的模型中最重要的参数。如果α太大,你将花费所有的时间在损失的地形周围跳跃,永远不会真正“下降”到盆地的底部(除非你的随机弹跳纯粹是运气)。相反地,如果α太小,那么它将需要很多次迭代(可能非常多)才能到达盆地的底部。寻找α的最优值会让您非常头疼——您将花费大量的时间为您的模型和数据集的这个变量寻找最优值。
9.1.6使用Python实现基本梯度下降
现在我们已经了解了梯度下降的基本知识,让我们用Python实现它,并使用它对一些数据进行分类。打开一个新文件,命名为gradient_descent.py,并插入以下代码:
第2-7行导入所需的Python包。我们以前见过所有这些导入,除了make_blobs,它是一个用于创建正态分布数据点的“blobs”的函数——在从头开始测试或实现我们自己的模型时,这是一个方便的函数。
然后在第9行定义sigmoid_activation函数。当绘制时,这个函数将类似于“S”形曲线(图9.4)。我们称它为激活函数,因为该函数将根据输入x“激活”并触发“ON”(输出值> 0.5)或“OFF”(输出值<= 0.5)。
我们可以通过以下预测方法来定义这种关系:
13 def predict(X, W):
14 # take the dot product between our features and weight matrix
15 preds = sigmoid_activation(X.dot(W))
16
17 # apply a step function to threshold the outputs to binary
18 # class labels
19 preds[preds <= 0.5] = 0
20 preds[preds > 0] = 1
21
22 # return the predictions
23 return preds
给定一组输入数据点X和权重W,我们对它们调用sigmoid_activation函数来获得一组预测(第15行)。然后我们对预测进行阈值设置:任何值<= 0.5的预测被设置为0,而任何值> 0.5的预测被设置为1(第19和20行)。然后将预测返回给第23行上的调用函数。
虽然还有其他(更好的)替代s形激活函数的方法,但它为我们讨论神经网络、深度学习和基于梯度的优化提供了一个很好的起点。我将在Starter Bundle的第10章和实践者Bundle的第7章中讨论其他激活函数,但目前,只需记住,sigmoid是一个非线性激活函数,我们可以使用它来限制我们的预测。
接下来,让我们解析命令行参数:
25 # construct the argument parse and parse the arguments
26 ap = argparse.ArgumentParser()
27 ap.add_argument("-e", "--epochs", type=float, default=100,
28 help="# of epochs")
29 ap.add_argument("-a", "--alpha", type=float, default=0.01,
30 help="learning rate")
31 args = vars(ap.parse_args())
我们可以为脚本提供两个(可选的)命令行参数:
——epoch:我们使用梯度下降训练分类器时将使用的epoch的数量。
——alpha:梯度下降的学习速率。我们通常看到0.1、0.01和0.001作为初始学习率值,但这也是一个超参数,您需要根据自己的分类问题进行调整。
33 # generate a 2-class classification problem with 1,000 data points,
34 # where each data point is a 2D feature vector
35 (X, y) = make_blobs(n_samples=1000, n_features=2, centers=2,
36 cluster_std=1.5, random_state=1)
37 y = y.reshape((y.shape[0], 1))
38
39 # insert a column of 1’s as the last entry in the feature
40 # matrix -- this little trick allows us to treat the bias
41 # as a trainable parameter within the weight matrix
42 X = np.c_[X, np.ones((X.shape[0]))]
43
44 # partition the data into training and testing splits using 50% of
45 # the data for training and the remaining 50% for testing
46 (trainX, testX, trainY, testY) = train_test_split(X, y,
47 test_size=0.5, random_state=42)
在第35行,我们调用了make_blobs,它生成了分成两个类的1000个数据点。这些数据点是2D的,这意味着“特征向量”的长度是2。这些数据点的标签不是0就是1。我们的目标是训练一个能够正确预测每个数据点的类标签的分类器。
第42行应用了“偏差技巧”(上面详述了),它允许我们明确地跳过跟踪偏差向量b,通过在我们的设计矩阵x中插入一个全新的列1作为最后一个条目,在所有特征向量中添加一个包含恒定值的列,可以让我们将偏差视为权重矩阵W中的一个可训练参数,而不是一个完全独立的变量。
一旦我们插入了1列,我们就在第46行和第47行将数据划分为训练和测试分区,使用50%的数据用于训练,50%的数据用于测试。
我们的下一个代码块处理随机初始化我们的权重矩阵使用均匀分布,这样它有相同的维数作为我们的输入特征(包括偏差):
49 # initialize our weight matrix and list of losses
50 print("[INFO] training...")
51 W = np.random.randn(X.shape[1], 1)
52 losses = []
你也可能看到两个0和1重量初始化,但正如我们将在这本书的后面发现,良好的初始化是训练一个神经网络的关键在合理的时间内,所以随机初始化以及简单的启发式胜出在绝大多数情况下[84]。
第52行初始化一个列表,记录每个epoch之后的损失。在Python脚本的最后,我们将绘制损失(理想情况下,这种损失应该随着时间的推移而减少)。
请注意,零类在100%的情况下是如何正确分类的,而一个类在99%的情况下是如何正确分类的。这种差异的原因是香草梯度下降只对每个epoch执行一次权值更新——在这个例子中,我们训练我们的模型为100个epoch,所以只有100个更新发生。根据权重矩阵的初始化和学习率的大小,我们可能无法学习一个可以分离点的模型(即使它们是线性可分的)。
对于简单的梯度下降,你最好以更小的学习速率训练更多的epoch来帮助克服这个问题。然而,正如我们将在下一节中看到的,梯度下降的一个变体,称为随机梯度下降,对每批训练数据执行一个权值更新,这意味着每个epoch有多个权值更新。这种方法导致更快、更稳定的收敛。
9.2随机梯度下降法(SGD)
相反,我们应该应用随机梯度下降(SGD),这是对标准梯度下降算法的一个简单修改,该算法计算梯度,并在小批量的训练数据上更新权值矩阵W,而不是在整个训练集上。虽然这种修改导致了“更多噪声”的更新,但它也允许我们沿着梯度采取更多的步骤(每批处理一步,而不是每个epoch一步),最终导致更快的收敛,对损失和分类精度没有负面影响。
9.2.1 Mini-batch SGD
回顾普通的梯度下降算法,应该(多少)很明显,该方法在大型数据集上运行非常缓慢。之所以这么慢,是因为梯度下降的每次迭代都要求我们在更新权值矩阵之前,对训练数据中的每个训练点进行预测。对于像ImageNet这样的图像数据集,我们有超过120万的训练图像,这个计算可能需要很长时间。
结果还表明,在沿着我们的权重矩阵迈出一步之前,计算每个训练点的预测是一种计算浪费,并且对我们的模型覆盖几乎没有帮助。
while True:
2 batch = next_training_batch(data, 256)
3 Wgradient = evaluate_gradient(loss, batch, W)
4 W += -alpha * Wgradient
香草梯度下降和SGD之间的唯一区别是添加了next_training_batch函数。我们不是在整个数据集上计算梯度,而是对数据进行采样,产生一批数据。我们在批处理上评估梯度,并更新我们的权值矩阵w。从实现的角度来看,我们也尝试在应用SGD之前随机化我们的训练样本,因为算法对批处理很敏感。
看完SGD的伪代码后,您会立即注意到引入了一个新参数:批处理大小。在SGD的“纯粹”实现中,您的小批量大小将是1,这意味着我们将从训练集中随机抽样一个数据点,计算梯度,并更新我们的参数。然而,我们经常使用> 1的小批量。典型的批量大小包括32、64、128和256。
那么,为什么要使用批量> 1呢?首先,批量大小> 1有助于减少参数更新(http://pyimg.co/pd5w0)中的方差,从而实现更稳定的收敛。其次,2的幂通常是可取的批量大小,因为他们允许内部线性代数优化库更有效。
一般来说,迷你批处理大小不是一个超参数,您不应该太担心[57]。如果你使用GPU来训练你的神经网络,你要确定有多少训练例子适合你的GPU,然后使用2的最接近的幂作为批处理的大小,这样批处理将适合GPU。对于CPU训练,您通常使用上面列出的批处理大小之一,以确保您获得线性代数优化库的好处。
9.2.2实施小批量SGD
Next_batch函数:
25 def next_batch(X, y, batchSize):
26 # loop over our dataset ‘X‘ in mini-batches, yielding a tuple of
27 # the current batched data and labels
28 for i in np.arange(0, X.shape[0], batchSize):
29 yield (X[i:i + batchSize], y[i:i + batchSize])
next_batch方法需要三个参数:X:我们的特征向量/原始图像像素强度训练数据集。2. y:与每个训练数据点相关联的类标签。3.batchSize:将返回的每个小批的大小。
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
import numpy as np
import argparse
def sigmoid_activation(x):
# 对制定的一个input计算sigmoid激活函数的指定值
return 1.0 / (1 + np.exp(-x))
def predict(X, W):
# 取特征和权值矩阵之间的点积
preds = sigmoid_activation(X.dot(W))
# 应用一个阶跃函数来阈值输出到二进制分类标签
preds[preds <= 0.5] = 0
preds[preds > 0.5] = 1
return preds
def next_batch(X, y, batchSize):
# 以小批量循环我们的数据集' X ',生成一个当前batch数据集和标签的元组
for i in np.arange(0, X.shape[0], batchSize):
yield (X[i: i + batchSize], y[i: i + batchSize])
# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-e", "--epochs", type=float, default=100, help="# of epochs")
ap.add_argument("-a", "--alpha", type=float, default=0.01,help="learning rate")
ap.add_argument("-b", "--batch-size", type=int, default=32,help="size of SGD mini-batches")
args = vars(ap.parse_args())
# 生成一个包含1000个数据点的2类分类问题,每个数据点是一个2D特征向量
(X, y) = make_blobs(n_samples=1000, n_features=2, centers=2, cluster_std=1.5, random_state=1)
y = y.reshape((y.shape[0], 1))
# 插入一列1作为特性矩阵的最后一项,这个小技巧可以让我们处理偏差,作为权重矩阵中的可训练参数
X = np.c_[X, np.ones((X.shape[0]))]
# 将数据分割成训练和测试分割,使用50%数据用于训练,其余50%用于测试
(trainX,testX,trainY,testY) = train_test_split(X, y,test_size=0.5,random_state=42)
# 初始化我们的权值矩阵和损失列表
print("[INFO] training...")
W = np.random.randn(X.shape[1], 1)
losses = []
# 循环每一个epoch
for epoch in np.arange(0, args["epochs"]):
# 初始化最终的losses数组对每一个epoch
epochLoss = []
# 循环得到当前的Batch对应的数据集
for (batchX, batchY) in next_batch(X, y,args["batch_size"]):
# 使用当前的小批量数据集x点乘上权重矩阵
preds = sigmoid_activation(batchX.dot(W))
# 得到当前的偏差
error = preds - batchY
epochLoss.append(np.sum(error ** 2))
gradient = batchX.T.dot(error)
W += -args["alpha"] * gradient
# 得到每一个epoch中的loss的平均值,然后将loss的平均值放入到losses中
loss = np.average(epochLoss)
losses.append(loss)
if epoch == 0 or (epoch + 1) % 5 == 0:
print("[INFO] epoch={}, loss={:.7f}".format(int(epoch + 1), loss))
# 评估分类器
print("[INFO] evaluating...")
preds = predict(testX, W)
print(classification_report(testY, preds))
# 绘制(测试)分类数据
plt.style.use("ggplot")
plt.figure()
plt.title("Data")
plt.scatter(testX[:,0], testX[:, 1], marker="o", c=testY, s=30)
# 绘制loss
plt.style.use("ggplot")
plt.figure()
plt.plot(np.arange(0, args["epochs"]), losses)
plt.title("Training Loss")
plt.xlabel("Epoch #")
plt.ylabel("Loss")
plt.show()
研究第100代末的实际损失值,您会注意到SGD获得的损失比普通梯度下降低一个数量级(分别为0.003和0.021)。这种差异是由于每个epoch的多次权值更新,从而使我们的模型有更多的机会从权值矩阵的更新中学习。这种影响在大型数据集上更加明显,比如ImageNet,在这里我们有数百万个训练示例,在我们的参数中有小的、增量的更新,可以导致低损耗(但不一定是最佳的)解决方案。
9.3扩展到SGD
在实践中,您将遇到两个主要的SGD扩展。第一个是momentum[86],这是一种用于加速SGD的方法,通过关注梯度点方向相同的维度,使SGD能够更快地学习。第二种方法是Nesterov加速度[87],是标准动量的延伸。
9.3.1 Momentum
应用于SGD的动量具有相同的效果——我们的目标是建立在标准权重更新的基础上,包括一个动量项,从而使我们的模型获得更低(和更高精确度)的损失在更小的epoch中。因此,动量项应该增加梯度指向相同方向的维度的更新强度,然后降低梯度转换方向的维度的更新强度[86,88]。
原来的更新规则:
我们现在引入动量项V,用γ标度:
动量项γ通常设为0.9;尽管另一种常见的做法是将γ设置为0.5,直到学习稳定下来,然后将其增加到0.9 -这是极其罕见的看到动量< 0.5。关于动量的更详细的回顾,请参考Sutton[89]和Qian[86]。
9.3.2 Nesterov的加速度
同样的想法也适用于SGD。如果我们积累了太多的动量,我们可能会超过一个局部最小值,然后继续前进。因此,有一个更智能的滚动将是有利的,它知道什么时候减速,这是Nesterov加速梯度[87]的切入点。
Nesterov加速度可以被理解为动量的修正更新,它让我们获得更新后参数的大致概念。看看Hinton的小批量梯度下降幻灯片概述[90],我们可以看到Nesterov加速的一个很好的可视化(图9.7)。
使用标准动量,我们计算梯度(蓝色小矢量),然后在梯度的方向上做一个大跳跃(蓝色大矢量)。在Nesterov加速下,我们首先会在之前的梯度(棕色向量)的方向上做一个大的跳跃,测量梯度,然后进行修正(红色向量)-绿色向量是Nesterov加速的最终修正更新(改编自Ruder[88])。
9.3.3传闻建议
动量是一个可以增加模型收敛性的重要项;与我们的学习速率和正则化惩罚(下一节将讨论)相比,我们往往不太担心这个超参数,这是迄今为止最重要的调整旋钮。
我个人的经验法则是,无论何时使用SGD,都要有动力。在大多数情况下,你可以将它设置为(并保留它)和0.9,尽管Karpathy[93]建议从0.5开始,并随着时间的增加将其增加到更大的值。
至于Nesterov加速,我倾向于在较小的数据集上使用它,但对于较大的数据集(如ImageNet),我几乎总是避免使用它。虽然Nesterov加速有良好的理论保证,但所有在ImageNet上训练过的主要出版物(e.x, AlexNet [94], VGGNet [95], ResNet [96], Inception[97]等)都使用了SGD的动量——这个研究小组没有一篇论文使用Nesterov加速。
我的个人经验使我发现,当在大数据集上训练深度网络时,使用动量而不使用Nesterov加速时,SGD更容易使用。另一方面,较小的数据集倾向于享受Nesterov加速的好处。然而,请记住,这是我的轶事观点,你的里程可能会有所不同。
9.4正则化
“机器学习中使用的许多策略都明确旨在减少测试误差,这可能会以增加训练误差为代价。这些策略统称为正规化。——古德费罗等人。
在本章前面的章节中,我们讨论了两个重要的损失函数:多类支持向量机损失和交叉熵损失。然后我们讨论了梯度下降,以及网络如何通过更新模型的权值参数来学习。虽然损失函数允许我们确定一组参数在给定分类任务中的表现如何(或差),但损失函数本身并不考虑权重矩阵的“外观”。
我们如何选择一组参数来确保我们的模型具有良好的泛化性?或者,至少,减少过度拟合的影响。答案是正规化。仅次于你的学习速度,正则化是你可以调整的模型中最重要的参数。
有各种类型的正则化技术,如L1正则化、L2正则化(通常称为“权值衰减”)和Elastic Net[98],用于更新损失函数本身,增加一个额外的参数来约束模型的容量。
我们也有可以明确添加到网络架构的正则化类型——dropout就是这种正则化的典型例子。然后,我们在训练过程中应用了隐式的正则化形式。隐式正则化的例子包括数据增加和早期停止。在这一节中,我们将主要关注通过修改损失和更新函数获得的参数化正则化。
在Starter Bundle的第11章中,我们将回顾dropout,然后在第17章中,我们将更深入地讨论过拟合,以及如何使用early stop作为正则化器。在实践者包中,您将发现作为正则化使用的数据扩展示例。
9.4.1什么是正则化,为什么需要正则化?
正规化帮助我们控制我们的模型能力,确保我们的模型更好地在他们没有训练的数据点上进行(正确的)分类,我们称之为泛化能力。如果我们不应用正则化,我们的分类器很容易变得过于复杂,并过度适合我们的训练数据,在这种情况下,我们失去了泛化到我们的测试数据的能力(以及测试集之外的数据点,如野外的新图像)。
然而,过多的正则化可能是一件坏事。我们可以冒着欠拟合的风险,在这种情况下,我们的模型在训练数据上表现得很差,并且不能对输入数据和输出类标签之间的关系建模(因为我们限制了模型容量太多)。例如,考虑下面的点图,以及适合这些点的各种函数(图9.8)。
橙色的线是欠拟合的一个例子——我们没有捕捉到点之间的关系。另一方面,蓝线是过拟合的一个例子——我们的模型中有太多的参数,当它触及数据集中的所有点时,它在点之间也有很大的变化。这不是我们所希望的平稳、简单的配合。然后我们有一个绿色的函数,它也命中了数据集中的所有点,但它是以一种更可预测、更简单的方式实现的。
正则化的目标是获得这些类型的“绿色函数”,很好地适合我们的训练数据,但避免过度拟合我们的训练数据(蓝色)或未能建模的潜在关系(黄色)。我们在第17章讨论了如何监测训练和发现欠拟合和过拟合;然而,目前,简单地理解正则化是机器学习的一个关键方面,我们使用正则化来控制模型的泛化。为了理解正则化以及它对我们的损失函数和权重更新规则的影响,让我们进入下一节。
9.4.2 Updating Our Loss and Weight Update To Include Regularization
现在,假设我们已经获得了一个权值矩阵W,使得训练集中的每个数据点都被正确分类,这意味着对于所有Li,我们的损失L = 0。
太棒了,我们得到了100%的准确率-但是让我问你一个关于这个权重矩阵的问题-它是唯一的吗?或者,换句话说,是否有更好的W选择来提高我们的模型泛化和减少过拟合的能力?
如果有这样一个W,我们怎么知道?我们如何将这种惩罚纳入损失函数中呢?答案是定义一个正则化惩罚,一个作用于权值矩阵的函数。正则化惩罚通常写成函数R(W)。下面式9.8给出了最常见的正则化惩罚L2正则化(也称为权值衰减):
我们在这里做的是循环遍历矩阵中的所有元素然后求平方和。L2正则化惩罚中的平方和不鼓励我们的矩阵W中较大的权重,而更喜欢较小的权重。为什么我们要阻止较大的重量值呢?总之,通过惩罚较大的权值,我们可以提高泛化能力,从而减少过拟合。
可以这样考虑——权重值越大,它对输出预测的影响就越大。权重值较大的维度几乎可以单独控制分类器的输出预测(当然,前提是权重值足够大),这几乎肯定会导致过拟合。
为了减轻各种维度对我们的输出分类的影响,我们应用正则化,从而寻找W值,使其考虑到所有维度,而不是少数具有较大值的维度。在实践中,您可能会发现正则化会略微损害您的训练准确性,但实际上会增加您的测试准确性。
同样,我们的损失函数具有相同的基本形式,只是我们现在加入了正则化:
第二项是新的-这是正则化的惩罚。λ变量是一个超参数,它控制我们正在应用的正则化的数量或强度。在实践中,学习率α和正则化项λ都是您将花费最多时间进行调整的超参数。
将交叉熵损失扩展为L2正则化,得到以下方程:
9.4.3正则化技术的类型
L2正则化:
我们还有L1正则化,它取绝对值而不是平方:
Elastic Net[98]正则化寻求L1和L2正则化的结合:
还有其他类型的正则化方法,比如直接修改网络的架构以及网络的实际训练方式——我们将在后面的章节中回顾这些方法。
用正则化方法你应该使用(包括根本没有),你应该把这个选择当作hyperparameter你需要优化并执行实验来确定正则化应该应用,如果是正则化方法,合适的λ的值是什么。关于正则化的更多细节,请参考Goodfellow等人的[10]的第7章,DeepLearing.net教程中的“正则化”部分[99],以及Karpathy的cs231n神经网络II讲座中的注释[100]。
9.4.4正则化应用于图像分类
from sklearn.linear_model import SGDClassifier
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from pyimagesearch.preprocessing import SimplePreprocessor
from pyimagesearch.datasets import SimpleDatasetLoader
from imutils import paths
import argparse
# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-d", "--dataset", required=True,help="path to input dataset")
args = vars(ap.parse_args())
# grab the list of image paths
print("[INFO] loading images...")
imagePaths = list(paths.list_images(args["dataset"]))
# 将图像转换为数组
sp = SimplePreprocessor(32, 32)
sdl = SimpleDatasetLoader(preprocessors=[sp])
(data, labels) = sdl.load(imagePaths, verbose=500)
data = data.reshape((data.shape[0], 3072))
# encode the labels as integers
le = LabelEncoder()
labels = le.fit_transform(labels)
# partition the data into training and testing splits using 75% of
# the data for training and the remaining 25% for testing
(trainX, testX, trainY, testY) = train_test_split(data, labels,
test_size=0.25, random_state=5)
# loop over our set of regularizers
for r in (None, "l1", "l2"):
# train a SGD classifier using a softmax loss function and the
# specified regularization function for 10 epochs
print("[INFO] training model with ‘{}‘ penalty".format(r))
model = SGDClassifier(loss="log", penalty=r, max_iter=10,
learning_rate="constant", eta0=0.01, random_state=42)
model.fit(trainX, trainY)
# evaluate the classifier
acc = model.score(testX, testY)
print("[INFO] ‘{}‘ penalty accuracy: {:.2f}%".format(r, acc * 100))
实际上,这个例子太小,无法显示应用正则化的所有优点——为此,我们必须等到开始训练卷积神经网络。然而,与此同时,只要我们能够正确地调整超参数,正则化可以提高我们的测试精度并减少过拟合。
9.5 Summary
梯度下降算法是通过学习速率来控制的:这是迄今为止在训练你自己的模型时最重要的参数。
如果您的学习速率太大,您只会在损失的情况下跳来跳去,而不会从数据中真正“学习”任何模式。另一方面,如果您的学习速率太小,它将需要大量的迭代才能达到合理的损失。为了让它正确,您需要将大部分时间花在调整学习速率上。
标签:函数,矩阵,withPython,损失,正则,Computer,Deep,梯度,我们 来源: https://blog.csdn.net/weixin_43749999/article/details/122775747