从神经网络简单的数学定义开始,沿着损失函数、激活函数和反向传播等方法进一步描述基本的优化算法。在理解这些基础后,本文详细描述了动量法等当前十分流行的学习算法。此外,本系列将在后面介绍 Adam 和遗传算法等其它重要的神经网络训练方法。
I. 简介
本文是作者关于如何「训练」神经网络的一部分经验与见解,除了介绍神经网络的基础概念外,这篇文章还描述了梯度下降(GD)及其部分变体。此外,该系列文章将在后面一部分介绍了当前比较流行的学习算法,例如:
- 动量随机梯度下降法(SGD)
- RMSprop 算法
- Adam 算法(自适应矩估计)
- 遗传算法
作者在第一部分以非常简单的神经网络开始介绍,简单到仅仅足够让人理解我们所谈论的概念。作者会解释什么是损失函数,以及「训练」神经网络或者任何其他的机器学习模型到底意味着什么。作者的解释并不是一个关于神经网络全面而深度的介绍,事实上,作者希望我们读者已经对这些相关的概念早已了然于心。如果读者想更好地理解神经网络具体是如何运行的,读者可以阅读《深度学习》等相关书籍,或参阅文末提供的相关学习资源列表。
本文作者以几年前在 kaggle 上进行的猫狗鉴别竞赛(https://www.kaggle.com/c/dogs-vs-cats)为例来解释所有的东西。在这个比赛中我们面临的任务是,给定一张图片,判断图中的动物是猫还是狗。
II. 定义神经网络
人工神经网络(ANN)的产生受到了人脑工作机制的启发。尽管这种模拟是很不严格的,但是 ANN 确实和它们生物意义上的创造者有几个相似之处。它们由一定数量的神经元组成。那么,我们来看一下一个单独的神经元吧。
单个神经元
我们接下来要谈论的神经元是一个与 Frank Rosenblatt 在 1957 年提出的最简单的被称作「感知机,perception」的神经元稍微有所不同的版本。我所做的所有修改都是为了简化,因为我在这篇文章中不会涉及神经网络的深入解释。我仅仅试着给出读者一个关于神经网络如何工作的直觉认识。
什么是神经元呢?它是一个数学函数,并以一定量的数值作为输入(随便你想要多少作为输入),我在上图画出的神经元有两个输入。我们将每个输入记为 x_k,这里 k 是输入的索引。对于每一个输入 x_k,神经元会给它分配另一个数 w_k,由这些参数 w_k 组成的向量叫做权重向量。正是这些权值才使得每个神经元都是独一无二的。在测试的过程中,权值是不会变化的,但是在训练的过程中,我们要去改变这些权值以「调节」我们的网络。我会在后面的文章中讨论这个内容。正如前面提到的,一个神经元就是一个数学函数。但是它是哪种函数呢?它是权值和输入的一种线性组合,还有基于这种组合的某种非线性函数。我会继续做进一步解释。让我们来看一下首先的线性组合部分。
输入和权值的线性组合
上面的公式就是我提到的线性组合。我们要将输入和对应的权值相乘,然后对所有的结果求和。结果就是一个数字。最后一部分—就是给这个数字应用某种非线性函数。今天最常用的非线性函数即一种被称作 ReLU(rectified linear unit) 的分段线性函数,其公式如下:
线性整流单元的表达式
如果我们的数字大于 0,我们就会使用这个数字,如果它小于 0,我们就会用 0 去代替它。这个被用在线性神经元上的非线性函数被称作激活函数。我们必须使用某种非线性函数的原因在后面会变得很明显。总结一下,神经元使用固定数目的输入和(标量),并输出一个标量的激活值。前面画出的神经元可以概括成一个公式,如下所示:
将我要写的内容稍微提前一下,如果我们以猫狗鉴别的任务为例,我们会把图片作为神经元的输入。也许你会疑问:当神经元被定义为函数的时候,如何向它传递图片。你应该记住,我们将图片存储在计算机中的方式是将它拿一个数组代表的,数组中的每一个数字代表一个像素的亮度。所以,将图片传递到神经元的方式就是将 2 维(或者 3 维的彩色图片)数组展开,得到一个一维数组,然后将这些数字传递到神经元。不幸的是,这会导致我们的神经网络会依赖于输入图片的大小,我们只能处理由神经网络定义的某个固定大小的图片。现代神经网络已经发现了解决这个问题的方法,但是我们在这里还是在这个限制下设计神经网络。
现在我们定义一下神经网络。神经网络也是一个数学函数,它就是很多相互连接的神经元,这里的连接指的是一个神经元的输出被用为另一个神经元的输入。下图是一个简单的神经网络,希望用这张图能够将这个定义解释得更加清楚。
一个简单的神经网络
上图定义的神经网络具有 5 个神经元。正如你所看到的,这个神经网络由 3 个全连接层堆叠而成,即每一层的每个神经元都连接到了下一层的每一个神经元。你的神经网络有多少层、每一层有多少个神经元、神经元之间是怎么链接的,这些因素共同定义了一个神经网络的架构。第一层叫做输入层,包含两个神经元。这一层的神经元并不是我之前所说的神经元,从某种意义而言,它并不执行任何计算。它们在这里仅仅代表神经网络的输入。而神经网络对非线性的需求源于以下两个事实:
- 我们的神经元是连在一起的;
- 基于线性函数的还是非线性函数。
所以,如果不对每个神经元应用一个非线性函数,神经网络也会是一个线性函数而已,那么它并不比单个神经元强大。最后一点需要强调的是:我们通常是想让一个神经网络的输出大小在 0 到 1 之间,所以我们会将它按照概率对待。例如,在猫狗鉴别的例子中,我们可以把接近于 0 的输出视为猫,将接近于 1 的输出视为狗。为了完成这个目标,我们会在最后一个神经元上应用一个不同的激活函数。我们会使用 sigmoid 激活函数。关于这个激活函数,你目前只需要知道它的返回值是一个介于 0 到 1 的数字,这正好是我们想要的。解释完这些之后,我们可以定义一个和上图对应的神经网络了。
定义一个神经网络的函数。w 的上标代表神经元的索引,下标代表输入的索引
最后,我们得到了某种函数,它以几个数作为输入,输出另一个介于 0 到 1 之间的数。实际上,这个函数怎样表达并不重要,重要的是我们通过一些权重将一个非线性函数参数化了,我们可以通过改变这些权重来改变这个非线性函数。
III. 损失函数
在开始讨论神经网络的训练之前,最后一个需要定义的就是损失函数了。损失函数是一个可以告诉我们,神经网络在某个特定的任务上表现有多好的函数。做这件事的最直觉的办法就是,对每一个训练样本,都沿着神经网络传递得到一个数字,然后将这个数字与我们想要得到的实际数字做差再求平方,这样计算出来的就是预测值与真实值之间的距离,而训练神经网络就是希望将这个距离或损失函数减小。
上式中的 y 代表我们想要从神经网络得到的数字,y hat 指的一个样本通过神经网络得到的实际结果,i 是我们的训练样本的索引。我们还是以猫狗鉴别为例。我们有一个数据集,由猫和狗的图片组成,如果图片是狗,对应的标签是 1,如果图片是猫,对应的标签是 0。这个标签就是对应的 y,在向神经网络传递一张图片的时候我们想通过神经网络的得到的结果。
为了计算损失函数,我们必须遍历数据集中的每一张图片,为每一个样本计算 y,然后按照上面的定义计算损失函数。如果损失函数比较大,那么说明我们的神经网络性能并不是很好,我们想要损失函数尽可能的小。为了更深入地了解损失函数和神经网络之间的联系,我们可以重写这个公式,将 y 换成网络的实际函数。
IV. 训练
在开始训练神经网络的时候,要对权值进行随机初始化。显然,初始化的参数并不会得到很好的结果。在训练的过程中,我们想以一个很糟糕的神经网络开始,得到一个具有高准确率的网络。此外,我们还希望在训练结束的时候,损失函数的函数值变得特别小。提升网络是有可能的,因为我们可以通过调节权值去改变函数。我们希望找到一个比初始化的模型性能好很多的函数。
问题在于,训练的过程相当于最小化损失函数。为什么是最小化损失而不是最大化呢?结果证明损失是比较容易优化的函数。
有很多用于函数优化的算法。这些算法可以是基于梯度的,也可以不是基于梯度的,因为它们既可以使用函数提供的信息,还可以使用函数梯度提供的信息。最简单的基于梯度的算法之一叫做随机梯度下降(SGD),这也是我在这篇文章中要介绍的算法。让我们来看一下它是如何运行的吧。
首先,我们要记住关于某个变量的导数是什么。我们拿比较简单的函数 f(x) = x 为例。如果还记得高中时候学过的微积分法则,我们就会知道,这个函数在每个 x 处的导数都是 1。那么导数能够告诉我们哪些信息呢?导数描述的是:当我么让自变量朝正方向变化无限小的步长时,函数值变化有多快的速率。它可以写成下面的数学形式:
它的意思是:函数值的变化量(方程的左边)近似等于函数在对应的某个变量 x 处的导数与 x 的增量的乘积。回到我们刚才所举的最简单的例子 f(x) = x,导数处是 1,这意味着如果我们将 x 朝正方向变化一小步ε,函数输出的变化等于 1 和ε的乘积,刚好是ε本身。检查这个规则是比较容易的。实际上这个并不是近似值,它是精确的。为什么呢?因为我们的导数对于每一个 x 都是相同的。但是这并不适用于绝大多数函数。让我们来看一个稍微复杂一点的函数 f(x) = x^2。
通过微积分知识我们可以知道,这个函数的导数是 2*x。现在如果我们从某个 x 开始移动某个步长的ε,很容易能够发现对应的函数增量并不精确地等于上面的公式中的计算结果。
现在,梯度是由偏导数组成的向量,这个向量的元素是这个函数所依赖的某些变量对应的导数。对于我们目前所考虑的简单函数来说,这个向量只有一个元素,因为我们所用的函数只有一个输入。对于更加复杂的函数(例如我们的损失函数)而言,梯度会包含函数对应的每个变量的导数。
为了最小化某个损失函数,我们可以怎么使用这个由导数提供的信息呢?还是回到函数 f(x) = x^2。显然,这个函数在 x=0 的点取得最小值,但是计算机如何知道呢?假设我们开始的时候得到的 x 的随机初始值为 2,此时函数的导数等于 4。这意味着如果 x 朝着正方向改变,函数的增量会是 x 增量的 4 倍,因此函数值反而会增加。
相反,我们希望最小化我们的函数,所以我们可以朝着相反的方向改变 x,也就是负方向,为了确保函数值降低,我们只改变一小步。但是我们一步可以改变多大呢? 我们的导数只保证当 x 朝负方向改变无限小的时候函数值才会减小。因此,我们希望用一些超参数来控制一次能够改变多大。这些超参数叫做学习率,我们后面会谈到。我们现在看一下,如果我们从-2 这个点开始,会发生什么。这里的导数是-4,这意味着如果朝着正方向改变 x,函数值会变小,这正是我们想要的结果。
注意到这里的规律了吗?当 x>0 的时候,我们导数值也大于 0,我们需要朝着负方向改变,当 x<0 的时候,我们导数值小于 0,我们需要朝着正方向改变,我们总需要朝着导数的反方向改变 x。让我们对梯度也引用同样的思路。梯度是指向空间某个方向的向量,实际上它指向的是函数值增加最剧烈的方向。由于我们要最小化我们的函数,所以我们会朝着与梯度相反的方向改变自变量。
现在我们应用这个思想。在神经网络中,我们将输入 x 和输出 y 视为固定的数。我们要对其求导数的变量是权值 w,因为我们可以通过改变这些权值类提升神经网络。如果我们对损失函数计算权值对应的梯度,然后朝着与梯度相反的方向改变权值,我们的损失函数也会随之减小,直至收敛到某一个局部极小值。这个算法就叫做梯度下降。在每一次迭代中更新权重的算法如下所示:
每一个权重值都要减去它对应的导数和学习率的乘积。
上式中的 Lr 代表的是学习率,它就是控制每次迭代中步长大小的变量。这是我们在训练神经网络的时候要调节的重要超参数。如果我么选择的学习率太大,会导致步进太大,以至于跳过最小值,这意味着你的算法会发散。如果你选择的学习率太小,收敛到一个局部极小值可能会花费太多时间。人们开发出了一些很好的技术来寻找一个最佳的学习率,然而这个内容超出本文所涉及的范围了。
不幸的是,我们不能应用这个算法来训练神经网络,原因在于损失函数的公式。
正如你可以在我之前的定义中看到的一样,我们损失函数的公式是和的平均值。从微积分原理中我们可以知道,微分的和就是和的微分。所以,为了计算损失函数的梯度,我们需要遍历我们的数据集中的每一个样本。在每一次迭代中进行梯度下降是非常低效的,因为算法的每次迭代仅仅以很小的步进提升了损失函数。
为了解决这个问题,还有另外一个小批量梯度下降算法。该算法更新权值的方法是不变的,但是我们不会去计算精确的梯度。相反,我们会在数据集的一个小批量上近似计算梯度,然后使用这个梯度去更新权值。Mini-batch 并不能保证朝着最佳的方向改变权值。事实上,它通常都不会。在使用梯度下降算法的时候,如果所选择的学习率足够小的话,能够保证你的损失函数在每一次迭代中都会减小。但是使用 Mini-batch 的时候并不是这样。你的损失函数会随着时间减小,但是它会有波动,也会具有更多的「噪声」。
用来估计梯度的 batch 大小是你必须选择的另一个超参数。通常,我们希望尽可能地选择能处理的较大 batch。但是我很少见到别人使用比 100 还大的 batch size。
mini-batch 梯度下降的极端情况就是 batch size 等于 1,这种形式的梯度下降叫做随机梯度下降(SGD)。通常在很多文献中,当人们说随机梯度下降的时候,实际上他们指的就是 mini-batch 随机梯度下降。大多数深度学习框架都会让你选择随机梯度下降的 batch size。
以上是梯度下降和它变体的基本概念。但近来越来越多的人在使用更高级的算法,其中大多数都是基于梯度的,作者下一部分就主要介绍这些最优化方法。
VII. 反向传播(BP)
关于基于梯度的算法,剩下的唯一一件事就是如何计算梯度了。最快速的方法就是解析给出每一个神经元架构的导数。我想,当梯度遇到神经网络的时候,我不应该说这是一个疯狂的想法。我们在前面定义的一个很简单的神经网络就已经相当艰难了,而它只有区区 6 个参数。而现代神经网络的参数动辄就是数百万。
第二种方法就是使用我们从微积分中学到的下面的这个公式去近似计算梯度,事实上这也是最简单的方法。