深入理解神经网络中的数学

192 阅读10分钟

深入理解神经网络中的数学


翻译: 原文在这里

简介

如今,针对深度学习,我们已经有了很多高级的特定的库或框架,比如Keras,Tensorflow和PyTorch。我们不再用再为权值矩阵的大小而担心,也不用费力去记住激活函数的导数公式。通常我们写一个神经网络,哪怕是非常精妙的网络,也不过是几个import和几行代码的事情。这为我们节省了很多debug时间,也极大地简化了我们的工作。然而,了解神经网络内部发生了什么会为我们带来更多好处,比如在模型架构选择时,在超参数微调时或者优化参数时。

Introduction

为了加深对神经网络的理解,我决定花时间做一些总结,揭示隐藏在其背后的数学。所以我写了这篇文章,一部分为了我自己——总结一下新学习的知识,一部分为了其他人——帮助他们理解难懂的概念。我会尽可能温柔地对待那些代数或是计算困难的人,但是如题所示,这将是一篇包含很多数学的文章。
在这里插入图片描述
以二分类问题的数据集为例,如上图所示,属于两个集合的点集形成了圆——这种方式对于许多ML算法是非常不方便的,但是一个简单的神经网络就可以解决这个问题。我们使用下图的神经网络结构——五个节点数目不等的全连接层。这是个非常简单的结构,但是足够解决我们的例子了。
在这里插入图片描述

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)

就这么简单,正如我所说,几个import和几行代码就可以写出一个网络并且可以正确地对测试集进行分类,几乎达到了100%的正确率。我们的工作精简到只提供对应架构的超参数(层数、每层的节点数、激活函数、epochs轮次数)。现在让我们看看背后发生了什么。噢…,我还加了个酷炫的可视化界面用来在整个训练过程中可视化,希望这能防止你睡着。
在这里插入图片描述

什么是神经网络

让我们从另一个关键问题开始:什么是神经网络? 这是个受生物学启发的方法,它可以学习并独立地找到数据之间的联系。如下图所示,网络是一组软件”神经元“在层内排列组成,”神经元“以一种可以相互交流的方式连接在一起。

单个神经元

每个神经元接受一系列 x \mathbf{x} x 值(编号1到n)作为输入,然后计算出预测值 y ^ \hat{y} y^​ 。向量 x \mathbf{x} x 实际上是一个样本的特征值,样本是 m m m 个训练集样本中的一个。每个神经元都有自己的参数集,通常用 w w w (权值的列向量) 和 b b b(偏置) 表示。在每次迭代,神经网络以当前的权值向量 w \mathbf{w} w 和偏置 b b b 计算向量 x \mathbf{x} x 的加权平均值。 最后计算结果通过一个非线性的激活函数 g g g 。我在本文中将提到最常用的激活函数方程。
在这里插入图片描述

单个神经层

接下来,让我们再思考整个神经层是如何执行计算的。我们将用到上一小节的知识,合并全连接层的计算,并将其向量化成矩阵公式。为了统一符号,公式中的 l l l 表示当前层,下标 i i i 表示该层的神经元编号。
在这里插入图片描述
重要提示: 当我们写单个神经元公式的时候,我们使用 x \mathbf{x} x 和 y ^ \hat{y} y^​ ,分别代表特征和预测值。当我们切换到层的统一表示时,我们使用向量 a \mathbf{a} a ——代表着对应层的激活。因此向量 x \mathbf{x} x 是激活层0——输入层。层中的每个神经元执行着大致相同的的计算:
z i [ l ] = w i T ⋅ a [ 1 − 1 ] + b i a i [ l ] = g [ l ] ( z i [ l ] ) z_{i}^{[l]}=\mathbf{w}_{i}^{T} \cdot \mathbf{a}^{[1-1]}+b_{i} \quad \mathbf{a}_{i}^{[l]}=g^{[l]}\left(z_{i}^{[l]}\right) zi[l]​=wiT​⋅a[1−1]+bi​ai[l]​=g[l](zi[l]​)
具体地,我们写下第二层的等式:
在这里插入图片描述
如上所示,每一层我们都执行类似的操作。使用for循环来实现显然不够高效,所以我们使用向量化加速计算过程。首先,将向量 w \mathbf{w} w 转置成水平形式堆叠起来,得到矩阵 W \mathbf{W} W 。类似地,我们将每个神经元的偏置也堆叠起来组成竖直矩阵 b \mathbf{b} b 。现在已经没有什么能够阻挡我们建立一个单矩阵方程了,单矩阵方程可以让我们一次性执行一层中全部神经元的计算。像之前那样,我们也写下矩阵和向量的维度。
在这里插入图片描述

向量化多样本(引入batch)

目前我们写下的等式都仅涉及单个样本。整个神经网络的学习过程中,你将用到多达百万条的数据。因此,下一步我们将向量化多样本。假设我们的数据集有 m m m 条数据,每个数据有 n x nx nx 个特征。首先,我们将一层中所有的竖直向量 x \mathbf{x} x , a \mathbf{a} a ,以及 z \mathbf{z} z 堆叠在一起,得到矩阵 X \mathbf{X} X , A \mathbf{A} A ,以及 Z \mathbf{Z} Z 。然后,我们重写之前列出来的等式,带入新得到的矩阵。
在这里插入图片描述
Z [ l ] = W [ l ] ⋅ A [ l − 1 ] + b [ l ] A [ l ] = g [ l ] ( Z [ l ] ) \mathbf{Z}^{[l]}=\mathbf{W}^{[l]} \cdot \mathbf{A}^{[l-1]}+\mathbf{b}^{[l]} \quad \mathbf{A}^{[l]}=g^{[l]}\left(\mathbf{Z}^{[l]}\right) Z[l]=W[l]⋅A[l−1]+b[l]A[l]=g[l](Z[l])

什么是激活函数以及我们为什么需要激活函数?

激活函数是神经网络的关键元素。如果没有激活函数,我们的神经网络就变成了一堆线性方程的集合,所以它自己也是一个线性方程。 这样的网络能力有限,不会比逻辑回归好多少。非线性元素的引入,使得网络在学习过程中更灵活、更复杂。激活函数对学习速率也有显著的影响,这也是选择激活函数的一个重要准则。 下图显示了一些常用的激活函数。如今,最受欢迎的隐藏层激活函数估计是ReLU。我们有时候也会用sigmoid,尤其是在解决二分类问题时,此时我们希望模型的输出值在0到1之间。
在这里插入图片描述

损失函数

在整个学习过程中信息的基本来源就是损失函数的值。总的来说,损失函数就是设计用来衡量我们离“理想”状态还有多远的。在我们这个例子中,我们在这里使用二元交叉熵,不同的问题我们使用不同的函数。二元交叉熵函数的公式描述如下:
J ( W , b ) = 1 m ∑ i = 1 m L ( y ^ ( i ) , y ( i ) ) L ( y ^ , y ) = − ( y log ⁡ y ^ + ( 1 − y ) log ⁡ ( 1 − y ^ ) ) \begin{array}{l}{J(W, b)=\frac{1}{m} \sum_{i=1}^{m} L\left(\hat{y}^{(i)}, y^{(i)}\right)} \\ {L(\hat{y}, y)=-(y \log \hat{y}+(1-y) \log (1-\hat{y}))}\end{array} J(W,b)=m1​∑i=1m​L(y^​(i),y(i))L(y^​,y)=−(ylogy^​+(1−y)log(1−y^​))​
其在整个训练过程中的变化可视化如下,下图显示了每次迭代后损失函数的下降和精确度的上升:
在这里插入图片描述

神经网络如何学习?

学习过程也就是改变参数 W \mathbf{W} W 和 b \mathbf{b} b 的值使得损失函数值最小。为了达到这一目标,我们需要微积分并使用梯度下降法来寻找函数的最小值。 在每次迭代,我们都要计算损失函数分别对每个神经网络参数的偏导数。对于那些不熟悉这种计算方式的同学,我只能提一句:偏导数可以描述一个函数的斜率变化。多亏了这些技术,我们才能调整变量使得值落到图中的山谷。为了更加直观地描述梯度下降法是如何工作的(也为了防止你们再次睡着),我做了个可视化。你从图中可以看到,我们是如何通过一系列的迭代使得我们从山顶落到山谷的。在神经网络上也是类似的方式——每次迭代计算出来的梯度指导我们应该往哪个方向移动。我们实验中的神经网络与此唯一的不同是,神经网络有更多的参数需要调整。那么…我们怎么计算如此复杂的导数呢?

反向传播

反向传播允许我们计算非常复杂的梯度,就比如我们需要的那个函数。神经网络的参数根据如下公式进行调整:
W [ l ] = W [ l ] − α d W [ l ] b [ l ] = b [ l ] − α d b [ l ] \begin{array}{l}{\mathbf{W}^{[l]}=\mathbf{W}^{[l]}-\alpha \mathbf{d} \mathbf{W}^{[l]}} \\ {\mathbf{b}^{[l]}=\mathbf{b}^{[l]}-\alpha \mathbf{d} \mathbf{b}^{[l]}}\end{array} W[l]=W[l]−αdW[l]b[l]=b[l]−αdb[l]​
上述公式中, α \alpha α 表示学习率——超参数,你可以用它来控制每次迭代调整的幅度,学习率的选择非常关键——如果设置过小,我们的神经网络学习将会非常慢;如果设置过大,我们可能得不到最小值。 d W \mathbf{d} \mathbf{W} dW 和 d b \mathbf{d} \mathbf{b} db 使用链式法则来计算,分别是损失函数对 W \mathbf{W} W 和 b \mathbf{b} b 的偏导数。下图显示了神经网络内部的一系列操作,我们可以直观地看出前向传播和反向传播是怎样一起优化损失函数的。
在这里插入图片描述
在这里插入图片描述

总结

我已经尽力向你解释了神经网络内部发生的数学。理解这个过程的基础会对你学习神经网络有很大帮助。我觉得我提到的都是最重要的,然而也只是冰山一角。我强烈建议大家自己动手实现一个小型的神经网络,不使用任何高级库,只使用numpy。此处可以参考我的另一篇博客,手把手教你使用numpy完成一个深度学习的框架!使用numpy搭建自己的深度学习框架(零)