您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center 汽车系统工程   模型库  
会员   
   
OCSMP认证课程:OCSMP-MU
4月9-10日 线上
基于模型的数据治理与数据中台
5月19-20日 北京+线上
网络安全原理与实践
5月21-22日 北京+线上
     
   
 订阅
  捐助
神经网络-标准神经网络(python3)
 
作者:SunJingyun
  3944  次浏览      35
 2019-12-17
 
编辑推荐:
本文主要介绍了标准神经网络以及何为前馈反向传播全连接神经网络,通过矩阵乘法公式惊醒简单介绍,希望对您的学习又所帮助。
本文来自于知乎,由火龙果软件Alice编辑、推荐。

前言

之前阅读了很多有关神经网络的书籍、论文以及博客,发现大多分为两种情况。一种是例如《深度学习》这类,里面堆砌了大量的理论和公式,如果细心研读的话的确可以将神经网络中的数学原理理解的很透彻。可是,当看完《深度学习》,如果有人让我做个简单的神经网络分类器,我可能还是一头雾水,不知从何下手。第二种是例如《XX实战》,《XX快速上手》这类,跟着书中的代码确实可以用神经网络解决实际问题,可是书中又没有对底层的数学原理进行比较透彻的讲解。经常是对数学原理做个简单介绍,让读者对其有个大致了解,然后就开始各种调用现成的包。调用一个包,几行代码就搭起一个神经网络,虽然感觉很畅快。可是只会调包又有什么用呢?

于是,在与同学进行讨论后,我们决定自己来总结这样一套内容。其中既对神经网络中的数学原理进行细致到每一个步骤的剖析,又能实际搭建一个能够执行具体任务的神经网络。我们打算采用一种全新的行文构造,来达到一步一剖析(数学原理),一步一实践(代码实现)的目标。这种行文构造是:首先提出某种需要解决的实际问题。以解决这个实际问题为出发点,从第一步到最后一步,在解决问题的每一个步骤中,将数学原理展开剖析,然后落实到代码实现。而在最后,还将会不厌其烦地采用用某些框架调包再次解决该问题。以达到上得厅堂(会用现成的框架快速的解决实际问题)、下得厨房(懂的神经网络每一步的数学原理)的学习效果。

标准神经网络

标准神经网络是最为普通、常规的神经网络。是其他神经网络如“卷积神经网络(CNNs)”、“递归/循环神经网络(RNNs)”的基础。其他神经网络都是在标准神经网络的基础上进行改造而来的。

标准神经网络有时也称为反向传播神经网络(Back propagation neural network)或简称为BP神经网络,有时也叫做前馈反向传播全连接神经网络。至于何为“反向传播”,何为“前馈”,以及何为“全连接”在后面会介绍到的。我们先根据图1,对神经网络的构造有个大体的认识。

图1:标准神经网络(包含输入层、隐藏层和输出层)

图1所示是一个标准的神经网络构造图。圆圈代表神经元,圆圈之间的箭头代表神经元之间的有向连接。我们首先将数据输入到输入层。数据有多少个值,输入层就有多少个神经元。然后输入层各个神经元内的数据值按着箭头的方向流向下一层,每个箭头都有不同的权重,所以数据值在流经不同的箭头时会与不同的权重相乘。就这样,将数据输入到输入层,就会在输出层得到我们想要的输出。如果输出和我们预期的结果有一定差距,那么就适当修改箭头的权重,使神经网络的输出向更加符合我们预期的方向靠近。

明确任务目标

首先我们需要一个难以进行线性分离的数据集。这里我们使用螺旋数据集,其通过如下代码生成。在这里我们不关注产生螺旋数据集的具体细节,因为这偏离了本文的主题。我们只需要复制粘贴下面这段代码,即可得到如图2所示的螺旋数据集:

N = 100 # 每类样本点的个数
D = 2 # 维度(每个样本点有2个特征,即横坐标和纵坐标)
K = 3 # 类别数(共三种不同类别的样本点)
X = np.zeros((N*K,D)) # 数据矩阵 (每一行是一个样本)
y = np.zeros(N*K, dtype='uint8') # 类标签
for j in range(K):
ix = list(range(N*j,N*(j+1)))
r = np.linspace(0.0,1,N)
t = np.linspace(j*4,(j+1)*4,N) + np.random.randn(N)*0.2
X[ix] = np.c_[r*np.sin(t), r*np.cos(t)]
y[ix] = j
# 将数据可视化
plt.scatter(X[:, 0], X[:, 1], c=y, s=40, cmap=plt.cm.Spectral)
plt.show()

图2:螺旋数据集

通过观察我们看到螺旋数据集中有三种不同类型的样本点,每个样本点有两个特征,分别是横坐标和纵坐标。于是我们的任务目标就来了:即把这些有两个特征的样本点正确分类。具体来说就是,将一个样本点的两个特征值(横坐标值与纵坐标值)作为输入,得到一个关于这个样本点属于某个类别的输出。如果这个输出符合我们的预期(即输出的类别就是该样本点的正确类别),那很完美,如果不符合我们的预期,就对神经网络中的权重进行调整。

预处理

通常我们需要对数据集进行预处理。在本例中,所有特征值已经很好的介于-1至1之间,因此我们可以跳过此步骤。

初始化输入层和隐藏层之间的参数

之前提到过,数据有多少个值,输入层就有多少个神经元。那么“数据有多少个值”是什么含义?其实就是指一个样本点有多少个特征。在这里,一个样本点有两个特征,那么输入层就有两个神经元。我们以某个样本点为例,比如一个坐标为(0.25, 0.50)的绿色样本点。输入到输入层之后如图3所示:

图3:将坐标为(0.25,0.50)的样本点输入到输入层

我们看到每个特征值都与隐藏层的所有神经元相连,这也就是“全连接”一词的由来。即每一个神经元都与后一层的所有神经元相连接。由图我们能够看出,如果隐藏层有两个神经元,那么输入层和隐藏层之间就需要有2×2=4个连接权重。

能够看到流入到隐藏层第一个神经元的数值应该为,但其实还应该加上一个称之为偏置的数值 [b] ,即 为流入到隐藏层第一个神经元的数值。同理, 为流入到隐藏层第二个神经元的数值。将其写为矩阵的形式,即如下:

其中 [公式] 和 [公式] 分别代表流入隐藏层第一个神经元和二个神经元的数值。

那么如果我们隐藏层有100个神经元,于是输入层和隐藏层之间就有2×100=200个连接权重。同理其矩阵乘法应是如下形式:

由于最开始我们也不知道这些连接权重以及偏置的值应该是多少,于是我们将这些权重的初始值设置为随机值。我们将其设置为服从均值为0,标准差为1的正态分布的随机值,这样更符合物理世界的一般规律,即靠近0附近的值更多一些。而将偏置初始化为0值:

# 初始化参数
W1 = 0.01 * np.random.randn(D, 100)
b1 = np.zeros((1,100))

可以试着将权重矩阵打印出来,能够直观地看到这是一个2行100列的矩阵。而偏置是一个1行100列的行向量。

从隐藏层流出

根据上述矩阵乘法公式,可以很容易的求出流入到隐藏层神经元的值。然而,流入到隐藏层神经元的值并非原封不动地再从隐藏层神经元流出。而是经过一个称之为“激活函数”的函数进行激活。简而言之,就是要把流入到隐藏层神经元的值作为某个函数的输入,得到该函数的输出,将该函数的输出作为流出隐藏层神经元的值。

这里我们使用ReLu函数作为隐藏层神经元的激活函数,即 [公式] 。ReLu函数图像如图4所示:

图4:ReLu函数图像

显然,当ReLu函数的输入小于0时,其输出为0。当ReLu函数的输入大于0时,其输出等于输入值。由此,可以算出隐藏层的输出,其代码如下:

hidden_layer = np.maximum(0, np.dot(X, W1) + b1)

其中,hidden_layer 代表隐藏层的输出。理论上来说这应该是一个1行100列的行向量。但是值得注意的是,我们并非只用单独一个样本点的特征向量做运算,而是用300个样本点的特征矩阵 [X] 同时做运算。300个样本点的特征矩阵 [X] 如下:

因此,最终得到的输出是300行100列的输出矩阵(尽管偏置向量设置为1行100列的,但是python在执行300行100列的矩阵与1行100列的向量相加时,会自动将1行100列的向量按行复制300份,使偏置成为一个300行100列其中每行都相等的矩阵)。

从输出层流出

之前我们初始化了输入层和隐藏层之间的参数,包括2行100列的权重矩阵和1行100列的偏置向量。这次我们用同样的方法,初始化隐藏层和输出层之间的参数。我们令输出层包含3个神经元。输出层使用3个神经元的原因是样本点分为三种类型。因此,在输出层的三个神经元的输出值中,若第一个神经元的输出值最大,则可认为该样本点属于一类。同理,若第二个神经元的输出值最大,则可认为该样本点属于二类。已知隐藏层的输出即hidden_layer是300行100列的行向量,因此隐藏层到输出层之间的权重矩阵应该是100行3列的矩阵。而偏置向量应该是1行3列的行向量。初始化隐藏层到输出层之间的权重矩阵和偏置向量代码如下:

W2 = 0.01 * np.random.randn(100,K)
b2 = np.zeros((1,K))

然后将隐藏层的输出值hidden_layer与参数做运算,得到输出层的输出:

scores = np.dot(hidden_layer, W2) + b2

我们将输出层的输出称为分数向量,因为输出的3个值分别代表了样本点可能属于每个类别的分数。最终得到的显然是一个300行3列的分数矩阵。其中每行代表每个样本点,每列代表对应每个类别的分数。我们可以将分数矩阵直观地打印出来,如图5:

图5:300行3列的分数矩阵

衡量与与真实情况的差距

接下来的一个关键因素就是损失函数,我们需要用它来计算我们的损失。那么何为”损失“?”损失“即预测结果和真实情况相差多少。真实情况和预测结果相差越大,损失值越大。真实情况和预测结果越接近,损失值越小。在这里,我们希望正确的类别应该比其他类别有更高的分数,若确实如此,则损失应该很低,否则损失应该很高。量化这种直觉的方法有很多种,但在这里我们使用与交叉熵损失。我们首先用 [F] 表示一个样本的类别分数向量(即包含三个数值的向量),于是损失函数如下:

我们首先对上述公式进行解读:首先 li 代表样本 i 的损失值。 yi代表样本 i 的真实类别标签(如果样本 i 属于一类,则类别标签是1),因此若样本 i 的类别分数向量为,且样本 i 属于一类,则 。而 j代表f中元素的索引。故对于样本分数向量为 的样本 i 来说,损失值

我们能够看出来,的值永远介于0和1之间,我们将这称为真实类别的归一化概率。显然,样本 i 的真实类别的分数越高,则真实类别的归一化概率越接近1,因此损失值 [公式] 越低。反之,若样本 i 的真实类别对应的分数较低,则真实类别的归一化概率越接近0,因此损失值 Li越高。

我们在这里求出300个样本的平均损失值,以衡量神经网络的性能。我们称之为平均交叉熵损失:

公式的前半部分自然是300个样本的交叉熵损失的平均值,即平均交叉熵损失。而后半部分,称之为正则化损失。何为”正则化损失“?我们通过一个例子来理解正则化损失,比如样本点 i 的两个特征即横纵坐标为(1.0,1.0)。而假设此时有两组不同的权重向量 。样本的特征与两组不同的权重做内积的结果相等,都为1。可是加上正则化损失后,很明显 。所以选择W2作为权重,正则化损失更小。正则化损失的作用在于增强泛化能力,去除权重的不确定性。

那么现在就可以根据公式以及已知的分数矩阵,计算损失了。我们首先将分数矩阵转换为概率矩阵:

# 得到样本数量
num_examples = X.shape[0]
# 得到非归一化概率
exp_scores = np.exp(scores)
# 得到每个样本对应各个类别的归一化概率
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)

现在我们有了一个300行3列的概率矩阵,并且我们已经将每一行的概率都进行了归一化,使得每一行的三个概率之和为1。现在我们就可以将每个样本对应真实类别的概率提取出来,做 [公式] 映射。

corect_logprobs = -np.log(probs[range(num_examples),y])

于是得到了一个包含300个元素的一维向量,其中每个元素都是相应样本的交叉熵损失值。接下来计算平均交叉熵损失以及正则化损失,并将二者相加:

# 计算损失:平均交叉熵损失和正则化损失
data_loss = np.sum(corect_logprobs)/num_examples
reg_loss = 0.5 * reg * np.sum(W1 * W1) + 0.5 * reg * np.sum(W2 * W2)
loss = data_loss + reg_loss

更新参数

我们的参数是随机初始化的,所以神经网络输出的结果必然和真实情况有所差距。而我们现在的目标是使这种差距尽可能的小,即找到参数W1、b1、W2、b2取什么值的时候,损失值最小。我们可以求损失对参数的导数,用梯度下降找到导数为0的点,即为极小值点。这里我们在 [公式] 和 [公式] 之间引入一个中间变量 [公式] ,其含义为一个归一化概率的向量。于是样本 i 的损失为:

我们现在想要知道样本 i 的分数向量 [公式] 中的元素 [公式] 如何改变才能减少损失 [公式] ,从而减少整体损失 [公式] 。因此,我们需要求出 [公式] 。而由于在 [公式] 和 [公式] 之间引入了中间变量 [公式] ,因此 [公式] 。容易得出 [公式] 。而对于 [公式] ,则分两种情况:

因此我们能够得到 [公式] 对 [公式] 的梯度向量。假设样本 i 的归一化概率向量为 [公式] ,且样本 i 的真实类别标签为1,则 [公式] 对 [公式] 的梯度向量为 [公式] 。以下代码为上述过程的实现。其中prob存储300个样本的归一化类别概率矩阵,dscore存储损失对分数的梯度矩阵:

dscores = probs
dscores[range(num_examples),y] -= 1
dscores /= num_examples

而由于scores = np.dot(hidden_layer, W2) + b2,因此可知scores对W2的梯度矩阵应该是hidden_layer的转置,而对b2的梯度矩阵应该所有元素都为1。且有损失对分数的梯度存储在dscores中,因此根据链式求导法则,损失对W2和b2的梯度如下代码:

# 利用反向传播求损失对输出层权重和偏置的梯度
dW2 = np.dot(hidden_layer.T, dscores)
db2 = np.sum(dscores, axis=0, keepdims=True)

同理,我们要想求损失对W1和b1的梯度,可以先求损失对hidden_layer的梯度,再求hiddenlayer对W1和b1的梯度,再根据链式求导法则即可求得损失对W1和b1的梯度。

根据链式求导法则,损失对hidden_layer的梯度如下代码,其中dhidden为损失对hidden_layer的梯度矩阵。

dhidden = np.dot(dscores, W2.T)

而由于隐藏层的激活函数是ReLu函数,即 [公式] 。所以隐藏层神经元输出大于0的,对ReLu函数输入值的导数为1;隐藏层神经元输出等于0的,对ReLu函数输入值的导数为0。因此损失对ReLu函数输入值即隐藏层输入值的梯度为:

# 沿ReLu函数进行反向传播
dhidden[hidden_layer <= 0] = 0

现在dhidden中存储的是损失对隐藏层输入的梯度矩阵。有了损失对隐藏层输入的梯度,且有了隐藏层输入对W1和b1的梯度,根据链式法则可以求得损失对W1以及b1的梯度:

# 最终求得损失对与隐藏层神经元相连的权重和偏置的梯度
dW1 = np.dot(X.T, dhidden)
db1 = np.sum(dhidden, axis=0, keepdims=True)

现在分别有了损失对W1,W2,b1,b2的梯度,即可对参数进行更新了。参数更新的公式如下(以W1中第一行第一列的元素 [公式] 为例):

这就是所谓的通过梯度下降求极小值点。如图6所示,当 [公式] 时,将 [公式] 减小一点,就会使 [公式] 更接近极小值。反之,当 [公式] 时,将 [公式] 增大一点,就会使 [公式] 更接近极小值。

图6:梯度下降寻找极小值点

对整个W1、W2、b1以及b2梯度矩阵利用上述公式执行参数更新操作:

# 参数更新
W1 += -step_size * dW1
b1 += -step_size * db1
W2 += -step_size * dW2
b2 += -step_size * db2

这样我们就得到了一组新的参数,并且应用这组新参数的神经网络的输出将会更加符合真实情况。我们将全部上述代码放在一个循环中,循环执行10000次。这样,我们的参数就更新了10000次,使得神经网络的输出向真实情况靠近了一万步。

测试准确度

# 计算在数据集上的类别预测精度
hidden_layer = np.maximum(0, np.dot(X, W) + b)
scores = np.dot(hidden_layer, W2) + b2
predicted_class = np.argmax(scores, axis=1)
print('training accuracy: %.2f' % (np.mean(predicted_class == y)))

当我们将300个样本点的特征矩阵输入到神经网络中,神经网络对300个样本点的所属类别进行预测。将预测结果与真实情况比对,最终输出正确率为98%。也就是说300个样本点中,神经网络正确预测了其中98%个样本点的类别。

   
3944 次浏览       35
相关文章

基于图卷积网络的图深度学习
自动驾驶中的3D目标检测
工业机器人控制系统架构介绍
项目实战:如何构建知识图谱
 
相关文档

5G人工智能物联网的典型应用
深度学习在自动驾驶中的应用
图神经网络在交叉学科领域的应用研究
无人机系统原理
相关课程

人工智能、机器学习&TensorFlow
机器人软件开发技术
人工智能,机器学习和深度学习
图像处理算法方法与实践