一文领会深度神经网络背后的数学原理

在学习神经网络的概念和工作原理中,通常绕不开一些数学知识。近日机器学习研究者 Piotr Skalski 撰文分析了神经网络蕴含的数学知识,包括单个神经元、单个神经层、激活函数、反向传播等这些概念背后的数学运算,帮助大家加深理解神经网络背后的数学原理。
如今,熟练使用像 Keras、TensorFlow 或 PyTorch 之类的专用框架和高级程序库后,我们不用再经常费心考虑权重矩阵的大小,或者记住激活函数和导数的公式什么的。有了这些库和框架,我们创建一个神经网络,哪怕是架构很复杂的网络,往往也只是需要几个导入和几行代码而已。这的确为我们节省了大量找 bug 的时间,让我们的工作也更加流程化。然而,熟知神经网络背后的工作原理,对于我们选择模型架构、调参或优化模型都有莫大的帮助。
引言
为了更深入的理解神经网络的工作原理,这个夏天我(作者 Piotr Skalski —— 译者注)花了大量的时间研究其背后的数学知识。最后我决定将我学到的东西整理后分享出来,帮助大家理解一些在学习过程中可能会感到困惑的概念。我会尽量让那些对代数和微积分不感冒的朋友读着不那么痛苦,但是正如文章题目所示,本文主要讲数学原理,所以会谈论大量的数学,在这里先提个醒。

举个例子,我们要解决一个数据集的二元分类问题,数据集如上所示。数据点组成了两个类别的圆圈状,这种安排对很多传统的机器学习算法来说都非常麻烦,但也有一小部分神经网络可以很好地处理这个问题。为了解决这个问题,我们会使用如下图所示结构的神经网络,它有 5 个全连接层,每层有不同数量的神经元。对于隐藏层,我们会使用 ReLU 作为激活函数,在输出层中使用 S 型函数。这是个相当简单的结构,但也足够解决我们的难题了。

Keras 解决方案
首先,我会展示一种解决方案——使用 Keras,当前最火热的机器学习库之一。

from keras.models import Sequential
from keras.layers import Dense
model = Sequential()
model.add(Dense(4, input_dim=2,activation='relu'))
model.add(Dense(6, activation='relu'))
model.add(Dense(6, activation='relu'))
model.add(Dense(4, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
model.fit(X_train, y_train, epochs=50, verbose=0)

就是这样而已。我在前面说过,只需几个导入、几行代码就能创建和训练一个模型,拿来解决分类问题几乎能达到 100% 的准确率。我们的工作归纳起来就是根据选择的模型结构,为模型提供超参数,比如网络层的数量、每层的神经元数量、激活函数和训练周期数量等等。下面我们来看看这背后发生了什么···等等,为了怕你睡着,我为训练过程还创建了一个很酷的可视化。

什么是神经网络?
我们首先来回答这个关键的问题:什么是神经网络?它是一种在生物学启发下创建的计算机程序,能够学习知识,独立发现数据中的关系。如图 2 所示,神经网络就是一系列的神经元排列在网络层中,网络层以某种方式连接在一起,从而相互之间实现沟通。
单个神经元
每个神经元会接受一系列的 x 值(从 1 到 n 的数字)作为输入,计算预测的 y-hat 值。向量 x 实际上包含了训练集中 m 个样本中一个样本的特征值。而且每个神经元会有它自己的一套参数,通常引用为 w(权重的列向量)和 b(偏差),在学习过程中偏差会不断变化。在每次迭代中,神经元会根据向量 x 的当前权向量 x 计算它的加权平均值,再和偏差相加。最后,计算的结果会传入一个非线性或函数 g 中。我在下面会提及一些最常见的激活函数。

图:单个神经元

单个网络层
现在,我们把范围缩小一点,思考一下神经网络的整个网络层是怎么进行数学运算的。我们会利用单个神经元的计算知识,在整个层中进行向量化,将这些计算融合进矩阵方程中。为了让数学符号一致,这些方程会写给选定的网络层。另外,下标的 i 符号标记了这一层的神经元顺序。

图:单个网络层

还有一件重要的事:在我们为单个神经元写方程时,我们使用 x 和 y-hat,它们分别表示特征列向量和预测值。当换成网络层的通用符号时,我们使用向量 a —— 意指对应网络层的激活。因此 x 向量是网络层 0(输入层)的激活。网络层中的每个神经元都按照如下方程式执行相同的运算:

让大家更清晰的看看,我们把第 2 层的公式写下来:

你可以看到,对每个网络层,我们必须执行一系列非常相似的运算。在这里使用 for 循环并不是非常高效,所以我们换成向量化来加快计算速度。首先,将权重 w 的水平向量堆放在一起,我们创建矩阵 W。同样地,我们将网络层中每个神经元的偏差堆放在一起,创建垂直向量 b。现在,我们可以顺利地创建一个矩阵方程式了,从而一次性计算该网络层的所有神经元。我们同样写下来用过的矩阵和向量的维度。

多个例子中的向量化
我们迄今所用的方程式只涉及了一个例子。在神经网络的学习过程中,你通常要处理大量的数据,最高可达数百万条。所以下一步就是在多个例子中实现向量化。假设我们的数据集有 m 个条目,每个有 nx 个特征。首先,我们将每一层的垂直向量 x,a 和 z 放在一起,分别创建矩阵 X,A 和 Z。然后,我们根据新创建的矩阵,重新编写之前列出的方程式。

什么是激活函数?我们为何需要它?
激活函数是神经网络中最重要的部分之一。没有激活函数,我们的神经网络就只是一些线性函数的组合,那样无非就是个线性函数而已。如果是这样,模型的扩展性就很有限了,比逻辑回归也强不到哪去。非线性部分能让模型有更大的灵活性,在学习过程中也能创建复杂的函数。
此外,激活函数对模型的学习速度也有重大影响,而学习速度是选择模型的主要标准之一。下图显示了一些常用的激活函数。当前,隐藏层中最常用的激活函数应该是 ReLU。在处理二元分类问题时,特别是想让模型返回在 0 到 1 之间的值时,我们有时也会使用 S 型函数,特别是在输出层中。

损失函数
学习过程中基本信息源就是损失函数的值。通常来讲,使用损失函数的目的就是展示我们离“理想”情况的差距。在我们这个例子中,我们使用了二元交叉熵,但根据我们处理的具体问题,可以使用不同的函数。我们所用的函数用如下公式表示,在学习过程中它的值的变化情况可视化动图如下。它显示了每次迭代中,损失函数的值在不断下降,准确度的值也不断增加。

神经网络是怎么学习的?
学习过程就是不断改变 W 和 b 参数的值,让损失函数实现最小化。为了能实现这个目标,我们会借助微积分,使用梯度下降法来找到函数最小值。在每次迭代中,我们会计算损失函数偏导数相对于每个神经网络参数的值。对于不太熟悉这种计算类型的人,我这里提示一下,导数能够描述函数的斜率。正因如此,我们能够知道该如何操作变量,从而在图中向下移动。为了能让大家直观感受梯度下降的工作原理,我预备了一点小小的可视化,如下图所示。你可以看到,随着训练批次增加,我们越来越靠近最小值。在我们的神经网络中,也是同样的工作方式——每次迭代计算出的梯度为我们显示了应该向哪个方向移动。主要的不同之处是在我们的神经网络中,我们有更多的参数可以调整。那么怎么计算如此复杂的导数呢?

反向传播
反向传播是一种算法,能让我们计算非常复杂的梯度,比如我们这个例子中需要的梯度。神经网络的参数按照如下公式进行调整。

在上面的方程式中,α 表示学习率——该超参数能让我们控制调整幅度的大小。选择学习率很关键,如果设置的太低,神经网络会学习的非常慢;如果设置的太高,我们就无法达到损失的最小值。使用链式法则以及损失函数对于 W 和 b 的偏导数来计算出 dW 和 db,这二者的大小分别等于 W 和 b。下面第二张图展示了神经网络中的操作顺序。我们可以清楚地看到正向传播和反向传播共同工作,优化损失函数。

结语
希望本文能帮助你理解一些神经网络背后的数学原理,掌握其中的数学基础知识对于你使用神经网络会大有帮助。虽然本文列出了一些重要内容,但它们也只是冰山一角。强烈建议你自己试着用一些简单的框架写一个小型的神经网络,比如 Numpy,不要借助很高级的框架,这样能加深你对机器学习的理解。
本文代码地址:
github.com/SkalskiP/ILe

参考资料:
towardsdatascience.com/

发表评论

电子邮件地址不会被公开。 必填项已用*标注