Keras深度学习实战(1)——神经网络基础与模型训练过程详解
- 0. 前言
- 1. 神经网络基础
- 1.1 简单神经网络的架构
- 1.2 神经网络的训练
- 1.3 神经网络的应用
- 2. 从零开始构建前向传播
- 2.1 计算隐藏层节点值
- 2.2 应用激活函数
- 2.3 计算输出层值
- 2.4 计算损失值
- 2.4.1 在连续变量预测过程中计算损失
- 2.4.2 在分类(离散)变量预测中计算损失
- 2.4.3 计算网络损失值
- 2.5 使用 Python 实现网络前向传播
- 3. 从零开始构建反向传播
- 小结
0. 前言
神经网络是一种性能强大的学习算法,其灵感来自大脑的运作方式。类似于神经元在大脑中彼此连接的方式,神经网络获取输入后,通过某些函数在网络中进行传递输入信息,连接在其后的一些神经元会被激活,从而产生输出。
1. 神经网络基础
1.1 简单神经网络的架构
人工神经网络受到人脑运作方式的启发。从本质上讲,它是对线性回归和逻辑回归的一种改进,神经网络在计算输出时引入了多种非线性函数。此外,神经网络在修改网络体系结构以利用结构化和非结构化数据跨多个域解决问题方面具有极大的灵活性。函数越复杂,网络对于输入的数据拟合能力就越大,因此预测的准确性就越高。神经网络的典型结构如下:
神经网络中的层 (layer
) 是一个或多个节点(或称计算单元)的集合,层中的每个节点都连接到下一层中的每个节点。输入层由预测输出值所需的输入变量组成。输出层中节点的数量取决于我们要预测连续变量还是分类变量。如果输出是连续变量,则输出层一个节点。
如果输出结果是 个类别的预测类的分类,则输出层中将有
在上图中, 是自变量, 是偏置项,类似于线性方程 里的 , 是赋予每个输入变量的权重。如果
函数是激活函数,用于在输入和它们相应的权重值的总和上引入非线性。可以通过使用多个隐藏层实现更强的非线性能力。
综上,神经网络是相互连接的层中节点权重的集合。该集合分为三个主要部分:输入层,隐藏层和输出层。神经网络中可以具有
1.2 神经网络的训练
训练神经网络实际上就是通过重复两个关键步骤来调整神经网络中的权重:前向传播和反向传播。
- 在前向传播中,我们将一组权重应用于输入数据,将其传递给隐藏层,对隐藏层计算后的输出使用非线性激活,通过若干个隐藏层后,将最后一个隐藏层的输出与另一组权重相乘,就可以得到输出层的结果。对于第一次正向传播,权重的值将随机初始化。
- 在反向传播中,尝试通过测量输出的误差,然后相应地调整权重以降低误差。神经网络重复正向传播和反向传播以预测输出,直到获得令误差较小的权重为止。
1.3 神经网络的应用
最近,神经网络在各种应用中的广泛采用。神经网络可以通过多种方式进行构建,以下是一些常见的构建方法:
底部的紫色框代表输入,其后是隐藏层(中间的黄色框),顶部的粉色框是输出层。一对一的体系结构是典型的神经网络,在输入和输出层之间具有隐藏层。不同体系结构的示例如下:
架构 | 示例 |
one-to-many | 输入是图像,输出是图像的预测类别概率 |
many-to-one | 输入是电影评论,输出评论是好评或差评 |
many-to-many | 将一种语言的句子使用神经网络翻译成另一种语言的句子 |
现代神经网络中经常用到的一种架构称为卷积神经网络 (Convolutional Neural Networks
, CNN
),可以用来理解图像中的内容并检测目标内容所在的位置,该体系架构如下所示(在之后的学习中会进行详细介绍):
神经网络在推荐系统,图像分析,文本分析和音频分析的都有着广泛的应用,神经网络能够灵活地使用多种体系结构解决问题,可以预料的是,神经网络的使用范围将会越来越广。
接下来,我们将根据神经网络训练的两个关键步骤——前向传播和反向传播——介绍神经网络模型的构建。
2. 从零开始构建前向传播
为了进一步了解前向传播的工作方式,我们将通过一个简单的示例来构建神经网络,其中神经网络的输入为 (1, 1)
,对应的输出为 0。
我们使用的神经网络具有一个隐藏层,一个输入层和一个输出层。由于要使输入层能够以更大的维度表示,因此隐藏层中的神经元数量多于输入层中的神经元。
2.1 计算隐藏层节点值
第一次进行正向传播时,首先需要为所有连接分配权重,这些权重是基于高斯分布随机选择的,但是神经网络训练过程之后的最终权重不需要服从特定分布,假定初始网络权重如下:
接下来,我们将输入与权重相乘以计算隐藏层中隐藏单元的值,隐藏层的节点单位值计算结果如下:
下图展示了计算隐藏层的节点值后的网络示意图:
在以上步骤中,我们计算了隐藏节点的值。为简单起见,我们并未在隐藏层的节点中添加偏置项。接下来,我们将通过激活函数传递隐藏层的值,以便在输出中增加非线性。NOTE:
如果我们不在隐藏层中应用非线性激活函数,则神经网络本质上将成为从输入到输出线性连接。
2.2 应用激活函数
可以在网络中的多个网络层中应用激活函数,使用它们可以实现高度非线性,这对于建模输入和输出之间的复杂关系非常关键。在我们的示例中,使用 Sigmoid
激活函数如下所示:
通过将 Sigmoid
激活函数应用于隐藏层,我们得到以下结果:
下图展示了隐藏层的应用非线性激活函数后节点值的情况:
关于更多激活函数的介绍,参考《深度学习常用激活函数》。
2.3 计算输出层值
现在我们已经计算了隐藏层的值,最后将计算输出层的值。在下图中,我们将隐藏层值通过随机初始化的权重值连接到输出层。计算隐藏层值和权重值乘积的总和,得到输出值:
使用隐藏层值和权重值,我们可以得到网络的输出值,如下图所示:
因为第一次正向传播使用随机权重,所以输出神经元的值与目标相差很大,相差为 +1.235
(目标值为0)。
2.4 计算损失值
损失值(也称为成本函数)是在神经网络中优化的值。为了了解如何计算损失值,我们分析以下两种情况:
- 连续变量预测
- 分类(离散)变量预测
2.4.1 在连续变量预测过程中计算损失
通常,当预测值为连续变量时,损失函数使用平方误差,也就是说,我们尝试通过更改与神经网络相关的权重值来最小化均方误差:
其中, 是实际值, 是我们对输入 进行变换以获得预测值 的网络模型,
2.4.2 在分类(离散)变量预测中计算损失
当要预测的变量是离散变量时(也就是说,变量中只有几个类别),我们通常使用分类交叉熵损失函数。当要预测的变量具有两个不同的值时,损失函数为二分类交叉熵,而当要预测的变量具有多个不同的值时,损失函数为多分类交叉熵。
- 二分类交叉熵公式如下:
- 多分类交叉熵定义如下:
其中, 是输入实际对应的真实值, 是输出的预测值,
2.4.3 计算网络损失值
由于我们在以上示例中预测的结果是连续的,因此损失函数值是均方误差,其计算方法如下:
2.5 使用 Python 实现网络前向传播
通过以上学习,我们知道了通过在输入数据之上执行以下步骤以在前向传播中可以得出误差值:
- 随机初始化权重
- 通过将输入值乘以权重来计算隐藏层节点值
- 对隐藏层值执行激活
- 将隐藏层值连接到输出层
- 计算平方误差损失
计算所有数据点的平方误差损失值:
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))
out = np.dot(hidden, weights[2]) + weights[3]
squared_error = (np.square(pred_out - outputs))
return squared_error
在前面的函数中,我们将输入变量值、权重(如果是第一次迭代,则随机初始化)以及数据集中的实际输出作为 feed_forward
函数的输入。
我们通过对输入和权重进行矩阵乘法来计算隐藏层的值。此外,将偏置值添加到隐藏层中:
pre_hidden = np.dot(inputs,weights[0])+ weights[1]
其中 weights[0]
是权重值,weights[1]
是偏置值,利用此权重和偏置就可以将输入层连接到隐藏层。计算隐藏层的值后,就可以在隐藏层的值上使用激活函数:
hidden = 1/(1+np.exp(-pre_hidden))
通过将隐藏层的输出乘以将隐藏层连接到输出的权重,然后在输出上添加偏置项,来计算隐藏层的输出:
pred_out = np.dot(hidden, weights[2]) + weights[3]
一旦计算出输出,我们就可以计算出每一输入的平方误差损失,如下所示:
squared_error = (np.square(pred_out - outputs))
在前面的代码中,pred_out
是预测输出,而 outputs
是输入应对应的实际输出。通过以上简单的步骤,我们便可以在网络前向传播时计算损失值。
3. 从零开始构建反向传播
在正向传播中,我们将输入层与隐藏层连接到输出层。 在反向传播中,我们使用相反的过程。 每次将神经网络中的每个权重进行少量更改。权重值的变化将对最终损失值(增加或减少的损失)产生影响,我们需要朝着减少损失的方向更新权重。通过每次轻微更新权重并测量权重更新导致的误差变化,我们可以完成以下操作:
- 确定权重更新的方向
- 确定权重更新的幅度
在实施反向传播之前,我们首先了解神经网络的另一重要概念:学习率。学习率有助于我们建立更稳定的算法。例如,在确定权重更新的大小时,我们不会一次性就对其进行大幅度更改,而是采取更谨慎的方法来缓慢地更新权重。这使模型获得更高的稳定性;在之后的学习中,我们还将研究学习率如何帮助提高稳定性。
更新权重以减少误差的整个过程称为梯度下降技术,随机梯度下降是将误差最小化的手段。更直观地讲,梯度代表差异(即实际值和预测值之间的差异),而下降则表示差异减小;随机代表选择随机样本进行训练,并据此做出决策。除了随机梯度下降外,还有许多其他优化技术可以用于减少损失值。之后的学习中,还将讨论不同的优化技术。
反向传播的工作原理如下:
- 利用前向传播过程计算损失值。
- 略微改变所有的权重。
- 计算权重变化对损失函数的影响。
- 根据权重更新是增加还是减少了损失值,在损失减少的方向上更新权重值。
对数据集中的所有数据执行 1 次训练过程(前向传播+反向传播),称为 1 个 epoch
。
为了进一步巩固我们对神经网络中反向传播的理解,让我们拟合一个已知的简单函数,查看如何得出权重。假设,待拟合函数为 ,我们期望得出权重值和偏置值(分别为 3 和 0)。
x | 1 | 3 | 4 | 8 | 10 |
y | 3 | 9 | 12 | 24 | 30 |
以上数据集可以表示为线性回归 ,我们将尝试计算 和 的值(虽然我们已知它们分别是 2 和 0,但我们的目的是研究如何使用梯度下降获得这些值),将 和 参数随机初始化为 和
- 初始化数据集,如下所示:
x = np.array([[1], [3], [4], [8], [10]])
y = np.array([[3], [9], [12], [24], [30]])
- 随机初始化权重和偏差值(在尝试确定 方程中 和
w = np.array([[[2.269]], [[1.01]]])
- 定义神经网络并计算平方误差损失值:
import numpy as np
def feed_forward(inputs, outputs, weights):
out = np.dot(inputs, weights[0]) + weights[1]
squared_error = np.square(out - outputs)
return squared_error
在上述代码中,对输入与随机初始化的权重值进行了矩阵乘法,然后将其与随机初始化的偏置值相加。得到输出值后,便可以计算出实际值与预测值之差的平方误差值。
- 少量增加每个权重和偏置值,并针对每个权重和偏差更新一次计算一个平方误差损失值。
如果平方误差损失值随权重的增加而减小,则权重值应增加,权重值应增加的大小与权重变化减少的损失值的大小成正比。反之亦然。另外,通过学习率确保增加的权重值小于因权重变化而导致的损失值变化,这样可以确保损失值更平稳地减小。
接下来,创建一个名为update_weights
的函数,该函数执行反向传播过程以更新在权重,该函数运行epochs
次:
from copy import deepcopy
def update_weights(inputs, outputs, weights, epochs):
for epoch in range(epochs):
- 将输入通过神经网络传递,以计算权重未更新时的损失:
org_loss = feed_forward(inputs, outputs, weights)
- 确保对权重列表进行深复制,由于权重将在后续步骤中进行操作,深复制可解决由于子变量的更改而影响父变量的问题:
wts_tmp = deepcopy(weights)
wts_tmp2 = deepcopy(weights)
- 循环遍历所有权重值,然后对其进行较小的更改 (
+0.0001
):
for ix, wt in enumerate(weights):
wts_tmp[ix] += 0.0001
- 当权重修改后,计算更新的前向传播损失。计算由于权重的微小变化而造成的损失变化,因为我们要计算所有输入采样的均方误差,因此将损失的变化除以输入的数据数量:
loss = feed_forward(inputs, outputs, wts_tmp)
del_loss = np.sum(org_loss - loss)/(0.0001*len(inputs))
以较小的值更新权重,然后计算其对损失值的影响,等效于计算权重变化的导数(即反向梯度传播)。
- 通过损失变化来更新权重。通过将损失的变化乘以一个很小的数字(0.01)来缓慢更新权重,这就是学习率参数:
wts_tmp2[ix] += del_loss*0.01
wts_tmp = deepcopy(weights)
- 返回更新的权重和偏差值:
weights = deepcopy(wts_tmp2)
return wts_tmp2
整体 update_weights()
函数如下所示:
from copy import deepcopy
def update_weights(inputs, outputs, weights, epochs):
for epoch in range(epochs):
org_loss = feed_forward(inputs, outputs, weights)
wts_tmp = deepcopy(weights)
wts_tmp2 = deepcopy(weights)
for ix, wt in enumerate(weights):
wts_tmp[ix] += 0.0001
loss = feed_forward(inputs, outputs, wts_tmp)
del_loss = np.sum(org_loss - loss)/(0.0001*len(inputs))
wts_tmp2[ix] += del_loss*0.01
wts_tmp = deepcopy(weights)
weights = deepcopy(wts_tmp2)
return wts_tmp2
通过更新网络 1000 次,查看训练后网络中的参数和偏置值:
weights = update_weights(x, y, w, 1000)
print(weights)
打印权重如下所示,可以看到其与预期的结果 (w=3.0
, b=0.0
) 非常接近:
[[[2.99929065]]
[[0.00478785]]]
神经网络中的还有一个重要参数是在计算损失值时需要考虑的批大小 (batch size
)。在以上示例中,我们同时为所有数据计算损失值。但是,当我们有成千上万个数据时,在计算损失值时增加大量数据的增量贡献将导致训练困难,甚至可能超出内存上限无法计算,因此通常在一个 epoch
中将数据分为多个 batch
送入网络进行训练,建立模型时常用的 batch
大小在 16 ~ 512
之间。
小结
在本文中, 我们了解了神经网络的相关基础知识,同时看到了神经网络常见的模型架构与其在实际中的广泛应用,同时利用 Python
从零开始实现了神经网络的训练过程——前向传播和反向传播,了解了神经网络的通用训练流程。