深层网络背后的数学
现在,我们可以使用许多高级的、专门的库和框架,如Keras、TensorFlow或PyTorch,我们不需要一直担心权重矩阵的大小,也不需要记住我们决定使用的激活函数的导数公式。通常我们只需要创建一个神经网络,即使是一个结构非常复杂的神经网络,也仅需要一些导入和几行代码。这节省了我们搜索bug的时间,并简化了我们的工作。然而,对神经网络内部发生的事情的了解对架构选择、超参数调优或优化等任务有很大帮助。
介绍
为了更多地了解神经网络是如何工作的,来看看隐藏在表面之下的数学。对于那些对代数和微积分不太熟悉的人,我会尽量温和一些,但正如标题所示,这是一篇涉及数学的文章。
图1.训练集的可视化
作为一个例子,我们将解决数据集的二元分类问题,如图1所示。属于两个类的点形成圆圈 - 这种安排对于许多传统的机器学习(ML)算法来说是不方便的,但是小的神经网络应该工作得很好。为了解决这个问题,我们将使用具有图2所示结构的NN。 - 五个全连接层,具有不同数量的单元。对于隐藏层,我们将使用ReLU作为激活函数,使用Sigmoid作为输出层。这是一个相当简单的架构,但复杂到足以成为我们讨论的有用例子。
图2.神经网络架构
KERAS解决方案
首先,我将介绍一个使用最流行的机器学习库之一KERAS的解决方案。Python代码如下:
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%的准确度对我们的测试集中的条目进行分类。我们的任务归结为根据所选择的架构提供超参数(层数,层中的神经元数,激活函数或epochs数)。现在让我们看一下幕后发生的事情。我在学习过程中创建了一个很酷的可视化
图3.在训练期间可用于适当类别的区域的可视化
神经网络是什么?
让我们从回答这个关键问题开始:什么是神经网络?这是一种生物启发的方法,可以构建能够学习和独立查找数据连接的计算机程序。如图2所示。网络是一组按层排列的软件“神经元”,以一种允许交流的方式连接在一起。
Single neuron
每个神经元接收一组x值(从1到n编号)作为输入并计算预测的y帽值。向量x实际上包含了训练集中m个例子中的一个的特征值。更重要的是,每个单元都有自己的一组参数,通常被称为w(权重列向量)和b(偏差),它们在学习过程中会发生变化。在每次迭代中,神经元根据向量x的当前权值w计算向量x的加权平均值并添加偏差。最后,通过一个非线性激活函数g来传递计算结果。我将在本文后面的部分提到一些最流行的激活函数。
图4. Single neuron
Single layer
现在让我们考虑如何计算整个神经网络层。我们将使用我们在单个单元内发生的知识,并在整个层向量化,将这些计算组合成矩阵方程。为了统一符号,将为选定的层编写方程[l]。顺便说一下,i标记了这层神经元的指数。
图5. Single layer
更重要的一点是:当我们编写单个单元的方程时,我们使用x和y-hat,它们分别是特征的列向量和预测值。在切换到层的通用符号时,我们使用向量a——表示对应层的激活。因此,x向量是第0层的激活——输入层。层中的每个神经元按照如下公式进行相似的计算:
为了清楚起见,让我们写下方程式,例如第2层:
如您所见,对于每个层,我们必须执行一些非常相似的操作。为此目的使用for循环并不十分有效,因此为了加快计算速度,我们将使用矢量化。首先,叠加在一起水平向量的权重w(转置)我们将构建矩阵w .类似地,我们将层中的每个神经元的偏差堆叠在一起,从而创建垂直向量b。现在没有什么可以阻止我们构建一个矩阵方程,它允许我们一次对层的所有神经元进行计算。让我们写下我们用过的矩阵和向量的维数。
Vectorizing跨多个例子
到目前为止我们画的方程只涉及到一个例子。在学习神经网络的过程中,你通常要处理大量的数据,多达数百万个条目。因此,下一步将跨多个示例进行矢量化。假设我们的数据集有m个条目,每个条目都有nx特性。首先,我们将把每一层的垂直向量x, a,和z放在一起分别创建x, a和z矩阵。然后,考虑到新创建的矩阵,我们重写了之前的简化方程。
什么是激活函数?我们为什么需要它?
激活函数是神经网络的关键要素之一。没有它们,我们的神经网络将成为线性函数的组合,因此它本身就只是一个线性函数。我们的模型具有有限的扩展性,不会超过逻辑回归。非线性元素允许在学习过程中更大的灵活性和创建复杂函数。激活函数对学习速度也有显著影响,这是它们选择的主要标准之一。图6显示了一些常用的激活函数。目前,最流行的隐藏层可能是ReLU。我们有时仍然使用sigmoid,特别是在输出层,当我们处理二元分类时,我们希望模型返回的值在0到1之间。
图6.最流行的激活函数及其导数的图表
损失函数
关于学习过程进展的基本信息来源是损失函数的值。一般来说,损失函数是用来显示我们离“理想”解决方案有多远。在我们的例子中我们使用了二元交叉熵,但是根据问题的不同我们可以应用不同的函数。我们使用的函数如下式所示,学习过程中其值的变化如图7所示。它显示了每一次迭代的损失函数值如何降低和精度增加。
图7.学习过程中准确度和损失值的变化
神经网络如何学习?
学习过程是关于改变W和b参数的值,使损失函数最小化。为了实现这一目标,我们将求助于微积分,使用梯度下降法来求出函数的最小值。在每次迭代中,我们将计算损失函数关于我们神经网络的每个参数的偏导数的值。对于那些不太熟悉这种计算方法的人,我只想提一下导数有一种神奇的能力来描述函数的斜率。由于这一点,我们知道如何操纵变量,以便在图中向下移动。为了形成关于梯度下降法工作原理的直觉,我准备了一个小的可视化。你可以看到,随着每一个连续的epoch,我们是如何走向最小的。在我们的NN中,它以同样的方式工作——在每次迭代中计算的梯度显示我们应该移动的方向。主要的区别是在我们的模范神经网络中,我们有更多的参数要操作。如何计算这些复杂的导数?
图8.运行中的梯度下降
反向传播
反向传播是一种算法,它允许我们计算一个非常复杂的梯度,就像我们需要的那样。根据以下公式调整神经网络的参数。
在上面的等式中,α表示学习率 - 一个超参数,它允许您控制执行调整的值。选择学习率至关重要 - 我们将其设置得太低,我们的NN学习得非常慢,我们设置得太高而且我们无法达到最低学习率。使用链规则计算dW和db,关于W和b的损失函数的偏导数。dW和db的大小分别与W和b的大小相同。图9显示了神经网络中的操作顺序。我们清楚地看到正向和反向传播如何协同工作以优化损失函数。
结论
希望我已经解释了在神经网络中发生的数学。至少了解这个过程的基础知识对NN的工作非常有帮助。我认为我提到的事情是最重要的,但它们只是冰山一角。我强烈建议您自己尝试编写这样一个小型的神经网络,不使用高级框架,只使用Numpy。