前言
提到人工智能算法,人工神经网络(ANN)是一个绕不过去的话题。但是对于新手,往往容易被ANN中一堆复杂的概念公式搞得头大,最后只能做到感性的认识,而无法深入的理解。正好最近笔者本人也在经历这个痛苦的过程,本着真理越辩越明的态度,索性坐下来认真的把这些头大的问题梳理一番,试试看能不能搞清楚ANN背后的数学原理。
其实ANN 的流程概括来说倒不是很复杂,以最简单的前馈神经网络为例,无非就是
- 搭建网络架构(含有待定参数)
- 通过比较输出与标签的差值定义损失函数(自变量为待定参数的函数)
- 随机给出一组初始参数
- 给出一个(sgd)/ 一批(Minibatch)训练样例(包括ANN的输入值和输出值)
- 前向传播得到预测标签和损失
- 利用梯度下降算法从后往前调整网络参数(误差反向传播,BP)
- 得到所有参数值
- 得到ANN并使用
其中4、5、6里的内容是需要反复迭代的(注意6是双重循环)。
在ANN的一堆操作里,梯度下降算法是一个相对独立的过程,不妨就让我们从梯度下降算法开始吧。
一、梯度下降到底在干什么
其实这个问题非常简单,只是大家被梯度下降复杂的过程搞蒙了,忘记了它的本质。梯度下降算法自始至终都在干一件事——就是找到函数的极值点,当然确切的说是极小值点。但是,这种方法不同于以往我们在高等数学里学到的找极值点的方法。那么我们首先就要问,求极值的经典方法不香吗?
1.1 求极值:传统的方法不香吗?
要回答这个问题,让我们先快速回顾一下在中学和大学里学到的传统的求极值点的方法。
对于一元函数来说,极值可能出现在一阶导函数为0的点(驻点)或是导数不存在的点。
例如要找到 f(x) = x^2 + 3x 的极值点 求导得 dy/dx = 2x + 3 令 dy/dx = 0 就得到 x = -1.5 时,导函数为0。
注意,上面找到的只是可能的极值点,也就是极值存在的必要条件。还需要验证一下充分条件,才能确定极值。这时,可以判断二阶导的正负性、或是判定一阶导在可能的极值点两边的正负情况。回到我们的例子
当 x < -1.5 时,2x + 3 < 0 当 x > -1.5 时,2x + 3 > 0 说明函数在x = -1.5 附近先下降、后上升 该点是一个极小值点
对于二元函数,情况更复杂一些。首先要找出该函数的驻点和偏导数不存在的点,这些点仍然只是可能的极值点。而二元函数的驻点需要同时满足两个偏导数为0的条件,即
显然,这里的驻点是需要解这样一个二元方程才能求得的。对于驻点分别求出其3个二阶偏导数的值,再根据一些规则才能判断是不是极值点。
还需注意这个判断规则是不同于一元函数的,因为一元函数极值的充分条件只需要考察一个二阶偏导数,而这里则需要综合考察二元函数的3个二阶偏导数,计算量明显增大了。对于偏导数不存在的情形还需要特判。
综合以上,我们可以看出使用经典方法虽然能准确的解出极值点,但当函数自变量的个数很多时,用这种方法求解极值点还真的不香。比如:
- 该方法方法不具有普适性。所谓普适性,就是不能简单的向多元推广。从一元到二元的例子可以看出,函数的自变量个数每增加一元,就要研究新的求解方案。可以想象如果是三元函数,其二阶偏导数的个数更多,则判断极值的充分条件还要来的更加复杂。而ANN中可能会求解上亿元函数的极值点。
- 其次,这种方法需要解多元方程组,而且这些方程还不一定都是线性的。对于这种多元的非线性方程组,我们的直观感受就是很难解出。事实上,虽然存在一些可供编程的数值计算解法,但计算量大,且求出的是近似解,具有一定的局限性。
基于此,为了找出多元函数的极值点,我们还需另寻他法。这种方法要简单易行,特别是要能简单的向任意元函数推广,而且这种方法要能够适应计算机数值计算的特点,毕竟我们这套程序肯定是要放在电脑上跑的。而这就是传说中的梯度下降算法。
1.2 什么是梯度?
梯度的概念其实也不难,但为了让尽可能多的人明白这一概念,我们还是从一元函数开始吧。不过现在我们的目标是——用纯粹数值计算的方法,从函数上的某一点出发,找到函数的极值。这里我们只考察极小值。
1.2.1 一元函数找极值:从枚举试探法到梯度下降法
以函数为例,让我们看看如何找到极值。
既然是从函数上的某一点出发,那么不妨设想我们在x = 1 的地方,这个地方是不是极小值点呢,我们可以试探一下。
向右走0.5,发现f(1.5) > f(1),说明这个方向是上升的方向,不应该选择这个方向; 向左走0.5,发现f(0.5) < f(1),说明这个方向是下降的方向,选择这个方向; 再向左走0.5,发现f(0) < f(0.5),说明这个方向是下降的方向,选择这个方向; 再向左走0.5,发现f(-0.5) > f(0),说明这个方向是上升的方向,不应该选择这个方向。 至此,我们可以将x = 0作为极小值点。
回顾这个过程,我们将寻找极小值点的过程抽象如下:
- 首先,选择一个方向
- 试着沿该方向走一小步,并据此判断该方向是否合理。如果合理,则走这一步;如果不合理,换一个方向
- 反复重复第二步,直到找到极小值点
当然这里还有几点值得注意
- 第一,对于一元函数来说我们只有向左走或向右走两个选项。换句话说,每一步我们的选择是有限的,是可以枚举的。因此,这个方法我把它称之为枚举试探法。
- 第二,判断方向其实不必这样试错,直接求导就好。如果某点的导数值 > 0,说明在该点处函数是递增的,为了找到极小值,应该向左走;而如果导数值 < 0,则反之向右走即可。
- 第三,这种方法是不一定能找到极小值的,能不能找到极值点受选择的起始点以及每次前进的步长这两个因素影响。
对于第二点,我们可以引出梯度的定义了。
梯度是一个向量,它总指向当前函数值增长最快的方向。而它的模长则是这个最快的增长率(导数)的值。想要得到梯度向量,也很简单,它在x, y, z……等方向上的分量(坐标)就是相应的导数值。于是我们求导就可以了。
对于一元函数,函数变化的方向只有两个,我们定义一种一维的向量来表示梯度,比如5i,-5i。i前的数为正时,代表向量指向x轴正向;i前的数为负时,代表向量指向x轴负向。由下图可以看出,按照上述定义规定的梯度向量自然的指向了函数增长的方向,是不是很神奇。
由于梯度的方向正是函数增长最快的方向,所以梯度的逆方向就成了函数下降最快的方向。当然对于一元函数来说,没有最快的方向的概念,因为毕竟就两个方向而已,根本没得比。不过有了梯度,我们就可以进一步简化上述寻找极小值点的过程:
- 首先,求出某点的梯度
- 沿梯度的反方向移动一小步
- 反复进行第一、二步,直到找到极小值点
仍以函数,起始点x = 1为例,让我们看看如何用梯度找到极值。
初始x = 1, 步长step = 0.5 #在我们的例子里,梯度的计算式为2xi。i是指向x轴正向的单位向量 求x = 1处的梯度为2i,梯度反方向为-i #注意这里我们只关注梯度的方向,至于梯度的模长则不必在意 沿此方向走一步,x新 = x旧 + step * 负梯度方向上单位向量的坐标 = 1 + 0.5 * (-1) = 0.5 求x = 0.5处的梯度为1i,梯度反方向为-i 沿此方向再走一步,x新 = 0.5 - 0.5 * 1 = 0 求x = 0处的梯度为0,说明到达极值点
以上就是用梯度找极小值点的过程,也就是梯度下降算法所做的事情,其实不难理解对吧。
可以看出,相比于枚举试探法,梯度下降法明显智能了许多,它直接给出了正确的方向,不需要我们一步步试探了。此外,使用梯度下降法不必再关注具体的函数值,只需要把注意力放在导数上,而且只关注一阶导数即可。
在后面,我们还将给上面提到的步长step换一个高大上的名字——学习率,这样就完全是机器学习里的叫法了。
这里用到梯度的时候,我进行了单位化操作,其实也可以不进行这一步,这样当函数变化比较剧烈的时候,移动的距离就比较多;函数变化比较平缓的时候,移动的距离就比较短。比如,在我们这个例子里,只需一轮迭代就能得到结果了。
初始x = 1, 学习率step = 0.5 #在我们的例子里,梯度的计算式为2xi。i是指向x轴正向的单位向量 求x = 1处的梯度为2i,梯度反方向为-2i #注意这里我们既关注梯度的方向,也关注梯度的模长 沿此方向走一步,x新 = x旧 + step * 负梯度的坐标 = 1 + 0.5 * (-2) = 0 求x = 0处的梯度为0,说明到达极值点
好了说完了梯度,对于前面第三点提到的找不到极值的情形,我们举两个具体的例子
还是函数,如果起始点选为0.4,而学习率仍为0.5,在采用单位化梯度向量的情形下,则无法找到事实上的极小值点
对于这种情况,我们可以通过减小学习率使结果尽可能精确,例如我们将学习率设置为0.1,就仍然能得到精确的结果。事实上,在实际操作中,一般也会把学习率设置为0.1。
而对于这种有多个极值点的函数,这种方法是没法找到全部极值点的,更遑论找到全局的极值点了。这时,我们可以在算法里加入一些随机性,使其有一定概率跳出可能陷入的局部极值点。
1.2.2 多元函数的梯度
前面说过梯度下降算法的好处之一在于可以很方便的向多维推广,现在我们以二元函数为例,看看梯度是如何帮助我们找到极值点的。
这次我们的函数变成了,起始点选择为(-5, -5),学习率仍设置为0.5。现在我们的目标是从这个点出发,找到该函数的极值点,我们知道这个极值点应该是(0, 0)。