🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎
📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝
📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
🖍foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟👋
文章目录
人工神经网络( ANN )是一种监督学习算法,其灵感来自人脑的运作方式。类似于人脑中神经元连接和激活的方式,神经网络接受输入并将其传递给函数,导致某些后续神经元被激活,从而产生输出。
有几种标准的 ANN 架构。通用逼近定理说,我们总能找到一个足够大的神经网络架构,它具有正确的权重集,可以准确地预测任何给定输入的任何输出。这意味着,对于给定的数据集/任务,我们可以创建一个架构并不断调整其权重,直到 ANN 预测出我们希望它预测的内容。调整权重直到发生这种情况称为训练神经网络。大型数据集和定制架构的成功培训是人工神经网络在解决各种相关任务中获得突出地位的原因。
计算机视觉中的一项突出任务是识别图像中存在的对象的类别。ImageNet 是一项旨在识别图像中存在的对象类别的竞赛。历年分类错误率下降情况如下:
2012 年是神经网络 (AlexNet) 被用于竞赛获胜解决方案的一年。从上图中可以看出,从 2011 年到 2012 年,通过利用神经网络,错误显着减少。从那时起,随着时间的推移,随着神经网络更深、更复杂,分类错误不断减少,并超过了人类水平的表现。这为我们在适用的情况下为自定义任务学习和实施神经网络提供了坚实的动力。
在本章中,我们将在一个简单的数据集上创建一个非常简单的架构,主要关注 ANN 的各种构建块(前馈、反向传播、学习率)如何帮助调整权重,以便网络学会预测预期输出从给定的输入。我们将首先从数学上了解什么是神经网络,然后从头开始构建一个以打下坚实的基础。然后我们将了解负责训练神经网络的每个组件并对其进行编码。总的来说,我们将涵盖以下主题:
- 比较人工智能和传统机器学习
- 学习人工神经网络构建块
- 实现前馈传播
- 实施反向传播
- 将前馈传播和反向传播放在一起
- 了解学习率的影响
- 总结一个神经网络的训练过程
比较人工智能和传统机器学习
传统上,系统是通过使用程序员编写的复杂算法来实现智能化的。
例如,假设您有兴趣识别照片中是否包含狗。在传统的机器学习( ML ) 设置中,ML 从业者或主题专家首先确定需要从图像中提取的特征。然后他们提取这些特征并将它们传递给一个编写良好的算法,该算法破译给定的特征,以判断图像是否是狗。 下图说明了相同的想法 :
采取以下样本:
从前面的图像中,一个简单的规则可能是,如果图像包含三个排列成三角形的黑色圆圈,则可以将其归类为狗。 然而,这条规则对松饼的这种欺骗性的特写镜头是无效的:
当然,当显示一张除了狗的脸以外的任何东西的图像时,这条规则也会失效。因此,自然地,我们需要为多种类型的准确分类创建的手动规则的数量可能是指数级的,尤其是当图像变得更加复杂时。因此,传统方法在非常受限制的环境中效果很好(例如,拍摄护照照片,所有尺寸都限制在毫米以内),而在不受限制的环境中效果不佳,因为每个图像都有很大差异。
我们可以将相同的思路扩展到任何领域,例如文本或结构化数据。过去,如果有人对通过编程来解决现实世界的任务感兴趣,他们就必须了解有关输入数据的所有内容并编写尽可能多的规则来涵盖所有场景。这很乏味,并且不能保证所有新场景都会遵循上述规则。
然而,通过利用人工神经网络,我们可以一步完成。
神经网络提供了结合特征提取(手动调整)的独特优势,并将这些特征一次性用于分类/回归,几乎不需要手动特征工程。这两个子任务都只需要标记数据(例如,哪些图片是狗,哪些图片不是狗)和神经网络架构。它不需要人类提出规则来对图像进行分类,这减轻了传统技术强加给程序员的大部分负担。
请注意,主要要求是我们为需要解决方案的任务提供大量示例。例如,在前面的例子中,我们需要为模型提供大量的狗和非狗图片,以便它学习特征。神经网络如何用于分类任务的高级视图如下:
现在我们已经对神经网络比传统计算机视觉方法表现更好的根本原因有了一个非常高级的概述,让我们在本章的各个部分中更深入地了解神经网络是如何工作的。
学习人工神经网络构建块
ANN 是张量(权重)和数学运算的集合,其排列方式可以松散地复制人脑的功能。它可以被视为一种数学函数,它将一个或多个张量作为输入并预测一个或多个张量作为输出。将这些输入连接到输出的操作安排被称为神经网络的架构——我们可以根据手头的任务进行定制,即根据问题是包含结构化(表格)还是非结构化(图像, text, audio) 数据(输入和输出张量的列表)。
ANN 由以下部分组成:
- 输入层:这些层将自变量作为输入。
- 隐藏(中间)层:这些层连接输入和输出层,同时在输入数据之上执行转换。此外,隐藏层包含节点(下图中的单位/圆圈)以将其输入值修改为更高/更低维的值。实现更复杂表示的功能是通过使用修改中间层节点值的各种激活函数来实现的。
- 输出层:这包含输入变量预期产生的值。
考虑到这一点,神经网络的典型结构如下:
输出层中节点的数量(上图中的圆圈)取决于手头的任务以及我们是在尝试预测连续变量还是分类变量。如果输出是连续变量,则输出有一个节点。如果输出是具有m个可能类别的分类,则输出层中将有m个节点。让我们放大其中一个节点/神经元,看看发生了什么。神经元按如下方式转换其输入:
在上图中,x 1 , x 2 , ..., x n是输入变量,w 0是偏差项(类似于线性/逻辑回归中的偏差)。
注意w 1 , w 2 , ..., w n是赋予每个输入变量的权重,w 0是偏置项。输出值a计算如下:
如您所见,它是权重和输入对的乘积之和,后跟一个附加函数f(偏置项 +乘积之和)。函数f 是用于在该乘积之和之上应用非线性的激活函数。关于激活函数的更多细节将在下一节中提供,关于前馈传播。此外,可以通过具有多个隐藏层、堆叠多个神经元来实现更高的非线性。
在高层次上,神经网络是节点的集合,其中每个节点都有一个可调整的浮点值,并且节点以图形的形式相互连接,以返回由网络架构决定的格式的输出。该网络由三个主要部分组成:输入层、隐藏层和输出层。请注意,您可以拥有更多数量 (n) 的隐藏层,术语深度学习指的是更多数量的隐藏层。通常,当神经网络必须理解复杂的事物(例如图像识别)时,需要更多的隐藏层。
了解神经网络的架构后,在下一节中,我们将学习前馈传播,这有助于估计网络架构的误差(损失)量。
实现前馈传播
为了对前馈传播的工作原理建立一个强有力的基础理解,我们将通过一个训练神经网络的玩具示例,其中神经网络的输入为 (1, 1),相应的(预期)输出为 0。这里,我们将根据这个单一的输入输出对找到神经网络的最佳权重。但是,您应该注意,实际上,会有数以千计的数据点用于训练 ANN。
本例的神经网络架构包含一个隐藏层,其中包含三个节点, 如下所示:
上图中的每个箭头都包含一个可调整的浮点值(权重)。我们需要找到 9 个(第一个隐藏层 6 个,第二个隐藏层 3 个)浮点数,这样当输入为 (1,1) 时,输出尽可能接近 (0)。这就是我们训练神经网络的意思。为了简单起见,我们还没有引入偏差值——基本逻辑保持不变。
在接下来的部分中,我们将了解有关上述网络的以下内容:
- 计算隐藏层值
- 执行非线性激活
- 估计输出层值
- 计算期望值对应的损失值
计算隐藏层单元值
我们现在将为所有连接分配权重。第一步,我们在所有连接中随机分配权重。通常,神经网络在训练开始之前使用随机权重进行初始化。 同样,为了简单,在介绍主题时,我们在学习前馈传播和反向传播时不会包括偏差值。但是我们将在从头开始实现前馈传播和反向传播的同时拥有它。
让我们从在 0 和 1 之间随机初始化的初始权重开始,但请注意,神经网络训练过程后的最终权重不需要在一组特定的 值之间。下图(左半部分)提供了网络中权重和值的正式表示,右半部分在网络中提供了随机初始化的权重。
在下一步中,我们将输入与权重相乘,以计算隐藏层中隐藏单元的值。
激活前隐藏层的单元值得到 如下:
这里计算的隐藏层的单位值(激活前)也如下 图所示:
现在,我们将通过非线性激活传递隐藏层值。请注意,如果我们不在隐藏层中应用非线性激活函数,无论存在多少隐藏层,神经网络都会变成从输入到输出的巨大线性连接。
应用激活函数
激活函数有助于对输入和输出之间的复杂关系进行建模。
一些常用的激活函数计算 如下(其中x是输入):
各种输入值的上述每个激活的可视化如下:
对于我们的示例,让我们使用 sigmoid(逻辑)函数进行激活。
通过将 sigmoid(逻辑)激活S(x)应用于三个隐藏层和,我们在 sigmoid 激活后得到以下值:
现在我们已经获得了激活后的隐藏层值,在下一节中,我们将获得输出层值。
计算输出层值
到目前为止,我们已经计算了应用 sigmoid 激活后的最终隐藏层值。使用激活后的隐藏层值和权重值(在第一次迭代中随机初始化),我们将计算网络的输出值:
我们执行隐藏层值和权重值的乘积之和来计算输出值。另一个提醒:我们排除了需要在每个单元(节点)添加的偏差项,只是为了简化我们现在对前馈传播和反向传播的工作细节的理解,并将在编码前馈传播和反向传播时将其包括在内:
因为我们从一组随机的权重开始,所以输出节点的值与目标有很大的不同。在这种情况下,差值为1.235 (请记住,目标是 0)。在下一节中,我们将学习如何计算与当前状态下的网络相关的损失值。
计算损失值
损失值(也称为成本函数)是我们在神经网络中优化的值。要了解如何计算损失值,让我们看两个场景:
- 分类变量预测
- 连续变量预测
在连续变量预测期间计算损失
通常,当变量是连续的时,损失值被计算为实际值和预测值之差的平方的平均值,也就是说,我们试图通过改变与 神经 网络相关的权重值来最小化均方误差。均方误差值计算如下:
在前面的等式中,是实际输出。是由神经网络计算的预测(其权重以 的形式存储) ,其中它的输入是,m是数据集中的行数。
关键点应该是这样一个事实,即对于每个独特的权重集,神经网络将预测不同的损失,我们需要找到损失为零的黄金权重集(或者,在现实场景中,接近于零)尽可能)。
在我们的示例中,假设我们预测的结果是连续的。在这种情况下,损失函数值是均方误差,计算 如下:
现在我们了解了如何计算连续变量的损失值,在下一节中,我们将学习计算分类变量的损失值。
在分类变量预测期间计算损失
当要预测的变量是离散的(即变量中只有少数类别)时,我们通常使用分类交叉熵损失函数。当要预测的变量中有两个不同的值时,损失函数是二元交叉熵。
二元交叉熵计算 如下 :
y是输出的实际值,p是输出的预测值,m是数据点的总数。
分类交叉熵计算 如下 :
y 是输出的实际值,p是输出的预测值,m是数据点的总数,C是类的总数。
可视化交叉熵损失的一种简单方法是查看预测矩阵本身。假设您在图像识别问题中预测五个类别——狗、猫、老鼠、牛和母鸡。神经网络必须在最后一层有五个神经元,并激活 softmax(下一节将详细介绍 softmax)。因此,它将被迫预测每个类别、每个数据点的概率。假设有五张图像,预测概率如下所示(每行中突出显示的单元格对应于目标类):
注意每一行总和为1。在第一行中,当目标为Dog,预测概率为0.88时,对应的损失为0.128(即0.88的log的负数)。类似地,计算其他损失。如您所见,当正确类别的概率较高时,损失值较小。如您所知,概率介于 0 和 1 之间。因此,可能的最小损失可以为 0(当概率为 1 时),而最大损失可以为无穷大,当概率为 0 时。
数据集中的最终损失是所有行中所有单个损失的平均值。
现在我们已经对计算均方误差损失和交叉熵损失有了深入的了解,让我们回到我们的玩具示例。假设我们的输出是一个连续变量,我们将在后面的部分中学习如何使用反向传播来最小化损失值。我们将更新权重值(之前随机初始化)以最小化损失()。但是,在此之前,让我们首先使用 NumPy 数组在 Python 中编写前馈传播代码,以巩固我们对其工作细节的理解。
代码中的前馈传播
编码前馈传播的高级策略如下:
- 在每个神经元上执行求和积。
- 计算激活。
- 在每个神经元重复前两个步骤,直到输出层。
- 通过将预测与实际输出进行比较来计算损失。
它将是一个函数,它将输入数据、当前神经网络权重和输出数据作为函数的输入,并返回当前网络状态的损失。
计算所有数据点的均方误差损失值的前馈函数如下:
我们强烈建议您通过单击每个笔记本中的在 Colab 中打开按钮来执行代码笔记本。示例截图如下:
单击在 Colab 中打开(在前面的屏幕截图中突出显示)后,您将能够毫不费力地执行所有代码,并且应该能够复制本书中显示的结果。
有了执行代码的方式,让我们继续代码前馈传播:
1.取输入变量值 ( inputs),weights(如果这是第一次迭代,则随机初始化)和outputs提供的数据集中的实际值作为feed_forward函数的参数:
-
import numpy
as np
-
def
feed_forward(
inputs, outputs, weights):
为了使这个练习更现实一点,我们将与每个节点相关联的偏差。因此,权重数组不仅包含连接不同节点的权重,还包含与隐藏/输出层中的节点相关的偏差。
2.通过执行将输入层连接到隐藏层的矩阵乘法 ( np.dot)inputs和权重值 ( ) 计算隐藏层值,并添加与隐藏层节点关联的偏置项 ( ):weights[0]weights[1]
pre_hidden = np.dot(inputs,weights[0])+ weights[1]
3.在上一步获得的隐藏层值之上应用 sigmoid 激活函数 – : pre_hidden
hidden = 1/(1+np.exp(-pre_hidden))
4.通过执行隐藏层np.dot激活值(hiddenweights[2] weights[3]
pred_out = np.dot(hidden, weights[2]) + weights[3]
5.计算整个数据集的均方误差值并返回均方误差:
-
mean_squared_
error
= np.mean(np.square(pred_out \
-
- outputs))
-
return mean_squared_
error
我们现在可以在前向通过网络时获得均方误差值。
在我们了解反向传播之前,让我们通过在 NumPy 中实现它们来了解我们之前构建的前馈网络的一些组成部分——激活函数和损失值计算,以便我们详细了解它们的工作原理。
代码中的激活函数
虽然我们在前面代码中的隐藏层值之上应用了 sigmoid 激活,但让我们检查一下常用的其他激活函数:
Tanh:一个值(隐藏层单元值)的tanh激活计算如下:
-
def
tanh(x):
-
return (np.
exp(x)
-np.
exp(
-x))
/(np.
exp(x)
+np.
exp(
-x))
ReLU:一个值(隐藏层单元值)的Rectified Linear Unit ( ReLU ) 计算如下:
-
def relu(x):
-
return np.where(x
>
0,x,
0)
Linear:值的线性激活是值本身。这表示如下:
-
def
linear(
x):
-
return x
Softmax:与其他激活不同,softmax 在值数组的顶部执行。这通常是为了确定输入属于给定场景中m个可能的输出类别之一的概率。假设我们试图将一个数字的图像分类到可能的 10 个类别之一(数字从 0 到 9)。在这种情况下,有 10 个输出值,其中每个输出值应表示输入图像属于 10 个类别之一的概率。
Softmax 激活用于为输出中的每个类提供一个概率值,计算如下:
-
def
softmax(x):
-
return np.
exp(x)/np.
sum(np.
exp(x))
请注意输入之上的两个操作x-将使所有值变为正数,并且除以所有此类指数将迫使所有值介于 0 和 1 之间。此范围与事件的概率一致。这就是我们返回概率向量的意思。 np.exp np.sum(np.exp(x))
现在我们已经了解了各种激活函数,接下来,我们将了解不同的损失函数。
代码中的损失函数
损失值(在神经网络训练过程中被最小化)通过更新权重值被最小化。定义合适的损失函数是建立一个工作可靠的神经网络模型的关键。构建神经网络时通常使用的损失函数如下:
1.均方误差:均方误差是输出的实际值和预测值之间的平方差。我们取误差的平方,因为误差可以是正数或负数(当预测值大于实际值时,反之亦然)。平方可确保正负误差不会相互抵消。我们计算平方误差的平均值,以便当数据集大小不同时,两个不同数据集的误差具有可比性。
预测输出值数组 ( p) 和实际输出值数组( ) 之间的均方误差y计算如下:
-
def
mse(p, y):
-
return np.
mean(np.
square(p - y))
当试图预测本质上是连续的值时,通常使用均方误差。
2.平均绝对误差:平均绝对误差的工作方式与均方误差非常相似。平均绝对误差通过对所有数据点的实际值和预测值之间的绝对差取平均值来确保正误差和负误差不会相互抵消。
p预测输出值数组 ( ) 和实际输出值数组( )之间的平均绝对误差y实现如下:
-
def
mae(p, y):
-
return np.
mean(np.
abs(p-y))
与均方误差类似,平均绝对误差通常用于连续变量。此外,一般来说,当要预测的输出值小于 1 时,最好将平均绝对误差作为损失函数,因为均方误差会显着降低损失的幅度(1 之间数字的平方) -1 是一个更小的数字)当预期输出小于 1 时。
3.二元交叉熵:交叉熵是衡量两种不同分布之间差异的指标:实际分布和预测分布。二进制交叉熵应用于二进制输出数据,与我们讨论的前两个损失函数(在连续变量预测期间应用)不同。
p预测值数组 ( ) 和实际值数组( )之间的二进制交叉熵y实现如下:
-
def
binary_cross_entropy(p, y):
-
return -np.mean(np.
sum((y
*np.log(p)
+(
1-y)
*np.log(
1-p))))
请注意,二元交叉熵损失在预测值远离实际值时具有较高的值,而在预测值和实际值接近时具有较低的值。
.4分类交叉熵:预测值数组 ( p) 和实际值数组( ) 之间的分类交叉熵y实现如下:
-
def
categorical_cross_entropy(p, y):
-
return -np.
mean(np.
sum(y*np.
log(p)))
到目前为止,我们已经了解了前馈传播以及构成它的各种组件,例如权重初始化、与节点相关的偏差、激活和损失函数。在下一节中,我们将学习反向传播,这是一种调整权重的技术,以便它们导致尽可能小的损失。
实施反向传播
在前馈传播中,我们将输入层连接到隐藏层,然后将隐藏层连接到输出层。在第一次迭代中,我们随机初始化权重,然后计算这些权重值导致的损失。在反向传播中,我们采用相反的方法。我们从前馈传播中获得的损失值开始,并以尽可能最小化损失值的方式更新网络的权重。
当我们执行以下步骤时,损失值会降低:
- 对神经网络中的每个权重进行少量更改——一次一个。
- 当重量值发生变化 ( ) 时,测量损失的变化 ( )。
- 将权重更新为(其中k是一个正值,是一个称为学习率的超参数)。
请注意,对特定权重所做的更新与通过少量更改而减少的损失量成正比。直观地说,如果改变一个权重可以大大减少损失,那么我们可以大量更新权重。但是,如果通过改变权重减少的损失很小,那么我们只更新少量。
如果前面的步骤在整个数据集上执行n次(我们已经完成了前馈传播和反向传播),它本质上会导致n epochs的训练。
由于典型的神经网络包含数千/百万(如果不是数十亿)权重,因此更改每个权重的值,并检查损失是增加还是减少并不是最优的。上述列表中的核心步骤是衡量重量变化时的“损失变化”。正如您可能在微积分中学习过的那样,测量它与计算关于权重的损失梯度相同。关于利用微积分的偏导数来计算与权重有关的损失梯度的更多信息,在下一节中,关于反向传播的链式法则。
在本节中,我们将通过一次少量更新一个权重来从头开始实现梯度下降,如本节开头所述。然而,在实现反向传播之前,让我们了解神经网络的另一个细节:学习率。
直观地说,学习率有助于建立对算法的信任。例如,在决定权重更新的幅度时,我们可能不会一次性大量更改权重值,而是更慢地更新它。
这导致在我们的模型中获得稳定性;我们将在了解学习率的影响部分中了解学习率如何有助于稳定性。
我们更新权重以减少错误的整个过程称为梯度下降。
随机梯度下降 是在前面的场景中如何最小化错误。前面提到,梯度代表差异(即权重值更新少量时损失值的差异),下降意味着减少。随机代表随机样本的选择,根据该样本做出决策。
除了随机梯度下降,许多其他类似的优化器有助于最小化损失值;下一章将讨论不同的优化器。
在接下来的两节中,我们将学习如何在 Python 中从头开始对反向传播的直觉进行编码,并将简要讨论反向传播如何使用链式法则进行工作。
代码中的梯度下降
梯度下降在 Python 中实现如下:
1.定义前馈网络并计算均方误差损失值,就像我们在代码中的前馈传播部分中所做的那样:
-
from
copy import deepcopy
-
import numpy
as np
-
def feed_forward(inputs, outputs, weights):
-
pre_hidden
= np.dot(inputs,weights[
0])
+ weights[
1]
-
hidden
=
1
/(
1
+np.exp(-pre_hidden))
-
pred_out
= np.dot(hidden, weights[
2])
+ weights[
3]
-
mean_squared_
error
= np.mean(np.square(pred_out \
-
- outputs))
-
return mean_squared_
error
2.将每个权重和偏差值增加一个非常小的量 (0.0001),并为每个权重和偏差更新一次计算一个整体平方误差损失值。
- 在下面的代码中,我们创建了一个名为 的函数update_weights,它执行梯度下降过程来更新权重。函数的输入是网络的输入变量 - inputs,预期outputs,weights(在训练模型开始时随机初始化)和模型的学习率 - (稍后将详细介绍学习率): lr
def update_weights(inputs, outputs, weights, lr):
- 确保您的 deepcopy权重列表。由于权重将在后面的步骤中进行操作, deepcopy因此确保我们可以使用多个权重副本而不会干扰实际权重。我们将创建原始权重集的三个副本,它们作为输入传递给函数 - original_weight、temp_weight和supdated_weights:
-
original_weights = deepcopy(
weights)
-
temp_weights = deepcopy(
weights)
-
updated_weights = deepcopy(
weights)
- 通过传递, , 并通过feed_forward函数inputs,outputs和original_weights计算具有原始权重集的损失值 (original_loss):
-
original_loss = feed_forward(
inputs, outputs, \
-
original_weights)
- 我们将遍历网络的所有层:
for i, layer in enumerate(original_weights):
- 我们的神经网络中共有四个参数列表——两个列表用于将输入连接到隐藏层的权重和偏置参数,另外两个用于连接隐藏层和输出层的权重和偏置参数列表。现在,我们遍历所有单独的参数,因为每个列表都有不同的形状,我们利用np.ndenumerate循环遍历给定列表中的每个参数:
for index, weight in np.ndenumerate(layer):
- 现在我们将原始权重集存储在temp_weights. 我们选择它存在于第 i 层的指标权重并将其增加一个小值。最后,我们使用神经网络的新权重集计算新损失:
-
temp_weights
= deepcopy(weights)
-
temp_weights[i][
index]
+
=
0.0001
-
_loss_
plus
= feed_forward(inputs, outputs, \
-
temp_weights)
在前面代码的第一行中,我们将重置temp_weights为原始权重集,因为在每次迭代中,当参数在给定时期内更新少量时,我们会更新不同的参数来计算损失。
- 我们计算由于权重变化引起的梯度(损失值的变化):
grad = (_loss_plus - original_loss)/(0.0001)
这种把一个参数更新一个很小的量,然后计算梯度的过程就相当于微分的过程。
- 最后,我们更新对应的第 i th层中存在的参数和index, 的updated_weights。更新后的权重值将与梯度的值成比例地减小。此外,我们没有将它完全减少一个等于梯度值的值,而是引入了一种通过使用学习率来缓慢建立信任的机制—— lr(更多关于学习率的内容,请参阅了解学习率的影响部分):
updated_weights[i][index] -= grad*lr
- 一旦所有层的参数值和层内的索引都更新了,我们就会返回更新后的权重值—— updated_weights:
return updated_weights, original_loss
神经网络中的其他参数之一是计算损失值时考虑的批量大小。
在前面的场景中,我们考虑了所有数据点来计算损失(均方误差)值。然而,在实践中,当我们有数千个(或在某些情况下,数百万个)数据点时,更多数据点在计算损失值时的增量贡献将遵循收益递减规律,因此我们将使用与我们拥有的数据点总数相比,批量大小要小得多。我们将一次使用一批应用梯度下降(在前馈传播之后),直到我们在一个训练周期内耗尽所有数据点。
构建模型时考虑的典型批量大小在 32 到 1,024 之间。
在本节中,我们了解了当权重值发生少量变化时,根据损失值的变化来更新权重值。在下一节中,我们将了解如何在不计算梯度的情况下一次一个梯度地更新权重。
使用链式法则实现反向传播
到目前为止,我们已经通过少量更新权重,然后计算原始场景(当权重不变时)的前馈损失与更新权重后的前馈损失之间的差异来计算与权重有关的损失梯度。以这种方式更新权重值的一个缺点是,当网络很大时,需要进行大量计算来计算损失值(实际上,计算要进行两次——一次是权重值不变,另一次是权重值会少量更新)。这导致更多的计算,因此需要更多的资源和时间。在本节中,我们将学习如何利用链式法则,它不需要我们手动计算损失值来得出与权重值有关的损失梯度。
在第一次迭代中(我们随机初始化权重),输出的预测值为 1.235。
为了得到理论公式,我们将权重和隐藏层值以及隐藏层激活值分别表示为w、h和a,如下所示:
请注意,在前面的图中,我们取了左图中的每个组件值,并在右图中对其进行了概括。
为了便于理解,在本节中,我们将了解如何使用链式法则计算仅关于 w 11的损失值梯度。相同的学习可以扩展到神经网络的所有权重和偏差。我们鼓励您练习并将链式规则计算应用于其余的权重和偏差值。
本书 GitHub 存储库文件夹中的chain_rule.ipynb笔记本包含使用链式法则计算网络中所有参数的权重和偏差变化的梯度的方法。Chapter01
此外,为了便于我们的学习,我们将只处理一个数据点,其中输入为 {1,1},预期输出为 {0}。
假设我们正在用 w 11计算损失值的梯度,让我们通过下图了解计算梯度时要包含的所有中间组件(未将输出连接到 w 11 的组件在下图):
从上图中,我们可以看到 w 11通过突出显示的路径 - 、和 对损失值做出贡献。
接下来,让我们制定如何分别获得、 和。
网络的损失值表示如下:
预测输出值计算如下:
隐藏层激活值(sigmoid激活)计算如下:
隐藏层值计算如下:
现在我们已经制定了所有方程,让我们计算损失值(C)变化对权重变化的影响 ,如下所示:
这称为链式法则。本质上,我们正在执行一系列差异化来获取我们感兴趣的差异化。
请注意,在前面的等式中,我们已经建立了一个偏微分方程链,这样我们现在能够分别对四个分量中的每一个执行偏微分,并最终计算损失值相对于权重的导数值。
上述等式中的各个偏导数计算如下:
- 损失值相对于预测输出值的偏导数 如下:
- 预测输出值相对于隐藏层激活值的偏导数如下:
- 隐藏层激活值相对于激活前隐藏层值的偏导如下:
请注意,前面的等式来自 sigmoid 函数a的导数是 a*(1-a)的事实。
- 激活前的隐藏层值相对于权重值的偏导如下:
有了这个,损失值的梯度是通过将每个偏微分项替换为前面步骤中计算的相应值来计算的,如下所示:
从前面的公式可以看出,我们现在能够计算权重值的微小变化(损失相对于权重的梯度)对损失值的影响,而无需通过重新计算前馈来强制我们的方式再次传播。
接下来,我们将继续更新权重值,如下所示:
这两种方法的工作版本,1)使用链式法则识别梯度,然后更新权重,2)通过学习权重值的微小变化对损失值的影响来更新权重值,从而得到相同的更新权重值值
在梯度下降中,我们按顺序执行权重更新过程(一次一个权重)。通过利用链式法则,我们了解到有一种替代方法可以计算少量重量变化对损失值的影响,但是,有机会并行执行计算。
因为我们正在跨所有层更新参数,所以更新参数的整个过程可以并行化。此外,鉴于在现实场景中,跨层可能存在数百万个参数,因此在 GPU 的不同核心上执行每个参数的计算会导致更新权重所花费的时间比遍历每个权重要快得多,一个一次。
现在我们从直觉的角度和利用链式法则对反向传播有了扎实的理解,在下一节中,我们将了解前馈和反向传播如何协同工作以达到最佳权重值。
将前馈传播和反向传播放在一起
在本节中,我们将构建一个带有隐藏层的简单神经网络,该网络将输入连接到我们在代码中的前馈传播部分中处理的同一玩具数据集上的输出,并利用我们在上一节中定义的函数执行反向传播以获得最佳权重和偏差值。 update_weights
我们定义模型如下:
1.输入连接到具有三个单元/节点的隐藏层。
2.隐藏层连接到输出,输出层有一个单元。
我们将按如下方式创建网络:
1.导入相关包并定义数据集:
-
from copy import deepcopy
-
import numpy as np
-
x = np.array(
[[1,1]])
-
y = np.array(
[[0]])
2.随机初始化权重和偏差值。
隐藏层中有三个单元,每个输入节点都连接到每个隐藏层单元。因此,总共有六个权重值和三个偏差值——一个偏差和两个权重(两个权重来自两个输入节点)对应于每个隐藏单元。此外,最后一层有一个单元连接到隐藏层的三个单元。因此,总共三个权重和一个偏差决定了输出层的值。随机初始化的权重如下:
-
W
= [
-
np.array([[-
0.0053,
0.3793],
-
[-
0.5820, -
0.5204],
-
[-
0.2723,
0.1896]], dtype
=np.float
32).T,
-
np.array([-
0.0140,
0.5607, -
0.0628], dtype
=np.float
32),
-
np.array([[
0.1528,-
0.1745,-
0.1135]],dtype
=np.float
32).T,
-
np.array([-
0.5516], dtype
=np.float
32)
-
]
在前面的代码中,第一个参数数组对应于将输入层连接到隐藏层的 2 x 3 权重矩阵。第二个参数数组表示与隐藏层的每个节点相关的偏差值。第三个参数数组对应于将隐藏层连接到输出层的 3 x 1 权重矩阵,最后一个参数数组表示与输出层相关的偏差。
3.通过前馈传播和反向传播的 100 个 epoch 运行神经网络——其功能已经在前面的部分中学习并定义为和 函数。 feed_forward update_weights
- 定义feed_forward函数:
-
def feed_forward(inputs, outputs, weights):
-
pre_hidden
= np.dot(inputs,weights[
0])
+ weights[
1]
-
hidden
=
1
/(
1
+np.exp(-pre_hidden))
-
pred_out
= np.dot(hidden, weights[
2])
+ weights[
3]
-
mean_squared_
error
= np.mean(np.square(pred_out \
-
- outputs))
-
return mean_squared_
error
- 定义update_weights函数:
-
def update_weights(inputs, outputs, weights, lr):
-
original_weights
= deepcopy(weights)
-
temp_weights
= deepcopy(weights)
-
updated_weights
= deepcopy(weights)
-
original_loss
= feed_forward(inputs, outputs, \
-
original_weights)
-
for i, layer
in enumerate(original_weights):
-
for
index, weight
in np.ndenumerate(layer):
-
temp_weights
= deepcopy(weights)
-
temp_weights[i][
index]
+
=
0.0001
-
_loss_
plus
= feed_forward(inputs, outputs, \
-
temp_weights)
-
grad
= (_loss_
plus
- original_loss)
/(
0.0001)
-
updated_weights[i][
index] -
= grad
*lr
-
return updated_weights, original_loss
- 更新超过 100 个时期的权重并获取损失值和更新后的权重值:
-
losses
= []
-
for epoch
in range(
100):
-
W, loss
= update_weights(x,y,W,
0.01)
-
losses.append(loss)
- 绘制损失值:
-
import matplotlib.pyplot
as plt
-
%matplotlib
inline
-
plt.plot(losses)
-
plt.title(
'Loss over increasing number of epochs')
-
plt.xlabel(
'Epochs')
-
plt.ylabel(
'Loss value')
前面的代码生成以下图:
如您所见,损失从 0.33 左右开始,然后稳步下降到 0.0001 左右。这表明权重是根据输入-输出数据调整的,当给定输入时,我们可以期望它预测我们在损失函数中与之比较的输出。输出权重如下:
-
[array([[
0.01424004, -
0.5907864 , -
0.27549535],
-
[
0.39883757, -
0.52918637,
0.18640439]], dtype
=float
32),
-
array([
0.00554004,
0.5519136 , -
0.06599568], dtype
=float
32),
-
array([[
0.3475135 ],
-
[-
0.05529078],
-
[
0.03760847]], dtype
=float
32),
-
array([-
0.22443289], dtype
=float
32)]
具有相同权重的相同代码的 PyTorch 版本在 GitHub 笔记本 ( Auto_gradient_of_tensors.ipynb) 中进行了演示。在理解下一章的核心 PyTorch 概念后,重新访问本节。无论网络是用 NumPy 还是 PyTorch 编写的,自己验证输入和输出确实是相同的。本章使用 NumPy 数组从头开始构建网络虽然不是最佳的,但可以帮助您为神经网络的工作细节打下坚实的基础。
5.一旦我们有了更新的权重,通过将输入传递给网络来对输入进行预测并计算输出值:
-
pre_hidden
= np.dot(x,W[
0])
+ W[
1]
-
hidden
=
1
/(
1
+np.exp(-pre_hidden))
-
pred_out
= np.dot(hidden, W[
2])
+ W[
3]
-
# -
0.017
前面代码的输出是 的值-0.017,这个值非常接近于预期的输出 0。随着我们训练更多的 epoch,这个值变得更接近于 0。 pred_out
到目前为止,我们已经了解了前馈传播和反向传播。我们在这里定义的函数中的关键部分是学习率——我们将在下一节中学习。 update_weights
了解学习率的影响
为了了解学习率如何影响模型的训练,让我们考虑一个非常简单的案例,我们尝试拟合以下等式(请注意,以下等式与我们迄今为止一直在研究的玩具数据集不同) :
请注意,y是输出,x是输入。使用一组输入和预期输出值,我们将尝试用不同的学习率拟合方程,以了解学习率的影响。
我们指定输入和输出数据集如下:
-
x =
[[1],[2],[3],[4]]
-
y =
[[3],[6],[9],[12]]
2.定义函数feed_forward。此外,在这种情况下,我们将修改网络,使我们没有隐藏层,架构如下:
请注意,在前面的函数中,我们正在估计参数w 和b:
-
from
copy import deepcopy
-
import numpy
as np
-
def feed_forward(inputs, outputs, weights):
-
pred_out
= np.dot(inputs,weights[
0])
+ weights[
1]
-
mean_squared_
error
= np.mean(np.square(pred_out \
-
- outputs))
-
return mean_squared_
error
3.就像我们在代码中的梯度下降中定义的那样定义函数: update_weights
-
def update_weights(inputs, outputs, weights, lr):
-
original_weights
= deepcopy(weights)
-
org_loss
= feed_forward(inputs, outputs,original_weights)
-
updated_weights
= deepcopy(weights)
-
for i, layer
in enumerate(original_weights):
-
for
index, weight
in np.ndenumerate(layer):
-
temp_weights
= deepcopy(weights)
-
temp_weights[i][
index]
+
=
0.0001
-
_loss_
plus
= feed_forward(inputs, outputs, \
-
temp_weights)
-
grad
= (_loss_
plus
- org_loss)
/(
0.0001)
-
updated_weights[i][
index] -
= grad
*lr
-
return updated_weights
4.将权重和偏差值初始化为随机值:
-
W = [np.array(
[[0]], dtype=np.float32),
-
np.array(
[[0]], dtype=np.float32)]
请注意,权重和偏差值随机初始化为 0 值。此外,输入权重值的形状为 1 x 1,因为输入中每个数据点的形状为 1 x 1,偏差值的形状是 1 x 1(因为输出中只有一个节点,每个输出都有一个值)。
5.让我们利用学习率为 0.01 的update_weights函数,循环 1,000 次迭代,并检查权重值 ( W) 如何随时间增加而变化:
-
weight_
value
= []
-
for epx
in range(
1000):
-
W
= update_weights(x,y,W,
0.01)
-
weight_
value.append(W[
0][
0][
0])
请注意,在前面的代码中,我们使用 0.01 的学习率并重复该函数以在每个 epoch 结束时获取修改后的权重。此外,在每个 epoch 中,我们将最近更新的权重作为输入,以在下一个 epoch 中获取更新的权重。 update_weights
6.绘制每个 epoch 结束时的权重参数值:
-
import matplotlib.pyplot
as plt
-
%matplotlib inline
-
epochs = range(
1,
1001)
-
plt.plot(epochs,weight_value)
-
plt.title(
'Weight value over increasing \
-
epochs
when learning rate
is
0.01
')
-
plt.xlabel(
'Epochs')
-
plt.ylabel(
'Weight value')
前面的代码导致权重值随着时间的增加而变化,如下所示请注意,在前面的输出中,权重值在正确的方向上逐渐增加,然后在 ~3 的最佳值处饱和。
为了了解学习率的值对获得最佳权重值的影响,让我们了解当学习率为 0.1 和学习率为 1 时,权重值如何随着时间的增加而变化。
下面的图表是我们在第5步修改对应的学习率值并执行第6步时得到的(生成下面图表的代码和我们之前学习的代码一样,只是学习率值有变化,可用在 GitHub 的相关笔记本中):
请注意,当学习率非常小时(0.01)时,权重值缓慢(在更多的时期)向最优值移动。然而,在学习率(0.1)稍高的情况下,权重值最初会振荡,然后迅速饱和(在更少的时期内)到最佳值。最后,当学习率很高时(1),权重值飙升到一个非常高的值,无法达到最佳值。
学习率低时权重值没有大幅飙升的原因是我们将权重更新限制为等于梯度 * 学习率的量,本质上导致学习时权重更新量很小率很小。但是,当学习率高时,权重更新高,之后损失的变化(当权重更新一个小值时)非常小,以至于权重无法达到最优值。
为了更深入地了解梯度值、学习率和权重值之间的相互作用,我们只运行该函数 10 个 epoch。此外,我们将打印以下值以了解它们如何随着时代的增加而变化: update_weights
- 每个时期开始时的权重值
- 体重更新前的损失
- 少量更新权重时的损失
- 梯度值
我们修改update_weights函数以打印前面的值,如下所示:
-
def update_weights(inputs, outputs, weights, lr):
-
original_weights
= deepcopy(weights)
-
org_loss
= feed_forward(inputs, outputs, original_weights)
-
updated_weights
= deepcopy(weights)
-
for i, layer
in enumerate(original_weights):
-
for
index, weight
in np.ndenumerate(layer):
-
temp_weights
= deepcopy(weights)
-
temp_weights[i][
index]
+
=
0.0001
-
_loss_
plus
= feed_forward(inputs, outputs, \
-
temp_weights)
-
grad
= (_loss_
plus
- org_loss)
/(
0.0001)
-
updated_weights[i][
index] -
= grad
*lr
-
if(i %
2
=
=
0):
-
print(
'weight value:', \
-
np.round(original_weights[i][
index],
2), \
-
'original loss:', np.round(org_loss,
2), \
-
'loss_plus:', np.round(_loss_
plus,
2), \
-
'gradient:', np.round(grad,
2), \
-
'updated_weights:', \
-
np.round(updated_weights[i][
index],
2))
-
return updated_weights
前面代码中以粗体突出显示的行是我们修改上一节中的update_weights函数的地方,首先,我们通过检查 if ( i % 2 == 0 ) 来检查我们当前是否正在处理权重参数(original_weights[i][index]),loss (org_loss), updated loss value (_loss_plus), ,因为另一个参数对应于偏差值(grad),然后我们打印原始权重值(updated_weights)
现在让我们了解在我们正在考虑的三种不同学习率中,前面的值如何随着时间的增加而变化:
- 学习率 0.01:我们将使用以下代码检查值:
-
W
= [np.array([[
0]], dtype
=np.float
32),
-
np.array([[
0]], dtype
=np.float
32)]
-
weight_
value
= []
-
for epx
in range(
10):
-
W
= update_weights(x,y,W,
0.01)
-
weight_
value.append(W[
0][
0][
0])
-
print(W)
-
import matplotlib.pyplot
as plt
-
%matplotlib inline
-
epochs
= range(
1,
11)
-
plt.plot(epochs,weight_
value)
-
plt.title(
'Weight value over increasing \
-
epochs when learning rate is 0.01')
-
plt.xlabel(
'Epochs')
-
plt.ylabel(
'Weight value')
前面的代码产生以下输出:
请注意,当学习率为 0.01 时,损失值缓慢下降,权重值也缓慢向最优值更新。现在让我们了解当学习率为 0.1 时前面的变化。
- 学习率 0.1:代码与学习率 0.01 场景中的代码保持一致,但是在这种情况下,学习率参数将是 0.1。使用改变的学习率参数值运行相同代码的输出如下:
让我们对比一下 0.01 和 0.1 的学习率场景——两者的主要区别如下:
当学习率为 0.01 时,与学习率为 0.1 相比,权重的更新速度要慢得多(当学习率为 0.01 时从第一个 epoch 的 0 到 0.45,当学习率为 0.1 时为 4.5)。更新速度较慢的原因是学习率较低,因为权重是通过梯度乘以学习率来更新的。
除了权重更新幅度,我们还要注意权重更新的方向:
当权重值小于最优值时梯度为负,当权重值大于最优值时梯度为正。这种现象有助于在正确的方向上更新权重值。
最后,我们将前面的内容与学习率 1 进行对比:
- 学习率 1:代码与学习率 0.01 场景中的代码保持一致,但是在这种情况下,学习率参数将为 1。使用更改的学习率参数运行相同代码的输出如下:
从上图中,我们可以看到权重已经偏离了一个非常大的值(在第一个 epoch 结束时,权重值为 45,在后面的 epoch 中进一步偏离了一个非常大的值)。除此之外,权重值移动到非常大的量,因此权重值的微小变化几乎不会导致梯度发生变化,因此权重会停留在那个高值上。
一般来说,学习率越低越好。这样,模型能够缓慢学习,但会将权重调整为最佳值。典型的学习率参数值范围在 0.0001 和 0.01 之间。
现在我们已经了解了神经网络的构建块——前馈传播、反向传播和学习率,在下一节中,我们将总结如何将这三者组合在一起训练神经网络的高级概述。
总结一个神经网络的训练过程
训练神经网络是通过重复两个关键步骤,即以给定的学习率进行前向传播和反向传播,为神经网络架构得出最佳权重的过程。
在前向传播中,我们对输入数据应用一组权重,将其传递给定义的隐藏层,对隐藏层的输出执行定义的非线性激活,然后通过乘以隐藏层将隐藏层连接到输出层层节点值与另一组权重来估计输出值。然后,我们最终计算出对应于给定权重集的整体损失。对于第一次前向传播,权重的值是随机初始化的。
在反向传播中,我们通过在减少整体损失的方向上调整权重来减少损失值(误差)。此外,权重更新的幅度是梯度乘以学习率。
重复前馈传播和反向传播的过程,直到我们获得尽可能小的损失。 这意味着,在训练结束时,神经网络已经调整了它的权重,以便它预测我们希望它预测的输出。在前面的玩具示例中,在训练之后,当{1,1}作为输入时,更新后的网络将预测值 0 作为输出,因为它被训练来实现这一目标。
概括
在本章中,在我们了解架构和人工神经网络的各种组件。接下来,我们了解了如何在实现前馈传播之前连接网络的各个层,以计算与网络当前权重对应的损失值。接下来我们实施了反向传播来了解优化权重以最小化损失值的方法。此外,我们了解了学习率如何在实现网络的最佳权重中发挥作用。此外,我们实现了网络的所有组件——前馈传播、激活函数、损失函数、链式法则和梯度下降,以从头开始更新 NumPy 中的权重,从而为下一章的构建奠定坚实的基础。
现在我们了解了神经网络的工作原理,我们将在下一章使用 PyTorch 实现一个,并在第三章深入研究可以在神经网络中调整的各种其他组件(超参数)。
问题
- 神经网络中的各个层是什么?
- 前馈传播的输出是什么?
- 连续因变量的损失函数与二元因变量以及分类因变量的损失函数有何不同?
- 什么是随机梯度下降?
- 反向传播练习有什么作用?
- 在反向传播过程中,跨层的所有权重的权重更新是如何发生的?
- 神经网络的哪些功能在训练神经网络的每个时期内发生?
- 与在 CPU 上训练相比,为什么在 GPU 上训练网络更快?
- 学习率如何影响训练神经网络?
- 学习率参数的典型值是多少?