PyTorch深度学习实战(1)——神经网络与模型训练过程详解
- 0. 前言
- 1. 传统机器学习与人工智能
- 2. 人工神经网络基础
- 2.1 人工神经网络组成
- 2.2 神经网络的训练
- 3. 前向传播
- 3.1 计算隐藏层值
- 3.2 执行非线性激活
- 3.3 计算输出层值
- 3.4 计算损失值
- 3.5 实现前向传播
- 4. 反向传播
- 4.1 反向传播流程
- 4.2 梯度下降
- 4.3 实现梯度下降算法
- 4.4 使用链式法则实现反向传播
- 5. 合并前向传播和反向传播
- 6. 神经网络训练过程总结
- 小结
0. 前言
人工神经网络 (Artificial Neural Network
, ANN
) 是一种监督学习算法,其灵感来自人类大脑的运作方式。类似于人脑中神经元连接和激活的方式,神经网络接受输入,通过某些函数在网络中进行传递,导致某些后续神经元被激活,从而产生输出。函数越复杂,网络对于输入的数据拟合能力就越大,因此预测的准确性就越高。
有多种不同的 ANN
架构,根据通用逼近定理,我们总能找到一个足够大的包含正确权重集的神经网络架构,可以准确地预测任何给定输入的输出结果。这意味着,对于给定的数据集/任务,我们可以创建一个架构并不断调整其权重,直到 ANN
预测出正确结果,调整网络权重的过程称为训练神经网络。
计算机视觉中的一项重要任务是识别图像中的对象类别,即图像分类,ImageNet
是图像分类领域的一项权威竞赛,历年分类准确率情况如下:
从上图可以看出,通过利用神经网络,模型错误率显着减少,随着时间的推移,神经网络逐渐变得更深、更复杂,分类错误率不断减少,并表现出超越人类的水平。
在本节中,我们将使用一个简单的数据集创建一个简单的神经网络架构,以了解 ANN
的各个组成部分(前向传播、反向传播、学习率等)对于模型权重调整的作用,以掌握神经网络如何根据给定输入学习预测输出。我们将首先介绍神经网络背后的数学原理,然后从零开始构建一个神经网络,并介绍用于训练神经网络的每个组成部分。
1. 传统机器学习与人工智能
传统应用程序中,系统是通过使用程序员编写的复杂算法来实现智能化的。例如,假设我们希望识别照片中是否包含狗。在传统的机器学习 (Machine Learning
, ML
) 中,需要机器学习研究人员首先确定需要从图像中提取的特征,然后提取这些特征并将它们作为输入传递给复杂算法,算法解析给定特征以判断图像中是否包含狗:
然而,如果要为多种类别图像分类手动提取特征,其数量可能是指数级的,因此,传统方法在受限环境中效果很好(例如,识别证件照片),而在不受限制的环境中效果不佳,因为每张图像之间都有较大差异。
我们可以将相同的思想扩展到其他领域,例如文本或结构化数据。过去,如果希望通过编程来解决现实世界的任务,就必须了解有关输入数据的所有内容并编写尽可能多的规则来涵盖所有场景,并且不能保证所有新场景都会遵循已有规则。
而神经网络内含了特征提取的过程,并将这些特征用于分类/回归,几乎不需要手动特征工程,只需要标记数据(例如,哪些图片是狗,哪些图片不是狗)和神经网络架构,不需要手动提出规则来对图像进行分类,这减轻了传统机器学习技术强加给程序员的大部分负担。
训练神经网络需要提供大量样本数据。例如,在前面的例子中,我们需要为模型提供大量的狗和非狗图片,以便它学习特征。神经网络用于分类任务的流程如下,其训练与测试是端到端 (end-to-end
) 的:
2. 人工神经网络基础
2.1 人工神经网络组成
ANN
是张量(权重, weights
)和数学运算的集合,其排列方式近似于松散的人脑神经元排列。可以将其视为一种数学函数,它将一个或多个张量作为输入并预测相应输出(一个或多个张量)。将输入连接到输出的操作方式称为神经网络的架构,我们可以根据不同的任务构建不同架构,即基于问题是包含结构化数据还是非结构化(图像,文本,语音)数据,这些数据就是输入和输出张量的列表。ANN
由以下部分组成:
- 输入层:将自变量作为输入
- 隐藏(中间)层:连接输入和输出层,在输入数据之上执行转换;此外,隐藏层利用节点单元(下图中的圆圈)将其输入值修改为更高/更低维的值;通过修改中间节点的激活函数可以实现复杂表示函数
- 输出层:输入变量产生的值
综上,神经网络的典型结构如下:
输出层中节点的数量(上图中的圆圈)取决于实际任务以及我们是在尝试预测连续变量还是分类变量。如果输出是连续变量,则输出有一个节点。如果输出是具有 m
个可能类别的分类,则输出层中将有 m
个节点。接下来,我们深入介绍节点/神经元的工作原理,神经元按如下方式转换其输入:
其中, x1 x_1x1, x2 x_2x2,…, xn x_nxn 是输入变量, w0 w_0w0 是偏置项(类似于线性/逻辑回归中的偏差); w1 w_1w1, w2 w_2w2,…, wn w_nwn 是赋予每个输入变量的权重,输出值 aaa 计算如下:
a=f( w 0+ ∑ wiN w i x i)a=f(w_0+\sum _{w_i} ^N w_ix_i) a=f(w0+wi∑Nwixi)
可以看到, aaa 是权重和输入对的乘积之和,之后使用一个附加函数 f ( w0+ ∑ w iNwixi)f(w_0+\sum _{w_i} ^N w_ix_i)f(w0+∑wiNwixi),函数 fff 是在乘积之和之上应用的非线性激活函数,用于在输入和它们相应的权重值的总和上引入非线性,可以通过使用多个隐藏层实现更强的非线性能力。
整体而言,神经网络是节点的集合,其中每个节点都有一个可调整的浮点值(权重),并且节点间相互连接,返回由网络架构决定的输出。网络由三个主要部分组成:输入层、隐藏层和输出层。我们可以使用多层 (n
) 隐藏层,深度学习通常表示具有多个隐藏层的神经网络。通常,当神经网络需要学习具有复杂上下文或上下文不明显的任务(例如图像识别)时,就必须具有更多隐藏层。
2.2 神经网络的训练
训练神经网络实际上就是通过重复两个关键步骤来调整神经网络中的权重:前向传播和反向传播。
- 在前向传播 (
feedforward propagation
) 中,我们将一组权重应用于输入数据,将其传递给隐藏层,对隐藏层计算后的输出使用非线性激活,通过若干个隐藏层后,将最后一个隐藏层的输出与另一组权重相乘,就可以得到输出层的结果。对于第一次正向传播,权重的值将随机初始化 - 在反向传播 (
backpropagation
) 中,尝试通过测量输出的误差,然后相应地调整权重以降低误差。神经网络重复正向传播和反向传播以预测输出,直到获得令误差较小的权重为止
3. 前向传播
为了进一步了解前向传播的工作方式,我们将通过一个简单的示例来构建神经网络,其中神经网络的输入为 (1,1)
,相应(预期)的输出为 0
,我们根据这一输入输出对找到神经网络的最佳权重。在实际的神经网络中,会有数以万计的数据样本点用于训练。
我们采用的策略如下:神经网络具有一个隐藏层,一个输入层和一个输出层,其中隐藏层包含三个节点,如下所示:
在上图中,每个箭头都包含一个可调整的浮点值(权重)。我们需要找到 9
个浮点数,当输入为 (1,1)
时,令输出尽可能接近 0
,这就是我们训练神经网络的目的(令输出尽可能接近目标值)。为简单起见,我们并未在隐藏层单元中添引入偏置项。接下来,我们将针对以上网络介绍以下内容:
- 计算隐藏层值
- 执行非线性激活
- 计算输出层值
- 计算损失值
3.1 计算隐藏层值
首先,为所有连接分配随机权重;通常,神经网络在训练开始之前使用随机权重进行初始化。为了简单起见,在本节中,前向传播和反向传播时不包括偏执值。
初始权重可以随机初始化至 0-1
之间,但神经网络训练过程后的最终权重不需要在特定的区间内。下图左侧给出了网络中权重和值的可视化表示,右侧给出了网络中随机初始化的权重:
接下来,将输入与权重相乘,计算隐藏层中的值。应用激活函数前,隐藏层的单元值如下:
h 11= x 1∗ w 11+ x 2∗ w 21=1∗0.8+1∗0.2=1h 12= x 1∗ w 12+ x 2∗ w 22=1∗0.4+1∗0.9=1.3h 13= x 1∗ w 13+ x 2∗ w 23=1∗0.3+1∗0.5=0.8h_{11}=x_1*w_{11}+x_2*w_{21}=1*0.8+1*0.2=1 \\ h_{12}=x_1*w_{12}+x_2*w_{22}=1*0.4+1*0.9=1.3 \\ h_{13}=x_1*w_{13}+x_2*w_{23}=1*0.3+1*0.5=0.8 h11=x1∗w11+x2∗w21=1∗0.8+1∗0.2=1h12=x1∗w12+x2∗w22=1∗0.4+1∗0.9=1.3h13=x1∗w13+x2∗w23=1∗0.3+1∗0.5=0.8
计算的应用激活前隐藏层的单元值可视化如下图所示:
接下来,我们通过非线性激活传递隐藏层值。需要注意的是,如果我们不在隐藏层中应用非线性激活函数,那么无论存在多少隐藏层,则神经网络本质上都将是从输入到输出线性连接。
3.2 执行非线性激活
激活函数有助于对输入和输出之间的复杂关系进行建模,使用它们可以实现高度非线性。一些常用的激活函数如下(其中 x
是输入):
Sigmoid(x)= 1 1+ e −x ReLU(x)= { x x>00 x≤0Tanh(x)=e x− e −xe x+ e −x Linear(x)=xSigmoid(x)=\frac 1 {1+e^{-x}} \\ ReLU(x)=\left\{ \begin{aligned} x \quad x>0\\ 0 \quad x≤0\\ \end{aligned} \right. \\ Tanh(x)=\frac {e^x-e^{-x}} {e^x+e^{-x}} \\ Linear(x) = x Sigmoid(x)=1+e−x1ReLU(x)={xx>00x≤0Tanh(x)=ex+e−xex−e−xLinear(x)=x
应用激活函数后,输入值对应的激活可视化如下:
使用 Sigmoid
激活函数,通过对隐藏层应用 sigmoid
激活 S ( x )S(x)S(x),可以得到以下结果:
a 11=sigmoid(1.0)=0.73a 12=sigmoid(1.3)=0.78a 13=sigmoid(0.8)=0.69a_{11} = sigmoid(1.0) = 0.73\\ a_{12} = sigmoid(1.3) = 0.78\\ a_{13} = sigmoid(0.8) = 0.69 a11=sigmoid(1.0)=0.73a12=sigmoid(1.3)=0.78a13=sigmoid(0.8)=0.69
3.3 计算输出层值
接下来,我们将应用激活函数后的隐藏层值通过随机初始化的权重值连接到输出层,使用激活后的隐藏层值和权重值计算网络的输出值:
计算隐藏层值和权重值乘积的总和,得到输出值。此外,为了简化对前向传播和反向传播工作细节的理解,我们暂时忽略每个单元(节点)中的偏置项:
output=0.73×0.3+0.79×0.5+0.69×0.9=1.235output = 0.73\times 0.3+0.79\times 0.5 + 0.69\times 0.9= 1.235 output=0.73×0.3+0.79×0.5+0.69×0.9=1.235
因为我们从一组随机权重开始,所以输出节点的值与目标值有较大差距,根据以上计算可以看到,差值为 1.235
(我们的目标是令目标值与模型输出值之间的差值为 0
)。在下一小节中,我们将学习如何计算当前状态下网络的损失值。
3.4 计算损失值
损失值( Loss values
,也称为成本函数 cost functions
)是我们需要在神经网络中优化的值。为了了解如何计算损失值,我们分析以下两种情况:
- 分类(离散)变量预测
- 连续变量预测
3.4.1 在连续变量预测过程中计算损失
通常,当变量是连续的时,可以计算实际值和预测值之差的平方的平均值作为损失值,也就是说,我们试图通过改变与神经网络相关的权重值来最小化均方误差:
J θ= 1 m ∑ i=1m( y i− y^i ) 2y^i= η θ( x i)J_{\theta}=\frac 1m\sum_{i=1}^m(y_i-\hat y _i)^2\\ \hat y_i=\eta_{\theta}(x_i) Jθ=m1i=1∑m(yi−y^i)2y^i=ηθ(xi)
其中, yi y_iyi 是实际输出,y ^i \hat y_iy^i 是由神经网络 η\etaη (权重为 θ\thetaθ )计算得到的预测输出,输入为 xi x_ixi, mmm 是数据集中训练时使用的样本数。
关键点在于,对于每组不同的权重,神经网络都会得到不同的损失值,理想情况下,我们需要找到令损失为零的最佳权重集,在现实场景中,则需要找到尽可能令损失值接近于零的权重集。
在上一小节的示例中,假设预测结果是连续值,损失函数使用均方误差,计算结果如下:
loss(error)=1.23 5 2=1.52loss(error)=1.235^2=1.52 loss(error)=1.2352=1.52
3.4.2 在分类(离散)变量预测过程中计算损失
当要预测的变量是离散的(即变量只有几个类别)时,我们通常使用分类交叉熵 (categorical cross-entropy
) 损失函数。当要预测的变量有两个不同的值时,损失函数为二元交叉熵 (binary cross-entropy
),二元交叉熵计算如下:
− 1 m ∑ i=1m( y i(log( p i)+(1− y i)log(1− p i))-\frac 1m\sum_{i=1}^m(y_i(log(p_i)+(1-y_i)log(1-p_i)) −m1i=1∑m(yi(log(pi)+(1−yi)log(1−pi))
分类交叉熵计算如下:
− 1 m ∑ j=1C ∑ i=1m y ilog( p i)-\frac 1m\sum_{j=1}^C\sum_{i=1}^my_ilog(p_i) −m1j=1∑Ci=1∑myilog(pi)
其中, yyy 是输出对应的真实值(即数据样本的标签), ppp 是输出的预测值, mmm 是数据样本总数, CCC 是类别总数。
可视化交叉熵损失的一种简单方法是查看预测矩阵。假设我们需要在图像识别问题中预测五个类别——狗、猫、马、羊和牛。神经网络在最后一层必须包含五个神经元,并使用 softmax
激活函数。此时,网络将输出数据样本属于每个类别的概率。假设有五张图像,预测概率如下所示,其中每行中突出显示单元格对应于图像的真实标签(也称目标类别,target class
):
每一行中的概率总和为 1
。在第一行中,当目标是 Dog
,预测概率为 0.88
时,对应的损失值为 0.128
;类似地,我们也可以计算其他损失值,当正确类别的概率较高时,损失值较小。由于概率介于 0
和 1
之间,因此,当概率为 1
时,可能的最小损失为 0
,而当概率为 0
时,最大损失可以为无穷大,模型的最终损失是所有行(训练数据样本)的损失平均值。
3.5 实现前向传播
实现前向传播的策略如下:
- 通过将输入值乘以权重来神经元输出值
- 计算激活值
- 在每个神经元上重复前两个步骤,直到输出层
- 将预测输出与真实值进行比较计算损失值
我们可以将以上过程封装为一个函数,将输入数据、当前神经网络权重和真实值作为函数输入,并返回网络的当前损失值:
import numpy as npdef 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
(1) 将输入变量值 (inputs
)、权重(weights
,如果是首次迭代,则随机初始化)和数据的实际输出 (outputs
) 作为 feed_forward
函数的参数:
import numpy as npdef feed_forward(inputs, outputs, weights):
由于为每个神经元节点添加偏置项,因此权重数组不仅包含连接不同节点的权重,还包含与隐藏/输出层中的节点相关的偏置项。
(2) 通过执行输入层和权重值 (weights[0]
) 的矩阵乘法(np.dot
)来计算隐藏层值,并将偏置值 (weights[1]
) 添加到隐藏层中,利用权重和偏置值就可以将输入层连接到隐藏层:
pre_hidden = np.dot(inputs, weights[0]) + weights[1]
(3) 在获得的隐藏层值 (pre_hidden
) 之上应用 sigmoid
激活函数:
hidden = 1/(1+np.exp(-pre_hidden))
(4) 通过对隐藏层激活值 (hidden
) 和权重 (weights[2]
,将隐藏层连接到输出层)执行矩阵乘法 (np.dot
) 计算输出层值,然后在输出上添加偏置项 (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
其中,pred_out
是预测输出,而 outputs
是输入应对应的实际输出。
4. 反向传播
4.1 反向传播流程
在前向传播中,我们将输入层连接到隐藏层,然后将隐藏层连接到输出层。在第一次迭代时,随机初始化权重,然后计算网络在当前权重值下的损失。在反向传播中,我们采用相反的方法。利用从前向传播中计算的损失值,并以尽可能最小化损失值为目标更新网络权重,网络权重更新步骤如下:
- 每次对神经网络中的每个权重进行少量更改
- 测量权重变化 (δW\delta W δW) 时的损失变化 (δL\delta L δL)
- 计算 −k δLδW -k\frac {\delta L}{\delta W} −kδWδL 更新权重(其中 kk k 为学习率,且为正值,是神经网络中重要的超参数)
对特定权重所做的更新与损失值的减少成正比,也就是说,如果改变一个权重可以大幅减少损失,那么权重的更新就会较大,但是,如果改变权重仅能小幅减少损失,那么就只需要小幅度更新权重。
在整个数据集上执行 n
次上述过程(包括前向传播和反向传播),表示模型进行了 n
个 epoch
的训练(执行一次称为一个 epoch
)。
由于神经网络通常可能包含数以百万计的权重,因此更改每个权重的值,并检查损失的变化在实践中并不是最佳方法。上述步骤的核心思想是计算权重变化时的“损失变化”,即计算损失值关于权重的梯度。
在本节中,我们将通过一次少量更新一个权重来从零开始实现梯度下降,但在实现反向传播之前,我们首先了解神经网络的另一关键超参数:学习率 (learning rate
)。
直观地说,学习率有助于构建更稳定的算法。例如,在确定权重更新的大小时,我们并不会一次性就对其进行大幅度更改,而是采取更谨慎的方法来缓慢地更新权重。这使模型获得更高的稳定性;在之后的学习中,我们还将研究学习率如何帮助提高网络稳定性。
4.2 梯度下降
更新权重以减少误差值的整个过程称为梯度下降 (gradient descent
)。随机梯度下降 (stochastic gradient descent
, SGD
) 是将误差最小化的一种方法,其中随机 (stochastic
) 表示随机选择数据集中的训练数据样本,并根据该样本做出决策。除了随机梯度下降外,还有许多其他优化器可以用于减少损失值。之后的学习中,我们还将学习不同的优化器。
接下来,我们将学习如何使用 Python
从零开始实现反向传播,并介绍如何使用链式法则进行反向传播。
4.3 实现梯度下降算法
(1) 定义前馈网络并计算均方误差损失值:
from copy import deepcopyimport numpy as npdef 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
确保我们可以处理多个权重副本,而不会影响实际权重,创建作为输入传递给函数的原始权重集的三个副本—— original_weights
、temp_weights
和 updated_weights
:
original_weights = deepcopy(weights)temp_weights = deepcopy(weights)updated_weights = deepcopy(weights)
将 inputs
、outputs
和 original_weights
传递给 feed_forward
函数,使用原始权重集计算损失值 (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 中
,循环每一参数并为其增加一个较小值,并使用神经网络的新权重集计算新损失:
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)
这种对参数更新一个很小的量,然后计算梯度的过程就相当于微分的过程。
通过损失变化来更新权重,并使用学习率 lr
令权重变化更稳定:
updated_weights[i][index] -= grad*lr
所有参数值更新后,返回更新后的权重值——updated_weights
:
return updated_weights, original_loss
神经网络中的另一个需要考虑的超参数是计算损失值时的批大小 (batch size
)。在以上示例中,我们使用了所有数据点来计算损失值。然而,在实践中,数据集中通常包含数数以万甚至百万计的数据点,过多的数据点在计算损失值时的增量贡献遵循收益递减规律,与我们数据样本总数相比,批大小要小得多。训练模型时,一次使用一批数据点应用梯度下降更新网络参数,直到我们在一次训练周期 (epoch
) 内遍历所有数据点。构建模型时,批大小通常在 32
到 1024
之间。
4.4 使用链式法则实现反向传播
我们已经学习了如何通过对权重进行小量更新,然后计算权重更新前后损失的差异来计算与权重有关的损失梯度。当网络参数较多时,以这种方式更新权重值需要进行大量计算来得到损失值,因此需要较大的资源和时间。在本节中,我们将学习如何利用链式法则来获取与权重值有关的损失梯度。
在上一小节的示例中,第一次迭代输出的预测值为 1.235
。将权重和隐藏层值以及隐藏层激活值分别表示为 www、 hhh 和 aaa:
在本节中,我们将了解如何使用链式法则计算损失值关于 w11 w_{11}w11 的梯度,可以使用相同的方式计算其他权重和偏置值。此外,为了便于了解链式法则,我们只需要处理一个数据点,其中输入为 {1,1}
,输出目标值为 {0}
。
要计算损失值关于 w11 w_{11}w11 的梯度,可以通过下图了解计算梯度时要包括的所有中间组件(使用黑色标记标示—— h11 h_{11}h11、 a11 a_{11}a11 和 y^ \hat yy^):
网络的损失值表示如下:
Los s MSE (C)=(y− y ^ ) 2Loss_{MSE}(C)=(y-\hat y)^2 LossMSE(C)=(y−y^)2
预测输出值 y^ \hat yy^ 计算如下:
y ^= a 11∗ w 21+ a 12∗ w 22+ a 13∗ w 23\hat y=a_{11}*w_{21}+a_{12}*w_{22}+a_{13}*w_{23} y^=a11∗w21+a12∗w22+a13∗w23
隐藏层激活值( sigmoid
激活)计算如下:
a 11= 1 1+ e − h 11a_{11}=\frac 1 {1+e^{-h_{11}}} a11=1+e−h111
隐藏层值计算如下:
h 11= x 1∗ w 11+ x 2∗ w 21h_{11}=x_1*w_{11}+x_2*w_{21} h11=x1∗w11+x2∗w21
计算损失值 CCC 的变化相对于权重 w11 w_{11}w11 的变化:
∂C∂ w 11 = ∂C∂ y ^ ∗ ∂ y ^∂ a 11 ∗ ∂ a 11∂ h 11 ∗ ∂ h 11∂ w 11 \frac {\partial C}{\partial w_{11}}=\frac {\partial C}{\partial \hat y}*\frac {\partial \hat y}{\partial a_{11}}*\frac {\partial a_{11}}{\partial h_{11}}*\frac {\partial h_{11}}{\partial w_{11}} ∂w11∂C=∂y^∂C∗∂a11∂y^∗∂h11∂a11∗∂w11∂h11
上式称为链式法则 (chain rule
),在上式中我们建立了一个偏微分方程链,分别对四个分量执行偏微分,并最终计算损失值相对于权重的导数值 w11 w_{11}w11。上式中的各个偏导数计算如下。
损失值相对于预测输出值 y^ \hat yy^ 的偏导数如下:
∂C∂ y ^ = ∂ ∂ y ^ (y− y ^ ) 2=−2∗(y− y ^)\frac {\partial C}{\partial \hat y}=\frac {\partial}{\partial \hat y}(y-\hat y)^2=-2*(y-\hat y) ∂y^∂C=∂y^∂(y−y^)2=−2∗(y−y^)
预测输出值 y^ \hat yy^ 相对于隐藏层激活值 a11 a_{11}a11 的偏导数如下:
∂ y ^∂ a 11 = ∂ ∂ a 11 ( a 11∗ w 21+ a 12∗ w 22+ a 13∗ w 23)= w 21\frac {\partial \hat y}{\partial a_{11}}=\frac {\partial}{\partial a_{11}}(a_{11}*w_{21}+a_{12}*w_{22}+a_{13}*w_{23})=w_{21} ∂a11∂y^=∂a11∂(a11∗w21+a12∗w22+a13∗w23)=w21
隐藏层激活值 a11 a_{11}a11 相对于隐藏层值 h11 h_{11}h11 的偏导如下:
∂ a 11∂ h 11 = a 11∗(1− a 11)\frac {\partial a_{11}}{\partial h_{11}}=a_{11}*(1-a_{11}) ∂h11∂a11=a11∗(1−a11)
隐藏层值 h11 h_{11}h11 相对于权重值 w11 w_{11}w11 的偏导如下:
∂ h 11∂ w 11 = ∂ ∂ w 11 ( x 1∗ w 11+ x 2∗ w 21)= x 1\frac {\partial h_{11}}{\partial w_{11}}=\frac {\partial}{\partial w_{11}}(x_1*w_{11}+x_2*w_{21})=x_1 ∂w11∂h11=∂w11∂(x1∗w11+x2∗w21)=x1
因此,损失值相对于 w11 w_{11}w11 的梯度可以表示为:
∂C∂ w 11 =−2∗(y− y ^)∗ w 21∗ a 11∗(1− a 11)∗ x 1\frac {\partial C}{\partial w_{11}}=-2*(y-\hat y)*w_{21}*a_{11}*(1-a_{11})*x_1 ∂w11∂C=−2∗(y−y^)∗w21∗a11∗(1−a11)∗x1
从上式可以看出,我们现在能够直接计算权重值的微小变化对损失值的影响(损失相对于权重的梯度),而无需重新计算前向传播。接下来,更新权重值:
updated_weight=original_weight−lr∗gradient_of_lossupdated\_weight=original\_weight-lr*gradient\_of\_loss updated_weight=original_weight−lr∗gradient_of_loss
5. 合并前向传播和反向传播
在本节中,我们将构建一个带有隐藏层(连接输入与输出)的简单神经网络,使用在上一小节中介绍的简单数据集,并利用 update_weights
函数执行反向传播以获得最佳权重和偏置值。模型定义如下:
- 输入连接到具有三个神经元(节点)的隐藏层。
- 隐藏层连接到具有一个神经元的输出层
接下来,使用 Python
创建神经网络:
(1) 导入相关库并定义数据集:
import numpy as np from copy import deepcopyimport matplotlib.pyplot as pltx = 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.float32).T, np.array([-0.0140, 0.5607, -0.0628], dtype=np.float32), np.array([[ 0.1528, -0.1745, -0.1135]], dtype=np.float32).T, np.array([-0.5516], dtype=np.float32)]
其中,第一个参数数组对应于将输入层连接到隐藏层的 2 x 3
权重矩阵;第二个参数数组表示与隐藏层的每个神经元相关的偏置值;第三个参数数组对应于将隐藏层连接到输出层的 3 x 1
权重矩阵,最后一个参数数组表示与输出层相关的偏置值。
(3) 在 100
个 epoch
内执行前向传播和反向传播,使用以上部分中定义的 feed_forward
和 update_weights
函数。
在训练期间,更新权重并获取损失值和更新后的权重值:
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]mean_squared_error = np.mean(np.square(out - outputs))return mean_squared_errordef 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*lrreturn updated_weights, original_loss
(4) 绘制损失值:
losses = []for epoch in range(100):W, loss = update_weights(x,y,W,0.01)losses.append(loss)plt.plot(losses)plt.title('Loss over increasing number of epochs')plt.xlabel('Epochs')plt.ylabel('Loss value')plt.show()
损失值最初为 0.33
左右,然后逐渐下降到 0.0001
左右,这表明权重是根据输入-输出数据调整的,当给定输入时,我们可以通过调整网络参数得到预期目标值。调整后的权重如下:
print(W)'''输出结果[array([[ 0.01424004, -0.5907864 , -0.27549535], [ 0.39883757, -0.52918637,0.18640439]], dtype=float32), array([ 0.00554004,0.5519136 , -0.06599568], dtype=float32), array([[ 0.3475135 ], [-0.05529078], [ 0.03760847]], dtype=float32), array([-0.22443289], dtype=float32)]'''
使用 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]print(pred_out)# [[-0.0174781]]
输出为 -0.017
,这个值非常接近预期的输出 0
,通过训练更多的 epoch
,pred_out
值会更接近 0
。
6. 神经网络训练过程总结
训练神经网络主要通过重复两个关键步骤,即以给定的学习率进行前向传播和反向传播,最终神经网络架构得到最佳权重。
在前向传播中,我们对输入数据应用一组权重,将其传递给定义的隐藏层,对隐藏层的输出执行非线性激活,然后通过将隐藏层节点值与另一组权重相乘来将隐藏层连接到输出层,以估计输出值;最终计算出对应于给定权重集的损失。需要注意的是,第一次前向传播时,权重的值是随机初始化的。
在反向传播中,通过在损失减少的方向上调整权重来减少损失值(误差),权重更新的幅度等于梯度乘以学习率。
重复前向传播和反向传播的过程,直到获得尽可能小的损失,在训练结束时,神经网络已经将其权重 θ\thetaθ 调整调整到近似最优值,以便获取期望的输出结果。
小结
在本节中,我们了解了传统机器学习与人工神经网络间的差异,并了解了如何在实现前向传播之前连接网络的各个层,以计算与网络当前权重对应的损失值;实现了反向传播以优化权重达到最小化损失值的目标。并实现了网络的所有关键组成——前向传播、激活函数、损失函数、链式法则和梯度下降,从零开始构建并训练了一个简单的神经网络。