导读
你可能又觉得梯度下降是一个完全没有接触过的新词。不过放心,依然能对上。
方向导数和梯度
在说明怎么进行梯度下降之前,我们需要复习一下方向导数和梯度。
就拿最简单的两个变量为例:
如你所见,下图就是一张海拔图。
有一天,你乘坐的飞机失事了,在山顶坠毁,只有你一个人活了下来。弹尽粮绝的你几近绝望。但是你看到眼前有一大片水源,你想着找到了水源就能够活下去。所以你决定在你饿倒、冻死、被野兽咬杀之前,尽快达到山底的水源,越快越好。在这种紧急情况下,已经没有时间让你把所有的路全都探索一遍了,因为你随时可能在山上出事。
在陡峭的山上,有若干个比较平缓的落脚点。你站在出发的落脚点上,环顾四周,发现了若干能够安全到达的落脚点。于是你在这几个里面选择了最低的一个,跳了过去。接着在下一个落脚点继续寻找更低的落脚点。
这就是“下降”,而梯度则是选择一条最优下降方向的参考。
所以问题来了,怎么样才能选出最优下降方向?如果就只是找遍目力所及的所有落脚点、计算落差,这样当然可行,但是代价太大。所以,我们在这里将使用一定的数学方法推算出来。
首先对于在平面 D D D(海平面)上具有连续一阶偏导数的二元函数 z = f ( x , y ) z=f(x,y) z=f(x,y)(山高计算式)上的任意一点,都有一个向量:
g r a d ⃗ f ( x , y ) = { ∂ f ∂ x , ∂ f ∂ y } \vec{grad}f(x,y)=\{\frac{\partial f}{\partial x},\frac{\partial f}{\partial y}\} gradf(x,y)={∂x∂f,∂y∂f}
又由于方向导数:
f L ⃗ ( x 0 , y 0 ) = ( ∂ f ∂ x , ∂ f ∂ y ) ⋅ ( c o s θ , s i n θ ) \vec{f_L}(x_0,y_0)=({\partial f\over\partial x},{\partial f\over\partial y})\cdot(cos\theta,sin\theta) fL(x0,y0)=(∂x∂f,∂y∂f)⋅(cosθ,sinθ)
所以,不难得出:
方向导数是二元函数 z = f ( x , y ) z=f(x,y) z=f(x,y)在各个方向上的变化量,而当方向导数最大的时候,必定是方向导数和梯度方向重合的时候。而方向导数最大时,函数增长最快;反之亦然,方向导数最大时的反方向函数减少最快。
也就是说,我们在下山的时候唯一需要注意的就是:寻找梯度。
当然我们还有一些需要注意的细节,下山的速度就是其中之一,它有个学术名称叫做学习率(learning rate
),多记为
η
\eta
η。当学习率提高的时候,下山的速率就非常快,很快你就会达到一个区域最优解,但也可能因为步长太大忽略了某个关键点甚至可能导致不收敛;而学习率降低的时候,下山速率就非常低,只不过这样的话你能够找到更多的落脚点,从而一定程度上减少了陷入局部最优解的可能。
拿下面这个图举个例子:
很清楚地看到,在 x ∈ ( 0 , 1 ) x\in(0,1) x∈(0,1)和 x ∈ ( 4 , 5 ) x\in(4,5) x∈(4,5)的时候,函数下降速度和上升速度都非常快,这个时候可以适当减少学习率,就像是单机FPS游戏里放慢步伐寻找隐藏点一样,避免错过了任何一个细节。但是过小的步伐会导致收敛速度非常慢,一直再重复无效的学习;而在 x ∈ ( 1 , 4 ) x\in(1,4) x∈(1,4)的时候,函数不管是上升还是下降都非常缓慢,基本没有什么起伏,可以适当加快步伐,就像是二刷GalGame回收CG一样,只抓重点,忽略细节。但是过大的步伐会导致你忽略掉 x ∈ ( 3 , 4 ) x\in(3,4) x∈(3,4)的极值点。
总的来说,步长小了可能会陷入局部最优解;而步长大了可能会远离最优解。这些都是我们应当避免的。
不过呢,三维我们还能勉强想象,但是拓展到四维、五维等超越3个维度的坐标系时,作为三次元的我们将无法想象那样的存在。所以,我们目前所能做的极限就是讨论三元变量的关系了。有没有三维以上的方向导数解法?当然有,本篇说明的就是通用解法,只不过对于三维以上的数据就无法说明几何意义了。
梯度下降是什么
经过复习,聪明的你应该能够明白,所谓梯度下降即是:
-
从图像的意义上来看,就可以总结为:任意选取一个落脚点,然后搜索周边看能不能找到更低的落脚点。这个点不出意外的话应该在梯度方向的反方向上。
-
从数学的意义上来看,就可以总结为:任意选取损失函数上的一个点,一步一步寻找区域极值,并坚信其中一个极值是全域最小值,逐渐逼近使得损失函数的值最小的点。有点贪心的味道了?
为什么选择梯度下降
正如回归函数和代价函数中结尾处所说的那样,屏蔽维度,提高可用性,梯度下降不仅适用于线性方程求解,而且还适用于机器学习的很多个领域。找到代价函数最小值和各维度的值,同时找出根据很多自变量变化的因变量拟合函数,可以在非常多的方面说明问题,比如决策、推荐、预测、分类等等。
随机梯度下降
好了,现在开始进入正题:梯度下降的步骤。
问题提出
我们先不要弄得太复杂,还是老例子:
时间回溯到你研究猫娘食量那会。你确确实实拿到了20万数据,每个数据都是以(年龄,食量)这样的坐标形式出现。这看起来没什么问题,只不过你突然发现猫娘们的食量增长速度在成年之后随着年龄的增长而不断趋于平稳,成年之前却疯狂增长。你觉得线性方程不再适用。这样的函数看起来更像是对数函数,是不是因为有其他因素的影响?
于是,你凭着对猫娘疯狂的热爱,开始了第二波数据收集。这次你拿到了年龄(age)、身高(height)、体重(weight)和食量(quantity)四个数据,构成了函数 Q = f ( a , h , w ) Q=f(a,h,w) Q=f(a,h,w)。
这次涉及了三个维度和一个自变量,虽然复杂了很多,但也勉强能够画出来。只不过梯度下降的图像意义便不再是下山了,而是构建一个平面或者瞄准某一个中心。
分析过程
你凭着自己的性癖……你随机选择了一位猫娘,将她的数据使用矩阵形式记录了下来:
n e k o 0 = [ a 0 h 0 w 0 ] neko_0=\left[\begin{matrix} a_0\\h_0\\w_0 \end{matrix}\right] neko0=⎣⎡a0h0w0⎦⎤
紧接着,根据代价函数公式,下一个数据就是:
n e k o 1 = [ a 1 h 1 w 1 ] = [ a 0 h 0 w 0 ] − η [ ∂ ∂ a f ( a 0 , h 0 , w 0 ) ∂ ∂ h f ( a 0 , h 0 , w 0 ) ∂ ∂ w f ( a 0 , h 0 , w 0 ) ] neko_1=\left[\begin{matrix} a_1\\h_1\\w_1 \end{matrix}\right] =\left[\begin{matrix} a_0\\h_0\\w_0 \end{matrix}\right] -\eta\left[\begin{matrix} {\partial\over\partial a} f(a_0,h_0,w_0)\\ {\partial\over\partial h} f(a_0,h_0,w_0)\\ {\partial\over\partial w} f(a_0,h_0,w_0) \end{matrix}\right] neko1=⎣⎡a1h1w1⎦⎤=⎣⎡a0h0w0⎦⎤−η⎣⎡∂a∂f(a0,h0,w0)∂h∂f(a0,h0,w0)∂w∂f(a0,h0,w0)⎦⎤
当然,
n
e
k
o
1
neko_1
neko1是有实际数值的,而使用学习率
、
n
e
k
o
0
neko_0
neko0和损失函数的偏导数
计算出来的
n
e
k
o
1
neko_1
neko1并不是真实的数据。那么使用哪一个数据?我全都要!。
一方面, n e k o 1 neko_1 neko1的真实数据和预测数据的差将会是代价函数的重要参考;另一方面,单纯的真实数据将会是目标函数稀疏或者密集的重要参考。因为现在猫娘的食量和年龄变成了非线性关系,也就是说在x取值范围同样长的情况下,不同取值范围中函数的变化量不一样,部分密集,部分稀疏。所以应对密集部分我们需要加大学习率,尽快过渡到稀疏部分,从而减少重复学习的时间;应对稀疏部分我们要减少学习率,尽量避免忽略了重要区间的情况。
好了,现在你通过数学计算拿到了 n e k o 1 neko_1 neko1,于是你准备获取 n e k o 2 neko_2 neko2。同样的,代入公式:
n e k o 2 = [ a 2 h 2 w 2 ] = [ a 1 h 1 w 1 ] − η [ ∂ ∂ a f ( a 1 , h 1 , w 1 ) ∂ ∂ h f ( a 1 , h 1 , w 1 ) ∂ ∂ w f ( a 1 , h 1 , w 1 ) ] neko_2=\left[\begin{matrix} a_2\\h_2\\w_2 \end{matrix}\right] =\left[\begin{matrix} a_1\\h_1\\w_1 \end{matrix}\right] -\eta\left[\begin{matrix} {\partial\over\partial a} f(a_1,h_1,w_1)\\ {\partial\over\partial h} f(a_1,h_1,w_1)\\ {\partial\over\partial w} f(a_1,h_1,w_1) \end{matrix}\right] neko2=⎣⎡a2h2w2⎦⎤=⎣⎡a1h1w1⎦⎤−η⎣⎡∂a∂f(a1,h1,w1)∂h∂f(a1,h1,w1)∂w∂f(a1,h1,w1)⎦⎤
经过一番辛苦,你也得出来了这个数据。剩下的步骤你希望计算机能够帮助运算,便大致写下了一串伪代码:
repeat until convergence {
[ a i h i w i ] : = [ a i h i w i ] − η [ ∂ ∂ a f ( a i , h i , w i ) ∂ ∂ h f ( a i , h i , w i ) ∂ ∂ w f ( a i , h i , w i ) ] \left[\begin{matrix} a_i\\h_i\\w_i \end{matrix}\right] := \left[\begin{matrix} a_i\\h_i\\w_i \end{matrix}\right] -\eta \left[\begin{matrix} {\partial\over\partial a} f(a_i,h_i,w_i)\\ {\partial\over\partial h} f(a_i,h_i,w_i)\\ {\partial\over\partial w} f(a_i,h_i,w_i) \end{matrix}\right] ⎣⎡aihiwi⎦⎤:=⎣⎡aihiwi⎦⎤−η⎣⎡∂a∂f(ai,hi,wi)∂h∂f(ai,hi,wi)∂w∂f(ai,hi,wi)⎦⎤
}
伪代码中=
和:=
略有区别,分别是相等和赋值的意思
细节决定成败
这样看似乎没有什么大问题,但是在实际代码实现中,我们尤其需要注意一个很难注意到的细节:
这个矩阵形式的写法意味着 a i a_i ai、 h i h_i hi、 w i w_i wi是同步更新的!!!
这个矩阵形式的写法意味着 a i a_i ai、 h i h_i hi、 w i w_i wi是同步更新的!!!
这个矩阵形式的写法意味着 a i a_i ai、 h i h_i hi、 w i w_i wi是同步更新的!!!
重要的事情说三遍!如果不是同步的话,错误的梯度下降伪代码将会变成这样:
repeat until convergence {
t e m p a = ∂ ∂ a f ( a i , h i , w i ) ⋯ ① a i = t e m p a t e m p h = ∂ ∂ h f ( a i , h i , w i ) ⋯ ② … temp_a={\partial\over\partial a} f(a_i,h_i,w_i)\cdots①\\ a_i = temp_a\\ temp_h={\partial\over\partial h} f(a_i,h_i,w_i)\cdots②\\ \ldots tempa=∂a∂f(ai,hi,wi)⋯①ai=tempatemph=∂h∂f(ai,hi,wi)⋯②…
}
注意到了吗?①式和②式中, a i a_i ai的值不同了!这是严重的逻辑错误!
正确的梯度下降伪代码就应该是这样:
repeat until convergence {
t e m p a = a i − η ∂ ∂ a f ( a i , h i , w i ) t e m p h = h i − η ∂ ∂ h f ( a i , h i , w i ) t e m p w = w i − η ∂ ∂ w f ( a i , h i , w i ) a i = t e m p a h i = t e m p h w i = t e m p w temp_a=a_i-\eta{\partial\over\partial a} f(a_i,h_i,w_i)\\ temp_h=h_i-\eta{\partial\over\partial h} f(a_i,h_i,w_i)\\ temp_w=w_i-\eta{\partial\over\partial w} f(a_i,h_i,w_i)\\ a_i = temp_a\\ h_i = temp_h\\ w_i = temp_w tempa=ai−η∂a∂f(ai,hi,wi)temph=hi−η∂h∂f(ai,hi,wi)tempw=wi−η∂w∂f(ai,hi,wi)ai=tempahi=temphwi=tempw
}
要么全部修改,要么全不修改。像极了数据库的原子性不是么?
在说明时我强调了是错误的梯度下降伪代码,而不是错误的伪代码,这是因为这本来就是一种正确的算法,但不是梯度下降,而是一种其他的什么代码。这就要各位读者继续读下去来寻找正确答案了。
收敛结果
没错,这就是通用解法,无论多少维,用这个矩阵解法都会有一个结果。随着学习的进行,导数或者偏导数都会逐渐变化, η \eta η也应当随之而变化。他也有另外一个名字,叫随机梯度下降,因为我们刚刚是随机选择初始点、根据梯度寻找下一跳、逐步靠近最优解,所以收敛的时候有很大的随机性,收敛时也会在局部最优附近疯狂抖动。
还是用个反常的例子类比:
你的猫娘很生气,你想逗她开心。你身边有很多东西,毛球、老鼠玩具、吉他、钢琴等等。你知道这些她都喜欢,但是你不知道这次怎么样才能让她开心。于是你开始疯狂试探。
有时候她笑了一下,有时候又挠你几下,有时候保持冷漠……在心情在变好和变坏之间反复横跳。变好的时候你肯定会加大力度,而变坏的时候你会迅速折返。
小技巧
当然,公式归公式,解法是解法,这两个可以不需要严格意义上保持一致。也就是说,如果 f ( a , h , w ) f(a,h,w) f(a,h,w)在求导的时候极大地增加了复杂度,就比如 1 + s i n x 1 − c o s x \sqrt{\frac{1+sinx}{1-cosx}} 1−cosx1+sinx,我们可以人为地分段、化简、求导,变成方便计算的其他公式。
一个尴尬的细节
好了,到了这里,相信各位读者对梯度下降也有一定的了解了。那么,各位有没有注意到一个小小的细节:我们是为什么需要根据一定的步长移动?其实是基于“最初随机选到的点并不是极值点”这一前提下展开的。那么我们就极端一点,最初就那么运气不好,选到了极值点,会怎么样呢?很显然,极值点的导数或偏导数都是0,最终我们的计算式也就化简成为了:
[ a i h i w i ] : = [ a i h i w i ] − η [ ∂ ∂ a f ( a i , h i , w i ) ∂ ∂ h f ( a i , h i , w i ) ∂ ∂ w f ( a i , h i , w i ) ] = [ a i h i w i ] − η × 0 = [ a i h i w i ] \left[\begin{matrix} a_i\\h_i\\w_i \end{matrix}\right] := \left[\begin{matrix} a_i\\h_i\\w_i \end{matrix}\right] -\eta \left[\begin{matrix} {\partial\over\partial a} f(a_i,h_i,w_i)\\ {\partial\over\partial h} f(a_i,h_i,w_i)\\ {\partial\over\partial w} f(a_i,h_i,w_i) \end{matrix}\right]= \left[\begin{matrix} a_i\\h_i\\w_i \end{matrix}\right] -\eta\times0= \left[\begin{matrix} a_i\\h_i\\w_i \end{matrix}\right] ⎣⎡aihiwi⎦⎤:=⎣⎡aihiwi⎦⎤−η⎣⎡∂a∂f(ai,hi,wi)∂h∂f(ai,hi,wi)∂w∂f(ai,hi,wi)⎦⎤=⎣⎡aihiwi⎦⎤−η×0=⎣⎡aihiwi⎦⎤
也就是说在这里我们将原地踏步。如果这里并不是全域最优解,而是局部最优解,我们也就像一开始介绍梯度下降是所说的一样:陷入局部最优解。这也正是步长过小造成的必然结果。
当然,既然有让你陷入局部最优解的情况,也有只能收敛到全局最优解的情况。用比较学术一点的话来说,就是“目标函数 f ( a , h , w ) f(a,h,w) f(a,h,w)是一个凹函数”(国外这里叫凸函数,因为看的方向不一样所以叫法不一样),即目标函数的二阶偏导数恒大于0。这就导致一阶偏导数最多也就1个零点,即最多有一个极值,原函数要么是个碗,极值即最值;要么就单调,端点是最值。
很尴尬,也很无奈。当然办法还是有的,也就是贪心算法。这就依靠大家打怪升级之后解锁新篇章了,这里只介绍这么多有关梯度的内容。
批量(Bacth
)梯度下降
我相信你会等不及找答案的。所以这里提供一个不涉及贪心算法的临时解决办法:批量梯度下降。
为什么是批量?在这里我们每次迭代都会整个训练集都试一遍,然后找到一个局部最优解,然后剪枝,重新遍历整个数据集,然后又找到下一个局部最优解,再剪枝……整个过程将会行走在非常标准的直通线路上。
当然,缺点也非常明显,整个训练一遍意味着需要大量的内存和大量的计算。如果训练集足够大,内存将无法一次性执行完,要么直接OutOfMemoryException
,要么在内存加载的内容全部计算完后等待磁盘经过漫长的时间加载所需的下一批数据。整体来说还是相当的憋屈,所以也在逐渐的弃用这个方法。
是不是有点能理解了呢?
附录
在吴恩达教授的课程中,解向量是这么求的:
[ a i h i w i ] : = [ a i h i w i ] − η [ ∂ ∂ a f ( a 0 , h 0 , w 0 ) ∂ ∂ h f ( a 0 , h 0 , w 0 ) ∂ ∂ w f ( a 0 , h 0 , w 0 ) ] \left[\begin{matrix} a_i\\h_i\\w_i \end{matrix}\right] := \left[\begin{matrix} a_i\\h_i\\w_i \end{matrix}\right] -\eta \left[\begin{matrix} {\partial\over\partial a} f(a_0,h_0,w_0)\\ {\partial\over\partial h} f(a_0,h_0,w_0)\\ {\partial\over\partial w} f(a_0,h_0,w_0) \end{matrix}\right] ⎣⎡aihiwi⎦⎤:=⎣⎡aihiwi⎦⎤−η⎣⎡∂a∂f(a0,h0,w0)∂h∂f(a0,h0,w0)∂w∂f(a0,h0,w0)⎦⎤
其中在循环之中不停调整 a 0 a_0 a0、 h 0 h_0 h0、 w 0 w_0 w0的值。实际上这么做的核心目的依然是保持同步,只不过这里把之前我们提到的 t e m p a temp_a tempa换成了 a i a_i ai而已,方便我们统一修改每次学习都会更改的 a i a_i ai、 h i h_i hi和 w i w_i wi。